Machine Learning

Введение: соревнование от финансовой группы Home Credit по определению риска дефолта заемщика

Время прочтения: 22 мин.

В статье рассматривается соревнование по машинному обучению «Home Credit Default Risk», цель которого – использовать исторические данные о заявках на получение кредита, чтобы предсказать, сможет ли заявитель погасить ссуду (определить риск дефолта заемщика). Прогнозирование того, вернет ли клиент ссуду или столкнется с трудностями, является критически важной бизнес-задачей, и Home Credit проводит конкурс на платформе Kaggle, чтобы увидеть, какие модели машинного обучения, способные помочь им в решении этой задачи, может разработать сообщество.

Это стандартная задача классификации с учителем:

  • Обучение с учителем: верные ответы включаются в состав обучающих данных, и цель состоит в том, чтобы обучить модель предсказывать эти ответы на основе имеющихся признаков.
  • Задача классификации: для каждого набора признаков необходимо предсказать ответ, который представляет собой двоичную переменную со значениями – 0 (заемщик выплатит кредит вовремя) или 1 (возникнут трудности с погашением кредита).

Данные

Данные предоставлены финансовой группой Home Credit, предлагающей кредитные линии (ссуды) населению, не охваченному банковскими услугами. Всего имеется 7 различных источников данных:

  • application_train / application_test: основные данные для обучения и тестирования с информацией о каждой кредитной заявке в Home Credit. Каждая ссуда представлена отдельной строкой, в которой признак SK_ID_CURR является уникальным идентификатором. Данные обучающей выборки имеют метку TARGET со значениями:
    • 0, если ссуда была возвращена;
    • 1, если ссуда не была погашена.
  • bureau: данные о предыдущих кредитах клиента в других финансовых учреждениях. Каждый предыдущий кредит в этом файле представлен отдельной строкой, при этом для каждой записи в обучающей выборке может иметься несколько записей о предыдущих кредитах.
  • bureau_balance: ежемесячные данные о предыдущих кредитах. Каждая строка представляет собой данные за один месяц срока действия предыдущего кредита. При этом каждый предыдущий кредит может иметь несколько строк, по одной на каждый месяц продолжительности кредита.
  • previous_application: предыдущие заявки на получение ссуд в Home Credit для клиентов, данные которых имеются в обучающей выборке. Каждая текущая ссуда в обучающей выборке может иметь несколько предыдущих ссуд, каждая из которых представлена в файле одной строкой и идентифицируется признаком SK_ID_PREV.
  • POS_CASH_BALANCE: исторические ежемесячные данные о покупках и выдаче наличных денег для клиентов, обслуживающихся в Home Credit. Каждая строка представляет собой данные за один месяц, при этом каждая предыдущая ссуда может иметь несколько строк в данном файле.
  • credit_card_balance: ежемесячные данные о предыдущих кредитных картах, которые клиенты получили в Home Credit. Каждая строка представляет собой информацию о балансе кредитной карты за один месяц. При этом одна кредитная карта может быть представлена несколькими строками.
  • installments_payment: история платежей по предыдущим кредитам в Home Credit, содержит по одной строке для каждого совершенного и пропущенного платежа.

Эта диаграмма показывает, как данные связаны между собой:

Кроме того, в рамках соревнования предоставлены описания всех признаков (в файле HomeCredit_columns_description.csv) и пример результирующего файла с предсказанными ответами.

В рамках данной статьи я буду использовать только основные данные для обучения и тестирования (application_train / application_test), который должны быть более понятными. Это позволит установить базовый уровень, который можно постепенно улучшать. В подобных соревнованиях лучше постепенно осмысливать проблему, чем сразу полностью погрузиться в нее и запутаться! Однако если вы хотите иметь хоть какую-то надежду на серьезный результат, в дальнейшем нужно будет использовать все данные.

Метрика: ROC AUC

