带你进入百度AI Sdutio 大赛赛前三名:《常规赛:MarTech Challenge 点击反欺诈预测》经验分享

221 阅读13分钟

比赛介绍

本小节是比赛基本信息介绍,对相关信息比较熟悉的可以跳过,之后看后面的方案。

基本信息

MarTech Challenge in Baidu AI Studio

MarTech 技术已经被广泛应用于商业广告分析与挖掘中,在搜索广告,信息流广告,营销预测,反欺诈发现,商品购买预测,智能创意生成中有广泛的应用。

赛题中提供约50万次点击数据,请预测用户的点击行为是否为正常点击,还是作弊行为。

标签:反欺诈预测

比赛时间:2020/09/01 - 2023/01/01

举办方:

赛题赛制

  • 即日起面向全社会公开报名,直到赛题下线。
  • 飞桨常规赛不设初赛、复赛,以当月每位参赛选手提交的最优成绩排名。每月竞赛周期为本月1日至月末最后1日。
  • 比赛期间选手每天可最多提交5次作品(预测结果+原始代码),系统自动选取最优成绩作为榜单记录。

参赛方式

本次竞赛面向全社会开放,不限年龄、身份、国籍,相关领域的个人、高等校、科研机构、企业单位、初创团队等人员均可报名参赛。

仅支持个人参赛。 不允许组队参赛。

比赛现状

目前榜首成绩:89.448

如果想进入比赛的每月前三名,必须要89.1以上。有些月份竞争激烈 89.1 不能进入前三名。

本文方案最高成绩: 89.102, 这个成绩在很多月份可以进入前三名。

赛题说明

任务说明

测试集中提供了会话sid及该会话的各维度特征值,基于训练集得出的模型进行预测,判断该会话sid是否为作弊行为。

数据集

选手报名后,可在【数据集】tab获取数据集,以及基线系统。

训练集: train.csv (50万条)
测试集: test1.csv(15万条)

字段说明

train.csv和test.csv字段说明

字段类型说明
sidstring样本id/请求会话sid
packagestring媒体信息,包名(已加密)
versionstring媒体信息,app版本
android_idstring媒体信息,对外广告位ID(已加密)
media_idstring媒体信息,对外媒体ID(已加密)
apptypeint媒体信息,app所属分类
timestampbigint请求到达服务时间,单位ms
locationint用户地理位置编码(精确到城市)
fea_hashint用户特征编码(具体物理含义略去)
fea1_hashint用户特征编码(具体物理含义略去)
cus_typeint用户特征编码(具体物理含义略去)
nttint网络类型 0-未知, 1-有线网, 2-WIFI, 3-蜂窝网络未知, 4-2G, 5-3G, 6–4G
carrierstring设备使用的运营商 0-未知, 46000-移动, 46001-联通, 46003-电信
osstring操作系统,默认为android
osvstring操作系统版本
lanstring设备采用的语言,默认为中文
dev_heightint设备高
dev_widthint设备宽
dev_ppiint屏幕分辨率

评估指标 Accuracy

image.png

image.png

参赛选手结果提交时应提交submission.csv文件,格式如下:

sidlabel
14406820
16068241
17746420
17425350
0或1

submission.csv 字段说明

字段说明
sid请求会话sid
label是否作弊,0为正常,1位作弊

提交注意事项

在线评估提交限制:每个选手每日最多提交5次系统结果进行在线评测,如果新提交结果好于之前提交结果,排行榜中的成绩将自动进行更新覆盖。

数据探索

train_data.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 500000 entries, 0 to 499999
Data columns (total 21 columns):
Unnamed: 0    500000 non-null int64
android_id    500000 non-null int64
apptype       500000 non-null int64
carrier       500000 non-null float64
dev_height    500000 non-null float64
dev_ppi       500000 non-null float64
dev_width     500000 non-null float64
label         500000 non-null int64
lan           316720 non-null object
media_id      500000 non-null int64
ntt           500000 non-null float64
os            500000 non-null object
osv           493439 non-null object
package       500000 non-null int64
sid           500000 non-null int64
timestamp     500000 non-null float64
version       500000 non-null object
fea_hash      500000 non-null object
location      500000 non-null int64
fea1_hash     500000 non-null int64
cus_type      500000 non-null int64
dtypes: float64(6), int64(10), object(5)
memory usage: 80.1+ MB

