当前位置: 首页 > article >正文

基于机器学习与深度学习的贷款批准预测

1.项目背景

该数据集源自Kaggle的“Playground Series - Season 4, Episode 10”竞赛,是通过在贷款批准预测数据集上训练的深度学习模型生成的数据,旨在使用借款人信息预测贷款批准结果,它通过模拟真实贷款审批场景,帮助金融机构评估借款人风险。

2.数据说明

字段名说明
id样本ID
person_age借款人年龄(岁)
person_income借款人年收入(美元)
person_home_ownership借款人房屋拥有情况,取值为:RENT(租房)、OWN(拥有)、MORTGAGE(按揭)、OTHER(其他)
person_emp_length借款人工作年限(年)
loan_intent贷款意图,取值包括:EDUCATION(教育)、MEDICAL(医疗)、PERSONAL(个人消费)、VENTURE(创业)、DEBTCONSOLIDATION(债务整合)、HOMEIMPROVEMENT(家庭改善)
loan_grade贷款信用等级,从A到G表示不同的信用等级,一般来说A是最好的,依次递减
loan_amnt贷款金额(美元)
loan_int_rate贷款利率(百分比)
loan_percent_income贷款金额占收入的比例
cb_person_default_on_file是否有违约记录(Y:有,N:无)
cb_person_cred_hist_length信用历史长度(年)
loan_status是否获得贷款批准(0:未获得,1:获得)

3.Python库导入及数据读取

import pandas as pd
import numpy as np
import seaborn as sns
import matplotlib.pyplot as plt
from scipy.stats import chi2_contingency,ks_2samp,spearmanr,f_oneway
from scipy import stats
from sklearn.preprocessing import StandardScaler
from sklearn.model_selection import train_test_split
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import classification_report,confusion_matrix,roc_curve, auc
import tensorflow as tf
from tensorflow.keras.models import Sequential
from tensorflow.keras.layers import Dense, Dropout, Input
from tensorflow.keras.optimizers import Adam
import shap
train_data = pd.read_csv('/home/mw/input/data9293/train.csv')
test_data = pd.read_csv('/home/mw/input/data9293/test.csv')

4.数据预览

print('训练集信息:')
print(train_data.info())
训练集信息:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 58645 entries, 0 to 58644
Data columns (total 13 columns):
 #   Column                      Non-Null Count  Dtype  
---  ------                      --------------  -----  
 0   id                          58645 non-null  int64  
 1   person_age                  58645 non-null  int64  
 2   person_income               58645 non-null  int64  
 3   person_home_ownership       58645 non-null  object 
 4   person_emp_length           58645 non-null  float64
 5   loan_intent                 58645 non-null  object 
 6   loan_grade                  58645 non-null  object 
 7   loan_amnt                   58645 non-null  int64  
 8   loan_int_rate               58645 non-null  float64
 9   loan_percent_income         58645 non-null  float64
 10  cb_person_default_on_file   58645 non-null  object 
 11  cb_person_cred_hist_length  58645 non-null  int64  
 12  loan_status                 58645 non-null  int64  
dtypes: float64(3), int64(6), object(4)
memory usage: 5.8+ MB
None
print('测试集信息:')
print(test_data.info())
测试集信息:
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 39098 entries, 0 to 39097
Data columns (total 12 columns):
 #   Column                      Non-Null Count  Dtype  
---  ------                      --------------  -----  
 0   id                          39098 non-null  int64  
 1   person_age                  39098 non-null  int64  
 2   person_income               39098 non-null  int64  
 3   person_home_ownership       39098 non-null  object 
 4   person_emp_length           39098 non-null  float64
 5   loan_intent                 39098 non-null  object 
 6   loan_grade                  39098 non-null  object 
 7   loan_amnt                   39098 non-null  int64  
 8   loan_int_rate               39098 non-null  float64
 9   loan_percent_income         39098 non-null  float64
 10  cb_person_default_on_file   39098 non-null  object 
 11  cb_person_cred_hist_length  39098 non-null  int64  
dtypes: float64(3), int64(5), object(4)
memory usage: 3.6+ MB
None
# 查看重复值
print(f'训练集中存在的重复值:{train_data.duplicated().sum()}')
print(f'测试集中存在的重复值:{test_data.duplicated().sum()}')
训练集中存在的重复值:0
测试集中存在的重复值:0
# 查看分类特征的唯一值
characteristic = train_data.select_dtypes(include=['object']).columns
print('训练集中分类变量的唯一值情况:')
for i in characteristic:
    print(f'{i}:')
    print(f'共有:{len(train_data[i].unique())}条唯一值')
    print(train_data[i].unique())
    print('-'*50)
训练集中分类变量的唯一值情况:
person_home_ownership:
共有:4条唯一值
['RENT' 'OWN' 'MORTGAGE' 'OTHER']
--------------------------------------------------
loan_intent:
共有:6条唯一值
['EDUCATION' 'MEDICAL' 'PERSONAL' 'VENTURE' 'DEBTCONSOLIDATION'
 'HOMEIMPROVEMENT']
--------------------------------------------------
loan_grade:
共有:7条唯一值
['B' 'C' 'A' 'D' 'E' 'F' 'G']
--------------------------------------------------
cb_person_default_on_file:
共有:2条唯一值
['N' 'Y']
--------------------------------------------------
#绘制箱线图来观察是否存在异常值
# 定义特征及其中文名称映射
feature_map = {
    'person_age': '借款人年龄',
    'person_income': '借款人年收入',
    'person_emp_length': '借款人工作年限',
    'loan_amnt': '贷款金额',
    'loan_int_rate': '贷款利率(百分比)',
    'loan_percent_income': '贷款金额占收入的比例',
    'cb_person_cred_hist_length': '信用历史长度(年)'
}
plt.figure(figsize=(20, 15))

for i, (col, col_name) in enumerate(feature_map.items(), 1):
    plt.subplot(2, 4, i)
    sns.boxplot(y=train_data[col])
    plt.title(f'训练集中{col_name}的箱线图', fontsize=14)
    plt.ylabel('数值', fontsize=12)
    plt.grid(axis='y', linestyle='--', alpha=0.7)

plt.tight_layout()
plt.show()
plt.figure(figsize=(20, 15))

for i, (col, col_name) in enumerate(feature_map.items(), 1):
    plt.subplot(2, 4, i)
    sns.boxplot(y=test_data[col])
    plt.title(f'测试集中{col_name}的箱线图', fontsize=14)
    plt.ylabel('数值', fontsize=12)
    plt.grid(axis='y', linestyle='--', alpha=0.7)

plt.tight_layout()
plt.show()

