Понимание текущего поведения клиентов в отношении различных типов предложений.

Код, используемый для выполнения этого анализа, а также для обучения модели, можно найти на моем GitHub.

Введение

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

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

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

Данные, которые нам предоставляются

Через Udacity Starbucks предоставила три основных набора данных, которые были объединены и очищены, поэтому все готово для анализа и построения модели. Ключевые особенности этого набора данных включают в себя:

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

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

Анализ данных: Анон? Докс? Есть ли различия?

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

  • Группа 1: Клиенты, добровольно предоставившие нам свои личные данные
  • Группа 2: Клиенты, которые не предоставили нам свои личные данные

Эти личные данные включают их индивидуальный возраст, пол и доход.

В дальнейшем клиенты, предоставившие свои личные данные, называются закрытыми клиентами, а те, кто этого не сделал, называются анонимными клиентами.

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

Как видно на графике выше, существенных различий в сделках обеих групп нет. Чтобы быть более конкретным: в предоставленных данных есть 6,89 транзакций на одного клиента и 8,36 транзакций на одного доксированного клиента. Таким образом, транзакционное поведение клиентов, предоставивших личную информацию, несколько снизилось.

Давайте продолжим и посмотрим, как эти две группы реагируют на три разных типа предложений, которые Starbucks разослала своим клиентам (см. рис. 2).

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

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

Если мы пойдем дальше, все станет еще интереснее. Существует дальнейшее снижение примерно на 35% для доксированных клиентов, которые также завершают предложение, независимо от типа предложения. Если мы посмотрим на анонимных клиентов, мы увидим дальнейшее снижение на 85%, когда дело доходит до завершения предложения.

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

Анализ данных: возраст и доход. Как они влияют на реакцию клиентов на предложения?

На данный момент для анализа можно использовать только доксированные данные, так как не все клиенты указали свой возраст и доход (а также пол). Поскольку в наборе данных есть несколько десятков тысяч доксированных точек данных, это не должно быть проблемой. Давайте посмотрим на соответствующие типы предложений, «купи один, получи один» и предложения со скидками, и посмотрим, есть ли различия в отношении возраста и распределения доходов клиентов.

  1. Распределение по возрасту для предложений "купи один, получи другой" (рис. 3):

Распределение выглядит нормально распределенным по всем «стадиям» предложения, однако есть некоторые моменты, которые мы можем сделать. Как видно на диаграмме, а также на последующих, отдел маркетинга Starbucks, кажется, имеет четкие «корзины» для своих клиентов в зависимости от их возраста и дохода, когда дело доходит до нацеливания на них предложений. На этой диаграмме мы видим заметное преимущество где-то в конце 40-летнего возраста, а также в середине 30-х годов, когда клиентам отправляется значительно больше предложений. Чтобы получить некоторое представление и, возможно, выяснить, почему это так, давайте возьмем в качестве примеров возраст 25, 40 и 50 лет и рассмотрим их более внимательно:

  • 25 лет:
    233 человека получили предложение | 185 просмотрели (~ 80%) | 100 завершили его (~ 43%)
  • 40 лет:
    369 получили предложение | 314 просмотрели его (~ 85%) | 196 завершили его (~ 53%)
  • 50 лет:
    494 человека получили предложение | 410 просмотрели его (~ 83%) | 320 завершили его (~ 65%)

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

2. Распределение по возрасту для дисконтных предложений (рис. 4):

Для сравнения давайте также посмотрим на предложение скидки. Здесь «закрома» еще более очевидны. Давайте посмотрим на тот же возраст, что и раньше:

  • 25 лет:
    244 человека получили предложение | 152 просмотрели (~ 62%) | 138 завершили его (~ 57%)
  • 40 лет:
    356 получили предложение | 269 ​​просмотрели его (~ 76%) | 228 завершили его (~ 64%)
  • 50 лет:
    488 человек получили предложение | 359 просмотрели (~ 74%) | 351 завершили его (~ 72%)

Как и раньше, Starbucks, кажется, знает возрастные группы, которые больше всего реагируют на их предложения, и больше ориентируется на них.

3. Доход распределение по предложениям "купи один, получи один" (рис. 5):

