本项目通过利用信用卡的历史交易数据,进行机器学习,构建信用卡反欺诈预测模型,提前发现客户信用卡被盗刷的事件。
数据集包含银行客户使用信用卡进行交易的数据。此数据集显示某年9月份两天内发生的交易,其中284,807笔交易中有492笔被盗刷。数据集非常不平衡, 积极的类(被盗刷)占所有交易的0.172%。
它只包含作为PCA转换结果的数字输入变量。不幸的是,由于保密问题,我们无法提供有关数据的原始功能和更多背景信息。特征V1,V2,… V28是使用PCA 获得的主要组件,没有用PCA转换的唯一特征是“时间”和“量”。特征’时间’包含数据集中每个事务和第一个事务之间经过的秒数。特征“金额”是交易金额,此特 征可用于实例依赖的成本认知学习。特征’类’是响应变量,如果发生被盗刷,则取值1,否则为0。 以上取自Kaggle官网对本数据集部分介绍(谷歌翻译),关于数据集更多介绍请参考《Credit Card Fraud Detection》。
首先,我们拿到的数据是持卡人两天内的信用卡交易数据,这份数据包含很多维度,要解决的问题是预测持卡人是否会发生信用卡被盗刷。信用卡持卡人是否会发生被盗刷只有两种可能,发生被盗刷或不发生被盗刷。又因为这份数据是打标好的(字段Class是目标列),也就是说它是一个监督学习的场景。于是,我们判定信用卡持卡人是否会发生被盗刷是一个二元分类问题,意味着可以通过二分类相关的算法来找到具体的解决办法,本项目选用的算法是逻辑斯蒂回归(Logistic Regression)。
数据是结构化数据 ,不需要做特征抽象。特征V1至V28是经过PCA处理,而特征Time和Amount的数据规格与其他特征差别较大,需要对其做特征缩放,将特征缩放至同一个规格。在数据质量方面 ,没有出现乱码或空字符的数据,可以确定字段Class为目标列,其他列为特征列。
这份数据是全部打标好的数据,可以通过交叉验证的方法对训练集生成的模型进行评估。70%的数据进行训练,30%的数据进行预测和评估。 &emsp&emsp现对该业务场景进行总结如下:
根据历史记录数据学习并对信用卡持卡人是否会发生被盗刷进行预测,二分类监督学习场景,选择逻辑斯蒂回归(Logistic Regression)算法。
数据为结构化数据,不需要做特征抽象,但需要做特征缩放。
导包
import numpy as np
import pandas as pd
from pandas import Series,DataFrame
import matplotlib.pyplot as plt
%matplotlib inline
credit = pd.read_csv("./creditcard.csv")
credit.shape
credit.head()
从上面可以看出,此数据有28万行,31列,数据为结构化数据,不需要抽特征转化,但特征Time和Amount的数据规格和其他特征不一样,需要对其做特征做特征缩放
#查看数据是否有缺失值
credit.isnull().any()
#产看数据类型
credit.info()
由显示结果可以看出,数据类型只有float64和int64,且无缺失值,方便后续处理
c_counts = credit["Class"].value_counts()
c_counts
#绘制图形:饼图和柱形图
plt.figure(figsize=(8,8))
ax = plt.subplot(1,2,1)
c_counts.plot(kind = "pie",autopct = "%0.3f%%",ax = ax)
ax = plt.subplot(1,2,2)
c_counts.plot(kind = "bar",ax = ax)
通过上面的图和数据可知,存在492例盗刷,占总样本的0.17%,由此可知,这是一个明显的数据类别不平衡问题,稍后我们采用过采样(增加数据)的方法对这种问题进行处理。
>>> credit["Time"].max()
172792.0
>>> credit["Time"].min()
0.0
>>> divmod(7200, 3600)[0]
2
>>> credit["Time"] = credit["Time"].map(lambda x: divmod(x, 3600)[0])
>>> credit["Time"].max()
47.0
#31个特征, 非常重要的
credit["V1"]
#28万多
cond0 = credit["Class"] == 0
#492
cond1 = credit["Class"] == 1
credit["V1"][cond0].plot(kind = "hist", bins = 500, normed = True)
credit["V1"][cond1].plot(kind = "hist", bins = 50, normed = True)
#目测,数据分的越开,就证明分类效果越好
credit.columns
cols = ['V1', 'V2', 'V3', 'V4', 'V5', 'V6', 'V7', 'V8', 'V9', 'V10',
'V11', 'V12', 'V13', 'V14', 'V15', 'V16', 'V17', 'V18', 'V19', 'V20',
'V21', 'V22', 'V23', 'V24', 'V25', 'V26', 'V27', 'V28']
cond_0 = credit["Class"] == 0
cond_1 =credit["Class"] == 1
plt.figure(figsize=(12, 28*6))
for i, col in enumerate(cols):
ax = plt.subplot(28, 1, i+1)
credit[col][cond_0].plot(kind = "hist", bins = 500,normed = True,ax= ax)
credit[col][cond_1].plot(kind = "hist", bins = 50,normed = True,ax= ax)
ax.set_title(col)
上图是不同变量在信用卡被盗刷和信用卡正常的不同分布情况,我们将选择在不同信用卡状态下的分布有明显区别的变量。因此剔除变量V8、V13 、V15 、V20 、V21 、V22、 V23 、V24 、V25 、V26 、V27 和V28变量。
>>> credit.shape
(284807, 31)
>>> drops = ["V8","V13","V15",'V20',
... 'V21', 'V22', 'V23', 'V24', 'V25', 'V26', 'V27', 'V28']
>>> credit2 = credit.drop(labels=drops, axis = 1)
>>> credit2.shape
(284807, 19)
Amount变量和Time变量的取值范围与其他变量相差较大,所以要对其进行特征缩放
sklearn.preprocessing.StandarScaler
credit2.head()
from sklearn.preprocessing import StandardScaler
credit2["Amount"].min()
credit2["Amount"].max()
#标准化
standarscaler = StandardScaler()
#对Time,Amount 进行数据的预处理
cols = ["Time", "Amount"]
credit2[cols] = standarscaler.fit_transform(credit2[cols])
credit2["Amount"].max()
credit2["Time"].max()
对特征的重要性进行排序,以进一步减少变量
利用GBDT梯度提升决策树进行特征重要性排序(GradientBoostingClassifier,这个是一个算法,和决策树, 逻辑斯蒂回归是一样的)
注意:gbdt和随机森林区别
导包
from sklearn.ensemble import GradientBoostingClassifier
#GradientBoostingClassifier,这个是一个算法, 和决策树, 逻辑斯蒂回归是一样的,
#只不过在这个地方使用这个算法进行特征重要性的排序而已
#https://blog.csdn.net/w28971023/article/details/8240756
clf = GradientBoostingClassifier()
X_train = credit2.iloc[:,:-1]
y_train = credit2["Class"]
clf.fit(X_train,y_train)
#这个值就是属性重要性的量化
feature_importance = clf.feature_importances_
feature_importance
cols = X_train.columns
cols
#从大到小进行排序,就可以看出来那个属性重要了
index = feature_importance.argsort()[::-1]
index
len(index)
plt.figure(figsize=(12,9))
plt.bar(np.arange(len(index)), feature_importance[index])
_ = plt.xticks(np.arange(len(index)), cols[index])
drops = ["V5","V8","V19","V1","Amount"]
credit3 = credit2.drop(labels = drops, axis = 1)
credit3.shape
目标变量“Class”正常和被盗刷两种类别的数量差别较大,会对模型学习造成困扰。举例来说,假如有100个样本,其中只有1个是被盗刷样本,其余99个全为正常样本,那么学习器只要制定一个简单的方法:即判别所有样本均为正常样本,就能轻松达到99%的准确率。而这个分类器的决策对我们的风险控制毫无意义。因此,在将数据代入模型训练之前,我们必须先解决样本不平衡的问题。 现对该业务场景进行总结如下:
过采样(oversampling),增加正样本使得正、负样本数目接近,然后再进行学习。
欠采样(undersampling),去除一些负样本使得正、负样本数目接近,然后再进行学习。 本次处理样本不平衡采用的方法是过采样,具体操作使用SMOTE(Synthetic Minority Oversampling Technique),SMOET的基本原理是:采样最邻近算法,计算出每个少数类样本的K个近邻,从K个近邻中随机挑选N个样本进行随机线性插值,构造新的少数样本,同时将新样本与原数据合成,产生新的训练集。更详细说明参考CMU关于SMOTE: Synthetic Minority Over-sampling Technique的介绍。
from sklearn.model_selection import train_test_split
from imblearn.over_sampling import SMOTE
#imblearn 这个包需要下载, pip install imblearn 就可以了
X = credit3.iloc[:,:-1]
y = credit3['Class']
X_train, X_test, y_train,y_test = train_test_split(X, y, test_size = 0.3)
#咱们要对X_train进行过采样, 因为要训练 X_train
#X_test
y_train.value_counts()
smote = SMOTE()
X_train_new, y_train_new = smote.fit_sample(X_train, y_train)
Series(y_train_new).value_counts()
#到目前为止,样本均衡了
import itertools
#画图的方法,用这个图画真实值和预测值的对比情况, 交叉矩阵
def plot_confusion_matrix(cm, classes,
title='Confusion matrix',
cmap=plt.cm.Blues):
"""
This function prints and plots the confusion matrix.
"""
plt.imshow(cm, interpolation='nearest', cmap=cmap)
plt.title(title)
plt.colorbar()
tick_marks = np.arange(len(classes))
plt.xticks(tick_marks, classes, rotation=0)
plt.yticks(tick_marks, classes)
threshold = cm.max() / 2.
for i, j in itertools.product(range(cm.shape[0]), range(cm.shape[1])):
plt.text(j, i, cm[i, j],
horizontalalignment="center",
color="white" if cm[i, j] > threshold else "black")
#若对应格子上面的数量不超过阈值则,上面的字体为白色,为了方便查看
plt.tight_layout()
plt.ylabel('True label')
plt.xlabel('Predicted label')
注意:查全率也叫召回率,查准率
from sklearn.linear_model import LogisticRegression
logic = LogisticRegression()
logic.fit(X_train_new, y_train_new)
y_ = logic.predict(X_test)
logic.score(X_test, y_test)
#交叉表
pd.crosstab(y_test, y_)
#本来是0 (没被盗刷的)的 预测成0 83246 本来是0 预测成1了 2053个 误判
#本来是1 ()预测成了1 131 本来是1 的预测成了0(没被盗刷) 13个 漏判
#混合矩阵(又称作交叉矩阵)
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, y_)
cm
cm[1,1]
#混合矩阵可以画一个图
#Recall:正确被检索到的正样本占所有应该被检索到的值的比例
plot_confusion_matrix(cm, [0,1], title= "Recall:%0.3f"%(cm[1,1]/ (cm[1,0]+ cm[1,1])))
利用GridSearchCV进行交叉验证和模型参数自动调优
from sklearn.model_selection import GridSearchCV
logic = LogisticRegression()
clf = GridSearchCV(logic, param_grid={"C":[1,0.1,10,100], "tol":[1e-3, 1e-4,1e-5]})
clf.fit(X_train_new, y_train_new)
clf.best_params_
clf.best_score_
y2_ = clf.predict(X_test)
confusion_matrix(y_test, y2_)
y3_ = clf.best_estimator_.predict(X_test)
confusion_matrix(y_test, y3_)
cm2 = confusion_matrix(y_test, y3_)
对比逻辑斯蒂回归和GridSearchCV结果
#目前是一样 Recall是一样的
plot_confusion_matrix(cm, [0,1], "recall:%0.3f"%(cm[1,1]/(cm[1,0]+cm[1,1])))
通过调优之后发现查准率降低了, 所以针对于这个就不需要调优了,证明咱们的原模型已经很优秀了
解决不同的问题,通常需要不同的指标来度量模型的性能。例如我们希望用算法来预测癌症是否是恶性的,假设100个病人中有5个病人的癌症是恶性, 对于医生来说,尽可能提高模型的**查全率(recall)比提高查准率(precision)**更为重要,因为站在病人的角度,发生漏发现癌症为恶性比发生误 判为癌症是恶性更为严重。
由此可见就上面的两个算法而言,明显lgb过拟合了,考虑到样本不均衡问题,故应该选用简单一点的算法(逻辑回归)来减少陷入过拟合的陷阱。
#预测每一行数据属于哪一类的概率
y_proba = clf.predict_proba(X_test)
y_proba
threshholds = [0.05, 0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]
#每一个阈值会对应一个查全率和查准率
for threshhold in threshholds:
y_ = y_proba[:, 1] > threshhold
print(confusion_matrix(y_test, y_))
由上图所见,随着阈值逐渐变大,Recall rate逐渐变小,Precision rate逐渐变大,AUC值先增后减
ROC曲线:
https://blog.csdn.net/shengyingpo/article/details/65633360
precision和recall是一组矛盾的变量。从上面混淆矩阵和PRC曲线可以看到,阈值越小,recall值越大,模型能找出信用卡被盗刷的数量也就更多,但换来的代价是误判的数量也较大。随着阈值的提高,recall值逐渐降低,precision值也逐渐提高,误判的数量也随之减少。通过调整模型阈值,控制模型反信用卡欺诈的力度,若想找出更多的信用卡被盗刷就设置较小的阈值,反之,则设置较大的阈值。 实际业务中,阈值的选择取决于公司业务边际利润和边际成本的比较;当模型阈值设置较小的值,确实能找出更多的信用卡被盗刷的持卡人,但随着误判数量增加,不仅加大了贷后团队的工作量,也会降低正常情况误判为信用卡被盗刷客户的消费体验,从而导致客户满意度下降,如果某个模型阈值能让业务的边际利润和边际成本达到平衡时,则该模型的阈值为最优值。当然也有例外的情况,发生金融危机,往往伴随着贷款违约或信用卡被盗刷的几率会增大,而金融机构会更愿意设置小阈值,不惜一切代价守住风险的底线。
附:机器学习参考文献
《统计学习方法》 李航
《机器学习》 周志华