样本分布分析

我们把训练集中的每一行成为一个样本。

所谓的样本分布,就是看训练集和测试集,样本是否分布均衡。在本题中,样本分布是均衡的。 经过简单分析,本赛题样本分布均衡,不做详细介绍。

缺失值处理

所谓缺失值,是指某些样本的某些值是空的,对此我们要进行缺失值的处理。 通常可采用的方案有填充和删除等。本文采用的填充。

异常值处理

本文中,字段 osv 的数据中有各种各样的格式,看起来不是那么规范的。通常可采用的方案有 删除、转换、填充、区别对待等。我们采用的是转换。

特征挖掘

由于题中给的特征之间,有些列之间有明显的关联关系,如果把它们当成单独的特征进行处理,就会丢失一些隐藏的关联性。

像本题中的 dev 相关的几个变量,是有关联关系的。

还有就是timestamp特征,其实是一个时间特征,我们可以把它转换为 年、月、日、星期、时分秒等特征。

还有一些特征值会明显影响结果,我们单独抽离一些特征来,提高该特征的影响程度。

标准化和归一化

本体采用的事xgboost模型进行分类。 Xgboost是在GBDT的基础上对boosting算法进行的改进,LightGBM (Light Gradient Boosting Machine)是一个实现 GBDT 算法的框架,GBDT的树是在上一棵树的基础上通过梯度下降求解最优解,归一化能收敛的更快,GBDT通过减少偏差来提高性能。

这两种算法并不像bagging集成算法那样随机森林那样,通过减少方差提高性能的,树之间建立关系是独立的,不需要标准化和归一化。

一共21列。

特征工程

'Unnamed: 0'

train_data['Unnamed: 0']
0              0
1              1
2              2
3              3
4              4
           ...  
499995    499995
499996    499996
499997    499997
499998    499998
499999    499999
Name: Unnamed: 0, Length: 500000, dtype: int64

该列其实就是样本的序号,从0到5w,初步看没有任何参考价值,可以移除。

train = train_data.drop(['Unnamed: 0', 'sid'], axis=1)

'android_id'

train['android_id_0'] = train['android_id'].map(lambda x: 0 if x == 0 else 1)
test['android_id_0'] = test['android_id'].map(lambda x: 0 if x == 0 else 1)

'timestamp'

从timestamp 提炼相应的特征 ,包含 weekday/day, minute, time_diff 其中:

weekday 星期 day 日期 time_diff 从 最早样本的时间开始计算时间偏移量

单独写成文件如下:

process_timestamp.py

import time
import pandas as pd
from datetime import datetime

def process_timestamp(train, test):
    
    def get_date(features):
        if 'timestamp' not in features.columns:
            return
        features2 = features.copy()
        # 除以1000 转化为日期格式
        features2['timestamp'] = features2['timestamp'].apply(lambda x: datetime.fromtimestamp(x/1000))

        # 创建时间索引
        temp = pd.DatetimeIndex(features2['timestamp'])
        features2['year'] = temp.year
        features2['month'] = temp.month
        features2['day'] = temp.day
        features2['week_day'] = temp.weekday
        features2['hour'] = temp.hour
        features2['minute'] = temp.minute


        start_time = features2['timestamp'].min()
        features2['time_diff'] = features2['timestamp'] - start_time

        features2['time_diff'] = features2['time_diff'].dt.days * 24 + features2['time_diff'].dt.seconds / 3600
        
        features2.drop(['timestamp'], axis = 1, inplace=True)

        features2.drop(['year', 'month'], axis = 1, inplace=True)
#         features2.drop(['week_day', 'day', 'minute'], axis = 1, inplace=True)
        
        return features2

    
    train = get_date(train)
    test = get_date(test)
    
    return train, test


'osv'

