摘要:关于度量指标,本模型选择f1_macro score,它是正常交易与欺诈交易f1_score的平均值。可增加类别1(欺诈交易)的f1_score,并通过f1_score的宏观平均值,确定该模型的性能。

全文共9825字,预计学习时长40分钟或更长


尼日利亚王子发来邮件?Python告诉你:假的

图片来源:me.me


在撒哈拉以南的非洲多地,银行普及率低是普遍现象。这是由(但不限于)以下因素造成:当地居民出于历史原因对银行机构的不信任、低且不稳定的居民收入和身份档案的不完善。此外,不少居民主要生活在偏远的农村,那里实体银行基础设施建设普遍匮乏。


鉴于上述问题,无数金融科技竞相涌现,旨在利用日趋升高的手机普及率与性价比来建立数字经济,为撒哈拉以南非洲人民面临的困境提供良方。


人们相信,这些金融科技会带来净正面效益,尤其是促进移动交易/无现金交易量的增加,降低携带现金的安全风险,并提高交易的便利性,进而促进经济活动的总体增加。对于非正式贸易较多的国家而言,移动交易会让逃税行为降至最低,增加商业运作透明度。


在津巴布韦,多亏了移动支付,政府才能向非正规企业征税,即通过Ecocash(津巴布韦头号移动支付平台)对一切贸易活动征税。税为国之本,一个国家可以在别的领域一无是处,但绝不能疏于税收。


尼日利亚王子发来邮件?Python告诉你:假的

图片来源:9gag


尼日利亚王子发来邮件?Python告诉你:假的

做出设想


移动支付的益处非常多,但有个问题不能忽略:试想在一个非洲国家,比方说瓦坎达,有一家支付公司致力于促进多方支付。就在便捷无缝的移动支付发展得如火如荼时,移动支付的一个弊端暴露了出来——网络诈骗。


想象一下,有一天你收到一封自称为尼日利亚王子的来信,并表示可以让你帮个小忙(比如给他汇一笔小钱)之后就能得到一笔巨额遗产……你该相信吗?也许你觉得是天上掉馅饼了,岂不知你早已被诈骗犯视为一块肥肉。


为了甄别网络诈骗,人们需要建立一个算法模型来预测给定交易是否涉嫌诈骗。鉴于此,本文以Kaggle上的信用卡欺诈侦测数据集为基础,利用机器学习来判断既定交易是否涉嫌欺诈。


数据集传送门:https://www.kaggle.com/mlg-ulb/creditcardfraud


尼日利亚王子发来邮件?Python告诉你:假的

采用数据集


首先,从正常交易与欺诈交易的数据可视化入手,探索这些数据具体如何呈现。


#loading the dataset
location = '/Users/emmanuels/Downloads/creditcard.csv'
dat = pd.read_csv(location)
#visualising transactions
ax = dat.groupby('Class').size().transform(lambda x:
(x/sum(x)*100)).plot.bar()
for a in ax.patches:
ax.text(a.get_x()+.06,a.get_height()+.5,\
str('{}%'.format(round(a.get_height(),3))),fontsize=24,
color='black')
old = [0,1]
new = ['Normal','Fraudulent']
ax.set_xticks(old)
ax.set_xticklabels(new,rotation=0,fontsize=28)


