☰
需求预测与分仓规划实战:从时间序列特征工程到库存优化
2026/10/3 5:13:25 网站建设 项目流程

简介:这是天池菜鸟需求预测与分仓规划比赛第二赛季的参赛作品包,面向物流电商、供应链优化方向的竞赛选手,也适合正在寻找毕设源码的学生。压缩包内共114个文件,以Java源码(24个java)、依赖库(23个jar)和XML工程配置(12个xml)为主,另有SQL数据库脚本、设计报告等,整体约24.09MB,结构清晰便于按模块查阅。目前已有248人浏览学习。资源完整覆盖从数据预处理、特征工程、多模型训练到模型融合与预测输出的流程,包含XGBoost、GBDT、随机森林、SVR、ARIMA等算法的实现,并针对补多/补少成本计算、多仓协同、异常促销日过滤等难点给出具体处理思路;设计报告还介绍滑窗特征、分仓与全国特征构造,以及基于成本的加权融合方法,作者线上成绩约88万,比纯规则方法降低11万,具有较强的实战参考价值。

1. 一份比赛源码包,背后是整条供应链的优化题

天池上的“菜鸟-需求预测与分仓规划”比赛,表面看是给一份赛后开源的项目源码,实际上是一道非常经典的“预测+决策”两段式赛题:先预测浙江省内每个仓库未来几天的需求量,再决定每个 SKU 在各个仓放多少货、补多少货。和单纯刷准确率的比赛不同,这个赛题的落地评测指标是库存成本——预测不准会多压库存,预测准了但分仓不合理同样会多花仓储费,所以最终拿名次的方案一定是一个“预测模型+库存策略”的组合拳。这份源码加数据库加设计报告的三件套,正好对应了数据预处理、模型训练和方案撰写三个完整交付环节,适合想系统接触供应链场景的新手选手,也适合已经把 LightGBM 跑熟、想看看时间序列特征工程怎么和业务目标对齐的进阶玩家。读者只需要带着“从订单到补货决策”这条主线去读,就能把一个比赛项目真正拆开。

2. 从原始订单到模型样本:先把业务数据变成能训练的时间序列

2.1 比赛给出的数据长什么样

菜鸟赛题提供的数据核心是两张表:订单表和商品信息表。订单表记录了历史每一天每个仓库按 SKU 的订单明细,商品表中则是 SKU 对应的品类、价格、重量等静态属性。天池的比赛通常不直接给你一个“按天汇总好了”的销量表,而是要自己从订单行项目里聚合出来。第一次打这种赛题的人最容易犯的错误,就是把订单明细直接拿去喂模型——LightGBM 对行级明细不是不能学,但会学出一堆无意义的噪声,而且训练样本量会爆炸到几千万行,特征工程变得很不现实。正确做法是先做时间粒度和空间粒度的统一:把订单明细按“日期+仓库+SKU”三个维度聚合成需求量,形成一条条干净的时间序列。

这一步在 SQL 里完成最稳妥。订单表一般有字段 order_time、warehouse_id、sku_id、quantity,其中 quantity 是订单行里的件数。注意有些比赛还有一个发货量和退货量,需求预测一般以“下单量”为准,因为补货计划要覆盖的是客户已经产生的需求,而不是实际履约的量。

-- 把订单明细聚合成按天、仓、SKU 的需求量 SELECT DATE(order_time) AS dt, warehouse_id AS wh_id, sku_id, SUM(quantity) AS demand, COUNT(DISTINCT order_id) AS order_cnt, COUNT(*) AS line_cnt FROM order_detail WHERE order_time >= '2020-01-01' GROUP BY DATE(order_time), warehouse_id, sku_id;

这段 SQL 的作用是把行级订单压缩成“一天一个仓一个 SKU 一条记录”的日频序列。这里几个字段需要说明一下:demand是后续所有预测的目标值;order_cnt和line_cnt不是拿来直接预测的,而是作为特征候选,因为它们能反映需求是集中在大单还是分散在小单,对库存策略有参考价值。WHERE过滤了历史开始日期,目的是避免和训练集外的脏数据混在一起。比赛数据量大的时候,这段查询跑在数据库上可能要几分钟,建议低位先存成临时表,后续所有特征工程都从这个临时表出发。

2.2 窗口特征才是时间序列模型的主干

