本案例使用一份真实的Lending Club贷款数据集,展示如何使用Pandas等工具对数据进行清洗。包括如何进行缺失值的检测和处理,如何进行异常数据的检测和处理,如何检测数据中的重复值等。

1.数据读取

本案例主要背景为贷款情况审查。银行可以通过个人贷款状况对个人信用进行分类,从而更好地避免金融诈骗的发生。本案例所选的数据集是来自LendingClub中统计的2018年第四季度的借贷数据。

数据集共有90112行,145列。此处挑选具有代表性的列说明其含义,完整的数据字典可以查看源数据网页中的DATA DICTIONARY。

列名 含义说明
id 为贷款列表分配的唯一信用ID。
member_id 为借款人成员分配的唯一信用证ID。
loan_amnt 借款人申请贷款的金额。如果在某个时间点,信贷部门减少贷款金额,那么它将反映在这个值中。
funded_amnt 在某时间点承诺的贷款总额。
funded_amnt_inv 当时投资者为该笔贷款承诺的总金额。
term 贷款月份。值以月为单位,可以是36或60。
int_rate 贷款利率
installment 如果贷款发放,借款人每月所需要还款的数额。
grade 信用证指定贷款等级
sub_grade 信用证指定贷款基础
emp_title 职业
emp_length 就业年限(年)。可能的值介于0和10之间,其中0表示一年以下,10表示十年或十年以上。
home_ownership 借款人在登记期间提供的或从信贷报告中获得的房屋所有权状况。其值为:租金、自有、抵押、其他
hardship_payoff_balance_amount 困难计划开始日期的收支差额
hardship_last_payment_amount 截至困难计划开始日期的最后一笔付款金额
disbursement_method 借款人获得贷款的方式。可能的值是:现金,直接支付
debt_settlement_flag 标记已注销的借款人是否与债务结算公司合作。
debt_settlement_flag_date 设置债务结算标志的最新日期
settlement_status 借款人结算计划的状态。可能的值有:完成、进行、中断、取消、拒绝
settlement_date 借款人同意结算计划的日期
settlement_amount 借款人同意偿还的贷款金额
settlement_percentage 结算金额占贷款支付余额的百分比
settlement_term 借款人在结算计划中的月数
In [1]:
## 导入类库
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib
import seaborn as sns

使用Pandas的read_csv()函数读取数据,并查看数据。

In [2]:
dataset = pd.read_csv("./input/lendingclub.csv")
dataset.head(5)
/explorer/pyenv/jupyter-py36/lib/python3.6/site-packages/IPython/core/interactiveshell.py:2717: DtypeWarning: Columns (123,124,125,128,129,130,133,139,140,141) have mixed types. Specify dtype option on import or set low_memory=False.
  interactivity=interactivity, compiler=compiler, result=result)
Out[2]:
id member_id loan_amnt funded_amnt funded_amnt_inv term int_rate installment grade sub_grade ... hardship_payoff_balance_amount hardship_last_payment_amount disbursement_method debt_settlement_flag debt_settlement_flag_date settlement_status settlement_date settlement_amount settlement_percentage settlement_term
0 NaN NaN 2500 2500 2500.0 36 months 13.56% 84.92 C C1 ... NaN NaN Cash N NaN NaN NaN NaN NaN NaN
1 NaN NaN 30000 30000 30000.0 60 months 18.94% 777.23 D D2 ... NaN NaN Cash N NaN NaN NaN NaN NaN NaN
2 NaN NaN 4000 4000 4000.0 36 months 18.94% 146.51 D D2 ... NaN NaN Cash N NaN NaN NaN NaN NaN NaN
3 NaN NaN 30000 30000 30000.0 60 months 16.14% 731.78 C C4 ... NaN NaN Cash N NaN NaN NaN NaN NaN NaN
4 NaN NaN 5550 5550 5550.0 36 months 15.02% 192.45 C C3 ... NaN NaN Cash N NaN NaN NaN NaN NaN NaN

5 rows × 145 columns

该数据集包含在规定时间内发放的所有贷款的完整贷款数据,包括当前贷款状态loan_status(延迟,全额支付等)和settlement_status(最新支付信息)等。包含通过前一个完成的日历季度发放的所有贷款的完整贷款数据。

查看数据集中行与列数量。

In [3]:
dataset.shape
Out[3]:
(90112, 145)

可见数据集共有90112行,145列。

2.数据清洗

2.1 缺失值的查看

首先介绍在数据缺失值处理中所经常用到的函数isnulldropnafillnanotnull

一般来说,在进行数据清洗的时候会先使用isnull函数来查看对应的缺失值所对应的地方,如果直接使用isnull函数来对数据进行缺失值的直接查看,那么返回一个布尔类型数据集,该数据集与原始数据格式相同,例如一个数据集使用了Numpy的数组存储,那么返回的就是含有布尔值的数组,如果使用的是Pandas的DataFrame存储,那么返回的就是含有布尔值的DataFrame

In [4]:
## 查缺失值的前10行数据
dataset.isnull().head(10)
Out[4]:
id member_id loan_amnt funded_amnt funded_amnt_inv term int_rate installment grade sub_grade ... hardship_payoff_balance_amount hardship_last_payment_amount disbursement_method debt_settlement_flag debt_settlement_flag_date settlement_status settlement_date settlement_amount settlement_percentage settlement_term
0 True True False False False False False False False False ... True True False False True True True True True True
1 True True False False False False False False False False ... True True False False True True True True True True
2 True True False False False False False False False False ... True True False False True True True True True True
3 True True False False False False False False False False ... True True False False True True True True True True
4 True True False False False False False False False False ... True True False False True True True True True True
5 True True False False False False False False False False ... True True False False True True True True True True
6 True True False False False False False False False False ... True True False False True True True True True True
7 True True False False False False False False False False ... True True False False True True True True True True
8 True True False False False False False False False False ... True True False False True True True True True True
9 True True False False False False False False False False ... True True False False True True True True True True

10 rows × 145 columns

可见直接调用isnull()函数处理大数据集只能得到一个存有True或者False的数据集,结果并不直观,需要对结果进一步处理。

查看数据中缺失值数量所占总数据量的百分比,从而使结果更加直观,以便进一步处理缺失值。

创建一个新的DataFrame数据表来存储每列数据中缺失值所占的百分比。

In [5]:
na_ratio = pd.DataFrame(dataset.isnull().sum()/len(dataset)*100,columns=['NA_Ratio'])

由于数据列过多,选取查看缺失值占总数据百分比大于90%的列名及其数值。

In [6]:
df_na = na_ratio[na_ratio['NA_Ratio']>=90].sort_values(by = 'NA_Ratio', ascending=False)
df_na
Out[6]:
NA_Ratio
id 100.000000
url 100.000000
desc 100.000000
member_id 100.000000
hardship_end_date 99.998890
hardship_last_payment_amount 99.998890
hardship_payoff_balance_amount 99.998890
orig_projected_additional_accrued_interest 99.998890
hardship_loan_status 99.998890
hardship_dpd 99.998890
hardship_length 99.998890
payment_plan_start_date 99.998890
hardship_start_date 99.998890
hardship_amount 99.998890
deferral_term 99.998890
hardship_status 99.998890
hardship_reason 99.998890
hardship_type 99.998890
debt_settlement_flag_date 99.997781
settlement_status 99.997781
settlement_date 99.997781
settlement_amount 99.997781
settlement_percentage 99.997781
settlement_term 99.997781
sec_app_mths_since_last_major_derog 96.001642

可见还是有较多的列几乎全部为空,对于这些列一般都可以简单地认为它在数据分析中提供极有限信息,所以可以直接删除。

由于数据列过多,选取缺失值占总数据百分比大于0.01%,小于80%的列名及其数值显示到图上。

In [7]:
df_na = na_ratio[(na_ratio['NA_Ratio']<80 ) & (na_ratio['NA_Ratio']>=0.01)].sort_values(by = 'NA_Ratio', ascending=False)
## 绘图
fig, ax = plt.subplots(figsize=(20,10))
nar = sns.barplot(x=df_na.index, y=df_na['NA_Ratio'],orient='v',palette="Blues_d")
ax.set_xticklabels(df_na.index, rotation=90, fontsize=20)
ax.set_ylabel('Ratio of Missing Values', fontsize=25)
ax.set_xlabel('Features', fontsize=25)
ax.set_title('Ratio of Missing Values by Features', fontsize=30)
for loc, value in zip(ax.patches, df_na.NA_Ratio):
    ax.text(loc.get_x()+loc.get_width()/2, loc.get_height()+2, round(value,2),ha='center', va='bottom',fontsize=16)
plt.show()

由此可见,部分数据中缺失值较多,故可以尝试使用多种方法删除或填补。

2.2 删除缺失值

通过删除缺失值过多的列来进行初步的数据清洗工作。

一般来说,删除缺失值所用的函数是dropna(),其原理是删除带有任何存有缺失值的行,对于真实数据集中不同列有不同的缺失值存在的地方,甚至可能有某一列全是缺失值,简单使用dropna()函数就会直接得到如下的结果。

In [8]:
dataset.dropna()
Out[8]:
id member_id loan_amnt funded_amnt funded_amnt_inv term int_rate installment grade sub_grade ... hardship_payoff_balance_amount hardship_last_payment_amount disbursement_method debt_settlement_flag debt_settlement_flag_date settlement_status settlement_date settlement_amount settlement_percentage settlement_term

0 rows × 145 columns

依据上面的表可以发现,简单使用dropna()函数只会让数据结果无效化,不仅删除了缺失值的信息,也删除了所有的暂时不需要删除的信息,故而依据之前所查看缺失值画的柱状图,可以发现缺失值比例在(0.01%,80%)的列中,除3列数据缺失值在56%以上,其余列数据的缺失值均小于17%,故可以简单认为在此数据集中缺失值在56%以上的数据列提供信息有限,故将缺失百分比56%以上的列数据全部删除。

In [9]:
null_index = list(na_ratio[na_ratio['NA_Ratio']>=56].index)
print("需要删除的缺失值较多的列为:",null_index)
需要删除的缺失值较多的列为: ['id', 'member_id', 'url', 'desc', 'mths_since_last_delinq', 'mths_since_last_record', 'mths_since_last_major_derog', 'annual_inc_joint', 'dti_joint', 'verification_status_joint', 'mths_since_recent_bc_dlq', 'mths_since_recent_revol_delinq', 'revol_bal_joint', 'sec_app_earliest_cr_line', 'sec_app_inq_last_6mths', 'sec_app_mort_acc', 'sec_app_open_acc', 'sec_app_revol_util', 'sec_app_open_act_il', 'sec_app_num_rev_accts', 'sec_app_chargeoff_within_12_mths', 'sec_app_collections_12_mths_ex_med', 'sec_app_mths_since_last_major_derog', 'hardship_type', 'hardship_reason', 'hardship_status', 'deferral_term', 'hardship_amount', 'hardship_start_date', 'hardship_end_date', 'payment_plan_start_date', 'hardship_length', 'hardship_dpd', 'hardship_loan_status', 'orig_projected_additional_accrued_interest', 'hardship_payoff_balance_amount', 'hardship_last_payment_amount', 'debt_settlement_flag_date', 'settlement_status', 'settlement_date', 'settlement_amount', 'settlement_percentage', 'settlement_term']

使用copy()函数拷贝一份原数据的副本进行处理,以免接下来的步骤出错导致需要重新读取数据。处理完毕后查看新数据集行与列的情况以确认删除成功。

In [10]:
dataset_copy = dataset.copy()

使用drop()函数直接删除整行或整列数据,其中参数axis控制以列(0)或者以行(1)的形式删除,inplace代表处理完毕后是否替换这个DataFrame

In [11]:
dataset_copy.drop(null_index,axis=1,inplace=True)
dataset_copy.shape
Out[11]:
(90112, 102)

在处理完毕后,可以发现行的数量没有改变,列数量减少到了102列,故而删除成功。

2.3 简易填补缺失值

删除掉含有较多缺失值的列后,需要填补剩余的包含缺失值的列。使用fillna()函数填补缺失值,该函数能自动定位到所有缺失值所在的位置,并将其补齐。

