from:https://zhuanlan.zhihu.com/p/30461746
- 本項目需解決的問題
本項目通過利用信用卡的歷史交易數據,進行機器學習,構建信用卡反欺詐預測模型,提前發現客戶信用卡被盜刷的事件。
- 建模思路

項目背景
數據集包含由歐洲持卡人於2013年9月使用信用卡進行交的數據。此數據集顯示兩天內發生的交易,其中284,807筆交易中有492筆被盜刷。數據集非常不平衡,積極的類(被盜刷)占所有交易的0.172%。
它只包含作為PCA轉換結果的數字輸入變量。不幸的是,由於保密問題,我們無法提供有關數據的原始功能和更多背景信息。特征V1,V2,... V28是使用PCA獲得的主要組件,沒有用PCA轉換的唯一特征是“時間”和“量”。特征'時間'包含數據集中每個事務和第一個事務之間經過的秒數。特征“金額”是交易金額,此特征可用於實例依賴的成本認知學習。特征'類'是響應變量,如果發生被盜刷,則取值1,否則為0。
以上取自Kaggle官網對本數據集部分介紹(谷歌翻譯),關於數據集更多介紹請參考《Credit Card Fraud Detection》。
1 場景解析(算法選擇)
1)首先,我們拿到的數據是持卡人兩天內的信用卡交易數據,這份數據包含很多維度,要解決的問題是預測持卡人是否會發生信用卡被盜刷。信用卡持卡人是否會發生被盜刷只有兩種可能,發生被盜刷或不發生被盜刷。又因為這份數據是打標好的(字段Class是目標列),也就是說它是一個監督學習的場景。於是,我們判定信用卡持卡人是否會發生被盜刷是一個二元分類問題,意味着可以通過二分類相關的算法來找到具體的解決辦法,本項目選用的算法是邏輯斯蒂回歸(Logistic Regression)。
2)分析數據:數據是結構化數據 ,不需要做特征抽象。特征V1至V28是經過PCA處理,而特征Time和Amount的數據規格與其他特征差別較大,需要對其做特征縮放,將特征縮放至同一個規格。在數據質量方面 ,沒有出現亂碼或空字符的數據,可以確定字段Class為目標列,其他列為特征列。
3)這份數據是全部打標好的數據,可以通過交叉驗證的方法對訓練集生成的模型進行評估。70%的數據進行訓練,30%的數據進行預測和評估。
現對該業務場景進行總結如下:
- 根據歷史記錄數據學習並對信用卡持卡人是否會發生被盜刷進行預測,二分類監督學習場景,選擇邏輯斯蒂回歸(Logistic Regression)算法。
- 數據為結構化數據,不需要做特征抽象,但需要做特征縮放。

2 數據預處理(Pre-processing Data)
- 前期准備
# Imports # Numpy,Pandas import numpy as np import pandas as pd import datetime # matplotlib,seaborn,pyecharts import matplotlib.pyplot as plt import matplotlib.gridspec as gridspec # plt.style.use('ggplot') #風格設置近似R這種的ggplot庫 import seaborn as sns sns.set_style('whitegrid') %matplotlib inline import missingno as msno # import sklearn from sklearn.linear_model import LogisticRegression from sklearn.ensemble import RandomForestClassifier from sklearn.model_selection import GridSearchCV from sklearn.model_selection import train_test_split from sklearn.metrics import confusion_matrix from sklearn.metrics import precision_recall_curve from sklearn.metrics import auc from sklearn.metrics import roc_auc_score from sklearn.metrics import roc_curve from sklearn.metrics import recall_score from sklearn.metrics import classification_report from sklearn.metrics import accuracy_score from sklearn.preprocessing import StandardScaler # 忽略彈出的warnings import warnings warnings.filterwarnings('ignore') pd.set_option('display.float_format', lambda x: '%.4f' % x) from imblearn.over_sampling import SMOTE import itertools
- 數據獲取與解析
data_cr = pd.read_csv('creditcard.csv' , encoding='latin-1') #讀取數據 data_cr.head() #查看表格默認前5行

從上面可以看出,數據為結構化數據,不需要抽特征轉化,但特征Time和Amount的數據規格和其他特征不一樣,需要對其做特征做特征縮放。
data_cr.shape #查看數據集的大小

本數據集大小為28萬行,31列。
data_cr.info() # 查看數據的基本信息

