课设选题目的时候,我见过太多名字叫"XX销售预测系统"的Demo,点进去一看,全是柱状图、折线图和一张看起来很唬人的大屏,真正点"预测"按钮,返回的要么是写死的假数据,要么干脆是随机数。所以当我拿到"基于大数据的月季电商销售预测分析系统"这个题目时,我给自己定的验收标准只有一条:预测必须真算,系统必须真能跑。最后交付的不光是能跑的网站,还有一套从数据清洗、特征工程到模型对比的完整链路。
这篇东西不是答辩PPT的复读,我想把做这个课程设计真正花时间的部分讲透:数据怎么准备才能建模、预测模型到底怎么选、可视化系统哪些地方容易翻车、万字文档怎么组织才不显得水。如果你也正在做电商销售预测、数据可视化分析这类课设,或者手里有个Python数据分析的大作业不知道从哪下手,这篇应该能帮你少走不少弯路。
1. 这个题目真正要交的东西:数据闭环,不是一张大屏
1.1 带"大数据"三个字的课设,难点到底在哪
我收到题目第一时间也以为重点是大屏可视化,毕竟市面上绝大多数成品系统都是"图表堆砌 + 一张大地图 + 几个滚动数字"。但真把需求拆开,"基于大数据的月季电商销售预测分析系统"这句话里,核心词其实是"销售预测"。换句话说,你要做的不是一个展示工具,而是一个能从历史数据里算出未来销量的分析系统。可视化只是让预测结果能被看懂的手段。
课程设计语境下的"大数据",绝大多数情况下不是百万级并发、分布式存储,而是"多张表、多维度、有一定数据量、需要统计分析"的项目。真正决定成绩上限的,是你能不能把这样一条闭环走通:原始数据 → 数据清洗 → 特征工程 → 模型训练 → 预测输出 → 可视化展示。每一步都有对应代码和结果截图,写到文档里就是实打实的功能点。
很多同学折在第一步,拿到数据直接塞进模型,出来的结果一塌糊涂,然后又不知道为什么。我见过最典型的案例是销售数据里有大量缺失值和重复记录没处理,模型MAE高达几百,最后强行调参,解释不清。所以这个题目本质上考的不是你会不会调库,而是你对数据的理解。
1.2 为什么拿月季做切入点这么"好写"
选题的时候我也犹豫过,要不要做通用的"电商商品销售预测"?后来发现拿月季做具体品类,反而比做全品类舒服得多。原因有三个:
第一,月季的SKU足够多,能撑起特征维度。鲜切花、盆栽苗、爬藤苗、大花品种、微型月季,价格可以从十几块的盲盒到几百块的进口苗,价格带跨度大,销量分布自然有层次,做分析时有得写。
第二,月季是典型的时令性商品。春季和秋季是种植高峰,母亲节、情人节前后鲜切花销量会有一个明显脉冲。这种规律性强的品类最适合做预测——你不需要特别复杂的模型,就能做出看起来很有道理的预测曲线。
第三,月季商品标题的信息量很大。品种名、花型、颜色、是否带盆、是否包邮这些信息都藏在标题文本里,可以作为特征工程的重要来源,甚至可以做一个简单的标题关键词频次分析,给可视化页面增加一个"大家都在买什么品种"的板块。
对比一下,如果做全品类预测,特征维度太杂,反而难以解释。具体到一个品类,业务逻辑清晰,模型输出也更好跟人讲明白。答辩的时候老师问"为什么选这个品类",这就是说服力。
2. 月季销售数据的准备与清洗:能进模型的数据才值钱
2.1 数据来源与字段设计:先把表结构定清楚
关于数据来源,我先声明一个原则:课程设计阶段,不要去做任何绕过平台风控的数据采集行为。我采用的合规路线是"公开数据整理 + 课程设计模拟数据补充",再结合实际业务规则把数据修正得足够真实。比如电商页面公开的销量区间、评价数量这些信息是可以看到的,但具体订单明细不可能拿到,这个时候就得自己构造时间序列,再让整体分布符合常识。
这套数据一共三张表,我把字段结构先定下来再写代码,后面所有功能都围绕这三张表展开。
商品基本信息表(product):
- id:商品ID,主键
- title:商品标题
- category:类目,比如鲜切花/盆栽/爬藤
- price:价格(元)
- sold_count:30天累计销量
- review_count:累计评价数
- location:发货地
- status:商品状态(在售/下架)
- is_free_shipping:是否包邮
日销量表(daily_sales):
- id:主键
- product_id:商品ID(外键)
- date:日期
- daily_sales:当日销量
- daily_revenue:当日销售额
评论关键词表(review_keyword):
- id:主键
- product_id:商品ID
- keyword:关键词
- frequency:出现频次
为什么要单独建日销量表?因为商品表里的30天累计销量是"某一时刻的快照",做时间序列预测必须要有按天粒度的历史数据。模拟数据的时候注意一个原则:销售要有周期性,周中偏低、周末偏高;其次大促节点前会有蓄水期,销量小幅爬升后再爆发,而不是每天均匀随机。这些规律不复杂,但决定了模型训练出来像不像样。
2.2 pandas清洗:缺失、重复、异常一个都不能漏
数据到手第一件事不是建模,而是先看一眼全貌。我习惯先跑df.info()和df.describe(),再逐列检查缺失率、唯一值数量、数值范围。真的是老生常谈,但每次都能揪出问题。
课程设计的数据量一般不大,清洗重点在三个地方:
缺失值:价格缺失,可以用同Category的中位数填充;销量缺失,优先看是不是周末/节假日,如果是,用上周同一天的数据填充,否则用前后两天均值;发货地缺失的直接标记为"未知",不要删行。这里有个容易踩的坑:直接dropna()把全表删线了,后面特征工程没数据可用。
重复值:用subset去重时,我选的是"商品标题+价格+上架日期"作为联合判重条件。因为同一个链接改价后会出现标题一样但价格不同的记录,这时候不该去重;标题、价格都一样才算真正重复。
异常值:我在日销量表中做了一个转换函数,用IQR方法找出极端值,销量高于上边界或低于下边界的记录打上标记。但这里建议你先看一眼再决定是否删除。比如某个商品"限时秒杀"当天销量是平时的20倍,这在业务上是合理的,不该一刀切删掉,可以保留并在特征工程里加一个"是否促销日"的标签。
import pandas as pd df = pd.read_csv("daily_sales.csv", parse_dates=["date"]) # 缺失值处理:销量为空用前后两天均值填充 df["daily_sales"] = df["daily_sales"].fillna( df["daily_sales"].rolling(3, center=True, min_periods=1).mean() ) # 重复值:按 商品+价格+日期 判重 df = df.drop_duplicates(subset=["product_id", "price", "date"]) # IQR异常值标记 q1 = df["daily_sales"].quantile(0.25) q3 = df["daily_sales"].quantile(0.75) iqr = q3 - q1 df["is_outlier"] = (df["daily_sales"] < q1 - 1.5 * iqr) | (df["daily_sales"] > q3 + 1.5 * iqr)清洗完一定要输出一份"清洗前后数据量对比"的统计表,比如"清洗前12000条,清洗后11786条,剔除重复214条,填充缺失68处"。这张表放进文档里,比写一百字"我做了数据清洗"都有说服力。
2.3 特征工程:把日期和标题变成模型认识的数字
模型不认日期,也不认"果汁阳台""龙沙宝石"这种标题,只认数字。特征工程就是把业务信息翻译成数字的过程。我最终用的特征组合是:
| 特征名 | 类型 | 说明 |
|---|---|---|
| month | int | 月份,1~12,捕捉季节性 |
| season | category | 春/夏/秋/冬,转独热编码 |
| is_promotion | int | 是否大促月份(如6月/11月/2月) |
| price_band | category | 价格带:低价/中价/高价 |
| is_cut_flower | int | 是否鲜切花 |
| is_free_shipping | int | 是否包邮 |
| review_count_log | float | 累计评价数取对数,消除量纲差异 |
| title_length | int | 标题字数,间接反映信息丰富度 |
| holiday_effect | float | 节假日影响系数,按月份映射 |
| weather_season_index | float | 季节性种植适宜指数,规则映射 |
这里面有两个点值得说。一个是price_band,直接用连续价格做特征,模型很容易过拟合某几个具体价格,比如"39.9元卖得好"就学成了"39.9附近卖得好",泛化很差。划分成"0-30/30-60/60-100/100+"四个价格带之后,模型学到的就是"低价冲动消费带销量高"这种更稳定的规律。
另一个是is_cut_flower这个特征,非常能打。分析月季数据会发现,鲜切花和盆栽苗的销量逻辑完全不同:鲜切花受节假日脉冲影响大,盆栽苗看季节和品种热度。不把这个维度拆出来,模型会两头打架,预测结果经常飘。我后来查特征重要度,is_cut_flower排在所有特征前两名,这从业务上是讲得通的。
标题文本也可以利用起来。我写了一个简单的分词统计,找出高频品种词,映射成"热门品种/普通品种"二值特征。虽然提升幅度不大,但"从标题提取信息"这一点写进文档,能很自然地引出自然语言处理的一小节,让项目显得更有层次。
特征工程的最后一步是:把所有特征和标签(目标变量,比如"未来7天销量")拼成一个宽表,然后按8:2划分为训练集和测试集。注意,这里不能用随机切分,必须按时间切分,我用前80%的时间段做训练,后20%做测试。原因放到第3节讲,但请先把这句话记住。
3. 预测部分怎么做:ARIMA、随机森林与LSTM的取舍
3.1 先分清要预测什么
很多人拿到题目就开始调模型,结果做出来的功能自己都讲不清楚"我预测的到底是什么"。这个项目里预测目标要分两种,别混在一起:
第一种是时间序列预测:根据某个商品过去N天的日销量,预测未来7天或30天的销量走势。这种适合做单品维度的趋势分析,在可视化页面上表现为"历史曲线+预测曲线"的叠加。
第二种是商品维度预测:给定一批商品的特征(价格带、类目、评价数、季节月份等),预测它们下一个周期的销量。这种适合做"哪些商品会卖得好"的分析,在页面上表现为"商品销量预测排行"。
我做的是两手都上:时间序列用ARIMA做主力,商品维度用随机森林做主力,两类结果都能在系统里看到。这样做的好处是,答辩时无论老师问"你会不会做时间预测"还是"你怎么评估特征对销量的影响",你都有对应的模块可以讲。
3.2 模型选择:为什么我选了随机森林和ARIMA,而不是LSTM
先放结论:这个小数据量的课设,LSTM不是最优选。
给出我的对比逻辑:
| 模型 | 数据需求 | 可解释性 | 调参成本 | 课设友好度 |
|---|---|---|---|---|
| ARIMA | 单变量时间序列,几百条就能训 | 高,可以用ACF/PACF图解释 | 低 | 很高 |
| 随机森林 | 有特征表就能训,几千条足够 | 高,有feature_importances_ | 低 | 很高 |
| XGBoost | 同上,但需要调参更多 | 中高 | 中 | 高 |
| LSTM | 需要大量序列数据,最好上万条 | 低,黑箱 | 高 | 低 |
很多人一看到"预测"就条件反射想到深度学习,但实际上课设数据量往往就几千条,LSTM很容易训出"看起来loss很低、实际预测全是平移"的假模型。你花两周调learning rate和hidden_size,不如花两周把特征工程做扎实。
ARIMA在这个项目里的定位很舒服:单商品销量预测,数据是一维的时间序列,用statsmodels库十几行代码就能跑通,还能画出ACF/PACF图放进文档,解释性极强。随机森林则负责多特征的商品销量预测,输出特征重要度,这又是文档里的一个亮点。
我个人最终采用的是"ARIMA做单品趋势预测 + 随机森林做商品销量预测",两条线互补,不会在答辩时被问倒。
3.3 评价指标和测试集切分:这一步错了全盘皆输
模型评估我用三个指标:MAE、RMSE、MAPE。
- MAE(平均绝对误差):预测值和真实值绝对差值的平均,单位跟销量一致,最直观。
- RMSE(均方根误差):大误差会被放大,适合看有没有"极端离谱"的预测。
- MAPE(平均绝对百分比误差):相对误差,方便在不同销量量级之间对比。
放一段我用的核心代码:
import numpy as np from sklearn.metrics import mean_absolute_error, mean_squared_error def evaluate_prediction(y_true, y_pred): mae = mean_absolute_error(y_true, y_pred) rmse = np.sqrt(mean_squared_error(y_true, y_pred)) mape = np.mean(np.abs((y_true - y_pred) / y_true)) * 100 return {"MAE": round(mae, 2), "RMSE": round(rmse, 2), "MAPE": round(mape, 2)}这里有一个必须强调的坑:时间序列的测试集不能随机切分。如果你用train_test_split默认的shuffle=True,未来日期的数据会被混进训练集,模型相当于"偷看了答案",这叫数据泄漏。体现在结果上就是训练时MAPE低得离谱,拿到新数据立刻崩。正确做法是直接按位置切分,比如前70%训练、后30%测试。
我的实测结果(基于模拟数据,仅供参考):ARIMA预测未来7天日销量的MAPE在15%到25%之间波动,短期趋势基本能抓住;随机森林在商品维度上MAPE约20%到30%,其中鲜切花类目的误差明显小于盆栽类,因为盆栽销量受品种短期热度影响更随机。写进文档的时候,我会明确标注"实验基于模拟数据,不同数据源结果不可直接对比",这话看起来朴素,但能避免答辩被质疑。
如果你做一个提升点,我建议试试将随机森林替换成XGBoost,加一个网格搜索调参,MAPE还能再降几个点。但基础版用随机森林已经足够拿分。
4. 从模型结果到可视化系统:Flask接口与ECharts的落地细节
4.1 技术栈怎么定:Flask + ECharts,别一上来就前后端分离
我知道现在很多教程都在推Vue + DRF或者React + FastAPI的前后端分离,但课程设计只有几周时间,前后端分离意味着你要同时处理跨域、构建、部署,任何一个环节出问题都严重影响进度。我的建议很务实:Flask做后端,前端直接放静态页面,用Ajax调接口,可视化用ECharts。
它的好处是:Python全栈一条龙,不需要Node环境;Flask的render_template可以直接把页面渲染出来,也可以返回JSON字符串给前端Ajax使用;调试方便,本地跑起来就是一个完整的系统。这东西拿去做演示完全够用。
4.2 后端接口设计:拆成四个接口,页面永远不卡
系统一共设计四个核心接口,每个接口只管一件事:
| 接口路径 | 作用 | 返回内容 |
|---|---|---|
| /api/overview | 顶部概览卡片 | 商品总数、销量Top3、月均销量、预测总量 |
| /api/sales_trend | 历史销售趋势 | 近12个月每月总销量、环比增长率 |
| /api/prediction | 模型预测结果 | 单品7天预测值、商品销量预测Top10 |
| /api/analysis | 多维统计 | 价格带销量分布、类目占比、特征重要度、高频关键词 |
后端写起来不复杂,核心是"从数据库取数 → 转成前端要的JSON结构 → 返回"。这里有个容易踩的坑:pandas里的numpy.float64类型不能直接传给jsonify,会报"Object of type float64 is not JSON serializable"。解决方法是统一转成Python原生类型:
from flask import Flask, jsonify import pandas as pd app = Flask(__name__) def json_safe(obj): if hasattr(obj, "item"): return obj.item() return obj @app.route("/api/prediction") def prediction(): result = model.predict_next_7days() return jsonify({ "dates": result["dates"], "predicted_sales": [json_safe(x) for x in result["values"]] })所有的预测结果,我还额外落了一份到数据库表prediction_result里。缓存下来有两个好处:一是页面重复请求不用每次都重新跑模型,响应速度快很多;二是演示的时候即使断网,历史预测结果也能正常展示。这个细节在答辩现场很加分,老师会看到你考虑到了工程稳定性。
4.3 前端图表怎么摆:让老师一眼看懂预测逻辑
可视化页面我分成四块:
第一块是顶部概览卡片,显示核心指标:商品总数、在售商品均价、月均销量、未来7天预测总销量,每个卡片配一个极简ECharts小环图,看起来专业。
第二块是核心预测区,一条双折线图,实线是历史销量、虚线是预测销量。这一块是门面,颜色一定要有区分度,比如我用深蓝色表示真实值、橙红色表示预测值,图例加粗,一眼能看懂"哪段是历史哪段是预测"。
第三块是特征分析区,左侧柱状图展示价格带对销量的影响,右侧条形图展示随机森林的特征重要度。特征重要度这个图特别能唬住外行,也让懂行的人知道你真的训练了模型,不是画着玩。
第四块是补充分析区,类目占比饼图、词云(展示常见品种词)、热力图(横轴星期、纵轴月份,颜色深浅表示销量高低)。热力图做出来非常出效果,而且能反应周期性规律。
ECharts使用中的两个小坑也提醒一下。第一个是容器宽度,图表容器不要用百分比直接套在flex布局里,ECharts在隐藏容器中初始化会拿到0宽度,必须等页面渲染完成后再初始化,或者用window.onresize绑定resize()方法。第二个是Y轴刻度,销量数据如果波动大,ECharts默认会截掉起点,看起来曲线"飞起来了",建议在Y轴配置里设置scale: true并加上axisLabel格式化,让数据范围展示更合理。
5. 源码组织、数据库和万字文档:课设交付物的完整拼图
5.1 项目目录怎么分:照着这个结构写不会乱
代码结构直接决定答辩演示顺序,我用的是这个目录结构:
project/ ├── app.py # Flask主入口 ├── models.py # 数据库模型定义 ├── database.db # SQLite数据库文件 ├── data/ │ ├── raw/ # 原始数据 │ └── processed/ # 清洗后数据 ├── predictors/ │ ├── arima_model.py # ARIMA时间序列预测 │ ├── rf_model.py # 随机森林销量预测 │ └── feature_engineering.py # 特征工程方法 ├── static/ │ ├── css/ │ ├── js/ │ └── images/ ├── templates/ │ └── index.html # 可视化主页面 ├── docs/ │ ├── 需求分析.md │ ├── 系统设计.md │ └── 实验报告.md └── requirements.txt数据库我用SQLite,零配置、单文件、好迁移。三张核心表之间的关联关系是:product表为主表,一对多关联daily_sales表;预测结果单独存一张prediction_result表,存商品ID、预测日期、预测值、模型类型、生成时间。表结构写清楚后,写文档时画一个简单的关系图,数据库设计部分就算过关了。
5.2 万字文档怎么写:别凑字数,按"能复现"的标准写
"万字文档"听起来吓人,但如果只是把代码贴一遍,贴一万字也拿不到高分。我的策略是围绕"这个系统到底怎么一步步做出来的"来写,完全对照真实流程,让一个没有源码的人照着文档能复现个七八成。
文档章节我这样安排:
- 项目背景与研究意义:讲月季电商的行业背景、为什么需要销量预测
- 需求分析:功能需求和非功能需求,配用例图
- 系统总体设计:系统架构图、技术选型对比(这里写为什么选Flask不选前后端分离)
- 数据库设计:三张表字段说明、ER图、关键索引
- 数据获取与预处理:数据来源合规说明、清洗前后对比、异常值处理策略
- 特征工程与模型选择:特征表、模型对比表、评价指标定义
- 实验结果分析:展示预测曲线截图、误差指标、特征重要度图
- 可视化系统实现:接口文档、页面截图、核心代码片段
- 系统测试:功能测试用例表、性能测试结果
- 总结与展望:不足和改进方向
写的时候有一个原则:每个章节都要有图和表。同样一段"我做了数据清洗",配上一张清洗前后的数据量对比表,再加一张异常值分布图,信息量完全不同。我在最终版本里一共放了24张图、11张表格,正文加上代码和注释,自然就到1.2万字了。但更重要的是,整个文档逻辑链是完整的,老师看目录就能知道你做了什么。
5.3 答辩高频问题:被问倒的地方我帮你提前堵上
我把答辩现场老师最可能问的问题列了个清单,每个我都准备了答法,你也可以按这个思路准备:
问:为什么测试集不能随机划分?答:因为销量数据是时间序列,未来数据会被混进训练集,模型相当于提前看到了答案,这叫数据泄漏。必须按时间顺序切分,保证训练的数据时间都早于测试数据。
问:MAE=20是什么意思?模型算准吗?答:MAE=20表示平均每个预测日的销量与真实销量相差20件。如果商品日销量的量级是100件,那MAPE就是20%左右,属于可接受范围;如果商品日销量就是20件,那这误差就太大了,不能用于实际决策。指标好不好取决于业务量级。
问:为什么用随机森林,不用深度学习?答:项目的数据量是几千条级别的,深度学习在小样本上容易过拟合,而且可解释性差。随机森林能输出特征重要度,可以看到"是否鲜切花""价格带"等特征的贡献排序,方便从业务角度验证模型合理性。如果后续数据量扩大,可以平滑升级到XGBoost或梯度提升树。
问:模拟数据能说明问题吗?答:模拟数据的生成不是随机拍脑袋,而是基于电商平台的公开销量区间和评价数等可见信息,结合行业销售规律设计了周期性、节假日效应、大促效应,模型验证的是方法链路的有效性。在真实业务数据上替换数据源即可,代码结构不需要改。
问:预测不准怎么办?答:先看数据质量,再看特征是否足够。我发现做这个项目的过程中,预测误差最大的单品往往都是促销活动导致销量突变的商品,这种波动天生难以预测,属于业务本身的噪声,不是模型的bug。在文档里专门写一节"误差分析",比硬着头皮说"模型很准"更可信。
线上演示最容易被忽视的小细节
最后再补充几个线上演示的经验。
演示时一定提前把数据库生成好、模型结果缓存好,不要现场训练。有一次我演示到预测页面,模型重新跑了30秒,页面一直转圈,场面一度尴尬。后来的方案是:页面加载时优先读取数据库里缓存的预测结果,只有触发"重新训练"按钮才真正跑模型。
还有一点,演示用的电脑分辨率如果是1080P,图表一定不要设计成超宽大屏,否则缩放会变形。我最终的页面宽度控制在1200px以内,适配笔记本演示,不需要外接大屏也能看得很清楚。
这个项目做完以后,我最深的体会是:课程设计的分数不取决于模型多高级,而取决于每一步有没有讲清楚"为什么"。数据清洗为什么要用前后均值填补而不是直接删行?特征为什么选价格带而不是用原始价格?测试集为什么不随机切分?把这些想明白,你的系统、文档和答辩自然就立起来了。