参数 说明
value 用于填充缺失值的标量值或字典对象
method 插值方式,如果函数调用时未指定其他参数的话,默认为ffill
axis 带填充的轴。默认为0,代表行,1代表列
inplace 是否修改调用者对象而不产生副本,True代表修改,False代表不修改
limit (对于前向和后向填充)可以连续填充的最大数量

对剩下的列进行不同的填补方式的处理。对缺失值还是较多的列分别进行平均数,众数以及中位数的填补。

In [12]:
less_null_index = list(na_ratio[(na_ratio['NA_Ratio']<=56) & (na_ratio['NA_Ratio']>=8)].index)
print("需要填补的缺失值的列为:",less_null_index)
需要填补的缺失值的列为: ['emp_title', 'emp_length', 'il_util', 'mths_since_recent_inq']

首先分别查看这四列的信息,以方便之后进行填补。

In [13]:
dataset_copy[less_null_index].head(5)
Out[13]:
emp_title emp_length il_util mths_since_recent_inq
0 Chef 10+ years 69.0 2.0
1 Postmaster 10+ years 88.0 4.0
2 IT Supervisor 10+ years 68.0 5.0
3 Mechanic 10+ years 89.0 13.0
4 Director COE 10+ years 72.0 3.0
In [14]:
dataset_copy[less_null_index].dtypes
Out[14]:
emp_title                 object
emp_length                object
il_util                  float64
mths_since_recent_inq    float64
dtype: object

通过查看可以得知,这四列数据分别为字符型以及浮点型数据,对这四列分别进行不同的办法的填补。

DataFrame求其一列即Series对象中的均值的方法为mean,众数的方法为mode,中位数的方法为median

首先是对字符型的填补。

emp_title列的意思是借款人在申请贷款时提供的职务,此处为空的情况下不能简单的进行填补,因为有可能空值代表该人无职业或者职业不明确,也没有好的办法对这些缺失值的职业进行判断,此处使用哑变量的思路进行填补,对缺失值进行新属性的添加,设置其为workless即可。

In [15]:
dataset_copy['emp_title'].fillna("workless", inplace=True)
dataset_copy['emp_title'].isnull().sum()
Out[15]:
0

可以看到在进行哑变量的填补之后,缺失值也被填补完毕。

emp_length列是所有贷款帐户用户的工作年限,此处尝试使用中位数的方法进行填补。

首先查看该列的中位数是多少,由于该列存储的是字符串数据,且都为年份,但是表达的含义是数值型,故而先使用正则表达式将其匹配转换为数值型,然后求其中位数。

首先导入正则表达式所需要的包re。

In [16]:
## 导入包
import re
In [17]:
pattern = re.compile(r'\d+')
## 初始化一个列表
result = []
## 因为有缺失值的存在,会使得正则表达式失败,使程序报错退出,故而引入异常检测来规避缺失值
for i in list(range(0,len(dataset_copy['emp_length']))):
    try:
        replace_number = int(pattern.findall(dataset_copy['emp_length'][i])[0])
        result.append(replace_number)
        ## 此处可以直接对原DataFrame的数据进行替换,若此处进行替换操作,会因为DataFrame拷贝操作而导致耗时极长
        ## dataset_copy['emp_length'][i] = replace_number
    except:
        pass

查看一下是否将所有的非缺失值存储到列表中了,此处使用notnull()函数来查看所有的非缺失值数量。

In [18]:
print("列表长度",len(result))
print("非缺失值的数量",dataset_copy['emp_length'].notnull().sum())
列表长度 81856
非缺失值的数量 81856

新建一个DataFrame来存储新的数据,由此新的DataFrame来计算得到所需的中位数的值,再填补回原数据中。

在此使用median函数来求得该列的中位数。

In [19]:
df = pd.DataFrame()
df['result'] = result
df.median()
Out[19]:
result    5.0
dtype: float64

可见其中位数的值是5,也代表若将客户的工作年限从小到大排列,其中间的工作年限是5年

将信息补完,再填补回原数据中。此处使用简单的字符串粘贴即可。

In [20]:
replace_str = str(int(df.median()))+ " years"
print(replace_str)
5 years

使用fillna()函数来填补缺失值。

In [21]:
dataset_copy['emp_length'].fillna(replace_str,inplace=True)
dataset_copy['emp_length'].isnull().sum()
Out[21]:
0

可见该列缺失值的数量为0,填补成功。

接下来对其余两列进行填补。

il_util列是所有贷款帐户的当前总余额占信用限额的比例,采取简单的均值填补的方式。

首先查看该列的均值为多少。

In [22]:
dataset_copy['il_util'].mean()
Out[22]:
68.29652988520913

接下来就使用fillna()函数来对其进行填补。

In [23]:
dataset_copy['il_util'].fillna(dataset_copy['il_util'].mean(),inplace = True)
dataset_copy['il_util'].isnull().sum()
Out[23]:
0

可以看到此时该列的缺失值的数量为0,填补成功。

接下来对最后一列进行填补。

mths_since_recent_inq列是距离上一次调查的时间(单位是月)。

首先查看该列的众数为多少。

In [24]:
dataset_copy['mths_since_recent_inq'].mode()
Out[24]:
0    1.0
dtype: float64

可见多数人距上一次贷款调查的时间为一个月左右,此处就用这个值进行填补。

使用fillna()函数进行填补

In [25]:
dataset_copy['mths_since_recent_inq'].fillna(dataset_copy['mths_since_recent_inq'].mode()[0],inplace=True)
dataset_copy['mths_since_recent_inq'].isnull().sum()
Out[25]:
0

缺失值的数量为0,该列填补成功。此时对这四列的简单填补成功,下面介绍一种更加简单的方式来对这四列进行填补。

首先拷贝一份原数据副本,避免再次读取原数据,造成不必要的内存占用。

In [26]:
dataset_copy_2 = dataset.copy()

在前面介绍fillna()函数时,其value参数可以用一个字典进行传入,这样对其四列需要填补的属性来进行一个字典的创建,就可以只需要一个fillna()函数来进行填补了。

首先创建一个字典用于存储填补缺失值所需要传入的字典。

因之前已经计算完毕了填补各列所需的值,此处就直接使用计算得到的值即可。