数据聚合完成后,接下来就是把“历史需求”变成模型能直接消费的数值特征。核心思想是:第 T 天的需求量,要用 T 天以前已知的信息去预测。这里有一个铁律——任何特征都不能包含 T 天当天的信息,更不能包含 T 天之后的信息,否则就是数据泄露。也就是说,训练集中第 T 条样本的特征窗口必须截止到 T-1,目标值是 T。

特征按用途可以分成三类。第一类是历史统计特征:过去 7 天、14 天、28 天的滚动均值、滚动标准差、滚动最大值。第二类是日期特征:星期几、月中第几天、是否月初/月末(菜鸟这种电商属性的需求,月底往往有一波采购高峰),以及是否节假日。第三类是静态特征:SKU 的品类编码、价格区间、上线时长。

import pandas as pd import numpy as np from pandas.tseries.offsets import Day # df 是上一步 SQL 聚合出来的结果,列为 dt / wh_id / sku_id / demand def build_window_features(df): df = df.sort_values(['sku_id', 'wh_id', 'dt']).reset_index(drop=True) # 按 SKU + 仓库分组,构造滚动统计量 grouped = df.groupby(['sku_id', 'wh_id'])['demand'] for window in [7, 14, 28]: df[f'demand_rolling_mean_{window}'] = grouped.transform( lambda x: x.shift(1).rolling(window).mean()) df[f'demand_rolling_std_{window}'] = grouped.transform( lambda x: x.shift(1).rolling(window).std()) df[f'demand_rolling_max_{window}'] = grouped.transform( lambda x: x.shift(1).rolling(window).max()) # 滞后特征:T-1、T-7、T-14 的需求 for lag in [1, 7, 14]: df[f'demand_lag_{lag}'] = grouped.transform( lambda x: x.shift(lag)) # 星期特征:0 表示周一 df['dayofweek'] = pd.to_datetime(df['dt']).dt.dayofweek df['dayofmonth'] = pd.to_datetime(df['dt']).dt.day # 月初和月末的布尔特征 df['is_month_start'] = df['dayofmonth'].isin([1, 2, 3]).astype(int) df['is_month_end'] = df['dayofmonth'].isin([28, 29, 30, 31]).astype(int) return df

shift(1)是整个函数最关键的动作,它把特征值全部向后挪一天,保证第 T 天的样本只看得到 T-1 天及以前的信息。rolling(window)括号里的 7、14、28 分别对应一周、两周、四周的周期,菜鸟这类物流赛题的需求有很强的星期周期性,所以 7 天的倍数窗口优先考虑。还要注意grouped.transform是按每个 SKU 和仓库独立计算的,如果全表一起算,不同仓库之间会因为销量基数差异导致特征失真。

2.3 样本切分直接决定线下验证的置信度

特征做好之后,下一个关键动作是划训练集和验证集。很多选手用随机切分,这在时间序列里是致命的——模型会“偷看”未来数据。比赛数据是 2019 年到 2020 年某个时段的日订单,正确的切分方式是按照时间顺序,比如用前 80% 时间做训练,后 20% 做验证。更严格一点,验证集的起点必须晚于训练集的终点,中间可以保留一段 gap,比如留 7 天作为缓冲。

# 按时间切分:最后 30 天做线上验证前的线下模拟 split_date = df['dt'].max() - pd.Timedelta(days=30) train_df = df[df['dt'] < split_date] valid_df = df[df['dt'] >= split_date] feature_cols = [c for c in df.columns if c not in ['dt', 'wr_id', 'sku_id', 'dema']] X_train = train_df[feature_cols] y_train = train_df['demand'] X_valid = valid_df[feature_cols] y_valid = valid_df['demand']

这 30 天的 gap 值不是随手填的,它代表“从最后一次数据观察到实际补货决策之间的延迟”。线下验证集越接近线上环境,调参才越有指导意义。还有一个容易忽略的细节:feature_cols里如果有类别型字段比如品类的 ID,LightGBM 里需要显式指定为categorical_feature,否则模型会当连续值处理,效果可能不差但解释性差很多。类别特征的缺失值处理也有讲究,建议用-1填充而不是 0,避免和真实为 0 的数值特征混淆。

3. 需求预测主体:用 LightGBM 搭出一个高性价比基线

3.1 为什么选择 GBDT 而不是 LSTM

