python数据分析实战之客户还款能力预测

文章目录

        • 1、明确需求和目的
        • 2、数据收集
        • 3、数据预处理
          • 3.1 数据整合
            • 3.1.1 加载相关库和数据集
            • 3.1.2 数据总体概览
          • 3.2 数据清洗
            • 3.2.1 多余列的删除
            • 3.2.2 确定数据集的标签
            • 3.2.3 缺失值的处理
            • 3.2.4 数据类型转换
            • 3.2.5 重复值处理
        • 4、模型训练
          • 4.1 使用逻辑回归训练
          • 4.2 使用KNN训练
        • 5、总结

1、明确需求和目的

  • 客户向P2P平台申请贷款时,平台会通过线上或线下的的方式让客户填写贷款申请表,收集客户信息,同时也会借助第三方平台如征信机构等信息,通过以往的历史数据可以训练出一个预测模型。
  • 当有新客户时,就可以根据新客户的信息来预测判断出客户是否有还款能力,从而决定是否向客户发放贷款。

2、数据收集

  • 数据集是某P2P平台发生借贷的业务数据,原始数据共有111个变量,39535条记录。

3、数据预处理

3.1 数据整合
3.1.1 加载相关库和数据集
  • 使用的库主要有:pandas、numpy、sklearn
  • 使用的数据集:某P2P平台发生借贷的业务数据(LoanStats3a.csv)
import numpy as np
import pandas as pd
import warnings

warnings.filterwarnings("ignore")          # 忽略警告信息  
# 加载数据集,第一行是字符串,所以要skiprows=1跳过第一行
loans_2020 = pd.read_csv('LoanStats3a.csv', skiprows=1) 
3.1.2 数据总体概览
print(loans_2020.info())             # 展示概览信息 
print(loans_2020.describe())         # 展示数值类型字段的统计信息 
print(loans_2020.sample(5))         # 随机抽样10条数据
print(loans_2020.shape)             # 展示形状信息
-----------------------------------   
(39535, 111)
3.2 数据清洗
3.2.1 多余列的删除

首先删除空白值超过一半的列

# 剔除空白值超过一半的列,thresh=n:至少有n个NaN的列才会去除
loans_2020 = loans_2020.dropna(thresh=len(loans_2020) / 2 , axis=1) 
# 输出到“loans_2020.csv”文件 , index=False表示不加索引
loans_2020.to_csv('loans_2020.csv', index=False)                 

重新加载数据集,查看数据信息:

loans_2020 = pd.read_csv("loans_2020.csv")
print(loans_2020.shape)
-------------------------------            # 去除多余的列之后,剩余54列
(39535, 54)

print("第一行的数据展示 \n",loans_2020.iloc[0])  #第一行的数据

进一步分析数据,继续删除无用的列(描述、URL、id等对模型建立没有什么作用的字段):

"""
desc:贷款描述
url:URL链接
id:用户ID
member_id:会员编号
funded_amnt:承诺给该贷款的总金额
funded_amnt_inv:投资者为该贷款承诺的总金额
grade:贷款等级。贷款利率越高,则等级越高
sub_grade:贷款子等级
emp_title:工作名称
issue_d:贷款月份
...
"""
loans_2020 = loans_2020.drop(['desc', 'url',"id", "member_id", "funded_amnt", "funded_amnt_inv", "grade", "sub_grade", "emp_title", "issue_d","zip_code", "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"], axis=1)
print(loans_2020.shape)
------------------------------    # 进一步去除多余的列之后,剩余32列
(39535, 32)

进一步分析,去掉特征中只有一种属性的列(在原始数据中的特征值只有一种属性的话,对于分类模型的预测是没有用的):

orig_columns = loans_2020.columns  	# 展现出所有的列
drop_columns = [] 			 # 初始化空列表

for col in orig_columns:
    # dropna()先删除空值,再找出唯一的属性,否则加上空值就是两个属性了
    col_series = loans_2020[col].dropna().unique()    # 去重,找出唯一属性的特征
    if len(col_series) == 1:  #如果该特征的属性只有一个属性,就给过滤掉该特征
        drop_columns.append(col)
        
loans_2020 = loans_2020.drop(drop_columns, axis=1)  # 去除特征中只有一种属性的列
print(drop_columns)
print(loans_2020.shape)
-----------------------
['initial_list_status', 'collections_12_mths_ex_med', 'policy_code', 'application_type', 'acc_now_delinq', 'chargeoff_within_12_mths', 'delinq_amnt', 'tax_liens']
(39535, 24)				# 去掉特征中只有一种属性的列之后,剩余24列
3.2.2 确定数据集的标签

