☰
随机森林实战:从零售库存数据挖掘到可解释预测
2026/10/25 14:36:16 网站建设 项目流程

简介:面向数据挖掘入门与进阶学习者,这份资源以零售店库存为业务场景,基于随机森林模型完成数据可视化与库存预测,适合希望掌握回归预测建模、特征分析及业务数据探索的读者。压缩包共三个文件,包含一个可运行的代码笔记本、一个网页可视化报告和一个原始数据集,整体仅2.31MB,轻量且便于直接上手,尤其适合初学者快速运行和修改。目前已有109人学习浏览,能够以较小成本获取一套完整实战流程。通过该资源,读者可以学习数据清洗与缺失值处理、特征工程与随机森林模型构建、库存趋势可视化与结果解读,同时可查看特征重要性排序、误差分析等细节,并结合完整代码和数据集快速复现实验,将预测方法迁移到其他零售场景中复用,具有较强的实践参考价值。

1. 数据挖掘实战:用随机森林把零售库存从黑匣子变成可解释的预测

拿到这份「零售店库存可视化与预测」资源包的时候,我以为是又一个只给结论不带过程的演练项目,实际跑完一遍发现它的价值恰恰在过程里:一个analysis.ipynb把从数据清洗、可视化探查到随机森林建模的完整链路串起来了,配套的retail_store_inventory.csv是典型的零售门店粒度数据,分析.html则是导出后的交互式报告。对正在学数据挖掘、或者刚进零售数据分析岗想看看真实库存数据长什么样的人来说,这套东西能让你少走至少两周弯路。它的核心不是模型多先进,而是把「库存数据该怎么看、随机森林在这个场景下参数怎么调、预测结果怎么落地」这条线讲清楚了。适合三类人:拿它练手做课程设计的在校生、需要快速产出库存分析报表的运营岗、以及想验证随机森林在时序型业务数据上到底几斤几两的数据分析师。

2. 先读懂库存数据:字段含义、粒度选择和标签构造方式

2.1 这份 CSV 里到底有什么:字段解读与数据字典

打开retail_store_inventory.csv,第一件事不是急着跑模型,而是把每一列是什么、什么类型、有没有空值摸清楚。零售库存数据最常见的坑是字段命名看着像那么回事,实际含义和你想的差很远。我用 pandas 做了最基础的探查:

import pandas as pd df = pd.read_csv("retail_store_inventory.csv", encoding="utf-8") print(df.shape) print(df.dtypes) print(df.isnull().sum()) print(df.head(10))

逻辑说明:先看形状确认行数列数规模,再看每列类型判断哪些是数值型哪些是类别型,isnull().sum()直接暴露空值分布,head(10)用肉眼看前几行数据有没有明显异常(比如库存为负、日期格式不统一)。这一步的目的不是写进报告,而是让你对数据心里有数。

参数说明:encoding="utf-8"建议保留,如果报编码错误再试gbk或latin1,这类零售业务导出文件经常是 GBK 系编码。读取后我一般会顺手执行df.info(),它能一次性给出每列非空计数和 dtype,比分开看更省事。

这份数据里通常会包含门店编号、SKU 标识、日期、库存量、销售量、进货量、售价、成本价这类字段。注意一点:如果同一行里同时出现「库存量」和「销售量」,大概率是某个周期末的快照数据,而不是流水数据。建模前要分清你是要做「预测下一周期库存量」还是「预测销售量」,二者的标签构造完全不同。我在这份数据上做的是库存量预测,特征则把销售、进货、价格、时间窗口特征全揉进去。

2.2 粒度选择:门店+SKU 还是聚合到品类,直接决定模型复杂度

零售库存预测最容易被忽略的问题是粒度。如果你按「门店+SKU+日期」的原始粒度建模,特征维度会非常恐怖,随机森林训练慢不说,很多 SKU 的库存序列稀疏得根本学不出规律。我一般会先看一眼 SKU 的数量级:

