做电商数据分析的人,多半遇到过这种场景:运营盯着后台的销量曲线,突然抬头问你,下周这个品还能不能补货?该补多少?你要是只甩过去一张“趋势向上”的图,对方多半不满意。我自己也是被这种问题反复逼着,才动手搞了一套商品销量预测系统。整个链路从 requests 采集公开页面数据开始,经过清洗、特征工程和 Hadoop/Spark 批处理,最后用 ARIMA 加机器学习模型做销量时序预测,再叠加一个可视化看板把结果直接摆出来。整个项目的核心问题只有一个:能不能提前几天告诉运营,未来七天的销量大概多少,波动范围多大。
这篇文章把当时的选型逻辑、代码细节、踩坑记录都翻出来,给正在做类似电商预测项目的人一个可以直接“抄作业”的底稿。文章里所有商品字段、店铺字段都做了脱敏,平台也只用“某电商平台”代称,代码逻辑和工程方案不影响复现。
1. 项目整体拆解:这不是一个爬虫项目,而是一个预测闭环
很多同学看到标题里有“requests爬虫”,以为这是个采集项目,实际上爬虫只是数据入口,真正的核心是后半段的销量时序预测和可视化分析。判断一个电商预测系统好不好,不看能爬多少条数据,而看预测结果能不能真的辅助补货、备货和活动排期。
1.1 这套系统要解决的真实痛点
电商运营每天都会面对三个问题:卖了多少、还剩多少、明天该备多少。前两个有后台数据就能回答,第三个需要预测能力。
库存备多了,资金压在上面,仓储成本跟着涨;备少了,订单流失,搜索排名还可能因为缺货权重下降。尤其是活动大促前后,销量会在短时间内暴涨后回落,人工拍脑袋定数量很容易出问题。这套系统把“人工拍脑袋”换成“模型给区间”:输入商品的历史销量,输出未来 7 天的点预测值和置信区间,再配合可视化看板,让运营直接看到趋势线、历史同期对比和异常波动。
系统功能拆开来看,主要由五块组成:
- 数据采集层:requests 抓取商品公开页面和销量相关字段;
- 数据存储层:原始数据落 MySQL,清洗后的特征数据生成宽表,方便后续分析;
- 预处理与特征工程层:Pandas 做基础清洗,Spark 做批量特征计算;
- 模型层:ARIMA 作为基线模型,机器学习模型做对比和融合;
- 可视化层:用看板展示销量趋势、预测结果、商品排名和异常预警。
这五块串起来,才是完整的预测闭环。如果只做其中某一环,比如只写爬虫或只建一个 ARIMA 模型,实际业务价值都会打折扣。
1.2 技术选型背后的取舍逻辑
为什么选 ARIMA,而不是一上来就上深度学习?因为预测建模要先有基线。ARIMA 在单变量销量序列上表现稳定,训练速度快,结果可解释性强,能直接给出置信区间。对电商销量这种带明显趋势和周期性的数据,ARIMA 的 baseline 通常已经能跑出一个不错的 MAPE,后续再用机器学习模型去补足非线性部分才更有意义。
那为什么还要引入 Hadoop/Spark?说实话,这套系统在几千条商品数据规模下,单机 Pandas 完全够用。但我还是把 Spark 放进来了,原因有两个:一是真实业务场景下数据量是百万级甚至亿级,提前把特征计算的并行化逻辑跑通,总比等数据涨上来再重构要好;二是项目本身需要展示大数据处理的能力,Hadoop 负责原始数据的分布式存储,Spark 负责批量特征计算和回测,这样架构上更完整,也更接近生产环境。
一句话总结选型思路:能用小工具解决的先解决,会成长的规模和复杂度交给大数据组件,模型层面保证可解释和可回退。
1.3 适合谁参考这套方案
如果你是在校学生,拿这个项目做课程设计或毕业设计,重点看模型部分和可视化部分,这两块最容易出亮点;如果你是在做电商数据运营的个人开发者,重点看数据预处理和特征工程,把历史销量数据洗干净、拆出有效特征,预测精度能明显提升;如果你所在团队已经有数据仓库,只是想加一个预测模块,那可以直接跳到第三章的 ARIMA 建模流程,结合业务数据改造即可。
2. 数据链路:从采集到数据仓库落地的完整实现
爬虫、清洗、入仓,这三步听起来普通,实际是整套系统里耗时最长、最容易翻车的环节。很多预测模型效果差,不是模型不够先进,而是喂进去的数据里有重复值、缺失值、异常值,或者时间序列被促销拉伸得面目全非。
2.1 requests 爬虫的设计边界与合规底线
爬虫部分我用了 requests + BeautifulSoup 的组合。请求时先设置合理的请求头,模拟普通浏览器的基本字段;再对目标列表页和详情页分别做请求解析,提取商品标题、价格、月销量、评价数、收藏数、上架时间等字段。
采集策略上必须注意三点:第一,控制请求频率,做好随机延时,避免对目标站点造成压力;第二,加异常重试机制,单个商品请求失败后最多重试两次,失败超过阈值就记录日志跳过;第三,做 URL 去重,防止重复采集同一商品浪费资源。
代码骨架大概是这样的:
import time import random import requests from bs4 import BeautifulSoup def fetch_item_page(session, item_id, max_retry=2): url = f"https://example.shop/item/{item_id}" headers = { "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64)", "Accept-Language": "zh-CN,zh;q=0.9", } for attempt in range(max_retry + 1): try: resp = session.get(url, headers=headers, timeout=10) if resp.status_code == 200: return parse_item(resp.text) except requests.RequestException: time.sleep(2 * (attempt + 1)) return None def parse_item(html): soup = BeautifulSoup(html, "html.parser") # 这里只演示解析逻辑,实际字段按目标页面结构调整 item = {} item["item_id"] = soup.select_one(".item-id").text.strip() item["title"] = soup.select_one(".item-title").text.strip() item["price"] = float(soup.select_one(".item-price").text.replace("¥", "")) item["month_sales"] = int(soup.select_one(".item-sales").text.strip()) return item这里要特别说清楚:代码只用于学习演示和自有授权数据的采集。任何爬虫项目在开工前,都应该先确认目标数据是否允许抓取,遵守平台规则和相关法律法规,绝不能碰需要绕过登录验证或加密参数的数据。合规是红线,没有商量的余地,别为了跑通一个项目把自己搭进去。
2.2 数据清洗与特征工程的关键细节
采集下来的原始数据非常脏。最典型的问题是销量字段缺失、价格字段混入促销文案、同一商品在列表页和详情页的销量口径不一致。清洗阶段我做了这几件事:
首先,统一字段类型。把价格、销量、评价数全部转成数值型,处理不了的非数值记录直接标为缺失。其次,处理时间序列缺口。商品不是每天都有人买,销量为 0 的日期不能当作缺失值删掉,要保留;但连续多天完全没有记录的日期要补 0 或做插值,否则 ARIMA 会认为时间断了。再次,处理重复记录。同一商品同一日期只保留一条汇总记录,以最新采集到的数据为准。
特征工程是整个系统里最值得投入时间的地方,比调模型参数更重要。我额外构造的特征包括:商品价格、价格环比变化、评价数、收藏数、是否参与促销、星期几、是否月初月末、是否为节假日、历史 7 日销量均值、历史 14 日销量均值。星期几和节假日特征对电商销量影响极大,工作日和周末的消费意愿差别很明显,大促前后更是天壤之别。
import pandas as pd def build_features(df): df = df.copy() df["date"] = pd.to_datetime(df["date"]) df = df.sort_values(["item_id", "date"]) df["weekday"] = df["date"].dt.weekday df["is_weekend"] = df["weekday"].isin([5, 6]).astype(int) df["price_diff"] = df.groupby("item_id")["price"].diff() g = df.groupby("item_id")["month_sales"] df["sales_7d_mean"] = g.transform(lambda x: x.rolling(7, min_periods=1).mean()) df["sales_14d_mean"] = g.transform(lambda x: x.rolling(14, min_periods=1).mean()) return df特征不是越多越好,尤其是时序模型,加入过多噪声特征反而会降低精度。先做一批基础特征,跑出基线,再逐个加入新特征观察指标变化,能留下的才留下。
2.3 Hadoop/Spark 在项目里的真实分工
在真实生产环境里,原始日志和商品快照通常以亿级规模存在 HDFS 上。直接用 Pandas 全量载入是不现实的,这时候 Spark 的价值才真正体现出来。
我在项目里把 Spark 用在两个地方:
一是原始数据的批量清洗。Spark 读取 HDFS 或本地文件系统中的原始记录,按商品 ID 和日期做 groupBy,计算每日销量总和、价格均值、评价数增量,然后写出宽表。这个过程天然支持分区并行,数据量大时提速明显。
二是批量回测。预测模型不能只看一次结果,要在多个商品上滚动回测。每个商品的时序数据可以分拆成独立任务,Spark 将这些任务分发到多个 Executor 上并行执行,把所有商品的评估指标汇总起来,选出全局最优参数。
from pyspark.sql import SparkSession from pyspark.sql.functions import col, sum, avg, count spark = SparkSession.builder.appName("sales_etl").getOrCreate() df = spark.read.format("parquet").load("hdfs:///data/raw_sales") daily = ( df.groupBy("item_id", "date") .agg( sum("sales").alias("total_sales"), avg("price").alias("avg_price"), count("sku_id").alias("sku_count"), ) ) daily.write.mode("overwrite").format("parquet").save("hdfs:///data/feature_sales")如果只是本地学习,Hadoop 和 Spark 可以不开集群,用 Docker 起一个单机伪分布式环境就够跑通流程。重点是把“数据在哪一层做清洗、在哪一层做聚合、在哪一层做训练”这条链路想清楚。架构的意义不在于堆组件,而在于给数据划分清楚职责边界。
3. 销量时序预测核心:ARIMA 建模全流程
ARIMA 是这个项目的预测主力模型。虽然名字听着像老古董,但它对单变量销量序列的表现依然能打,而且它的弱点可以通过后面的机器学习模型补上。这一章完整讲一遍从平稳性检验到参数定阶再到预测评估的过程。
3.1 平稳性检验、差分与季节性判断
ARIMA 的前提是序列平稳,但原始销量序列通常不平稳。销量有上升趋势,有周期性波动,还可能受大促影响产生突变。直接建模会导致参数估计失真。所以第一步是做 ADF 单位根检验,看序列是否平稳。
from statsmodels.tsa.stattools import adfuller def check_stationarity(series, name="sales"): result = adfuller(series.dropna()) print(f"{name} ADF p-value: {result[1]:.6f}") return result[1] < 0.05如果 p 值大于 0.05,说明序列不平稳,需要差分。一阶差分通常能消除趋势,如果还有明显的季节性,要做季节性差分。电商销量存在很强的周季节效应,周一低、周末高,很多商品还受平台大促节奏影响。这种场景下,不要只做普通 ARIMA,ARIMA(p,d,q) 加上季节性成分会更贴合数据。
我自己习惯的做法是:先画序列图,肉眼看趋势、周期、突变点;再做一次 ADF 检验确认不平稳性;接着做一阶差分,再次检验。分步来,不要直接堆参数。
3.2 ARIMA 参数定阶与建模实操
确定差分阶数 d 之后,接下来是定 p 和 q。两个办法:一是看自相关图 ACF 和偏自相关图 PACF,拖尾和截尾特征能给出大致范围;二是网格搜索 AIC/BIC,遍历 p 和 q 的候选组合,选择信息准则最小的那组参数。
网格搜索是实践中最稳的做法,代码也不复杂:
import itertools import warnings from statsmodels.tsa.arima.model import ARIMA warnings.filterwarnings("ignore") def select_arima_order(series, p_range=range(0, 5), q_range=range(0, 5)): best_aic = float("inf") best_order = None for p, q in itertools.product(p_range, q_range): try: model = ARIMA(series, order=(p, 1, q)) result = model.fit() if result.aic < best_aic: best_aic = result.aic best_order = (p, 1, q) except Exception: continue return best_order, best_aic确定参数后,就可以用完整序列训练模型,并预测未来 7 天销量。ARIMA 的 fit 对象直接提供 forecast 和置信区间:
from statsmodels.tsa.arima.model import ARIMA model = ARIMA(train_sales, order=(best_order[0], 1, best_order[2])) result = model.fit() forecast_result = result.get_forecast(steps=7) forecast_mean = forecast_result.predicted_mean forecast_ci = forecast_result.conf_int(alpha=0.2)这里 alpha 取 0.2,意味着 80% 置信区间。预测业务上一般不追求 95% 的区间,因为电商销量波动太大,95% 区间宽到没有决策价值,反而 80% 区间能给出更实用的补货上下限。
3.3 机器学习对比模型与评估指标
只靠 ARIMA 我不太放心,所以在同一套特征上又跑了机器学习模型做对比。机器学习模型的优势是能把价格、促销、星期、节假日这些外部特征一起喂进去,捕捉非线性关系。缺点是数据量太少时容易过拟合,需要做滚动验证。
机器学习模型的特征构造思路和 2.2 节一样,重点是用 lag 特征把历史销量信息编码进样本:
def make_supervised(df, target="month_sales", lags=[7, 14, 21]): df = df.copy() g = df.groupby("item_id")[target] for lag in lags: df[f"lag_{lag}"] = g.shift(lag) df = df.dropna(subset=[f"lag_{lag}" for lag in lags]) return df训练时不能用普通随机划分,必须做 walk-forward 验证,也就是按时间顺序切训练集和验证集,拿过去预测未来,防止时间泄漏。
评估指标我同时看三个:MAE 衡量平均绝对误差,RMSE 对大误差更敏感,MAPE 则适合和运营沟通“平均偏差百分之多少”。实际跑下来,ARIMA 在平稳期商品上的 MAPE 能控制在 15% 到 30% 之间,机器学习模型在促销波动明显的商品上略优,但整体差距并不大。
一个很实用的做法是模型融合:把 ARIMA 的预测值和机器学习模型的预测值做加权平均,权重根据最近 14 天的验证误差动态调整。融合模型的稳定性通常比单模型好,误差不会突然爆炸,这在业务上非常重要。宁可每天错 20%,也不想有一天突然错 80%。
| 模型 | MAE | RMSE | MAPE | 优点 | 不足 |
|---|---|---|---|---|---|
| ARIMA | 12.3 | 18.7 | 21.6% | 可解释、稳定 | 难捕捉复杂促销 |
| XGBoost | 11.8 | 19.5 | 19.8% | 特征灵活 | 数据少时易过拟合 |
| 融合模型 | 10.6 | 16.9 | 17.2% | 综合两者优势 | 实现略复杂 |
4. 可视化与分析报表:怎么把预测结果讲清楚
模型算完之后,真正给运营看的不是一行行预测数字,而是能快速形成判断的图表。可视化做得好不好,直接决定这套系统有没有人愿意用。我见过很多项目,模型指标很漂亮,但图表一塌糊涂,上线两周就被业务方弃用。
4.1 图表选型与设计逻辑
我的原则是:一屏看懂核心信息,图表数量尽量少。主看板放了四类信息:
销量趋势折线图放最中间,展示最近 90 天历史销量和未来 7 天预测销量,预测部分用虚线,置信区间用半透明色带,一眼能看出未来的波动范围。
商品排行柱状图放在右侧,展示预测销量增速最快的 Top 10 商品,运营可以直接从这里挑出需要重点关注补货的链接。
散点图展示价格与销量增速的关系,用来观察提价后销量下滑的程度,辅助定价决策。
顶部 KPI 卡片放四个数字:今日预测销量、明日预测销量、未来 7 天总销量、高库存预警商品数。
选图表库时我优先用了 ECharts,因为上手快、文档全、交互能力足够强。实际展示预测置信区间时,用 ECharts 的 stacked 面积图很容易实现色带效果。关键是不要为了炫技去做 3D 图表或过多的动效,电商看板追求的是信息密度和反应速度。
4.2 预测结果与交互联动设计
交互上考虑了三个实用功能:商品搜索框、日期范围选择器、预测置信区间开关。用户输入商品 ID 或标题关键词,主图表立即切换到这个商品的预测曲线;调整日期范围,历史数据和预测数据同步缩放;关闭置信区间开关,图表更清爽,适合导出周报。
这里有一个经验:预测模型的输出必须能回溯。我们要给每次预测结果打上“预测生成日期”的标签,这样运营看到某天预测不准时,可以反查是模型的问题还是数据采集的问题。看板后端我加了一个预测结果快照表,每次跑完预测将结果落库,前端默认展示最新一次预测,但可以回看历史任意一次预测,相当于给预测系统加了回放功能,排查问题非常方便。
5. 项目跑通经验:环境搭建、常见坑与后续扩展
代码写出来只是开始,能稳定跑完整个链路才算真正完成。这一章把环境配置、关键问题和扩展方向整理出来,都是实际操作中一一踩过的坑。
5.1 本地环境配置与大数据组件取舍
如果只是验证核心预测流程,推荐最低配置:Python 3.8+,安装 pandas、numpy、statsmodels、scikit-learn、requests、beautifulsoup4、pyecharts 或 ECharts 前端库。数据量在万条以内时,单机跑完全没问题。
Hadoop 和 Spark 开完整集群对个人电脑压力很大,推荐两种方式:第一种用 Docker 启动一个单节点 Spark 容器,做任务调度和并行计算验证;第二种直接用 Spark Local 模式,代码里设置 master=“local[*]”,既能用上机器所有核,又不需要搭建集群。我实际跑的项目里有很多阶段直接用 Spark Local 就能满足,只有演示分布式场景时才切换到集群模式。
MySQL 用来存清洗前的原始数据,特征宽表生成后用 Parquet 格式存到本地或 HDFS。SQLite 适合快速原型,但多机访问和并发写入能力不足,真要做到看板多人使用,还是建议 MySQL。
5.2 常见问题速查表
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
| ADF 检验结果不稳定 | 样本窗口太短,销量波动大 | 改用更长历史窗口,或对数据做对数变换 |
| ARIMA 输出很多 warning 说收敛不了 | 数据量太少或存在过多缺失值 | 提高差分后序列质量,减少异常值,放宽收敛阈值 |
| 预测结果是一条水平线 | 差分过度,模型只学到了均值 | 检查 d 值,回到 ACF/PACF 重新定阶 |
| Spark Local 模式跑批任务报内存不足 | 默认 executor 内存太小 | 设置 spark.driver.memory=4g,或加大分区数 |
| 爬虫采集一段时间后请求频发失败 | 请求频率过高触发限流 | 增加随机延时,降低并发,检查代理和请求头 |
| 机器学习模型验证集指标波动大 | 时间窗口划分不合适,外部特征泄漏 | 改用 walk-forward 验证,严格做特征时间对齐 |
| 看板加载慢 | 图表一次性渲染太多数据点 | 后端按商品和日期范围聚合,限制单图数据点数 |
时序预测项目还有一个很容易踩的坑:日期索引类型不一致。Pandas 里如果 date 列没转成 datetime64,画图时横轴会乱序,模型预测后按日期合并也会出 bug。建议在数据进入管线入口处,统一做一次 pd.to_datetime 和 sort_values,后续所有环节用同一份预处理函数,不要散落着改。
5.3 后续扩展思路
这套系统做完之后,我一直在想几个可以扩展的方向。
第一个是引入更细粒度的数据。当前预测是基于“月销量”换算日销量的,颗粒度太粗。如果能拿到每日订单量、加购量、收藏量、退款量,模型可以拆成“流量预测 + 转化率预测”,精度会上一个台阶,也能定位销量波动到底是流量问题还是转化问题。
第二个是增加活动日程参数。电商销量的最大干扰项是大促。把活动开始时间、结束时间、折扣力度作为外部回归变量引入模型,能有效减少大促期间的预测偏差。
第三个是自动化训练和告警。现在模型是定时批量跑,如果能在每天结束后自动判断“今天实际销量是否超出 80% 置信区间”,一旦触发就推送提醒给运营,系统就从一个“预测工具”变成一个“自动监控工具”,业务价值会高很多。
写在最后:一个调参经验的补充
最后分享一个我调参数时的实际感受:ARIMA 的 p、q 参数不要迷信网格搜索的最优结果,网格搜索在历史数据上 AIC 最低,不代表未来预测最准,尤其当序列里有促销突变时,低 AIC 的模型反而可能过度拟合。我后来加了一个约束条件,候选模型的 AIC 和最优质差距在 10 以内时,优先选择参数更少、结构更简单的那个,预测稳定性比纯看 AIC 好得多。这个习惯沿用到了整套系统的所有模型调参里,效果很实在。做预测系统就是这样,模型复杂度永远要让位于业务可用性。