简介:面向数据挖掘初学者与零售运营人员的实战资源,围绕零售店库存数据完成可视化分析与随机森林预测建模。包内包含完整数据集、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放从压缩包解出的原始 CSV,data/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(level=0, drop=True) ) 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_size=0.2, random_state=42),这在库存预测里会翻车。库存数据是强时间序列数据,如果随机抽样,训练集里会出现未来日期的样本,验证集里会出现过去日期的样本,模型学到的其实是“穿越时空”的规律,上线后一测真实未来数据就崩。
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_estimators=200, max_depth=12, min_samples_leaf=5, random_state=42, ) model.fit(X_train, y_train) pred = model.predict(X_valid) mae = mean_absolute_error(y_valid, pred) print(f"MAE: {mae:.2f}")逻辑说明:选这五个特征作为初始特征集,fillna(0)只用于处理缺失的销量特征,避免让训练报错;stock_quantity当作回归目标,这里预测的是某个门店某个 SKU 某一天的期末库存。MAE即平均绝对误差,单位与库存数量一致,比如 MAE 等于 8,意味着平均每个 SKU 每天预测误差 8 件。
参数说明:n_estimators=200是树的数量,太小会导致结果不稳定,太大只增加计算时间不带来明显精度提升;max_depth=12限制每棵树深度,防止决策树无限分裂记住训练集细节;min_samples_leaf=5强制每个叶节点至少 5 个样本,让预测结果更平滑;random_state=42固定随机种子,保证每次运行结果一致,方便复现和对比调参。
如果你跑完发现 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_state=42), param_grid, cv=3, scoring="neg_mean_absolute_error", n_jobs=-1, verbose=1, ) search.fit(X_train, y_train) print(search.best_params_)逻辑说明:这里使用三折交叉验证,但必须注意GridSearchCV默认的KFold是随机切分,在时间序列数据里会再次引入数据泄漏。所以在配合库存预测项目时,我不会直接用GridSearchCV,而是使用TimeSeriesSplit或干脆手动方式评估不同参数组合在valid集上的表现。
我做参数调整时的具体建议是:
n_estimators:300 棵之后收益递减。如果你的数据只有几万行,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_features=3或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", dpi=200, bbox_inches="tight")逻辑说明:sample_sku只保留验证集中某个 SKU 的样本,再调用已经训练好的model.predict,把预测结果放回原 DataFrame,方便和实际库存画在同一个坐标轴上。bbox_inches="tight"保存图片时会自动裁掉多余白边,适合直接贴进报表。
参数说明:图的大小figsize按 12:5 的宽高比设置,看时间序列折线比较舒服;dpi=200在放大时不会模糊,如果你做课程报告或者打印,至少需要 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( index="store_id", columns="category", values="stock_quantity", aggfunc="mean", ) pivot_predict = valid_reg.pivot_table( index="store_id", columns="category", values="pred_stock", aggfunc="mean", ) fig, axes = plt.subplots(1, 2, figsize=(16, 6)) sns.heatmap(pivot_actual, ax=axes[0], cmap="YlOrRd", cbar_kws={"label": "实际库存"}) sns.heatmap(pivot_predict, ax=axes[1], cmap="YlOrRd", cbar_kws={"label": "预测库存"}) axes[0].set_title("实际平均库存热力图") axes[1].set_title("预测平均库存热力图") plt.tight_layout() plt.savefig("output/store_category_heatmap.png", dpi=150)逻辑说明:这里把验证集里每个门店、每个品类的库存取平均值,再透视为行是门店、列是品类的矩阵。sns.heatmap按数值大小映射颜色,深红色代表库存根本,浅黄色代表库存较低。左右两张图对比,如果某个单元格实际是深红、预测是浅黄,说明模型认为这个品类该降库存了,但实际情况还没降下来,这正是要提醒运营的异常点。
参数说明:aggfunc="mean"使用日均库存而非期末库存,因为单日盘点会受到补货到货的突然影响,日均值更稳定。cmap="YlOrRd"的颜色顺序从黄到红,颜色越深库存越高,符合业务直觉。如果你的品类数量太多,比如超过 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", index=False) 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", index=False) trend = ( result.groupby("date")[["stock_quantity", "pred_stock"]] .mean() .reset_index() ) trend.to_csv("output/forecast_time_trend.csv", index=False)逻辑说明:第一张表forecast_store_daily.csv给出每天每个门店的总库存和预测总库存,适合大屏顶部的时间趋势折线图。第二张表forecast_sku_alert.csv对每个门店 SKU 计算平均预测库存,如果低于等于 5 件就打上风险标记,这个阈值你可以按业务调整,比如药店保健品可以设 2 件,生鲜电商要设 20 件以上。第三张表forecast_time_trend.csv是全平台整体库存均值变化趋势,适合大屏底部的滚动指标。
参数说明:stock_risk阈值设为 5 时会产生大量预警,因为周末促销后很多 SKU 都会低于 5 件。我在实际项目中会用分位数法,例如把所有预测库存低于 5% 分位数的 SKU 标记为风险,这样预警数量不至于爆炸。导出 CSV 时统一使用index=False,免得大屏对接时多出一列索引。
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_sales=model.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 或者把模型嵌入定时调度,这个方向的成本曲线非常平滑。希望我的这些踩坑经验能让你少走一段弯路,把精力花在真正影响预测效果的业务和数据理解上。
本文还有配套的精品资源,点击获取