在原始数据中,没有0或者1的还不还款的特征,但是有“loan_status”这个特征,意思是当前“贷款的状态” ,可将贷款状态当作是标签。

首先查看一下贷款状态的信息:

print(loans_2020['loan_status'].value_counts())		# 计算该列特征的属性的个数
"""
Fully Paid:全部还清的贷款,后面给他打个“1”
Charged Off:违约的贷款,后面给他打个“0”
Late (16-30 days)  :延期了16-30 days
Late (31-120 days):延期了31-120 days , 所以这些都不确定的属性,相当于“取保候审”
"""
--------------------------------------------
Fully Paid            33693
Charged Off            5612
Current                 201
Late (31-120 days)       10
In Grace Period           9
Late (16-30 days)         5
Default                   1
Name: loan_status, dtype: int64

主要考虑的是全部还清和违约的贷款信息,将其映射成二分类:

loans_2020 = loans_2020[(loans_2020['loan_status'] == "Fully Paid") |
                        (loans_2020['loan_status'] == "Charged Off")]
# 将全部还清和违约的贷款信息映射成一个二分类,用0,1 表示
status_replace = {
    "loan_status": {
        "Fully Paid": 1, 	# 完全还清
        "Charged Off": 0,	# 违约
    }
}

loans_2020 = loans_2020.replace(status_replace)   # replace:执行的是查找并替换的操作

查看一下转换的效果,并输出到“filtered_loans_2020.csv”文件

print(loans_2020['loan_status'].value_counts())#计算该列特征的属性的个数
loans_2020.to_csv('filtered_loans_2020.csv', index=False) 
3.2.3 缺失值的处理
  • 当筛选出特征和标签之后, 还要做缺失值、字符值、标点符号、%号、str值等处理,才能将数据交给sklearn进行训练。
  • 删除缺失值(仅仅适合缺失数量较少的情况)
  • 填充缺失值(数值变量使用均值或中位数进行填充,类别变量使用众数填充或单独作为一个类别)

重新加载数据集,首先查看缺失值情况:

loans = pd.read_csv('filtered_loans_2020.csv')
null_counts = loans.isnull().sum()  # 用pandas的isnull统计一下每列的缺失值,给累加起来
print(null_counts) 
------------------------------------------
loan_amnt                  0
term                       0
int_rate                   0
installment                0
emp_length              1073
home_ownership             0
annual_inc                 0
verification_status        0
loan_status                0
pymnt_plan                 0
purpose                    0
title                     11
addr_state                 0
dti                        0
delinq_2yrs                0
earliest_cr_line           0
inq_last_6mths             0
open_acc                   0
pub_rec                    0
revol_bal                  0
revol_util                50
total_acc                  0
last_credit_pull_d         1
pub_rec_bankruptcies     449
dtype: int64

从统计出的结果可以看出‘title’、‘revol_util’和’last_credit_pull_d’ 相对于数据总量来说较少,可以直接去掉缺失值所在的行。而‘pub_rec_bankruptcies ’和’emp_length’缺失值较多,需要根据它们的数据类型进行相应的缺失值填充,所以先查看它们的数据类型:

print(loans['emp_length'].dtypes)
print(loans['pub_rec_bankruptcies'].dtypes)
--------------------
object
float64

根据它们的数据类型进行相应的缺失值填充,并删除数据量较少的缺失值:

# 使用中位数填充数值变量
loans.fillna({"pub_rec_bankruptcies": loans["pub_rec_bankruptcies"].median()}, inplace=True)
# 使用众数填充类别变量
loans.fillna({"emp_length": loans["emp_length"].mode().iloc[0]}, inplace=True)
loans = loans.dropna(axis=0)      # 删除其余缺失值
loans.isnull().sum()              # 查看处理之后的效果
3.2.4 数据类型转换
  • 由于sk-learn库不接受字符型的数据,所以还需将特征中字符型的数据进行处理。

首先查看数据类型:

print(loans.dtypes.value_counts())
----------------------------------
object     12
float64    11
int64       1
dtype: int64

查看一下object类型的数据:

# Pandas里select_dtypes只选定“object”的类型,然后进行数据查看
object_columns_df = loans.select_dtypes(include=["object"])
print(object_columns_df.iloc[0])
---------------------------------------
term                     36 months
int_rate                    10.65%
emp_length               10+ years
home_ownership                RENT
verification_status       Verified
pymnt_plan                       n
purpose                credit_card
title                     Computer
addr_state                      AZ
earliest_cr_line            Jan-85
revol_util                  83.70%
last_credit_pull_d          Nov-16
Name: 0, dtype: object
"""
term:分期多少个月
int_rate:利息,10.65%,后面还要把%去掉
emp_length:10年的映射成10,9年的映射成9
home_ownership:房屋所有权,是租的、还是自己的、还是抵押出去了,那就用0 1 2来代替
...
"""

