一、项目介绍
数据集来自于某银行信用卡交易数据,显示两天内发生的交易几率,284,807笔交易中发生了492笔盗刷。
二、项目目的
通过信用卡的历史交易数据,利用机器学习算法构建信用卡反欺诈预测模型,提前发现客户信用卡被盗刷事件,并且要求模型准确度在95%以上最大化召回率。
三、项目过程
1. 建模准备
-
模型选择:
该项目主要解决持卡人是否会发生信用卡被盗刷事件,属于二分类问题,可以通过二分类算法找到具体解决办法,例如可以选用逻辑回归模型构建模型(Logistic Regression) -
数据处理:
数据表中,V1,V2,...V28以及amount列属于特征字段,其中v1-v28已做脱敏处理,但amount数据规格与其他特征差别较大,需进行特征缩放;
交易数据有284,807条,其中492笔发生盗刷事件,数据集不平衡需要需要进行欠采样处理。
1.1 特征缩放:归一化处理- 将Amount的值转化为-1,1之间
from sklearn.preprocessing import StandardScaler
data['normAmount'] = StandardScaler().fit_transform(data['Amount'].values.reshape(-1,1))
data = data.drop(['Time','Amount'],axis=1)
1.2 欠采样处理:
这里我们先进行随机欠采样方法,将0样本的数量缩小到1样本相同数量
# 求类别为1的数据的个数
number_records_fraud = len(data[data.Class == 1])
# 类别为1的索引集合
fraud_indices = np.array(data[data.Class == 1].index)
# 类别为0的索引集合
normal_indices = np.array(data[data.Class == 0].index)
# 在类别为0中,取与类别1相同数目的随机数
random_normal_indices = np.random.choice(normal_indices,size=number_records_fraud,replace = False) # 得到的是索引
# 合并1样本和新的0样本索引
under_sample_indices = np.concatenate([fraud_indices,random_normal_indices])
# 取新索引数据集
under_sample_data = data.iloc[under_sample_indices]
如何平衡样本
- 不平衡样本会影响模型的评估效果,严重的会带来过拟合或欠拟合现象。所以我们需要让正负样本在模型训练中拥有相同的数量。在这里,数据集中样本较多的一类成为“大众类”,样本较少的一类称为“小众类”
- 处理方法:
1. 下采样(Undersamping,欠采样):
以小众类的样本数为标准,在大众类样本中取得与小众类的样本数相同数量。
弊端:下采样是选择部分大众类,下采样会由于丢失信息而导致欠拟合,往往还会丢失重要信息。
改进方法1:EasyEnsemble将多数类样本随机划分成n个子集,每个子集的数量等于小众类样本的数量,这相当于欠采样。接着将每个子集与小众类样本结合起来分别训练一个模型,最后将n个模型集成,这样虽然每个子集的样本少于总体样本,但集成后总信息量并不减少。
改进方法2:BalanceCascade则是采用了有监督结合Boosting的方式(Boosting方法是一种用来提高弱分类算法准确度的方法,这种方法通过构造一个预测函数系列,然后以一定的方式将他们组合成一个预测函数)。在第n轮训练中,将从大众类样本中抽样得来的子集与小众类样本结合起来训练一个基学习器H,训练完后大众类中能被H正确分类的样本会被剔除。在接下来的第n+1轮中,从被剔除后的大众类样本中产生子集用于与小众类样本结合起来训练,最后将不同的基学习器H集成起来。
2. 上采样(Oversampling,过采样):
以大众类为标准,生成一些小众类样本,使大、小众类样本数相同。
弊端:上采样是复制多份小众类,后面结果的小众类中会出现一些重复样本,这可能会导致过拟合现象。
改进方法:通过抽样方法在小众类样本中加入白噪声(比如高斯噪声)变成新样本一定程度上可以缓解这个问题。如年龄,原年龄=新年龄+random(0,1)
上采样代表算法:SMOTE 算法
SMOTE是通过对小众样本进行插值来获取新样本的。比如对于每个小众类样本a,从 a最邻近的样本中选取 样本b,然后在对 ab 中随机选择一点作为新样本。
2.创建模型
2.1 数据拆分:训练集、测试集
2.2 交叉验证
2.3 模型评估方法
2.4 正则化惩罚项
2.5 构建模型
- 2.5.1 使用交叉验证,寻找最佳正则化惩罚项
- 2.5.2 使用下采样数据进行训练,下采样数据进行测试
- 2.5.3 使用下采样数据进行训练,原始数据进行测试
- 2.5.4 不同阈值对结果的影响
- 2.5.5 比较:使用原始数据进行训练与测试
2.1 数据拆分
利用sklearn的train_test_split将数据源切分为训练集和测试集。
- 训练集:用于训练模型的数据集
- 测试集:用于对模型结果进行测试的数据集
# 导入切分数据模块(训练集和测试集)
from sklearn.model_selection import train_test_split
# 切分原始数据集
x_train,x_test,y_train,y_test = train_test_split(x_data, y_data, test_size = 0.3, random_state = 0)
# 切分下采样数据集
x_train_undersample, x_test_undersample, y_train_undersample ,y_test_undersample = train_test_split(x_data_undersample ,y_data_undersample, test_size = 0.3, random_state = 0)
# random_state 为随机数种子
原始数据训练集数目: 199364
原始数据测试集数目: 85443
原始数据总数目: 284807
下采样训练集数目: 688
下采样测试集数目: 296
下采样总数目: 984
2.2 交叉验证
交叉验证是在机器学习建立模型和验证模型参数时常用的办法,就是重复的使用数据,把得到的样本数据进行切分,组合为不同的训练集和测试集,用训练集来训练模型,用测试集来评估模型预测的好坏。在此基础上可以得到多组不同的训练集和测试集,某次训练集中的某样本在下次可能成为测试集中的样本,即所谓"交叉"。
- 常见形式:
1. Holdout 验证:
常识来说,Holdout 验证并非一种交叉验证,因为数据并没有交叉使用。 随机从最初的样本中选出部分,形成交叉验证数据,而剩余的就当做训练数据。 一般来说,少于原本样本三分之一的数据被选做验证数据。
2. K-fold cross-validation:
K折交叉验证,初始采样分割成K个子样本,一个单独的子样本被保留作为验证模型的数据,其他K-1个样本用来训练。交叉验证重复K次,每个子样本验证一次,平均K次的结果或者使用其它结合方式,最终得到一个单一估测。这个方法的优势在于,同时重复运用随机产生的子样本进行训练和验证,每次的结果验证一次,10折交叉验证是最常用的
3. 留一验证:
正如名称所建议, 留一验证(LOOCV)意指只使用原本样本中的一项来当做验证资料, 而剩余的则留下来当做训练资料。 这个步骤一直持续到每个样本都被当做一次验证资料。 事实上,这等同于和K-fold 交叉验证是一样的,其中K为原本样本个数。 在某些情况下是存在有效率的演算法,如使用kernel regression 和Tikhonov regularization。
-
当数据量较少时,我们就可以使用K折交叉验证,来训练优化选择模型;而当数据量较多时,将数据分成三份(训练集、验证集、测试集),训练集用来训练模型,验证集用来进行验证判断模型好坏,测试集用来进行最后检测。
2.3 模型评估方法
混淆矩阵:
- 混淆矩阵的每一列代表了预测类别,每一列的总数表示预测为该类别的数据的数目;每一行代表了数据的真实归属类别,每一行的数据总数表示该类别的数据实例的数目,每一列中的数值表示真实数据被预测为该类的数目。
精确率: - 预测为正的结果中,实际为正的占比,精确率代表了模型预测的正确程度。
- pre = TP / (FP + TP)
召回率: - 实际为正中,预测也为正的占比,召回率代表了实际为正的用户被预测出来的概率程度。
- recall = TP / (FN + TP)
F1-score - F1 = 2(精确率*召回率)/(精确率+召回率)
- 验证一个模型的好坏是根据其精确率和召回率指标的高低,但他们是两个互相矛盾的指标,高精确率就意味着低召回率,而F1值是综合上述两个指标的评估指标,用于综合反映整体的指标。
2.4 正则化惩罚项
正则化是结构风险最小化策略的实现,是在经验风险上加上一个正则化项或惩罚项。正则化项一般是负责模型复杂度的单调递增函数,模型越复杂,正则化值就越大。正则化的作用是选择经验风险与模型复杂度同时较小的模型。正则化符合奥卡姆剃刀原理,在所有可能选择的模型中,能够很好地解释数据并且十分简单的才是最好的模型。
- L1正则化中可以对|w|求累加和,但是只直接计算绝对值求累加和的话,[1.0.0,0,0]和[0.25,0.25,0.25,0.25,0.25]的结果是相同的,无法做出区分。
- L2正则化惩罚力度更大,对权重参数求平方和,目的是让大的更大,相对惩罚更多。入系数,表示正则化惩罚力度。à值越大,表示惩罚力度越大,对模型影响越大。如果入值较小,意味着惩罚力度较小,不会对结果产生太大影响。
L1与L2的选择:
- 如果数据集中有很多特征,而这些特征中并不是每一个都对结果有重要影响,那么就应该选择L1正则化的模型。
- 但如果数据集中特征本来就不多,而且每一个都有很重要的作用,那么就应该选择L2正则化的模型。
2.5 模型构建
(1)K折验证法,选择最佳正则化惩罚项:
def printing_Kfold_scores(x_train_data,y_train_data):
# 得到一个可迭代对象,遍历5次,每次出来一个2值列表,存放每次的训练集和测试集(分成5块,返回的时索引值)
fold = KFold(5,shuffle=False)
# 不同正则化惩罚力度参数值
c_param_range = [0.01,0.1,1,10,100]
# 创建不同正则化惩罚力度表
result_table = pd.DataFrame(index = range(len(c_param_range)), columns = ['C_parameter','Mean recall score'])
result_table['C_parameter'] = c_param_range
j = 0
# 每次传入一个惩罚力度,进行交叉验证,输出平均得分到惩罚力度表
for c_param in c_param_range:
print("- - "*20)
print("第{}个参数值:".format(j),c_param)
print("- - "*20)
# 创建列表,接受每次交叉验证效果得分
recall_accs = []
# 根据每次交叉验证,创建模型(即训练不同的训练集)
for iteration, indices in enumerate(fold.split(x_train_data)): # enumerate,返回序号和数据集
# 创建逻辑回归模型,传入训练集
lr = LogisticRegression(C = c_param, penalty = "l1",solver='liblinear')
lr.fit(x_train_data.iloc[indices[0]], y_train_data.iloc[indices[0]].values) #indices[0]是交叉验证的训练集索引,这里是取对应训练集数据
# 进行预测,传入验证集
y_predict_undersample = lr.predict(x_train_data.iloc[indices[1]].values) #indices[1]是交叉验证的测试集索引
# 返回每次模型的recall_score得分
recall_acc = recall_score(y_train_data.iloc[indices[1]].values, y_predict_undersample) #传入测试集已知结果、预测结果
# 将每次交叉验证模型得分加入recall_accs列表
recall_accs.append(recall_acc)
print("第 {} 次遍历,recall得分为:{}".format(iteration,recall_acc))
# 将每次交叉验证模型得分的平均分加入到不同正则化惩罚力度结果表中
print("")
avg_score = np.mean(recall_accs)
print('该惩罚力度平均得分: ', avg_score)
print(" - - - - 进惩罚力度得分表- - - - -")
result_table.iloc[j,1] = avg_score
print(result_table)
print('')
j += 1
# best_c 为不同惩罚力度效果表中,效果最好的惩罚力度
result_table['Mean recall score'] = result_table['Mean recall score'].astype("float64") #修改列的类型,因为被强制改为了object类型
best_c = result_table.iloc[result_table['Mean recall score'].idxmax()]['C_parameter']
# 建模结束
print("模型训练结束","* " * 30)
print("效果最好的正则化惩罚力度为:",best_c)
print("* " * 30)
return best_c
运行结果:
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
第0个正则化参数: 0.01
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
第 0 次遍历,recall得分为:0.9846153846153847
第 1 次遍历,recall得分为:0.9846153846153847
第 2 次遍历,recall得分为:0.9367088607594937
第 3 次遍历,recall得分为:0.9841269841269841
第 4 次遍历,recall得分为:0.9436619718309859
该惩罚力度平均得分: 0.9667457171896465
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
第1个正则化参数: 0.1
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
第 0 次遍历,recall得分为:0.9230769230769231
第 1 次遍历,recall得分为:0.9230769230769231
第 2 次遍历,recall得分为:0.8354430379746836
第 3 次遍历,recall得分为:0.9365079365079365
第 4 次遍历,recall得分为:0.9154929577464789
该惩罚力度平均得分: 0.906719555676589
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
第2个正则化参数: 1
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
第 0 次遍历,recall得分为:0.9384615384615385
第 1 次遍历,recall得分为:0.8769230769230769
第 2 次遍历,recall得分为:0.8607594936708861
第 3 次遍历,recall得分为:0.9365079365079365
第 4 次遍历,recall得分为:0.9436619718309859
该惩罚力度平均得分: 0.9112628034788848
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
第3个正则化参数: 10
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
第 0 次遍历,recall得分为:0.9538461538461539
第 1 次遍历,recall得分为:0.8923076923076924
第 2 次遍历,recall得分为:0.8481012658227848
第 3 次遍历,recall得分为:0.9365079365079365
第 4 次遍历,recall得分为:0.9436619718309859
该惩罚力度平均得分: 0.9148850040631107
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
第4个正则化参数: 100
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
第 0 次遍历,recall得分为:0.9538461538461539
第 1 次遍历,recall得分为:0.8923076923076924
第 2 次遍历,recall得分为:0.8354430379746836
第 3 次遍历,recall得分为:0.9365079365079365
第 4 次遍历,recall得分为:0.9436619718309859
该惩罚力度平均得分: 0.9123533584934904
模型训练结束
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
效果最好的正则化惩罚力度为: 0.01
* * * * * * * * * * * * * * * * * * * * * * * * * * * * * *
- 根据以上运行结果,选择的正则化惩罚项为0.01
(2)使用下采样数据进行训练,下采样数据进行测试
传递正则化惩罚项0.01,使用L1正则化方法,同时绘制出混淆矩阵,计算模型评估指标。
lr = LogisticRegression(C = best_c, penalty = "l1",solver='liblinear')
# 训练模型——传递下采样后的训练集
lr.fit(x_train_undersample, y_train_undersample.values)
# 预测
y_pred_undersample = lr.predict(x_test_undersample)
# 绘制混淆矩阵 传入参数为(真实值、预测值)
cnf_matrix = confusion_matrix(y_test_undersample,y_pred_undersample)
# 混淆矩阵图形
print("混淆矩阵:\n",cnf_matrix,"\n")
# 设置浮点数为小数点后两位
np.set_printoptions(precision=2)
# 输出标签<1>召回率值(被召回的数目/数目总和)
print("标签<1>的召回率为:", str(round(cnf_matrix[1,1] / (cnf_matrix[1,1] + cnf_matrix[1,0]),4)*100) + '%')
print("标签<1>的精确率为:", str(round(cnf_matrix[1,1] / (cnf_matrix[1,1] + cnf_matrix[0,1]),4)*100) + '%')
# plot绘制
class_names = [0, 1] # 分类:0,1类别
plt.figure()
plot_confusion_matrix(cnf_matrix, classes=class_names, title = 'Confusion matrix') #利用plot定义函数绘制
plt.show()
输出结果:
召回率为93.28%,精确率为93.92%
(3)预测原始数据
使用以上训练出的结果,对原始数据(未下采样处理)进行预测
lr = LogisticRegression(C = best_c, penalty = "l1",solver='liblinear')
# 训练模型——依然使用下采样后的数据进行训练
lr.fit(x_train_undersample, y_train_undersample.values)
# 预测模型——使用全体数据预测
y_pred = lr.predict(x_test)
# 创建混淆矩阵
cnf_matrix = confusion_matrix(y_test,y_pred)
print("标签<1>的召回率为:", str(round(cnf_matrix[1,1] / (cnf_matrix[1,1] + cnf_matrix[1,0]),4)*100) + '%')
print("标签<1>的精确率为:", str(round(cnf_matrix[1,1] / (cnf_matrix[1,1] + cnf_matrix[0,1]),4)*100) + '%')
召回率93.2%,精确率1.84%,该模型存在较大的误差率,准确度很低
(4)不同阈值对结果的影响
在逻辑回归中,lr.predict_proba返回的时分类概率,我们可以调整阈值(默认为0.5,即大于0.5为对应的分类),得到不同的精确率和召回率
# 得到预测分类概率
y_pred_undersample_proba = lr.predict_proba(x_test_undersample)
# 阈值列表
thresholds = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9]
plt.figure(figsize=(12,12))
j = 1
for i in thresholds:
#
y_test_predictions_high_recall = y_pred_undersample_proba[:,1] > i
# 创建相应子图
plt.subplot(3,3,j)
j += 1
# 创建混淆矩阵 (预测数据的真实值,预测值)
cnf_matrix = confusion_matrix(y_test_undersample,y_test_predictions_high_recall)
np.set_printoptions(precision=2)
print("当阈值设置为 {} 时:".format(i))
print(" ")
# 正确率
precession = cnf_matrix[1,1] / (cnf_matrix[1,1] + cnf_matrix[1,0])
# 召回率
recall = cnf_matrix[1,1] / (cnf_matrix[1,1] + cnf_matrix[0,1])
# F1值 2*(正确率*召回率)/(正确率+召回率)
f1 = 2*(precession*recall)/(precession+recall)
print("标签<1>的召回率为:", str(round(cnf_matrix[1,1] / (cnf_matrix[1,1] + cnf_matrix[1,0]),4)*100) + '%')
print("标签<1>的精确率为:", str(round(cnf_matrix[1,1] / (cnf_matrix[1,1] + cnf_matrix[0,1]),4)*100) + '%')
print("* * * * * *F1值为:",str(round(f1,4) * 100) + '%')
print("- " * 40)
# 绘制plot混淆矩阵图
class_names = [0,1]
plot_confusion_matrix(cnf_matrix, classes=class_names, title='Threshold >= %s'%i)
结果:
当阈值设置为 0.1 时:
标签<1>的召回率为: 100.0%
标签<1>的精确率为: 50.339999999999996%
* * * * * *F1值为: 66.97%
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
当阈值设置为 0.2 时:
标签<1>的召回率为: 100.0%
标签<1>的精确率为: 50.339999999999996%
* * * * * *F1值为: 66.97%
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
当阈值设置为 0.3 时:
标签<1>的召回率为: 100.0%
标签<1>的精确率为: 50.339999999999996%
* * * * * *F1值为: 66.97%
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
当阈值设置为 0.4 时:
标签<1>的召回率为: 99.33%
标签<1>的精确率为: 60.660000000000004%
* * * * * *F1值为: 75.32%
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
当阈值设置为 0.5 时:
标签<1>的召回率为: 93.28999999999999%
标签<1>的精确率为: 93.92%
* * * * * *F1值为: 93.60000000000001%
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
当阈值设置为 0.6 时:
标签<1>的召回率为: 85.22999999999999%
标签<1>的精确率为: 99.22%
* * * * * *F1值为: 91.7%
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
当阈值设置为 0.7 时:
标签<1>的召回率为: 84.56%
标签<1>的精确率为: 100.0%
* * * * * *F1值为: 91.64%
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
当阈值设置为 0.8 时:
标签<1>的召回率为: 73.83%
标签<1>的精确率为: 100.0%
* * * * * *F1值为: 84.94%
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
当阈值设置为 0.9 时:
标签<1>的召回率为: 51.68000000000001%
标签<1>的精确率为: 100.0%
* * * * * *F1值为: 68.14%
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- 当阈值选择0.6时,精确率为99.22%大于95%,召回率为85.22%为最高,满足条件
(5)比较:使用原始数据训练模型及预测
这里我们使用原始数据对模型进行训练,对比使用下采样的预测结果
best_c = printing_Kfold_scores(x_train,y_train)
- 得到最佳惩罚项为10
预测全体数据:
lr = LogisticRegression(C = best_c, penalty = "l1",solver='liblinear')
# 训练模型——使用原始数据进行训练
lr.fit(x_train, y_train.values)
# 预测模型——使用全体数据预测
y_pred = lr.predict(x_test)
# 创建混淆矩阵
cnf_matrix = confusion_matrix(y_test,y_pred)
print("标签<1>的召回率为:", str(round(cnf_matrix[1,1] / (cnf_matrix[1,1] + cnf_matrix[1,0]),4)*100) + '%')
print("标签<1>的精确率为:", str(round(cnf_matrix[1,1] / (cnf_matrix[1,1] + cnf_matrix[0,1]),4)*100) + '%')
# plot
class_names = [0, 1] # 分类:0,1类别
plt.figure()
plot_confusion_matrix(cnf_matrix, classes=class_names, title = 'Confusion matrix') #利用plot定义函数绘制
plt.show()
召回率为61.9%,精确率为88.35,%,评估指标均不如下采样训练结果,因此对于不平衡数据需要进行平衡处理。
四、总结
- 通过K折交叉验证方法,计算得到最佳正则化惩罚项为0.01。
- 在创建模型过程中,使用下采样方法,根据需求将阈值调整到0.6时,得到的召回率为85.22%,精确率为99.22%,F1值为91.7%,为符合初衷要求的最优模型。
- 通过对比,使用原始数据进行训练模型,召回率仅为61.9%,精确率为88.35%,存在较大的误差率以及召回程度很低,因此使用对于不平衡的数据,我们需要进行下采样或过采样处理。
- 后续可以根据不同的业务需求,调整阈值得到不同的召回率和精确率。