Aнализ данных при планировании закупок и рекламных кампаний

Цель: сформулировать рекомендацию руководству компании о том, на каких играх стоит сосредоточиться в будущем году.

Задачи: проанализировать накопленные данные о продажах в разные года, определить тренды и проверить гипотезы.

In [1]:
# Необходимые библиотеки

import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
from scipy import stats as st

%matplotlib inline
In [2]:
# Загрузка данных

games_data = pd.read_csv('games_data.csv') 
ratind_data = pd.read_csv('rating.csv') # данные о возрастном рейтинге ESRB

Подготовка данных

Описание данных

  • platform — платформа
  • sales_year — год продажи
  • genre — жанр игры
  • online — количество продаж (в сотнях тысяч)
  • offline — количество продаж (в сотнях тысяч)
  • critic_score — оценка критиков (максимум 100)
  • user_score — оценка пользователей (максимум 10)
  • rating — возрастной рейтинг ESRB
  • game_id — id игры
In [3]:
games_data.head()
Out[3]:
platform sales_year genre online offline critic_score user_score rating game_id
0 PS3 2017.0 Action 702.0 909.0 97.0 8.2 M 20838
1 X360 2017.0 Action 966.0 514.0 97.0 8.1 M 48673
2 X360 2017.0 Shooter 904.0 424.0 88.0 3.4 M 17801
3 PS4 2015.0 Shooter 603.0 586.0 NaN NaN NaN 12200
4 3DS 2017.0 Role-Playing 528.0 419.0 NaN NaN NaN 11432
In [4]:
games_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4026 entries, 0 to 4025
Data columns (total 9 columns):
platform        4026 non-null object
sales_year      4026 non-null float64
genre           4026 non-null object
online          4026 non-null float64
offline         4026 non-null float64
critic_score    1812 non-null float64
user_score      2382 non-null object
rating          2315 non-null object
game_id         4026 non-null int64
dtypes: float64(4), int64(1), object(4)
memory usage: 283.2+ KB
In [5]:
# Проверка на дубликаты

games_data.duplicated().sum()
Out[5]:
0
In [6]:
print('В столбце с оценкой пользователей содержится', len(games_data[games_data['user_score'] == 'tbd']), 'строк, содержащих tbd')
В столбце с оценкой пользователей содержится 350 строк, содержащих tbd
In [7]:
# Общее количество продаж: сумма онлайн и оффлайн покупок

games_data['revenue'] = games_data['online'] + games_data['offline']

Промежуточные выводы

  • В датасете отсутствуют дубликаты в строках
  • В столбце с оценкой пользователей содержится 7575 "tbd", что означает to be determined - еще пока не определен. Такие значения будут заменены на пропуски (NaN).
  • Пропуски в данных заполняться не будут. Заполнить рейтинг даже по медианным значениям довольно опасно, т.к. на рейтинговые данные влияет слишком много факторов: компания производитель, уровень графики игры, жанр и другие. Предсказание или замена таких субъективных метрик может сильно исказить финальный результат.
  • Замена типа данных будет проивзодиться в столбцах:
    • на целочисленный тип (int): год продажи, выручка (общая, online и offline)
    • на данных с плавающей точкой (float): оценка пользователей (т.к. в столбце есть пропущенные значения, то в целочисленный тип перевести нельзя)

Замена типа данных необходима для упрощения дальнейших расчетов.

In [8]:
# Замена tbd в столбце с оценкой пользователей

games_data.loc[games_data['user_score'] == 'tbd', 'user_score'] = np.nan 
In [9]:
# Изменение типов данных

games_data['sales_year'] = games_data['sales_year'].astype('int')
games_data['online'] = games_data['online'].astype('int')
games_data['offline'] = games_data['offline'].astype('int')
games_data['revenue'] = games_data['revenue'].astype('int')
games_data['user_score'] = games_data['user_score'].astype('float')
In [10]:
# Проверка

games_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 4026 entries, 0 to 4025
Data columns (total 10 columns):
platform        4026 non-null object
sales_year      4026 non-null int64
genre           4026 non-null object
online          4026 non-null int64
offline         4026 non-null int64
critic_score    1812 non-null float64
user_score      2032 non-null float64
rating          2315 non-null object
game_id         4026 non-null int64
revenue         4026 non-null int64
dtypes: float64(2), int64(5), object(3)
memory usage: 314.6+ KB

Исследовательский анализ данных

Постройте распределение по годам продажи для игр с отзывом и без отзыва

In [13]:
# Создаем таблицу для подсчета 

years_bt_review = (
    pd.DataFrame(games_data[(games_data['critic_score'].notnull()) & (games_data['user_score'].notnull())] \
    .groupby('sales_year')['sales_year'].count())
        .join(
                pd.DataFrame(games_data[(games_data['critic_score'].isnull()) | (games_data['user_score'].isnull())]\
                             .groupby('sales_year')['sales_year'].count()), lsuffix='_nan', rsuffix='_not_nan'
            )
    )
