{ "cells": [ { "cell_type": "markdown", "metadata": {}, "source": [ "\n", "##
Открытый курс по машинному обучению. Сессия № 3\n", "\n", "###
Автор материала: Александр Ничипоренко" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##
Индивидуальный проект по анализу данных
" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Данные лежат здесь: https://yadi.sk/d/mJbzt5pV3Uf5Zt" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "import numpy as np\n", "import pandas as pd\n", "import seaborn as sns\n", "from matplotlib import pyplot as plt\n", "from sklearn.preprocessing import OneHotEncoder, StandardScaler\n", "from sklearn.pipeline import make_pipeline\n", "from sklearn.model_selection import cross_val_score, TimeSeriesSplit, GridSearchCV\n", "from sklearn.metrics import accuracy_score,classification_report,f1_score,roc_auc_score,roc_curve,precision_recall_curve\n", "from sklearn.linear_model import LogisticRegression\n", "from sklearn.ensemble import RandomForestClassifier\n", "from lightgbm import LGBMClassifier as lgbmc\n", "from catboost import CatBoostClassifier as catc\n", "from xgboost import XGBClassifier as xgbc\n", "plt.rcParams['figure.figsize'] = (20,20)\n", "sns.set(style=\"darkgrid\");\n", "%matplotlib inline" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 1. Описание набора данных и признаков." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "Одной из главных целей для любой компании является удержание своих клиентов. В торговле успехом данного процесса является совершение повторных покупок клиентами в интервал времени, который характеризует потребление различных видов товаров:\n", "\n", "- Продукты: каждый день - еженедельно;\n", "- Хозтовары: каждые 2 недели - месяц;\n", "- Одежда: раз в три месяца - раз в полгода;\n", "- Крупная и дорогая электроника: раз в 1-2 года;\n", "- Автомобиль: раз в 3-5 лет.\n", "\n", "В данном проекте будут исследованы данные одного заказчика (менеджеров крупного интернет-гипермаркета, основным ассортиментом которого являются товары повседневного спроса) и построена модель, предсказывающая вероятность оттока клиента.\n", "\n", "Заказчик определил отток таким образом: клиент не сделает повторный заказ в течение трёх месяцев.\n", "Такая постановка обусловлена тем, что почти 80% клиентов делают свой повторный заказ в течение 3-х месяцев. Таким образом поставлена цель научиться определять 20% клиентов, которые этого не сделают. \n", "\n", "После этого, уже можно разрабатывать различные подходы к стимулированию данного пула клиентов к повторной сделке с помощью различных маркетинговых методов.\n", "\n", "Данные были получены от заказчика и сведены в один DataFrame. Посмотрим на них.\n", "\n" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df = pd.read_csv('data.csv',index_col='Client')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.head(10)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.shape" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.info()" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "for index,value in enumerate(df.columns):\n", " print (index,\":\",value)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видно у нас **273** столбца, целевая переменная - **target**, 271 - количественный и 1 категориальный признак(\"Y M\" = \"Год Месяц\").\n", "Каждая строка - описание клиента (история его покупок за текущий и предыдущие 6 месяцев) в месяц последней покупки.\n", "\n", "#### Теперь о количественных признаках.\n", "\n", "###### Сокращения:\n", "- **R** - Revenue - Выручка от продажи;\n", "- **S** - Strings - Кол-во строк - разных позиций (артикулов);\n", "- **O** - Orders - Кол-во заказов;\n", "- **Q** - Qnt - Кол-во штук;\n", "\n", "- **R_1 ... R_6, R_NOW**- Выручка по месяцам. NOW - месяц, соответствующий Y_M, _6 - предыдущий, _1 - 6 месяцев назад.\n", "- **Month**: от 1 до 12 (январь-декабрь).\n", "- **Y** или **N** в **R_Y_NOW, O_Y_NOW, R_N_NOW, R_N_NOW** - выручка/заказы в зависимости от способа оформления заказа. **Y** - через сайт, **N** - по телефону.\n", "\n", "- **Orders-1003 ... Orders-Other** - заказы за 7 месяцев (от _1 до _NOW) по выделенной группе товаров (70 \"топовых\" групп: 1003, ..., 931) или по остальным (Other). Аналогично и с выручкой и кол-ву штук.\n", "\n", "- **Other** - Кол-во групп товаров, купленных за 7 месяцев, входящих в группу \"Other\"." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 2. Первичный анализ признаков" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим количество пропусков в данных. Как видно их нет." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sum(df.isnull().sum())" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Посмотрим среднее количество \"отточных клиентов\" в наборе данных." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print ('% клиентов, склонных к оттоку:', round(df['target'].mean()*100,2))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Получается даже меньше 20%. Выборка не сбалансирована." ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "##### Посмотрим сколько у нас \"отточных\" клиентов ежемесячно." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "churn=pd.crosstab(index=df['Y_M'],columns=df['target'])\n", "churn['%']=round(churn[1]/churn[0]*100,2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "churn.T" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "churn_m=pd.crosstab(index=df['Month'],columns=df['target'])\n", "churn_m['%']=round(churn_m[1]/churn_m[0]*100,2)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "churn_m.T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Видна некоторая сезонность, в конце года \"отточных\" клиентов больше, летом - меньше. Осенью клиенты делают закупки активнее." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###### Числовых показателей у нас много. Будем рассматривать их небольшими группами.\n", "Для начала посмотрим на статистичекское описание ежемесячных показателей." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.drop(columns=['target','Month']).iloc[:,:29].describe().T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###### Можно заметить, что:\n", "\n", "- Масштабы признаков сильно различаются (наиболее сильно: выручка и заказы);\n", "- Есть отрицательные значения - это возвраты;\n", "- Статистики по предыдущим периодам сильно скошены (среднее больше медианы) из-за того, что много нулей и величины распределены не нормально;\n", "- Максимальные значения - очень сильно отличаются от средних, кто-то покупает покупает много товаров или дорогие товары;\n", "- Обычно клиенты делают один заказ в течение месяца;\n", "- В предыдущий месяц (_6) клиенты заказывают меньше, чем в другие предыдущие." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Посмотрим на значения показателей (выручка, заказы) по способу оформления заказа." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df.drop(columns=['target','Month']).iloc[:,29:29+14*2].describe().T" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Заметно, что преобладают заказы, оформленные через Интернет." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "#### Посмотрим разницу по показателям в зависимости от целевого признака." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ch_1=df[df['target']==1].drop(columns=['target','Month']).iloc[:,:29].describe().T" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ch_0=df[df['target']==0].drop(columns=['target','Month']).iloc[:,:29].describe().T" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "ch_0-ch_1" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видно, клиенты, которые нас интересуют - покупают меньше. В особенности, в предыдущие периоды." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "##### Посмотрим, какие товары заказывают и на какие товары тратят деньги наши клиенты." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "goods=pd.pivot_table(data=df,values=df.iloc[:,202:273],columns=df['target'],aggfunc=np.sum)\n", "goods['%_churn']=goods[1]/goods[0]*100\n", "goods.sort_values(by='%_churn',ascending=False).head(10)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видно, по разным категориям товаров доля затраченных денег отличается. Таким образом, если клиент потратил сумму на какую-то группу товаров, то вероятность его ухода как понижается, так и повышается в зависимости от этой группы." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "goods_ord=pd.pivot_table(data=df,values=df.iloc[:,60:131],columns=df['target'],aggfunc=np.sum)\n", "goods_ord['%_churn']=goods_ord[1]/goods_ord[0]*100\n", "goods_ord.sort_values(by='%_churn',ascending=False).head(10)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sns.heatmap(np.corrcoef(goods_ord['%_churn'],goods['%_churn']));" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видно, заказы и деньги по товарным категориям коррелируют." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "###### Посмотрим, сколько товарных групп из \"Other\" покупают разные клиенты." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "Other_0=df[df['target']==0]['Other'].describe()\n", "Other_1=df[df['target']==1]['Other'].describe()\n", "Other=pd.concat([Other_0,Other_1],axis=1,names=['Total','1'])\n", "Other.columns=['0','1']\n", "Other" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Как видно, уходящие клиенты покупают обычно в два раза меньше товаров из категории \"Другое\"." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 3. Первичный визуальный анализ признаков" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Визуализируем распределение целевого класса." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=[8, 5])\n", "sns.countplot(df['target']);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Далее будем исследовать распределения признаков в зависимости от значения **\"target\"**. Для скошенных влево распределений будем применять **log(1+x)** преобразование и отсекать экстремально большие значения (>95%-99% квантили)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=[20, 15])\n", "for i in range(1,8):\n", " plt.subplot(3, 3, i)\n", " sns.distplot(np.log1p(df[df['target']==1].iloc[:,i].apply(lambda x: 0 if x<0 else x)),kde=False,norm_hist=True,color='r',label='target: 1')\n", " sns.distplot(np.log1p(df[df['target']==0].iloc[:,i].apply(lambda x: 0 if x<0 else x)),kde=False,norm_hist=True,color='g',label='target: 0')\n", " plt.legend()\n", " plt.title('log1x')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "plt.figure(figsize=[20, 15])\n", "for i in range(1,8):\n", " plt.subplot(3, 3, i)\n", " sns.distplot(df[(df['target']==1) & (df.iloc[:,i]0 else 0) for i in range(0,6)]).sum(axis=0)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sns.factorplot(y='target',x='B_M',data=df_train,kind='bar');" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "df_train['TG_total']=np.array([df_train.iloc[:,i].apply(lambda x: 1 if x >0 else 0) for i in range(200,270)]).sum(axis=0)" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "sns.factorplot(y='target',x='TG_total',data=df_train,kind='bar',aspect=5);" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "По всем трём признакам видна закономерность - чем меньше значение, тем выше доля оттока.\n", "Проверим качество с добавлением этих признаков для xgboost." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "X_train = df_train.drop(columns='target')\n", "y_train = df_train['target']" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "xgcv = cross_val_score(xg,X_train,y_train,cv=tscv,scoring='roc_auc')" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "print ('xg_cv_score',np.mean(xgcv),\"+-\",np.std(xgcv))" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Качество не улучшилось. Настроим параметры на кросс-валидации." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "XG_params = {'n_estimators': [100,200,300,400,500],\n", " 'seed':[17],\n", " 'max_depth': [3,4,5,6,7,8],\n", " 'learning_rate': [0.01,0.05,0.1]}" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "xggs = GridSearchCV(xg,param_grid=XG_params,scoring='roc_auc',cv=tscv)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 10. Прогноз для тестовой или отложенной выборки" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "xg.fit(X_train,y_train)\n", "df_valid['Orders_total']=df_valid.iloc[:,15:22].sum(axis=1)\n", "df_valid['B_M']=np.array([df_valid.iloc[:,i].apply(lambda x: 1 if x >0 else 0) for i in range(0,6)]).sum(axis=0)\n", "df_valid['TG_total']=np.array([df_valid.iloc[:,i].apply(lambda x: 1 if x >0 else 0) for i in range(200,270)]).sum(axis=0)\n", "\n", "X_valid = df_valid.drop(columns='target')\n", "y_valid = df_valid['target']\n", "\n", "y_pred_proba=xg.predict_proba(X_valid)[:,1]" ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, "outputs": [], "source": [ "roc_auc_score(y_valid,y_pred_proba)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Результат получился выше, чем на валидации. Это связано с тем, что у нас была TimeSeriesValidation." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Часть 11. Выводы " ] }, { "cell_type": "markdown", "metadata": { "collapsed": true }, "source": [ "После исследования и построения модели можно сделать следюущие выводы:\n", " \n", "1)В принципе, получена модель с неплохим качеством, которая может помогать определять клиентов, которые не закупятся в ближайшие 3 месяца.\n", "\n", "2)Возможные пути улучшения модели - добавить признаки другого типа (взаимодействия с клиентами), покрутить признаки.\n", "\n", "3)Логистическая регрессия показала неплохие результаты, можно попробовать немного поднастроить её вместо построения многих деревьев.\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.4" } }, "nbformat": 4, "nbformat_minor": 1 }