金融反欺诈项目

构建信用卡反欺诈预测模型

本项目需解决的问题

本项目通过利用信用卡的历史交易数据,进行机器学习,构建信用卡反欺诈预测模型,提前发现客户信用卡被盗刷的事件。

1.项目背景

数据集包含由欧洲持卡人于2013年9月使用信用卡进行交易的数据。此数据集显示两天内发生的交易,其中284,807笔交易中有492笔被盗刷。数据集非常不平衡,
积极的类(被盗刷)占所有交易的0.172%。

它只包含作为PCA转换结果的数字输入变量。不幸的是,由于保密问题,我们无法提供有关数据的原始功能和更多背景信息。特征V1,V2,... V28是使用PCA
获得的主要组件,没有用PCA转换的唯一特征是“时间”和“量”。特征'时间'包含数据集中每个事务和第一个事务之间经过的秒数。特征“金额”是交易金额,此特
征可用于实例依赖的成本认知学习。特征'类'是响应变量,如果发生被盗刷,则取值1,否则为0。
以上取自Kaggle官网对本数据集部分介绍(谷歌翻译),关于数据集更多介绍请参考《Credit Card Fraud Detection》。

2.场景解析(算法选择)

  1. 首先,我们拿到的数据是持卡人两天内的信用卡交易数据,这份数据包含很多维度,要解决的问题是预测持卡人是否会发生信用卡被盗刷。信用卡持卡人是否会发生被盗刷只有两种可能,发生被盗刷或不发生被盗刷。又因为这份数据是打标好的(字段Class是目标列),也就是说它是一个监督学习的场景。于是,我们判定信用卡持卡人是否会发生被盗刷是一个二元分类问题,意味着可以通过二分类相关的算法来找到具体的解决办法,本项目选用的算法是逻辑斯蒂回归(Logistic Regression)。
  2. 分析数据:数据是结构化数据 ,不需要做特征抽象。特征V1至V28是经过PCA处理,而特征Time和Amount的数据规格与其他特征差别较大,需要对其做特征缩放,将特征缩放至同一个规格。在数据质量方面 ,没有出现乱码或空字符的数据,可以确定字段Class为目标列,其他列为特征列。
  3. 这份数据是全部打标好的数据,可以通过交叉验证的方法对训练集生成的模型进行评估。70%的数据进行训练,30%的数据进行预测和评估。
    现对该业务场景进行总结如下:
    1. 根据历史记录数据学习并对信用卡持卡人是否会发生被盗刷进行预测,二分类监督学习场景,选择逻辑斯蒂回归(Logistic Regression)算法。
    1. 数据为结构化数据,不需要做特征抽象,但需要做特征缩放。

3.项目代码实现

1数据获取与解析

  • 导包
import numpy as np
import pandas as pd
from pandas import Series,DataFrame

import matplotlib.pyplot as plt
%matplotlib inline

from imblearn.over_sampling import SMOTE
  • 导入数据
credit = pd.read_csv('creditcard.csv')
credit.tail()

Time    V1  V2  V3  V4  V5  V6  V7  V8  V9  ... V21 V22 V23 V24 V25 V26 V27 V28 Amount  Class
284802  172786.0    -11.881118  10.071785   -9.834783   -2.066656   -5.364473   -2.606837   -4.918215   7.305334    1.914428    ... 0.213454    0.111864    1.014480    -0.509348   1.436807    0.250034    0.943651    0.823731    0.77    0
284803  172787.0    -0.732789   -0.055080   2.035030    -0.738589   0.868229    1.058415    0.024330    0.294869    0.584800    ... 0.214205    0.924384    0.012463    -1.016226   -0.606624   -0.395255   0.068472    -0.053527   24.79   0
284804  172788.0    1.919565    -0.301254   -3.249640   -0.557828   2.630515    3.031260    -0.296827   0.708417    0.432454    ... 0.232045    0.578229    -0.037501   0.640134    0.265745    -0.087371   0.004455    -0.026561   67.88   0
284805  172788.0    -0.240440   0.530483    0.702510    0.689799    -0.377961   0.623708    -0.686180   0.679145    0.392087    ... 0.265245    0.800049    -0.163298   0.123205    -0.569159   0.546668    0.108821    0.104533    10.00   0
284806  172792.0    -0.533413   -0.189733   0.703337    -0.506271   -0.012546   -0.649617   1.577006    -0.414650   0.486180    ... 0.261057    0.643078    0.376777    0.008797    -0.473649   -0.818267   -0.002415   0.013649    217.00  0
5 rows × 31 columns

从上面可以看出,数据为结构化数据,不需要抽特征转化,但特征Time和Amount的数据规格和其他特征不一样,需要对其做特征做特征缩放
表明此数据有28万行,31列

credit.shape
out:(284807, 31)