pd.set_option("display.max_rows", None)
train_data['osv'].value_counts()

结果如下:

8.1.0                                 151419
9                                      71332
6.0.1                                  31714
7.1.1                                  26503
9.0.0                                  24385
6.0                                    23674
5.1                                    19602
8.1                                    19301
5.1.1                                  17387
8.0.0                                  15127
7.1.2                                  12068
4.4.4                                  10612
7.0                                     9346
Android_8.1.0                           8043
Android_9                               7085
5.0.2                                   5019
7.9.0                                   4348
7.9.2                                   4312
8.0                                     4000
7.1                                     3816
9.0                                     1882
3.8.6                                   1733
4.4.2                                   1681
6.0.0                                   1568
5.1.0                                   1421
4.1                                     1399
2.3                                     1328
5.0                                     1309
4.3                                     1220
7.0.0                                   1107
Android_8.0.0                           1085
4.4                                      695
Android_7.1.1                            656
3.9.0                                    620
Android_6.0.1                            582
7.8.9                                    517
4.2.2                                    496
7.8.7                                    383
8.0.1                                    310
5.0.1                                    278
Android_5.1.1                            252
Android_7.1.2                            212
6.1                                      183
7.8.0                                    176
3.8.4                                    170
Android_5.1                              163
8                                        155
Android_6.0                              142
4.1.2                                    142
5.0.0                                    138
7.7.7                                    129
Android_7.0                              117
7.8.2                                    104
3.8.0                                    103
21100                                     93
7.7.3                                     90
Android_4.4.4                             85
7.2.1                                     82
4.3.0                                     82
7                                         74
4.2.1                                     72
7.7.5                                     70
6.1.0                                     64
5.2                                       60
7.8.5                                     59
7.8.4                                     59
7.7.2                                     57
21000                                     52
5                                         47
4.1.1                                     46
7.6.7                                     45
7.7.0                                     43
3.8.3                                     40
6.0_23                                    38
7.6.8                                     38
3.7.6                                     35
7.6.4                                     34
7.8.8                                     27
Android_5.0.2                             26
9.0.5                                     26
3.7.8                                     25
7.1.3                                     22
7.7.4                                     22
7.1.0                                     21
4.0.3                                     20
7.6.9                                     20
4.2                                       20
4.0.4                                     20
Android_5.0                               19
4.4.5                                     18
7.8.6                                     17
6.2.1                                     16
3.1.1                                     13
Android_4.4.2                             13
4.4.3                                     12
Android_4.3                               10
4.4.0                                      9
6.0.1_19                                   9
3.0.3                                      8
2.3.6                                      8
Android_4.1.2                              7
2.1.2                                      7
6.1.2                                      7
7930                                       7
2.3.4                                      7
2.2.3                                      6
6                                          6
4.3.3                                      4
3.2                                        4
2.3.5                                      4
7.3.1                                      4
4.4W                                       4
5.3.0-FL2-20180717.9010                    3
Android_4.2.2                              3
5.1_22                                     3
4.4.2_19                                   3
5.3                                        2
4.0.2                                      2
7.6.6                                      2
71200                                      2
7910                                       2
4.3.1                                      2
5.2.0                                      2
3.2.0-FL2-20180726.9015                    1
9.1.0                                      1
5.0.3                                      1
5.0_21                                     1
8.0.2                                      1
5.1.1_22                                   1
6.1.0-RS-20190305.1125                     1
4.0                                        1
Android 4.2.9                              1
6.0.2                                      1
Android_4.4.3                              1
Android_4.2.1                              1
9.1                                        1
7920                                       1
2.0.1                                      1
6.0 十核2.0G_HD                              1
4                                          1
%E6%B1%9F%E7%81%B5OS+5.0                   1
Android_5.0.1                              1
2.3.7                                      1
2.2.2                                      1
f073b_changxiang_v01_b1b8_20180915         1
4.2.3.2                                    1
Android 7.1                                1
10.3.3                                     1
2.9.2                                      1
5.3.0                                      1
11                                         1
5.1.1-F-20180719.9007                      1
Android 5.12                               1
Android 4.3.1                              1
Name: osv, dtype: int64

