• 2022科大讯飞商品销量智能预测挑战赛—参赛总结


    2022科大讯飞商品销量智能预测挑战赛—参赛总结

    摘要

    • 比赛网址https://challenge.xfyun.cn/topic/info?type=product-sales
    • 赛题任务:本次大赛提供了商品销量历史数据作为训练样本,参赛选手需基于提供的样本构建模型,预测商品未来三个月的销售量。初赛提供了2018年2月1日至2020年12月31日的若干商品历史销量数据和订单数据,预测其2021年1月至3月的销量数据。决赛提供了2018年2月1日至2021年3月31日的若干商品历史销量数据和订单数据,预测其2021年4月至6月的销量数据。相当于复赛时提供了初赛测试集的标签,这一点在复赛时可以利用起来,能够节省不少提交次数。而且赛后也可以继续研究。
    • 数据分析:提供的数据集的销量是细分到每一天的,但预测是要细分到月份的,因此,可以有两种角度去预测销量。第一是按天进行预测,但是有些特征是没有提供细分到天的数据的,特征相对按月预测会少一点;第二种是按月预测,这就需要先将销量按月汇总起来构建标签。最后我是选择了第二种。
    • 特征构建:主要用到了lag和rolling聚合的mean、std、median特征。
    • 模型选择:采用比赛常用模型:lightgbm、xgboost和catboost分别构建回归模型,最后并用TPE搜索最优参数组合。
    • 训练方案:每个模型均使用5折交叉验证,以product_id均衡划分训练集和验证集,对测试集的预测值取平均即得到该模型最终的预测值。
    • 模型融合:对三个5折交叉验证后的模型结果,采用stacking进行模型融合。
    • 赛题得分:初赛最终成绩:0.70854分,排名:第71名;复赛最终成绩:0.74399分,排名:第46名。
    • 总结结论:这是我参加比赛以来第二次进入复赛阶段的比赛,很有纪念意义,虽然跟前排大佬还有很大的差距,但是从中也学习到了很多知识,有机会下次再来~

    赛题任务

    特征构建

    并没有构建过多的特征,因为我发现即使构建更多的lag或者rolling特征,也不会有提升,甚至是降分的。期间有通过根据特征重要性、shap、与标签的相关性来筛选特征,但筛选后的效果也不是很好。一直都没有找到比较强的特征。下面是部分代码:

    feats_cols=[]
        
    for i in range(1, 17):
        for f in ['label', 'order', 'start_stock', 'end_stock']:
            data[f+'_shift_%d'%i] = data.groupby('product_id')[f].shift(i+2)
            if i <= 12:
                feats_cols.append(f+'_shift_%d'%i)
    
    for i in [3, 6, 12]:
        for f in ['label', 'order', 'start_stock', 'end_stock']:
            data[f+'_mean_%d'%i] = data[[f+'_shift_%d'%i for i in range(1, i+1)]].mean(axis=1)
            data[f+'_std_%d'%i] = data[[f+'_shift_%d'%i for i in range(1, i+1)]].std(axis=1)
            data[f+'_median_%d'%i] = data[[f+'_shift_%d'%i for i in range(1, i+1)]].median(axis=1)
            feats_cols.extend([f+'_mean_%d'%i,f+'_std_%d'%i,f+'_median_%d'%i])
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14

    训练方案

    使用StratifiedKFold按product_id均衡划分训练集和验证集进行5折交叉验证,期间对比过使用KFold来划分,实验验证StratifiedKFold要更胜一筹,分别训练lightgbm、xgboost和catboost三个模型。训练代码如下:

    def train_model_with_nfold(data,train,test,feat_cols,feats_cols,category_cols,
        lgb_params=None,xgb_params=None,cat_params=None,model_types=['lgb','xgb','cat'],fold_num=5,
        seeds=[2022],stratified=True,num_boost_round=10000,early_stopping_rounds=200,verbose=200,
        un_select_cols=[]
        ):
    
        score_lgb = np.zeros(fold_num)
        score_xgb = np.zeros(fold_num)
        score_cat = np.zeros(fold_num)
        score = np.zeros(fold_num)
        oof_lgb = np.zeros(len(train))
        oof_xgb = np.zeros(len(train))
        oof_cat = np.zeros(len(train))
        oof = np.zeros(len(train))
        pred_y = pd.DataFrame()
    
        for seed in seeds:
            for model_type in model_types:
                if stratified:
                    kf = StratifiedKFold(n_splits=fold_num, shuffle=True, random_state=seed)
                else:
                    kf = KFold(n_splits=fold_num, shuffle=True, random_state=seed)
                if model_type == 'cat':
                    feat_cols = [col for col in feats_cols if col not in un_select_cols+['label']]
                    np.random.shuffle(feat_cols)
                if model_type == 'xgb':
                    if 1001 in train['product_id'].values:
                        LE=LabelEncoder()
                        for col in category_cols:
                            LE.fit(data[col].astype(str))
                            train[col]=LE.transform(train[col].astype(str))
                            test[col]=LE.transform(test[col].astype(str))
                else:
                    train = data[data['label'].notna()].reset_index(drop=True)
                    test = data[data['label'].isna()].reset_index(drop=True)
                for fold, (train_idx, val_idx) in enumerate(kf.split(train[feat_cols], train['product_id'])):
                    print(f'-----------------fold:{fold+1} -----------------seed:{seed} -----------------model_type:{model_type}')
                    if model_type == 'lgb':
                        lgb_params['seed']=seed
                        tra = lgb.Dataset(train.loc[train_idx, feat_cols],train.loc[train_idx, 'label'])
                        val = lgb.Dataset(train.loc[val_idx, feat_cols],train.loc[val_idx, 'label'])
                        model = lgb.train(lgb_params, tra, valid_sets=[val], num_boost_round=num_boost_round,categorical_feature=category_cols,
                                        callbacks=[lgb.early_stopping(early_stopping_rounds), lgb.log_evaluation(verbose)])
    
                        score_lgb[fold]=model.best_score['valid_0']['rmse'] / len(seeds)
                        score[fold]=model.best_score['valid_0']['rmse'] / len(seeds) / len(model_types)
                        oof_lgb[val_idx] += model.predict(train.loc[val_idx, feat_cols], num_iteration=model.best_iteration) / len(seeds)
                        oof[val_idx] += model.predict(train.loc[val_idx, feat_cols], num_iteration=model.best_iteration) / len(seeds) / len(model_types)
                        pred_y[f'fold{fold}_seed{seed}_{model_type}'] = model.predict(test[feat_cols], num_iteration=model.best_iteration)
                    elif model_type == 'xgb':
                        xgb_params['seed']=seed
                        train_matrix = xgb.DMatrix(train.loc[train_idx, feat_cols] , label=train.loc[train_idx, 'label'])
                        valid_matrix = xgb.DMatrix(train.loc[val_idx, feat_cols] , label=train.loc[val_idx, 'label'])
                        watchlist = [(train_matrix, 'train'),(valid_matrix, 'eval')]
                        model = xgb.train(xgb_params, train_matrix, num_boost_round=num_boost_round, evals=watchlist, verbose_eval=verbose, early_stopping_rounds=early_stopping_rounds)
    
                        score_xgb[fold]=model.best_score / len(seeds)
                        score[fold]=model.best_score / len(seeds) / len(model_types)
                        oof_xgb[val_idx] += model.predict(valid_matrix,iteration_range=(0,model.best_iteration+1)) / len(seeds)
                        oof[val_idx] += model.predict(valid_matrix,iteration_range=(0,model.best_iteration+1)) / len(seeds) / len(model_types)
                        pred_y[f'fold{fold}_seed{seed}_{model_type}'] = model.predict(xgb.DMatrix(test[feat_cols]),iteration_range=(0,model.best_iteration+1))
                    else:
                        cat_params['random_seed']=seed
                        model = cat.CatBoostRegressor(num_boost_round=num_boost_round, **cat_params)
                        trn_x = train.loc[train_idx, feat_cols]
                        trn_y = train.loc[train_idx, 'label']
                        val_x = train.loc[val_idx, feat_cols]
                        val_y = train.loc[val_idx, 'label']
                        model.fit(trn_x, trn_y, eval_set=(val_x, val_y),cat_features=category_cols, use_best_model=True, verbose=verbose)
                        
                        score_cat[fold] = model.best_score_['validation']['RMSE'] / len(seeds)
                        score[fold] = model.best_score_['validation']['RMSE'] / len(seeds) / len(model_types)
                        oof_cat[val_idx] += model.predict(val_x) / len(seeds)
                        oof[val_idx] += model.predict(val_x) / len(seeds) / len(model_types)
                        pred_y[f'fold{fold}_seed{seed}_{model_type}'] = model.predict(test[feat_cols])
    
        print(f'score_lgb={score_lgb.mean()}\nscore_xgb={score_xgb.mean()}\nscore_cat={score_cat.mean()}\nscore={score.mean()}')
    
        return oof_lgb,oof_xgb,oof_cat,oof,pred_y
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 可以看到,加入了随机数种子seed的循环,但其实只用了一个2022,因为试验过多个seed的情况,效果不升反降,所以就没采用了;
    • 还有,针对xgboost模型,如果是类别特征,最好是将它们进行LabelEncoder编码之后再输入到模型中训练,因为xgboost不支持处理类别特征,并且,在我的试验当中,即使是像本赛题中的product_id等数值型类别特征,编码与不编码,分数分别是0.654与0.577,相差还是很大的;
    • 还有,针对catboost模型,发现输入特征的排列顺序对结果的预测有非常大的影响(对lgb和xgb的影响相对较小),所以用shuffle对特征进行随机排序,并对排序后所预测出的结果是否与其他两个模型的预测结果相当,而选择合适的shuffle次数后的特征顺序,作为catboost模型的特征输入。

    模型融合

    一开始的模型融合都是采用简单的算术平均融合,后来试了下stacking融合,确实效果要更好一点点(好4个千分点左右)。模型融合可谓是竞赛中最后的杀手锏,初赛中,lightgbm、xgboost和catboost三个模型最优的线上分数分别为:0.68144,0.69092,0.66796。采用stacking融合后,分数为0.70854,融合代码如下:

    def RidgeCV(train,oof_lgb,oof_xgb,oof_cat,pred_y,fold_num=5,seed=2022):
        oof=pd.DataFrame([oof_lgb,oof_xgb,oof_cat]).T
        oof.columns=['lgb','xgb','cat']
        oof['product_id']=train['product_id']
        oof['label']=train['label']
    
        y=pd.DataFrame([pred_y[[i for i in pred_y.columns if 'lgb' in i]].mean(1),pred_y[[i for i in pred_y.columns if 'xgb' in i]].mean(1),pred_y[[i for i in pred_y.columns if 'cat' in i]].mean(1)]).T
        y.columns=['lgb','xgb','cat']
    
        kf = StratifiedKFold(n_splits=fold_num, shuffle=True, random_state=seed)
        oof_res=np.zeros(len(oof))
        prediction=np.zeros(len(y))
        for fold, (train_idx, val_idx) in enumerate(kf.split(oof[['lgb','xgb','cat']], oof['product_id'])):
            train_x=oof.loc[train_idx,['lgb','xgb','cat']]
            train_y=oof.loc[train_idx,'label']
            
            valid_x=oof.loc[val_idx,['lgb','xgb','cat']]
            valid_y=oof.loc[val_idx,'label']
            
            model = Ridge(random_state=2022)
            model.fit(train_x,train_y)
            
            oof_res[val_idx]=model.predict(valid_x)
            prediction+=model.predict(y)/fold_num
    
        return oof_res,prediction
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26

    赛题得分

    初赛

    复赛
    由于种种原因,复赛并没有认真在打(主要还是太菜了,上不动了-_-!!),于是复赛只是沿用了初赛时的训练方案,只是测试集更换了一下,以及训练集多出了3个月的数据。复赛分数为:0.74399,排名:48/62


    以上即为本文的全部内容,若需要全部源代码的,请关注公众号《Python王者之路》,回复关键词:20220910,即可获取。


    写在最后

    好久没有完整地打一次数据挖掘类的比赛了,之前大部分都是参加CV类型的比赛,挖掘类比赛最重要的一环应该是特征的构建了,好的特征可以实现质的飞跃。

    这次比赛中我构建的特征还是过于简单了,这也是跟前排差距大的主要原因,因为缺乏对赛题背后具体业务的理解,也就很难挖掘出比较强的特征。

    预见到这个瓶颈之后,我干脆放弃去寻找强的特征,而是更加专注于模型的方面,于是从一开始只用一个lightgbm模型,到后来直接把其他两个树模型也都考虑进来。

    虽然,我为了上分也做了很多努力,研究基于规则的方案;特征的筛选方面;及在baseline的基础上扩充了多个随机数种子和特征顺序随机排列等训练方案,即使最终效果也没提升多少,但是我从中也学习到了一些知识。

    比如说:特征的排列顺序的影响、TPE最优参数搜索、stacking融合等。期待之后,前排大佬的分享,让我膜拜膜拜大佬们的上分技巧哈哈哈~

    今天,刚好是中秋节和教师节两个节日重叠的日子,那么,在此,

    祝大家中秋节快乐、特别是老师们节日快乐!!!

  • 相关阅读:
    PBX与VoIP:它们之间有什么区别?
    【Hack The Box】linux练习-- Nibbles
    ISO/IEC 27001:2022 发布(中文),信息安全、网络安全和隐私保护 信息安全管理系统 要求
    【图像处理 】卡尔曼滤波器原理
    线性代数3:矢量方程
    数据库的备份和恢复
    【微软漏洞分析】Windows-Research-Kernel-WRK 代码分析 安全例程(2) 令牌和安全描述符
    Netty,Tcp,socket的java框架,netty学习
    进程与线程的记忆方法
    机器学习周记(第三十八周:语义分割)2024.5.6~2024.5.12
  • 原文地址:https://blog.csdn.net/sinat_39629323/article/details/126500790