Отучившись на нескольких онлайн-курсах, попробовал занять позицию, связанную с Machine Learning — на входе получил тестовое задание о кредитном скоринге. Свое решение которой здесь и привожу:
Задание
Данные содержат информацию о выданных кредитах, требуется предсказать вероятность успешного возврата кредита.
Тренировочная выборка содержится в файле train.csv, тестовая — test.csv.
Информация о значениях признаков содержится в файле feature_descr.xlsx.
Целевой признак — loan_status (бинарный). 1 означает что кредит успешно вернули.
В рамках тестового задания вам предлагается:
- Обучить модель на предоставленных данных, найти качество полученной модели.
- Записать предсказания (вероятности) для тестового набора в файл results.csv
- Продемонстрировать результаты анализа в графическом виде (ROC-curve)
Тщательный выбор фич и подбор гиперпараметров можно не проводить.
Решение
import pandas as pd
import sys
print "Python version: {}".format(sys.version)
print "Pandas version: {}".format(pd.__version__)
Python version: 2.7.13 |Anaconda 4.3.1 (64-bit)| (default, Dec 19 2016, 13:29:36) [MSC v.1500 64 bit (AMD64)]
Pandas version: 0.19.2
Тут стоит заметить что использовался Python 2.7
pd.set_option('display.max_columns', 500)
pd.set_option('display.width', 1000)
Читаем данные из train.csv, используя record_id как индекс:
train = pd.read_csv('train.csv', index_col='record_id')
train.head()
loan_amnt | term | int_rate | installment | grade | sub_grade | emp_title | emp_length | home_ownership | annual_inc | verification_status | issue_d | loan_status | pymnt_plan | purpose | zip_code | addr_state | dti | delinq_2yrs | earliest_cr_line | inq_last_6mths | mths_since_last_delinq | open_acc | pub_rec | revol_bal | revol_util | total_acc | initial_list_status | collections_12_mths_ex_med | policy_code | application_type | acc_now_delinq | tot_coll_amt | tot_cur_bal | total_rev_hi_lim | |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
record_id | |||||||||||||||||||||||||||||||||||
453246940 | 15000.0 | 36 months | 11.99 | 498.15 | B | B3 | Quality Assurance Specialist | 4 years | MORTGAGE | 70000.0 | Verified | Oct-2013 | 1 | n | debt_consolidation | 156xx | PA | 13.85 | 0.0 | Dec-1991 | 1.0 | NaN | 17.0 | 3.0 | 12540.0 | 61.2 | 32.0 | f | 0.0 | 1.0 | INDIVIDUAL | 0.0 | 0.0 | 295215.0 | 20500.0 |
453313687 | 3725.0 | 36 months | 6.03 | 113.38 | A | A1 | NaN | n/a | MORTGAGE | 52260.0 | Source Verified | Oct-2012 | 1 | n | credit_card | 339xx | FL | 19.43 | 0.0 | Oct-2000 | 0.0 | NaN | 7.0 | 0.0 | 3730.0 | 26.3 | 9.0 | f | 0.0 | 1.0 | INDIVIDUAL | 0.0 | 0.0 | 25130.0 | 14200.0 |
453283543 | 16000.0 | 36 months | 11.14 | 524.89 | B | B2 | KIPP NYC | 3 years | RENT | 67500.0 | Source Verified | Apr-2013 | 1 | n | debt_consolidation | 104xx | NY | 14.77 | 0.0 | Jul-2001 | 0.0 | NaN | 9.0 | 0.0 | 11769.0 | 60.5 | 22.0 | f | 0.0 | 1.0 | INDIVIDUAL | 0.0 | 193.0 | 41737.0 | 19448.0 |
453447199 | 4200.0 | 36 months | 13.33 | 142.19 | C | C3 | Receptionist | < 1 year | MORTGAGE | 21600.0 | Not Verified | Mar-2015 | 0 | n | major_purchase | 982xx | WA | 39.00 | 0.0 | May-2003 | 0.0 | 47.0 | 9.0 | 0.0 | 6797.0 | 46.9 | 19.0 | w | 0.0 | 1.0 | INDIVIDUAL | 0.0 | 165.0 | 28187.0 | 14500.0 |
453350283 | 6500.0 | 36 months | 12.69 | 218.05 | B | B5 | Medtox Laboratories | 10+ years | RENT | 41000.0 | Not Verified | Jan-2012 | 1 | n | credit_card | 551xx | MN | 18.35 | 0.0 | Sep-1990 | 0.0 | NaN | 8.0 | 0.0 | 14674.0 | 82.4 | 12.0 | f | 0.0 | 1.0 | INDIVIDUAL | 0.0 | NaN | NaN | NaN |
train.shape
(200189, 35)
Смотрим, какие типы данных в столбцах:
train.dtypes
loan_amnt float64
term object
int_rate float64
installment float64
grade object
sub_grade object
emp_title object
emp_length object
home_ownership object
annual_inc float64
verification_status object
issue_d object
loan_status int64
pymnt_plan object
purpose object
zip_code object
addr_state object
dti float64
delinq_2yrs float64
earliest_cr_line object
inq_last_6mths float64
mths_since_last_delinq float64
open_acc float64
pub_rec float64
revol_bal float64
revol_util float64
total_acc float64
initial_list_status object
collections_12_mths_ex_med float64
policy_code float64
application_type object
acc_now_delinq float64
tot_coll_amt float64
tot_cur_bal float64
total_rev_hi_lim float64
dtype: object
Исследуем поля на наличие пропусков:
train.isnull().sum()
loan_amnt 0
term 0
int_rate 0
installment 0
grade 0
sub_grade 0
emp_title 11124
emp_length 0
home_ownership 0
annual_inc 0
verification_status 0
issue_d 0
loan_status 0
pymnt_plan 0
purpose 0
zip_code 0
addr_state 0
dti 0
delinq_2yrs 0
earliest_cr_line 0
inq_last_6mths 0
mths_since_last_delinq 110568
open_acc 0
pub_rec 0
revol_bal 0
revol_util 154
total_acc 0
initial_list_status 0
collections_12_mths_ex_med 44
policy_code 0
application_type 0
acc_now_delinq 0
tot_coll_amt 47957
tot_cur_bal 47957
total_rev_hi_lim 47957
dtype: int64
Следующие столбцы имеют пропуски:
emp_title — The job title supplied by the Borrower when applying for the loan
— Должность заемщика, вещь вроде полезная, но нестандартизирована, слишком много различных значений; отбрасываем этот столбец, но добавим флаг, указана ли была должность вообще:
train['is_title_known'] = train['emp_title'].map(lambda x: 0 if x == 'n/a' else 1)
train.drop('emp_title', axis=1, inplace=True)
mths_since_last_delinq — The number of months since the borrower's last delinquency
— кол-во месяцев с момента последнего неосуществления платежа в установленный срок. Заменить пропуски на 0 будет неправильно, заменим их на max в этом столбце и добавим столбец is_delinq_occurs с флагом, был ли факт неплатежа ранее
import numpy as np
import math
train['is_delinq_occurs'] = train['mths_since_last_delinq'].map(lambda x: 0 if math.isnan(x) else 1)
max_mths_since_last_delinq = np.nanmax(train.mths_since_last_delinq.values)
train['mths_since_last_delinq'].fillna(max_mths_since_last_delinq, inplace=True)
revol_util — Revolving line utilization rate, or the amount of credit the borrower is using relative to all available revolving credit
collections_12_mths_ex_med — Number of collections in 12 months excluding medical collections
tot_coll_amt — Total collection amounts ever owed
tot_cur_bal — Total current balance of all accounts
total_rev_hi_lim — Total revolving high credit/credit limit
Мне неизвестны тонкости этого бизнес-домена, поэтому заменяем пропуски на нули с помощью функции fillna():
train.fillna(0, inplace=True)
train.isnull().sum()
loan_amnt 0
term 0
int_rate 0
installment 0
grade 0
sub_grade 0
emp_length 0
home_ownership 0
annual_inc 0
verification_status 0
issue_d 0
loan_status 0
pymnt_plan 0
purpose 0
zip_code 0
addr_state 0
dti 0
delinq_2yrs 0
earliest_cr_line 0
inq_last_6mths 0
mths_since_last_delinq 0
open_acc 0
pub_rec 0
revol_bal 0
revol_util 0
total_acc 0
initial_list_status 0
collections_12_mths_ex_med 0
policy_code 0
application_type 0
acc_now_delinq 0
tot_coll_amt 0
tot_cur_bal 0
total_rev_hi_lim 0
is_title_known 0
is_delinq_occurs 0
dtype: int64
— Пропусков теперь нет
Разбиваем данные на X и Y:
def extract_XY(data):
X = data.drop(['loan_status'], axis=1)
Y = data['loan_status']
return X, Y
X, Y = extract_XY(train)
print X.shape, Y.shape
(200189, 35) (200189L,)
Предварительно надо разобраться с нечисловыми столбцами
Добавим методы для LabelEncoder и OneHotEncoder:
from sklearn.preprocessing import LabelEncoder
from sklearn.preprocessing import OneHotEncoder
# Добавляет в DataFrame df новый столбец с именем column_name+'_le', содержащий номера категорий,
# соответствующие столбцу column_name. Исходный столбец column_name удаляется
#
def encode_with_LabelEncoder(df, column_name):
label_encoder = LabelEncoder()
label_encoder.fit(df[column_name])
df[column_name+'_le'] = label_encoder.transform(df[column_name])
df.drop([column_name], axis=1, inplace=True)
return label_encoder
# Кодирование с использованием ранее созданного LabelEncoder
#
def encode_with_existing_LabelEncoder(df, column_name, label_encoder):
df[column_name+'_le'] = label_encoder.transform(df[column_name])
df.drop([column_name], axis=1, inplace=True)
# Вначале кодирует столбец column_name при помощи LabelEncoder, потом добавляет в DataFrame df новые столбцы
# с именами column_name=<категория_i>. Столбцы column_name и column_name+'_le' удаляются
# Usage: df, label_encoder = encode_with_OneHotEncoder_and_delete_column(df, column_name)
#
def encode_with_OneHotEncoder_and_delete_column(df, column_name):
le_encoder = encode_with_LabelEncoder(df, column_name)
return perform_dummy_coding_and_delete_column(df, column_name, le_encoder), le_encoder
# То же, что предыдущий метод, но при помощи уже существующего LabelEncoder
#
def encode_with_OneHotEncoder_using_existing_LabelEncoder_and_delete_column(df, column_name, le_encoder):
encode_with_existing_LabelEncoder(df, column_name, le_encoder)
return perform_dummy_coding_and_delete_column(df, column_name, le_encoder)
# Реализует Dummy-кодирование
#
def perform_dummy_coding_and_delete_column(df, column_name, le_encoder):
oh_encoder = OneHotEncoder(sparse=False)
oh_features = oh_encoder.fit_transform(df[column_name+'_le'].values.reshape(-1,1))
ohe_columns=[column_name + '=' + le_encoder.classes_[i] for i in range(oh_features.shape[1])]
df.drop([column_name+'_le'], axis=1, inplace=True)
df_with_features = pd.DataFrame(oh_features, columns=ohe_columns)
df_with_features.index = df.index
return pd.concat([df, df_with_features], axis=1)
term — The number of payments on the loan. Values are in months and can be either 36 or 60.
— Кол-во платежей, стоит оставить
import numpy as np
print np.unique(X['term'])
[' 36 months' ' 60 months']
term_le_encoder = encode_with_LabelEncoder(X,'term')
grade, sub_grade — loan grade & subgrade
— Некие "класс и подкласс заема", grade соответствует риску займа (с нарастанием риска от A к G), sub_grade — то же, только c больше детализацией, — стоит оставить
print np.unique(X['grade'])
print np.unique(X['sub_grade'])
['A' 'B' 'C' 'D' 'E' 'F' 'G']
['A1' 'A2' 'A3' 'A4' 'A5' 'B1' 'B2' 'B3' 'B4' 'B5' 'C1' 'C2' 'C3' 'C4' 'C5'
'D1' 'D2' 'D3' 'D4' 'D5' 'E1' 'E2' 'E3' 'E4' 'E5' 'F1' 'F2' 'F3' 'F4' 'F5'
'G1' 'G2' 'G3' 'G4' 'G5']
grade_le_encoder = encode_with_LabelEncoder(X,'grade')
sub_grade_le_encoder = encode_with_LabelEncoder(X,'sub_grade')
emp_length — Employment length in years. Possible values are between 0 and 10 where 0 means less than one year and 10 means ten or more years
— Срок занятости — стоит оставить
print np.unique(X['emp_length'])
['1 year' '10+ years' '2 years' '3 years' '4 years' '5 years' '6 years'
'7 years' '8 years' '9 years' '< 1 year' 'n/a']
X, emp_length_le_encoder = encode_with_OneHotEncoder_and_delete_column(X,'emp_length')
home_ownership — The home ownership status provided by the borrower during registration. Our values are: RENT, OWN, MORTGAGE, OTHER
— Флажок, принадлежит ли владельцу его текущий дом — стоит оставить
print np.unique(X['home_ownership'])
['ANY' 'MORTGAGE' 'NONE' 'OTHER' 'OWN' 'RENT']
X, home_ownership_le_encoder = encode_with_OneHotEncoder_and_delete_column(X,'home_ownership')
verification_status
— В описании столбцов данного столбца нету, но судя из названия — это информация о том, являются ли данные о заемщике проверенными. Стоит оставить
print np.unique(X['verification_status'])
['Not Verified' 'Source Verified' 'Verified']
X, verification_status_le_encoder = encode_with_OneHotEncoder_and_delete_column(X,'verification_status')
issue_d — The month which the loan was funded
— Месяц (и год), в который предоставлялся заем (вроде как). В период спада экономики возвратность займов может падать — стоит оставить
print np.unique(X['issue_d'])
['Apr-2008' 'Apr-2009' 'Apr-2010' 'Apr-2011' 'Apr-2012' 'Apr-2013'
'Apr-2014' 'Apr-2015' 'Aug-2007' 'Aug-2008' 'Aug-2009' 'Aug-2010'
'Aug-2011' 'Aug-2012' 'Aug-2013' 'Aug-2014' 'Aug-2015' 'Dec-2007'
'Dec-2008' 'Dec-2009' 'Dec-2010' 'Dec-2011' 'Dec-2012' 'Dec-2013'
'Dec-2014' 'Dec-2015' 'Feb-2008' 'Feb-2009' 'Feb-2010' 'Feb-2011'
'Feb-2012' 'Feb-2013' 'Feb-2014' 'Feb-2015' 'Jan-2008' 'Jan-2009'
'Jan-2010' 'Jan-2011' 'Jan-2012' 'Jan-2013' 'Jan-2014' 'Jan-2015'
'Jul-2007' 'Jul-2008' 'Jul-2009' 'Jul-2010' 'Jul-2011' 'Jul-2012'
'Jul-2013' 'Jul-2014' 'Jul-2015' 'Jun-2007' 'Jun-2008' 'Jun-2009'
'Jun-2010' 'Jun-2011' 'Jun-2012' 'Jun-2013' 'Jun-2014' 'Jun-2015'
'Mar-2008' 'Mar-2009' 'Mar-2010' 'Mar-2011' 'Mar-2012' 'Mar-2013'
'Mar-2014' 'Mar-2015' 'May-2008' 'May-2009' 'May-2010' 'May-2011'
'May-2012' 'May-2013' 'May-2014' 'May-2015' 'Nov-2007' 'Nov-2008'
'Nov-2009' 'Nov-2010' 'Nov-2011' 'Nov-2012' 'Nov-2013' 'Nov-2014'
'Nov-2015' 'Oct-2007' 'Oct-2008' 'Oct-2009' 'Oct-2010' 'Oct-2011'
'Oct-2012' 'Oct-2013' 'Oct-2014' 'Oct-2015' 'Sep-2007' 'Sep-2008'
'Sep-2009' 'Sep-2010' 'Sep-2011' 'Sep-2012' 'Sep-2013' 'Sep-2014'
'Sep-2015']
Датам можно поставить в соответствие числа так, что более поздней дате соответствует большее число:
def month_to_decimal(month):
month_dict = {'Jan':0, 'Feb':1/12., 'Mar':2/12., 'Apr':3/12., 'May':4/12., 'Jun':5/12.,
'Jul':6/12., 'Aug':7/12., 'Sep':8/12., 'Oct':9/12., 'Nov':10/12., 'Dec':11/12.}
return month_dict[month]
def convert_date(month_year):
month_and_year = month_year.split('-')
return float(month_and_year[1]) + month_to_decimal(month_and_year[0])
# Some check
convert_date('Mar-2011')
2011.1666666666667
def encode_with_func(df, column_name, func_name):
df[column_name+'_le'] = df[column_name].map(func_name)
df.drop(column_name, axis=1, inplace=True)
encode_with_func(X, 'issue_d', convert_date)
pymnt_plan — Indicates if a payment plan has been put in place for the loan
— Флажок был ли план платежей, стоит оставить
print np.unique(X['pymnt_plan'])
['n' 'y']
pymnt_plan_le_encoder = encode_with_LabelEncoder(X,'pymnt_plan')
purpose — A category provided by the borrower for the loan request
— Категория, для чего брался заем, стоит оставить
print np.unique(X['purpose'])
['car' 'credit_card' 'debt_consolidation' 'educational' 'home_improvement'
'house' 'major_purchase' 'medical' 'moving' 'other' 'renewable_energy'
'small_business' 'vacation' 'wedding']
X,purpose_le_encoder = encode_with_OneHotEncoder_and_delete_column(X,'purpose')
zip_code — The first 3 numbers of the zip code provided by the borrower in the loan application
addr_state — The state provided by the borrower in the loan application
— Почтовый индекс и адрес
print len(np.unique(X['zip_code']))
print len(np.unique(X['addr_state']))
877
51
X.drop(['zip_code'], axis=1, inplace=True)
addr_state_le_encoder = encode_with_LabelEncoder(X,'addr_state')
earliest_cr_line — The month the borrower's earliest reported credit line was opened
print np.unique(X['earliest_cr_line'])
['Apr-1964' 'Apr-1965' 'Apr-1966' 'Apr-1967' 'Apr-1968' 'Apr-1969'
'Apr-1970' 'Apr-1971' 'Apr-1972' 'Apr-1973' 'Apr-1974' 'Apr-1975'
'Apr-1976' 'Apr-1977' 'Apr-1978' 'Apr-1979' 'Apr-1980' 'Apr-1981'
'Apr-1982' 'Apr-1983' 'Apr-1984' 'Apr-1985' 'Apr-1986' 'Apr-1987'
'Apr-1988' 'Apr-1989' 'Apr-1990' 'Apr-1991' 'Apr-1992' 'Apr-1993'
'Apr-1994' 'Apr-1995' 'Apr-1996' 'Apr-1997' 'Apr-1998' 'Apr-1999'
'Apr-2000' 'Apr-2001' 'Apr-2002' 'Apr-2003' 'Apr-2004' 'Apr-2005'
'Apr-2006' 'Apr-2007' 'Apr-2008' 'Apr-2009' 'Apr-2010' 'Apr-2011'
'Apr-2012' 'Aug-1959' 'Aug-1960' 'Aug-1965' 'Aug-1966' 'Aug-1967'
'Aug-1968' 'Aug-1969' 'Aug-1970' 'Aug-1971' 'Aug-1972' 'Aug-1973'
'Aug-1974' 'Aug-1975' 'Aug-1976' 'Aug-1977' 'Aug-1978' 'Aug-1979'
'Aug-1980' 'Aug-1981' 'Aug-1982' 'Aug-1983' 'Aug-1984' 'Aug-1985'
'Aug-1986' 'Aug-1987' 'Aug-1988' 'Aug-1989' 'Aug-1990' 'Aug-1991'
'Aug-1992' 'Aug-1993' 'Aug-1994' 'Aug-1995' 'Aug-1996' 'Aug-1997'
'Aug-1998' 'Aug-1999' 'Aug-2000' 'Aug-2001' 'Aug-2002' 'Aug-2003'
'Aug-2004' 'Aug-2005' 'Aug-2006' 'Aug-2007' 'Aug-2008' 'Aug-2009'
'Aug-2010' 'Aug-2011' 'Dec-1950' 'Dec-1956' 'Dec-1958' 'Dec-1960'
'Dec-1961' 'Dec-1962' 'Dec-1963' 'Dec-1964' 'Dec-1965' 'Dec-1966'
'Dec-1967' 'Dec-1968' 'Dec-1969' 'Dec-1970' 'Dec-1971' 'Dec-1972'
'Dec-1973' 'Dec-1974' 'Dec-1975' 'Dec-1976' 'Dec-1977' 'Dec-1978'
'Dec-1979' 'Dec-1980' 'Dec-1981' 'Dec-1982' 'Dec-1983' 'Dec-1984'
'Dec-1985' 'Dec-1986' 'Dec-1987' 'Dec-1988' 'Dec-1989' 'Dec-1990'
'Dec-1991' 'Dec-1992' 'Dec-1993' 'Dec-1994' 'Dec-1995' 'Dec-1996'
'Dec-1997' 'Dec-1998' 'Dec-1999' 'Dec-2000' 'Dec-2001' 'Dec-2002'
'Dec-2003' 'Dec-2004' 'Dec-2005' 'Dec-2006' 'Dec-2007' 'Dec-2008'
'Dec-2009' 'Dec-2010' 'Dec-2011' 'Feb-1957' 'Feb-1964' 'Feb-1965'
'Feb-1966' 'Feb-1967' 'Feb-1968' 'Feb-1969' 'Feb-1970' 'Feb-1971'
'Feb-1972' 'Feb-1973' 'Feb-1974' 'Feb-1975' 'Feb-1976' 'Feb-1977'
'Feb-1978' 'Feb-1979' 'Feb-1980' 'Feb-1981' 'Feb-1982' 'Feb-1983'
'Feb-1984' 'Feb-1985' 'Feb-1986' 'Feb-1987' 'Feb-1988' 'Feb-1989'
'Feb-1990' 'Feb-1991' 'Feb-1992' 'Feb-1993' 'Feb-1994' 'Feb-1995'
'Feb-1996' 'Feb-1997' 'Feb-1998' 'Feb-1999' 'Feb-2000' 'Feb-2001'
'Feb-2002' 'Feb-2003' 'Feb-2004' 'Feb-2005' 'Feb-2006' 'Feb-2007'
'Feb-2008' 'Feb-2009' 'Feb-2010' 'Feb-2011' 'Feb-2012' 'Jan-1946'
'Jan-1954' 'Jan-1955' 'Jan-1956' 'Jan-1959' 'Jan-1960' 'Jan-1961'
'Jan-1962' 'Jan-1963' 'Jan-1964' 'Jan-1965' 'Jan-1966' 'Jan-1967'
'Jan-1968' 'Jan-1969' 'Jan-1970' 'Jan-1971' 'Jan-1972' 'Jan-1973'
'Jan-1974' 'Jan-1975' 'Jan-1976' 'Jan-1977' 'Jan-1978' 'Jan-1979'
'Jan-1980' 'Jan-1981' 'Jan-1982' 'Jan-1983' 'Jan-1984' 'Jan-1985'
'Jan-1986' 'Jan-1987' 'Jan-1988' 'Jan-1989' 'Jan-1990' 'Jan-1991'
'Jan-1992' 'Jan-1993' 'Jan-1994' 'Jan-1995' 'Jan-1996' 'Jan-1997'
'Jan-1998' 'Jan-1999' 'Jan-2000' 'Jan-2001' 'Jan-2002' 'Jan-2003'
'Jan-2004' 'Jan-2005' 'Jan-2006' 'Jan-2007' 'Jan-2008' 'Jan-2009'
'Jan-2010' 'Jan-2011' 'Jan-2012' 'Jul-1958' 'Jul-1961' 'Jul-1963'
'Jul-1964' 'Jul-1965' 'Jul-1966' 'Jul-1967' 'Jul-1968' 'Jul-1969'
'Jul-1970' 'Jul-1971' 'Jul-1972' 'Jul-1973' 'Jul-1974' 'Jul-1975'
'Jul-1976' 'Jul-1977' 'Jul-1978' 'Jul-1979' 'Jul-1980' 'Jul-1981'
'Jul-1982' 'Jul-1983' 'Jul-1984' 'Jul-1985' 'Jul-1986' 'Jul-1987'
'Jul-1988' 'Jul-1989' 'Jul-1990' 'Jul-1991' 'Jul-1992' 'Jul-1993'
'Jul-1994' 'Jul-1995' 'Jul-1996' 'Jul-1997' 'Jul-1998' 'Jul-1999'
'Jul-2000' 'Jul-2001' 'Jul-2002' 'Jul-2003' 'Jul-2004' 'Jul-2005'
'Jul-2006' 'Jul-2007' 'Jul-2008' 'Jul-2009' 'Jul-2010' 'Jul-2011'
'Jul-2012' 'Jun-1957' 'Jun-1959' 'Jun-1963' 'Jun-1964' 'Jun-1965'
'Jun-1966' 'Jun-1967' 'Jun-1968' 'Jun-1969' 'Jun-1970' 'Jun-1971'
'Jun-1972' 'Jun-1973' 'Jun-1974' 'Jun-1975' 'Jun-1976' 'Jun-1977'
'Jun-1978' 'Jun-1979' 'Jun-1980' 'Jun-1981' 'Jun-1982' 'Jun-1983'
'Jun-1984' 'Jun-1985' 'Jun-1986' 'Jun-1987' 'Jun-1988' 'Jun-1989'
'Jun-1990' 'Jun-1991' 'Jun-1992' 'Jun-1993' 'Jun-1994' 'Jun-1995'
'Jun-1996' 'Jun-1997' 'Jun-1998' 'Jun-1999' 'Jun-2000' 'Jun-2001'
'Jun-2002' 'Jun-2003' 'Jun-2004' 'Jun-2005' 'Jun-2006' 'Jun-2007'
'Jun-2008' 'Jun-2009' 'Jun-2010' 'Jun-2011' 'Jun-2012' 'Mar-1960'
'Mar-1961' 'Mar-1963' 'Mar-1964' 'Mar-1965' 'Mar-1966' 'Mar-1967'
'Mar-1968' 'Mar-1969' 'Mar-1970' 'Mar-1971' 'Mar-1972' 'Mar-1973'
'Mar-1974' 'Mar-1975' 'Mar-1976' 'Mar-1977' 'Mar-1978' 'Mar-1979'
'Mar-1980' 'Mar-1981' 'Mar-1982' 'Mar-1983' 'Mar-1984' 'Mar-1985'
'Mar-1986' 'Mar-1987' 'Mar-1988' 'Mar-1989' 'Mar-1990' 'Mar-1991'
'Mar-1992' 'Mar-1993' 'Mar-1994' 'Mar-1995' 'Mar-1996' 'Mar-1997'
'Mar-1998' 'Mar-1999' 'Mar-2000' 'Mar-2001' 'Mar-2002' 'Mar-2003'
'Mar-2004' 'Mar-2005' 'Mar-2006' 'Mar-2007' 'Mar-2008' 'Mar-2009'
'Mar-2010' 'Mar-2011' 'Mar-2012' 'May-1959' 'May-1960' 'May-1962'
'May-1963' 'May-1964' 'May-1965' 'May-1966' 'May-1967' 'May-1968'
'May-1969' 'May-1970' 'May-1971' 'May-1972' 'May-1973' 'May-1974'
'May-1975' 'May-1976' 'May-1977' 'May-1978' 'May-1979' 'May-1980'
'May-1981' 'May-1982' 'May-1983' 'May-1984' 'May-1985' 'May-1986'
'May-1987' 'May-1988' 'May-1989' 'May-1990' 'May-1991' 'May-1992'
'May-1993' 'May-1994' 'May-1995' 'May-1996' 'May-1997' 'May-1998'
'May-1999' 'May-2000' 'May-2001' 'May-2002' 'May-2003' 'May-2004'
'May-2005' 'May-2006' 'May-2007' 'May-2008' 'May-2009' 'May-2010'
'May-2011' 'May-2012' 'Nov-1954' 'Nov-1955' 'Nov-1956' 'Nov-1958'
'Nov-1959' 'Nov-1960' 'Nov-1961' 'Nov-1962' 'Nov-1964' 'Nov-1965'
'Nov-1966' 'Nov-1967' 'Nov-1968' 'Nov-1969' 'Nov-1970' 'Nov-1971'
'Nov-1972' 'Nov-1973' 'Nov-1974' 'Nov-1975' 'Nov-1976' 'Nov-1977'
'Nov-1978' 'Nov-1979' 'Nov-1980' 'Nov-1981' 'Nov-1982' 'Nov-1983'
'Nov-1984' 'Nov-1985' 'Nov-1986' 'Nov-1987' 'Nov-1988' 'Nov-1989'
'Nov-1990' 'Nov-1991' 'Nov-1992' 'Nov-1993' 'Nov-1994' 'Nov-1995'
'Nov-1996' 'Nov-1997' 'Nov-1998' 'Nov-1999' 'Nov-2000' 'Nov-2001'
'Nov-2002' 'Nov-2003' 'Nov-2004' 'Nov-2005' 'Nov-2006' 'Nov-2007'
'Nov-2008' 'Nov-2009' 'Nov-2010' 'Nov-2011' 'Oct-1954' 'Oct-1958'
'Oct-1959' 'Oct-1960' 'Oct-1961' 'Oct-1962' 'Oct-1963' 'Oct-1964'
'Oct-1965' 'Oct-1966' 'Oct-1967' 'Oct-1968' 'Oct-1969' 'Oct-1970'
'Oct-1971' 'Oct-1972' 'Oct-1973' 'Oct-1974' 'Oct-1975' 'Oct-1976'
'Oct-1977' 'Oct-1978' 'Oct-1979' 'Oct-1980' 'Oct-1981' 'Oct-1982'
'Oct-1983' 'Oct-1984' 'Oct-1985' 'Oct-1986' 'Oct-1987' 'Oct-1988'
'Oct-1989' 'Oct-1990' 'Oct-1991' 'Oct-1992' 'Oct-1993' 'Oct-1994'
'Oct-1995' 'Oct-1996' 'Oct-1997' 'Oct-1998' 'Oct-1999' 'Oct-2000'
'Oct-2001' 'Oct-2002' 'Oct-2003' 'Oct-2004' 'Oct-2005' 'Oct-2006'
'Oct-2007' 'Oct-2008' 'Oct-2009' 'Oct-2010' 'Oct-2011' 'Oct-2012'
'Sep-1956' 'Sep-1959' 'Sep-1960' 'Sep-1962' 'Sep-1963' 'Sep-1964'
'Sep-1965' 'Sep-1966' 'Sep-1967' 'Sep-1968' 'Sep-1969' 'Sep-1970'
'Sep-1971' 'Sep-1972' 'Sep-1973' 'Sep-1974' 'Sep-1975' 'Sep-1976'
'Sep-1977' 'Sep-1978' 'Sep-1979' 'Sep-1980' 'Sep-1981' 'Sep-1982'
'Sep-1983' 'Sep-1984' 'Sep-1985' 'Sep-1986' 'Sep-1987' 'Sep-1988'
'Sep-1989' 'Sep-1990' 'Sep-1991' 'Sep-1992' 'Sep-1993' 'Sep-1994'
'Sep-1995' 'Sep-1996' 'Sep-1997' 'Sep-1998' 'Sep-1999' 'Sep-2000'
'Sep-2001' 'Sep-2002' 'Sep-2003' 'Sep-2004' 'Sep-2005' 'Sep-2006'
'Sep-2007' 'Sep-2008' 'Sep-2009' 'Sep-2010' 'Sep-2011']
Формат аналогичен столбцу issue_d, кодируем той же функцией convert_date:
encode_with_func(X, 'earliest_cr_line', convert_date)
initial_list_status — The initial listing status of the loan. Possible values are – W, F
— Некий параметр заема, оставляем
print np.unique(X['initial_list_status'])
['f' 'w']
initial_list_status_le_encoder = encode_with_LabelEncoder(X,'initial_list_status')
application_type — Indicates whether the loan is an individual application or a joint application with two co-borrowers
— Индикатор, заем брался одним человеком или в группе с кем-то. Стоит оставить
print np.unique(X['application_type'])
['INDIVIDUAL' 'JOINT']
application_type_le_encoder = encode_with_LabelEncoder(X,'application_type')
Теперь все признаки должны быть числовыми:
X.dtypes
loan_amnt float64
int_rate float64
installment float64
annual_inc float64
dti float64
delinq_2yrs float64
inq_last_6mths float64
mths_since_last_delinq float64
open_acc float64
pub_rec float64
revol_bal float64
revol_util float64
total_acc float64
collections_12_mths_ex_med float64
policy_code float64
acc_now_delinq float64
tot_coll_amt float64
tot_cur_bal float64
total_rev_hi_lim float64
is_title_known int64
is_delinq_occurs int64
term_le int64
grade_le int64
sub_grade_le int64
emp_length=1 year float64
emp_length=10+ years float64
emp_length=2 years float64
emp_length=3 years float64
emp_length=4 years float64
emp_length=5 years float64
...
emp_length=n/a float64
home_ownership=ANY float64
home_ownership=MORTGAGE float64
home_ownership=NONE float64
home_ownership=OTHER float64
home_ownership=OWN float64
home_ownership=RENT float64
verification_status=Not Verified float64
verification_status=Source Verified float64
verification_status=Verified float64
issue_d_le float64
pymnt_plan_le int64
purpose=car float64
purpose=credit_card float64
purpose=debt_consolidation float64
purpose=educational float64
purpose=home_improvement float64
purpose=house float64
purpose=major_purchase float64
purpose=medical float64
purpose=moving float64
purpose=other float64
purpose=renewable_energy float64
purpose=small_business float64
purpose=vacation float64
purpose=wedding float64
addr_state_le int64
earliest_cr_line_le float64
initial_list_status_le int64
application_type_le int64
dtype: object
X.shape
(200189, 65)
Признаки подготовлены
Для решения выбрал логистическую регрессию — она дает ответ в виде набора вероятностей и работает достаточно быстро
При решении применяем кросс-валидацию по 5 блокам с перемешиванием
Качеством считаем площадь под ROC-кривой — AUC-ROC величину
from sklearn.cross_validation import KFold
from sklearn.metrics import roc_auc_score
from sklearn.cross_validation import cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
records_count = Y.count()
kf = KFold(n=records_count, n_folds=5, shuffle=True)
def my_scorer(estimator, testX, testY):
predicted_testY = estimator.predict_proba(testX)[:, 1]
return roc_auc_score(testY, predicted_testY)
scaler = StandardScaler()
scaledX = scaler.fit_transform(X)
def LogR_teach(C_value):
clf = LogisticRegression(penalty='l2', C=C_value)
return cross_val_score(clf, scaledX, Y, cv=kf, scoring=my_scorer).mean()
def check_quality_for_different_C():
for power in range(-4, 2):
C = math.pow(10, power)
quality = LogR_teach(C)
print 'C=', C, ', quality=', quality
check_quality_for_different_C()
C= 0.0001 , quality= 0.707991113828
C= 0.001 , quality= 0.709654648448
C= 0.01 , quality= 0.709779198127
C= 0.1 , quality= 0.709776206257
C= 1.0 , quality= 0.709775629602
C= 10.0 , quality= 0.709775731556
Лучшее качество 0.71 достигается при С=0.1
from sklearn.model_selection import train_test_split
X_train, X_test, y_train, y_test = train_test_split(scaledX, Y, test_size=.2, random_state=0)
clf = LogisticRegression(penalty='l2', C=0.1)
clf.fit(X_train, y_train)
y_score = clf.predict_proba(X_test)[:, 1]
Чертим ROC-кривую:
import matplotlib.pyplot as plt
from sklearn.metrics import roc_curve
plt.figure()
line_width = 2
fpr, tpr, thresholds = roc_curve(y_test, y_score)
plt.plot(fpr, tpr, color='darkorange', lw=line_width, label='LogRegression, C=0.1')
plt.plot([0, 1], [0, 1], color='navy', lw=line_width, linestyle='--')
plt.xlim([0.0, 1.0])
plt.ylim([0.0, 1.0])
plt.xlabel('False Positive Rate')
plt.ylabel('True Positive Rate')
plt.title('Receiver operating characteristic')
plt.legend(loc="lower right")
plt.show()
Вычислим предсказания для тестового набора
Загружаем данные:
test = pd.read_csv('test.csv', index_col='record_id')
Делаем действия аналогичные проделанным с train набором:
test['is_title_known'] = test['emp_title'].map(lambda x: 0 if x == 'n/a' else 1)
test.drop('emp_title', axis=1, inplace=True)
test['is_delinq_occurs'] = test['mths_since_last_delinq'].map(lambda x: 0 if math.isnan(x) else 1)
max_mths_since_last_delinq = np.nanmax(test.mths_since_last_delinq.values)
test['mths_since_last_delinq'].fillna(max_mths_since_last_delinq, inplace=True)
test.fillna(0, inplace=True)
test.isnull().sum()
loan_amnt 0
term 0
int_rate 0
installment 0
grade 0
sub_grade 0
emp_length 0
home_ownership 0
annual_inc 0
verification_status 0
issue_d 0
loan_status 0
pymnt_plan 0
purpose 0
zip_code 0
addr_state 0
dti 0
delinq_2yrs 0
earliest_cr_line 0
inq_last_6mths 0
mths_since_last_delinq 0
open_acc 0
pub_rec 0
revol_bal 0
revol_util 0
total_acc 0
initial_list_status 0
collections_12_mths_ex_med 0
policy_code 0
application_type 0
acc_now_delinq 0
tot_coll_amt 0
tot_cur_bal 0
total_rev_hi_lim 0
is_title_known 0
is_delinq_occurs 0
dtype: int64
— Пропусков нету, как и ожидаем
print test.shape
(66730, 36)
Подготовим нечисловые столбцы:
Используем энкодеры созданные ранее, при обработке train набора
encode_with_existing_LabelEncoder(test, 'term', term_le_encoder)
encode_with_existing_LabelEncoder(test, 'grade', grade_le_encoder)
encode_with_existing_LabelEncoder(test, 'sub_grade', sub_grade_le_encoder)
test = encode_with_OneHotEncoder_using_existing_LabelEncoder_and_delete_column(test, 'emp_length', emp_length_le_encoder)
test = encode_with_OneHotEncoder_using_existing_LabelEncoder_and_delete_column(test, 'home_ownership', home_ownership_le_encoder)
test = encode_with_OneHotEncoder_using_existing_LabelEncoder_and_delete_column(test, 'verification_status', verification_status_le_encoder)
encode_with_func(test, 'issue_d', convert_date)
encode_with_existing_LabelEncoder(test, 'pymnt_plan', pymnt_plan_le_encoder)
test = encode_with_OneHotEncoder_using_existing_LabelEncoder_and_delete_column(test, 'purpose', purpose_le_encoder)
test.drop(['zip_code'], axis=1, inplace=True)
encode_with_existing_LabelEncoder(test, 'addr_state', addr_state_le_encoder)
encode_with_func(test, 'earliest_cr_line', convert_date)
encode_with_existing_LabelEncoder(test, 'initial_list_status', initial_list_status_le_encoder)
encode_with_existing_LabelEncoder(test, 'application_type', application_type_le_encoder)
X.dtypes
loan_amnt float64
int_rate float64
installment float64
annual_inc float64
dti float64
delinq_2yrs float64
inq_last_6mths float64
mths_since_last_delinq float64
open_acc float64
pub_rec float64
revol_bal float64
revol_util float64
total_acc float64
collections_12_mths_ex_med float64
policy_code float64
acc_now_delinq float64
tot_coll_amt float64
tot_cur_bal float64
total_rev_hi_lim float64
is_title_known int64
is_delinq_occurs int64
term_le int64
grade_le int64
sub_grade_le int64
emp_length=1 year float64
emp_length=10+ years float64
emp_length=2 years float64
emp_length=3 years float64
emp_length=4 years float64
emp_length=5 years float64
...
emp_length=n/a float64
home_ownership=ANY float64
home_ownership=MORTGAGE float64
home_ownership=NONE float64
home_ownership=OTHER float64
home_ownership=OWN float64
home_ownership=RENT float64
verification_status=Not Verified float64
verification_status=Source Verified float64
verification_status=Verified float64
issue_d_le float64
pymnt_plan_le int64
purpose=car float64
purpose=credit_card float64
purpose=debt_consolidation float64
purpose=educational float64
purpose=home_improvement float64
purpose=house float64
purpose=major_purchase float64
purpose=medical float64
purpose=moving float64
purpose=other float64
purpose=renewable_energy float64
purpose=small_business float64
purpose=vacation float64
purpose=wedding float64
addr_state_le int64
earliest_cr_line_le float64
initial_list_status_le int64
application_type_le int64
dtype: object
print test.shape
(66730, 65)
Масштабируем признаки, используя ранее созданный scaler:
scaled_test = scaler.transform(test)
Обучаем классификатор с выбранным оптимальным значением С:
clf = LogisticRegression(penalty='l2', C=0.1)
clf.fit(scaledX, Y)
LogisticRegression(C=0.1, class_weight=None, dual=False, fit_intercept=True,
intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
verbose=0, warm_start=False)
Предсказываем:
prediction = clf.predict_proba(scaled_test)[:, 1]
Убедимся, что предсказанные вероятности находятся на отрезке [0,1] и не совпадают между собой (т.е. что модель не получилась константной):
print min(prediction), max(prediction)
0.0 0.999999996487
Преобразуем prediction в DataFrame:
result = pd.DataFrame(np.array(prediction), columns=['prob1'], index=test.index)
print result[result['prob1']>0.5].count()
print result.count()
prob1 711
dtype: int64
prob1 66730
dtype: int64
Немного странно конечно, что из 67К записей только у 0.7К вероятность больше 0.5
Сохраняем результаты:
result.to_csv('result.csv', encoding='utf8')
Приложение
Код здесь: https://bitbucket.org/andrei_punko/credit-scoring-test-task
Судя по информации в Интернете — качество 70-80% для скоринга считается хорошим, но все же есть сомнения, поэтому выслушаю предложения как можно было его улучшить
Автор: andd3dfx