我用 Python 预测 10 家门店未来 30 天销量:时间序列最容易踩的坑都在这里
通过网盘分享的文件:02 销售预测实战项目_完整版.zip 链接:
https://pan.baidu.com/s/1-WKULhDrKn-Mv0oLmn45rA?pwd=43q7 提取码: 43q72 年、7310 条门店流水,从趋势分解到备货决策的完整链路。含代码,数据在文末(开箱即跑)。
这是我做的第二个完整实战项目。第一个是电商行为分析的漏斗+RFM,这次换了个更有挑战的方向:预测。
为什么说更有挑战?因为预测要求模型对未来负责。描述过去可以慢慢分析,预测错了就是真金白银的损失——备货多了积压占资金,备货少了断货丢客人。
而时间序列预测里,有个坑能让你的模型"线下 95 分、上线 30 分",很多人栽在上面还不知道为什么。
一、数据说明
某连锁零售企业 10 家门店、2 年(2024-2025)的日销售数据:
| 字段 | 含义 |
|---|---|
| store_id | 门店编号(S01~S10) |
| date | 日期 |
| sales | 当日销售额(万元) |
| 城市 / 门店类型 | 门店属性(旗舰店/标准店/社区店) |
规模:7,310 条记录,总销售额 33,917 万元。数据为教学模拟,但业务结构完全真实(后面你会看到)。
二、先看清规律,再谈模型
拿到时间序列,第一步不是建模,是看。
图里那条浅色的日线噪声很大,但蓝色的 30 日滚动平均一出来,趋势立刻清楚了:整体在涨。
这是看时间序列的第一个技巧:日线看趋势等于自虐,直接上滚动平均。
total=df.groupby("date")["sales"].sum()ax.plot(total.index,total.rolling(30,center=True).mean())# 关键周季节:零售最强的规律
周一平均 3.76 万元,周六 5.57 万元——周末比周初高约 48%。
这个发现直接决定了一件事:备货绝不能按"日均"一刀切,必须按星期几分开算。
更细心的观察:右图显示各门店周末强度不同——社区店更依赖周末客流,标准店相对平缓。这是后面"分门店建模"的依据。
年季节:夏冬双峰
6-8 月和 10-12 月是两个高峰,3-4 月最低。典型零售结构。
还有一类重要事件:大促日(双11、618、国庆、春节前)会形成脉冲式尖峰。
关键认知:这些尖峰不是噪声,是信号。新手常犯的错是把它们当异常值删掉——那等于把模型最需要学的信息扔了。正确做法是做成显式特征。
三、时间序列分解:把规律拆成零件
把序列拆成三层:趋势 × 周季节 × 残差。分解后如果残差还是白噪声,说明规律提取干净了。
ACF:用数据决定滞后阶数
高手的做法不是"我猜 lag_7 有用",而是让数据说话:
ACF 在7、14、21、28处出现尖峰——这是周季节的"指纹"。它直接告诉我们:必须加 7 的倍数作为特征。
defacf(x,nlags=30):x=np.asarray(x,float);x=x[~np.isnan(x)];x=x-x.mean()n=len(x);denom=np.sum(x**2)return[np.sum(x[:n-k]*x[k:])/denomforkinrange(nlags+1)]四、特征工程:这里是分水岭
我构建了三类特征:
| 类型 | 特征 | 捕捉 |
|---|---|---|
| 日历 | 星期几、月份、是否周末、是否大促 | 季节与事件 |
| 滞后 | lag_1/2/3/7/14/28 | 自相关(惯性) |
| 滚动 | 近7/14/30天均值与标准差 | 近期水平与波动 |
结果符合直觉:roll_mean_7(近7日均值)与lag_7(上周同日)相关性最高——"近期水平 + 周同期"是预测的两大核心信号。
⚠️ 重点:数据穿越(Data Leakage)
这是时间序列第一大坑,我见过太多人栽在这里。
看这段代码,你觉得有问题吗?
df["ma7"]=df["sales"].rolling(7).mean()有。默认的rolling(7)是包含当前行的——也就是说,"今天"的特征里混进了"今天"的真实销量。而我们的任务是预测今天。等于考试前偷看了答案。
正确写法:
df["ma7"]=df["sales"].shift(1).rolling(7).mean()# 先整体后移,再滚动症状:线下评估漂亮得不像话(R² 0.99),一上线就崩。看到"好得不真实"的分数,第一反应就该查数据穿越。
还有两个隐蔽的穿越:
# ❌ 归一化用了全量数据(含未来)scaler.fit(df[feats])scaler.fit(train[feats])# ✅# ❌ 目标编码用了全量标签df["store_mean"]=df.groupby("store")["sales"].transform("mean")# 含未来# ✅ 只用训练集统计再映射五、评估:为什么不能随机切分
时间序列不能用随机 K 折交叉验证。
因为随机切分会把未来数据放进训练集,模型"见过未来",评估结果虚高。正确做法有三种:
| 方式 | 可用性 |
|---|---|
| 随机 K 折 | ❌ 严重穿越 |
| 时间切分(前80%训练) | ✅ 简单可用 |
| 滚动向前验证 | ✅✅最贴近真实上线 |
滚动向前验证长这样:
窗口1: [训练 1~100] → 预测 101~107 窗口2: [训练 1~107] → 预测 108~114 窗口3: [训练 1~114] → 预测 115~121每个窗口都用"当时可得的数据"训练,模拟业务真实节奏。多个窗口的平均表现才可信。
别忘了建立基线
任何模型都必须先打败朴素基线,否则毫无价值:
- 上周同日(
lag_7):“下周六 ≈ 上周六” - 近7日均值(
roll_mean_7)
零售数据的自相关很强,这两个基线往往已经"不太差"。如果复杂模型打不过lag_7,说明模型没用,别上线。
六、建模对比
我用 Ridge 回归和随机森林做了对比:
| 模型 | 特点 |
|---|---|
| Ridge 回归 | 系数可解释,roll_mean_7/lag_7权重最大 |
| 随机森林 | 更强,能捕捉"双11 且周六"这类交互效应 |
两者都能跟上销量的起伏节奏(周末高、促销尖),但都在极端促销日低估——这是所有预测模型的共同局限。
补充一个树模型的固有缺陷:随机森林无法外推——预测值不会超出训练集见过的范围。遇到持续增长或新高的场景会系统性低估。Ridge 没这个问题,但要保证趋势能线性外推。
七、找错:误差分析比调参有用
模型跑完,最重要的一步是分析它在哪错。
误差在门店间不均匀:社区店误差明显更大(销售波动本来就大)。一刀切的备货策略一定在这类门店出问题。
周末误差高于工作日——周末人流不确定性更强。结论:周末备货的安全系数必须单独上调。
八、从预测到决策:这才是业务买单的地方
老板不关心 MAE 是 0.8 还是 0.75,他关心该备多少货。
先给不确定性:只报"预测 5.2 万"是不专业的,要说"预测 5.2 万,大概率在 4.6~5.8 万之间"。
安全库存公式
建议备货 = 预测值 + z × 预测误差标准差 z = 1.28 → 缺货率约 10% z = 1.65 → 约 5% z = 2.33 → 约 1%为什么用 P90 而不是均值?
因为备货是不对称风险:
| 失误 | 后果 | 严重度 |
|---|---|---|
| 备少了 | 断货、丢客、伤口碑 | 高 |
| 备多了 | 占资金、临期损耗 | 中 |
所以取预测分布的高分位。这是风险偏好驱动的决定,不是统计教条。
关键在于:没有"最优解",只有"业务能接受的平衡点"。数据分析师的职责是把权衡量化清楚(满足率 vs 超备比例),让业务方决策,而不是替他拍脑袋。
九、结论汇总
| # | 发现 | 行动 |
|---|---|---|
| 1 | 模型优于"上周同日"基线 | 先在误差最小的 3 家门店试点 |
| 2 | 周末误差更高 | 周末安全系数上调 5%~10% |
| 3 | 社区店误差最大 | 保留人工复核 |
| 4 | 大促日系统性低估 | "模型 + 人工规则"双轨制 |
| 5 | 门店间差异大 | 分门店定制安全系数 |
| 6 | 夏冬双峰 | 旺季前提前备货 |
| 7 | 80% 预测区间有效 | 按 P90 下达,控制缺货率约 10% |
十、写在最后
做完这个项目,我最想分享的不是模型技巧,而是三个认知:
- 时间序列的坑都在"细节"里——随机切分、滚动含当前值、归一化用全量,任何一个都能让你的模型上线即崩;
- 必须建立基线——打败不了
lag_7的模型没有价值,别急着调参; - 承认模型边界——大促、新品、结构突变,模型都搞不定。主动说明边界,比夸大能力更能赢得信任。