我们发现大部分数据是 a.b.c这种类型。也有很多数据不是规范格式的。于是我们把他们转换a.bc 这种格式,后续方便当成小数来处理。

单独抽离process_osv.py文件:

import re

def process_osv(features, test1):
    
    features['osv'] = features['osv'].apply(f)
    test1['osv'] = test1['osv'].apply(f)

    return features, test1


def f(x):

    x = str(x)
    x = x.replace('Android_', '')
    x = x.replace('Android ', '')
    
    if x == '%E6%B1%9F%E7%81%B5OS+5.0':
        return 5.0
    if x == '6.0 十核2.0G_HD':
        return 6.0
    if x == 'f073b_changxiang_v01_b1b8_20180915':
        return 0
    if x == '6.0.1_19':
        return 6.01
    if x == 'nan':
        return 0
    if x == 'GIONEE_YNGA':
        return 0
    
    if x == '4.4W':
        return 4.4
    
    if x.find('.') > 0:
        index = x.find('.')    
        x = x[0:index] + '.' + re.sub(r'\D', "", x[index+1:])[0:2]
    x = float(x)
    
        
        
    if x > 10000:
        return x / 10000
    if x > 1000:
        return x / 1000
    
    return x

'os'

首先分析特征分布:

train_data['os'].value_counts()

结果:

android    303175
Android    196825
Name: os, dtype: int64

我们可以发现,所有数据都是安卓系统。

我们可以初步把该特征舍弃,因为所有特征都是android,没啥区别。

但是大家也可以保留试试,根据最后准确率看看效果。

我个人把它保留了。理由如下:

train_data[train_data['label'] == 0]['os'].value_counts()

结果:

android    180903
Android     76857
Name: os, dtype: int64

发现 lable == 0的样本里面,该特征的两个值,占比约 2.x:1。

train_data[train_data['label'] == 1]['os'].value_counts()

结果:

android    122272
Android    119968
Name: os, dtype: int64

发现 label == 1 的样本里,2个值的占比约 1:1。 所以可以认为该特征对最后分类结果有一定影响。不需要舍弃该样本。

编写函数为单独文件:process_os.py

import pandas as pd


def process_os(features, test):
    
    def f(x):
        if x == 'android':
            return 0
        return 1
    
    features['os'] = features['os'].apply(f)
    test['os'] = test['os'].apply(f)

    return features, test


'fea_hash'

train_data['fea_hash'].apply(lambda x: len(x)).value_counts()
10    378925
9     108904
8      11235
7        740
6         93
38        37
39        28
37        16
5         11
36         3
33         2
32         2
1          2
31         1
30         1

我们发现该特征长度分布非常不均匀。 要么是 10以为的长度,要么是30几位的长度。我们来看看数据:

train_data[train_data['fea_hash'].apply(lambda x: len(x) > 10 )]['fea_hash']

结果:

4214        2408:84e4:554:54fd:b36:6770:1a58:927d
6767       2408:84e4:196:9d1f:a41f:deff:fef9:8c60
7742      2408:84ec:4008:ae53:fcc6:f9ff:fe08:44ba
10086     2409:893c:1e0a:1e18:cc0b:52a0:6104:4d40
19696      2408:84e4:103:21f5:4d40:76b9:47e5:930c
                           ...                   
494813       2408:84e1:e9:e956:564:59e8:9f05:506b
494835    2409:8928:24d8:bb5a:c526:4398:37dd:f2c8
495653    2409:8a3c:5917:c670:7827:91e2:ed47:d29d
495873    2409:8a30:c452:3a70:ec8b:f096:6288:6986
499386     2408:84e4:516:974f:419b:32d3:d69f:443b
Name: fea_hash, Length: 90, dtype: object

也就是说,在长度 > 10 的情况下,该特征值都是类似某 设备物理地址。

再看看 长度 < 10的情况下,特征值的情况:

train_data[train_data['fea_hash'].apply(lambda x: len(x) < 10 )]['fea_hash']

