Авторы публикации — Дмитрий Сергеев и Юлия Петропавловская.

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

image



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


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


  • Контракты клиентов
  • Посещаемость
  • Заморозки
  • Коммуникации между клиентами и клубом

Тестовые и тренировочные наборы данных были разбиты по месяцам. Train содержал информацию о клиентах за декабрь 2015 года, а Test — за март 2016. Для каждой из категорий мы объединили Train и Test части для дальнейшей обработки.


Контракты клиентов


Контракты стали первым набором данных, за который мы взялись, так как именно там содержалась целевая переменная — "продлил ли клиент свой договор", а также коды контрактов и клиентов в количестве 17631 штук, послуживших ключами для объединения всех остальных датасетов. Небольшое количество пропущенных значений в переменных были восстановлены модами. Затем создали фичи для сезона (зима, весна...), месяца и дня, в который был заключен контракт с клубом, и переменные "длительность контракта", "остаток дней заморозки" и "остаток бонусов на счету". Различные категориальные переменные, такие как возрастная группа, сегмент клуба и т.д. оставили без изменений.


Посещаемость


Начали с создания переменной — длительность разового похода в фитнес-клуб.

image

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

Также в датасете присутствовали категориальные переменные, градации которых мы решили сгруппировать в более общие категории. Например, "КатегорияТренера":


additional = ['Сотрудник СПА', 'Врач']
coach = ['Тренер мастер', "Тренер фитнес"]
coach_vip = ["Тренер персональный", "Тренер элит"]
other = ['Другое']

Аналогично — "НаправлениеУслуги":


sport = ["Тренажерный зал", "Водные программы", "Аэробика", "Боевые искусства", "Mind Body", 
         "Танцевальные программы", "Игровые программы", "Йога", "Групповые программы"]

health_beauty = ["Солярий", "Парикмахерские услуги", "Лечебный массаж", "Маникюр, педикюр", "Массаж_SF",
                "Терапевтические процедуры", "Физиотерапевтические процедуры", "Косметические услуги",
                "Аппаратная косметология", "Окрашивание", "Аппаратная косметология_SF", "Врачи", "Врачи_SF",
                "Продажа косметических товаров", "Инъекции", "Прочие услуги SPA", "Лечебное питание", "Инъекции_SF", "SPA"]

Наконец, добавили переменную с частотой посещения клуба в месяц и суммарные количества посещений в различные сезоны (зима, весна..) и сгруппировали данные по кодам клиентов, содержавшихся в датасете по контрактам. Итого, из 3 700 000 записей осталось ~15 000 наблюдений.


Заморозки


Изначально мы выяснили, что в датасете имеются дубликаты. После небольшого исследования оказалось, что один и тот же номер контракта с одинаковыми операциями по заморозке содержится и в Train, и в Test, так как клиентская история заморозок переносилась в тестовый набор. Чтобы в будущем избежать переобучения моделей, мы выкинули повторяющиеся значения из теста.

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

image


Коммуникации


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

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

Сначала разбили переменную "Вид взаимодействия" на 3 дамми:


  • личная встреча
  • телефон
  • другое

Затем посчитали для каждого клиента общее и успешное ("состоялось") число коммуникаций. Разделив одно на другое получили переменную "доля успешных коммуникаций".

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

В результате, из 1 500 000 строк получили 15500 и объединили их с финальным датасетом. После преобразования категориальных переменных в дамми количество столбцов раздулось до 72 штук.


Машинное обучение


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


  • Random Forest
  • Neural Network
  • SVM
  • k-NN
  • Naive Bayes
  • Logit regression
  • Decision Stumps

Каждый из классификаторов, в целом, показывал очень неплохие результаты на валидации. Random Forest на 1000 деревьев с 10-fold cv давал 0.9499 AUC, двухслойная нейронная сеть смогла поднять результат до 0.98, а гроза соревнований на Kaggle, XGB, показал впечатляющие 0.982. Также xgboost помог с визуализацией важности признаков:

image

Первая тройка достаточно ожидаема — "длина контракта", "остаток бонусных баллов" и "средняя длина визита". Также в первой десятке "количество успешных коммуникаций", "остаток дней заморозки" и, внезапно, "посещал ли фитнес зимой".