In [27]:
fill_dict = {
 less_null_index[0]:"workless",
 less_null_index[1]:"5 years",
 less_null_index[2]:dataset_copy_2[less_null_index[2]].mean(),
 less_null_index[3]:dataset_copy_2[less_null_index[3]].mode()[0]
}
print(fill_dict)
{'emp_title': 'workless', 'emp_length': '5 years', 'il_util': 68.29652988520913, 'mths_since_recent_inq': 1.0}
In [28]:
dataset_copy_2.fillna(fill_dict,inplace=True)
dataset_copy_2[less_null_index].isnull().sum()
Out[28]:
emp_title                0
emp_length               0
il_util                  0
mths_since_recent_inq    0
dtype: int64

可以发现各列的缺失值数量都为0,填补成功。

2.4 向前向后与插值法进行缺失值的填补

进行前向与后向填补时,也是使用上文介绍的fillna()函数,对该函数中的method参数进行设置,设置为bfill即为后值向前填补,设置为pad即为前值向后填补。

接下来对剩下缺失百分比在3%左右的列进行前后方式以及插值法填补,首先查看缺失百分比在2.5%到4%的列有哪些。

In [29]:
less_null_index = list(na_ratio[(na_ratio['NA_Ratio']<=4) & (na_ratio['NA_Ratio']>=2.5)].index)
print("需要填补的缺失值的列为:",less_null_index)
需要填补的缺失值的列为: ['next_pymnt_d', 'mths_since_rcnt_il', 'mo_sin_old_il_acct']

查看此三列的信息。

In [30]:
dataset_copy[less_null_index].head(5)
Out[30]:
next_pymnt_d mths_since_rcnt_il mo_sin_old_il_acct
0 Mar-2019 2.0 140.0
1 Mar-2019 3.0 163.0
2 Mar-2019 5.0 62.0
3 Mar-2019 4.0 53.0
4 Mar-2019 4.0 195.0
In [31]:
dataset_copy[less_null_index].dtypes
Out[31]:
next_pymnt_d           object
mths_since_rcnt_il    float64
mo_sin_old_il_acct    float64
dtype: object

next_pymnt_d列是指下一个计划还款日期,对所有缺失值均使用缺失值所在的后一个非缺失值的值来进行填补。

首先简单查看缺失值在该列中的位置,方便之后进行对比,此处选取索引为253到259的几行来作为对比。

In [32]:
dataset_copy['next_pymnt_d'][dataset_copy['next_pymnt_d'].isnull()].head(10)
Out[32]:
72     NaN
112    NaN
191    NaN
253    NaN
259    NaN
352    NaN
394    NaN
441    NaN
577    NaN
581    NaN
Name: next_pymnt_d, dtype: object
In [33]:
dataset_copy['next_pymnt_d'][250:265]
Out[33]:
250    Mar-2019
251    Mar-2019
252    Mar-2019
253         NaN
254    Mar-2019
255    Mar-2019
256    Mar-2019
257    Mar-2019
258    Mar-2019
259         NaN
260    Mar-2019
261    Mar-2019
262    Mar-2019
263    Mar-2019
264    Mar-2019
Name: next_pymnt_d, dtype: object

可以看到此处有两个缺失值,接下来进行填补,填补完毕之后再查看其效果。

In [34]:
dataset_copy['next_pymnt_d'] = dataset_copy['next_pymnt_d'].fillna(method = "bfill")
dataset_copy['next_pymnt_d'][250:265]
Out[34]:
250    Mar-2019
251    Mar-2019
252    Mar-2019
253    Mar-2019
254    Mar-2019
255    Mar-2019
256    Mar-2019
257    Mar-2019
258    Mar-2019
259    Mar-2019
260    Mar-2019
261    Mar-2019
262    Mar-2019
263    Mar-2019
264    Mar-2019
Name: next_pymnt_d, dtype: object

可见填补完毕后,缺失值消失,且均填补为了后值。

此处使用前值向后填补来填补mths_since_rcnt_il列。

mths_since_rcnt_il列的含义是最近分期付款账户开立后的月份时间。

In [35]:
dataset_copy['mths_since_rcnt_il'][dataset_copy['mths_since_rcnt_il'].isnull()].head(10)
Out[35]:
67    NaN
94    NaN
211   NaN
212   NaN
222   NaN
294   NaN
336   NaN
375   NaN
379   NaN
384   NaN
Name: mths_since_rcnt_il, dtype: float64

同样选取中间部分来方便之后进行对比查看。

In [36]:
dataset_copy['mths_since_rcnt_il'][370:390]
Out[36]:
370     19.0
371      4.0
372     24.0
373      6.0
374      6.0
375      NaN
376      4.0
377    206.0
378     23.0
379      NaN
380     12.0
381      8.0
382     17.0
383      9.0
384      NaN
385     23.0
386      2.0
387     15.0
388     72.0
389     19.0
Name: mths_since_rcnt_il, dtype: float64
In [37]:
dataset_copy['mths_since_rcnt_il'] = dataset_copy['mths_since_rcnt_il'].fillna(method = "pad")
dataset_copy['mths_since_rcnt_il'][370:390]
Out[37]:
370     19.0
371      4.0
372     24.0
373      6.0
374      6.0
375      6.0
376      4.0
377    206.0
378     23.0
379     23.0
380     12.0
381      8.0
382     17.0
383      9.0
384      9.0
385     23.0
386      2.0
387     15.0
388     72.0
389     19.0
Name: mths_since_rcnt_il, dtype: float64

填补完毕后,该列不存在缺失值了。

接下来对mo_sin_old_il_acct列使用插值法进行填补。

mo_sin_old_il_acct列代表客户首次开立分期付款账户至今的累计月份数。

In [38]:
dataset_copy['mo_sin_old_il_acct'][dataset_copy['mo_sin_old_il_acct'].isnull()].head(10)
Out[38]:
67    NaN
94    NaN
211   NaN
212   NaN
222   NaN
294   NaN
336   NaN
375   NaN
379   NaN
384   NaN
Name: mo_sin_old_il_acct, dtype: float64

此处选取索引为210到225的行来作为对比查看。

