数据集及完整代码已上传到github:https://github.com/JIANG54864/ml_exp/tree/main/exp1

信用风险是指银行向用户提供金融服务后,用户不还款的概率。信用风险一直是银行贷款决策中广泛研究的领域。信用风险对银行和金融机构,特别是商业银行来说,起着至关重要的作用,但是一直以来都比较难管理。

本实验以贷款违约为背景,要求使用贝叶斯决策论的相关知识在训练集上构建模型,在测试集上进行贷款违约预测并计算分类准确度。实验目的是使用贝叶斯处理实际问题,不得使用现成工具包直接进行分类。

训练数据集train.csv包含9000条数据,测试数据集test.csv包含1000条数据。注意,训练集和测试集中都有缺失值存在。

以下是字段说明:

字段 描述
loan_id 贷款记录唯一标识
user_id 借款人唯一标识
total_loan 贷款数额
year_of_loan 贷款年份
interest 当前贷款利率
monthly_payment 分期付款金额
grade 贷款级别
employment_type 所在公司类型
industry 工作领域
work_year 工作年限
home_exist 是否有房
censor_status 审核情况
issue_date 贷款发放的月份
use 贷款用途类别
post_code 贷款人申请时邮政编码
region 地区编码
debt_loan_ratio 债务收入比
del_in_18month 借款人过去18个月逾期30天以上的违约事件数
scoring_low 借款人在贷款评分中所属的下限范围
scoring_high 借款人在贷款评分中所属的上限范围
known_outstanding_loan 借款人档案中未结信用额度的数量
known_dero 贬损公共记录的数量
pub_dero_bankrup 公开记录清除的数量
recircle_bal 信贷周转余额合计
recircle_util 循环额度利用率
initial_list_status 贷款的初始列表状态
app_type 是否个人申请
earlies_credit_mon 借款人最早报告的信用额度开立的月份
title 借款人提供的贷款名称
policy_code 公开可用的策略代码=1新产品不公开可用的策略代码=2
f系列匿名特征 匿名特征f0-f4,为一些贷款人行为计数特征的处理
early_return 借款人提前还款次数
early_return_amount 贷款人提前还款累积金额
early_return_amount_3mon 近3个月内提前还款金额
isDefault 贷款是否违约(预测标签)

首先进行数据预处理。取值唯一、取值各不相同的特征对于预测结果没有意义,因此先将他们删除:

1
2
3
4
5
6
# 将取值唯一、取值各不相同的特征删除
for name in data.columns:
unique = data[name].unique().shape[0]
full = data[name].shape[0]
if (unique == 1) | (unique == full):
data = data.drop(name, axis=1)

查看一下有哪些特征有缺失:

1
2
# 查看缺失值
print(data.isnull().sum(axis=0).sort_values(ascending=False))

用中位数填充空值:

1
2
3
4
5
6
7
8
9
# 使用中位数填充空值
# 匿名特征f0-f4,为一些贷款人行为计数特征的处理
data['f0'].fillna(int(data['f0'].median()), inplace=True)
data['f1'].fillna(int(data['f1'].median()), inplace=True)
data['f2'].fillna(int(data['f2'].median()), inplace=True)
data['f3'].fillna(int(data['f3'].median()), inplace=True)
data['f4'].fillna(int(data['f4'].median()), inplace=True)
data['work_year'].fillna('0', inplace=True)

还有一些与违约没有关系的特征我们手动删除:

1
2
3
4
5
# 删除无关属性
drop_elements=['issue_date', 'earlies_credit_mon', 'title']
for ele in drop_elements:
data.drop(ele, axis=1, inplace=True)
# axis=1:表示操作方向为列(axis=0 表示行)

数据中存在大量连续值的属性,不能直接计算似然,需要将连续属性离散化。我使用 pandas.cut()函数来进行分箱。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
def discrete(data):
mydata = pd.read_csv('input/train.csv')
discreteness = ['total_loan', 'interest', 'region', 'post_code', 'monthly_payment', 'debt_loan_ratio', 'scoring_low', 'scoring_high', 'known_outstanding_loan', 'recircle_b', 'recircle_u', 'f0', 'f2', 'f3', 'f4', 'early_return_amount', 'early_return_amount_3mon']
# 通过describe方法获取分箱的依据,并且对特殊情况进行特殊处理
for columnName in discreteness:
temp = mydata[columnName].describe()
temp1 = data[columnName].describe()
if temp['min'] != temp['25%']:# 相等的话则前25%的数据点都具有相同的值
dis = pd.cut(data[columnName], bins=[temp1['min'], temp['25%'], temp['50%'], temp['75%'], temp1['max'] + 1], right=False, include_lowest=True, labels=[0, 1, 2, 3])
elif temp['25%'] != temp['50%']:# 如果第25百分位数(temp['25%'])不等于中位数(temp['50%']),则说明数据在较低部分有一定的分散性,但中间部分相对集中
dis = pd.cut(data[columnName], bins=[temp1['min'], temp['50%'], temp['75%'], temp1['max'] + 1], right=False, include_lowest=True, labels=[0, 1, 2])
else:
dis = pd.cut(data[columnName], bins=[temp1['min'], temp['75%'], temp1['max'] + 1], right=False, include_lowest=True, labels=[0, 1])

