import numpy as np
import pandas as pd
import matplotlib
import matplotlib.pyplot as plt
import seaborn as sns
from imblearn.pipeline import make_pipeline as imb_make_pipeline
from imblearn.under_sampling import RandomUnderSampler
from imblearn.ensemble import BalancedBaggingClassifier, EasyEnsemble
from sklearn.preprocessing import Imputer, RobustScaler, FunctionTransformer
from sklearn.ensemble import RandomForestClassifier, VotingClassifier, GradientBoostingClassifier
from sklearn.model_selection import train_test_split, cross_val_score, cross_val_predict
from sklearn.metrics import (roc_auc_score, confusion_matrix, accuracy_score, roc_curve,
precision_recall_curve, f1_score)
from sklearn.pipeline import make_pipeline
df = pd.read_csv("loans.csv")
print(df.dtypes)
credit_policy int64
purpose object
int_rate float64
installment float64
log_annual_inc float64
dti float64
fico int64
days_with_cr_line float64
revol_bal int64
revol_util float64
inq_last_6mths float64
delinq_2yrs float64
pub_rec float64
not_fully_paid int64
print(df.isnull().sum())
credit_policy 0
purpose 0
int_rate 0
installment 0
log_annual_inc 4
dti 0
fico 0
days_with_cr_line 29
revol_bal 0
revol_util 62
inq_last_6mths 29
delinq_2yrs 29
pub_rec 29
not_fully_paid 0
pos = df[df["not_fully_paid"] == 1].shape[0]
neg = df[df["not_fully_paid"] == 0].shape[0]
plt.figure(figsize=(8, 6))
sns.countplot(df["not_fully_paid"])
plt.xticks((0, 1), ["Оплачено полностью", "Оплачено не полностью"])
plt.xlabel("")
plt.ylabel("Число заемщиков")
Начальные соображения и предобработка данных
Для моделирования будем применять алгоритмические композиции, представляющие собой объединения моделей в более сложную для уменьшения ошибок обобщения. Такой подход полагается на предположение, что каждая модель рассматривает различные аспекты данных, захватывая часть общей истинной картины. Сочетая независимо обученные модели, можно достичь лучших результатов, чем при использовании их одиночных экземпляров. Это приводит к более точным предсказаниям и меньшим ошибкам обобщения.
Производительность алгоритмических композиций почти всегда возрастает с ростом числа используемых моделей. Объединение максимально различных моделей уменьшает корреляцию между ними и повышает производительность композиции — коррелирующие между собой модели дают производительность идентичную или даже худшую, чем одиночная модель.
Кратко рассмотрим наиболее распространенные подходы к построению алгоритмических композиций:
Стратегии работы с пропущенными значениями
В реальных выборках встречаются пропуски данных. Это может быть вызвано тем, что клиенты не заполнили часть банковских форм, изменились сами формы и т. д. Одна из хороших практик учета отсутствующих данных — генерация бинарных функций. Такие функции принимают значение 0 или 1, указывающие на то, присутствует ли в записи значение признака или оно пропущено.
Другими распространенными практиками являются следующие подходы:
df = pd.get_dummies(df, columns=["purpose"], drop_first=True)
for feature in df.columns:
if np.any(np.isnan(df[feature])):
df["is_" + feature + "_missing"] = np.isnan(df[feature]) * 1
X = df.loc[:, df.columns != "not_fully_paid"].values
y = df.loc[:, df.columns == "not_fully_paid"].values.flatten()
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.2, shuffle=True, random_state=0, stratify=y)
print("Оригинальные размеры данных: ", X_train.shape, X_test.shape)
Оригинальные размеры данных: (7662, 24) (1916, 24)
train_indices_na = np.max(np.isnan(X_train), axis=1)
test_indices_na = np.max(np.isnan(X_test), axis=1)
X_train_dropna, y_train_dropna = X_train[~train_indices_na, :][:, :-6], y_train[~train_indices_na]
X_test_dropna, y_test_dropna = X_test[~test_indices_na, :][:, :-6], y_test[~test_indices_na]
print("После выкидывания NA: ", X_train_dropna.shape, X_test_dropna.shape)
После выкидывания NA: (7610, 18) (1906, 18)
rf_clf = RandomForestClassifier(n_estimators=500, max_features=0.25, criterion="entropy", class_weight="balanced")
pip_baseline = make_pipeline(RobustScaler(), rf_clf)
scores = cross_val_score(pip_baseline, X_train_dropna, y_train_dropna,
scoring="roc_auc", cv=10)
print("Среднее значение AUC базовой модели {}".format(scores.mean()))
Среднее значение AUC базовой модели 0.662.
rf_clf.fit(RobustScaler().fit_transform(Imputer(strategy="median").fit_transform(X_train)), y_train)
importances = rf_clf.feature_importances_
indices = np.argsort(rf_clf.feature_importances_)[::-1]
plt.figure(figsize=(12, 6))
plt.bar(range(1, 25), importances[indices], align="center")
plt.xticks(range(1, 25),
df.columns[df.columns != "not_fully_paid"][indices],
rotation=90)
plt.title("Значимость признаков")
X_train = X_train[:, :-6]
X_test = X_test[:, :-6]
Стратегии работы с несбалансированными выборками
Лучшими метриками для несбалансированных наборов данных считаются AUC (площадь под ROC-кривой) и f1-score. Но одних метрик недостаточно — классовый дисбаланс влияет на процесс обучения модели, делая ее предвзятой. В этом случае используются следующие подходы:
rf_clf = RandomForestClassifier(n_estimators=500,
max_features=0.25,
criterion="entropy",
class_weight="balanced")
pip_orig = make_pipeline(Imputer(strategy="mean"), RobustScaler(),
rf_clf)
scores = cross_val_score(pip_orig,
X_train, y_train,
scoring="roc_auc", cv=10)
print("AUC оригинальной модели: ", scores.mean())
pip_undersample = imb_make_pipeline(Imputer(strategy="mean"),
RobustScaler(),
RandomUnderSampler(),
rf_clf)
scores = cross_val_score(pip_undersample,
X_train, y_train,
scoring="roc_auc", cv=10)
print("AUC модели без большей части мажоритарных примеров: ", scores.mean())
resampled_rf = BalancedBaggingClassifier(base_estimator=rf_clf,
n_estimators=10,
random_state=0)
pip_resampled = make_pipeline(Imputer(strategy="mean"),
RobustScaler(),
resampled_rf)
scores = cross_val_score(pip_resampled,
X_train, y_train,
scoring="roc_auc", cv=10)
print("AUC модели EasyEnsemble: ", scores.mean())
AUC оригинальной модели: 0.663
AUC модели без большей части мажоритарных примеров: 0.658
AUC модели EasyEnsemble 0.671
resampled_rf.fit(X_train_dropna, y_train_dropna)
print(y_test_dropna[-3], y_test_dropna[-2])
print(resampled_rf.predict([X_test_dropna[-3]]), resampled_rf.predict([X_test_dropna[-2]]))
1 0
[1] [0]