Главная » Хабрахабр » 3-е место в отборочном этапе DataScienceGame 2018

3-е место в отборочном этапе DataScienceGame 2018

image

DataScienceGame — это международное студенческое соревнование, которое проводится на ежегодной основе. Недавно закончился отборочный этап DataScienceGame2018, который проходил в формате kaggle InClass. Нашей команде удалось оказаться на 3м месте среди более чем 100 команд и при этом НЕ пройти в финальный этап.

Командное взаимодействие

КомандаНа больших соревнованиях на kaggle команды обычно формируются по ходу из людей с близким скором по лидерборду(типичный пример команды), а поэтому представляют разные города и, зачастую, разные страны. Тут же по условиям соревнования каждая команда должна была состоять из 4х человек из одного учебного заведения(мы представляли МФТИ). А, значит, у большинства участников, как мне кажется, все обсуждения проходили в оффлайне. У нас, например, вся команда жила на одном этаже общежития, поэтому мы просто собирались по вечерам у кого-нибудь в комнате.

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

Описание задачи

По данным из истории, предоставленным банком BNP, нужно было предсказать, заинтересуется ли пользователь некоторой ценной бумагой(Isin) на следующей неделе или нет. При этом “заинтересованность” определялась колонкой TradeStatus, которая описывала статус сделки и имела следующие уникальные значения:

  1. Сделка была совершена(то есть пользователь купил/продал бумагу)
  2. Пользователь посмотрел бумагу, но не совершил сделки
  3. Пользователь отложил бумагу для покупки/продажи в будущем
  4. Сделка не была совершена по техническим причинам
  5. Холдинг

Итак, если TradeStatus принимает значение 1)-4), то считается, что пользователь заинтересовался данной бумагой и не заинтересовался во всех остальных случаях. При этом пункт 4) обозначал, что строчка с данной сделкой фиктивная, и сделана для удобной отчетности. А именно, в конце каждого месяца проводилось сравнение состояния портфеля каждого пользователя с его состоянием месяц назад, и, если, например, у пользователя каким-то образом в портфеле количество определенной ценной бумаги увеличилось на 10к, то заводилась эта самая строчка с пометкой “покупка” и номиналом 10к. Строки с пометкой «холдинг» имели целевую переменную 0(пользователь не заинтересовался).

Например, пользователь с id=15 решил отложить для покупки в будущем бумагу, имеющую id=7. Если задуматься, то можно понять, что датасет собирался следующим образом: пользователи проявляли активность на сайте банка — просматривали/покупали бумаги, и все эти действия записывались в базу данных. Тут же в базе данных появлялась соответствующая строчка с таргетом 1(пользователь заинтересовался)

Id пользователя

Id ценной бумаги

Тип сделки

Статус сделки

Дополнительные поля

Таргет

15

7

Покупка

Отложено на будущее

...

1

Плюс к этому добавлялись ежемесячные записи со статусом холдинг и таргетом 0. Например, у пользователя 15 по каким-то причинам увеличилось количество акции 93(возможно, он купил ее на другом сайте), при этом сам он с данной бумагой на сайте BNP никак не взаимодействовал(не интересовался).

Id пользователя

Id ценной бумаги

Тип сделки

Статус сделки

Дополнительные поля

Таргет

15

93

Покупка

Холдинг

...

0

Но, очевидно, банку BNP, нет никакого смысла предсказывать эти самые холдинги, ведь их можно однозначно восстановить из базы. Значит существует другой тип ноликов, которых нет в тренировочной таблице, а именно — любые тройки “пользователь — бумага — тип сделки”, которые не попали в базу данных. То есть пользователь НЕ заинтересовался некоторой акцией, значит никак не провзаимодействовал с ней в системе BNP, значит соответствующая строка не появилась в базе данных, а значит она должна иметь таргет 0. А это говорит о том, что подобные строки для тренировки нужно сгенерировать самим(см. раздел «Составление тренировочной выборки»). Все это могло навести некоторую путаницу, ведь многие участники, наверняка, подумали — есть датасет, есть нолики и единички — можно предсказывать. Но не так все просто.

В тесте есть только тройки “пользователь — бумага — тип сделки” и для каждой такой тройки нужно предсказать, появится ли она на следующей неделе. Итак, в трейне есть таблица с историей сделок(то есть интеракции “пользователь — бумага — тип сделки” и некоторая дополнительная информация по ним) и куча других табличек с характеристиками пользователя, акции, глобального состояния рынка. Например, нужно предсказать, заинтересуется ли пользователь id=8 акцией id=46 с типом сделки «продажа»?

Id пользователя

Id ценной бумаги

Тип сделки

Таргет

8

46

Продажа

?

Особенности построения датасета

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