结果:

6         369486452
15        738486495
28        596797663
29        276796533
31        634714332
            ...    
499975    471301994
499976    406243511
499985    699472385
499987    364899549
499995    861755946
Name: fea_hash, Length: 120985, dtype: object

所以发现该特征不是单一特征,里面包含了2种不同的情况,需要分别处理。

处理结果:添加新特征 fea_hash_len

# fea_hash
all_df['fea_hash_len'] = all_df['fea_hash'].map(lambda x:len(str(x)))
train.loc[:,'fea_hash_len'] = all_df[all_df['label'].notnull()]['fea_hash_len']
test.loc[:, 'fea_hash_len'] = all_df[all_df['label'].isnull()]['fea_hash_len']

'fea1_hash'

同上 ,添加新特征 fea1_hash_len

# fea1_hash_len
all_df['fea1_hash_len'] = all_df['fea1_hash'].map(lambda x:len(str(x)))
train.loc[:,'fea1_hash_len'] = all_df[all_df['label'].notnull()]['fea1_hash_len']
test.loc[:, 'fea1_hash_len'] = all_df[all_df['label'].isnull()]['fea1_hash_len']

'dev_rate'

我们发现有 'dev_width', 'dev_height' 两个特征。我们构造宽高比 'dev_rate'

# 设别高宽比、设备面积 dev_height, dev_width
all_df['dev_rate'] = all_df['dev_height']/all_df['dev_width']
train.loc[:,'dev_rate'] = all_df[all_df['label'].notnull()]['dev_rate']
test.loc[:, 'dev_rate'] = all_df[all_df['label'].isnull()]['dev_rate']

'dev_area'

同理构造面积特征 'dev_area'


# 面积 dev_height, dev_width
all_df['dev_area'] = all_df['dev_height']*all_df['dev_width']
train.loc[:,'dev_area'] = all_df[all_df['label'].notnull()]['dev_area']
test.loc[:, 'dev_area'] = all_df[all_df['label'].isnull()]['dev_area']

'dev_area_ppi'

注,该特征从其他选手方案中抄过来的,此时编写该文章的时候发现不是很合理。大家可自行调整,调整过后也许会比本人分数更高。

个人认为合理的是:

all_df['dev_ppi'] * all_df['dev_area']

这个物理含义就是屏幕像素总个数。

某选手方案:


# 分辨率 dev_ppi
all_df['dev_area_ppi'] = all_df['dev_ppi']/all_df['dev_area']
all_df['dev_area_ppi'][np.isnan(all_df['dev_area_ppi'])] = 0

train.loc[:,'dev_area_ppi'] = all_df[all_df['label'].notnull()]['dev_area_ppi']
test.loc[:, 'dev_area_ppi'] = all_df[all_df['label'].isnull()]['dev_area_ppi']

其他特征处理

某些特征值对样本影响巨大,比例更影响分类权重。

用该方式,增加一些特征。 主要思路如下:

假设 在lan字段中。 lan == zh-CN 的样本中 label == 0 和label == 1的样本数量比例超过3 我们就增加一个新特征 lan0,取值 1.

import pandas as pd


# 数据探索,找到导致1的关键特征值
def find_positive_key_features_info(train, selected_cols):
    features = train.drop(['Unnamed: 0', 'label'], axis=1)
    positive_key_features_info = {}
    
    for selected in selected_cols:
        positive_key_features_info[selected] = find_positive_key_values(train, selected, 3)
    return positive_key_features_info

# 数据探索,找到导致1的关键特征值
def find_negtive_key_features_info(train, selected_cols):
    features = train.drop(['Unnamed: 0', 'label'], axis=1)
    negtive_key_features_info = {}
    
    
    for selected in selected_cols:
        negtive_key_features_info[selected] = find_negtive_key_values(train, selected, 3)
    return negtive_key_features_info



