Автор статьи: 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()

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

  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

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

data.info()
data.isnull().sum()

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

data['GraduateOrNot'].unique()
data['FrequentFlyer'].unique()
data['EverTravelledAbroad'].unique()
data['Employment Type'].unique()
print(data['Employment Type']. value_counts ()['Government Sector'])
print(data['Employment Type']. value_counts ()['Private Sector/Self Employed'])
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()

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

data.describe()
data.corr()
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()

Датасет почти готов к работе с моделью.  Разобьем его на 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

Для оценки качества обучения будем использовать метрики: 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)

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

from sklearn.metrics import confusion_matrix

cm = confusion_matrix(y_test, knn_predictions_y)
cm 

Для визуального отображения строим график 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 )

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

Пробуем подобрать гиперпараметры на кроссвалидации с помощью метода градиентного спуска. Получаем лучшее значение 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)

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

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

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

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)

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

cm = confusion_matrix(y_test, lr_y_prediction)
cm
roc_auc(lr, X_test, y_test )

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

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

featureImportance = pd.DataFrame({'feature': X.columns,  'importance': lr.coef_[0]})
featureImportance
featureImportance.set_index('feature', inplace=True)
featureImportance.sort_values(['importance'], ascending=False, inplace=True)
featureImportance['importance'].plot.bar()

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

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)

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

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

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

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) 

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

Вариант №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))

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

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

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)

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

print("Train quality:")
quality_report(dt.predict(X_train), y_train)
print("\nTest quality:")
quality_report(dt.predict(X_test), y_test)
cm = confusion_matrix(y_test, dt_y_pred)
cm
roc_auc(dt, X_test, y_test )

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

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)

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

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

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)  
cm = confusion_matrix(y_test, bagging_model_y_pred)
cm
roc_auc(bagging_model, X_test, y_test )

В результате применения 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)
roc_auc(rf, X_test, y_test )
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)

Попробуем использовать бустинги. Начнем с градиентного бустинга. Далее посмотрим 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)
roc_auc(gb, X_test, y_test )
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)

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)
roc_auc(cb, X_test, y_test )
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)

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)
roc_auc(lgbm, X_test, y_test )
lgbm = LGBMClassifier()

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

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)
roc_auc(svm, X_test, y_test )
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)
roc_auc(gnb, X_test, y_test )
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)

Теперь сравниваем все модели между собой. Будем ориентироваться на наиболее показательные метрики 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

По результатам обучения 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()
data_new.shape
data_new.info()
data_new.duplicated().sum()
data_new.drop_duplicates(inplace = True)
data_new = data_new.reset_index(drop = True)
data_new.info()

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

data_new['GraduateOrNot'].unique()
data_new['GraduateOrNot'] = data_new['GraduateOrNot'].apply(lambda x: 1 if x=='Yes' else 0)
data_new['FrequentFlyer'].unique()
data_new['FrequentFlyer'] = data_new['FrequentFlyer'].apply(lambda x: 1 if x=='Yes' else 0)
data_new['EverTravelledAbroad'].unique()
data_new['EverTravelledAbroad'] = data_new['EverTravelledAbroad'].apply(lambda x: 1 if x=='Yes' else 0)
data_new['Employment Type'].unique()
print(data_new['Employment Type']. value_counts ()['Government Sector'])
print(data_new['Employment Type']. value_counts ()['Private Sector/Self Employed'])
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()

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

data_new.describe()
data_new.corr()
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()

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

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

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

X_new_test = scaler.transform(data_new)
X_train.shape, X_new_test.shape, y_train.shape
lgbm_predictions_new_y = lgbm.predict(X_new_test)
data_new['predicted_TravelInsurance'] = lgbm_predictions_new_y

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

data_new.head()

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

data_new['Predicted_TravelInsurance'].value_counts(normalize=True)*100
print(data_new['Predicted_TravelInsurance']. value_counts ()[0])
print(data_new['Predicted_TravelInsurance']. value_counts ()[1])

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

  1. Подбирала размер тестовой выборки в методе train_test_split():  test_size = 0.2, test_size = 0.25, test_size = 0.3. Более высокие результаты метрик были при test_size = 0.2.

  2. Изменяла параметр stratify в методе train_test_split(): stratify = None, stratify = y. Параметр stratify позволяет сохранить исходное соотношение классов. Он применяется, если в небольшом датасете есть несбалансированность классов, т. е. одно значение (например, отсутствие покупки страховки) встречается значительно чаще, чем второе значение (покупка страховки). При этом метрики были выше с дефолтным параметром stratify.

  3. Пробовала удалить признаки с самой низкой корреляцией (GraduateOrNot, ChronicDiseases), чтобы они не мешали работе модели. Это не привело к улучшению метрик. 

  4. Подавала в модель копию датасета, содержащую только признаки со средней корреляцией (EverTravelledAbroad, AnnualIncome, FrequentFlyer). Это также не помогло увеличить метрики. 

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

Я постаралась написать статью максимально подробно и доходчиво, чтобы, возможно, она была полезна такому же новичку, как я. 

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

В заключение хочу поблагодарить команду преподавателей курса Machine Learning. Basic OTUS во главе с руководителем Марией Тихоновой @mashkka_t за профессионализм, глубину подхода к обучающей программе, формат преподнесения информации, менторство, позитив, поддержку и вдохновение в процессе обучения!

Комментарии (0)