Прогноз покупки страховки клиентами туроператора

736d69499eb2dc4f52949a58685728bc.png

be5447f00816000e2aa2bf956c6cb81c.jpgАвтор статьи: Alieva Natalie

Выпускница OTUS

Всем привет! В этом посте хочу рассказать о своем проекте, в котором я попыталась сделать прогноз покупки страховки клиентами туроператора методами ML, изученными на курсе Machine Learning. Basic от образовательного ресурса для IT-специалистов OTUS. 

Данные я брала с Kaggle:   https://www.kaggle.com/datasets/sellingstories/travel-company-insurance-prediction.

База данных показалась мне интересной по двум причинам:

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

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

Я подумала, что это неплохой шанс не только закрепить полученные знания, но и попробовать свои силы в написании статьи, после чего приступила к работе.  Язык разработки Python, среда разработки Jupiter Notebook. 

Используя первый датасет с целевым признаком, я обучила 10 моделей. Для каждой модели делала подбор гиперпараметров на кроссвалидации с помощью градиентного бустинга. Сравнивая метрики (f1-score, auc-roc-score), я выбрала оптимальную модель, в которую подала второй датасет без целевого признака для предсказания покупки страховки. 

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

Работа с датасетом №1 

Импортируем необходимые библиотеки:

import numpy as np
import numpy.linalg as la
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
import seaborn as sns
sns.set_style('darkgrid')

import plotly.express as px

from mpl_toolkits.mplot3d import Axes3D
from matplotlib import cm
from sklearn import datasets
from sklearn import preprocessing
from sklearn.preprocessing import StandardScaler
from sklearn.metrics import mean_squared_error, mean_absolute_error, r2_score

%matplotlib inline
np.random.seed(123)

import warnings
warnings.filterwarnings("ignore")

Загружаем датасет и делаем анализ. Датасет содержит 9 признаков, среди них есть категориальные. 

data = pd.read_csv('Travel_Company_Old_Clients.csv', sep = ';')
data.head()

d3f3bc4ddf9bd3e4336ddb8a95851c4c.png

Описание признаков

  1. Age — Age Of The Customer;

  2. Employment Type — The Sector In Which Customer Is Employed;

  3. GraduateOrNot — Whether The Customer Is College Graduate Or Not;

  4. AnnualIncome — The Yearly Income Of The Customer In Indian Rupees;

  5. FamilyMembers — Number Of Members In Customer’s Family;

  6. ChronicDisease — Whether The Customer Suffers From Any Major Disease Or Conditions Like Diabetes/High BP or Asthama, etc.;

  7. FrequentFlyer — Derived Data Based On Customer’s History Of Booking Air Tickets On Atleast 4 Different Instances In The Last 2 Years 2017–2019;

  8. EverTravelledAbroad — Has The Customer Ever Travelled To A Foreign Country;

  9. TravelInsurance (1st File Only) — Did The Customer Buy Travel Insurance Package During Introductory Offering.

Проверяем размер датасета: 682 строчки, 9 столбцов. 

data.shape

b0f9377f87af41b9f0ac828a36aed47b.png

Датасет содержит 4 категориальных признака. Нет пропущенных значений. 

data.info()

6b83d8a3cbb0b601fc94b92f9caabefb.png

data.isnull().sum()

39219f83bdee87c3bc831b994ccd1590.png

Датасет содержит 113 дубликатов. Удаляем дубликаты, после этого размер датасета становится 569 строк, количество столбцов без изменений.  

data.duplicated().sum()

de146b7a2b04200630d4cae72cd34504.png

data.drop_duplicates(inplace = True)
data = data.reset_index(drop = True)
data.info()

d26d0161f333077639dff99e42832389.png

Визуализируем признаки. 

data.hist(figsize=(20, 20))

d9e68216ffefb13bb6aed460c508988b.png670df2659fa7d82f44352647e93f3a76.pngbb1771529f9344e4574c690a26ec8e96.png

Более детально посмотрим на каждый признак и его детализацию по таргету. Датасет содержит клиентов в возрастном диапазоне от 25 до 35 лет. Наиболее частое значение 28 лет, в основном они не покупают страховку. 

sns.countplot(x = data['Age'], hue=data['TravelInsurance'])
# структура признака, выраженная в %
data['Age'].value_counts(normalize=True)*100

30f855f6307121043b06adfb30fd43df.png3b7e770939546af44bfd28bb6eeafb4c.png

84% клиентов имеют высшее образование, при этом как люди с образованием, так и без предпочитают обходиться без страховки. 

sns.countplot(x = data['GraduateOrNot'],hue=data['TravelInsurance'])
data['GraduateOrNot'].value_counts(normalize=True)*100

2b491473f6e014ca9fd067a3aad09c45.png

Страховку предпочитают покупать клиенты с более высоким уровнем дохода. 

sns.countplot(x = data['AnnualIncome'],hue=data['TravelInsurance'])
plt.xticks(rotation = 90)
plt.show()
data['AnnualIncome'].value_counts(normalize=True)*100