Как только вы разберетесь с данными (в этом очень помогает прочтение описания признаков), следующим шагом должно стать понимание метрике, по которой оценивается работа. В данном случае это общепринятая метрика классификации, известная как площадь под кривой ошибок (ROC AUC, также иногда называемая AUROC).

Метрика ROC AUC может показаться пугающей, но она относительно проста, если разобраться с двумя отдельными концепциями.

Кривая ошибок (ROC) отображает соотношение между долей объектов от общего количества носителей признака, верно классифицированных как несущие признак, и долей объектов от общего количества объектов, не несущих признака, ошибочно классифицированных как несущие признак:

Отдельные линии на графике обозначают кривую для одной модели, а движение вдоль линии указывает на изменение порога, используемого для классификации положительных экземпляров. Порог начинается с 0 в правом верхнем углу и переходит в 1 в левом нижнем углу. Кривая, которая находится левее и выше других, указывает на лучшую модель. Например, модель, представленная синей линией, лучше красной, которая, в свою очередь, лучше черной (эта диагональная линия указывает на наивную модель случайного угадывания).

Площадь под кривой (AUC) уже содержит объяснение в своем названии. Это просто область под кривой ROC (интеграл кривой). Этот показатель находится в диапазоне от 0 до 1, при этом лучшая модель получает более высокий балл. Модель, которая просто угадывает результат случайным образом, будет иметь ROC AUC = 0,5.

Когда мы оцениваем классификатор в соответствии с метрикой ROC AUC, мы генерируем не точные прогнозы 0 или 1, а скорее вероятность от 0 до 1. Это может сбивать с толку, потому что обычно мы предпочитаем думать с точки зрения точности, однако, когда мы сталкиваемся с проблемой несбалансированных классов (далее мы увидим, что это так) точность — не лучшая метрика. Например, если бы я хотел построить модель, которая могла бы обнаруживать террористов с точностью 99,9999%, я бы просто сделал модель, предсказывающую, что каждый человек не является террористом. Ясно, что это будет неэффективно (полнота будет равна нулю), поэтому мы будем использовать более сложные показатели, такие как ROC AUC или оценка F1, чтобы более точно отразить эффективность классификатора. Модель с высоким значением ROC AUC также будет иметь высокую точность, но кроме этого ROC AUC лучше отражает и другие характеристики модели.

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

Импорт

Я буду использовать типичный стек библиотек для работы с данными: numpy и pandas для манипулирования данными, sklearn preprocessing для работы с категориальными переменными, matplotlib и seaborn для построения графиков и диаграмм. Также импортирую дополнительные модули для упрощения работы.

import os
import numpy as np
import pandas as pd
pd.set_option('display.max_columns', None)
from sklearn.preprocessing import LabelEncoder
import matplotlib.pyplot as plt
import seaborn as sns

# Подавление предупреждений
import warnings
warnings.filterwarnings('ignore')

Чтение данных

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

# Список доступных файлов
print(os.listdir("../input/"))

[‘POS_CASH_balance.csv’, ‘bureau_balance.csv’, ‘application_train.csv’, ‘previous_application.csv’, ‘installments_payments.csv’, ‘credit_card_balance.csv’, ‘sample_submission.csv’, ‘application_test.csv’, ‘bureau.csv’]

# Тренировочные данные
app_train = pd.read_csv('../input/application_train.csv')
print('Training data shape: ', app_train.shape)
app_train.head()

Training data shape: (307511, 122)

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

# Тестовые данные
app_test = pd.read_csv('../input/application_test.csv')
print('Testing data shape: ', app_test.shape)
app_test.head()

Testing data shape: (48744, 121)

Тестовых данных значительно меньше, и в них отсутствует столбец TARGET.

Исследовательский анализ данных (Exploratory Data Analysis – EDA)