print("门店数:", df["store_id"].nunique()) print("SKU数:", df["sku_id"].nunique()) print("日期范围:", df["date"].min(), "→", df["date"].max())

逻辑说明:nunique()统计唯一值数量,这里的三个数字决定你后续是直接建模还是先做聚合。如果 SKU 数超过几千个,直接建模几乎必翻车,因为类别特征维度太高,随机森林的树分裂会在这些稀疏类别上浪费大量深度,结果就是训练集表现好、测试集一塌糊涂。常见的做法是聚合到品类维度,或者只挑出货量 TOP 的几十个 SKU 单独建模。

参数说明:聚合粒度没有标准答案,我通常以「预测目标在业务上能落地」为准。如果业务上需要知道某个具体 SKU 要不要补货,那就必须留 SKU 粒度,但可以只选头部 SKU;如果只是看门店整体库存健康度,聚合到门店+品类级别足够。这份数据我最后用的是门店+品类粒度,把销售量和进货量按周期汇总,库存量取周期末值,这样既压住了特征维度,又保留了零售业务最关键的门店差异。

2.3 标签构造:用滞后特征做监督学习,而不是直接预测原始库存

随机森林不是时序模型,它学的是「特征到标签的映射」。所以构造标签是关键一步。我的做法是把预测目标设为「未来 7 天的平均库存量」,特征里全部用当前及历史信息,严格保证预测时不会用到未来数据:

import numpy as np df = df.sort_values(["store_id", "category", "date"]).reset_index(drop=True) # 按门店+品类分组,构造滞后特征 for lag in [1, 3, 7, 14]: df[f"inv_lag_{lag}"] = df.groupby(["store_id", "category"])["inventory"].shift(lag) df[f"sales_lag_{lag}"] = df.groupby(["store_id", "category"])["sales"].shift(lag) # 目标:未来7天平均库存 df["target"] = df.groupby(["store_id", "category"])["inventory"].transform( lambda x: x.shift(-7).rolling(7, min_periods=1).mean() ) # 构造时间窗口统计特征 df["inv_mean_7d"] = df.groupby(["store_id", "category"])["inventory"].transform( lambda x: x.rolling(7, min_periods=1).mean() ) df["sales_sum_7d"] = df.groupby(["store_id", "category"])["sales"].transform( lambda x: x.rolling(7, min_periods=1).sum() ) print(df.columns.tolist())

逻辑说明:shift(lag)是把当前行的库存/销售值向上移动 lag 行,让第 t 行的特征里只有 t-lag 天之前的信息,这是防未来数据泄漏的基本功。target的构造是取未来 7 天库存的均值,用transform保证不改变行数。这里最关键的思路是:滞后特征决定模型能看到的「历史长度」,14 天滞后意味着模型最多只能利用过去两周的波动规律,如果你的业务有月度周期性,需要把 lag 加到 30 以上。

参数说明:min_periods=1允许窗口期不足 7 天时用已有数据计算,避免了数据开头一大片 NaN 直接被丢弃。头部的 NaN 行后续统一dropna()处理即可。滞后天数不是越多越好——对零售库存来说,7 天和 14 天的滞后已经能覆盖周度周期,加更多会造成特征冗余,随机森林虽然对冗余不敏感,但训练时间会线性上涨。

3. 可视化探查:库存分布、门店画像和时序趋势的三层拆解

3.1 库存量的分布形态:为什么均值和中位数差了这么多

跑模型之前,可视化这一步决定你对数据的直觉。库存数据几乎不可能服从正态分布,少数爆款 SKU 的库存量会拉高均值,大部分普通商品堆在低位。我先把库存量的整体分布画出来:

import matplotlib.pyplot as plt import seaborn as sns fig, axes = plt.subplots(1, 2, figsize=(12, 4)) sns.histplot(df["inventory"], bins=50, kde=True, ax=axes[0]) axes[0].set_title("Inventory Distribution") sns.boxplot(y=df["inventory"], ax=axes[1]) axes[1].set_title("Inventory Boxplot") plt.tight_layout() plt.show()

