Решил совместить две задачи : прослушать курс на Stepik DS Глеба Михайлова и принять участие в соревновании по классификации на Kaggle. Задача соревнования : кредитный скоринг, по нескольким параметрам оценить платежеспособность клиента и выдать рекомендацию дать или не дать ему кредит. Соревнование называется Loan Approval Prediction (Прогноз одобрения кредита).
Используемые библиотеки :
import numpy as np
import seaborn as sns
from sklearn.model_selection import train_test_split
from catboost import CatBoostClassifier
from catboost import Pool
from catboost import cv
from sklearn.metrics import roc_auc_score
import phik
from sklearn.metrics import log_loss
Как обычно, данные представляют собой два файла : train.csv и test.csv. Загружаем и смотрим на обучающие данные :
df = pd.read_csv('train.csv')
df.head().T
id | 0 | 1 | 2 | 3 | 4 |
---|---|---|---|---|---|
person_age | 37 | 22 | 29 | 30 | 22 |
person_income | 35000 | 56000 | 28800 | 70000 | 60000 |
person_home_ownership | RENT | OWN | OWN | RENT | RENT |
person_emp_length | 0.0 | 6.0 | 8.0 | 14.0 | 2.0 |
loan_intent | EDUCATION | MEDICAL | PERSONAL | VENTURE | MEDICAL |
loan_grade | B | C | A | B | A |
loan_amnt | 6000 | 4000 | 6000 | 12000 | 6000 |
loan_int_rate | 11.49 | 13.35 | 8.9 | 11.11 | 6.92 |
loan_percent_income | 0.17 | 0.07 | 0.21 | 0.17 | 0.1 |
cb_person_default_on_file | N | N | N | N | N |
cb_person_cred_hist_length | 14 | 2 | 10 | 5 | 3 |
loan_status | 0 | 0 | 0 | 0 | 0 |
Описание переменных :
person_home_ownership : домовладение человека;
person_emp_length : длиной занятости в годах;
loan_intent : намерение получить кредит;
loan_grade : уровень кредита;
loan_amnt : сумма кредита;
loan_int_rate : процентная ставка по кредиту;
loan_percent_income : процентный доход по кредиту
cb_person_default_on_file : это переменная, которая указывает, имел ли человек ранее дефолт. Значение 0 означает отсутствие дефолта, а значение 1 — дефолт. Дефолт происходит, когда заёмщик не может своевременно вносить платежи, пропускает платежи или прекращает вносить платежи по процентам или основному долгу.
cb_person_cred_hist_length : длина кредитной истории. Она представляет собой количество лет личной истории с момента первого взятого у заёмщика кредита
loan_status : целевая переменная, 1 - дать кредит, 0 - отказ.
Буду решать задачу соревнования вместе с Глебом Михайловым по его курсу Data Science на Stepik
Поехали !!!
Начнем с краткого анализа данных :
Количество строк
len(df)
58645
Проверяем пропуски
df.isna().mean()
id 0.0 person_age 0.0 person_income 0.0 person_home_ownership 0.0 person_emp_length 0.0 loan_intent 0.0 loan_grade 0.0 loan_amnt 0.0 loan_int_rate 0.0 loan_percent_income 0.0 cb_person_default_on_file 0.0 cb_person_cred_hist_length 0.0 loan_status 0.0 dtype: float64Пропусков нет и это прекрасно, посмотрим на целевую переменную
df['loan_status'].value_counts()
0 50295 1 8350 Name: loan_status, dtype: int64
Выведем в процентах
df['loan_status'].value_counts(normalize=True)
0 0.857618 1 0.142382 Name: loan_status, dtype: float64Видим, что процент клиентов с одобрением кредита 14,2%, это можно было сделать
по другому
df['loan_status'].mean()
0.14238212976383324Далее будем решать задачу с помощью "Человеческого обучения", как его называет
Глеб Михайлов. Для начала разбиваем наши данные на обучающие (train) валидацию
(val) и тест (test) со стратификацией, чтобы в этих данных был одинаковый процент
положительных решений.
train, test = train_test_split(df,train_size=0.6,random_state=42,stratify=df['loan_status'])
val, test = train_test_split(test,train_size=0.5,random_state=42,stratify=test['loan_status'])
Проверяем, так ли это
print(train['loan_status'].mean(),val['loan_status'].mean(),test['loan_status'].mean())
0.14238212976383324 0.14238212976383324 0.14238212976383324
Таким образом мы защищаемся от случайностей при разбиении и это особенно важно тогда, когда выборка небольшая.
Далее переходим на создание модели вручную. Для этого необходимо оценить важность показателей и в этом может помочь Phik. PhiK — это новый практичный коэффициент корреляции, основанный на нескольких усовершенствованиях проверки гипотезы Пирсона о независимости двух переменных. Мы выведем его для нашей целевой переменной и это позволит оценить важность показателей.
Так как для "человеческого обучения" валидационная выборка не нужна, будем работать на полных обучающих данных.
train_full = pd.concat([train,val])
Библиотека phik у нас импортирована поэтому просто запускаем строчку
phik_overview = train_full.phik_matrix()
Нас интересуют коэффициенты только с целевой переменной
phik_overview['loan_status'].sort_values(ascending=False)
loan_status 1.000000 loan_int_rate 0.534303 loan_grade 0.432421 loan_percent_income 0.428259 person_home_ownership 0.363694 cb_person_default_on_file 0.294463 loan_amnt 0.202300 loan_intent 0.146960 person_income 0.033794 cb_person_cred_hist_length 0.029110 person_emp_length 0.029090 person_age 0.026509 id 0.000000 Name: loan_status, dtype: float64
Для своей модели отбираем первые три показателя и посмотрим что они из себя
представляют.
df[['loan_int_rate','loan_grade','loan_percent_income']].describe(include='all')
loan_int_rate | loan_grade | loan_percent_income | |
---|---|---|---|
count | 58645.000000 | 58645 | 58645.000000 |
unique | NaN | 7 | NaN |
top | NaN | A | NaN |
freq | NaN | 20984 | NaN |
mean | 10.677874 | NaN | 0.159238 |
std | 3.034697 | NaN | 0.091692 |
min | 5.420000 | NaN | 0.000000 |
25% | 7.880000 | NaN | 0.090000 |
50% | 10.750000 | NaN | 0.140000 |
75% | 12.990000 | NaN | 0.210000 |
max | 23.220000 | NaN | 0.830000 |
Как видим, два показателя непрерывные и один категориальный, будем работать также как Глеб, последовательно. Начнем с loan_int_rate : процентная ставка по кредиту, посмотрим на ее распределение
train_full['loan_int_rate'].hist()
Как видим, это непрерывная переменная в диапазоне от 5.42 до 23.22. Для модели нам нужно разбить ее на интервалы и по ним сделать группировку с выводом доли положительных решений
train_full['loan_int_rate_group']=pd.qcut(train_full['loan_int_rate'],5)
train_full.groupby('loan_int_rate_group')['loan_status'].agg(['count','mean'])
count | mean | |
---|---|---|
loan_int_rate_group | ||
(5.419, 7.51] | 10636 | 0.042027 |
(7.51, 9.99] | 8616 | 0.069058 |
(9.99, 11.49] | 9426 | 0.099936 |
(11.49, 13.49] | 9770 | 0.127636 |
(13.49, 23.22] | 8468 | 0.407298 |
Видим, что не зря phik выбрал эту переменную, чем выше процентная ставка по кредиту, тем выше процент одобренных клиентов. Однако такой вариант деления не подходит для модели, потому как на тесте оно может быть другое. Поэтому сделаем разбиение вручную.
train_full['loan_int_rate_group'] = pd.cut(train_full['loan_int_rate'],[0,7.5,10,11.5,13.5,float('inf')])
train_full.groupby('loan_int_rate_group')['loan_status'].agg(['count','mean'])
count | mean | |
---|---|---|
loan_int_rate_group | ||
(0.0, 7.5] | 8910 | 0.039506 |
(7.5, 10.0] | 10681 | 0.067316 |
(10.0, 11.5] | 9087 | 0.100473 |
(11.5, 13.5] | 9770 | 0.127636 |
(13.5, inf] | 8468 | 0.407298 |
Проделаем эту ту же процедуру для второй непрерывной переменной.
Распределение
train_full['loan_percent_income'].hist()
Разбиваем на интервалы, сначала автоматически, потом вручную
train_full['loan_percent_income_group']=pd.qcut(train_full['loan_percent_income'],5)
train_full.groupby('loan_percent_income_group')['loan_status'].agg(['count','mean'])
count | mean | |
---|---|---|
loan_percent_income_group | ||
(-0.001, 0.08] | 10850 | 0.065161 |
(0.08, 0.12] | 8855 | 0.072388 |
(0.12, 0.17] | 10276 | 0.087583 |
(0.17, 0.23] | 7910 | 0.120101 |
(0.23, 0.83] | 9025 | 0.385817 |
count | mean | |
---|---|---|
loan_percent_income_group | ||
(-0.1, 0.08] | 10850 | 0.065161 |
(0.08, 0.12] | 8855 | 0.072388 |
(0.12, 0.17] | 10276 | 0.087583 |
(0.17, 0.23] | 7910 | 0.120101 |
(0.23, 1.0] | 9025 | 0.385817 |
count | mean | |
---|---|---|
loan_grade | ||
A | 16840 | 0.049525 |
B | 16281 | 0.101898 |
C | 8812 | 0.134249 |
D | 4044 | 0.598417 |
E | 792 | 0.619949 |
F | 118 | 0.584746 |
G | 29 | 0.827586 |
loan_int_rate_group | loan_grade | loan_percent_income_group | score_3f | |
---|---|---|---|---|
0 | (0.0, 7.5] | A | (-0.1, 0.08] | 0.012801 |
1 | (0.0, 7.5] | A | (0.08, 0.12] | 0.012582 |
2 | (0.0, 7.5] | A | (0.12, 0.17] | 0.014479 |
3 | (0.0, 7.5] | A | (0.17, 0.23] | 0.019541 |
4 | (0.0, 7.5] | A | (0.23, 1.0] | 0.236634 |
0 | 1 | 2 | 3 | 4 | |
---|---|---|---|---|---|
id | 22182 | 19105 | 2238 | 22981 | 26246 |
person_age | 23 | 31 | 35 | 25 | 22 |
person_income | 60000 | 42000 | 34000 | 120000 | 60000 |
person_home_ownership | MORTGAGE | MORTGAGE | RENT | MORTGAGE | MORTGAGE |
person_emp_length | 7.0 | 7.0 | 4.0 | 3.0 | 6.0 |
loan_intent | DEBTCONSOLIDATION | PERSONAL | PERSONAL | VENTURE | MEDICAL |
loan_grade | A | A | D | A | A |
loan_amnt | 6500 | 5000 | 6400 | 10000 | 6000 |
loan_int_rate | 8.32 | 7.51 | 17.49 | 5.99 | 5.99 |
loan_percent_income | 0.11 | 0.12 | 0.19 | 0.08 | 0.1 |
cb_person_default_on_file | N | N | Y | N | N |
cb_person_cred_hist_length | 3 | 9 | 5 | 3 | 3 |
loan_status | 0 | 0 | 0 | 0 | 0 |
loan_int_rate_group | (7.5, 10.0] | (7.5, 10.0] | (13.5, inf] | (0.0, 7.5] | (0.0, 7.5] |
loan_percent_income_group | (0.08, 0.12] | (0.08, 0.12] | (0.17, 0.23] | (-0.1, 0.08] | (0.08, 0.12] |
score_3f | 0.016494 | 0.016494 | 0.578492 | 0.012801 | 0.012582 |
id 0.0 person_age 0.0 person_income 0.0 person_home_ownership 0.0 person_emp_length 0.0 loan_intent 0.0 loan_grade 0.0 loan_amnt 0.0 loan_int_rate 0.0 loan_percent_income 0.0 cb_person_default_on_file 0.0 cb_person_cred_hist_length 0.0 loan_status 0.0 loan_int_rate_group 0.0 loan_percent_income_group 0.0 score_3f 0.0 dtype: float64
0.8679813948006164
Проделаем всю процедуру для тестовой части
test['loan_int_rate_group'] = pd.cut(test['loan_int_rate'],[0,7.5,10,11.5,13.5,float('inf')])
test['loan_percent_income_group']=pd.cut(test['loan_percent_income'],[-0.1,0.08,0.12,0.17,0.23,1])
test = test.merge(model,how='left',on=['loan_int_rate_group','loan_grade','loan_percent_income_group'])
test.isna().mean()
id 0.000000 person_age 0.000000 person_income 0.000000 person_home_ownership 0.000000 person_emp_length 0.000000 loan_intent 0.000000 loan_grade 0.000000 loan_amnt 0.000000 loan_int_rate 0.000000 loan_percent_income 0.000000 cb_person_default_on_file 0.000000 cb_person_cred_hist_length 0.000000 loan_status 0.000000 loan_int_rate_group 0.000000 loan_percent_income_group 0.000000 score_3f 0.000512 dtype: float64Как видим, у нас есть пропуски, т.е. для некоторых сочетаний трех показателей в модели не нашлось значений.
test.score_3f.isna().sum()
6
Это произошло в 6-ти случаях, конечно это не много, но не приятно. Заменим пропуски
средним значением и оценим точность
test['score_3f'] = test['score_3f'].fillna(train_full['loan_status'].mean())
roc_auc_score(test['loan_status'],test['score_3f'])
0.8573724308019809На тесте оценка оказалась всего на 1% хуже. Для соревнования повторим всю
процедуру без разделения на обучающую и тестовую части.
df['loan_int_rate_group'] = pd.cut(df['loan_int_rate'],[0,7.5,10,11.5,13.5,float('inf')]) df['loan_percent_income_group']=pd.cut(df['loan_percent_income'],[-0.1,0.08,0.12,0.17,0.23,1])
model = df.groupby(['loan_int_rate_group','loan_grade','loan_percent_income_group'])['loan_status'].mean().reset_index() model = model.rename({'loan_status':'score_3f'},axis=1)
df = df.merge(model,how='left',on=['loan_int_rate_group','loan_grade','loan_percent_income_group'])
df.isna().mean()
id 0.0 person_age 0.0 person_income 0.0 person_home_ownership 0.0 person_emp_length 0.0 loan_intent 0.0 loan_grade 0.0 loan_amnt 0.0 loan_int_rate 0.0 loan_percent_income 0.0 cb_person_default_on_file 0.0 cb_person_cred_hist_length 0.0 loan_status 0.0 loan_int_rate_group 0.0 loan_percent_income_group 0.0 score_3f 0.0 dtype: float64roc_auc_score(df['loan_status'],df['score_3f'])
0.8660921723507949Как видно, точность получилась немного хуже, чем на тестовой части. Добавим в
модель еще две категориальные переменные person_home_ownership и
cb_person_default_on_file и посмотрим насколько изменится точность.
model = df.groupby(['loan_int_rate_group','loan_grade','loan_percent_income_group',
'person_home_ownership','cb_person_default_on_file'])
['loan_status'].mean().reset_index()
model = model.rename({'loan_status':'score_5f'},axis=1)
df = df.merge(model,how='left',on=['loan_int_rate_group','loan_grade',
'loan_percent_income_group','person_home_ownership','cb_person_default_on_file'])
roc_auc_score(df['loan_status'],df['score_5f'])
0.8925680735159565Точность выросла на 3%, конечно что с такой точность нельзя рассчитывать выиграть
соревнования или хотя бы быть в первой сотне, но в так сказать в учебных целях
стоит попробовать.
id 0.0 person_age 0.0 person_income 0.0 person_home_ownership 0.0 person_emp_length 0.0 loan_intent 0.0 loan_grade 0.0 loan_amnt 0.0 loan_int_rate 0.0 loan_percent_income 0.0 cb_person_default_on_file 0.0 cb_person_cred_hist_length 0.0 dtype: float64
Пропусков нет, проводим все процедуры, что были выше
df_test['loan_int_rate_group'] = pd.cut(df_test['loan_int_rate'],[0,7.5,10,11.5,13.5,float('inf')])
df_test['loan_percent_income_group']=pd.cut(df_test['loan_percent_income'],[-0.1,0.08,0.12,0.17,0.23,1])
df_test = df_test.merge(model,how='left',on=['loan_int_rate_group','loan_grade',
'loan_percent_income_group','person_home_ownership','cb_person_default_on_file'])
df_test.isna().mean()
id 0.000000 person_age 0.000000 person_income 0.000000 person_home_ownership 0.000000 person_emp_length 0.000000 loan_intent 0.000000 loan_grade 0.000000 loan_amnt 0.000000 loan_int_rate 0.000000 loan_percent_income 0.000000 cb_person_default_on_file 0.000000 cb_person_cred_hist_length 0.000000 loan_int_rate_group 0.000000 loan_percent_income_group 0.000000 score_5f 0.001535 dtype: float64Как видим, не для всех показателей нашлось решение, заменяем пропуски средним и
переименовываем целевой показатель
df_test = df_test.rename({'score_5f':'loan_status'},axis=1)
df_test['score_5f'] = df_test['score_5f'].fillna(df['loan_status'].mean())
Выводим индекс и целевой показатель в отдельный датафрейм
df_subm=df_test[['id','loan_status']] df_subm.head()
id | loan_status | |
---|---|---|
0 | 58645 | 1.000000 |
1 | 58646 | 0.087179 |
2 | 58647 | 0.719298 |
3 | 58648 | 0.029263 |
4 | 58649 | 0.436242 |
Записываем файл с решением и отправляем на Kaggle
df_subm.to_csv('class00.csv', index=False)
Получаем оценку 0.88816 и 1533 место из 2477. По крайней мере не позорно. В
следующей статье перейдем вместе с Глебом Михайловым от
"Человеческого обучения", к машинному.
Комментариев нет:
Отправить комментарий