Разработка
Как предсказать LTV клиента с помощью машинного обучения и Python
Мы смогли применить модель машинного обучения к инженерным данным, чтобы предсказать, сколько клиент потратит за определенный период времени.
Введение
У вас много клиентов, но вы не знаете, что с ними делать?
Понимание того, как ведут себя ваши клиенты или пользователи, необходимо, чтобы максимально использовать их внимание. Сегодня мы можем использовать доступные нам объемы данных, чтобы предсказать, какую ценность каждый клиент принесет компании в определенный период времени.
Способность предсказывать, сколько клиент будет стоить в будущем, может быть полезна в следующих случаях:
- Предоставляет управляемую данными метрику на уровне каждого клиента, помогающую принимать упреждающие решения, которые повлияют на бизнес.
- Обогащает базу данных клиентов, добавляя еще одно измерение, по которому можно выполнять сегментацию клиентов.
- Можно сделать еще один шаг, чтобы предсказать, сколько клиент потратит на разные категории продуктов.
- Наличие у бизнеса большей информации о предпочтениях клиентов позволяет лучше удовлетворять их с технической точки зрения.
В этом руководстве, используя набор данных Kaggle, мы сделаем следующее:
- Соберем и объединим наши данные.
- Преобразуем наш набор данных в богатые функции, используя метод, известный как рекурсивный RFM (значение Recency, Frequency, Monetary).
- Сделаем модель, которая сможет предсказывать ценность пользователей на основе этих данных.
Блокнот, который я использовал для урока, можно найти здесь.
Шаг 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 (Recency Frequency Monetary) — это метод осмысленной количественной оценки клиентов, который может служить хорошей отправной точкой, когда дело доходит до выполнения любой аналитики данных о транзакциях, специфичных для клиентов.
Давность, частота и денежная стоимость фиксируют, когда клиент совершил свою последнюю транзакцию, как часто он возвращался и какова была средняя продажа для каждого клиента. Мы можем добавить к этому любые другие доступные функции (такие как GrossMargin, Age, CostToRetain) или другие прогнозируемые функции (риск оттока или анализ настроения). Ниже мы рассмотрим, как рассчитать каждый из основных параметров.
Мы можем разделить существующие данные обучения на наблюдаемый период и будущий период. Если мы хотим предсказать, сколько покупатель потратит через год, мы установим продолжительность будущего периода равным одному году, а остальную часть времени будем рассматривать как наблюдаемую (как показано ниже).
Это позволяет нам уточнить модель для прогноза суммы, потраченной в будущем периоде, с использованием функций, рассчитанных в наблюдаемый период.
# 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'))]
Здесь мы вводим понятие отсечки (cut-off). Это просто то место, где заканчивается наблюдаемый период, и определяет, до какой даты мы должны вычислить наши функции.
- Давность: время с момента последней транзакции (часы/дни/недели). Нам нужно установить отсечку, чтобы рассчитать Давность. Например: через сколько дней с момента отсечки они совершили транзакцию?
# 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'})
- Возраст: время с момента первой транзакции. Для этой функции мы просто найдем количество дней с момента первой транзакции каждого клиента. Опять же, нам понадобится отсечка, чтобы рассчитать время между отсечкой и первой транзакцией.
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 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)
В идеале это может дать информацию об удержании клиентов в течение определенного периода времени. Это может выглядеть примерно так:
Для меток мы просто будем суммировать сумму, потраченную каждым клиентом в будущем периоде.
labels = future.groupby(id_col)[value_col].sum()
В некоторых случаях, выполняя это один раз для всего набора данных и подбирая модель для прогнозирования меток, можно получить приемлемую точность. Однако, если присмотреться, то можно спросить: а вдруг в наблюдаемый период произошло что-то интересное? И это правильный вопрос. Простое выполнение этого один раз для набора данных игнорирует всю сезонность в данных и рассматривает только один конкретный период для меток. Тут мы введем то, что я называю рекурсивным RFM.
Рекурсивный RFM
Проблема прогнозирования жизненной ценности клиентов в определенный период времени может быть сведена к простому прогнозированию того, сколько они потратят в будущем периоде. Давайте применим то, что мы узнали о RFM до сих пор, и прокрутим его через набор данных.
Допустим, данные начинаются слева в начале года. Мы выберем частоту (например, один месяц) и пройдемся по набору данных, вычислив наши функции на основе наблюдаемых (o) и сгенерировав наши метки для будущего (f). Идея состоит в том, чтобы рекурсивно вычислить эти функции, чтобы модель могла узнать, как поведение клиентов меняется с течением времени.
Для этой части алгоритма мы сначала получим дату каждого интервала в диапазоне набора данных и используем каждую из этих дат в качестве точки отсечки для вычисления наших функций и меток RFM. Напомним, что в нашем примере мы выбрали периодичность 1 месяц.
Для каждой даты отсечки (co):
- Вычислим функции RFM из всех строк (i) до отсечения ( i → co )
- Вычислим метки из строк (i) между отсечкой и через месяц после отсечки (co → i → co + частота)
- Сделаем внешнее объединение функций и меток на основе идентификатора клиента для создания набора данных для заполнения клиентов, которые не совершали никаких транзакций.
Объединим все наборы данных в цикле.
Это показано в коде ниже:
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 ) # Compute labels from future labels = future.groupby(id_col)[value_col].sum() # 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)
Шаг 3: Модель
Когда дело доходит до науки о данных, машинное обучение (и все, что с ним связано) — это просто метод, используемый для оценки взаимосвязи между переменными. Поиск правильной модели для ваших данных — это еще один путь к получению наилучших возможных результатов для вашего варианта использования. Истинная ценность науки о данных заключается в использовании этих методов для принятия обоснованных решений в реальном мире.
В этом примере мы попробуем Random Forest Regressor, так как он очень прост в своей реализации, и поэтому его очень легко сразу использовать.
from sklearn.ensemble import RandomForestRegressor # Initialize and fit model on train dataset rf = RandomForestRegressor().fit(X_train, y_train)
После получения модели мы можем просмотреть наши прогнозы на тестовом наборе в кадре данных.
from sklearn.metrics import mean_squared_error # 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) # Compute error train_rmse = mean_squared_error(predictions.true, predictions.preds)**0.5 test_rmse = mean_squared_error(predictions_test.true, predictions_test.preds)**0.5 print(f"Train RMSE: {train_rmse}, Test RMSE: {test_rmse}")
Вывод:
Train RMSE: 10.608368028113563, Test RMSE: 28.366171873961612
Поскольку у нас есть регрессивная задача, давайте использовать среднеквадратичную ошибку (RMSE) в качестве нашей метрики ошибки, которая будет вычисляться для прогнозов как для данных обучения, так и для данных тестирования отдельно. Это рассчитывается по следующему уравнению:
С тестовым среднеквадратичным отклонением ~ 28,4 наши прогнозы отклоняются примерно на 28.40 доллара на еще невидимых данных. Кроме того, RMSE нашего тренировочного набора данных значительно ниже, чем RMSE нашего теста, что указывает на переобучение. Другими словами, модель слишком зависит от обучающих данных, чтобы делать свои прогнозы, и затрудняется предсказать данные, которые она раньше не видела. Для человека это аналогично «закрытости», ему нужно научиться находить связи, которые можно обобщить на невидимые данные. Анализируя наши результаты, мы видим, что есть возможности для улучшения.
Именно на этом этапе мы можем выполнить некоторую настройку гиперпараметров или попробовать разные модели, чтобы увидеть, что лучше всего подходит для наших данных. Существуют такие инструменты, как H2O AutoML, которые позволяют вам попробовать 10 или даже 100 моделей для одной задачи.
Вывод
Тем не менее, мы определили метод значимой количественной оценки клиентов, который может быть полезен и в других задачах, включая сегментацию клиентов или риска оттока. Мы смогли применить модель машинного обучения к инженерным данным, чтобы предсказать, сколько клиент потратит за определенный период времени. Это всего лишь один из многих способов использования данных о клиентах для повышения вовлеченности и удержания клиентов, особенно если они используются вместе с задачами, упомянутыми выше.