Отучившись на нескольких онлайн-курсах, попробовал занять позицию, связанную с 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()

image


Вычислим предсказания для тестового набора


Загружаем данные:


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)


  1. Chudo199
    23.03.2017 11:23

    Чем обусловлен выбор Python 2?


    1. andd3dfx
      23.03.2017 11:27
      +1

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


    1. Stas911
      24.03.2017 23:44

      Несколько раз пытался перейти на 3.х. Крайний раз, когда это сделал, буквально на следующий день ставил утилиты для управления DC/OS и они, разумеется, попросили 2.7.


  1. yorko
    23.03.2017 14:01
    +1

    Форматирование статьи в целом не дружелюбное глазу.

    Основной контент статьи должен быть текстом, а не кодом и не выводом.
    Таблицы markdown лучше скриншотить. Английский вперемежку с русским…


    1. andd3dfx
      23.03.2017 14:59
      +1

      >> Форматирование статьи в целом не дружелюбное глазу
      Это экспорт ноутбука в markdown формат, который поддерживается Хабром

      >> Английский вперемежку с русским
      Описание признаков было на английском, я решил — правильным будет не портить его переводом

      Да, возможно в некоторых местах вывод можно убрать


      1. yorko
        23.03.2017 15:29
        +1

        Так вот именно! Статья на Хабре – это не просто экспорт ноутбука в markdown формат.


        1. andd3dfx
          23.03.2017 16:13

          Вы правы, я подправлю некоторые места

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


        1. varagian
          23.03.2017 19:50

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


          скрин таблицы


          1. andd3dfx
            23.03.2017 20:52

            В чем она у вас так отображается? У меня в FF она просто обрезана справа
            Неужели вы думаете, что я бы выложил материал с таблицей в таком виде, как на вашем скрине.

            Убрал таблицу вообще. На смысл это не влияет

            Убрать код под спойлеры? — Тогда чтобы прочитать — придется прокликать каждый блок вместо того, чтобы читать и скролить. Не бывать тому)


            1. varagian
              24.03.2017 11:33
              +1

              Chrome Version 57.0.2987.110 (64-bit) Ubuntu 14.04 LTS


              Например есть вывод X.dtypes и большая таблица под ним фактически c экран — неужели она вся нужна читателю? Она бы отлично зашла под спойлер с объяснениями.


              Статья — это история/рассказ, а не аннотация к коду — читателю надо дать контекст, идею, highlight, объяснения и тд (имхо), иначе человек должен копаться в коде и разбираться, что происходит самостоятельно.


  1. olferuk
    23.03.2017 15:47
    +1

    Из статьи не очень понятно, чем мотивирован выбор между Label- и OneHot-кодированием для конкретного отдельно взятого признака. Чем это обосновано? Пробовали ли кодировать по-другому (вот тут их смотрите, сколько)?


    1. andd3dfx
      23.03.2017 15:58

      Выбор между Label- и OneHot-кодированием производился в сторону Label-, если признак имел слишком много уникальных значений, чтобы не увеличивать чрезмерно кол-во признаков

      Для некоторых признаков (как штат например) до того, как сделать Label-, использовалось OneHot-кодирование; его оставлял, если качество ощутимо улучшалось

      Другие виды кодирования для категориальных признаков не использовались


  1. MiniGN
    23.03.2017 20:41

    Не пробовали другие алгоритмы — svm, rg, gb?

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


    1. andd3dfx
      23.03.2017 20:56

      Еще пробовал GradientBoosting, качество было соизмеримым, считало дольше

      Спасибо, но меня только смутило, что в тестовом наборе получился малый процент тех, кто отдаст кредит. Подумал, что пропустил что-то