很多人看到时间序列就觉得该上 LSTM 或者 Transformer 这类深度学习模型,但实战中这类比赛里 GBDT 类模型往往更稳。原因有三个:一是比赛样本量虽然不小,但按“日+仓+SKU”分组后每组序列长度其实有限,深度学习对长序列的建模优势发挥不出来;二是特征工程里已经手动注入了周期性和滞后信息,GBDT 学非线性关系的能力足够吃下这些信息;三是 GBDT 对缺失值、离群点的鲁棒性远好于神经网络,初赛阶段不用花太多时间做数据清洗。后续如果想要更高分,可以在这个基线之上做 LightGBM 和 Prophet 的融合,这放到最后一章讲。

LightGBM 的直方图算法使得它在训练时间和内存占用上比 XGBoost 更友好,几千万的样本跑几分钟就能出结果。目标函数选regression_l2或者huber都可以。先看一个最简训练脚本:

import lightgbm as lgb params = { 'objective': 'regression_l2', 'metric': 'mae', 'learning_rate': 0.05, 'num_leaves': 63, 'max_depth': 7, 'min_child_samples': 30, 'feature_fraction': 0.8, 'bagging_fraction': 0.8, 'bagging_freq': 1, 'verbose': -1, 'seed': 2024 } dtrain = lgb.Dataset(X_train, label=y_train, categorical_feature=['sku_id', 'wh_id', 'category_id']) dvalid = lgb.Dataset(X_valid, label=y_valid, reference=dtrain) model = lgb.train( params, dtrain, num_boost_round=3000, valid_sets=[dvalid], callbacks=[lgb.early_stopping(100), lgb.log_evaluation(100)] ) model.save_model('lgb_baseline.txt', num_iteration=model.best_iteration)

learning_rate设到 0.05,是为了配合num_boost_round=3000这类大轮数做慢速拟合,训练轮数的上限放得很宽,由早停来控制最后落到哪一步。num_leaves和max_depth是一对需要联动的参数,叶子数越多模型容量越大,但太大会过拟合,63 在那个数据规模下属于中等容量。如果后续发现训练集上的 MAE 远小于验证集,优先把num_leaves降到 31 试试。

另外特别注意categorical_feature里的sku_id和wh_id,如果把 SKU 当成数值特征喂进去,模型会按 ID 大小排序去切分裂点,完全失去语义。这个坑在初赛阶段几乎每个队伍都会踩一次。

3.2 目标值变换:预测需求量之前先看分布

天数销量数据往往是右偏的,大多数 SKU 平时每天卖几个到十几个,但大促和月底会出现几十上百的尖峰。LightGBM 对直接用原始值拟合并不是不行,但 MAE 对离群点很敏感,少数尖峰会拉偏模型。常见的处理方式是用对数变换让目标分布更接近正态。

# 训练前对目标做 log1p 变换 train_df['demand_log'] = np.log1p(train_df['demand']) valid_df['demand_log'] = np.log1p(valid_df['demand']) # 预测后做反向变换 pred_demand_exp = np.expm1(model.predict(X_valid)) pred_demand_clip = np.maximum(pred_demand_exp, 0)

log1p的好处是当真实需求为 0 时变换后还是 0,反向变换时expm1也能保住这个性质。预测结果出来后必须做一次np.maximum(pred, 0)的下限裁剪,否则模型在小需求量区间可能出现负值,而负需求在业务上等于给库存倒扣钱,这在评测里会很吃亏。对数变换不是万能的,如果验证集 MAE 没有明显改善,建议切回原始值,说明你的样本里零需求比例很低,变换带来的收益不明显。

3.3 模型验证:不能只看整体 MAE

需求预测的评测最终是库存成本导向,但中间过程仍然要看 MAE、WMAE 这类误差指标。这里有个容易被忽略的分层视角:按 SKU 分层看误差,按仓库分层看误差。整体 MAE 漂亮,不代表每个仓都准。如果某个仓库的预测误差过大,后面的分仓规划在它身上必然出问题。

valid_df['pred'] = pred_demand_clip valid_df['abs_err'] = (valid_df['pred'] - valid_df['demand']).abs() # 按仓库统计 MAE wh_mae = valid_df.groupby('wh_id')['abs_err'].mean().sort_values(ascending=False) # 按 SKU 统计 MAE,关注头部 SKU 的预测质量 sku_mae = valid_df.groupby('sku_id')['abs_err'].mean().sort_values(ascending=False) print(wh_mae.head(10)) print(sku_mae.head(10))

