记一次完整的机器学习竞赛经历


前言

此次竞赛的题目为‘信用卡盗刷侦测’,主办方收集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)
    数据中的空值往往是不可避免,不管产生空值的原因是什么,空值都代表了数据的缺失。

  1. 查看数据的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		...
  1. 处理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之间的关联性
  1. 方差(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)
  1. 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()  
  1. 互信息
    – 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

  • 前向特征搜索

  1. 总共有N个可用特征,初始特征集F为空。
  2. 第一轮:每次从N中取出1个特征,添加至F,得到Fi,利用交叉验证来得到Fi的错误率。选择错误率最低Fi,更新F。
  3. 第二轮:每次从N-1中取1个特征,…
  4. 直到添加特征错误率不再降低,那么取出当前F即为最佳特征。
  • 后向特征搜索
  1. 总共有N个可用特征,初始特征集F为N。
  2. 第一轮:每次从F中删除1个特征,得到Fi,利用交叉验证来得到Fi的错误率。选择错误率最低Fi,更新F。
  3. 第二轮:每次从N-1中删除1个特征,…
  4. 直到删除特征错误率不再降低,那么取出当前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. 连续特征:
    1.rounding: 不需要太多小数位时,可以缩小精度,甚至可以round后变成分类特征。

    2.log:可以使特征值分布正太化。log(x),x越大log(x)增速越慢,可压缩大值、扩张小值。

    3.离散化:使用阀值,变成二分类,或取区间分箱,变成多分类特征。

  2. 分类特征:
    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.稀少特征值:计算分类特征的每种类别样本数,太少的统一归为一个新值。

  • 特征构建
  1. 对于构建特征阶段,通常需要将训练集及测试集合并考虑,同步变换。
  2. 连续特征组合:特征间加、减、乘、除等组合方式,构成新特征。
  3. 分类特征组合:直接按字串方式前后拼接,再采用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='')
  1. 聚合(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='')
  1. 频率编码(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='')


模型

  1. 本地Cross Validation是很重要的一环,在一个竞赛中若能找到一个最佳的CV方式,即本地提升LB也相应提升,那就已经成功的一半了,但这并不容易,往往会出现本地提升LB下降或本地下降LB却上升的情况,这时你要相信本地CV还是LB就见仁见智了。
  2. CV方式:sklearn上提供了多种CV方式,常用的有KFold、StratifiedKFold、GroupKFold,也可以根据数据本身的资讯,例如:按时间分割、按序号分割之类。可以尝试多种CV方式,或者同时考虑多种CV。
  3. CV通常采用5折,每折CV都有一折验证集,同时每折CV都对测试集进行预测。最后将5折CV的5次验证集预测结果拼在一起即获得对整个训练集的预测结果,使用评估方式计算作为最后的CV结果。另外,将5折CV的5次测试集预测结果求和取平均即为最后的测试集预测结果。
  • Stacking
    在竞赛中,各路大神往往还会在单模型的基础上再做一次ensemble,ensemble的方式主要有4种:bagging、boosting、Blending、Stacking。在最后模型融合的阶段,通常使用Blending或Stacking。
    在这里插入图片描述
  1. Stacking:
    Stacking的流程其实与单模的CV很相似,以5折stacking为例如上图:每一拆CV都对验证集有一个预测值,5折合并起来就可以得到一个完整的训练集预测结果(概率),同时每一折都对测试集预测,5次预测求和取平均得到一个测试集预测结果(概率)。3个模型就会得到3个训练集预测结果和3个测试集预测结果,将它们都拼接起来,就得到一个新有3个特征 的训练集和有3个特征的测试集,再用一个简单的模型去对此训练集训练,然后对测试集预测得到最终结果。
  2. Blending:
    Blending与Stacking不同在于没有使用CV,例如:用70%的训练集数据训练第一层的多个单模,对剩下30%的训练集数据预测,同时对全部测试集预测。第二层只用30%训练集数据的预测结果作为训练数据,然后对测试集预测结果作预测得到最终结果。
  • 2
    点赞
  • 11
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值