机器学习训练营——机器学习爱好者的自由交流空间(入群联系qq:2279055353)
案例介绍
由于不良或缺少信用记录,很多人难以获得贷款。Home Credit
是一家金融服务机构,致力于向无银行账户的人群提供积极且安全的借贷经历。Home Credit利用多种来源的数据,例如,电话费、消费交易信息,预测客户的偿还能力。本案例使用Home Credit提供的客户借贷历史数据,预测该申请人是否有能力还贷,从而决定是否向他发放贷款。这是一个标准的有监督分类问题。
有监督:训练数据带有类标签,目标任务是训练一个模型预测特征的标签。
分类:标签是0-1二值变量。1代表重新放贷有困难;0代表按时重新放贷。
数据描述
该案例包括7个不同来源的数据文件:
application_train/application_test: 包括每一笔贷款的信息,每行包括一笔贷款,由特征SK_ID_CURR
识别。训练集的TARGET
是二值变量,1表示不能放贷;0表示能放贷。
bureau: 客户以前的信用情况。每一行表示以前的一次贷款,在application
数据里的每一个贷款申请可能有多个以前的信贷记录。
bureau_balance:bureau
里以前贷款的月数据。每行表示以前贷款的月记录,所以一笔以前的贷款可能有多行记录。
previous_application:Home Credit
客户以前的贷款申请记录。每一笔当前的申请都有多个以前的贷款。每一笔以前的申请由特征SK_ID_PREV
标识。
POS_CASH_BALANCE: 以前的销售或现金贷款客户的月数据。
credit_card_balance: 以前的信用卡客户月余额。
installments_payment: Home Credit客户以前贷款的支付历史,每一行对应一笔支付记录。
我们用一张图表示这些数据集的关系。
首先,我们导入一个数据科学任务必需的4个Python库:numpy
,pandas
,sklearn
,matplotlib
.
# numpy and pandas for data manipulationimport numpy as npimport pandas as pd # sklearn preprocessing for dealing with categorical variablesfrom sklearn.preprocessing import LabelEncoder# File system manangementimport os# Suppress warnings import warningswarnings.filterwarnings('ignore')# matplotlib and seaborn for plottingimport matplotlib.pyplot as pltimport seaborn as sns
我们列出所有可用的9个数据文件:一个训练集(with target), 一个检验集(without target), 一个例子提交文件,6个贷款信息文件。
# List files availableprint(os.listdir("../input/"))
# Training dataapp_train = pd.read_csv('../input/application_train.csv')print('Training data shape: ', app_train.shape)app_train.head()
训练集有307511行(每行对应一笔贷款),122个特征(包括预测变量target
)。再看看检验集。
# Testing data featuresapp_test = pd.read_csv('../input/application_test.csv')print('Testing data shape: ', app_test.shape)app_test.head()
Testing data shape: (48744, 121)
探索性数据分析
在这个阶段,我们计算统计量、作图显示数据的结构、趋势、变量间的关系。EDA
对下面的建模有指示性的作用,例如,提示我们选用哪些变量。
Target的分布
预测变量target
取0, 1两个值。0表示按时放贷,1表示客户有还款困难。我们先看看这两类的个数,并用频数直方图表示。
print(app_train['TARGET'].value_counts())
0 282686
1 24825
Name: TARGET, dtype: int64
app_train['TARGET'].astype(int).plot.hist()plt.show()
1值明显少于0值,这是一个不平衡类问题。我们将考虑复杂的机器学习模型,给类赋予权值反映这种不平衡性。
检查缺失值
我们检查数据的各列缺失值数量和比例。
# Function to calculate missing values by column# Funct def missing_values_table(df):# Total missing valuesmis_val = df.isnull().sum()# Percentage of missing valuesmis_val_percent = 100 * df.isnull().sum() / len(df)# Make a table with the resultsmis_val_table = pd.concat([mis_val, mis_val_percent], axis=1)# Rename the columnsmis_val_table_ren_columns = mis_val_table.rename(columns = {0 : 'Missing Values', 1 : '% of Total Values'})# Sort the table by percentage of missing descendingmis_val_table_ren_columns = mis_val_table_ren_columns[mis_val_table_ren_columns.iloc[:,1] != 0].sort_values('% of Total Values', ascending=False).round(1)# Print some summary informationprint ("Your selected dataframe has " + str(df.shape[1]) + " columns.\n""There are " + str(mis_val_table_ren_columns.shape[0]) +" columns that have missing values.")# Return the dataframe with missing informationreturn mis_val_table_ren_columns
# Missing values statisticsmissing_values = missing_values_table(app_train)print(missing_values.head(20))
Your selected dataframe has 122 columns.
There are 67 columns that have missing values.
建模时,我们将使用XGBoost
模型处理这些缺失值。另一种处理方式是,对缺失比例高的列直接从数据里删除,但不知道该列是否对建模有用。因此,我们暂时保留所有列。
列类型
我们统计列变量的类型的数量。其中,int64
,float64
型是数值变量,object
包括字符串,是类别变量。
# Number of each type of columnprint(app_train.dtypes.value_counts())
float64 65
int64 41
object 16
dtype: int64
我们再看看类别变量包含的不同的类别数。
# Number of unique classes in each object columnprint(app_train.select_dtypes('object').apply(pd.Series.nunique, axis = 0))
NAME_CONTRACT_TYPE 2
CODE_GENDER 3
FLAG_OWN_CAR 2
FLAG_OWN_REALTY 2
NAME_TYPE_SUITE 7
NAME_INCOME_TYPE 8
NAME_EDUCATION_TYPE 5
NAME_FAMILY_STATUS 6
NAME_HOUSING_TYPE 6
OCCUPATION_TYPE 18
WEEKDAY_APPR_PROCESS_START 7
ORGANIZATION_TYPE 58
FONDKAPREMONT_MODE 4
HOUSETYPE_MODE 3
WALLSMATERIAL_MODE 7
EMERGENCYSTATE_MODE 2
dtype: int64
大多数类别变量具有很少的类别值,需要类别变量编码成数值表示。
编码类别变量
在继续之前,我们需要处理类别变量。通常,一个机器学习模型不能处理类别变量,但也有例外,象LightGBM
. 因此,我们必须编码这些类别变量为数值型。主要有两种方法:
Label encoding: 给类别变量的每个类别值分配一个整数,不产生新列。例如,
如果类别变量取二值,适合用这种编码方式;如果类别变量取多值,下面的one-hot
编码更合适。
One-hot encoding: 为每一个类别值产生一个新列。每一个观测在它对应的类别列里接收1,否则接收0.
Label Encoding and One-Hot Encoding
在这里,对于两类变量我们使用Label Encoding
, scikit-learn函数LabelEncoder
实现;对于多类变量使用one-hot Encoding
, pandas函数get_dummies
实现。
# Create a label encoder objectle = LabelEncoder()le_count = 0# Iterate through the columnsfor col in app_train:if app_train[col].dtype == 'object':# If 2 or fewer unique categoriesif len(list(app_train[col].unique())) <= 2:# Train on the training datale.fit(app_train[col])# Transform both training and testing dataapp_train[col] = le.transform(app_train[col])app_test[col] = le.transform(app_test[col])# Keep track of how many columns were label encodedle_count += 1print('%d columns were label encoded.' % le_count)
3 columns were label encoded.
# one-hot encoding of categorical variablesapp_train = pd.get_dummies(app_train)app_test = pd.get_dummies(app_test)print('Training Features shape: ', app_train.shape)print('Testing Features shape: ', app_test.shape)
Training Features shape: (307511, 243)
Testing Features shape: (48744, 239)
比对训练集和检验集
检验集和训练集需要相同的特征。One-hot Encoding在训练集里产生了更多的列,因为有一些类别变量的类没有出现在检验集里。为了删除在训练集而不在检验集的列,我们需要比对这两个数据框。首先,从训练集里提取target
列,这个量检验集里没有。
train_labels = app_train['TARGET']# Align the training and testing data, keep only columns present in both dataframesapp_train, app_test = app_train.align(app_test, join = 'inner', axis = 1)# Add the target back inapp_train['TARGET'] = train_labelsprint('Training Features shape: ', app_train.shape)print('Testing Features shape: ', app_test.shape)
Training Features shape: (307511, 240)
Testing Features shape: (48744, 239)
异常变量
变量的观测异常可能有很多种原因,例如,测量误差、极端值。DAYS_BIRTH
列是负值,这是因为该值是相对于当前贷款申请日推算的。我们通过乘-1, 再除以365, 把它转换成年的表示。
print((app_train['DAYS_BIRTH'] / -365).describe())
count 307511.000000
mean 43.936973
std 11.956133
min 20.517808
25% 34.008219
50% 43.150685
75% 53.923288
max 69.120548
Name: DAYS_BIRTH, dtype: float64
从结果看,这些贷款“年龄”是合理的,没有异常值。再来看看’DAYS_EMPLOYED’,
print(app_train['DAYS_EMPLOYED'].describe())
count 307511.000000
mean 63815.045904
std 141275.766519
min -17912.000000
25% -2760.000000
50% -1213.000000
75% -289.000000
max 365243.000000
Name: DAYS_EMPLOYED, dtype: float64
最大值是差不多1000年,太夸张了!让我们提取异常客户集,看看他们是否比其他客户有更高或更低的违约率。
anom = app_train[app_train['DAYS_EMPLOYED'] == 365243]non_anom = app_train[app_train['DAYS_EMPLOYED'] != 365243]print('The non-anomalies default on %0.2f%% of loans' % (100 * non_anom['TARGET'].mean()))print('The anomalies default on %0.2f%% of loans' % (100 * anom['TARGET'].mean()))print('There are %d anomalous days of employment' % len(anom))
The non-anomalies default on 8.66% of loans
The anomalies default on 5.40% of loans
There are 55374 anomalous days of employment
我们看到,异常客户有更低的违约率。异常值似乎有一些重要作用,因此,我们用not a numuber
(np.nan)填充它们,然后产生一个新的布尔列,表示该值是否异常。
# Create an anomalous flag columnapp_train['DAYS_EMPLOYED_ANOM'] = app_train["DAYS_EMPLOYED"] == 365243# Replace the anomalous values with nanapp_train['DAYS_EMPLOYED'].replace({365243: np.nan}, inplace = True)app_train['DAYS_EMPLOYED'].plot.hist(title = 'Days Employment Histogram');plt.xlabel('Days Employment');plt.show()
在检验集上重复相同的操作。
app_test['DAYS_EMPLOYED_ANOM'] = app_test["DAYS_EMPLOYED"] == 365243app_test["DAYS_EMPLOYED"].replace({365243: np.nan}, inplace = True)print('There are %d anomalies in the test data out of %d entries' % (app_test["DAYS_EMPLOYED_ANOM"].sum(), len(app_test)))
There are 9274 anomalies in the test data out of 48744 entries
相关性分析
在处理了类别变量和异常变量后,我们继续进行探索性数据分析。下面,我们将要探索特征与target
变量的相关性。
# Find correlations with the target and sortcorrelations = app_train.corr()['TARGET'].sort_values()# Display correlationsprint('Most Positive Correlations:\n', correlations.tail(15))print('\nMost Negative Correlations:\n', correlations.head(15))
Most Positive Correlations:
OCCUPATION_TYPE_Laborers 0.043019
FLAG_DOCUMENT_3 0.044346
REG_CITY_NOT_LIVE_CITY 0.044395
FLAG_EMP_PHONE 0.045982
NAME_EDUCATION_TYPE_Secondary / secondary special 0.049824
REG_CITY_NOT_WORK_CITY 0.050994
DAYS_ID_PUBLISH 0.051457
CODE_GENDER_M 0.054713
DAYS_LAST_PHONE_CHANGE 0.055218
NAME_INCOME_TYPE_Working 0.057481
REGION_RATING_CLIENT 0.058899
REGION_RATING_CLIENT_W_CITY 0.060893
DAYS_EMPLOYED 0.074958
DAYS_BIRTH 0.078239
TARGET 1.000000
Name: TARGET, dtype: float64
Most Negative Correlations:
EXT_SOURCE_3 -0.178919
EXT_SOURCE_2 -0.160472
EXT_SOURCE_1 -0.155317
NAME_EDUCATION_TYPE_Higher education -0.056593
CODE_GENDER_F -0.054704
NAME_INCOME_TYPE_Pensioner -0.046209
DAYS_EMPLOYED_ANOM -0.045987
ORGANIZATION_TYPE_XNA -0.045987
FLOORSMAX_AVG -0.044003
FLOORSMAX_MEDI -0.043768
FLOORSMAX_MODE -0.043226
EMERGENCYSTATE_MODE_No -0.042201
HOUSETYPE_MODE_block of flats -0.040594
AMT_GOODS_PRICE -0.039645
REGION_POPULATION_RELATIVE -0.037227
Name: TARGET, dtype: float64
表示客户贷款年龄的DAYS_BIRTH
有最大的正相关系数,但是,这个特征的值是负的,意味着客户年龄越大,越不可能违约。为了方便理解,我们使用DAYS_BIRTH
的绝对值,那么相关系数将是负的。
# Find the correlation of the positive days since birth and targetapp_train['DAYS_BIRTH'] = abs(app_train['DAYS_BIRTH'])app_train['DAYS_BIRTH'].corr(app_train['TARGET'])
-0.07823930830982694
负线性关系表示:客户年龄越大,越倾向按时还贷。我们作客户年龄的直方图。
# Set the style of plotsplt.style.use('fivethirtyeight')# Plot the distribution of ages in yearsplt.hist(app_train['DAYS_BIRTH'] / 365, edgecolor = 'k', bins = 25)plt.title('Age of Client'); plt.xlabel('Age (years)'); plt.ylabel('Count');plt.show()
为了可视化年龄对target
的影响,我们作对应不同target值的年龄核密度估计图。
plt.figure(figsize = (10, 8))# KDE plot of loans that were repaid on timesns.kdeplot(app_train.loc[app_train['TARGET'] == 0, 'DAYS_BIRTH'] / 365, label = 'target == 0')# KDE plot of loans which were not repaid on timesns.kdeplot(app_train.loc[app_train['TARGET'] == 1, 'DAYS_BIRTH'] / 365, label = 'target == 1')# Labeling of plotplt.xlabel('Age (years)'); plt.ylabel('Density'); plt.title('Distribution of Ages');plt.show()
尽管并不显著相关(相关系数-0.07),但是Age
的确对target
有影响,它可能在机器学习模型里有用。
Exterior Sources
与target负相关性最强的3个变量是EXT_SOURCE_1
,EXT_SOURCE_2
, andEXT_SOURCE_3
. 这三个特征表示来自外部数据源的标准化分数,代表某种累积的信用评级。我们看看它们之间的相关系数,并画出热图。
# Extract the EXT_SOURCE variables and show correlationsext_data = app_train[['TARGET', 'EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3', 'DAYS_BIRTH']]ext_data_corrs = ext_data.corr()ext_data_corrs
plt.figure(figsize = (8, 6))# Heatmap of correlationssns.heatmap(ext_data_corrs, cmap = plt.cm.RdYlBu_r, vmin = -0.25, annot = True, vmax = 0.6)plt.title('Correlation Heatmap');plt.show()
所有3个EXT_SOURCE
与target存在负相关,表示随着EXT_SOURCE增大,客户更容易贷到款。DAYS_BIRTH
与EXT_SOURCE_1
正相关(0.6), 这表明分数的因素之一是客户年龄。下面我们看看这3个特征的分布。
plt.figure(figsize = (10, 12))# iterate through the sourcesfor i, source in enumerate(['EXT_SOURCE_1', 'EXT_SOURCE_2', 'EXT_SOURCE_3']):# create a new subplot for each sourceplt.subplot(3, 1, i + 1)# plot repaid loanssns.kdeplot(app_train.loc[app_train['TARGET'] == 0, source], label = 'target == 0')# plot loans that were not repaidsns.kdeplot(app_train.loc[app_train['TARGET'] == 1, source], label = 'target == 1')# Label the plotsplt.title('Distribution of %s by Target Value' % source)plt.xlabel('%s' % source); plt.ylabel('Density');plt.tight_layout(h_pad = 2.5)
EXT_SOURCE_3
显示了最不同的分布,该变量对预测一个申请者是否能按时偿还贷款是有用的。
未完待续
如果觉得《信贷违约风险评估模型(上篇):探索性数据分析》对你有帮助,请点赞、收藏,并留下你的观点哦!