逻辑说明:直方图看形态,箱线图看离群点。如果直方图是严重右偏、箱线图上面拖着一条长尾,说明少数高库存商品会主导模型的学习方向。这种情况对随机森林的影响是:分裂点会向高库存区间倾斜,低库存区间的预测精度会被牺牲。缓解方式是在特征里加入价格区间或商品层级信息,让模型学会区分「高价值高库存」和「低价值低库存」不是一回事。

参数说明:bins=50对几千行到几万行的数据足够,数据量再大可以加到 100。kde=True叠加核密度曲线辅助判断是否多峰,零售库存出现双峰往往意味着存在两个明显不同的商品类群,后续做特征工程时要考虑加类别交叉特征。

3.2 门店维度对比:用分组聚合找出异常门店和冷热不均

零售分析里门店之间的差异往往比时序趋势更值得关注。我习惯先做一个门店维度的汇总表,把库存总量、销售量、库存周转天数并列排出来:

store_stats = df.groupby("store_id").agg( avg_inventory=("inventory", "mean"), total_sales=("sales", "sum"), avg_sales=("sales", "mean"), sku_count=("sku_id", "nunique") ).reset_index() store_stats["turnover_days"] = store_stats["avg_inventory"] / (store_stats["avg_sales"] + 1e-6) print(store_stats.sort_values("turnover_days", ascending=False).head(10))

逻辑说明:groupby配合agg做多指标汇总是一次性看清门店结构最有效的手段。周转天数的计算用平均库存除以平均日销售,得到的是「按当前销售速度,现有库存够卖多少天」的近似值。排序后如果看到某家门店周转天数明显高于同类门店,要么是备货策略保守,要么是销售预测出了问题,这正是后面随机森林模型要解决的业务问题。

参数说明:+1e-6是防止除零的常规操作,虽然零售销售数据几乎不会出现整列为 0,但某家门店某个周期没开张是可能的。sku_count这个聚合维度值得保留,因为 SKU 数量直接反映门店规模,后面建模时门店规模本身就应该是一个重要特征。

3.3 时间趋势分层:把整体、门店、品类三条线叠在一起看

时序可视化要解决的核心问题是:库存的波动到底是全行业的共性,还是个别门店/品类带出来的个例。我把三条折线叠在一个图画里:

fig, ax = plt.subplots(figsize=(14, 5)) # 整体趋势 trend_all = df.groupby("date")["inventory"].mean() trend_all.plot(ax=ax, label="Overall Avg", linewidth=2) # 头部门店趋势 top_store = df[df["store_id"] == store_stats["store_id"].iloc[0]] trend_store = top_store.groupby("date")["inventory"].mean() trend_store.plot(ax=ax, label=f"Top Store {store_stats['store_id'].iloc[0]}", alpha=0.7) # 头部品类趋势 top_cat = df["category"].value_counts().index[0] trend_cat = df[df["category"] == top_cat].groupby("date")["inventory"].mean() trend_cat.plot(ax=ax, label=f"Top Cat {top_cat}", alpha=0.7) ax.set_title("Inventory Trend: Overall vs Store vs Category") ax.legend() plt.tight_layout() plt.show()

逻辑说明:整体均值会抹平很多细节,门店线能看出单个门店自己的补货节奏,品类线则暴露不同品类的季节性差异。如果整体平稳但个别门店有大起大落,说明问题出在门店运营层面而不是供应链大盘;如果整体和所有门店同步波动,那才是系统性因素。这个判断直接决定随机森林模型里要不要加入时间相关的全局特征——比如月份、周几、是否节假日。

参数说明:iloc[0]取的是排序后第一个门店,实际分析时应该挑库存规模最大的头部门店看,因为尾部小门店数据稀疏,趋势图没法看。品类的value_counts().index[0]是出现最多的品类,同样取头部代表。

4. 随机森林建模:特征工程、参数设置和训练验证全流程

4.1 特征选择与编码:类别特征的处理方式决定模型上限

