前言
此次竞赛的题目为‘信用卡盗刷侦测’,主办方收集120天信用卡交易数据,0-90天作为训练集,90-120天作为测试集,去除label共22个可用特征。首先定义此题为二分类问题,且正负样本通常会极度不均衡。
拓扑图
数据分析与处理
pandas读取数据:
train_data = pd.read_csv('train.csv')
test_data = pd.read_csv('test.csv')
print("rows:",train_data.shape[0]," columns:", train_data.shape[1])
print("rows:",test_data.shape[0]," columns:", test_data.shape[1])
>>> rows: 1521787 columns: 23
>>> rows: 421665 columns: 22
pandas查看前n条数据:
train_data.head(n)
pandas查看更多细节:计数、均值、标准差、百分位数(50%即中位数)、最小值、最大值。
# 空值(NAN)column不会被计算,只计算值类型为数字的column。
train_data.describe()
-
查看正负样本分布
1.对于二分类任务,首先将label转化为0和1,通常1代表正样本,即问题本身关注类。此题 正常交易(0)/盗刷交易(1)。2.label转化成功后因为值是0/1的关系,可以直接对label求和,和值即正样本数。或者画出直方图比较直观。
-
空值(NAN)
数据中的空值往往是不可避免,不管产生空值的原因是什么,空值都代表了数据的缺失。
- 查看数据的NAN:
# 检查NAN,pd.isnull(arr) = df.isnull() 值是NAN为True ,sum() 对每个column中true个数求和,sort_values()排序 ascending为False降序。
# df.count()计算每个column总数(不包括NAN),df.isnull().count() 计算总数包括空值。
total = train_data.isnull().sum().sort_values(ascending = False)
percent = (train_data.isnull().sum()/train_data.isnull().count()).sort_values(ascending = False)
pd.concat([total, percent], axis=1, keys=['Total', 'Percent']).transpose()
>>> flbmk flg_3dsmk fraud_ind bacno cano ...
>>> Total 12581.00 12581.00 0.0 0.0 0.0 ...
>>> Percent 0.008267 0.008267 0.0 0.0 0.0 ...
- 处理NAN:
– 若有NAN的样本数很少,可以直接采取删除NAN样本。
– 连续特征:通常采用均值填充。
– 分类特征:通常采用众数填充。
– 对于LightGBM、XGBoost等算法默认可处理NAN(会将NAN分发给最佳子树)。
– 给NAN一个特色的值,例如-999。
df[col].fillna(-999, inplace=True)
-
删除重复样本
-
离群值处理
– 画箱形图查看异常值。
– 两特征间画二维散点图。 -
查看各特征各值数据分布
all_features = train_data.columns
i = 0
f, axes = plt.subplots(6,4,figsize=(16,28))
for feature in columName:
i += 1
plt.subplot(6,4,i)
train_data[feature].hist()
plt.xlabel(feature, fontsize=12)
plt.show()
特征工程
1. 特征选择
-
特征间的相关性
pandas.Series.corr(method) —> method : {‘pearson’, ‘kendall’, ‘spearman’}pearson:针对线性数据的相关系数计算,针对非线性数据便会有误差。
kendall:用于反映分类变量相关性的指标,即针对无序序列的相关系数,非正太分布的数据
spearman:非线性的,非正太分析的数据的相关系数
# 查看特征两两之间的相关性(线性pearson)
plt.figure(figsize = (14,14))
data_corr = train_data.corr(method = 'pearson')
sns.heatmap(data_corr,fmt = '0.2f',annot = True,xticklabels=data_corr.columns,yticklabels=data_corr.columns,cmap="Reds")
plt.show()
- 特征核密度分布
不同类别不同特征的核密度分布,可以直观的看出某一特征在不同类别上分布差异性。
# 查看两种类别对于不同特征的核密度估计图
all_features = train_data.columns
i = 0
trueData = train_data[train_data['label'] == 0]
fraudData = train_data[train_data['label'] == 1]
f, axes = plt.subplots(6,4,figsize=(16,28))
for feature in all_features :
i += 1
plt.subplot(6,4,i)
sns.kdeplot(trueData[feature],bw=0.5,label="Class = 0")
sns.kdeplot(fraudData[feature],bw = 0.5,label="Class = 1")
plt.xlabel(feature, fontsize=12)
plt.show()
- Filter:通过特征与label之间的关联性
- 方差(Variance)
– sklearn:from sklearn.feature_selection import VarianceThreshold (删除了所有方差不满足指 定阈值的特征。默认情况下,它删除所有零方差特征,即在所有样本中具有相同值的特征。)
– 但是对于样本类别数量极度不均衡的数据集,此种方式不太适用,因为特征方差很小时样本特征值基相 同,但少数不同的特征值可能恰好属于少数样本类,这样刚好区分了多数样本类和少数样本类,如果直接按方差小于阈值删除此特征损失将是巨大。
– 对于样本类别数量极度不均衡的数据集,可以将不同类别样本取出,删除不同类别同一特征相同特征值出现次数都大于90%的特征,这意味着此特征方差小且在不同类别中值几乎一致,无法区分不同类别样本。
all_features = train_data.columns
trueData = train_data[train_data['label'] == 0]
fraudData = train_data[train_data['label'] == 1]
big_value_colums = []
for i in all_features:
pe_t = trueData[i].value_counts()/trueData[i].count()
pe_f = fraudData[i].value_counts()/fraudData[i].count()
if pe_f.values[0] > 0.9 and pe_t.values[0] > 0.9 and pe_f.index[0] == pe_t.index[0]:
big_value_colums.append(i)
print(big_value_colums)
- F检验
– sklearn: from sklearn.feature_selection import f_classif (F检验仅捕获单变量与label之间的线性相关性)
f,p = f_classif(train_data,train_label)
tmp = pd.DataFrame({'Feature': features, 'Feature importance': f}).sort_values(by='Feature importance',ascending=False)
s = sns.barplot(x='Feature',y='Feature importance',data=tmp)
s.set_xticklabels(s.get_xticklabels(),rotation=90)
plt.show()
- 互信息
– sklearn: from sklearn.feature_selection import mutual_info_classif (互信息可以捕获单变量与label之间的任何相关性,主要非线性相关性。)
mi = mutual_info_classif(train_data,train_label)
tmp = pd.DataFrame({'Feature': features, 'Feature importance': mi}).sort_values(by='Feature importance',ascending=False)
s = sns.barplot(x='Feature',y='Feature importance',data=tmp)
s.set_xticklabels(s.get_xticklabels(),rotation=90)
plt.show()
- Wrapper:通过一个目标函数,一步步递归筛选特征
RFE(recursive feature elimination)
– sklearn:from sklearn.feature_selection import RFE(通过一个模型,利用其 coef_ 属性 或 feature_importances_ 属性 ,一步一步的删除最不重要的特征,直到最终达到要选择的特征数。)
svc = SVC()
rfe = RFE(estimator=svc, n_features_to_select=1, step=1)
rfe.fit(train_data, train_label)
tmp = pd.DataFrame({'Feature': features, 'Feature importance': rfe.ranking_}).sort_values(by='Feature importance',ascending=False)
s = sns.barplot(x='Feature',y='Feature importance',data=tmp)
plt.show() s.set_xticklabels(s.get_xticklabels(),rotation=90)
plt.show()
-
Embedded:通过模型选择特征
– L1正则项
– 基于树算法:直接看 feature_importances_ 属性 -
降维
– PCA
– LDA -
前向特征搜索
- 总共有N个可用特征,初始特征集F为空。
- 第一轮:每次从N中取出1个特征,添加至F,得到Fi,利用交叉验证来得到Fi的错误率。选择错误率最低Fi,更新F。
- 第二轮:每次从N-1中取1个特征,…
… - 直到添加特征错误率不再降低,那么取出当前F即为最佳特征。
- 后向特征搜索
- 总共有N个可用特征,初始特征集F为N。
- 第一轮:每次从F中删除1个特征,得到Fi,利用交叉验证来得到Fi的错误率。选择错误率最低Fi,更新F。
- 第二轮:每次从N-1中删除1个特征,…
… - 直到删除特征错误率不再降低,那么取出当前F即为最佳特征。
- 置换验证(permutation)
首先用未挑选的特征训练一个模型,对验证集预测使用评估方式计算一个值作为baseline,然后轮流对验证集每一个特征进行随机打乱并重新使用模型预测,如果结果比baseline更好则说明此特征可能会破坏模型。
def permutation_importance(X, y, model):
perm = {}
y_true = model.predict(X)
baseline= roc_auc_score(y, y_true)
for cols in X.columns:
value = X[cols].copy()
X[cols] = np.random.permutation(X[cols].values)
y_true_sub = model.predict(X)
perm[cols] = roc_auc_score(y, y_true_sub) - baseline
X[cols] = np.array(value)
return perm
2. 特征处理
-
特征缩放
– 标准化:(X - mean)/ std ;数据分布改变,变成均值为0,方差为1。
– 归一化:(X- min)/(max - min);数据分布不变,取值区间变成[0,1]。
– 中心化: X - mean ;数据分布改变,变成均值为0,方差不变。 -
特征变换
-
连续特征:
1.rounding: 不需要太多小数位时,可以缩小精度,甚至可以round后变成分类特征。2.log:可以使特征值分布正太化。log(x),x越大log(x)增速越慢,可压缩大值、扩张小值。
3.离散化:使用阀值,变成二分类,或取区间分箱,变成多分类特征。
-
分类特征:
1.二值化:将多分类变成二分类,例如:颜色有5种,可变成是否有颜色。2.one hot encoding:因为分类特征转成数值时,可能会带来距离方面的问题。例如:5种颜色分标记别为0、1、2、3、4。颜色本身没有先后、大小、重要之分,因此对于knn、logistic、svm、DNN等对数值大小、距离敏感的算法应该转成one hot encoding。但是基于树的算法对数值大小、距离不敏感,通常不做转换。若类别数量巨大也不适合使用one hot encoding。
3.高基分类特征:类别数量巨大的分类特征可以使用:Bin Counting , Feature hash , Mean encoding等编码方法。
4.稀少特征值:计算分类特征的每种类别样本数,太少的统一归为一个新值。
- 特征构建
- 对于构建特征阶段,通常需要将训练集及测试集合并考虑,同步变换。
- 连续特征组合:特征间加、减、乘、除等组合方式,构成新特征。
- 分类特征组合:直接按字串方式前后拼接,再采用label encoding 或 one hot encoding 编码,构成新特征。
def encode_CB(col1,col2,train,test):
nm = col1+'_'+col2
train[nm] = train[col1].astype(str)+'_'+train[col2].astype(str)
test[nm] = test[col1].astype(str)+'_'+test[col2].astype(str)
le = LabelEncoder()
le.fit(list(train[nm].astype(str).values) + list(test[nm].astype(str).values))
train[nm] = le.transform(list(train[nm].astype(str).values))
test[nm] = le.transform(list(test[nm].astype(str).values))
print(nm,', ',end='')
- 聚合(Group by):按某个分类特征将样本分组,对每个分组求其他特征的均值(mean)、标准差(std)、众数(mode)、最值、原特征值与mean、mode之差等,构成新特征。
def encode_mean_std(main_columns, uids, aggregations=['mean'], train_df=train_data, test_df=test_data,
fillna = True):
# AGGREGATION OF MAIN WITH UID FOR GIVEN STATISTICS
for main_column in main_columns:
for col in uids:
for agg_type in aggregations:
new_col_name = main_column+'_'+col+'_'+agg_type
#拼接训练集和测试集
temp_df = pd.concat([train_df[[col, main_column]], test_df[[col,main_column]]])
#求BG
temp_df = temp_df.groupby([col])[main_column].agg([agg_type]).reset_index().rename(columns={agg_type: new_col_name})
# 取出目标列作为索引
temp_df.index = list(temp_df[col])
# 生成map对应的字典
temp_df = temp_df[new_col_name].to_dict()
train_df[new_col_name] = train_df[col].map(temp_df).astype('float32')
test_df[new_col_name] = test_df[col].map(temp_df).astype('float32')
if fillna:
train_df[new_col_name].fillna(-1,inplace=True)
test_df[new_col_name].fillna(-1,inplace=True)
print("'"+new_col_name+"'",', ',end='')
def encode_mode(main_columns, uids, train_df=train_data, test_df=test_data):
for main_column in main_columns:
for col in uids:
new_col_name = main_column+'_'+ col+'_mode'
comb = pd.concat([train_df[[col]+[main_column]],test_df[[col]+[main_column]]],axis=0)
t1 = comb.groupby([col, main_column]).size().reset_index()
t1.columns = [col, main_column, 'count']
t2 = t1.groupby([col])['count'].max().reset_index()
t2.columns = [col, 'max_count']
t1 = t1.merge(t2, on=[col], how='left')
t1 = t1[t1['count']==t1['max_count']]
comb = t1.groupby([col])[main_column].mean().reset_index()
# 取出目标列作为索引
comb.index = list(comb[col])
# 生成map对应的字典
comb = comb[main_column].to_dict()
train_df[new_col_name] = train_df[col].map(comb).astype('float32')
test_df[new_col_name] = test_df[col].map(comb).astype('float32')
print("'"+new_col_name+"'",', ',end='')
- 频率编码(Count encoding):计算每个特征各值出现的频率,构成新特征。
def encode_FE(train, test, cols):
for col in cols:
df = pd.concat([train[col],test[col]])
vc = df.value_counts(dropna=True, normalize=True).to_dict()
nm = col+'_FE'
train[nm] = train[col].map(vc)
train[nm] = train[nm].astype('float32')
test[nm] = test[col].map(vc)
test[nm] = test[nm].astype('float32')
print(nm,', ',end='')
模型
-
三种基于树模型:
LightGBM、XGBoost、CatBoost。关于这三个模型的参数详解及使用方法请看我另一篇博客:XGBoost / LightGBM参数及用法详解。 -
本地CV:
- 本地Cross Validation是很重要的一环,在一个竞赛中若能找到一个最佳的CV方式,即本地提升LB也相应提升,那就已经成功的一半了,但这并不容易,往往会出现本地提升LB下降或本地下降LB却上升的情况,这时你要相信本地CV还是LB就见仁见智了。
- CV方式:sklearn上提供了多种CV方式,常用的有KFold、StratifiedKFold、GroupKFold,也可以根据数据本身的资讯,例如:按时间分割、按序号分割之类。可以尝试多种CV方式,或者同时考虑多种CV。
- CV通常采用5折,每折CV都有一折验证集,同时每折CV都对测试集进行预测。最后将5折CV的5次验证集预测结果拼在一起即获得对整个训练集的预测结果,使用评估方式计算作为最后的CV结果。另外,将5折CV的5次测试集预测结果求和取平均即为最后的测试集预测结果。
- Stacking:
在竞赛中,各路大神往往还会在单模型的基础上再做一次ensemble,ensemble的方式主要有4种:bagging、boosting、Blending、Stacking。在最后模型融合的阶段,通常使用Blending或Stacking。
- Stacking:
Stacking的流程其实与单模的CV很相似,以5折stacking为例如上图:每一拆CV都对验证集有一个预测值,5折合并起来就可以得到一个完整的训练集预测结果(概率),同时每一折都对测试集预测,5次预测求和取平均得到一个测试集预测结果(概率)。3个模型就会得到3个训练集预测结果和3个测试集预测结果,将它们都拼接起来,就得到一个新有3个特征 的训练集和有3个特征的测试集,再用一个简单的模型去对此训练集训练,然后对测试集预测得到最终结果。 - Blending:
Blending与Stacking不同在于没有使用CV,例如:用70%的训练集数据训练第一层的多个单模,对剩下30%的训练集数据预测,同时对全部测试集预测。第二层只用30%训练集数据的预测结果作为训练数据,然后对测试集预测结果作预测得到最终结果。