Углубленный учебник с использованием Python, pandas и scikit-learn, анализа RFM и SMOTE.

Введение

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

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

Риск оттока клиентов – это вероятность того, что клиент откажется от сотрудничества с компанией. Следовательно, мы можем определить его как:

Churn Risk = 1 - Probability of purchase over a determined period

Понимание того, как ведут себя ваши клиенты, необходимо, чтобы максимально использовать их покровительство. Сегодня мы можем использовать объемы доступных нам данных, чтобы предсказать, насколько вероятно, что клиент продолжит взаимодействие с вашим бизнесом. Это может быть ценно в следующих отношениях:

  • Предоставляет управляемую данными метрику на уровне клиента, помогающую принимать упреждающие решения, которые повлияют на бизнес.
  • Обогащает базу данных клиентов, добавляя еще один параметр, по которому выполняется сегментация клиентов.
  • Можно сделать еще один шаг, чтобы точно предсказать, когда клиент вернется.
  • Понимание ваших входящих и исходящих клиентов дает представление об уровне удержания клиентов, а также об общем состоянии вашей клиентской базы.

В этом руководстве, используя набор данных Kaggle, найденный здесь, мы предпримем следующие шаги:

  • Соберите и объедините наши данные.
  • Преобразуйте наш набор данных в расширенные функции и метки, используя метод, известный как рекурсивный RFM (Recency-Frequency-Monetary Value).
  • Соответствуйте модели, которая может предсказать на этих данных.

Во многом этот подход похож на тот, который мы использовали для Пожизненной ценности клиента, за исключением того, что мы генерируем метки по-другому. Блокнот, который я использовал для этого урока, можно найти здесь.

Шаг 1: Сбор данных

Для наших данных о клиентах нам, по сути, нужны только 3 столбца: идентификатор клиента, дата/время транзакции и стоимость транзакции. Мы также можем добавить другие функции, но вы должны обязательно агрегировать их по клиентам на этапе разработки функций. Мы можем использовать дату для извлечения дня недели, месяца, часа и всех временных характеристик, связанных с каждой транзакцией. Если есть разные категории транзакций, эти столбцы также могут быть введены.

import pandas as pd

# Load transaction data from CSV
df = pd.read_csv(data_path) # path to your data

# Convert Date column to date-time object
df.Date = pd.to_datetime(df.Date)
df.head(10)

Вывод:

Шаг 2: Разработка функций

Новизна, частота и денежная стоимость (RFM)

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

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

Это работает следующим образом: мы можем разделить данные обучения на наблюдаемый период и будущийпериод. Если мы хотим предсказать, сколько клиент потратит через год, мы установим продолжительность периода будущее равным одному году, а остальные подпадем под наблюдается (как показано ниже).

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

# Data before cut off
observed = df[df[date_col] < cut_off

# Data after cut off
future = df[(df[date_col] > cut_off) & (df[date_col] < cut_off + pd.Timedelta(label_period_days, unit='D'))]

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

  • Давность: время с момента последней транзакции (часы/дни/недели). Нам нужно установить отсечку, чтобы рассчитать давность. Например: сколько дней с момента отсечки они совершили транзакцию?
# Copy transactions
cut_off = df.Date.max()
recency = df[df.Date < cut_off].copy()

# Group customers by latest transaction
recency = recency.groupby(customer_id_column)[date_column].max()
recency = (max_date - recency).dt.days).reset_index().rename(
columns={date_column:'recency'})
  • Частота. Количество различных периодов времени, в течение которых клиент совершал транзакцию. Это позволит нам отслеживать, сколько транзакций совершил клиент и когда они произошли. Мы также можем сохранить практику расчета этих метрик по предельной дате, так как это будет удобно позже.
# Copy transactions
cut_off = df.Date.max()
frequency = df[df.Date < cut_off].copy()

# Set date column as index
frequency.set_index(date_column, inplace=True)
frequency.index = pd.DatetimeIndex(frequency.index)

# Group transactions by customer key and by distinct period
# and count transactions in each period
frequency = frequency.groupby([customer_id_column, pd.Grouper(freq="M", level=date_column)]).count()
# (Optional) Only count the number of distinct periods a transaction # occurred. Else, we will be calculating total transactions in each # period instead.

frequency[value_column] = 1 # Store all distinct transactions

# Sum transactions
frequency = frequency.groupby(customer_id_column).sum().reset_index().rename(
columns={value_column : 'frequency'})
  • Денежная ценность: средняя сумма продаж. Здесь мы просто вычисляем среднюю сумму продаж по всем транзакциям для каждого клиента. Мы можем дополнительно добавить функцию «TotalAmountSpent», взяв сумму вместо среднего на последнем шаге.
# Copy transactions
cut_off = df.Date.max()
value = df[df.Date < cut_off].copy()

# Set date column as index
value.set_index(date_column, inplace=True)
value.index = pd.DatetimeIndex(value.index)

