
1. 拿到赛题一先别急着敲代码1.1 把问题拆成三个层次数模竞赛的赛题一通常是一套题里数据最规整、套路最固定的一道题但恰恰是这道题最容易让队伍在第一天晚上就陷入“好像什么都做了又好像什么都没做”的状态。以最常见的销售预测与库存规划类赛题一为例题目一般会给你一张包含历史销售记录、商品属性、门店信息、节假日标记等字段的表格让你预测未来某段时间的销量并在此基础上给出订货或库存分配方案。收到题之后我习惯把任务拆成三层去看第一层是表面任务题目文字里明确写出来的“预测”“优化”“评估”要求第二层是数据任务我需要哪些字段、数据有没有缺失、时间范围是不是对齐的第三层是提交任务论文里至少要有模型、结果、检验方法代码要能跑出图和数据表。这三层想清楚之后队员之间才不会在第一天晚上各干各的最后合不到一起。1.2 三天时间怎么分配才不慌我见过太多队伍第一天就开始写复杂模型结果第二天发现数据预处理根本没做完第三天通宵补论文。实际打下来时间分配比模型本身更能决定成绩。第一天上午通读题目提取所有要求下午到晚上做数据清洗和探索性分析这一步至少占掉整整一天。第二天上午搭基准模型把最简单的线性回归或决策树跑通下午再升级到随机森林、梯度提升树这类集成模型如果题目明确要求做多步预测再考虑时间序列模型。第三天上午做优化方案比如库存分配、补货计划下午写论文、画图、校核结果晚上留出两三个小时做敏感性分析和常见错误排查。我自己的习惯是每天晚上结束前三个人必须对着同一张结果表做一次“能不能自圆其说”的检查。模型参数可以后续调但逻辑线不能断。数据、代码、论文三方要对齐否则评委翻到附录时模型图和结果表对不上这是最致命的。2. 数据预处理赛题一的分水岭2.1 数据探查阶段该看什么拿到数据的第一件事不是建模而是先搞清楚你到底拿到了什么。用Python打开数据后我会先看四个东西每个字段的类型、缺失比例、取值范围、时间跨度。import pandas as pd import numpy as np df pd.read_csv(sales_data.csv, encodingutf-8-sig) print(df.info()) print(df.describe(includeall).T) print(缺失值统计) print(df.isnull().sum())这段代码看起来很基础但它的作用是把“我以为的数据”变成“真实的数据”。比如日期字段经常被读成字符串销量字段可能出现负数门店编号可能存在格式不统一这些都要靠df.info()先暴露出来。我在一次模拟题里就栽过某门店的销量在连续三天内全是0看起来像正常歇业后来才发现是数据导出时把缺失写成了0导致整个模型对这些门店的预测系统性偏低。所以建议在探查阶段直接写一个简单的数据审核清单数值列是否都在合理区间、类别列的取值数量是否和业务逻辑一致、时间索引是否连续。这个清单不花时间但能避免后面所有步骤建立在脏数据上。2.2 缺失值与异常值别用同一种方法处理所有字段赛题一的缺失值处理最怕的就是一行df df.dropna()删掉所有含空值的行。很多赛题数据是故意留坑的比如部分商品在部分门店没有销售记录真实原因可能是确实没货也可能是卖完了但没录入这两种情况对模型的意义完全不同。我的经验是分字段处理销量、库存这类数值字段如果缺失比例低于5%用同门店同商品过去7天的均值填充如果缺失比例较高就单独加一个“是否缺失”的特征列让模型自己去学缺失背后的信息。日期字段缺失则直接删除该行因为时间序列的索引一旦断裂后续构造滞后特征时会出现错位。异常值方面我建议用分位数而不是标准差来判断。销售数据常常是长尾分布用3倍标准差会把正常的大促峰值误判成异常值。我会用箱线图看一下再把小于0或大于99.9%分位数的值单独摘出来检查确认是数据错误还是真实业务波动。2.3 特征工程滞后特征和滚动统计量是赛题一的核心赛题一的数据通常按“门店-商品-日期”展开直接拿原始销量喂给模型效果很差因为模型看不到“昨天卖了多少”这类关键信息。所以特征工程里滞后特征和滚动统计量是重中之重。df df.sort_values([store_id, product_id, date]).reset_index(dropTrue) df[lag_1] df.groupby([store_id, product_id])[sales].shift(1) df[lag_7] df.groupby([store_id, product_id])[sales].shift(7) df[rolling_mean_7] df.groupby([store_id, product_id])[sales].transform( lambda x: x.rolling(7, min_periods1).mean().shift(1) ) df[rolling_std_7] df.groupby([store_id, product_id])[sales].transform( lambda x: x.rolling(7, min_periods1).std().shift(1) )这里的shift(1)是为了避免数据泄漏——预测当天的特征不能包含当天的信息只能使用前一天及之前的数据。很多新手直接用rolling(7).mean()结果模型在验证集上表现特别好一到了未来真实预测就崩盘就是因为特征里混入了目标变量自身。如果题目里给了节假日或促销标记也一定要转换成数值特征。直接把“节假日”变成0和1很多时候不够我习惯再加一个“节假日前一天”“节假日结束后一天”的标记因为销量往往在节前就开始爬升。这个细节在赛题一里经常能提升2到3个百分点。3. 建模思路从简单到复杂每一步都要有说法3.1 先跑通一个基准模型再谈优化我不止一次在赛后看到队伍后悔一上来就训练了一个结构复杂的深度学习模型跑了大半天最后效果反而不如普通线性回归。赛题一的评分看重的是逻辑完整性和结果可解释性而不是模型复杂度。所以我的建议是先用多元线性回归或者决策树做一个基准模型把训练集和验证集的误差指标算出来。这样做有三个目的第一确认数据管道没有错误第二给后续复杂模型提供一个对照基线第三如果基准模型效果已经很好说明题目数据规律比较明显没必要硬上复杂模型。from sklearn.model_selection import train_test_split from sklearn.linear_model import LinearRegression from sklearn.metrics import mean_absolute_error, mean_squared_error feature_cols [lag_1, lag_7, rolling_mean_7, rolling_std_7, is_holiday] X df[feature_cols].fillna(0) y df[sales] # 按时间顺序切分而不是随机切分 train_idx df[date] 2023-01-01 val_idx df[date] 2023-01-01 model_lr LinearRegression() model_lr.fit(X[train_idx], y[train_idx]) pred_val model_lr.predict(X[val_idx]) print(MAE:, mean_absolute_error(y[val_idx], pred_val)) print(RMSE:, mean_squared_error(y[val_idx], pred_val, squaredFalse))注意到我没有使用train_test_split的默认随机切分而是按日期切分。赛题给你的是时间序列数据随机切分会把未来信息混进训练集验证结果会虚高。这一点请务必刻在脑子里。3.2 为什么随机森林和梯度提升树更适合赛题一如果基准模型误差不够理想我会在第二天上午换成随机森林或者梯度提升树。这两类模型可以捕捉销量与特征之间的非线性关系也几乎不需要对特征做标准化处理类别特征也更方便。from sklearn.ensemble import RandomForestRegressor model_rf RandomForestRegressor(n_estimators200, max_depth12, random_state42) model_rf.fit(X[train_idx], y[train_idx]) importance pd.Series(model_rf.feature_importances_, indexfeature_cols).sort_values() print(importance)随机森林的feature_importances_会直接告诉你哪些特征对销量影响最大。这一步在写论文时非常有用你可以把特征重要性图放到附录评委看到的是你“理解了解题过程”而不是单纯丢出一个模型结果。一般赛题一的规律是滞后1天销量、滞后7天销量、7天滚动均值的特征重要性排在前三这时候你在论文里写“近期销售惯性是影响销量的主要因素”就有了数据支撑。如果还想更进一步可以试试梯度提升树。实测下来梯度提升树在小样本、带缺失的数据上通常比随机森林稍好但调参时间更长。赛题一的三天赛程里我更推荐把时间花在特征工程和结果分析上而不是反复调learning_rate和n_estimators。3.3 库存分配优化把预测结果变成可执行的方案很多赛题一除了预测还要求你给出下一阶段的库存分配或补货计划。这时候就需要把预测结果代入一个规划模型里。以库存分配为例目标函数通常是在总补货量有限的条件下最小化总缺货成本与库存持有成本。我习惯用scipy.optimize.linprog因为它写起来直观解释起来也很方便。from scipy.optimize import linprog # 假设有 n 个商品每个商品的单位缺货成本为 c_shortage # 单位持有成本为 c_hold预测销量为 pred_sales当前库存为 stock n len(pred_sales) c_shortage 8.0 c_hold 1.5 # 决策变量每个商品的补货量x 0 # 目标min sum(c_shortage * max(pred - stock - x, 0) c_hold * (stock x - pred)) # 线性化后等价于引入松弛变量但更简单的近似是直接按预测比例分配总补货量 total_replenish 20000 allocated pred_sales / pred_sales.sum() * total_replenish # 如果想用严格线性规划可写成 # c [c_hold] * n # 简化版本 # A_ub 和 b_ub 按约束条件构造如果你的队伍对线性规划不熟我给一个保守的兜底方案按预测销量的占比分配总补货量再根据库存上下限做截断。这虽然不是最优解但在论文里配上清晰的假设和讨论至少能保证不丢逻辑分。想拿高分再写严格的线性规划把约束条件、变量含义都列清楚。无论用哪种方案都要保证代码里输出的表格包含三列商品编号、预测销量、建议补货量。评委和后续分析都能直观看到你的方案是怎么从预测结果过渡到决策结果的。4. 源代码实现细节每一步都对应得分点4.1 工程化地组织代码别把所有内容堆在一个文件里赛题一要提交源代码评委不一定会逐行运行但代码结构和可读性本身就是隐性得分点。我见过太多队伍交上来一个几千行的.py文件中间穿插几十个魔法数字没有任何注释这种代码即使结果正确也很难让评委在评审时产生好感。我推荐至少分成三个文件data_preprocess.py管数据读取和清洗model_train.py管训练和预测optimize_plan.py管库存分配。每个文件里用if __name__ __main__:做入口函数只做一件事。调试的时候直接在终端里单步跑三个模块互不干扰。代码注释不是写给评委看的是写给三天后通宵改论文的你自己看的。你一定会忘记当时为什么写某个逻辑注释写清楚“为什么”而不是“是什么”后面能省下大把时间。4.2 训练与预测代码的完整结构下面是我常用的训练和预测代码骨架你可以直接复制后按赛题数据调整列名。整体流程是读数据、构造特征、切分、训练、输出预测表。import pandas as pd import numpy as np from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error def build_features(df): df df.sort_values([store_id, product_id, date]).reset_index(dropTrue) df[lag_1] df.groupby([store_id, product_id])[sales].shift(1) df[lag_7] df.groupby([store_id, product_id])[sales].shift(7) df[rolling_mean_7] df.groupby([store_id, product_id])[sales].transform( lambda x: x.rolling(7, min_periods1).mean().shift(1) ) df[rolling_std_7] df.groupby([store_id, product_id])[sales].transform( lambda x: x.rolling(7, min_periods1).std().shift(1) ) df[is_holiday] df[is_holiday].astype(int) return df df_raw pd.read_csv(sales_data.csv, encodingutf-8-sig) df build_features(df_raw) feature_cols [lag_1, lag_7, rolling_mean_7, rolling_std_7, is_holiday] X df[feature_cols].fillna(0) y df[sales] cutoff_date 2023-01-01 train_mask df[date] cutoff_date val_mask df[date] cutoff_date model RandomForestRegressor(n_estimators300, max_depth14, random_state42) model.fit(X[train_mask], y[train_mask]) y_pred_train model.predict(X[train_mask]) y_pred_val model.predict(X[val_mask]) print(Train MAE:, mean_absolute_error(y[train_mask], y_pred_train)) print(Val MAE:, mean_absolute_error(y[val_mask], y_pred_val)) print(Val RMSE:, mean_squared_error(y[val_mask], y_pred_val, squaredFalse))这段代码里有一个小细节值得多说一句fillna(0)会在每个门店每个商品的前几天因为滞后量不存在而出现0值。严格来说这些样本在训练时应该被剔除或单独标记否则模型会学到“没有历史数据时销量是0”的伪规律。我倾向的做法是将这些样本的滞后特征保留为缺失让模型自行处理缺失路径而不是全部填0。在随机森林里可以直接填-999这种脱离正常取值范围的标记值效果会比填0更好。这在论文里可以写成“本研究对早期样本采用独立缺失编码处理”。4.3 结果输出不仅要预测值还要预测区间赛题一通常更看重预测精度但如果你能把预测区间也做出来论文会有明显加分。随机森林本身可以通过多棵树的预测值分布来构造区间不需要额外写复杂的贝叶斯模型。def predict_with_interval(model, X): preds np.array([tree.predict(X) for tree in model.estimators_]) lower np.percentile(preds, 5, axis0) upper np.percentile(preds, 95, axis0) mean preds.mean(axis0) return mean, lower, upper mean_pred, lower_pred, upper_pred predict_with_interval(model, X[val_mask])预测区间的价值在库存分配场景中非常明显你可以按上界做补货保证现货率按下界做保守补货减少库存积压。论文里补一句“按90%预测区间上界制定补货计划可将缺货率控制在5%以内”这比单纯展示一个预测均值有说服力得多。4.4 可视化输出图比表格更吃香赛题评审时评委看论文的时间很短图表是第一眼的重点。我个人的经验是预测值与真实值的对比曲线必须画特征重要性图必须画库存分配方案的柱状图或饼图必须画。三张图就能撑起结果分析章节的核心内容。import matplotlib.pyplot as plt import matplotlib matplotlib.rcParams[font.sans-serif] [SimHei] matplotlib.rcParams[axes.unicode_minus] False val_dates df.loc[val_mask, date] plt.figure(figsize(12, 5)) plt.plot(val_dates, y[val_mask], label实际销量, linewidth1) plt.plot(val_dates, mean_pred, label预测销量, linewidth1) plt.fill_between(val_dates, lower_pred, upper_pred, alpha0.2, label90%预测区间) plt.legend() plt.title(验证集销量预测对比) plt.xlabel(日期) plt.ylabel(销量) plt.xticks(rotation45) plt.tight_layout() plt.savefig(forecast_compare.png, dpi150)注意中文字体问题Windows下的SimHei换到其他机器可能不可用。提交代码时最好用英文标签或写明字体设置说明否则评委或队友复现时容易报错。这个小问题经常在答辩现场出洋相。5. 结果分析、模型检验与论文配合5.1 用多维度误差指标把模型“说清楚”论文结果部分不能只写一个MAE就完事。我建议至少报告四个指标MAE、RMSE、MAPE、R方。每个指标反映的信息不一样MAE直观程度高RMSE会放大较大误差的惩罚MAPE适合比较不同量级商品的预测效果R方反映模型相对均值基准的解释力度。from sklearn.metrics import r2_score mae mean_absolute_error(y[val_mask], mean_pred) rmse mean_squared_error(y[val_mask], mean_pred, squaredFalse) mape np.mean(np.abs((y[val_mask] - mean_pred) / y[val_mask])) * 100 r2 r2_score(y[val_mask], mean_pred) print(fMAE: {mae:.2f}, RMSE: {rmse:.2f}, MAPE: {mape:.2f}%, R2: {r2:.3f})如果MAPE偏高别慌先看看是不是某些商品销量基数太小导致百分比被拉大。比如一件商品预测值是2实际值是5误差3绝对值看着不大但MAPE高达150%。这种情况下我会按销量区间分组再算一次误差论文里专门讨论“低销量商品的相对误差偏大建议以MAE为主要评价指标”这不仅解释了误差来源也体现了分析的严谨性。5.2 敏感性分析让评委看到你的方案经得起推敲赛题一的论文比较加分的一个环节是敏感性分析。比如你做了库存优化可以问如果总补货量减少10%、增加10%缺货成本和库存持有成本会怎么变化如果你用了随机森林可以问如果去掉滞后7天特征模型误差会升高多少写一个小循环就能完成。for total in [18000, 20000, 22000]: allocated_temp pred_sales / pred_sales.sum() * total shortage_cost np.maximum(pred_sales - stock - allocated_temp, 0).sum() * 8 hold_cost np.maximum(stock allocated_temp - pred_sales, 0).sum() * 1.5 print(f总补货量 {total}: 缺货成本 {shortage_cost:.2f}, 持有成本 {hold_cost:.2f})这种简单的敏感性分析说明你的方案不是拍脑袋定出来的而是能应对不同业务条件的。我见过有队伍把这项放在正文最后直接拉高了整体印象分。5.3 摘要和图表把最能打的结果放在最前面摘要是一篇数模论文最重要的一段没有之一。赛题一的摘要要在半页以内写清楚研究了什么问题、用了什么数据、建立了什么模型、得到什么核心结果、误差是多少。很多队伍把摘要写成“本文研究了……”结果全是对问题复述没有数字这就浪费了最宝贵的位置。我自己的写法是第一句点明问题背景和任务第二句概括数据规模与处理思路第三句写出主体模型及核心参数范围第四句放核心误差指标和优化结果第五句点明灵敏度分析的结论。全程不允许出现“初步探讨”“有所提升”这类模糊表述要说就是“MAE从41.3降到35.8”“总成本降低12%”。绘图上还有个小建议所有坐标轴标签必须写清楚单位和含义曲线要区分颜色和线型附录里的代码截图完全没有必要但核心代码片段放在附录并配有注释是评委快速理解你方案的重要途径。6. 常见问题与排查技巧实录6.1 时间序列的切分陷阱赛题一最常见的坑就是我前面提到的用随机切分验证模型。有位队员曾经拿着一个验证MAE只有个位数的模型兴冲冲跑来找我结果一看代码用的是train_test_split默认参数测试集里包含了训练集日期的前后数据模型相当于开卷考试。改成时间顺序切分之后MAE翻了快一倍这才是真实水平。切分时必须先对日期排序而且要按整段连续时间切不能把某个门店的某几天单独抽出来当测试集因为滞后特征会让这些测试样本看到测试期之后的信息。6.2 滞后特征导致的数据泄漏构造lag_1后如果训练集和验证集的切分边界没有处理好第1天的验证样本会用到前一天训练集里的真实值这本身没问题。问题出在“预测未来一个时间段”的场景假设你要预测1月1日到1月7日的销量而这7天是连续的那么1月2日的lag_1应该用1月1日的预测值而不是真实值。很多队伍在这里直接套用训练阶段构造滞后特征的方式导致提交的预测表每个点都“看到”了前一个点的真实值成绩自然不对。处理办法是滚动预测第一天用真实值生成预测后面每一天用前一天的预测值更新滞后特征。虽然误差会累积但这是符合题目设定的合理做法。如果题目允许也可以在模型中直接预测整段时间的平均销量避免逐日累积误差。这个选择要根据赛题描述来定但务必要在论文里写清楚。6.3 优化模型跑不出结果或结果明显不合理用linprog时最常见的问题是约束写反比如把x 0写成x 0或者A_ub的符号反了。赛前可以先用一个两个商品的小例子手算一遍确认目标值和手算一致后再套真实数据。如果优化结果出现负补货量检查变量边界如果提示infeasible把约束逐步注释掉看看是哪个约束导致无解。另一个经常被忽略的问题是单位不统一预测销量是按日算的库存成本是按周算的两个数字放在同一个目标函数里解出来的方案必然畸形。动手优化前把所有成本、销量、库存的单位换算成同一个时间口径这一步看起来简单但能省掉你两小时的排查时间。6.4 画图时中文乱码和坐标轴重叠提交的代码换到别的电脑上跑中文标签经常会变成方块。最稳妥的方法是在画图代码开头动态选择可用字体或者干脆把图表标签写成英文。坐标轴日期重叠的问题更常见需要在plt.xticks里设置间隔plt.xticks(ticksval_dates[::7], labelsval_dates[::7], rotation45)每隔7天显示一个日期图面会清爽很多。此类“小问题”在评审时不会给你加分但连续出现三四个会给评委留下代码质量不高的印象进而怀疑你结果的可信度。如果条件允许赛前准备好一个统一风格的绘图模板函数所有图表都用它生成论文排版会整齐很多。我在几次模拟题里踩过这些坑之后已经习惯把绘图参数集中到一个配置区域改起图来不用满文件找散落的rcParams。最后分享一个我保留到现在的习惯每次跑完模型都把关键预测结果落成CSV命名为prediction_版本号.csv绝不覆盖旧文件。三天赛程里你一定会反复调整特征和参数没有版本管理你会在最后一天完全不知道当前最优结果是哪个版本跑出来的。这个习惯用在数模赛题一上比任何工具都管用。
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。