Отучившись на нескольких онлайн-курсах, попробовал занять позицию, связанную с 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()
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'
...
'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')
Приложение
> Код здесь
Судя по информации в Интернете — качество 70-80% для скоринга считается хорошим, но все же есть сомнения, поэтому выслушаю предложения как можно было его улучшить
Комментарии (14)
yorko
23.03.2017 14:01+1Форматирование статьи в целом не дружелюбное глазу.
Основной контент статьи должен быть текстом, а не кодом и не выводом.
Таблицы markdown лучше скриншотить. Английский вперемежку с русским…andd3dfx
23.03.2017 14:59+1>> Форматирование статьи в целом не дружелюбное глазу
Это экспорт ноутбука в markdown формат, который поддерживается Хабром
>> Английский вперемежку с русским
Описание признаков было на английском, я решил — правильным будет не портить его переводом
Да, возможно в некоторых местах вывод можно убратьyorko
23.03.2017 15:29+1Так вот именно! Статья на Хабре – это не просто экспорт ноутбука в markdown формат.
andd3dfx
23.03.2017 16:13Вы правы, я подправлю некоторые места
Но все же основной контент статьи может быть и кодом, потому что статей, где много текста и так хватает.
А насчет вывода значений — ведь при анализе вы решаете что делать дальше в зависимости от результатов вывода предыдущих шагов, поэтому это тоже полезная информация по сути
varagian
23.03.2017 19:50Согласен, код лучше убрать под спойлеры и тд. Перепроверить как выглядит текст, например слишком много огромных заголовков и таблица — эээ того:
скрин таблицыandd3dfx
23.03.2017 20:52В чем она у вас так отображается? У меня в FF она просто обрезана справа
Неужели вы думаете, что я бы выложил материал с таблицей в таком виде, как на вашем скрине.
Убрал таблицу вообще. На смысл это не влияет
Убрать код под спойлеры? — Тогда чтобы прочитать — придется прокликать каждый блок вместо того, чтобы читать и скролить. Не бывать тому)varagian
24.03.2017 11:33+1Chrome Version 57.0.2987.110 (64-bit) Ubuntu 14.04 LTS
Например есть вывод X.dtypes и большая таблица под ним фактически c экран — неужели она вся нужна читателю? Она бы отлично зашла под спойлер с объяснениями.
Статья — это история/рассказ, а не аннотация к коду — читателю надо дать контекст, идею, highlight, объяснения и тд (имхо), иначе человек должен копаться в коде и разбираться, что происходит самостоятельно.
olferuk
23.03.2017 15:47+1Из статьи не очень понятно, чем мотивирован выбор между Label- и OneHot-кодированием для конкретного отдельно взятого признака. Чем это обосновано? Пробовали ли кодировать по-другому (вот тут их смотрите, сколько)?
andd3dfx
23.03.2017 15:58Выбор между Label- и OneHot-кодированием производился в сторону Label-, если признак имел слишком много уникальных значений, чтобы не увеличивать чрезмерно кол-во признаков
Для некоторых признаков (как штат например) до того, как сделать Label-, использовалось OneHot-кодирование; его оставлял, если качество ощутимо улучшалось
Другие виды кодирования для категориальных признаков не использовались
MiniGN
23.03.2017 20:41Не пробовали другие алгоритмы — svm, rg, gb?
На мой взгляд, если задача была показать базовое владение инструментами, то все вполне грамотно.
А так можно было бы добавить еще отбор фич.andd3dfx
23.03.2017 20:56Еще пробовал GradientBoosting, качество было соизмеримым, считало дольше
Спасибо, но меня только смутило, что в тестовом наборе получился малый процент тех, кто отдаст кредит. Подумал, что пропустил что-то
Chudo199
Чем обусловлен выбор Python 2?
andd3dfx
В пройденных курсах и литературе использовался 2й. Также указывалось, что многие библиотеки написаны именно на второй версии, поэтому выбрал его
Stas911
Несколько раз пытался перейти на 3.х. Крайний раз, когда это сделал, буквально на следующий день ставил утилиты для управления DC/OS и они, разумеется, попросили 2.7.