小超市供销存优化:从销量预测到库存决策的实战指南
简介一份针对2017年BDCI“小超市供销存管理优化”赛题的完整Python源代码包主要面向数据分析与机器学习初学者、竞赛参与者以及需要供应链/库存预测方案的开发者。代码围绕小超市销售与库存数据演示从特征提取、数据清洗、模型选择、训练评估到预测优化的完整流程并提供模型解释与迭代思路。压缩包共20个文件、仅1.22MB组织紧凑11个csv文件存放原始数据、预测结果及中间数据便于对照实验5个py脚本涵盖model1、model2、model3三种模型实现以及输出和预测模块可直接运行复现赛题方案2个docx分别为项目报告和下载者使用说明1个pptx为项目汇报另有txt运行顺序说明结构清晰。目前已有309人学习下载。通过该资源可以拿到一套可复现的BDCI赛题代码理解不同机器学习模型在小超市供销存场景中的建模细节并迁移到类似业务预测任务中。1. 2017BDCI 小超市供销存管理优化这个赛题到底在优化什么2017BDCI 小超市供销存管理优化本质上不是让你做一个纯销量预测而是给你一家模拟小超市的销售、采购、库存流水让你在有限成本约束下把“进、销、存”三个环节的决策串起来。它适合正在学机器学习但想碰真实业务约束的人也适合做供应链方向但缺少落地场景的工程师。赛题的价值在于预测只是手段真正决定排名的是一套能解释、能回测、能扛住促销和季节性波动的决策策略。读完这篇你能从原始表一路做到可执行的补货建议。2. 读懂赛题数据供销存三个表怎么关联预测目标怎么定2.1 数据结构与字段销售、采购、库存的核心表先别急着建模把三张表的关系理清楚。常见做法是拿到三个 CSV销售表一般包含销售日期、商品编码、销售数量、销售金额采购表包含采购日期、商品编码、采购数量、采购单价库存表包含库存日期、商品编码、库存数量。也许还有商品信息表包含商品类别、进价、售价。我一般先用 pandas 读进来做一下字段类型检查和空值统计。以下代码是第一步import pandas as pd sales pd.read_csv(sales.csv, parse_dates[sale_date]) purchase pd.read_csv(purchase.csv, parse_dates[purchase_date]) inventory pd.read_csv(inventory.csv, parse_dates[inv_date]) print(sales.info()) print(purchase.info()) print(inventory.info()) print(sales.isnull().sum(), purchase.isnull().sum(), inventory.isnull().sum())逻辑说明parse_dates 把日期列转成 datetime方便后面做时间窗口聚合。先看 info 能发现列名是否对齐、有没有缺失。注意实际赛题文件可能列名不同比如用“销售时间”“商品ID”你需要根据字典做映射。这里我建议不要直接用原列名先统一成英文小写后面写特征工程不折腾。参数说明read_csv 里的 sep 参数默认是逗号如果文件是制表符或分号记得改成 sep\t 或 sep;。另外一个坑是某些字段的值里含逗号比如商品名称带逗号会导致列错位用 quoting1 能缓解。不过赛题数据一般是规范的这个问题主要在你自己爬数据时遇到。接下来要按商品和时间对齐三张表。销售表每天有记录但采购和库存不是每天都有尤其是采购可能几天一次。我常用的方式是把采购表和库存表按日期重采样到每一天再前向填充。def resample_daily(df, date_col, value_col): df df.set_index(date_col) # 按天聚合没有记录的天数不填充 daily df[value_col].resample(D).sum() # 采购数量是累计发生库存数量是时点值这里先统一做 ffill return daily purchase_daily resample_daily(purchase, purchase_date, purchase_qty).ffill() inventory_daily resample_daily(inventory, inv_date, stock_qty).ffill()逻辑说明resample(D).sum() 把同一日的多条记录合并。ffill 表示当天没有采购记录时沿用最近一次已知值这对库存和采购日期这种稀疏事件适用。但要注意时点库存直接用 ffill 会忽略当天销售导致的库存减少需要后续结合销售修正。这里就引入了供销存联动的第一个选择你要建的是“日粒度”特征还是“周粒度”特征。赛题给的预测窗口通常是一周或一个月如果你的特征也是日粒度模型要自己学滞后关系训练慢而且容易过拟合。我一般先做周聚合把销售数量、采购数量、库存均值按周合并预测下一周的总销量。这样可以明显降低噪声也让决策输出更贴近实际补货频率。小超市通常每周订一到两次货按周预测更合理。2.2 目标函数与评价指标不只是预测销量赛题的评价指标决定了你怎么设计标签。很多新手上来就套 MAE 或 RMSE但供销存优化里缺货损失和库存积压成本不对等。如果赛题要求你输出补货量后台会用模拟器跑一段时间统计缺货次数、库存周转、利润率等综合得分。这时候你如果只优化销量预测误差很可能在得分上翻车。常见做法是把问题拆成两段先预测未来 n 天的销量分布再基于分布做库存决策。如果你只能拿到评分脚本最好在本地做一个模拟器用历史数据回测你的决策策略。模拟器不一定复杂核心是把每天的销量、期初库存、采购到货量按顺序跑一遍计算期末库存和缺货量。下面给一个简化版的回测函数def simulate(initial_stock, sales_series, order_plan, lead_time1): stock initial_stock shortage 0 total_order 0 for t in range(len(sales_series)): # 到货 if t lead_time: stock order_plan.get(t - lead_time, 0) total_order order_plan.get(t - lead_time, 0) # 销售 sale sales_series[t] if stock sale: stock - sale else: shortage sale - stock stock 0 return shortage, total_order参数说明lead_time 是从下单到到货的天数小超市可能是一天大超市可能是三天。order_plan 是一个字典key 是下单日value 是下单量。这个函数没有考虑保质期和退货但作为基线够了。你可以在本地用它对比不同策略得出一个保守的缺货惩罚权重。评价指标公式虽然在赛题说明里会有但我建议你自己定义业务指标缺货率 缺货天数 / 总销售天数库存周转率 总销售成本 / 平均库存成本。这两个指标互相矛盾你需要画 Pareto 图找到平衡点。很多获奖方案不是预测最准的而是在缺货率和周转率之间找到了合理的折中。2.3 把赛题转成监督学习样本构建与标签设计之所以这一节单独讲是因为我见过太多人直接拿“商品-日期”当样本预测当天销量然后发现模型精度上不去。问题在于日期特征太稀疏尤其是小超市单个商品可能经常连续几天销量为 0。正确的做法是构建一个周期性样本用过去 4 周的窗口特征预测未来 1 周的总销量。def build_samples(sales, product_list, start_date, end_date): records [] for pid in product_list: for d in pd.date_range(start_date, end_date, freqW-MON): # 历史窗口d 往前推 4 周 hist sales[(sales.product_id pid) (sales.sale_date d) (sales.sale_date d - pd.Timedelta(weeks4))] # 未来窗口d 到 d6 天 future sales[(sales.product_id pid) (sales.sale_date d) (sales.sale_date d pd.Timedelta(weeks1))] if len(future) 0: records.append({ product_id: pid, week_start: d, hist_sales: hist.sales_qty.sum(), target: future.sales_qty.sum() }) return pd.DataFrame(records)逻辑说明用 freqW-MON 生成每周一作为周起始日。历史窗口固定为 4 周目标为未来 7 天销量。注意条件 future 样本只要未来至少有一天有记录就把它纳入。这个设计把问题从“预测逐日销量”变成“预测周销量”噪声更小也符合补货决策需求。这里有一个数据泄漏的隐患历史窗口应严格小于目标窗口的起始日代码里用了 d不会把当周数据包含进去这个写对了。如果你不小心写成 d那么未来销量的一部分被当成了历史特征回测分数虚高真实场景彻底翻车。后面第 5 章我会专门展开。参数说明窗口长度 4 周不是绝对的你可以用 6 周或者 8 周。小超市受节假日影响大如果用 4 周碰到春节或国庆历史均值可能被拉高。我一般会同时加一个“去年同期窗口”特征因为年度季节性在小超市更明显。但赛题可能只给了 1 年数据这时只能退而求其次用最近 8 周加权平均。3. 用 Python 搭建供销存优化基线特征工程与模型选型3.1 特征工程时间、商品、价格、节假日怎么组合销量预测的特征工程优先级时间窗口特征 价格和促销特征 商品静态特征 外部节假日特征。很多人一上来就堆了上百个特征最后 LightGBM 训出来效果一般因为特征之间多重共线性太强而且小超市数据量不大容易过拟合。我先给一个最低可行的特征集合。时间特征包括周数、月份、星期几、是否为月初可能影响发工资后的消费。商品特征包括商品类别、进价、售价、毛利率。历史特征包括过去 1/2/4 周销量、过去 4 周销量均值、过去 4 周销量标准差、过去 4 周最大销量、过去 4 周零销售天数。价格特征包括当前售价比前一周售价的变化量、是否降价。下面代码演示如何生成这些特征def add_features(df, sales, price_history): # 时间特征 df[week_of_year] df[week_start].dt.isocalendar().week df[month] df[week_start].dt.month df[is_month_start] df[week_start].dt.day 4 # 历史销量特征 df[hist_mean] df.groupby(product_id)[hist_sales].transform(mean) df[hist_std] df.groupby(product_id)[hist_sales].transform(std) df[hist_max] df.groupby(product_id)[hist_sales].transform(max) df[zero_days] df[hist_sales].apply(lambda x: (x 0).sum()) # 价格变化 df df.merge(price_history, on[product_id, week_start], howleft) df[price_change] df[current_price] - df[prev_price] return df逻辑说明hist_sales 在上一节构建样本时是 4 周销量总和这里不能再做 groupby因为它已经是聚合结果。实际上 hist_sales 是一个具体的值你需要保留原始每日销售再进行窗口统计。示例代码里 transform(mean) 是按商品颗粒度求均值意义不大。更合理的做法是在构建样本时就用每天记录算出多个窗口特征。为了避免误导我把上面的代码当做一个演示结构真正实现时要为每个商品计算滑动窗口。我建议用合并的方式避免循环。先把销售表按周聚合再对每个商品做 shift。核心逻辑是weekly_sales sales.groupby([product_id, week_start])[sales_qty].sum().reset_index() weekly_sales weekly_sales.sort_values([product_id, week_start]) weekly_sales[prev_1w] weekly_sales.groupby(product_id)[sales_qty].shift(1) weekly_sales[prev_2w] weekly_sales.groupby(product_id)[sales_qty].shift(2) weekly_sales[prev_4w_mean] ( weekly_sales.groupby(product_id)[sales_qty] .transform(lambda x: x.rolling(4, min_periods1).mean().shift(1)) )参数说明shift(1) 表示取上一周的销量shift 不会引入未来数据因为它是按行顺序取前一行。rolling(4).mean().shift(1) 表示过去 4 周不包括本周的均值min_periods1 让数据不足 4 周时也给出部分均值避免产生大量 NaN。如果你直接用 rolling 而不 shift会把当周销量纳入均值导致标签泄漏。节假日特征在赛题里比较微妙。有些赛题明确禁止使用节假日信息因为测试集可能覆盖到没有节假日的区间即便不禁止也需要小心节假日标签如果是从未来日期表里查出来的本身就是合法的外部特征但如果你的特征里包含节假日导致的销量脉冲模型会把“节假日”和“销量激增”强关联导致节后预测过高。我通常只加入一个“是否节假日”的布尔特征不加入“距离节假日还有几天”这种连续特征除非你能确定模型有足够数据学习。3.2 模型选型从线性回归到 LightGBM 的取舍小超市数据量不大往往只有几十个商品、半年的日记录。这种情况下深度模型容易过拟合线性回归和树模型更稳。我推荐按这个顺序尝试岭回归双对数变换后→ 梯度提升树 → LightGBM。为什么不是随机森林优先随机森林对连续特征的边界处理不如 GBDT而且销量分布长尾RF 在极端值上表现差。具体到 2017 年的赛题环境当时最常用的是 LightGBM。它有三个理由适合这里一、支持 categorical 特征商品编码可以不用 one-hot直接用 category 类型二、叶节点分裂对缺失值有内部处理历史特征缺失不用你填三、训练快可以快速试特征组合。如果用 sklearn 的 GradientBoostingRegressor在小数据上也完全够用但调参时间更长。我建议把模型封装成一个函数方便对比。下面是一段 LightGBM 训练代码import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit def train_lgb(df, features, targettarget): # data df.dropna(subset[target]) # 按 week_start 排序保证切分不泄漏 data df.sort_values(week_start).reset_index(dropTrue) X data[features] y data[target] tscv TimeSeriesSplit(n_splits4) for fold, (tr_idx, va_idx) in enumerate(tscv.split(X)): X_tr, X_va X.iloc[tr_idx], X.iloc[va_idx] y_tr, y_va y.iloc[tr_idx], y.iloc[va_idx] model lgb.LGBMRegressor( objectiveregression, n_estimators200, learning_rate0.05, num_leaves31, random_state42, n_jobs-1 ) model.fit(X_tr, y_tr, eval_set[(X_va, y_va)], eval_metricl1, callbacks[lgb.early_stopping(20), lgb.log_evaluation(0)]) return model, tscv逻辑说明TimeSeriesSplit 是 sklearn 自带的时间序列交叉验证它按照顺序切训练集和验证集不会打乱数据。相比随机 KFold它避免了未来样本进入训练集的问题。early_stopping 用验证集上的 l1 误差决定最佳迭代轮数。注意这里 final model 是在最后一个 fold 上训练的想要更稳可以保留每个 fold 的模型做集成。参数说明num_leaves31 是 LightGBM 默认值在小数据上可以降到 15 防止过拟合。learning_rate0.05 配合 200 棵树足够如果数据量增加可以调整到 0.01 并增加 n_estimators。model.fit 里 eval_set 只用于早停不会影响最终模型的参数。如果你不想引入 LightGBM 的额外依赖sklearn 的 HistGradientBoostingRegressor 也能给出近似效果。它同样支持 category 特征并且训练快。不过它在 early stopping 上的接口没有 LightGBM 灵活我个人还是倾向用 LightGBM。问为什么不用 XGBoost在小数据上两者精度差别不大但 LightGBM 处理类别特征更方便代码更简洁。3.3 基线代码框架训练、验证、预测一条龙现在我把前面的步骤串成一条最小可运行流程。这个流程的输入是 sales、purchase、inventory 三张表输出是未来一周每个商品的预测销量和补货建议。我先给一个可运行脚本的骨架然后讲哪些地方要根据数据调整。def main(): # 1. 读取与预处理 sales, purchase, inventory load_data() # 2. 构建周样本 df build_samples(sales, sales.product_id.unique(), start_date2017-07-01, end_date2017-12-31) # 3. 特征工程 df add_features(df, sales) # 4. 训练与交叉验证 features [week_of_year, month, is_month_start, prev_1w, prev_2w, prev_4w_mean] model, cv train_lgb(df, features) # 5. 预测未来一周 future_weeks pd.date_range(2018-01-01, periods4, freqW-MON) future_df build_future_samples(sales, product_list, future_weeks) future_df add_features(future_df, sales) future_df[pred] model.predict(future_df[features]) return future_df[[product_id, week_start, pred]]逻辑说明这个骨架把数据处理和模型解耦。其中 build_future_samples 和 build_samples 的区别是未来样本没有 target只有历史特征。你需要保证特征列名完全一致否则 predict 会报错。参数说明start_date 和 end_date 要根据数据范围调整。训练集的截止时间必须早于你想预测的期间否则时间穿越。我这里写 2017 年是一个示例实际赛题数据可能更早你需要根据给定的训练集决定。运行之后第一件事不是看预测精度而是看预测值分布。小超市销量经常是多数商品销量低、少数商品销量高直接用回归模型容易预测出负数。处理方式对 target 做 log1p 变换训练时用 log1p(target)预测后 expm1 还原。下面是改法model.fit(X_tr, np.log1p(y_tr), eval_set[(X_va, np.log1p(y_va))], eval_metricl1) pred np.expm1(model.predict(X))说明log1p 对零值友好避免 log0。预测结果还原后不会是负数。但注意如果你的真实销量是离散的 0、1、2 这种预测值 25.7 本身无法直接使用需要结合库存策略做取整。取整不是四舍五入而是向上取整因为缺货损失通常大于过量库存成本。这个问题我下一章讲。4. 库存与采购策略联动预测出来之后怎么决策4.1 安全库存与订货点参数怎么定预测出了未来一周销量不等于你要把预测值直接作为采购量。真实场景里有供应商配送周期、有需求波动、有库存资金成本所以需要安全库存作为缓冲。安全库存常用公式是 Z × σ_d × sqrt(L)其中 Z 是服务水平对应的标准正态分位数σ_d 是日销量的标准差L 是补货提前期的天数。这个公式来自库存管理经典模型在小超市场景下可以简化。一般我会先算每个商品的周销量均值和标准差然后定义服务水平。小超市服务水平取 0.90 到 0.95 比较合理太高0.99会导致大量积压太低0.85又会频繁缺货。下面代码给出参数计算def safety_stock(mean_daily, std_daily, lead_time_days, service_level0.95): from scipy.stats import norm z norm.ppf(service_level) return z * std_daily * np.sqrt(lead_time_days)逻辑说明如果输入的是周销量需要先除以 7 得到日均。公式假设需求独立且服从正态分布小超市未必满足但作为基线够用。如果实际数据偏态明显可以改用分位数法取过去 4 周销量的 90% 分位数减去均值作为安全库存。参数说明lead_time_days 在采购表中可以通过“下单日期”和“到货日期”之差计算。如果数据里没有两个日期只能用一个小常数比如 1 天或 3 天。这里不要拍脑袋最好统计采购日期的间隔取中位数。如果采购表只有采购日期没有到货日期那就没法直接算只能用固定值并说明假设。订单点的定义是当库存下降到某个阈值时触发补货。阈值 提前期内的预期需求 安全库存。代码逻辑reorder_point mean_daily * lead_time_days safety_stock这句话非常关键。如果你直接用预测销量当订货点会忽略提前期内的消耗。比如提前期 3 天日销 10 件安全库存 5 件订货点就是 35 件。库存还剩 20 件时看起来还有货但等 3 天后到货时已经缺货了。这就是为什么很多预测模型好用但补货决策依然翻车的原因。4.2 补货计划生成让预测结果变成可执行的采购单有了订货点我们还要定期生成补货计划。小超市常用周期性补货比如每周一盘点一次所以这里采用“定期补货”而非连续补货。流程是每周初查看当前库存如果库存低于订货点则下单采购一个固定数量目标是让补货后的库存达到目标库存水平。目标库存水平 订货周期内需求 提前期内需求 安全库存。用公式表达就是 (T L) × 日均销量 安全库存。下面代码生成采购单def generate_order(current_stock, forecast_sales, lead_time_days, review_days7, service_level0.95): # 对预测销量求均值与标准差 mean_daily forecast_sales / review_days std_daily forecast_sales.std() / review_days if hasattr(forecast_sales, std) else mean_daily * 0.3 ss safety_stock(mean_daily, std_daily, lead_time_days, service_level) target_level (review_days lead_time_days) * mean_daily ss order_qty max(0, target_level - current_stock) return order_qty逻辑说明target_level 是补货后的期望库存order_qty 是差额。如果 current_stock 已经高于 target_level则不补货。这里用 forecast_sales 除以 review_days 得到日均需求。如果 forecast_sales 是一个单独数字没有 std 属性就手动假设一个变异系数 0.3这是经验值你可以改成从历史数据里算出的真实变异系数。参数说明review_days7 表示每周补一次。如果门店要求最低起订量还需要加一个判断当 order_qty 0 且低于起订量时直接按起订量下单。这个在代码里加一行就行但很多人忘掉导致生成一堆小订单物流成本高。补货计划直接输出的是一张采购单至少包含商品编码、建议采购数量、预计到货日期、当前库存、预测销量。我一般还会加一个“紧急程度”字段用预测销量与当前库存的比值分档方便采购员优先处理。4.3 模拟回测用历史数据验证决策效果预测能不能用必须回到模拟器里验证。前面第 2 章给了一个简化 simulate 函数这节我把它扩展成带周期性补货的回测流程用来对比不同策略。def backtest(df_sales, initial_stock, strategy, lead_time1, review_days7): stock initial_stock.copy() records [] for d in pd.date_range(df_sales.sale_date.min(), df_sales.sale_date.max(), freqD): # 每天统计销量 date d.date() if d.weekday() % review_days 0: order strategy(d, stock) # 记录下单在 lead_time 天后到货 # 处理到货、扣减库存、记录缺货 # 省略具体实现 return pd.DataFrame(records)逻辑说明这个骨架只是为了演示回测的结构完整实现需要考虑一次订多日、提前期错位、节假日停运。你可以在本地把日粒度跑通然后对比两个策略A 策略用预测销量作为补货量B 策略用目标库存水平补货。结果通常 B 策略在缺货率上更优因为 B 策略考虑了提前期和安全库存。回测指标建议用三个缺货率缺货天数 / 总天数、平均库存成本平均库存量 × 商品成本、总采购额。你需要在三者之间画折衷曲线。不要只用“预测误差”评价策略这样会错过很多业务细节。5. 供销存优化常见问题与避坑指南5 个踩坑记录这一章把我在模拟项目 X 和类似赛题里反复踩过的 5 个坑写出来每条按现象、原因、解决来讲。希望你能跳过这些坑。5.1 数据泄漏用了未来信息而不自知现象交叉验证分数很高但提交到隐藏测试集后分数骤降。检查特征时发现某个特征在时间维度上已经包含了未来的销量信息。原因最常见的来源有两个。一个是构建样本时使用了包含当周的 rolling 窗口rolling(4)默认包含当前行导致特征里混入目标另一个是从全部数据集做标准化时用了未来数据的均值与方差比如用全年的均值去归一化训练集前几个月的样本。解决窗口特征统一使用shift(1)确保特征只依赖过去。标准化或归一化必须在每个时间折折内用训练子集的统计量进行不能全局 fit。检验方法把特征矩阵按时间排序随机选择一行人工确认该行所有特征的时间戳都早于标签时间。5.2 销量为 0 的样本处理别让缺货掩盖真实需求现象某个商品连续 3 天库存为 0销量表里这 3 天就是 0。模型学到的是“库存 0 → 销量 0”看起来很准实际是因为根本没货可卖。原因销售表记录的是实际成交量不是需求量。当库存为 0 时真实需求未被满足表里不会出现。如果直接用这份数据训练模型会把缺货当正常状态预测值系统性偏低。解决在训练集中剔除库存为 0 的日子或者用前几周销量中位数估计潜在需求再或者仅预测有库存时的销量。如果赛题数据里库存表能给你当天的库存量优先用库存 0 的样本。如果拿不到至少将连续零销量样本单独建标签做二分类是否缺货再预测数量。5.3 价格变动与促销的混淆特征时分清因果现象加入了“是否促销”特征后模型在促销期间销量预测明显偏高但实际促销只在某些商品上生效模型却给所有商品都加上了促销增量。原因促销特征与商品特征发生了交互而模型没有足够数据区分“该商品促销”和“全店促销”。小超市促销通常是单商品或相互关联的几款商品不是全店统一。如果特征里只有一个全局促销标志模型会把它当全局效应。解决把所有促销相关特征改造成商品粒度该商品是否降价、降价幅度、是否参与买一送一。同时加入促销前一周的基础销量作为对照。如果数据不够直接删除促销特征让模型从历史价格变化斜率和销量变化里自行学相关性比强行塞一个脏特征更稳。5.4 时间序列交叉验证的切法不能随机 shuffle现象用KFold(shuffleTrue)验证模型误差得到的指标比 TimeSeriesSplit 好很多于是你很高兴地提交结果测试集分数变差。原因随机 KFold 会把未来样本混进训练集模型“偷看”了未来销量趋势验证时占了便宜。真实数据是时序的必须保序切分。解决一律使用TimeSeriesSplit或者手动按日期切分。训练集永远在验证集之前。切分时还要注意“gap”当特征窗口长度为 4 周时训练集最后 4 周的数据会与验证集起始数据窗口重叠可能导致轻微泄漏。最佳实践是在训练集和验证集之间留出至少一个特征窗口长度的间隙比如预测目标周之前 4 周的数据都从训练集剔除。5.5 库存成本参数拍脑袋决策结果的敏感度现象同一套预测模型把安全库存系数从 0.9 改成 0.95缺货率下降 10%但库存成本上升 25%。你觉得 0.95 更好却说不清为什么。原因决策策略里的服务水平直接影响补货量。这个参数不是模型调出来的而是业务权衡。如果不做敏感性分析任何结论都站不住脚。解决跑一组服务水平扫描实验比如 0.85、0.90、0.95、0.99分别记录缺货率和库存成本画出曲线。在曲线上找拐点通常 0.92 或 0.95 附近是性价比最高的区域。这个实验用第 4 章的 backtest 函数就能做只需要循环几个参数值几分钟就能出图。以后呈现方案的时候附上这张曲线比任何预测精度指标都有说服力。6. 把方案落地成可交付代码的进阶技巧模块化配置与结果留痕前面几章的代码能跑通一条基线但交付给业务方或提交给评委时还差两个基本功模块化配置和结果留痕。开发时你可能有十几个参数散布在代码里一旦要复盘根本找不到当时用了哪些参数。我习惯把所有可调参数收敛到一个 yaml 或 json 配置里训练脚本只读取配置不写死参数。举个例子import yaml with open(config.yaml) as f: config yaml.safe_load(f) model_params config[model] data_params config[data] strategy_params config[strategy]配置文件样例model: {learning_rate: 0.05, n_estimators: 200, num_leaves: 31}data: {window_size: 4, lead_time_days: 1, review_days: 7}strategy: {service_level: 0.95, min_order_qty: 0}。这样每次实验只改 config不会误伤代码逻辑。同时训练结束后把 config 和指标输出到同一个目录便于事后追溯。结果留痕是更重要的习惯。每跑一次实验就在指定目录下生成结果文件包括验证集预测值与真实值、测试集预测值、模型参数、特征重要性、回测指标。我用一个简单函数自动保存def run_and_save(config_name, model, pred_df, metrics): output_dir f./runs/{config_name} os.makedirs(output_dir, exist_okTrue) pred_df.to_csv(f{output_dir}/pred.csv, indexFalse) model.save_model(f{output_dir}/model.txt) with open(f{output_dir}/metrics.json, w) as f: json.dump(metrics, f, ensure_asciiFalse)这样你可以对不同 config 做横向对比哪个商品类别预测误差大哪个策略缺货率高一眼就能看出来。我在这类赛题和项目中吃过亏模型调优时只记录分数不记录特征最后想回退到某个效果好的组合只能靠记忆非常痛苦。后来强制自己每次实验都保存全量结果再也没出过这种问题。另一个进阶技巧是模型集成。小数据上单模型容易波动把 LightGBM 和岭回归的结果做加权平均往往能显著降低方差。权重可以用验证集上的误差倒数来确定或者简单一点直接取平均。如果时间紧张我一般先跑一个 LightGBM 和一个岭回归验证集误差接近的话就用 1:1 平均如果 LightGBM 误差明显小就用 0.7:0.3。集成输出后再进入补货策略环节。这套方案最大的收获不是排名而是让我理解了“预测 决策”的闭环。预测模型再准如果决策策略不考虑提前期和安全库存实际收益很难提升。反过来策略再完善预测偏差如果太大安全库存也只能靠高库存成本硬扛。希望这一整套从数据处理、特征工程、模型训练到策略回测的路径能帮你少走弯路把手上的销售数据变成真正能指导进货的采购单。本文还有配套的精品资源点击获取