Исследовательский анализ данных (EDA) — это открытый процесс, в ходе которого необходимо рассчитывать статистики и строить графики, чтобы найти тенденции, аномалии, закономерности или взаимосвязи в данных. Цель EDA — узнать, что могут нам сказать наши данные. Обычно он начинается с общего обзора, а затем сужается к конкретным областям, когда мы находим интересные области данных. Результаты могут быть интересны сами по себе, или их можно использовать для принятия решения о выборе модели, например, помогая нам решить, какие признаки использовать.

Изучим распределение целевой метки

Цель — это то, что нас просят предсказать: либо 0, если ссуда была выплачена вовремя, либо 1, если у клиента возникли трудности с оплатой. Сначала мы можем изучить количество кредитов, попадающих в каждую категорию.

app_train['TARGET'].value_counts()
app_train['TARGET'].astype(int).plot.hist();

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

Проверим пропущенные значения

Теперь мы можем посмотреть количество и процент пропущенных значений в каждом столбце.

# Функция для расчета пропущенных значений в столбцах датафрейма
def missing_values_table(df):
        # Общее количество пропущенных значений
        mis_val = df.isnull().sum()
        # Доля пропущенных значений
        mis_val_percent = 100 * df.isnull().sum() / len(df)
        # Таблица с результатом расчета
        mis_val_table = pd.concat([mis_val, mis_val_percent], axis=1)
        # Переименовываем столбцы
        mis_val_table_ren_columns = mis_val_table.rename(
        columns = {0 : 'Missing Values', 1 : '% of Total Values'})
        # Сортируем по столбцу с долей пропущенных значений в порядке убывания
        mis_val_table_ren_columns = mis_val_table_ren_columns[
            mis_val_table_ren_columns.iloc[:,1] != 0].sort_values(
        '% of Total Values', ascending=False).round(1)
        # Вывод сводной информации
        print("Your selected dataframe has " + str(df.shape[1]) + " columns.\n"      
            "There are " + str(mis_val_table_ren_columns.shape[0]) +
              " columns that have missing values.")
        return mis_val_table_ren_columns

# Статистика пропущенных значений
missing_values = missing_values_table(app_train)
missing_values.head(10)

Your selected dataframe has 122 columns.

There are 67 columns that have missing values.

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

Типы данных признаков

Настало время посмотреть на количество признаков каждого типа данных. int64 и float64 — числовые переменные (которые могут быть дискретными или непрерывными). Признаки с типом данных object содержат строки и являются категориальными признаками.

# Количество признаков каждого типа данных
app_train.dtypes.value_counts()

Теперь узнаем количество уникальных записей в каждом из столбцов типа object (категориальных).

# Количество уникальных значений в каждом из категориальных признаков
app_train.select_dtypes('object').apply(pd.Series.nunique, axis = 0)

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

Кодирование категориальных переменных

Прежде чем продолжить, необходимо разобраться с категориальными переменными. К сожалению, модели машинного обучения не могут работать с категориальными переменными (за исключением некоторых моделей, таких как LightGBM). Следовательно, мы должны найти способ закодировать (представить) эти переменные в виде чисел, прежде чем передавать их модели. Есть два основных способа осуществить этот процесс:

Кодирование метки (Label encoding): назначаем каждой уникальной категории в категориальной переменной целое число. Новые столбцы не создаются. Пример показан ниже:

Однопроходное кодирование (One-hot encoding): создаем новый столбец для каждой уникальной категории в категориальной переменной. Каждое наблюдение получает 1 в столбце для соответствующей категории и 0 во всех других новых столбцах.

Проблема с кодированием меток состоит в том, что оно дает категориям произвольный порядок. Значение, присвоенное каждой из категорий, является случайным и не отражает каких-либо неотъемлемых аспектов категории. В приведенном выше примере программист получает метку 4, а специалист по данным — 1, но если бы мы повторили тот же процесс снова, метки могли бы быть перевернутыми или совершенно другими. Фактическое присвоение целых чисел произвольно. Следовательно, когда мы выполняем кодирование меток, модель может использовать относительное значение функции (например, программист = 4 и специалист по данным = 1) для присвоения весов, а это совсем не то, чего мы хотим. Если у нас есть только два уникальных значения для категориальной переменной (например, мужчина / женщина), тогда кодирование метки подойдет, но для более чем двух уникальных категорий безопасным вариантом является однопроходное кодирование.

