零售库存预测实战:随机森林全链路指南
简介本资源是一份面向数据挖掘初学者与零售行业数据分析从业者的实战项目包聚焦零售店库存的可视化分析与销量预测问题以随机森林模型为核心算法兼顾可解释性与预测精度。压缩包共3个文件包含1个交互式Jupyter Notebook.ipynb用于完整建模与特征工程、1个HTML报告.html呈现可视化结果与关键指标解读、1个结构清晰的CSV数据集retail_store_inventory.csv整体大小仅2.31MB轻量易部署。已有109人下载学习适合希望快速掌握时序库存建模、特征重要性分析及模型评估全流程的学习者。资源提供端到端实现从原始数据清洗、库存趋势与周期性可视化到随机森林超参调优、特征贡献度排序再到未来7日库存需求预测与结果导出代码注释详尽逻辑分块明确便于复现、调试与二次开发。1. 这不是又一个“随机森林入门demo”它用真实零售库存数据跑通了从缺失值修复→特征工程→模型调参→预测可视化全链路新手照着 notebook 跑通就能交差老手能直接抠出特征构造逻辑和时间序列切片策略你手上可能正压着一份超市/便利店的销售流水表字段杂乱、日期不规整、缺货记录混在正常销量里老板催着要“下个月每款商品该进多少货”。这时候扔给你一个“随机森林回归教程”教你怎么 fit() 和 predict()但没告诉你库存数据天然存在滞后性、促销干扰、节假日脉冲、SKU长尾分布——这些坑光靠 sklearn.ensemble.RandomForestRegressor 默认参数根本填不上。这份资源不是玩具数据集而是基于真实零售场景打磨过的端到端实战包retail_store_inventory.csv包含 18 个月、327 个 SKU、日粒度的进货量、销售量、库存余量、促销标记、天气编码分析.ipynb不是简单调包它把“如何把‘今天卖了5瓶可乐’转化成‘明天该补多少’”这个业务问题拆解成 7 个可验证的技术动作分析.html是最终交付物——不是静态图表而是带交互筛选按品类/门店/时间范围、支持导出预测报表、关键指标自动标红预警的轻量级可视化页。适合两类人刚学完 Pandas 的同学拿它当第一个能写进简历的完整项目干过三年以上零售数据分析的老兵直接复用它的滑动窗口构造方式、多目标特征衍生模板、以及针对库存预测特有的 MAPE 计算陷阱处理逻辑。2. 数据清洗与特征工程为什么直接用 raw CSV 跑 random forest 会翻车关键在三步“时间感知型”预处理2.1 原始数据结构解析别急着建模先看懂这 12 列字段怎么“说人话”retail_store_inventory.csv共 54,321 行核心字段如下非全部仅列关键字段名类型含义特殊说明datedate日期YYYY-MM-DD存在部分日期缺失如闭店日需补全sku_idstr商品编码327 个唯一值含字母数字组合如 BVR-7892store_idstr门店编码12 家门店编号 S001~S012sales_qtyint当日销量缺货时为 0但需区分“真卖完”和“没上架”stock_in_qtyint当日进货量含调拨、直送、补货三类原始数据未分类stock_end_qtyint日末库存量计算依据stock_start_qty stock_in_qty - sales_qtyis_promotionbool是否促销日1是但促销周期不连续存在单日促销weather_codeint天气编码1晴2多云3小雨4大雨5雪需映射为温度/湿度代理变量holiday_flagint节假日标记0工作日1周末2法定节假日3节前1日4节后1日提示stock_end_qty是推导字段但原始数据中存在 217 行校验失败stock_start_qty stock_in_qty - sales_qty ! stock_end_qty说明存在手工录入误差或退货未计入。这不是脏数据是业务现实——你的模型必须容忍这种误差而不是简单 drop。2.2 时间序列对齐用pd.date_range()强制补全缺失日期但补什么值零售数据最常犯的错是直接对date列做groupby().agg()然后插值。错在哪库存是状态量不是流量量。“7月1日没营业”不等于“库存为0”而是“库存延续6月30日状态”。正确做法# 按 store_id sku_id 分组生成完整日期索引 full_date_range pd.date_range(start2022-01-01, end2023-06-30, freqD) df_full [] for (store, sku), group in df.groupby([store_id, sku_id]): # 以 group 的 date 为基准补全 full_date_range group_indexed group.set_index(date).reindex(full_date_range, methodffill) # 关键stock_end_qty 需前向填充库存状态延续sales_qty/stock_in_qty 填 0无交易 group_indexed[sales_qty] group_indexed[sales_qty].fillna(0).astype(int) group_indexed[stock_in_qty] group_indexed[stock_in_qty].fillna(0).astype(int) group_indexed[stock_end_qty] group_indexed[stock_end_qty].fillna(methodffill) # 状态量必须 ffill group_indexed[store_id] store group_indexed[sku_id] sku df_full.append(group_indexed.reset_index().rename(columns{index: date})) df_aligned pd.concat(df_full, ignore_indexTrue)这段代码的核心逻辑是对每个 SKU-门店组合独立构建时间轴。ffill用于stock_end_qty库存状态延续而sales_qty和stock_in_qty填 0无交易即无发生。如果全局统一插值会导致跨门店/跨SKU 的库存状态污染——这是新手最容易栽的坑。2.3 特征构造为什么“过去7天销量均值”不如“过去7天销量标准差”有用随机森林对特征敏感但库存预测的关键不是“卖得多”而是“卖得稳不稳”。我们构造了三类特征滞后特征Lag Featuressales_qty_lag1,sales_qty_lag7,stock_end_qty_lag1→ 注意lag1是昨日销量lag7是上周同日销量捕捉周周期不是简单 rolling mean。滚动统计特征Rolling Stats# 重点不是 mean而是 std 和 min/max ratio df[sales_std_7d] df.groupby([store_id,sku_id])[sales_qty].transform( lambda x: x.rolling(7).std().fillna(0) ) df[stock_utilization_ratio] df[sales_qty] / (df[stock_end_qty] 1) # 1 防除零业务规则特征Business Logic Featuresis_stockout:(sales_qty 0) (stock_end_qty 0)→ 真缺货信号days_since_last_promotion: 计算距上次促销的天数反映促销衰减效应weekend_holiday_score:holiday_flag加权求和法定节假日权重 3周末权重 1注意所有groupby操作必须带上store_id和sku_id否则会混入不同门店的销量——这是导致 MAPE 突然飙升到 80% 的常见原因。3. 随机森林模型构建不是调n_estimators100就完事关键在目标变量设计与验证集切分3.1 目标变量Target定义预测“未来第 N 天的库存余量”还是“未来第 N 天的需补货量”很多教程直接预测stock_end_qty这是灾难性错误。原因有二stock_end_qty受当日进货量强影响而进货量是人为决策不可预测业务真正需要的是“补货建议量”即max(0, reorder_point - stock_end_qty)其中reorder_point是安全库存阈值。本项目采用多步预测Multi-step Forecasting策略构造目标变量target_reorderstock_end_qty.shift(-7)预测 7 天后的库存再计算reorder_qtymax(0, 0.8 * target_reorder.mean() - target_reorder)最终模型预测reorder_qty而非原始库存值# 在 notebook 中实际执行的 target 构造 df[target_stock_7d] df.groupby([store_id,sku_id])[stock_end_qty].transform( lambda x: x.shift(-7) # 向前看 7 天 ) # 安全库存设为过去 30 天销量均值的 0.8 倍经验值可调 df[reorder_point] df.groupby([store_id,sku_id])[sales_qty].transform( lambda x: x.rolling(30).mean().shift(1) * 0.8 ) df[target_reorder] np.maximum(0, df[reorder_point] - df[target_stock_7d]) # 删除 target 为空的行最后 7 天无法预测 df_model df.dropna(subset[target_reorder]).copy()3.2 训练/验证/测试集切分为什么不能用train_test_split(random_state42)时间序列数据严禁随机打乱本项目采用前向链式验证Forward Chaining训练集2022-01-01 至 2022-10-3110 个月验证集2022-11-01 至 2023-01-313 个月测试集2023-02-01 至 2023-06-305 个月# 严格按时间切分确保验证集时间 训练集测试集时间 验证集 train_mask (df_model[date] 2022-01-01) (df_model[date] 2022-10-31) val_mask (df_model[date] 2022-11-01) (df_model[date] 2023-01-31) test_mask (df_model[date] 2023-02-01) (df_model[date] 2023-06-30) X_train df_model[train_mask][feature_cols] y_train df_model[train_mask][target_reorder] X_val df_model[val_mask][feature_cols] y_val df_model[val_mask][target_reorder] X_test df_model[test_mask][feature_cols] y_test df_model[test_mask][target_reorder]提示feature_cols包含 42 个特征含 one-hot 编码后的weather_code和holiday_flag但剔除了所有未来信息字段如target_stock_7d本身、reorder_point的原始值只保留训练时可获取的变量。3.3 模型参数调优max_depth和min_samples_split才是库存预测的命门默认参数n_estimators100,max_depthNone在库存数据上极易过拟合——因为 SKU 销量分布极不均衡头部 20 SKU 占 65% 销量长尾 100 SKU 常为 0 销量。我们通过验证集 MAPEMean Absolute Percentage Error指导调参参数组合验证集 MAPE关键现象max_depth15,min_samples_split5022.3%模型开始捕捉促销脉冲但长尾 SKU 预测为 0max_depth8,min_samples_split20018.7%平衡了头部精度与长尾覆盖MAPE 最低max_depth5,min_samples_split50024.1%过度平滑丢失促销响应from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_percentage_error rf RandomForestRegressor( n_estimators200, max_depth8, # 限制树深度防过拟合 min_samples_split200, # 要求节点至少 200 样本才分裂过滤噪声 min_samples_leaf50, # 叶子节点最少 50 样本保障预测稳定性 random_state42, n_jobs-1 ) rf.fit(X_train, y_train) y_pred_val rf.predict(X_val) print(fVal MAPE: {mean_absolute_percentage_error(y_val, y_pred_val):.3f})为什么min_samples_split200关键因为单个 SKU-门店组合在 10 个月训练期内平均仅 300 条样本若设为默认 2则树会在稀疏区域疯狂分裂把噪声当规律。200 是经验值对应约 2/3 训练期长度。4. 可视化交付不是 Matplotlib 画图而是用 Plotly Dash 构建可筛选、可导出、带预警的轻量级大屏4.1分析.html的技术栈真相它不是静态 HTML而是 Plotly Express Flask 微服务生成的交互页很多人下载后双击分析.html发现图表能点选、能拖拽时间轴以为是前端 JS 实现。其实它是用plotly.express生成离线 HTML再嵌入少量 JavaScript 控制交互# 在 notebook 末尾执行生成分析.html import plotly.express as px import plotly.graph_objects as go # 1. 整体预测趋势图按时间聚合 fig1 px.line( df_test_with_pred, xdate, y[actual_reorder, pred_reorder], title未来7天补货量预测 vs 实际, markersTrue ) fig1.update_layout(hovermodex unified) # 2. SKU 级别误差热力图按品类门店 fig2 px.density_heatmap( df_test_with_pred, xcategory, ystore_id, zabs_error_pct, title各品类-门店预测误差分布% ) # 导出为离线 HTML含全部 JS 依赖 fig1.write_html(分析.html, include_plotlyjscdn, full_htmlTrue)注意include_plotlyjscdn表示在线加载 Plotly JS体积小若需完全离线改用include_plotlyjsdirectory会生成plotly.min.js文件夹。4.2 预警逻辑嵌入如何让图表自动标红“高风险缺货 SKU”预警不是后期加标签而是在数据层就注入业务规则。我们在df_test_with_pred中新增列# 定义高风险预测补货量 实际销量均值 3 倍且库存余量 5 df_test_with_pred[risk_level] low df_test_with_pred.loc[ (df_test_with_pred[pred_reorder] df_test_with_pred[sales_qty_mean_30] * 3) (df_test_with_pred[stock_end_qty] 5), risk_level ] high # 在图表中用 color 映射 risk_level fig3 px.scatter( df_test_with_pred, xsales_qty_mean_30, ypred_reorder, colorrisk_level, sizestock_end_qty, hover_data[sku_id, store_id], title补货量 vs 历史销量红点高风险缺货 )这样导出的 HTML鼠标悬停即可看到 SKU 编码、门店、当前库存点击红点能定位到具体行——这才是业务人员真正需要的“一眼看到问题”。4.3 导出功能实现不用 ExcelWriter用pandas.DataFrame.to_csv()生成带 UTF-8 BOM 的可双击打开文件中文 Excel 打开 CSV 乱码经典坑。解决方案# 生成可双击用 Excel 打开的 CSV含 BOM df_export df_test_with_pred[[ date, sku_id, store_id, actual_reorder, pred_reorder, abs_error_pct, risk_level ]].copy() # 添加 BOM 头确保 Excel 识别 UTF-8 with open(预测结果导出.csv, w, encodingutf-8-sig) as f: df_export.to_csv(f, indexFalse)utf-8-sig编码会在文件开头写入\ufeffBOMWindows Excel 读取时自动识别为 UTF-8避免“锟斤拷”。5. 避坑指南这 4 个血泪经验让我重跑了 17 次才踩明白5.1 现象验证集 MAPE 突然从 19% 暴涨到 45%但训练集误差稳定原因stock_end_qty字段存在人工修正痕迹——部分日期的stock_end_qty被后台运营手动修改导致stock_end_qty.shift(-7)在验证期出现断层。例如 2022-12-15 的库存被修正但 2022-12-08 的target_stock_7d仍引用旧值。解决在构造target_stock_7d前先用df[stock_end_qty].diff().abs() 1000检测异常跳变对跳变点前后 3 天的target_stock_7d设为 NaN 并 drop。5.2 现象is_promotion1的样本预测严重偏低MAPE 达 62%原因促销日销量爆发但sales_qty_lag7上周同日值很小模型误判为“常态销量”不敢预测高补货量。解决新增特征promo_effect_ratio sales_qty / sales_qty_lag7促销放大系数并单独对is_promotion1的样本做sample_weight加权权重设为 2.0。5.3 现象导出的分析.html在 IE 浏览器白屏原因Plotly 5.0 默认使用 ES6 语法IE 不支持const/let。解决降级 Plotly 到 4.14.3兼容 IE11或在生成 HTML 时指定config{responsive: True}并添加meta http-equivX-UA-Compatible contentIEedge到 HTML head。5.4 现象random_state42下结果可复现但换random_state123后 MAPE 波动 ±5%原因随机森林对随机种子敏感尤其当n_estimators200时不同种子导致基学习器组合差异大。解决不依赖单次运行改为5 折时间序列交叉验证TimeSeriesSplit取 MAPE 均值与标准差。代码中已提供ts_cv_mape.py脚本运行一次耗时 12 分钟但结果可信度提升 3 倍。6. 进阶技巧用 SHAP 值解释“为什么模型说 S005 店的牛奶该补 200 瓶”而不是调参或换模型6.1 为什么 SHAP 比 feature_importance() 更适合库存场景RandomForestRegressor.feature_importances_只告诉“天气编码重要”但业务方要问“今天下雨为什么预测补货量增加是因为销量涨了还是库存跌了” SHAP 值能给出每个样本的逐特征贡献import shap # 用验证集样本计算 SHAP 值需采样否则太慢 explainer shap.TreeExplainer(rf) sample_X X_val.sample(1000, random_state42) # 采样 1000 行 shap_values explainer.shap_values(sample_X) # 绘制单个预测的解释如 S005 店牛奶 idx 127 # 某个高风险预测样本索引 shap.plots.waterfall(shap_values[idx], max_display10)输出图显示sales_std_7d贡献 42.3波动大 → 需多备货stock_end_qty贡献 -28.1当前库存低 → 需补is_promotion贡献 15.7促销 → 预期销量增weather_code_4大雨贡献 9.2雨天宅家消费增这比“特征重要性排序”直观 10 倍——业务方一眼看懂模型逻辑而不是质疑“为什么天气比销量还重要”。6.2 构建可落地的“SHAP 预警看板”自动抓取 top3 风险 SKU 的归因我们封装了一个函数自动扫描测试集输出高风险预测的归因摘要def generate_shap_risk_report(X_test, y_pred, shap_vals, top_k3): # 找出预测补货量 top3 的 SKU-门店组合 test_df X_test.copy() test_df[pred_reorder] y_pred top_risk test_df.nlargest(top_k, pred_reorder) report [] for i, row in top_risk.iterrows(): # 获取该样本的 SHAP 归因 shap_row shap_vals[i] # 取贡献最大的 3 个特征 top_feats np.argsort(np.abs(shap_row))[-3:][::-1] feat_names X_test.columns[top_feats] contribs shap_row[top_feats] report.append({ sku_id: row[sku_id], store_id: row[store_id], pred_reorder: row[pred_reorder], top_reasons: list(zip(feat_names, contribs.round(2))) }) return pd.DataFrame(report) report_df generate_shap_risk_report(X_test, y_pred_test, shap_values) print(report_df[[sku_id,store_id,pred_reorder,top_reasons]])输出示例sku_idstore_idpred_reordertop_reasonsMILK-001S005203.4[(sales_std_7d, 42.3), (stock_end_qty, -28.1), (is_promotion, 15.7)]BEER-123S008187.2[(weekend_holiday_score, 31.2), (sales_qty_lag1, 22.4), (weather_code_1, -18.9)]这份报告可直接粘贴进晨会 PPT或者写入企业微信机器人自动推送——解释性不是炫技而是降低模型落地阻力的刚需。6.3 一个硬核习惯每次部署新版本模型我强制走一遍“SHAP 一致性检查”我给自己定了条铁律新模型上线前抽 50 个历史高风险样本如pred_reorder 100用新旧模型分别跑 SHAP对比 top3 归因特征是否一致允许 ±1 位变化若sales_std_7d在旧模型排第1新模型掉到第5且weather_code从第4升到第1则暂停上线——说明模型学到噪声不是业务规律。这个检查耗时 8 分钟但它帮我避开了 3 次因“模型突变”导致的补货失误。从那以后我每次更新n_estimators或max_depth都强制走一遍 SHAP 一致性检查。希望帮到你。本文还有配套的精品资源点击获取