通过训练集和测试集的对比,不难发现在训练集中存在一些异常值,比如训练集中出现了120岁以上的年龄,而测试集最大年龄约为95岁,因此120岁以上的年龄在训练集中可能是异常值,并且在工作年限那里,有人的竟然工作了120年!肯定是不正确的,其他的看起来没啥问题,虽然存在一些比较高的特殊值,但是也是在生活中可能出现的,接下来,还要检查数据中逻辑是否有问题,即工作年限和年龄的关系,还有计算贷款金额和收入占比的比例是否正确等等。

# 使用"person_age"和"person_emp_length"筛选出年龄超过120岁或工作年限超过120年的异常数据
abnormal_data = train_data[(train_data['person_age'] > 120) | (train_data['person_emp_length'] > 120)]
abnormal_data
idperson_ageperson_incomeperson_home_ownershipperson_emp_lengthloan_intentloan_gradeloan_amntloan_int_rateloan_percent_incomecb_person_default_on_filecb_person_cred_hist_lengthloan_status
41079410792860350MORTGAGE123.0MEDICALD2500015.950.35Y61
473364733612336000MORTGAGE7.0PERSONALB670010.750.18N40
492524925221192000MORTGAGE123.0VENTUREB2000011.490.10N20
  • ID为41079和49252的年龄分别为28和21岁,但工作年限为123年,存在明显异常。
  • ID为47336的年龄为123岁,但工作年限为7年,也属于异常数据。
# 删除年龄超过120或工作年限超过120的记录
train_data = train_data[(train_data['person_age'] <= 120) & (train_data['person_emp_length'] <= 120)]
train_age_emp = train_data.copy()
train_age_emp['age_minus_emp_length']  = train_age_emp['person_age'] - train_age_emp['person_emp_length']
train_age_emp['age_minus_emp_length'].describe()
count    58642.000000
mean        22.852392
std          6.755495
min          2.000000
25%         17.000000
50%         22.000000
75%         26.000000
max         82.000000
Name: age_minus_emp_length, dtype: float64

好家伙,年龄与工龄之差的最小值竟然是2,最大值是82,明显不符合实际情况,看看测试集是啥情况。

test_age_emp = test_data.copy()
test_age_emp['age_minus_emp_length']  = test_age_emp['person_age'] - test_age_emp['person_emp_length']
test_age_emp['age_minus_emp_length'].describe()
count    39098.000000
mean        22.879713
std          6.774023
min          2.000000
25%         17.000000
50%         22.000000
75%         26.000000
max         92.000000
Name: age_minus_emp_length, dtype: float64

好家伙,同样出现了年龄与工龄之差的最小值是2,而且最大值是92,同样不符合实际情况。

for i in range(8,19):
    abnormal_age_emp_difference_train_under_count = train_age_emp[train_age_emp['age_minus_emp_length'] < i].shape[0]
    abnormal_age_emp_difference_test_under_count = test_age_emp[test_age_emp['age_minus_emp_length'] < i].shape[0]
    print(f"训练集中年龄与工龄之差在{i}以下的数据有{abnormal_age_emp_difference_train_under_count}条,占比{round(abnormal_age_emp_difference_train_under_count/len(train_age_emp)*100,2)}%")
    print(f"测试集中年龄与工龄之差在{i}以下的数据有{abnormal_age_emp_difference_test_under_count}条,占比{round(abnormal_age_emp_difference_test_under_count/len(test_age_emp)*100,2)}%")
    print('-'*50)
训练集中年龄与工龄之差在8以下的数据有5条,占比0.01%
测试集中年龄与工龄之差在8以下的数据有10条,占比0.03%
--------------------------------------------------
训练集中年龄与工龄之差在9以下的数据有7条,占比0.01%
测试集中年龄与工龄之差在9以下的数据有11条,占比0.03%
--------------------------------------------------
训练集中年龄与工龄之差在10以下的数据有10条,占比0.02%
测试集中年龄与工龄之差在10以下的数据有12条,占比0.03%
--------------------------------------------------
训练集中年龄与工龄之差在11以下的数据有11条,占比0.02%
测试集中年龄与工龄之差在11以下的数据有12条,占比0.03%
--------------------------------------------------
训练集中年龄与工龄之差在12以下的数据有14条,占比0.02%
测试集中年龄与工龄之差在12以下的数据有14条,占比0.04%
--------------------------------------------------
训练集中年龄与工龄之差在13以下的数据有21条,占比0.04%
测试集中年龄与工龄之差在13以下的数据有15条,占比0.04%
--------------------------------------------------
训练集中年龄与工龄之差在14以下的数据有26条,占比0.04%
测试集中年龄与工龄之差在14以下的数据有20条,占比0.05%
--------------------------------------------------
训练集中年龄与工龄之差在15以下的数据有48条,占比0.08%
测试集中年龄与工龄之差在15以下的数据有37条,占比0.09%
--------------------------------------------------
训练集中年龄与工龄之差在16以下的数据有980条,占比1.67%
测试集中年龄与工龄之差在16以下的数据有619条,占比1.58%
--------------------------------------------------
训练集中年龄与工龄之差在17以下的数据有13692条,占比23.35%
测试集中年龄与工龄之差在17以下的数据有9117条,占比23.32%
--------------------------------------------------
训练集中年龄与工龄之差在18以下的数据有15332条,占比26.15%
测试集中年龄与工龄之差在18以下的数据有10184条,占比26.05%
--------------------------------------------------

初步观察,发现差值在16以下占比比较少,可以认定满16岁就可以开始工作,也就是“年龄-工龄”不能小于16。

for i in range(30, 60, 10):
    abnormal_age_emp_difference_train_above_count = train_age_emp[train_age_emp['age_minus_emp_length'] > i].shape[0]
    abnormal_age_emp_difference_test_above_count = test_age_emp[test_age_emp['age_minus_emp_length'] > i].shape[0]
    print(f"训练集中年龄与工龄之差在{i}以上的数据有{abnormal_age_emp_difference_train_above_count}条,占比{round(abnormal_age_emp_difference_train_above_count/len(train_age_emp)*100,2)}%")
    print(f"测试集中年龄与工龄之差在{i}以上的数据有{abnormal_age_emp_difference_test_above_count}条,占比{round(abnormal_age_emp_difference_test_above_count/len(test_age_emp)*100,2)}%")
    print('-'*50)
训练集中年龄与工龄之差在30以上的数据有6994条,占比11.93%
测试集中年龄与工龄之差在30以上的数据有4692条,占比12.0%
--------------------------------------------------
训练集中年龄与工龄之差在40以上的数据有1217条,占比2.08%
测试集中年龄与工龄之差在40以上的数据有820条,占比2.1%
--------------------------------------------------
训练集中年龄与工龄之差在50以上的数据有288条,占比0.49%
测试集中年龄与工龄之差在50以上的数据有196条,占比0.5%
--------------------------------------------------

初步观察,发现差值40岁以上占比比较少,可以认定“年龄-工龄”不能大于40,现在开始处理,针对差值小于16的,采用“调整后的年龄 = 现有年龄 + (16 - 差值)”,针对差值大于40的,采用“调整后的工龄 = 现有工龄 + (差值 - 40)”。