In [39]:
dataset_copy['mo_sin_old_il_acct'][210:225]
Out[39]:
210    174.0
211      NaN
212      NaN
213     96.0
214    138.0
215    233.0
216    170.0
217    237.0
218    130.0
219    141.0
220    166.0
221     83.0
222      NaN
223    120.0
224     84.0
Name: mo_sin_old_il_acct, dtype: float64

简单的删除、填充、替换缺失数据会导致整体数据方差的变化,从而导致数据信息量的变换。使用插值法可解决此问题,即使用interpolate()函数填补缺失值。

该函数的主要参数是method,常见的插入方法包括:linear, time, index, valuesspline等,参数不赋值时默认为线性插入法linear,即用该列数据缺失值前一个数据和后一个数据建立插值直线,然后使用缺失点在线性插值函数的函数值填充该缺失值。对于method参数设置为polynomialspline等,需要设置对应的order参数,代表所使用方法的逼近次数。

该函数的默认值填补是使用了在一个或多个缺失值的前后非空值部分,将其等分填入,即简单的拉格朗日插值法。

In [40]:
dataset_copy['mo_sin_old_il_acct'] = dataset_copy['mo_sin_old_il_acct'].interpolate()
dataset_copy['mo_sin_old_il_acct'][210:225]
Out[40]:
210    174.0
211    148.0
212    122.0
213     96.0
214    138.0
215    233.0
216    170.0
217    237.0
218    130.0
219    141.0
220    166.0
221     83.0
222    101.5
223    120.0
224     84.0
Name: mo_sin_old_il_acct, dtype: float64

接下来对interpolate()函数的更多参数进行介绍。

选取部分列作为备选,方便接下来进行处理。

In [41]:
null_index = list(na_ratio[ (na_ratio['NA_Ratio']>=1) & (na_ratio['NA_Ratio']<2.5) ].index)
print("缺失值列为:",null_index)
缺失值列为: ['bc_open_to_buy', 'bc_util', 'mths_since_recent_bc', 'num_tl_120dpd_2m', 'percent_bc_gt_75']

interpolate函数中,将method参数设置为polynomial代表多项式插值,进一步将order参数设置为2,代表使用二次多项式插值。

bc_open_to_buy列代表着在循环银行卡上的购买金额,选取503行到510行查看数据。

In [42]:
dataset_copy['bc_open_to_buy'][503:510]
Out[42]:
503    12854.0
504        NaN
505     1292.0
506    33050.0
507    20920.0
508    17096.0
509     9650.0
Name: bc_open_to_buy, dtype: float64

我们使用二次多项式插值法后将其缺失值进行填补。

In [43]:
dataset_copy['bc_open_to_buy'].interpolate(method = "polynomial", order = 2, inplace=True)
dataset_copy['bc_open_to_buy'][503:510]
Out[43]:
503    12854.000000
504     2142.755582
505     1292.000000
506    33050.000000
507    20920.000000
508    17096.000000
509     9650.000000
Name: bc_open_to_buy, dtype: float64

使用interpolate来进行三次样条插值的方式,将method参数设置为spline,将order参数设置为3

bc_util列代表所有银行卡帐户的当前总余额与信用限额的比例。

选取1129行到1135行来查看数据。

In [44]:
dataset_copy['bc_util'][1129:1135]
Out[44]:
1129    71.3
1130     NaN
1131    61.5
1132     NaN
1133    22.5
1134    38.5
Name: bc_util, dtype: float64
In [45]:
dataset_copy['bc_util'].interpolate(method = "spline", order = 3, inplace=True)
dataset_copy['bc_util'][1129:1135]
Out[45]:
1129    71.300000
1130    78.194795
1131    61.500000
1132    39.055359
1133    22.500000
1134    38.500000
Name: bc_util, dtype: float64

对剩下的所有列的缺失值进行简单的向前填补处理。

In [46]:
dataset_copy.fillna(method = "bfill",inplace = True)

查看所有数据里面是否还有缺失值。

In [47]:
dataset_copy.isnull().sum().sum()
Out[47]:
0

可见,经过一系列的处理后数据中已经没有缺失值。

在缺失值处理中还有使用模型填补的处理方式,其代表是使用KNN算法对缺失值进行填补。该方法一般适用于在数据集中只有一列或者两列有缺失值的时候,因为较多缺失值进行了填补之后会导致数据本身就存在一定误差,与事实有较大偏离,那么在运算模型的时候,这些填补过的数据列就会产生较大影响,从而使得最后的填补建模以及预测或者回归建模都会存在较大的误差,而只有较少数据列有缺失的时候,此时使用建模方法进行填充就等于使用别的所有的无缺失值的列来预测该存在缺失值的列,从而就转化为了一个建模与预测的问题。除了KNN算法,还有随机森林、GBDT等集成方法可以使用。使用模型进行填补的方法需要用到扩展包Fancyimpute或者Sklearn,其具体做法也与之后的数据建模类似。

2.5 异常值处理

在数据预处理时很容易忽略异常值的处理,因为其发现难度较大。但是异常值的存在是会对模型产生非常大的影响的,所以找到异常值并处理异常值很重要。

处理异常值的过程中,较难的是如何找到,一般来说会绘制箱线图或者该列的折线图来进行异常值的查看,找到异常值后可以有各种方法来对其进行处理,例如直接删除该数据,或者进行各类填补,此处填补方式与缺失值类似就不多介绍,主要介绍如何找到缺失值。

mths_since_rcnt_il列是最近分期付款账户开立后的月份,使用describe函数查看其分布。

In [48]:
dataset_copy['mths_since_rcnt_il'].describe()
Out[48]:
count    90112.000000
mean        20.259066
std         24.966505
min          0.000000
25%          6.000000
50%         13.000000
75%         24.000000
max        505.000000
Name: mths_since_rcnt_il, dtype: float64

可以发现最大值异常的大,有异常值的趋势,但是是否真的符合以及是否只有最大值是异常值还有待进一步挖掘。 故而此处绘制箱线图来查看数据的分布。

导入所需要的包Seaborn。

In [49]:
# 导入包
import seaborn as sns
In [50]:
## 绘图
sns.set(style="whitegrid", color_codes=True)
sns.boxplot(x='mths_since_rcnt_il',data=dataset)
plt.show()

由箱线图可以发现,值在300以上较为分散,且相对别的数据这几个数据较为离群,故而简单的认为对于该列,大于300的均为离群值,即异常值。

