写在前面
本文将带来最近一场比赛的方案分享,这是一场有关时间序列的问题,虽然没有进决赛,不过很多点还是非常值得学习的。希望能给大家带来帮助,也欢迎与我进行更多讨论。
这里也将从代码出发,来分享我的解题思路。
正文
1.数据说明
赛题给出了历史销量数据包含60个车型在22个省份,从2016年1月至2017年12月的销量。参赛队伍需要预测接下来4个月(2018年1月至2018年4月),这60个车型在22个省份的销量;参赛参赛队伍需自行划分训练集数据进行建模。
数据文件包括:
[训练集]历史销量数据:train_sales_data_v1.csv
[训练集]车型搜索数据:train_search_data_v1.csv
[训练集]汽车垂直媒体新闻评论数据和车型评论数据:train_user_reply_data_v1.csv
[评测集]2018年1月至4月的各车型各省份销量预测:evaluation_public.csv
初赛和复赛并无太多差异,主要是车型增多而已。
2.评测指标
在我看来对评测指标的理解,也是能够帮助上分的。
初赛复赛阶段的在线评分采用NRMSE(归一化均方根误差)的均值作为评估指标。首先单独计算每个车型在每个细分市场(省份)的NRMSE,再计算所有NRMSE的均值,计算方式为:
其中,
车型第
个样本的真实值,
为第
个样本的预测值,
为k车型的预测样本数量(n=4),
为真实值的平均值,
为需要预测的车型数量。
为最终评价指标,值为0-1之间,越接近1模型越准确。
不难理解这里需要按车型在不同省份的得分,然后整合起来得到最终的分数。
关键来了,这样的评价指标有一个特点,销量小的车型对评分带来更大的影响,依据这个特征,可以考虑添加样本权重,以及最终融合方式。
下面给出了计算权重的方式,当然也是出于经验的方式构造的,可以有更多优化。
代码语言:javascript复制# 样本权重信息
data['n_salesVolume'] = np.log(data['salesVolume'] 1)
df_wei = data.groupby(['province','model'])['n_salesVolume'].agg({'mean'}).reset_index().sort_values('mean')
df_wei.columns = ['province','model','wei']
df_wei['wei'] = 10 - df_wei['wei'].values
data = data.merge(df_wei, on=['province','model'], how='left')
只需在模型训练的时候加上权重信息即可,初赛有千分位的提升,复赛没有具体测。
代码语言:javascript复制dtrain = lgb.Dataset(df[all_idx][features], label=df[all_idx]['n_label'], weight=df[all_idx]['wei'].values)
下面给出评价指标的代码:
代码语言:javascript复制def score(data, pred='pred_label', label='label', group='model'):
data['pred_label'] = data['pred_label'].apply(lambda x: 0 if x < 0 else x).round().astype(int)
data_agg = data.groupby('model').agg({
pred: list,
label: [list, 'mean']
}).reset_index()
data_agg.columns = ['_'.join(col).strip() for col in data_agg.columns]
nrmse_score = []
for raw in data_agg[['{0}_list'.format(pred), '{0}_list'.format(label), '{0}_mean'.format(label)]].values:
nrmse_score.append(
mse(raw[0], raw[1]) ** 0.5 / raw[2]
)
print(1 - np.mean(nrmse_score))
return 1 - np.mean(nrmse_score)
3.特征工程
方案主要构造了传统的时序特征,很多时候也都是从这几个方面进行扩展,或者更好的描绘这几种特性。遇到这类问题,从这四点考虑,准没错的。此次比赛我对异常的方面做的很少,也没有找到有效办法。不过我始终坚信,异常点方面还是能提不好分的。
比赛预测省份下车型的每月汽车销量,我们可以从不同维度来提取特征,可以从微观,也可以从宏观角度。我主要从两点考虑构造,同时也构造了popularity的相关特征
代码语言:javascript复制1. 省份/车型/月份:汽车销量,popularity
2. 车型/月份:汽车销量
具体区间怎么选择,统计方式怎么选择?下面给出我的方案
代码语言:javascript复制def getStatFeature(df_, month, flag=None):
df = df_.copy()
stat_feat = []
# 确定起始位置
if (month == 26) & (flag):
n = 1
elif (month == 27) & (flag):
n = 2
elif (month == 28) & (flag):
n = 3
else:
n = 0
print('进行统计的起始位置:',n,' month:',month)
######################
# 省份/车型/月份 粒度 #
#####################
df['adcode_model'] = df['adcode'] df['model']
df['adcode_model_mt'] = df['adcode_model'] * 100 df['mt']
for col in tqdm(['label']):
# 平移
start, end = 1 n, 9
df, add_feat = get_shift_feature(df, start, end, col, 'adcode_model_mt')
stat_feat = stat_feat add_feat
# 相邻
start, end = 1 n, 8
df, add_feat = get_adjoin_feature(df, start, end, col, 'adcode_model_mt', space=1)
stat_feat = stat_feat add_feat
start, end = 1 n, 7
df, add_feat = get_adjoin_feature(df, start, end, col, 'adcode_model_mt', space=2)
stat_feat = stat_feat add_feat
start, end = 1 n, 6
df, add_feat = get_adjoin_feature(df, start, end, col, 'adcode_model_mt', space=3)
stat_feat = stat_feat add_feat
# 连续
start, end = 1 n, 3 n
df, add_feat = get_series_feature(df, start, end, col, 'adcode_model_mt', ['sum','mean','min','max','std','ptp'])
stat_feat = stat_feat add_feat
start, end = 1 n, 5 n
df, add_feat = get_series_feature(df, start, end, col, 'adcode_model_mt', ['sum','mean','min','max','std','ptp'])
stat_feat = stat_feat add_feat
start, end = 1 n, 7 n
df, add_feat = get_series_feature(df, start, end, col, 'adcode_model_mt', ['sum','mean','min','max','std','ptp'])
stat_feat = stat_feat add_feat
for col in tqdm(['popularity']):
# 平移
start, end = 4, 9
df, add_feat = get_shift_feature(df, start, end, col, 'adcode_model_mt')
stat_feat = stat_feat add_feat
# 相邻
start, end = 4, 8
df, add_feat = get_adjoin_feature(df, start, end, col, 'adcode_model_mt', space=1)
stat_feat = stat_feat add_feat
start, end = 4, 7
df, add_feat = get_adjoin_feature(df, start, end, col, 'adcode_model_mt', space=2)
stat_feat = stat_feat add_feat
start, end = 4, 6
df, add_feat = get_adjoin_feature(df, start, end, col, 'adcode_model_mt', space=3)
stat_feat = stat_feat add_feat
# 连续
start, end = 4, 7
df, add_feat = get_series_feature(df, start, end, col, 'adcode_model_mt', ['sum','mean','min','max','std','ptp'])
stat_feat = stat_feat add_feat
start, end = 4, 9
df, add_feat = get_series_feature(df, start, end, col, 'adcode_model_mt', ['sum','mean','min','max','std','ptp'])
stat_feat = stat_feat add_feat
##################
# 车型/月份 粒度 #
##################
df['model_mt'] = df['model'] * 100 df['mt']
for col in tqdm(['label']):
colname = 'model_mt_{}'.format(col)
tmp = df.groupby(['model_mt'])[col].agg({'mean'}).reset_index()
tmp.columns = ['model_mt',colname]
df = df.merge(tmp, on=['model_mt'], how='left')
# 平移
start, end = 1 n, 9
df, add_feat = get_shift_feature(df, start, end, colname, 'adcode_model_mt')
stat_feat = stat_feat add_feat
# 相邻
start, end = 1 n, 8
df, add_feat = get_adjoin_feature(df, start, end, colname, 'model_mt', space=1)
stat_feat = stat_feat add_feat
start, end = 1 n, 7
df, add_feat = get_adjoin_feature(df, start, end, colname, 'model_mt', space=2)
stat_feat = stat_feat add_feat
start, end = 1 n, 6
df, add_feat = get_adjoin_feature(df, start, end, colname, 'model_mt', space=3)
stat_feat = stat_feat add_feat
# 连续
start, end = 1 n, 3 n
df, add_feat = get_series_feature(df, start, end, colname, 'model_mt', ['sum','mean'])
stat_feat = stat_feat add_feat
start, end = 1 n, 5 n
df, add_feat = get_series_feature(df, start, end, colname, 'model_mt', ['sum','mean'])
stat_feat = stat_feat add_feat
start, end = 1 n, 7 n
df, add_feat = get_series_feature(df, start, end, colname, 'model_mt', ['sum','mean'])
stat_feat = stat_feat add_feat
return df,stat_feat
这里面涉及到三个函数:
代码语言:javascript复制def get_shift_feature(df_, start, end, col, group):
'''
历史平移特征
col : label,popularity
group: adcode_model_mt, model_mt
'''
df = df_.copy()
add_feat = []
for i in range(start, end 1):
add_feat.append('shift_{}_{}_{}'.format(col,group,i))
df['{}_{}'.format(col,i)] = df[group] i
df_last = df[~df[col].isnull()].set_index('{}_{}'.format(col,i))
df['shift_{}_{}_{}'.format(col,group,i)] = df[group].map(df_last[col])
del df['{}_{}'.format(col,i)]
return df, add_feat
def get_adjoin_feature(df_, start, end, col, group, space):
'''
相邻N月的首尾统计
space: 间隔
Notes: shift统一为adcode_model_mt
'''
df = df_.copy()
add_feat = []
for i in range(start, end 1):
add_feat.append('adjoin_{}_{}_{}_{}_{}_sum'.format(col,group,i,i space,space)) # 求和
add_feat.append('adjoin_{}_{}_{}_{}_{}_mean'.format(col,group,i,i space,space)) # 均值
add_feat.append('adjoin_{}_{}_{}_{}_{}_diff'.format(col,group,i,i space,space)) # 首尾差值
add_feat.append('adjoin_{}_{}_{}_{}_{}_ratio'.format(col,group,i,i space,space)) # 首尾比例
df['adjoin_{}_{}_{}_{}_{}_sum'.format(col,group,i,i space,space)] = 0
for j in range(0, space 1):
df['adjoin_{}_{}_{}_{}_{}_sum'.format(col,group,i,i space,space)] = df['adjoin_{}_{}_{}_{}_{}_sum'.format(col,group,i,i space,space)]
df['shift_{}_{}_{}'.format(col,'adcode_model_mt',i j)]
df['adjoin_{}_{}_{}_{}_{}_mean'.format(col,group,i,i space,space)] = df['adjoin_{}_{}_{}_{}_{}_sum'.format(col,group,i,i space,space)].values/(space 1)
df['adjoin_{}_{}_{}_{}_{}_diff'.format(col,group,i,i space,space)] = df['shift_{}_{}_{}'.format(col,'adcode_model_mt',i)].values -
df['shift_{}_{}_{}'.format(col,'adcode_model_mt',i space)]
df['adjoin_{}_{}_{}_{}_{}_ratio'.format(col,group,i,i space,space)] = df['shift_{}_{}_{}'.format(col,'adcode_model_mt',i)].values /
df['shift_{}_{}_{}'.format(col,'adcode_model_mt',i space)]
return df, add_feat
def get_series_feature(df_, start, end, col, group, types):
'''
连续N月的统计值
Notes: shift统一为adcode_model_mt
'''
df = df_.copy()
add_feat = []
li = []
df['series_{}_{}_{}_{}_sum'.format(col,group,start,end)] = 0
for i in range(start,end 1):
li.append('shift_{}_{}_{}'.format(col,'adcode_model_mt',i))
df['series_{}_{}_{}_{}_sum'.format( col,group,start,end)] = df[li].apply(get_sum, axis=1)
df['series_{}_{}_{}_{}_mean'.format(col,group,start,end)] = df[li].apply(get_mean, axis=1)
df['series_{}_{}_{}_{}_min'.format( col,group,start,end)] = df[li].apply(get_min, axis=1)
df['series_{}_{}_{}_{}_max'.format( col,group,start,end)] = df[li].apply(get_max, axis=1)
df['series_{}_{}_{}_{}_std'.format( col,group,start,end)] = df[li].apply(get_std, axis=1)
df['series_{}_{}_{}_{}_ptp'.format( col,group,start,end)] = df[li].apply(get_ptp, axis=1)
for typ in types:
add_feat.append('series_{}_{}_{}_{}_{}'.format(col,group,start,end,typ))
return df, add_feat
看到这里,我的特征基本上就做完了,没有复杂的构造,都比较基础。
上面代码还有一部分没有交代清楚
代码语言:javascript复制 # 确定起始位置
if (month == 26) & (flag):
n = 1
elif (month == 27) & (flag):
n = 2
elif (month == 28) & (flag):
n = 3
else:
n = 0
这部分主要用来确定提取特征的起始位置,主要在于建模方式的选择。
4.建模策略
测试集需要预测四个月的汽车销量,可选建模策略还是蛮多的,个人主要方案是一步一步预测,首先得到1月份的结果,然后将1月份合并到训练集,预测2月份结果,然后是3月,4月。
这样可能会累计误差,所有也可以跳跃式提取。
代码语言:javascript复制for month in [25,26,27,28]:
m_type = 'xgb'
flag = False # False:连续提取 True:跳跃提取
st = 4 # 保留训练集起始位置
# 提取特征
data_df, stat_feat = get_stat_feature(data, month, flag)
# 特征分类
num_feat = ['regYear'] stat_feat
cate_feat = ['adcode','bodyType','model','regMonth']
# 类别特征处理
if m_type == 'lgb':
for i in cate_feat:
data_df[i] = data_df[i].astype('category')
elif m_type == 'xgb':
lbl = LabelEncoder()
for i in tqdm(cate_feat):
data_df[i] = lbl.fit_transform(data_df[i].astype(str))
# 最终特征集
features = num_feat cate_feat
print(len(features), len(set(features)))
# 模型训练
sub, model = get_train_model(data_df, month, m_type, st)
data.loc[(data.regMonth==(month-24))&(data.regYear==2018), 'salesVolume'] = sub['forecastVolum'].values
data.loc[(data.regMonth==(month-24))&(data.regYear==2018), 'label' ] = sub['forecastVolum'].values
这里m_type确定选择的模型,flag确定是否采样跨越式提取特征,st保留训练集起始位置。基本上最终方案只是在这几个参数上进行修改,然后融合得到。
5.融合方式
融合方式也可以有很多,stacking和blending,这里只选择了blending,并尝试了两者加权方式,算术平均和几何平均。
算术平均:
几何平均:
由于评分规则,算术平均会使融合的结果偏大,如:
显然不符合本赛题评价指标的直觉,越小的值对评分影响越大,算术平均会导致更大的误差。所以选择几何平均,能够使结果偏向小值,如下:
这个操作也是最终分数有近一个千的提升。在之前的比赛也使用过这种方法,非常值得借鉴。在最近的“全国高校新能源创新大赛”中的也依然适用。