Об относительных достоинствах этих подходов ведутся споры, и некоторые модели могут без проблем работать с категориальными переменными, закодированными метками. Один из участников рассматриваемого мной соревнования – Kaggle-master Will Koehrsen, считает, что для категориальных переменных с множеством классов наиболее безопасным подходом является однопроходное кодирование, поскольку оно не навязывает произвольные значения категориям. И в этом с ним согласны многие специалисты в области машинного обучения и анализа данных. Единственным недостатком этого метода является то, что количество признаков (измерений данных) может увеличиваться из-за категориальных переменных со многими категориями. Чтобы справиться с этим, можно выполнить однопроходное кодирование с последующим применением метода PCA или другими методами уменьшения размерности, чтобы уменьшить количество измерений (при этом, все еще пытаясь сохранить полезную информацию).

В своем примере я буду использовать Label Encoding для любых категориальных переменных только с 2 категориями и One-Hot Encoding для любых категориальных переменных с более чем 2 категориями. Возможно, потребуется изменить этот подход по мере того, как мы углубимся в проект, но пока можно посмотреть, к чему это приведет. Я также не буду использовать какие-либо методы уменьшение размерности.

Label Encoding и One-Hot Encoding

Реализуем описанные выше методы: для любой категориальной переменной (dtype == object) с двумя уникальными категориями я буду использовать кодирование меток, а для любой категориальной переменной с более чем двумя уникальными категориями – однопроходное кодирование.

Для кодирования меток воспользуюсь методом LabelEncoder из библиотеки Scikit-Learn, а для однопроходного кодирования – функцией pandas get_dummies(df).

# Создаем объект label encoder
le = LabelEncoder()
le_count = 0
# Проходим по всем столбцам
for col in app_train:
    if app_train[col].dtype == 'object':
        # Если признак имеет 2 или менее уникальных значения
        if len(list(app_train[col].unique())) <= 2:
            # Обучаем LabelEncoder на тренировочных данных
            le.fit(app_train[col])
            # Трансформируем обучающий и тестовый датафреймы
            app_train[col] = le.transform(app_train[col])
            app_test[col] = le.transform(app_test[col])
            
            # Подсчитываем, сколько признаков обработано методом LabelEncoder
            le_count += 1
print('%d columns were label encoded.' % le_count)

3 columns were label encoded.

# Применяем one-hot encoding к категориальным признакам
app_train = pd.get_dummies(app_train)
app_test = pd.get_dummies(app_test)
print('Training Features shape: ', app_train.shape)
print('Testing Features shape: ', app_test.shape)

Training Features shape: (307511, 243)

Testing Features shape: (48744, 239).

Выравнивание данных обучения и тестирования

В данных для обучения и тестирования должны быть одни и те же признаки (столбцы). Однопроходное кодирование создало больше столбцов в обучающих данных, потому что в них имелись категориальные переменные с категориями, не представленными в данных тестирования. В связи с этим необходимо выровнять фреймы данных. Сначала извлекаем целевой столбец из данных обучения (потому что его нет в данных тестирования, но нужно сохранить эту информацию). При выполнении выравнивание необходимо убедиться, что установлен параметр axis = 1, чтобы выровнять фреймы данных на основе столбцов, а не строк!

train_labels = app_train['TARGET']
# Выравниваем тренировочный и тестовый датафреймы, сохраняем только признаки, имеющиеся в обеих таблицах
app_train, app_test = app_train.align(app_test, join = 'inner', axis = 1)
# Возвращаем метку с ответами обратно
app_train['TARGET'] = train_labels
print('Training Features shape: ', app_train.shape)
print('Testing Features shape: ', app_test.shape)

Training Features shape:  (307511, 240)

