女人穿内衣,数据却比面料还难懂。以前我也以为销售分析就是把Excel拉个透视表,直到接过一个内衣品牌的真实脱敏销售数据,整整几十万条订单记录,SKU数量过千,光尺码就有XS到XXL加罩杯ABCD的组合。用Python跑完清洗和可视化之后我才发现,那些躺在数据库里的数据,才是真正决定下个季度该备多少货、补哪个尺码的关键。这套基于Python的时尚内衣销售数据可视化和预测系统,就是用pandas做数据处理,用pyecharts搭可视化大屏,再用机器学习模型预测未来几周销售趋势的完整实践。
这篇文章适合正在做电商数据分析、毕业设计选题、或者需要搭建销售预测系统的朋友参考。我踩过的坑、调过的参数、以及那些一眼看上去没道理但实际很灵的处理方式,都会写清楚。
1. 项目背景与整体设计思路
1.1 为什么内衣品类需要专门做预测系统
刚接到需求的时候,我也觉得内衣无非是快消品的一种,按常规分析流程走一遍就行。真正看数据才发现,这个品类有几个特点是其他服装类目不太一样的。
第一,SKU复杂度极高。一件文胸可能有钢圈款、无痕款、聚拢款、运动款,每个款式再叠加ABCDE罩杯和70到95底围,组合出来就是几十个甚至上百个SKU。备货不像T恤那样一个码备几千件,而是每个尺码都要精确到几百件,否则就会出现80B卖断货、85C积压一整年的情况。
第二,销售周期有强烈的季节性和节假日脉冲。春季和秋季换季时销量明显上涨,6月、11月还有电商大促的冲击。如果不用模型去量化这种规律,仅靠人工经验判断,很容易在备货节奏上滞后半拍。
第三,退货率比想象中的高,尤其是线上渠道。内衣因尺码不合身导致的退货会反过来干扰数据,拉低销售额算出来的趋势,如果不做预处理,模型就会把退货误判成真实需求下降。
这些特点决定了这套系统不能只做一张好看的大屏,而是要回答三个核心问题:哪些款式值得加大备货,哪些尺码存在结构性缺货,以及未来四周大概会卖出多少件。
1.2 技术选型:为什么是Python全家桶
技术栈没有选复杂的分布式框架。数据量虽然名义上叫大数据,实际也就是几十万到几百万行的级别,单机Python完全跑得动。整个系统的核心组件如下:
- 数据处理层:pandas、numpy,负责字段清洗、透视聚合、特征提取。
- 可视化层:pyecharts,配合Flask搭建Web看板。选择pyecharts而不是matplotlib,是因为它在浏览器端的交互性更好,图表可以缩放、拖拽、提示数据,适合给业务运营去看,而不是只有程序员能看懂。
- 预测层:先用statsmodels做时间序列基线,再用scikit-learn的机器学习模型做主力预测。
- 展示层:Flask + HTML + ECharts,图表数据通过JSON接口传给前端渲染。
选这套组合的原因很简单:生态成熟、案例多、遇到报错容易搜到解决方案。而像Spark、Flink这样的真正的分布式组件,在这个数据规模下属于杀鸡用牛刀,徒增部署成本,没有必要。
提示:如果你的数据量真的到了亿级,再考虑用Spark做预处理,但项目初期建议还是先用pandas把逻辑跑通,验证思路正确之后再迁移,不然排查问题会很痛苦。
2. 数据采集与预处理实操
2.1 数据来源与字段结构
本次用的数据是某内衣品牌线上渠道的销售记录,已经做过脱敏处理,包含时间范围12个月的订单流水,总计约46万条记录。字段主要包括:
- order_id:订单编号
- order_date:下单日期
- product_category:产品类目(文胸、内裤、塑身衣、家居服)
- style_code:款式编码
- style_name:款式名称
- color:颜色
- size_group:尺码组(例如75B,80C)
- quantity:购买数量
- price:成交单价(已扣除优惠)
- amount:成交金额
- user_region:收货省份
- is_promotion:是否促销订单
- return_flag:是否退货
最需要注意的就是return_flag字段。很多刚做数据分析的人会把退货订单也纳入销量统计,导致预测结果虚高。本次处理中,我选择把退货订单从训练集里剔除,只保留真实成交的数据。因为预测的目的是指导备货和补货,而退货订单带来的库存压力是完全不同的一套逻辑,混在一起会让模型学不到真实的需求信号。
2.2 数据清洗的三个关键动作
清洗阶段有三次印象深刻的处理。
第一次是缺失值。原始数据里省份字段有2%的缺失,尺码字段有0.5%的缺失。省份缺失的处理方式是用订单IP归属地映射补全,路径上我在数据字典里找到了一个ip_city字段,虽然名字叫city,但里面存的确实是省份,补了一下准确率还算满意。尺码缺失就直接剔除,因为尺码是分析内衣品类的核心维度,瞎猜一个码比没有更糟糕。
第二次是异常值。内衣的购买数量理论上不会超过10件一次,但数据里确实出现了单笔订单购买999件的记录。查了订单备注才发现是渠道B端采购,也就是企业批量采购员工福利,这类订单需要单独标记出来,分析C端零售趋势时排除掉,不然会把日常销量均值拉爆。
第三次是日期字段格式化。订单日期是字符串格式,直接转成datetime类型后进行按周聚合。这一步需要注意一个细节,转类型时连续报错了好几次,原因是源数据里混入了"2024/6/00"这样的错误日期,后来在转换前加了一层正则校验才解决。
import pandas as pd import numpy as np import re df = pd.read_csv('underwear_sales.csv', encoding='utf-8-sig') # 日期清洗 def clean_date(s): if re.match(r'\d{4}/\d{1,2}/\d{1,2}', str(s)): return pd.to_datetime(s) return pd.NaT df['order_date'] = df['order_date'].apply(clean_date) df = df.dropna(subset=['order_date']) # 剔除退货 df = df[df['return_flag'] == 0] # 剔除异常大单 df = df[df['quantity'] <= 20]2.3 特征工程:让模型认识内衣销售规律
模型不能直接吃日期和款式的字符串,必须转成数字特征。这一步很关键,很大程度上决定了后续预测效果的上限。我构造了以下这些特征:
- 时间特征:月份、星期几、是否月初、是否节假日、距离最近一次大促的天数。
- 促销特征:是否促销、促销等级(满减、折扣、秒杀)。
- 款式特征:该款式过去4周的平均销量、过去8周的销量增长率、款式上架时长。
- 尺码特征:该尺码在所属类目的销量占比、历史售罄率。
构造特征是建模环节里最琐碎也最容易出错的一步。最容易踩的坑就是未来数据泄露。比如我在做特征工程时,一开始直接用全量数据计算"平均销量",然后把整个数据集丢进模型训练和验证,结果MAPE低得离谱,线上验证却差得远。后来才反应过来,计算平均值的时候用到了未来的数据,模型等于提前看到了答案。正确的做法是在训练集和验证集上分别计算特征,严格按照时间顺序切分,不能跨越切分点。
train_end = pd.Timestamp('2024-06-30') train_df = df[df['order_date'] <= train_end] test_df = df[df['order_date'] > train_end] # 训练集上计算款式历史均值 style_avg_train = train_df.groupby('style_code')['quantity'].mean().rename('style_avg_4w') train_df = train_df.merge(style_avg_train, on='style_code', how='left') # 测试集上必须使用训练集计算出的均值,不能重新计算 test_df = test_df.merge(style_avg_train, on='style_code', how='left')3. 数据可视化分析:把指标变成可读的业务结论
3.1 可视化大屏的布局与指标设计
可视化不是把图表堆在一起就完事了。要思考业务方打开大屏之后首先想看什么。这次做的运营看板主要分四个区域:
- 顶部核心KPI区:总销售额、总销量、客单价、退货率,带环比。
- 中部品类结构区:文胸/内裤/塑身衣/家居服的销售额占比(饼图),以及趋势曲线(折线图)。
- 下部细分维度区:款式TOP10排行(横向柱状图)、尺码分布热力图、地区分布地图。
- 右下角数据洞察区:用文本形式自动生成销售提示,比如"75B尺码销量环比上升18%,建议补货"。
用pyecharts实现时,我最常用的是Bar、Line、Pie、Map这几种类型。大屏整体用Flask搭建,pyecharts生成的图表通过render_embed()嵌入HTML页面,数据刷新靠后端定时重新计算。
from pyecharts.charts import Bar, Pie, Line from pyecharts import options as opts # 款式TOP10柱状图 bar = ( Bar() .add_xaxis(top10_styles.tolist()) .add_yaxis("销售额(元)", top10_values.tolist()) .set_global_opts( title_opts=opts.TitleOpts(title="内衣款式销售额TOP10"), yaxis_opts=opts.AxisOpts(name="销售额") ) )3.2 从图表中读出的关键业务结论
数据可视化不是为了好看,而是为了发现规律。我在做完图之后,读出了几个之前看不到的结论。
第一个结论是,销售额并不仅仅是靠大促拉动。把全年销售数据按周拆线后明显看到,3月到4月有一个小高峰,这个时间段并非节假日,后来查了天气预报数据才发现这部分城市进入了早春升温。内衣的换季需求比服装更敏感,气温连续三天超过20度之后,薄款和无痕款的销量就会上涨。后续做预测模型时,把气温数据加入特征做了一版实验,MAPE确实下降了2个百分点左右。
第二个结论是尺码分布极度向中间聚集。80B、75B、70C这三个尺码的销量占比加起来接近40%,而85D、90C这类大底围大体型尺码虽然占比低,但退货率最高。原因是很多用户对自身尺码认知不准确,拿到手发现不合适只能退。这个发现引发了业务侧的调整动作,在商品详情页增加尺码指南和AI量体入口,退货率在接下来两个月中下降了5%。
第三个结论是地区分布有明显的分层。广东、江苏、浙江是内衣消费前三的省份,但不同省份偏好的产品风格有差异。广东偏好透气薄款,江浙偏好聚拢款,这个规律用地图和数据交叉表对比后就非常直观。
3.3 可视化避坑经验
做可视化时踩过的最深的一个坑是中文乱码。pyecharts本身对中文支持没有太大问题,但服务器操作系统没有安装中文字体时,导出的图表上全都是方块。后来在服务器上安装了fonts-wqy-zenhei,并指定pyecharts的字体配置才解决。
另外一个坑是地图图表的省份名称和数据库里的省份名称不一致。数据库里存的是"广东省",而地图组件要求的是"广东",导致地图渲染不出来。解决办法是在读取数据时做省份别名映射,统一口径。
region_mapping = { '广东省': '广东', '广西壮族自治区': '广西', '内蒙古自治区': '内蒙古', '新疆维吾尔自治区': '新疆' } df['region_short'] = df['user_region'].replace(region_mapping)4. 销售预测模型构建:从时间序列到机器学习
4.1 模型选型:为什么不用LSTM
预测模块是整个系统的技术核心。常见的销售预测模型有ARIMA、Prophet、XGBoost、LightGBM,还有深度学习里的LSTM。我最终没有选择LSTM,原因有两个。
第一,数据量不足以支撑深度学习模型的训练。LSTM需要大规模时间序列数据才能学到复杂的时序依赖,而内衣销售数据只有12个月,按周聚合后只有52个样本点,喂给LSTM大概率过拟合。
第二,可解释性差。业务方问"为什么预测下个月卖5000件"的时候,深度学习模型很难给出一份清晰的解释,而XGBoost可以输出特征重要性,能直观看出来是促销因素主导还是季节性因素主导。
最终选型是:用statsmodels做ARIMA作为基准模型,用XGBoost作为主模型,预测以周为单位,提前4周滚动预测。ARIMA的定位是验证趋势方向是否正确,XGBoost负责给出更精确的数值预测。
4.2 销量聚合与数据集划分
预测的目标是每个SKU在未来4周的销量。但单SKU的时间序列更稀疏,直接预测难度太大。实际操作中做了分层聚合:整体大盘用日维度预测,款式级别用周维度预测,尺码级别用月维度预测。粒度越细,数据越稀疏,预测难度越大,需要根据业务需求做取舍。
以款式级别的周度预测为例。先按周聚合每种款式的销量,然后构造训练集,过去6周特征预测未来1周销量。用滚动时间窗口切分,前70%做训练,后30%做验证。做这一点时,我从最初简单随机抽样改成了时间序列顺序切分,理由很简单:销售预测是用过去预测未来,如果用随机抽样,会把未来数据混进训练集,等于作弊。
from sklearn.model_selection import TimeSeriesSplit tscv = TimeSeriesSplit(n_splits=5) for train_index, test_index in tscv.split(X): X_train, X_test = X.iloc[train_index], X.iloc[test_index] y_train, y_test = y.iloc[train_index], y.iloc[test_index]4.3 XGBoost训练与参数调整
XGBoost用起来不复杂,关键是参数调整。我用的参数组合是经过几轮网格搜索确定的。
import xgboost as xgb model = xgb.XGBRegressor( n_estimators=500, max_depth=5, learning_rate=0.05, subsample=0.8, colsample_bytree=0.8, reg_lambda=1.0, eval_metric='mae', early_stopping_rounds=50 ) model.fit( X_train, y_train, eval_set=[(X_train, y_train), (X_test, y_test)], verbose=False )重点说一下max_depth和learning_rate这两个参数。max_depth控制树的复杂度,太深容易过拟合,内衣销售数据特征交互没有想象中那么多,5层足够了。learning_rate设小一点,0.05配合500棵树,虽然训练时间长一点,但精度更好。早停机制必须开,不然会在验证集上越走越偏。
4.4 模型评估与效果对比
预测效果的评估指标我同时看了MAE(平均绝对误差)和MAPE(平均绝对百分比误差)。MAE直观反映平均每个SKU差多少件,MAPE反映误差比例。
最终结果让我比较满意:整体销量的MAPE大约在11.3%,款式级别的MAPE在18.5%左右。ARIMA的MAPE则分别是17.8%和29.4%。对比下来,XGBoost的优势非常明显。
单纯追求MAPE最低也不一定就对业务友好。内衣销售预测如果误差是正向的(预测多于实际),会导致库存积压。如果误差是负向的(实际多于预测),会导致缺货损失。实际项目中我把训练时的损失函数改成了非对称损失,预测偏高一点罚得轻,预测偏低一点罚得重,这样能把缺货风险控制得更严。
def asymmetric_mae(y_true, y_pred): residual = y_true - y_pred return np.mean(np.where(residual > 0, 2.0 * np.abs(residual), 1.0 * np.abs(residual)))从业务结果看,这个调整让畅销款的缺货率下降了约20%,代价是总库存量上升了约6%,算是划算的交换。
5. 系统实现与部署:Flask + ECharts整合
5.1 系统整体架构与工作流程
整套系统部署在单台Linux服务器上,运行内存16G,训练XGBoost模型完全够用。核心模块可以切分为四块:
- 数据模块:定时从数据库读取最新销售数据,执行清洗和特征工程,产出建模宽表。
- 训练模块:定时(每周末)重新训练模型,输出最新参数文件。
- 预测模块:加载模型,对下一周期销量进行预测,写入MySQL的prediction表。
- 可视化模块:Flask提供HTTP接口,ECharts做前端渲染。
模块之间的调度用crontab实现,每天凌晨3点跑一次数据更新,每周一凌晨5点跑一次模型重训,避免影响白天业务系统的数据库压力。
5.2 核心代码:Flask接口和ECharts对接
这里把可视化模块的核心代码简化一下展示。前端通过fetch请求后端的/api/sales_summary接口,拿到JSON数据后渲染图表。
from flask import Flask, jsonify, render_template import pandas as pd import json app = Flask(__name__) @app.route('/') def index(): return render_template('dashboard.html') @app.route('/api/sales_summary', methods=['GET']) def sales_summary(): summary = { 'total_sales': float(total_sales), 'total_qty': float(total_qty), 'avg_price': float(avg_price), 'return_rate': float(return_rate), 'daily_trend': daily_trend_list, 'category_ratio': category_ratio_list, 'top10_styles': top10_styles_list, 'size_heatmap': size_heatmap_list } return jsonify(summary) if __name__ == '__main__': app.run(host='0.0.0.0', port=8080, debug=False)前端页面中使用ECharts初始化折线图和饼图,具体的配置项比较常规,需要注意的是一定要在HTML里正确引入ECharts的CDN链接,并且确保后端返回的数据结构和前端图表数据结构一一对应,否则会出现"图表渲染空白但接口正常"的问题,新手常常在这个地方耗很久。
注意:pyecharts和ECharts是两套东西。pyecharts是用Python生成图表配置,ECharts是前端JavaScript图表库。Flask项目中可以直接在Python里用pyecharts生成图表HTML片段嵌入页面,也可以用JSON数据传给前端绘制。前者写起来快,后者灵活度更高。项目里我混用两种方式,KPI卡片用JSON传值,复杂图表用pyecharts生成。
5.3 部署时踩过的三个坑
第一个坑是MySQL数据库连接时区问题。Flask读取数据时发现读取的日期比实际日期少了8小时,原因是MySQL连接字符串里没设置时区参数。解决办法是加上init_command=SET time_zone = '+8:00'。
第二个坑是定时任务与模型训练并发冲突。某次周一凌晨训练任务和预测任务同时执行,导致预测结果写入了训练中使用的历史表,数据被污染。后来在代码里加了文件锁,训练期间预测任务等待训练完成后再执行。
第三个坑是Python环境迁移后缺少依赖。迁移服务器时直接拷贝代码目录,跑起来提示ModuleNotFoundError。后来用了conda导出完整环境列表,在新服务器上重建环境,一步到位解决。
conda env export > environment.yml5.4 预测结果如何落到业务动作
系统做好之后,真正产生价值的地方在于和业务动作闭环。预测不是为了在报表里多一列数字,而是为了指导决策。
初期模型预测出下一周75B码聚拢款的需求量预计是800件,而当前库存只有560件,系统自动发出补货预警。采购部门根据预警提前两周备货,当周销量实际达到765件,几乎没有缺货也没有积压。
反过来看,某个无痕款预测销量持续走低,但库存还有3000多件。系统给出降价促销建议,运营团队针对这个款式做了满两件打折活动,最终在30天内清掉了80%的库存。这就是预测系统在需求预测之外联动库存管理和营销决策的价值。系统本身不产生决策,也不替代人做决策,它只是把原来依赖经验拍脑袋的事情变成数据透明化的参考依据。
6. 常见问题排查与避坑实录
6.1 预测不准:先检查特征还是先调参数
初学者遇到预测不准,第一反应就是去调模型参数。我的经验是:先排查特征和训练数据切分是否合理,再考虑调参。比如以下情况我都遇到过:
- 训练集和测试集有重叠(未来数据泄漏),结果看似很好实则是假的。
- 促销特征没有聚合到款式粒度,模型看不到促销信息,预测大促周严重偏低。
- 把退货数据算进销量,导致节假日后的第一周预测值被高估。
- 节假日特征没有细分具体节日,统一打一个"节假日"标签,导致不同节日的促销系数无法区分。
以上任何一种情况的优先级都高于调高max_depth。先让数据说话,再让参数干活。
6.2 特征工程直接决定模型上限
做特征工程时,我有个跟大多数人不太一样的做法:特征数量尽量控制在30到50个之间,不要贪多。XGBoost虽然能处理高维特征,但特征越多,噪声越大,可解释性越差。
我最终保留下来的关键特征按重要性排序如下:
| 特征 | 重要性 | 说明 |
|---|---|---|
| 促销标志 | 高 | 促销周期销售波动最大 |
| 温度(周均) | 高 | 气温触发换季需求 |
| 款式历史销量 | 高 | 热门款式延续性 |
| 距离大促天数 | 中 | 大促前后需求波动 |
| 星期几 | 中 | 周中周末差异 |
| 尺码售罄率 | 中 | 反映结构性缺货 |
值得强调的是温度特征。内衣这种品类对气温的敏感度超过我预期,气温超过25度时薄款和无痕款销量猛增,聚拢款销量下跌。这个特征来自外部天气API,接入到管道中后预测精度有明显提升。
6.3 数据量不足时如何提升预测稳定性
很多实际业务场景里,数据量不足以支撑复杂模型,或者SKU粒度过细导致稀疏。这种情况下有几个实用技巧:
- 冷启动款式的预测借用同类款式的平均销量做基准,再乘以款式评分系数。
- 新上架款式用前4周数据作为参考,但把回归系数向大盘均值收缩,避免被异常值带偏。
- 采用分层贝叶斯思路,把小样本款式向全局均值收缩,在代码中实现为样本量加权平均。
这本质上是经验方法和统计方法的折中。在数据不太充分的时候,最怕的就是对某几个周异常值过度敏感,收缩处理能够让预测更稳健。
6.4 系统上线后的监控怎么做
系统上线不是结束,监控才是持续的事情。我设置了一个简单的监控脚本,每周跑完模型后自动生成预测结果,并与实际上报数据做对比,如果MAPE连续两周超过20%,就会发邮件报警。
报警逻辑触发过两次。第一次是因为大促活动日期提前,模型里对距离大促天数的特征计算没跟上,导致预测值偏低。第二次是因为天气接口数据源服务中断,温度特征变成空值,模型自动补了前向填充,预测值偏差较大。这两次报警都提示我需要在特征工程管道里对上游数据源做高可用保障,而不只是依赖模型本身。
自动化监控的价值就是提前暴露问题,而不是等业务侧发现预测结果不靠谱再来找技术排查。每次模型重训后,我都要求系统留一份本次训练的关键超参数、训练集时间范围和验证集MAE快照,方便事后回溯是哪一次改动导致预测质量下降。
7. 一点个人经验和后续扩展
整套项目做下来,最大的体会是数据分析的价值不在代码本身,而在于对业务的理解和拆解。同样是销售预测,不同品类的数据规律可能完全不同,直接拿通用的模型套上去,效果往往不理想。做内衣这个项目,真正让我提升预测精度的不是更复杂的算法,而是对尺码结构、季节温度、退货逻辑这三个业务细节的理解。
这个系统后续如果要扩展的话,有几个可以考虑的方向:一是引入更多外部数据源,比如地区天气、节假日安排、面料价格指数,让特征体系更丰富;二是做尺码级的库存模拟,在预测基础上叠加库存周转优化算法,直接输出补货建议单,把预测和分析更紧密地结合到供应链动作里;三是尝试用Prophet做对比基线,特别是当积累到两到三年的历史数据之后,节假日效应的拟合会比XGBoost更稳定。
最后再分享一个小技巧。做这类项目,一定要把数据处理和分析过程沉淀成可复用的工具函数,不要把每一步都写在零散的notebook里。我把清洗、特征工程、模型训练、预测、可视化的管道封装成了一个pipeline脚本,每次接手新数据,只需要调整字段映射和过滤规则,就能跑出一套完整的看板和预测。这才是这套系统真正值钱的地方。