会先写一些思考和结论,力求每一步把重点直白点写出来,4万字
一、数据相关情况及背景
(1)数据源,kaggle上的
Credit Card Fraud Detection (kaggle.com)
国内好像想搞到还不是那么容易,附带个百度盘链接,文件143MB
链接:https://pan.baidu.com/s/1aGpmY-4XYIfiTbqHZAqfTw
提取码:1024
长这样的,
(2)数据基本解释:
September 2013 by European cardholders.
This dataset presents transactions that occurred in two days, where we have 492 frauds out of 284,807 transactions. The dataset is highly unbalanced, the positive class (frauds) account for 0.172% of all transactions.
2013年两天内的信用卡交易记录,特征V1-V28都是PCA降维并不知道具体含义的(纯数值型),TIME特征从1到2天时间的秒,2天总计是17万多秒,而数据是28万多,故有一些Time是包含多个数据的,Amount交易金额,数值型。
标签Label 0 则是正常交易,1则是欺诈,比例差距极大
from collections import Counter
counter= Counter(data.Class)
print(counter)
print(counter[0]/len(data))
# Counter({0: 284315, 1: 492}) # 0,1标签数量
# 0.9982725143693799 # 负样本占比极大
(3)目标:建立模型,预测信用卡欺诈(盗刷),在这种情况下,我们的目标毫无疑问是想要比较高的Recall,同时保证Precision不能低,由于0样本数据占比太高,所以就算将所有数据都无脑预测为0,acuuracy也很高,起步就是99.8%以上。如果我们想提高召回率(查全率),很容易把一些正常交易误判为欺诈,即FP会很多,导致Precision变低。
二、先说结论
1.这个案例,不能用accuracy,ROC曲线面积来衡量模型好坏,拟采用PR曲线下的面积作为模型评价标准(最终对比不同数据子集,PR面积和ROC面积,在好的子集上都高于差的子集);
2.最终评价标准,比如漏掉1个欺诈,我们损失500块,误判一个正常交易为欺诈,会损失200块,通过更改阈值,取一个损失最小值;但实际情况,比如你是创业初期搞口碑抢市场,可以多关注精确率,容忍一些搞事情的,还可以通过其他手段,比如疑似欺诈,模型判断概率>0.1,则进行人脸识别、定位之类的其他手段。
3.欠采样,结果很差;过采样,结果还行,但不如直接用原数据,通过更改正负样本权重,效果更好;(注意数据泄露问题,本文将演示错误示范)
4.使用过滤法选出了2个特征子集VS原本的全特征集,三份数据结果对比,从LightGbm的embedded思想来看,有一些特征还是最好剔除掉的。
5.我们缺少一个关键指标,需要结合实际业务判断,漏掉一个欺诈和误判一个正常交易,带来的损失究竟有多大,不可能两边都完美,要结合比例取一个最优解;
三、本文用到方法
1.特征工程中的Filter,embedded
2.欠采样(随机、Nearmiss)、过采样(Borderline SMOTE)
3.几个聚类、逻辑回归(暂定只用坐标轴下降法)、xgboost、lightgbm、决策树、Adaboost、随机森林及其调参
4.分类问题绝大部分常用评价指标,ROC, PR 曲线
四、正文
还得是代码+图文+讲解好点,我们一步步往下走
导包:
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import matplotlib.gridspec
import seaborn as sns
import missingno as msno
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.model_selection import GridSearchCV, train_test_split
from sklearn.metrics import roc_curve, auc, roc_auc_score, recall_score, accuracy_score, classification_report, \
confusion_matrix, precision_recall_curve,precision_score,average_precision_score,f1_score
from sklearn.preprocessing import StandardScaler,RobustScaler
import itertools
import warnings
warnings.filterwarnings('ignore')
"设置PANDAS小数精度"
pd.options.display.float_format = lambda x: '%.2f' % x # 后期要换回6
pd.set_option('expand_frame_repr', False) # 禁止自动换行
rc = {'font.sans-serif': 'KaiTi',
'axes.unicode_minus': False}
sns.set(context='notebook', style='ticks', rc=rc) # 用来正常显示中文标签
1.查看数据基本情况
没有null空值!
将time秒转为小时hour:
# 将小时变成int,方便绘图X轴显示
data['Hour'] = data['Time'].apply(lambda x: int(divmod(x, 3600)[0]))
# divmod除以并取模,[0]为整数部分[1]为余数
data.sample(10)
1.1查看特征之间的关系
data_cor = data.corr()
# 遮住右上角
hide_area = np.zeros_like(data_cor)
hide_area_index = np.triu_indices_from(hide_area, k=0) # 矩阵右上角索引
hide_area[hide_area_index] = 1
# 画布设置与画图
plt.figure(figsize = (10,8))
with sns.axes_style(style='white'):
sns.heatmap(data_cor,vmin=-1,vmax=1,cmap='RdBu_r',mask=hide_area,square=True,cbar=True)
注意,通过皮尔逊相关系数,查看一个连续值变量和一个分类型的label是不可取的!!!
撇开Class,看看特征之间的关系,有没有多重共性或者极其相似这种。
# 正常和盗刷对比----查看不同特征之间的关系,不含与Class的关系(样本分布不均衡)
x_fraud = data.loc[data['Class'] == 1]
x_norm = data.loc[data['Class'] == 0]
cor_norm = x_norm.loc[:, x_norm.columns != 'Class'].corr()
cor_fraud = x_fraud.loc[:, x_fraud.columns != 'Class'].corr()
# 设置热力图遮盖部分
hide_area = np.zeros_like(cor_norm)
hide_area_index = np.triu_indices_from(hide_area, k=0) # 矩阵右上角索引
hide_area[hide_area_index] = 1
# 画布设置与画图
gridkws = {'width_ratios': (1, 1, 0.1), 'wspace': 0.25} # 前设置三个图占宽度比例,后者设置间隔
fig2, (ax1, ax2, ax3) = plt.subplots(1, 3, figsize=(18, 8), gridspec_kw=gridkws)
cmap_custom = sns.diverging_palette(60, 180, center='light', as_cmap=True) # 自定义颜色diverging_palette
# 绘制热力图---
with sns.axes_style(style='white'):
sns.heatmap(cor_norm,vmin=-1,vmax=1,cmap='RdBu_r',mask=hide_area,square=True,ax=ax1,cbar=False)
ax1.set_title('正常消费情况')
sns.heatmap(cor_fraud, ax=ax2, vmin=-1, vmax=1,cbar_ax=ax3
# ,cbar_kws={'orientation':'vertical','ticks':[-1,-0.5,0,0.5,1]}
, square=True, linewidths=0.5, linecolor='white', mask=hide_area
, cmap='RdBu_r'
)
ax2.set_title('Fraud')
可以发现,在正常和欺诈之间,差异挺大的;
1.2 各种画图过一遍数据情况
看看交易金额的分布情况:
如果不更改Y轴,则看不出正常消费的规律,数据多且分布不均衡时尤为突出
被盗刷的金额普遍很小,说明干坏事要悄咪咪的
sns.despine()
fig3, (ax31, ax32) = plt.subplots(2, 1
# , sharex='col'
)
ax31.hist(x=data['Amount'][data['Class'] == 1], bins=30)
ax31.set_title('欺诈')
ax32.hist(x=data['Amount'][data['Class'] == 0], bins=100)
ax32.set_title('正常消费')
plt.xlabel('刷卡金额')
plt.ylabel('交易次数统计')
plt.tight_layout()
ax32.set_yscale('log')
看交易时间的分布,虽然只有2天,我们分成48个小时了:
sns.catplot(x_fraud,x='Hour',kind='count',height=6,aspect=2.5,palette='Spectral_r')
sns.catplot(x_norm,x='Hour',kind='count',height=6,aspect=2.5,palette='Set2_r')
正常交易数据量大,呈现一定的规律性,欺诈数据,不同时间有区别;
我们总数据量是28万多,其中0: 284315, 1: 492
看看不同时间段,交易金额有没有什么区别:
fig4,(ax1,ax2) = plt.subplots(nrows=2,ncols=1,sharex='all',figsize=(12,6))
ax1.scatter(data[data['Class']==1]['Hour'],data[data['Class']==1]['Amount'],c='red',s=4,alpha=0.5)
ax1.set_title('Fraud')
ax2.scatter(data[data['Class']==0]['Hour'],data[data['Class']==0]['Amount'],s=4,alpha=0.5)
ax2.set_title('Normal')
plt.xlabel('Hour')
plt.xticks(np.arange(0,48))
plt.ylabel('Amount')
其实也看不出什么,大概是金额低的普遍比较多,欺诈盗刷的,不同时间段是有区别的;
特征分布,箱线图查看,应该有一些特征,在两种类别之间差异较大,看看:
data = pd.read_csv("d:/t_datas/creditcard.csv")
col_names = data.columns.tolist()
col_names.remove('Class')
plt.figure(figsize=(14,36))
for index,col in enumerate(col_names):
plt.subplot(6,5,index+1)
sns.boxplot(data,x='Class',y=col,palette={0:'lightgreen',1:'firebrick'})
plt.title(f'{col} VS Class')
plt.tight_layout()
很大一张图,可以看到有一些特征,在不同类别间,不同哦!不过注意,由于0样本太大,即是是0样本的异常值点,比如>1.5IQR的数量,可能也远远大于1样本的数量。(可以自己去看看)
降维看看,TSNE太耗时间了
from sklearn.decomposition import PCA,TruncatedSVD
from sklearn.manifold import TSNE
import time
rs = RobustScaler()
scale_data = pd.read_csv("d:/t_datas/creditcard.csv")
scale_data[['Time','Amount']]=rs.fit_transform(data[['Time','Amount']])
scale_data.drop('Class',axis=1,inplace=True)
t0 = time.time()
de_data1 = PCA(n_components=2,random_state=1).fit_transform(scale_data)
de_data1 = np.c_[de_data1,data.Class]
t1 = time.time()
print('cost time',t1-t0)
t2 = time.time()
de_data2 = TruncatedSVD(n_components=2,random_state=1).fit_transform(scale_data)
de_data2 = np.c_[de_data2,data.Class]
t3 = time.time()
print('cost time',t3-t2)
# cost time 0.5407612323760986
# cost time 0.4224820137023926
为了快速进入主题,一些不太重要的查看就略过,不过我基本上带着得差不多了;
2.特征工程
总共是V1-V28+TIME+AMOUNT,我们将TIME变成HOUR,总共30个特征列,此时得考虑,是不是一些特征并无卵用,如果把没用的特征放进去,一方面增加训练量,另一方面造成模型方差变大;
2.1过滤法画图。
fig, ax = plt.subplots(8,4,figsize=(16,28))
v_features = data.columns.tolist()
v_features.remove('Class')
i = 0
plt.tight_layout()
for feature in v_features:
i+=1
plt.subplot(8,4,i)
sns.distplot(data[cond1][feature],bins=50,color='red',label='盗刷',axlabel=False)
sns.distplot(data[cond2][feature],bins=50,color='green',label='正常',axlabel=False)
plt.xlabel(feature)
可以看到,有的特征分布,明显不同,肉眼看去,那种均值、方差、偏度峰度差异很大的,明显就是好特征,上述代码看不太清,你可以运行下面这个(图太多,截取一小部分,我用的jupyter notebook演示的,用pycharm的话,要改一下生成画布的代码):
plt.figure(figsize=(10,5*30))
gs = matplotlib.gridspec.GridSpec(30,1)
for index,col in enumerate(v_features):
ax = plt.subplot(gs[index])
sns.distplot(data[cond1][col],bins=50,color='red',label='盗刷',axlabel=False)
sns.distplot(data[cond2][col],bins=50,color='green',label='正常',axlabel=False)
plt.title(f'特征{col}的分布')
plt.legend()
"通过对比特征在正常、盗刷的分布,如果分布基本重合则丢,分布区分明显则重要"
"经过对比,V1-V7,V9-V12,V14,V16-V19看起来不错"
droplist = ['V8', 'V13', 'V15', 'V20', 'V21', 'V22', 'V23', 'V24', 'V25', 'V26', 'V27', 'V28']
肉眼看起来,总觉得并不是那么靠谱,再试试其他的,如果大家结果都差不多,那就证明选的特征选对了;
2.2方差分析法——ANOVA
原理是是比对同一个特征,对不同类别的 组间方差/组内方差,如果一个特征,组间方差除以组内方差,数值越大,则证明这个特征越具有可区别性(自己起的名字),即这个特征,不同类别,差异很明显,由于使用F分布,除以自由度,会消除样本数量不同造成的差异。
x_cols = [col for col in data.columns if col not in ['Class','Time']]
data_x = data[x_cols]
data_y = data['Class']
from sklearn.feature_selection import chi2,SelectKBest,VarianceThreshold,f_classif
selector = SelectKBest(f_classif,k='all')
selector.fit(data_x,data_y)
feature_res = dict(zip(selector.feature_names_in_,selector.scores_))
sorted_dict = sorted(feature_res.items(),key = lambda x:x[1],reverse=True)
sorted_dict # 根据评分从高到低选出了,但是F检验一般用于线性关系的检验
2.3 距离相关系数
当然HOUR这个特征列,还是要给个面子保留的
# 耗时两分半,先注释掉--结果同f_classify差不多
# import dcor
# res_d = {}
# for col in data_x.columns:
# res_d[col] = dcor.distance_correlation(data_x[col],data_y)
# sorted(res_d.items(),key = lambda x:x[1],reverse=True)
>>>
[('V14', 0.17286562356194035),
('V17', 0.15995970344425356),
('V12', 0.14455715736078245),
('V10', 0.1354151275158626),
('V16', 0.11405024570369811),
('V7', 0.11058438775467186),
('V3', 0.10585266689186222),
('V11', 0.09920965821496412),
('V4', 0.09883832262084359),
('V2', 0.07966726683198543),
('V18', 0.07356932249992948),
('V9', 0.07205006666172296),
('V27', 0.0667170390277387),
('V21', 0.06559141527797416),
('V5', 0.06382011670904869),
('V1', 0.06239302997528928),
('V8', 0.05808713841167627),
('V28', 0.04667250516679421),
('V6', 0.04519370744946623),
('V20', 0.03796519473601439),
('V19', 0.03621335387413822),
('V23', 0.025584211723587953),
('Amount', 0.011218725053936675),
('V25', 0.010543143224819434),
('V24', 0.009779891782730726),
('V22', 0.007074758115304627),
('V13', 0.006626737421578729),
('V26', 0.0065036800190319405),
('V15', 0.004587400803129857),
('Hour', 0.0)]
直接把结果放一块,在这种情况下,f_classif就是皮尔逊相关系数的 未恢复量纲 版,距离相关系数可以克服数据之间非线性的关系。
前期我们看到了,盗刷的金额一般比较小,而正常消费有一些很高的金额,我们这里给Amount一个面子,如果筛选特征,就取到金额列,比金额列数值更小的则不要。
2.4 其他方法
MIC最大信息系数和Relief这两种方法,在数据不平衡中,因为其原理大概是高维空间划分多个子空间,然后进行抽样比对,这样很容易抽到0而不容易抽到1,所以放弃。
基本过滤法,适合用的差不多都到这了。
提前剧透:从使用树模型lightgbm的结果来看,方差分析结果是最好的,绘图其次,全特征最低,没用xgboost是因为调参太耗时,lightgbm结果略次于xgboost,由于其他常见模型。
2.5 三份数据
最终,我决定用三份数据,看看到底选对没
data_x_m ,就是manual绘图人工看的,选的看起来比较差异化的特征
data_x_f,用方差分析f_classify搞的,截止包含到上图Amount金额列,之后的6个丢掉
data_x_full,就是全部啦,不过所有数据,都是丢了Time换成Hour
droplist = ['V8', 'V13', 'V15', 'V20', 'V21', 'V22', 'V23', 'V24', 'V25', 'V26', 'V27', 'V28']
droplist.extend(['Class','Time'])
data_x_m = data.drop(labels=droplist,axis=1) # 剩18个
# f_classify 金额之后的,剩26个
data_x_f = data.drop(labels=['V13','V26','V15','V25','V23','V22','Class','Time'],axis=1)
data_x_full = data.drop(['Class','Time'],axis=1) # 丢掉Time 后剩31个
len(data_x_m.columns),len(data_x_f.columns),len(data_x_full.columns)
2.6 随机森林看特征重要性
# 先用随机森林,查看以下特征重要性
# 树模型的话,数据就不用归一化了
def rft_impo(x,y,split=False,label_weight=None):
rft_clf = RandomForestClassifier(n_estimators=1000,max_depth=8,bootstrap=True,n_jobs=-1,
max_samples=0.6,
class_weight=label_weight)
# 我们用dataframe存储特征重要性结果
impo_df = pd.DataFrame(columns=x.columns)
# 索引就从1开始吧
make_index =1
if not split:
x_train = x
y_train = y
rft_clf.fit(x_train,y_train)
importances = rft_clf.feature_importances_ # 获取重要性
#存储一个结果
impo_df.loc[make_index] = importances
if split: # 如果进行多次划分
skf = StratifiedKFold(n_splits=5,shuffle=True,random_state=1)
for train_index,test_index in skf.split(x,y):
# 分层划分数据集
x_train,x_test = x.loc[train_index,:],x.loc[test_index,:]
y_train,y_test = y[train_index],y[test_index]
rft_clf.fit(x_train,y_train)
importances = rft_clf.feature_importances_
# cols = rft_clf.feature_names_in_
impo_df.loc[make_index] = importances
make_index+=1 # 要加入这个条件循环写入
return impo_df.T # 比较方便查看
def impo_plot(dataframe,yanse=None):
# 先将比较大的放前面
sorted_impo = dataframe.sort_values(by=1,ascending=False)# 降序排列,列名就是1
names = sorted_impo.index
impors = sorted_impo.values.reshape(1,-1)[0] # 排序后格式不转化下报错
# 绘图
plt.bar(x=range(len(names)),height=impors,color=yanse)
plt.plot(range(len(names)),np.cumsum(impors),drawstyle='steps') # 阶梯图
plt.xticks(range(len(names)),names,rotation='vertical', fontsize=14)
plt.title('特征重要性')
impo_plot(impo_df_1,yanse='firebrick')
可以看到,过滤法、随机森林查看,是有一些区别的,比较靠前的总是那么几个,但我们确定的是,多个方法都排名靠后的,绝对是比较差的特征。(这个随机森林内容是我后面补的...)
3.数据标准化
一般的基于欧式距离的东西,对标准化比较敏感,树模型就无所谓,我们考虑 StandardScale和RobustScale进行对比。
把RobustScale拉过来对比,因为可能会有离群值异常点之类的,看看消除是否会调高模型分数。
StandardScale : x_i=(x_i-μ)/σ ,μ均值,σ标准差
RobustScale: x_i = (x_i - median)/IQR ,median中位数,IQR即四分位距
懒得打公式了...
# 考虑2个处理方式,原数据不动,复制一下
from sklearn.preprocessing import RobustScaler,StandardScaler
from sklearn.model_selection import train_test_split,ShuffleSplit,StratifiedKFold,StratifiedShuffleSplit
pd.options.display.float_format=lambda x:'%.6f'%x
#StandardScaler标准化
data_x_full_ss = data_x_full.copy()
data_x_f_ss = data_x_f.copy()
data_x_m_ss = data_x_m.copy()
data_x_full_ss[['Amount','Hour']] = RobustScaler().fit_transform(data_x_full_ss[['Amount','Hour']])
data_x_f_ss[['Amount','Hour']] = RobustScaler().fit_transform(data_x_f_ss[['Amount','Hour']])
data_x_m_ss[['Amount','Hour']] = RobustScaler().fit_transform(data_x_m_ss[['Amount','Hour']])
# 拟采用分位数标准化
data_x_full_rs = data_x_full.copy()
data_x_f_rs = data_x_f.copy()
data_x_m_rs = data_x_m.copy()
data_x_full_rs[['Amount','Hour']] = RobustScaler().fit_transform(data_x_full_rs[['Amount','Hour']])
data_x_f_rs[['Amount','Hour']] = RobustScaler().fit_transform(data_x_f_rs[['Amount','Hour']])
data_x_m_rs[['Amount','Hour']] = RobustScaler().fit_transform(data_x_m_rs[['Amount','Hour']])
保留这么多变量,是因为会写多个方法,直接传参数进行对比;
当数据不平衡的时候,要么降|下|欠采样,要么过|上采样,要么就用原数据(废话,那么我们三个都演示一遍,同时看看效果)
4.欠采样
欠采样方法有很多:不太明白的可以看大神的文章
数据预处理-上采样(过采样)与下采样(欠采样) - 知乎 (zhihu.com)
RandomUnderSampler,简单的从0样本中,抽和1样本数量一样的,组成一个小样本,不聪明;
ClusterCentroids---原型生成(prototype generation),就是会通过聚类造出新的0样本,此时样本的数据并不是原来的,而是新生成的,不考虑;
EasyEnsemble 和 BalanceCascade 不考虑,不如直接原数据。
TomekLinks,EditedNearestNeighbours可以减少多数类样本,实测,只减少了一点点,比蚊子腿还少;
# from imblearn.under_sampling import EditedNearestNeighbours
# enn = EditedNearestNeighbours(n_jobs=-1,kind_sel='mode')
# X_resampled, y_resampled = enn.fit_resample(data_x_full, data_y)
# y_resampled.value_counts()
# kind_sel=all仅仅只删除了不到两百个
# kind_sel=mode仅仅只删除了19个
# from imblearn.under_sampling import TomekLinks
# tl = TomekLinks(n_jobs=-1)
# X_resampled_nm1, y_resampled = tl.fit_resample(data_x_full, data_y)
# y_resampled.value_counts()
# 仅仅只删除了26个
最终,留给我们的只有Nearmiss,注意Nearmiss有三种方法,其中第一种不太可取,太容易受到异常值的影响了;
4.1 数据泄露-错误示范
------------------接下来,我要进行数据泄露的错误示范了------------------
from imblearn.under_sampling import NearMiss
nm = NearMiss(version=2,n_jobs=-1,sampling_strategy='majority')
x_resampled, y_resampled = nm.fit_resample(data_x_full, data_y)
y_resampled.value_counts()
# 0 492
# 1 492
# 完整地演示错误的方法---数据泄露
# 这一步,错在,降采样时候,选出来的0样本,都是被1样本挑选的
继续在错误道路上演示,往下看就明白了:
先拿逻辑回归、SVC、KNN三个模型试试水:
from sklearn.svm import SVC
from sklearn.neighbors import KNeighborsClassifier
classifiers= {'Logistic':LogisticRegression()
,'SVC':SVC(probability=True)
,'KNN':KNeighborsClassifier()}
x_train, x_test, y_train, y_test = train_test_split(x_resampled, y_resampled, test_size=0.2
,random_state=1,stratify=y_resampled)
# 存储结果
result_df = pd.DataFrame()
for name,clf in classifiers.items():
clf.fit(x_train,y_train)
# 预测
y_pred_label = clf.predict(x_test) # 为0和1的标签
y_pred_prob = clf.predict_proba(x_test)[:,1] # 0到1之间的浮点数,为1的概率
# 获取AUC
auc = roc_auc_score(y_test,y_pred_prob) # 此处传概率
# 获取AP
ap = average_precision_score(y_test,y_pred_prob) # 此处传概率
# 获取固定阈值的三个指标
precision = precision_score(y_test,y_pred_label) # 传预测标签
accuracy = accuracy_score(y_test,y_pred_label) # 传预测标签
recall = recall_score(y_test,y_pred_label) # 传预测标签
# 计算f1_score = 2P*R/(P+R)
f1 = f1_score(y_test,y_pred_label)
# 结果写入
result_df.loc[name,'准确率'] = accuracy
result_df.loc[name,'精确率'] = precision
result_df.loc[name,'召回率'] = recall
# AUC、AP、F1写入
result_df.loc[name,'F1'] = f1
result_df.loc[name,'AUC'] = auc
result_df.loc[name,'AP'] = ap
result_df
不到1000个数据,0.8训练,0.2验证,结果不好才怪,实际上我们的目的是,用降采样的数据训练模型,然后预测原本严重失衡的数据,如果效果好则证明这个方法行得通。几个评价稍微往下一点再解释
随意找下最佳参数
lr_para = {"penalty": ['l1', 'l2'], 'C': [0.001, 0.01, 0.1, 1, 10, 100, 1000],'max_iter':[100,300,500]}
knn_para = {"n_neighbors": list(range(3,8,2)), 'algorithm': ['auto', 'ball_tree', 'kd_tree', 'brute']}
svc_para = {'C': [0.1, 0.4, 0.7, 1], 'kernel': ['rbf', 'poly', 'sigmoid', 'linear']}
lr_grid = GridSearchCV(LogisticRegression(solver='liblinear'),lr_para,scoring='average_precision',n_jobs=-1,cv=5)
lr_grid.fit(x_resampled, y_resampled)
print(lr_grid.best_params_)
lr_best = lr_grid.best_estimator_
print(lr_grid.cv_results_['mean_test_score'].mean())
knn_grid = GridSearchCV(KNeighborsClassifier(),knn_para,scoring='average_precision',n_jobs=-1,cv=5)
knn_grid.fit(x_resampled, y_resampled)
print(knn_grid.best_params_)
knn_best = knn_grid.best_estimator_
print(knn_grid.cv_results_['mean_test_score'].mean())
svc_grid = GridSearchCV(SVC(probability=True),svc_para,scoring='average_precision',n_jobs=-1,cv=5)
svc_grid.fit(x_resampled, y_resampled)
print(svc_grid.best_params_)
svc_best = svc_grid.best_estimator_
print(svc_grid.cv_results_['mean_test_score'].mean())
{'C': 0.1, 'max_iter': 100, 'penalty': 'l2'}
0.9277273724880362
{'algorithm': 'auto', 'n_neighbors': 7}
0.9563412794432787
{'C': 1, 'kernel': 'rbf'}
0.9305041035689255
甚至绘制一个学习曲线
# 绘制学习曲线
from sklearn.model_selection import learning_curve
def plot_learn_curve(model,x,y,ylim=None,cv=5,n_jobs=-1,scoring='accuracy',title=None):
x_train, x_test, y_train, y_test = train_test_split(x, y,
test_size=0.2, random_state=1,stratify=y)
train_nums,train_scores,test_scores = learning_curve(model,x_train
,y_train,cv=cv,scoring=scoring,n_jobs=-1
,train_sizes=np.linspace(0.1,1,5))
# 根据训练数量,计算cv个结果
mean_train_score = np.mean(train_scores,axis=1)
std_train_score = np.std(train_scores,axis=1,ddof=0)
mean_test_score = np.mean(test_scores,axis=1)
std_test_score = np.std(test_scores,axis=1,ddof=0)
# 绘制阴影带
plt.fill_between(train_nums,mean_train_score-std_train_score,mean_train_score+std_train_score
,alpha=0.2,color='lightblue')
plt.fill_between(train_nums,mean_test_score-std_test_score,mean_test_score+std_test_score
,alpha=0.2,color='indianred')
# 绘制折线
plt.plot(train_nums,mean_train_score,'o-',color='lightblue',markersize=4,alpha=.5,label='训练')
plt.plot(train_nums,mean_test_score,'o-',color='r',markersize=4,alpha=.5,label='测试')
plt.legend(loc='lower right')
plt.grid(axis='y')
plt.title(title)
if ylim:
plt.ylim(ylim)
return plt
plot_learn_curve(lr_best,x_resampled,y_resampled,ylim=(0.8,1.01),title='逻辑回归')
plot_learn_curve(knn_best,x_resampled,y_resampled,ylim=(0.8,1.01),title='KNN')
plot_learn_curve(svc_best,x_resampled,y_resampled,ylim=(0.8,1.01),title='SVC')
# Y轴是准确率 = TP+TN / ALL
我们降采样,数据从28万多,变成了492:492,我们用这不到1000的数据,训练了模型,最终还是要去预测本来就是不平衡的原数据。
def check_result(estimaotr,x,y):
"这一步,我们用欠采样的数据去预测原数据集,看看效果"
result_df = pd.DataFrame()
skf = StratifiedKFold(n_splits=5,shuffle=True,random_state=1)
round=1
for train_index,test_index in skf.split(x,y):
x_train,x_test = x.iloc[train_index],x.iloc[test_index]
y_train,y_test = y[train_index],y[test_index]
# 预测
y_pred_label = estimaotr.predict(x_test) # 为0和1的标签
y_pred_prob = estimaotr.predict_proba(x_test)[:,1] # 0到1之间的浮点数,为1的概率
# 获取AUC
auc = roc_auc_score(y_test,y_pred_prob) # 此处传概率
# 获取AP
ap = average_precision_score(y_test,y_pred_prob)
# 获取固定阈值的三个指标
precision = precision_score(y_test,y_pred_label)
accuracy = accuracy_score(y_test,y_pred_label)
recall = recall_score(y_test,y_pred_label)
# 计算f1_score = 2P*R/(P+R)
f1 = f1_score(y_test,y_pred_label)
# 结果写入
result_df.loc[round,'准确率'] = accuracy
result_df.loc[round,'精确率'] = precision
result_df.loc[round,'召回率'] = recall
# AUC、AP、F1写入
result_df.loc[round,'F1'] = f1
result_df.loc[round,'AUC'] = auc
result_df.loc[round,'AP'] = ap
round+=1
# 输出和返回结果
result_df.loc['mean',:] = result_df.mean(axis=0)
return result_df
结果:
首先精确率理所当然地崩掉了,只用了不到500个数据训练,现在来了28万+/5,肯定结果差;
召回率肯定挺好,毕竟都是训练过的数据,做过的题目;
准确率来看,逻辑回归最好,矮个中的高个。
评价指标解释:
f1 = 2*precision*recall /(precision+recall )
AUC(area under curve)= ROC曲线下的面积,越接近1越好,AUC与手动设定阈值无关。
AP = PR曲线下的面积,越接近1越好,由于ROC曲线对数据不平衡不敏感,故我们基本上用AP作为模型好坏判断标准;注意AP用的是average_precision_score这个方法,与auc(recalls,precisions)计算曲线下的面积略有差异,AP相当于一个非常逼近真实值的近似值;
AUC,AP,PR曲线下的面积,不是本文讲解的重点,不明白的请查阅相关文章,我也很难短时间解释...
4.2 正确示范
从上述欠采样方法来看,小数据训练模型,再预测大的失衡的数据,结果很差,但别忘了我说过这是个错误示范,以下为正确示范,虽然结果还是欠采样不可行,但是我们没有数据泄露(data leakage)
# 验证数据不能提前泄露给模型,始终留出来一部分用于验证
from imblearn.under_sampling import NearMiss
lr_para = {"penalty": ['l1', 'l2'], 'C': [0.001, 0.01, 0.1, 1, 10, 100, 100],'max_iter':[100,300,500]}
skf = StratifiedKFold(n_splits=5,random_state=1,shuffle=True)
result_df = pd.DataFrame()
num_iter = 1
for train_index,test_index in skf.split(data_x_full_ss,data_y):
x_train_original,x_test_original = data_x_full_ss.iloc[train_index],data_x_full_ss.iloc[test_index]
y_train_original,y_test_original = data_y[train_index],data_y[test_index]
# 正确做法,先分5折,利用其中4折进行降采样
x_undersample,y_undersample=NearMiss(version=3,n_jobs=-1).fit_resample(x_train_original,y_train_original)
# 用降采样的平衡数据训练模型
model = LogisticRegression(random_state=1,n_jobs=-1,solver='liblinear')
# 此处再添加一个gridsearchcv
grid = GridSearchCV(model,lr_para,scoring='average_precision',n_jobs=-1,cv=5)
grid.fit(x_undersample,y_undersample)
best_model = grid.best_estimator_
print('找到的最佳参数',grid.best_params_)
print('AP最佳',grid.best_score_)
# 预测原始数据集
y_pred_label = best_model.predict(x_test_original)
y_pred_prob = best_model.predict_proba(x_test_original)[:,1]
# 下面的跟之前一样--------------------------
# 获取AUC
auc = roc_auc_score(y_test_original,y_pred_prob) # 此处传概率
# 获取AP
ap = average_precision_score(y_test_original,y_pred_prob) # 此处传概率
# 获取固定阈值的三个指标
precision = precision_score(y_test_original,y_pred_label) # 传预测标签
accuracy = accuracy_score(y_test_original,y_pred_label) # 传预测标签
recall = recall_score(y_test_original,y_pred_label) # 传预测标签
# 计算f1_score = 2P*R/(P+R)
f1 = f1_score(y_test_original,y_pred_label)
# 写入结果
result_df.loc[num_iter,'准确率'] = accuracy
result_df.loc[num_iter,'精确率'] = precision
result_df.loc[num_iter,'召回率'] = recall
# AUC、AP、F1写入
result_df.loc[num_iter,'F1'] = f1
result_df.loc[num_iter,'AUC'] = auc
result_df.loc[num_iter,'AP'] = ap
num_iter+=1
result_df
# 可以看到召回还行,但是精确率极低,即FP很多,因为训练的0数据太少,学习不充分
# 对比上一个结果,差异很大,原因是上一个方法,欠采样的数据,存在数据泄露的问题
简单来讲,我们要始终留一折数据,不让模型知道,正确示范中,我们把数据首先分为5折,始终有1折用于测试,另外4折用来进行降采样,然后训练模型,中间套了一个网格搜索最佳参数,然后用最佳参数的模型,去预测根本就没有接触过的新数据(留的那一折),可以看到,召回率从接近1掉到0.83左右,这才是正确的方法。
5.过采样
5.1 逻辑回归
基本上代码同欠采样差不多,我们考虑好一点的BorderlineSMOTE,避免SMOTE受到异常值的影响。
from imblearn.over_sampling import SMOTE,BorderlineSMOTE
from sklearn.model_selection import RandomizedSearchCV
def check_oversample(x,y,kind='borderline-1'):
# 超参数选择
lr_para = {"penalty": ['l1', 'l2'], 'C': [0.001, 0.01, 0.1, 1, 10, 100, 1000],'max_iter':[100,300,500]}
# 边界smote
blsm = BorderlineSMOTE(random_state=1,n_jobs=-1,kind=kind)
result_df = pd.DataFrame()
num_iter = 1
for train_index,test_index in skf.split(x,y):
x_train_original,x_test_original = x.iloc[train_index],x.iloc[test_index]
y_train_original,y_test_original = y[train_index],y[test_index]
# 用4折去生成新的数据
x_oversample,y_oversample = blsm.fit_resample(x_train_original,y_train_original)
model = LogisticRegression(random_state=1,n_jobs=-1,solver='liblinear')
# 上采样,数据量大,该演示将网格范围缩小一些
# 添加一个RandomizedSearchCV,只选4个,选多了太耗时间
randcv = RandomizedSearchCV(model,lr_para,n_iter=4,scoring='average_precision_score',n_jobs=-1,cv=5,random_state=1)
randcv.fit(x_oversample,y_oversample)
model_best = randcv.best_estimator_
print(randcv.best_params_)
# 预测原始数据集
y_pred_label = model_best.predict(x_test_original)
y_pred_prob = model_best.predict_proba(x_test_original)[:,1]
# 获取AUC
auc = roc_auc_score(y_test_original,y_pred_prob) # 此处传概率
# 获取AP
ap = average_precision_score(y_test_original,y_pred_prob) # 此处传概率
# 获取固定阈值的三个指标
precision = precision_score(y_test_original,y_pred_label) # 传预测标签
accuracy = accuracy_score(y_test_original,y_pred_label) # 传预测标签
recall = recall_score(y_test_original,y_pred_label) # 传预测标签
# 计算f1_score = 2P*R/(P+R)
f1 = f1_score(y_test_original,y_pred_label)
# 写入结果
result_df.loc[num_iter,'准确率'] = accuracy
result_df.loc[num_iter,'精确率'] = precision
result_df.loc[num_iter,'召回率'] = recall
# AUC、AP、F1写入
result_df.loc[num_iter,'F1'] = f1
result_df.loc[num_iter,'AUC'] = auc
result_df.loc[num_iter,'AP'] = ap
num_iter+=1
result_df.loc['mean',:] = result_df.mean(axis=0)
return result_df
注意一个细节:我们RandomizedSearchCV(scoring='average_precision'),此处的average_precision从字面上看是平均精确率,实际上是AP面积。
3.3. Metrics and scoring: quantifying the quality of predictions — scikit-learn 1.4.1 documentation
sklearn.metrics.average_precision_score — scikit-learn 1.4.1 documentation
当然,由于我们过采样了, 数据平衡了,用scoring='accuracy'也没问题。
由于要找最佳参数,这样训练量就大了,就只用RandomizedSearchCV。
可以看到,精确率还是崩掉了,召回率看起来还行。
5.2 Xgboost-调参
搞不好是我们模型没选对,就没用一点强力一点的模型,换xgboost试试,但是xgboost中间套一个找最佳参数,只能用sklearn的API,训练时间太久了,代码如下,该代码我没跑完,不建议运行。
# 运行时间很长,不建议使用
# import xgboost as xgb
# from xgboost import XGBClassifier
# def check_oversample2(x,y,kind='borderline-1'):
# # 超参数选择
# xgboost_para = {"max_depth": [6,7,8], 'learning_rate': [0.01,0.05,0.1,0.2]
# ,'n_estimators':[300,500,1000],'reg_lambda':[0.1,1,10],'gamma':[0.001,0.01,0.1]}
# # 边界smote
# blsm = BorderlineSMOTE(random_state=1,n_jobs=-1,kind=kind)
# result_df = pd.DataFrame()
# num_iter = 1
# for train_index,test_index in skf.split(x,y):
# x_train_original,x_test_original = x.iloc[train_index],x.iloc[test_index]
# y_train_original,y_test_original = y[train_index],y[test_index]
# # 用4折去生成新的数据
# x_oversample,y_oversample = blsm.fit_resample(x_train_original,y_train_original)
# model = XGBClassifier(objective='binary:logistic',booster='gbtree',tree_method='exact',
# grow_policy='depthwise',n_jobs=-1,subsample=0.8,colsample_bytree=0.8
# ,random_state=1,early_stopping_rounds=20)
# # 上采样,数据量大,该演示将网格范围缩小一些
# # 添加一个RandomizedSearchCV,只选4个,选多了太耗时间
# randcv = RandomizedSearchCV(model,xgboost_para,n_iter=10,scoring='average_precision',n_jobs=-1,cv=5,random_state=1)
# randcv.fit(x_oversample,y_oversample,eval_set = [(x_oversample,y_oversample)])
# model_best = randcv.best_estimator_
# print(randcv.best_params_)
# # 预测原始数据集
# y_pred_label = model_best.predict(x_test_original)
# y_pred_prob = model_best.predict_proba(x_test_original)[:,1]
# # 获取AUC
# auc = roc_auc_score(y_test_original,y_pred_prob) # 此处传概率
# # 获取AP
# ap = average_precision_score(y_test_original,y_pred_prob) # 此处传概率
# # 获取固定阈值的三个指标
# precision = precision_score(y_test_original,y_pred_label) # 传预测标签
# accuracy = accuracy_score(y_test_original,y_pred_label) # 传预测标签
# recall = recall_score(y_test_original,y_pred_label) # 传预测标签
# # 计算f1_score = 2P*R/(P+R)
# f1 = f1_score(y_test_original,y_pred_label)
# # 写入结果
# result_df.loc[num_iter,'准确率'] = accuracy
# result_df.loc[num_iter,'精确率'] = precision
# result_df.loc[num_iter,'召回率'] = recall
# # AUC、AP、F1写入
# result_df.loc[num_iter,'F1'] = f1
# result_df.loc[num_iter,'AUC'] = auc
# result_df.loc[num_iter,'AP'] = ap
# num_iter+=1
# result_df.loc['mean',:] = result_df.mean(axis=0)
# return result_df
5.3 Xgboost-简易版
那我们手动写点参数,主要是跳过网格搜索参数那一项:
from xgboost import XGBClassifier
def check_oversample_xgb_simple(x,y,kind='borderline-1',tree_method='auto'):
# 超参数选择,就不网格搜索了
# xgboost_para = {"max_depth": [6,7,8], 'learning_rate': [0.01,0.05,0.1,0.2]
# ,'n_estimators':[300,500,1000],'reg_lambda':[0.1,1,10],'gamma':[0.001,0.01,0.1]}
# 边界smote
blsm = BorderlineSMOTE(random_state=1,n_jobs=-1,kind=kind)
result_df = pd.DataFrame()
num_iter = 1
for train_index,test_index in skf.split(x,y):
x_train_original,x_test_original = x.iloc[train_index],x.iloc[test_index]
y_train_original,y_test_original = y[train_index],y[test_index]
# 用4折去生成新的数据
x_oversample,y_oversample = blsm.fit_resample(x_train_original,y_train_original)
model = XGBClassifier(n_estimators=1000,max_depth=7,
grow_policy='depthwise',learning_rate=0.1,
verbosity=0,objective='binary:logistic',
booster='gbtree',tree_method=tree_method, # exact,approx,hist 三选1
n_jobs=-1,gamma=0.01,
subsample=0.8,colsample_bytree=0.8,reg_lambda=0.2,
random_state=1,early_stopping_rounds=20,
eval_metric='aucpr')
# 直接训练,搜索参数太耗时
model.fit(x_oversample,y_oversample,eval_set = [(x_oversample,y_oversample)])
print(model.get_params)
# 预测原始数据集
y_pred_label = model.predict(x_test_original)
y_pred_prob = model.predict_proba(x_test_original)[:,1]
# 获取AUC
auc = roc_auc_score(y_test_original,y_pred_prob) # 此处传概率
# 获取AP
ap = average_precision_score(y_test_original,y_pred_prob) # 此处传概率
# 获取固定阈值的三个指标
precision = precision_score(y_test_original,y_pred_label) # 传预测标签
accuracy = accuracy_score(y_test_original,y_pred_label) # 传预测标签
recall = recall_score(y_test_original,y_pred_label) # 传预测标签
# 计算f1_score = 2P*R/(P+R)
f1 = f1_score(y_test_original,y_pred_label)
# 写入结果
result_df.loc[num_iter,'准确率'] = accuracy
result_df.loc[num_iter,'精确率'] = precision
result_df.loc[num_iter,'召回率'] = recall
# AUC、AP、F1写入
result_df.loc[num_iter,'F1'] = f1
result_df.loc[num_iter,'AUC'] = auc
result_df.loc[num_iter,'AP'] = ap
num_iter+=1
result_df.loc['mean',:] = result_df.mean(axis=0)
return result_df
XGBOOST不愧是大杀器,结果看起来不错!!!而且我只是随意输了点参数。
注意我们的eval_metric='aucpr',PR曲线下的面积,过采样下,选error,map都可以。
XGBoost Parameters — xgboost 2.0.3 documentation
总结:过采样下,逻辑回归小弱鸡效果较差,XGBOOOST结果还不错;那么,我们再试试直接用原数据训练模型。
6.直接用原数据
6.1 xgboost原数据
那我们接着用xgboost,直接用原数据,用原生train方法快点(后面实测,速度和sklearn没太大区别,离谱)
import xgboost as xgb
def use_xgboost(x,y,tree_method='auto',max_depth=6,l2=1,subsample=0.8
,colsample_bytree=0.8,eta=0.1,gamma=0.01,seed=1,scale_pos_weight=577,thre=0.5):
result_df = pd.DataFrame()
num_iter =1
for train_index,test_index in skf.split(x,y):
x_train,x_test = x.loc[train_index,:],x.loc[test_index,:]
y_train,y_test = y[train_index],y[test_index]
# 转为Dmatrix
dtrain = xgb.DMatrix(x_train,label=y_train)
dtest = xgb.DMatrix(x_test,label=y_test)
# 写参数
para_dict = {'booster':'gbtree',
'objective':'binary:logistic',
'eval_metric':['error','aucpr'],
'max_depth': max_depth,
'lambda' :l2,
'subsample':subsample,
'colsample_bytree':colsample_bytree,
'eta':eta,
'seed':seed,
'nthread':-1,
'gamma':gamma,
'scale_pos_weight': scale_pos_weight, # 0比1 的比例
}
# 其他
eval_list = [(dtrain,'train'),(dtest,'test')]
# 训练
xgb_model = xgb.train(para_dict,dtrain,num_boost_round=2000,evals=eval_list,
early_stopping_rounds=20,verbose_eval=100)
# 预测
y_pred_prob = xgb_model.predict(dtest,iteration_range=(0,xgb_model.best_ntree_limit)) # 0到1之间的浮点数,为1的概率
# 输出为概率,用阈值变为标签,可再加个阈值调节
y_pred_label = (y_pred_prob>=thre)*1
# 保存结果
# 获取AUC
auc = roc_auc_score(y_test,y_pred_prob) # 此处传概率
# 获取AP
ap = average_precision_score(y_test,y_pred_prob) # 此处传概率
# 获取固定阈值的三个指标
precision = precision_score(y_test,y_pred_label) # 传预测标签
accuracy = accuracy_score(y_test,y_pred_label) # 传预测标签
recall = recall_score(y_test,y_pred_label) # 传预测标签
# 计算f1_score = 2P*R/(P+R)
f1 = f1_score(y_test,y_pred_label)
# 结果写入
result_df.loc[num_iter,'准确率'] = accuracy
result_df.loc[num_iter,'精确率'] = precision
result_df.loc[num_iter,'召回率'] = recall
# AUC、AP、F1写入
result_df.loc[num_iter,'F1'] = f1
result_df.loc[num_iter,'AUC'] = auc
result_df.loc[num_iter,'AP'] = ap
num_iter+=1
result_df.loc['mean',:] = result_df.mean(axis=0)
return result_df
注意我写的这个方法,参数可以稍微自由点调整,把scale_pos_weight=577,即负/正样本比例,主要看这个AP,0.81比过采样的0.85低一些。
6.2 取消权重比例
原数据太多,很容易让各位看官懵逼,解释下,data_x_full_ss,即data_x_full(全特征)+StandardScaler标准化(金额和小时两列)。scale_pos_weight=1跟没设是一样的。
可以看到,同过采样相比(忽略设了正负样本权重那个结果),AP变高了,0.85->0.86,精确率提高不少0.86->0.95,召回降低了0.817->0.7906,主要AP变高了就好,召回率精确率这东西,可以通过设置阈值调整。
比如:一般我们认为>=0.5级为正样本(label=1),但没有规定非的是0.5,我们设0.4,0.3,甚至可以来个循环遍历,阈值降低,一般情况下,召回率是绝对会提高,精确率在大部分情况下是会下降的,但精确率在一小段区间内,可能会存在不降反而增加一点的情况;因为阈值降低则TP、FP都变多,TP/(TP+FP),但可能有一小段阈值区间,TP变多的影响力大于FP变多的影响,但总体会下降的。
通过更改阈值,实现保大还是保小的难题,但模型的AP越高,我们认为该模型的性能越好。
树模型+xgboost果然有点搞头,那么,既然原数据的结果更好,那我们就没必要费那个劲搞过采样了,直接在原数据上动刀子,如果要调参,要么接受cpu--tree_methpd==exact的一段时间的等待,要么换gpu_hist,不过直方图的效果始终要差点...
7. 测试其他模型
----------------以下内容全部用原数据(比例严重失衡)--------------
文章内容已经快3万字,下面我们用原数据,简要点带过几个不同算法,比较几个主要参数的不同,同时对比一下我们选个3种特征选择方法,2种标准化的区别,写了一些方法,可以复制过去自己试试。每次我都要带上random_state的原因是,我搞这个案例,发现不同的数据集划分种子,结果差异还不小。
都是原数据,按照label比例平衡划分5份,先说结果,再附上代码。
7.1 knn
用欧氏距离的,金额列有很大的数字,所以标准化后结果会好一些,但总体来看KNN效果一般化,AP均值0.82左右;其中weight_algo='distance'好于weight_algo='uniform',但注意两点之间distance极小,可能会导致权重过大的问题,两种归一化效果差不多。
def use_knn2(n_neigh=5,n_spli=5,weight_algo='uniform',in_x=None,in_y=None,scale=None,n_job=-1):
# 调用两个标准化
ss = StandardScaler()
rs = RobustScaler()
# 调用knn,划分数据集
knn = KNeighborsClassifier(n_neighbors=n_neigh,weights=weight_algo,n_jobs=n_job)
skf = StratifiedKFold(n_splits=5,shuffle=True,random_state=1)
result_df = pd.DataFrame(columns=['准确率','精确率','召回率','AUC','AP','F1','K'])
num_iter = 0
# 如果有标准化则使用,只有['Hour','Amount']两列标准化
if scale =='StandardScaler':
in_x_new = in_x.copy() # 不动原数据,不加copy也可以
in_x_new[['Hour','Amount']] = ss.fit_transform(in_x_new[['Hour','Amount']])
in_x = in_x_new # 传递回去,主要是懒得改代码了
if scale =='RobustScaler':
in_x_new = in_x.copy() # 不动原数据,不加copy也可以
in_x_new[['Hour','Amount']] = rs.fit_transform(in_x_new[['Hour','Amount']])
in_x = in_x_new # 传递回去
for train_index,test_index in skf.split(in_x,in_y):
# 分层划分数据集
x_train,x_test = in_x.loc[train_index,:],in_x.loc[test_index,:]
y_train,y_test = in_y[train_index],in_y[test_index]
# 训练
knn.fit(x_train,y_train)
# 预测
y_pred_label = knn.predict(x_test) # 为0和1的标签
y_pred_prob = knn.predict_proba(x_test)[:,1] # 0到1之间的浮点数,为1的概率
# 获取AUC
auc = roc_auc_score(y_test,y_pred_prob) # 此处传概率
# 获取AP
ap = average_precision_score(y_test,y_pred_prob) # 此处传概率
# 获取固定阈值的三个指标
precision = precision_score(y_test,y_pred_label) # 传预测标签
accuracy = accuracy_score(y_test,y_pred_label) # 传预测标签
recall = recall_score(y_test,y_pred_label) # 传预测标签
# 计算f1_score = 2P*R/(P+R)
f1 = f1_score(y_test,y_pred_label)
# 结果写入
result_df.loc[num_iter,'准确率'] = accuracy
result_df.loc[num_iter,'精确率'] = precision
result_df.loc[num_iter,'召回率'] = recall
# AUC、AP、F1写入
result_df.loc[num_iter,'F1'] = f1
result_df.loc[num_iter,'AUC'] = auc
result_df.loc[num_iter,'AP'] = ap
result_df.loc[num_iter,'K'] = n_neigh # 记录找了多少个K
num_iter+=1
# 输出一波结果
print('无脑为0的准确率',in_y.value_counts()[0]/len(in_y))
return result_df
7.2 NearestCentroid质心聚类
这是聚类算法的一种,大概是高维空间,训练时定好一个高维空间的圆心和半径,这样predict就很快,KNN每次都要算所有数据很麻烦,结果还不如KNN,毕竟没有算的那么仔细。
from sklearn.neighbors import NearestCentroid
def use_nc(in_x=None,in_y=None,scale=None,met='euclidean'):
ss = StandardScaler()
rs = RobustScaler()
nc = NearestCentroid(metric=met)
skf = StratifiedKFold(n_splits=5,shuffle=True,random_state=1)
result_df = pd.DataFrame(columns=['准确率','精确率','召回率','F1'])
# 如果有标准化则使用,只有['Hour','Amount']两列标准化
if scale =='StandardScaler':
in_x_new = in_x.copy() # 不动原数据
in_x_new[['Hour','Amount']] = ss.fit_transform(in_x_new[['Hour','Amount']])
in_x = in_x_new # 传递回去,主要是懒得改代码了
if scale =='RobustScaler':
in_x_new = in_x.copy() # 不动原数据
in_x_new[['Hour','Amount']] = rs.fit_transform(in_x_new[['Hour','Amount']])
in_x = in_x_new # 传递回去
num_iter = 1
for train_index,test_index in skf.split(in_x,in_y):
# 分层划分数据集
x_train,x_test = in_x.loc[train_index,:],in_x.loc[test_index,:]
y_train,y_test = in_y[train_index],in_y[test_index]
nc.fit(x_train,y_train)
y_pred = nc.predict(x_test) # 这个好像没有计算概率的方法
# 计算3个指标
precision = precision_score(y_test,y_pred)
accuracy = accuracy_score(y_test,y_pred)
recall = recall_score(y_test,y_pred)
# 计算f1_score = 2P*R/(P+R)
f1 = f1_score(y_test,y_pred)
result_df.loc[num_iter,'准确率'] = accuracy
result_df.loc[num_iter,'精确率'] = precision
result_df.loc[num_iter,'召回率'] = recall
result_df.loc[num_iter,'F1'] = f1
result_df.loc[num_iter,'第几次'] = num_iter # 记录找了多少个K
num_iter+=1
result_df.loc['mean',:] = result_df.mean(axis=0)
# print(nc.centroids_)
return result_df
7.3 AdaBoostClassifier
可以改变训练样本中预测出错的权重,所以用AdaBoost不需要考虑设置样本权重的问题,就算设为577:1,多次迭代下来,基本没区别,最好自己先定一个base_decision_tree传给AdaBoost,效果还行,缺点是boosting中的龟速,跟并行训练完全不沾边,我并不想再运行这东西了,并且拒绝给出代码。
7.4RandomForestClassifier
不用想肯定没有xgboost厉害,不过这个模型,是一个比较好进行特征筛选的模型。随机森林,总体效果较xgboost差一些,并且不应该设置样本权重,设置了效果会变差,看来树模型都不太吃样本权重这一套。
from sklearn.metrics import auc
def use_rft(x,y,split=False,depth=6,row_sample=0.8,label_weight=None,r_state=1):
"""
1.树深度6-8,可自选
2.行采样0.8,可更改
3.列采样不动了,默认sqrt
4.重点看看class_weight,默认的1比1和balanced的区别
5.随机种子r_state固定数据划分
6.split,否,则只划分一次,是,则划5折
7.写到后面发现应该写2个方法,代码短点,但是这样合成一个比较直观吧
"""
rft_clf = RandomForestClassifier(n_estimators=1000,max_depth=depth,n_jobs=-1,
max_samples=row_sample,class_weight=label_weight)
result_df = pd.DataFrame() # 存结果
round = 1 # 索引就从1开始吧
if not split:
x_train,x_test,y_train,y_test = train_test_split(x,y,test_size=0.2
,random_state=r_state,shuffle=True
,stratify=y)
rft_clf.fit(x_train,y_train)
y_pred_label = rft_clf.predict(x_test)
y_pred_prob = rft_clf.predict_proba(x_test)[:,1] # 取为1的概率
#用标签算的指标
accuracy = accuracy_score(y_test,y_pred_label)
precision = precision_score(y_test,y_pred_label)
recall = recall_score(y_test,y_pred_label)
# 用概率算的指标
fpr,tpr,thresholds = roc_curve(y_test,y_pred_prob)
under_roc = auc(fpr,tpr) # 计算AUC面积
# 计算PR曲线面积--auc计算法
pp,rr,tt = precision_recall_curve(y_test,y_pred_prob)
under_pr = auc(rr,pp)
# 综合指标
f1 = f1_score(y_test,y_pred_label)
# 写入结果
result_df.loc[round,'准确率'] = accuracy
result_df.loc[round,'精确率'] = precision
result_df.loc[round,'召回率'] = recall
result_df.loc[round,'ROC面积'] = under_roc
result_df.loc[round,'PR面积'] = under_pr
result_df.loc[round,'F1_score'] = f1
if split: # 如果进行多次划分
skf = StratifiedKFold(n_splits=5,shuffle=True,random_state=r_state)
for train_index,test_index in skf.split(x,y):
# 分层划分数据集
x_train,x_test = x.loc[train_index,:],x.loc[test_index,:]
y_train,y_test = y[train_index],y[test_index]
rft_clf.fit(x_train,y_train)
y_pred_label = rft_clf.predict(x_test)
y_pred_prob = rft_clf.predict_proba(x_test)[:,1] # 取为1的概率
# 全部复制粘贴即可
#用标签算的指标
accuracy = accuracy_score(y_test,y_pred_label)
precision = precision_score(y_test,y_pred_label)
recall = recall_score(y_test,y_pred_label)
# 用概率算的指标
fpr,tpr,thresholds = roc_curve(y_test,y_pred_prob)
under_roc = auc(fpr,tpr) # 计算AUC面积
# 计算PR曲线面积--auc计算法
pp,rr,tt = precision_recall_curve(y_test,y_pred_prob)
under_pr = auc(rr,pp)
# 综合指标
f1 = f1_score(y_test,y_pred_label)
# 写入结果
result_df.loc[round,'准确率'] = accuracy
result_df.loc[round,'精确率'] = precision
result_df.loc[round,'召回率'] = recall
result_df.loc[round,'ROC面积'] = under_roc
result_df.loc[round,'PR面积'] = under_pr
result_df.loc[round,'F1_score'] = f1
round+=1 # 索引变化一下
return result_df
7.5 SVC
数据一大这模型就很慢,算了吧。
7.6 lightgbm
从PR曲线面积来看,总体优于上述模型,但略低于XGBOOST,如果在数据很大, xgboost调参搞不住的情况下,lightgbm是个不错的选择,结果差一点,但优于其他算法,主要速度快,可以方便调参。
import lightgbm
lightgbm.__version__
'4.1.0'
直接调个参,因为要调的还不少,就随机搜索下,跟最佳结果很逼近了:
from sklearn.model_selection import RandomizedSearchCV
def use_lightgbm_grid(x,y,r_state=1):
t1 = time.time()
skf = StratifiedKFold(n_splits=5,shuffle=True,random_state=r_state)
num_iter = 1
# 要调的参数
light_paras = {'max_depth':[6,7,8,9],'learning_rate':[0.01,0.05,0.1,0.2]
,'n_estimators':[100,300,700,1000],'min_child_weight':[0.00001,0.0001,0.001,0.01]
,'min_child_samples':[5,10,15,20],'colsample_bytree':[0.6,0.7,0.8]
,'reg_lambda':[0.1,0.5,1,5,10]}
# 初始化模型
model_light = LGBMClassifier(boosting_type='gbdt',num_leaves=31,objective='binary'
,class_weight=None,subsample=1,random_state=1
,n_jobs=-1,importance_type='gain'
)
randse = RandomizedSearchCV(model_light,light_paras,n_iter=20,scoring='average_precision'
,n_jobs=-1,cv=5,random_state=r_state)
randse.fit(x,y)
print(randse.best_params_)
print(randse.best_score_)
best_light = randse.best_estimator_
t2 = time.time()
print('耗时',t2-t1)
return best_light
my_light_model = use_lightgbm_grid(data_x_full,data_y)
调参结果:
# {'reg_lambda': 5, 'n_estimators': 700, 'min_child_weight': 0.001, 'min_child_samples': 10, 'max_depth': 6, 'learning_rate': 0.1, 'colsample_bytree': 0.7}
# 0.8105555895294811
# 耗时 284.40642070770264
用最佳模型训练:
r = 1
for train_index,test_index in skf.split(data_x_full,data_y):
# 分层划分数据集
x_train,x_test = data_x_full.loc[train_index,:],data_x_full.loc[test_index,:]
y_train,y_test = data_y[train_index],data_y[test_index]
my_light_model.fit(x_train,y_train)
y_pred_prob = my_light_model.predict_proba(x_test,pred_contrib=False)[:,1]
y_pred_label = my_light_model.predict(x_test,pred_contrib=False)
# 用标签算的指标
accuracy = accuracy_score(y_test,y_pred_label)
precision = precision_score(y_test,y_pred_label)
recall = recall_score(y_test,y_pred_label)
# 综合指标
f1 = f1_score(y_test,y_pred_label)
# 用概率算的指标
fpr,tpr,thresholds = roc_curve(y_test,y_pred_prob)
under_roc = auc(fpr,tpr) # 计算AUC面积
# 计算PR曲线面积--auc计算法
pp,rr,tt = precision_recall_curve(y_test,y_pred_prob)
under_pr = auc(rr,pp)
# 保存结果
result_df.loc[r,'准确率'] = accuracy
result_df.loc[r,'精确率'] = precision
result_df.loc[r,'召回率'] = recall
result_df.loc[r,'F1_score'] = f1
result_df.loc[r,'ROC面积'] = under_roc
result_df.loc[r,'PR面积'] = under_pr
r+=1
result_df.loc['mean',:] = result_df.mean(axis=0)
result_df
7.7 问题思考
有一个很奇怪的地方,原数据平衡划分5折,训练效果总是比用train_test_split只划分一次差,在train_test_split中,设了平衡划分,在随机森林和逻辑回归中都是这样,难道是两种数据集划分方式,内部是有明显差异的,还是某几个数据金额有极值的影响,此处抛个砖,坐等一位大神解答!!!
其实到这里,本文的结果基本出来了,首先以AP(Area under PR Curve)作为一个模型的评价指标,同时注意看是不是存在召回不错但是精确率很低的情况,模型会预测一个概率,通过搜索不同的阈值,比较总体损失,找到最小损失,比较建议用xgboost,效果比较好,缺点是搜索最佳参数,耗时非常久,不行就用lightgbm代替;
电影总是要搞点什么曲折离奇的,此处演示又踩到坑了,逻辑回归判断不平衡数据,是有很大问题的。
五、以为找到答案
1.使用逻辑回归+原数据
想着逻辑回归嘛,训练比较快,就用这个捣鼓一下算了,直到仔细看了一下结果,发现不对;
from sklearn.metrics import auc
def train_and_save(model,x_train,y_train,x_test,y_test,r,thre):
"""
因为计算概率、预测值,结果写入保存都是相同的,
这里将他们写成一个方法,代码短一点
"""
result_df = pd.DataFrame()
model.fit(x_train,y_train)
y_pred_prob = model.predict_proba(x_test)[:,1] # 为1的概率
y_pred_label = (y_pred_prob>=thre)*1 # 其实乘不乘1都行
# 用标签算的指标
accuracy = accuracy_score(y_test,y_pred_label)
precision = precision_score(y_test,y_pred_label)
recall = recall_score(y_test,y_pred_label)
# 用概率算的指标
fpr,tpr,thresholds = roc_curve(y_test,y_pred_prob)
under_roc = auc(fpr,tpr) # 计算AUC面积
# 计算PR曲线面积--auc计算法
pp,rr,tt = precision_recall_curve(y_test,y_pred_prob)
under_pr = auc(rr,pp)
# 另外一个AP
ap_approxi = average_precision_score(y_test,y_pred_prob)
# 综合指标
f1 = f1_score(y_test,y_pred_label)
# DF添加数据必须用loc,用iloc新建会报错
result_df.loc[r,'准确率'] = accuracy
result_df.loc[r,'精确率'] = precision
result_df.loc[r,'召回率'] = recall
result_df.loc[r,'ROC面积'] = under_roc
result_df.loc[r,'PR面积'] = under_pr
result_df.loc[r,'AP'] = ap_approxi
result_df.loc[r,'F1_score'] = f1
return result_df
# ---------------------------------------------------------------------------
def use_logistic2(x,y,thre=0.5,scale=False,sol=None,split=False,pena='l2'
,c_weight='balanced',r_state=1,beitai=None):
"""
sol即solver的缩写,逻辑回归的几个solver差异请自行研究
beitai,备胎,如果penalty搞成弹性网络,则可以用L1,L2两个正则,
beitai即L1正则系数,beitai=0.3,则L2是0.7
"""
ss = StandardScaler()
rs = RobustScaler()
num_iter = 1
# 初始化模型
model_lr = LogisticRegression(penalty=pena,C=1,class_weight=c_weight
,random_state=1,solver=sol,max_iter=1000
,warm_start=False,n_jobs=-1,l1_ratio=beitai)
# 如果有标准化则使用,只有['Hour','Amount']两列标准化
if scale =='StandardScaler':
# 如果直接fit_transform,原数据就会被标准化,我们并不想动原数据
# 所以用新变量接收,转化后传回去
x_new = x.copy()
x_new[['Hour','Amount']] = ss.fit_transform(x_new[['Hour','Amount']])
x = x_new # 传递回去
if scale =='RobustScaler':
x_new = x.copy()
x_new[['Hour','Amount']] = rs.fit_transform(x_new[['Hour','Amount']])
x = x_new # 传递回去
if split:
skf = StratifiedKFold(n_splits=5,shuffle=True,random_state=r_state)
temp_df = pd.DataFrame()
for train_index,test_index in skf.split(x,y):
# 分层划分数据集
x_train,x_test = x.loc[train_index,:],x.loc[test_index,:]
y_train,y_test = y[train_index],y[test_index]
# 训练
one_piece = train_and_save(model_lr,x_train,y_train,x_test,y_test,num_iter,thre)
temp_df = pd.concat([temp_df,one_piece],axis=0)
num_iter+=1
# 求个平均
temp_df.loc['mean',:] = temp_df.mean(axis=0)
return temp_df
# 简单的方法,只划分一次
if not split:
# 注意设置stratify=y,平衡划分数据
x_train,x_test,y_train,y_test = train_test_split(x,y,test_size=0.2
,random_state=r_state,shuffle=True
,stratify=y)
return train_and_save(model_lr,x_train,y_train,x_test,y_test,num_iter,thre)
df3_lr1 = use_logistic2(data_x_full,data_y,scale='StandardScaler',sol='liblinear',split=True,thre=0.5,c_weight=None,r_state=1)
df3_lr1
# 默认阈值下,召回率,全特征最高,F检验第二,绘图最低
# 默认阈值下,精确率,绘图最高,全特征第二,F检验最低
这一步我们将结果保存单独拎出来,可以缩短点代码;
逻辑回归的结果结果:
(1)逻辑回归的实验,如果设置了class_weight=balanced,则召回会变高,但代价是精确率太低,低到0.1以下,这基本是难以接受的,通过几组对比,class_weight中1的权重越大,召回越高,精确越低,PR面积越小,ROC会变大,所以不建议设class_weight,如果实际业务中,漏判一个欺诈数据代价太大,可尝试用比较小的样本权重一点点试;
(2)阈值默认0.5,只能往下降,阈值往上加,recall降低,同时precision也降低,虽然误判FP减少了,但是TP减少影响更大;而阈值降低召回提高,精确率却并没有太大幅度降低,因为增加的TP影响更大;
(3)随机种子影响比较大,所以我之前的代码都有带一个随机种子,用来固定数据集划分的,尝试换不同的种子,结果差异较大。并且1次性划分和5折划分,基本都是1次性划分结果好一些。
(4)RobustScaler标准化要好点,对于异常值没那么敏感
(5)三份数据集,绘图查看,f_classify方差分析,全特征,从PR曲线面积和默认阈值下的recall,precision来看,方差分析的结果好一点,但总体差异不太明显,同时随机种子影响比较大,得多次实验求平均才可以确定和验证。
上述代码,可以随意修改很多参数,抄过去换数据集、换权重、改阈值即可,
过程数据比较多就不放了,可自行研究,
直接写结果:
以逻辑回归找最优解为例:
# 先找个最佳参数
paras_lr = {'C':[0.01,0.1,0.5,1,10,30],'penalty':['l1','l2'],'max_iter':[100,500,1000]}
model_lr = LogisticRegression(solver='liblinear',n_jobs=-1,random_state=1)
grid_search = GridSearchCV(model_lr,paras_lr,scoring='average_precision',n_jobs=-1,cv=5)
grid_search.fit(data_x_full_rs,data_y)
best_para = grid_search.best_params_
best_model = grid_search.best_estimator_
# 计算不同阈值下,精确率、召回率的变化,并保存
from sklearn.metrics import auc
def do_it2(estimator,x,y):
model = estimator
skf = StratifiedKFold(n_splits=5,shuffle=True,random_state=1)
round=1
t_round=1
result_df = pd.DataFrame()
threshold_df = pd.DataFrame()
# 列表用来计算均值
roc_list,pr_list = [],[]
for train_index,test_index in skf.split(x,y):
# 分层划分数据集
x_train,x_test = x.loc[train_index,:],x.loc[test_index,:]
y_train,y_test = y[train_index],y[test_index]
model.fit(x_train,y_train)
y_pred_prob = model.predict_proba(x_test)[:,1]
# 跟阈值没关系的指标
fpr,tpr,thresholds = roc_curve(y_test,y_pred_prob)
under_roc = auc(fpr,tpr) # 计算AUC面积
# 计算PR曲线面积--auc计算法
pp,rr,tt = precision_recall_curve(y_test,y_pred_prob)
under_pr = auc(rr,pp)
# 与阈值无关,同数据集划分有关
roc_list.append(under_roc)
pr_list.append(under_pr)
for t in np.arange(0.1,1,0.1):
y_pred_label = (y_pred_prob>=t)*1
# 用标签算的指标
accuracy = accuracy_score(y_test,y_pred_label)
precision = precision_score(y_test,y_pred_label)
recall = recall_score(y_test,y_pred_label)
f1 = f1_score(y_test,y_pred_label)
# 跟阈值有关的
threshold_df.loc[t_round,'阈值'] = np.round(t,1)
threshold_df.loc[t_round,'准确率'] = accuracy
threshold_df.loc[t_round,'精确率'] = precision
threshold_df.loc[t_round,'召回率'] = recall
threshold_df.loc[t_round,'F1_score'] = f1
t_round +=1
# 存入平均结果
result_df.loc[round,'ROC面积'] = np.mean(roc_list)
result_df.loc[round,'PR面积'] = np.mean(pr_list)
round+=1
result_df.loc['mean',:] = result_df.mean(axis=0)
return result_df,threshold_df
其实只要获得了预测概率的数组,通过调整阈值,在FP、FN两个值之间取舍,此时PR面积已经是最佳,根据实际业务,判断保大还是保小。
ROC曲线PR曲线是不会随着阈值变的,但会随着训练验证数据而变化。
# 假如漏掉一个正例,即FN多一个,会损失1000块
# 假如误判一个负例,即FP多一个,会损失200块
# 模型最终评价标准,还是让业务总体的损失最小
def comp_money_loss(y,fn_loss=1000,fp_loss=200,path='d:/credit_thre.xlsx'):
table = pd.read_excel(path)
# table = pd.read_excel('d:/credit_thre.xlsx')
numbers = len(y) # 样本总数量
num_neg = np.unique(y,return_counts=True)[1][0] # 为0的数量,N
num_pos = np.unique(y,return_counts=True)[1][1] # 为1的数量,P
result_df = pd.DataFrame()
for index,threshold in enumerate(table['阈值']):
recall_i = table.loc[index,'召回率']
precision_i = table.loc[index,'精确率']
FP = num_pos*recall_i*(1/precision_i -1) # 误判的数量
FN = num_pos*(1-recall_i) # 漏掉的数量
money_loss = fn_loss*FN + fp_loss*FP
# 保存下吧
result_df.loc[index,'阈值']= threshold
result_df.loc[index,'精确率']= precision_i
result_df.loc[index,'召回率']= recall_i
result_df.loc[index,'误判数'] = FP
result_df.loc[index,'漏网数'] = FN
result_df.loc[index,'赔钱数']= money_loss
return result_df
上面的PR面积只有0.74,本来就产生了怀疑,此处阈值越小赔钱越少,明显有问题!
2.使用lightgbm+原数据
不是很科学,阈值越往下降,赔钱数越小,用lightGbm试下,直接套用我上面代码中,LightGbm随机搜索了一个最佳参数,套用现成方法, 从下图来看,比如正负错判一个,要赔100块,这样阈值肯定在一个地方可以取到极小值,从红框框处,大概知道阈值应该在0.4-0.5之间,我只是粗略找了一下,实际业务中可能要进一步细找。
这说明逻辑回归模型,得出的三份数据差异不大要被推翻,这时三份数据得重新找最佳参数,重新对比结果,如下:
--------------全特征结果如下图-----------------------
--------------方差分析结果如下图-----------------------
模型的ROC、PR曲线面积来看,方差分析>绘图肉眼看>全特征
当然参数是随机搜索,并不一定是最佳的,自己实验的时候,要把数据集划分的种子固定住,不然结果差异不小(少放个绘图特征图,减少文章长度)
--------------全特征金钱损失如下图-----------------------
--------------方差分析金钱损失如下图-----------------------
其实召回率,也不是特别让人满意,因为我在随机搜索参数的时候,并没有给样本权重,并且演示的时候,误判一个(FP)和漏网一个(FN)都是损失一百块,所以最小损失是FP+FN之和最小,实际得根据两者的赔钱数来定,讲道理漏网一个一般要多赔点钱,比如你的卡被别人盗刷了几千块,那你肯定要疯狂投诉找别人赔钱,比如你的卡被意外锁住,顶多赔偿个优惠券之类的(某多的路子)。
乍一看,似乎全特征赔钱数的最小值最小,实际上我们阈值分得比较粗糙,如果再按照0.001细分,胜负尤未定,方差分析的PR、ROC 在五折中是最高的,所以最优解大概率是方差分析上。
六、总结
实际中可能判对一个TP,TN也得加钱,不过基本思路是这样,可以看到逻辑回归还是多少有点不太行的,LightGbm还不错,并且我随机搜索的参数,可能离最优解稍微还有点距离,随机种子但也差不多了;不过XGBOOST应该更好,就是调参太耗时;
看过一些文章、比赛讲解,降采样都是首先被踢出局,挑一千条数据,去预测30万条数据,肯定结果很差,搞bagging的话,直接用原数据不就得了;
其次过采样,判断出一个ROC很高的模型,但实际业务中,数据本身就是这样不平衡,搞出一大堆假数据,然后说生成了一个模型,在这个“新造出来的平衡数据”中成绩相当好,这没有任何意义。
直接用原数据吧,算出了结果又没有完全算出,需要一个损失比例,任何损失都可以量化的嘛,比如被盗刷了,一般要赔钱,正常交易被意外锁定了,一般少赔点或者损失了一个客户(其实在应用端应该还有其他手段),所以是找出了一个方向供参考。