Testing Features shape:  (48744, 239)

Теперь наборы данных для обучения и тестирования имеют одинаковые признаки, которые требуются для машинного обучения. Количество признаков значительно выросло за счет «однопроходного» кодирования. В какой-то момент вы, вероятно, захотите попробовать уменьшить размерность (удалить признаки, которые не имеют отношения к делу), чтобы уменьшить размер наборов данных.

Возвращаемся к исследовательскому анализу данных

Аномалии

Одна из проблем, на которую вы всегда должны обращать внимание при выполнении EDA, — это аномалии в данных. Они могут возникнуть из-за неправильно набранных чисел, ошибок в измерительном оборудовании или они могут являться достоверными, но экстремальными измерениями. Одним из способов поиска аномалий является просмотр статистики столбца с помощью метода describe. Числа в столбце DAYS_BIRTH отрицательны, поскольку они записаны относительно текущей заявки на получение кредита. Чтобы увидеть эту статистику в годах, мы можем умножить столбец на -1 и разделить на количество дней в году:

(app_train['DAYS_BIRTH'] / -365).describe()

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

app_train['DAYS_EMPLOYED'].describe()

Это выглядит неправильно – максимальное значение (кроме того, что оно положительное) — около 1000 лет!

app_train['DAYS_EMPLOYED'].plot.hist(title = 'Days Employment Histogram');
plt.xlabel('Days Employment');

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

anom = app_train[app_train['DAYS_EMPLOYED'] == 365243]
non_anom = app_train[app_train['DAYS_EMPLOYED'] != 365243]
print('The non-anomalies default on %0.2f%% of loans' % (100 * non_anom['TARGET'].mean()))
print('The anomalies default on %0.2f%% of loans' % (100 * anom['TARGET'].mean()))
print('There are %d anomalous days of employment' % len(anom))

The non-anomalies default on 8.66% of loans

The anomalies default on 5.40% of loans

There are 55374 anomalous days of employment

Это выглядит очень интересно – оказывается, аномалии имеют меньшую частоту дефолта.

Обработка аномалий зависит от конкретной ситуации и не имеет установленных правил. Один из самых безопасных подходов — просто установить для аномальных значений пропуски, а затем заполнить их перед машинным обучением. В этом случае, поскольку все аномалии имеют одинаковое значение, можно заполнить их одинаковым значением на случай, если все эти ссуды имеют что-то общее. Возможно, аномальные значения имеют некоторую важность, поэтому желательно сообщить модели машинного обучения, действительно ли мы сами заполнили эти значения. В качестве решения заполним аномальные значения не числом (np.nan), а затем создадим новый логический столбец, указывающий, было ли значение аномальным.

# Создаем признак, показывающий наличие аномального значения
app_train['DAYS_EMPLOYED_ANOM'] = app_train["DAYS_EMPLOYED"] == 365243
# Заполняем аномальные значения значением nan
app_train['DAYS_EMPLOYED'].replace({365243: np.nan}, inplace = True)
app_train['DAYS_EMPLOYED'].plot.hist(title = 'Days Employment Histogram');
plt.xlabel('Days Employment');

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

Чрезвычайно важное замечание: все, что мы делаем с данными обучения, необходимо делать и с данными тестирования. Обязательно создайте новый столбец и заполните аномальные значения np.nan в данных тестирования.

app_test['DAYS_EMPLOYED_ANOM'] = app_test["DAYS_EMPLOYED"] == 365243
app_test["DAYS_EMPLOYED"].replace({365243: np.nan}, inplace = True)
print('There are %d anomalies in the test data out of %d entries' % (app_test["DAYS_EMPLOYED_ANOM"].sum(), len(app_test)))

There are 9274 anomalies in the test data out of 48744 entries

Корреляции