# 定义一个函数来调整年龄和工龄
def adjust_age_and_emp_length(df):
    # 计算年龄减工龄的差值
    df['age_minus_emp_length'] = df['person_age'] - df['person_emp_length']
    
    # 差值小于16:增加年龄
    df.loc[df['age_minus_emp_length'] < 16, 'person_age'] += 16 - df['age_minus_emp_length']
    
    # 差值大于40:增加工龄
    df.loc[df['age_minus_emp_length'] > 40, 'person_emp_length'] += df['age_minus_emp_length'] - 40
    
    # 重新计算“年龄减工龄”的差值,以验证调整效果
    df['age_minus_emp_length'] = df['person_age'] - df['person_emp_length']
    return df
train_age_emp = adjust_age_and_emp_length(train_age_emp)
print(f"处理后的训练集中年龄与工龄之差小于16的数据有{train_age_emp[train_age_emp['age_minus_emp_length'] < 16].shape[0]}条,大于40的数据有{train_age_emp[train_age_emp['age_minus_emp_length'] > 40].shape[0]}条")
train_data['person_age'] = train_age_emp['person_age']
train_data['person_emp_length'] = train_age_emp['person_emp_length']
处理后的训练集中年龄与工龄之差小于16的数据有0条,大于40的数据有0条
test_age_emp = adjust_age_and_emp_length(test_age_emp)
print(f"处理后的测试集中年龄与工龄之差小于16的数据有{test_age_emp[test_age_emp['age_minus_emp_length'] < 16].shape[0]}条,大于40的数据有{test_age_emp[test_age_emp['age_minus_emp_length'] > 40].shape[0]}条")
test_data['person_age'] = test_age_emp['person_age']
test_data['person_emp_length'] = test_age_emp['person_emp_length']
处理后的测试集中年龄与工龄之差小于16的数据有0条,大于40的数据有0条
train_age_emp['age_minus_hist_length'] = train_age_emp['person_age'] - train_age_emp['cb_person_cred_hist_length']
test_age_emp['age_minus_hist_length'] = test_age_emp['person_age'] - test_age_emp['cb_person_cred_hist_length']
train_age_emp['age_minus_hist_length'].describe()
count    58642.000000
mean        21.755022
std          3.158081
min         -1.000000
25%         20.000000
50%         21.000000
75%         23.000000
max         62.000000
Name: age_minus_hist_length, dtype: float64

又出现异常值了,经过处理后的年龄和信用历史长度的差值,竟然出现了负数,明显不符合常理,看一下测试集有没有异常。

test_age_emp['age_minus_hist_length'].describe()
count    39098.000000
mean        21.755716
std          3.154762
min          7.000000
25%         20.000000
50%         21.000000
75%         23.000000
max         67.000000
Name: age_minus_hist_length, dtype: float64

还好,测试集这里相对正常,那么针对训练集中差值小于7的视为异常值,查看训练集中差值小于7的数据。

print(f"查看训练集中差值小于7的数据有{train_age_emp[train_age_emp['age_minus_hist_length'] < 7].shape[0]}条")
查看训练集中差值小于7的数据有1条

还好,只有一条数据是小于7的,就是那一条负数了,这里直接删除这条数据。

train_age_emp[train_age_emp['age_minus_hist_length'] < 7]
idperson_ageperson_incomeperson_home_ownershipperson_emp_lengthloan_intentloan_gradeloan_amntloan_int_rateloan_percent_incomecb_person_default_on_filecb_person_cred_hist_lengthloan_statusage_minus_emp_lengthage_minus_hist_length
218272182728186480MORTGAGE2.0PERSONALA100005.420.05N29026.0-1

直接在原始数据中删除Id21827这条数据。

train_data = train_data[train_data['id'] != 21827]

至于贷款金额和收入占比的比例,这里也不验证了,肯定有错误的,毕竟从之前分析年龄>120的数据,算了一下就发现有问题,这里直接重新计算并且替换吧。

train_data['loan_percent_income'] = round(train_data['loan_amnt'] / train_data['person_income'],2)
test_data['loan_percent_income'] = round(test_data['loan_amnt'] / test_data['person_income'],2)

5.一致性检验

5.1可视化分析

# 添加一个列来区分训练集和测试集
train_data['dataset'] = 'train'
test_data['dataset'] = 'test'
# 合并数据
combined_df = pd.concat([train_data, test_data], ignore_index=True)
plt.figure(figsize=(20,15))
plt.subplot(3,4,1)
sns.kdeplot(data=combined_df, x='person_age', hue='dataset', common_norm=False)
plt.title(f'借款人年龄在训练集和测试集核密度分布图')
plt.xlabel('借款人年龄')
plt.ylabel('密度')
plt.legend(title='数据集', labels=['train', 'test'])

plt.subplot(3,4,2)
sns.kdeplot(data=combined_df, x='person_income', hue='dataset', common_norm=False)
plt.title(f'借款人年收入在训练集和测试集核密度分布图')
plt.xlabel('借款人年收入(美元)')
plt.ylabel('密度')
plt.legend(title='数据集', labels=['train', 'test'])

plt.subplot(3,4,3)
person_home_ownership_counts = combined_df.groupby(['dataset', 'person_home_ownership']).size().unstack()
person_home_ownership_proportions = person_home_ownership_counts.div(person_home_ownership_counts.sum(axis=1), axis=0)
person_home_ownership_proportions.plot(kind='bar', stacked=True, ax=plt.gca())
plt.title(f'借款人房屋拥有情况在训练集和测试集中占比分布')
plt.xlabel('数据集')
plt.xticks(rotation=0)
plt.legend(loc='upper right')
plt.ylabel('占比')

plt.subplot(3,4,4)
sns.kdeplot(data=combined_df, x='person_emp_length', hue='dataset', common_norm=False)
plt.title(f'借款人工作年限在训练集和测试集核密度分布图')
plt.xlabel('借款人工作年限')
plt.ylabel('密度')
plt.legend(title='数据集', labels=['train', 'test'])

plt.subplot(3,4,5)
loan_intent_counts = combined_df.groupby(['dataset', 'loan_intent']).size().unstack()
loan_intent_proportions = loan_intent_counts.div(loan_intent_counts.sum(axis=1), axis=0)
loan_intent_proportions.plot(kind='bar', stacked=True, ax=plt.gca())
plt.title(f'贷款意图在训练集和测试集中占比分布')
plt.xlabel('数据集')
plt.xticks(rotation=0)
plt.legend(loc='upper right')
plt.ylabel('占比')

plt.subplot(3,4,6)
loan_grade_counts = combined_df.groupby(['dataset', 'loan_grade']).size().unstack()
loan_grade_proportions = loan_grade_counts.div(loan_grade_counts.sum(axis=1), axis=0)
loan_grade_proportions.plot(kind='bar', stacked=True, ax=plt.gca())
plt.title(f'贷款信用等级在训练集和测试集中占比分布')
plt.xlabel('数据集')
plt.xticks(rotation=0)
plt.legend(loc='upper right')
plt.ylabel('占比')