data[columnName] = dis
return

统计数据中各列值的出现次数,并分别保存所有数据和非违约数据(isDefault为0)的统计结果。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
def train(data, alldata, positives):
for column in data.columns:
datamap = {}
# 统计每一列数据中各个值的出现次数,并按索引升序排序
line = data[column].value_counts().sort_index(ascending=True)
for index in line.index:
datamap[index] = line[index]
alldata[column] = datamap
positive_data = data.loc[data['isDefault'] == 0]# isDefault贷款是否违约(预测标签)
for column in positive_data.columns:
datamap = {}
line = positive_data[column].value_counts().sort_index(ascending=True)
for index in line.index:
datamap[index] = line[index]
positives[column] = datamap

重点来了,如何使用贝叶斯公式预测是否违约。

计算基础不违约概率,这里假设所有客户的违约概率相同。

1
probability_0 = alldata['isDefault'][0] / (alldata['isDefault'][0]+alldata['isDefault'][1])

遍历数据集中的每一行数据,计算每一特征对违约概率的影响,注意要避免零概率问题,贝叶斯公式如果乘0就没有意义了。当某个特征值在目标类别中未出现(positive_num=0)时,分子加1保证概率不为零,同时分母对每个可能的特征值都加1次虚拟计数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
def predict(data, alldata, positives):
lista = []
# 计算先验概率:P(y=0) 和 P(y=1)
total_samples = alldata['isDefault'][0] + alldata['isDefault'][1]
p_y0 = alldata['isDefault'][0] / total_samples
p_y1 = alldata['isDefault'][1] / total_samples

# 遍历每个待预测样本
for index in data.index:
likelihood_y0 = 1.0
likelihood_y1 = 1.0

# 计算每个特征的条件概率(应用拉普拉斯平滑)
for column in data.columns:
value = data.loc[index, column]
unique_count = len(alldata[column].keys())

# 计算P(x_i|y=0):特征值在不违约类中的条件概率
positive_num = positives[column].get(value, 0)
prob_y0 = (positive_num + 1) / (alldata['isDefault'][0] + unique_count)
likelihood_y0 *= prob_y0

# 计算P(x_i|y=1):特征值在违约类中的条件概率
global_num = alldata[column].get(value, 0)
negative_num = global_num - positive_num
prob_y1 = (negative_num + 1) / (alldata['isDefault'][1] + unique_count)
likelihood_y1 *= prob_y1

# 计算归一化后的后验概率
numerator_y0 = p_y0 * likelihood_y0
numerator_y1 = p_y1 * likelihood_y1
denominator = numerator_y0 + numerator_y1
# 处理分母为零的特殊情况
posterior_y0 = numerator_y0 / denominator if denominator != 0 else 0.5

# 根据后验概率生成预测结果
lista.append(0 if posterior_y0 > 0.5 else 1)

return lista

说明一下上述代码的贝叶斯公式

  1. 先验概率

$
P(y=0) = \frac{\text{alldata[‘isDefault’][0]}}{\text{alldata[‘isDefault’][0]} + \text{alldata[‘isDefault’][1]}}
$

$P(y=1) = \frac{\text{alldata[‘isDefault’][1]}}{\text{alldata[‘isDefault’][0]} + \text{alldata[‘isDefault’][1]}}
$

  1. 条件概率

对每个特征 $x_i$的取值 (v),其在类别 (y=c) 中的条件概率为:

$P(x_i = v | y=c) = \frac{\text{count}(x_i = v | y=c) + 1}{\text{count}(y=c) + K_i} $

  1. 后验概率

通过贝叶斯定理计算归一化后的后验概率:
$
P(y=0 | x) = \frac{P(y=0) \cdot \prod{i=1}^n P(x_i | y=0)}{P(y=0) \cdot \prod{i=1}^n P(xi | y=0) + P(y=1) \cdot \prod{i=1}^n P(x_i | y=1)}
$

$P(y=1 | x) = 1 - P(y=0 | x)
$