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

Набор данных содержит смоделированные данные, которые имитируют поведение клиентов в мобильном приложении Starbucks Rewards. Раз в несколько дней Starbucks рассылает предложение пользователям мобильного приложения. Предложение может быть просто рекламой напитка или реальным предложением, таким как скидка или BOGO (купите один, получите второй бесплатно). Некоторые пользователи могут не получать никаких предложений в течение определенных недель.

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

Чтобы проверить код, вы можете просмотреть Jupyter Notebook в моем профиле Github.

Дата файлы

Ниже приведены файлы данных, предоставленные

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

Подход

  1. Первый шаг — прочитать файлы данных с помощью pandas и изучить содержащиеся в них данные.
  2. Очистите и внимательно изучите данные, визуализируйте их, а затем выполните проектирование функций.
  3. Третий шаг — объединить все три набора данных в один, а также пометить предложения как удачные или неудачные. Предложение считается успешным, если клиент получил, просмотрел и совершил покупку в течение периода действия предложения.
  4. Четвертый шаг — построить модель или модели, оценить производительность, выполнить настройку параметров и разработать лучшую модель.

Давайте познакомимся с данными, которые у нас есть ..

Давайте прочитаем файлы данных с помощью pandas.

portfolio = pd.read_json('data/portfolio.json', orient='records', lines=True)
profile = pd.read_json('data/profile.json', orient='records', lines=True)
transcript = pd.read_json('data/transcript.json', orient='records', lines=True)

Фрейм данных Portfolio содержит следующую информацию.

portfolio.head()

Есть два столбца, которым может потребоваться одно горячее кодирование; каналы и offer_type. Мы позаботимся об этом позже. Теперь давайте посмотрим на набор данных профиля.

profile.head()

Становление_member_on — это одно из полей, которое мы изменим. Мы также заметили несколько записей с возрастом 118 лет и отсутствием информации о доходах и поле. Нам нужно поближе взглянуть на них и очистить набор данных. Окончательный набор данных — это стенограммы. Итак, давайте посмотрим на это.

transcripts.head()

Поле значения имеет offer_id, и для фактических транзакций вы увидите сумму, потраченную клиентом.

Давайте исследовать и сделать его красивее

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

Давайте переименуем столбец идентификатора в «offer_id».

portfolio.rename(columns={"id": "offer_id"}, inplace=True)

В столбце каналов были категориальные значения, и я использовал MultiLabelBinarizer () из scikit learn» для одного горячего кодирования этих значений.

binarizer_obj = MultiLabelBinarizer()
binarizer_obj.fit(portfolio['channels'])
offer_channels_df = pd.DataFrame(binarizer_obj.transform(portfolio['channels']),columns=binarizer_obj.classes_)

Следующим шагом было одно горячее кодирование столбца offer_type, и я использовал метод get_dummies от pandas.

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

  1. Проверьте строки с возрастом 118 и отсутствующими значениями дохода и пола.
  2. Переименуйте столбец id в customer_id
  3. Удалите все строки с нулевыми значениями
age_outlier_df = profile[profile.age == 118]
age_outlier_df.shape

Мы видим, что есть 2175 записей. И во всех этих записях не было информации о доходах или поле. Я предполагаю, что дата рождения не является обязательным полем в приложении. А всем, кто не вводит дату рождения, приложение назначает возраст по умолчанию. Мы собираемся удалить эти поля из набора данных.

После удаления записей с возрастом 118 лет мы получили гораздо более чистый набор данных. Мы также пошли дальше и переименовали столбец id в customer_id. Так становится намного понятнее. Нет строк со значениями nan.

Теперь давайте взглянем на гендерные столбцы.

profile.groupby('gender')['age'].count().plot(x='gender',y='count',kind="bar",title="Gender Distribution");

Вы увидите, что есть несколько записей с указанием пола как «O». Эти записи составляют всего 1,4 % от общего числа записей. Поэтому я собираюсь удалить эти записи из набора данных.

Давайте исследуем распределение доходов клиентов женского и мужского пола.

current_palette = sns.color_palette()
sns.set(font_scale=1.5)
sns.set_style('white')
fig, ax = plt.subplots(figsize=(15, 6),
                       nrows=1,
                       ncols=2,
                       sharex=True,
                       sharey=True)
plt.sca(ax[0])
sns.distplot(male_customers['income'] * 1E-3,
             color=current_palette[1])
plt.xlabel('Income [000]')
plt.ylabel('P(Income)')
plt.title('Male Customer Income')
plt.sca(ax[1])
sns.distplot(female_customers['income'] * 1E-3,
             color=current_palette[0])
plt.xlabel('Income [000]')
plt.ylabel('P(Income)')
plt.title('Female Customer Income')
plt.tight_layout()

Из вышеприведенных диаграмм видно, что минимальный и максимальный доход клиентов-мужчин и женщин примерно одинаков. Тем не менее, доход клиентов-мужчин несколько смещен в сторону более низких значений по сравнению с доходами клиентов-женщин, когда речь идет о возрастных диапазонах от 80 до 120 лет.

Я также изменил следующее:

  1. became_member_on в формате даты
  2. Получим год из даты. Добавлен новый столбец «membershipstart».
  3. Один горячий закодированный членский старт
  4. Создал возрастной диапазон из столбца возраст
  5. Один горячий закодировал значения возрастного диапазона
  6. Один горячий кодировал столбец пола

После всех изменений у нас есть приведенный ниже список столбцов в наборе данных профиля. Чтобы проверить код, вы можете просмотреть Jupyter Notebook в моем профиле Github.

