воскресенье, 20 октября 2024 г.

Соревнование на Kaggle : классификация с "Человеческим обучением" по Глебу Михайлову

     Решил совместить две задачи : прослушать курс на Stepik DS Глеба Михайлова и принять участие в соревновании по классификации на Kaggle. Задача соревнования : кредитный скоринг, по нескольким параметрам оценить платежеспособность клиента и выдать рекомендацию дать или не дать ему кредит. Соревнование называется Loan Approval Prediction (Прогноз одобрения кредита).

Используемые библиотеки :

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
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

id01234
person_age3722293022
person_income3500056000288007000060000
person_home_ownershipRENTOWNOWNRENTRENT
person_emp_length0.06.08.014.02.0
loan_intentEDUCATIONMEDICALPERSONALVENTUREMEDICAL
loan_gradeBCABA
loan_amnt600040006000120006000
loan_int_rate11.4913.358.911.116.92
loan_percent_income0.170.070.210.170.1
cb_person_default_on_fileNNNNN
cb_person_cred_hist_length1421053
loan_status00000

Описание переменных

person_income : доход человека;
 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_rateloan_gradeloan_percent_income
count58645.0000005864558645.000000
uniqueNaN7NaN
topNaNANaN
freqNaN20984NaN
mean10.677874NaN0.159238
std3.034697NaN0.091692
min5.420000NaN0.000000
25%7.880000NaN0.090000
50%10.750000NaN0.140000
75%12.990000NaN0.210000
max23.220000NaN0.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'])


countmean
loan_int_rate_group
(5.419, 7.51]106360.042027
(7.51, 9.99]86160.069058
(9.99, 11.49]94260.099936
(11.49, 13.49]97700.127636
(13.49, 23.22]84680.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'])

countmean
loan_int_rate_group
(0.0, 7.5]89100.039506
(7.5, 10.0]106810.067316
(10.0, 11.5]90870.100473
(11.5, 13.5]97700.127636
(13.5, inf]84680.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'])

countmean
loan_percent_income_group
(-0.001, 0.08]108500.065161
(0.08, 0.12]88550.072388
(0.12, 0.17]102760.087583
(0.17, 0.23]79100.120101
(0.23, 0.83]90250.385817

train_full['loan_percent_income_group']=pd.cut(train_full['loan_percent_income'],[-0.1,0.08,0.12,0.17,0.23,1])
train_full.groupby('loan_percent_income_group')['loan_status'].agg(['count','mean'])


countmean
loan_percent_income_group
(-0.1, 0.08]108500.065161
(0.08, 0.12]88550.072388
(0.12, 0.17]102760.087583
(0.17, 0.23]79100.120101
(0.23, 1.0]90250.385817

Третья переменная категориальная и видно, что процент одобрений увеличивается от категории A к категории G

train_full.groupby('loan_grade')['loan_status'].agg(['count','mean'])

countmean
loan_grade
A168400.049525
B162810.101898
C88120.134249
D40440.598417
E7920.619949
F1180.584746
G290.827586


Наша "Человеческая модель" будет представлять средние значения целевой переменной по группировке по трем показателям

model = train_full.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)

model.head()

loan_int_rate_grouploan_gradeloan_percent_income_groupscore_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


Соединяем модель с обучающими данными

train_full = train_full.merge(model,how='left',on=['loan_int_rate_group','loan_grade','loan_percent_income_group'])

train_full.head().T



01234
id221821910522382298126246
person_age2331352522
person_income60000420003400012000060000
person_home_ownershipMORTGAGEMORTGAGERENTMORTGAGEMORTGAGE
person_emp_length7.07.04.03.06.0
loan_intentDEBTCONSOLIDATIONPERSONALPERSONALVENTUREMEDICAL
loan_gradeAADAA
loan_amnt650050006400100006000
loan_int_rate8.327.5117.495.995.99
loan_percent_income0.110.120.190.080.1
cb_person_default_on_fileNNYNN
cb_person_cred_hist_length39533
loan_status00000
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_3f0.0164940.0164940.5784920.0128010.012582


Проверяем наличие пропусков

train_full.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: float64

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

roc_auc_score(train_full['loan_status'],train_full['score_3f'])

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: float64

roc_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%, конечно что с такой точность нельзя рассчитывать выиграть
соревнования или хотя бы быть в первой сотне, но в так сказать в учебных целях
стоит попробовать.

Загружаем тестовые данные и смотрим пропуски

df_test = pd.read_csv('test.csv')
df_test.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
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()

idloan_status
0586451.000000
1586460.087179
2586470.719298
3586480.029263
4586490.436242

Записываем файл с решением и отправляем на Kaggle

df_subm.to_csv('class00.csv', index=False)

Получаем оценку 0.88816 и 1533 место из 2477. По крайней мере не позорно. В
следующей статье перейдем вместе с Глебом Михайловым от 
"Человеческого обучения", к машинному.




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

Отправить комментарий