上传数据后,按类别对数据集进行分组(0为正常交易,1为欺诈交易),并返回类别总数。虽然apply(https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.apply.html)操作会让任何lambda函数调用于已分组的数据集,但在这个情况下,使用transform(https://pandas.pydata.org/pandas-docs/stable/reference/api/pandas.DataFrame.transform.html)指令能保证所得返回值会与输入值规模相同。接下来在lambda函数中将其转化为占交易总量的百分比,并绘制出标有相关百分比的条形图。


尼日利亚王子发来邮件?Python告诉你:假的

正常交易与欺诈交易的百分比


正如所料,绝大多数交易都是非欺诈性的。但这出现了个小问题,毕竟衡量机器学习模型的性能优劣仅靠准确率是不够的。例如,要是预测每桩交易都是正常的,那准确率便是99.827%,这听上去很赞,但若真是如此,那便会因未能甄别出欺诈交易而造成金钱损失。因此,无论使用何种指标来衡量算法性能,关键还是在于衡量该算法正确甄别欺诈交易的能力。


尼日利亚王子发来邮件?Python告诉你:假的

时间-交易量可视化

接下来,根据交易达成时间研究交易量。鉴于该数据集并不显示时间数据,因此我们只能得到自首次成交纪录以来所记录的秒数。

plt.plot( 'Time', 'Amount', data=dat, marker='o', color='blue',
linewidth=2, linestyle='dashed')
plt.xlabel('Seconds since first transaction')
plt.ylabel('Size of transaction')
plt.rcParams["figure.figsize"] = (30,20)
plt.rcParams["font.size"] = "20"



尼日利亚王子发来邮件?Python告诉你:假的

时间-成交量可视化


按时间将交易量可视化后,似乎能够得出某种模式。交易量在25000秒开始走高,约20000秒后达到峰值,之后持续下降。该周期持续约500000秒(约14个小时),同样的模式在125000秒处再次出现。这大概就是一天的周期,交易量先从当天的开始逐渐走高,在中间某个时刻达到峰值,之后在打烊前衰减。可以想见,关于欺诈交易何时更易发生这个问题,或许能在该模式中找到蛛丝马迹。

dat['Hours'] = dat['Time']/3600


为了在分析中囊括上述信息,我们可以创建一个柱状图,指出一桩交易发生时的节点(小时)。再次申明,该推测可能会成为有效判断交易是否涉嫌欺诈的重要特征。


尼日利亚王子发来邮件?Python告诉你:假的

时间-欺诈交易占比可视化


接下来,根据上一段所做的假设,我们将按时间可视化欺诈交易占交易总量的比例,以探究欺诈交易是否集聚在某个特定时间段内。

#visualising amount spent per hour by class
dat_grouped =
dat.groupby(['Hours','Class'])['Amount'].sum()
ax_three = dat_grouped.groupby(level=0).apply(lambda
x:round(100*x/x.sum(),3)).unstack().plot.bar(stacked=True)
for i in ax_three.patches:
width,height=i.get_width(),i.get_height()
x,y = i.get_xy()
horiz_offset=1
vert_offset=1
labels = ['Normal','Fraudulent']
ax_three.legend(bbox_to_anchor=(horiz_offset,vert_offset),labels=labels)
if height > 0:
ax_three.annotate('{:.2f} %'.format(height),
(i.get_x()+.15*width,
i.get_y()+.5*height),
rotation=90)


为了实现可视化,数据集按小时与类别予以分类,并返回每小时进行的正常交易与欺诈交易的总和。接着调用lambda函数将这两个类别的数量转化为百分比,创建堆积条形图并注释相关的百分比,并定位百分比以此确保图表可读性。


尼日利亚王子发来邮件?Python告诉你:假的

Data Viz 实际数据可视化专家100%致力于他们的工作


尼日利亚王子发来邮件?Python告诉你:假的

时间-欺诈交易占比可视化


鉴于数据仅涉及48小时内所达成的交易,任何观察得出的模式都可以解释成偶然现象。但我们认为,将发生特定交易的一小时内欺诈交易占比作为一个特征添加进来会很有意思。据推测,交易时间段这一特征(联合其他特征)所包含的相关信息,可能有助于判断交易是否涉嫌欺诈。欺诈交易在一天内某些时段可能更易发生。

a= dat.groupby(['Hours','Class'])['Amount'].sum()
b =pd.DataFrame(a.groupby(level=0).apply(lambda
x:round(100*x/x.sum(),3)).unstack())
b=b.reset_index()
b.columns = ['Hours','Normal','Fraudulent']
dat = pd.merge(dat,b[['Hours','Fraudulent']],on='Hours', how='inner')


为了做出该柱状图,首先根据小时计算出正常交易与欺诈交易各自占比,再将数据的行转换成列,以此将类别信息以柱状图予以展现。

接着合并数据帧,从刚刚创建的数据帧中提取出欺诈交易列。和用SQL一样,两个数据帧按照共有特征/键——小时数进行合并。于是,数据帧有了一个新的数据列,限制特定交易小时内欺诈交易的占比。


尼日利亚王子发来邮件?Python告诉你:假的

机器学习


要想分类,首先从逻辑回归开始。简要回顾下,该算法即logit函数执行变换操作,回归出某事件的发生概率。


尼日利亚王子发来邮件?Python告诉你:假的


统计学=机器学习


dat.columns[df.isnull().any()]
dat['Fraudulent'] = dat['Fraudulent'].fillna(0)
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import train_test_split
#from sklearn.feature_selection import RFE
from sklearn.metrics import confusion_matrix
from sklearn.metrics import classification_report
log_reg = LogisticRegression(penalty='l1')
y = dat['Class']
X = dat.drop(['Class'],axis=1)
X_train,X_test,y_train,y_test = train_test_split(X,y,test_size=0.3,random_state=100)
log_reg.fit(X_train,y_train)
#predicting with models
predicts = log_reg.predict(X_test)
#f1 score is harmonic mean of recall and precision scoreconfusion_matrix(y_test,predicts)
####RESULTS####
array([[85276, 25],
[ 46, 96]])


接下来核查NAN值,并在必要情况下将所有NAN值用0替换。清理过程后导入分析所需的数据包,将其分为训练数据与测试数据,再将前者导入逻辑回归模型。该模型专门使用l1正则化过程(套索回归-最小绝对收缩与选择算子)(https://ai.stanford.edu/~ang/papers/aaai06-l1logisticregression.pdf)。L2最小化平方和,l1则最小化目标值与预期值的绝对差值之和。该正则化过程旨在防止系数的过拟合,以此提高该模型学习归纳新数据的能力。

如前文所述,准确度不是衡量该模型性能优劣的有效指标。因此我们先使用混淆矩阵(误差矩阵)来测试该模型对于正常交易与欺诈交易的分类精度——尤其考量模型能否正确甄别出欺诈交易。

根据混淆矩阵,该模型对正常交易的分类准确度为99.974%,对欺诈交易的分类准确度为67.60%。

print(classification_report(y_test,predicts))
####RESULTS####
precision recall f1-score support
0 1.00 1.00 1.00 85301
1 0.79 0.68 0.73 142
micro avg 1.00 1.00 1.00 85443
macro avg 0.90 0.84 0.86 85443
weighted avg 1.00 1.00 1.00 85443


为便于比较,可以打印出所预测的分类报告。该报告返回f1-score,这对保证比较一致性来说很重要。f1-score算是个加权数,是精确率(即检索结果中真阳性样本占所有真阳性预测的比例)与召回率(即检索结果中真阳性样本占所有实际真阳性样本的比例)的调和均值,可显示该模型正确分类正常交易与欺诈交易的性能。可增加类别1(欺诈交易)的f1_score,并通过f1_score的宏观平均值,确定该模型的性能。


尼日利亚王子发来邮件?Python告诉你:假的

递归特征消除


并非所有特征同样重要。有些特征不会为该模型性能增值,甚至会起到反作用。当然,这一切主要与所采用模型有关。某些诸如决策树与随机森林之类的模型可自行选出最相关的特征,因此无须专门进行特征选择。


特征选择的原理在于选择能优化某个分数的特征。主要方法有正向选择(从一个特征开始不断添加新的特征直到得出最优解)与反向选择(正向选择的逆过程)。就本文而言,更倾向于递归特征消除。这是一种反向选择,去除模型中最无关紧要的特征,该方法的关键在于衡量重要性的指标。

from sklearn.feature_selection import RFECV
selector = RFECV(log_reg,step=3,cv=5,scoring='f1_macro')
selector = selector.fit(X,y)


本文专门选择带有交叉验证的递归特征消除,即用五倍交叉验证得出特征的最优数。关于度量指标,本模型选择f1_macro score,它是正常交易与欺诈交易f1_score的平均值。将迭代设置为3,因此每次迭代将删去3个特征。

#showing the name of the features that have been selectedcols = list(X.columns)
temp =pd.Series(selector.support_,index=cols)
selected_features = temp[temp==True].index
selected_features
#### RESULTS ####
Index(['V1', 'V3', 'V4', 'V5', 'V6', 'V7', 'V8', 'V9', 'V10', 'V11', 'V12',
'V13', 'V14', 'V15', 'V16', 'V17', 'V19', 'V20', 'V21', 'V22', 'V23',
'V24', 'V25', 'V27', 'V28', 'Fraudulent'],
dtype='object')


为了掌握每次递归特征消除后所返回的特征,笔者创建了一个pandas series, 将选定特征映射于相关列标题。最终,这次过程消除了V2,V15,V18,小时,量与时间这几列。

X_rfecv = dat[selected_features].values
X_train,X_test,y_train,y_test =
train_test_split(X_rfecv,y,test_size=0.3,random_state=100)
log_reg.fit(X_train,y_train)
#predicting with models
predicts = log_reg.predict(X_test)
#f1 score is harmonic mean of recall and precision score
confusion_matrix(y_test,predicts)
#### RESULTS ####
array([[85279, 22],
[ 48, 94]])


使用rfe过程过滤出来的特征被导入到逻辑回归模型后,发现模型预测正常交易的性能略有提高,而欺诈交易的预测性能却有所衰减。不过,宏f1_score却与所有列作为特征时毫无变化。


尼日利亚王子发来邮件?Python告诉你:假的

另觅良方


本文决定采用决策树、随机森林与随机搜索交叉验证来获得最优参数的近似值,并利用随机森林将宏f1_score平均值提升到0.88。

random_forest = RandomForestClassifier()
param_grid = {"criterion":['gini','entropy'],
'n_estimators':range(20,200,20),
'max_depth':range(20,200,20),
}
tuned_rfc =
RandomizedSearchCV(random_forest,param_grid,cv=5,scoring='f1')
tuned_rfc.fit(X_train,y_train)
tuned_preds = tuned_rfc.predict(X_test)
confusion_matrix(y_test,tuned_preds)
#### RESULTS ####
array([[85288, 13],
[ 35, 107]])


随机搜索包可用来预设笔者想测试的形参(形式参数),以此得到本模型最优形参。这一原理即在一切可能的形参中找到最优组合。顾名思义,与一一检验所有指明形参的排列不同,该方法所创建的组合是随机的,使得此类超参数调整法比其他诸如网格搜索的方法更为迅捷。毕竟,后者的运行时间长得令人煎熬,跟看一部DC电影有得一拼。


尼日利亚王子发来邮件?Python告诉你:假的

DC电影好看——个鬼


凭借随机搜索与f1衡量参数,本文将最优参数与训练数据予以拟合,得出测试结果,并将其返至混淆矩阵。如结果所示,正常交易与欺诈交易的准确度均有所提升。

print(classification_report(y_test,tuned_preds))
#### RESULTS ####
precision recall f1-score support
0 1.00 1.00 1.00 85301
1 0.89 0.75 0.82 142
micro avg 1.00 1.00 1.00 85443
macro avg 0.95 0.88 0.91 85443
weighted avg 1.00 1.00 1.00 85443


更棒的是,宏f1平均值也有所升高,该模型终于可甄别出更多的欺诈交易了。

尼日利亚王子发来邮件?Python告诉你:假的

XGBoost! (极限梯度提升)


最后,我们决定采用饱受赞誉的XGBoost分类器,并通过交叉验证的随机搜索予以微调。XGBoost是极限梯度提升的英文缩写。根据各项Kaggle竞赛结果来看,该机器学习功能强大,性能优越。更有人说,就连洗过衣服后离奇失踪的袜子究竟去往何方,XGBoost都能帮你做出推测。


尼日利亚王子发来邮件?Python告诉你:假的

袜子军队集结!


据官方“白皮书”,XGBoost是在梯度提升框架下运行的梯度提升树算法。简言之,它将预测不出(或效果不佳)理想结果的弱学习器/树进行强化。这意味着新树可校正先前决策树预测的残留错误(即纠错)。只要做出明确指定,该过程将一直延续,具体由模型所用参数决定。决策树做出的每个校正都会并入模型中,只要对每次校正赋予加权,便能控制所用模型对训练数据的适应速度。


决策树的工作原理与XGBoost有相通之处。在决策树中,数据分为多个子集/叶子,并给每一片叶子赋值(信息增益),并构建更多的枝杈(即进一步分割数据),不断选择拥有最高值的叶子,直到达到预定水平/纵深。对整体而言(如随机森林),不同决策树运行相同过程,彼此互不干扰。这与提升树类似,但两者区别在于模型的训练方式。研究不同观点与调研后,XGBoost在训练速度方面似乎比梯度提升更为优越,甚至还超越了随机森林,此外XGBoost的数据结果往往也更好。


from xgboost import XGBClassifier
y = dat['Class']
X = dat.drop(['Class'],axis=1)
X_train,X_test,y_train,y_test =
train_test_split(X,y,test_size=0.3,random_state=100)
xgb = XGBClassifier()
#fine tuning XGB parameters
params = {'min_child_weight':[5,15],
'subsample':[0.6,0.8,1.0],
'gamma':[1,5,10,15],
'learning_rate': [0.01,0.05,0.1],
'colsample_bytree':[0.6,0.8,1.0],
'max_depth':[2,3,5,10],
'n-estimaors': range(50,1000,50)
}
#randomized search
random_search = RandomizedSearchCV(xgb,params,cv=5,n_iter=5,scoring='f1',random_state=100)
random_search.fit(X_train,y_train)
confusion_matrix(y_test,opt_predicts)
#### RESULTS ####
array([[85290, 11],
[ 31, 111]])


在阅读了关于gamma基准与学习率后,我们决定再次使用随机搜索得出最优参数,将模型与训练数据拟合,使模型得到轻微的改进。如今,该模型对正常交易的分类准确率为99.987%,对欺诈交易的分类准确率为78.16%。

print(classification_report(y_test,opt_predicts))
#### RESULTS ####
precision recall f1-score support
0 1.00 1.00 1.00 85301
1 0.91 0.78 0.84 142
micro avg 1.00 1.00 1.00 85443
macro avg 0.95 0.89 0.92 85443
weighted avg 1.00 1.00 1.00 85443


更棒的是,宏F1平均值也略有提高。在这其中,最有趣的是实现性能飞跃的功臣不是选用了不同的模型,而是特征工程。可以想见,倘若对所取数据有更深的理解与感悟,模型的可行性将大幅提高。例如,作为一家支付平台,研究员对给定用户的数据平均规模,量与交易时间会有更深的认识。与单单从原始数据中归纳出模棱两可的模式相比,所获得的总特征要更好。


尼日利亚王子发来邮件?Python告诉你:假的


提升虽小,但聊胜于无


尼日利亚王子发来邮件?Python告诉你:假的

交叉验证


最后一步,对XGBoost数据进行交叉验证。本文案例采用十倍分层交叉验证,其原理为每一层均能很好地体现整个数据集各层的数据,而非仅囊括一类(例如仅包括0类,即正常交易)。对于本文案例而言,这尤为重要,毕竟其中一个类别占多于99%。


strat = StratifiedKFold(n_splits=5, shuffle=True)
strat.get_n_splits(X)
model_score = []
for train_index, test_index in strat.split(X,y):
X_train,X_test,y_train,y_test = X.iloc[train_index],
X.iloc[test_index],y.iloc[train_index],y.iloc[test_index]
random_search.fit(X_train, y_train)
xgb_predicts=random_search.predict(X_test)
model_score.append(f1_score(y_test,xgb_predicts,average='macro',labels=np.unique(xgb_predicts)))
scores_table = pd.DataFrame({"F1 Score" :model_score})
scores_table


用交叉验证专门针对宏f1值进行验证。据初步测试显示,该值约为0,92.


尼日利亚王子发来邮件?Python告诉你:假的



接着将5个测试添加到dataframe(数据帧)中,所得结果在某种程度上对初步测试是个佐证。

尼日利亚王子发来邮件?Python告诉你:假的

留言 点赞 关注

我们一起分享AI学习与发展的干货

欢迎关注全平台AI垂类自媒体 “读芯术”

相关文章