做 Python 电商数据预测,我把爬虫、Transformer 和 Django 串成了完整方案
之前在做电商销量数据相关的分析项目时,我明显感觉到一个痛点:很多教程只讲其中某一个环节,比如“用爬虫抓商品数据”“用 pandas 做可视化”“调一个 Transformer 模型”。但实际业务里,数据获取、清洗、建模、部署是环环相扣的,少了哪一块都无法在真实场景里跑通。
这篇文章围绕“Python 电商数据分析——销量预测”这个主线,把数据采集、分析可视化、Transformer 销量预测、Django 后端展示串成一个可运行的完整闭环。新手可以跟着搭建环境并理解整体流程,有基础的开发者可以直接参考代码做扩展。文章会给出核心代码、配置文件、运行命令和常见报错排查,力求在本地环境也能复现。项目思路以常见电商场景为例,不绑定特定平台,重点在方法论和技术栈整合。
1. 技术链路与核心概念
1.1 为什么要把爬虫、Transformer、Django 放在一起讨论
电商数据分析并不是一个独立任务,而是一套数据流水线。先从公开数据源抓取商品信息、价格、评论数和销量表现,然后对抓取到的数据进行清洗,因为原始数据大概率存在缺失、重复、单位不一致等问题。清洗之后可以做可视化和特征分析,辅助理解销量波动规律。如果要做销量预测,则需要构建时序数据集并训练模型,Transformer 是当前效果较好的序列建模方式之一。最后要让他人访问分析结果,就需要一个 Web 后端,Django 是最常见的 Python 框架。
所以这条链路通俗理解是:
- 爬虫:解决“数据从哪来”的问题。
- pandas + pyecharts:解决“数据长什么样”的问题。
- Transformer:解决“未来销量怎么变”的问题。
- Django:解决“结果怎么给别人用”的问题。
如果只看单独一环,在真实业务中很难落地。比如 Transformer 模型训练得再好,没有数据就是空谈;爬虫抓到的数据不经过清洗,直接训练模型的误差也会很大。因此理解全链路比只掌握某个库更重要。
1.2 销量预测场景中的数据特点
电商销量数据有两个显著特点:
第一个特点是周期性。销量往往表现出天级别的规律,例如工作日和周末会有差异,也有周级别的趋势。很多商品会受季节、大促和搜索热度影响。
第二个特点是序列性。销量预测本质上是时间序列预测问题:我们使用过去 N 天的销量来预测未来 M 天的销量。这里就涉及序列数据的窗口切分,也就是用滑窗方式构造训练样本。
Transformer 最初是为自然语言处理设计的,它的核心是注意力机制。注意力机制可以让模型关注一段时间内最重要的历史信息,因此 Transformer 被扩展到了时间序列预测领域,并在部分场景中表现优于 LSTM。
1.3 Django 在项目中的定位
Django 是一个重量级 Python Web 框架,自带 ORM、Admin 后台、模板系统和请求处理机制。本项目中 Django 并不是用来做算法训练的,而是负责三件事:
- 对外提供销量预测的 HTTP 接口。
- 展示数据可视化图表页面。
- 管理采集和预测的结果数据。
使用 Django 的最大好处是,后续如果想给前端 App、小程序或者报表系统提供数据服务,只需复用同一个 ORM 模型和 API 逻辑,不需要重写业务代码。
2. 环境准备与项目结构
2.1 版本说明
本文代码以 Python 3.10 以上版本为基础,如果你的电脑安装的是 3.8 或 3.9,大部分代码也可以运行。建议使用虚拟环境隔离项目依赖,避免不同项目之间包版本冲突。版本需要根据你的实际环境调整,本文示例以常见组合为例。
核心依赖如下:
| 依赖库 | 主要用途 |
|---|---|
| requests | 发起 HTTP 请求,抓取网页数据 |
| pandas | 数据清洗与结构化处理 |
| pyecharts | 可视化图表生成 |
| numpy | 数值计算 |
| torch | Transformer 模型训练与推理 |
| django | Web 后端与 API 开发 |
| django-cors-headers | 解决本地跨域请求问题(可选) |
安装时可以使用下面的命令:
pip install requests pandas pyecharts numpy torch django django-cors-headers国内网络环境下,如果下载速度较慢,可以临时指定清华镜像源:
pip install requests pandas pyecharts numpy torch django django-cors-headers -i https://pypi.tuna.tsinghua.edu.cn/simple2.2 项目目录设计
良好的目录结构能降低协作成本。建议按功能模块拆分:
ecommerce_analysis/ ├── crawler/ │ ├── __init__.py │ └── spider.py ├── analysis/ │ ├── __init__.py │ ├── data_clean.py │ └── visualize.py ├── model/ │ ├── __init__.py │ ├── dataset.py │ ├── transformer.py │ └── train.py ├── web_ui/ │ ├── manage.py │ ├── web_ui/ │ │ ├── __init__.py │ │ ├── settings.py │ │ ├── urls.py │ │ └── wsgi.py │ └── analysis_app/ │ ├── __init__.py │ ├── models.py │ ├── views.py │ ├── urls.py │ └── templates/ └── data/ ├── raw_data.csv └── clean_data.csv其中crawler模块负责采集,analysis模块负责清洗与图形化分析,model模块负责销量预测模型训练,web_ui是 Django 工程目录。
在项目的根目录下,可以创建一个requirements.txt文件,方便其他环境安装依赖。依赖内容根据实际安装版本生成即可,不需要手工指定太细的版本号。
3. 爬虫模块:获取商品销量与评论数据
3.1 爬虫是整个项目的数据源头
训练模型需要历史销量数据。这里以模拟公开商品数据为例,展示如何用 requests 抓取一个商品列表页面的基础信息。
电商网站的结构经常调整,而且反爬策略各不相同,所以本文不针对任何特定平台做逆向,而是给出通用思路。在实际项目中,需要先确认目标数据源是否允许采集,并遵守平台规则和 robots 协议。建议优先使用公开 API 或官方数据,实在没有的情况下再考虑页面解析。
这段代码的作用是:
- 构建请求头,模拟普通浏览器访问。
- 使用 requests.get 获取页面文本。
- 利用简单字符串拆分提取商品标题、价格和销量字符串。
# 文件路径:crawler/spider.py import requests import re import pandas as pd class SimpleShopSpider: """ 一个简单通用的商品页面示例爬虫。 生产环境中应替换为真实合规的数据源,并控制采集频率。 """ def __init__(self): self.session = requests.Session() self.session.headers.update({ "User-Agent": "Mozilla/5.0 (Windows NT 10.0; Win64; x64) " "AppleWebKit/537.36 (KHTML, like Gecko) " "Chrome/120.0.0.0 Safari/537.36" }) def fetch_page(self, url: str) -> str: response = self.session.get(url, timeout=10) response.encoding = response.apparent_encoding response.raise_for_status() return response.text def parse_item_blocks(self, html: str, pattern: str): """ 这里只是演示一种通用正则提取方式。 真实项目中建议使用 lxml 或 BeautifulSoup 做结构化解析。 """ return re.findall(pattern, html, re.S) def demo_run(self): demo_html = """ <div class="item"> <span class="title">无线蓝牙耳机</span> <span class="price">199.00</span> <span class="sales">月销 2000+</span> </div> <div class="item"> <span class="title">智能手环</span> <span class="price">149.00</span> <span class="sales">月销 3500+</span> </div> """ title_list = self.parse_item_blocks(demo_html, r'class="title">(.*?)</span>') price_list = self.parse_item_blocks(demo_html, r'class="price">(.*?)</span>') sales_list = self.parse_item_blocks(demo_html, r'class="sales">月销\s*(\d+)\+') rows = [] for title, price, sales in zip(title_list, price_list, sales_list): rows.append({ "title": title.strip(), "price": float(price), "sales": int(sales) }) return pd.DataFrame(rows) if __name__ == "__main__": spider = SimpleShopSpider() df = spider.demo_run() print(df)这个示例中有几个需要注意的点。第一,requests.Session可以复用连接,减少多次请求的资源开销。第二,response.encoding = response.apparent_encoding是常用的中文编码修正方式,如果页面是 GBK 编码,这里可以避免乱码。第三,正则提取只适合结构简单的静态页面,真实网页推荐改用 XPath。
跑完上面代码后,可以看到类似下面的输出:
title price sales 0 无线蓝牙耳机 199.0 2000 1 智能手环 149.0 3500到此爬虫部分已经完成了从网页到 pandas DataFrame 的转换。
3.2 爬虫运行无输出但 exit code 0 的排查思路
开发阶段很常见的一个现象是,爬虫程序运行后没有打印任何内容,控制台只显示proceed finished with exit code 0。这不一定是代码出错了,很可能是程序逻辑中没有触发输出语句,或者if __name__ == "__main__"模块没有执行预期逻辑。
排查时可以按照这样的顺序:
- 检查主入口是否有 print 输出。
- 检查函数返回值是否被接收。
- 如果请求真实网页,检查是否被反爬拦截,返回了空页面或验证码页面。
- 检查正则表达式是否匹配到数据,可以先用少量文本单独测试。
- 确认目标页面需要登录或者 Cookie,如果缺少 Cookie,页面内容与预期完全不同。
在实际项目中,爬虫采集一定要控制频率,比如在两次请求之间加time.sleep(1)到time.sleep(3),避免给目标服务器造成压力。
4. 数据清洗与可视化分析
4.1 用 pandas 处理缺失值和重复值
采集到的 CSV 数据通常存在三类问题:空值、重复记录、文本中有多余符号。下面是一个典型的数据清洗代码。
# 文件路径:analysis/data_clean.py import pandas as pd def load_raw_data(file_path: str) -> pd.DataFrame: df = pd.read_csv(file_path) print(f"原始数据形状:{df.shape}") return df def clean_sales_data(df: pd.DataFrame) -> pd.DataFrame: # 1. 删除完全重复的记录 df = df.drop_duplicates() # 2. 移除销量为空的行 df = df.dropna(subset=["sales"]) # 3. 价格、销量转为数值类型,出错时使用 NaN df["price"] = pd.to_numeric(df["price"], errors="coerce") df["sales"] = pd.to_numeric(df["sales"], errors="coerce") # 4. 删除仍然为 NaN 的行 df = df.dropna(subset=["price", "sales"]) # 5. 日期字段统一格式(如果有) if "date" in df.columns: df["date"] = pd.to_datetime(df["date"], errors="coerce") df = df.dropna(subset=["date"]) df = df.sort_values("date") return df数据清洗过程中最容易被忽略的是errors="coerce"。如果原始 Excel 或 CSV 中某些单元格是“暂无报价”这样的文本,直接用pd.to_numeric会报错,而加上errors="coerce"后会把无法转换的内容置为 NaN,后续统一过滤掉即可。
删除空值时要注意业务含义。如果销量为空是因为商品刚上架没有销量,直接删除可能导致样本减少,此时可以结合业务决定是否填充为 0。批量数据操作前建议先对数据做备份。
4.2 销售趋势可视化
可视化环节推荐 pyecharts,因为它生成的是网页图表,能够方便后续嵌入 Django 页面,与 ECharts 风格统一。
下面生成一个折线图,展示每天销量变化趋势。
# 文件路径:analysis/visualize.py from pyecharts.charts import Line from pyecharts import options as opts import pandas as pd def draw_sales_trend(df: pd.DataFrame, output_path: str = "sales_trend.html"): # 假设 df 包含 date 和 sales 两列 df = df.sort_values("date") date_list = df["date"].astype(str).tolist() sales_list = df["sales"].tolist() line = ( Line() .add_xaxis(date_list) .add_yaxis( "销量", sales_list, is_smooth=True, is_symbol_show=False, linestyle_opts=opts.LineStyleOpts(width=2, color="#5470C6"), ) .set_global_opts( title_opts=opts.TitleOpts(title="商品销量趋势"), tooltip_opts=opts.TooltipOpts(trigger="axis"), yaxis_opts=opts.AxisOpts(name="销量"), xaxis_opts=opts.AxisOpts(name="日期", axislabel_opts=opts.LabelOpts(rotate=30)), ) ) line.render(output_path) print(f"图表已生成:{output_path}")如果需要对销量和价格做联合分析,可以使用散点图:
from pyecharts.charts import Scatter def draw_price_sales_scatter(df: pd.DataFrame): data_pair = df[["price", "sales"]].dropna().values.tolist() scatter = ( Scatter() .add_xaxis([item[0] for item in data_pair]) .add_yaxis("销量", [item[1] for item in data_pair]) .set_global_opts( title_opts=opts.TitleOpts(title="价格与销量关系"), xaxis_opts=opts.AxisOpts(name="价格"), yaxis_opts=opts.AxisOpts(name="销量"), ) ) return scatter可视化不只是为了好看,更重要的是帮助分析人员判断序列是否有明显趋势、周期性,以及价格波动和销量之间的关系。训练 Transformer 模型前,做一次趋势图观察可以避免盲目训练。
5. Transformer 销量预测模型
5.1 为什么销量预测可以用 Transformer
在销量预测场景中,我们通常会使用滑窗序列,例如用前 7 天销量预测下一天销量。输入是一个长度为 7 的序列,输出是一个值。相比算法细节,更关键的是数据如何编码成序列。
Transformer 模型中最重要的两个结构是位置编码和多头注意力。位置编码用于给模型提供顺序信息,多头注意力可以帮助模型学习“在第 3 天的销量和第 5 天销量之间可能存在强关联”这样的模式。
从工程实现角度,这里使用 PyTorch 自带的TransformerEncoder来搭建一个简化版预测模型,能够处理任意长度的历史窗口。完整的多头注意力实现比较复杂,但通过 PyTorch 封装,我们可以把注意力集中在业务逻辑上。
5.2 时间序列窗口数据集
先把销量数据切分成样本。假设窗口大小为window_size=7,那么前 7 天的销量向量对应第 8 天的销量标签。
# 文件路径:model/dataset.py import numpy as np import torch from torch.utils.data import Dataset class SalesDataset(Dataset): def __init__(self, sales_values: np.ndarray, window_size: int = 7, predict_step: int = 1): # 将数据转为 float32,便于后续计算 self.data = sales_values.astype(np.float32) self.window_size = window_size self.predict_step = predict_step def __len__(self): return len(self.data) - self.window_size - self.predict_step + 1 def __getitem__(self, idx): x_start = idx x_end = idx + self.window_size y_index = x_end + self.predict_step - 1 x = self.data[x_start:x_end] y = self.data[y_index] return torch.from_numpy(x).unsqueeze(-1), torch.tensor(y)这里有两个细节值得说明。第一,x的 shape 被改成[window_size, 1],因为 TransformerEncoder 期望输入维度是[sequence_length, batch_size, feature_size]。第二,输出只取未来一天的值,如果要做多步预测,可以在训练后递归把预测结果加入窗口。
构造训练集和测试集时,要避免数据泄露,也就是说不能用测试集的数据去参与训练集的归一化。正确做法是先用训练集计算均值和标准差,再用同样的参数处理测试集。
# 归一化示例 from sklearn.preprocessing import StandardScaler scaler = StandardScaler() train_values = train_df[["sales"]].values scaler.fit(train_values) train_scaled = scaler.transform(train_values) test_scaled = scaler.transform(test_df[["sales"]].values)5.3 构建 Transformer 预测模型
定义模型时,我们使用nn.TransformerEncoderLayer和nn.TransformerEncoder。由于销量数据通常是连续值,所以这里使用回归头输出一个标量值。
# 文件路径:model/transformer.py import math import torch import torch.nn as nn class PositionalEncoding(nn.Module): def __init__(self, d_model: int, max_len: int = 1000, dropout: float = 0.1): super().__init__() self.dropout = nn.Dropout(p=dropout) pe = torch.zeros(max_len, d_model) position = torch.arange(0, max_len, dtype=torch.float).unsqueeze(1) div_term = torch.exp(torch.arange(0, d_model, 2).float() * (-math.log(10000.0) / d_model)) pe[:, 0::2] = torch.sin(position * div_term) pe[:, 1::2] = torch.cos(position * div_term) pe = pe.unsqueeze(0).transpose(0, 1) self.register_buffer("pe", pe) def forward(self, x): # x: [sequence_length, batch_size, d_model] x = x + self.pe[: x.size(0), :] return self.dropout(x) class SalesTransformer(nn.Module): def __init__(self, feature_size: int = 1, d_model: int = 32, nhead: int = 4, num_layers: int = 2, window_size: int = 7): super().__init__() self.d_model = d_model self.input_proj = nn.Linear(feature_size, d_model) self.positional_encoding = PositionalEncoding(d_model) encoder_layer = nn.TransformerEncoderLayer(d_model=d_model, nhead=nhead, batch_first=False) self.transformer_encoder = nn.TransformerEncoder(encoder_layer, num_layers=num_layers) self.output_proj = nn.Linear(d_model, 1) def forward(self, x): # x 输入 shape: [batch_size, window_size, feature_size] # TransformerEncoder 要求 [sequence_length, batch_size, feature_size] x = x.permute(1, 0, 2) x = self.input_proj(x) * math.sqrt(self.d_model) x = self.positional_encoding(x) x = self.transformer_encoder(x) # 取最后一个时间步的输出做预测 x = x[-1, :, :] out = self.output_proj(x) return out.squeeze(-1)模型的关键点是permute,这一步把[batch_size, window_size, feature_size]转成[window_size, batch_size, feature_size]。新手在这里容易忘记维度变化,如果出现报错,优先检查张量形状。d_model是内部隐藏维度,如果数据量较小,设置 32 或 64 就够,没必要设置 512 那样的大维度。
5.4 训练脚本
训练思路比较简单:确定模型、确定损失函数、确定优化器,然后按批次迭代。每次反向传播前需要把上一轮梯度清零,否则梯度会累积。
# 文件路径:model/train.py import numpy as np import torch import torch.nn as nn from torch.utils.data import DataLoader from dataset import SalesDataset from transformer import SalesTransformer def build_loader(values: np.ndarray, window_size: int = 7, batch_size: int = 32, shuffle: bool = True): dataset = SalesDataset(values, window_size=window_size) loader = DataLoader(dataset, batch_size=batch_size, shuffle=shuffle) return loader def train_model(train_loader, model, epochs=30, lr=1e-3): device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) optimizer = torch.optim.Adam(model.parameters(), lr=lr) criterion = nn.MSELoss() for epoch in range(epochs): model.train() total_loss = 0.0 for x_batch, y_batch in train_loader: x_batch = x_batch.to(device) y_batch = y_batch.to(device) optimizer.zero_grad() pred = model(x_batch) loss = criterion(pred, y_batch) loss.backward() optimizer.step() total_loss += loss.item() * x_batch.size(0) avg_loss = total_loss / len(train_loader.dataset) if (epoch + 1) % 5 == 0: print(f"Epoch {epoch + 1}/{epochs}, Loss: {avg_loss:.6f}") return model训练轮数epochs并不总是越多越好。实际项目中发现,数据量较小的时候训练 20 到 50 轮就足够了,轮数太多可能让模型记住训练集噪声,导致验证集误差上升。可以在每个 epoch 后打印验证集损失,选择验证集损失最低的模型参数保存。
保存模型时,推荐只保存状态字典而不是整个模型对象,这样在版本升级后更容易加载。
torch.save(model.state_dict(), "sales_transformer.pth")5.5 简单推理与模型评估
训练完成后,模型需要还原预测值。由于我们之前做了标准化,预测结果要先反标准化才能得到真实的销量数字。
def predict_next_sales(model, recent_values: np.ndarray, scaler, window_size: int = 7): model.eval() device = torch.device("cuda" if torch.cuda.is_available() else "cpu") model.to(device) if len(recent_values) < window_size: raise ValueError(f"最近数据长度必须大于等于 {window_size}") last_window = recent_values[-window_size:].reshape(1, window_size, 1) last_window_tensor = torch.from_numpy(last_window.astype(np.float32)).to(device) with torch.no_grad(): scaled_pred = model(last_window_tensor).cpu().numpy() # scaler 在训练时对 2D 数据做标准化,因此这里需要 reshape pred_real = scaler.inverse_transform(scaled_pred.reshape(-1, 1)) return float(pred_real[0][0])评估指标常用 MAE 和 RMSE,它们的含义都是误差越小越好。RMSE 对大误差更敏感,如果数据中存在个别异常大促日,RMSE 会明显放大,此时可以结合 MAE 一起观察。
6. Django 后端整合与页面展示
6.1 创建 Django 项目与子应用
前面几节完成了爬虫、分析和建模,现在用 Django 把能力封装成一个 Web 应用。第一步先创建项目。
django-admin startproject web_ui cd web_ui python manage.py startapp analysis_app创建完成后,把analysis_app添加到INSTALLED_APPS中。
# 文件路径:web_ui/web_ui/settings.py INSTALLED_APPS = [ "django.contrib.admin", "django.contrib.auth", "django.contrib.contenttypes", "django.contrib.sessions", "django.contrib.messages", "django.contrib.staticfiles", "corsheaders", "analysis_app", ]如果前后端分离开发,需要允许跨域请求,则在settings.py中追加:
MIDDLEWARE = [ "corsheaders.middleware.CorsMiddleware", ... ] CORS_ALLOW_ALL_ORIGINS = True在正式项目中,CORS_ALLOW_ALL_ORIGINS不建议设置为 True,因为等于允许任何网站访问后端接口。更安全的做法是指定允许的域名列表。
6.2 设计销量数据模型
Django ORM 模型只需要定义几个字段,后续可以通过python manage.py makemigrations和python manage.py migrate同步数据库。
# 文件路径:web_ui/analysis_app/models.py from django.db import models class Product(models.Model): title = models.CharField(max_length=255, verbose_name="商品标题") price = models.FloatField(verbose_name="价格") created_at = models.DateTimeField(auto_now_add=True, verbose_name="创建时间") class Meta: verbose_name = "商品" verbose_name_plural = "商品" class DailySales(models.Model): product = models.ForeignKey(Product, on_delete=models.CASCADE, verbose_name="商品") date = models.DateField(verbose_name="日期") sales = models.FloatField(verbose_name="销量") class Meta: verbose_name = "每日销量" verbose_name_plural = "每日销量" unique_together = ("product", "date")这里用到ForeignKey关联商品和每日销量。注意unique_together = ("product", "date")可以防止同一天同一条商品写入多条记录,这比在代码里做重复判断更可靠。
6.3 构建预测结果 API
接下来提供两个接口,一个是获取可视化数据,另一个是执行预测。为了简化,这里把训练好的模型放在analysis_app/services.py中统一加载。
# 文件路径:web_ui/analysis_app/services.py import pickle import numpy as np class SalesPredictor: def __init__(self, model_path: str, scaler_path: str, window_size: int = 7): self.window_size = window_size # 这里建议使用模型加载函数,避免在 view 中重复读取 self.model = load_sales_model(model_path) with open(scaler_path, "rb") as f: self.scaler = pickle.load(f) def predict(self, recent_sales: list): values = np.array(recent_sales, dtype=np.float32) return predict_next_sales(self.model, values, self.scaler, self.window_size)实际代码中,我建议把训练好的.pth文件放在web_ui/models/目录下,并在settings.py中配置路径变量,这样不会散落得到处都是。
6.4 编写视图与路由
视图函数中,先把数据库中的最近 N 天销量取出来,再调用模型得到预测结果,最后返回 JSON。
# 文件路径:web_ui/analysis_app/views.py import json from django.http import JsonResponse from django.views.decorators.csrf import csrf_exempt from .models import Product, DailySales @csrf_exempt def predict_sales_api(request): if request.method != "POST": return JsonResponse({"code": 400, "message": "仅支持 POST 请求"}, status=400) body = json.loads(request.body or "{}") product_id = body.get("product_id") if not product_id: return JsonResponse({"code": 400, "message": "缺少 product_id"}, status=400) sales_rows = DailySales.objects.filter(product_id=product_id).order_by("-date")[:30] sales_values = [row.sales for row in reversed(sales_rows)] if len(sales_values) < 7: return JsonResponse({"code": 400, "message": "历史销量不足 7 天,无法预测"}, status=400) predictor = load_predictor() next_sales = predictor.predict(sales_values) return JsonResponse({"code": 0, "data": {"predict_sales": round(next_sales, 2)}})这段代码中我加了两个判断:请求方法必须为 POST,历史数据必须足够。原因在于销量预测至少需要一个长度为window_size的窗口。把这类判断放在业务入口处,可以减少调用模型时出现维度错误。
路由配置分为两步。首先在web_ui/web_ui/urls.py中包含子应用的路由:
# 文件路径:web_ui/web_ui/urls.py from django.contrib import admin from django.urls import path, include urlpatterns = [ path("admin/", admin.site.urls), path("api/", include("analysis_app.urls")), ]然后在子应用中增加路由:
# 文件路径:web_ui/analysis_app/urls.py from django.urls import path from . import views urlpatterns = [ path("predict/", views.predict_sales_api), ]这里并不需要在真实项目中暴露 Django 默认的 Admin 后台,如果只是为了给内部使用,可以保留;如果是生产环境,建议对 Admin 路径做二次保护,例如结合django-admin-honeypot之类的插件,或至少修改默认 URL。
6.5 页面展示数据
如果不想单独开发前端页面,可以直接在 Django 的模板文件中引用 ECharts 的 CDN,通过一个简单的页面读取本地静态 JSON 数据。下面是一个折线图页面模板思路。
<!-- 文件路径:web_ui/analysis_app/templates/sales_chart.html --> <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="UTF-8"> <title>销量趋势分析</title> <script src="https://cdn.jsdelivr.net/npm/echarts@5/dist/echarts.min.js"></script> </head> <body> <div id="chart" style="width: 90%; height: 500px; margin: 20px auto;"></div> <script> fetch("/api/sales_trend/?product_id=1") .then(function (response) { return response.json(); }) .then(function (data) { var chart = echarts.init(document.getElementById("chart")); chart.setOption({ title: { text: "商品销量趋势" }, tooltip: { trigger: "axis" }, xAxis: { type: "category", data: data.dates }, yAxis: { type: "value" }, series: [{ type: "line", data: data.sales, smooth: true }] }); }); </script> </body> </html>实际开发中,如果使用本地可视化工具生成的 HTML 文件,也可以用 iframe 嵌入,但在 Django 项目里更推荐直接调用 ECharts 的 API 数据。这样数据更新后图表会同步变化,不需要重新生成整个 HTML。
7. 全流程串联与运行结果
7.1 执行顺序建议
整个项目最终的执行顺序如下:
- 启动爬虫脚本,抓取原始数据并保存为 CSV。
- 运行数据清洗脚本,输出干净数据文件。
- 用 pandas 处理出按天的销量序列。
- 运行模型训练脚本,得到
sales_transformer.pth和归一化器文件。 - 启动 Django 服务。
- 调用预测 API 验证结果。
可以先写一个根目录的run_all.py调度脚本,方便一键执行前四步,但这只是为了本地测试方便,正式项目中不建议把训练步骤和 Web 服务启动混在一起。
7.2 Django 启动与 API 调用
一切就绪后,使用下面的命令启动 Web 服务:
python manage.py runserver启动成功后访问http://127.0.0.1:8000,如果看到 Django 默认页面,说明项目正常。然后用 curl 模拟 POST 请求:
curl -X POST http://127.0.0.1:8000/api/predict/ \ -H "Content-Type: application/json" \ -d '{"product_id": 1}'返回内容大致如下:
{ "code": 0, "data": { "predict_sales": 326.55 } }请记住,预测结果只是基于历史趋势给出的参考值。实际销量会受到促销、商品评价、搜索热度、库存和平台流量分配影响,所以线上使用预测结果时需要配合人工校验。
7.3 如何判断预测结果是否合理
最简单的判断方式是把预测值和最近几天的实际销量画在同一张图上。如果最近一周销量一直稳定在 300 左右,模型预测 326,属于合理波动。如果销量中有大促日,比如 11 月 11 日销量是平日的 20 倍,而模型没有把促销特征纳入,那么预测值很可能严重偏小。此时应该补充额外的特征列,例如“是否促销日”“是否是周末”。
8. 高频问题排查与解决思路
下面是整理出来的 JSON 格式异常排查表,适用于项目联调阶段。
| 问题现象 | 常见原因 | 解决思路 |
|---|---|---|
启动时提示ModuleNotFoundError: No module named 'torch' | 当前 Python 环境中未安装 PyTorch | 执行pip install torch,并检查是否激活了正确的虚拟环境 |
执行迁移时报错django.db.utils.OperationalError: no such table | 还没执行migrate | 先运行python manage.py makemigrations analysis_app,再运行python manage.py migrate |
| 爬虫运行后无输出 | 主入口逻辑没有 print 或正则没匹配到结果 | 增加 print 调试,先用本地 HTML 字符串测试正则 |
| 训练时 loss 为 NaN | 数据中存在无穷值或学习率过大 | 检查标准化结果,确认没有 inf,调低 lr 到 1e-4 |
| Transformer 维度报错 | 输入张量 shape 不符合[seq_len, batch, feature] | 打印每个张量的 shape,确认 permute 是否生效 |
| Django 接口返回 403 | CSRF 验证失败 | 如果是内部调试接口可使用@csrf_exempt;生产环境建议使用认证和 CSRF Token |
| 预测结果一直是同一个固定值 | 模型没有收敛或输入窗口数据标准化方式错误 | 检查训练是否完成,打印训练集和测试集损失 |
在实际排查中,最常用的是打印中间结果的 shape。尤其在 PyTorch 项目中,维度不匹配是最高频问题。可以写一行临时代码:
print("x shape:", x.shape)然后在出错前观察输出即可。这类调试代码在确认无误后记得删除,或者用logging.debug保留下来。
9. 工程化建议与生产环境注意事项
9.1 数据处理与合规
电商数据采集必须遵守法律法规和平台规则。不要把爬虫模块做成绕过登录、暴力遍历等高风险工具。实际方案里应优先使用平台开放的 API、公开数据集,或者自行记录业务数据库中的历史数据。对于需要登录才能访问的内容,必须获得授权再采集。
采集代码中要加入频率控制、超时设置、重试机制。不要对目标网站发起高并发请求,否则既容易触发风控,也可能给目标服务器带来压力。
9.2 模型训练的工程化
模型训练中最重要的习惯是固定随机种子和拆分训练测试集。固定随机种子可以保证实验的可复现性:
import random import numpy as np import torch def set_seed(seed: int = 42): random.seed(seed) np.random.seed(seed) torch.manual_seed(seed) if torch.cuda.is_available(): torch.cuda.manual_seed_all(seed)此外,不要把测试集数据用于调参,也不要在验证集上反复迭代太多次。更严谨的做法是切分出训练集、验证集和测试集三部分。基于时间序列数据时,不要使用普通随机切分,而应该按时间顺序切分,避免未来数据泄露到训练集中。
9.3 Django 生产部署
Django 内置的开发服务器只用于本地调试,生产环境推荐使用 Gunicorn 或 uWSGI 配合 Nginx 部署。如果只是对内提供 API,也可以用 Gunicorn 直接启动:
gunicorn web_ui.wsgi:application -b 0.0.0.0:8000 -w 4-w 4表示启动 4 个 worker 进程。具体 worker 数量应该根据服务器 CPU 核心数和接口耗时设置,一般建议2 * CPU 核心数 + 1,但这并不是绝对公式。
数据库连接配置也要修改,测试阶段可以使用 SQLite,正式环境建议切换为 MySQL 或 PostgreSQL。切换时需要修改settings.py中的数据库配置,并提前在目标数据库创建好库和用户名。
9.4 日志与监控
线上服务加入完整的日志非常关键。Django 的settings.py中默认有 LOGGING 配置,可以自定义日志文件输出路径。模型预测接口还需要记录请求参数、模型版本、预测时间,这样当天后预测结果出现偏差时,可以快速定位是哪一版模型或哪一批数据导致的。
建议在返回预测结果的响应中带上model_version字段,同时保存请求摘要。这是比较容易被忽略的细节,但如果后续需要模型迭代回溯,会非常有用。
9.5 模块解耦与代码维护
爬虫、清洗、模型训练、Django Web 服务解耦后,任何一处升级都不会影响其他模块。例如把爬虫模块替换成读取数据库数据,Django 和模型模块不需要改动。训练模型时如果从 PyTorch 换到其他框架,也应该通过模型加载接口隔离变化。
如果要长期维护这个项目,建议为关键函数补上类型注解和简单的 docstring。比如predict_next_sales的参数和返回类型写清楚后,其他人接手时不用从函数体反推数据结构。
10. 总结与下一步学习建议
从这篇实战教程中,可以掌握一条完整的电商数据分析链路:用 Python 爬虫获取数据,用 pandas 清洗数据,用 pyecharts 呈现趋势,用 Transformer 完成销量预测,再用 Django 把预测能力封装成 API。
如果想把模型效果做得更好,下一步可以从三个方向深入。
第一是特征扩展。除了历史销量之外,还可以加入价格、折扣力度、评论数、收藏数、搜索指数等特征,模型可以从多维特征中学习到更复杂的销量影响因素。
第二是调参与结构优化。可以先通过网格搜索确定窗口大小、层数、学习率等超参数。如果数据量较大,可以尝试更大维度的d_model,但要注意防止过拟合。
第三是部署升级。尝试把训练好的模型封装成独立的推理服务,再通过 Django 或其他网关调用,这样可以让模型更新不会影响 Web 服务稳定性。
实际业务落地中,不要盲信任何单一模型,销量预测本质上是概率估计。建议保存所有历史预测结果,定期做回测对比,像维护业务指标一样维护模型误差。希望这篇文章能够帮你快速打通从数据采集到 Web 展示的完整链路,你可以在本地环境把代码跑通后,再逐步替换成自己业务中的真实数据。