Пишем скоринговую модель на Python

МЕНЮ


Искусственный интеллект
Поиск
Регистрация на сайте
Помощь проекту

ТЕМЫ


Новости ИИРазработка ИИВнедрение ИИРабота разума и сознаниеМодель мозгаРобототехника, БПЛАТрансгуманизмОбработка текстаТеория эволюцииДополненная реальностьЖелезоКиберугрозыНаучный мирИТ индустрияРазработка ПОТеория информацииМатематикаЦифровая экономика

Авторизация



RSS


RSS новости


Кредитный скоринг – одна из наиболее распространенных задач среди множества проблем, решаемых финансовыми аналитиками. В данной статье мы разберемся с ключевыми шагами для написания собственной скоринговой модели на Python.

Два наиболее важных вопроса кредитования:

1) Насколько рискованно поступает заемщик, беря кредит?
2) Учитывая известный риск заемщика, должны ли мы его кредитовать?

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

Вопрос, который максимально интересует банк в вопросе кредитования потребителей -- это сможет ли онона вернуть займ. Соответственно, все модели будут пытаться предугадать вероятность возврата( или дефолта) данного заемщика на основе имеющихся факторов и параметров: заработка, кредитной истории, уже имеющиеся кредиты.

В реальной жизни при построении данных моделей мы сталкиваемся с неполными данными, поэтому в данной статье мы сфокусируемся на двух моментах:

  • Что делать, если часть данных отсутствует?
  • Как строить классификаторы для несбалансированных выборок?
В качестве реальных данных воспользуемся выборкой из 9.5 тысяч записей с сайта LendingClub.com с мая 2007 года по февраль 2010 года. При этом стоить обратить максимальное внимание на тот факт, что на данный временный отрезок пришёлся пик кризиса, поэтому данные будут представлять собой так называемый "стрессовый сценарий". Поэтому для поведения заемщиков в некризисные периоды нам нужно будет найти другую выборку, скажем с 2013 по 2016 год, но при этом нам придется решать схожие проблемы с данными.

В качестве библиотек и функций импортируем следующие:

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)

В результате можно видеть, что единственным категориальным признаком является признак purpose:

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("Число заемщиков")

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

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

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

Кратко рассмотрим наиболее распространенные подходы к построению алгоритмических композиций:

  • Смешивание (blending, блендинг): усреднение прогнозов группы моделей.
  • Бэггинг (баггинг, bagging, bootstrap aggregating): независимое построение моделей на различных наборах данных с последующим выбором предсказания по результатам голосования моделей.
  • Бустинг (boosting): последовательное построение моделей, при котором каждая модель учится с учетом результатов предыдущей модели. Чтобы избежать ошибок переобучения, каждая новая модель учится на результатах всех предыдущих моделей.
  • Стекинг (stacking): построение k моделей базовых учеников с дальнейшей подгонкой модели под метаклассификатор.
В нашей модели мы будем использовать бэггинговую модель случайного леса (Random Forest). Обратим внимание, что далее в коде мы проведем предварительную обработку данных:

1) Создадим переменные признака purpose вместо используемых категориальных значений.
2) Разобьем данные на обучающую (80%) и тестовую (20%) выборки.
3) Чтобы выбросы в данных оказывали меньшее влияние на обучение, стандартизируем выборку при помощи метода RobustScaler.

Этот метод центрует данные вокруг медианы и масштабирует их с использованием межквартильного диапазона.

Стратегии работы с пропущенными значениями
В реальных выборках встречаются пропуски данных. Это может быть вызвано тем, что клиенты не заполнили часть банковских форм, изменились сами формы и т. д. Одна из хороших практик учета отсутствующих данных — генерация бинарных функций. Такие функции принимают значение 0 или 1, указывающие на то, присутствует ли в записи значение признака или оно пропущено.

Другими распространенными практиками являются следующие подходы:

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

Начнем с создания бинарных функций для отсутствующих значений, а затем вычислим показатель AUC для различных моделей на обучающей выборке:

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

Выделим в качестве целевой переменной признак not_fully_paid и разобьем данные на обучающую и тестовую выборки:

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)

Построим классификатор случайного леса и определим показатель AUC для данной модели:

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. Но одних метрик недостаточно — классовый дисбаланс влияет на процесс обучения модели, делая ее предвзятой. В этом случае используются следующие подходы:

  • Удалить часть экземпляров мажоритарного класса так, чтобы сравнять число положительных и отрицательных примеров (недостаток: существенная потеря данных.
  • Дополнить повторами миноритарные представители класса, чтобы сравнять число положительных и отрицательных примеров (недостаток: возможное переобучение на повторяющихся примерах).
  • Избирательно настроить классификатор на объекты мажоритарного/миноритарного классов.
  • Создать синтетические объекты миноритарного класса.
В большинстве приложений неправильная классификация меньшинства (ложноотрицательная классификация) стоит намного дороже ложноположительной. В контексте кредитования потеря денег из-за незаслуживающего доверия заемщика обходится существенно выше, чем отсутствие возможности кредитования надежного заемщика. Поэтому мы можем назначать этим классам различные веса и отсечки.

В качестве примера сравним результат прошлой модели случайного леса с теми, где мы либо сбалансируем выборку, удалив часть мажоритарных записей (under-sample подход), либо воспользуемся синтетическим подходом:

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

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

Такие сбалансированные классификаторы можно обучать и делать на них предсказания аналогично традиционным:

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]

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


Источник: sfeducation.ru

Комментарии: