数据竞赛:联通套餐个性化匹配

机器学习是一个理论结合实践的学科,手头没有实际数据和案例的时候,看一看数据竞赛就不错。这是2018年的一个数据比赛,当年第一、第二的优秀选手都慷慨分享了他们的代码,可以根据代码回顾一下Top选手当时的思路,共同学习一下。

原文首发与我的公众号
在这里插入图片描述

背景

比赛链接:https://www.datafountain.cn/competitions/311

电信产业作为国家基础产业之一,覆盖广、用户多,在支撑国家建设和发展方面尤为重要。随着互联网技术的快速发展和普及,用户消耗的流量也成井喷态势,近年来,电信运营商推出大量的电信套餐用以满足用户的差异化需求,面对种类繁多的套餐,如何选择最合适的一款对于运营商和用户来说都至关重要,尤其是在电信市场增速放缓,存量用户争夺愈发激烈的大背景下。

套餐的个性化推荐,能够在信息过载的环境中帮助用户发现合适套餐,也能将合适套餐信息推送给用户。解决的问题有两个:信息过载问题和用户无目的搜索问题。各种套餐满足了用户有明确目的时的主动查找需求,而个性化推荐能够在用户没有明确目的的时候帮助他们发现感兴趣的新内容。

任务

此题利用已有的用户属性(如个人基本信息、用户画像信息等)、终端属性(如终端品牌等)、业务属性、消费习惯及偏好匹配用户最合适的套餐,对用户进行推送,完成后续个性化服务。可知,为一个分类任务,评价指标为Macro F1。

标签共有11个类别,目标就是根据给定信息预测用户的套餐。有选手找到了ID对应的套餐:
在这里插入图片描述

其中,各个类别的总数分别如下。有意思的是,早在18年的时候,这个数据集最常用的套餐就是腾讯大王卡。
在这里插入图片描述

很多时候,对任务的理解是能够出奇制胜的一招,然而本题任务比较明显,就是分类了。

数据

初赛数据A有70W数据,复赛数据B有35W数据。因此,整个模型跑起来比较耗时间。虽然大家拿到的原始数据都一样,但选择合适的数据作为输入是结果好坏的关键一步,也是经常被忽视的一步。对于任务理解和数据选择这种奠基基础的工作还是要足够的重视。方向选对了,才是拼努力和经验的时候。

第一名的方案中构建了一个白名单机制,因为训练集和测试集中存在重复样本。虽然用户ID在数据是不同的,但很多特征却是一样的,根据本月话费、上月话费、上上月话费、当月流量、上月流量、充值次数、年龄、通话时常,精确到小数点后四位仍然完全相同的用户则视为重复用户。注意的是很多队伍判别重复样本用了所有特征,而第一的方案做了减法,用以下8个特征找到了接近15%的重复样本。

cc = ['1_total_fee',
    '2_total_fee',
    '3_total_fee',
    'month_traffic',
    'pay_times',
    'last_month_traffic',
    'service2_caller_time',
    'age']

当然,大家的第一想法是既然测试集中有重复样本,那直接把重复样本的标签拿过来当预测,保证了重复样本的完全准确。其实不然,GBDT类模型还是很强的,本来对重复样本的预测率就接近完全准确。所以第一的方案为测试集中非重复样本单独训练了一个模型,模型使用的训练集去掉了重复样本。而重复样本可以直接从训练集中查询,这也告诉我们数据样本的选取对于结果有多重要。

除了白名单的应用外,由于初赛和复赛分别有两套数据,还有技巧地应用了这两套数据。先用初赛+复赛数据训练得到分类概率,然后分类概率作为特征加到复赛数据中,再用复赛数据进行训练得到结果。

特征

特征工程是数据挖掘比赛耗时最多、最值得花精力的事了。特征工程包括特征转化、选择、和生成。白话一点的说法就是,换一换、减一减、加一加。尤其是如何增加新的特征,常规的尝试是根据经验运算一下,再就是从业务角度、机理角度来考虑。可以根据给定列,心里先想一下可以构建哪些特征,再画画图看看效果,扔进模型看看效果。

数值特征

在这里插入图片描述

首先当然是利用原有数据,原始数据中提供的本月话费、上月话费、上上月话费。

['1_total_fee', '2_total_fee', '3_total_fee',  '4_total_fee']

整数特征(rounding feature)和小数特征 对过去四个月每个月的话费都计算话费的整数、整数的最后一位、小数、小数位是否为0、小数位是否为5、小数位的最后一位、小数位是5的几倍等

train[column+'_int'] = train[column].fillna(-1).astype('int')
train[column+'_int_last'] = train[column+'_int']%10 #last int 
train[column+'_decimal'] = round(((train[column]-train[column+'_int'])*100).fillna(-1)).astype('int')    #decimal
train[column+'_decimal_is_0'] = (train[column+'_decimal']==0).astype('int')
train[column+'_decimal_is_5'] = (train[column+'_decimal']%5==0).astype('int') 
train[column+'_decimal_last'] = train[column+'_decimal']%10
train[column+'_decimal_last2'] = train[column+'_decimal']//5 
train[column+'_extra_fee'] = ((train[column]*100)-600)%1000
train[column+'_27perMB'] = ((train[column+'_extra_fee']%27 == 0)&(train[column+'_extra_fee'] != 0)).astype('int')
train[column+'_15perMB'] = ((train[column+'_extra_fee']%15 == 0)&(train[column+'_extra_fee'] != 0)).astype('int')

分箱特征 对数值特征进行分箱,这里分箱的是下面的二阶特征进行分箱

