Раз и навсегда. Как автоматизировать построение когорт с Python и Pandas

Скорее всего вы уже знакомы с когортным анализом и знаете, что он позволяет сделать вывод о направлении развития продукта. 
Само по себе построение когорт, при наличии времени, не так уж и сложно. В развивающемся продукте строить когорты следует для оценки каждого изменения. Туториалов как строить когорты в excel хватает. 
А что если продуктов несколько? И в каждом по несколько сегментов пользователей, поведение которых могло измениться? Каждые 5–7 минут затраченные на один сегмент выливаются в часы рутинной работы.

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

когортный анализ позволяет сделать вывод о направлении развития продукта

Постановка задачи

Представьте, что у вас есть мобильное приложение по заказу и доставке еды. На сервере хранятся данные по пользователям, которые пришли к вам и что-то купили в определенное время, кто-то даже несколько раз. В какой-то момент в приложение внедрили систему лояльности. Стали ли клиенты после ее внедрения покупать чаще или на большую сумму? Давайте узнаем.

Для этого нам понадобятся:

  • Данные для анализа
  • Python
  • Pandas
  • NumPy
  • Seaborn
  • iPython

Или

Данные для анализа + Anaconda + Seaborn

И конечно же когорты, чтобы ответить на вопрос!

Хочу решение прямо сейчас!

Лень читать мануал и есть опыт работы с Pandas? Скрольте в конец статьи и скачайте готовый шаблон в формате IPython :)

Шаг за шагом

Загрузим все библиотеки и свои данные. Для работы и получения результатов рекомендую использовать блокноты iPython.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib as mpl
pd.set_option(‘max_columns’, 50)
mpl.rcParams[‘lines.linewidth’] = 2
# чтобы графики строились непосредственно в блокноте iPython
%matplotlib inline
df = pd.read_excel(‘example.xlsx’) # наш файл с данными, должен быть в той же директории, что и сам блокнот
df.shape #размерность матрицы
df.head() #покажем выборку полученного dataset

1. Группировка данных по периоду заказа

Определим в OrderPeriod месяц и год когда была совершена покупка (у вас это может быть другое целевое действие)

df[‘OrderPeriod’] = df.orderDTE.apply(lambda x: x.strftime(‘%Y-%m’))
df.head()

Теперь в DataFrame добавлен столбец OrderPeriod — дата в формате Год-Месяц, отображающая месяц совершения нашего целевого действия(покупки). Один уровень для когортного анализа готов.

2. Группировка данных по первой покупке клиента

Важный шаг, на котором мы узнаем когда клиент совершил свою первую покупку. Именно здесь мы определяем в какую когорту попадет клиент. Для этого необходимо выбрать минимальное значение OrderPeriod для каждого ClientID и добавить в наш DataFrame. На этом же шаге добавим столбец TotalOrders для каждой строки с покупкой и заполним нулями.

df.set_index(‘ClientID’, inplace=True) #добавим индекс в dataFrame по ClientID
df[‘JoinMonth’] = df.groupby(level=0)[‘orderDTE’].min().apply(lambda x: x.strftime(‘%Y-%m’)) #добавим столбец JoinMonth
df.reset_index(inplace=True) #переиндексируем df
df.insert(len(df.columns), “TotalOrders”, 0, allow_duplicates=False) #создадим столбец в котором далее поместим количество заказов
df.head()

3. Объединим группы.

Это немного похоже на то, как работают сводные таблицы в Excel. Верхняя группировка по JoinMonth, следом по OrderPeriod.

На этом же шаге сделаем ряд вычислений. Определим количество покупок по количеству элементов в одном OrderPeriod и количество уникальных клиентов и общую сумму.

grouped = df.groupby([‘JoinMonth’, ‘OrderPeriod’])
# количество уникальных пользователей и общее количество заказов и выручка за период
cohorts = grouped.agg({‘ClientID’: pd.Series.nunique,
‘TotalOrders’: pd.Series.count,
‘Revenue’: np.sum})
# переименуем некоторые столбцы для большей наглядности
cohorts.rename(columns={‘ClientID’: ‘TotalUsers’,
‘TotalOrders’: ‘TotalOrders’}, inplace=True)
cohorts.head()
когорта января 2015 года, пользователей еще не так много

4. Упорядочивание.

Анализ данных проще, когда есть порядок. Добавим порядковое значение CohortPeriod для каждого из OrderPeriod. 
CohortPeriod определим как порядковый номер месяца в массиве начинающийся с 1. Но можно вести отчет с 0, если так удобнее.

def cohort_period(df):
 df[‘CohortPeriod’] = np.arange(len(df)) + 1 # отсчет с 1
return df
cohorts = cohorts.groupby(level=0).apply(cohort_period)
cohorts.head()
Порядок более всего помогает ясному усвоению.

5. Нет ли ошибок?

В анализе данных важна точность. Если при запуске следующего куска кода ошибки не появились, то все ок!

x = df[(df.JoinMonth == ‘2015–01’) & (df.OrderPeriod == ‘2015–01’)]
y = cohorts.ix[(‘2015–01’, ‘2015–01’)]
assert(x[‘ClientID’].nunique() == y[‘TotalUsers’])
assert(x[‘Revenue’].sum() == y[‘Revenue’])
x = df[(df.JoinMonth == ‘2015–01’) & (df.OrderPeriod == ‘2015–09’)]
y = cohorts.ix[(‘2015–01’, ‘2015–09’)]
assert(x[‘ClientID’].nunique() == y[‘TotalUsers’])
assert(x[‘Revenue’].sum() == y[‘Revenue’])
x = df[(df.JoinMonth == ‘2015–05’) & (df.OrderPeriod == ‘2015–09’)]
y = cohorts.ix[(‘2015–05’, ‘2015–09’)]
assert(x[‘ClientID’].nunique() == y[‘TotalUsers’])
assert(x[‘Revenue’].sum() == y[‘Revenue’])