Остальные модели, кроме решающих пеньков, в среднем, давали по 0.92-0.94 AUC и были добавлены в ансамбль для уменьшения коррелированности между различными предсказаниями.

Ансамбль задумывался в виде двух уровней — на первом сотня decision stumps, предсказания по которым объединялись при помощи принципа большинства голосов (majority vote), т.е. если 51 пенёк был "за", а 49 "против", то ставилась единица. На втором — подключались предсказания по остальным классификаторам для последующего объединения.

image

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

image

aj — веса, с которыми предсказания входят в ансамбль
yj(x) — индивидуальные предсказания классификаторов
p — число используемых моделей

Веса определялись путём минимизации logloss-а ансамбля, при помощи замечательной функции minimize, возвращавшей оптимальные значения вектора весов x0.


from scipy.optimize import minimize
opt = minimize(ensemble_logloss, x0=[1, 1, 1, 1, 1, 1, 1])

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

В результате такого отбора отпала логистическая регрессия и, сожалению, все пеньки, зато AUC вырос ещё на пару тысячных процента и составил 0.98486. Totally worth it.

Наконец, на тестовом датасете были сделаны предсказания, и чтобы иметь хотя бы какое-то представление об их качестве, были построены две гистограммы: первая для предсказанных ансамблем вероятностей продления клиентом контракта на валидационной выборке, вторая — на тестовой выборке.

image

Если предположить, что Train и Test выборки были более-менее однородны, и число продлившихся должно быть примерно равно числу отказавшихся, то налицо более чем двукратное завышение моделями вероятности продления контракта. Однако мы решили довериться решению ансамбля и не стали наказывать его за излишнюю наглостьоптимистичность прогноза. И как оказалось — не зря.

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

Ссылка на репозиторий.

Поделиться с друзьями
-->

Комментарии (4)


  1. imwode
    05.07.2016 22:28

    Однако мы решили довериться решению ансамбля и не стали наказывать его за излишнюю наглостьоптимистичность прогноза. И как оказалось — не зря.

    Я, может, невнимательно читал — почему не зря-то?


    1. Skolopendriy
      06.07.2016 20:47

      Под «не зря» подразумевалась победа в хакатоне :)
      Судя по всему, на тестовых данных именно такое решение показало наибольшую точность


  1. molec
    06.07.2016 11:53

    Мне кажется, что посещение фитнес-клуба — это в первую очередь процесс. Соответственно, прекращение посещений — терминальное событие в этом процессе. Исходя из этого логичнее было бы предсказывать дату прекращения посещений, ну или вероятность прекращения в ближайшее время, если это удобнее для текущего метода.
    Бизнес-цель работы — дать менеджменту время (в идеале месяц-два), чтобы замотивировать потерявшего интерес посетителя к продолжению, т.е. предоставить систему, маркирующую клиентов как имеющих тенденцию к непродлению абонемента.
    Исходя из этого, логичнее было бы:
    1) использовать методы анализа процессов, а не конечных наборов данных
    2) все же искать поведенческие паттерны, характерные для клиентов, потерявших интерес к занятиям. (наверняка среди них будут нерегулярность посещений, частые заморозки, уменьшение количества потребляемых услуг, неравномерная длительность посещений, снижение частоты посещений)
    Резюмируя, мое мнение — выбранный метод вообще не соответствует задаче (не исключаю, что и предоставленный заказчиком набор данных не слишком ей соответствовал).
    Статья довольно куцая и из нее тяжело понять, как конкретно работала представленная система и какой точности предсказания удалось добиться, что были за исходные данные.


    1. Skolopendriy
      06.07.2016 15:11

      Добрый день! Безусловно, вы правы, — цели бизнеса отличаются от целей данного соревнования. Для бизнеса важно понимать процесс взаимодействия клиента с предоставляемыми услугами, отслеживать тенденции, предупреждать возможный отток и т.д. Соответственно необходим ввод различных метрик для учёта паттернов и изменений, а также методов для удержания клиентов, отнесённых к зоне риска.

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

      В результате, получили некоторую «гибридную» систему, которая искала придуманные нами признаки во временных рядах и отмечала их наличие/отсутствие в конечном наборе данных. К сожалению, точность на тестовых данных организаторами не сообщалась, но на валидации AUC составил порядка 0.98.