说明数据类型只有float64和int64,且无缺失值,方便后续处理

credit.dtypes
credit.isnull().any()

2.特征工程

counts = credit.Class.value_counts()
# 查看总数据中正负例的数目, 1代表被倒刷的次数
0    284315
1       492
Name: Class, dtype: int64
  • 画图直观的观察正负例的数目
plt.figure(figsize=(10,8))
ax = plt.subplot(1,2,1)
counts.plot(kind='pie', autopct='%.4f%%', ax=ax)
# plt.axis('equal')

# 画柱状图
ax = plt.subplot(1,2,2)
counts.plot(kind='bar')

正负例图形展示.png

通过上面的图和数据可知,存在492例盗刷,占总样本的0.17%,由此可知,这是一个明显的数据类别不平衡问题,稍后我们采用过采样(增加数据)的方法对这种问题进行处理。

  • 特征转换,将时间从单位每秒化为单位每小时
# 7201秒 --> ?个小时
# 使用divmod
divmod(7201, 3600)
out: (2, 1)
# 使用map修改已有的列:Time
credit.Time = credit.Time.map(lambda x: divmod(x, 3600)[0])
credit.tail()
# 查看Time列更改了吗?
  • 特征选择
    先画一个特征的直方图(hist)
# 比较直观的方法.
# 通过画图比较.
cond0 = credit['Class'] == 0
cond1 = credit['Class'] == 1
# V1中0类的数据
v1_0 = credit['V1'][cond0]
v1_1 = credit['V1'][cond1]

v1_0.plot(kind='hist', density=True, bins=500)
v1_1.plot(kind='hist', density=True, bins=50)
特征V1的直方图.png
credit.columns

out:Index(['Time', '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', 'Amount',
       'Class'],
      dtype='object')
cols = ['Time', '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', 'Amount']
cond0 = credit['Class'] == 0
cond1 = credit['Class'] == 1
#  研究所有特征的分布情况
plt.figure(figsize=(10, 30* 6))

for i, col in enumerate(cols):
    v1_0 = credit[col][cond0]
    v1_1 = credit[col][cond1]
    
    ax = plt.subplot(30,1, i+1)
    v1_0.plot(kind='hist', density=True, bins=500, ax=ax)
    v1_1.plot(kind='hist', density=True, bins=50, ax=ax)
    ax.set_title(col)

上面的for循环会画出所有cols的特征直方图太大就不展示了
上图是不同变量在信用卡被盗刷和信用卡正常的不同分布情况,我们将选择在不同信用卡状态下的分布有明显区别的变量。因此剔除变量V8、V13 、V15 、V20、V21 、V22、 V23 、V24 、V25 、V26 、V27 和V28变量。

  • 剔除不明显的特征
# V8、V13 、V15 、V20 、V21 、V22、 V23 、V24 、V25 、V26 、V27 和V28变量
cols = ['V8', 'V13', 'V15', 'V20', 'V21' , 'V22', 'V23', 'V24', 'V25', 'V26', 'V27', 'V28']
credit.drop(columns=cols, inplace=True)
credit.shape

out:(284807, 19)
  • 特征缩放
    Amount变量和Time变量的取值范围与其他变量相差较大,所以要对其进行特征缩放
    sklearn.preprocessing.StandardScaler
credit.Amount.max()
out:25691.16
from sklearn.preprocessing import StandardScaler
ss = StandardScaler()
credit[['Amount', 'Time']] = ss.fit_transform(credit[['Amount', 'Time']])

credit[['Amount', 'Time']].head()

    Amount       Time
0   0.244964    -1.960264
1   -0.342475   -1.960264
2   1.160686    -1.960264
3   0.140534    -1.960264
4   -0.073403   -1.960264
credit.Amount.max()
out:102.36224270928423
# 通过Amount的最大值可以看出特征值被缩放了

3.对特征的重要性进行排序,以进一步减少变量

利用GBDT梯度提升决策树进行特征重要性排序

from sklearn.ensemble import GradientBoostingClassifier
data = credit.iloc[:,:-1]
target = credit.Class

gbdt = GradientBoostingClassifier()
gbdt.fit(data, target)
# 找到重要特征
feature_importances = gbdt.feature_importances_
# 重要特征下标排序
# argsort返回的排序之后的下标
inds = feature_importances.argsort()[::-1]
# 重要特征降序排列
data.columns[inds]
out:Index(['V18', 'V9', 'V14', 'V3', 'V12', 'V2', 'V17', 'V10', 'V6', 'V16',
       'Time', 'V7', 'V5', 'V4', 'V19', 'V11', 'V1', 'Amount'],
      dtype='object')
  • 通过画柱状图查看重要特征
