{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "
\n", "## Открытый курс по машинному обучению. Сессия № 3\n", "\n", "###
Автор материала: Кирилл Трубанёв " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Индивидуальный проект по анализу данных \"Modulbank AI Hack MSK\"
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Задача бинарной классификации**: по имеющимся обезличенным данным о клиентах банка спрогнозировать подключение клиентом услуги «ИКС».\n", "\n", "Оригинал задачи https://boosters.pro/champ_modulbank_msk\n", "\n", "Отборочный онлайн этап 20.03.2018 – 17.04.2018\n", "\n", "**Рейтинг** строится на 30% тестового датасета, финальный рейтинг будет построен на 70% тестового датасета и может отличаться.\n", "\n", "На момент взятия задачи публичный рейтинг был ... \n", "\n", "\n", "... финальный рейтинг был ... " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**План исследования**\n", " - Описание набора данных и признаков\n", " - Первичный анализ данных\n", " - Первичный визуальный анализ данных \n", " - Инсайты, найденные зависимости\n", " - Выбор метрики\n", " - Выбор модели\n", " - Предобработка данных\n", " - Кросс-валидация и настройка гиперпараметров модели\n", " - Создание новых признаков и описание этого процесса\n", " - Построение кривых валидации и обучения \n", " - Прогноз для тестовой или отложенной выборки\n", " - Выводы \n", " \n", " Более детальное описание [тут](https://goo.gl/cJbw7V)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import time\n", "import numpy as np\n", "import pandas as pd\n", "from matplotlib import pyplot as plt\n", "%matplotlib inline\n", "import os.path\n", "import seaborn as sns\n", "from sklearn.preprocessing import MinMaxScaler\n", "from sklearn.cluster import KMeans\n", "from sklearn.model_selection import train_test_split, GridSearchCV, StratifiedKFold, learning_curve, validation_curve\n", "import xgboost as xgb\n", "from sklearn.metrics import roc_auc_score" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "RANDOM_SEED = 17" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "pd.set_option('display.max_rows', 40)\n", "pd.set_option('display.max_columns', 20)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 1. Описание набора данных и признаков" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Датасет из соревнования Modulbank, где почти всё описание помещается в одну фразу \"*по обезличенным данным о клиентах банка нужно спрогнозировать подключение клиентом услуги «ИКС»*\". Поэтому описать процесс сбора данных, решаемую задачу, её ценность, целевой и прочие признаки достоверно не возможно.\n", "\n", "НО, по опыту банковских проектов и понимая, что это не продакш-задача, а учебная задача хакатона чтобы отселектить толковых сайнтистов для последующего хантинга в банк, стОит выдвинуть догадки о данных и признаках:\n", "- Собирали данные о клиентах банка, подключивших или не подключивших услугу \"ИКС\"\n", "- Услуга \"ИКС\" - это какой-то банковский продукт/сервис, который генерирует банку доход, но и требует расхода на его продажу. Поэтому, как минимум, имеет смысл как можно точнее понимать вероятность приобретения услуги чтобы наиболее эффективно продвигать эту услугу\n", "- Целевой признак - бинарный факт подключения услуги \"ИКС\"\n", "- Признаки - это какие-то характеристики клиента, причём пока непонятно, связанные или не связанные с целевым признаком (вот и посмотрим)\n", "- Данные обезличенные - то есть о физическом смысле признаков, и тем более о том, что за клиенты, предстоит только догадываться \n", "- Одна строка данных - это один клиент. При этом хочется предполагать, что один клиент не встречается в данных несколько раз с разными описывающими признаками и/или с разным значением целевого признака\n", "- Данные напрямую не явлются временным рядом (но при этом не обязательно на данных нельзя ввести отношение порядка по какой-то функции от подмножества признаков)\n", "- Наверняка будут всякие учебные приколы с кривыми форматами данных, выбросами, пропусками, зашумливанием, силовым переопределением таргета для каких-то наблюдений, разными распределениями трейна и теста и прочими гениальными идеями организаторов как бы нагенерить побольше способов отделять более толковых сайнтистов от менее бестолковых " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Авторы пишут нам, что \"*Таргетом является первый столбец в данных. 1 - услуга подключена, 0 – отказ от подключения. Датасет содержит анонимный набор из переменных*\".\n", "\n", "Посмотрим сами источники трейна и теста ...\n", "\n", "\n", "\n", "... и ещё посмотрим размеры файлов чтобы быть уверенными, что они влезут в оперативку:\n", "\n", "\n", "Структура данных понятна, суммарный размер ~50Мб, загружаем:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "PATH_TO_DATA = 'C:\\\\Users\\\\Kirill\\\\MLcource_open\\\\ModulBank_X' # поставьте тут свой путь\n", "df_train = pd.read_csv(os.path.join(PATH_TO_DATA, 'train.csv'), sep='\\t', index_col=0)\n", "df_test = pd.read_csv(os.path.join(PATH_TO_DATA, 'test.csv'), sep='\\t', index_col=0)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# сразу сделаем один датасет всех данных чтобы не копипастить код преобразований данных между датасетами\n", "df = pd.concat([df_train, df_test], axis=0)\n", "df.rename(columns={'0': 'y'}, inplace=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# TODO перенести это куда-то потом\n", "df_train['0'] = (df_train['0']).astype('int') # один раз поправим тип таргета тут " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.head(5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# сразу выделим таргет и удалим его столбец из данных \n", "y = df_train['0']\n", "df_train.drop(columns=['0'], inplace=True)\n", "df_test.drop(columns=['0'], inplace=True)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# посмотрим что у нас загрузилось и сформировалось\n", "df_train.head(5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_test.head(5)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "y.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "На этом **разумное** вполнение задачи \"*Описан процесс сбора данных (если применимо), есть подробное описание решаемой задачи, в чем ее ценность, дано описание целевого и прочих признаков*\" можно считать завершённым. Идём дальше." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 2. Первичный анализ данных" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Итак, нам нужно сделать вот что:\n", "*Исследованы признаки, их взаимодействия, влияние на целевой признак. Исследовано распределение целевого признака (в случае задачи регрессии проведены стат-тесты на нормальность и скошенность (skewness) распределения). Если необходимо, объясняется, почему и как можно преобразовать целевой признак. Изучены выбросы и пропуски в данных*" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# посмотрим сколько вообще у нас чего есть\n", "df_train.shape, df_test.shape, df.shape" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# посмотрим типы признаков и null-значения\n", "df.info(verbose=True, max_cols=350, memory_usage='deep', null_counts=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Все 345 признаков int или float, все not null (кроме неизвестной части таргета) - ну уже хорошо." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_descr = df.describe(include='all').transpose()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_descr.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Признаков много, глазами весь дескрайб не проверишь.\n", "\n", "Есть подозрение, что все уже \"отминимаксено\" в [0..1]. Проверим.." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_descr['min'].value_counts()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_descr['max'].value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что все признаки (и даже целевой) в диапазоне [0..1], причём есть те, кто не достаёт до ширины всего диапазона т.е. min > 0 или max < 1." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим, есть ли совсем нулевые признаки..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_descr[df_descr['min'] == df_descr['max']]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Есть два совсем нулевых признака - 140 и 164." ] }, { "cell_type": "raw", "metadata": {}, "source": [ "Посмотрим, кто как выбрасывается..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "round(df.apply(lambda x: np.abs(x - x.mean()) / x.std() > 3).sum(axis=0) * 100 / df.shape[0], 0).value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что больше половины признаков вообще не выбрасываются (0% количества значений далее 3 сигм от матожидания). У почти всех остальных признаков выбросов менее 5%, и только у 21 признака выбросов больше 5%, но и то меньше 8%.\n", "\n", "В целом у нас очень плотно распределёный датасет." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим корреляции всех со всеми..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_corr = df.corr()\n", "sns.set(rc={'figure.figsize':(20,20)})\n", "sns.heatmap(df_corr);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Можно выделить несколько групп признаков:\n", "- первые полторы сотни положительно разно-коррелированных между собой признаков (разно-оранжевые квардраты с диагоналями)\n", "- дюжину признаков в диапазоне 140-190 с сильными корреляциями к какому-то одному другому признаку (отдельные светлые точки)\n", "- признаки с прямой и обратной корреляцией вида \"один ко многим\" в районе 190-230-х и особенно весь блок 310-340 (пёстные квадраты с вертикальными и горизонтальными одноцветными полосками)\n", "- нулевые признаки (белые полоски, мы с ними уже выше познакомились)\n", "\n", "Все остальные признаки между собой и с таргетом не особо связаны." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Поизучаем таргет.." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df['y'].value_counts()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В трейне около 18% (=5423/30500) класса 1 - похоже, у нас плоховато сбалансированная выборка.\n", "Запомним эту гипотезу для дальнейшей её адресации и пойдём дальше.\n", "\n", "Посмотрим корреляции признаков с таргетом..." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_corr['y'][1:].sort_values(ascending=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Есть пара дюжин заметных прямых (в начале списка) и обратных (в конце списка) корреляций. Нет сильно скоррелированных признаков, что исключает тривиальные решения задачи - ну и хорошо.\n", "\n", "Первичный анализ данных можно считать завершённым, идём дальше." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 3. Первичный визуальный анализ данных" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Тут хорошо бы сделать вот что:\n", "*Построены визуализации (распределения признаков, матрица корреляций и т.д.), описана связь с анализом, данным в пункте 2. Присутствуют выводы*\n", "\n", "Корреляции мы уже посмотрели выше.\n", "Интересно было бы понимать распределения признаков, например, чтобы отделить дискретные и непрерывные, у дискретных поугадывать физический смысл, выделить кандидатов на OneHotEncoding и т.п." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, axes = plt.subplots(115, 3, figsize=(20,460))\n", "for ix in range(1, 115 * 3 + 1):\n", " row_ix, col_ix = divmod(ix - 1, 3)\n", " axes[row_ix, col_ix].plot(df[str(ix)].sort_values(ascending=True).values, '.')\n", " axes[row_ix, col_ix].tick_params(axis='both', labelbottom=False, labelleft=False)\n", " axes[row_ix, col_ix].set_ylabel(str(ix))\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Признаки можно отнести к нескольким группам (причём иногда к нескольким сразу):\n", "- бинарные (только min и max) - ничего не делаем\n", "- много max - будем делать флажок is_max\n", "- много min - будем делать флажок is_min\n", "- много max, много min, остальные между - будем делать флажки is_max, is_min, is_between\n", "- дискретные с несколькими значениями - будем выделять значения категорий и кодировать ohe-колоночками\n", "- непрерывные - ничего не будем делать\n", "- с выбросами - будем делать признак is_outlier_up или is_outlier_down и переопределять значение\n", "- полностью с одним значением - просто дропнем\n", "\n", "Посмотрим на всех глазами и запомним какой признак к каким группам относится:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# неохота всё в кавычках строками писать - поэтому быстро заполню цифорки в множествах, а потом сконвертирую \n", "many_maxes = {99, 100, 211, 212, 214, 314, 315, 319, 324, 325, 326, 327, 333, 334, 335}\n", "many_mins = set(i for i in range(66, 91+1)) | set(i for i in range(109, 116+1)) | {118, 119, 121, 122, 125, 321, 331, 341}\n", "many_mins_and_maxes_and_betw = ['13']\n", "leveled = {7, 92, 93, 94, 98, 99, 100, 101, 102, 103, 106, 107, 108, 117, 118, 120, 121, 124, 127, 194, 212, 213, 214, 311,\n", " 312, 313, 314, 315, 317, 341 } | set(i for i in range(319, 331+1)) | set(i for i in range(333, 336+1))\n", "outliered = {121, 122, 123, 195, 197, 198, 204, 206, 321, 323, 336}\n", "all_const = ['140', '164']\n", "\n", "many_maxes = [str(i) for i in many_maxes]\n", "many_mins = [str(i) for i in many_mins]\n", "leveled = [str(i) for i in leveled]\n", "outliered = [str(i) for i in outliered]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "По аутлаерам выберем коэффициент для дальнейшего применения метода межквартильного размаха при отсечении выбросов" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df[outliered].boxplot(figsize=(20,5), whis=1.5); #1.5 - по умолчанию, многовато оставляет за границей" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df[outliered].boxplot(figsize=(20,5), whis=2.0); # лучше" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df[outliered].boxplot(figsize=(20,5), whis=2.5); # ещё лучше" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df[outliered].boxplot(figsize=(20,5), whis=3.0); # достаточно хорошо" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df[outliered].boxplot(figsize=(20,5), whis=3.5); # многовато: сожрали интерквартильным размахом почти все выбросы" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим, как признаки связаны с таргетом:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "fig, axes = plt.subplots(69, 5, figsize=(20,230))\n", "for ix in range(1, 69 * 5 + 1):\n", " row_ix, col_ix = divmod(ix - 1, 5)\n", " sns.boxplot(x='y', y=str(ix), data=df, ax=axes[row_ix, col_ix])\n", " axes[row_ix, col_ix].set_ylabel(str(ix))\n", " axes[row_ix, col_ix].tick_params(axis='both', labelbottom=False, labelleft=False)\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Признаков, по-разному ведущих себя на при разных значениях таргета, полтора десятка. Запомним их чтобы потом сравнить с важностью в бустинге." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "target_diff_features = {61, 61, 65, 86, 87, 88, 298, 312, 323, 324, 333, 334, 335, 341}\n", "target_diff_features = [str(i) for i in target_diff_features]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Смотреть по всем признакам распределения, подбирать трансформацию к нормальному распределению, логарифмировать, искать попарные влияния не будем т.к. задача обезличена и связи с физическим миром уничтожены, в угадайку времени играть нет, проект учебный, а не кегловый на корову.\n", "\n", "Готово, для каждого признака сформировали мнение о нём и планы по его дальнейшей трансформации. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 4. Инсайты, найденные зависимости" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Тут хорошо бы сделать вот что:\n", "*Найдены и выдвинуты предположения о природе различных корреляций/пропусков/закономерностей и выбросов, найденных в предыдущих пунктах. Есть пояснение, почему они важны для решаемой задачи*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Поскольку задача специально обезличена и оторвана авторами от физического мира, выявлять гипотезы на основании физики сильно затруднительно.\n", "\n", "Зная хотя бы, что это клиенты банка, то хорошо бы, конечно, поразгадывать по значениям переменных, где там пол, возраст, дата, остаток по счёту, пользование другими финансовыми продуктами и т.п. Но задача учебная, важно знать что так можно делать и понимать применимость к этому датасету, но до дедлайна нет времени часами сидеть разгадывать. \n", "\n", "Но, понимая, откуда задача, всё же можно сделать несколько предположений и далее их потестировать в модели:\n", "- клиентами Модульбанка являются малые предприниматели - ребята, которым всё время не хватает оборотных средств, и которым её негде взять, кроме как лично на себя (залогов, поручительств и прочего у малого бизнеса нет)\n", "- знаем, что таргет - это подключение какого-то продукта или услуги. Ключевыми факторами в принятии решений компаниями малого бизнеса почти всегда является личное благосостояние самого предпринимателя. Возможно, это нам поможет объяснить распределения и связи выявленных фич с таргетом \n", "- в частности, клиентов в банке много, работает закон больших чисел в описании их типовых поведений т.е. поведение клиентов с точки зрения потребления финансовых продуктов и услуг, как правило, достаточно шаблонно. Таким образом, можно будет выделить несколько типовых профилей / сегментов / кластеров - и таргет будет насколько-то ими объясняться.\n", "\n", "Давайте посмотрим на корреляции с таргетом тех признаков, которые мы выделили как наиболее характеризующие:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_corr['y'][target_diff_features].abs().sort_values(ascending=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Распределение значений отмеченных нами признаков интересное: 323/324 похожи на FICO score физических лиц, 86/87/88 похожи на рассчитываемый лимит автоматического скоринга. Тогда, если гипотетически таргет - это какой-то кредитный продукт, например, овердрафт к счёту [а что ещё может волновать банк? :-) ], то модель начинает обретать смысл:\n", "- овердрафт к счёту могут подключить только те, кому скоринг одобрил, и не берут те, котому скоринг не одобрил (см. бокс-плоты 86/87/88)\n", "- овердрафт к счёту подключают те, у кого FICO score не высокий (см. бокс-плоты 323, 324) т.к. те, у кого FICO score невысокий, не закредитованы и им деньги не очень нужны\n", "\n", "На этом главную гипотезу модели и найденные зависимости можно, с учётом обезличенности данных, считать описанными. Едем дальше." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 5. Выбор метрики" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Тут хорошо бы сделать вот что:\n", "*Есть разумное обоснование выбора метрики качества модели. Описаны моменты, влияющие на выбор метрики качества (решаемая задача, цель решения, количество классов, дисбаланс классов, прочее)*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Вообще-то авторы нам прямо сказали, что **оценкой качества в задаче является ROC-AUC:**\n", "\n", "" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Но мы так просто не сдадимся и порассуждаем, а какую бы мы сами выбрали метрику для такой задачи.\n", "\n", "Итак:\n", "- это задача классификации - значит, применима метрика точности (accuracy)\n", "- но это задача бинарной классификации - значит, лучше подходят метрики на основе матрицы несоответствий: точность, полнота, F1\n", "- классы несбалансированные - значит, обычные точность и полнота не подойдут - нам нужна устойчивая к несбалансированным классам метрика, например (вот удивительно, да?), ROC AUC\n", "- в данной задаче модель будет использоваться для наиболее точного прогноза на закрытых тестовых данных организаторов - значит, нас волнует по факту не значение спрогнозированной вероятности, а относительный порядок наших прогнозов - и тогда тем более ROC AUC\n", "- в жизни модель будет использоваться не для классификации, а для прогнозирования вероятности приобретения продукта ИКС - вот тут уже ROC AUC будет менее важен, и на первый план выйдут метрики регресии, такие как средняя <какая-нибудь> ошибка, R-квадрат." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 6. Выбор модели" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Тут хорошо бы сделать вот что:\n", "*Произведен выбор модели. Описан процесс выбора и связь с решаемой задачей*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Ну, во-первых, " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если на мгновение отложить в сторону XGBoost, то авторитетные ресурсы рекомендуют нам SVM или решающий лес - и можно предположить почему: бинарная классификация, фич много, данных немного, переобучаться не хочется.\n", "\n", "\n", "\n", "Нейронка вряд ли зайдёт: маловато наблюдений. Да и трешевых столбцов много, что, при общей низкой скорости обучения сетки, сделает её применение на моём ноуте совсем неразумным.\n", "\n", "Как вариант, можно постекать разные модели, но вспоминаем тут, что это учебная задача, а не кегл на корову. Хотя, мини-стекинг будет - см. кластеризацию далее. \n", "\n", "Собственно, может быть, ещё нам в решении задачи поможет кластеризация методом k-средних (если её скормить основной модели) - посмотрим." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 7. Предобработка данных " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Тут хорошо бы сделать вот что:\n", "*Проведена предобработка данных для конкретной модели. При необходимости есть и описано масштабирование признаков, заполнение пропусков, замены строк на числа, OheHotEncoding, обработка выбросов, отбор признаков с описанием используемых для этого методов. Корректно сделано разбиение данных на обучающую и отложенную части*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### 7.1 Предобработка данных" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для этого датасета получилось, что прямо отдельно в этом пункте никакого кода предобработки не требуется т.к.:\n", "- все признаки уже отмасштабированы в интервал 0..1 (или даже чуть-чуть уже)\n", "- выбросы я буду переопределять значениями поближе к основной массе товарищей позже, после создания новых признаков is_outlier \n", "- признаки с аутлаерами я буду перемасштабировать позже, после переопределения выбросов\n", "- пропусков в признаках нет - мы это уже посмотрели\n", "- строк нет - всё сразу числами, мы это уже посмотрели\n", "- OneHotEncoding-похожее преобразование я буду делать в создании новых признаков, восстанавливая значения категорий из дискретных признаков\n", "- отбор признаков для определения целевого набора признаков я буду делать позже, в настройке гиперпараметров, т.к. набор признаков сам по себе является гиперпараметром" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### (анонс) 7.2 Разбиение данных на обучающую и отложенную части" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Разбиение данных на обучающую и отложенную части я сделаю после Части \"Создание новых признаков и описание этого процесса\" чтобы не копипастить код преобразований данных для каждого датасета (а их у нас разведётся немало - позже покажу почему), не ошибаться с заменами после копипастов и т.п." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Таким образом, все нужные механизмы предобработки данных для этого датасета понятны и делаться будут, просто не в части 7, а в других частях проекта." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 8 (в оригинале 9). Создание новых признаков и описание этого процесса" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Примечание: я поменял местами 8 и 9 т.к. генерю новые признаки ДО того, как подбираю параметры модели*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Тут хорошо бы сделать вот что:\n", "*Созданы новые признаки. Дано обоснование: логическое (например, у птиц температура тела на несколько градусов выше человеческой, значит вирус ХХХ не выживет в такой среде), физическое (например, радуга означает, что источник света расположен сзади; расчет величины по физическому закону с использованием данных признаков) или другое (скажем, признак построен после визуализации данных). Обоснование разумно описано. Полезность новых признаков подтверждена статистически или с помощью соответствующей модели*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Какие признаки добавлять - мы уже установили при анализе данных и визуальном анализе." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для leveled-признаков посмотрим, сколько их надо будет добавить:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "vc_sizes = []\n", "for col in leveled:\n", " vc_sizes.append(df[col].nunique())\n", "print(np.sum(vc_sizes))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "6441 колонок многовато для дальнейшего обучения на ноутбуке, выберем разумное число поменьше:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "np.sort(vc_sizes)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "85 представляется разумным т.к. дальше единичные признаки - мы избежим добавления 4,5 тысяч колонок" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Пройдёмся по признакам, посмотрим, к каким группам они относились, и достроим нужные комплекты по группам." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "for col in df.columns:\n", " if col in many_maxes:\n", " max_val = df[col].max()\n", " df[col + '_is_max'] = (df[col] == max_val) * 1\n", " \n", " if col in many_mins:\n", " min_val = df[col].min()\n", " df[col + '_is_min'] = (df[col] == min_val) * 1\n", " \n", " if col in many_mins_and_maxes_and_betw:\n", " max_val = df[col].max()\n", " min_val = df[col].min()\n", " df[col + '_is_max'] = (df[col] == max_val) * 1\n", " df[col + '_is_min'] = (df[col] == min_val) * 1\n", " df[col + '_is_between'] = ((df[col] > min_val) & (df[col] < max_val)) * 1\n", " \n", " if col in leveled:\n", " if df[col].nunique() <= 85:\n", " vc = df[col].value_counts().reset_index(drop=False)\n", " for i_vc in vc.index:\n", " df[col + '_' + str(i_vc)] = (df[col] == vc.loc[i_vc]['index']) * 1\n", "\n", " if col in outliered:\n", " q25, q75 = np.percentile(df[col], [25, 75])\n", " iqr = q75 - q25\n", " \n", " min_val = q25 - (iqr * 3.0) # 3.0 мы выбрали в визуальном анализе данных по аутлаерам\n", " max_val = q75 + (iqr * 3.0)\n", " \n", " df[col + '_is_outlier_down'] = (df[col] < min_val) * 1\n", " df[col + '_is_outlier_up'] = (df[col] > max_val) * 1\n", " \n", " df.loc[df[col] < min_val, col] = min_val\n", " df.loc[df[col] > max_val, col] = max_val" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Константные признаки просто дропнем:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.drop(columns=all_const, inplace=True)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Смасштабируем всё в [0..1] т.к. нам может понадобиться применять разные модели, а им на вход часто нужны [0..1]:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "scl = MinMaxScaler()\n", "df = pd.concat([df['y'], pd.DataFrame(scl.fit_transform(df.drop(columns=['y'])), index=df.index, columns = df.columns[1:])],\n", " axis='columns')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "У нас, предположительно (т.к. не знаем физику данных), несколько десятков категориальных признаков. Полезным подходом для вытягивания смысла из категориальных признаков является их переклеивание между собой парами, тройками и т.д., но я воздержусь: и так уже на получившихся 2-х тысячах признаков мой ноут пыхтит по 15-30 минут. " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Другая тема - кластеризация клиентов банка - может дать прирост если её скормить основной модели - попробуем.\n", "Причина простая: поведение клиентов с точки зрения потребления финансовых продуктов и услуг, как правило, достаточно шаблонно т.е. можно выделить несколько типовых профилей / сегментов / кластеров.\n", "\n", "Количество классов не знаем, но раз это клиенты банка - значит, разных профилей поведения там или вообще небольшое или обозримое количество. Поэтому попробуем 5, 10 и 15. Обучать можно на всём датасете без таргета т.к. все признаки тестового датасета у нас есть и какие-то другие данные не будут скармливаться модели. " ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "k5_pred = KMeans(n_clusters=5, random_state=RANDOM_SEED).fit_predict(df.drop(columns=['y']))\n", "k10_pred = KMeans(n_clusters=10, random_state=RANDOM_SEED).fit_predict(df.drop(columns=['y']))\n", "k15_pred = KMeans(n_clusters=15, random_state=RANDOM_SEED).fit_predict(df.drop(columns=['y']))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df = pd.concat(\n", " [df, pd.DataFrame(np.hstack((k5_pred[None].T, k10_pred[None].T, k15_pred[None].T)),\n", " index=df.index, columns=['k5', 'k10', 'k15'])],\n", " axis='columns')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "К изначальным 345 мы нагенерили ещё плюс 2 тысячи признаков - оооочень вряд ли прям все они нам нужны для дальнейшей настройки модели.\n", "\n", "Будем выберать самые полезные признаки для модели не в этом пункте, а позже, при настройке гиперпараметров.\n", "\n", "На этом генерация признаков и её описание завершено." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### (флешбек из части 7) 7.2 Разбиение данных на обучающую и отложенную части" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Поскольку к моменту выполнения индивидуального проекта он-лайн этап с возможностью загрузки решения уже завершился и нет возможности проверяться на оригинальных тестовых данных, придётся сделать себе локальные трейн и тест из имеющегося трейна (поскольку считаем, что упорядоченности в данных нет, то для разумности и непредвзятости при формировании тестовых данных просто (1) сохраним от исходной выборки относительную долю теста от данных и (2) распределение таргета + (3) рандомизируем)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_train_local, df_test_local, y_train_local, y_test_local = \\\n", " train_test_split(df[df['y'].notna()].drop(columns=['y']),\n", " df[df['y'].notna()]['y'],\n", " test_size=df_test.shape[0] / (df_train.shape[0] + df_test.shape[0]),\n", " stratify=df[df['y'].notna()]['y'],\n", " random_state=RANDOM_SEED)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Авторы датасета говорят нам, что:\n", "*Рейтинг строится на 30% тестового датасета, финальный рейтинг будет построен на 70% тестового датасета и может отличаться.*\n", "\n", "Участники топ-6 мест в публичном и финальном рейтинге полностью не совпадают т.е. явно имеет место переобучение на лидерборде.\n", "\n", "Хорошо, давайте тоже сделаем себе локальные public и private тестовые датасеты где и будем оценивать наше переобучение:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_test_local_public, df_test_local_private, y_train_local_public, y_test_local_private = \\\n", " train_test_split(df_test_local,\n", " y_test_local,\n", " test_size=0.7,\n", " stratify=y_test_local,\n", " random_state=RANDOM_SEED)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "# проверим что получилось\n", "df_train_local.shape, df_test_local.shape, y_train_local.shape, y_test_local.shape" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_test_local_public.shape, df_test_local_private.shape, y_train_local_public.shape, y_test_local_private.shape" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 9 (в оригинале 8). Кросс-валидация и настройка гиперпараметров модели" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "*Примечание: я поменял местами 8 и 9 т.к. подбираю параметры модели ПОСЛЕ того, как генерю новые признаки*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "С помощью неглубокого, пракически дефолтного, бустинга выявим наиболее полезные фичи:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "params = {\n", " 'n_estimators': 75,\n", " 'subsample': 0.5,\n", " 'scale_pos_weight': y[y == 0].size / y[y == 1].size,\n", " 'base_score': y_train_local.mean(),\n", " 'random_state': RANDOM_SEED\n", "}\n", "clf = xgb.XGBClassifier(**params)\n", "clf.fit(df_train_local, y_train_local)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим на важность фич:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sns.set(rc={'figure.figsize':(20,40)})\n", "xgb.plot_importance(clf);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Достанем важность фич чтобы с экрана не перепечатывать ))" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ft_weights = pd.DataFrame(clf.feature_importances_, columns=['weights'], index=df.columns[1:])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "plot_importance говорит нам в том числе и про минимальные значимые фичи - посмотрим, какое у них значение значимости, и посмотрим сколько вообще значимых фич" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ft_weights.loc['k10']" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ft_weights[ft_weights['weights'] >= 0.001930].size" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ft_weights.sort_values(by='weights', ascending=False).iloc[:160]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Фичи с важностью ниже 0.001931 уже не нужны. Возьмём дальше с нами всех тех, кто больше:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "imp_cols = ft_weights[ft_weights['weights'] >= 0.001930].index" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "imp_cols" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Итого у нас получилось 150 фич, важных для модели.\n", "Посмотрим, сколько там тех, которые мы увидели на визуальном анализе:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ft_weights.loc[np.intersect1d(target_diff_features, imp_cols)].sort_values(by='weights', ascending=False)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выводы нашего визуального анализа подтверждаются: 6 фич, увиденных глазами, - среди важных с точки зрения модели.\n", "Также эти фичи как раз находятся в двух регионах, увиденных нам ранее на хитмэпе при анализе данных: положительно разно-коррелированные и пёстрые квадраты." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Подход к кросс-валидации следующий: в предположении, что нужные фичи мы уже нагенерили, далее ключевыми параметрами нашего упражнения будут бустер, количество фич, learning_rate, max_depth, n_estimators, subsample. Другие параметры менее важны чем эти - поэтому сначала настроимся тут." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Поскольку выборка плоховато сбалансированная - сразу поставим поставим scale_pos_weight заинициализируем классификатор средним по таргету.\n", "\n", "Дока по XGBoost говорит нам: \"*If you care only about the ranking order (AUC) of your prediction -\n", "balance the positive and negative weights via scale_pos_weight and use AUC for evaluation*\" - да, мы боремся только за AUC-метрику, будем ставить scale_pos_weight. Дока рекомендует \"*...typical value to consider: sum(negative cases) / sum(positive cases)*\" - с него и начнём." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Дока по XGBoost говорит нам: \"*kwargs is unsupported by Sklearn. We do not guarantee that parameters passed via this argument will interact properly with Sklearn.*\" - ок, услышали, но попробуем т.к. уж очень наглядно и лаконично получается." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "params = {\n", " 'booster': 'gbtree',\n", " \n", " # параметры деревянного бустера\n", " 'max_depth': 5,\n", " 'learning_rate': 0.1,\n", " 'n_estimators': 75,\n", " 'subsample': 1,\n", " 'scale_pos_weight': y[y == 0].size / y[y == 1].size,\n", " \n", " # параметры обучения\n", " 'objective': 'binary:logistic',\n", " 'base_score': y_train_local.mean(),\n", " 'eval_metric': 'auc',\n", " 'random_state': RANDOM_SEED,\n", " \n", "}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "param_grid = [{\n", " #'learning_rate': [0.005, 0.01, 0.05, 0.1, 0.2, 0.4, 1], # 1-ый поиск, наметили 0.1 с 0.734137473946\n", " #'max_depth': [4, 5, 6, 7], # 2-ой поиск, наметили 5 с 0.73388072958\n", " #'n_estimators': np.linspace(50, 200, 7).astype('int'), # 3-ий поиск, наметили 75 с 0.734311200295\n", " 'subsample': [0.5, 0.75, 1.0] # 4-ый поиск, наметили 1 c 0.734311200295\n", "}]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Будем проводить 5-кратную стратифицированную кросс-валидацию т.к. рекомендуемую индустрией 10-кратную на ноуте долго считать даже на нашем небольшом датасете" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=RANDOM_SEED)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "clf = xgb.XGBClassifier(**params)\n", "gs = GridSearchCV(estimator=clf, param_grid=param_grid, scoring='roc_auc', cv=cv)\n", "gs=gs.fit(df_train_local[imp_cols], y_train_local)\n", "print(gs.best_score_)\n", "print(gs.best_params_)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 10. Построение кривых валидации и обучения" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Тут хорошо бы сделать вот что:\n", "*Построены кривые валидации и обучения. Дана верная интерпретация*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим с помощью кривых обучения как ведёт себя модель с точки зрения смещения и дисперсии:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "params = {\n", " 'booster': 'gbtree',\n", " 'max_depth': 5,\n", " 'learning_rate': 0.1,\n", " 'n_estimators': 75,\n", " 'subsample': 1,\n", " 'scale_pos_weight': y[y == 0].size / y[y == 1].size,\n", " 'objective': 'binary:logistic',\n", " 'base_score': y_train_local.mean(),\n", " 'eval_metric': 'auc',\n", " 'random_state': RANDOM_SEED,\n", "}\n", "\n", "train_sizes, train_scores, test_scores = learning_curve(estimator=xgb.XGBClassifier(**params),\n", " X=df_train_local[imp_cols],\n", " y=y_train_local,\n", " train_sizes=[0.25, 0.5, 0.75, 1.0],\n", " cv=cv,\n", " scoring='roc_auc',\n", " shuffle=True,\n", " random_state=RANDOM_SEED)\n", "\n", "train_scores_mean = np.mean(train_scores, axis=1)\n", "train_scores_std = np.std(train_scores, axis=1)\n", "test_scores_mean = np.mean(test_scores, axis=1)\n", "test_scores_std = np.std(test_scores, axis=1)\n", "\n", "fig = plt.figure(figsize=(20, 5))\n", "\n", "plt.xlabel('train_sizes')\n", "plt.ylabel('roc_auc')\n", "plt.ylim(0.5, 1.01)\n", "\n", "plt.plot(train_sizes,\n", " train_scores_mean,\n", " label=\"Training score\",\n", " color=\"b\", marker='o')\n", "\n", "plt.plot(train_sizes,\n", " test_scores_mean, \n", " label=\"CV score\",\n", " color=\"g\", marker='s')\n", "\n", "plt.fill_between(train_sizes, \n", " train_scores_mean - train_scores_std,\n", " train_scores_mean + train_scores_std, \n", " alpha=0.2, color=\"b\")\n", "\n", "plt.fill_between(train_sizes,\n", " test_scores_mean - test_scores_std,\n", " test_scores_mean + test_scores_std,\n", " alpha=0.2, color=\"g\")\n", "\n", "plt.axhline(y=0.75869667, color='r', ls='dotted')\n", "plt.axhline(y=1.0, color='k', ls='dashed')\n", "\n", "plt.legend(loc=\"best\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Целевое значение взято от лучшего официального результата соревнования 0.75869667. Это наводит на мысли о том, что маловато фичей в данных чтобы определить таргет - хотя, конечно, надо топить за 1.0.\n", "\n", "Видим, что наша такая быстро собранная модель обладает достаточно высокой дисперсией: большой разрыв между верностью на тренировочном наборе и точностью на кросс-валидации - подумаем что нам может помочь.\n", "Вариант найти побольше тренировочных данных нам не подходит: данных просто больше нет. Вариант нагенерить побольше токовых признаков, конечно, классный, и всегда должен в первую очередь использоваться, но в моменте неприменимый: делаем учебную задачу, а не гоняем за корову. \n", "\n", "Попробуем уменьшить сложность модели:\n", "- снизим количество признаков\n", "- уменьшим максимальную глубину деревьев\n", "- ещё увеличим min_child_weight\n", "- добавим больше \"случайности\" за счет subsample" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ft_weights[ft_weights['weights'] >= 0.005].size" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "params = {\n", " 'booster': 'gbtree',\n", " 'max_depth': 3, # изменено с 5\n", " 'learning_rate': 0.1,\n", " 'n_estimators': 75,\n", " 'subsample': 0.5, # изменено с 1.0\n", " 'scale_pos_weight': 1.25 * y[y == 0].size / y[y == 1].size, # увеличено на 25%\n", " 'objective': 'binary:logistic',\n", " 'base_score': y_train_local.mean(),\n", " 'eval_metric': 'auc',\n", " 'random_state': RANDOM_SEED,\n", "}\n", "\n", "imp_cols_subset = ft_weights[ft_weights['weights'] >= 0.005].index # в два раза сокращено количество фич\n", "\n", "train_sizes, train_scores, test_scores = learning_curve(estimator=xgb.XGBClassifier(**params),\n", " X=df_train_local[imp_cols_subset],\n", " y=y_train_local,\n", " train_sizes=[0.25, 0.5, 0.75, 1.0],\n", " cv=cv,\n", " scoring='roc_auc',\n", " shuffle=True,\n", " random_state=RANDOM_SEED)\n", "\n", "train_scores_mean = np.mean(train_scores, axis=1)\n", "train_scores_std = np.std(train_scores, axis=1)\n", "test_scores_mean = np.mean(test_scores, axis=1)\n", "test_scores_std = np.std(test_scores, axis=1)\n", "\n", "fig = plt.figure(figsize=(20, 5))\n", "\n", "plt.xlabel('train_sizes')\n", "plt.ylabel('roc_auc')\n", "plt.ylim(0.5, 1.01)\n", "\n", "plt.plot(train_sizes,\n", " train_scores_mean,\n", " label=\"Training score\",\n", " color=\"b\", marker='o')\n", "\n", "plt.plot(train_sizes,\n", " test_scores_mean, \n", " label=\"CV score\",\n", " color=\"g\", marker='s')\n", "\n", "plt.fill_between(train_sizes, \n", " train_scores_mean - train_scores_std,\n", " train_scores_mean + train_scores_std, \n", " alpha=0.2, color=\"b\")\n", "\n", "plt.fill_between(train_sizes,\n", " test_scores_mean - test_scores_std,\n", " test_scores_mean + test_scores_std,\n", " alpha=0.2, color=\"g\")\n", "\n", "plt.axhline(y=0.75869667, color='r', ls='dotted')\n", "plt.axhline(y=1.0, color='k', ls='dashed')\n", "\n", "plt.legend(loc=\"best\")\n", "plt.show()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видим, что наши методы борьбы с дисперсией помогли: модель теперь ведёт себя намного более единообразно на тренировочных данных и кросс-валидации, чем ещё шаг назад." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Повыбираем значение для количества деревьев с помощью проверочных кривых:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "params = {\n", " 'booster': 'gbtree',\n", " 'max_depth': 3,\n", " 'learning_rate': 0.1, \n", " 'subsample': 0.5,\n", " 'scale_pos_weight': 1.25 * y[y == 0].size / y[y == 1].size,\n", " 'objective': 'binary:logistic',\n", " 'base_score': y_train_local.mean(),\n", " 'eval_metric': 'auc',\n", " 'random_state': RANDOM_SEED,\n", "}\n", "\n", "n_estimators_range = np.linspace(1, 200, 5).astype('int')\n", "\n", "train_scores, test_scores = validation_curve(\n", " xgb.XGBClassifier(**params),\n", " df_train_local[imp_cols_subset], y_train_local,\n", " param_name = 'n_estimators',\n", " param_range = n_estimators_range,\n", " cv=cv,\n", " scoring='roc_auc'\n", ")\n", "\n", "train_scores_mean = np.mean(train_scores, axis=1)\n", "train_scores_std = np.std(train_scores, axis=1)\n", "test_scores_mean = np.mean(test_scores, axis=1)\n", "test_scores_std = np.std(test_scores, axis=1)\n", "\n", "fig = plt.figure(figsize=(20, 5))\n", "\n", "plt.xlabel('n_estimators')\n", "plt.ylabel('roc_auc')\n", "plt.ylim(0.5, 1.01)\n", "\n", "plt.plot(n_estimators_range,\n", " train_scores_mean,\n", " label=\"Training score\",\n", " color=\"b\", marker='o')\n", "\n", "plt.plot(n_estimators_range,\n", " test_scores_mean, \n", " label=\"CV score\",\n", " color=\"g\", marker='s')\n", "\n", "plt.fill_between(n_estimators_range, \n", " train_scores_mean - train_scores_std,\n", " train_scores_mean + train_scores_std, \n", " alpha=0.2, color=\"b\")\n", "\n", "plt.fill_between(n_estimators_range,\n", " test_scores_mean - test_scores_std,\n", " test_scores_mean + test_scores_std,\n", " alpha=0.2, color=\"g\")\n", "\n", "plt.axhline(y=1, color='k', ls='dashed')\n", "plt.axhline(y=0.75869667, color='r', ls='dotted')\n", "\n", "plt.legend(loc=\"best\")\n", "plt.show()\n", "\n", "i = np.argmax(test_scores_mean)\n", "print(\"Best cross-validation result ({0:.2f}) obtained for {1} trees\".format(test_scores_mean[i], \n", " n_estimators_range[i]))" ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Видим, что:\n", "- у модели разброс ошибки быстро падает и становится совсем небольшим - модель стабилизируется,\n", "- доля правильных ответов на обучении продолжает расти, хотя на валидации замирает,\n", "- даже с найденным хорошим значением количества деревьев до хорошего результата ROC-AUC ещё грести и грести. \n", "\n", "И похоже, с моделью всё норм и мы просто не сгенерили хорошие фичи для повышения точности." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 11. Прогноз для тестовой или отложенной выборки" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Тут хорошо бы сделать вот что:\n", "*Указаны результаты на тестовой выборке или LB score. Результаты на тестовой выборке сравнимы с результатами на кросс-валидации. Если тестовая выборка создавалась автором проекта, то механизм создания должен быть непредвзят и объяснен (применен разумный механизм выборки, в простейшем случае – рандомизация)*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сначала обучимся на обработанной полной локальной трейновой выборке с подобранными параметрами:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "%%time\n", "\n", "params = {\n", " 'booster': 'gbtree',\n", " 'max_depth': 3,\n", " 'learning_rate': 0.1, \n", " 'n_estimators': 150,\n", " 'scale_pos_weight': 1.25 * y[y == 0].size / y[y == 1].size,\n", " 'objective': 'binary:logistic',\n", " 'base_score': y_train_local.mean(),\n", " 'eval_metric': 'auc',\n", " 'random_state': RANDOM_SEED,\n", "}\n", "\n", "clf = xgb.XGBClassifier(**params).fit(df_train_local[imp_cols_subset], y_train_local)\n", "\n", "print('ROC AUC on local train: {:1.5f}'.format(roc_auc_score(y_true=y_train_local,\n", " y_score=clf.predict(df_train_local[imp_cols_subset]))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Теперь прогнозируем для обработанной полной локальной тестовой выборки и смотрим, сколько же по метрике задачи у нас получилось:" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print('ROC AUC on local test: {:1.5f}'.format(roc_auc_score(y_true=y_test_local,\n", " y_score=clf.predict(df_test_local[imp_cols_subset]))))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Результат на отложенном тесте достаточно достаточно близок к результату на кросс-валидации - считаем, что построили умеренно смещённую и умеренно переобученную модель. А дальше - к выводам!" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 11. Выводы " ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Тут хорошо бы сделать вот что:\n", "*Описана ценность решения, возможности применения, дальнейшие пути развития и улучшения решения*" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Главный результат упражнения (помимо овладевания ящиком инструментов машинного обучения) - это ещё раз подчёркнутая важность извлечения признаков из предметной области: можно бегать вокруг с моделями, типовыми генерируемыми признаками, подбором параметров - но всё будет тщетным пока не влез в данные и не начал в них жить до уровня чуйки на закономерности.\n", "\n", "Применение этой модели, впрочем как и моделей победителей соревнований, под вопросом: 0.75 ROC-AUC лучшей модели относительно 0.5 случайного гадания - это слабовато для продакшна, когда нужно будет адресовать или не адресовать клиента с предложением и, соответственно, тратить маркетиновый бюджет. Хотя, это уже лучше, чем совсем 50 на 50.\n", "\n", "Дальнейшие шаги - крутить признаки, строить их взаимные поведения, прогнозировать сами признаки на основе других признаков, объединять разнотипные алгоритмы, которые будут принимать во внимание разные признаки для одного прогноза.\n", "\n", "Но это уже совсем другая история..." ] } ], "metadata": { "kernelspec": { "display_name": "Python 3", "language": "python", "name": "python3" }, "language_info": { "codemirror_mode": { "name": "ipython", "version": 3 }, "file_extension": ".py", "mimetype": "text/x-python", "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", "version": "3.6.3" } }, "nbformat": 4, "nbformat_minor": 1 }