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

本项目需解决的问题¶

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

建模思路¶

1.jpg

项目背景¶

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

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

场景解析(算法选择)¶

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

1数据获取与解析¶

In [2]:
import numpy as np

import pandas as pd
from pandas import Series,DataFrame

import matplotlib.pyplot as plt
%matplotlib inline #不用再写show

from imblearn.over_sampling import SMOTE

从上面可以看出,数据为结构化数据,不需要抽特征转化,但特征Time和Amount的数据规格和其他特征不一样,需要对其做特征做特征缩放。

In [3]:
credit = pd.read_csv('./creditcard.csv')
credit.shape
Out[3]:
(284807, 31)
In [4]:
credit.head()
Out[4]:
Time V1 V2 V3 V4 V5 V6 V7 V8 V9 ... V21 V22 V23 V24 V25 V26 V27 V28 Amount Class
0 0.0 -1.359807 -0.072781 2.536347 1.378155 -0.338321 0.462388 0.239599 0.098698 0.363787 ... -0.018307 0.277838 -0.110474 0.066928 0.128539 -0.189115 0.133558 -0.021053 149.62 0
1 0.0 1.191857 0.266151 0.166480 0.448154 0.060018 -0.082361 -0.078803 0.085102 -0.255425 ... -0.225775 -0.638672 0.101288 -0.339846 0.167170 0.125895 -0.008983 0.014724 2.69 0
2 1.0 -1.358354 -1.340163 1.773209 0.379780 -0.503198 1.800499 0.791461 0.247676 -1.514654 ... 0.247998 0.771679 0.909412 -0.689281 -0.327642 -0.139097 -0.055353 -0.059752 378.66 0
3 1.0 -0.966272 -0.185226 1.792993 -0.863291 -0.010309 1.247203 0.237609 0.377436 -1.387024 ... -0.108300 0.005274 -0.190321 -1.175575 0.647376 -0.221929 0.062723 0.061458 123.50 0
4 2.0 -1.158233 0.877737 1.548718 0.403034 -0.407193 0.095921 0.592941 -0.270533 0.817739 ... -0.009431 0.798278 -0.137458 0.141267 -0.206010 0.502292 0.219422 0.215153 69.99 0

5 rows × 31 columns

表明此数据有28万行,31列

In [5]:
credit.isnull().any()  #判断空值的方法
Out[5]:
Time      False
V1        False
V2        False
V3        False
V4        False
V5        False
V6        False
V7        False
V8        False
V9        False
V10       False
V11       False
V12       False
V13       False
V14       False
V15       False
V16       False
V17       False
V18       False
V19       False
V20       False
V21       False
V22       False
V23       False
V24       False
V25       False
V26       False
V27       False
V28       False
Amount    False
Class     False
dtype: bool

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

In [6]:
credit.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 284807 entries, 0 to 284806
Data columns (total 31 columns):
Time      284807 non-null float64
V1        284807 non-null float64
V2        284807 non-null float64
V3        284807 non-null float64
V4        284807 non-null float64
V5        284807 non-null float64
V6        284807 non-null float64
V7        284807 non-null float64
V8        284807 non-null float64
V9        284807 non-null float64
V10       284807 non-null float64
V11       284807 non-null float64
V12       284807 non-null float64
V13       284807 non-null float64
V14       284807 non-null float64
V15       284807 non-null float64
V16       284807 non-null float64
V17       284807 non-null float64
V18       284807 non-null float64
V19       284807 non-null float64
V20       284807 non-null float64
V21       284807 non-null float64
V22       284807 non-null float64
V23       284807 non-null float64
V24       284807 non-null float64
V25       284807 non-null float64
V26       284807 non-null float64
V27       284807 non-null float64
V28       284807 non-null float64
Amount    284807 non-null float64
Class     284807 non-null int64
dtypes: float64(30), int64(1)
memory usage: 67.4 MB

2特征工程¶

In [7]:
c_counts  = credit['Class'].value_counts()
c_counts
Out[7]:
0    284315
1       492
Name: Class, dtype: int64
In [8]:
type(c_counts)
Out[8]:
pandas.core.series.Series
In [9]:
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)
Out[9]:
<matplotlib.axes._subplots.AxesSubplot at 0x18f0a2ed940>
No description has been provided for this image

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

特征转换,将时间从单位每秒化为单位每小时¶