Теперь мы попытаемся понять, как Starbucks учитывает различные доходы своих доксированных клиентов, когда дело доходит до рассылки предложений. Прежде всего: четкие границы для разных групп доходов еще более четко отличимы друг от друга. Здесь группы, кажется, от 0 до 50 тысяч в год, от 50 тысяч до где-то около 75 тысяч, от 75 тысяч до 100 тысяч и оттуда вверх. Однако людям с доходом от 50 до 75 тысяч в год отправляется гораздо больше предложений, чем людям с другим доходом. Может быть, потому, что в эту группу попадает больше клиентов, а может быть, потому, что Starbucks по какой-то причине фокусируется на них. Тем не менее, мы можем взглянуть на ответы на предложения. Как и прежде, мы выбираем для сравнения три разных диапазона доходов: 40к — 50к, 60к — 70к и 80к — 90к.

  • Доход 40к — 50к:
    3210 получили предложение | 2569 просмотрели его (~ 80%) |
    1289 завершили его (~40%)
  • Доход 60к — 70к:
    4106 получили предложение | 3537 просмотрели его (~ 86%) |
    2332 прошли его (~ 57%)
  • Доход 80к — 90к:
    2537 получили предложение | 2149 просмотрели его (~ 85%) |
    1868 завершили его (~74%)

В этом случае может быть некоторый потенциал, чтобы немного скорректировать ситуацию, поскольку группа с доходом 80–90 тысяч одинаково хорошо реагирует на предложения «купи один, получи один» по сравнению с людьми с доходом от 60 до 70 тысяч. Однако могут быть причины, по которым людям с более высоким доходом рассылается меньше предложений. Возможно, только потому, что их меньше.

4. Доход распределение по скидкам (рис. 6):

Теперь давайте сравним группы с одним и тем же доходом друг с другом по предложениям скидок, которые им были отправлены.

  • Доход 40к — 50к:
    3336 получили предложение | 1947 просмотрели его (~ 58%) |
    1725 завершили его (~ 52%)
  • Доход 60к — 70к:
    4190 получили предложение | 3047 просмотрели его (~ 73%) |
    2687 завершили его (~ 64%)
  • Доход 80к — 90к:
    2543 получили предложение | 2005 просмотрели его (~ 79%) |
    1913 г. завершил его (~ 75%)

Как и прежде, мы видим, что состоятельные люди, похоже, знают о предложениях и умеют экономить. Ответы на предложения скидок очень похожи на предложения «купи один, получи один».

Построение модели

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

  1. Общая подготовка данных в виде дальнейшей очистки, обработки отсутствующих данных и удаления непригодных/нерелевантных точек данных.
  2. Выбор всех соответствующих функций из набора данных для прогнозирования целевого значения, в данном случае идентификатора предложения.
  3. Для обработки категориальных значений необходимо кодирование данных.
  4. Поскольку мы хотим предсказать категориальное целевое значение, масштабирование признаков используется для дальнейшего повышения производительности модели.
  5. Разделение данных на обучающий и тестовый наборы.
  6. Решите, какую метрику производительности использовать.
  7. Выбор подходящей модели/классификатора.
  8. Обучите модель и предскажите целевые значения, используя обучающий и тестовый наборы.
  9. Оцените производительность модели, используя выбранную метрику производительности.
  10. Дальнейшее повышение производительности выбранной модели путем настройки гиперпараметров.

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

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

Выбор функции

Во-первых: что мы вообще хотим предсказать? Цель состоит в том, чтобы предсказать наиболее подходящее предложение для нового клиента, представленное в наших данных как «offer_id». Всего есть 10 разных, что также означает, что мы хотим предсказать категориальное значение. Позже это повлияет на наш выбор классификаторов.

Теперь мы выбираем те функции, которые нас интересуют и которые, по нашему мнению, помогут модели предсказать целевое значение. Поэтому мы формируем наш фрейм данных, чтобы включить следующие функции:
'время', 'offer_completed', 'offer_received', 'offer_viewed', 'транзакция', 'offer_id', 'всего', 'пол', 'возраст' , 'доход', 'электронная почта', 'мобильный', 'социальный', 'Интернет', 'became_member_in'.

data_model.columns.tolist()

['time', 'offer_completed', 'offer_received', 'offer_viewed', 'transaction',
 'offer_id', 'total', 'gender', 'age', 'income', 'email', 'mobile', 'social',
 'web', 'became_member_in']

Категорические признаки

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

offer_id_mapping = {
    0:0,
    '0b1e1539f2cc45b7b9fa7c272da2e1d7':1,
    '2298d6c36e964ae4a3e7e9706d1fb8c2':2,
    '2906b810c7d4411798c6938adc9daaa5':3,
    '3f207df678b143eea3cee63160fa8bed':4,
    '4d5c57ea9a6940dd891ad53e9dbe8da0':5,
    '5a8bc65990b245e5a138643cd4eb9837':6,
    '9b98b8c7a33c4b65b9aebfe6a799e6d9':7,
    'ae264e3637204a6fb9bb56bc8210ddfd':8,
    'f19421c1d4aa40978ebb69ca19b0e20d':9,
    'fafdcd668e3743c1bb461111dcafc2a4':10,
}