plt.subplot(3,4,7)
sns.kdeplot(data=combined_df, x='loan_amnt', hue='dataset', common_norm=False)
plt.title(f'贷款金额在训练集和测试集核密度分布图')
plt.xlabel('贷款金额(美元)')
plt.ylabel('密度')
plt.legend(title='数据集', labels=['train', 'test'])

plt.subplot(3,4,8)
sns.kdeplot(data=combined_df, x='loan_int_rate', hue='dataset', common_norm=False)
plt.title(f'贷款利率在训练集和测试集核密度分布图')
plt.xlabel('贷款利率(百分比)')
plt.ylabel('密度')
plt.legend(title='数据集', labels=['train', 'test'])

plt.subplot(3,4,9)
sns.kdeplot(data=combined_df, x='loan_percent_income', hue='dataset', common_norm=False)
plt.title(f'贷款金额占收入的比例在训练集和测试集核密度分布图')
plt.xlabel('贷款金额占收入的比例')
plt.ylabel('密度')
plt.legend(title='数据集', labels=['train', 'test'])

plt.subplot(3,4,10)
cb_person_default_on_file_counts = combined_df.groupby(['dataset', 'cb_person_default_on_file']).size().unstack()
cb_person_default_on_file_proportions = cb_person_default_on_file_counts.div(cb_person_default_on_file_counts.sum(axis=1), axis=0)
cb_person_default_on_file_proportions.plot(kind='bar', stacked=True, ax=plt.gca())
plt.title(f'违约记录情况在训练集和测试集中占比分布')
plt.xlabel('数据集')
plt.xticks(rotation=0)
plt.legend(loc='upper right')
plt.ylabel('占比')

plt.subplot(3,4,11)
sns.kdeplot(data=combined_df, x='cb_person_cred_hist_length', hue='dataset', common_norm=False)
plt.title(f'信用历史长度在训练集和测试集核密度分布图')
plt.xlabel('信用历史长度(年)')
plt.ylabel('密度')
plt.legend(title='数据集', labels=['train', 'test'])


plt.tight_layout()
plt.show()
# 删除训练集和测试集中的 dataset 列
train_data.drop(columns=['dataset'], inplace=True)
test_data.drop(columns=['dataset'], inplace=True)

5.2KS检验

numerical_features = test_data.select_dtypes(include=['int64','float64']).columns[1:]
results = []
for feature in numerical_features:
    statistic, p_value = ks_2samp(train_data[feature], test_data[feature])
    results.append({'Feature': feature,'Statistic': statistic, 'p-value': p_value})
results_df = pd.DataFrame(results)
results_df
FeatureStatisticp-value
0person_age0.0045810.706661
1person_income0.0049980.599303
2person_emp_length0.0029110.988323
3loan_amnt0.0043350.768399
4loan_int_rate0.0043060.775348
5loan_percent_income0.0060150.362652
6cb_person_cred_hist_length0.0030130.983011

5.3卡方检验

results = []
categorical_features = train_data.select_dtypes(include=['object']).columns
for feature in categorical_features:
    table = pd.crosstab(train_data[feature], test_data[feature])
    chi2, p, dof, expected = chi2_contingency(table)
    results.append({'Feature': feature, 'Statistic': chi2, 'p-value': p})
results_df = pd.DataFrame(results)
results_df
FeatureStatisticp-value
0person_home_ownership12.9492840.164915
1loan_intent22.6023360.600772
2loan_grade15.4621600.998897
3cb_person_default_on_file0.1452980.703070

综上所述,可以认为训练集和测试集在特征分布上是一致的,因此可以只对训练集进行进一步的分析和模型训练,将简化分析过程,并确保模型在测试集上的评估具有代表性。

6.描述性分析

train_data.describe(include='all')
idperson_ageperson_incomeperson_home_ownershipperson_emp_lengthloan_intentloan_gradeloan_amntloan_int_rateloan_percent_incomecb_person_default_on_filecb_person_cred_hist_lengthloan_status
count58641.00000058641.0000005.864100e+045864158641.000000586415864158641.00000058641.00000058641.0000005864158641.00000058641.000000
uniqueNaNNaNNaN4NaN67NaNNaNNaN2NaNNaN
topNaNNaNNaNRENTNaNEDUCATIONANaNNaNNaNNNaNNaN
freqNaNNaNNaN30594NaN1227120983NaNNaNNaN49940NaNNaN
mean29321.28026527.5686646.404244e+04NaN4.842499NaNNaN9217.13309810.6778590.159707NaN5.8132540.142375
std16929.6135916.0168273.792517e+04NaN4.040348NaNNaN5563.4265673.0346430.094312NaN4.0281580.349437
min0.00000020.0000004.200000e+03NaN0.000000NaNNaN500.0000005.4200000.000000NaN2.0000000.000000
25%14660.00000023.0000004.200000e+04NaN2.000000NaNNaN5000.0000007.8800000.090000NaN3.0000000.000000
50%29321.00000026.0000005.800000e+04NaN4.000000NaNNaN8000.00000010.7500000.140000NaN4.0000000.000000
75%43982.00000030.0000007.560000e+04NaN7.000000NaNNaN12000.00000012.9900000.210000NaN8.0000000.000000
max58644.00000084.0000001.900000e+06NaN44.000000NaNNaN35000.00000023.2200003.120000NaN30.0000001.000000
  • 借款人群体较年轻,主要是租房者,可能反映了年轻人对贷款的需求较高。
  • 教育是最常见的贷款目的,这可能与年轻的借款人群体特征相关。
  • 大多数借款人拥有较好的信用评级(A级最多),且大部分没有违约记录。
  • 贷款批准率是14%。

7.贷款获批的影响因素分析

7.1可视化分析

plt.figure(figsize=(20,12))
plt.subplot(2,3,1)
sns.boxplot(x=train_data['loan_status'],y=train_data['person_age'])
plt.title('年龄与贷款批准情况的关系')
plt.xlabel('贷款批准情况')
plt.ylabel('借款人年龄')

plt.subplot(2,3,2)
sns.boxplot(x=train_data['loan_status'],y=train_data['person_income'])
plt.title('年收入与贷款批准情况的关系')
plt.xlabel('贷款批准情况')
plt.ylabel('借款人年收入(美元)')

plt.subplot(2,3,3)
sns.boxplot(x=train_data['loan_status'],y=train_data['person_emp_length'])
plt.title('工作年限与贷款批准情况的关系')
plt.xlabel('贷款批准情况')
plt.ylabel('借款人工作年限')