Теперь, когда мы разобрались с категориальными переменными и выбросами, продолжим работу с EDA. Один из способов попытаться понять данные — это поиск корреляций между признаками и целью. Мы можем рассчитать коэффициент корреляции Пирсона между каждой переменной и целью, используя метод фрейма данных .corr.

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

  • .00–0.19 «очень слабый»
  • .20-.39 «слабый»
  • .40–0.59 «умеренный»
  • 0,60–0,79 «сильный»
  • 0,80–1,0 «очень сильный»
# Найдем корреляцию признаков с целевой меткой и отсортируем результат
correlations = app_train.corr()['TARGET'].sort_values()
# Отобразим результат вычислений
print('Most Positive Correlations:\n', correlations.tail(15))
print('\nMost Negative Correlations:\n', correlations.head(15))

Посмотрим на некоторые из наиболее значимых корреляций: у признака DAYS_BIRTH — самая положительная корреляция (кроме TARGET, потому что корреляция переменной с самой собой всегда равна 1). Если посмотреть в описании, DAYS_BIRTH — это возраст клиента в отрицательных днях на момент выдачи кредита. Корреляция положительная, но значение этого признака на самом деле отрицательное, что означает, что по мере того, как клиент становится старше, он с меньшей вероятностью не выплатит свой кредит (т.е. цель == 0). Это немного сбивает с толку, поэтому стоит взять абсолютное значение признака, чтобы корреляция стала отрицательной.

Влияние возраста на погашение

app_train['DAYS_BIRTH'] = abs(app_train['DAYS_BIRTH'])
app_train['DAYS_BIRTH'].corr(app_train['TARGET'])

-0.07823930830982694

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

Стоит подробнее проанализировать эту переменную. Во-первых, можно построить гистограмму возраста. Чтобы сделать график более понятным, проведем ось x через годы.

plt.style.use('fivethirtyeight')
# Построим распределение по возрасту в годах
plt.hist(app_train['DAYS_BIRTH'] / 365, edgecolor = 'k', bins = 25)
plt.title('Age of Client'); plt.xlabel('Age (years)'); plt.ylabel('Count');

Само по себе возрастное распределение не говорит нам ничего, кроме того, что в нем отсутствуют явные выбросы, поскольку все возрасты выглядят разумно. Чтобы визуализировать влияние возраста на цель, мы построим график оценки плотности ядра (KDE), раскрашенный значением цели. График оценки плотности ядра показывает распределение одной переменной и может рассматриваться как сглаженная гистограмма (она создается путем вычисления ядра, обычно гауссовского, в каждой точке данных, а затем усреднения всех отдельных ядер для получения единой сглаженной кривой). Для этого графика мы будем использовать seaborn kdeplot.

plt.figure(figsize = (10, 8))
sns.kdeplot(app_train.loc[app_train['TARGET'] == 0, 'DAYS_BIRTH'] / 365, label = 'target == 0')
sns.kdeplot(app_train.loc[app_train['TARGET'] == 1, 'DAYS_BIRTH'] / 365, label = 'target == 1')
plt.xlabel('Age (years)'); plt.ylabel('Density'); plt.title('Distribution of Ages');

Кривая target == 1 смещена в сторону младшего конца диапазона. Хотя это несущественная корреляция (коэффициент корреляции -0,07), эта переменная, вероятно, будет полезна в модели машинного обучения, поскольку она действительно влияет на цель. Давайте посмотрим на эту взаимосвязь с другой стороны: средняя непогашенная задолженность по возрастным группам.

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

age_data = app_train[['TARGET', 'DAYS_BIRTH']]
age_data['YEARS_BIRTH'] = age_data['DAYS_BIRTH'] / 365
age_data['YEARS_BINNED'] = pd.cut(age_data['YEARS_BIRTH'], bins = np.linspace(20, 70, num = 11))
age_data.head(10)
# Группируем по ячейкам и считаем среднее значение
age_groups  = age_data.groupby('YEARS_BINNED').mean()
age_groups
plt.figure(figsize = (8, 8))
plt.bar(age_groups.index.astype(str), 100 * age_groups['TARGET'])
plt.xticks(rotation = 75); plt.xlabel('Age Group (years)'); plt.ylabel('Failure to Repay (%)')
plt.title('Failure to Repay by Age Group');