输出结果如果发现排名靠前的仓库或 SKU 恰好是库存成本占比最高的那批,那说明预测误差结构需要单独修正。可以给这些高成本 SKU 单独加训练权重,或者在特征里加重它们的个性化特征。这类分层验证的代码花不了 10 分钟,但对后面分仓规划的参数设置帮助很大。

4. 分仓规划:把预测结果翻译成补货计划

4.1 库存成本结构决定了分仓不是单纯的预测题

赛题的评测代价是:库存持有成本 + 缺货惩罚。库存持有成本是每天每件货物放在仓库里的费用,缺货惩罚是需求来了但没货可卖造成的损失。这个结构意味着分仓规划要做的是“在哪个仓放多少货”,目标是把全仓的整体库存成本压到最低。

预测模块输出的是每个仓库每天的期望需求量,分仓模块要回答的是:每个 SKU 在每个仓里应该保有多少库存。一个朴素的思路是目标库存法——每个仓每个 SKU 维持一个目标库存水平,每天根据预测需求和当前库存计算出补货量。公式为:

补货量 = max(目标库存 - 当前库存 - 在途库存, 0)

目标库存怎么定是关键。最简单的做法是用未来 N 天预测需求的累加再乘以一个安全系数。N 是补货提前期,也就是从下单到货物到达仓库的天数。安全系数则要权衡持有成本和缺货成本,持有成本高就把安全系数调小,缺货惩罚高就调大。

# 按预测结果的未来 7 天累计值作为目标库存基线 target_days = 7 forecast['target_stock'] = ( forecast.groupby(['sku_id', 'wh_id'])['pred_demand'] .rolling(target_days, min_periods=1).sum() .reset_index(level=0, drop=True) ) # 安全库存 = 预测日均值 * 安全系数 (安全系数按 SKU 的预测波动率动态调整) sku_volatility = valid_df.groupby('sku_id')['demand'].std() / (valid_df.groupby('sku_id')['demand'].mean() + 1) forecast['safety_stock'] = forecast['pred_demand'] * forecast['sku_id'].map(sku_volatility).fillna(0.3) * 1.5 forecast['target_stock'] = forecast['target_stock'] + forecast['safety_stock']

安全库存系数取 1.5 算是一个可调的起点。如果某个 SKU 历史需求波动大,它的安全库存会高一些,这符合业务直觉——需求不稳的货要多备一点。min_periods=1是为了避免预测序列开头因为窗口不足产生空值。注意这里按sku_id分组算波动率,再把波动率映射回预测表,保持了粒度一致。如果直接对全表所有 SKU 混在一起算波动率,结果会被头部大销量 SKU 覆盖掉。

4.2 一个可落地的补货执行流程

分仓规划最终要输出一张补货计划表:日期、仓库、SKU、补货量。直接写循环去补每一行的效率很差,用 DataFrame 向量化操作可以把整个过程压缩成一段清晰的代码。

def generate_replenishment_plan(forecast, inventory, lead_time=2): plan = [] for sku_id in forecast['sku_id'].unique(): sku_data = forecast[forecast['sku_id'] == sku_id] sku_inv = inventory[inventory['sku_id'] == sku_id] for _, row in sku_data.iterrows(): wh_id = row['wh_id'] target = row['target_stock'] on_hand = sku_inv.loc[sku_inv['wh_id'] == wh_id, 'stock'].sum() in_transit = sku_inv.loc[sku_inv['wh_id'] == wh_id, 'in_transit'].sum() replenish = max(target - on_hand - in_transit, 0) if replenish > 0: plan.append({ 'dt': row['dt'], 'wh_id': wh_id, 'sku_id': sku_id, 'replenish_qty': replenish }) return pd.DataFrame(plan) replenish_plan = generate_replenishment_plan(forecast, current_inventory, lead_time=2)

lead_time=2表示补货提前期 2 天,也就是说今天的补货计划要覆盖未来 6 天加安全库存。如果补货提前期更长,可以调大target_days,这会直接影响整体库存水位。循环遍历在 SKU 数量较大时运行会偏慢,但作为基线方案逻辑清晰,可读性好。实际比赛里很多选手到这一步就停了,补货计划产出后直接按表提交,这已经能拿到一个中上等的排名。真正的优化空间在如何为每个 SKU 和仓库设置差异化的目标库存参数,而不是一个全局统一的安全系数。

4.3 分仓需求分配:预测了每个仓之后要不要再做一次跨仓调配