查看这些异常值。

In [51]:
dataset_copy['mths_since_rcnt_il'][dataset_copy['mths_since_rcnt_il']>300]
Out[51]:
3036     505.0
8144     331.0
11951    303.0
13328    315.0
20015    355.0
22236    316.0
22237    316.0
22648    384.0
47891    355.0
57039    408.0
57293    373.0
57453    314.0
65981    345.0
68434    423.0
88220    395.0
Name: mths_since_rcnt_il, dtype: float64

使用drop()函数来删除这些异常值所存在的行。

In [52]:
print("删除异常值之前的行数",len(dataset_copy['mths_since_rcnt_il']))
dataset_copy.drop(list(dataset_copy['mths_since_rcnt_il'][dataset_copy['mths_since_rcnt_il']>300].index),axis = 0,inplace=True)
print("删除异常值之后的行数",len(dataset_copy['mths_since_rcnt_il']))
删除异常值之前的行数 90112
删除异常值之后的行数 90097

2.6 重复值处理

一般检测重复数据使用duplicated函数。

In [53]:
## 重置索引
dataset_copy.reset_index(inplace = True)
dataset_copy.drop('index',axis=1,inplace=True)
dataset_copy[dataset_copy.duplicated()]
Out[53]:
loan_amnt funded_amnt funded_amnt_inv term int_rate installment grade sub_grade emp_title emp_length ... percent_bc_gt_75 pub_rec_bankruptcies tax_liens tot_hi_cred_lim total_bal_ex_mort total_bc_limit total_il_high_credit_limit hardship_flag disbursement_method debt_settlement_flag

0 rows × 102 columns

可见数据中不存在重复值。为了演示重复值检测的方法,此处从数据中随机选取一个行并将其添加到数据中。

In [54]:
random_number = np.random.randint(0,len(dataset_copy['loan_amnt']))
print("随机选择的行为:",random_number)
随机选择的行为: 53486
In [55]:
print("粘贴前的行数为:",len(dataset_copy['loan_amnt']))
duplicated_row = dataset_copy.iloc[random_number]
dataset_copy.loc[len(dataset_copy['loan_amnt'])] = duplicated_row
print("粘贴后的行数为:",len(dataset_copy['loan_amnt']))
粘贴前的行数为: 90097
粘贴后的行数为: 90098

此时再使用duplicated()函数来查看重复值。

In [56]:
dataset_copy[dataset_copy.duplicated()]
Out[56]:
loan_amnt funded_amnt funded_amnt_inv term int_rate installment grade sub_grade emp_title emp_length ... percent_bc_gt_75 pub_rec_bankruptcies tax_liens tot_hi_cred_lim total_bal_ex_mort total_bc_limit total_il_high_credit_limit hardship_flag disbursement_method debt_settlement_flag
90097 10000 10000 10000.0 60 months 11.55% 220.18 B B4 Compliance Auditor 2 years ... 28.6 0 0 94975 51884 16700 49375 N Cash N

1 rows × 102 columns

此时可以看到复制的一行重复值已经被找到。

接下来就是删除重复值,一般使用drop_duplicated()来删除,其参数keep设置为first时,代表删除重复值时保留第一次出现的数据,设置为last时代表删除重复值时保留最后出现的数据,设置为False时代表去除所有重复的数据,inplace代表是否替换原DataFrame

In [57]:
dataset_copy.drop_duplicates(keep='first',inplace=True)
In [58]:
dataset_copy[dataset_copy.duplicated()]
Out[58]:
loan_amnt funded_amnt funded_amnt_inv term int_rate installment grade sub_grade emp_title emp_length ... percent_bc_gt_75 pub_rec_bankruptcies tax_liens tot_hi_cred_lim total_bal_ex_mort total_bc_limit total_il_high_credit_limit hardship_flag disbursement_method debt_settlement_flag

0 rows × 102 columns

删除完毕后再次调用duplicated()函数来查看重复值,可以发现重复值已经被删除。

3.数据保存

在做完数据清洗后会对新的“好数据”进行存储,以方便之后的建模等工作时可以直接调用。

3.1 Python自带文件写入函数的存储

Python自带的函数写入文件较为简单,首先需要将文件作为对象读取,也就是使用open()函数将文件载入到内存中并创建一个对应的对象,其中第一个字符串代表着文件的路径,第二个w代表是对该文件进行写处理,若为r则代表对该文件进行读处理。

In [59]:
fw = open("./input/output.csv","w")

在创建完毕写入文件对象后,可以使用write()或者writelines()函数将内容传入,二者的区别在于write()函数只能传入字符串对象,而writelines()函数可以传入列表作为写入的内容,需要在列表中添加换行符进行换行。

此处使用write()作为写入的函数使用,此处输出查看传入的字符串参数来检查写入到文件的内容。

首先读取DataFrame的列名,并将其写入到文件的第一行,因为写入文件函数write()的参数需要是一个字符串,所以首先对读取到的列名进行简单的字符串粘贴,且在最后加入转义字符\n进行换行,方便接下来的内容的写入。

因为此处输出的文件是csv格式文件,该文件格式是逗号分隔符,故而需要在每一个不同的列或者内容后面添加逗号用以区分,不同格式文件有不同存储规则,例如txt一般是空格分隔符文件,则需要在不同内容后面加入空格予以区分。

In [60]:
columns = list(dataset_copy.columns)
columns_write = ""
for column in columns:
    columns_write = columns_write+","+column
