Маркетологи везде. Иногда складывается ощущение, что никто не занимается производством, а все только лишь продают. Ощущение, отчасти, обманчивое поскольку маркетинг — это ремесло, где необходимо быть заметным, а сами маркетологи могут поднимать вокруг своей персоны огромное количество шума (как, например, Торшина).
Но, только лишь отчасти внимание к маркетингу объясняется шумностью маркетологв. Это действительно огромная сфера сфера деятельности, которая в 2018 году заняла 7% рынка вакансий по данным hh.ru.
Те, кого мы привыкли объединять словом «маркетологи» — представляют из себя довольно разношёрстную компанию. Сюда входят и PR-специалисты, которые формируют имидж человека или компании, интернет-маркетологи, из-за которых мы видим рекламу на сайтах, криэйторы, придумывающие «креативы», SEOшникии, SMMщики и ещё более специфические породы.
— Пойдёшь ко мне в штат?
Татарский ещё раз посмотрел на плакат с тремя пальмами и англоязычным обещанием вечных метаморфоз.
— Кем? — спросил он.
— Криэйтором.
— Это творцом? — переспросил Татарский. — Если перевести?
Ханин мягко улыбнулся.
— Творцы нам тут на хуй не нужны, — сказал он. — Криэйтором, Вава, криэйтором.
Но всё-таки в своём первоначальном значении маркетолог — это специалист, занимающийся анализом рынка. Рынок, как известно, составляют спрос и предложение, и маркетолога больше всего интересует первая составляющая рынка — спрос. Сегодня мы рассмотрим типичное задание для специалиста по анализу данных в маркетинге.
Исходные данные:
Задание:
Ответы необходимо представить в виде отчета с необходимыми выводами и графиками.
Первый шаг в любом анализе — разведка. Взглянем на основные признаки и попробуем понять структуру данных.
Для этого вначале импортируем необходимые библиотеки и загрузим данные. Я буду использовать Python и его пакеты.
import numpy as np import pandas as pd import seaborn as sns import matplotlib.pyplot as plt sns.set() %matplotlib inline %config InlineBackend.figure_format = 'svg' pd.options.display.max_columns = 100 date_cols = ["Attributed Touch Time", "Install Time", "Event Time"] installs = pd.read_csv("https://nagornyy.me/datasets/marketing1/installs.csv.zip", sep="\t", parse_dates=date_cols) payments = pd.read_csv("https://nagornyy.me/datasets/marketing1/payments.csv.zip", sep="\t", parse_dates=date_cols)
installs.head()
Attributed Touch Type | Attributed Touch Time | Install Time | Event Time | Event Name | Event Value | Event Revenue | Event Revenue Currency | Event Revenue USD | Event Source | Is Receipt Validated | Partner | Channel | Keywords | Campaign | Campaign ID | Adset | Adset ID | Ad | Ad ID | Ad Type | Site ID | Sub Site ID | Sub Param 1 | Sub Param 2 | Sub Param 3 | Sub Param 4 | Sub Param 5 | Cost Model | Cost Value | Cost Currency | Contributor 1 Partner | Contributor 1 Media Source | Contributor 1 Campaign | Contributor 1 Touch Type | Contributor 1 Touch Time | Contributor 2 Partner | Contributor 2 Media Source | Contributor 2 Campaign | Contributor 2 Touch Type | Contributor 2 Touch Time | Contributor 3 Partner | Contributor 3 Media Source | Contributor 3 Campaign | Contributor 3 Touch Type | Contributor 3 Touch Time | Region | Country Code | State | City | Postal Code | DMA | IP | WIFI | Operator | Carrier | Language | AppsFlyer ID | IMEI | Platform | Device Type | OS Version | App Version | SDK Version | App ID | App Name | Bundle ID | Is Retargeting | Retargeting Conversion Type | Attribution Lookback | Reengagement Window | Is Primary Attribution | User Agent | HTTP Referrer | Original URL | Install App Store | Match Type | Contributor 1 Match Type | Contributor 2 Match Type | Contributor 3 Match Type | Device Category | Google Play Referrer | Google Play Click Time | Google Play Install Begin Time | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | click | 2018-09-30 23:42:00 | 2018-09-30 23:56:00 | 2018-09-30 23:56:00 | install | NaN | NaN | NaN | NaN | SDK | NaN | NaN | NaN | NaN | AOM_2018_test_1_WW | NaN | NaN | NaN | AOM_Appstore_01_En+V | NaN | NaN | 1550428 | NaN | NaN | NaN | NaN | NaN | NaN | cpi | 2.60 | USD | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | EU | AT | 9 | Vienna | 1100 | None | 84.113.35.31 | True | NaN | NaN | de-AT | 1538347375900-8545045 | NaN | ios | iPad Air 2 | 12 | 1.7.4 | v4.8.7 | id1175370741 | Age Of Magic | com.playkot.ageofmagic | False | NaN | 7d | NaN | NaN | ageofmagic/3 CFNetwork/974.2.1 Darwin/18.0.0 | NaN | https://app.appsflyer.com/id1175370741?pid=uni... | NaN | id_matching | NaN | NaN | NaN | tablet | NaN | NaN | NaN |
1 | click | 2018-09-30 09:15:00 | 2018-09-30 23:50:00 | 2018-09-30 23:50:00 | install | NaN | NaN | NaN | NaN | SDK | NaN | NaN | NaN | NaN | AOM_2018_test_1_WW | NaN | NaN | NaN | AOM_Appstore_01_En+V | NaN | NaN | 2718129 | NaN | NaN | NaN | NaN | NaN | NaN | cpi | 3.50 | USD | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | EU | UK | OXF | Abingdon | ox13 5jb | 826045 | 37.60.86.18 | True | NaN | NaN | en-GB | 1538291762135-7959619 | NaN | ios | iPhone 7 Plus | 11.4.1 | 1.7.4 | v4.8.7 | id1175370741 | Age Of Magic | com.playkot.ageofmagic | False | NaN | 7d | NaN | NaN | ageofmagic/3 CFNetwork/902.2 Darwin/17.7.0 | NaN | https://app.appsflyer.com/id1175370741?pid=uni... | NaN | id_matching | NaN | NaN | NaN | phone | NaN | NaN | NaN |
2 | click | 2018-09-24 15:23:00 | 2018-09-30 23:49:00 | 2018-09-30 23:49:00 | install | NaN | NaN | NaN | NaN | SDK | NaN | NaN | NaN | NaN | AOM_2018_test_1_WW | NaN | NaN | NaN | AOM_AGM_8_NO_Sound_europe | NaN | NaN | 1550428 | NaN | NaN | NaN | NaN | NaN | NaN | cpi | 0.55 | USD | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | EU | BE | BRU | Brussels | None | None | 109.130.165.97 | True | NaN | NaN | fr-BE | 1537799053703-9915353 | NaN | ios | iPhone X | 12 | 1.7.4 | v4.8.7 | id1175370741 | Age Of Magic | com.playkot.ageofmagic | False | NaN | 7d | NaN | NaN | ageofmagic/3 CFNetwork/974.2.1 Darwin/18.0.0 | NaN | https://app.appsflyer.com/id1175370741?pid=uni... | NaN | id_matching | NaN | NaN | NaN | phone | NaN | NaN | NaN |
3 | impression | 2018-09-30 13:08:00 | 2018-09-30 23:47:00 | 2018-09-30 23:47:00 | install | NaN | NaN | NaN | NaN | SDK | NaN | NaN | NaN | NaN | AOM_2018_test_1_FR | NaN | NaN | NaN | AOM_Appstore_01_En+V | NaN | NaN | 2731992 | NaN | NaN | NaN | NaN | NaN | NaN | cpi | 3.74 | USD | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | EU | FR | OCC | Perpignan | 66000 | 250066 | 5.48.50.147 | True | NaN | NaN | fr-FR | 1538345877931-2516524 | NaN | ios | iPhone 8 | 11.4.1 | 1.7.4 | v4.8.7 | id1175370741 | Age Of Magic | com.playkot.ageofmagic | False | NaN | 1d | NaN | NaN | ageofmagic/3 CFNetwork/902.2 Darwin/17.7.0 | NaN | https://impression.appsflyer.com/id1175370741?... | NaN | id_matching | NaN | NaN | NaN | phone | NaN | NaN | NaN |
4 | click | 2018-09-30 23:05:00 | 2018-09-30 23:41:00 | 2018-09-30 23:41:00 | install | NaN | NaN | NaN | NaN | SDK | NaN | NaN | NaN | NaN | AOM_2018_test_1_WW | NaN | NaN | NaN | AOM_Appstore_01_En+V | NaN | NaN | 1458550 | NaN | NaN | NaN | NaN | NaN | NaN | cpi | 3.50 | USD | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | EU | UK | BNH | Brighton | bn2 0aa | 826045 | 31.54.86.92 | True | NaN | NaN | en-GB | 1538341538555-9884715 | NaN | ios | iPhone 7 | 11.4 | 1.7.4 | v4.8.7 | id1175370741 | Age Of Magic | com.playkot.ageofmagic | False | NaN | 7d | NaN | NaN | ageofmagic/3 CFNetwork/901.1 Darwin/17.6.0 | NaN | https://app.appsflyer.com/id1175370741?pid=uni... | NaN | id_matching | NaN | NaN | NaN | phone | NaN | NaN | NaN |
В таблице содержатся данные о 13098 установках и 84 признаках:
installs.shape
Как видно, многие переменные довольно бессмысленны — они или содержат только пропущенные значения или или заполнены лишь одним ненулевым значением. Доля непустых значений в переменной находится в колонке Filled share
, а количество уникальных значение в колонке N unique
.
pd.DataFrame({ "Filled share": ((~installs.isnull()).sum() / len(installs)).round(2), "N unique": installs.nunique() }).head()
Filled share | N unique | |
---|---|---|
Attributed Touch Type | 1.0 | 2 |
Attributed Touch Time | 1.0 | 10476 |
Install Time | 1.0 | 10723 |
Event Time | 1.0 | 10723 |
Event Name | 1.0 | 1 |
Похожим образом обстоят дела и с таблицей payments, где содержится информация ровно об одном типе событий — внутриигровых покупках. Разумеется, её размер меньше предыдущей таблицы, поскольку не все пользователи, установившие приложение, дошли до этапа покупки.
payments.head()
Attributed Touch Type | Attributed Touch Time | Install Time | Event Time | Event Name | Event Value | Event Revenue | Event Revenue Currency | Event Revenue USD | Event Source | Is Receipt Validated | Partner | Channel | Keywords | Campaign | Campaign ID | Adset | Adset ID | Ad | Ad ID | Ad Type | Site ID | Sub Site ID | Sub Param 1 | Sub Param 2 | Sub Param 3 | Sub Param 4 | Sub Param 5 | Cost Model | Cost Value | Cost Currency | Contributor 1 Partner | Contributor 1 Media Source | Contributor 1 Campaign | Contributor 1 Touch Type | Contributor 1 Touch Time | Contributor 2 Partner | Contributor 2 Media Source | Contributor 2 Campaign | Contributor 2 Touch Type | Contributor 2 Touch Time | Contributor 3 Partner | Contributor 3 Media Source | Contributor 3 Campaign | Contributor 3 Touch Type | Contributor 3 Touch Time | Region | Country Code | State | City | Postal Code | DMA | IP | WIFI | Operator | Carrier | Language | AppsFlyer ID | Platform | Device Type | OS Version | App Version | SDK Version | App ID | App Name | Bundle ID | Is Retargeting | Retargeting Conversion Type | Attribution Lookback | Reengagement Window | Is Primary Attribution | User Agent | HTTP Referrer | Original URL | Install App Store | Match Type | Contributor 1 Match Type | Contributor 2 Match Type | Contributor 3 Match Type | Device Category | Google Play Referrer | Google Play Click Time | Google Play Install Begin Time | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
0 | click | 2018-09-30 23:33:00 | 2018-09-30 23:35:00 | 2018-09-30 23:52:00 | af_purchase | {"af_revenue":"1.99","af_currency":"GBP","af_q... | 1.99 | GBP | 2.595963 | SDK | NaN | NaN | NaN | NaN | AOM_2018_test_1_WW | NaN | NaN | NaN | AOM_Appstore_01_En+V | NaN | NaN | 2731992 | NaN | NaN | NaN | NaN | NaN | NaN | cpi | 3.50 | USD | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | EU | UK | BST | Bristol | bs4 1aa | 826047 | 92.234.108.92 | True | NaN | NaN | en-GB | 1538343242930-8513482 | ios | iPhone 8 | 11.4 | 1.7.4 | v4.8.7 | id1175370741 | Age Of Magic | com.playkot.ageofmagic | False | NaN | 7d | NaN | True | ageofmagic/3 CFNetwork/901.1 Darwin/17.6.0 | NaN | https://app.appsflyer.com/id1175370741?pid=uni... | NaN | id_matching | NaN | NaN | NaN | phone | NaN | NaN | NaN |
1 | click | 2018-09-23 21:01:00 | 2018-09-24 16:21:00 | 2018-09-30 23:36:00 | af_purchase | {"af_revenue":"8.99","af_currency":"EUR","af_q... | 8.99 | EUR | 10.450791 | SDK | NaN | NaN | NaN | NaN | AOM_2018_test_1_WW | NaN | NaN | NaN | AOM_Appstore_01_En+V | NaN | NaN | 1527921 | NaN | NaN | NaN | NaN | NaN | NaN | cpi | 2.00 | USD | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | EU | ES | B | Barcelona | 8024 | None | 93.176.147.87 | True | NaN | NaN | es-ES | 1537732876768-5561101 | ios | iPhone 7 Plus | 11.4.1 | 1.7.4 | v4.8.7 | id1175370741 | Age Of Magic | com.playkot.ageofmagic | False | NaN | 7d | NaN | True | ageofmagic/3 CFNetwork/902.2 Darwin/17.7.0 | NaN | https://app.appsflyer.com/id1175370741?pid=uni... | NaN | id_matching | NaN | NaN | NaN | phone | NaN | NaN | NaN |
2 | click | 2018-09-23 21:01:00 | 2018-09-24 16:21:00 | 2018-09-30 23:33:00 | af_purchase | {"af_revenue":"7.99","af_currency":"EUR","af_q... | 7.99 | EUR | 9.288300 | SDK | NaN | NaN | NaN | NaN | AOM_2018_test_1_WW | NaN | NaN | NaN | AOM_Appstore_01_En+V | NaN | NaN | 1527921 | NaN | NaN | NaN | NaN | NaN | NaN | cpi | 2.00 | USD | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | EU | ES | B | Barcelona | 8024 | None | 93.176.147.87 | True | NaN | NaN | es-ES | 1537732876768-5561101 | ios | iPhone 7 Plus | 11.4.1 | 1.7.4 | v4.8.7 | id1175370741 | Age Of Magic | com.playkot.ageofmagic | False | NaN | 7d | NaN | True | ageofmagic/3 CFNetwork/902.2 Darwin/17.7.0 | NaN | https://app.appsflyer.com/id1175370741?pid=uni... | NaN | id_matching | NaN | NaN | NaN | phone | NaN | NaN | NaN |
3 | click | 2018-09-26 14:32:00 | 2018-09-26 15:00:00 | 2018-09-30 23:14:00 | af_purchase | {"af_revenue":"40","af_currency":"CNY","af_qua... | 40.00 | CNY | 5.823179 | SDK | NaN | NaN | NaN | NaN | AOM_2018_test_1_CN | NaN | NaN | NaN | AOM_CN_TR+V | NaN | NaN | 2731992 | NaN | NaN | NaN | NaN | NaN | NaN | cpi | 3.50 | USD | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | AS | CN | HB | Jingmen | 448000 | 156175 | 113.57.183.59 | False | NaN | NaN | en-CN | 1537990383550-7399746 | ios | iPhone 7 Plus | 11.4.1 | 1.7.4 | v4.8.7 | id1175370741 | Age Of Magic | com.playkot.ageofmagic | False | NaN | 7d | NaN | True | ageofmagic/3 CFNetwork/902.2 Darwin/17.7.0 | NaN | https://app.appsflyer.com/id1175370741?pid=uni... | NaN | id_matching | NaN | NaN | NaN | phone | NaN | NaN | NaN |
4 | click | 2018-09-26 06:01:00 | 2018-09-27 07:12:00 | 2018-09-30 23:10:00 | af_purchase | {"af_revenue":"4.99","af_currency":"USD","af_q... | 4.99 | USD | 4.990000 | SDK | NaN | NaN | NaN | NaN | AOM_2018_test_1_US | NaN | NaN | NaN | AOM_app_store_EN_Fixed+V | NaN | NaN | 93825 | NaN | NaN | NaN | NaN | NaN | NaN | cpi | 4.39 | USD | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | NaN | US | AZ | Tempe | 85283 | 753 | 98.171.18.25 | True | NaN | NaN | en-US | 1537905740951-9564760 | ios | iPad Pro (10.5 inch) | 12 | 1.7.4 | v4.8.7 | id1175370741 | Age Of Magic | com.playkot.ageofmagic | False | NaN | 7d | NaN | True | ageofmagic/3 CFNetwork/974.2.1 Darwin/18.0.0 | NaN | https://app.appsflyer.com/id1175370741?pid=uni... | NaN | id_matching | NaN | NaN | NaN | tablet | NaN | NaN | NaN |
payments.shape
pd.DataFrame({ "Filled share": ((~payments.isnull()).sum() / len(payments)).round(2), "N unique": payments.nunique() }).head()
Filled share | N unique | |
---|---|---|
Attributed Touch Type | 1.0 | 2 |
Attributed Touch Time | 1.0 | 1434 |
Install Time | 1.0 | 1435 |
Event Time | 1.0 | 5013 |
Event Name | 1.0 | 1 |
Посмотрим, какие переменные есть в таблице установок, но нет в таблице платежей и наоборот.
set(payments.columns) ^ set(installs.columns)
Только одна уникальная переменная присутствует в таблице установок — IMEI
, но она абсолютно бесполезна, поскольку содержит лишь пустые значения.
Ответим на первый вопрос задания, а именно, окупилась ли рекламная кампания. Рекламная компания представляет собой систему мероприятий, направленных на определённую аудиторию и использующих определённые площадки. В нашем случае, площадками являются сайты, а кампании направлены на аудиторию разных регионов. Видимо, названия кампания расшифровываются следующим образом: World Wide (AOM_2018_test_1_WW
), China (AOM_2018_test_1_CN
и AOM_2018_test_1_CN_WL_2
), United States (AOM_2018_test_1_US
), France (AOM_2018_test_1_FR
), Denmark (AOM_2018_test_1_DE
) и т.д.
Для рассчёта окупаемости кампании воспользуемся известным показателем окупаемости — Return on Investment. ROI рассчитывается следующим образом:
Показатель Gain from Investment можно узнать из переменной Event Revenue USD
(стоимость внутриигровой покупки, которую сделал пользователь), а о Cost of Investment может сказать переменная Cost Value
(стоимость привлечения нового пользователя).
Итак, начнём. Сразу бросается в глаза, что в таблице c установками содержится информация о меньшем количестве кампаний, чем в таблице с платежами. Это может быть по разным причинам, но в данном случае не существенно.
gain = payments.groupby("Campaign")["Event Revenue USD"].sum().sort_values() cost = installs.groupby("Campaign")["Cost Value"].sum() roi = (gain - cost) / cost * 100 roi.sort_values(ascending=False)
Выводы:
AOM_2018_test_1_CA
, AOM_2018_test_CN_Retention
и AOM_2018_test_CN_WL
невозможно посчитать ROI, поскольку неизвестны затраты на привлечение пользователей в рамках этих кампаний (отсутствуют в таблице с установками).AOM_2018_test_1_CN
, AOM_2018_test_1_WW
и AOM_2018_test_1_DE
), а три остальные убыточные.AOM_2018_test_1_CN
(), наиболее убыточная — AOM_2018_test_1_US
().Каких сабпаблишеров стоит отключить, а на каких поднять ставку? Сабпаблишером является уникальный Site ID
в рамках уникального Campaign
.
В качестве индикатора прибыльности сабпаблишера будем использовать CPI — отношение бюджета рекламной кампании к количеству установок приложения. Будем использовать простейшую стратегию: если CPI маленький — повышаем ставку, чтобы её предельная полезность была максимальная, если слишком большой — отключаем.
Причем, необходимо считать CPI для каждого сабпаблишера в каждой кампании, поскольку они могут пересекаться. Да и в целом логично предположить, что сабпаблишер, который принёс мало установок в рамках одной кампании, покажет совершенно другой результат в рамках другой, потому что в первом случае кампания могла не попасть в аудиторию сабпаблишера.
installs_groupby = installs.groupby(["Campaign", "Site ID"]) ad_cost_by_site_and_campaign = installs_groupby["Cost Value"].sum() num_install_by_site_and_campaign = installs_groupby.size() cpi = ad_cost_by_site_and_campaign / num_install_by_site_and_campaign
cpi.head()
Итак, CPI для каждого сабпаблишера в каждой кампании рассчитан. Взглянем на распределение:
for campaign in installs.Campaign.unique(): plt.figure() plt.title(f"Распределение CPI в кампании\n{campaign} по сабпаблишерам") cpi.loc[campaign].hist();
Только в кампании AOM_2018_test_1_WW
распределение более-менее похоже на нормальное. Самый странный случай — кампания AOM_2018_test_1_CN
, в которой все сабпаблишеры имеют одинаковый CPI. Спишем такие аномалии на особенности выборки для задания — кажется, она была не случайной.
Из-за таких аномалий стратегию ставок для каждой кампании стоит считать отдельно. В кампании AOM_2018_test_1_WW
я бы отключил сабпаблишеров, которые находятся в нижнем дециле и повысил ставки у сабпаблишеров из верхнего дециля.
first_decile = cpi.loc["AOM_2018_test_1_WW"].quantile(.10) last_decile = cpi.loc["AOM_2018_test_1_WW"].quantile(.90) print(f"Исключаем, если CPI ниже {first_decile}, повышаем ставки, если CPI выше {last_decile}")
raise_rates_WW = cpi.loc["AOM_2018_test_1_WW"][cpi.loc["AOM_2018_test_1_WW"] > last_decile] exclude_WW = cpi.loc["AOM_2018_test_1_WW"][cpi.loc["AOM_2018_test_1_WW"] < first_decile] # сохраним список сабпаблишеров, которых следует исключить и которым надо повысить ставки raise_rates_WW.to_excel("output/raise_rates_WW.xlsx") exclude_WW.to_excel("output/exclude_WW.xlsx")
В остальных кампаниях разброс значений CPI для разных сабпаблишеров не сильно велик (например, в кампании AOM_2018_test_1_US
разница между паблешерами с самым низким и самым высоким CPI составляет одну десятую, а в AOM_2018_test_1_CN
вообще у всех CPI одинаков), поэтому я бы не стал выносить решение об исключении или повышении ставок до тех пор, пока в моём распоряжении не будет полная выборка.
«Кривая LTV – L(t) – это функция, отражающая средний доход с игрока в зависимости от времени с момента регистрации. Например, если за месяц с момента регистрации пользователи в среднем приносят по $2,55, то говорят, что «LTV 30 равен 2,55». То есть L (30) = 2,55».
Для рассчёта LTV нам нужны следующие переменные:
Install Time
Event Time
Event Revenue USD
IP
, но, если я всё правильно понял, более точно идентифицурует пользователя поле AppsFlyer ID
.Теперь непосредственно про построение LTV — её можно построить разными способами, для самого простого из которых достаточно «весь доход приложения за период разделить на общее количество пользователей за тот же период». Но это не наш путь, ведь итоговая цель — построение прогнозной модели.
В нашем случае LTV будет рассчитваться частное от кумулятивной суммы покупок пользователей и кумулятивного количества этих пользователей за разные промежутки времени:
ltv = payments[["AppsFlyer ID", "Install Time", "Event Time", "Event Revenue USD"]].copy() ltv["install_date"] = ltv["Install Time"].dt.round("D") ltv["purchase_date"] = ltv["Event Time"].dt.round("D") def get_cum_sum(date): return ltv['Event Revenue USD'].where(ltv['purchase_date']<=date).sum() ltv['cum_sum'] = ltv['purchase_date'].map(lambda x: get_cum_sum(x)) def get_users_cumcount(date): return ltv["AppsFlyer ID"].where(ltv['install_date'] <=date).count() ltv['users_cum_count'] = ltv['purchase_date'].map(lambda x: get_users_cumcount(x)) ltv['ltv'] = ltv['cum_sum'] / ltv['users_cum_count'] ltv['day'] = pd.to_timedelta(ltv['purchase_date'] - ltv['install_date'].min()).dt.days + 1
ltv_forecast = ltv.drop_duplicates(subset=["cum_sum", "day"])
plt.scatter(ltv_forecast['day'], ltv_forecast['ltv']) plt.xlabel('Days') plt.ylabel('LTV')
На данном графике по оси X отложено количество дней, прошедших с момента установки приложения до момента совершения первой покупки, а по оси Y — LTV. Видно, что чем больше дней проходит, тем выше LTV.
Спрогнозируем его будущие значения на 60 дней вперёд при помощи авторегрессионной модели ARIMA первого пордка по всем компонентам (по хорошему надо протестировать разные модели, но это всё-таки только тестовое задание).
Функция автокорреляции показывает наличие корреляции первого и второго порядков, так что можно ожидать неплохой модели.
from statsmodels.graphics.tsaplots import plot_acf plot_acf(ltv_forecast.ltv, lags=20);
ts_ltv = pd.Series(data=ltv_forecast.ltv.values, index=ltv_forecast.day.values).sort_index()
from statsmodels.tsa.arima_model import ARIMA model = ARIMA(ts_ltv, order=(2,1,1)); # просто модель model_fit = model.fit();
Предсказываем на 60 дней вперёд
model_fit.plot_predict(end=90);
Для анализа LTV можно также воспользоваться пакетом lifetimes. Он содержит множество модулей для работы с LTV и постоянно развивается. Lifetimes позволяет производить разные виды анализа — предсказывать, вероятность того, что пользователь вернётся, исходя из его поведения в будущем, рассчитать вероятное количество покупок пользователя в следующий период и т.д. Вот, например, вероятность возвращения пользователя:
import lifetimes as lt ltv_summary = lt.utils.summary_data_from_transaction_data(ltv, 'AppsFlyer ID', 'Event Time', monetary_value_col='Event Revenue USD') bgf = lt.BetaGeoFitter(penalizer_coef=0.0) bgf.fit(ltv_summary['frequency'], ltv_summary['recency'], ltv_summary['T']) lt.plotting.plot_probability_alive_matrix(bgf);
Самый простой способ способ проверить эти гипотезы — построить графики. Зачастую, всё будет понятно из них.
Начнём с первой гипотезы и для начала построим круговые диаграммы, отражающие количество девайсов каждого вида и средний платёж их владельцев.
def iphone_ipad(device: str): for apple_device in ["iphone", "ipad"]: if apple_device in device.lower(): return apple_device payments["iphone_ipad"] = payments["Device Type"].apply(iphone_ipad)
plt.title("Распределение iPhone и iPad в выборке") payments["iphone_ipad"].value_counts().plot(kind="pie", autopct='%.2f');
plt.title("Средняя сумма платежей с iPhone и iPad в выборке") payments[["iphone_ipad", "Event Revenue USD"]] \ .groupby("iphone_ipad").mean()["Event Revenue USD"].plot(kind="pie", autopct='%.2f');
Видим, что средняя сумма платежей весьма близка. Простой графический анализ в этом случае не поможет ответить на вопрос, а вот t-test поможет. t-test — это тест, который проверяет гипотезу о равенстве средних в двух выборках. В нашем случае это будут средние платежи владельцев iPhone и iPad. Но как и любой статистический тест, он имеет некоторые ограничения — распределение в выборках должно быть нормальным, и их дисперсии не должны сильно отличаться.
iphone_revenue = payments.query("iphone_ipad == 'iphone'")["Event Revenue USD"] ipad_revenue = payments.query("iphone_ipad == 'ipad'")["Event Revenue USD"]
Проверим, сильно ли различаются дисперсии:
iphone_revenue.var(), ipad_revenue.var()
Даже на глаз видно, что не сильно. Тогда посмотрим на распределение:
iphone_revenue.plot(kind="hist"), ipad_revenue.plot(kind="hist")
Распределение явно не нормальное — следствие всё той же неплоной выборки. Попробуем логарифмировать, а если не поможет — применим трансформацию Бокса-Кокса.
np.log(iphone_revenue).plot(kind="hist"), np.log(ipad_revenue).plot(kind="hist")
Да, логарифмирование сделало распределение более-менее нормальным. По крайнем мере, вряд ли на такой выборке удатся добиться чего-то сильно лучшего.
Время применять t-test:
from scipy.stats import ttest_ind ttest_ind(iphone_revenue, ipad_revenue)
p-value показывает вероятность того, что нулевая гипотеза о равенстве средних верна. В нашем случае p-value намного больше 0.05, поэтому мы не можем утверждать, что средние цены не равны. Следовательно ответ: различие в платежах с iPhone и iPad статистически не значимо, нельзя сказать, что iPhone платит лучше iPad.
Начнём с перейдём ко второй гипотезе. Посмотрим на распределение среднего количество платежей по девайсу:
payments.groupby("Device Type")["Event Revenue USD"].mean().sort_values(ascending=True).plot.barh();
Весьма странная картина — с чего бы владельцам уже сильно устаревшего iPhone 6 быть самыми платежеспособными? Возможно, распределение количества устройств поможет дать ответ.
payments["Device Type"].value_counts(ascending=True)
Теперь всё ясно — дело в очень перекошеной выборке. В наборе данных имеется всего один человек с iPhone 6, и да — он платит больше, чем владельцы остальных устройств в среднем. Однако, на такой выборке нельзя делать статистически значимых выводов, на ней невозможно оценить ошибку. Для корректного ответа на поставленный вопрос я бы попросил полную выборку данных.
Определить окупилась ли кампания
AOM_2018_test_1_CA
, AOM_2018_test_CN_Retention
и AOM_2018_test_CN_WL
невозможно посчитать ROI, поскольку неизвестны затраты на привлечение пользователей в рамках этих кампаний (отсутствуют в таблице с установками).AOM_2018_test_1_CN
, AOM_2018_test_1_WW
и AOM_2018_test_1_DE
), а три остальные убыточные.AOM_2018_test_1_CN
(), наиболее убыточная — AOM_2018_test_1_US
().Каких сабпаблишеров стоит отключить, а на каких поднять ставку?*
Список предполагаемых сабпаблишеров для кампании AOM_2018_test_1_WW находится в файлах output/raise_rates_WW.xlsx
и output/exclude_WW.xlsx
. Для принятия решения по поводу остальных компаний мало данных.
Построить кривую LTV и спрогнозировать значение на 2 месяца вперед
Сделано при помощи модели ARIMA.
Проверить гипотезы: iPhone платит лучше iPad; iPhone X самый платящий девайс.
iPhone платит лучше iPad — различие в платежах с iPhone и iPad статистически не значимо, нельзя сказать, что iPhone платит лучше iPad.
iPhone X самый платящий девайс — нельзя утверждать однозначно, малый размер выборки.
Комментарии