非均衡样本下的信用卡欺诈分析
本文是针对一份kaggle上信用卡数据的建模分析,主要内容包含:
- 理解数据:通过直方图、箱型图等辅助理解数据分布
- 预处理:归一化和分布情况;数据分割
- 随机采样:上采样和下采样,主要是欠采样(下采样)
- 异常检测:如何从数据中找到异常点,并且进行删除
- 数据建模:利用逻辑回归和神经网络进行建模分析
- 模型评价:分类模型的多种评价指标
原notebook地址为:https://www.kaggle.com/code/janiobachmann/credit-fraud-dealing-with-imbalanced-datasets/notebook
所谓的【非均衡】:信用卡数据中欺诈和非欺诈的比例是不均衡的,肯定是非欺诈的比例占据绝大多数。本文提供一种方法:如何处理这种极度不均衡的数据
导入库
导入各种库和包:绘图、特征工程、降维、分类模型、评价指标相关等
1 | import numpy as np |
基本信息
读取数据,查看基本信息
数据的形状如下:
In [3]:
1 | df.shape |
Out[3]:
1 | (284807, 31) |
In [4]:
1 | # 缺失值的最大值 |
Out[4]:
1 | 0 |
结果表明是没有缺失值的。
下面是查看数据中字段的相关类型,我们发现有30个float64类型,1个int64类型
In [5]:
1 | pd.value_counts(df.dtypes) |
Out[5]:
1 | float64 30 |
In [6]:
1 | columns = df.columns |
Out[6]:
1 | Index(['Time', 'V1', 'V2', 'V3', 'V4', 'V5', 'V6', 'V7', 'V8', 'V9', 'V10', |
查看数据的统计信息:
1 | df.describe() |
正负样本不均衡
In [8]:
1 | df["Class"].value_counts(normalize=True) |
Out[8]:
1 | 0 0.998273 # 不欺诈 |
我们发现属于0类的样本远高于属于1的样本,非常地不均衡。这就是本文重点关注的问题。
In [9]:
1 | # 绘图 |
通过柱状图也能够明显观察到非欺诈-0 和 欺诈-1的比例是极度不均衡的。
查看特征分布
部分特征的分布,发现存在偏态状况:
直方图分布
In [10]:
1 | fig, ax = plt.subplots(1,2,figsize=(18,6)) |
观察两个字段Amount和Time在不同取值下的分布情况,发现:
- Amount的偏态现象严重,极大多数的数据集中在左侧
- Time中,数据主要集中在两个阶段
特征分布箱型图
查看每个特征取值的箱型图:
1 | # 两个基本参数:设置行、列 |
数据预处理
数据缩放和分布
针对Amount和Time字段的归一化操作。原始数据中的其他字段已经进行了归一化的操作。
- StandardScaler:将数据减去均值除以标准差
- RobustScaler:如果数据有离群点,有对数据中心化和数据的缩放鲁棒性更强的参数
In [13]:
1 | from sklearn.preprocessing import StandardScaler, RobustScaler |
In [14]:
删除原始字段,使用归一化后的字段和数据
1 | df['Amount'].values.reshape(-1,1) # 个人添加 |
技巧1:新字段位置
将新生成的字段放在最前面
1 | # 把两个缩放的字段放在最前面 |
分割数据(基于原DataFrame)
在开始进行随机欠采样之前,我们需要将原始数据进行分割。
尽管我们会对数据进行欠采样和上采样,但是我们希望在测试的时候,仍然是使用原始的数据集(原来的数据顺序)
In [18]:
1 | from sklearn.model_selection import train_test_split |
查看Class中0-no fraud和1-fraud的比例:
In [19]:
1 | df["Class"].value_counts(normalize=True) |
Out[19]:
1 | 0 0.998273 |
生成特征数据集X和标签数据y:
In [20]:
1 | X = df.drop("Class", axis=1) |
In [21]:
技巧2:生成随机索引
1 | sfk = StratifiedKFold( |
将生成的数据转成numpy数组:
In [22]:
1 | original_Xtrain = original_X_train.values |
查看训练集 original_ytrain 和 original_ytest 的唯一值以及每个唯一值所占的比例:
In [23]:
技巧3:数据唯一值及比例
1 | # 训练集 |
In [24]:
1 | print(train_counts_label / len(original_ytrain)) |
欠采样
原理
欠采样也称之为下采样,主要是通过删除原数据中类别较多的数据,从而和类别少的数据达到平衡,以免造成模型的过拟合。
步骤
- 确定数据不平衡度是多少:通过value_counts()来统计,查看每个类别的数量和占比
- 在本例中一旦我们确定了fraud的数量,我们就需要将no-fraud的数量采样和其相同,形成50%:50%
- 实施采样之后,随机打乱采样的子样本
缺点
下采样会造成数据信息的缺失。比如原数据中no-fraud有284315条数据,但是经过欠采样只有492,大量的数据被放弃了。
实施采样
取出欺诈的数据,同时从非欺诈中取出相同长度的数据:
1 | # 欺诈的数据 |
均匀分布
现在我们发现样本是均匀的:
In [28]:
1 |
|
Out[28]:
1 | 1 492 |
In [29]:
1 | # 显示比例 |
Out[29]:
1 | 1 0.5 |
In [30]:
当我们再次查看数据分布的时候发现:已经是均匀分布了
1 | sns.countplot("Class", |
相关性分析
相关性分析主要是通过相关系数矩阵来实现的。下面绘制基于原始数据和欠采样数据的相关系数矩阵图:
系数矩阵热力图
In [31]:
1 | f, (ax1, ax2) = plt.subplots(2,1,figsize=(24, 20)) |
小结:
- 正相关:特征V2、V4、V11、V19是正相关的。值越大,结果越可能出现fraud
- 负相关:特征V17, V14, V12 和 V10 是负相关的;值越小,结果越可能出现fraud
箱型图
In [32]:
负相关的特征箱型图
1 | # 负相关的数据 |
正相关特征的箱型图:
1 | # 正相关 |
异常检测
目的
异常检测的目的主要是:发现数据中的离群点来进行删除。
方法
- IQR:我们通过第75个百分位和第25个百分位之间的差异来计算。我们的目标是创建一个超过第75和 25 个百分位的阈值,以防某些实例超过此阈值,如果超过阈值该实例将被删除。
- 箱型图boxplot:除了很容易看到第 25 和第 75 个百分位数(正方形的两端)之外,还很容易看到极端异常值(超出下限和上限的点)
异常值去除权衡
在通过四分位法删除异常值的时候,我们通过将一个数字(例如1.5)乘以(四分位距)来确定阈值。该阈值越高,检测到的异常值越少,反之检测到的异常值越多。这个比例(例如1.5)我们可以在实际进行控制
特征分布-直方图
In [34]:
1 | # 查看3个特征的分布 |
技巧4:删除离群点
删除3个特征下的离群点,以V12为例:
In [35]:
第一步先确定上下分位数的值:
1 | # 数组 |
In [36]:
1 | # 确定上下限 |
In [37]:
找出满足要求的离群点的数据
1 | # 确定离群点 |
对单个列字段的数据执行下面删除离群点的操作:
In [38]:
1 | # 技巧:如何删除异常值 |
对其他的特征执行相同的操作:
可以看到:欠采样之后的数据原本是984,现在变成了978条数据,删除了6个离群点的数据
In [39]:
1 | # 对V10和V14执行同样的操作 |
In [40]:
1 | # 对V10和V14执行同样的操作 |
查看删除了异常点后的数据:
In [42]:
1 | f, (ax1, ax2, ax3) = plt.subplots(1,3,figsize=(20,10)) |
降维和聚类
理解t-SNE
详细地址:https://www.youtube.com/watch?v=NEaUSP4YerM
欠采样数据降维
对3种不同方法实施欠采样:
In [43]:
1 | X = new_df.drop("Class", axis=1) |
In [44]:
1 | # PCA降维 |
In [45]:
1 | # TruncatedSVD降维 |
绘图
In [46]:
1 | f, (ax1, ax2, ax3) = plt.subplots(1,3,figsize=(24,6)) |
基于欠采样的分类建模
4个分类模型
采用4个不同模型的分类来训练数据,看哪个模型在欺诈数据上表现的更好。首先需要对数据进行划分:训练集和测试集
In [47]:
1 |
|
In [48]:
1 | # 2、数据已经归一化,直接切分 |
In [49]:
1 | # 3、将数据转成数组,然后传给模型 |
In [50]:
1 | # 4、创建4个模型 |
网格搜索
针对不同测模型实施网格搜索,寻找最优参数
In [51]:
1 | from sklearn.model_selection import GridSearchCV |
Out[51]:
1 | LogisticRegression(C=0.1) |
In [52]:
1 | # k近邻 |
Out[52]:
1 | KNeighborsClassifier(n_neighbors=2) |
In [53]:
1 | # 支持向量机分类 |
Out[53]:
1 | SVC(C=0.9, kernel='linear') |
In [54]:
1 | # 决策树 |
Out[54]:
1 | DecisionTreeClassifier(max_depth=3, min_samples_leaf=5) |
重新训练并评分
基于最优参数重新计算得分:
In [55]:
1 | lr_score = cross_val_score(best_para_lr, X_train, y_train,cv=5) |
In [56]:
1 | knn_score = cross_val_score(best_para_knn, X_train, y_train,cv=5) |
In [57]:
1 | svc_score = cross_val_score(best_para_svc, X_train, y_train,cv=5) |
In [58]:
1 | dt_score = cross_val_score(best_para_dt, X_train, y_train,cv=5) |
小结:通过不同模型的交叉验证得分我们发现,逻辑回归模型是最高的
基于欠采样数据的交叉验证
主要是基于Near-Miss算法来实现欠采样:
- Near-miss-1:选择到最近的三个样本平均距离最小的多数类样本
- Near-miss-2:选择到最远的三个样本平均距离最小的多数类样本
- Near-miss-3:为每个少数类样本选择给定数目的最近多数类样本
- 最远距离:选择到最近的三个样本平均距离最大的多样类样本
In [59]:
1 | undersample_X = df.drop("Class", axis=1) |
使用近邻缺失Near-Miss算法来查看数据分布:
In [60]:
1 | undersample_X = df.drop("Class", axis=1) |
以网格搜索过后的逻辑回归模型来实施交叉验证:
In [61]:
1 | for train, test in sfk.split(undersample_Xtrain, undersample_ytrain): |
绘制学习曲线
In [62]:
1 | from sklearn.model_selection import ShuffleSplit, learning_curve |
In [63]:
1 | def plot_learning_curve(est1,est2,est3,est4,X,y,ylim=None,cv=None,n_jobs=1,train_sizes=np.linspace(0.1, 1, 5)): |
In [64]:
1 | cv = ShuffleSplit(n_splits=100, |
roc曲线
In [65]:
1 | from sklearn.metrics import roc_curve, roc_auc_score |
In [66]:
1 | lr_pred = cross_val_predict(best_para_lr, |
In [67]:
1 | print('Logistic Regression: ', roc_auc_score(y_train, lr_pred)) |
In [68]:
1 | log_fpr, log_tpr, log_thresold = roc_curve(y_train, lr_pred) |
探索逻辑回归评价指标
探索在逻辑回归模型的分类评价指标:
In [69]:
1 | def logistic_roc_curve(log_fpr, log_tpr): |
1 | from sklearn.metrics import precision_recall_curve |
In [71]:
1 | from sklearn.metrics import recall_score, precision_score, f1_score, accuracy_score |