仔细分析一下数据,发现还有一些数据列可以进行删除:

"""
last_credit_pull_d:LC撤回最近的月份   
earliest_cr_line:第一次借贷时间
addr_state:家庭邮编
title:标题,内容和"purpose表达的意思相近,保留一个即可
pymnt_plan:是否已为贷款实施付款计划,里面都为N,可直接删除
"""
loans = loans.drop(
    ["last_credit_pull_d", "earliest_cr_line", "addr_state", "title", "pymnt_plan"], axis=1)

使用 Label Encoding 进行数据的转换:

# 包含%的,直接删除%,然后再转换类型即可
loans["int_rate"] = loans["int_rate"].str.rstrip("%").astype("float")
loans["revol_util"] = loans["revol_util"].str.rstrip("%").astype("float")

# emp_length做成字典,emp_length当做key ,value里还是字典 ,"10+ years": 10...
mapping_dict = {
    "emp_length": {
        "10+ years": 10,
        "9 years": 9,
        "8 years": 8,
        "7 years": 7,
        "6 years": 6,
        "5 years": 5,
        "4 years": 4,
        "3 years": 3,
        "2 years": 2,
        "1 year": 1,
        "< 1 year": 0,
        "n/a": 0
    }
}
# 调用replace函数,进行类型转换
loans = loans.replace(mapping_dict)

使用 One-Hot Encoding 进行剩余数据的转换,此处选择使用pandas的get_dummies()函数,直接映射为数值型:

"""
home_ownership:房屋所有权
verification_status:身份保持证明
emp_length:客户公司名称
purpose:贷款的意图
term:贷款分期的时间
"""
cat_columns = ["home_ownership", "verification_status", "emp_length", "purpose", "term"]
dummy_df = pd.get_dummies(loans[cat_columns])    # pd.get_dummies() 实现独热编码
loans = pd.concat([loans, dummy_df], axis=1)	# concat() 连接处理之后的列,
loans = loans.drop(cat_columns, axis=1)			# 将原有的列删除

查看一下现在数据的形状,并将其输出到“cleaned_loans_2020.csv”文件 :

print(loans.shape)
--------------------     # 列数变多是因为进行独热编码之后,一列会变成多列
(39243, 49)

print(loans.dtypes.value_counts())    # 查看现在数据的类型
-------------------
uint8      35
float64    13
int64       1
dtype: int64

loans.to_csv('cleaned_loans_2020.csv', index=False)
3.2.5 重复值处理

查看是否有重复值,有的话直接删除:

print(loans.duplicated().sum())    # 查看重复值的数量
--------------------------------  # 没有重复值,不需要处理
0

4、模型训练

  • 前面花费了大量的时间在进行数据处理,这足以说明在数据分析中数据准备的工作有多重要,有了好的数据才能预测出好的分类结果。
  • 此处预测客户是否有还款能力,属于二分类问题,而对于二分类问题,一般情况下,首选逻辑回归。
  • 首先定义模型效果的评判标准。根据贷款行业的实际情况,在这里我们假设将钱借给了没有还款能力的人,结果损失一千,将钱借给了有偿还能力的人,从每笔中赚0.1的利润,而其余情况收益为零,就相当于预测对十个人才顶上预测错一个人的收益,所以精度不再适用于此模型,为了实现利润最大化,不仅要求模型预测recall率较高,同时是需要让fall-out率较低,故这里采用两个指标TPR和FPR(直接使用auc也可以)。
4.1 使用逻辑回归训练

使用逻辑回归算法进行模型训练:

from sklearn.linear_model import LogisticRegression 
from sklearn.model_selection import train_test_split

loans = pd.read_csv("cleaned_loans_2020.csv")		# 加载处理好之后的数据集
train_cols = loans.columns.drop("loan_status")    # 删除loan_status这一列

X = loans[train_cols]        # 特征值
y = loans["loan_status"]     # loan_status作为目标值

# 切分训练集和测试集
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size=0.20, random_state=0)

lr = LogisticRegression() 
lr.fit(X_train, y_train) 			# 开始训练
y_hat = lr.predict(X_test) 			# 开始预测
y_hat[:10]      # 0:代表没有偿还  1:代表偿还
-----------------------------------
array([1, 1, 1, 1, 1, 1, 1, 1, 1, 1], dtype=int64)

