Задача с Kaggle: Predicting Red Hat Business Value

В этой заметке задокументирую основные шаги решения конкурсной задачи от Red Hat. Конкурс проходил на площадке Kaggle.

Времени лично у меня было очень мало – около 7-10 дней. Поэтому финальный результат – ТОП 12%, хотя до последнего дня держался в ТОП 9%. В принципе это неплохо (учитывая, что это мое второе подобное соревнование), но можно было лучше.

Вот здесь можно скачать упрощенный вариант моего решения. Эта версия кода дает результат порядка 99% AUC (это далеко не лучшее мое решение, но зато очень наглядное и простое; в финальном варианте я использовал микс из более сложных моделей)

Исходные данные

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

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

Используемая метрика: AUC.

Оригинальное описание задачи:

Like most companies, Red Hat is able to gather a great deal of information over time about the behavior of individuals who interact with them. They’re in search of better methods of using this behavioral data to predict which individuals they should approach—and even when and how to approach them.

In this competition, Kagglers are challenged to create a classification algorithm that accurately identifies which customers have the most potential business value for Red Hat based on their characteristics and activities.

With an improved prediction model in place, Red Hat will be able to more efficiently prioritize resources to generate more business and better serve their customers.

Пример входных данных:
people.head() # Первые 5 строк из таблицы people. Размерность таблицы: (189118, 40)
char_1 group_1 char_2 char_37 char_38
people_id
ppl_100 type 2 group 17304 type 2 False 36
ppl_100002 type 2 group 8688 type 3 False 76
ppl_100003 type 2 group 33592 type 3 True 99
ppl_100004 type 2 group 22593 type 3 True 76
ppl_100006 type 2 group 6534 type 3 False 84

Большинство признаков – категориальные. Что они обозначают, можно только догадываться 🙂

atrain.head() # Первые 5 строк из таблицы с информацией об активности клиентов. Размерность таблицы: (2197291, 14) – более двух миллионов строк.

activity_id date activity_category char_1 char_10 outcome
people_id
ppl_100 act2_1734928 2023-08-26 type 4 NaN type 76 0
ppl_100 act2_2434093 2022-09-27 type 2 NaN type 1 0
ppl_100 act2_3404049 2022-09-27 type 2 NaN type 1 0
ppl_100 act2_3651215 2023-08-04 type 2 NaN type 1 0
ppl_100 act2_4109017 2023-08-26 type 2 NaN type 1 0

Предсказание нужно сделать на таблице act_test.csv – она повторяет структуру act_test.csv за исключением столбца outcome, который и является целью наших предсказаний.

Краткая история решения

Вся задача свелась к трем вещам:

  1. Правильная предобработка входных данных. В частности огромное значение имело то, какие данные выделить под cross validation.
  2. Создание признаков и их фильтрация.
  3. Эксперименты с моделями.

Забегая вперед, отмечу, что в в исходных данных обнаружилась утечка информации, которой воспользовались все участники. На части данных можно было просто посмотреть значения двух признаков и сделать абсолютно верное предсказание безо всяких моделей. Как я понял, такое частенько случается в конкурсных задачах. Соответственно финальный результат у всех участников был чуть лучше, чем это возможно в реальности. Но разница не столь существенна. Безо всяких трюков можно было получить AUC около 98%. С утечкой информации – около 99.5%.

Теперь по этапам решения задачи.

Предобработка данных.

Что делал на этом шаге:

  • Закодировал категориальные признаки числами.
  • Заполнил пропуски в данных числами, которые не пересекаются с используемым диапазоном.
  • Перевел признаки в бинарный вид (one hot encoding) и сразу сохранил данные в формате sparse матриц (csr_matrix). В моем случае это было просто необходимо, так как в подготовленной тренировочной таблице было 36288 столбцов и около двух миллионов строк.
  • Выделил данные под cross validation.

Последний пункт стоит раскрыть поподробнее. Он был чуть ли не самым главным. Суть в следующем:
– в тестовой таблице, на которой нам предстояло делать предсказания, не было людей, которые были в тренировочных (размеченных) данных. То есть люди в test и train не пересекались. Чтобы хорошо обучить модель, нужно было создать cross validation set, который бы правдоподобно эмулировал эту ситуацию. То есть в cross val не должно было быть информации об активности тех людей, которые были в тренировочной таблице.

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

Еще пара строк про sparse матрицы. Все вычисления я делал на своем ноутбуке с 8 ГБ оперативной памяти, поэтому необходимость в таком способе хранения возникла сама собой. Я использовал сжатые про строкам sparse-матрицы (Compressed Sparse Row matrix) из модуля scipy.sparse.

Для создания матриц был выбран довольно хитрый вариант:

  • csr_matrix((data, (row_ind, col_ind)), [shape=(M, N)])
    where data, row_ind and col_ind satisfy the relationship a[row_ind[k], col_ind[k]] = data[k].
  • По сути этот способ позволяет одновременно закодировать данные в бинарный вид и преобразовать в sparse матрицу.

Создание признаков и их фильтрация

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

Некоторые новые признаки, которые успел придумать и проверить:

  •  isweekend – показывает, является ли дата активности выходным днем.
  • delta – количество дней между минимальной и максимальной датой активности для каждого человека.
  • deltareg – время, прошедшее от начала регистрации до каждой активности.
  • ndates – количество активностей для каждого человека.
  • максимальная/минимальная дата активности.
  • И так далее…

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

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

Эксперименты с моделями

Первым делом я попробовал нейронные сети (библиотека keras). Но в итоге остановился на xgboost.

Данные для нейронной сети я не кодировал в бинарный вид, потому что на моем ноутбуке получалась непосильная вычислительная задача. С нейронками в целом у меня получался неплохой результат (кажется, около 95% AUC), но это явно не подходило для Kaggle.

С xgboost получалось гораздо лучше (около 99%):

redhat01

В финале я смешивал результаты работы двух моделей:

  • gblinear (xgboost)
  • gbtree (xgboost)

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

Но тут главное – сам принцип. В данном случае неплохо сработал микс из деревьев и линейной модели (gblinear).

Мой итоговый результат на private leaderboard: 0.991703 (ТОП 12%)
У победителя 0.995124

То есть борьба шла за доли процента.

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

Но все равно собой доволен.

Leave a Reply

Your email address will not be published. Required fields are marked *