years_bt_review.columns = ['Нет оценок', 'Есть хотя бы одна оценка']
In [14]:
years_bt_review.plot(kind='bar', alpha=0.5)
plt.title('Распределение количества игр по годам продажи относительно наличия оценок'+ "\n")
plt.xlabel('Год')
plt.ylabel('Количество')


plt.show()
In [15]:
years_bt_review['Доля без оценок'] = years_bt_review['Нет оценок'] / (years_bt_review['Нет оценок'] \
                                                    + years_bt_review['Есть хотя бы одна оценка']) * 100

years_bt_review['Доля с оценкой'] = 100 - years_bt_review['Доля без оценок']

years_bt_review
Out[15]:
Нет оценок Есть хотя бы одна оценка Доля без оценок Доля с оценкой
sales_year
2015 221 385 36.468647 63.531353
2016 227 275 45.219124 54.780876
2017 738 945 43.850267 56.149733
2018 577 658 46.720648 53.279352

Вывод

Количество и доля реализуемых игр с оценками на протяжении всего исследуемого периода всегда больше, чем у игр без оценок. Не смотря на высокие показатели в 2017 году, максимальная доля проданных игр с оценкой отмечена в 2015 году. Наиболее удачный год для реализации игр без оценок - 2018. Стоит проанализировать стратегию продаж в 2015 и в 2018 годах для увеличения показателей.

Постройте ящик с усами по общим продажам, опишите полученный результат

In [18]:
games_data['revenue'].plot(kind='box', ylim=(-10,200))
plt.title('Распределение выручки за все года')
games_data.boxplot(column='revenue', by='sales_year').set_title("\n")

plt.show()
In [20]:
games_data['revenue'].describe()
Out[20]:
count    4026.000000
mean       37.252360
std        99.358828
min         0.000000
25%         0.000000
50%         8.000000
75%        31.000000
max      1611.000000
Name: revenue, dtype: float64
In [21]:
# Сколько игр не получили денег с продажи с неуказанным рейтингом

len((games_data[(games_data['revenue'] == 0) &
           (games_data['critic_score'].isnull()) & 
           (games_data['user_score'].isnull()) 
          ]
))
Out[21]:
1037

Выводы

  • Медианная выручка с продажи игр менее 800 тысяч рублей.
  • Более 25% игр не принесли компании доходов. Примечательно, что почти у всех (95%) нерентабельных игр не указаны оценки пользователей и критиков. Возможно, есть проблема с данными - они были неправильно выгружены.
  • Максимальная прибыль зафиксирована в 2017 году. К тому же, не смотря на небольшое количество проданных игр в 2015 году, в этот год была реализована одна из самых дорогих игр - за 120 000 000 рублей.
In [26]:
# Проверка зависимости года и рейтинга

games_data.boxplot(by='sales_year', column='user_score').set_title("")
plt.title('Распределение рейтинга зрителей по годам'+ '\n\n')
games_data.boxplot(by='sales_year', column='critic_score').set_title("")
plt.title('Распределение рейтинга критиков по годам' + '\n\n')

plt.show()

Выводы

  • Пользователи меньше всего оценили игру на PS3 в жанре Sport. Ее рейтинг составил всего 0.2 пункта.
  • Больше всего пользователям понравились игры, реализованные в 2015 году на платформах PC и PSV. Игры получили оценку в 9.3 пункта.
  • Критиков больше всего неудовлетворила гоночная игра на платформе PS3 - ей присудили всего 13 пунктов.
  • Самый большой интерес критиков вызвали игры в жанре экшен. Причем игры выпущены на разных игровых платформах, но получили одинаковый максимальный балл - 97 пунктов.
  • Наивысший рейтинг пользователи поставили игре в 2015 году, а 2016 год был самым неудачным для производителей игр - в этот год больше всего игр получили маленький рейтинг от пользователей.
  • Оценки критиков по годам распределились несколько иначе, чем оценки пользователей. Самые неудавные игры, по мнению критиков, были реализованы в 2017 году, а уже в 2018 году проданные игры получили самые высокие оценки критиков.

Составьте портрет пользователя для онлайн и офлайн продаж

In [29]:
def get_top_5(group, type_sale):
    """
    Функция для определения портрета пользователя в зависимости от канала продаж.
    """
    
    top_5 = games_data.groupby(group)[type_sale].sum().reset_index()
    top_5['rate'] = 100 * top_5[type_sale] / top_5[type_sale].sum()
    
    return top_5.sort_values(by=type_sale, ascending=False).head()

Топ-5 предпочтительных жанров

In [30]:
# Топ для онлайн-продаж

get_top_5('genre', 'online')
Out[30]:
genre online rate
0 Action 23088 27.819884
8 Shooter 19443 23.427842
10 Sports 10866 13.092986
7 Role-Playing 8377 10.093866
3 Misc 6967 8.394886
In [31]:
# Топ для онлайн-продаж

get_top_5('genre', 'offline')
Out[31]:
genre offline rate
0 Action 19910 29.730618
8 Shooter 14798 22.097121
10 Sports 8847 13.210787
7 Role-Playing 6229 9.301457
6 Racing 4056 6.056624

Вывод