Очевидно, что это не случайность и возникает вопрос: как такое могло получиться? Если посмотреть на тройки “пользователь — бумага — тип сделки” из тестовой таблицы, то легко заметить, что количество сделок с типом «покупка» и «продажа» в точности совпадает, причем таблица строго отсортирована по этому признаку: сначала все покупки, потом все продажи. Так получилась таблица, в который типы сделок(покупка/продажа) расположены в произвольном порядке:
Например, так: организаторы взяли все реальные записи из своей базы данных за неделю, на которую нам нужно сделать предсказание(такие строки имеют таргет 1), каким-то образом сгенерировали новые строки(таргет у них 0), не совпадающими с описанными выше.

Id пользователя

Id ценной бумаги

Тип сделки

Таргет

8

46

Продажа

1

2

6

Покупка

1

158

73

Покупка

1

3

29

Продажа

0

67

9

Покупка

0

17

465

Продажа

0

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

Id пользователя

Id ценной бумаги

Тип сделки

Таргет

8

46

Покупка

0

2

6

Покупка

1

158

73

Покупка

1

3

29

Покупка

0

67

9

Покупка

0

17

465

Покупка

0

Остается последний шаг: сделать то же самое, но заменяя «покупку на продажу» и расставить правильные таргеты:

Id пользователя

Id ценной бумаги

Тип сделки

Таргет

8

46

Продажа

1

2

6

Продажа

0

158

73

Продажа

0

3

29

Продажа

0

67

9

Продажа

0

17

465

Продажа

0

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

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

Id пользователя

Id ценной бумаги

Тип сделки

Таргет

8

46

Продажа

?

8

152

Продажа

?

8

73

Покупка

?

Сокомандник посчитал, что это нормально, и изначально был отсортированный по id пользователей датасет, а организаторы просто плохо его пошафлили(например, если бы шафл был устроен на случайных перестановках и таких перестановок было совершено недостаточно много). Пытаясь убедиться в этом, он перебрал четыре шафла из разных библиотек, но нигде таких частых повторений не возникло. Тест тоже имел такую особенность. Появилась мысль, что нолики организаторы не сгенерировали, а просто взяли старые пары из трейна. Чтобы проверить, я решил сделать следующее: каждой паре «пользователь — бумага» из теста сопоставить номер строки из трейна, когда данная пара первый раз встретилась и сделать plot от этого. То есть, например, мы смотрим на первую строчку в тесте, пусть она имеет id пользователя = 8 и id = бумаги = 15. Теперь проходимся по тренировочной таблице сверху вниз и ищем, когда данная пара первй раз появилась, пусть это будет, например, 51-я строчка. Мы получили сопоставление: 1я строчка в тесте была в трейне 51-й, значит наносим на график точку с координатами (1, 51). Делаем так для всего теста и получаем следующий график:

image

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

А это значит, что никакого лика здесь нет и быть не может.

Составление тренировочной выборки

Так как в тесте нужно сделать предсказание для троек на одну неделю, то разобьем тренировочный датасет на недели(при этом на каждой неделе встречается в среднем 20к троек “пользователь — бумага — тип сделки”). Теперь для любой тройки мы можем сказать, встретилась ли она на конкретной неделе или нет. При этом положительные тройки у нас уже есть(это все записи с данной недели в таблице трэйна), а отрицательные нужно как-то сгенерировать. Есть много вариантов как это сделать. Например, можно перебрать абсолютно все тройки, которых не было на определенной неделе в тренировочном датасете. Понятно, что тогда выборка будет сильно несбалансированной, и это плохо. Можно сначала сгенерировать пользователей пропорционально частоте их встречаемости в датасете, а потом как-то добавлять к ним в соответствие акции. Но при таком подходе будет куча строчек, для которых нельзя посчитать разумные статистики, что тоже плохо. Как сделали мы: взяли всевозможные тройки, которые ранее встречались в трейне, скопировали, заменив buy/sell на противоположный и сконкатенировали эти две таблицы. Понятно, что так могли возникнуть дупликаты(например, если пользователь когда-либо и покупал, и продавал акцию), но их было немного, и после удаления получалась таблица на 500к уникальных троек. Ну и все, теперь на каждую неделю для каждой такой тройки можно сказать, встретилась она или нет(и сколько раз?).

А именно, возьмем последнюю доступную неделю из трейна, посмотрим, встречалась ли на этой неделе каждая тройка “customer — isin — buy or sell”. Поскольку по сути мы имеем дело с временными рядами — пользователь в каждую неделю просматривает конкретное объявление сколько-то раз, то таблицу для обучения классификатора будем строить классическим для временных рядов способом. А в качестве фичей посчитаем различные статистики, например, за последние 6 недель(подробнее про статистики в разделе «Признаки»). Это будет таргет. Так можно сделать несколько раз, тем самым увеличивая “по высоте” трейн, но при этом интервал, по которому мы считаем статистики, естественно, уменьшается. Теперь забудем про существование последней недели и сделаем все то же самое, но для предпоследней недели и сконкатенируем полученные таблицы. Поясняющая картинка: Мы повторяли такую операцию 10 раз, потому что если сделать больше, то в таргет бы попал праздник новый год и связанные с ним проблемы, которые ухудшили бы итоговое качество модели.

