ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于随机森林的零售库存预测与可视化实战

基于随机森林的零售库存预测与可视化实战 简介面向数据挖掘初学者与零售运营人员的实战资源围绕零售店库存数据完成可视化分析与随机森林预测建模。包内包含完整数据集、Jupyter Notebook分析代码以及HTML可视化报告可直接对照运行适合学习特征处理、缺失值排查、模型训练与结果解释的完整流程。资源共3个文件涵盖csv表格数据、ipynb代码脚本和html结果展示压缩包总大小约为2.31MB结构紧凑便于下载后快速上手。目前已有109人学习下载。通过该资源可以掌握随机森林在库存预测中的具体应用理解数据清洗、特征选择、模型评估等关键环节并获得一份可直接复用的分析模板同时还能参考HTML报告中的可视化图表布局对完成课程设计或入门数据挖掘项目具有实际参考价值。1. 随机森林模型与零售库存预测这套实战项目的完整落地路径一家门店的库存预测如果只靠店长拍脑袋最容易出现的结果是畅销款看不见补货周期滞销款堆到仓库死角。这个标题里的“数据挖掘实战-基于随机森林模型的零售店库存可视化与预测”解决的就是这个问题用一份带数据集的代码包把门店历史订单、库存流水、促销记录喂给随机森林回归算法得到未来几周每个 SKU 的库存量再把结果画成折线和热力图让运营者一眼看出哪些门店要补货、哪些品类已经积压。适合正在学数据挖掘、准备课程设计或刚开始接触供应链数据分析的从业者也适合已经在用 Excel 做库存台账、想换一套可解释可扩展方案的人。这套路线不依赖大数据集群一台普通笔记本就能跑通关键是理解数据怎么清洗、模型参数怎么调、预测结果怎么变成业务能看懂的图表。2. 零售库存数据准备字段清洗、特征工程与时间切分2.1 解压后先重建目录别让原始数据被清洗代码覆盖从标题里的 .rar 包拿到代码和数据后我建议先不要急着跑源码。多数压缩包解压出来只有几个 CSV 和若干.py文件文件多了之后很容易把原始数据污染掉。我一般会先建一个标准目录把原始数据、中间结果、模型输出分开。mkdir -p retail_inventory/{data/raw,data/processed,notebooks,src,output}这行命令创建了五个目录data/raw放从压缩包解出的原始 CSVdata/processed放清洗和特征工程之后的数据src放训练与预测脚本output放可视化图片和预测结果表notebooks放探索性分析。逻辑很简单任何数据清洗都不应该原地修改原始文件否则某一步写错了连后悔药都没得吃。如果你拿到的压缩包已经自带了目录我也建议把关键副本提前备份一份。零售店库存数据常见的格式是一行记录对应某天某个门店某个 SKU 的库存量字段可能包括日期、门店编号、商品编码、当前库存、当日销量、是否促销、到货量等。先把这些字段名打印出来确认有没有中文列名、有没有空表头再决定下一步。2.2 字段级探索与特征工程把库存数据变成随机森林听得懂的特征随机森林和线性回归不一样它对字段缩放的容忍度很高但非常在意特征有没有含义。直接拿“当前库存”当唯一特征去预测“未来库存”模型学到的只是把昨天的库存搬过去没有任何决策意义。我习惯先把原始表读进来做一次字段体检。import pandas as pd df pd.read_csv(data/raw/store_inventory.csv, parse_dates[date]) print(df.info()) print(df.isna().sum()) print(df[store_id].nunique(), df[sku_id].nunique())逻辑说明parse_dates把日期列解析成时间类型保证后面做时间切片时不报错info()能看到每列的非空数量和数据类型isna().sum()快速定位缺失严重的列nunique()判断门店数和 SKU 数如果 SKU 数量过多后面建模时要考虑按高频 SKU 单独建模因为每个 SKU 的销量分布差异极大。参数说明parse_dates只对真正的日期列使用如果你把门店编号传入也会被强行解析成时间格式nunique()在数据量大的时候会比较慢可以改成df[sku_id].drop_duplicates().shape[0]。字段体检之后进入特征工程。零售库存预测里最有价值的特征不是库存本身而是销量历史趋势、促销影响、周期性、以及补货间隔。下面这段代码我几乎每次都会用到。df[lag_7_sales] df.groupby([store_id, sku_id])[sales].shift(7) df[rolling_14_sales] ( df.groupby([store_id, sku_id])[sales] .rolling(14) .mean() .reset_index(level0, dropTrue) ) df[days_since_last_restock] df.groupby([store_id, sku_id])[date].diff().dt.days df[is_weekend] df[date].dt.dayofweek 5 df[is_promo] df[promo_flag].fillna(0).astype(int)逻辑说明lag_7_sales是每个 SKU 一周前的销量用来捕捉周忠诚度rolling_14_sales是过去两周的平均销量用来平滑短期波动days_since_last_restock计算上一次入库日期到今天的天数这个概念对库存预测特别重要如果这个值很大说明该 SKU 已经很久没补货库存很可能已经见底is_weekend和is_promo是两个布尔特征用来告诉模型周末和促销对销量的拉动。参数说明shift(7)默认是按行顺序错位所以必须先按date排好序再执行否则特征对应到错误日期rolling(14)窗口大小可以根据门店补货周期调整快消品用 7 到 14 天耐用品可以用 30 天。这里有一个常见的坑如果某个 SKU 在数据起始前没有历史销量lag_7_sales会是空值随机森林建模时可以直接保留空值让模型自己处理但如果你用的算法不支持缺失值就需要fillna(0)或按门店均值填充。2.3 按时间切分训练集、验证集、测试集库存预测不能随机打散很多数据挖掘初学者拿到数据就train_test_split(x, y, test_size0.2, random_state42)这在库存预测里会翻车。库存数据是强时间序列数据如果随机抽样训练集里会出现未来日期的样本验证集里会出现过去日期的样本模型学到的其实是“穿越时空”的规律上线后一测真实未来数据就崩。TRAIN_END 2024-06-30 VALID_END 2024-09-30 df df.sort_values([store_id, sku_id, date]) train df[df[date] TRAIN_END] valid df[(df[date] TRAIN_END) (df[date] VALID_END)] test df[df[date] VALID_END] print(train.shape, valid.shape, test.shape)逻辑说明先把数据按门店、SKU、日期排序确保时间顺序正确然后按日期切出三段。比如用前 6 个月训练接下来 3 个月做验证调整参数最后 3 个月当测试评估真实效果。这样切分的核心原因是模型上线时面对的永远是未来数据验证集也要遵循同样的条件。参数说明切分日期没有固定标准取决于数据跨度。如果压缩包内数据只有一年我一般按 6 个月、3 个月、3 个月切如果数据有两年以上可以训练 12 个月、验证 6 个月、测试 6 个月。注意这里有一个细节valid中的日期虽然晚于train但特征如lag_7_sales会用到前 7 天数据而那 7 天可能属于训练集这是正常的不属于数据泄漏因为预测时我们确实知道过去 7 天销量。做完切分后可以把处理后的数据集保存成parquet或csv供建模脚本读取。我习惯输出成feather格式速度快而且保留数据类型但跨平台分享时 CSV 更稳妥。3. 随机森林回归建模从最小可运行代码到参数调优3.1 为什么随机森林回归算法适合零售店库存预测我最初做过一批线性回归和 ARIMA 的库存预测效果在促销节点上非常差。零售库存数据里充满了非线性关系促销带来的销量可能是平时的 3 到 5 倍补货到货后库存突然跳升节假日效应又和月份强相关。随机森林回归算法不需要显式指定这些交互项它会在每个决策树节点自动寻找最优切分特征和切分点天然能捕捉“周末促销缺货”这种组合效应。另一个优势是它对异常值和缺失值不敏感。门店手工录入库存数量时经常出现负库存、四位小数库存、空值随机森林的分裂过程对这些脏数据的容忍度比线性模型高很多。加上feature_importances_可以输出特征重要性让我们能反向验证业务直觉比如“上次补货间隔”是不是真的比“当日销量”更重要。它的缺点也很明显不能外推数据范围之外的数值。如果训练数据里某 SKU 最大库存只有 5000预测结果永远不会超过 5000这会导致大促爆单时预测偏低。这个问题后面避坑章节会单独说这里先建立预期。3.2 最小可运行代码训练随机森林模型并计算误差建模阶段不用一开始就追求完美先跑通一个最小可用版本让后续调参在同一个基准上对比。下面这段代码是项目骨架。from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error features [ lag_7_sales, rolling_14_sales, days_since_last_restock, is_weekend, is_promo, ] X_train train[features].fillna(0) y_train train[stock_quantity] X_valid valid[features].fillna(0) y_valid valid[stock_quantity] model RandomForestRegressor( n_estimators200, max_depth12, min_samples_leaf5, random_state42, ) model.fit(X_train, y_train) pred model.predict(X_valid) mae mean_absolute_error(y_valid, pred) print(fMAE: {mae:.2f})逻辑说明选这五个特征作为初始特征集fillna(0)只用于处理缺失的销量特征避免让训练报错stock_quantity当作回归目标这里预测的是某个门店某个 SKU 某一天的期末库存。MAE即平均绝对误差单位与库存数量一致比如 MAE 等于 8意味着平均每个 SKU 每天预测误差 8 件。参数说明n_estimators200是树的数量太小会导致结果不稳定太大只增加计算时间不带来明显精度提升max_depth12限制每棵树深度防止决策树无限分裂记住训练集细节min_samples_leaf5强制每个叶节点至少 5 个样本让预测结果更平滑random_state42固定随机种子保证每次运行结果一致方便复现和对比调参。如果你跑完发现 MAE 大得离谱先不要急着调参回到特征工程检查rolling_14_sales是不是因为groupby后没有正确重设索引导致错位到别的 SKU 上。这个错位导致的误差会把模型直接带偏而且肉眼很难看出来。3.3 三个必调参数n_estimators、max_depth、min_samples_leaf参数调优是这个项目里最花时间也最像玄学的部分。我常用的思路不是一次性把所有参数交给网格搜索而是分优先级调整。第一优先级是max_depth第二是min_samples_leaf第三才是n_estimators。先看一段用网格搜索做参数对比的代码。from sklearn.model_selection import GridSearchCV param_grid { n_estimators: [100, 300], max_depth: [8, 12, None], min_samples_leaf: [2, 5], } search GridSearchCV( RandomForestRegressor(random_state42), param_grid, cv3, scoringneg_mean_absolute_error, n_jobs-1, verbose1, ) search.fit(X_train, y_train) print(search.best_params_)逻辑说明这里使用三折交叉验证但必须注意GridSearchCV默认的KFold是随机切分在时间序列数据里会再次引入数据泄漏。所以在配合库存预测项目时我不会直接用GridSearchCV而是使用TimeSeriesSplit或干脆手动方式评估不同参数组合在valid集上的表现。我做参数调整时的具体建议是n_estimators300 棵之后收益递减。如果你的数据只有几万行100 到 200 棵足够几十万行时 300 棵会更稳定。采用随机森林需要跑多长时间这个问题完全取决于数据规模。几万行时 200 棵树大约十几秒百万行就要几分钟。max_depth库存数据通常不超过 15 层。设得太深会记住促销噪音设得太浅会忽略门店差异。可以先跑一个深度列表 8、10、12、None对比验证集 MAE。min_samples_leaf零售库存经常有大量零销量 SKU这类 SKU 会让树末端的样本非常稀疏。我建议设到 5 以上如果数据里零库存占比高甚至可以设到 10。还有一个隐藏参数值得关注max_features。默认取sqrt在特征数量少于 10 个时几乎每次分裂只考虑 3 个特征会导致每棵树差异偏大。如果你的特征总数只有 5 个我建议设置max_features3或None让每次分裂有机会看到全部特征。4. 库存可视化从单店拟合曲线到可视化大屏的数据导出4.1 单 SKU 拟合曲线先看一个例子再相信整体指标MAE 是整体指标但库存预测落地时店长关心的是某一个具体商品明天会不会断货。所以建模完成后第一步可视化应该是挑一个销量波动较大的 SKU画出实际库存和预测库存的对比折线。import matplotlib.pyplot as plt sample_sku valid[valid[sku_id] SKU-10086].copy() sample_sku[pred_stock] model.predict(sample_sku[features].fillna(0)) plt.figure(figsize(12, 5)) plt.plot(sample_sku[date], sample_sku[stock_quantity], label实际库存) plt.plot(sample_sku[date], sample_sku[pred_stock], label预测库存) plt.title(单SKU库存走势对比) plt.xlabel(日期) plt.ylabel(库存数量) plt.legend() plt.savefig(output/sku_10086_stock.png, dpi200, bbox_inchestight)逻辑说明sample_sku只保留验证集中某个 SKU 的样本再调用已经训练好的model.predict把预测结果放回原 DataFrame方便和实际库存画在同一个坐标轴上。bbox_inchestight保存图片时会自动裁掉多余白边适合直接贴进报表。参数说明图的大小figsize按 12:5 的宽高比设置看时间序列折线比较舒服dpi200在放大时不会模糊如果你做课程报告或者打印至少需要 300 dpi。如果发现折线中预测值完全是一条直线说明特征里没有时间变化信息检查一下lag_7_sales是否真的生成了不同值。4.2 门店 x 品类的库存热力图用 seaborn 快速发现积压区域单 SKU 曲线适合诊断不适合管理层做决策。管理者更想知道哪个门店的哪个品类库存最高这时候需要一张热力图。import seaborn as sns valid_reg valid.copy() valid_reg[pred_stock] model.predict(valid[features].fillna(0)) pivot_actual valid_reg.pivot_table( indexstore_id, columnscategory, valuesstock_quantity, aggfuncmean, ) pivot_predict valid_reg.pivot_table( indexstore_id, columnscategory, valuespred_stock, aggfuncmean, ) fig, axes plt.subplots(1, 2, figsize(16, 6)) sns.heatmap(pivot_actual, axaxes[0], cmapYlOrRd, cbar_kws{label: 实际库存}) sns.heatmap(pivot_predict, axaxes[1], cmapYlOrRd, cbar_kws{label: 预测库存}) axes[0].set_title(实际平均库存热力图) axes[1].set_title(预测平均库存热力图) plt.tight_layout() plt.savefig(output/store_category_heatmap.png, dpi150)逻辑说明这里把验证集里每个门店、每个品类的库存取平均值再透视为行是门店、列是品类的矩阵。sns.heatmap按数值大小映射颜色深红色代表库存根本浅黄色代表库存较低。左右两张图对比如果某个单元格实际是深红、预测是浅黄说明模型认为这个品类该降库存了但实际情况还没降下来这正是要提醒运营的异常点。参数说明aggfuncmean使用日均库存而非期末库存因为单日盘点会受到补货到货的突然影响日均值更稳定。cmapYlOrRd的颜色顺序从黄到红颜色越深库存越高符合业务直觉。如果你的品类数量太多比如超过 20 个建议按销量 Top 20 品类筛选后再画否则热力图横向密密麻麻没法看。4.3 把预测结果整理成可视化大屏要用的三张表很多企业级数据可视化项目最后都要求做一张大屏而不是只有本地 matplotlib 图片。大屏的数据来源通常是数据库或 CSV我一般会把预测结果导出成三张标准表分别是门店汇总表、商品预警表、时序趋势表。result valid[[date, store_id, sku_id, category, stock_quantity]].copy() result[pred_stock] pred daily_summary ( result.groupby([date, store_id])[[stock_quantity, pred_stock]] .sum() .reset_index() ) daily_summary.to_csv(output/forecast_store_daily.csv, indexFalse) sku_alert ( result.groupby([store_id, sku_id]) .agg( avg_stock(stock_quantity, mean), avg_pred_stock(pred_stock, mean), ) .reset_index() ) sku_alert[stock_risk] (sku_alert[avg_pred_stock] 5).astype(int) sku_alert.to_csv(output/forecast_sku_alert.csv, indexFalse) trend ( result.groupby(date)[[stock_quantity, pred_stock]] .mean() .reset_index() ) trend.to_csv(output/forecast_time_trend.csv, indexFalse)逻辑说明第一张表forecast_store_daily.csv给出每天每个门店的总库存和预测总库存适合大屏顶部的时间趋势折线图。第二张表forecast_sku_alert.csv对每个门店 SKU 计算平均预测库存如果低于等于 5 件就打上风险标记这个阈值你可以按业务调整比如药店保健品可以设 2 件生鲜电商要设 20 件以上。第三张表forecast_time_trend.csv是全平台整体库存均值变化趋势适合大屏底部的滚动指标。参数说明stock_risk阈值设为 5 时会产生大量预警因为周末促销后很多 SKU 都会低于 5 件。我在实际项目中会用分位数法例如把所有预测库存低于 5% 分位数的 SKU 标记为风险这样预警数量不至于爆炸。导出 CSV 时统一使用indexFalse免得大屏对接时多出一列索引。5. 库存预测避坑清单5 个最容易翻车的现场5.1 数据泄漏训练时用了不该出现的未来信息现象验证集 MAE 只有 3看起来很漂亮但在测试集上 MAE 直接跳到 25。原因构建特征时用了未来的销售量比如不小心把shift(-7)当成shift(7)模型等于提前看到了下周销量预测只是把答案抄了一遍。解决检查所有shift的窗口方向训练和验证切分后重新生成特征绝不使用跨切分边界的滚动窗口。5.2 零销量 SKU 太多模型学会偷懒现象预测结果里大量 SKU 都是 0店长看了说“你这不如直接按库存表抄”。原因零售店有大量长尾 SKU一个月只卖几件决策树在分裂时发现预测 0 的平方误差最小于是学成了零回归。解决先把月销量低于 5 件的 SKU 单独分出来。经验是对零销量 SKU 不建随机森林而是直接用“上次销售日期 平均补货周期”做规则判断或者训练一个分类器先判断是否有销量再回归预测具体数值。5.3 随机森林无法外推大促预测集体偏低现象促销前库存每天都在涨模型预测却一直贴着历史最大值。原因随机森林的叶节点输出是训练样本均值训练数据里没有出现过 8000 件的库存预测永远不会超过 8000。解决一方面对促销日单独建模另一方面把预测目标替换成“库存变化量”而不是“库存绝对值”。预测变化量时即使历史库存最大只有 8000模型也能预测出 1500 的变化从而得到 9500。5.4 feature_importance 被高相关特征误导现象特征重要性显示rolling_14_sales最重要但把它删掉后模型效果反而更好。原因这个特征和lag_7_sales高度相关特征重要性会在相关特征之间分散或掩盖导致判断错误。解决用排列重要性permutation_importance或 SHAP 值替代默认的feature_importances_。随机森林默认重要性计算的是节点不纯度下降受特征选择顺序影响很大不能作为唯一决策依据。5.5 超参数调优时忘了固定随机种子现象同一组参数两次运行 MAE 差了 2 倍你以为参数出了问题其实是模型随机性。原因每棵树的样本抽样和特征选择都是随机的如果不固定random_state结果自然不稳定。解决训练、调参、最终测试全部使用相同的random_state。调参时用TimeSeriesSplit而非默认的KFold因为库存数据一旦被随机切分验证结果会虚高这是这条路线里最容易忽略的一个坑。6. 从预测到补货建议验证模型价值的小技巧当模型跑通、可视化做完之后真正让项目产生业务价值的是把预测结果换算成补货建议。我常用的方法是计算“预测可用天数”当前库存除以未来 7 天平均预测销量。如果预测可用天数小于补货提前期就生成补货清单。这段逻辑不复杂但能把随机森林的输出变成门店店长能直接执行的建议。stock_on_hand valid.groupby([store_id, sku_id])[stock_quantity].last() pred_sales_7day ( valid.assign(pred_salesmodel.predict(valid[features].fillna(0))) .groupby([store_id, sku_id])[pred_sales] .mean() * 7 ) lead_time 5 # 门店补货提前期单位天 replenish pd.DataFrame({ current_stock: stock_on_hand, pred_sales_7day: pred_sales_7day, }) replenish[stock_days] replenish[current_stock] / replenish[pred_sales_7day].replace(0, 1) replenish[suggest_order] replenish[stock_days] lead_time这段代码的输出是一张带suggest_order布尔值的表直接给仓库配货员使用。我在实际项目中的经验是业务方很难相信一个黑匣子的库存绝对值预测但他们对“哪些 SKU 需要在 5 天内补货”这种判断题接受度很高因为规则透明且可以纠错。另外建议做一次真正的业务验证选取 10 家门店跑一个月预测补货另外 10 家门店维持原来的手工补货月底对比缺货次数和滞销库存金额。随机森林模型能不能提效只有这种对比能下结论。我自己踩过的坑是第一版模型只在可视化大屏上很好看缺货率反而没改善原因是预测的是库存量而不是补货需求量店铺库存高也不代表补货正确。改成直接预测“未来 7 天销量”再结合当前库存算补货缺货率才明显下降。这套随机森林方案之所以值得投入是因为它不挑硬件、不依赖复杂架构又能跑出可解释的中短期库存预测结果。如果后续数据量涨上来再考虑换 LightGBM 或者把模型嵌入定时调度这个方向的成本曲线非常平滑。希望我的这些踩坑经验能让你少走一段弯路把精力花在真正影响预测效果的业务和数据理解上。本文还有配套的精品资源点击获取
返回列表