6. Считаем Retention.

Тот шаг из-за которого все затевалось.
Посмотрим как меняется процент возврата в каждой группе. Чтобы сделать это нужно создать в Pandas ряд содержащий JoinMonth и размер ряда.

# переиндексируем DataFrame
cohorts.reset_index(inplace=True)
cohorts.set_index([‘CohortPeriod’,’JoinMonth’], inplace=True)
# создадим ряд содержаший размер каждой когорты JoinMonth
cohort_group_size = cohorts[‘TotalUsers’].groupby(level=1).first()
cohort_group_size.head()
# получим:
JoinMonth
2015-01 23
2015-02 102
2015-03 79
2015-04 82
2015-05 100
Name: TotalUsers, dtype: int64

Сейчас нам предстоит разделить значение TotalUsers в когортах по cohort_group_size.

Поскольку операции в DataFrame осуществляются на основе индексов объектов, то давайте создадим матрицу, где каждый столбец представляет CohortPeriod и каждая строка является JoinMonth, соответствующие этой группе.

Чтобы проиллюстрировать, что мы получим, выведем заголовки созданного ранее ряда cohorts

cohorts[‘TotalUsers’].head()

Получим:
JoinMonth CohortPeriod
2015–01 1 6
 2 1
 3 1
 4 2
 5 2
Name: TotalUsers, dtype: int64

Готово, теперь есть когорта, в которой посчитан retention пользователей по месяцам в привычном нам виде. Давайте посмотрим на нее.

cohorts[‘TotalUsers’].unstack(1).head(15)

Изменим данные, так, чтобы показать доли возврата от первоначального размера когорты.

user_retention = cohorts[‘TotalUsers’].unstack(1).divide(cohort_group_size, axis=0)
user_retention.head()

Цель достигнута, когорты retention построены!

Бонус. Визуализируй это!

На предыдущем шаге мы могли закончить, но возможно кто-то уже поставил библиотеку seaborn и задался вопросом зачем?

Теперь мы добрались до того момента, когда можно и нужно визуализировать данные по когортам. Но т.к. индексы мы задали по столбцам, в которых удобно расположился CohortPeriod, то для нужд визуализации придется транспонировать матрицу так, чтобы по столбцам шли значения JoinMonth.

# переиндексируем DataFrame
cohorts.reset_index(inplace=True)
cohorts.set_index([‘JoinMonth’,’CohortPeriod’], inplace=True) #транспонировали, просто поменяв местами JoinMonth и CohortPeriod и далее добавим axis=1
# создадим ряд содержаший размер каждой когорты JoinMonth
cohort_group_size = cohorts[‘TotalUsers’].groupby(level=0).first()
cohorts[‘TotalUsers’].unstack(0)
user_retention = cohorts[‘TotalUsers’].unstack(0).divide(cohort_group_size, axis=1)
user_retention[[‘2015–02’, ‘2015–05’, ‘2015–07’]].plot(figsize=(20,10))
plt.title(‘Cohorts: User Retention’)
plt.xticks(np.arange(1, 12.1, 1)) # разбивка оси X
plt.xlim(1, 12) #ось X
plt.ylabel(‘% of Cohort Purchasing’);
Теперь мы добрались до того момента когда можно визуализировать данные по когортам.
графическое представление февральской, июньской и июльской когорт

Для того чтобы построить красивую табличку Retention типа той, что в Google Analytics, воспользуемся библиотекой seaborn. Если еще не поставили, то в терминале введите pip install seaborn.

cimport seaborn as sns
sns.set(style=’ticks’)
plt.figure(figsize=(24, 16))
plt.title(‘Cohorts: User Retention’)
sns.heatmap(user_retention.T, mask=user_retention.T.isnull(), annot=True, fmt=’.0%’);
в мае включили систему лояльности и получили улчучшение возврата клиентов

Еще один бонус-трек. Когорта частоты покупок.

Построим когорты по средней частоте покупок (Количество покупок за интервал / Количество уникальных клиентов). Для этого создадим новый ряд c = cohorts[‘TotalOrders’]/cohorts[‘TotalUsers’]

c = cohorts[‘TotalOrders’]/cohorts[‘TotalUsers’]
opc = c.unstack(0)
opc[[‘2015–02’,’2015–07', ‘2015–09’]].plot(figsize=(20,10))
plt.title(‘Cohorts: OrdersPerUser’)
plt.xticks(np.arange(1, 12.1, 1)) # разбивка оси X
plt.xlim(1, 12) #ось X
plt.ylabel(‘% of Cohort Purchasing’);
#cohortsOPC = cohorts[‘TotalOrders’]/cohorts[‘TotalUsers’]
import seaborn as sns
sns.set(style=’ticks’)
plt.figure(figsize=(24, 16))
plt.title(‘Cohorts: Orders Per User’)
sns.heatmap(opc.T, mask=opc.T.isnull(), annot=True, fmt=’.3');

Для тех, кто решил проскролить, и тех, кто все-таки дочитал до конца

На вход подаем файл содержащий столбцы со значениями:

  • Client_ID — идентификаторы клиентов
  • OrderDTE — содержит для клиентов даты целевых действий(покупка, публикация, твит, шер итп.), по которым будете строить когорту. Обратите внимание на формат.
  • Revenue — показывающее сумму выручки от операции.

Ссылка на iPython блокнот

При подготовке статьи использовались: