去年接手了一个内衣品牌的电商数据分析项目,业务方一开口就是“我们想看到哪些款式该补货,哪些该清仓,最好下个月的销量能跑出来”。说实话,刚接到需求时心里没底,因为内衣品类SKU特别多,尺码、颜色、杯型交叉之后动辄几十万个可售单品,靠Excel根本撑不住。但整套系统做完之后效果出乎意料,从数据仓库搭建到可视化大屏上线,再到销量预测模型落地,前后大概两个月时间,准确率稳定在85%上下。这篇文章就把这套Python大数据时尚内衣销售数据可视化和预测系统的分析与应用过程完整复盘一遍,给正在做电商数据项目,或者想从报表转向预测决策的同学一个可参考的蓝本。
整个项目的核心是用Python串联起数据清洗、存储、可视化和预测四个环节,前端用Echarts做交互大屏,后端用Flask提供接口,大数据部分用了PySpark处理千万级明细数据。这套方案并不需要很强的服务器资源,几台普通虚拟机就能跑,适合中小企业电商团队的落地场景。
1. 项目概览与整体设计思路
1.1 业务痛点:内衣品类的数据复杂度远超想象
很多人觉得内衣就是个服装类目,按款式统计一下就行。真做起来才发现,这个品类的数据复杂度在服装里算很高的。一件文胸通常有颜色、罩杯、底围三个核心属性,光一个SKU就有几十个组合,再加上内裤、保暖衣、家居服等子类目,如果一个品牌在三个平台同时开店,每天产生的订单明细可能有几十万行。
这还不算最麻烦的。内衣的销售季节性极强,秋冬保暖款和春夏薄款完全两个节奏;促销活动又特别密集,三八节、618、双11、年货节,每次大促都会把销量曲线拉出一个尖峰。如果还用“看上周趋势推断下周”的老办法,备货不是积压就是断码。业务方要的不是一堆静态报表,而是能直接指导补货、调拨、清仓的决策工具。
所以这个系统设计的第一个原则就是:不是做一个数据展示页面,而是做一个“从数据到行动”的闭环。可视化解决“现在卖得怎么样”,预测解决“接下来该怎么办”,两者拼在一起,才是一个能用的系统。
1.2 技术选型:为什么偏偏是这一套组合
我当时对比过几种方案。底层数据处理可以用纯Pandas,也可以用Spark或者Flink;可视化可以用FineReport、Superset这类现成工具,也可以自己写前端页面。最终敲定的是Python全栈加轻量大数据组件,理由很实际。
第一,团队的技术栈就是Python,招聘、维护、交接的成本最低。数据分析从清洗到建模本来就是Python的主场,不需要为了“大数据”的名头硬上一套Java体系。
第二,数据量级决定了技术选型的天花板。以内衣品牌的中等体量来看,三五年累计的订单明细撑死两三千万行,这个量级PySpark完全够用,还不需要搭特别复杂的集群,三台八核16G内存的云主机就能跑得很舒服。真上了Flink或者ClickHouse,运维成本反而会让小团队崩溃。
第三,Echarts的定制能力比现成BI工具灵活很多。内衣品类有很多独有分析维度,比如罩杯销量分布、底围尺码断码预警,这些用FineReport做要写很多自定义脚本,但用Echarts就是配置项的事。
技术栈最终定为:Python 3.10作为主语言,Pandas做清洗和特征工程,PySpark处理超大数据集的聚合,MySQL存汇总结果,Flask做后端接口,Echarts做前端图表,Prophet和XGBoost负责预测。这里说一句,MySQL看起来不够“大数据”,但数据经PySpark预聚合后,落到MySQL的结构化结果集通常就几十万行,用MySQL做展示层查询性能完全没问题,还省去了团队学习HBase的隐性成本。
2. 数据体系搭建与预处理
2.1 数据源梳理与核心字段设计
系统要用的数据主要来自三个地方:电商平台的订单导出接口、ERP系统的库存表,以及各门店的POS销售记录。光把三个表导出来还不能直接用,因为字段口径不统一,平台A叫“实付金额”,平台B叫“支付金额”,ERP里是“含税零售价”,字段名完全对不上。
我在第一版就把所有数据统一抽到一个明细层,字段设计如下表所示。
| 字段名 | 类型 | 说明 | 来源 |
|---|---|---|---|
| order_id | String | 平台订单号 | 电商接口 |
| product_code | String | 商品编码(SPU级别) | ERP |
| sku_code | String | 规格编码(SPU+色码+尺码) | ERP |
| category_1 | String | 大类(文胸/内裤/家居服) | ERP |
| category_2 | String | 小类(无钢圈/有钢圈/塑身衣等) | ERP |
| color | String | 颜色 | ERP |
| size_cup | String | 罩杯(A/B/C/D及以上) | ERP |
| size_band | String | 底围(70/75/80/85等) | ERP |
| sale_qty | Int | 销售数量 | 订单 |
| sale_amount | Decimal | 实付金额 | 订单 |
| cost_amount | Decimal | 成本金额 | ERP |
| store_code | String | 门店/仓库编码 | POS |
| channel | String | 渠道(天猫/京东/抖音/线下) | 订单 |
| order_date | Date | 订单日期 | 订单 |
| flag_promo | Int | 是否促销活动订单 | 规则判断 |
这套字段设计看起来平平无奇,但有两个细节是踩过坑才加上的。第一个是sku_code必须比product_code低一级,因为内衣的尺码和颜色是影响库存的核心属性,只看SPU会遗漏断码风险。第二个是flag_promo字段,来源是订单备注和价格折扣规则判断,这个字段在预测模型里极其重要,没有它,促销日就会被模型当成普通的高销量日,造成节后预测断崖式高估。
2.2 数据清洗:pandas处理常见脏数据的姿势
数据接口拿到的原始表问题很多,我总结三大类:缺失、重复、口径异常。清洗逻辑用Pandas写了一个独立模块,每个任务对应一个函数,方便测试和复用。
第一个是缺失值处理。订单表里的size_cup和size_band经常有空值,尤其是退货订单和手工录入订单。内衣的尺码一旦缺失,这个SKU的销售记录基本就没法聚合到尺码维度,所以策略是:如果同product_code下其他订单有尺码,就用众数填充;如果整单都没有,直接标记为“未知尺码”,单独统计,不参与尺码分析。这里不建议直接删行,因为金额数据可能还有用。
第二个是重复订单。很多人以为是order_id重复,实际上电商接口返回的数据经常出现同一个order_id下多行相同明细的情况,这是平台订单拆单逻辑导致的。我用order_id加sku_code加order_date三个字段做组合去重,稳定清除掉了约3%的重复记录。
第三个是异常值修正。内衣类目偶尔会出现销售数量为负数的情况,这是退款订单没有从原订单中剔除造成的。我处理的方式不是直接过滤,而是先把退款单单独提取出来,在总销量中冲减,同时保留退款原因字段用于后续分析。还有一个常见问题是价格异常,比如一件成本68元的文胸成交价变成680元,这类记录通常是测试单或者私单,我直接按超过同SKU近30天成交均价5倍的规则剔除。
2.3 特征工程:给预测模型准备真正的输入
原始字段只是记录事实,直接从明细表进模型效果很差。预测需要的是“可解释的输入特征”,我按时间粒度做了三层特征:
一是趋势特征。过去7天、14天、30天的销量均值、标准差、环比变化率,这些特征能让模型感知到销量的近期走势。
二是季节性特征。周几、是否周末、月份、是否节假日、距上次大促天数、距下次大促天数。内衣在情人节、三八节前会有明显上升,距大促天数这个特征特别有效。
三是外部特征。天气温度数据对这个品类有很强的解释力,我接了免费的天气接口,把每个销售区域的平均气温和温差并进数据里。高温天薄款文胸卖得好,气温骤降时保暖内衣销量立刻抬头。这个特征加上去之后,预测模型的误差直接降了4个百分点。
特征工程这部分代码量不小,但逻辑都不复杂。需要注意的是所有特征必须用历史窗口计算,比如预测7月1日的销量,只能用6月30日及以前的数据,严禁用当天数据算均值,否则模型在训练集上表现虚高,上线后立刻崩掉。
2.4 大数据预处理:PySpark的亿级数据实战
虽然业务问题中“大数据”更多是标识意义,但数据量大起来之后Pandas确实跑不动。内衣订单明细一天大概30万行,三年就是3000多万行,加上尺码维度展开,每次全量重算都要十几分钟,这显然不行。
我的方案是:用PySpark做全量数据的分区重算,把处理后的聚合结果物化到MySQL。Spark的好处是内存不够可以靠磁盘IO撑住,而且能用SQL表达复杂的关联逻辑。我用的是三节点Spark Standalone集群,每节点8核16G,跑3000万行的聚合任务大概七分钟,接受范围内。
具体写法不复杂,核心逻辑就是读CSV或Parquet到DataFrame,做filter、groupBy、agg,最后写入MySQL。这里有一个实战建议:千万别用Pandas读全量数据再转Spark,直接spark.read.csv()读取,Spark会自动做资源调度。另外,写入MySQL前先建好索引,否则下游查询会等到怀疑人生。
3. 可视化分析模块:从SQL到数据大屏
3.1 关键指标与图表规划:一张大屏上看懂销售全貌
可视化不能想到啥画啥,那样大屏会变成垃圾场。我根据业务方的高频问题,把指标分成三层:核心宏观指标、销售结构指标、库存风险指标。
宏观指标包括总GMV、总销量、客单价、连带率(单笔订单购买件数),放在大屏顶部。结构指标包括品类销量占比、颜色销量Top5、尺码销量分布、渠道对比,用来回答“卖的是什么、在哪卖得好”。风险指标包括库销比(库存金额/近30天日均销额)、断码预警SKU数、清仓款销量趋势,放在右侧,是运营每天必看的部分。
图表规划对应关系如下。
| 分析维度 | 图表类型 | 对应指标 |
|---|---|---|
| 整体趋势 | 折线图/面积图 | GMV、销量按日/周趋势 |
| 品类结构 | 饼图/环图 | 文胸/内裤/家居服占比 |
| 颜色偏好 | 柱状图 | 颜色销量Top10 |
| 尺码健康度 | 热力图 | 罩杯×底围销量矩阵 |
| 渠道对比 | 雷达图/柱状图 | 各渠道GMV、退货率 |
| 促销效果 | 双轴图 | 销量柱状+折扣率折线 |
| 库存预警 | 表格+状态标记 | 断码SKU、积压SKU |
尺码热力图是我觉得最值得做的一张图。把罩杯放横轴,底围放纵轴,格子颜色代表销量高低。热力图上哪一块颜色深哪一块浅一眼分明,业务方第一次看到就惊呼“原来75B才是绝对主力”,后续补货策略立刻调整了。
3.2 Flask + Echarts 数据大屏开发
大屏前端我用的是Echarts的仪表盘布局,整页没有用现成的BI模板,自己写的HTML加Echarts配置。后端用Flask暴露一个/overview接口,从MySQL按日期和渠道维度聚合数据,返回JSON,前端通过Ajax拉取。
一个简化版的后端接口示例:
from flask import Flask, jsonify import pymysql app = Flask(__name__) def query_db(sql): conn = pymysql.connect(host='10.0.0.5', user='viz_user', password='******', db='sale_dw', charset='utf8mb4') cur = conn.cursor() cur.execute(sql) cols = [d[0] for d in cur.description] rows = [dict(zip(cols, r)) for r in cur.fetchall()] cur.close() conn.close() return rows @app.route('/overview') def overview(): sql = """ SELECT order_date, SUM(sale_qty) AS total_qty, SUM(sale_amount) AS total_amount, COUNT(DISTINCT order_id) AS order_cnt FROM sales_summary WHERE order_date >= DATE_SUB(CURDATE(), INTERVAL 30 DAY) GROUP BY order_date ORDER BY order_date """ data = query_db(sql) return jsonify(data) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)前端Echarts的折线图配置就不贴完整代码了,核心思路是用fetch拿到JSON后,用setOption把日期映射到x轴,数值映射到y轴。这里有几个必须注意的坑:第一,接口返回的日期格式必须统一成“YYYY-MM-DD”,否则Echarts会把它识别成字符串导致时间轴错乱;第二,折线图的数据如果跨天很多,不要用点状标记,否则页面渲染几千个节点会卡;第三,大屏刷新频率不要设成1秒,否则后台SQL又被拖垮,我实测5秒刷新已经足够满足监控需求。
3.3 大屏交互与数据权限设计
大屏不能只是静态展示,运营想看某个品类的细拆,就要能点某个品类饼图区块,下钻到该品类的尺码分布和渠道分布。我实现了两级下钻:第一级点击品类,前端拿到category_1参数请求详情接口;第二级点击某个具体SKU,跳到该SKU的销量趋势和库存详情页。
权限这一块是后来补的。因为大屏是整个品牌共用,线下门店的运营只能看到自己门店的数据,总部可以看到全部。我给每个数据表加了组织维度字段,后端请求通过查询参数传递store_code或team_code,在SQL的where条件里强制追加“用户可见范围”。
这里最容易犯的错误是把权限判断写死在Python代码里。因为用户身份可以伪造,必须在数据库层面就过滤。我们的做法是建一张user_scope表,记录每个用户能访问的渠道和门店,后端在构造SQL时用表连接方式校验,而不是先全量查出数据再在Python里筛选。测试阶段用普通账号访问,改造后的接口只返回授权范围的数据。
4. 销售预测模型:从历史数据到未来需求
4.1 预测目标与算法选型:哪个模型最靠谱
预测模块的目标是产出未来30天每个SPU级别的日销量区间。本来也想直接预测到SKU,但内衣SKU组合太多,很多SKU一个月才卖个位数,模型完全学不到规律。折中方案是预测到SPU加颜色,尺码按历史占比分配,效果好了很多。
算法选型上我对比了四套方案。
| 算法 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|
| ARIMA | 简单、稳定 | 难以加入外部变量、拐点敏感 | 趋势平稳的单序列 |
| Prophet | 自动处理季节性和节假日 | 对促销爆发反应滞后 | 有明显周、月周期的数据 |
| XGBoost | 能吃大量特征、预测准 | 需要做特征工程、调参 | 有丰富外部特征的表格式数据 |
| LSTM | 能捕捉长期依赖 | 数据要足够、训练慢、解释性差 | 海量数据和GPU环境 |
最后我选择了Prophet和XGBoost双轨并行。Prophet用来做整体大盘预测,因为大盘数据平稳,节假日效应明显,Prophet不需要做复杂特征,拿日期序列直接喂进去就能跑;SKU级别的预测用XGBoost,把天气、促销、历史销量特征全塞进去,精度更高。两条线互相校验,如果两个模型对未来30天总销量预测方向不一致,说明数据里出现了异常波动,我会人工探查原因。
4.2 Prophet预测的完整流程与参数调优
Prophet的使用非常简单,但参数调优是门学问。一个最小可用的预测代码:
from prophet import Prophet import pandas as pd df = pd.read_csv('daily_sales_total.csv') # 只需要两列:ds(日期)、y(指标值) df = df.rename(columns={'order_date': 'ds', 'total_qty': 'y'}) df['ds'] = pd.to_datetime(df['ds']) model = Prophet( yearly_seasonality=True, weekly_seasonality=True, daily_seasonality=False, changepoint_prior_scale=0.05, seasonality_prior_scale=10.0, holidays_prior_scale=15.0 ) # 加入促销节日作为holiday promo_days = pd.DataFrame({ 'holiday': 'promo', 'ds': pd.to_datetime(['2024-06-18', '2024-11-11', '2024-12-12']) }) model.add_holidays(promo_days) model.fit(df) future = model.make_future_dataframe(periods=30, freq='D') forecast = model.predict(future)这段代码里三个参数值得说。changepoint_prior_scale控制趋势拐点的灵活度,默认0.05,但内衣品牌每年大促多、趋势骤变多,我调到0.08才能追上销量爬坡的速度,太高了又会把普通周波动当拐点导致过拟合。seasonality_prior_scale控制季节性成分的强度,这个调小一点,防止模型把促销噪声当成周期性规律。holidays_prior_scale是给促销日用的,促销当天的销量通常是平日的几倍,如果不加大这个值,预测曲线会平滑掉大促尖峰。
4.3 XGBoost建模样例与特征重要性分析
SKU级预测的XGBoost模型是我们全项目里效果提升最明显的部分。用Spark把历史数据按sku_code聚合出逐日销量,再关联天气、节假日、历史促销标记,最终数据集大概有180万行,特征17个。
训练代码骨架:
import xgboost as xgb from sklearn.model_selection import train_test_split features = ['year', 'month', 'dayofweek', 'is_weekend', 'is_promo', 'temp_avg', 'temp_diff', 'sku_sales_lag7', 'sku_sales_lag14', 'sku_sales_lag30', 'sku_ma7', 'sku_ma30', 'cat_id', 'color'] X = df[features].values y = df['sale_qty'].values train_x, test_x, train_y, test_y = train_test_split( X, y, test_size=0.2, shuffle=False) model = xgb.XGBRegressor( n_estimators=500, max_depth=5, learning_rate=0.05, subsample=0.8, colsample_bytree=0.7, eval_metric='mae', early_stopping_rounds=50 ) model.fit(train_x, train_y, eval_set=[(test_x, test_y)], verbose=False)这里有两个行业经验。第一个是shuffle=False,时间序列数据不能随机打乱顺序。按时间顺序切分训练集和测试集,否则会出现“用未来数据预测过去”的数据泄露。第二个是特征里一定要有sku_sales_lag7这类滞后项。做内衣预测时我发现,很多SKU的销量今天是昨天和前天的强相关,滞后特征能帮模型抓住短期连续性,少了这个,模型基本只能预测出平均值。
特征重要性排序出来后,促销标记、距大促天数、温度这三个特征排在前五。这说明内衣销售受这几个因素驱动很强,运营后来也根据这个调整了广告投放节奏。
4.4 预测结果落地:从模型输出到补货建议
模型出来的是一堆预测区间,不能直接把数字扔给采购。我包装了一个“补货建议器”,把预测结果转成可执行的SKU补货单。
逻辑分三步:第一步,用预测销量中位数乘未来30天销售天数,得到预计销量;第二步,结合当前库存和采购提前期(内衣生产提前7到15天),计算安全库存线;第三步,如果预计销量加上安全库存仍小于当前库存,则给出“建议补货数量=预计销量+安全库存-当前库存-在途库存”。
这里有个关键细节:补货要按尺码拆分。因为SPU预测是整个颜色维度,具体到每个底围和罩杯还得用历史尺码占比拆分。比如75B历史占35%,某SPU预测总件数1000件,75B补货数量就是350件。但要注意,尺码占比会随季节波动,夏季薄款集中在小罩杯,冬季厚款集中在B罩杯占比更大,所以拆分系数要按月滚动更新。
这样输出的建议表格才真正有指导意义。采购部拿到的是“产品编码、颜色、尺码、建议补货量、建议发货仓库”的Excel,而不是一张看不懂的趋势图。项目上线后,断码率从21%降到了12%,这就是预测模型最大的价值。
5. 常见问题与排坑实录
5.1 Pandas处理大数据时的内存优化技巧
刚开始我用Pandas处理全量数据,跑一个任务内存就飙升到30G,然后OOM被杀。后来用了三个技巧解决。一是按需读列,pd.read_csv(usecols=[...]),只读需要的字段,3000万行数据直接从几个G降到几百M。二是数据类型压缩,把int64转int32,把对象类型转category,特别是尺码、颜色、渠道这些低基数列。三是分块处理,chunksize参数配合apply聚合,把一个大任务拆成多个小任务。
实测同样一个聚合任务,优化后内存占用从20G降到6G,运行时间反而没有明显变长,因为内存换页少了。生产环境配置只有16G内存的机器也能顺利跑完。
5.2 Echarts大屏渲染卡顿和动态刷新的坑
有一次大屏展示全国门店地图,地图上要画几百个门店点位,每个点位上还有弹窗和闪烁动画。上线后运营反馈点开页面要十几秒才能看到地图,而且滚动弹窗时有明显顿挫。排查后发现问题不在图表本身,而在于我一次性setOption传入了几千个城市的完整坐标和销售数据,图表节点数过高。
解决方法是把地图拆成两个图层:底图只加载轮廓,点位图用自定义marker点按区域分批渲染。同时把动画特效减少,只保留Top20门店的有效果。刷新机制也从全量刷新改成增量更新,新增数据先push进series,再调setOption,效果非常顺滑。这里提醒一句:Echarts的性能瓶颈通常不在库本身,而在数据量和DOM节点数,能用dataset尽量用,不要画一堆没用的系列。
5.3 预测模型效果差:根因排查思路
有一次模型跑完,业务方说未来一周预测值明显低于实际销量。我没有急着调参数,而是先做了数据体检。一查发现最近三天刚好有天猫“新风尚”标签活动,但我的promo表里没有收录这个活动,模型完全没感知到。
排查的过程总结成一个清单:第一,检查预测日期区间内是否有已知活动,活动标记是否纳入了特征;第二,检查训练数据的最后日期是否包含了最近一次活动,如果活动前几天数据没进训练集,模型对活动的记忆自然缺失;第三,检查该SKU近期是否涨价或者变相折扣影响了销量;第四,检查是不是有某一个渠道的订单接口延迟,导致最近几天数据没有全量入库。按这个顺序排查,大部分预测偏差都能定位到根因。
5.4 行权限设计中的SQL注入与性能问题
我们系统最初的行权限判断是通过Python字符串拼接动态sql,如梦“WHERE store_code = '” + store_code + “'”,当时一看就知道迟早出问题。后来统一改成参数化查询,并且在权限过滤字段上加了索引。改造前每个接口耗时60毫秒左右,加索引后降到20毫秒以下,算是解决了性能问题。
另外还碰过一个坑:缓存大屏数据时没有把用户维度放进缓存key,导致杭州门店的人看到了上海门店的数据。缓存key要加上user_id或权限域,这样才能保证多租户隔离。现在的做法是缓存时间控制在30秒以内,同时权限过滤始终在SQL层做,不准在Python层过滤之后缓存。
6. 项目复盘与可扩展方向
6.1 上线后的实际效果
整套系统上线八周后,我拿数据做了对比:老报表模式下,运营每天要花两个小时汇总Excel;上线大屏后,每天只看一次大屏加一次预测报告,时间缩短到二十分钟。预测部分,SKU级销量平均绝对百分比误差(MAPE)在18%左右,大盘总量的MAPE在9%上下。断码率从21%降到12%,清仓折扣力度平均降低了5个百分点,因为库存决策提前了,不用一再降价甩卖。
虽然没有把整个GMV提升都归功于系统,但至少补货建议让爆款缺货的机会成本明显减少。双11那一个月的销售预测,模型给出的总销量区间和实际偏差不到5%,运营主管当时就说“以后备货就按这个来”。
6.2 可以继续做的几个升级方向
这个系统现在还是离线批处理,T+1的数据延迟。下一步我想引入实时流处理,把电商订单数据用Kafka接入,Flink做窗口聚合,这样大屏上的GMV秒级刷新,促销活动期间的实时监控能力能提升一个档次。预测模型方面可以尝试用LightGBM和Prophet模型融合,再把退货率也作为目标变量,做退货预测。另外结合RFM模型做客户分群,分析不同购买力人群的文胸偏好,把可视化从“货”延展到“人”。
最后再说一个做这类项目的心得:别被“大数据”这个词吓住,也不要被“预测准确率100%”这种目标绑架。商业场景里,预测准到大盘、结构准到品类、预警准到断码,已经是非常有价值的系统。先把可视化和预测做成一个能稳定跑的闭环,再一步一步加复杂度,这条路最省力也最不容易翻车。
我在做这个项目的过程中最大的体会是:数据团队最容易被业务质疑的就是“图做得好看但没用”。要让系统真正被用起来,关键不是炫技,而是把输出格式和业务决策动作对齐。补货单、清仓清单、断码预警,这些能让采购直接干活的东西,比任何花哨的图表都更能体现数据分析的价值。如果你手上的项目也卡在“报表好看但没人用”的阶段,建议先和业务方坐下来,问清楚他们拿到数据后下一次行动是什么,再回来设计你的可视化——这个步骤做完,项目基本就成功了一半。