ownership_and_loan = pd.crosstab(train_data['person_home_ownership'], train_data['loan_status'])
ownership_and_loan_percent = ownership_and_loan.div(ownership_and_loan.sum(axis=1), axis=0) * 100
ax4 = plt.subplot(2,3,(4,6))
ownership_and_loan_percent[1].plot(kind='bar',ax=ax4)
plt.title('房屋拥有情况与贷款批准情况的关系')
plt.xlabel('借款人房屋拥有情况')
plt.ylabel('贷款批准率')
plt.xticks(rotation=0)
for p in ax4.patches:
    ax4.annotate(f"{p.get_height():.1f}%", (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='center', xytext=(0, 5), textcoords='offset points')

plt.tight_layout()
plt.show()

由于数据是模拟生成的,在这个数据中,可以看到,年龄、年收入和贷款批准情况是没显著关系的,通过批准的工作年限中位数反而比未通过的工作年限中位数低,并且租房和其他的通过率比按揭房屋和拥有房屋的通过率高,这并不符合常识。

plt.figure(figsize=(20,20))
plt.subplot(4,3,1)
sns.boxplot(x=train_data['loan_status'],y=train_data['loan_amnt'])
plt.title('贷款金额与贷款批准情况的关系')
plt.xlabel('贷款批准情况')
plt.ylabel('贷款金额(美元)')

plt.subplot(4,3,2)
sns.boxplot(x=train_data['loan_status'],y=train_data['person_income'])
plt.title('贷款利率与贷款批准情况的关系')
plt.xlabel('贷款批准情况')
plt.ylabel('贷款利率(百分比)')

plt.subplot(4,3,3)
sns.boxplot(x=train_data['loan_status'],y=train_data['loan_percent_income'])
plt.title('贷款金额占收入的比例与贷款批准情况的关系')
plt.xlabel('贷款批准情况')
plt.ylabel('贷款金额占收入的比例')

plt.subplot(4,3,4)
sns.boxplot(x=train_data['loan_status'],y=train_data['cb_person_cred_hist_length'])
plt.title('信用历史长度与贷款批准情况的关系')
plt.xlabel('贷款批准情况')
plt.ylabel('信用历史长度')

default_and_loan = pd.crosstab(train_data['cb_person_default_on_file'], train_data['loan_status'])
default_and_loan_percent = default_and_loan.div(default_and_loan.sum(axis=1), axis=0) * 100
ax5 = plt.subplot(4,3,(5,6))
default_and_loan_percent[1].plot(kind='bar',ax=ax5)
plt.title('违约记录情况与贷款批准情况的关系')
plt.xlabel('违约记录情况')
plt.ylabel('贷款批准率')
plt.xticks(rotation=0)
for p in ax5.patches:
    ax5.annotate(f"{p.get_height():.1f}%", (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='center', xytext=(0, 5), textcoords='offset points')

intent_and_loan = pd.crosstab(train_data['loan_intent'], train_data['loan_status'])
intent_and_loan_percent = intent_and_loan.div(intent_and_loan.sum(axis=1), axis=0) * 100
ax7 = plt.subplot(4,3,(7,9))
intent_and_loan_percent[1].plot(kind='bar',ax=ax7)
plt.title('贷款意图与贷款批准情况的关系')
plt.xlabel('贷款意图')
plt.ylabel('贷款批准率')
plt.xticks(rotation=0)
for p in ax7.patches:
    ax7.annotate(f"{p.get_height():.1f}%", (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='center', xytext=(0, 5), textcoords='offset points')

grade_and_loan = pd.crosstab(train_data['loan_grade'], train_data['loan_status'])
grade_and_loan_percent = grade_and_loan.div(grade_and_loan.sum(axis=1), axis=0) * 100
ax10 = plt.subplot(4,3,(10,12))
grade_and_loan_percent[1].plot(kind='bar',ax=ax10)
plt.title('贷款信用等级与贷款批准情况的关系')
plt.xlabel('贷款信用等级')
plt.ylabel('贷款批准率')
plt.xticks(rotation=0)
for p in ax10.patches:
    ax10.annotate(f"{p.get_height():.1f}%", (p.get_x() + p.get_width() / 2., p.get_height()),
                ha='center', va='center', xytext=(0, 5), textcoords='offset points')

plt.tight_layout()
plt.show()

在通过批准的数据中,贷款金额和贷款金额与收入比例的中位数比未通过的贷款金额中位数高,贷款利率的中位数比未通过的贷款利率中位数低,信用历史长度与贷款审批的关系看着不显著,而有违约的通过率更高,信用评分在G级的通过率也是最高的,考虑有些可能是样本太小导致的,比如某个类别只有3个样本,可是有一例通过批准了,这样就会导致他的通过率特别的高,所以还要使用置信区间分析,通过计算通过率的置信区间。这可以了解结果的可靠性。

def calculate_confidence_interval(successes, total, confidence=0.95):
    """
    计算二项分布的置信区间
    
    :param successes: 成功次数(这里是获得贷款批准的次数)
    :param total: 总样本数
    :param confidence: 置信水平,默认95%
    :return: (下限, 上限, 点估计)
    """
    if total == 0:
        return 0, 0, 0
    
    point_estimate = successes / total
    z = stats.norm.ppf((1 + confidence) / 2)
    margin_of_error = z * np.sqrt((point_estimate * (1 - point_estimate)) / total)
    
    lower_bound = max(0, point_estimate - margin_of_error)
    upper_bound = min(1, point_estimate + margin_of_error)
    
    return lower_bound, upper_bound, point_estimate
def analyze_feature(df, feature_name):
    """
    分析特定特征的贷款批准率及其置信区间
    
    :param df: 数据框
    :param feature_name: 特征名称
    """
    grouped = df.groupby(feature_name)
    results = []
    
    for group, data in grouped:
        total = len(data)
        approved = data['loan_status'].sum()
        lower, upper, point = calculate_confidence_interval(approved, total)
        
        results.append({
            'Group': group,
            'Total Samples': total,
            'Approval Rate': point,
            'CI Lower': lower,
            'CI Upper': upper
        })
    
    return pd.DataFrame(results)
print("房屋拥有情况的置信区间分析:")
analyze_feature(train_data, 'person_home_ownership')
房屋拥有情况的置信区间分析:
GroupTotal SamplesApproval RateCI LowerCI Upper
0MORTGAGE248200.0597100.0567620.062658
1OTHER890.1685390.0907670.246312
2OWN31380.0137030.0096350.017771
3RENT305940.2225600.2178990.227221
  1. “按揭”样本量大,置信区间较窄,说明这个估计比较精确,按揭房主的贷款批准率相对较低。
  2. “其他”样本量最小,导致置信区间非常宽,这个类别的结果不太可靠,难以得出确切结论。
  3. “拥有房产”样本量比较小,置信区间相对较窄,自有房产者的贷款批准率出人意料地低。
  4. “租房”最大的样本量,置信区间很窄,估计非常精确,租房者的贷款批准率明显高于其他类别。
print("违约记录的置信区间分析:")
analyze_feature(train_data, 'cb_person_default_on_file')
违约记录的置信区间分析:
GroupTotal SamplesApproval RateCI LowerCI Upper
0N499400.1151180.1123190.117917
1Y87010.2988160.2891980.308434

有违约记录的申请者获得贷款批准的概率明显高于无违约记录的申请者。

print("贷款意图的置信区间分析:")
analyze_feature(train_data, 'loan_intent')
贷款意图的置信区间分析:
GroupTotal SamplesApproval RateCI LowerCI Upper
0DEBTCONSOLIDATION91330.1893130.1812790.197348
1EDUCATION122710.1077340.1022480.113219
2HOMEIMPROVEMENT62800.1737260.1643560.183097
3MEDICAL109330.1781760.1710030.185349
4PERSONAL100140.1328140.1261670.139461
5VENTURE100100.0928070.0871230.098491

样本分布都比较均匀,这使得所有估计都相对可靠,债务整合、家庭改善和医疗贷款批准率高,教育、个人消费的批准率相对较低,创业的批准率是最低的。

print("贷款信用等级的置信区间分析:")
analyze_feature(train_data, 'loan_grade')
贷款信用等级的置信区间分析:
GroupTotal SamplesApproval RateCI LowerCI Upper
0A209830.0491830.0462570.052109
1B203980.1023140.0981550.106473
2C110360.1353750.1289920.141758
3D50330.5934830.5799130.607053
4E10090.6253720.5955060.655237
5F1490.6107380.5324490.689028
6G330.8181820.6865880.949775

A到D级的结果较为可靠,因为样本量大,置信区间窄,批准率与信用等级呈现反比关系,这与传统信贷逻辑相悖。

7.2斯皮尔曼相关性分析

# 定义映射关系
grade_order = {'A': 1, 'B': 2, 'C': 3, 'D': 4, 'E': 5, 'F': 6, 'G': 7}
defaul_mapping = {'N': 0, 'Y':1}
# 将有序变量转换为数值
train_data['loan_grade'] = train_data['loan_grade'].map(grade_order)
test_data['loan_grade'] = test_data['loan_grade'].map(grade_order)
train_data['cb_person_default_on_file'] = train_data['cb_person_default_on_file'].map(defaul_mapping)
test_data['cb_person_default_on_file'] = test_data['cb_person_default_on_file'].map(defaul_mapping)
def plot_spearmanr(data,features,title,wide,height):
    # 计算斯皮尔曼相关性矩阵和p值矩阵
    spearman_corr_matrix = data[features].corr(method='spearman')
    pvals = data[features].corr(method=lambda x, y: spearmanr(x, y)[1]) - np.eye(len(data[features].columns))

    # 转换 p 值为星号
    def convert_pvalue_to_asterisks(pvalue):
        if pvalue <= 0.001:
            return "***"
        elif pvalue <= 0.01:
            return "**"
        elif pvalue <= 0.05:
            return "*"
        return ""

    # 应用转换函数
    pval_star = pvals.applymap(lambda x: convert_pvalue_to_asterisks(x))

    # 转换成 numpy 类型
    corr_star_annot = pval_star.to_numpy()

    # 定制 labels
    corr_labels = spearman_corr_matrix.to_numpy()
    p_labels = corr_star_annot
    shape = corr_labels.shape

    # 合并 labels
    labels = (np.asarray(["{0:.2f}\n{1}".format(data, p) for data, p in zip(corr_labels.flatten(), p_labels.flatten())])).reshape(shape)

    # 绘制热力图
    fig, ax = plt.subplots(figsize=(height, wide), dpi=100, facecolor="w")
    sns.heatmap(spearman_corr_matrix, annot=labels, fmt='', cmap='coolwarm',
                vmin=-1, vmax=1, annot_kws={"size":10, "fontweight":"bold"},
                linecolor="k", linewidths=.2, cbar_kws={"aspect":13}, ax=ax)

    ax.tick_params(bottom=False, labelbottom=True, labeltop=False,
                left=False, pad=1, labelsize=12)
    ax.yaxis.set_tick_params(labelrotation=0)

    # 自定义 colorbar 标签格式
    cbar = ax.collections[0].colorbar
    cbar.ax.tick_params(direction="in", width=.5, labelsize=10)
    cbar.set_ticks([-1, -0.5, 0, 0.5, 1])
    cbar.set_ticklabels(["-1.00", "-0.50", "0.00", "0.50", "1.00"])
    cbar.outline.set_visible(True)
    cbar.outline.set_linewidth(.5)

    plt.title(title)
    plt.show()
features = train_data.drop(['id','person_home_ownership','loan_intent'],axis=1).columns.tolist()
plot_spearmanr(train_data,features,'各变量之间的斯皮尔曼相关系数热力图',12,15)

目标变量(是否获得贷款批准)与 借款人年龄、信用历史长度不相关,与借款人年收入、借款人工作年限呈弱负相关,与贷款信用等级、贷款利率、 贷款金额占收入的比例呈中等正相关(注意:贷款信用等级越大表示信用越差),与贷款金额、违约记录呈弱正相关。

7.3卡方检验

def chi_square_test(var1, var2):
    contingency_table = pd.crosstab(train_data[var1], train_data[var2])
    chi2, p, dof, expected = stats.chi2_contingency(contingency_table)
    return chi2, p

loan_status_chi_square_results = {}
cat_features = ['person_home_ownership', 'loan_intent']
loan_status_chi_square_results = {feature: chi_square_test(feature, 'loan_status') for feature in cat_features}

loan_status_chi_square_df = pd.DataFrame.from_dict(loan_status_chi_square_results,orient='index',columns=['Chi-Square','P-Value'])
loan_status_chi_square_df
Chi-SquareP-Value
person_home_ownership3426.0178020.000000e+00
loan_intent659.6231162.632671e-140

通过卡方检验,发现借款人房屋拥有情况和贷款意图的P值很小,因此认为与是否获得贷款批准是显著相关的。

8.预测贷款批准通过情况

8.1数据预处理

考虑后续会使用逻辑回归、神经网络模型,还是对数据采取标准化和独热编码,同时剔除斯皮尔曼相关性分析中不显著的两个变量,然后划分数据,注意,这里不打算平衡样本了,因为我在后续的分析中,发现逻辑回归平衡样本后对类别1的预测反而下降了,可能是过度采样带来的噪声,当然这种只是特例,假如读者在分析其他数据时,发现不平衡会导致少数类的预测准确率几乎接近0的话,还是要平衡样本,或者调整决策阈值。

new_train_data = train_data.drop(columns=['id','person_age','cb_person_cred_hist_length'])
new_test_data = test_data.drop(columns=['id','person_age','cb_person_cred_hist_length'])
# 独热编码和标签编码的列
one_hot_features = ['person_home_ownership', 'loan_intent']
# 独热编码类别特征,并删除第一个类别避免虚拟变量陷阱
new_train_data = pd.get_dummies(new_train_data, columns=one_hot_features, drop_first=True)
new_test_data = pd.get_dummies(new_test_data, columns=one_hot_features, drop_first=True)

# 将布尔值转换为数值型(0 和 1)
new_train_data = new_train_data.astype(int)
new_test_data = new_test_data.astype(int)
numerical_features = ['person_income', 'person_emp_length','loan_amnt','loan_int_rate','loan_percent_income']
# 对数值型特征进行标准化
scaler = StandardScaler()
new_train_data[numerical_features] = scaler.fit_transform(new_train_data[numerical_features])
new_test_data[numerical_features] = scaler.transform(new_test_data[numerical_features])
x = new_train_data.drop(['loan_status'],axis=1)
y = new_train_data['loan_status']
x_train,x_test,y_train,y_test = train_test_split(x,y,test_size=0.2,random_state=15) #28分

8.2逻辑回归

# 初始化逻辑回归模型
log_reg = LogisticRegression(random_state=15)
# 训练模型
log_reg.fit(x_train, y_train)
y_pred_log = log_reg.predict(x_test)
class_report_log = classification_report(y_test, y_pred_log)
print('逻辑回归模型评估如下:')
print(class_report_log)
逻辑回归模型评估如下:
              precision    recall  f1-score   support

           0       0.91      0.98      0.94     10019
           1       0.75      0.43      0.54      1710

    accuracy                           0.90     11729
   macro avg       0.83      0.70      0.74     11729
weighted avg       0.89      0.90      0.88     11729
cm = confusion_matrix(y_test,y_pred_log)

plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='g', cmap='Blues', 
            xticklabels=['预测值 0', '预测值 1'], 
            yticklabels=['真实值 0', '真实值 1'])
plt.title('逻辑回归模型预测的混淆矩阵')
plt.show()
#绘制ROC曲线
fpr, tpr, _ = roc_curve(y_test, y_pred_log)
roc_auc = auc(fpr, tpr)

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC曲线(面积 = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlabel('假阳率')
plt.ylabel('真阳率')
plt.title('逻辑回归模型的ROC曲线')
plt.legend(loc="lower right")
plt.show()

8.3随机森林

rf_clf = RandomForestClassifier(random_state=15)
rf_clf.fit(x_train, y_train)
y_pred_rf = rf_clf.predict(x_test)
class_report_rf = classification_report(y_test, y_pred_rf)
print('随机森林模型评估如下:')
print(class_report_rf)
随机森林模型评估如下:
              precision    recall  f1-score   support

           0       0.95      0.99      0.97     10019
           1       0.89      0.68      0.77      1710

    accuracy                           0.94     11729
   macro avg       0.92      0.83      0.87     11729
weighted avg       0.94      0.94      0.94     11729
cm = confusion_matrix(y_test,y_pred_rf)

plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='g', cmap='Blues', 
            xticklabels=['预测值 0', '预测值 1'], 
            yticklabels=['真实值 0', '真实值 1'])
plt.title('随机森林模型预测的混淆矩阵')
plt.show()
#绘制ROC曲线
fpr, tpr, _ = roc_curve(y_test, y_pred_rf)
roc_auc = auc(fpr, tpr)

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC曲线(面积 = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlabel('假阳率')
plt.ylabel('真阳率')
plt.title('随机森林模型的ROC曲线')
plt.legend(loc="lower right")
plt.show()

8.4多层感知机

# 建立神经网络模型
mlp_model = Sequential([
    Input(shape=(x_train.shape[1],)),  # 指定输入形状
    Dense(64, activation='relu'),      # 隐藏层
    Dense(32, activation='relu'),      # 隐藏层
    Dense(1, activation='sigmoid')     # 输出层
])
# 编译模型
mlp_model.compile(optimizer='adam', loss='binary_crossentropy', metrics=['accuracy'])
mlp_model.fit(x_train, y_train, epochs=50, batch_size=32, validation_data=(x_test, y_test), verbose=0)
y_pred_mlp = (mlp_model.predict(x_test) >= 0.6).astype(int) # 这里把概率大于0.6的才看作1,这是调整分类阈值来提高准确率的
class_report_mlp = classification_report(y_test, y_pred_mlp)
print('多层感知机评估如下:')
print(class_report_mlp)
多层感知机评估如下:
              precision    recall  f1-score   support

           0       0.95      0.99      0.97     10019
           1       0.92      0.67      0.78      1710

    accuracy                           0.94     11729
   macro avg       0.93      0.83      0.87     11729
weighted avg       0.94      0.94      0.94     11729
cm = confusion_matrix(y_test,y_pred_mlp)

plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='g', cmap='Blues', 
            xticklabels=['预测值 0', '预测值 1'], 
            yticklabels=['真实值 0', '真实值 1'])
plt.title('多层感知机模型预测的混淆矩阵')
plt.show()
#绘制ROC曲线
fpr, tpr, _ = roc_curve(y_test, y_pred_mlp)
roc_auc = auc(fpr, tpr)

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC曲线(面积 = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlabel('假阳率')
plt.ylabel('真阳率')
plt.title('多层感知机模型的ROC曲线')
plt.legend(loc="lower right")
plt.show()

8.5深度神经网络

# 构建深度神经网络模型
deep_model = Sequential([
    Input(shape=(x_train.shape[1],)),
    Dense(128, activation='relu'),  # 第一层隐藏层,128节点
    Dropout(0.3),                   # Dropout层,丢弃30%的节点
    Dense(64, activation='relu'),   # 第二层隐藏层,64节点
    Dropout(0.3),
    Dense(32, activation='relu'),   # 第三层隐藏层,32节点
    Dropout(0.3),
    Dense(16, activation='relu'),   # 第四层隐藏层,16节点
    Dense(1, activation='sigmoid')  # 输出层,Sigmoid激活
])
# 编译模型
deep_model.compile(optimizer=Adam(learning_rate=0.001), loss='binary_crossentropy', metrics=['accuracy'])

# 训练模型
deep_model.fit(x_train, y_train, epochs=100, batch_size=32, validation_data=(x_test, y_test), verbose=0)
y_pred_deep = (deep_model.predict(x_test) >= 0.6).astype(int)
class_report_deep = classification_report(y_test, y_pred_deep)
print('深度神经网络模型评估如下:')
print(class_report_deep)
深度神经网络模型评估如下:
              precision    recall  f1-score   support

           0       0.94      0.99      0.97     10019
           1       0.93      0.65      0.77      1710

    accuracy                           0.94     11729
   macro avg       0.94      0.82      0.87     11729
weighted avg       0.94      0.94      0.94     11729
cm = confusion_matrix(y_test,y_pred_deep)

plt.figure(figsize=(8, 6))
sns.heatmap(cm, annot=True, fmt='g', cmap='Blues', 
            xticklabels=['预测值 0', '预测值 1'], 
            yticklabels=['真实值 0', '真实值 1'])
plt.title('深度神经网络模型的混淆矩阵')
plt.show()
#绘制ROC曲线
fpr, tpr, _ = roc_curve(y_test, y_pred_deep)
roc_auc = auc(fpr, tpr)

plt.figure(figsize=(8, 6))
plt.plot(fpr, tpr, color='darkorange', lw=2, label='ROC曲线(面积 = %0.2f)' % roc_auc)
plt.plot([0, 1], [0, 1], color='navy', lw=2, linestyle='--')
plt.xlabel('假阳率')
plt.ylabel('真阳率')
plt.title('深度神经网络模型的ROC曲线')
plt.legend(loc="lower right")
plt.show()

综合看下来,各个模型的预测效果都不错,虽然可以进一步优化参数,但是考虑优化参数耗时特别久,耗费的时间或许不值得(当然,如果是一个真实数据,有大型服务器的企业,能提高一些精度也是十分有必要的)。

8.6SHAP分析

# 创建SHAP解释器
explainer = shap.DeepExplainer(deep_model, x_test.values[:1000])  # 使用前1000个样本作为背景

# 计算SHAP值
shap_values = explainer.shap_values(x_test.values)
print(f"shap_values最后一个维度是{shap_values.shape[-1]}")
shap_values最后一个维度是1

shap_values最后一个维度是1,需要去除,因为对于大多数机器学习模型,SHAP值通常是二维数组:(样本数, 特征数),但在某些深度学习框架或SHAP实现中,可能会输出三维数组:(样本数, 特征数, 1),而且当最后一个维度是1时,它不包含额外信息,只是一个冗余的维度。

shap_values = shap_values.squeeze(-1)

# 绘制 SHAP 汇总图
plt.figure(figsize=(12, 8))
shap.summary_plot(shap_values, x_test, plot_type="bar", show=False)
plt.title("深度神经网络的 SHAP 特征重要性")
plt.tight_layout()
plt.show()
# 计算并打印特征重要性
feature_importance = np.abs(shap_values).mean(0)
importance_df = pd.DataFrame(list(zip(x_test.columns, feature_importance)), 
                             columns=['特征', '重要性'])
importance_df = importance_df.sort_values('重要性', ascending=False)
importance_df
特征重要性
2loan_grade0.050171
0person_income0.045789
3loan_amnt0.039707
4loan_int_rate0.022440
9person_home_ownership_RENT0.021598
14loan_intent_VENTURE0.013976
1person_emp_length0.008896
10loan_intent_EDUCATION0.008064
8person_home_ownership_OWN0.006981
11loan_intent_HOMEIMPROVEMENT0.005900
13loan_intent_PERSONAL0.004905
12loan_intent_MEDICAL0.003385
6cb_person_default_on_file0.002048
7person_home_ownership_OTHER0.000121
5loan_percent_income0.000000
# 绘制 SHAP 值的分布图
plt.figure(figsize=(12, 8))
shap.summary_plot(shap_values, x_test, show=False)
plt.title("SHAP 值分布")
plt.tight_layout()
plt.show()

通过SHAP值分析,发现深度神经网络中,重要的特征因素是贷款信用等级、借款人年收入、贷款的利率。

9.多模型预测测试集

new_y_pred_log = log_reg.predict(new_test_data)
new_y_pred_rf = rf_clf.predict(new_test_data)
new_y_pred_mlp = (mlp_model.predict(new_test_data) >= 0.6).astype(int).ravel()
new_y_pred_deep = (deep_model.predict(new_test_data) >= 0.6).astype(int).ravel()
test_data['log_pred'] = new_y_pred_log
test_data['rf_pred'] = new_y_pred_rf
test_data['mlp_pred'] = new_y_pred_mlp
test_data['nn_pred'] = new_y_pred_deep
test_data.head()
idperson_ageperson_incomeperson_home_ownershipperson_emp_lengthloan_intentloan_gradeloan_amntloan_int_rateloan_percent_incomecb_person_default_on_filecb_person_cred_hist_lengthlog_predrf_predmlp_prednn_pred
0586452369000RENT3.0HOMEIMPROVEMENT62500015.760.36021111
1586462696000MORTGAGE6.0PERSONAL31000012.680.10140000
2586472630000RENT5.0VENTURE5400017.190.13121110
3586483350000RENT4.0DEBTCONSOLIDATION170008.900.14070000
45864926102000MORTGAGE8.0HOMEIMPROVEMENT41500016.320.15140000

10.总结

本项目从影响因素分析建立模型两个主要角度,深入探讨了贷款批准通过的影响因素,并基于 SHAP(SHapley Additive exPlanations)对深度神经网络模型行了分析。得出以下结论:

  1. 贷款批准通过的影响因素主要有:借款人年收入、借款人工作年限、贷款信用等级、贷款利率、 贷款金额占收入的比例、贷款金额、违约记录、借款人房屋拥有情况和贷款意图。
  2. 四类模型预测效果均比较理想。
  3. 通过SHAP值分析,发现深度神经网络中,重要的特征因素是贷款信用等级、借款人年收入、贷款的利率。
  4. 由于数据是模拟生成的,许多地方出现了与常识不符合的地方,本项目仅供学习参考,是来自Kaggle上的一个比赛,截止目前完成该项目的时候,第一名的准确率得分是0.97378,而我的得分只有0.93379,推测可能是没有进行参数优化,或者进一步特征工程导致的。

http://www.kler.cn/news/353905.html

相关文章:

  • Vue 3 和 Vue 2区别
  • 若依框架中spring security的完整认证流程,及其如何使用自定义用户表进行登录认证,学会轻松实现二开,嘎嘎赚块乾
  • 开发中众多框架的个人理解,Unity设计模式,MVC,MVVM框架
  • 【WebGIS实例】怎么将GCJ02坐标系的经纬度转换为WGS84坐标系?
  • 短视频矩阵源码开发/抖音矩阵系统OEM搭建/短视频源码开发知识分享
  • 第十四届单片机嵌入式蓝桥杯
  • 单一执行和循环执行的例行性工作
  • 【C++11】可变模板参数详解
  • Python RabbitMQ 入门 pika
  • Java之集合介绍
  • 在移动设备上扫描登机牌条形码,有哪些挑战 ?
  • 2010年国赛高教杯数学建模C题输油管的布置解题全过程文档及程序
  • 【C语言】自定义类型:联合和枚举
  • 【算法】约瑟夫环问题
  • vue使用js-xlsx导入本地excle表格数据,回显在页面上
  • 本地群晖NAS安装phpMyAdmin管理MySQ数据库实战指南
  • 深度解析LMS(Least Mean Squares)算法
  • 单链表的定义
  • 多进程思维导图
  • 在ArcGISPro中使用 SAR 数据和深度学习绘制洪水地图