Заказчик: банк "Бета-Банк".
Данные: исторические данные о поведении клиентов и расторжении договоров с банком.
Описание данных:
Каждый объект в наборе данных — это информация о поведении одного пользователя за месяц.
RowNumber — индекс строки в данных
CustomerId — уникальный идентификатор клиента
Surname — фамилия
CreditScore — кредитный рейтинг
Geography — страна проживания
Gender — пол
Age — возраст
Tenure — сколько лет человек является клиентом банка
Balance — баланс на счёте
NumOfProducts — количество продуктов банка, используемых клиентом
HasCrCard — наличие кредитной карты
IsActiveMember — активность клиента
EstimatedSalary — предполагаемая зарплата
Exited — факт ухода клиента. Целевой признак (1 - клиент ушел, 0 - актуальный клиент банка)
Источник данных: https://www.kaggle.com/barelydedicated/bank-customer-churn-modeling
Бизнес-цель: снижение оттока клиентов - ориентация на сохранение текущих клиентов.
Задача: построить модель прогнозирования оттока клиентов.
Метрика: F1-мера >= 0.59 на отложенной выборке. Дополнительно требуется измерять AUC-ROC.
# # Установим библиотеки, которых нет на сервере
!pip install sweetviz -q
!pip install catboost -q
!pip install missingno -q
# Библиотека pandas-profiling не всегда устанавливается стабильно, поэтому принудительно зададим стабильные версии
!pip install pandas_profiling==1.4.1 -q
!pip install pandas==0.25.3 -q
# Обновим версию scikit-learn для корректной работы RandomizedSearchCV
!pip install -U scikit-learn -q
Библиотеки sweetviz и pandas_profiling помогут в проведении более тщательного EDA анализа. Исследовательский анализ можно делать и с помощью ручного вызова функций дефолтных библиотек. Данные библиотеки выбраны для максимизации комфорта презентации результатов анализа бизнес-пользователям.
# Импортируем библиотеки, с помощью которых будем обрабатывать данные
# Игнорирование предупреждений об ошибках
import warnings
warnings.filterwarnings('ignore')
from IPython.display import display
# EDA анализ
import sweetviz as sv
import pandas_profiling
# Работа с данными
import pandas as pd
import numpy as np
import random
# Визуализация данных
import matplotlib.pyplot as plt
import seaborn as sns
import missingno as msno
%matplotlib inline
# Предобработка
from sklearn.preprocessing import StandardScaler #масштабирование
from sklearn.impute import MissingIndicator #индикатор пропусков
from sklearn.utils import shuffle #перемешивание данных
from sklearn.model_selection import train_test_split #разделение на выборки
# Модели
from sklearn.linear_model import LogisticRegression
from sklearn.tree import DecisionTreeClassifier
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from catboost import CatBoostClassifier
# Подбор гиперпараметров и лучшей модели
from sklearn.model_selection import RandomizedSearchCV, StratifiedKFold, cross_val_score
from sklearn.pipeline import Pipeline
from sklearn.model_selection import ParameterGrid
# Метрики
from sklearn.metrics import roc_curve, roc_auc_score, confusion_matrix, classification_report, precision_score, recall_score
# Установка настроек для отображения всех колонок и строк при печати
pd.set_option('display.max_columns', None)
pd.set_option('display.max_rows', None)
# Настройки для печати нескольких выводов данных в одной ячейке
from IPython.core.interactiveshell import InteractiveShell
InteractiveShell.ast_node_interactivity = "all"
SEED = 42
# Блок самописных функций используемых в проекте
def upsample(features, target, repeat):
features_zeros = pd.DataFrame(features[target == 0])
features_ones = pd.DataFrame(features[target == 1])
target_zeros = pd.Series(target[target == 0])
target_ones = pd.Series(target[target == 1])
features_upsampled = pd.concat([features_zeros] + [features_ones] * repeat)
target_upsampled = pd.concat([target_zeros] + [target_ones] * repeat)
features_upsampled, target_upsampled = shuffle(
features_upsampled, target_upsampled, random_state=42)
return features_upsampled, target_upsampled
def downsample(features, target, fraction):
features_zeros = pd.DataFrame(features[target == 0])
features_ones = pd.DataFrame(features[target == 1])
target_zeros = pd.DataFrame(target[target == 0])
target_ones = pd.DataFrame(target[target == 1])
features_downsampled = pd.concat(
[features_zeros.sample(frac=fraction, random_state=42)] + [features_ones])
target_downsampled = pd.concat(
[target_zeros.sample(frac=fraction, random_state=42)] + [target_ones])
features_downsampled, target_downsampled = shuffle(
features_downsampled, target_downsampled, random_state=42)
return features_downsampled, target_downsampled
def balance_viz(df_name, title, ax=None):
df_name.value_counts().plot(kind='bar', ax=ax, title=title);
def frame_shape(frame_dict):
for name, data in frame_dict.items():
print(f"Размер выборок {name}: features {data[0].shape}, target {data[1].shape}")
def metric_info(features, target, model):
# вычисления
y_pred_proba = model.predict_proba(features)[::,1]
predicted = model.predict(features)
roc_auc = round(roc_auc_score(target, y_pred_proba), 3)
precision = round(precision_score(target, predicted), 3)
recall = round(recall_score(target, predicted), 3)
f1 = round(2 * (precision * recall) / (precision + recall), 3)
fpr, tpr, threshold = roc_curve(target, y_pred_proba)
# числовые значения метрик
print('ROC-AUC', roc_auc)
print()
print(classification_report(target, predicted))
# матрица ошибок
matrix = confusion_matrix(target, predicted)
dataframe = pd.DataFrame(matrix, index=['tenure_0', 'tenure_1'], columns=['tenure_0', 'tenure_1'])
sns.heatmap(dataframe, annot=True, cbar=None, cmap='Blues', fmt='.4g')
plt.title('Матрица ошибок');
plt.tight_layout();
plt.ylabel('Истинный класс');
plt.xlabel('Пердсказанный класс');
plt.show();
# roc-auc кривая
plt.plot(fpr, tpr, color='darkorange',
lw=2, label=f'ROC curve (area = {roc_auc})');
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--');
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('ROC-кривая')
plt.legend(loc="lower right");
plt.show();
# важность признаков
feature_importance = model.feature_importances_
feature_imp = pd.DataFrame(sorted(zip(feature_importance, features.columns)), columns=['Value','Feature'])
sns.barplot(x="Value", y="Feature", data=feature_imp.sort_values(by="Value", ascending=False))
plt.title('Feature importance')
plt.tight_layout()
plt.show();
return [roc_auc, precision, recall, f1]
def distribution_viz(df_name):
sns.distplot(features_train['CreditScore'], label='Before');
sns.distplot(df_name['CreditScore'], label="After");
plt.legend();
plt.show();
def rand_tenure(num):
return random.randint(df['Tenure'].min(), df['Tenure'].max())
Считываем данные
# Прочитаем данные и запишем в переменную df
name = 'Churn.csv'
try:
df = pd.read_csv('datasets\\{}'.format(name))
except:
print(print('Файл {} не найден и будет скачиваться по сети.'.format(name)))
df = pd.read_csv('https://code.s3.yandex.net/datasets/{}'.format(name))
Файл Churn.csv не найден и будет скачиваться по сети. None
Запусим библиотеку pandas_profiling и посмотрим как выглядят статистики
pandas_profiling.ProfileReport(df)
Dataset info
Number of variables | 14 |
---|---|
Number of observations | 10000 |
Total Missing (%) | 0.6% |
Total size in memory | 1.1 MiB |
Average record size in memory | 112.0 B |
Variables types
Numeric | 8 |
---|---|
Categorical | 3 |
Boolean | 3 |
Date | 0 |
Text (Unique) | 0 |
Rejected | 0 |
Unsupported | 0 |
RowNumber
Numeric
Distinct count | 10000 |
---|---|
Unique (%) | 100.0% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Infinite (%) | 0.0% |
Infinite (n) | 0 |
Mean | 5000.5 |
---|---|
Minimum | 1 |
Maximum | 10000 |
Zeros (%) | 0.0% |
Quantile statistics
Minimum | 1 |
---|---|
5-th percentile | 500.95 |
Q1 | 2500.8 |
Median | 5000.5 |
Q3 | 7500.2 |
95-th percentile | 9500 |
Maximum | 10000 |
Range | 9999 |
Interquartile range | 4999.5 |
Descriptive statistics
Standard deviation | 2886.9 |
---|---|
Coef of variation | 0.57732 |
Kurtosis | -1.2 |
Mean | 5000.5 |
MAD | 2500 |
Skewness | 0 |
Sum | 50005000 |
Variance | 8334200 |
Memory size | 78.2 KiB |
Value | Count | Frequency (%) | |
2047 | 1 | 0.0% | |
5424 | 1 | 0.0% | |
1338 | 1 | 0.0% | |
7481 | 1 | 0.0% | |
5432 | 1 | 0.0% | |
9526 | 1 | 0.0% | |
3379 | 1 | 0.0% | |
1330 | 1 | 0.0% | |
7473 | 1 | 0.0% | |
9518 | 1 | 0.0% | |
Other values (9990) | 9990 | 99.9% |
Minimum 5 values
Value | Count | Frequency (%) | |
1 | 1 | 0.0% | |
2 | 1 | 0.0% | |
3 | 1 | 0.0% | |
4 | 1 | 0.0% | |
5 | 1 | 0.0% |
Maximum 5 values
Value | Count | Frequency (%) | |
9996 | 1 | 0.0% | |
9997 | 1 | 0.0% | |
9998 | 1 | 0.0% | |
9999 | 1 | 0.0% | |
10000 | 1 | 0.0% |
CustomerId
Numeric
Distinct count | 10000 |
---|---|
Unique (%) | 100.0% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Infinite (%) | 0.0% |
Infinite (n) | 0 |
Mean | 15691000 |
---|---|
Minimum | 15565701 |
Maximum | 15815690 |
Zeros (%) | 0.0% |
Quantile statistics
Minimum | 15565701 |
---|---|
5-th percentile | 15579000 |
Q1 | 15629000 |
Median | 15691000 |
Q3 | 15753000 |
95-th percentile | 15803000 |
Maximum | 15815690 |
Range | 249989 |
Interquartile range | 124710 |
Descriptive statistics
Standard deviation | 71936 |
---|---|
Coef of variation | 0.0045846 |
Kurtosis | -1.1961 |
Mean | 15691000 |
MAD | 62263 |
Skewness | 0.0011491 |
Sum | 156909405694 |
Variance | 5174800000 |
Memory size | 78.2 KiB |
Value | Count | Frequency (%) | |
15812607 | 1 | 0.0% | |
15741078 | 1 | 0.0% | |
15635776 | 1 | 0.0% | |
15740223 | 1 | 0.0% | |
15738174 | 1 | 0.0% | |
15662397 | 1 | 0.0% | |
15594812 | 1 | 0.0% | |
15715643 | 1 | 0.0% | |
15713594 | 1 | 0.0% | |
15787318 | 1 | 0.0% | |
Other values (9990) | 9990 | 99.9% |
Minimum 5 values
Value | Count | Frequency (%) | |
15565701 | 1 | 0.0% | |
15565706 | 1 | 0.0% | |
15565714 | 1 | 0.0% | |
15565779 | 1 | 0.0% | |
15565796 | 1 | 0.0% |
Maximum 5 values
Value | Count | Frequency (%) | |
15815628 | 1 | 0.0% | |
15815645 | 1 | 0.0% | |
15815656 | 1 | 0.0% | |
15815660 | 1 | 0.0% | |
15815690 | 1 | 0.0% |
Surname
Categorical
Distinct count | 2932 |
---|---|
Unique (%) | 29.3% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Smith | 32 |
---|---|
Scott | 29 |
Martin | 29 |
Other values (2929) |
Value | Count | Frequency (%) | |
Smith | 32 | 0.3% | |
Scott | 29 | 0.3% | |
Martin | 29 | 0.3% | |
Walker | 28 | 0.3% | |
Brown | 26 | 0.3% | |
Yeh | 25 | 0.2% | |
Shih | 25 | 0.2% | |
Genovese | 25 | 0.2% | |
Maclean | 24 | 0.2% | |
Wright | 24 | 0.2% | |
Other values (2922) | 9733 | 97.3% |
CreditScore
Numeric
Distinct count | 460 |
---|---|
Unique (%) | 4.6% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Infinite (%) | 0.0% |
Infinite (n) | 0 |
Mean | 650.53 |
---|---|
Minimum | 350 |
Maximum | 850 |
Zeros (%) | 0.0% |
Quantile statistics
Minimum | 350 |
---|---|
5-th percentile | 489 |
Q1 | 584 |
Median | 652 |
Q3 | 718 |
95-th percentile | 812 |
Maximum | 850 |
Range | 500 |
Interquartile range | 134 |
Descriptive statistics
Standard deviation | 96.653 |
---|---|
Coef of variation | 0.14858 |
Kurtosis | -0.42573 |
Mean | 650.53 |
MAD | 78.377 |
Skewness | -0.071607 |
Sum | 6505288 |
Variance | 9341.9 |
Memory size | 78.2 KiB |
Value | Count | Frequency (%) | |
850 | 233 | 2.3% | |
678 | 63 | 0.6% | |
655 | 54 | 0.5% | |
705 | 53 | 0.5% | |
667 | 53 | 0.5% | |
684 | 52 | 0.5% | |
670 | 50 | 0.5% | |
651 | 50 | 0.5% | |
683 | 48 | 0.5% | |
660 | 48 | 0.5% | |
Other values (450) | 9296 | 93.0% |
Minimum 5 values
Value | Count | Frequency (%) | |
350 | 5 | 0.1% | |
351 | 1 | 0.0% | |
358 | 1 | 0.0% | |
359 | 1 | 0.0% | |
363 | 1 | 0.0% |
Maximum 5 values
Value | Count | Frequency (%) | |
846 | 5 | 0.1% | |
847 | 6 | 0.1% | |
848 | 5 | 0.1% | |
849 | 8 | 0.1% | |
850 | 233 | 2.3% |
Geography
Categorical
Distinct count | 3 |
---|---|
Unique (%) | 0.0% |
Missing (%) | 0.0% |
Missing (n) | 0 |
France | |
---|---|
Germany | |
Spain |
Value | Count | Frequency (%) | |
France | 5014 | 50.1% | |
Germany | 2509 | 25.1% | |
Spain | 2477 | 24.8% |
Gender
Categorical
Distinct count | 2 |
---|---|
Unique (%) | 0.0% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Male | |
---|---|
Female |
Value | Count | Frequency (%) | |
Male | 5457 | 54.6% | |
Female | 4543 | 45.4% |
Age
Numeric
Distinct count | 70 |
---|---|
Unique (%) | 0.7% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Infinite (%) | 0.0% |
Infinite (n) | 0 |
Mean | 38.922 |
---|---|
Minimum | 18 |
Maximum | 92 |
Zeros (%) | 0.0% |
Quantile statistics
Minimum | 18 |
---|---|
5-th percentile | 25 |
Q1 | 32 |
Median | 37 |
Q3 | 44 |
95-th percentile | 60 |
Maximum | 92 |
Range | 74 |
Interquartile range | 12 |
Descriptive statistics
Standard deviation | 10.488 |
---|---|
Coef of variation | 0.26946 |
Kurtosis | 1.3953 |
Mean | 38.922 |
MAD | 7.941 |
Skewness | 1.0113 |
Sum | 389218 |
Variance | 109.99 |
Memory size | 78.2 KiB |
Value | Count | Frequency (%) | |
37 | 478 | 4.8% | |
38 | 477 | 4.8% | |
35 | 474 | 4.7% | |
36 | 456 | 4.6% | |
34 | 447 | 4.5% | |
33 | 442 | 4.4% | |
40 | 432 | 4.3% | |
39 | 423 | 4.2% | |
32 | 418 | 4.2% | |
31 | 404 | 4.0% | |
Other values (60) | 5549 | 55.5% |
Minimum 5 values
Value | Count | Frequency (%) | |
18 | 22 | 0.2% | |
19 | 27 | 0.3% | |
20 | 40 | 0.4% | |
21 | 53 | 0.5% | |
22 | 84 | 0.8% |
Maximum 5 values
Value | Count | Frequency (%) | |
83 | 1 | 0.0% | |
84 | 2 | 0.0% | |
85 | 1 | 0.0% | |
88 | 1 | 0.0% | |
92 | 2 | 0.0% |
Tenure
Numeric
Distinct count | 12 |
---|---|
Unique (%) | 0.1% |
Missing (%) | 9.1% |
Missing (n) | 909 |
Infinite (%) | 0.0% |
Infinite (n) | 0 |
Mean | 4.9977 |
---|---|
Minimum | 0 |
Maximum | 10 |
Zeros (%) | 3.8% |
Quantile statistics
Minimum | 0 |
---|---|
5-th percentile | 1 |
Q1 | 2 |
Median | 5 |
Q3 | 7 |
95-th percentile | 9 |
Maximum | 10 |
Range | 10 |
Interquartile range | 5 |
Descriptive statistics
Standard deviation | 2.8947 |
---|---|
Coef of variation | 0.57921 |
Kurtosis | -1.1651 |
Mean | 4.9977 |
MAD | 2.4859 |
Skewness | 0.016153 |
Sum | 45434 |
Variance | 8.3794 |
Memory size | 78.2 KiB |
Value | Count | Frequency (%) | |
1.0 | 952 | 9.5% | |
2.0 | 950 | 9.5% | |
8.0 | 933 | 9.3% | |
3.0 | 928 | 9.3% | |
5.0 | 927 | 9.3% | |
7.0 | 925 | 9.2% | |
4.0 | 885 | 8.8% | |
9.0 | 882 | 8.8% | |
6.0 | 881 | 8.8% | |
10.0 | 446 | 4.5% | |
(Missing) | 909 | 9.1% |
Minimum 5 values
Value | Count | Frequency (%) | |
0.0 | 382 | 3.8% | |
1.0 | 952 | 9.5% | |
2.0 | 950 | 9.5% | |
3.0 | 928 | 9.3% | |
4.0 | 885 | 8.8% |
Maximum 5 values
Value | Count | Frequency (%) | |
6.0 | 881 | 8.8% | |
7.0 | 925 | 9.2% | |
8.0 | 933 | 9.3% | |
9.0 | 882 | 8.8% | |
10.0 | 446 | 4.5% |
Balance
Numeric
Distinct count | 6382 |
---|---|
Unique (%) | 63.8% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Infinite (%) | 0.0% |
Infinite (n) | 0 |
Mean | 76486 |
---|---|
Minimum | 0 |
Maximum | 250900 |
Zeros (%) | 36.2% |
Quantile statistics
Minimum | 0 |
---|---|
5-th percentile | 0 |
Q1 | 0 |
Median | 97199 |
Q3 | 127640 |
95-th percentile | 162710 |
Maximum | 250900 |
Range | 250900 |
Interquartile range | 127640 |
Descriptive statistics
Standard deviation | 62397 |
---|---|
Coef of variation | 0.8158 |
Kurtosis | -1.4894 |
Mean | 76486 |
MAD | 56661 |
Skewness | -0.14111 |
Sum | 764860000 |
Variance | 3893400000 |
Memory size | 78.2 KiB |
Value | Count | Frequency (%) | |
0.0 | 3617 | 36.2% | |
105473.74 | 2 | 0.0% | |
130170.82 | 2 | 0.0% | |
113063.83 | 1 | 0.0% | |
80242.37 | 1 | 0.0% | |
134320.23 | 1 | 0.0% | |
90218.9 | 1 | 0.0% | |
155196.17 | 1 | 0.0% | |
95386.82 | 1 | 0.0% | |
125961.74 | 1 | 0.0% | |
Other values (6372) | 6372 | 63.7% |
Minimum 5 values
Value | Count | Frequency (%) | |
0.0 | 3617 | 36.2% | |
3768.69 | 1 | 0.0% | |
12459.19 | 1 | 0.0% | |
14262.8 | 1 | 0.0% | |
16893.59 | 1 | 0.0% |
Maximum 5 values
Value | Count | Frequency (%) | |
216109.88 | 1 | 0.0% | |
221532.8 | 1 | 0.0% | |
222267.63 | 1 | 0.0% | |
238387.56 | 1 | 0.0% | |
250898.09 | 1 | 0.0% |
NumOfProducts
Numeric
Distinct count | 4 |
---|---|
Unique (%) | 0.0% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Infinite (%) | 0.0% |
Infinite (n) | 0 |
Mean | 1.5302 |
---|---|
Minimum | 1 |
Maximum | 4 |
Zeros (%) | 0.0% |
Quantile statistics
Minimum | 1 |
---|---|
5-th percentile | 1 |
Q1 | 1 |
Median | 1 |
Q3 | 2 |
95-th percentile | 2 |
Maximum | 4 |
Range | 3 |
Interquartile range | 1 |
Descriptive statistics
Standard deviation | 0.58165 |
---|---|
Coef of variation | 0.38012 |
Kurtosis | 0.58298 |
Mean | 1.5302 |
MAD | 0.53911 |
Skewness | 0.74557 |
Sum | 15302 |
Variance | 0.33832 |
Memory size | 78.2 KiB |
Value | Count | Frequency (%) | |
1 | 5084 | 50.8% | |
2 | 4590 | 45.9% | |
3 | 266 | 2.7% | |
4 | 60 | 0.6% |
Minimum 5 values
Value | Count | Frequency (%) | |
1 | 5084 | 50.8% | |
2 | 4590 | 45.9% | |
3 | 266 | 2.7% | |
4 | 60 | 0.6% |
Maximum 5 values
Value | Count | Frequency (%) | |
1 | 5084 | 50.8% | |
2 | 4590 | 45.9% | |
3 | 266 | 2.7% | |
4 | 60 | 0.6% |
HasCrCard
Boolean
Distinct count | 2 |
---|---|
Unique (%) | 0.0% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Mean | 0.7055 |
---|
1 | |
---|---|
0 |
Value | Count | Frequency (%) | |
1 | 7055 | 70.5% | |
0 | 2945 | 29.4% |
IsActiveMember
Boolean
Distinct count | 2 |
---|---|
Unique (%) | 0.0% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Mean | 0.5151 |
---|
1 | |
---|---|
0 |
Value | Count | Frequency (%) | |
1 | 5151 | 51.5% | |
0 | 4849 | 48.5% |
EstimatedSalary
Numeric
Distinct count | 9999 |
---|---|
Unique (%) | 100.0% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Infinite (%) | 0.0% |
Infinite (n) | 0 |
Mean | 100090 |
---|---|
Minimum | 11.58 |
Maximum | 199990 |
Zeros (%) | 0.0% |
Quantile statistics
Minimum | 11.58 |
---|---|
5-th percentile | 9851.8 |
Q1 | 51002 |
Median | 100190 |
Q3 | 149390 |
95-th percentile | 190160 |
Maximum | 199990 |
Range | 199980 |
Interquartile range | 98386 |
Descriptive statistics
Standard deviation | 57510 |
---|---|
Coef of variation | 0.57459 |
Kurtosis | -1.1815 |
Mean | 100090 |
MAD | 49677 |
Skewness | 0.0020854 |
Sum | 1000900000 |
Variance | 3307500000 |
Memory size | 78.2 KiB |
Value | Count | Frequency (%) | |
24924.92 | 2 | 0.0% | |
109029.72 | 1 | 0.0% | |
182025.95 | 1 | 0.0% | |
82820.85 | 1 | 0.0% | |
30314.04 | 1 | 0.0% | |
143265.65 | 1 | 0.0% | |
148305.82 | 1 | 0.0% | |
21254.06 | 1 | 0.0% | |
56297.85 | 1 | 0.0% | |
113481.02 | 1 | 0.0% | |
Other values (9989) | 9989 | 99.9% |
Minimum 5 values
Value | Count | Frequency (%) | |
11.58 | 1 | 0.0% | |
90.07 | 1 | 0.0% | |
91.75 | 1 | 0.0% | |
96.27 | 1 | 0.0% | |
106.67 | 1 | 0.0% |
Maximum 5 values
Value | Count | Frequency (%) | |
199909.32 | 1 | 0.0% | |
199929.17 | 1 | 0.0% | |
199953.33 | 1 | 0.0% | |
199970.74 | 1 | 0.0% | |
199992.48 | 1 | 0.0% |
Exited
Boolean
Distinct count | 2 |
---|---|
Unique (%) | 0.0% |
Missing (%) | 0.0% |
Missing (n) | 0 |
Mean | 0.2037 |
---|
0 | |
---|---|
1 |
Value | Count | Frequency (%) | |
0 | 7963 | 79.6% | |
1 | 2037 | 20.4% |
Инструмент pandas_profiling позволяет изучить основную информацию о значениях и статистиках по признакам. Автоматический анализатор позволяет быстро получить информацию о типах данных, распределениях, наличиях пропусков и дублей в наборе данных. Так же можно увидеть информацию о корреляции между признаками или признаками и целевой переменной. Данный инструмент не позволяет ответить на все вопросы о наших данных, но помогает довольно быстро оценить основные проблемы, присутствующие в данных и определиться с инструментами для более детального анализа.
Анализ текущих данных в pandas_profiling показал, что данные достаточно "чистые":
Tenure
- 9.1%.Exited
7963 значения мажоритарного класса '0' и 2037 значений миноритарного класса '1'.Surname
, у которой большое количество уникальных значений 2932CustomerId
и RowNumber
имеют 100% уникальных значенийТребуется дальнейший анализ и принятие решения по данным признакам.
Визуализируем пропуски в данных с целью нахождения закономерностей
msno.matrix(df)
<AxesSubplot:>
Посмотрим более внимательно на строки с пропусками
df[df['Tenure'].isnull()].sample(10)
RowNumber | CustomerId | Surname | CreditScore | Geography | Gender | Age | Tenure | Balance | NumOfProducts | HasCrCard | IsActiveMember | EstimatedSalary | Exited | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
8411 | 8412 | 15719479 | Chukwuhaenye | 619 | Spain | Female | 56 | NaN | 0.00 | 2 | 1 | 1 | 42442.21 | 0 |
4634 | 4635 | 15583353 | Floyd | 610 | Spain | Female | 45 | NaN | 0.00 | 1 | 1 | 0 | 38276.84 | 1 |
3971 | 3972 | 15790809 | Lo Duca | 685 | Spain | Male | 40 | NaN | 74896.92 | 1 | 1 | 0 | 198694.20 | 0 |
5921 | 5922 | 15627203 | Hsu | 508 | Spain | Male | 54 | NaN | 0.00 | 1 | 1 | 1 | 175749.36 | 0 |
3067 | 3068 | 15579781 | Buccho | 806 | Germany | Male | 31 | NaN | 138653.51 | 1 | 1 | 0 | 190803.37 | 0 |
2289 | 2290 | 15789097 | Keeley | 644 | France | Male | 48 | NaN | 0.00 | 2 | 0 | 1 | 44965.54 | 1 |
1682 | 1683 | 15662758 | Watson | 620 | France | Male | 41 | NaN | 97925.11 | 1 | 1 | 0 | 85000.32 | 0 |
254 | 255 | 15665834 | Cheatham | 696 | Spain | Male | 28 | NaN | 0.00 | 1 | 0 | 0 | 176713.47 | 0 |
60 | 61 | 15651280 | Hunter | 742 | Germany | Male | 35 | NaN | 136857.00 | 1 | 0 | 0 | 84509.57 | 0 |
1124 | 1125 | 15627305 | Pan | 606 | Spain | Male | 35 | NaN | 0.00 | 1 | 1 | 0 | 106837.06 | 1 |
Явных паттернов в пропуках нет, из чего делаем вывод, что пропуски имеют случайную природу.
В качестве вариантов выбора стратегий замены пропусков можно рассматривать несколько вариантов: - замена средним (на текущих данных критично повлияет на изменение распределения) - удаление строк с пропусками (допустимо, т.к. пропусков <10%, но может быть потеряна ценная информация в других признаках) - проверить важность признака на этапе моделирования и в случае, если он не критичен, удалить сам признак - замена пропусков рандомными значениями (обязательно требуется проверка сохранности распределения признака)
Посмотрим на распределение данных по нашей целевой переменной Exited
df['Exited'].value_counts()
0 7963 1 2037 Name: Exited, dtype: int64
Посмотрим на долевое распределение классов
print('Класс 0 {:.0%}'.format(1-df['Exited'].mean()))
print('Класс 1 {:.0%}'.format(df['Exited'].mean()))
Класс 0 80% Класс 1 20%
Проверим корреляцию между признаками
sns.set(rc={'figure.figsize':(11.7,8.27)})
sns.heatmap(df.corr(), annot = True, cmap="YlGnBu")
<AxesSubplot:>
Построим график scatterplot
sns.set()
sns.pairplot(df, size = 2.5)
plt.show();
Корреляционный анализ не выявил признаков, требующих отдельной обработки.
Вывод
По итогу анализа предоставленных данных требуется провести следующие преобразования:
Tenure_flag
и проставить в ней флаги для обозначения пропусков (1-пропуск есть, 0-пропуска нет)Tenure
пропуски заменить Рандомным значениемSurname
- не является характеристикой уникальности клиентаCustomerId
и RowNumber
- дублирует индексGender
и Geography
в числовые с применением техники One-Hot EncodingAge
, Balance
, Estimatedsalary
и Balance
к единому масштабуdf.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 10000 entries, 0 to 9999 Data columns (total 14 columns): RowNumber 10000 non-null int64 CustomerId 10000 non-null int64 Surname 10000 non-null object CreditScore 10000 non-null int64 Geography 10000 non-null object Gender 10000 non-null object Age 10000 non-null int64 Tenure 9091 non-null float64 Balance 10000 non-null float64 NumOfProducts 10000 non-null int64 HasCrCard 10000 non-null int64 IsActiveMember 10000 non-null int64 EstimatedSalary 10000 non-null float64 Exited 10000 non-null int64 dtypes: float64(3), int64(8), object(3) memory usage: 1.1+ MB
Создадим признак-индикатор пропусков
miss_ind = MissingIndicator()
miss_ind.fit(df[['Tenure']])
df['Tenure_flag'] = miss_ind.transform(df[['Tenure']])
df['Tenure_flag'] = df['Tenure_flag'].replace(True, 1).astype('int64')
MissingIndicator()
Заменим NAN в признаке Tenure
рандомными значениями. Первым делом визуализиуем распределение признака до замены
df['Tenure'].plot(kind='hist');
Для замены пропусков будем использовать самописную функцию rand_tenure
df.loc[df['Tenure'].isnull(), 'Tenure'] = df.loc[df['Tenure'].isnull(), 'Tenure'].apply(rand_tenure)
df['Tenure'] = df['Tenure'].astype('int64')
Проверим распределение после замены значений
df['Tenure'].plot(kind='hist')
<AxesSubplot:ylabel='Frequency'>
Распределение не изменило свой вид.
Удалим признаки RowNumber
, Surname
и CustomerId
df.drop(['RowNumber', 'Surname', 'CustomerId'], axis=1, inplace=True)
Преобразуем категориальные признаки Gender
и Geography
в числовые с помощью OHE
df = pd.get_dummies(df, drop_first=True)
df.info()
<class 'pandas.core.frame.DataFrame'> RangeIndex: 10000 entries, 0 to 9999 Data columns (total 13 columns): CreditScore 10000 non-null int64 Age 10000 non-null int64 Tenure 10000 non-null int64 Balance 10000 non-null float64 NumOfProducts 10000 non-null int64 HasCrCard 10000 non-null int64 IsActiveMember 10000 non-null int64 EstimatedSalary 10000 non-null float64 Exited 10000 non-null int64 Tenure_flag 10000 non-null int64 Geography_Germany 10000 non-null uint8 Geography_Spain 10000 non-null uint8 Gender_Male 10000 non-null uint8 dtypes: float64(2), int64(8), uint8(3) memory usage: 810.7 KB
Разделим данные на выборки: обучающую (60%), валидационную (20%) и тестовую (20%). В отдельной переменной сохраним объединенный набор объединяющий обучающую и валидационную выборки для переобучения финальной модели и проверки на отложенной тестовой выборке. Будем использовать паремтр stratify для задания пропорции классов при разделении.
# В переменную features поместим датасет
# целевой признак поместим в переменную target
features = df.copy()
target = df['Exited']
# Поделим данные на обучающий и тестовый наборы
features_train_valid, features_test, target_train_valid, target_test = train_test_split(
features, target, test_size=0.20, random_state=SEED, stratify=target)
# Далее обучающий набор делим на обучающий и валидационный
features_train, features_valid, target_train, target_valid = train_test_split(
features_train_valid, target_train_valid, test_size=0.20, random_state=SEED, stratify=target_train_valid)
Посмотрим на размер получившихся наборов данных
frame_dict = {'train_valid': [features_train_valid, target_train_valid],
'train': [features_train, target_train],
'valid': [features_valid, target_valid],
'test': [features_test, target_test]}
frame_shape(frame_dict)
Размер выборок train_valid: features (8000, 13), target (8000,) Размер выборок train: features (6400, 13), target (6400,) Размер выборок valid: features (1600, 13), target (1600,) Размер выборок test: features (2000, 13), target (2000,)
Стандартизируем признаки EstimatedSalary
, Balance
, CreditScore
, Age
методом StandardScaler
numeric = ['EstimatedSalary', 'Balance', 'CreditScore', 'Age']
scaler = StandardScaler()
scaler.fit(features_train[numeric])
features_train[numeric] = scaler.transform(features_train[numeric])
features_valid[numeric] = scaler.transform(features_valid[numeric])
features_test[numeric] = scaler.transform(features_test[numeric])
features_train_valid[numeric] = scaler.fit_transform(features_train_valid[numeric])
StandardScaler()
Визуализируем соотношение классов целевого признака в получившихся выборках
fig, ax = plt.subplots(nrows=1, ncols=4, figsize=(20, 5))
ax1, ax2, ax3, ax4 = ax.flatten();
for title, info in {'train_valid': [target_train_valid, ax1],
'train': [target_train, ax2],
'valid': [target_valid, ax3],
'test': [target_test, ax4]}.items():
balance_viz(info[0], title, info[1])
Запустим инструмент sweetviz для анализа схожести распределения признаков и целевого признака.
sv_report = sv.compare([features_train_valid, "Train_valid"], [features_test, "Test"], target_feat='Exited')
sv_report.show_notebook()
HBox(children=(HTML(value=''), FloatProgress(value=0.0, layout=Layout(flex='2'), max=14.0), HTML(value='')), l…
Деление на выборки произведено корректно, балансировка и распределние целевого класса сохранены.
Удалим целевой признак из выборок features
features_train_valid = features_train_valid.drop(['Exited'], axis=1)
features_train = features_train.drop(['Exited'], axis=1)
features_valid = features_valid.drop(['Exited'], axis=1)
features_test = features_test.drop(['Exited'], axis=1)
frame_shape(frame_dict)
Размер выборок train_valid: features (8000, 13), target (8000,) Размер выборок train: features (6400, 13), target (6400,) Размер выборок valid: features (1600, 13), target (1600,) Размер выборок test: features (2000, 13), target (2000,)
Вывод
Tenure
создали индикатор пропусков, заменили пропуски медианой и изменили тип данных на intRowNumber
, Surname
и CustomerId
EstimatedSalary
, Balance
, CreditScore
, Age
В рамках исследования выявлен факт сильного дисбаланса выборки - значений одного из прогнозируемых классов значительно больше. Данный факт может отрицательно сказаться на результат использования. В ходе дальнейшего решения сначала запустим модели на несбалансированном датасете, выберем лучшую модель, а потом произведем балансировку классов и проверим как это отобразится на качестве модели.
В процессе моделирования апробируем следующие модели: LogisticRegression, DecisionTreeClassifier, RandomForestClassifier, CatBoostClassifier, GradientBoostingClassifier
Сформируем пайплайн
pipe = Pipeline([
('model', LogisticRegression(random_state=SEED))
])
params = [{
'model': [LogisticRegression(random_state=SEED, solver='liblinear')],
'model__penalty': ['l1', 'l2'],
'model__C': [0.001, 1, 100],
'model__class_weight' : [None, 'balanced']
}, {
'model': [DecisionTreeClassifier(random_state=SEED)],
'model__max_depth': list(range(5, 16, 5)),
'model__max_features': range(1, 4),
'model__min_samples_leaf':range(5, 16, 5),
'model__class_weight' : [None, 'balanced']
}, {
'model': [RandomForestClassifier(random_state=SEED)],
'model__n_estimators': list(range(10, 201, 10)),
'model__max_depth': list(range(5, 16)),
'model__class_weight' : [None, 'balanced']
}, {
'model': [CatBoostClassifier(random_state=SEED, logging_level='Silent')]
}, {
'model': [GradientBoostingClassifier(random_state=SEED)]
}
]
Запустим алгоритм RandomizedSearchCV - случайный поиск гиперпараметров из сетки
kf = StratifiedKFold(n_splits=4, shuffle=True, random_state=SEED)
rs = RandomizedSearchCV(pipe, params, scoring='f1', cv=kf, verbose=False, return_train_score=True, n_iter=200, random_state=SEED)
rs = rs.fit(features_train, target_train)
Сгруппируем результаты RandomizedSearchCV в таблицу
rs_results = pd.DataFrame(rs.cv_results_)
models = ['LogisticRegression', 'DecisionTreeClassifier', 'RandomForestClassifier', 'CatBoostClassifier', 'GradientBoostingClassifier']
cols = ['mean_fit_time', 'mean_score_time', 'mean_train_score', 'mean_test_score']
res = pd.DataFrame(columns=cols)
for model in models:
model_idx = rs_results['param_model'].astype('str').str.contains(model)
best_fit = rs_results[model_idx].sort_values(by='rank_test_score').head(1)[cols]
res = res.append(round(best_fit, 3))
res.loc[best_fit.index, 'model'] = model
res.sort_values(by='mean_test_score', ascending=True)
mean_fit_time | mean_score_time | mean_test_score | mean_train_score | model | |
---|---|---|---|---|---|
163 | 0.084 | 0.011 | 0.497 | 0.500 | LogisticRegression |
19 | 0.035 | 0.005 | 0.539 | 0.632 | DecisionTreeClassifier |
37 | 23.167 | 0.010 | 0.572 | 0.818 | CatBoostClassifier |
186 | 0.516 | 0.038 | 0.615 | 0.774 | RandomForestClassifier |
Расчитаем и визуализируем показатели метрик для модели RandomForestClassifier (продемонстрировавшей лучшие результаты) на валидационном наборе данных
final_estimator = rs.best_estimator_._final_estimator
res_disbalance = metric_info(features_valid, target_valid, final_estimator)
ROC-AUC 0.857 precision recall f1-score support 0 0.90 0.88 0.89 1274 1 0.58 0.64 0.61 326 accuracy 0.83 1600 macro avg 0.74 0.76 0.75 1600 weighted avg 0.84 0.83 0.83 1600
Вывод
Проведено моделирование с использованием следующих алгоритмов:
Лучшие показатели продемонстрировала модель RandomForestClassifier
Модель продемонстрировала хорошее значение метрик ROC-AUC и F1-score (для отрицательных случаев, когда клиент остается с банком), но при этом демонстрирует плохую прогнозную способность по определению интересующих бизнес положительных случаев (когда клиент уходит) - низкие значения метрик F1-score и recall. Предположительно причиной такого поведения являтся критичный дисбаланс классов.
CreditScore
)features_train_downsampled, target_train_downsampled = downsample(features_train, target_train, 0.3)
frame_shape({'train_downsampled': [features_train_downsampled, target_train_downsampled]})
distribution_viz(features_train_downsampled)
balance_viz(target_train_downsampled['Exited'], 'target_train_downsampled')
Размер выборок train_downsampled: features (2833, 12), target (2833, 1)
Расчитаем и визуализируем показатели метрик
model_down = rs.best_estimator_.fit(features_train_downsampled, target_train_downsampled)
res_down = metric_info(features_valid, target_valid, model_down[0])
ROC-AUC 0.856 precision recall f1-score support 0 0.93 0.79 0.86 1274 1 0.49 0.76 0.59 326 accuracy 0.79 1600 macro avg 0.71 0.78 0.72 1600 weighted avg 0.84 0.79 0.80 1600
Использование методики downsampling привело к увеличению значения метрики recall для положительного класса, но при этом понизилось значение метрики precision, как результат, значение метрики F1-score для положительного класса по-прежнему низкое. Попробуем применить методику upsampling.
CreditScore
)features_train_upsampled, target_train_upsampled = upsample(features_train, target_train, 4)
frame_shape({'train_upsampled': [features_train_upsampled, target_train_upsampled]})
distribution_viz(features_train_upsampled)
balance_viz(target_train_upsampled, 'target_train_upsampled')
Размер выборок train_upsampled: features (10312, 12), target (10312,)
Расчитаем и визуализируем показатели метрик
model_up = rs.best_estimator_.fit(features_train_upsampled, target_train_upsampled)
res_up = metric_info(features_valid, target_valid, model_up[0])
ROC-AUC 0.861 precision recall f1-score support 0 0.92 0.86 0.89 1274 1 0.56 0.72 0.63 326 accuracy 0.83 1600 macro avg 0.74 0.79 0.76 1600 weighted avg 0.85 0.83 0.84 1600
Применение методики upsampling позволило найти точку равновесия во всех трех метриках для положительного класса. Для более наглядного сравнения сгруппируем метрики по трем экспериментам в одну таблицу
table = [res_disbalance, res_down, res_up]
df_table = pd.DataFrame(table, index=['model_disbalance', 'model_down', 'model_up'], columns=['roc_auc', 'precision', 'recall', 'f1'])
df_table
roc_auc | precision | recall | f1 | |
---|---|---|---|---|
model_disbalance | 0.857 | 0.578 | 0.638 | 0.607 |
model_down | 0.856 | 0.486 | 0.761 | 0.593 |
model_up | 0.861 | 0.562 | 0.718 | 0.630 |
Вывод
Для устранения дисбаланса данных реализованы методики downsampling и upsampling. Более качественные результаты получены после применения upsampling.
Будем работать с обединенным набором данных "обучающий + валидационный".
CreditScore
)features_train_valid_upsampled, target_train_valid_upsampled = upsample(features_train_valid, target_train_valid, 4)
frame_shape({'train_valid_upsampled': [features_train_valid_upsampled, target_train_valid_upsampled]})
distribution_viz(features_train_valid_upsampled)
balance_viz(target_train_valid_upsampled, 'target_train_valid_upsampled')
Размер выборок train_valid_upsampled: features (12890, 12), target (12890,)
result_model_up = rs.best_estimator_.fit(features_train_valid_upsampled, target_train_valid_upsampled)
res = metric_info(features_test, target_test, result_model_up[0])
ROC-AUC 0.866 precision recall f1-score support 0 0.92 0.84 0.88 1593 1 0.54 0.73 0.62 407 accuracy 0.82 2000 macro avg 0.73 0.78 0.75 2000 weighted avg 0.85 0.82 0.83 2000
Т.к. после переобучения модели на объединенных данных повысилось количество ошибок I рода (отрицательные примеры, неверно классифицированные как положительные), есть смысл поиска оптимального порога классификации.
probabilities_valid = result_model_up.predict_proba(features_test)
probabilities_one_valid = probabilities_valid[:, 1]
for threshold in np.arange(0, 1.1, 0.1):
predicted_valid = probabilities_one_valid > threshold
precision = precision_score(target_test, predicted_valid)
recall = recall_score(target_test, predicted_valid)
roc_auc = roc_auc_score(target_test, predicted_valid)
print("Порог = {:.2f} | Точность = {:.3f}, Полнота = {:.3f}, ROC-AUC = {:.3f}".format(
threshold, precision, recall, roc_auc))
Порог = 0.00 | Точность = 0.203, Полнота = 1.000, ROC-AUC = 0.500 Порог = 0.10 | Точность = 0.221, Полнота = 0.993, ROC-AUC = 0.551 Порог = 0.20 | Точность = 0.269, Полнота = 0.978, ROC-AUC = 0.650 Порог = 0.30 | Точность = 0.347, Полнота = 0.904, ROC-AUC = 0.735 Порог = 0.40 | Точность = 0.437, Полнота = 0.821, ROC-AUC = 0.775 Порог = 0.50 | Точность = 0.538, Полнота = 0.730, ROC-AUC = 0.785 Порог = 0.60 | Точность = 0.659, Полнота = 0.600, ROC-AUC = 0.760 Порог = 0.70 | Точность = 0.774, Полнота = 0.462, ROC-AUC = 0.714 Порог = 0.80 | Точность = 0.873, Полнота = 0.322, ROC-AUC = 0.655 Порог = 0.90 | Точность = 0.960, Полнота = 0.118, ROC-AUC = 0.558 Порог = 1.00 | Точность = 0.000, Полнота = 0.000, ROC-AUC = 0.500
Наиболее оптимальным видится порог 0.6. Проверим как это повлияет на значения матрицы ошибок.
threshold = 0.6
result_thr = ((result_model_up.predict_proba(features_test)[:, 1]) > threshold).astype(int)
matrix = confusion_matrix(target_test, result_thr)
dataframe = pd.DataFrame(matrix, index=['tenure_0', 'tenure_1'], columns=['tenure_0', 'tenure_1'])
sns.heatmap(dataframe, annot=True, cbar=None, cmap='Blues', fmt='.4g')
plt.title('Матрица ошибок');
plt.tight_layout();
plt.ylabel('Истинный класс');
plt.xlabel('Пердсказанный класс');
plt.show();
Мы вернулись к показателям, которые можно считать оптимальными для поставленной бизнес-задачи.
Вывод
Произведена проверка объединенной модели (обучающая + валидационная выборки) на отложенной выборке (тестовые данные). Результаты показали хорошие показатели целевых метрик F1-score и ROC-AUC. При этом обнаружено ухудшение ситуации по ошибкам I рода (отрицательные примеры, неверно классифицированные как положительные) - их количество увеличилось. Для исправления ситуации была произведена операция подбора оптимального порога классификации.
Проанализирован набор данных банк "Бета-Банк". Перед исследованием стояла задача построения классификационной модели по прогнозированию оттока клиентов (бинарная классификация).
В качестве метрики было обозначено F1-мера >= 0.59 на отложенной выборке. Дополнительно требовалось измерять AUC-ROC.
В процессе анализа были выявлены следующие особенности данных:
Tenure
Exited
Surname
, CustomerId
, RowNumber
После предобработки набор данных был разбит на обучающую, валидационную и тестовую выборки. Были отмасштабированы признаки имеющие большой разброс значений.
На обучающей выборке алгоритмы обучались выявлять зависимости в данных. В алгоритме по подбору гиперпараметров использовалась стратегия кросс-валидации, которая позволяет минимизировать проблему переобучения алгоритма. На отложенной тестовой выборке проверялась работа алгоритма, который, в сравнении с другими, показал лучшие результаты в процессе обучения.
В процессе моделирования были использованы следующие алгоритмы:
Для алгоритмов задавались диапазоны гиперпараметров для нахождения их оптимальных значений.
Лучшие показатели продемонстрировала модель RandomForestClassifier.
Т.к. изначально в данных была выявлена проблема дисбаланса, были опробированы методы его устранения: downsampling и upsampling. Метод upsampling продемонстрировал более качественные результаты.
Победившая модель после применения методики upsampling продемонстрировала на отложенной выборке значение F1-меры равное 0.62, что больше чем требуемый baseline (0.59). Помимо F1-меры были проанализированы метрики: AUC-ROC, Precision, Recall. В результате анализа было выявлено, что метрика AUC-ROC может демонстрировать соразмерно одинаковые результаты, в то время как по метрикам Precision и Recall могут наблюдаться критичные проблемы в связи с наличием ошибок I и II рода. Обе можно считать критичными для поставленной бизнес задачи. В связи с этим для выбора оптимального классификатора был осуществлен подбор порога алгоитма для более качественного определения класса целевого признака.
Несмотря на хорошие показатели целевых метрик, есть задел для улучшения модели, т.к. результаты показали:
Для получившейся модели наиболее важными оказались признаки "Возраст" и "Количество продуктов", "Баланс".