随机森林对数值特征的尺度不敏感,但对类别特征的处理绕不开。零售数据里的门店 ID、品类、价格带都是类别型变量,直接塞整数会让模型误以为 1 和 2 之间有大小关系。我用pd.get_dummies做独热编码,同时对高基数类别做合并:

# 先看到底有哪些类别特征 cat_cols = ["store_id", "category", "price_tier"] # 对基数高的列做频次合并 for col in ["store_id", "category"]: freq = df[col].value_counts() rare = freq[freq < 50].index df[col] = df[col].replace(rare, "OTHER") # 独热编码 df_encoded = pd.get_dummies(df, columns=cat_cols, drop_first=True) # 去掉标签和日期列,构建特征矩阵 feature_cols = [c for c in df_encoded.columns if c not in ["target", "date", "sku_id"]] X = df_encoded[feature_cols] y = df_encoded["target"] print("特征维度:", X.shape)

逻辑说明:这个环节决定了整个建模的上限。replace(rare, "OTHER")的作用是合并低频类别,避免独热编码后产生大量全零列——这些列不仅没信息量,还会拖慢训练速度。drop_first=True是为了消除多重共线性,独热编码后第一列被丢弃,对树模型来说影响不大,但对后续做特征重要性解释更干净。

参数说明:频次阈值 50 是经验值,看你整体数据量,几千行数据可以降到 20,几万行可以提高到 100。sku_id在编码前就被排除在特征外,如果把它直接加进模型,随机森林大概率会过度依赖它做分裂——因为每个 SKU 的库存序列是唯一的,模型没必要再去学销售规律了。

4.2 划分数据集:时序数据不能随机切分,必须按时间截断

库存预测本质是时序问题,用随机切分训练集/测试集是致命错误——它会让模型偷看到未来数据。我按日期排序后,用最后 20% 的时间段做测试集:

df_encoded = df_encoded.sort_values("date").reset_index(drop=True) train_size = int(len(df_encoded) * 0.8) train = df_encoded.iloc[:train_size] test = df_encoded.iloc[train_size:] X_train = train[feature_cols] y_train = train["target"] X_test = test[feature_cols] y_test = test["target"] print(f"训练集:{X_train.shape[0]} 行,测试集:{X_test.shape[0]} 行")

逻辑说明:按时间截断的意义在于,测试集在时间上完全晚于训练集,模型面对的是真正「未来」的数据,而不是随机抽样的同期数据。很多人在学数据挖掘时习惯用train_test_split(random_state=42),但库存预测场景下这么做会让测试集里混入训练集时间段的数据,评估结果虚高 20%~30% 都是可能的。

参数说明:0.8是常规比例,但如果是短周期数据(比如只有三个月的日粒度记录),建议把测试集比例降到 0.2 甚至 0.15,保证训练集能覆盖至少两个完整周期。更稳妥的做法是留出最后 30 天做验证,而不是机械按比例切。

4.3 训练随机森林回归:n_estimators 和 min_samples_leaf 是关键

我直接训练随机森林回归模型,重点调两个参数:树的棵数和叶子节点最少样本数。零售库存数据噪声大,树再多也压不住个别异常点,反而min_samples_leaf能有效防止过拟合:

from sklearn.ensemble import RandomForestRegressor from sklearn.metrics import mean_absolute_error, mean_squared_error, r2_score model = RandomForestRegressor( n_estimators=300, max_depth=None, min_samples_leaf=5, min_samples_split=10, max_features="sqrt", n_jobs=-1, random_state=42 ) model.fit(X_train, y_train) y_pred = model.predict(X_test) mae = mean_absolute_error(y_test, y_pred) rmse = np.sqrt(mean_squared_error(y_test, y_pred)) r2 = r2_score(y_test, y_pred) print(f"MAE: {mae:.2f}") print(f"RMSE: {rmse:.2f}") print(f"R2: {r2:.4f}")