通過查看數據信息得知,數據的類型基本是float64和int64數據類型。
data_cr.describe().T #查看數據基本統計信息

msno.matrix(data_cr) # 查看缺失值情況

通過上圖可以獲知,數據集不存在缺失值,因此不需作缺失值處理。
3 特征工程(Feature Engineering)
# 目標變量分布可視化
fig, axs = plt.subplots(1,2,figsize=(14,7))
sns.countplot(x='Class',data=data_cr,ax=axs[0])
axs[0].set_title("Frequency of each Class")
data_cr['Class'].value_counts().plot(x=None,y=None, kind='pie', ax=axs[1],autopct='%1.2f%%')
axs[1].set_title("Percentage of each Class")
plt.show()

# 查看目標列的情況 data_cr.groupby('Class').size()

數據集284,807筆交易中有492筆是信用卡被盜刷交易,信用卡被盜刷交易占總體比例為0.17%,信用卡交易正常和被盜刷兩者數量不平衡,樣本不平衡影響分類器的學習,稍后我們將會使用過采樣的方法解決樣本不平衡的問題。
- 特征衍生
特征Time的單為秒,我們將其轉化為以小時為單位對應每天的時間。
data_cr['Hour'] =data_cr["Time"].apply(lambda x : divmod(x, 3600)[0]) #單位轉換
- 特征選擇(數據探索)
查看信用卡正常用戶與被盜刷用戶之間的區別。
Xfraud = data_cr.loc[data_cr["Class"] == 1] # update Xfraud & XnonFraud with cleaned data XnonFraud = data_cr.loc[data_cr["Class"] == 0] correlationNonFraud = XnonFraud.loc[:, data_cr.columns != 'Class'].corr() mask = np.zeros_like(correlationNonFraud) indices = np.triu_indices_from(correlationNonFraud) mask[indices] = True grid_kws = {"width_ratios": (.9, .9, .05), "wspace": 0.2} f, (ax1, ax2, cbar_ax) = plt.subplots(1, 3, gridspec_kw=grid_kws, \ figsize = (14, 9)) cmap = sns.diverging_palette(220, 8, as_cmap=True) ax1 =sns.heatmap(correlationNonFraud, ax = ax1, vmin = -1, vmax = 1, \ cmap = cmap, square = False, linewidths = 0.5, mask = mask, cbar = False) ax1.set_xticklabels(ax1.get_xticklabels(), size = 16); ax1.set_yticklabels(ax1.get_yticklabels(), size = 16); ax1.set_title('Normal', size = 20) correlationFraud = Xfraud.loc[:, data_cr.columns != 'Class'].corr() ax2 = sns.heatmap(correlationFraud, vmin = -1, vmax = 1, cmap = cmap, \ ax = ax2, square = False, linewidths = 0.5, mask = mask, yticklabels = False, \ cbar_ax = cbar_ax, cbar_kws={'orientation': 'vertical', \ 'ticks': [-1, -0.5, 0, 0.5, 1]}) ax2.set_xticklabels(ax2.get_xticklabels(), size = 16); ax2.set_title('Fraud', size = 20); cbar_ax.set_yticklabels(cbar_ax.get_yticklabels(), size = 14);

從上圖可以看出,信用卡被盜刷的事件中,部分變量之間的相關性更明顯。其中變量V1、V2、V3、V4、V5、V6、V7、V9、V10、V11、V12、V14、V16、V17和V18以及V19之間的變化在信用卡被盜刷的樣本中呈性一定的規律。
- 盜刷交易、交易金額和交易次數的關系
f, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(16,4)) bins = 30 ax1.hist(data_cr["Amount"][data_cr["Class"]== 1], bins = bins) ax1.set_title('Fraud') ax2.hist(data_cr["Amount"][data_cr["Class"] == 0], bins = bins) ax2.set_title('Normal') plt.xlabel('Amount ($)') plt.ylabel('Number of Transactions') plt.yscale('log') plt.show()

信用卡被盜刷發生的金額與信用卡正常用戶發生的金額相比呈現散而小的特點,這說明信用卡盜刷者為了不引起信用卡卡主的注意,更偏向選擇小金額消費。
- 大家哪個時間段最愛消費?
sns.factorplot(x="Hour", data=data_cr, kind="count", palette="ocean", size=6, aspect=3)