# Get mean or total sales amount for each customer
value = value.groupby(customer_id_column[value_column].mean().reset_index().rename(columns={value_column : 'value'})
  • Возраст: время с момента первой транзакции. Для этой функции мы просто найдем количество дней с момента первой транзакции каждого клиента. Опять же, нам понадобится отсечка, чтобы рассчитать время между отсечкой и первой транзакцией.
# Copy transactions
cut_off = df.Date.max()
age = df[df.Date < cut_off].copy()

# Get date of first transaction
first_purchase = age.groupby(customer_id_column)[date_column].min().reset_index()

# Get number of days between cut off and first transaction
first_purchase['age'] = (cut_off - first_purchase[date_column]).dt.days

Мы можем обернуть все эти функции вместе со следующей функцией:

def customer_rfm(data, cut_off, date_column, customer_id_column, value_column, freq='M'):
  cut_off = pd.to_datetime(cut_off)
  
   # Compute Recency
  recency = customer_recency(data, cut_off, date_column, customer_id_column)
  
  # Compute Frequency
  frequency = customer_frequency(data, cut_off, date_column, customer_id_column, value_column, freq=freq)
  
  # Compute average value
  monetary_value = customer_value(data, cut_off, date_column, customer_id_column, value_column)
  
  # Compute age
  age = customer_age(data, cut_off, date_column, customer_id_column)
  
  # Merge all columns
  return recency.merge(frequency, on=customer_id_column).merge(on=customer_id_column).merge(age, on=customer_id_column).merge(monetary_value, on=customer_id_column)

В идеале это может собирать информацию об удержании клиентов в течение определенного периода времени. Это может выглядеть примерно так:

Для ярлыков мы бы просто установили 1 для тех, кто купил что-то в будущем периоде, и 0 для всех, кто этого не сделал.

def generate_churn_labels(future):
   future['DidBuy'] = 1
   return future[['Customer_ID', 'DidBuy']]

В некоторых случаях, выполняя это один раз для всего набора данных и подбирая модель для прогнозирования меток, можно получить приемлемую точность. Однако, если вы присмотритесь, вы можете спросить: а что, если в наблюдаемый период произошло что-то интересное? Какой правильный вопрос задать. Простое выполнение этого один раз для набора данных игнорирует всю сезонность в данных и рассматривает только один конкретный период метки. Здесь мы представляем то, что я называю рекурсивным RFM.

Рекурсивный RFM

Давайте применим то, что мы знаем о RFM до сих пор, и прокрутим его через набор данных.

Допустим, данные начинаются слева в начале года. Мы выберем периодичность (например, один месяц) и пройдемся по набору данных, вычислив наши функции на основе наблюдаемых (o) и сгенерировав наши метки на основе будущее (f). Идея состоит в том, чтобы рекурсивно вычислить эти функции, чтобы модель узнала, как поведение клиентов меняется с течением времени.

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

Для каждой даты прекращения (co):

  • Вычислить функции RFM из всех строк (i) до отсечения ( ico)
  • Вычислить метки в строках (i) между отсечкой и через месяц после отсечки (coico strong> + частота)
  • Внешнее объединение функций и меток на основе идентификатора клиента для создания набора данных для заполнения клиентов, которые не совершали никаких транзакций.

Объедините все наборы данных в цикле.

Это реализовано в коде ниже:

def recursive_rfm(data, date_col, id_col, value_col, freq='M', start_length=30, label_period_days=30):
  # Resultant list of datasets
  dset_list = []
  # Get start and end dates of dataset
  start_date = data[date_col].min() + pd.Timedelta(start_length, unit="D")
  end_date = data[date_col].max() - pd.Timedelta(label_period_days, unit="D")
  # Get dates at desired interval
  dates = pd.date_range(
  start=start_date, end=end_date, freq=freq
  data[date_col] = pd.to_datetime(data[date_col]
  )
  for cut_off in dates:
     # split by observed / future
     observed = data[data[date_col] < cut_off
     future = data[
                    (data[date_col] > cut_off) &
                    (data[date_col] < cut_off + pd.Timedelta(
                     label_period_days,  unit='D'))
                  ]
     # Get relevant columns
     rfm_columns = [date_col, id_col, value_col]
     _observed = observed[rfm_columns]
     # Compute features from observed
     rfm_features = customer_rfm(
          _observed, cut_off, date_col, id_col, value_col
     )
     # Set label for everyone who bought in 'future' as 1'
     labels = generate_churn_labels(future)
     # Outer join features with labels to ensure customers 
     # not in observed are still recorded with a label of 0
     dset = rfm_features.merge(
          labels, on=id_col, how='outer'
     ).fillna(0)
     dset_list.append(dset)
  # Concatenate all datasets
  full_dataset = pd.concat(dset_list, axis=0)
  res = full_dataset[full_dataset.recency != 0].dropna(axis=1, how='any')
  return res

rec_df = recursive_rfm(data_for_rfm, 'Date', 'Customer_ID', 'Sales_Amount')

Теперь, когда мы сгенерировали наш набор данных, все, что нам нужно сделать, это перетасовать и выполнить разделение обучения/тестирования на наших данных. Мы будем использовать 80% для обучения и 20% для тестирования.

from sklearn.model_selection import train_test_split

rec_df = rec_df.sample(frac=1) # Shuffle

# Set X and y
X = rec_df[['recency', 'frequency', 'value', 'age']]
y = rec_df[['Sales_Amount']].values.reshape(-1)

# Set test ratio and perform train / test split
test_size = 0.2
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=test_size, random_state=42, shuffle=True)

Примечание: дисбаланс классов

В задаче классификации иногда классы, которые мы хотим предсказать, несбалансированы в наборе данных. Например, если есть 10 наблюдений и два класса; 2 из них могут относиться к классу_0, а остальные 8 — к классу_1. Это может привести к смещению модели, поскольку она учитывает значительно больше одного класса, чем другого. Мы определяем класс меньшинства как класс с меньшим количеством наблюдений, а класс большинства — как класс с большим количеством наблюдений. В нашем уроке это будет выглядеть примерно так:

Чтобы исправить это, можно либо занизить-выборку большинства класса, либо завысить-выборку меньшинства класса. . Выборка — это практика отбора подмножества данных для выполнения некоторой операции. Кроме того, недостаточная/избыточная выборка — это когда мы либо дублируем наблюдения (избыточные), либо удаляем наблюдения (недостаточные), относящиеся к соответствующему классу. Определенно стоит поэкспериментировать, какой вариант лучше всего подойдет для поставленной задачи и данных, с которыми вы работаете.

SMOTE (Synthetic Minority Oversampling Technique) — это инструмент, который мы можем использовать для этого.

from imblearn.over_sampling import SMOTE

oversample = SMOTE()
X_train_over, y_train_over = oversample.fit_resample(X_train, y_train)

pd.Series(y_train_over).value_counts()

Вывод:

Шаг 3: Модель

Когда дело доходит до науки о данных, машинное обучение (и все, что с ним связано) — это просто метод, используемый для оценки взаимосвязи между переменными. Поиск правильной модели для ваших данных — это еще один путь к получению наилучших возможных результатов для вашего варианта использования. Истинная ценность науки о данных заключается в использовании этих методов для принятия обоснованных решений в реальном мире.

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

from sklearn.ensemble import RandomForestRegressor

# Initialize and fit model on train dataset
rf = RandomForestClassifier().fit(X_train, y_train)

# Fit on over-sampled data as well
rf_over = RandomForestClassifier().fit(X_train_over, y_train_over)

После подгонки мы можем просмотреть наши прогнозы на тестовом наборе в кадре данных.

from sklearn.metrics import accuracy_score

# Create Dataframe and populate with predictions and actuals
# Train set
predictions = pd.DataFrame()
predictions['true'] = y_train
predictions['preds'] = rf.predict(X_train)

# Test set
predictions_test = pd.DataFrame()
predictions_test['true'] = y_test
predictions_test['preds'] = rf.predict(X_test)
predictions_test['preds_over'] = rf_over.predict(X_test)

# Compute error
train_acc = accuracy_score(predictions.true, predictions.preds)
test_acc = accuracy_score(
predictions_test.true, predictions_test.preds)
test_acc_over = accuracy_score(
predictions_test.true, predictions_test.preds_over)
print(f"Train Acc: {train_acc:.4f}, Test Acc: {test_acc:.4f}, Test Acc Oversampled: {test_acc_over:.4f}")

Выход:

Train Acc: 0.9863, Test Acc: 0.8772, Test Acc Oversampled: 0.8671

Полученные результаты

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

Мы видим, что в этом случае использование SMOTE не принесло пользы, однако в вашем случае это может быть не так, исходя из ваших данных.

Теперь, когда наша модель обучена, мы можем использовать функцию predict_proba(), чтобы получить вероятности, связанные с каждым прогнозом. Вот график прогнозируемого распределения вероятностей. Помните, что вероятность, предсказываемая моделью, — это вероятность того, что клиент будет взаимодействовать с бизнесом, и мы ищем вероятность того, что он не будет, поэтому мы можем просто вычесть каждую вероятность из 1.

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

Заключение

Методы проектирования функций, такие как рекурсивный RFM, позволяют использовать богатые функции для описания клиентов. Как видно здесь, эти функции могут быть полезны для анализа их поведения и прогнозирования того, что они могут делать в будущем. Мы также рассмотрели, как при необходимости справиться с дисбалансом классов с помощью SMOTE. Риск оттока — лишь одна из таких предсказуемых метрик. Другие включают пожизненную ценность клиента и сегментацию клиентов. Что особенного в риске оттока, так это то, что он сделал шаг вперед, чтобы определить вероятность того, что клиенты сделают что-то более конкретное, например, купят определенную категорию продукта, или вероятность взаимодействия в каждый день недели. Потенциал клиентской аналитики далеко идущий и всегда проницательный, особенно для бизнеса.

Обязательно ознакомьтесь с другими моими статьями, чтобы узнать больше о науке о данных и машинном обучении для бизнеса.

Если вам понравилась эта статья, подпишитесь на меня, чтобы узнать больше о клиентской аналитике!