def find_key_values(train, selected_feature, ratio):
    temp0 = train[train['label'] == 0]
    temp = pd.DataFrame(columns=[0,1])
    temp[0] = temp0[selected_feature].value_counts() / len(temp0)

    temp1 = train[train['label'] == 1]
    temp[1] = temp1[selected_feature].value_counts() / len(temp1)
    
    temp[2] = temp[1] / temp[0]
    
    result = temp[temp[2] > ratio].sort_values(2, ascending=False).index
    
    return result

def find_negtive_key_values(train, selected_feature, ratio):
    temp0 = train[train['label'] == 0]
    temp = pd.DataFrame(columns=[0,1])
    temp[0] = temp0[selected_feature].value_counts() / len(temp0)

    temp1 = train[train['label'] == 1]
    temp[1] = temp1[selected_feature].value_counts() / len(temp1)
    
    temp[2] = temp[0] / temp[1]
    
    result = temp[temp[2] > ratio].sort_values(2, ascending=False).index
    
#     print('negtive', result)
    return result

def find_positive_key_values(train, selected_feature, ratio):
    temp0 = train[train['label'] == 0]
    temp = pd.DataFrame(columns=[0,1])
    temp[0] = temp0[selected_feature].value_counts() / len(temp0)

    temp1 = train[train['label'] == 1]
    temp[1] = temp1[selected_feature].value_counts() / len(temp1)
    
    temp[2] = temp[1] / temp[0]
    
    result = temp[temp[2] > ratio].sort_values(2, ascending=False).index
    
#     print('positive', result)
    return result



def add_key_features(train, features,  test1, selected_cols):
    
#     key_features_info = find_key_features_info(train, selected_cols)
    
#     for feature in key_features_info:
#         if len(key_features_info[feature]) > 0:
#             features[feature + '1'] = features[feature].apply(f, args=(feature, key_features_info))
#             test1[feature + '1'] = test1[feature].apply(f, args=(feature, key_features_info))
#     key_features_info = find_key_features_info(train, selected_cols)
    
    positive_key_features_info = find_positive_key_features_info(train, selected_cols)    
    
    for feature in positive_key_features_info:
        if len(positive_key_features_info[feature]) > 0:
            features[feature + '1'] = features[feature].apply(f, args=(feature, positive_key_features_info))
            test1[feature + '1'] = test1[feature].apply(f, args=(feature, positive_key_features_info))
    
    
    negtive_key_features_info = find_negtive_key_features_info(train, selected_cols)
    
    for feature in negtive_key_features_info:
        if len(negtive_key_features_info[feature]) > 0:
            features[feature + '0'] = features[feature].apply(f, args=(feature, negtive_key_features_info))
            test1[feature + '0'] = test1[feature].apply(f, args=(feature, negtive_key_features_info))
            
            
            
    return features, test1

    
def f(x, feature, key_features_info):
    if x in key_features_info[feature]:
        return 1
    else:
        return 0

其他倍数特征

和上一小节思路相同,我们把所有特征导致分类结果的比例占比计算出来。

process_feature_times.py 每个特征都会新增一个 'feature'_times 的特征。

该方案明显增加了特征值个数,会导致计算变慢。

import pandas as pd
import numpy as np


def process_feature_times(train, test, feature):
    len_label_0 = len(train[train['label'] == 0])
    len_label_1 = len(train[train['label'] == 1])
    all_df = pd.concat([train, test])
    
    series_0 = train[train['label'] == 0][feature].value_counts()
    series_1 = train[train['label'] == 1][feature].value_counts()

    keys = all_df[feature].value_counts().keys()
    
    temp = pd.DataFrame(columns=['feature',0, 1])
    temp['feature'] = keys
    
    temp[0] = temp['feature'].apply(lambda x: series_0[x] if x in series_0 else 0)
    
    temp[1] = temp['feature'].apply(lambda x: series_1[x] if x in series_1 else 0)

    import math

    def f(x):
        if x == 0:
            return 0
        elif x == np.inf:
            return 0
        elif x > 1:
            return int(x)
        elif x < 1:
            return -int(1/x)
        else:
            return 0


    temp[0] = temp['feature'].map(lambda x: series_0[x] / len_label_0 if x in series_0 else 0)
    temp[1] = temp['feature'].map(lambda x: series_1[x] / len_label_1 if x in series_1 else 0)
    
    temp[2] = temp[0] / temp[1]
    temp[3] = temp[2].map(lambda x: f(x))
    lan_2_times = temp[["feature", 3]].set_index("feature").to_dict(orient='dict')[3]
    
    train[feature + '_times'] = train[feature].map(lambda x: 0 if type(x) == float else lan_2_times[x])
    test[feature + '_times'] = test[feature].map(lambda x: 0 if type(x) == float else lan_2_times[x])
    return train, test