Прослеживается четкая тенденция: молодые заемщики чаще не возвращают кредит. Уровень невозврата составляет более 10% для трех младших возрастных групп и ниже 5% для самой старшей возрастной группы.

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

Внешние источники

Три переменных с наиболее сильной отрицательной корреляцией с целью: EXT_SOURCE_1, EXT_SOURCE_2 и EXT_SOURCE_3. Согласно документации, эти признаки представляют собой «нормализованную оценку из внешнего источника данных». У меня нет полной уверенности, что именно это означает, но, возможно, это совокупный кредитный рейтинг, рассчитанный с использованием многочисленных источников данных.

Давайте посмотрим на эти переменные.

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

ext_data = app_train[['TARGET', 'EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3', 'DAYS_BIRTH']]
ext_data_corrs = ext_data.corr()
ext_data_corrs
plt.figure(figsize = (8, 6))
# Тепловая карта корреляций
sns.heatmap(ext_data_corrs, cmap = plt.cm.RdYlBu_r, vmin = -0.25, annot = True, vmax = 0.6)
plt.title('Correlation Heatmap');

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

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

plt.figure(figsize = (10, 12))
for i, source in enumerate(['EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3']):
    plt.subplot(3, 1, i + 1)
    sns.kdeplot(app_train.loc[app_train['TARGET'] == 0, source], label = 'target == 0')
    sns.kdeplot(app_train.loc[app_train['TARGET'] == 1, source], label = 'target == 1')
    plt.title('Distribution of %s by Target Value' % source)
    plt.xlabel('%s' % source); plt.ylabel('Density');
plt.tight_layout(h_pad = 2.5)

EXT_SOURCE_3 отображает наибольшую разницу между значениями цели. Можно ясно видеть, что этот признак имеет некоторое отношение к вероятности возврата кредита заемщиком. Связь не очень сильная (на самом деле все они считаются очень слабыми), но эти признаки все равно будут полезны для модели машинного обучения, чтобы предсказать, вернет ли кандидат ссуду вовремя.

Парный график

В качестве дополнительного исследовательского сюжета мы можем построить парный график признаков EXT_SOURCE и признака DAYS_BIRTH. График пар – отличный инструмент для исследования, потому что он позволяет видеть отношения между несколькими парами признаков, а также распределения отдельных признаков. Здесь мы используем библиотеку визуализации seaborn и функцию PairGrid, чтобы создать график пар с диаграммами рассеяния в верхнем треугольнике, гистограммами на диагонали и графиками плотности ядра 2D и коэффициентами корреляции в нижнем треугольнике.

plot_data = ext_data.drop(columns = ['DAYS_BIRTH']).copy()
plot_data['YEARS_BIRTH'] = age_data['YEARS_BIRTH']
plot_data = plot_data.dropna().loc[:100000, :]

# Функция для расчета коэффициента корреляции между двумя признаками
def corr_func(x, y, **kwargs):
    r = np.corrcoef(x, y)[0][1]
    ax = plt.gca()
    ax.annotate("r = {:.2f}".format(r),
                xy=(.2, .8), xycoords=ax.transAxes,
                size = 20)

# Создаем объект парного графика
grid = sns.PairGrid(data = plot_data, size = 3, diag_sharey=False,
                    hue = 'TARGET', 
                    vars = [x for x in list(plot_data.columns) if x != 'TARGET'])
grid.map_upper(plt.scatter, alpha = 0.2)
grid.map_diag(sns.kdeplot)
grid.map_lower(sns.kdeplot, cmap = plt.cm.OrRd_r);
plt.suptitle('Ext Source and Age Features Pairs Plot', size = 32, y = 1.05);

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

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

При подготовке статьи были использованы материалы из открытых источников: источник_1, источник_2.

Советуем почитать