3a98161a4ad26a56f4554ab52da42b59.pngba0bce99d288ae697b5457f5b7045e4a.png

Наиболее часто состав семьи представляет от 3 до 5 человек. Не прослеживается сильной взаимосвязи между составом семьи и покупкой страховки. 

sns.countplot(x = data['FamilyMembers'],hue=data['TravelInsurance'])
data['FamilyMembers'].value_counts(normalize=True)*100

58280dfbfb87d29e85f700949d88dcaa.png

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

sns.countplot(x = data['ChronicDiseases'],hue=data['TravelInsurance'])
data['ChronicDiseases'].value_counts(normalize=True)*100

e76f5e82c3370943a5def8546d71ba9c.png

77% клиентов не пользуются услугами авиакомпаний. Большинство тех, кто путешествует на самолете, покупают страховку. 

sns.countplot(x = data['FrequentFlyer'],hue=data['TravelInsurance'])
data['FrequentFlyer'].value_counts(normalize=True)*100

bdb1e0ecfbd513c8e1b5b901f8cb47a9.png

80% клиентов совершают поездки внутри страны. Подавляющее большинство тех, кто путешествует заграницу, покупают страховку. 

sns.countplot(x = data['EverTravelledAbroad'],hue=data['TravelInsurance'])
data['EverTravelledAbroad'].value_counts(normalize=True)*100

db54d6b2b76a46f8c7c8f5accbcffea2.png

73% клиентов работают в негосударственном секторе. Они чаще покупают страховку, чем клиенты, работающие в госкорпорациях. 

sns.countplot(x = data['Employment Type'],hue=data['TravelInsurance'])
data['Employment Type'].value_counts(normalize=True)*100

436c03a4a62e2d22b29d324858c3827c.png

Покупка страховки — целевой признак. 64% клиентов страховку не покупают.  

sns.countplot(x = data['TravelInsurance'],hue=data['TravelInsurance'])
data['TravelInsurance'].value_counts(normalize=True)*100

ced6c58d8bcc077bf2a26788b72b0584.png

Также мне нравится анализировать признаки с помощью plotly. В частности, можно сразу посмотреть распределение признака, частоту значений, квартили, наличие/отсутствие выбросов. Приведу для примера несколько графиков. Видно, что признаки из датасета не содержат выбросы, значит датасет не нуждается в дополнительной работе над аномалиями. 

fig = px.histogram(data, x='Age', color='TravelInsurance', marginal = 'box')
fig.show() 

21ccb099d26dc33e4635abd7a7f505f6.png

fig = px.histogram(data, x='AnnualIncome', color='TravelInsurance', marginal = 'box')
fig.show()

af3017bfbc533456e1634beac1467e27.png

fig = px.histogram(data, x='FamilyMembers', color='TravelInsurance', marginal = 'box')
fig.show()

5bad147fe69120497ef8eb93740fb499.png

Иногда бывают полезны графики, построенные с помощью seaborn. Они позволяют отразить сразу несколько признаков. Также приведу пару примеров построения. 

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

sns.relplot(data=data, x='AnnualIncome', y='Age', hue='TravelInsurance', col='EverTravelledAbroad', palette='bright', height=4)

ee98a3684a4e57d207797317d2f38bc3.png

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

sns.relplot(data=data, x='AnnualIncome', y='Age', hue='TravelInsurance', col='ChronicDiseases', palette='bright', height=4)

9ba63b3f1dcf820ee28f3f4204e99078.png

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

А сейчас поработаем с категориальными признаками. Сначала проверим вариабельность значений каждого категориального признака, чтобы определиться со способом перекодировки. Видим, что первые три признака содержат варианты Yes/No, значит их можно перекодировать в бинарный формат 1/0. 

Признак Employment Type также содержит два уникальных значения, но частота вхождения этих значений сильно отличается: 149 значений Government Sector и 420 значений Private Sector/Self Employed. Если мы просто перекодируем по частоте вхождения, то из-за несбалансированности значений, модель может неверно интерпретировать и придать более частому значению больший вес. Чтобы избежать этой неверной интерпретации моделью перекодируем последний признак с помощью One Hot Encoding. 

data['GraduateOrNot'].unique()

00220a77a0972cc9ca6de923336f2183.png

data['FrequentFlyer'].unique()

9d652afc7904e907a1025d532b0d68e8.png

data['EverTravelledAbroad'].unique()

55e9d408239e9bb6e4a503a9854fd61c.png

data['Employment Type'].unique()
print(data['Employment Type']. value_counts ()['Government Sector'])
print(data['Employment Type']. value_counts ()['Private Sector/Self Employed'])

f0bfe58dc27a309fd42da6f4927c1cb3.png59ece9d71d19d4006c76010509560981.png

data['GraduateOrNot'] = data['GraduateOrNot'].apply(lambda x: 1 if x=='Yes' else 0)
data['FrequentFlyer'] = data['FrequentFlyer'].apply(lambda x: 1 if x=='Yes' else 0)
data['EverTravelledAbroad'] = data['EverTravelledAbroad'].apply(lambda x: 1 if x=='Yes' else 0)

Используем One Hot Encoding для признака Employment Type. После этого проверим результат всех перекодировок с помощью head().

from category_encoders import OneHotEncoder

enc = OneHotEncoder()
enc.fit_transform(data[['Employment Type']]).head()
data = data.drop(['Employment Type'], axis = 1).join(enc.fit_transform(data[['Employment Type']], axis = 0))
data.head()

0f414306d99528a6b4f9ba145f0417f6.png

Проверим статистические показатели и взаимосвязь признаков.  Как видим из матрицы корреляций и тепловой карты, признаки коррелируют очень слабо. Есть единичные признаки с условно средним уровнем корреляции. 

data.describe()

55647490c474acff1d3653d5d20a25d9.png

data.corr()

46203fc755aa9e7180ca9fed7a573146.png

plt.subplots(figsize=(10,7))
sns.heatmap(data.corr(), cbar=True, annot=True, square=True, fmt='.2f', annot_kws={'size': 10},\
            cmap=sns.color_palette("coolwarm", 10000), vmin=-1, center=0)
plt.show()

479b3bc1aaa2c8b8e1ea1f1adf5b1fd8.png

Датасет почти готов к работе с моделью.  Разобьем его на X и у. 

X = data.copy()
X.drop(['TravelInsurance'], axis=1, inplace=True)

y = data['TravelInsurance']

Нормализуем данные и проверим размер выборок train, test

-----------------------------------------------------------------------------------------------------------------------------------------

from sklearn.model_selection import train_test_split
from sklearn.preprocessing import StandardScaler

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.2, random_state = 42)

scaler = StandardScaler()
X_train = scaler.fit_transform(X_train)
X_test = scaler.transform(X_test)
X_train.shape, X_test.shape, y_train.shape, y_test.shape

2bc9eec4f47b6889526904c522bf0e66.png

Для оценки качества обучения будем использовать метрики: accuracy_score, recision_score, recall_score, f1_score, roc_auc_score. При этом в основном будем ориентироваться на f1_score и roc_auc_score, как более показательные. 

from sklearn.metrics import accuracy_score, precision_score, recall_score, f1_score

def quality_report(prediction, actual):
    print("Accuracy: {:.3f}\nPrecision: {:.3f}\nRecall: {:.3f}\nf1_score: {:.3f}".format(
        accuracy_score(prediction, actual),
        precision_score(prediction, actual),
        recall_score(prediction, actual),
        f1_score(prediction, actual)
    ))

Обучаем модель методом ближайших соседей. После этого проверим качество обучения на ранее выбранных метриках. 

from sklearn.neighbors import KNeighborsClassifier

knn = KNeighborsClassifier(n_neighbors=13)
knn.fit(X_train, y_train)
knn_predictions_y = knn.predict(X_test)
-----------------------------------------------------------------------------------------------------------------------------------------
print("Train quality:")
quality_report(knn.predict(X_train), y_train)
print("\nTest quality:")
quality_report(knn.predict(X_test), y_test)

9c7b85f0f618e8bc1bf08b8fb06a3551.png

Построим матрицу ошибок. 

from sklearn.metrics import confusion_matrix

cm = confusion_matrix(y_test, knn_predictions_y)
cm 

06c77d1b84343953328d92c767787952.png

Для визуального отображения строим график Roc- Auc, а также рассчитаем roc_auc_score

from sklearn.metrics import roc_auc_score
import sklearn.metrics as metrics
from sklearn.metrics import roc_curve

def roc_auc(model, X_test, y_test ):
    y_scores = model.predict_proba(X_test)
    fpr, tpr, thresh = roc_curve(y_test, y_scores[:,1], pos_label = 1)
    roc_auc = metrics.auc(fpr, tpr)
    plt.title('Receiver Operating Characteristic')
    plt.plot(fpr, tpr, 'b', label = 'AUC = %0.2f' % roc_auc)
    plt.legend(loc = 'lower right')
    plt.plot([0, 1], [0, 1],'r--')
    plt.ylabel('True Positive Rate')
    plt.xlabel('False Positive Rate')
    plt.show()
roc_auc(knn, X_test, y_test )

03feef83793ebccabe266aeb392e2b5a.png

Вот он, момент истины… Получаем не самые высокие метрики. 

Пробуем подобрать гиперпараметры на кроссвалидации с помощью метода градиентного спуска. Получаем лучшее значение k = 13. 

from sklearn.model_selection import GridSearchCV

def grid_optimization(model, parameters, X_train, y_train, X_test):
    gs = GridSearchCV(model,               # Classifier object to optimize
                      parameters,          # Grid of the hyperparameters
                      scoring='accuracy',  # Claasification quality metric to optimize
                      cv=5,                # Number of folds in KFolds cross-validation (CV)
                      n_jobs=-1, 
                      verbose=True
                     )

    # Run Grid Search optimization
    gs.fit(X_train, y_train)
    print('Best parameters: ', gs.best_params_)    
    print('Best Accuracy Through Grid Search : {:.3f}'.format(gs.best_score_))
knn = KNeighborsClassifier()

