{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "#
Тема 3. Линейные модели классификации и регрессии\n", "##
Практика. Идентификация пользователя с помощью логистической регрессии\n", "\n", "Тут мы воспроизведем парочку бенчмарков нашего соревнования и вдохновимся побить третий бенчмарк, а также остальных участников. Вопросов не будет, ориентир – [leaderboard](https://www.kaggle.com/c/catch-me-if-you-can-intruder-detection-through-webpage-session-tracking2/leaderboard) соревнования." ] }, { "cell_type": "code", "execution_count": 1, "metadata": {}, "outputs": [], "source": [ "import pickle\n", "import numpy as np\n", "import pandas as pd\n", "from tqdm import tqdm_notebook\n", "from scipy.sparse import csr_matrix, hstack\n", "from sklearn.preprocessing import StandardScaler\n", "from sklearn.metrics import roc_auc_score\n", "from sklearn.linear_model import LogisticRegression, LogisticRegressionCV\n", "%matplotlib inline\n", "from matplotlib import pyplot as plt\n", "import seaborn as sns\n", "from sklearn.model_selection import train_test_split" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Загрузка и преобразование данных\n", "Зарегистрируйтесь на [Kaggle](www.kaggle.com), если вы не сделали этого раньше. Первым делом загрузим обучающую и тестовую выборки и посмотрим на данные." ] }, { "cell_type": "code", "execution_count": 2, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
site1time1site2time2site3time3site4time4site5time5...time6site7time7site8time8site9time9site10time10target
session_id
21669562013-01-12 08:05:5755.02013-01-12 08:05:57NaNNaTNaNNaTNaNNaT...NaTNaNNaTNaNNaTNaNNaTNaNNaT0
54843562013-01-12 08:37:2355.02013-01-12 08:37:2356.02013-01-12 09:07:0755.02013-01-12 09:07:09NaNNaT...NaTNaNNaTNaNNaTNaNNaTNaNNaT0
772929462013-01-12 08:50:13946.02013-01-12 08:50:14951.02013-01-12 08:50:15946.02013-01-12 08:50:15946.02013-01-12 08:50:16...2013-01-12 08:50:16948.02013-01-12 08:50:16784.02013-01-12 08:50:16949.02013-01-12 08:50:17946.02013-01-12 08:50:170
\n", "

3 rows × 21 columns

\n", "
" ], "text/plain": [ " site1 time1 site2 time2 site3 \\\n", "session_id \n", "21669 56 2013-01-12 08:05:57 55.0 2013-01-12 08:05:57 NaN \n", "54843 56 2013-01-12 08:37:23 55.0 2013-01-12 08:37:23 56.0 \n", "77292 946 2013-01-12 08:50:13 946.0 2013-01-12 08:50:14 951.0 \n", "\n", " time3 site4 time4 site5 \\\n", "session_id \n", "21669 NaT NaN NaT NaN \n", "54843 2013-01-12 09:07:07 55.0 2013-01-12 09:07:09 NaN \n", "77292 2013-01-12 08:50:15 946.0 2013-01-12 08:50:15 946.0 \n", "\n", " time5 ... time6 site7 \\\n", "session_id ... \n", "21669 NaT ... NaT NaN \n", "54843 NaT ... NaT NaN \n", "77292 2013-01-12 08:50:16 ... 2013-01-12 08:50:16 948.0 \n", "\n", " time7 site8 time8 site9 \\\n", "session_id \n", "21669 NaT NaN NaT NaN \n", "54843 NaT NaN NaT NaN \n", "77292 2013-01-12 08:50:16 784.0 2013-01-12 08:50:16 949.0 \n", "\n", " time9 site10 time10 target \n", "session_id \n", "21669 NaT NaN NaT 0 \n", "54843 NaT NaN NaT 0 \n", "77292 2013-01-12 08:50:17 946.0 2013-01-12 08:50:17 0 \n", "\n", "[3 rows x 21 columns]" ] }, "execution_count": 2, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# загрузим обучающую и тестовую выборки\n", "train_df = pd.read_csv('data/train_sessions.csv', index_col='session_id')\n", "test_df = pd.read_csv('data/test_sessions.csv', index_col='session_id')\n", "\n", "# приведем колонки time1, ..., time10 к временному формату\n", "times = ['time%s' % i for i in range(1, 11)]\n", "train_df[times] = train_df[times].apply(pd.to_datetime)\n", "test_df[times] = test_df[times].apply(pd.to_datetime)\n", "\n", "# отсортируем данные по времени\n", "train_df = train_df.sort_values(by='time1')\n", "\n", "# посмотрим на заголовок обучающей выборки\n", "train_df.head(3)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "В обучающей выборке содержатся следующие признаки:\n", " - site1 – индекс первого посещенного сайта в сессии\n", " - time1 – время посещения первого сайта в сессии\n", " - ...\n", " - site10 – индекс 10-го посещенного сайта в сессии\n", " - time10 – время посещения 10-го сайта в сессии\n", " - target – целевая переменная, 1 для сессий Элис, 0 для сессий других пользователей\n", " \n", "Сессии пользователей выделены таким образом, что они не могут быть длиннее получаса или 10 сайтов. То есть сессия считается оконченной либо когда пользователь посетил 10 сайтов подряд либо когда сессия заняла по времени более 30 минут.\n", "\n", "В таблице встречаются пропущенные значения, это значит, что сессия состоит менее, чем из 10 сайтов. Заменим пропущенные значения нулями и приведем признаки к целому типу. Также загрузим словарь сайтов и посмотрим, как он выглядит:" ] }, { "cell_type": "code", "execution_count": 3, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "всего сайтов: 48371\n" ] }, { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
site
25075www.abmecatronique.com
13997groups.live.com
42436majeureliguefootball.wordpress.com
30911cdt46.media.tourinsoft.eu
8104www.hdwallpapers.eu
\n", "
" ], "text/plain": [ " site\n", "25075 www.abmecatronique.com\n", "13997 groups.live.com\n", "42436 majeureliguefootball.wordpress.com\n", "30911 cdt46.media.tourinsoft.eu\n", "8104 www.hdwallpapers.eu" ] }, "execution_count": 3, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# приведем колонки site1, ..., site10 к целочисленному формату и заменим пропуски нулями\n", "sites = ['site%s' % i for i in range(1, 11)]\n", "train_df[sites] = train_df[sites].fillna(0).astype('int')\n", "test_df[sites] = test_df[sites].fillna(0).astype('int')\n", "\n", "# загрузим словарик сайтов\n", "with open(r\"data/site_dic.pkl\", \"rb\") as input_file:\n", " site_dict = pickle.load(input_file)\n", "\n", "# датафрейм словарика сайтов\n", "sites_dict_df = pd.DataFrame(list(site_dict.keys()), index=list(site_dict.values()), columns=['site'])\n", "print(u'всего сайтов:', sites_dict_df.shape[0])\n", "sites_dict_df.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Выделим целевую переменную и объединим выборки, чтобы вместе привести их к разреженному формату." ] }, { "cell_type": "code", "execution_count": 4, "metadata": {}, "outputs": [], "source": [ "# наша целевая переменная\n", "y_train = train_df['target']\n", "\n", "# объединенная таблица исходных данных\n", "full_df = pd.concat([train_df.drop('target', axis=1), test_df])\n", "\n", "# индекс, по которому будем отделять обучающую выборку от тестовой\n", "idx_split = train_df.shape[0]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Для самой первой модели будем использовать только посещенные сайты в сессии (но не будем обращать внимание на временные признаки). За таким выбором данных для модели стоит такая идея: *у Элис есть свои излюбленные сайты, и чем чаще вы видим эти сайты в сессии, тем выше вероятность, что это сессия Элис и наоборот.*\n", "\n", "Подготовим данные, из всей таблицы выберем только признаки `site1, site2, ... , site10`. Напомним, что пропущенные значения заменены нулем. Вот как выглядят первые строки таблицы:" ] }, { "cell_type": "code", "execution_count": 5, "metadata": {}, "outputs": [ { "data": { "text/html": [ "
\n", "\n", "\n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", " \n", "
site1site2site3site4site5site6site7site8site9site10
session_id
21669565500000000
5484356555655000000
77292946946951946946945948784949946
114021945948949948945946947945946946
146670947950948947950952946951946947
\n", "
" ], "text/plain": [ " site1 site2 site3 site4 site5 site6 site7 site8 site9 \\\n", "session_id \n", "21669 56 55 0 0 0 0 0 0 0 \n", "54843 56 55 56 55 0 0 0 0 0 \n", "77292 946 946 951 946 946 945 948 784 949 \n", "114021 945 948 949 948 945 946 947 945 946 \n", "146670 947 950 948 947 950 952 946 951 946 \n", "\n", " site10 \n", "session_id \n", "21669 0 \n", "54843 0 \n", "77292 946 \n", "114021 946 \n", "146670 947 " ] }, "execution_count": 5, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# табличка с индексами посещенных сайтов в сессии\n", "full_sites = full_df[sites]\n", "full_sites.head()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Сессии представляют собой последовательность индексов сайтов и данные в таком виде неудобны для линейных методов. В соответствии с нашей гипотезой (у Элис есть излюбленные сайты) надо преобразовать эту таблицу таким образом, чтобы каждому возможному сайту соответствовал свой отдельный признак (колонка), а его значение равнялось бы количеству посещений этого сайта в сессии. Это делается в две строчки:" ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [], "source": [ "from scipy.sparse import csr_matrix" ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [], "source": [ "csr_matrix?" ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [], "source": [ "# последовательность с индексами\n", "sites_flatten = full_sites.values.flatten()\n", "\n", "# искомая матрица\n", "full_sites_sparse = csr_matrix(([1] * sites_flatten.shape[0], sites_flatten,\n", " range(0, sites_flatten.shape[0] + 10, 10)))[:, 1:]" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Еще один плюс использования разреженных матриц в том, что для них имеются специальные реализации как матричных операций, так и алгоритмов машинного обучения, что подчас позволяет ощутимо ускорить операции за счет особенностей структуры данных. Это касается и логистической регрессии. Вот теперь у нас все готово для построения нашей первой модели.\n", "\n", "## Задание №1. Построение первой модели\n", "Итак, у нас есть алгоритм и данные для него, построим нашу первую модель, воспользовавшись релизацией [логистической регрессии](http://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html) из пакета `sklearn` с параметрами по умолчанию. Первые 90% данных будем использовать для обучения (обучающая выборка отсортирована по времени), а оставшиеся 10% для проверки качества (validation). \n", "\n", "**Напишите простую функцию, которая будет возвращать качество модели на отложенной выборке, и обучите наш первый классификатор**." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [], "source": [ "def get_auc_lr_valid(X, y, C=1.0, ratio = 0.9, seed=17):\n", " '''\n", " X, y – выборка\n", " ratio – в каком отношении поделить выборку\n", " C, seed – коэф-т регуляризации и random_state логистической регрессии\n", " '''\n", " train_X, test_X, train_y, test_y = train_test_split(X, y, test_size=(1-ratio), random_state=seed, stratify=y)\n", " model = LogisticRegression(random_state = seed, C = C, max_iter=1_000_000).fit(train_X, train_y) \n", " return roc_auc_score(test_y, model.predict_proba(test_X)[:, 1])" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Посмотрите, какой получился ROC AUC на отложенной выборке.**" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.9715402278166047" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "get_auc_lr_valid(full_sites_sparse[:idx_split], y_train)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Будем считать эту модель нашей первой отправной точкой (baseline). Для построения модели для прогноза на тестовой выборке **необходимо обучить модель заново уже на всей обучающей выборке** (пока наша модель обучалась лишь на части данных), что повысит ее обобщающую способность:" ] }, { "cell_type": "code", "execution_count": 11, "metadata": {}, "outputs": [], "source": [ "# функция для записи прогнозов в файл\n", "def write_to_submission_file(predicted_labels, out_file,target='target', index_label=\"session_id\"):\n", " predicted_df = pd.DataFrame(predicted_labels,index = np.arange(1, predicted_labels.shape[0] + 1),columns=[target])\n", " predicted_df.to_csv(out_file, index_label=index_label)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Обучите модель на всей выборке, сделайте прогноз для тестовой выборки и сделайте посылку в соревновании**." ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [], "source": [ "model = LogisticRegression(random_state = 17, C = 1.0, max_iter=1_000_000).fit(full_sites_sparse[:idx_split], y_train)\n", "prediction = model.predict_proba(full_sites_sparse[idx_split:])[:, 1] # Вероятностная оценка\n", "write_to_submission_file(prediction, 'submit1.csv')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Если вы выполните эти действия и загрузите ответ на [странице](https://inclass.kaggle.com/c/catch-me-if-you-can-intruder-detection-through-webpage-session-tracking2) соревнования, то воспроизведете первый бенчмарк \"Logit\".\n", "\n" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Score = 0.90734" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Задание №2. Улучшение модели, построение новых признаков" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Создайте такой признак, который будет представлять собой число вида ГГГГММ от той даты, когда проходила сессия, например 201407 -- 2014 год и 7 месяц. Таким образом, мы будем учитывать помесячный [линейный тренд](http://people.duke.edu/~rnau/411trend.htm) за весь период предоставленных данных." ] }, { "cell_type": "code", "execution_count": 13, "metadata": {}, "outputs": [], "source": [ "start_month = full_df['time1'].dt.strftime('%Y%M').astype(int)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Добавьте новый признак, предварительно отмасштабировав его с помощью `StandardScaler`, и снова посчитайте ROC AUC на отложенной выборке." ] }, { "cell_type": "code", "execution_count": 14, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.9724011586358688" ] }, "execution_count": 14, "metadata": {}, "output_type": "execute_result" } ], "source": [ "scaler = StandardScaler() # стандартизация функции\n", "s_start_month = scaler.fit_transform(start_month.values.reshape(-1,1))\n", "temp = hstack((full_sites_sparse, s_start_month), format='csr') # сложение массивов\n", "get_auc_lr_valid(temp[:idx_split], y_train)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**Добавьте два новых признака: start_hour и morning.**\n", "\n", "Признак `start_hour` – это час в который началась сессия (от 0 до 23), а бинарный признак `morning` равен 1, если сессия началась утром и 0, если сессия началась позже (будем считать, что утро это если `start_hour равен` 11 или меньше).\n", "\n", "**Посчитйте ROC AUC на отложенной выборке для выборки с:**\n", "- сайтами, `start_month` и `start_hour`\n", "- сайтами, `start_month` и `morning`\n", "- сайтами, `start_month`, `start_hour` и `morning`" ] }, { "cell_type": "code", "execution_count": 15, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "0.9810031128822105\n", "0.9822335578738269\n", "0.9830308986868448\n" ] } ], "source": [ "s_hour = StandardScaler()\n", "s_morn = StandardScaler()\n", "\n", "start_hour = full_df['time1'].dt.hour.values.reshape(-1,1)\n", "morning = (start_hour <= 11).astype(int).reshape(-1,1)\n", "start_hour = s_hour.fit_transform(start_hour)\n", "morning = s_morn.fit_transform(morning)\n", "\n", "print(get_auc_lr_valid(hstack((temp, start_hour), format='csr')[:idx_split], y_train))\n", "print(get_auc_lr_valid(hstack((temp, morning), format='csr')[:idx_split], y_train))\n", "print(get_auc_lr_valid(hstack((temp, start_hour, morning), format='csr')[:idx_split], y_train))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Задание№3. Подбор коэффицициента регуляризации\n", "\n", "Итак, мы ввели признаки, которые улучшают качество нашей модели по сравнению с первым бейслайном. Можем ли мы добиться большего значения метрики? После того, как мы сформировали обучающую и тестовую выборки, почти всегда имеет смысл подобрать оптимальные гиперпараметры -- характеристики модели, которые не изменяются во время обучения. Например, ранее вы проходили решающие деревья, глубина дерева это гиперпараметр, а признак, по которому происходит ветвление и его значение -- нет. В используемой нами логистической регрессии веса каждого признака изменяются и во время обучения находится их оптимальные значения, а коэффициент регуляризации остается постоянным. Это тот гиперпараметр, который мы сейчас будем оптимизировать.\n", "\n", "Посчитайте качество на отложенной выборке с коэффициентом регуляризации, который по умолчанию `C=1`:" ] }, { "cell_type": "code", "execution_count": 16, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "0.9830308986868448" ] }, "execution_count": 16, "metadata": {}, "output_type": "execute_result" } ], "source": [ "res_df = hstack((temp, start_hour, morning), format='csr')\n", "get_auc_lr_valid(res_df[:idx_split], y_train)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Постараемся побить этот результат за счет оптимизации коэффициента регуляризации. Возьмем набор возможных значений C и для каждого из них посчитаем значение метрики на отложенной выборке.\n", "\n", "Найдите `C` из `np.logspace(-3, 1, 10)`, при котором ROC AUC на отложенной выборке максимален. " ] }, { "cell_type": "code", "execution_count": 17, "metadata": {}, "outputs": [], "source": [ "clf = LogisticRegressionCV(Cs=list(np.logspace(-3,1,10)), cv=5, random_state=17, max_iter=1_000_000).fit(res_df[:idx_split], y_train)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Наконец, обучите модель с найденным оптимальным значением коэффициента регуляризации и с построенными признаками `start_hour`, `start_month` и `morning`. Если вы все сделали правильно и загрузите это решение, то повторите второй бенчмарк соревнования." ] }, { "cell_type": "code", "execution_count": 18, "metadata": {}, "outputs": [], "source": [ "prediction2 = clf.predict_proba(res_df[idx_split:])[:, 1]\n", "write_to_submission_file(prediction2, 'submit2.csv')" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Score = 0.92380" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ " Ссылки:" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "https://habrahabr.ru/company/ods/blog/323890/" ] } ], "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.8.5" } }, "nbformat": 4, "nbformat_minor": 4 }