for column in ['4-fea-dealta', '3-fea-dealta', '2-fea-dealta', '1-fea-dealta','1-3-fea-dealta','1-min-fea-dealta']:
    train[column+'_is_0'] = (train[column]==0).astype('int')
    train[column+'_is_6000'] = ((train[column]%6000 == 0)&(train[column] != 0)).astype('int') 
    train[column+'_is_5'] = ((train[column]%5 == 0)&(train[column] != 0)).astype('int')
    train[column+'_is_10'] = ((train[column]%10 == 0)&(train[column] != 0)).astype('int')
    train[column+'_is_15'] = ((train[column]%15 == 0)&(train[column] != 0)).astype('int')
    train[column+'_is_27'] = ((train[column]%27 == 0)&(train[column] != 0)).astype('int')
    train[column+'_is_30'] = ((train[column]%30 == 0)&(train[column] != 0)).astype('int')
    train[column+'_is_50'] = ((train[column]%50 == 0)&(train[column] != 0)).astype('int')
    train[column+'_is_100'] = ((train[column]%100 == 0)&(train[column] != 0)).astype('int')
    train[column+'_is_500'] = ((train[column]%500 == 0)&(train[column] != 0)).astype('int')

构造的第一个特征就是这几个月话费的最低值,毕竟目标是预测套餐,想想我们生活中的场景,有几个月可能会超,但最低一般能够反应套餐的本身水平。

train['fea-min'] = train[[str(1+i) +'_total_fee' for i in range(4)]].min(axis = 1)

判断四个月的话费是否是一个整数。如果是小数,可能是由于超出了套餐费用叠加而来。

二阶特征

由于每个月的话费是套餐类型的重要特征,二阶或高阶特征则是通过对两个以上特征进行运算获得新特征。例如对原特征求差值

train['4-fea-dealta'] = round((train['4_total_fee'] - train['3_total_fee'])*100).fillna(999999.9).astype('int')
train['3-fea-dealta'] = round((train['3_total_fee'] - train['2_total_fee'])*100).fillna(999999.9).astype('int')
train['2-fea-dealta'] = round((train['2_total_fee'] - train['1_total_fee'])*100).fillna(999999.9).astype('int')
train['1-fea-dealta'] = round((train['4_total_fee'] - train['1_total_fee'])*100).fillna(999999.9).astype('int')  
train['1-3-fea-dealta'] = round((train['3_total_fee'] - train['1_total_fee'])*100).fillna(999999.9).astype('int') 
train['1-min-fea-dealta'] = round((train['1_total_fee'] - train['fea-min'])*100).fillna(999999.9).astype('int') 

统计特征

对1-2-3-4_total_fee进行min ,max ,std ,mean等操作

前四个月话费最低值
前四个月话费不同值的个数

grid特征

grid特征其实是我自己起的名字,大意是每一个用户对应着一个特征,我们也可以看做是每一个特征对应着用户,因此可以找到特征相关的特点,得到一个特征为行名的表,然后融合到用户表的里。

比如,首先筛选了过去四个月使用话费比较多花样的用户集,至少出现三种不同的数

df['fea_unum'] = df[['1_total_fee','2_total_fee','3_total_fee', '4_total_fee']].nunique(axis=1)
df.drop_duplicates(subset =['1_total_fee','2_total_fee','3_total_fee', '4_total_fee'],inplace=True)
df = df[df.fea_unum>2]

大致是把count encoder进一步推向极致的玩法: 比较复杂和高阶也不一定起作用的特征。

类别特征

在这里插入图片描述

类别特征比较简单,原数据中的特征进行LabelEncoder即可

序列特征

第二名除了常规的特征外,还训练了word2vec的embedding特征。word2vec是2018年bert流行之前最常用的词嵌入模型,用于给单词编码,也是成绩上分的一个关键。当然了,想到word2vec的使用首先需要理解序列特征的含义,根据上下文(context)推测中心词,以及根据中心词推测上下文,从而来构建序列中蕴含的信息。对数据和业务的理解,能够解读出序列蕴含的信息从而正确、灵活的运用。

其实,除了word2vec词嵌入特征外,1_total_fee至4_total_fee上,4个月的词频统计也可以看作NLP中的CountVectorizer。

所以根据前四个月的话费用NLP的各项技术也对结果有所提高

模型

对于结构化的表格数据,lightgbm、xgboost、catbost为代表的GBDT类模型几乎占据统治地位,是首选模型。

params = {
    'metric': 'multi_logloss',
    'num_class':11,
    'boosting_type': 'gbdt', 
    'objective': 'multiclass',
    'feature_fraction': 0.7,
    'learning_rate': 0.02,
    'bagging_fraction': 0.7,   
    'num_leaves': 64,
    'max_depth': -1, 
    'num_threads': 16, 
    'seed': 2018, 
    'verbose': -1,   
    }

验证

分层交叉验证,取样时,根据目标label的分布每次取样中的label也满足同样分布。

folds = StratifiedKFold(n_splits= num_folds, shuffle=True, random_state=1234)

技巧总结

数据:如何更好的选取训练样本?这里的关键是选取非重复样本进行学习。如何在决赛更加合理利用新增数据?
特征:除了在数据集上,精细的做一做各种尝试外。word2vec来自NLP的也值得尝试
后处理:针对比赛的评价函数Macro F1, 例如830的类别F1最低,只有不到0.3,严重拉低了整体macro-F1的分数,可以对其进行特殊后处理,增加子类别的F1,以达到分数最大化。

1st solution:https://github.com/PPshrimpGo/BDCI2018-ChinauUicom-1st-solution
2nd solution:https://github.com/PandasCute/2018-CCF-BDCI-China-Unicom-Research-Institute-top2
6th solution:https://github.com/ZengHaihong/2018_CCF_BDCI_ChinaUnicom_Package_Match_Rank6

联系方式

公众号搜索:YueTan

おすすめ

転載: blog.csdn.net/weixin_38812492/article/details/110305120