plt.figure(figsize=(12,9))
plt.bar(np.arange(18), feature_importances[inds])
_ = plt.xticks(np.arange(18), data.columns[inds])
重要特征.png
# 根据上图,发现V7, v5, V4, V19 , V11, V1, Amount可以剔除
credit.drop(columns=['V7', 'V5', 'V4', 'V19', 'V11', 'V1', 'Amount'], inplace=True)
credit.shape
out:(284807, 12)

4.模型训练

处理样本不平衡问题
目标变量“Class”正常和被盗刷两种类别的数量差别较大,会对模型学习造成困扰。举例来说,假如有100个样本,其中只有1个是被盗刷样本,其余99个全为正常样本,那么学习器只要制定一个简单的方法:即判别所有样本均为正常样本,就能轻松达到99%的准确率。而这个分类器的决策对我们的风险控制毫无意义。因此,在将数据代入模型训练之前,我们必须先解决样本不平衡的问题。
现对该业务场景进行总结如下:

  1. 过采样(oversampling),增加正样本使得正、负样本数目接近,然后再进行学习。
  2. 欠采样(undersampling),去除一些负样本使得正、负样本数目接近,然后再进行学习。
    本次处理样本不平衡采用的方法是过采样,具体操作使用SMOTE(Synthetic Minority Oversampling Technique),SMOET的基本原理是:采样最邻近算法,计算出每个少数类样本的K个近邻,从K个近邻中随机挑选N个样本进行随机线性插值,构造新的少数样本,同时将新样本与原数据合成,产生新的训练集。更详细说明参考CMU关于SMOTE: Synthetic Minority Over-sampling Technique的介绍。

SMOTE过采样

# 不用对所有的样本做过采样操作.
# 只需要对训练数据进行过采样.   ************
from sklearn.model_selection import train_test_split
data = credit.iloc[:,:-1].copy()
target = credit.Class.copy()
X_train,X_test, y_train,y_test = train_test_split(data, target, test_size=0.3)
smote = SMOTE()
X_resampled, y_resampled = smote.fit_sample(X_train, y_train)

数据过采样结果显示

X_resampled.shape
out:(398050, 11)

y_train.value_counts()
out:0    199025
    1       339
Name: Class, dtype: int64

Series(y_resampled).value_counts()
1    199025
0    199025
dtype: int64

自定义可视化函数(不用记拿来用就可以了)

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')

单独的逻辑回归求得查全率Recall rate

Recall也叫召回率

from sklearn.linear_model import LogisticRegression
logistic = LogisticRegression()
logistic.fit(X_resampled, y_resampled)
logistic.score(X_test, y_test)
out:0.9796706576314034
# 预测
y_ = logistic.predict(X_test)
# 交叉表的结果符合AUC的求召回率的图
# 用交叉表做个对比
pd.crosstab(index=y_, columns=y_test, rownames=['预测值'], colnames=['真实值'])

out:
真实值   0       1
预测值     
    0   83567   14
    1   1723    139
# 混淆矩阵
# confusion matrix
from sklearn.metrics import confusion_matrix
cm = confusion_matrix(y_test, y_)
cm
# 混淆矩阵与AUC图的行列交换位置
out:array([[83567,  1723],
           [   14,   139]], dtype=int64)
# 计算查全率/召回率
recall = cm[1,1] / (cm[1,0] + cm[1,1])
recall
out: 0.9084967320261438
# 画出混淆矩阵图
plot_confusion_matrix(cm, [0,1], title='Recall: %.4f' % recall)
混淆矩阵图.png

利用GridSearchCV进行交叉验证和模型参数自动调优

from sklearn.model_selection import GridSearchCV
logistic = LogisticRegression()

param_grid = {
    'C': [0.1,1,10],
    'tol': [0.00005,0.0001, 0.0005], 
#     'solver': ['newton-cg', 'lbfgs', 'liblinear', 'sag', 'saga']
}
gv = GridSearchCV(estimator=logistic, param_grid=param_grid, n_jobs=5, cv=5)
gv.fit(X_resampled, y_resampled)
  • 预测
# 最佳模型参数
gv.best_params_
gv.best_score_
out:0.9267654817234016
gv.best_estimator_.score(X_test, y_test)
out:0.9796706576314034
estimator = gv.best_estimator_
# 预测
y_gv = estimator.predict(X_test)
  • 结果可视化
    对比逻辑斯蒂回归和GridSearchCV结果
cm_gv = confusion_matrix(y_test, y_gv)
cm_gv

array([[83567,  1723],
       [   14,   139]], dtype=int64)
recall_gv = cm_gv[1,1] / (cm_gv[1,0]  + cm_gv[1,1])
recall_gv
out:0.9084967320261438
# 混淆矩阵
plot_confusion_matrix(cm, [0,1], title='Recall: %.4f' % recall_gv)

GV的混淆矩阵图.png

得出结论逻辑斯蒂回归算法本身优化的比较好,调参之后效果不大.

模型评估