Для набора данных транскриптов я переименовал столбец человека в «customer_id». Я также очистил данные в столбце значений. Если это была транзакция, в столбце значения была сумма транзакции, а если это была запись предложения, у нее было offer_id значение. Затем я создал 2 набора данных из данных транскриптов. Один с информацией о предложении, а другой с информацией о транзакциях.

В конце всех этих действий у нас есть 4 разных набора данных:

  1. портфолио
  2. профиль
  3. offer_data
  4. транзакции

Процесс объединения

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

  • Получить по предложениям, полученным и просмотренным
  • Рассчитайте дату начала и дату окончания предложения (для этого пригодились поля продолжительности и времени)
  • Получить транзакции, завершенные между датой начала и датой окончания предложения

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

Добавлен новый столбец «offer_successful». Если предложение было успешным, столбец помечался как 1, если нет — как 0.

После создания комбинированного набора данных я выполнил еще несколько визуализаций.

  • Продолжительность и успех

  • Сложно или успешно

  • Награда или успех

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

  • offer_id
  • Пользовательский ИД
  • возраст
  • Пол
  • стал_членом_на

Модели

Прежде чем мы сможем обучить модель, мы должны разделить данные на обучающие и тестовые данные.

X = dataset.copy().drop(columns=['offer_successful'])
y = dataset['offer_successful']
(X_train,
 X_test,
 y_train,
 y_test) = train_test_split(X,
                            y,
                            test_size=0.2,
                            random_state=random_state)

пробовал 3 разные модели. Давайте посмотрим на каждый из них.

SVM

Первой моделью, которую я попробовал, были машины опорных векторов. Вот отличная статья о SVM. Подробнее о модели Support Vector Machine можно прочитать здесь.

svc_model = SVC(random_state=random_state)
svc_model.fit(X_train, y_train)
y_pred = svc_model.predict(X_test)
cm = confusion_matrix( y_test, y_pred)
sns.heatmap(cm, annot=True);

Я использовал матрицу путаницы, чтобы визуализировать прогноз.

Даже для такого небольшого набора данных модель SVM требовала много времени для обучения модели. Мы можем попробовать настроить переменные C и гамма, а также попытаться точно настроить модель. Чем выше значения C и гаммы, тем больше времени потребуется для обучения модели.

Логистическая регрессия

Следующей испробованной моделью была модель логистической регрессии. Это одна из самых быстрых моделей для обучения. Я использовал значения по умолчанию для параметров и использовал решатель как «liblinear», поскольку это довольно небольшой набор данных.

Подробнее о логистической регрессии можно прочитать здесь.

lr_model = LogisticRegression(random_state=random_state,
                            solver='liblinear')
lr_model.fit(X_train, y_train)
y_lr_pred = lr_model.predict(X_test)
cm = confusion_matrix( y_test, y_lr_pred)
sns.heatmap(cm, annot=True);

И вот результаты

Классификатор случайного леса

В первый раз, когда я запускал случайный классификатор прогнозов с параметрами по умолчанию, результаты были ужасными. Точность составила всего 69%. Я использовал Поиск по сетке, чтобы найти лучшие значения параметров.

Подробнее о классификаторе Random Forest можно прочитать здесь.

rf_clf = RandomForestClassifier(random_state=random_state)
# Number of trees in random forest
n_estimators = [100, 200, 300]
# Maximum number of levels in tree
max_depth = [int(x) for x in range(5, 11)]
# Minimum number of samples required to split a node
min_samples_split = [2, 4, 6]
# Minimum number of samples required at each leaf node
min_samples_leaf = [ 2, 4]
# Create the random grid
random_grid = {'n_estimators': n_estimators,
               'max_depth': max_depth,
               'min_samples_split': min_samples_split,
               'min_samples_leaf': min_samples_leaf}
randf = GridSearchCV(estimator = rf_clf,
                               param_grid  = random_grid,
                               cv = 3,
                               verbose=2,
                               n_jobs = 3)
randf.fit(X_train, y_train)
#predict
y_rf_opt_pred = randf.best_estimator_.predict(X_test)
rf_cm1 = confusion_matrix( y_test, y_rf_opt_pred)
sns.heatmap(rf_cm1, annot=True);

Вот результаты:

После использования поиска по сетке и переобучения модели я смог получить точность и оценку f1 74%.

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

Результаты

Ранжирование моделей на основе точности обучающих данных

  • Точность модели RandomForestClassifier: 0,738
  • Точность модели логистической регрессии: 0,725
  • Точность модели SVM: 0,722

Ранжирование моделей на основе тренировочных данных F1-score

  • Модель RandomForestClassifier f1-оценка: 0,74
  • Модель логистической регрессии f1-оценка: 0,73
  • Модель SVM f1-оценка: 0,72

Результаты показывают, что модель случайного леса работает лучше.

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

Ниже приведены основные функции, используемые моделью классификатора случайного прогноза:

  • Сложность предложения
  • Предложить награду
  • Продолжительность предложения
  • Доход клиента
  • 2018 (год, когда пользователь создал учетную запись)
  • социальные (доступны ли предложения через социальные сети)

Проблемы, с которыми столкнулись

Было довольно легко исследовать данные и визуализировать различные функции. Самыми сложными аспектами для меня были

  1. Разработка функций
  2. Объединение наборов данных в один
  3. Определение того, было ли предложение успешным
  4. Определение параметров, которые необходимо настроить для улучшения моделей

Вывод

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

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

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