前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >2019CCF-BDCI-乘用车细分市场销量预测方案(Top1%)

2019CCF-BDCI-乘用车细分市场销量预测方案(Top1%)

作者头像
Coggle数据科学
发布2021-12-24 10:09:30
6280
发布2021-12-24 10:09:30
举报
文章被收录于专栏:Coggle数据科学Coggle数据科学

写在前面

本文将带来最近一场比赛的方案分享,这是一场有关时间序列的问题,虽然没有进决赛,不过很多点还是非常值得学习的。希望能给大家带来帮助,也欢迎与我进行更多讨论。

这里也将从代码出发,来分享我的解题思路。

正文

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的均值,计算方式为:

其中,

y_{ki}
y_{ki}

车型第

i
i

个样本的真实值,

\widehat{y_{ki}}
\widehat{y_{ki}}

为第

i
i

个样本的预测值,

n_k
n_k

为k车型的预测样本数量(n=4),

\overline{y_k}
\overline{y_k}

为真实值的平均值,

m
m

为需要预测的车型数量。

Score
Score

为最终评价指标,值为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,并尝试了两者加权方式,算术平均和几何平均。

算术平均:

pred=\alpha*pred_a+(1-\alpha)pred_b
pred=\alpha*pred_a+(1-\alpha)pred_b

几何平均:

pred=pred_{a}^{\beta}\times pred_{b}^{1-\beta}
pred=pred_{a}^{\beta}\times pred_{b}^{1-\beta}

由于评分规则,算术平均会使融合的结果偏大,如:

显然不符合本赛题评价指标的直觉,越小的值对评分影响越大,算术平均会导致更大的误差。所以选择几何平均,能够使结果偏向小值,如下:

这个操作也是最终分数有近一个千的提升。在之前的比赛也使用过这种方法,非常值得借鉴。在最近的“全国高校新能源创新大赛”中的也依然适用。

本文参与 腾讯云自媒体分享计划,分享自作者个人站点/博客。
原始发表:2019-11-26 ,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体分享计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 写在前面
  • 正文
  • 1.数据说明
  • 2.评测指标
  • 3.特征工程
  • 4.建模策略
  • 5.融合方式
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档