# Estimate grid of the classifier hyperparameters
parameters = {'n_neighbors': [3,5,7,9,11,13,15,17,19,21,23,25,27,29,31]}

grid_optimization(knn, parameters, X_train, y_train, X_test)

20ec1e973e52927fdc9363045c2aa4cf.png

С целью поиска оптимальной модели с более высокими метриками попробуем использовать другие модели классификации.  

Логистическая регрессия

Аналогично обучим модель и посмотрим на результат.

from sklearn.linear_model import LogisticRegression
 
lr = LogisticRegression(penalty = 'l2')
lr.fit(X_train, y_train)
lr_y_prediction = lr.predict(X_test)
print("Train quality:")
quality_report(lr.predict(X_train), y_train)
print("\nTest quality:")
quality_report(lr.predict(X_test), y_test)

3ed341ffd44dfba7a6b5025597a227da.png

Построим матрицу ошибок и Roc-Auc. 

cm = confusion_matrix(y_test, lr_y_prediction)
cm

a7f8556c060a40b7c996c0e6139fb25b.png

roc_auc(lr, X_test, y_test )

4c20363d7af13d93e75324890ebf8572.png

По результатам метрик понимаем, что логистическая регрессия не самая оптимальная модель. 

Визуализируем влияние признаков на предсказательную роль модели.

featureImportance = pd.DataFrame({'feature': X.columns,  'importance': lr.coef_[0]})
featureImportance

1bd41041b57040166481e6c66c5ffdea.png

featureImportance.set_index('feature', inplace=True)
featureImportance.sort_values(['importance'], ascending=False, inplace=True)
featureImportance['importance'].plot.bar()

b096f7f8df9fbd5900f7e2743c0c040f.png

Подбираем гиперпараметры для логистической регрессии. 

log_reg = LogisticRegression()
parameters = {'penalty': ['l1', 'l2', 'elasticnet', None], 'C': [0.001, 0.01, 0.1, 1] }
grid_optimization(log_reg, parameters, X_train, y_train, X_test)

588da381c735dfb917cb61c74b6b3c58.png

Продолжаем поиск оптимальной модели. Остальной алгоритм работы проводим аналогично: обучаем модель, смотрим метрики, подбираем гиперпараметры. 

Дерево решений

На примере данной модели мне было интересно попробовать разные способы визуализации деревьев. Желание использовать разные варианты пришло не сразу: дело в том, что по мере построения дерева, у меня что-то ломалось. Поэтому я решила, что не будет лишним иметь в своем учебном арсенале разные варианты визуализации деревьев.  

from sklearn.tree import DecisionTreeClassifier

dt = DecisionTreeClassifier(max_depth=3, min_samples_leaf=10, min_samples_split=2, criterion='gini')
dt.fit(X_train, y_train)
dt_y_pred = dt.predict(X_test)

Код ниже построит дерево решений в текстовом формате. 

from sklearn.tree import export_text

tree_rules = export_text(dt, feature_names = list(X.columns))
print(tree_rules) 

3d670e47559720fcb29be14382b67857.png

Теперь посмотрим это же дерево в графическом виде. Попробуем это сделать несколькими способами. 

Вариант №1

fig = plt.figure(figsize=(20,6), dpi=80, facecolor='w', edgecolor='k')
_ = tree.plot_tree(dt, feature_names=X.columns, class_names=True, filled=True)

Вариант №2

import os
os.environ["PATH"] += os.pathsep + "C:/Program Files/Graphviz/bin/"
import graphviz
# DOT data
dot_data = tree.export_graphviz(dt, out_file=None, 
                                feature_names=list(X.columns),  
                                class_names=True,
                                filled=True)

# Draw graph
graph = graphviz.Source(dot_data, format="png") 
graph

Код ниже позволяет сохранить построенное дерево в виде картинки. 

graph.render("decision_tree_graphivz")

Вариант №3

from sklearn.tree import export_graphviz
import os, graphviz,pydotplus

os.environ["PATH"] += os.pathsep + 'C:/Program Files (x86)/Graphviz2.38/bin/'
def plot_tree(model, cols, fname='temp_tree.png'):
    dot_data = export_graphviz(model, filled=True, rounded=True, feature_names=cols, out_file=None)
    pydot_graph = pydotplus.graph_from_dot_data(dot_data)
    pydot_graph.write_png(fname)
    img = plt.imread(fname)
    plt.imshow(img)
plt.figure(figsize=(25, 25))
plt.axis('off')
plot_tree(dt, list(X.columns))

Во всех трех случаях мы получим вот такое дерево:

5ec4d2bdb7773d7dceb1c9930be043fa.png

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

from dtreeviz import *
from dtreeviz.models.shadow_decision_tree import ShadowDecTree

shadow_tree = ShadowDecTree.get_shadow_tree(dt,X_train, y_train, feature_names = list(X.columns), target_name='TravelInsurance')
model = DTreeVizAPI(shadow_tree)
model.view(scale=2.0)

7248602266c37dd0e46c58da7c5c21a1.png

Вернемся к метрикам и подбору гиперпараметров. 