columns_write = columns_write[1:]
print(columns_write)
fw.write(columns_write+"\n")
loan_amnt,funded_amnt,funded_amnt_inv,term,int_rate,installment,grade,sub_grade,emp_title,emp_length,home_ownership,annual_inc,verification_status,issue_d,loan_status,pymnt_plan,purpose,title,zip_code,addr_state,dti,delinq_2yrs,earliest_cr_line,inq_last_6mths,open_acc,pub_rec,revol_bal,revol_util,total_acc,initial_list_status,out_prncp,out_prncp_inv,total_pymnt,total_pymnt_inv,total_rec_prncp,total_rec_int,total_rec_late_fee,recoveries,collection_recovery_fee,last_pymnt_d,last_pymnt_amnt,next_pymnt_d,last_credit_pull_d,collections_12_mths_ex_med,policy_code,application_type,acc_now_delinq,tot_coll_amt,tot_cur_bal,open_acc_6m,open_act_il,open_il_12m,open_il_24m,mths_since_rcnt_il,total_bal_il,il_util,open_rv_12m,open_rv_24m,max_bal_bc,all_util,total_rev_hi_lim,inq_fi,total_cu_tl,inq_last_12m,acc_open_past_24mths,avg_cur_bal,bc_open_to_buy,bc_util,chargeoff_within_12_mths,delinq_amnt,mo_sin_old_il_acct,mo_sin_old_rev_tl_op,mo_sin_rcnt_rev_tl_op,mo_sin_rcnt_tl,mort_acc,mths_since_recent_bc,mths_since_recent_inq,num_accts_ever_120_pd,num_actv_bc_tl,num_actv_rev_tl,num_bc_sats,num_bc_tl,num_il_tl,num_op_rev_tl,num_rev_accts,num_rev_tl_bal_gt_0,num_sats,num_tl_120dpd_2m,num_tl_30dpd,num_tl_90g_dpd_24m,num_tl_op_past_12m,pct_tl_nvr_dlq,percent_bc_gt_75,pub_rec_bankruptcies,tax_liens,tot_hi_cred_lim,total_bal_ex_mort,total_bc_limit,total_il_high_credit_limit,hardship_flag,disbursement_method,debt_settlement_flag
Out[60]:
1428

写入文件内容,将每一行的内容全部粘贴为一个字符串,再使用write()函数进行写入。

In [61]:
for i in range(0,len(dataset_copy['loan_amnt'])):
    str_write = ""
    for column in columns:
        str_write = str_write+","+str(dataset_copy[column][i])
    str_write = str_write[1:]
    ## 输出查看
    if i<5:
        print(str_write)
    fw.write(str_write+"\n")
2500,2500,2500.0, 36 months, 13.56%,84.92,C,C1,Chef,10+ years,RENT,55000.0,Not Verified,Dec-2018,Current,n,debt_consolidation,Debt consolidation,109xx,NY,18.24,0,Apr-2001,1,9,1,4341,10.3%,34,w,2386.02,2386.02,167.02,167.02,113.98,53.04,0.0,0.0,0.0,Feb-2019,84.92,Mar-2019,Feb-2019,0,1,Individual,0,0,16901,2,2,1,2,2.0,12560,69.0,2,7,2137,28.0,42000,1,11,2,9,1878.0,34360.0,5.9,0,0,140.0,212,1,1,0,1.0,2.0,0,2,5,3,3,16,7,18,5,9,0.0,0,0,3,100.0,0.0,1,0,60124,16901,36500,18124,N,Cash,N
30000,30000,30000.0, 60 months, 18.94%,777.23,D,D2,Postmaster ,10+ years,MORTGAGE,90000.0,Source Verified,Dec-2018,Current,n,debt_consolidation,Debt consolidation,713xx,LA,26.52,0,Jun-1987,0,13,1,12315,24.2%,44,w,29387.75,29387.75,1507.11,1507.11,612.25,894.86,0.0,0.0,0.0,Feb-2019,777.23,Mar-2019,Feb-2019,0,1,Individual,0,1208,321915,4,4,2,3,3.0,87153,88.0,4,5,998,57.0,50800,2,15,2,10,24763.0,13761.0,8.3,0,0,163.0,378,4,3,3,4.0,4.0,0,2,4,4,9,27,8,14,4,13,0.0,0,0,6,95.0,0.0,1,0,372872,99468,15000,94072,N,Cash,N
4000,4000,4000.0, 36 months, 18.94%,146.51,D,D2,IT Supervisor,10+ years,MORTGAGE,92000.0,Source Verified,Dec-2018,Current,n,debt_consolidation,Debt consolidation,985xx,WA,16.74,0,Feb-2006,0,10,0,5468,78.1%,13,w,3831.93,3831.93,286.71,286.71,168.07,118.64,0.0,0.0,0.0,Feb-2019,146.51,Mar-2019,Feb-2019,0,1,Individual,0,686,305049,1,5,3,5,5.0,30683,68.0,0,0,3761,70.0,7000,2,4,3,5,30505.0,1239.0,75.2,0,0,62.0,154,64,5,3,64.0,5.0,0,1,2,1,2,7,2,3,2,10,0.0,0,0,3,100.0,100.0,0,0,385183,36151,5000,44984,N,Cash,N
30000,30000,30000.0, 60 months, 16.14%,731.78,C,C4,Mechanic,10+ years,MORTGAGE,57250.0,Not Verified,Dec-2018,Current,n,debt_consolidation,Debt consolidation,212xx,MD,26.35,0,Dec-2000,0,12,0,829,3.6%,26,w,29339.02,29339.02,1423.21,1423.21,660.98,762.23,0.0,0.0,0.0,Feb-2019,731.78,Mar-2019,Feb-2019,0,1,Individual,0,0,116007,3,5,3,5,4.0,28845,89.0,2,4,516,54.0,23100,1,0,0,9,9667.0,8471.0,8.9,0,0,53.0,216,2,2,2,2.0,13.0,0,2,2,3,8,9,6,15,2,12,0.0,0,0,5,92.3,0.0,0,0,157548,29674,9300,32332,N,Cash,N
5550,5550,5550.0, 36 months, 15.02%,192.45,C,C3,Director COE,10+ years,MORTGAGE,152500.0,Not Verified,Dec-2018,Current,n,credit_card,Credit card refinancing,461xx,IN,37.94,0,Sep-2002,3,18,0,53854,48.1%,44,w,5302.5,5302.5,377.95,377.95,247.5,130.45,0.0,0.0,0.0,Feb-2019,192.45,Mar-2019,Feb-2019,0,1,Individual,0,0,685749,1,7,2,3,4.0,131524,72.0,1,4,17584,58.0,111900,2,4,6,8,40338.0,23746.0,64.0,0,0,195.0,176,10,4,6,20.0,3.0,0,4,6,6,10,23,9,15,7,18,0.0,0,0,4,100.0,60.0,0,0,831687,185378,65900,203159,N,Cash,N