解决不同的问题,通常需要不同的指标来度量模型的性能。例如我们希望用算法来预测癌症是否是恶性的,假设100个病人中有5个病人的癌症是恶性,对于医生来说,尽可能提高模型的查全率(recall)比提高查准率(precision)更为重要,因为站在病人的角度,发生漏发现癌症为恶性比发生误判为癌症是恶性更为严重。

考虑设置阈值,来调整预测被盗刷的概率,依次来调整模型的查全率(Recall)

y_proba = logistic.predict_proba(X_test)
y_proba

array([[0.92512744, 0.07487256],
       [0.96328134, 0.03671866],
       [0.91636295, 0.08363705],
       ...,
       [0.95078228, 0.04921772],
       [0.80758171, 0.19241829],
       [0.92044424, 0.07955576]])
y_proba[:,1] > 0.05
out:array([ True, False,  True, ..., False,  True,  True])
from sklearn.metrics import auc, roc_curve
# 设定10个阈值,来得到不同阈值的查全率
thresholds = [0.05, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8,0.9]
cms = []
recalls = []
precisions = []
fprs = []
aucs = []


for threshold in thresholds:
    # 根据阈值判断结果究竟属于哪一类 .
    y_ = y_proba[:,1] > threshold # y_就是预测的结果了.
    cm = confusion_matrix(y_test, y_)
    recall = cm[1,1] / (cm[1,0] + cm[1,1])
    # 计算精确率
    precision = cm[1,1] / (cm[1,1] + cm[0,1])
    precisions.append(precision)
    
    fpr, tpr, thresholds_roc = roc_curve(y_test, y_)
    auc_ = auc(fpr, tpr)
    aucs.append(auc_)
    
    cms.append(cm)
    recalls.append(recall)
    print('threshold : %f  recall : %.4f' % (threshold, recall))

threshold : 0.050000  recall : 0.9935
threshold : 0.100000  recall : 0.9673
threshold : 0.200000  recall : 0.9542
threshold : 0.300000  recall : 0.9281
threshold : 0.400000  recall : 0.9085
threshold : 0.500000  recall : 0.9085
threshold : 0.600000  recall : 0.8824
threshold : 0.700000  recall : 0.8758
threshold : 0.800000  recall : 0.8758
threshold : 0.900000  recall : 0.8693
  • 画出不同阈值的混淆矩阵图
# 把这个召回率画出来.
plt.figure(figsize=(3*6, 4*6))
for i, threshold in enumerate(thresholds):
    ax = plt.subplot(4,3, i+1)
    plot_confusion_matrix(cms[i], [0,1], "Threshold: %f, Recall:%.4f" % (threshold, recalls[i]))

图片太大了不放了

趋势图

# Recall rate  Precision rate  auc 放在一张图中,观察随着阈值的变化而变化的趋势.
plt.figure(figsize=(12,9))
plt.plot(thresholds, recalls, c='g', label='recalls')
plt.plot(thresholds, precisions, c='r', label='Precisions')
plt.plot(thresholds, aucs, c='k', label='AUCS')
plt.legend()
不同评估标准的趋势图.png

由上图所见,随着阈值逐渐变大,Recall rate逐渐变小,Precision rate逐渐变大,AUC值先增后减

找出模型最优的阈值

precision和recall是一组矛盾的变量。从上面混淆矩阵和PRC曲线可以看到,阈值越小,recall值越大,模型能找出信用卡被盗刷的数量也就更多,但换来的代价是误判的数量也较大。随着阈值的提高,recall值逐渐降低,precision值也逐渐提高,误判的数量也随之减少。通过调整模型阈值,控制模型反信用卡欺诈的力度,若想找出更多的信用卡被盗刷就设置较小的阈值,反之,则设置较大的阈值。
实际业务中,阈值的选择取决于公司业务边际利润和边际成本的比较;当模型阈值设置较小的值,确实能找出更多的信用卡被盗刷的持卡人,但随着误判数量增加,不仅加大了贷后团队的工作量,也会降低正常情况误判为信用卡被盗刷客户的消费体验,从而导致客户满意度下降,如果某个模型阈值能让业务的边际利润和边际成本达到平衡时,则该模型的阈值为最优值。当然也有例外的情况,发生金融危机,往往伴随着贷款违约或信用卡被盗刷的几率会增大,而金融机构会更愿意设置小阈值,不惜一切代价守住风险的底线。

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 158,736评论 4 362
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,167评论 1 291
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 108,442评论 0 243
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 43,902评论 0 204
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,302评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,573评论 1 216
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,847评论 2 312
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,562评论 0 197
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,260评论 1 241
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,531评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,021评论 1 258
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,367评论 2 253
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,016评论 3 235
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,068评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,827评论 0 194
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,610评论 2 274
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,514评论 2 269

推荐阅读更多精彩内容