print("Train quality:")
quality_report(dt.predict(X_train), y_train)
print("\nTest quality:")
quality_report(dt.predict(X_test), y_test)

749959f8d72e5f06b9f9e6b4745ea9c6.png

cm = confusion_matrix(y_test, dt_y_pred)
cm

0b5772e77217965d8367af5bb370d9f5.png

roc_auc(dt, X_test, y_test )

39e4ba8ee75306703eb59eb3aaa74a30.png

Подбираем гиперпараметры. 

dt = DecisionTreeClassifier()

parameters = {'max_depth':[1, 2, 3, 4, 5, 6, 7, 8, 9, 10, None], 
              'min_samples_split' : [2,5,10,20],
              'min_samples_leaf':[1, 5, 10,50],
              'criterion' :['gini', 'entropy', 'log_loss']
             }

grid_optimization(dt, parameters, X_train, y_train, X_test)

754f63593b0dc61be2bc4c4d957bfc04.png

Посмотрим какие признаки оказали наибольшее влияние на построение дерева. 

dt.feature_importances_
pd.DataFrame(dt.feature_importances_, index = list(X.columns), columns = ['feature importance']).sort_values('feature importance', ascending = False)

f5b658a52329a6cd1ba8992ce3199dfa.png

Bagging

Попробуем обучить ансамбль деревьев. 

from sklearn.ensemble import BaggingClassifier

bagging_model = BaggingClassifier(base_estimator=DecisionTreeClassifier(max_depth=3), n_jobs=-1, n_estimators=350)
bagging_model.fit(X_train, y_train)
bagging_model_y_pred = bagging_model.predict(X_test)
print("Train quality:")
quality_report(bagging_model.predict(X_train), y_train)
print("\nTest quality:")
quality_report(bagging_model.predict(X_test), y_test)  

2055b1505489fc10798ad71d1057190d.png

cm = confusion_matrix(y_test, bagging_model_y_pred)
cm

59eb42d0b4d97ade15a9f2b48755de3f.png

roc_auc(bagging_model, X_test, y_test )

431857578e17bd200bae890737ce8f1b.png

В результате применения Bagging удалось незначительно улучшить метрики (f1_score, roc_auc_score). 

Random Forest 

Модель Random Forest продемонстрировала результаты, сопоставимые с Bagging. 

from sklearn.ensemble import RandomForestClassifier

rf = RandomForestClassifier(n_estimators=50, n_jobs=-1, max_depth = 3, max_features = None, oob_score=True, 
                            min_samples_split = 3, min_samples_leaf = 5, bootstrap = True, criterion = 'log_loss')
rf.fit(X_train, y_train)
rf_y_pred = rf.predict(X_test)
print("Train quality:")
quality_report(rf.predict(X_train), y_train)
print("\nTest quality:")
quality_report(rf.predict(X_test), y_test)

ecfffb226c1c357d1d7941dc74f4c9be.png

roc_auc(rf, X_test, y_test )

357861fed342e5d9dd3bb0c2ada131af.png

rf = RandomForestClassifier()

parameters = {'n_estimators': [5,10,50, 100, 200, 300], 
              'max_features' : [None, 1, 3, 5, 7], 
              'max_depth':[None, 1, 2, 3, 4, 5, 6, 7], 
              'criterion': ['gini', 'entropy', 'log_loss'],
              'bootstrap': [True, False]
             }

grid_optimization(rf, parameters, X_train, y_train, X_test)

8cca799e9501affe352c16ad7d9c1f6f.png

Попробуем использовать бустинги. Начнем с градиентного бустинга. Далее посмотрим CatBoostClassifier и LGBMClassifier.

Градиентный бустинг

Градиентный бустинг продемонстрировал чуть более высокий показатель f1_score, в то же время уменьшился roc_auc_score

from sklearn.ensemble import GradientBoostingClassifier

gb = GradientBoostingClassifier(n_estimators=10, learning_rate=1.0, max_depth=3, random_state=0, max_features = None)
gb.fit(X_train, y_train)
gb_y_pred = gb.predict(X_test)
print("Train quality:")
quality_report(gb.predict(X_train), y_train)
print("\nTest quality:")
quality_report(gb.predict(X_test), y_test)

eb527011ab557693f0be078ddf019f53.png

roc_auc(gb, X_test, y_test )

ccb6be65b1ea949cc83374fe2f55334c.png

gb = GradientBoostingClassifier()

parameters = {'max_depth':[1, 3, 5, 7, 10, None], 
              'n_estimators': [5,10,50, 100, 500]
             }

grid_optimization(gb, parameters, X_train, y_train, X_test)

84e3c7ffac01991cb08735f24119ae7f.png

CatBoost 

Модель CatBoost позволяет улучшить roc_auc_score по сравнение с градиентным бустингом. 

from catboost import CatBoostClassifier 

cb = CatBoostClassifier(iterations=10, learning_rate=0.5)
cb.fit(X_train, y_train)
cb_y_pred = cb.predict(X_test)
print("Train quality:")
quality_report(cb.predict(X_train), y_train)
print("\nTest quality:")
quality_report(cb.predict(X_test), y_test)

