资讯详情

资讯详情

随机森林实战:从零售库存数据挖掘到可解释预测

简介面向数据挖掘入门与进阶学习者这份资源以零售店库存为业务场景基于随机森林模型完成数据可视化与库存预测适合希望掌握回归预测建模、特征分析及业务数据探索的读者。压缩包共三个文件包含一个可运行的代码笔记本、一个网页可视化报告和一个原始数据集整体仅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, encodingutf-8) print(df.shape) print(df.dtypes) print(df.isnull().sum()) print(df.head(10))逻辑说明先看形状确认行数列数规模再看每列类型判断哪些是数值型哪些是类别型isnull().sum()直接暴露空值分布head(10)用肉眼看前几行数据有没有明显异常比如库存为负、日期格式不统一。这一步的目的不是写进报告而是让你对数据心里有数。参数说明encodingutf-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(dropTrue) # 按门店品类分组构造滞后特征 for lag in [1, 3, 7, 14]: df[finv_lag_{lag}] df.groupby([store_id, category])[inventory].shift(lag) df[fsales_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_periods1).mean() ) # 构造时间窗口统计特征 df[inv_mean_7d] df.groupby([store_id, category])[inventory].transform( lambda x: x.rolling(7, min_periods1).mean() ) df[sales_sum_7d] df.groupby([store_id, category])[sales].transform( lambda x: x.rolling(7, min_periods1).sum() ) print(df.columns.tolist())逻辑说明shift(lag)是把当前行的库存/销售值向上移动 lag 行让第 t 行的特征里只有 t-lag 天之前的信息这是防未来数据泄漏的基本功。target的构造是取未来 7 天库存的均值用transform保证不改变行数。这里最关键的思路是滞后特征决定模型能看到的「历史长度」14 天滞后意味着模型最多只能利用过去两周的波动规律如果你的业务有月度周期性需要把 lag 加到 30 以上。参数说明min_periods1允许窗口期不足 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], bins50, kdeTrue, axaxes[0]) axes[0].set_title(Inventory Distribution) sns.boxplot(ydf[inventory], axaxes[1]) axes[1].set_title(Inventory Boxplot) plt.tight_layout() plt.show()逻辑说明直方图看形态箱线图看离群点。如果直方图是严重右偏、箱线图上面拖着一条长尾说明少数高库存商品会主导模型的学习方向。这种情况对随机森林的影响是分裂点会向高库存区间倾斜低库存区间的预测精度会被牺牲。缓解方式是在特征里加入价格区间或商品层级信息让模型学会区分「高价值高库存」和「低价值低库存」不是一回事。参数说明bins50对几千行到几万行的数据足够数据量再大可以加到 100。kdeTrue叠加核密度曲线辅助判断是否多峰零售库存出现双峰往往意味着存在两个明显不同的商品类群后续做特征工程时要考虑加类别交叉特征。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, ascendingFalse).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(axax, labelOverall Avg, linewidth2) # 头部门店趋势 top_store df[df[store_id] store_stats[store_id].iloc[0]] trend_store top_store.groupby(date)[inventory].mean() trend_store.plot(axax, labelfTop Store {store_stats[store_id].iloc[0]}, alpha0.7) # 头部品类趋势 top_cat df[category].value_counts().index[0] trend_cat df[df[category] top_cat].groupby(date)[inventory].mean() trend_cat.plot(axax, labelfTop Cat {top_cat}, alpha0.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, columnscat_cols, drop_firstTrue) # 去掉标签和日期列构建特征矩阵 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_firstTrue是为了消除多重共线性独热编码后第一列被丢弃对树模型来说影响不大但对后续做特征重要性解释更干净。参数说明频次阈值 50 是经验值看你整体数据量几千行数据可以降到 20几万行可以提高到 100。sku_id在编码前就被排除在特征外如果把它直接加进模型随机森林大概率会过度依赖它做分裂——因为每个 SKU 的库存序列是唯一的模型没必要再去学销售规律了。4.2 划分数据集时序数据不能随机切分必须按时间截断库存预测本质是时序问题用随机切分训练集/测试集是致命错误——它会让模型偷看到未来数据。我按日期排序后用最后 20% 的时间段做测试集df_encoded df_encoded.sort_values(date).reset_index(dropTrue) 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_state42)但库存预测场景下这么做会让测试集里混入训练集时间段的数据评估结果虚高 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_estimators300, max_depthNone, min_samples_leaf5, min_samples_split10, max_featuressqrt, n_jobs-1, random_state42 ) 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(fMAE: {mae:.2f}) print(fRMSE: {rmse:.2f}) print(fR2: {r2:.4f})逻辑说明n_estimators300是我在这个数据规模上的习惯值几百棵树足够稳定再多只会线性增加训练时间。max_depthNone让树自由生长配合min_samples_leaf5来限制叶子大小。这两个参数是配合关系不限深度限制叶子大小模型能捕捉复杂交互但不会被单个异常样本带偏。max_featuressqrt是回归任务的推荐值让每棵树只随机看一部分特征降低树之间的相关性。参数说明n_jobs-1用满所有 CPU 核处理几万行数据基本是秒级。random_state42固定随机种子保证每次跑出来的结果可复现。如果 MAE 相对库存均值占比超过 20%说明特征工程做得不够不要急着调参——回去看滞后特征和门店聚合特征是否合理。4.4 特征重要性解读与预测结果可视化黑匣子模型的落地靠特征重要性说话。我打印 Top 10 特征再画一张测试集真实值与预测值的散点对比图importances pd.Series(model.feature_importances_, indexfeature_cols) top_features importances.nlargest(10) print(top_features) # 预测 vs 真实散点 plt.figure(figsize(6, 6)) plt.scatter(y_test, y_pred, alpha0.4, s20) 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)取前十个最重要的特征打印不要打印全部——几十个特征打印出来没人看也分析不过来。散点图的alpha0.4在多点重叠时能看清密度分布这点在样本量过万时必须加。5. 避坑与排查库存预测场景下最容易翻车的五个细节5.1 日期解析失败导致全表排序错乱现象运行pd.to_datetime(df[date])时部分行报错或者排序后数据乱七八糟预测结果 MAE 高得离谱。原因零售业务导出的 CSV 里日期列经常混着多种格式比如2024/1/5、2024-01-05、20240105pandas 默认解析器遇到混搭格式会直接把整列变成 object 类型排序时按字符串排时间顺序全乱。模型学到的是打乱后的序列滞后特征全部失效。解决读入数据后第一时间强制统一日期格式解析失败的行用errorscoerce转成 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(descriptionRetail inventory prediction) parser.add_argument(--input, typestr, defaultretail_store_inventory.csv) parser.add_argument(--lag_days, typeint, nargs, default[1, 3, 7, 14]) parser.add_argument(--forecast_horizon, typeint, default7) parser.add_argument(--n_estimators, typeint, default300) parser.add_argument(--min_samples_leaf, typeint, default5) parser.add_argument(--output, typestr, defaultprediction_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指标几乎没动训练时间却翻倍。从那以后我每次拿到新的库存数据都强制走一遍「字段探查 → 粒度确认 → 滞后特征构造 → 时序切分 → 基线模型 → 参数扫描」的流程任何一步跳过都会在后面的结果里以意想不到的方式报复回来。这套包本身已经把前五步做完了你只需要在第六步上多做几次实验库存预测这件事基本就能在业务里站住脚了希望帮到你。本文还有配套的精品资源点击获取
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →