机器学习
1 逻辑回归案例 制作评分卡
1.1 项目介绍
目标:制作金融申请评分卡
特征介绍
特征 | 名称 | 描述 |
---|---|---|
SeriousDlqin2yrs | 好坏客户 | 出现90天及更长时间的逾期行为,用于定义好坏客户。 |
RevolvingUtilizationOfUnsecuredLines | 可用额度比值 | 贷款或信用卡可用额度与总额度的比例。 |
age | 年龄 | 借款人借款时年龄。 |
NumberOfTime30-59DaysPastDueNotWorse | 逾期30-59天笔数 | 过去两年内出现30-59天逾期但没有发展得更坏的次数。 |
DebtRatio | 负债率 | 每月用于偿还债务、赡养费、生活费等费用与月收入的比例。 |
MonthlyIncome | 月收入 | 月收入 |
NumberOfOpenCreditLinesAndLoans | 信贷数量 | 开放式贷款和信贷数量。 |
NumberOfTimes90DaysLate | 逾期90天笔数 | 过去两年内出现90天逾期或者更坏情况的次数。 |
NumberRealEstateLoansOrLines | 固定资产贷款量 | 抵押贷款和房地产贷款数量,包括房屋净值信贷额度。 |
NumberOfTime60-89DaysPastDueNotWorse | 逾期60-89天笔数 | 过去两年内出现60-89天逾期但没有发展得更坏的次数。 |
NumberOfDependents | 家属数量 | 家庭中不包括自身的家属人数,例如子女、配偶等。 |
1.2 数据导入
import numpy as np
import pandas as pd
from pandas import DataFrame, Series
import matplotlib.pyplot as plt
%matplotlib inline
# 定义字典,用于修改列索引的名称。
column_dict = {
'SeriousDlqin2yrs': '好坏客户',
'RevolvingUtilizationOfUnsecuredLines': '可用额度比值',
'age': '年龄',
'NumberOfTime30-59DaysPastDueNotWorse': '逾期30-59天笔数',
'DebtRatio': '负债率',
'MonthlyIncome': '月收入',
'NumberOfOpenCreditLinesAndLoans': '信贷数量',
'NumberOfTimes90DaysLate': '逾期90天笔数',
'NumberRealEstateLoansOrLines': '固定资产贷款量',
'NumberOfTime60-89DaysPastDueNotWorse': '逾期60-89天笔数',
'NumberOfDependents': '家属数量'
}
# 读取并查看数据
# index_col=0表示使用数据的首列作为行索引。
data = pd.read_csv('./rankingcard.csv', index_col=0)
data.head()
'''
SeriousDlqin2yrs RevolvingUtilizationOfUnsecuredLines age NumberOfTime30-59DaysPastDueNotWorse DebtRatio MonthlyIncome NumberOfOpenCreditLinesAndLoans NumberOfTimes90DaysLate NumberRealEstateLoansOrLines NumberOfTime60-89DaysPastDueNotWorse NumberOfDependents
1 1 0.766127 45 2 0.802982 9120.0 13 0 6 0 2.0
2 0 0.957151 40 0 0.121876 2600.0 4 0 0 0 1.0
3 0 0.658180 38 1 0.085113 3042.0 2 1 0 0 0.0
4 0 0.233810 30 0 0.036050 3300.0 5 0 0 0 0.0
5 0 0.907239 49 1 0.024926 63588.0 7 0 1 0 0.0
'''
# 修改列索引名称。
data.rename(columns=column_dict, inplace=True)
data.head()
'''
好坏客户 可用额度比值 年龄 逾期30-59天笔数 负债率 月收入 信贷数量 逾期90天笔数 固定资产贷款量 逾期60-89天笔数 家属数量
1 1 0.766127 45 2 0.802982 9120.0 13 0 6 0 2.0
2 0 0.957151 40 0 0.121876 2600.0 4 0 0 0 1.0
3 0 0.658180 38 1 0.085113 3042.0 2 1 0 0 0.0
4 0 0.233810 30 0 0.036050 3300.0 5 0 0 0 0.0
5 0 0.907239 49 1 0.024926 63588.0 7 0 1 0 0.0
'''
1.3 数据清洗
1.3.1 处理重复的行数据
# 查看重复数据数量
data.duplicated().sum() # 609
# 删除重复数据
data.drop_duplicates(inplace=True)
# 恢复行索引
data.index = range(data.shape[0])
data.duplicated().sum() # 0
data.shape # (149391, 11)
1.3.2 处理缺失数据
检查存在缺失数据的列。
data.isnull().any(axis=0)
'''
好坏客户 False
可用额度比值 False
年龄 False
逾期30-59天笔数 False
负债率 False
月收入 True
信贷数量 False
逾期90天笔数 False
固定资产贷款量 False
逾期60-89天笔数 False
家属数量 True
'''
data.isnull().sum()
'''
好坏客户 0
可用额度比值 0
年龄 0
逾期30-59天笔数 0
负债率 0
月收入 29221
信贷数量 0
逾期90天笔数 0
固定资产贷款量 0
逾期60-89天笔数 0
家属数量 3828
'''
“家属数量”列缺失数据比较少,直接删除,
“月收入”列存在的缺失数据比较多,使用均值填充。
data = data.loc[data['家属数量'].notnull()]
data.fillna({'月收入': data['月收入'].mean()}, inplace=True)
# 恢复行索引
data.index = range(data.shape[0])
data.isnull().sum()
'''
好坏客户 0
可用额度比值 0
年龄 0
逾期30-59天笔数 0
负债率 0
月收入 0
信贷数量 0
逾期90天笔数 0
固定资产贷款量 0
逾期60-89天笔数 0
家属数量 0
'''
1.3.3 处理异常数据
需要排除银行数据中不符合常识的数据,例如收入不能为负数。
data.describe()
data.describe([0.1, 0.25, 0.5, 0.75, 0.9, 0.99])
年龄必须大于0。
(data['年龄'] == 0).sum() # 1
data = data.loc[data['年龄'] != 0]
特征名称 | 描述 | 最大值 |
---|---|---|
逾期30-59天笔数 | 过去两年内出现30-59天逾期但没有发展得更坏的次数。 | 365 * 2 / 30 ~ 24 |
逾期60-89天笔数 | 过去两年内出现60-89天逾期但没有发展得更坏的次数。 | 365 * 2 / 60 ~ 12 |
逾期90天笔数 | 过去两年内出现90天逾期或者更坏情况的次数。 | 365 * 2 / 90 ~ 8 |
data = data.loc[data['逾期30-59天笔数'] < 24]
data = data.loc[data['逾期60-89天笔数'] < 12]
data = data.loc[data['逾期90天笔数'] < 8]
# 恢复行索引
data.index = range(data.shape[0])
# data.shape # (145290, 11)
1.4 特征工程
1.4.1 查看标签(好坏客户)的分布情况
data['好坏客户'].value_counts() / data['好坏客户'].value_counts().sum()
'''
0 0.933485
1 0.066515
'''
数据标签(好坏客户)的分布不均匀。
1.4.2 特征选择
1.4.2.1 单变量分析
单变量分析是分析每个自变量与因变量之间的关系,一般用于处理样本特征数量不多且能够理解每个特征的意义的情况。
此处以年龄(自变量)和好坏客户(因变量)为例进行分析。
对年龄进行分箱操作,分成5组。
age_cut = pd.cut(data['年龄'], bins=5)
查看不同年龄段的用户数量。
# 方式1
age_cut.value_counts()
'''
(38.2, 55.4] 57988
(55.4, 72.6] 46130
(20.914, 38.2] 27996
(72.6, 89.8] 12623
(89.8, 107.0] 553
'''
# 方式2 对Series进行分组处理。
data['好坏客户'].groupby(by=age_cut).count()
'''
(38.2, 55.4] 57988
(55.4, 72.6] 46130
(20.914, 38.2] 27996
(72.6, 89.8] 12623
(89.8, 107.0] 553
'''
获取各个年龄段分组的用户数量。
total_user_age = data['好坏客户'].groupby(by=age_cut).count()
获取各个分组中坏用户的数量。
0表示好用户,1表示坏用户,因此可以直接求和来获取坏用户数量。
bad_user_age = data['好坏客户'].groupby(by=age_cut).sum()
连接操作
age_cut_group = pd.concat((total_user_age, bad_user_age), axis=1)
age_cut_group.columns = ['总客户数量', '坏客户数量']
'''
总客户数量 坏客户数量
年龄
(20.914, 38.2] 27996 2933
(38.2, 55.4] 57988 4593
(55.4, 72.6] 46130 1841
(72.6, 89.8] 12623 285
(89.8, 107.0] 553 12
'''
填加表示好客户数量的列和坏客户占比的列。
age_cut_group['好客户数量'] = age_cut_group['总客户数量'] - age_cut_group['坏客户数量']
age_cut_group['坏客户数量占比'] = age_cut_group['坏客户数量'] / age_cut_group['总客户数量']
'''
总客户数量 坏客户数量 好客户数量 坏客户数量占比
年龄
(20.914, 38.2] 27996 2933 25063 0.104765
(38.2, 55.4] 57988 4593 53395 0.079206
(55.4, 72.6] 46130 1841 44289 0.039909
(72.6, 89.8] 12623 285 12338 0.022578
(89.8, 107.0] 553 12 541 0.021700
'''
from pylab import mpl
mpl.rcParams['font.sans-serif'] = ['FangSong']
mpl.rcParams['axes.unicode_minus'] = False
ax1 = age_cut_group[['好客户数量', '坏客户数量']].plot.bar(figsize=(10,5))
ax1.set_xticklabels(age_cut_group.index, rotation=15)
ax1.set_ylabel('客户数量')
ax2 = age_cut_group['坏客户数量占比'].plot(figsize=(10, 5))
ax2.set_xticklabels([0, 20, 29, 38, 47, 55, 64, 72, 81, 89, 98, 107])
ax2.set_ylabel("坏客户率")
ax2.set_title("坏客户率随年龄的变化趋势图")
1.4.2.2 IV&WOE编码
封装计算WOE的函数get_woe_data,参数cut表示每个分组的数据。
这里将坏用户设置为正例样本,好用户设置为反例样本。
def get_woe_data(cut):
gb = data['好坏客户'].value_counts() # 整个样本中正反例(好坏客户)样本数量
gi = pd.crosstab(cut, data['好坏客户']) # 每组中正反例(好坏客户)样本数量
gbi = (gi[1] / gi[0]) / (gb[1] / gb[0])
woe = np.log(gbi)
return woe
封装计算IV的函数get_iv_data,参数cut表示每个分组的数据。
def get_iv_data(cut):
gb = data['好坏客户'].value_counts() # 整个样本中好坏客户样本数量
gi = pd.crosstab(cut, data['好坏客户']) # 每组中正反例样本数量
gbi = (gi[1] / gi[0]) / (gb[1] / gb[0])
woe = np.log(gbi)
# iv = (py - pn) * woe 中的py表示分组中正例样本数量与整个样本集中正例样本数量的比值。
iv = ((gi[1] / gb[1]) - (gi[0] / gb[0])) * woe
对比使用cut和qcut分箱
- 使用cut
cut1_temp = pd.cut(data["可用额度比值"], 4)
pd.crosstab(cut1_temp, data['好坏客户'])
'''
好坏客户 0 1
可用额度比值
(-50.708, 12677.0] 135616 9664
(12677.0, 25354.0] 8 0
(25354.0, 38031.0] 1 0
(38031.0, 50708.0] 1 0
'''
get_woe_data(cut1_temp)
'''
(-50.708, 12677.0] 0.000074
(12677.0, 25354.0] -inf
(25354.0, 38031.0] -inf
(38031.0, 50708.0] -inf
'''
- 使用qcut
cut1 = pd.qcut(data["可用额度比值"], 4)
pd.crosstab(cut1, data['好坏客户'])
'''
好坏客户 0 1
可用额度比值
(-0.001, 0.0311] 35645 678
(0.0311, 0.158] 35575 747
(0.158, 0.558] 34493 1829
(0.558, 50708.0] 29913 6410
'''
get_woe_data(cut1)
'''
(-0.001, 0.0311] -1.320723
(0.0311, 0.158] -1.221840
(0.158, 0.558] -0.295494
(0.558, 50708.0] 1.101060
'''
因此,对于特征可用额度比值,使用qcut函数进行分箱操作。
计算IV
get_iv_data(cut1)
'''
可用额度比值
(-0.001, 0.0311] 0.254452
(0.0311, 0.158] 0.226047
(0.158, 0.558] 0.019226
(0.558, 50708.0] 0.487474
'''
获取最优的分箱数
分箱数量的不同会导致特征IV值各不相同,因此可以使用学习曲线寻找特征最优时的分箱数。
这里以年龄特征为例,取分箱个数范围为5-15进行测试。
ivs = []
bins = []
for i in range(5, 15):
cut = pd.cut(data['年龄'], bins=i)
iv_value = get_iv_data(cut).sum()
ivs.append(iv_value)
bins.append(i)
plt.plot(bins,ivs)
特征选择
分箱操作
cut1 = pd.qcut(data["可用额度比值"], 4) # 使用qcut分箱
cut2 = pd.cut(data["年龄"], 8)
bins3 = [-1, 0, 1, 3, 5, 13]
cut3 = pd.cut(data["逾期30-59天笔数"], bins3)
cut4 = pd.qcut(data["负债率"], 3)
cut5 = pd.qcut(data["月收入"], 4)
cut6 = pd.cut(data["信贷数量"], 4)
bins7 = [-1, 0, 1, 3, 5, 20]
cut7 = pd.cut(data["逾期90天笔数"], bins7)
bins8 = [-1, 0, 1, 2, 3, 33]
cut8 = pd.cut(data["固定资产贷款量"], bins8)
bins9 = [-1, 0, 1, 3, 12]
cut9 = pd.cut(data["逾期60-89天笔数"], bins9)
bins10 = [-1, 0, 1, 2, 3, 5, 21]
cut10 = pd.cut(data["家属数量"], bins10)
计算出每个特征对应的IV值。
cut1_iv = get_iv_data(cut1).sum()
cut2_iv = get_iv_data(cut2).sum()
cut3_iv = get_iv_data(cut3).sum()
cut4_iv = get_iv_data(cut4).sum()
cut5_iv = get_iv_data(cut5).sum()
cut6_iv = get_iv_data(cut6).sum()
cut7_iv = get_iv_data(cut7).sum()
cut8_iv = get_iv_data(cut8).sum()
cut9_iv = get_iv_data(cut9).sum()
cut10_iv = get_iv_data(cut10).sum()
from pylab import mpl
mpl.rcParams['font.sans-serif'] = ['FangSong']
mpl.rcParams['axes.unicode_minus'] = False
IV = pd.DataFrame([cut1_iv, cut2_iv, cut3_iv, cut4_iv, cut5_iv, cut6_iv, cut7_iv, cut8_iv, cut9_iv, cut10_iv],
index=['可用额度比值', '年龄', '逾期30-59天笔数', '负债率', '月收入', '信贷数量', '逾期90天笔数', '固定资产贷款量', '逾期60-89天笔数',
'家属数量'], columns=['IV'])
iv = IV.plot.bar(color='b', alpha=0.3, rot=30, figsize=(10, 5), fontsize=(10))
iv.set_title('特征变量与IV值分布图', fontsize=(15))
iv.set_xlabel('特征变量', fontsize=(15))
iv.set_ylabel('IV', fontsize=(15))
使用IV值替换原始特征数据。
def map_op(cut):
# 获取每一列特征分箱后的映射关系表
dic = get_iv_data(cut).to_dict()
# 进行映射操作,将原始数据替换成特征的IV值
return cut.values.map(dic)
cuts = [cut1, cut2, cut3, cut4, cut5, cut6, cut7, cut8, cut9, cut10]
cuts_iv_list = []
for cut in cuts:
cuts_iv_list.append(map_op(cut))
data_arr = np.array(cuts_iv_list)
new_df = pd.DataFrame(data=data_arr).T
new_df.columns = ['可用额度比值', '年龄', '逾期30-59天笔数', '负债率', '月收入', '信贷数量', '逾期90天笔数', '固定资产贷款量', '逾期60-89天笔数', '家属数量']
new_df['好坏客户'] = data['好坏客户']
空值存在的原因:
分箱所设置的箱子范围中可能不包含所有的特征数据,做映射时所有分组范围外的数据不能匹配到任何分组的IV值,因此为空值。
new_df.isnull().sum()
'''
可用额度比值 0
年龄 0
逾期30-59天笔数 0
负债率 0
月收入 0
信贷数量 0
逾期90天笔数 0
固定资产贷款量 1
逾期60-89天笔数 0
家属数量 0
好坏客户 0
'''
new_df.dropna(axis=0, inplace=True)
1.5 构建逻辑回归模型
属于二分类问题
from sklearn.model_selection import train_test_split
from sklearn.metrics import roc_auc_score, f1_score
from sklearn.linear_model import LogisticRegression
feature = new_df.iloc[:, :-1] # 取前十列数据,为特征。
target = new_df.iloc[:, -1] # 取最后一列数据,为标签。
x_train, x_test, y_train, y_test = train_test_split(feature, target, test_size=0.1, random_state=0)
l = LogisticRegression().fit(x_train, y_train)
y_pred = l.predict(x_test)
f1_score(y_test, y_pred, average='micro') # 0.9364718838185697
# 二分类问题需要使用ROC评估模型。
y_score = l.predict_proba(x_test)[:, 1] # 正例样本(坏用户)的分类概率
roc_auc_score(y_test, y_score) # 0.7756002482215016
# 评分:每个用户分到反例样本(好用户)的概率 * 100
l.predict_proba(x_test)[:, 0] * 100
2 无监督学习与聚类算法
2.1 无监督学习
有监督学习的模型算法使用的样本数据既需要特征数据,也需要标签数据。
有监督学习的模型算法只需要使用特征数据。
2.2 聚类算法
2.2.1 介绍
聚类算法主要工作是将数据划分成有意义或有用的组(或簇),这种划分可以基于我们的业务需求或建模需求来完成,也可以单纯地帮助我们探索数据的自然结构和分布。
2.2.2 聚类和分类区别
3 KMeans算法
3.1 介绍
3.1.1 簇与质心
簇:KMeans算法将一组N个样本的特征矩阵X划分为K个无交集的簇,直观上来看是簇是一个又一个聚集在一起的数据,在一个簇中的数据就认为是同一类,簇就是聚类的结果表现。
质心:簇中所有数据的均值u通常被称为这个簇的“质心”(centroids)。
在一个二维平面中,一簇数据点的质心的横坐标就是这一簇数据点的横坐标的均值,质心的纵坐标就是这一簇数据点的纵坐标的均值。同理可推广至高维空间。
质心的个数与聚类后的类别数是一致的。
3.1.2 KMeans算法原理
在KMeans算法中,簇的个数K是一个超参数,需要我们人为输入来确定。
KMeans的核心任务是根据我们设定好的K,找出K个最优的质心,并将离这些质心最近的数据分别分配到这些质心代表的簇中去。具体过程可以总结如下:
在每次迭代中被分配到这个质心上的样本都是一致的,即每次新生成的簇都是一致的,所有的样本点都不会再从一个簇转移到另一个簇,质心就不会变化了。
这个过程在可以由下图来显示,我们规定,将数据分为4簇(K=4),其中白色X代表质心的位置:
3.1.3 分类结果
聚类算法聚出的类有什么含义呢?这些类有什么样的性质?
我们认为,被分在同一个簇中的数据是有相似性的,而不同簇中的数据是不同的,当聚类完毕后,我们就要分别去研究每个簇中的样本都有什么样的性质,从而根据业务需求制定不同的商业或者科技策略。
聚类算法追求**“簇内差异 小,簇外差异 大”**,而这个“差异“,由样本点到其所在簇的质心的距离来衡量。
对于一个簇来说,所有样本点到质心的距离之和越小,我们就认为这个簇中的样本越相似,簇内差异就越小。而距离的衡量方法有多种,令x表示簇中的一个样本点,u表示该簇中的质心,n表示每个样本点中的特征数目,i表示组成点的每个特征,则该样本点到质心的距离可以由以下距离来度量:
3.2 损失函数
3.2.1 簇内平方和
如我们采用欧几里得距离,则一个簇中所有样本点到质心的距离的平方和为簇内平方和,使用簇内平方和就可以表示簇内差异的大小。
3.2.2 整体平方和
将一个数据集中的所有簇的簇内平方和相加,就得到了整体平方和(Total Cluster Sum of Square),又叫做total inertia。整体平方和越小,代表着每个簇内样本越相似,聚类的效果就越好。
KMeans追求的是,求解能够让簇内平方和最小化的质心。实际上,在质心不断变化不断迭代的过程中,整体平方和是越来越小的。当整体平方和最小的时候,质心就不再发生变化了。如此,K-Means的求解过程,就变成了一个最优化问题。
因此我们认为:
在KMeans中,我们在一个固定的簇数K下,通过最小化整体平方和来求解最佳质心,并基于质心的存在去进行聚类。并且,整体距离平方和的最小值其实可以使用梯度下降来求解。因此,有许多博客和教材都这样写道:簇内平方和/整体平方和是KMeans的损失函数。
但是也有人认为:
损失函数本质是用来衡量模型的拟合效果的(损失越小,模型的拟合效果越好),只有有着求解参数需求的算法,才会有损失函数。Kmeans不求解什么参数,它的模型本质也没有在拟合数据,而是在对数据进行一种探索。所以如果你去问大多数数据挖掘工程师,甚至是算法工程师,他们可能会告诉你说,K-Means不存在 什么损失函数,整体平方和更像是Kmeans的模型评估指标,而非损失函数。
3.3 代码实现
class sklearn.cluster.KMeans (n_clusters=8, init=’k-means++’, n_init=10, max_iter=300, tol=0.0001, precompute_distances=’auto’, verbose=0, random_state=None, copy_x=True, n_jobs=None, algorithm=’auto’)
n_clusters
n_clusters是KMeans中的k,表示着我们告诉模型我们要分几类。这是KMeans当中唯一一个必填的参数,默认为8 类,但通常我们的聚类结果会是一个小于8的结果。通常,在开始聚类之前,我们并不知道n_clusters究竟是多少, 因此我们要对它进行探索。
random_state
用于初始化质心的生成器。
KMeans的首次探索
当我们拿到一个数据集,如果可能的话,我们希望能够通过绘图先观察一下这个数据集的数据分布,以此来为我们聚类时输入的n_clusters做一个参考。 首先,我们来自己创建一个数据集使用make_blobs。这样的数据集是我们自己创建,所以是有标签的。
from sklearn.datasets import make_blobs
import matplotlib.pyplot as plt
X, y = make_blobs(n_samples=500, n_features=2, centers=4, random_state=10)
X.shape # (500, 2)
plt.scatter
# 将原始已经有类别的样本数据绘制在散点图中,每一个类别使用不同颜色来表示
color = ['red', 'pink', 'orange', 'gray']
fig, ax1 = plt.subplots(1)
for i in range(4):
ax1.scatter(X[y == i, 0], X[y == i, 1], c=color[i], s=8)
plt.show()
聚类效果
from sklearn.cluster import KMeans
n_clusters = 4
cluster = KMeans(n_clusters=n_clusters)
cluster.fit(X)
KMeans(n_clusters=4)
基于这个分布,我们来使用Kmeans进行聚类。首先,我们要假设一下,这个数据中有几簇。
# 重要属性Labels_,查看聚好的类别,每个样本所对应的类
y_pred = cluster.labels_
y_pred
# 重要属性cLuster_centers_,查看质心
centroid = cluster.cluster_centers_
centroid
# 重要属性inertia_,查看总距离平方和
inertia = cluster.inertia_
inertia
# 画出聚类结果
color = ['red', 'pink', 'orange', 'gray']
fig, ax1 = plt.subplots(1)
for i in range(n_clusters):
ax1.scatter(X[y_pred == i, 0], X[y_pred == i, 1], c=color[i], s=8)
plt.show()