5a9dceb26c990f9e3a2a88c235d11d82.png

roc_auc(cb, X_test, y_test )

2cc8d7c241c021870ab952c2d253aade.png

cb = CatBoostClassifier()

parameters = {'iterations': [5,10,50, 70, 100], 
              'learning_rate':[0.01, 0.1, 0.15, 0.3, 0.5],
             }

grid_optimization(cb, parameters, X_train, y_train, X_test)

6d9cf67e8a5dd13c6c484ff942855ed2.png

LightGBM

Применение LightGBM обеспечило самый высокий показатель roc_auc_score без ухудшения показателя f1_score. Конечно, результаты далеки от идеала, но тем не менее хотя бы небольшое улучшение метрики. 

from lightgbm import LGBMClassifier 

lgbm = LGBMClassifier(n_estimators=10)
lgbm.fit(X_train, y_train)
lgbm_y_pred = lgbm.predict(X_test)
print("Train quality:")
quality_report(lgbm.predict(X_train), y_train)
print("\nTest quality:")
quality_report(lgbm.predict(X_test), y_test)

1cb8a044eec77238be24bb3ecba95f06.png

roc_auc(lgbm, X_test, y_test )

01d5b62e074f778eecbcb228be47f5de.png

lgbm = LGBMClassifier()

parameters = {'n_estimators': [5,10,50, 100, 200, 300]}
grid_optimization(lgbm, parameters, X_train, y_train, X_test)

2ae5be0191b9e0152ad30a1e3de96603.png

SVC

Метод опорных векторов показал ухудшение f1_score.

from sklearn.svm import SVC

svm = SVC(kernel='rbf', degree=1, gamma='scale', C=1.0, probability=True)
svm.fit(X_train, y_train)
svm_y_pred = svm.predict(X_test)
print("Train quality:")
quality_report(svm.predict(X_train), y_train)
print("\nTest quality:")
quality_report(svm.predict(X_test), y_test)

8a07c61be1783abeb71f1198323d5e85.png

roc_auc(svm, X_test, y_test )

9e4ab2e6a429e2448006431c8af42b19.png

svm = SVC()

parameters = {'C':[1.0, 10.0, 20.0, None,], 
              'kernel': ['linear', 'poly', 'rbf', 'sigmoid', 'rbf'],
              'gamma' : ['scale', 'auto'],
              'degree' : [1, 3]
              }

grid_optimization(svm, parameters, X_train, y_train, X_test)

Naive bayes

Naive bayes понизил результат метрики roc_auc_score

from sklearn.naive_bayes import GaussianNB

gnb = GaussianNB(priors = None, var_smoothing = 1e-09)
gnb.fit(X_train, y_train)
gnb_y_pred = gnb.predict(X_test)
print("Train quality:")
quality_report(gnb.predict(X_train), y_train)
print("\nTest quality:")
quality_report(gnb.predict(X_test), y_test)

f2d4510f0aa770000ec01b94d3d13e11.png

roc_auc(gnb, X_test, y_test )

7c50b7edaff3acbadbc960fa4e15a980.png

gnb = GaussianNB()

parameters = { 'priors': [None, [0.1,]* len(['TravelInsurance']),],
               'var_smoothing': [1e-9, 1e-6, 1e-12],
             }

grid_optimization(gnb, parameters, X_train, y_train, X_test)

eae49e46ac38ba96b7479f24e0b6f654.png

Теперь сравниваем все модели между собой. Будем ориентироваться на наиболее показательные метрики f1_score и roc_auc_score