In [10]:
7200%3600
Out[10]:
0
In [11]:
divmod(7201,3600)[0]
Out[11]:
2
In [12]:
# map apply
credit['Time'] = credit['Time'].map(lambda x:divmod(x,3600)[0])
#map 只能处理series
#apply既能处理series,也能处理dataframe
#agg处理dataframe,仅仅针对聚合函数
In [13]:
credit['Time']
Out[13]:
0          0.0
1          0.0
2          0.0
3          0.0
4          0.0
5          0.0
6          0.0
7          0.0
8          0.0
9          0.0
10         0.0
11         0.0
12         0.0
13         0.0
14         0.0
15         0.0
16         0.0
17         0.0
18         0.0
19         0.0
20         0.0
21         0.0
22         0.0
23         0.0
24         0.0
25         0.0
26         0.0
27         0.0
28         0.0
29         0.0
          ... 
284777    47.0
284778    47.0
284779    47.0
284780    47.0
284781    47.0
284782    47.0
284783    47.0
284784    47.0
284785    47.0
284786    47.0
284787    47.0
284788    47.0
284789    47.0
284790    47.0
284791    47.0
284792    47.0
284793    47.0
284794    47.0
284795    47.0
284796    47.0
284797    47.0
284798    47.0
284799    47.0
284800    47.0
284801    47.0
284802    47.0
284803    47.0
284804    47.0
284805    47.0
284806    47.0
Name: Time, Length: 284807, dtype: float64

特征选择¶

In [14]:
# ! ! ! V1~V28

# 28万多条数据
cond0 = credit['Class'] == 0

# 492
cond1 = credit['Class'] == 1

# hist 直方图,柱状图


credit['V1'][cond0].plot(kind = 'hist',bins = 500)
credit['V1'][cond1].plot(kind = 'hist',bins = 50)
Out[14]:
<matplotlib.axes._subplots.AxesSubplot at 0x18f0f3635c0>
No description has been provided for this image
In [15]:
credit.columns
Out[15]:
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')
In [16]:
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']

drop_list = ['V1','V3','V5']

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)
C:\Users\softpo.DESKTOP-PN692CT\Anaconda3\lib\site-packages\matplotlib\axes\_axes.py:6462: UserWarning: The 'normed' kwarg is deprecated, and has been replaced by the 'density' kwarg.
  warnings.warn("The 'normed' kwarg is deprecated, and has been "
No description has been provided for this image
In [17]:
drops = ['V13','V15','V20','V22','V23','V24','V25','V26','V27','V28']

credit2 = credit.drop(labels=drops,axis = 1)
In [ ]:
 

上图是不同变量在信用卡被盗刷和信用卡正常的不同分布情况,我们将选择在不同信用卡状态下的分布有明显区别的变量。因此剔除变量V8、V13 、V15 、V20 、V21 、V22、 V23 、V24 、V25 、V26 、V27 和V28变量。

In [18]:
credit.shape
Out[18]:
(284807, 31)
In [19]:
credit2.shape
Out[19]:
(284807, 21)

特征缩放¶

Amount变量和Time变量的取值范围与其他变量相差较大,所以要对其进行特征缩放
sklearn.preprocessing.StandarScaler

In [20]:
from sklearn.preprocessing import StandardScaler
In [21]:
credit2['Amount'].max()
Out[21]:
25691.16
In [22]:
credit2['Amount'].min()
Out[22]:
0.0
In [23]:
credit2['V8'].max()
Out[23]:
20.0072083651213
In [24]:
credit2['V8'].min()
Out[24]:
-73.21671845526741
In [25]:
standScaler = StandardScaler() 

cols = ['Time','Amount']

credit2[cols] = standScaler.fit_transform(credit2[cols])
In [26]:
credit2['Amount'].max()
Out[26]:
102.36224270928423
In [27]:
credit2['Amount'].min()
Out[27]:
-0.35322939296682354
In [28]:
credit2['Time'].min()
Out[28]:
-1.9602638886856412
In [29]:
credit2['Time'].max()
Out[29]:
1.6044448928637376

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

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

In [30]:
from sklearn.ensemble import GradientBoostingClassifier
In [31]:
clf = GradientBoostingClassifier()

X_train = credit2.iloc[:,:-1]

y_train = credit2['Class']
clf.fit(X_train,y_train)
Out[31]:
GradientBoostingClassifier(criterion='friedman_mse', init=None,
              learning_rate=0.1, loss='deviance', max_depth=3,
              max_features=None, max_leaf_nodes=None,
              min_impurity_decrease=0.0, min_impurity_split=None,
              min_samples_leaf=1, min_samples_split=2,
              min_weight_fraction_leaf=0.0, n_estimators=100,
              presort='auto', random_state=None, subsample=1.0, verbose=0,
              warm_start=False)
In [32]:
X_train.shape
Out[32]:
(284807, 20)
In [33]:
feature_importances_ = clf.feature_importances_
feature_importances_
Out[33]:
array([2.13317223e-03, 0.00000000e+00, 2.34971456e-02, 3.24365427e-02,
       0.00000000e+00, 0.00000000e+00, 3.95172432e-03, 0.00000000e+00,
       0.00000000e+00, 9.08812387e-02, 8.30824308e-03, 0.00000000e+00,
       2.64104332e-02, 7.28912511e-02, 2.58168549e-03, 1.35408766e-02,
       7.23309518e-01, 0.00000000e+00, 5.81693232e-05, 0.00000000e+00])
In [34]:
cols = X_train.columns
cols
Out[34]:
Index(['Time', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6', 'V7', 'V8', 'V9', 'V10',
       'V11', 'V12', 'V14', 'V16', 'V17', 'V18', 'V19', 'V21', 'Amount'],
      dtype='object')
In [35]:
# 从大到小进行排列
index = feature_importances_.argsort()[::-1]
index
Out[35]:
array([16,  9, 13,  3, 12,  2, 15, 10,  6, 14,  0, 18,  8,  7,  5,  4, 11,
       17,  1, 19], dtype=int64)
In [36]:
len(index)
Out[36]:
20
In [37]:
plt.figure(figsize=(12,9))
plt.bar(np.arange(len(index)),feature_importances_[index])

_ = plt.xticks(np.arange(len(index)),cols[index])
No description has been provided for this image
In [38]:
drops = ['V7','V21','V8','V5','V4','V11','V19','V1','Amount']

credit3 = credit2.drop(labels=drops,axis = 1)
credit3.shape
Out[38]:
(284807, 12)

模型训练¶

处理样本不平衡问题

目标变量“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过采样¶

In [39]:
from sklearn.model_selection import train_test_split
In [40]:
X = credit3.iloc[:,:-1]

y = credit3['Class']
In [41]:
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size = 0.3)
In [42]:
# X_train,y_train 作为训练数据