每天早上9點到晚上11點之間是信用卡消費的高頻時間段。
- 盜刷交易、交易金額和交易時間的關系
f, (ax1, ax2) = plt.subplots(2, 1, sharex=True, figsize=(16,6)) ax1.scatter(data_cr["Hour"][data_cr["Class"] == 1], data_cr["Amount"][data_cr["Class"] == 1]) ax1.set_title('Fraud') ax2.scatter(data_cr["Hour"][data_cr["Class"] == 0], data_cr["Amount"][data_cr["Class"] == 0]) ax2.set_title('Normal') plt.xlabel('Time (in Hours)') plt.ylabel('Amount') plt.show()

print ("Fraud Stats Summary")
print (data_cr["Amount"][data_cr["Class"] == 1].describe())
print ()
print ("Normal Stats Summary")
print (data_cr["Amount"][data_cr["Class"] == 0].describe())

從上圖可以看出,在信用卡被盜刷樣本中,離群值發生在客戶使用信用卡消費更低頻的時間段。信用卡被盜刷數量案發最高峰在第一天上午11點達到43次,其余發生信用卡被盜刷案發時間在晚上時間11點至第二早上9點之間,說明信用卡盜刷者為了不引起信用卡卡主注意,更喜歡選擇信用卡卡主睡覺時間和消費頻率較高的時間點作案;同時,信用卡發生被盜刷的最大值也就只有2,125.87美元。
#Select only the anonymized features. v_feat = data_cr.ix[:,1:29].columns plt.figure(figsize=(16,28*4)) gs = gridspec.GridSpec(28, 1) for i, cn in enumerate(data_cr[v_feat]): ax = plt.subplot(gs[i]) sns.distplot(data_cr[cn][data_cr["Class"] == 1], bins=50) sns.distplot(data_cr[cn][data_cr["Class"] == 0], bins=100) ax.set_xlabel('') ax.set_title('histogram of feature: ' + str(cn))














上圖是不同變量在信用卡被盜刷和信用卡正常的不同分布情況,我們將選擇在不同信用卡狀態下的分布有明顯區別的變量。因此剔除變量V8、V13 、V15 、V20 、V21 、V22、 V23 、V24 、V25 、V26 、V27 和V28變量。這也與我們開始用相關性圖譜觀察得出結論一致。同時剔除變量Time,保留離散程度更小的Hour變量。
droplist = ['V8', 'V13', 'V15', 'V20', 'V21', 'V22', 'V23', 'V24', 'V25', 'V26', 'V27', 'V28','Time'] data_new = data_cr.drop(droplist, axis = 1) data_new.shape # 查看數據的維度

特征從31個縮減至18個(不含目標變量)。
- 特征縮放
由於特征Hour和Amount的規格和其他特征相差較大,因此我們需對其進行特征縮放。
# 對Amount和Hour 進行特征縮放 col = ['Amount','Hour'] from sklearn.preprocessing import StandardScaler # 導入模塊 sc =StandardScaler() # 初始化縮放器 data_new[col] =sc.fit_transform(data_new[col])#對數據進行標准化 data_new.head()

- 對特征的重要性進行排序
構建X變量和Y變量。
x_feature = list(data_new.columns) x_feature.remove('Class') x_val = data_new[x_feature] y_val = data_new['Class']
利用隨機森林的feature importance對特征的重要性進行排序。
names = data_cr[x_feature].columns from sklearn.ensemble import RandomForestClassifier clf=RandomForestClassifier(n_estimators=10,random_state=123)#構建分類隨機森林分類器 clf.fit(x_val, y_val) #對自變量和因變量進行擬合 names, clf.feature_importances_ for feature in zip(names, clf.feature_importances_): print(feature)

plt.style.use('fivethirtyeight') plt.rcParams['figure.figsize'] = (12,6) ## feature importances 可視化## importances = clf.feature_importances_ feat_names = names indices = np.argsort(importances)[::-1] fig = plt.figure(figsize=(20,6)) plt.title("Feature importances by RandomTreeClassifier") plt.bar(range(len(indices)), importances[indices], color='lightblue', align="center") plt.step(range(len(indices)), np.cumsum(importances[indices]), where='mid', label='Cumulative') plt.xticks(range(len(indices)), feat_names[indices], rotation='vertical',fontsize=14) plt.xlim([-1, len(indices)])

4 模型訓練

- 處理樣本不平衡
前面提到,目標列Class呈現較大的樣本不平衡,會對模型學習造成困擾。樣本不平衡常用的解決方法有過采樣和欠采樣,本項目處理樣本不平衡采用的是過采樣的方法,具體操作使用SMOTE(Synthetic Minority Oversampling Technique)。關於處理樣本不平衡的方法介紹,可以參考我上一篇報告《Lending Club——構建貸款違約預測模型》對樣本不平衡處理的方法介紹。
# 構建自變量和因變量 X = data_cr[x_feature] y = data_cr["Class"] n_sample = y.shape[0] n_pos_sample = y[y == 0].shape[0] n_neg_sample = y[y == 1].shape[0] print('樣本個數:{}; 正樣本占{:.2%}; 負樣本占{:.2%}'.format(n_sample, n_pos_sample / n_sample, n_neg_sample / n_sample)) print('特征維數:', X.shape[1])

from imblearn.over_sampling import SMOTE # 導入SMOTE算法模塊 # 處理不平衡數據 sm = SMOTE(random_state=42) # 處理過采樣的方法 X, y = sm.fit_sample(X, y) print('通過SMOTE方法平衡正負樣本后') n_sample = y.shape[0] n_pos_sample = y[y == 0].shape[0] n_neg_sample = y[y == 1].shape[0] print('樣本個數:{}; 正樣本占{:.2%}; 負樣本占{:.2%}'.format(n_sample, n_pos_sample / n_sample,

- 構建分類器進行訓練
from sklearn.linear_model import LogisticRegression
clf1 = LogisticRegression() # 構建邏輯回歸分類器
clf1.fit(X, y)

predicted1 = clf.predict(X) # 通過分類器產生預測結果 print("Test set accuracy score: {:.5f}".format(accuracy_score(predicted1, y,)))

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) thresh = 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] > thresh else "black") plt.tight_layout() plt.ylabel('True label') plt.xlabel('Predicted label') ################################################################################## # Compute confusion matrix cnf_matrix = confusion_matrix(y, predicted1) # 生成混淆矩陣 np.set_printoptions(precision=2) print("Recall metric in the testing dataset: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1])) # Plot non-normalized confusion matrix class_names = [0,1] plt.figure() plot_confusion_matrix(cnf_matrix , classes=class_names , title='Confusion matrix') plt.show()

y_pred1_prob = clf1.predict_proba(X)[:, 1] # 閾值默認值為0.5
fpr, tpr, thresholds = roc_curve(y,y_pred1_prob)
roc_auc = auc(fpr,tpr)
# 繪制 ROC曲線
plt.title('Receiver Operating Characteristic')
plt.plot(fpr, tpr, 'b',label='AUC = %0.5f'% roc_auc)
plt.legend(loc='lower right')
plt.plot([0,1],[0,1],'r--')
plt.xlim([-0.1,1.0])
plt.ylim([-0.1,1.01])
plt.ylabel('True Positive Rate')
plt.xlabel('False Positive Rate')
plt.show()

5 模型評估與優化
上一個步驟中,我們的模型訓練和測試都在同一個數據集上進行,這樣導致模型產生過擬合的問題。
一般來說,將數據集划分為訓練集和測試集有3種處理方法:1、留出法(hold-out),2、交叉驗證法(cross-validation),3、自助法(bootstrapping)
本次項目采用的是交叉驗證法划分數據集,將數據划分為3部分:訓練集(training set)、驗證集(validation set)和測試集(test set)。讓模型在訓練集進行學習,在驗證集上進行參數調優,最后使用測試集數據評估模型的性能。
模型調優我們采用網格搜索調優參數(grid search),通過構建參數候選集合,然后網格搜索會窮舉各種參數組合,根據設定評定的評分機制找到最好的那一組設置。
結合cross-validation和grid search,具體操作我們采用scikit learn模塊model_selection中的GridSearchCV方法。關於GridSearchCV的更多介紹可以參考我上一篇報告《Lending Club——構建貸款違約預測模型》。
- cross-validation+grid search
X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.3, random_state = 0) # random_state = 0 每次切分的數據都一樣 # 構建參數組合 param_grid = {'C': [0.01,0.1, 1, 10, 100, 1000,], 'penalty': [ 'l1', 'l2']} grid_search = GridSearchCV(LogisticRegression(), param_grid, cv=10) # 確定模型LogisticRegression,和參數組合param_grid ,cv指定10折 grid_search.fit(X_train, y_train) # 使用訓練集學習算法