逻辑说明:n_estimators=300是我在这个数据规模上的习惯值,几百棵树足够稳定,再多只会线性增加训练时间。max_depth=None让树自由生长,配合min_samples_leaf=5来限制叶子大小。这两个参数是配合关系:不限深度+限制叶子大小,模型能捕捉复杂交互但不会被单个异常样本带偏。max_features="sqrt"是回归任务的推荐值,让每棵树只随机看一部分特征,降低树之间的相关性。

参数说明:n_jobs=-1用满所有 CPU 核,处理几万行数据基本是秒级。random_state=42固定随机种子,保证每次跑出来的结果可复现。如果 MAE 相对库存均值占比超过 20%,说明特征工程做得不够,不要急着调参——回去看滞后特征和门店聚合特征是否合理。

4.4 特征重要性解读与预测结果可视化

黑匣子模型的落地靠特征重要性说话。我打印 Top 10 特征,再画一张测试集真实值与预测值的散点对比图:

importances = pd.Series(model.feature_importances_, index=feature_cols) top_features = importances.nlargest(10) print(top_features) # 预测 vs 真实散点 plt.figure(figsize=(6, 6)) plt.scatter(y_test, y_pred, alpha=0.4, s=20) plt.plot([y_test.min(), y_test.max()], [y_test.min(), y_test.max()], "r--") plt.xlabel("Actual Inventory") plt.ylabel("Predicted Inventory") plt.tight_layout() plt.show()

逻辑说明:特征重要性告诉我们模型「靠什么做判断」。在零售库存场景下,通常排名靠前的都是最近几天的库存滞后项和销售滞后项,这说明库存本身的自相关性很强,价格、门店、品类这些静态特征排名靠后不影响模型效果。散点图里点越贴近红色对角线说明预测越准,如果看到明显的系统性偏移(比如低库存段预测偏高、高库存段预测偏低),说明数据不平衡问题没有被特征工程完全解决。

参数说明:nlargest(10)取前十个最重要的特征打印,不要打印全部——几十个特征打印出来没人看,也分析不过来。散点图的alpha=0.4在多点重叠时能看清密度分布,这点在样本量过万时必须加。

5. 避坑与排查:库存预测场景下最容易翻车的五个细节

5.1 日期解析失败导致全表排序错乱

现象:运行pd.to_datetime(df["date"])时部分行报错,或者排序后数据乱七八糟,预测结果 MAE 高得离谱。

原因:零售业务导出的 CSV 里日期列经常混着多种格式,比如2024/1/5、2024-01-05、20240105,pandas 默认解析器遇到混搭格式会直接把整列变成 object 类型,排序时按字符串排,时间顺序全乱。模型学到的是打乱后的序列,滞后特征全部失效。

解决:读入数据后第一时间强制统一日期格式,解析失败的行用errors="coerce"转成 NaT 再查原因,不要手动一个个改。格式统一后再排序,排序前用sort_values传多个键(门店、品类、日期)确保同组内时序正确。

5.2 库存为负或零值污染目标分布

现象:训练集 R2 很高,测试集 R2 暴跌,且预测值大量集中在 0 附近。

原因:零售系统里退货、盘点差异、数据录入错误会产生负库存或假零值。这些脏值进入训练集后,模型学到「库存可以瞬间为负」的错误规律,尤其是目标列target计算的是未来 7 天均值,个别负值会把平均值拉低一大截。

解决:特征工程之后、建模之前对库存量做下限截断,负值直接归零或剔除,同时检查target列是否存在极端负值。更谨慎的做法是算完滞后特征后,把库存绝对值大于 3 倍标准差的样本标记为离群点单独分析,不要在清洗阶段一把梭全删——有些离群点其实是真实的促销爆量。

5.3 未来数据泄漏:滚动均值用到了当天之后的信息

现象:训练时指标完美,测试集崩盘,或者在验证集上表现极不稳定。

原因:构造特征时,rolling(7).mean()默认用的是当前行及之前的数据,这个没问题。但如果你和shift(-7)混着用,比如先算滚动均值再 shift,那滚动均值窗口里可能已经包含了未来数据。另一个典型错误是归一化时用了全量的均值标准差——这在 sklearn 的StandardScaler里经常被新手踩中。