# 训练时,保证样本均衡,将X_train和y_train样本
# 测试时候,样本不均衡,没问题的
In [43]:
y_train.value_counts()
Out[43]:
0    199019
1       345
Name: Class, dtype: int64
In [44]:
smote = SMOTE()

# ndarray
X_train_new,y_train_new = smote.fit_resample(X_train,y_train)
In [45]:
type(X_train_new)
Out[45]:
numpy.ndarray
In [46]:
type(y_train_new)
Out[46]:
numpy.ndarray
In [47]:
y_train_new
Out[47]:
array([0, 0, 0, ..., 1, 1, 1], dtype=int64)
In [48]:
y_train_new.shape
Out[48]:
(398038,)
In [49]:
y_train_new
Out[49]:
array([0, 0, 0, ..., 1, 1, 1], dtype=int64)
In [51]:
Series(y_train_new).value_counts()
Out[51]:
1    199019
0    199019
dtype: int64

自定义可视化函数¶

In [52]:
# for 循环
import itertools
In [50]:
# 画图方法
# 绘制真实值和预测值对比情况
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也叫召回率¶

In [53]:
from sklearn.linear_model import LogisticRegression
In [55]:
logistic = LogisticRegression()

# 样本均衡的数据进行训练
logistic.fit(X_train_new,y_train_new)
Out[55]:
LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False)
In [56]:
y_ = logistic.predict(X_test)
In [58]:
# 交叉表
pd.crosstab(y_test,y_)
Out[58]:
col_0 0 1
Class
0 83648 1648
1 27 120
In [1]:
#正例精确度
83648 / (83648 + 27)
Out[1]:
0.9996773229757993
In [2]:
#正例召回率
83648 / (83648 + 1648)
Out[2]:
0.9806790470830988
In [3]:
#负例的精确度
120 / (120 + 1648)
Out[3]:
0.06787330316742081
In [4]:
#负例的召回率
120 / (120 + 27)
Out[4]:
0.8163265306122449
In [59]:
#盗刷数据正确率和召回率


from sklearn.metrics import confusion_matrix
In [61]:
# 混合矩阵
cm = confusion_matrix(y_test,y_)
cm
Out[61]:
array([[83648,  1648],
       [   27,   120]], dtype=int64)
In [63]:
# Recall------“正确被检索的正样本item(TP)"占所有"应该检索到的item(TP+FN)"的比例

plot_confusion_matrix(cm,[0,1],title='Recall:%0.3f'%(cm[1,1]/(cm[1,0] + cm[1,1])))
No description has been provided for this image

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