比赛里有时会出现“需求预测”和“分仓规划”分开的两个环节:需求预测是对全网总需求或每个仓需求做预测,分仓规划则是在仓库之间分配货物。如果预测是分仓进行的,分仓规划就不需要再做需求分解,直接按目标库存法处理。如果预测的是全网总需求,那就要做一次按比例拆仓。拆仓常见依据是历史销量占比:

仓 A 分配比例 = 仓 A 历史需求 / 所以仓历史需求之和

这个比例要定期更新,不能一口气用全量历史。比如只用最近 30 天算比例,跟随近期趋势。拆仓后同样落到目标库存公式里,整体流程和上面保持一致。注意不要为了追求“参数精巧”去引入复杂的多目标优化求解器,在比赛有限的迭代周期里,一个稳定的基线加两三个差异化的参数调整,比花一周去调一个混合整数规划模型要划算得多。

5. 高频踩坑复盘:预测翻车和分仓失效的典型现场

5.1 负预测值导致补货计划异常

现象:预测脚本跑完后,pred_demand出现大量负值,尤其在零需求的稀疏 SKU 上,负值占比能到 5% 以上。带入分仓规划后出现“负补货量”,逻辑直接乱掉。

原因:部分 SKU 在验证期内大量天数需求为 0,模型学到的分布以低值区间为主,预测值的分布自然往负偏移。log1p变换能缓解,但没能根治,因为零膨胀分布本身就不适合用普通回归目标去拟合。

解决:在反向变换前增加一个概率修正——先用分类模型(或者阈值判断)预测“该 SKU 当天是否会有需求”,再把回归模型的预测值乘以这个概率。实际操作中一个简单的用法是把历史需求为 0 的比例作为经验概率乘上去,或者直接在最终结果上加np.maximum(pred, 0)兜底。两者结合最稳妥。

5.2 训练集和验证集出现同一天数据

现象:线下验证 MAE 看着非常好,但提交线上分数差一大截,典型的线下线上不一致。

原因:切分时没有清理时间边界上的 overlap。有些选手在做滞后特征时对全表shift(1),如果验证集的起点在训练集窗口内的滞后特征上引用了训练集的目标值,训练和验证其实共享了部分信息。

解决:严格按“训练集最后一天 + 1 天的滞后窗口”来生成验证集特征,最笨的办法是训练集和验证集分开两个数据管道跑特征工程,不要共用一个grouped.transform的结果。再检查一遍切分代码,确保train_df['dt'].max()小于valid_df['dt'].min(),gap 至少留 1 天。

5.3 SKU 和仓库编码被当成数值特征

现象:模型跑完看特征重要性,sku_id排到第一名,重要性超过 30%。

原因:这基本可以断定 SKU ID 被当成了连续数值特征。LightGBM 默认把所有数值列当连续量处理,ID 这种编码恰恰在数值上是有先后顺序的——ID 相近的 SKU 被归到同一个子树里,模型学到了编号的虚假规律。

解决:在构造lgb.Dataset时显式指定categorical_feature,另外把 SKU 的高频编码换成基于统计量的 embedding 特征(比如每个 SKU 历史平均销量、历史需求标准差),这类特征比原始 ID 更有业务含义。

5.4 验证指标只看 MAE,导致分仓参数调错方向

现象:线下 MAE 一直在降低,但提交库存成本分数却不变甚至变差。

原因:MAE 是平均绝对误差,库存成本是缺货成本和持有成本的分段线性组合。这两个目标的梯度不一致:MAE 降低更多来自高频低量 SKU 的精度提升,而库存成本的敏感点在大销量高成本 SKU 的缺货和积压。

解决:线下增加一个成本仿真函数,按赛题规则把预测结果翻译为成本值,调参时同时看 MAE 和 cost。如果发现 MAE 降但 cost 涨,说明模型在牺牲高成本 SKU 的精度去换整体低频区域的平均表现,需要增加高成本 SKU 的训练权重,或者调整安全系数让预测偏差的分布更对称。

6. 进阶:集成预测与成本仿真评估,把手上的方案再顶高一个台阶

到了这一步,基线已经能稳定跑通,接下来值得投入的是两件事:第一是用融合把单模型预测方差降下来;第二是建一个与赛题规则一致的成本仿真器,让每一轮迭代都看成本而不是看误差。这两个技术点都做熟之后,这套方案的泛化能力才算真正立住了。