compare_models = pd.DataFrame({
    'Model Name': ['KNN', 'LR', 'DecisionTree', 'Bagging', 'RandomForest', 
                   'GBoost', 'CatBoost', 'LightGBM', 'SVM', 'Naive Bayes'],
    'True Positive': [confusion_matrix(y_test, knn_predictions_y).ravel()[0], 
                      confusion_matrix(y_test, lr_y_prediction).ravel()[0], 
                      confusion_matrix(y_test, dt_y_pred).ravel()[0], 
                      confusion_matrix(y_test, bagging_model_y_pred).ravel()[0], 
                      confusion_matrix(y_test, rf_y_pred).ravel()[0], 
                      confusion_matrix(y_test, gb_y_pred).ravel()[0], 
                      confusion_matrix(y_test, cb_y_pred).ravel()[0], 
                      confusion_matrix(y_test, lgbm_y_pred).ravel()[0], 
                      confusion_matrix(y_test, svm_y_pred).ravel()[0], 
                      confusion_matrix(y_test, gnb_y_pred).ravel()[0]],
    'True Negative': [confusion_matrix(y_test, knn_predictions_y).ravel()[1], 
                      confusion_matrix(y_test, lr_y_prediction).ravel()[1], 
                      confusion_matrix(y_test, dt_y_pred).ravel()[1], 
                      confusion_matrix(y_test, bagging_model_y_pred).ravel()[1],
                      confusion_matrix(y_test, rf_y_pred).ravel()[1], 
                      confusion_matrix(y_test, gb_y_pred).ravel()[1], 
                      confusion_matrix(y_test, cb_y_pred).ravel()[1], 
                      confusion_matrix(y_test, lgbm_y_pred).ravel()[1], 
                      confusion_matrix(y_test, svm_y_pred).ravel()[1], 
                      confusion_matrix(y_test, gnb_y_pred).ravel()[1]],
    'False Positive': [confusion_matrix(y_test, knn_predictions_y).ravel()[2], 
                      confusion_matrix(y_test, lr_y_prediction).ravel()[2], 
                      confusion_matrix(y_test, dt_y_pred).ravel()[2], 
                      confusion_matrix(y_test, bagging_model_y_pred).ravel()[2],  
                      confusion_matrix(y_test, rf_y_pred).ravel()[2], 
                      confusion_matrix(y_test, gb_y_pred).ravel()[2], 
                      confusion_matrix(y_test, cb_y_pred).ravel()[2], 
                      confusion_matrix(y_test, lgbm_y_pred).ravel()[2], 
                      confusion_matrix(y_test, svm_y_pred).ravel()[2], 
                      confusion_matrix(y_test, gnb_y_pred).ravel()[2]],
    'False Negative': [confusion_matrix(y_test, knn_predictions_y).ravel()[3], 
                      confusion_matrix(y_test, lr_y_prediction).ravel()[3], 
                      confusion_matrix(y_test, dt_y_pred).ravel()[3], 
                      confusion_matrix(y_test, bagging_model_y_pred).ravel()[3], 
                      confusion_matrix(y_test, rf_y_pred).ravel()[3], 
                      confusion_matrix(y_test, gb_y_pred).ravel()[3], 
                      confusion_matrix(y_test, cb_y_pred).ravel()[3], 
                      confusion_matrix(y_test, lgbm_y_pred).ravel()[3], 
                      confusion_matrix(y_test, svm_y_pred).ravel()[3], 
                      confusion_matrix(y_test, gnb_y_pred).ravel()[3]],
    'Accuracy': [accuracy_score(y_test, knn_predictions_y), 
                 accuracy_score(y_test, lr_y_prediction), 
                 accuracy_score(y_test, dt_y_pred), 
                 accuracy_score(y_test, bagging_model_y_pred), 
                 accuracy_score(y_test, rf_y_pred), 
                 accuracy_score(y_test, gb_y_pred), 
                 accuracy_score(y_test, cb_y_pred),
                 accuracy_score(y_test, lgbm_y_pred), 
                 accuracy_score(y_test, svm_y_pred), 
                 accuracy_score(y_test, gnb_y_pred)],            
    'Precision' : [precision_score(y_test, knn_predictions_y), 
                   precision_score(y_test, lr_y_prediction), 
                   precision_score(y_test, dt_y_pred), 
                   precision_score(y_test, bagging_model_y_pred), 
                   precision_score(y_test, rf_y_pred), 
                   precision_score(y_test, gb_y_pred), 
                   precision_score(y_test, cb_y_pred), 
                   precision_score(y_test, lgbm_y_pred), 
                   precision_score(y_test, svm_y_pred), 
                   precision_score(y_test, gnb_y_pred)],     
    'Recall' : [recall_score(y_test, knn_predictions_y), 
                recall_score(y_test, lr_y_prediction), 
                recall_score(y_test, dt_y_pred),
                recall_score(y_test, bagging_model_y_pred),
                recall_score(y_test, rf_y_pred), 
                recall_score(y_test, gb_y_pred), 
                recall_score(y_test, cb_y_pred), 
                recall_score(y_test, lgbm_y_pred), 
                recall_score(y_test, svm_y_pred), 
                recall_score(y_test, gnb_y_pred)],          
    'F1 Score' : [f1_score(y_test, knn_predictions_y), 
                  f1_score(y_test, lr_y_prediction), 
                  f1_score(y_test, dt_y_pred), 
                  f1_score(y_test, bagging_model_y_pred), 
                  f1_score(y_test, rf_y_pred), 
                  f1_score(y_test, gb_y_pred), 
                  f1_score(y_test, cb_y_pred), 
                  f1_score(y_test, lgbm_y_pred), 
                  f1_score(y_test, svm_y_pred), 
                  f1_score(y_test, gnb_y_pred)],  
    'AUC Score' : [roc_auc_score(y_test, knn.predict_proba(X_test)[:,1]), 
                  roc_auc_score(y_test, lr.predict_proba(X_test)[:,1]), 
                  roc_auc_score(y_test, dt.predict_proba(X_test)[:,1]),
                  roc_auc_score(y_test, bagging_model.predict_proba(X_test)[:,1]),
                  roc_auc_score(y_test, rf.predict_proba(X_test)[:,1]), 
                  roc_auc_score(y_test, gb.predict_proba(X_test)[:,1]), 
                  roc_auc_score(y_test, cb.predict_proba(X_test)[:,1]), 
                  roc_auc_score(y_test, lgbm.predict_proba(X_test)[:,1]), 
                  roc_auc_score(y_test, svm.predict_proba(X_test)[:,1]), 
                  roc_auc_score(y_test, gnb.predict_proba(X_test)[:,1])],  
})
compare_models