模型训练

五折交叉验证

采用五折交叉验证,提高准确率:

from sklearn.model_selection import KFold, StratifiedKFold
from sklearn.metrics import accuracy_score

n_splits = 5
def ensemble_model(clf, train_x, train_y, test):
    sk = StratifiedKFold(n_splits=n_splits, shuffle=True, random_state=2021)
    prob = []
    mean_acc = 0
    for k, (train_index, val_index) in enumerate(sk.split(train_x, train_y)):
        print('第{}个子模型, training'.format(k+1))

        train_x_real = train_x.iloc[train_index]
        train_y_real = train_y.iloc[train_index]
        val_x = train_x.iloc[val_index]
        val_y = train_y.iloc[val_index]
        
        clf = clf.fit(train_x_real, train_y_real)
        val_y_pred = clf.predict(val_x)
        
        acc_val = accuracy_score(val_y, val_y_pred)
        print('第{}个子模型, acc{}'.format(k+1, acc_val))
        mean_acc += acc_val / n_splits
        
        test_y_pred = clf.predict_proba(test)[:, -1]
        prob.append(test_y_pred)
    print(mean_acc)
    mean_prob = sum(prob) / n_splits
    return mean_prob

开始训练

# train = train.drop(['label'], axis=1)

import time

# 12, 5000, 0.9 0.86, 0.1, 0.025 = 89 =》 88.684
# 13,  2000,                          0.889304 => 88.71
# 提炼 dev 特征后 89.0242 => 89.032
# 13  提炼 倍数关系 8914.01  => 89.0453
# 17, 3000 运行非常慢。

max_depth = 13
n_estimators = 2000
subsample=0.9
colsample_bytree=0.86
reg_lambda = 0.1
learning_rate = 0.001

clf = xgb.XGBClassifier(
    max_depth = max_depth,
    n_estimators = n_estimators,
    subsample=subsample,
    colsample_bytree=colsample_bytree,
    learning_rate = learning_rate,
    objective = 'binary:logistic',
    tree_method = 'gpu_hist',
    min_child_samples = 3,
    eval_metric='auc',
    reg_lambda = reg_lambda
)

result = ensemble_model(clf, train, labels, test)
print(result)

网格搜索

由于超参数特别多,需要耗费的时间很长,大家可以尝试网格搜索,也许会找到一组参数,相信最后结果,一定可以比本文方案,准确率提高。

模型选取

本文使用的xgboost模型。网上看到其他选手的catboost准确率会更高。大家也可以自行尝试。

还有一些深度神经网络算法,准确率也许会更高。大家可以尝试。

生成提交的 CSV文件

a = pd.DataFrame(test_data['sid'])
a['label'] = result
a['label'] = a['label'].apply(lambda x: 0 if x < 0.5 else 1)

import datetime
timestr = datetime.datetime.now().strftime('%y-%m-%d %H %M %S')

a.to_csv('result/v9' + 'max_depth=5subsample=0.72colsample_bytree=0.72reg_lambda = 0.7' + '.csv', index=False)

感悟

有时候每改动一点代码,就需要跑一次,比较费时间。大家可以做好每次代码的版本管理,方便代码和最后结果一一对应,方便改进。

本文方案最高成绩: 89.102, 这个成绩在很多月份可以进入前三名。

比赛是长期赛,每个月一个榜单。

文中也提到了很多优化方案,希望大家多多尝试,一定可以拿到更好成绩。

有任何疑问,想探讨代码细节的,可以留言或者联系我,大家一起交流。