In [64]:
from sklearn.model_selection import GridSearchCV
In [73]:
logistic = LogisticRegression()
clf = GridSearchCV(logistic,param_grid={'tol':[1e-3,1e-4,1e-5],'C':[1,0.1,10,100],'penalty':['l1','l2']},cv = 10,iid = False,n_jobs=-1)
clf.fit(X_train_new,y_train_new)
Out[73]:
GridSearchCV(cv=10, error_score='raise',
       estimator=LogisticRegression(C=1.0, class_weight=None, dual=False, fit_intercept=True,
          intercept_scaling=1, max_iter=100, multi_class='ovr', n_jobs=1,
          penalty='l2', random_state=None, solver='liblinear', tol=0.0001,
          verbose=0, warm_start=False),
       fit_params=None, iid=False, n_jobs=-1,
       param_grid={'tol': [0.001, 0.0001, 1e-05], 'C': [1, 0.1, 10, 100]},
       pre_dispatch='2*n_jobs', refit=True, return_train_score='warn',
       scoring=None, verbose=0)
In [66]:
clf.best_score_
Out[66]:
0.9477637813475095
In [74]:
clf.best_score_
Out[74]:
0.9478115174030656
In [75]:
clf.best_params_
Out[75]:
{'C': 0.1, 'tol': 0.0001}

预测¶

In [71]:
y3_ = clf.best_estimator_.predict(X_test)
In [72]:
confusion_matrix(y_test,y3_)
Out[72]:
array([[83648,  1648],
       [   27,   120]], dtype=int64)
In [68]:
y2_ = clf.predict(X_test)
In [69]:
confusion_matrix(y_test,y2_)
Out[69]:
array([[83648,  1648],
       [   27,   120]], dtype=int64)
In [79]:
y4_ = clf.predict(X_test)

cm2 = confusion_matrix(y_test,y4_)

结果可视化¶

对比逻辑斯蒂回归和GridSearchCV结果¶

In [78]:
plot_confusion_matrix(cm,[0,1],title='Logistic Recall:%0.3f'%(cm[1,1]/(cm[1,0] + cm[1,1])))
No description has been provided for this image
In [80]:
plot_confusion_matrix(cm2,[0,1],title='GridSearchCV Recall:%0.3f'%(cm2[1,1]/(cm2[1,0] + cm2[1,1])))
No description has been provided for this image
In [ ]:
 

模型评估¶

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

由此可见就上面的两个算法而言,明显lgb过拟合了,考虑到样本不均衡问题,故应该选用简单一点的算法(逻辑回归)来减少陷入过拟合的陷阱。

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

In [81]:
# 概率
#刷卡诈骗的容忍度?
y_proba_ = clf.predict_proba(X_test)
y_proba_
Out[81]:
array([[0.93548127, 0.06451873],
       [0.98059225, 0.01940775],
       [0.82028904, 0.17971096],
       ...,
       [0.95243485, 0.04756515],
       [0.94441579, 0.05558421],
       [0.98251652, 0.01748348]])
In [92]:
from sklearn.metrics import auc,roc_curve
In [100]:
thresholds = [0.05,0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]

recalls = []

precissions = []

aucs = []

cms = []
for threshold in thresholds:
    y_ = y_proba_[:,1] >= threshold
    
    cm = confusion_matrix(y_test,y_)
    
    recalls.append(cm[1,1]/(cm[1,0] + cm[1,1]))
    
    precissions.append((cm[0,0] + cm[1,1])/cm.sum())
    
    fpr,tpr,_ = roc_curve(y_test,y_)
    
    auc_ = auc(fpr,tpr)
    
    aucs.append(auc_)
    
    cms.append(cm)
In [103]:
plt.figure(figsize=(24,18))
for i,cm in enumerate(cms):
    plt.subplot(3,4,i+1)
    plot_confusion_matrix(cm,[0,1],title='thresholds:%0.2f,Recall:%0.2f'%(thresholds[i],cm[1,1]/(cm[1,0] + cm[1,1])))
No description has been provided for this image

趋势图¶

In [99]:
plt.figure(figsize=(12,6))

plt.plot(thresholds,recalls,label = 'Recall')

plt.plot(thresholds,precissions,label = 'Precission')

plt.plot(thresholds,aucs,label = 'auc')

plt.legend()

plt.xlabel('thresholds')

# plt.ylim(0.5,1.2)
Out[99]:
Text(0.5,0,'thresholds')
No description has been provided for this image

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

找出模型最优的阈值¶

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