集成方面,最省力的做法是 LightGBM 和 Prophet 的加权平均。Prophet 擅长捕捉趋势和季节项,LightGBM 擅长学习复杂交互特征,两个模型的错误模式差异大,融合后可以显著降低方差。实现上分三步:分别训练两个模型得到预测值,然后线性回归学习权重。权重不必跑到全网全局统一,按 SKU 分桶学习效果更好。

from prophet import Prophet from sklearn.linear_model import LinearRegression # 1. 用 Prophet 拟合单个 SKU 的时间序列(注意这里只演示一个 SKU 的流程) sku_example = df[df['sku_id'] == 'SKU001'][['dt', 'demand']].rename( columns={'dt': 'ds', 'demand': 'y'}) prophet_model = Prophet(yearly_seasonality=False, weekly_seasonality=True, daily_seasonality=False) prophet_model.fit(sku_example) # 2. 得到 Prophet 的预测结果 future = pd.DataFrame({'ds': pd.date_range(start=valid_start, periods=30, freq='D')}) prophet_pred = prophet_model.predict(future)['yhat'].values # 3. 把 LightGBM 预测和 Prophet 预测按 SKU 做线性加权 lgb_pred = model.predict(X_valid) X_weight = np.column_stack([lgb_pred, prophet_pred]) weight_model = LinearRegression().fit(X_weight, y_valid) print('融合权重:', weight_model.coef_)

yearly_seasonality=False是因为比赛数据长度通常不足一年,强行拟合年度季节性只会带来过拟合。weekly_seasonality=True对应电商物流的周效应。LinearRegression拟合权重时不用加截距项也可以,但带上截距能吸收两个模型的系统偏差。注意融合过程在 SKU 数量多时比较耗时,常见做法是只对 TOP 30% 销量的 SKU 做融合,长尾 SKU 继续用 LightGBM 单模型的预测,切分阈值设在销量排名百分位 30 的位置,具体看训练时间预算。如果发现某个 SKU 上 Prophet 预测明显偏向趋势外推而实际销量已经下滑,那这个 SKU 就不适合融合,直接给 Prophet 权重设为 0。

成本仿真器这块,实现思路不复杂但非常实用。给定预测结果和真实需求,按赛题给出的仓库库存和 SKU 成本参数,模拟逐日的库存变化,计算每天的持有成本和缺货成本累加。有了这个仿真器,调安全库存系数、补货提前期、目标库存天数时再也不用靠猜,每个参数改变带来的成本变化可以直接拿数字说话。

def simulate_cost(forecast_df, true_df, params): holding_cost = params['holding_cost'] # 每件每天持有成本 stockout_cost = params['stockout_cost'] # 每件缺货惩罚 init_stock = params['init_stock'] # 各仓各 SKU 初始库存 stock = init_stock.copy() total_cost = 0.0 for dt in true_df['dt'].unique(): day_true = true_df[true_df['dt'] == dt] day_fcst = forecast_df[forecast_df['dt'] == dt] for _, row in day_true.iterrows(): key = (row['wh_id'], row['sku_id']) available = stock.get(key, 0) + day_fcst.replenish_qty if available >= row['demand']: stock[key] = available - row['demand'] total_cost += stock[key] * holding_cost else: stockout = row['demand'] - available total_cost += stockout * stockout_cost stock[key] = 0 return total_cost

仿真器里最容易踩的细节是补货到货时间的建模:当天的补货量是当天生效还是提前期后才生效,对成本影响很大。规范做法是在第 T 天执行补货计划,但库存要第 T+lead_time 天才增加,这期间的缺货依然要算惩罚。上面的简化代码把补货当成当天生效,实际使用时要加一个arrival_date字段延迟生效。仿真器输出的成本值不要和线上绝对分数对标,差一个常数偏移量很正常,关键是看不同参数配置下成本的相对变化趋势。参数优化可以用最简单的手动网格搜索,先固定holding_cost,扫stockout_cost,再联合调target_days和安全系数,收敛很快。

最后说一个我自己迭代这类供应链赛题的教训:不要一上来就追求模型的复杂度和刷榜的刺激感,先把成本仿真器搭好,让每次模型迭代都有明确的业务解释再往前推。一套源码、一个数据库、一份设计报告,最大的价值不是那个最终分数,而是你能不能把这套“预测到决策”的流程完整复述给下一个学习者。希望这个复现路径能帮你在自己的项目里少走几段弯路。

本文还有配套的精品资源,点击获取

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询