本次比赛是一个经典点击率预估(CTR)的数据挖掘赛,需要选手通过训练集数据构建模型,然后对验证集数据进行预测,预测结果进行提交。
本题的任务是构建一种模型,根据用户的测试数据来预测这个用户是否点击广告。这种类型的任务是典型的二分类问题,模型的预测输出为 0 或 1 (点击:1,未点击:0)
用户的数据包括不同种类的信息,需要对数据进行相应的分析,来对不同信息进行相应的处理。
在进行数据探索之前,我们先分析一下用户的可能类型:
1、根据用户点击量数据大小,我们将用户分为两部分:A、数据充足型;B、数据稀疏型
对于A类型的用户,我们在构建模型后,能够相对较好的拟合和预测。对于B类型的用户,稀疏数据不能很好的体现用户的点击量分布,因此模型就很难预测。
对于B类用户,点击的数据不能支撑起模型的预测,就需要根据用户的其他信息来预测,就比如年龄段、、性别等数据。
2、数据集中,很可能有很多脏数据,信息不全或者信息虚假等,都需要在训练前对数据进行预处理。
我们从官网下载数据集后,根据赛事官方手册,对数据信息进行相应的分析:
Label:用户是否点击,是我们要训练和预测的二分类对象。
ID信息:user_id,用户ID,同一个用户ID包含多行数据,分别表示同一个用户的多次行为数据,构建样本特征时,需要根据相同用户来进行提取mean、nunique等特征,用来反应同一个用户的特征信息;
log_id,样本ID,是采样时的标号,无实际意义。
用户基本信息:age、gender、residence、city、city_rank、series_dev、series_group、emui_dev、device_name、device_size,参与模型训练,对结果的影响程度不同,后续可以考虑对这些数据进行组合和加权,来提取更高级的特征。
广告相关信息:task_id、adv_id、creat_type_cd、adv_prim_id、inter_type_cd、slot_id、site_id、spread_app_id、Tags、app_second_class、app_score、ad_click_list_001、ad_click_list_002、ad_click_list_003、ad_close_list_001、ad_close_list_002、ad_close_list_003,参与模型训练,预计广告部分信息对结果的预测影响较大,后续同样可以考虑对信息的重要程度进行评估,从而提取更高级的特征。
行为状态信息、时间信息:net_type(行为发生的网络状态)、pt_d(时间戳)。
本次比赛官方禁止使用穿越特征:
测试集数据不得用于全局特征的统计计算;
测试数据集样本中不得使用当前时刻“之后”的数据构造特征
Label、cilLabel、pro:用户点击、点赞、文章浏览进度,可以根据点赞、浏览进度构建特征预测点击率。
ID信息:u_userId,用户ID,原理同目标域ID信息。
用户硬件软件相关信息:u_phonePrice、u_browserLifeCycle、u_browserMode,参与训练,重要程度需要后续评估。
信息流相关信息:u_feedLifeCycle、u_refreshTimes、u_newsCatInterests、u_newsCatDislike、u_newsCatInteres tsST、u_click_ca2_news,同上。
文章相关信息:i_docId、i_s_sourceId、i_regionEntity、i_cat、i_entities、i_dislikeTimes、i_upTimes、I_dtype,同上。
其他信息:e_ch、e_m、e_po、e_pl、e_rn、e_section,同上,具体信息内涵未明确说明。
时间信息:e_et,时间戳,原理同目标域时间戳
在官网下载数据到本地后,对训练集、测试集的目标域、源域信息分别读取
train_data_ads = pd.read_csv('./2022_3_data/train/train_data_ads.csv')
train_data_feeds = pd.read_csv('./2022_3_data/train/train_data_feeds.csv')
test_data_ads = pd.read_csv('./2022_3_data/test/test_data_ads.csv')
test_data_feeds = pd.read_csv('./2022_3_data/test/test_data_feeds.csv')
输出一下数据表格的shape可以看到,目标域数据包括31列特征;源域包括28列特征。
我们对每列数据进行统计,来观察数据的nunique和缺失值个数等信息:
def StatsTrain(train):
stats = []
for col in train.columns:
stats.append(
(col, train[col].nunique(), round(train[col].isnull().sum() * 100 / train.shape[0], 3), round(train[col].value_counts(normalize=True, dropna=False).values[0] * 100, 3), train[col].dtype))
stats_df = pd.DataFrame(stats, columns=['特征', '属性个数', '缺失值占比', '最多属性占比', '特征类型'])
stats_df.sort_values('缺失值占比', ascending=False)[:10]
return stats_df
StatAds = StatsTrain(train_data_ads_)
StatFeeds = StatsTrain(train_data_feeds_)
其中,label只有0和1两个值,但0占据了绝大多数数据,数据严重的不平衡,后续训练时要注意对准确率和召回率的设计。源域中,u_userId有大量不同值,且与目标域的user_id并不匹配,因此训练时源域信息实际上没有与目标域过多的配合使用。
训练集是7天的数据,测试集是1天的数据。由于训练集和测试集的数据之间有时间重叠的情况,因此如果我们用未来的数据进行训练来预测,实际上是不合理的,因此我们可以将训练集中时间戳大于测试集最小时间戳的信息去掉,来避免穿越特征的情况。
# 处理特征穿越, 删除测试集期间内的数据
minDate = min(test_data_ads.pt_d.min(), test_data_feeds.e_et.min())
train_data_ads = train_data_ads[train_data_ads.pt_d < minDate]
train_data_feeds = train_data_feeds[train_data_feeds.e_et < minDate]
我们将训练集测试集的数据合并到一起,方便统一进行特征工程。为此,我们增加一个编号来区分原始的训练集和测试集。将数据集合并,并释放内存。
train_data_ads['istest'] = 0
test_data_ads['istest'] = 1
data_ads = pd.concat([train_data_ads, test_data_ads], axis=0, ignore_index=True)
train_data_feeds['istest'] = 0
test_data_feeds['istest'] = 1
data_feeds = pd.concat([train_data_feeds, test_data_feeds], axis=0, ignore_index=True)
del train_data_ads, test_data_ads, train_data_feeds, test_data_feeds
gc.collect()
注:以上对原始数据的处理来自DataWhale提供的内容(鱼佬和坤佬的代码)。
注意到,部分数据是‘object’类型,因此我们要对其进行编码。
在编码后,根据user_id来将源域数据合并到目标域数据中:
train_feeds = data_feeds[data_feeds.istest==0]
cols = [f for f in train_feeds.columns if f not in ['label','istest','u_userId']]
for col in tqdm(cols):
tmp = train_feeds.groupby(['u_userId'])[col].nunique().reset_index()
tmp.columns = ['user_id', col+'_feeds_nuni']
data_ads = data_ads.merge(tmp, on='user_id', how='left')
cols = [f for f in train_feeds.columns if f not in ['istest','u_userId','u_newsCatInterests','u_newsCatDislike','u_newsCatInterestsST','u_click_ca2_news','i_docId','i_s_sourceId','i_entities']]
for col in tqdm(cols):
tmp = train_feeds.groupby(['u_userId'])[col].mean().reset_index()
tmp.columns = ['user_id', col+'_feeds_mean']
data_ads = data_ads.merge(tmp, on='user_id', how='left')
在这里,我们只提取了源域数据的nunique值和mean值来构建特征。
构建好特征后,观察到源域数据的缺失值在18%左右。
我们简单的用后一个出现的非缺失值填充数据。
data_ads = data_ads.fillna(method='bfill', axis=0).fillna(0)
然后将数据划分为训练集和测试集:
cols = [f for f in data_ads.columns if f not in ['label','istest']]
x_train = data_ads[data_ads.istest==0][cols]
x_test = data_ads[data_ads.istest==1][cols]
y_train = data_ads[data_ads.istest==0]['label']
我们使用CatBoost来训练模型,为了构建合适的验证集,我们运用k折交叉验证的方法。
统计广告域的样本 ctr 预估值,计算 GAUC 和 AUC 评测指标,具体公式如下:
(其中,初赛:α 为 0.7,为 0.3)
AUC 为全体样本的 AUC 统计;
GAUC 为分组 AUC 的加权求和,以用户为维度分组,分组权值为分组内曝光量/总曝光)