lr.predict_proba(X_test)  # 获取预测的概率值,包含数据属于每个类别的概率。
------------------------------------------------------
array([[0.20101861, 0.79898139],
       [0.22535089, 0.77464911],
       [0.15100305, 0.84899695],
       ...,
       [0.21337007, 0.78662993],
       [0.16564957, 0.83435043],
       [0.11056326, 0.88943674]])

建立混淆矩阵,查看模型预测情况:

from sklearn.metrics import confusion_matrix

# 传入真实值与预测值,创建混淆矩阵。
matrix = confusion_matrix(y_true=y_test, y_pred=y_hat) 
print(matrix)
--------------------------------
[[ 3 1067]
 [ 3 6776]]

从以上结果可以看出,假正例和真正例比例约为1:6.7,说明预测错误的正例太多了,模型效果不好,需要进一步进行处理。

分析原因可能为样本不均衡造成的,考虑权重后使用逻辑回归进行训练:

"""
class_weight:可以调整正反样本的权重
balanced:希望正负样本平衡一些
"""
lr = LogisticRegression(class_weight="balanced")
lr.fit(X_train, y_train) 			# 开始训练
y_hat = lr.predict(X_test) 			# 开始预测
matrix = confusion_matrix(y_true=y_test, y_pred=y_hat)  
print(matrix)
-------------------------------
[[ 668  402]
 [2963 3816]]

考虑权重之后,假正例和真正例比例情况有所改善,但是假负例太多了,说明很多能够还款的客户都预测为不能还款,这样就损失了一些客户,结果也是不好的,可以看一下AOC值:

from sklearn.metrics import roc_curve
from sklearn.metrics import auc

probo = lr.predict_proba(X_test)    # 使用概率来作为每个样本数据的分值
fpr, tpr, thresholds = roc_curve(y_true=y_test, y_score=probo[:, 1], pos_label=1)
print("AUC面积值:", auc(fpr, tpr))
-----------------------------------
AUC面积值: 0.6307932827188969

可以看出AUC值也不是很大,效果一般,可以考虑使用其它算法进行模型的训练。

4.2 使用KNN训练

使用KNN算法进行模型训练:

from sklearn.neighbors import KNeighborsClassifier
from sklearn.model_selection import GridSearchCV

knn = KNeighborsClassifier()

# 使用网格交叉验证方法选取合适的超参数
grid = {"n_neighbors": range(1, 20, 1), "weights": ['uniform', 'distance']}
gs = GridSearchCV(estimator=knn, param_grid=grid, scoring="accuracy", n_jobs=-1, cv=5, verbose=10, iid=True)
gs.fit(X_train, y_train)

print(" 最好的分值:", gs.best_score_)
print(" 最好的超参数组合:", gs.best_params_)
print(" 最好的超参数训练好的模型:", gs.best_estimator_)
------------------------------------
最好的分值: 0.8555456456647768
最好的超参数组合: {'n_neighbors': 19, 'weights': 'uniform'}
最好的超参数训练好的模型: KNeighborsClassifier(algorithm='auto', leaf_size=30, metric='minkowski',
                     metric_params=None, n_jobs=None, n_neighbors=19, p=2,
                     weights='uniform')

建立混淆矩阵,查看模型预测情况:

estimator = gs.best_estimator_        # 最好的超参数训练好的模型
y_hat = estimator.predict(X_test)
matrix = confusion_matrix(y_true=y_test, y_pred=y_hat)  
print(matrix)
--------------------------------------
[[   1 1069]
 [   7 6772]]

查看AUC值:

probo = lr.predict_proba(X_test)    # 使用概率来作为每个样本数据的分值
fpr, tpr, thresholds = roc_curve(y_true=y_test, y_score=probo[:, 1], pos_label=1)
print("AUC面积值:", auc(fpr, tpr))
---------------------------------
AUC面积值: 0.6269799669953802

从结果可以看出,KNN算法得出来的情况和逻辑回归差不多。

5、总结

  • 通过以上例子,对某P2P平台发生借贷的业务数据进行了处理,并使用两种不同的算法进行模型的训练,虽然模型的效果不太好,但还是可以看出数据分析建模的一般流程为:数据处理和模型学习。
  • 数据处理,需要对原始数据进行清洗以及特征提取。
  • 模型学习,涉及长时间的模型参数调整和算法选择。
  • 模型效果不理想时,可以考虑的调整策略有:

(1)调整正负样本的权重参数

(2)调整模型参数

(3)使用原数据,生成新特征。

(4)更换模型算法

(5)同时几个使用,进行模型预测,然后取最好的结果。

你可能感兴趣的:(python数据分析)