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

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,推测可能是没有进行参数优化,或者进一步特征工程导致的。

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/893118.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

【Linux系统编程】环境基础开发工具使用

目录 1、Linux软件包管理器yum 1.1 什么是软件包 1.2 安装软件 1.3 查看软件包 1.4 卸载软件 2、Linux编辑器-vim 2.1 vim的概念 2.2 vim的基本操作 2.3 vim的配置 3、Linux编译器-gcc/g 3.1 gcc编译的过程​编辑​编辑​编辑 3.2 详解链接 动态链接 静态链接 4…

深度解析LMS(Least Mean Squares)算法

目录 一、引言二、LMS算法简介三、LMS算法的工作原理四、LMS算法的特点五、LMS算法的应用场景六、LMS算法的局限性七、总结八、进一步探讨 一、引言 自适应滤波器是一种动态调整其参数以适应变化环境的信号处理工具&#xff0c;广泛应用于噪声消除、信道均衡和系统识别等领域。…

Axure RP电商系统商城PC+app+后台买家卖端高保真原型模板及元件库

AxureRP电商商城PCapp后台买家卖端高保真原型模板本套包含三份原型图素材 APP买家端原型简介&#xff1a; 包含了用户中心、会员成长、优惠券、积分、互动社区、运营推广、内容推荐、商品展示、订单流程、订单管理、售后及服务等完整的电商体系功能架构和业务流程。 本模板由…

Rancher—多集群Kubernetes管理平台

目录 一、Rancher 简介1.1 Rancher 和 k8s 的区别 二、Rancher 安装及配置2.1 安装 rancher2.2 登录 Rancher 平台2.3 Rancher 管理已存在的 k8s 集群2.4 创建名称空间 namespace2.5 创建 Deployment 资源2.6 创建 service2.7 Rancher 部署监控系统 一、Rancher 简介 Rancher …

中国科学院大学与美团发布首个交互式驾驶世界模型数据集DrivingDojo:推进交互式与知识丰富的驾驶世界模型

中国科学院大学与美团发布首个交互式驾驶世界模型数据集DrivingDojo&#xff1a;推进交互式与知识丰富的驾驶世界模型 Abstract 驾驶世界模型因其对复杂物理动态的建模能力而受到越来越多的关注。然而&#xff0c;由于现有驾驶数据集中的视频多样性有限&#xff0c;其卓越的建…

uniapp学习(004-2 组件 Part.2生命周期)

零基础入门uniapp Vue3组合式API版本到咸虾米壁纸项目实战&#xff0c;开发打包微信小程序、抖音小程序、H5、安卓APP客户端等 总时长 23:40:00 共116P 此文章包含第31p-第p35的内容 文章目录 组件生命周期我们主要使用的三种生命周期setup(创建组件时执行)不可以操作dom节点…

我对软件工程的理解

1 引言 从事软件行业这么年&#xff0c;写了10年代码&#xff0c;又从事了多年的项目产品方面的工作&#xff0c;一些每天用到的软件工程的方法&#xff0c;虽然天天都在用但一些概念总感觉似是而非&#xff0c;正好借假期的时间&#xff0c;好好整理下&#xff0c;以供自己或…

【你也能从零基础学会网站开发】浅谈一下SQL Server 2000中的NULL值到底有什么用处

&#x1f680; 个人主页 极客小俊 ✍&#x1f3fb; 作者简介&#xff1a;程序猿、设计师、技术分享 &#x1f40b; 希望大家多多支持, 我们一起学习和进步&#xff01; &#x1f3c5; 欢迎评论 ❤️点赞&#x1f4ac;评论 &#x1f4c2;收藏 &#x1f4c2;加关注 NULL 是什么 …

2d实时数字人聊天语音对话使用案例,对接大模型

参看: https://github.com/wan-h/awesome-digital-human-live2d 电脑环境: ubuntu 1060ti 下载: git clone https://github.com/wan-h/awesome-digital-human-live2d.gitdocker部署; cd awesome-digital-human-live2d docker-compose -f docker-compose-quickStart.ya…

Spring AI Java程序员的AI之Spring AI(一)

SpringAI 基础使用 前言Spring AIChatClientImageClientOpenAiAudioTranscriptionClientEmbeddingClient 总结 前言 Spring AI&#xff0c;听着名字就感觉很好使用&#xff0c;快速上手&#xff0c;虽然功能没有太完善&#xff0c;但是社区活跃度很高&#xff0c;可以看看源码…