Масштабирование функций

Для прогнозирования категориальных значений также рекомендуется каким-либо образом масштабировать/нормализовать функции для дальнейшего повышения производительности моделей. Здесь мы использовали подход масштабирования Min-Max из модуля sklearn:

cols = X.columns.tolist()

scaler = MinMaxScaler()
X[cols] = scaler.fit_transform(X[cols])

Разделение набора данных

Если данные готовы и полностью подготовлены для моделирования, набор данных разбивается на два набора: обучающий набор и тестовый набор, где 2/3 данных используются для обучения и 1/3 для тестирования модели.

X_train, X_test, y_train, y_test = 
    train_test_split(X, y, test_size=0.33, random_state=42)

print(f'Length of X.train: {X_train.shape[0]} and X.test: {X_test.shape[0]}')
Length of X.train: 182750 and X.test: 90012

Показатель эффективности

Чтобы выяснить, какая модель работает лучше всего, нам нужна метрика производительности, чтобы сделать их сопоставимыми. В программе Data Science довольно часто используется F1-Score, так как он достаточно сбалансирован и в некоторой степени компенсирует несбалансированные наборы данных. Для этой модели мы будем использовать F1-Score в качестве основной метрики производительности. Кроме того, мы рассчитываем показатели точности и полноты для каждой рассматриваемой нами модели.

Модель/классификатор

Поскольку все модели, алгоритмы и классификаторы различаются по своей производительности в зависимости от набора данных, на котором они обучаются, мы тестируем пару из них и вычисляем их производительность для сравнения:

  • Случайный лесной классификатор

Чтобы начать с классификатора случайного леса из модуля sklearn, он обучается с n_estimators, установленным на 100. Ниже отчета о производительности:

print("Precision-score:",precision_score(y_true=y_test, y_pred=y_pred_rfc, average='weighted'))
print("F1-score:",f1_score(y_true=y_test, y_pred=y_pred_rfc, average='weighted'))
print("Recall-score:",recall_score(y_true=y_test, y_pred=y_pred_rfc, average='weighted'))
print("------------------------------------------------------")
print("In depth classification report:\n",classification_report(y_true=y_test, y_pred=y_pred_rfc))
Precision-score: 0.7367081382900627
F1-score: 0.7367724273466818
Recall-score: 0.7368795271741545
------------------------------------------------------
In depth classification report:
               precision    recall  f1-score   support

           0       1.00      1.00      1.00     40963
           1       1.00      1.00      1.00      4082
           2       0.45      0.45      0.45      5856
           3       0.52      0.52      0.52      4686
           4       0.33      0.33      0.33      3380
           5       0.41      0.39      0.40      5434
           6       0.51      0.50      0.51      4177
           7       0.52      0.52      0.52      4798
           8       0.62      0.63      0.62      5379
           9       0.41      0.42      0.42      5404
          10       0.45      0.46      0.46      5853

    accuracy                           0.74     90012
   macro avg       0.57      0.57      0.57     90012
weighted avg       0.74      0.74      0.74     90012
  • Классификатор повышения градиента

Как и классификатор случайного леса, классификатор повышения градиента обучается с n_estimators = 100.

print("Precision-score:",precision_score(y_true=y_test, y_pred=y_pred_gbc, average='weighted'))
print("F1-score:",f1_score(y_true=y_test, y_pred=y_pred_gbc, average='weighted'))
print("Recall-score:",recall_score(y_true=y_test, y_pred=y_pred_gbc, average='weighted'))
print("------------------------------------------------------")
print("In depth classification report:\n",classification_report(y_true=y_test, y_pred=y_pred_gbc))
Precision-score: 0.741202800687273
F1-score: 0.7396625125975725
Recall-score: 0.7418122028174021
------------------------------------------------------
In depth classification report:
               precision    recall  f1-score   support

           0       1.00      1.00      1.00     40963
           1       1.00      1.00      1.00      4082
           2       0.45      0.47      0.46      5856
           3       0.53      0.52      0.53      4686
           4       0.35      0.35      0.35      3380
           5       0.45      0.39      0.42      5434
           6       0.52      0.36      0.43      4177
           7       0.54      0.55      0.54      4798
           8       0.60      0.73      0.66      5379
           9       0.47      0.37      0.41      5404
          10       0.43      0.53      0.47      5853

    accuracy                           0.74     90012
   macro avg       0.57      0.57      0.57     90012