e8f2d41a0d7ebded24a932d4fdf8657e.png

По результатам обучения 10 моделей ни одна из них не приблизила метрики roc_auc_score и f1_score хотя бы к 0,90–0,95. Это связано с низкой корреляцией признаков при небольшом размере датасета. Самый высокий показатель roc_auc_score получаем с помощью моделей: KNN, SVM. Самый высокий f1_score, применяя модели: GBoost, Decision Tree, Bagging, Random Forest, Cat Boost, LightGBM. Для дальнейшей работы я выбрала модель LightGBM, т. к. она демонстрирует один из наиболее высоких результатов по обоим метрикам. 

Загрузим второй датасет и сделаем прогноз покупки страховки

Датасет №2 имеет размер 1303 строки, 8 столбцов. Не содержит целевого признака. 4 категориальных признака. Нет пропусков. 483 дубликата. После удаления дубликатов датасет содержит 820 строк. 

data_new = pd.read_csv('Travel_Company_New_Clients.csv', sep = ';')
data_new.head()

8db1c4c9f49e9f0b777db0ff39f3c106.png

data_new.shape

050bfe7381584c7158bf765954e53c49.png

data_new.info()

7625ab5543df4d73e5bedd154df233b0.png

data_new.duplicated().sum()

b232ec7c7b8a963227546f9addeaa057.png

data_new.drop_duplicates(inplace = True)
data_new = data_new.reset_index(drop = True)
data_new.info()

3e4cf56c7021e3f83a230f89a50a9db2.png

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

data_new['GraduateOrNot'].unique()

d646ab7e472a482e506cafa3d9d94f40.png

data_new['GraduateOrNot'] = data_new['GraduateOrNot'].apply(lambda x: 1 if x=='Yes' else 0)
data_new['FrequentFlyer'].unique()

dd3fe1199a1b65a2b22bce5fde7e0df7.png

data_new['FrequentFlyer'] = data_new['FrequentFlyer'].apply(lambda x: 1 if x=='Yes' else 0)
data_new['EverTravelledAbroad'].unique()

e5dede6f472ffc1cd1a574a84e6430a1.png

data_new['EverTravelledAbroad'] = data_new['EverTravelledAbroad'].apply(lambda x: 1 if x=='Yes' else 0)
data_new['Employment Type'].unique()

c0526e81e7215d86c28acf367ea13149.png

print(data_new['Employment Type']. value_counts ()['Government Sector'])
print(data_new['Employment Type']. value_counts ()['Private Sector/Self Employed'])

4460f607a94d0d9f611379cdee5a8eb1.png

enc = OneHotEncoder()
enc.fit_transform(data_new[['Employment Type']])
data_new = data_new.drop(['Employment Type'], axis = 1).join(enc.fit_transform(data_new[['Employment Type']], axis = 0))
data_new.head()

9e9cd91fb0a5ec9467664c76229dc090.png

Аналогично предыдущему датасету, в новом датасете признаки низко скоррелированы между собой. 

data_new.describe()

2cde0c9c295924d576e41e29b800321a.png

data_new.corr()

25ff733691d23ef703c8c0f1ea115e16.png

plt.subplots(figsize=(10,7))
sns.heatmap(data_new.corr(), cbar=True, annot=True, square=True, fmt='.2f', annot_kws={'size': 10},\
            cmap=sns.color_palette("coolwarm", 10000), vmin=-1, center=0)
plt.show()

3d48b5309e2a3a12839f2430e7b77cc9.png

Для того, чтобы понять можем ли мы в ранее выбранную модель LightGBM подавать новый датасет, нужно определить на сколько новый датасет похож на тот, на котором обучалась модель. Можем сравнивать попарно признаки между собой, например, расположив рядом два графика и визуально сравнить их сходство. Либо можем применить метод compare (при этом, если мы используем дефолтные параметры keep_equal, keep_shape, то метод покажет только различия). 

data_1.compare(data_2, result_names=('data_old', 'data_new'), align_axis=0)

c212fb6ad065a7d0011300afdb8ca949.png

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

X_new_test = scaler.transform(data_new)
X_train.shape, X_new_test.shape, y_train.shape

fe77573e7bcbb5436ff5d4e673c2ec31.png

lgbm_predictions_new_y = lgbm.predict(X_new_test)
data_new['predicted_TravelInsurance'] = lgbm_predictions_new_y

Проверяем, что модель действительно предсказала целевое значение — покупку страховки клиентами туроператора.  Смотрим первые пять строк датасета и видим, что появилась колонка Predicted_TravelInsurance cо значениями. 

data_new.head()

68bfa844b5892a4d02b6d62bd36b93b1.png

Модель спрогнозировала, что 25% клиентов (205 человек) купят страховку, остальные 75% клиентов (615 человек) не будут покупать страховку. 

data_new['Predicted_TravelInsurance'].value_counts(normalize=True)*100
© Habrahabr.ru