手动反爬虫: 原博地址
知识梳理不易,请尊重劳动成果,文章仅发布在CSDN网站上,在其他网站看到该博文均属于未经作者授权的恶意爬取信息
如若转载,请标明出处,谢谢!
前言
对于信用卡欺诈的分类项目,一直想找个时间进行梳理一下,国庆假期结束第一天,终于抽空来填补一下这个大坑了,文章主要是采用逻辑回归对信用卡欺诈进行检测,比较详细的梳理了整个过程和进行建模前后的一些注意事项,重点在于混淆矩阵的理解,文中也举了三个很接地气的场景辅助理解
1. 加载数据,观察问题
数据来源于已经脱敏的数据,上传至资源,首先导入相关的库进行数据的读取
import warnings
warnings.filterwarnings('ignore')
import pandas as pd
import matplotlib.pyplot as plt
import numpy as np
%matplotlib inline
import os
os.chdir(r'C:\Users\86177\Desktop\机器学习案例')
data = pd.read_csv('creditcard.csv')
print('总数据数量:{}\n'.format(len(data)))
data.head()
输出结果为:(其中v1-v28代表着用户的交易特征是已经脱敏后的数据,Amount字段就代表着这笔交易的总金额,最后的Class就是实际的交易情况代表着正常或者不正常,不正常为1,正常为0,关于Time字段没有具体解释的含义,可以暂时不考虑,也就是在面对数据时,并不是所有的字段都是可以拿来用的,无用的数据可以进行舍弃)
总数据数量:284807
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
首先查看一下要用到的字段特征总共有v1-v28和Amount,可以发现都是属于数值型数据,因此这里就不用进行数据类型转化了,但是可以发现数值的大小却存在着很大的变化,因此为了避免大数值数据的影响,这里首先应该将Amount字段的数据转化;其次就是观察一下标签的分布情况
#简单的看一下标签的数值分布情况
data.Class.value_counts().plot(kind = 'bar',rot = 0)
plt.title('Fraud class distribution')
plt.ylabel('Class')
plt.xlabel('Num')
输出结果为:(发现标签为1的数量几乎没有,因此可以打印输出看一下)
统计某个字段的数据分布情况可以有两种方式
#第一种方式
pd.value_counts(data.Class).sort_index()
#第二种方式
data.Class.value_counts().sort_index()
输出结果为:(里面有sort参数进行数据的升序或者降序排列)
0 284315
1 492
Name: Class, dtype: int64
查看标签分布的结果,那么问题就出来了,这里的0和1差距特别大,可以对比学习数学和体育的情况,当考试的时候几乎每次都是体育得满分,而数学一直不理想的时候,那么在进行学习的时候就会有意识的往体育这边靠,对比要创建的模型的标签,也是同理因为0的标签数据特别大,相当于很大程度上一直在进行学习到的是0的特征,所以在进行预测的时候,就会把输入的特征当做标签为0的进行输出
2. 针对问题给出解决方案
解决方案:将标签0和1的数据转化为一致然后再进行模型输入的训练
第一种方案:标签0数据和标签1数据一样少(降采样 under sample)
第二种方案:标签1数据和标签0数据一样多(过采样 over sample)
那么究竟哪种方案比较好,就需要都进行实践最终进行比较才知道,那么接下来就需要进行数据预处理了,将Amount字段特征转化为同等范围内的数值(也就是标准化,常用的有两种,一种是0-1标准化,还有一种是Z-score标准化)
首先是Z-score标准化
#Z-score : 新数据=(原数据-均值)/标准差
#如果手写标准化
data['Amount_norm'] = (data['Amount'] - data['Amount'].mean())/ (data['Amount'].std())
#如果使用sklearn中的标准化
from sklearn.preprocessing import StandardScaler
data['norm_Amount'] = StandardScaler().fit_transform(data['Amount'].values.reshape(-1,1))
data[['Amount_norm','norm_Amount']]
输出的结果为:
如果是0-1标准化的话(这里只是介绍,根据v1-v28的特征取值的分布情况,可以知道是经过Z-score标准化后的结果,这里拓展一下,如果处理的特征都是在0-1之间的话,就需要使用到0-1标准化)
#Min-max 标准化:新数据=(原数据-最小值)/(最大值-最小值)
#手写0-1标准化
data['norm_min_max'] = (data['Amount'] - data['Amount'].min())/ (data['Amount'].max() - data['Amount'].min())
#使用sklearn中工具包
from sklearn.preprocessing import MinMaxScaler
data['min_max_norm'] = MinMaxScaler().fit_transform(data['Amount'].values.reshape(-1,1))
data[['norm_min_max','min_max_norm']]
输出的结果为
至此数据标准化的过程就处理完了(因为v1-v28已经经过标准化脱敏了,所以这里就不用再进行了),就需要删除一些无效的数据进行接下来的数据切分
#0-1标准化的过程这里只是进行示例,只需要保留Z-score标准化的结果就可以了
data.drop(['Time','Amount','norm_Amount','norm_min_max','min_max_norm'],axis = 1,inplace = True)
data.head()
输出的结果为:(相当于删除了Time和Amount字段,然后增加了Amount_norm字段)
V1 V2 V3 V4 V5 V6 V7 V8 V9 v10 ... V21 V22 V23 V24 V25 V26 V27 V28 Class Amount_norm
0 -1.359807 -0.072781 2.536347 1.378155 -0.338321 0.462388 0.239599 0.098698 0.363787 0.090794 ... -0.018307 0.277838 -0.110474 0.066928 0.128539 -0.189115 0.133558 -0.021053 0 0.244964
1 1.191857 0.266151 0.166480 0.448154 0.060018 -0.082361 -0.078803 0.085102 -0.255425 -0.166974 ... -0.225775 -0.638672 0.101288 -0.339846 0.167170 0.125895 -0.008983 0.014724 0 -0.342474
2 -1.358354 -1.340163 1.773209 0.379780 -0.503198 1.800499 0.791461 0.247676 -1.514654 0.207643 ... 0.247998 0.771679 0.909412 -0.689281 -0.327642 -0.139097 -0.055353 -0.059752 0 1.160684
3 -0.966272 -0.185226 1.792993 -0.863291 -0.010309 1.247203 0.237609 0.377436 -1.387024 -0.054952 ... -0.108300 0.005274 -0.190321 -1.175575 0.647376 -0.221929 0.062723 0.061458 0 0.140534
4 -1.158233 0.877737 1.548718 0.403034 -0.407193 0.095921 0.592941 -0.270533 0.817739 0.753074 ... -0.009431 0.798278 -0.137458 0.141267 -0.206010 0.502292 0.219422 0.215153 0 -0.073403
3. 数据集切分
根据上面提到了两种方案,首先进行降采样实践
第一种实现方式:根据异常数据的个数然后随机获取相同数目的正常数据的索引,然后将正常数据的索引和非正常数据的索引合并,提取对应的数据即可(针对于numpy熟练的玩家,可以这么整,容易理解)
# 得到所有异常样本的索引
number_records_fraud = len(data[data.Class == 1])
fraud_indices = np.array(data[data.Class == 1].index)
# 得到所有正常样本的索引
normal_indices = data[data.Class == 0].index
# 在正常样本中随机采样出指定个数的样本,并取其索引
random_normal_indices = np.random.choice(normal_indices, number_records_fraud, replace = False) #注意replace参数的使用
random_normal_indices = np.array(random_normal_indices)
# 有了正常和异常样本后把它们的索引都拿到手
under_sample_indices = np.concatenate([fraud_indices,random_normal_indices])
# 根据索引得到降采样所有样本点
under_sample_data = data.iloc[under_sample_indices,:]
#再将获得的降采样的数据进行特征和标签的划分
X_undersample = under_sample_data.iloc[:, under_sample_data.columns != 'Class']
y_undersample = under_sample_data.iloc[:, under_sample_data.columns == 'Class']
# 降采样 样本比例
print("正常样本所占整体比例: ", len(under_sample_data[under_sample_data.Class == 0])/len(under_sample_data))
print("异常样本所占整体比例: ", len(under_sample_data[under_sample_data.Class == 1])/len(under_sample_data))
print("降采样策略总体样本数量: ", len(under_sample_data))
输出的结果为:
正常样本所占整体比例: 0.5
异常样本所占整体比例: 0.5
降采样策略总体样本数量: 984
第二种实现方式:获取不正常标签数据的个数,然后直接使用sample,再进行数据的合并即可(针对于pandas熟悉的玩家,也是比较推荐的方式,易操作)
#直接使用dataframe采样,不用索引取值
normal_data = data[data.Class == 0]
fraud_data = data[data.Class == 1]
under_sample_normal_data = normal_data.sample(len(fraud_data))
under_sample_data = pd.concat([fraud_data,under_sample_normal_data])
print("正常样本所占整体比例: ", len(under_sample_data[under_sample_data.Class == 0])/len(under_sample_data))
print("异常样本所占整体比例: ", len(under_sample_data[under_sample_data.Class == 1])/len(under_sample_data))
print("降采样策略总体样本数量: ", len(under_sample_data))
输出的结果为:(这种方式简单有效,而且执行效率比较高,运行速度也比较快)
正常样本所占整体比例: 0.5
异常样本所占整体比例: 0.5
降采样策略总体样本数量: 984
因此就可以查看一下标签数据是不是一样多了,核实一下
数据采样结束后就可以进行切分了,那么为什么要进行切分呢?
举个例子:就像考研英语一样,首先刷的是最近三-五年以外的试卷真题(如果考英语一的就是直接拿英语二的真题来刷),而最后进行冲刺检验的时候才开始刷最近几年的真题。那么这里的数据划分也是一样的道理,测试集是非常宝贵的,就像最近几年的真题,不能上来就去做,不然就是糟蹋了资源。
如果没有经历过考研,可以类比一下平时的练习、模拟考试和高考。假使有100套题,平时使用的80套,最后的检验20套,测试集数据就相当于最终的高考试卷,那么只有一次机会(复读的话再说),就是真正可以检测出水平,因此这部分数据不要轻易动,只有最后才能使用,也就是20套题用来最后随机抽一套用来高考。那么平时学习的时候也需要进行成绩的评估,来进行检测,因此也就要有模拟考试,这样就需要将剩下的80套题中分出一部分来作为模拟考试的试卷,而最后的就是作为训练集的试卷了,所以训练集、测试集和验证集就可以对比高考的这个过程如下
数据集 | 题库试卷 |
---|---|
训练集 | 日常练习试卷(80%的数据中的80%) |
验证集 | 模拟考试试卷(80%的数据中的20%) |
测试集 | 高考考试试卷(20%的数据) |
需要将宝贵的测试集部分划出来这一步是毫无疑问的,那么还有一个问题就是针对测试集以外的数据(也就是训练集和验证集),划分的方式,能不能还按照固定的20%和80%进行划分呢,这里就有一个问题:如果划分的这20%试卷的难度都比较简单或者都比较难,就会导致每次测验的结果会想两级进行偏转,也就造成模型效果差,为了避免这种情况就需要有种方法来解决这个问题————交叉验证
交叉验证整个过程就可以进行下面的图例进行解答(假定100进行切分,以下只是切分的一种情况)
那么最后一个问题具体的划分数据的比例如何确定呢?一定是要二八开吗?这个是不确定的,需要根据数据量的大小来决定,8:2,7:3,9:1一般都是比较常见的,还有特殊的情况,比如数据量特别大的时候后也可以利用上面提到的pandas的sample方法取指定长度的数据,不需要一定要满足特定比例(假使数据有3000000条,10%就是30w了,可能由于机器的性能,这里可以指定取5w条数据进行测试就可以了)
接着就是开始切分了,注意是train_test_split
针对测试集和非测试集的划分(也叫留出法,按照一定比例切分数据,交叉验证在此之后进行),特别留意由于是随机的切分,要进行降采样和过采样的对比分析,这里指定切分的方式和随机种子两个参数,需要确保数据输入是一样的,这样才能将两种方案的结果进行对比,否则就没有可比性
from sklearn.model_selection import train_test_split
#取出所有的特征数据和标签数据
X = data.iloc[:, data.columns != 'Class']
y = data.iloc[:, data.columns == 'Class']
#整个数据集进行划分,注意random_state一定要设置一样的,因为要进行过采样的对比
X_train, X_test, y_train, y_test = train_test_split(X,y,test_size = 0.3, random_state = 0)
print('初始训练集包含样本数量:',len(X_train))
print('初始测试集包含样本数量:',len(X_test))
print('初始样本总数:',len(X_train) + len(X_test))
#降采样数据进行划分
X_train_undersample, X_test_undersample, y_train_undersample, y_test_undersample = train_test_split(
X_undersample,y_undersample,test_size = 0.3,random_state = 0
)
print("")
print("降采样训练集包含样本数量: ", len(X_train_undersample))
print("降采样测试集包含样本数量: ", len(X_test_undersample))
print("降采样样本总数: ", len(X_train_undersample)+len(X_test_undersample))
输出的结果为:(先思考一下为啥还要将原始的数据集进行切分呢?后面会进行解答)
初始训练集包含样本数量: 199364
初始测试集包含样本数量: 85443
初始样本总数: 284807
降采样训练集包含样本数量: 688
降采样测试集包含样本数量: 296
降采样样本总数: 984
4. 评估方法对比
可以参考一下分类模型的常用评价指标,这里再详细介绍一下混淆矩阵,举个例子进行
假如某班级有男生80人,女生20人,共100人。目标是找出所有的女生。现在某人挑选出50人,其中20人是女生,另外还错误的把30个男生当做女生挑选出来了,结合混淆矩阵的列表分析
相关(Relevant),正类 | 无关(NonRelevant),负类 | |
---|---|---|
被检索到(Retrieved) | True Positive(TP) 正类判定为正类,例子中就是正确的判定“这位是女生 ” | False Positive(FP) 负类判定为正类,“存伪”,例子中就是分明为男生却判定为女生,当下的“伪娘”行为,这个错常有人犯 |
未被检索到(Not Retrieved) | False Negatives(FN) 正类判定为负类,“去真”,例子中就是分明是女生,结果判定为男生,比如梁山伯 | True Negatives(TN) 负类判定为负类,也就是一个男生被判定为男生,比如这种纯爷们的,就一定得判断在此处 |
结合示例,得到四个值分别为:TP=20, FP=30, FN=0, TN=50
准确率(Accuracy):正确分类样本占总样本的比例
查准率(Precision):(TP/(TP+FP))= 20/(20+30) = 40%
召回率(Recall):(TP/(TP+FN))= 20/(20+0) = 100%
特异度(Specificity):(TN/(TN+FP))=50/(50+30) = 62.5%
对于准确率是常遇见的评估方法,那么剩下三个在什么时候用?这里列举三个场景供大家理解
第一个场景就是当前贴近生活的新冠病毒的检测,经过检测后就会得到阳性(感染)或者阴性(未感染),在这种情况下三个指标会更倾向于关注哪个?
查准率就是:在检测阳性的人中有多少人真的感染了;
召回率就是:在感染的人中,有多少人是被正确的检测出来了;
特异度:在未感染的人中,正确预测了多少。
个人答案就是:关注检测的这些人究竟是哪些人员是真正的感染者,也就是携带病毒的人,这样的话就可以及时隔离,防止病毒的扩散。如果是查准率,也就是结果里面存在着“假阳性FP”,相当于是没有患病的却被确诊患病,但是这并不影响病毒的扩散,因为这部分人是无害的,如果使用特异度这个指标偏向未感染的人员了,而最后这个召回率就代表这存在有没有被检测出来的携带病毒的人员,那么这部分人员就相当于一个移动的炸弹,因此需要格外注意。最终可以确定该场景下应该选择的评估方法为召回率
第二个场景就是邮件分类,如何采用指标进行是否为垃圾邮件的判定,在这种情况下三个指标会更倾向于关注哪个?(如果分类器判断为垃圾邮件就会直接被扔掉)
个人答案就是查准率,如果出现了“假阴性FN”,说明垃圾邮件被当做正常邮件发送出去了,但是这并不影响啥,放在那也行,手动再删除可以,如果是“假阳性FP”,那就说明正常邮件被当做垃圾邮件了给丢了,很可能这是一封工作面试通知单,或者交易验证码,就会导致很大的问题,所以这里要求的就是查准率,不要把正常邮件给错分了
第三个场景就是法官判决,如何采用指标进行是否为犯罪的判定,在这种情况下三个指标会更倾向于关注哪个?
个人答案就是特异度,这个场景中注重的是法制精神同时也要体验出人文情怀,不希望把无罪人认定为有罪,更不要将有罪的人判定为无辜而放过,应关注的就是那些无罪的就是无罪,也就是避免给无辜的人来定罪,哪怕有一部分人会因此逍遥法外(FN),最终的结果就是倾向于特异度越大越好
介绍完三种场景后,也就相当于把这三个评估的方法的试用条件给说明白了,那么回到这个案例上,进行评估信用卡交易是否存在欺诈的话,对应的混淆矩阵就是如下
那么这次的模型,关注的点就在异常的交易,也就是欺诈这个数据的本身,即欺诈数据被正确分类的数量占欺诈数据的比例,对应着召回率:(TP/(TP+FN)),既然评估方法确定了,就可以进行数据的交叉验证了(因为验证是需要一个指标告诉哪一次交叉验证的结果比较好),所以交叉验证应该是在确定了评估方法之后
5. 逻辑回归模型
封装函数,方便日后调用,只需要传入训练集数据和标签数据就可以了,不过之后也可以使用参数自动搜索,这里封装的函数相当于将交叉验证的过程给一步步拆解出来,方便理解
#关于召回率:Recall = TP/(TP+FN)
from sklearn.linear_model import LogisticRegression
from sklearn.model_selection import KFold,cross_val_score,cross_val_predict
from sklearn.metrics import confusion_matrix, recall_score, classification_report
def printing_Kfold_scores(x_train_data, y_train_data):
fold = KFold(5, shuffle = False)
#定义不同的惩罚力度,数值越大惩罚力度越小,可以查看官网
c_param_range = [0.01,0.1,1,10,100]
#展示结果用的表格
result_table = pd.DataFrame(index = range(len(c_param_range),2),columns = ['C_parameter','Mean recall score'])
result_table['C_parameter'] = c_param_range
#k-fold表示k折的交叉验证,这里会得到两个索引集合:训练集 = indices[0],验证集 = indices[1]
j = 0
#循环遍历不同的参数
for c_param in c_param_range:
print('-----------------------------')
print('正则化惩罚力度:', c_param)
print('-----------------------------')
print('')
recall_accs = []
#一步步分解来执行交叉验证
for iteration,indices in enumerate(fold.split(x_train_data)):
#指定算法模型,并且给定参数,也可以尝试一下惩罚参数选择'l2'
lr = LogisticRegression(C = c_param,penalty = 'l1',solver='liblinear')
#训练模型,注意索引不要给错了,训练的时候一定传入的是训练集,所以X和Y的索引都是0
lr.fit(x_train_data.iloc[indices[0],:], y_train_data.iloc[indices[0],:].values.ravel())
#建立好模型后,预测模型结果,这里用的就是验证集,索引为1
y_pred_undersample = lr.predict(x_train_data.iloc[indices[1],:].values)
#有了预测结果之后就可以来进行评估了,这里recall_score需要传入预测值和真实值
recall_acc = recall_score(y_train_data.iloc[indices[1],:].values, y_pred_undersample)
#一会还要算平均,这里就把每一步的结果保存起来
recall_accs.append(recall_acc)
print('Iteration {}: 召回率 = {}'.format(iteration,recall_acc))
#当执行完所有交叉验证后,计算平均结果
result_table.loc[j,'Mean recall score'] = np.mean(recall_accs)
j += 1
print('')
print('平均召回率',np.mean(recall_accs))
print('')
#找到最好的参数,哪一个recall高,自然就是最好的了
best_c = result_table.loc[result_table['Mean recall score'].astype(float).idxmax()]['C_parameter']
#打印最好的结果
print('************************************************')
print('效果最好的模型所选的参数 = ',best_c)
print('************************************************')
return best_c
调用函数,传递参数
best_c = printing_Kfold_scores(X_train_undersample,y_train_undersample)
输出的结果为:(注意交叉验证的过程是不涉及到测试集的,所以这里传入的参数都是训练集的数据)
-----------------------------
正则化惩罚力度: 0.01
-----------------------------
Iteration 0: 召回率 = 0.958904109589041
Iteration 1: 召回率 = 0.9452054794520548
Iteration 2: 召回率 = 1.0
Iteration 3: 召回率 = 0.972972972972973
Iteration 4: 召回率 = 0.9848484848484849
平均召回率 0.9723862093725106
-----------------------------
正则化惩罚力度: 0.1
-----------------------------
Iteration 0: 召回率 = 0.8493150684931506
Iteration 1: 召回率 = 0.863013698630137
Iteration 2: 召回率 = 0.9491525423728814
Iteration 3: 召回率 = 0.9459459459459459
Iteration 4: 召回率 = 0.9090909090909091
平均召回率 0.9033036329066049
-----------------------------
正则化惩罚力度: 1
-----------------------------
Iteration 0: 召回率 = 0.863013698630137
Iteration 1: 召回率 = 0.8767123287671232
Iteration 2: 召回率 = 0.9830508474576272
Iteration 3: 召回率 = 0.9459459459459459
Iteration 4: 召回率 = 0.9090909090909091
平均召回率 0.9155627459783485
-----------------------------
正则化惩罚力度: 10
-----------------------------
Iteration 0: 召回率 = 0.8767123287671232
Iteration 1: 召回率 = 0.8904109589041096
Iteration 2: 召回率 = 0.9830508474576272
Iteration 3: 召回率 = 0.9459459459459459
Iteration 4: 召回率 = 0.9242424242424242
平均召回率 0.924072501063446
-----------------------------
正则化惩罚力度: 100
-----------------------------
Iteration 0: 召回率 = 0.8767123287671232
Iteration 1: 召回率 = 0.9041095890410958
Iteration 2: 召回率 = 0.9830508474576272
Iteration 3: 召回率 = 0.9459459459459459
Iteration 4: 召回率 = 0.9242424242424242
平均召回率 0.9268122270908433
************************************************
效果最好的模型所选的参数 = 0.01
************************************************
根据结果发现,参数的大小对于模型的好坏是有较大的影响,这里0.01数值最小,但是代表着最大的惩罚力度,取得了最好的结果,而10,100.这里的数值很大,但是取倒数(反过来)就是很小的惩罚力度,所以最终两者模型的得分就相近。
读到这里,还需要注意一下,上面的模型得分是针对于训练集中的数据,也就是相当于平时的练习和做模拟试卷,真正的测试集还没有使用。因此,为了充分的发挥自己高考的实力,这里就应该选取刚刚不同参数下,得分最高的模型来进行导入的测试集数据,查看最终的考试分数
为了方便查看召回率的数值,可以进行混淆矩阵中各类数据的可视化,这里还是直接封装函数,后续使用到时直接调用(关于绘图的问题,建议直接复制别人已经封装好的函数,然后按照要求导入数据即可)
def plot_confusion_matrix(cm,classes,
title = 'Confusion matrix',
cmap = plt.cm.Blues):
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.xlabel('True label')
plt.ylabel('Predicted label')
接着调用函数,进行可视化展示,需要传递两个参数(混合矩阵的值,和分类的名称)
import itertools
lr = LogisticRegression(C=best_c,penalty = 'l1',solver='liblinear')
lr.fit(X_train_undersample,y_train_undersample.values.ravel())
y_pred_undersample = lr.predict(X_test_undersample.values)
#计算所需要的值
cnf_matrix = confusion_matrix(y_test_undersample, y_pred_undersample)
np.set_printoptions(precision = 2)
print('召回率: ', cnf_matrix[1,1]/(cnf_matrix[1,0] + cnf_matrix[1,1]))
#绘制
class_names = [0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix,
classes=class_names,
)
plt.show()
输出的结果为:
至此模型训练就结束了吗?100分的话最终得分95分也不错了,但是别忘了最开始留的提问,为啥要把原始数据进行测试集和非测试集的划?看一下上面的代码,在进行模型测试的时候使用的是降采样的数据集中的测试数据(1:1),这个数据只是为了能够训练到输入为欺诈的数据,并非是真实的数据情况,因此最后一步来查看模型的好坏应该是使用真实的测试数据来进行
lr = LogisticRegression(C = best_c, penalty = 'l1',solver='liblinear')
lr.fit(X_train_undersample,y_train_undersample.values.ravel())
y_pred = lr.predict(X_test.values)
# 计算所需值
cnf_matrix = confusion_matrix(y_test,y_pred)
np.set_printoptions(precision=2)
print("召回率: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))
# 绘制
class_names = [0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix
, classes=class_names
, title='Confusion matrix')
plt.show()
输出的结果为:(又有问题来了,刚刚降采样的数据中看不出来的,经过实际的数据集后发现,本身数据是正常的,经过分类之后,模型认定有10873个数据都是异常的,而实际识别出来的异常样本才138个,也就是说这个模型为了实现异常的检测,却误杀了1w多条正常的数据,有种宁可错杀1w,不可放过1个的意味了,那么有没有什么方法解决呢?后面就介绍了一种,比如调整阈值)
6. 阈值对结果的影响
逻辑回归中使到的激活函数,一般是sigmoid函数,根据输出一个范围在0-1之间的值,结果大于0.5的样本归入1类,小于0.5的样本归入0类,那么这个0.5就是一个阈值,并非说是指定的,只能是0.5,是可以进行改变的,下面直接先给代码,然后再进行分析解释
# 用之前最好的参数来进行建模
lr = LogisticRegression(C = 0.01, penalty = 'l1',solver='liblinear')
# 训练模型,还是用降采样的数据集
lr.fit(X_train_undersample,y_train_undersample.values.ravel())
# 得到预测结果的概率值
y_pred_undersample_proba = lr.predict_proba(X_test_undersample.values)
#指定不同的阈值
thresholds = [0.1,0.2,0.3,0.4,0.5,0.6,0.7,0.8,0.9]
plt.figure(figsize=(10,10))
j = 1
# 用混淆矩阵来进行展示
for i in thresholds:
y_test_predictions_high_recall = y_pred_undersample_proba[:,1] > i
plt.subplot(3,3,j)
j += 1
cnf_matrix = confusion_matrix(y_test_undersample,y_test_predictions_high_recall)
np.set_printoptions(precision=2)
print("给定阈值为:",i,"时测试集召回率: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))
class_names = [0,1]
plot_confusion_matrix(cnf_matrix
, classes=class_names
, title='Threshold >= %s'%i)
输出结果为:(可以发现给定的阈值较低时候,召回率是1,也就是说只要有异常的数据全部都被抓进来了,原因就是这个标准太低了,而0.9时候这个标准又太苛刻了,因此通过这种遍历不同阈值的方式可以找到相对好一些的参数数据,比如阈值这里选择到0.6也可)
7. 方案对比分析
第二种方案,就是进行过采样,这里可以直接调用工具包,需要先安装imblearn库
import pandas as pd
from imblearn.over_sampling import SMOTE
from sklearn.metrics import confusion_matrix
from sklearn.model_selection import train_test_split
然后加载数据进行过采样,注意这里直接就是读取数据后进行标签的特征和标签的指定,没有进行标准化和多余数据的删除
credit_cards=pd.read_csv('creditcard.csv')
columns=credit_cards.columns
# 在特征中去除掉标签
features_columns=columns.delete(len(columns)-1)
features=credit_cards[features_columns]
labels=credit_cards['Class']
数据切分,留意随机种子的设定还有切分的比例值都要保持一致
features_train, features_test, labels_train, labels_test = train_test_split(features,
labels,
test_size=0.3,
random_state=0)
使用imblearn中的smote工具包进行数据的扩充
#需要先初始化一下,然后传入数据特征和标签
oversampler=SMOTE(random_state=0)
os_features,os_labels=oversampler.fit_sample(features_train,labels_train)
#查看一下生成的数据量
len(os_labels[os_labels==1])
#199019
最后进行交叉验证,模型训练寻找惩罚项的最佳参数
#使用之前,注意数据格式要转化为一致
os_features = pd.DataFrame(os_features)
os_labels = pd.DataFrame(os_labels)
best_c = printing_Kfold_scores(os_features,os_labels)
输出的结果为:(可以发现还是惩罚项力度为0.1时候,模型得分最高)
-----------------------------
正则化惩罚力度: 0.01
-----------------------------
Iteration 0: 召回率 = 0.9142857142857143
Iteration 1: 召回率 = 0.88
Iteration 2: 召回率 = 0.9716742539200809
Iteration 3: 召回率 = 0.9622269398419737
Iteration 4: 召回率 = 0.9619254588164358
平均召回率 0.938022473372841
-----------------------------
正则化惩罚力度: 0.1
-----------------------------
Iteration 0: 召回率 = 0.9142857142857143
Iteration 1: 召回率 = 0.88
Iteration 2: 召回率 = 0.9728882144663632
Iteration 3: 召回率 = 0.9640986345421885
Iteration 4: 召回率 = 0.9640483877045989
平均召回率 0.939064190199773
-----------------------------
正则化惩罚力度: 1
-----------------------------
Iteration 0: 召回率 = 0.9142857142857143
Iteration 1: 召回率 = 0.88
Iteration 2: 召回率 = 0.97298937784522
Iteration 3: 召回率 = 0.9627042847990754
Iteration 4: 召回率 = 0.9644880475335084
平均召回率 0.9388934848927036
-----------------------------
正则化惩罚力度: 10
-----------------------------
Iteration 0: 召回率 = 0.9142857142857143
Iteration 1: 召回率 = 0.88
Iteration 2: 召回率 = 0.9673495194739504
Iteration 3: 召回率 = 0.9644126772771239
Iteration 4: 召回率 = 0.9644378006959187
平均召回率 0.9380971423465414
-----------------------------
正则化惩罚力度: 100
-----------------------------
Iteration 0: 召回率 = 0.9142857142857143
Iteration 1: 召回率 = 0.88
Iteration 2: 召回率 = 0.973115832068791
Iteration 3: 召回率 = 0.9633323702689462
Iteration 4: 召回率 = 0.9640735111233937
平均召回率 0.938961485549369
************************************************
效果最好的模型所选的参数 = 0.1
************************************************
最后就是使用生成的数据进行模型测试,绘制混淆矩阵
lr = LogisticRegression(C = best_c, penalty = 'l1',solver='liblinear')
lr.fit(os_features,os_labels.values.ravel())
y_pred = lr.predict(features_test.values)
# 计算混淆矩阵
cnf_matrix = confusion_matrix(labels_test,y_pred)
np.set_printoptions(precision=2)
print("召回率: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))
# 绘制
class_names = [0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix
, classes=class_names
, title='Confusion matrix')
plt.show()
输出的结果为:(可以发现这个结果相较于之前的降采样过程,模型的得分有着明显的下降,注意这里的还有一个区别是数据导入后没有进行标准化和多余数据的处理,看到这里了,有兴趣的可以试一下,使用处理后的数据,最后生成的数据会如何)
这里就直接给出完整的代码,使用处理过后的数据带入模型主要传递的数据代码,
#主要是这两行代码,输入的数据不同
features = data.iloc[:,:-2]
labels = data['Class']
#后面代码一样
features_train, features_test, labels_train, labels_test = train_test_split(features, labels,test_size=0.3, random_state=0)
oversampler=SMOTE(random_state=0)
os_features,os_labels=oversampler.fit_sample(features_train,labels_train)
os_features = pd.DataFrame(os_features)
os_labels = pd.DataFrame(os_labels)
best_c = printing_Kfold_scores(os_features,os_labels)
输出的结果为:(最后的输出的对应的惩罚项力度的最佳参数这里就不再是0.1了,变成10)
-----------------------------
正则化惩罚力度: 0.01
-----------------------------
Iteration 0: 召回率 = 0.9357142857142857
Iteration 1: 召回率 = 0.904
Iteration 2: 召回率 = 0.9119625695498229
Iteration 3: 召回率 = 0.8968055573002374
Iteration 4: 召回率 = 0.8966799402062633
平均召回率 0.9090324705541217
-----------------------------
正则化惩罚力度: 0.1
-----------------------------
Iteration 0: 召回率 = 0.9357142857142857
Iteration 1: 召回率 = 0.904
Iteration 2: 召回率 = 0.9128730399595346
Iteration 3: 召回率 = 0.8978230557614281
Iteration 4: 召回率 = 0.8977602472144409
平均召回率 0.9096341257299378
-----------------------------
正则化惩罚力度: 1
-----------------------------
Iteration 0: 召回率 = 0.9357142857142857
Iteration 1: 召回率 = 0.904
Iteration 2: 召回率 = 0.9129994941831057
Iteration 3: 召回率 = 0.8979612345647996
Iteration 4: 召回率 = 0.8978481791802229
平均召回率 0.9097046387284827
-----------------------------
正则化惩罚力度: 10
-----------------------------
Iteration 0: 召回率 = 0.9357142857142857
Iteration 1: 召回率 = 0.904
Iteration 2: 召回率 = 0.9131006575619626
Iteration 3: 召回率 = 0.8979486728554021
Iteration 4: 召回率 = 0.8978733025990177
平均召回率 0.9097273837461337
-----------------------------
正则化惩罚力度: 100
-----------------------------
Iteration 0: 召回率 = 0.9357142857142857
Iteration 1: 召回率 = 0.904
Iteration 2: 召回率 = 0.9130247850278199
Iteration 3: 召回率 = 0.8979612345647996
Iteration 4: 召回率 = 0.8978984260178126
平均召回率 0.9097197462649435
************************************************
效果最好的模型所选的参数 = 10.0
************************************************
最后查看一下导入原始数据后的模型得分
lr = LogisticRegression(C = best_c, penalty = 'l1',solver='liblinear')
lr.fit(os_features,os_labels.values.ravel())
y_pred = lr.predict(features_test.values)
# 计算混淆矩阵,这里直接使用的就是原数据的标签进行
cnf_matrix = confusion_matrix(labels_test,y_pred)
np.set_printoptions(precision=2)
print("召回率: ", cnf_matrix[1,1]/(cnf_matrix[1,0]+cnf_matrix[1,1]))
# 绘制
class_names = [0,1]
plt.figure()
plot_confusion_matrix(cnf_matrix
, classes=class_names
, title='Confusion matrix')
plt.show()
输出的结果为:(很显然是要比数据不处理时候来的高一些,但是对于降采样来说召回率要低了很多,原因嘛很明显,从几百条数据生成几十万条数据,毕竟是虚拟的,和实际数据的偏差还是太大,但是有一点是改善了,就是错杀好人的现象比较少了,又原来的1w多数据,降到2千多一点)
总结
(1)在此项目梳理过程中,首选对数据进行了观察,发现了其中样本不均衡的问题,其实做任务工作之前都一定要先进行数据检查,看看数据有什么问题,针对这些问题来选择解决方案。
(2)这里提出了两种方法,降采样和过采样,两条路线来进行对比实验,任何实际问题来了之后,都不会一条路走到黑的,没有对比就没有伤害,通常都会得到一个基础模型,然后对各种方法进行对比,找到最合适的,所以在任务开始之前,一定得多动脑筋多一手准备,得到的结果才有可选择的余地。
(3)在建模之前,需要对数据进行各种预处理的操作,比如数据标准化,缺失值填充等,这些都是必要操作,由于数据本身已经给定了特征,此处还没有提到特征工程这个概念,后续项目中会逐步引入,其实数据预处理的工作是整个任务中最为最重也是最苦的一个阶段,数据处理的好不好对结果的影响是最大的。
(4)先选好评估方法,再进行建模。建模的目的就是为了得到结果,但是不可能一次就得到最好的结果,肯定要尝试很多次,所以一定得有一个合适的评估方法,可以用这些通用的,比如Recall,准确率等,也可以根据实际问题自己指定评估指标。
(5)选择合适的算法,这里使用的是逻辑回归,也详细分析了其中的细节,之后还会使用其他算法,并不一定非要用逻辑回归来完成这个任务,其他算法可能效果会更好。但是有一点就是在机器学习中并不是越复杂的算法越实用,恰恰相反,越简单的算法反而应用的越广泛。逻辑回归就是其中一个典型的代表了,简单实用,所以任何分类问题都可以把逻辑回归当做一个待比较的基础模型了。
(6)模型的调参也是很重要的,之前通过实验也发现了不同的参数可能会对结果产生较大的影响,这一步也是必须的,后续实战内容还会来强调调参的细节,这里就简单概述一下了。对于参数我建立大家在使用工具包的时候先看看其API文档,知道每一个参数的意义,再来实验选择合适的参数值。
(7)得到的结果一定要和实际任务结合在一起,有时候虽然得到的结果指标还不错,但是实际应用却成了问题,所以测试环节也是必不可少的。