weighted avg       0.74      0.74      0.74     90012
  • Логистическая регрессия

И последнее, но не менее важное: классификатор логистической регрессии:

print("Precision-score:",precision_score(y_true=y_test, y_pred=y_pred_lrc, average='weighted'))
print("F1-score:",f1_score(y_true=y_test, y_pred=y_pred_lrc, average='weighted'))
print("Recall-score:",recall_score(y_true=y_test, y_pred=y_pred_lrc, average='weighted'))
print("------------------------------------------------------")
print("In depth classification report:\n",classification_report(y_true=y_test, y_pred=y_pred_lrc))
Precision-score: 0.6778460676206762
F1-score: 0.6747262443057118
Recall-score: 0.6804981558014487
------------------------------------------------------
In depth classification report:
               precision    recall  f1-score   support

           0       1.00      1.00      1.00     40963
           1       1.00      1.00      1.00      4082
           2       0.27      0.35      0.30      5856
           3       0.33      0.29      0.31      4686
           4       0.32      0.28      0.30      3380
           5       0.26      0.21      0.23      5434
           6       0.51      0.41      0.46      4177
           7       0.42      0.51      0.46      4798
           8       0.60      0.69      0.64      5379
           9       0.26      0.09      0.13      5404
          10       0.27      0.40      0.32      5853

    accuracy                           0.68     90012
   macro avg       0.48      0.48      0.47     90012
weighted avg       0.68      0.68      0.67     90012

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

Настройка гиперпараметров

Производительность модели может быть дополнительно увеличена путем настройки гиперпараметров. Чтобы добиться этого, мы используем подход поиска по сетке, чтобы найти оптимальные параметры для классификатора случайного леса, чтобы он работал наилучшим образом. На данный момент настраиваются только два параметра («n_estimators» и «max_depth»), учитывая тот факт, что во время настройки модели были доступны только ограниченные вычислительные ресурсы.

params = {
    'n_estimators': [50, 100, 150],
    'max_depth': [None, 10, 20, 30],
}

grid_search = GridSearchCV(estimator=rfc, param_grid=params, cv=5, scoring='f1_macro')

grid_search.fit(X_train, y_train)

rfc_tuned = grid_search.best_estimator_

y_pred_rfc_tuned = rfc_tuned.predict(X_test)

print("Precision-score:",precision_score(y_true=y_test, y_pred=y_pred_rfc_tuned, average='weighted'))
print("F1-score:",f1_score(y_true=y_test, y_pred=y_pred_rfc_tuned, average='weighted'))
print("Recall-score:",recall_score(y_true=y_test, y_pred=y_pred_rfc_tuned, average='weighted'))
print("------------------------------------------------------")
print("In depth classification report:\n",classification_report(y_true=y_test, y_pred=y_pred_rfc_tuned))
Precision-score: 0.7379764376951514
F1-score: 0.7379395546275304
Recall-score: 0.7381460249744478
------------------------------------------------------
In depth classification report:
               precision    recall  f1-score   support

           0       1.00      1.00      1.00     40963
           1       1.00      1.00      1.00      4082
           2       0.45      0.45      0.45      5856
           3       0.53      0.52      0.52      4686
           4       0.34      0.35      0.34      3380
           5       0.42      0.38      0.40      5434
           6       0.51      0.49      0.50      4177
           7       0.53      0.53      0.53      4798
           8       0.62      0.64      0.63      5379
           9       0.41      0.43      0.42      5404
          10       0.45      0.47      0.46      5853

    accuracy                           0.74     90012
   macro avg       0.57      0.57      0.57     90012
weighted avg       0.74      0.74      0.74     90012

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

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

feature_importances = rfc_tuned.feature_importances_ *100

print("Feature Importance:")

for name, importance in zip(X_train.columns, feature_importances):
    print(f"{name}: {importance:.4f}")
Feature Importance:
time: 3.5651
offer_completed: 1.2726
offer_received: 1.5736
offer_viewed: 1.2490
transaction: 10.3583
total: 11.3432
gender: 0.4937
age: 5.9089
income: 5.7877
email: 15.8875
mobile: 15.1400
social: 12.4466
web: 13.6440
became_member_in: 1.3299

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

Заключение и дальнейшие шаги

Заключение

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

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

Дальнейшие шаги

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