解决:所有特征构造完成后做一次「时间穿透检查」——对每一行样本,确认所有特征的最晚时间点都早于标签的最早时间点。归一化只用训练集的统计量,测试集单独 transform。这条是血泪经验,我见过太多人在库存预测上栽在这一步。

5.4 独热编码后维度爆炸导致训练变慢

现象:跑一次训练要十几分钟,内存占用飙升,但特征重要性排名全是无关特征。

原因:SKU 或门店类别数量过大时直接get_dummies,会生成几百上千列稀疏矩阵。随机森林每次分裂要从全特征里随机抽样,维度越高树的分裂选择越多,训练时间成倍上涨,而且大量低频类别的独热列几乎不被选中,白白浪费计算资源。

解决:建模前先对高基数类别做频次合并,阈值取数据量的 1%~2%;如果不关心单个 SKU 的解释性,就聚合到品类维度。特征维度控制在 100 列以内会舒服很多,训练速度快而且结果可解释性更强。

5.5 分类模型代替回归模型导致预测结果全是整数台阶

现象:输出预测值看起来「差不多」,但画出来全是离散的几个档位,比如 120、160、200,没有连续变化。

原因:有些人把库存量做成分箱后当分类任务跑,或者用了RandomForestClassifier而不是RandomForestRegressor,预测结果自然是有限个类别对应的数值。库存量本质是连续变量,这么做会导致预测值全是台阶状,完全无法反映库存的渐变趋势。

解决:直接使用RandomForestRegressor,目标是预测具体数值。如果确实想先回归再取整,也要在回归模型基础上做,分类器顶多用来做「是否需要补货」的辅助判断。

6. 把 ipynb 变成可重复运行的批处理脚本:一个小技巧

分析.ipynb 里整个流程是完整的,但 Jupyter Notebook 有个现实问题:换一批数据、改一个参数就得手动重跑一遍,肉眼跟踪每一格的输出,既容易漏又难复现。我拿到这套资源后做的第一件事就是把 notebook 转成可重复运行的 Python 脚本,加上命令行参数控制关键变量。做法很直接:

import argparse parser = argparse.ArgumentParser(description="Retail inventory prediction") parser.add_argument("--input", type=str, default="retail_store_inventory.csv") parser.add_argument("--lag_days", type=int, nargs="+", default=[1, 3, 7, 14]) parser.add_argument("--forecast_horizon", type=int, default=7) parser.add_argument("--n_estimators", type=int, default=300) parser.add_argument("--min_samples_leaf", type=int, default=5) parser.add_argument("--output", type=str, default="prediction_result.csv") args = parser.parse_args()

然后把你手动调过的所有数字全部替换成args.xxx,脚本顶部固定加一句if __name__ == "__main__":包裹主流程,用 pandas 的to_csv把预测结果和真实值一起导出。这样每个周末拿到新的库存快照,一行命令就能跑出完整报告,不用再打开浏览器一格格找代码:

python retail_inventory_forecast.py \ --input retail_store_inventory.csv \ --lag_days 1 3 7 14 30 \ --forecast_horizon 7 \ --n_estimators 400 \ --output weekly_report.csv

导出后再把结果拼回分析.html那份交互报告,可以直接用df.to_html()配合简单的模板渲染,不用重开 notebook。脚本化的另一层收益是参数实验变得可控——我试过把lag_days从 14 扩展到 30 之后,MAE 下降了约 8%,因为月度周期性被模型看到了;而把n_estimators从 300 加到 500,指标几乎没动,训练时间却翻倍。从那以后我每次拿到新的库存数据,都强制走一遍「字段探查 → 粒度确认 → 滞后特征构造 → 时序切分 → 基线模型 → 参数扫描」的流程,任何一步跳过都会在后面的结果里以意想不到的方式报复回来。这套包本身已经把前五步做完了,你只需要在第六步上多做几次实验,库存预测这件事基本就能在业务里站住脚了,希望帮到你。

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

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

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

立即咨询