Клиентв обоих каналов дистрибьюции игр одинаковые предпочтения к тематическим игровым направлениям. Что примечательно, процентное соотношение интересов пользователей почти не отличается между интернет и оффлайн продажами.

В какие года пользователи были наиболее активны (больше всего покупали игры)

In [32]:
# Активность по годам в интернет-продажах

get_top_5('sales_year', 'online')
Out[32]:
sales_year online rate
2 2017 39240 47.282235
3 2018 28574 34.430239
0 2015 10684 12.873685
1 2016 4493 5.413840
In [33]:
# Активность по годам в оффлайн-продажах

get_top_5('sales_year', 'offline')
Out[33]:
sales_year offline rate
2 2017 28446 42.477004
3 2018 23731 35.436328
0 2015 9672 14.442719
1 2016 5119 7.643949

Вывод

Больше всего клиенты интернет-магазина и сети розничных магазинов покупали игры в 2017 году. Стоит отметить, что выручка обоих каналов продаж распределена равномерно. Возможно, необходимо усилить рекламные кампании для интернет-магазина, т.к. в целом в последние года пользователи все чаще делают покупки в интернет-магазинах, чем в оффлайн.

Какой рейтинг ESRB встречается чаще всего (наиболее частотная категория)

In [36]:
# Рейтинг ESRB для интернет-покупок

get_top_5('rating', 'online').merge(ratind_data,left_on='rating', right_on='rating_type')
Out[36]:
rating online rate rating_type decryption
0 M 30842 43.671944 M От 17 лет
1 E 17040 24.128459 E Для всех
2 E10+ 11770 16.666195 E10+ Для всех старше 10 лет
3 T 10928 15.473932 T Подросткам 13—19 лет
4 EC 42 0.059472 EC Для детей младшего возраста
In [37]:
# Рейтинг ESRB для оффлайн-магазина

get_top_5('rating', 'offline').merge(ratind_data,left_on='rating', right_on='rating_type')
Out[37]:
rating offline rate rating_type decryption
0 M 25021 45.239387 M От 17 лет
1 E 14171 25.621972 E Для всех
2 T 8313 15.030375 T Подросткам 13—19 лет
3 E10+ 7800 14.102842 E10+ Для всех старше 10 лет
4 RP 3 0.005424 RP Рейтинг ожидается

Вывод

В обоих каналах продаж лидируют игры с рейтингом "От 17 лет" и "Для всех" - более 65% от всех проданных игр. Стоит отметить, что игры для детей младшего возраста выбирают чаще всего клиенты интернет-магазина. Вероятно, это связано с нехваткой времени для покупок в розничных магазинах. Можно подробнее рассмотреть этот инсайд для корректировки стратегии продвижения игр в интернет-магазинах.

Проверка гипотез

Средний пользовательский рейтинг платформ PS4 и PS3 одинаковый

Н0 - cредний пользовательский рейтинг платформ PS4 и PS3 одинаковый, т.е. средние значения выборок равны.

Н1 - cредний пользовательский рейтинг платформ PS4 и PS3 разный, т.е. средние значения выборок не равны.

Для проверки гипотезы будет использоваться критерий p-value.

In [38]:
results = st.ttest_ind(games_data[(games_data['platform'] == "PS4") & (games_data['user_score'].notna())]['user_score'], \
                       games_data[(games_data['platform'] == "PS3") & (games_data['user_score'].notna())]['user_score'])
alpha = 0.05

print('p-значение: ', results.pvalue)
if results.pvalue < alpha:
    print('Принимаем альтернативную гипотезу')
else:
    print('Опровергнуть нулевую гипотезу нельзя')
p-значение:  0.26218390192537744
Опровергнуть нулевую гипотезу нельзя

Вывод

Полученное значение p-value говорит о том, что средний пользовательский рейтинг платформ PS4 и PS3 отличается, но с вероятностью в почти 26% такое различие можно получить случайно. Это слишком высокая вероятность для того, чтобы делать выводы о значимом различии между средними значениями рейтинга.

Средний пользовательский рейтинг жанров Action и Sports различается

Н0 - средний пользовательский рейтинг жанров Action и Sports одинаковый, т.е. средние значения выборок раввны.

Н1 - средний пользовательский рейтинг жанров Action и Sports разный, т.е. средние значения выборок не равны.

Для проверки гипотезы будет использоваться критерий p-value.

In [39]:
results = st.ttest_ind(games_data[(games_data['genre'] == "Action") & (games_data['user_score'].notna())]['user_score'], \
                       games_data[(games_data['genre'] == "Sports")& (games_data['user_score'].notna())]['user_score'])
alpha = 0.05

print('p-значение: ', results.pvalue)
if results.pvalue < alpha:
    print('Принимаем альтернативную гипотезу')
else:
    print('Опровергнуть нулевую гипотезу нельзя')
p-значение:  5.1974550252152054e-24
Принимаем альтернативную гипотезу

Вывод

p-значение достаточно мало, что дает основание отвергнуть предположение об отсутствии различий между рейтингами жанров Action и Sports, т.е. маловероятна случайность результатов теста гипотезы.