无论是读取文件还是写入文件,一旦创建了文件对象则需要对其进行关闭,释放内存。在Python中直接使用close()函数即可。

In [62]:
fw.close()

在Python中提供with方法来简易创建并关闭文件对象的操作。该方法可以不对文件进行一次手动的读取与关闭,该方法会自动关闭文件对象,使得代码更加简单直观。

In [63]:
with open("./input/output.csv","w") as fw:
    columns_write = ""
    ## 写入列名
    for column in columns:
        columns_write = columns_write+","+column
    columns_write = columns_write[1:]
    print(columns_write)
    fw.write(columns_write+"\n")
    ## 写入内容
    for i in range(0,len(dataset_copy['loan_amnt'])):
        str_write = ""
        for column in columns:
            str_write = str_write+","+str(dataset_copy[column][i])
        str_write = str_write[1:]
        ## 输出查看
        if i<3:
            print(str_write)
        fw.write(str_write+"\n")
loan_amnt,funded_amnt,funded_amnt_inv,term,int_rate,installment,grade,sub_grade,emp_title,emp_length,home_ownership,annual_inc,verification_status,issue_d,loan_status,pymnt_plan,purpose,title,zip_code,addr_state,dti,delinq_2yrs,earliest_cr_line,inq_last_6mths,open_acc,pub_rec,revol_bal,revol_util,total_acc,initial_list_status,out_prncp,out_prncp_inv,total_pymnt,total_pymnt_inv,total_rec_prncp,total_rec_int,total_rec_late_fee,recoveries,collection_recovery_fee,last_pymnt_d,last_pymnt_amnt,next_pymnt_d,last_credit_pull_d,collections_12_mths_ex_med,policy_code,application_type,acc_now_delinq,tot_coll_amt,tot_cur_bal,open_acc_6m,open_act_il,open_il_12m,open_il_24m,mths_since_rcnt_il,total_bal_il,il_util,open_rv_12m,open_rv_24m,max_bal_bc,all_util,total_rev_hi_lim,inq_fi,total_cu_tl,inq_last_12m,acc_open_past_24mths,avg_cur_bal,bc_open_to_buy,bc_util,chargeoff_within_12_mths,delinq_amnt,mo_sin_old_il_acct,mo_sin_old_rev_tl_op,mo_sin_rcnt_rev_tl_op,mo_sin_rcnt_tl,mort_acc,mths_since_recent_bc,mths_since_recent_inq,num_accts_ever_120_pd,num_actv_bc_tl,num_actv_rev_tl,num_bc_sats,num_bc_tl,num_il_tl,num_op_rev_tl,num_rev_accts,num_rev_tl_bal_gt_0,num_sats,num_tl_120dpd_2m,num_tl_30dpd,num_tl_90g_dpd_24m,num_tl_op_past_12m,pct_tl_nvr_dlq,percent_bc_gt_75,pub_rec_bankruptcies,tax_liens,tot_hi_cred_lim,total_bal_ex_mort,total_bc_limit,total_il_high_credit_limit,hardship_flag,disbursement_method,debt_settlement_flag
2500,2500,2500.0, 36 months, 13.56%,84.92,C,C1,Chef,10+ years,RENT,55000.0,Not Verified,Dec-2018,Current,n,debt_consolidation,Debt consolidation,109xx,NY,18.24,0,Apr-2001,1,9,1,4341,10.3%,34,w,2386.02,2386.02,167.02,167.02,113.98,53.04,0.0,0.0,0.0,Feb-2019,84.92,Mar-2019,Feb-2019,0,1,Individual,0,0,16901,2,2,1,2,2.0,12560,69.0,2,7,2137,28.0,42000,1,11,2,9,1878.0,34360.0,5.9,0,0,140.0,212,1,1,0,1.0,2.0,0,2,5,3,3,16,7,18,5,9,0.0,0,0,3,100.0,0.0,1,0,60124,16901,36500,18124,N,Cash,N
30000,30000,30000.0, 60 months, 18.94%,777.23,D,D2,Postmaster ,10+ years,MORTGAGE,90000.0,Source Verified,Dec-2018,Current,n,debt_consolidation,Debt consolidation,713xx,LA,26.52,0,Jun-1987,0,13,1,12315,24.2%,44,w,29387.75,29387.75,1507.11,1507.11,612.25,894.86,0.0,0.0,0.0,Feb-2019,777.23,Mar-2019,Feb-2019,0,1,Individual,0,1208,321915,4,4,2,3,3.0,87153,88.0,4,5,998,57.0,50800,2,15,2,10,24763.0,13761.0,8.3,0,0,163.0,378,4,3,3,4.0,4.0,0,2,4,4,9,27,8,14,4,13,0.0,0,0,6,95.0,0.0,1,0,372872,99468,15000,94072,N,Cash,N
4000,4000,4000.0, 36 months, 18.94%,146.51,D,D2,IT Supervisor,10+ years,MORTGAGE,92000.0,Source Verified,Dec-2018,Current,n,debt_consolidation,Debt consolidation,985xx,WA,16.74,0,Feb-2006,0,10,0,5468,78.1%,13,w,3831.93,3831.93,286.71,286.71,168.07,118.64,0.0,0.0,0.0,Feb-2019,146.51,Mar-2019,Feb-2019,0,1,Individual,0,686,305049,1,5,3,5,5.0,30683,68.0,0,0,3761,70.0,7000,2,4,3,5,30505.0,1239.0,75.2,0,0,62.0,154,64,5,3,64.0,5.0,0,1,2,1,2,7,2,3,2,10,0.0,0,0,3,100.0,100.0,0,0,385183,36151,5000,44984,N,Cash,N

3.2 Pandas中函数进行文件的存储

在Pandas中,可以直接对格式为DataFrame的数据进行文件的存储。使用Pandas中的to_csv()函数可以进行csv文件的输出,因为不需要写入索引信息,所以此处对index参数设置为False

In [64]:
dataset_copy.to_csv("./input/output.csv",index = False)

Pandas同样支持很多其他格式文件的输出,例如输出txt文件可以将to_csv()函数的sep参数设置为"\s"分隔符。

若想输出excel文件则可以使用to_excel()函数,json文件可使用to_json()函数等。