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

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

Все начинается с данных

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

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

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

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

И в машинное обучение

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

train_carts = (order_products_train_df.groupby('user_id',as_index=False)
                                      .agg({'product_id':(lambda x: set(x))})
                                      .rename(columns={'product_id':'latest_cart'}))
df_X = df_X.merge(train_carts, on='user_id')
df_X['in_cart'] = (df_X.apply(lambda row: row['product_id'] in row['latest_cart'], axis=1).astype(int))

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

Поскольку продукт с идентификатором 1 не был заказан ранее, его значения в столбце in_cart равны 0. Следовательно, этот столбец in_cart будет объектом классификации. Если пользователь с большей вероятностью изменит порядок определенного элемента, мы получим прогноз 1, в противном случае - 0.

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

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

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

Одна вещь, которую я не упомянул в начале, - это то, что мои классы предсказания были довольно несбалансированными. Разработанная функция in_cart показала, что порядок товаров менялся примерно один раз из десяти. Чтобы компенсировать этот дисбаланс классов, я использовал балансировку веса по умолчанию в sklearn:

lr_balanced = LogisticRegression(class_weight='balanced', C=1000000)

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

Выводы

В конце концов, эта простая логистическая регрессия с недавно разработанными функциями и ручной балансировкой классов дала довольно хорошие результаты. С F1 = 0,381 я не сильно отставал от лидеров Kaggle, которые колебались в районе F1 = 0,41 с множеством заявок и более причудливых моделей.

И если мы посмотрим на матрицу путаницы, мы сможем лучше понять, что на самом деле представляет мой результат F1. С точностью 0,3 моя модель могла правильно предсказать 30% всех переупорядоченных элементов, а отзыв 0,52 определил, сколько всего истинных и ложных срабатываний может предсказать моя модель. По крайней мере, клиенты могут быть уверены, что им не нужно будет оформлять отложенный заказ.

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

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

Однако порядок подсчета товара, добавляемого в корзину, играет отрицательную роль в шансах на повторный заказ товара, что вполне логично: вначале мы видели, что средний размер корзины для пользователя составляет около 10 товаров. Если товар постоянно помещается 15-м товаром в корзине покупок, то, скорее всего, он не попадет в среднюю корзину.

Конец

И это все на сегодня. Здесь вы можете найти мою невероятно веселую презентацию. Весь код также доступен в этом репозитории.

Я также использовал AWS EC2 (Amazon Web Services, Elastic Cloud 2) для моделирования и настоятельно рекомендую Руководство Криса Албона по настройке виртуальной машины и Jupyter Notebook для работы на ней.