results = pd.DataFrame(grid_search.cv_results_) best = np.argmax(results.mean_test_score.values) print("Best parameters: {}".format(grid_search.best_params_)) print("Best cross-validation score: {:.5f}".format(grid_search.best_score_))

y_pred = grid_search.predict(X_test) print("Test set accuracy score: {:.5f}".format(accuracy_score(y_test, y_pred,)))

print(classification_report(y_test, y_pred))


print("Best parameters: {}".format(grid_search.best_params_))
print("Best cross-validation score: {:.5f}".format(grid_search.best_score_))

對混淆矩陣可視化。
# Compute confusion matrix
cnf_matrix = confusion_matrix(y_test, y_pred) # 生成混淆矩陣
np.set_printoptions(precision=2)
print("Recall metric in the testing dataset: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))
# Plot non-normalized confusion matrix
class_names = [0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix
, classes=class_names
, title='Confusion matrix')
plt.show()

從上可以看出,經過交叉驗證訓練和參數調優后,模型的性能有較大的提升,recall值從0.80上升到0.93,上升幅度達到16.25%。
- 模型評估
解決不同的問題,通常需要不同的指標來度量模型的性能。例如我們希望用算法來預測癌症是否是惡性的,假設100個病人中有5個病人的癌症是惡性,對於醫生來說,盡可能提高模型的查全率(recall)比提高查准率(precision)更為重要,因為站在病人的角度,發生漏發現癌症為惡性比發生誤判為癌症是惡性更為嚴重。
y_pred_proba = grid_search.predict_proba(X_test) #predict_prob 獲得一個概率值 thresholds = [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9] # 設定不同閾值 plt.figure(figsize=(15,10)) j = 1 for i in thresholds: y_test_predictions_high_recall = y_pred_proba[:,1] > i#預測出來的概率值是否大於閾值 plt.subplot(3,3,j) j += 1 # Compute confusion matrix cnf_matrix = confusion_matrix(y_test, y_test_predictions_high_recall) np.set_printoptions(precision=2) print("Recall metric in the testing dataset: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1])) # Plot non-normalized confusion matrix class_names = [0,1] plot_confusion_matrix(cnf_matrix , classes=class_names


from itertools import cycle thresholds = [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9] colors = cycle(['navy', 'turquoise', 'darkorange', 'cornflowerblue', 'teal', 'red', 'yellow', 'green', 'blue','black']) plt.figure(figsize=(12,7)) j = 1 for i,color in zip(thresholds,colors): y_test_predictions_prob = y_pred_proba[:,1] > i #預測出來的概率值是否大於閾值 precision, recall, thresholds = precision_recall_curve(y_test, y_test_predictions_prob) area = auc(recall, precision) # Plot Precision-Recall curve plt.plot(recall, precision, color=color, label='Threshold: %s, AUC=%0.5f' %(i , area)) plt.xlabel('Recall') plt.ylabel('Precision') plt.ylim([0.0, 1.05]) plt.xlim([0.0, 1.0]) plt.title('Precision-Recall Curve') plt.legend(loc="lower left")

- 最優閾值
precision和recall是一組矛盾的變量。從上面混淆矩陣和PRC曲線可以看到,閾值越小,recall值越大,模型能找出信用卡被盜刷的數量也就更多,但換來的代價是誤判的數量也較大。隨着閾值的提高,recall值逐漸降低,precision值也逐漸提高,誤判的數量也隨之減少。通過調整模型閾值,控制模型反信用卡欺詐的力度,若想找出更多的信用卡被盜刷就設置較小的閾值,反之,則設置較大的閾值。
實際業務中,閾值的選擇取決於公司業務邊際利潤和邊際成本的比較;當模型閾值設置較小的值,確實能找出更多的信用卡被盜刷的持卡人,但隨着誤判數量增加,不僅加大了貸后團隊的工作量,也會降低誤判為信用卡被盜刷客戶的消費體驗,從而導致客戶滿意度下降,如果某個模型閾值能讓業務的邊際利潤和邊際成本達到平衡時,則該模型的閾值為最優值。當然也有例外的情況,發生金融危機,往往伴隨着貸款違約或信用卡被盜刷的幾率會增大,而金融機構會更願意不惜一切代價守住風險的底線。