image

Подробнее про временные ряды и валидацию на временных рядах можно почитать тут.

Признаки

Как я уже говорил, было много таблиц, как-то характеризующих пользователя, акцию или глобальное состояние рынка(курсы главных валют и некоторые индикаторы). Но все они почти не улучшали качество, а главными признаками были статистики, посчитанные для пар “customer — isin”, и троек “customer — isin — buy or sell”, например, такие:

  • Как часто встречалась пара/тройка за последние 1, 2, 5, 20, 100 недель?
  • Статистики по временным промежуткам между встречаниями пары/тройки в датасете(mean, std, max, min)
  • Расстояние во времени до первого/последнего раза, когда встретилась пара/тройка
  • Доля каждого значения TradeStatus у пары/тройки
  • Статистики по тому, сколько раз за неделю пара/тройка встречается(mean, std, max, min)

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

С учетом дополнительных таблиц набиралось около 300 признаков. В коде это все выражалось функцией длиной в 200 строк, которая генерировала подобные признаки для каждого из десяти кусков трейна(для той части, где таргет, например, 7я неделя мы не должны использовать информацию за 8ю и 9ю). Как я уже говорил, мы сгенерировали 500к уникальных троек и в качестве таргетов брали последние 10 недель, следовательно “в высоту” тренировочная таблица была 500к*10 = 5кк строк.

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

Модели или боремся за тысячные

Стоит отметить, что почти три недели никто не мог побить baseline от BNP, который имел скор 0.794(ROC AUC) на лидерборде и это при том, что решение “просто посчитать количество раз, которое пара встречалась ранее” давало на лидерборде 0.71, а некоторые участники получали и все 0.74 без применения машинного обучения.

Как я уже сказал, наша тренировочная выборка состояла из 1. Но мы машинное обучение применяли, более того, в последний день соревнования(который по счастливой случайности совпал с концом сессии) мы решили упороться if you know what i mean и сделать большой блендинг из разных моделей, обученных на разных подмножествах признаков с разным количеством недель в трейне. Размер теста был 400к, в то время как предполагаемое число единичек было 20к(в среднем на деле встречается такое количество уникальных троек). 5кк строк, причем таргетов-единичек среди них примерно 150к. Поэтому во всех своих моделях мы регулировали параметр scale_pos_weight, который расставляет веса классам. То есть доля единичек в тесте была значительно выше, чем в трейне. Матрица корреляций предсказаний наших моделей показана на рисунке: Подробнее про этот параметр можно посмотреть на разборе лучшего решения одного из прошлогодних DataScienceGame.

image

80204. Как видно, у нас было много довольно разных моделей, что позволило получить на лидерборде скор 0.

Почему мы не поедем во Францию на финальный этап

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

  1. Не более 20 лучших команд
  2. Не более 5 лучших команд из страны
  3. Не более 1 команды из учебного заведения

И все бы хорошо, если бы на втором месте не оказалась другая команда из МФТИ со скором 0.80272. То есть мы отстали всего на 0.00068. Обидно, но тут уж ничего не поделаешь. Скорее всего, организаторы сделали такие правила чтобы люди из одного университета никаким образом не помогали друг другу, но в нашем случае, мы ничего не знали о соседней команде и никак с ней не контактировали.

Итоги

В этом году в сентябре в Париже будут бороться за первое место 5 команд из России, одна из Украины и две команды из Германии и Финляндии, состоящие из русскоговорящих студентов. Итого 8 команд ру-комьюнити, что в очередной раз доказывает доминацию ру-сегмента датасаенса. А я перевожусь в шарагу тренируюсь и работаю над собой, чтобы в следующем году все-таки побороть отборочный этап.


Оставить комментарий

Ваш email нигде не будет показан
Обязательные для заполнения поля помечены *

*

x

Ещё Hi-Tech Интересное!

[Перевод] Java для Playstation 2 — возможно ли это?

Введение Этим проектом я хотел ответить на один вопрос: возможно ли написать Java API для Playstation 2 и создать на нём графическое демо. Не хочу раскрывать спойлеры, но ответом будет «да». Но вместо того, чтобы дизассемблировать в ассемблерный код Java, ...

Проектные нормы в микроэлектронике: где на самом деле 7 нанометров в технологии 7 нм?

Современные микроэлектронные технологии — как «Десять негритят». Стоимость разработки и оборудования так велика, что с каждым новым шагом вперёд кто-то отваливается. После новости об отказе GlobalFoundries от разработки 7 нм их осталось трое: TSMC, Intel и Samsung. А что такое, ...