大数据治理:构建数据驱动的智能决策体系

✅作者简介&#xff1a;2022年博客新星 第八。热爱国学的Java后端开发者&#xff0c;修心和技术同步精进。 &#x1f34e;个人主页&#xff1a;Java Fans的博客 &#x1f34a;个人信条&#xff1a;不迁怒&#xff0c;不贰过。小知识&#xff0c;大智慧。 &#x1f49e;当前专栏…

Edge论文的创新点

创新点及其来源 1. 从灰度边缘重建RGB图像的方法&#xff08;EdgRec&#xff09; 基于的方法&#xff1a;传统的重建方法&#xff0c;如使用自动编码器或生成模型来重建正常样本的图像&#xff0c;并通过对原始图像和重建图像的比较来检测异常。 重建过程&#xff1a; 训练阶…

Spring Boot: 构建高效中小型医院网站

1 绪论 1.1研究背景 随着计算机技术的成熟、普及&#xff0c;现代信息技术革命的迅猛发展,正冲击并进而改变着经济和社会结构。信息化的程度已经成为一个国家&#xff0c;一个企业&#xff0c;一个组织仍至一个人发展的基础和竞争成败的关键。 在实际的生活中&#xff0c;用户都…

Oracle Expdp按条件导出-指定表数据

1.场景描述 业务需求&#xff1a;导出A机构、2024的数据&#xff0c;以dmp格式&#xff0c;保留导出日志。首先&#xff0c;需要分析库中需要导出的表清单、表的机构字段约束、表的时间约束&#xff1b;然后再导出。 2.方案分析 本次采用Oracle的expdp数据泵方式导出&#xf…

集合框架12:Set集合概述、Set接口使用

视频链接&#xff1a;13.24 Set接口使用_哔哩哔哩_bilibilihttps://www.bilibili.com/video/BV1zD4y1Q7Fw?spm_id_from333.788.videopod.episodes&vd_sourceb5775c3a4ea16a5306db9c7c1c1486b5&p24 1、Set集合概述 特点&#xff1a;无序、无下标&#xff0c;元素不可…

现今 CSS3 最强二维布局系统 Grid 网格布局

深入学习 CSS3 目前最强大的布局系统 Grid 网格布局 Grid 网格布局的基本认识 Grid 网格布局: Grid 布局是一个基于网格的二位布局系统&#xff0c;是目前 CSS 最强的布局系统&#xff0c;它可以同时对列和行进行处理&#xff08;它将网页划分成一个个网格&#xff0c;可以任…

限流是什么?如何限流?怎么限流?

概述 什么是限流 对某一时间窗口内的请求数进行限制,保持系统的可用性和稳定性,防止因流量暴增而导致的系统运行缓慢或宕机 为什么要限流 因为互联网系统通常都要面对大并发大流量的请求,在突发情况下(最常见的场景就是秒杀、抢购),瞬时大流量会直接将系统打垮,无法…

DS堆的实际应用(10)

文章目录 前言一、堆排序建堆排序 二、TopK问题原理实战创建一个有一万个数的文件读取文件并将前k个数据创建小堆用剩余的N-K个元素依次与堆顶元素来比较将前k个数据打印出来并关闭文件 测试 三、堆的相关习题总结 前言 学完了堆这个数据结构的概念和特性后&#xff0c;我们来看…

DVWA | Files Upload(文件上传)通关笔记

概念 **文件上传漏洞**是网络安全中常见的漏洞之一&#xff0c;攻击者可以利用该漏洞上传恶意文件&#xff0c;进而在服务器上执行恶意代码、绕过权限验证或获取敏感数据。文件上传漏洞主要发生在允许用户上传文件的Web应用程序中&#xff0c;比如图像、文档上传功能等。 ###…

dayjs日期格式化,开发uniapp或unicloud前后端进行时间格式转换

一、 为什么要用日期格式化 因为在开发项目过程中&#xff0c;会遇到各种各样的日期格式&#xff0c;有的显示完整的年-月-日 时:分:秒&#xff0c;而有的场景就只显示月-日等格式&#xff0c;还有就是显示当前时间和注册时间的间隔时长等&#xff0c;场景非常多&#xff0c;如…