简介:这份资源是一篇面向计算机相关专业学生与数据分析学习者的完整论文文档,围绕Python地铁客流数据分析与预测系统的设计与实现展开,适合作为毕业设计、课程设计或机器学习入门项目的参考方案。文档聚焦杭州、深圳等城市地铁短时客流预测问题,涵盖数据预处理、HDFS数据加载、Spark数据分析、Spark MLlib预测建模、pyeharts可视化以及后端管理等模块,并给出管理员与用户两端的功能划分,如出行高峰时段、限流站点、客流趋势预测等具体设计。资源包共1个docx文件,约4.99MB,内容为完整论文正文,包含摘要、系统架构与技术选型说明,便于读者理解Hadoop、Spark、MySQL与动态Web应用的整合思路。目前已有671人学习下载,适合需要参考选题结构、算法模型与可视化实现路径的读者研读借鉴。
1. 从一份 docx 标题说起:地铁客流数据到底能预测什么
早晚高峰挤过地铁的人都懂那种感觉:站台上人贴人,广播一遍遍喊"请往车厢中部走",可你根本挪不动。对地铁运营方来说,这不是体验问题,而是安全问题——客流一旦超过站台承载能力,踩踏风险陡增。所以"python地铁客流数据分析与预测系统"这个标题背后,真正要解决的是:用历史刷卡数据,提前知道明天早高峰某个站会来多少人,好决定要不要加开列车、要不要启动限流。
这份 docx 标题里藏着三个层次:数据分析(看清过去)、预测建模(推算未来)、系统实现(把模型变成能用的工具)。适合谁看?做课程设计的学生、刚转行做数据分析的工程师、以及想给运营部门做一套客流看板的开发者。它不需要你有多深的机器学习功底,但需要你会 python 基础语法、能装环境、愿意跟数据死磕。接下来我按"数据怎么来→怎么分析→怎么预测→怎么变成系统→坑在哪"这条线,把整套方案拆开讲清楚。
2. 数据从哪来、长什么样:地铁 AFC 数据的清洗与特征工程
2.1 先搞清楚 AFC 数据的三张核心表
地铁自动售检票系统(AFC)产生的原始数据,通常不是一张大宽表,而是拆成几张关联表。常见做法是拿到三张:进站刷卡记录、出站刷卡记录、站点基础信息。进站表一般包含卡号、进站时间、进站站点编号;出站表包含卡号、出站时间、出站站点编号;站点表包含站点编号、站点名称、所属线路、是否换乘站。
这里第一个容易翻车的地方是:进站和出站是两条独立记录,靠卡号关联。但现实中存在"只进不出"(比如卡丢了、或者当天没出站)和"只出不进"(比如用了单程票但进站记录丢失)的情况。如果你直接按卡号 inner join,会丢掉大量记录,导致客流被低估。我一般会先统计进出站记录数差异,差异超过 5% 就要警惕数据质量问题。
import pandas as pd # 读取原始数据,注意编码,地铁数据常见 gbk 或 utf-8 entry = pd.read_csv('entry_records.csv', encoding='gbk') exit_ = pd.read_csv('exit_records.csv', encoding='gbk') stations = pd.read_csv('stations.csv', encoding='gbk') # 先看数据规模和缺失情况,别急着合并 print('进站记录数:', len(entry)) print('出站记录数:', len(exit_)) print('进站缺失值:\n', entry.isnull().sum()) print('出站缺失值:\n', exit_.isnull().sum()) # 统一时间格式,这一步不做后面全乱 entry['进站时间'] = pd.to_datetime(entry['进站时间'], errors='coerce') exit_['出站时间'] = pd.to_datetime(exit_['出站时间'], errors='coerce') # 统计只进不出的比例 entry_cards = set(entry['卡号']) exit_cards = set(exit_['卡号']) only_entry = entry_cards - exit_cards print('只进不出卡数占比: {:.2%}'.format(len(only_entry) / len(entry_cards)))这段代码的逻辑是:先摸清数据底数,再统一时间类型,最后量化数据质量问题。参数上,errors='coerce'会把无法解析的时间变成 NaT,方便后续统计;如果你发现缺失率超过 10%,就要考虑是不是导出时字段错位了。站点表要单独校验,确认每个站点编号都能在进站表里找到对应,否则会出现"幽灵站点"。
2.2 客流统计的粒度选择:15 分钟还是 1 小时
做客流预测,时间粒度直接决定模型难度和实用性。粒度太粗(比如按天),预测出来只能用于宏观规划,没法指导早高峰加车;粒度太细(比如按 1 分钟),数据噪声大、模型难收敛,而且运营调度也来不及响应。我一般选 15 分钟作为基础粒度,既能捕捉早高峰的爬坡过程,又不会太碎。
统计口径上,进站客流按"进站时间"归入对应时段,出站客流按"出站时间"归入。但要注意:出站客流反映的是列车到达后的疏散压力,和进站客流不是一回事。做站台限流预测,应该用进站客流;做车厢拥挤度预测,才需要结合出站和换乘数据。很多论文把这两个混在一起,结果模型学出来的东西没法用。
# 按 15 分钟粒度统计进站客流 entry['时段'] = entry['进站时间'].dt.floor('15min') flow_15min = entry.groupby(['进站站点编号', '时段']).size().reset_index(name='进站人数') # 补全缺失时段:有些站点某些时段没人进站,但那是 0 不是缺失 all_slots = pd.date_range( start=entry['进站时间'].min().floor('15min'), end=entry['进站时间'].max().floor('15min'), freq='15min' ) station_ids = stations['站点编号'].unique() full_index = pd.MultiIndex.from_product([station_ids, all_slots], names=['进站站点编号', '时段']) flow_full = flow_15min.set_index(['进站站点编号', '时段']).reindex(full_index, fill_value=0).reset_index() # 加上时间特征,供后面建模用 flow_full['小时'] = flow_full['时段'].dt.hour flow_full['分钟'] = flow_full['时段'].dt.minute flow_full['星期'] = flow_full['时段'].dt.dayofweek flow_full['是否周末'] = (flow_full['星期'] >= 5).astype(int)这里的关键操作是reindex补全。如果不补,模型会以为"没有记录"等于"没有客流",但实际上只是那个时段没人刷卡。补全后数据量会变大,但这是必须的。时间特征里,是否周末是最基础的,后面还可以加"是否节假日""是否调休工作日",这些对预测精度影响很大。
2.3 特征工程:把时间、站点、天气都变成模型能吃的数
原始数据只有站点编号和时间,直接喂给模型效果很差。需要构造几类特征:时间类(小时、分钟、星期、是否高峰)、站点类(是否换乘站、所属线路数、历史平均客流)、外部类(天气、节假日)。其中"历史平均客流"是最强的特征之一,但构造时要小心数据泄漏——不能用未来数据算历史均值。
我一般用"前 7 天同一时段均值"作为历史特征,计算时严格按时间顺序滚动。天气数据如果拿不到,可以先用"是否下雨"这种二值特征代替,从公开气象接口按天抓取即可。注意:天气对地面交通影响大,对地铁影响相对小,但暴雨天进站客流会明显下降,这个特征值得加。
# 构造前 7 天同一时段均值特征,严格避免数据泄漏 flow_full = flow_full.sort_values(['进站站点编号', '时段']) flow_full['前7天同时段均值'] = ( flow_full.groupby(['进站站点编号', '小时', '分钟'])['进站人数'] .transform(lambda x: x.shift(1).rolling(7, min_periods=1).mean()) ) # 标记早晚高峰:7-9 点、17-19 点 flow_full['是否早高峰'] = ((flow_full['小时'] >= 7) & (flow_full['小时'] < 9)).astype(int) flow_full['是否晚高峰'] = ((flow_full['小时'] >= 17) & (flow_full['小时'] < 19)).astype(int) # 合并站点属性 flow_full = flow_full.merge(stations[['站点编号', '是否换乘站', '线路数']], left_on='进站站点编号', right_on='站点编号', how='left')shift(1)是防泄漏的核心,它保证计算当前时段特征时,用的是之前的数据。rolling(7)表示取 7 个历史点求均值。如果你用expanding或者不 shift,模型在训练集上表现会好得离谱,一到测试集就崩,这就是典型的"后悔药没处买"。
3. 用 python 做客流分析:从可视化到异常检测
3.1 三行代码画出站点客流热力图
分析阶段最直观的产出是热力图:横轴是时间,纵轴是站点,颜色深浅代表客流大小。这样一眼就能看出哪些站点是"客流大户",哪些时段是"压力峰值"。用 matplotlib 或 seaborn 都能画,但要注意中文字体问题,否则标题全是方框。
import matplotlib.pyplot as plt import seaborn as sns # 设置中文字体,Windows 用 SimHei,Mac 用 Arial Unicode MS plt.rcParams['font.sans-serif'] = ['SimHei'] plt.rcParams['axes.unicode_minus'] = False # 取某一天的数据画热力图 one_day = flow_full[flow_full['时段'].dt.date == pd.Timestamp('2024-03-15').date()] pivot = one_day.pivot_table(index='进站站点编号', columns='小时', values='进站人数', aggfunc='sum') plt.figure(figsize=(14, 8)) sns.heatmap(pivot, cmap='YlOrRd', linewidths=0.5) plt.title('各站点分时客流热力图') plt.xlabel('小时') plt.ylabel('站点编号') plt.tight_layout() plt.savefig('heatmap.png', dpi=150)这段代码里,pivot_table把长表转成宽表,aggfunc='sum'表示同一站点同一小时的多条记录求和。热力图适合快速定位问题站点,但如果你要对比不同天的差异,最好用折线图叠加。注意dpi=150保证导出图片清晰,论文里能用。
3.2 异常检测:哪些站点的客流"不对劲"
客流数据里常有异常:某站点突然客流暴涨(可能是附近有大型活动),或者连续几天客流骤降(可能是站点施工封闭)。这些异常如果不处理,会带偏预测模型。我一般用 3σ 原则做初筛,再用孤立森林(Isolation Forest)做精细检测。
3σ 原则简单但有效:计算每个站点历史客流的均值和标准差,超出均值 ±3 倍标准差的点标记为异常。缺点是假设数据服从正态分布,而客流数据明显不是。所以我会先用它粗筛,再用孤立森林对残差做二次检测。
from sklearn.ensemble import IsolationForest import numpy as np # 按站点分组计算 z-score flow_full['z_score'] = flow_full.groupby('进站站点编号')['进站人数'].transform( lambda x: (x - x.mean()) / (x.std() + 1e-6) ) flow_full['粗筛异常'] = (flow_full['z_score'].abs() > 3).astype(int) # 孤立森林做精细检测,contamination 设为预估异常比例 features = flow_full[['进站人数', '前7天同时段均值', '小时', '是否周末']].fillna(0) iso = IsolationForest(contamination=0.02, random_state=42) flow_full['精细异常'] = iso.fit_predict(features) flow_full['精细异常'] = (flow_full['精细异常'] == -1).astype(int) # 两种方法都标记为异常的,才认为是真异常 flow_full['最终异常'] = flow_full['粗筛异常'] & flow_full['精细异常'] print('检测到异常记录数:', flow_full['最终异常'].sum())contamination=0.02表示预估 2% 的数据是异常,这个值要根据实际数据调整。如果设太大,正常波动会被误判;设太小,真异常会漏掉。我一般先看粗筛结果,如果粗筛比例就在 2% 左右,那 contamination 就设 2%。孤立森林的random_state固定后结果可复现,论文里要写清楚。
3.3 客流分布分析:早高峰到底有多"尖"
做预测之前,得先知道客流分布的形态。早高峰不是均匀的,它有一个明显的爬坡和回落过程。我一般会算两个指标:峰值因子(峰值客流/全天均值)和高峰小时系数(高峰小时客流/全天客流)。这两个指标能告诉你,这个站点的客流是"尖峰型"还是"平缓型"。
尖峰型站点(比如 CBD 附近的换乘站)对预测精度要求更高,因为一旦预测偏低,站台瞬间就满了。平缓型站点(比如郊区终点站)预测误差容忍度大一些。分析阶段把站点分个类,后面建模时可以针对不同类型用不同策略。
# 计算每个站点的峰值因子和高峰小时系数 daily_stats = flow_full.groupby(['进站站点编号', flow_full['时段'].dt.date]).agg( 全天客流=('进站人数', 'sum'), 峰值客流=('进站人数', 'max') ).reset_index() daily_stats['峰值因子'] = daily_stats['峰值客流'] / (daily_stats['全天客流'] / 96 + 1e-6) # 高峰小时系数:早高峰 7-9 点客流占全天比例 peak_hours = flow_full[flow_full['是否早高峰'] == 1].groupby( ['进站站点编号', flow_full['时段'].dt.date] )['进站人数'].sum().reset_index(name='早高峰客流') daily_stats = daily_stats.merge(peak_hours, on=['进站站点编号', '时段'], how='left') daily_stats['高峰小时系数'] = daily_stats['早高峰客流'] / (daily_stats['全天客流'] + 1e-6) # 按峰值因子分类 daily_stats['站点类型'] = pd.cut( daily_stats['峰值因子'], bins=[0, 3, 6, np.inf], labels=['平缓型', '中等型', '尖峰型'] ) print(daily_stats.groupby('站点类型')['进站站点编号'].nunique())96 是一天 15 分钟粒度的时段数(24×4)。峰值因子越大,说明客流越集中。分类阈值 3 和 6 是我根据经验定的,你可以根据实际数据分布调整。这一步的产出是站点标签,后面建模时可以把它作为特征,也可以用来分群建模。
4. 预测模型怎么选、怎么训:从 ARIMA 到 LSTM 的落地对比
4.1 基线模型:先跑通 ARIMA 再谈深度学习
很多人一上来就上 LSTM,结果数据量不够、调参调到崩溃,最后效果还不如一个简单的移动平均。我的建议是:先跑通 ARIMA 或季节性 ARIMA(SARIMA)作为基线,再尝试复杂模型。基线模型的好处是训练快、可解释、不容易过拟合,而且能帮你判断数据里到底有多少可预测的信号。
ARIMA 的三个参数 (p, d, q) 分别对应自回归阶数、差分阶数、移动平均阶数。客流数据通常有日周期和周周期,所以要用 SARIMA,加上季节性参数 (P, D, Q, s),其中 s=96(一天 96 个 15 分钟时段)。参数确定可以用 ACF/PACF 图初判,再用 AIC 准则网格搜索。
from statsmodels.tsa.statespace.sarimax import SARIMAX import warnings warnings.filterwarnings('ignore') # 取单个站点的客流序列 station_id = 'S001' ts = flow_full[flow_full['进站站点编号'] == station_id].set_index('时段')['进站人数'].asfreq('15min').fillna(0) # 划分训练集和测试集,按时间切分,不能随机切 train_size = int(len(ts) * 0.8) train, test = ts[:train_size], ts[train_size:] # 训练 SARIMA 模型,参数先用经验值 model = SARIMAX( train, order=(1, 1, 1), seasonal_order=(1, 1, 1, 96), enforce_stationarity=False, enforce_invertibility=False ) result = model.fit(disp=False) # 预测测试集长度 forecast = result.forecast(steps=len(test)) print('预测前 10 个值:', forecast[:10].values)order=(1,1,1)是最简配置,seasonal_order=(1,1,1,96)表示日周期。enforce_stationarity=False在数据不够平稳时能避免报错。SARIMA 的缺点是训练慢,96 的季节周期会让计算量很大,如果数据超过一个月,建议先聚合到 1 小时粒度再跑。
4.2 LSTM 建模:数据窗口怎么切、网络怎么搭
LSTM 适合捕捉长序列依赖,但前提是数据量够。我一般要求单个站点至少有 3 个月的历史数据,否则 LSTM 很容易过拟合。数据窗口的切法很关键:用过去 N 个时段预测下一个时段,N 一般取 96(一天)或 192(两天)。窗口太小,模型学不到日周期;窗口太大,训练慢且容易梯度消失。
网络结构上,两层 LSTM 加一层全连接就够了。第一层 LSTM 返回序列,第二层 LSTM 只返回最后一个时间步的输出,然后接全连接层输出预测值。Dropout 设 0.2 防止过拟合,优化器用 Adam,学习率 0.001。
import torch import torch.nn as nn from torch.utils.data import DataLoader, TensorDataset # 构造滑动窗口数据集 def create_sequences(data, window=96): X, y = [], [] for i in range(len(data) - window): X.append(data[i:i+window]) y.append(data[i+window]) return np.array(X), np.array(y) # 归一化,用训练集的均值和方差 mean, std = train.mean(), train.std() train_norm = (train - mean) / (std + 1e-6) test_norm = (test - mean) / (std + 1e-6) window = 96 X_train, y_train = create_sequences(train_norm.values, window) X_test, y_test = create_sequences(test_norm.values, window) # 转成 tensor X_train_t = torch.FloatTensor(X_train).unsqueeze(-1) y_train_t = torch.FloatTensor(y_train) X_test_t = torch.FloatTensor(X_test).unsqueeze(-1) y_test_t = torch.FloatTensor(y_test) # 定义 LSTM 模型 class FlowLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=0.2) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): out, _ = self.lstm(x) out = self.fc(out[:, -1, :]) # 只取最后一个时间步 return out.squeeze() model = FlowLSTM() criterion = nn.MSELoss() optimizer = torch.optim.Adam(model.parameters(), lr=0.001) # 训练循环 dataset = TensorDataset(X_train_t, y_train_t) loader = DataLoader(dataset, batch_size=64, shuffle=True) for epoch in range(50): model.train() total_loss = 0 for batch_x, batch_y in loader: optimizer.zero_grad() pred = model(batch_x) loss = criterion(pred, batch_y) loss.backward() optimizer.step() total_loss += loss.item() if (epoch + 1) % 10 == 0: print(f'Epoch {epoch+1}, Loss: {total_loss/len(loader):.4f}')unsqueeze(-1)是把一维序列变成 (样本数, 时间步, 特征数) 的三维张量,LSTM 要求这个形状。out[:, -1, :]取最后一个时间步的输出,因为我们要预测的是下一个时刻的值。训练 50 轮是经验值,如果 loss 还在降可以继续,但要注意验证集是否过拟合。
4.3 模型评估:MAE、RMSE 和"高峰时段误差"
评估指标不能只看整体 MAE,因为高峰时段的误差代价远大于平峰时段。我一般会分开算:整体 MAE、高峰时段 MAE、平峰时段 MAE。如果高峰 MAE 是平峰的 3 倍以上,说明模型在关键场景下不可靠,需要针对性优化。
from sklearn.metrics import mean_absolute_error, mean_squared_error model.eval() with torch.no_grad(): pred_test = model(X_test_t).numpy() # 反归一化 pred_test = pred_test * std + mean y_test_real = y_test * std + mean # 整体指标 mae = mean_absolute_error(y_test_real, pred_test) rmse = np.sqrt(mean_squared_error(y_test_real, pred_test)) print(f'整体 MAE: {mae:.2f}, RMSE: {rmse:.2f}') # 分时段指标 test_hours = test.index[window:].hour peak_mask = ((test_hours >= 7) & (test_hours < 9)) | ((test_hours >= 17) & (test_hours < 19)) print(f'高峰 MAE: {mean_absolute_error(y_test_real[peak_mask], pred_test[peak_mask]):.2f}') print(f'平峰 MAE: {mean_absolute_error(y_test_real[~peak_mask], pred_test[~peak_mask]):.2f}')反归一化这一步容易忘,忘了的话指标会小得离谱,但那是假的。test.index[window:]是因为前 window 个点被用作输入,没有对应的预测目标。分时段评估能暴露模型短板,如果高峰 MAE 太大,可以考虑对高峰时段单独建模,或者给高峰样本更高权重。
5. 从模型到系统:Flask 接口、前端看板和部署踩坑
5.1 用 Flask 把模型包成预测接口
模型训练完只是半成品,要变成系统得有个接口。Flask 轻量、上手快,适合做课程设计级别的系统。核心逻辑是:接收站点编号和预测时间范围,返回预测客流值。模型在服务启动时加载一次,不要每次请求都重新加载,否则响应慢得没法用。
from flask import Flask, request, jsonify import joblib app = Flask(__name__) # 启动时加载模型和归一化参数 model = FlowLSTM() model.load_state_dict(torch.load('lstm_model.pth')) model.eval() scaler_mean, scaler_std = joblib.load('scaler.pkl') @app.route('/predict', methods=['POST']) def predict(): data = request.json station_id = data.get('station_id') recent_flow = data.get('recent_flow') # 过去 96 个时段客流 if len(recent_flow) != 96: return jsonify({'error': '需要 96 个历史数据点'}), 400 # 归一化并预测 x = (np.array(recent_flow) - scaler_mean) / (scaler_std + 1e-6) x_tensor = torch.FloatTensor(x).unsqueeze(0).unsqueeze(-1) with torch.no_grad(): pred = model(x_tensor).item() pred_real = pred * scaler_std + scaler_mean return jsonify({'station_id': station_id, 'predicted_flow': round(pred_real, 2)}) if __name__ == '__main__': app.run(host='0.0.0.0', port=5000, debug=False)debug=False在生产环境必须设,否则会暴露源码。host='0.0.0.0'允许外部访问,如果只在本机测试可以改成127.0.0.1。接口返回前把预测值反归一化,保证前端拿到的是真实客流数。
5.2 前端看板:用 ECharts 画预测曲线
前端不需要太复杂,一个折线图加一个站点选择框就够了。ECharts 是国内用得最多的可视化库,文档全、例子多。核心是把后端返回的预测值和历史值拼在一起,用不同颜色区分。
// 假设后端返回 { history: [...], prediction: [...] } fetch('/predict', { method: 'POST', headers: {'Content-Type': 'application/json'}, body: JSON.stringify({station_id: 'S001', recent_flow: recentFlow}) }) .then(res => res.json()) .then(data => { const chart = echarts.init(document.getElementById('chart')); chart.setOption({ title: {text: '站点客流预测'}, xAxis: {type: 'category', data: timeLabels}, yAxis: {type: 'value', name: '客流量'}, series: [ {name: '历史客流', type: 'line', data: data.history, smooth: true}, {name: '预测客流', type: 'line', data: data.prediction, lineStyle: {type: 'dashed'}, smooth: true} ] }); });预测曲线用虚线,和历史曲线区分开。smooth: true让曲线更平滑,但会掩盖真实波动,如果要做精确分析可以关掉。前端部署时注意跨域问题,Flask 端加CORS支持,或者用 Nginx 做反向代理。
5.3 部署时最容易忽略的三件事
第一,模型文件路径。开发时用相对路径没问题,部署到服务器后工作目录变了,torch.load('lstm_model.pth')会找不到文件。我一般用os.path.dirname(os.path.abspath(__file__))拼绝对路径。
第二,依赖版本。requirements.txt里要锁版本,尤其是 torch 和 numpy,不同版本 API 可能不兼容。我踩过一次坑:服务器上 numpy 版本太低,np.float报错,排查了半天。
第三,并发性能。Flask 默认单线程,多个请求同时进来会排队。如果只是课程设计演示,够用;如果要给多人用,得上 gunicorn 加多 worker。但注意:LSTM 模型不是线程安全的,多 worker 时每个进程要独立加载模型,内存占用会翻倍。
6. 避坑指南:地铁客流预测里那些血泪教训
6.1 数据泄漏:模型在训练集上"作弊"
现象:训练集 MAE 只有 2,测试集 MAE 飙到 50,差距大得离谱。
原因:构造特征时用了未来数据。最常见的是算"历史均值"时没 shift,把当前时刻的值也算进去了;或者归一化时用了全量数据的均值和方差,而不是只用训练集。
解决:所有滚动统计必须shift(1);归一化参数只能从训练集计算,然后应用到测试集。检查方法是:把训练集和测试集的评估指标都打印出来,如果差距超过 30%,基本可以确定有泄漏。
6.2 时间粒度选错:15 分钟太碎,1 小时太粗
现象:按 15 分钟建模,预测曲线全是锯齿,模型学不到规律;按 1 小时建模,早高峰的爬坡过程被抹平,预测值总是偏低。
原因:粒度选择没有结合业务需求。15 分钟粒度下,单个站点的客流量可能只有个位数,噪声占比大;1 小时粒度又太粗,无法反映高峰的快速变化。
解决:先做粒度对比实验,分别用 15 分钟、30 分钟、1 小时建模,看哪个粒度的预测误差最小且业务上可用。我一般选 15 分钟,但对客流量小的站点会聚合到 30 分钟。
6.3 忽略节假日和调休:模型在特殊日期集体翻车
现象:工作日预测很准,一到节假日或调休工作日,预测值偏差巨大。
原因:训练数据里节假日样本太少,模型没学到节假日的模式。调休工作日更麻烦,它表面是工作日,实际客流像周末。
解决:把"是否节假日""是否调休"作为特征加进去,节假日样本少的话可以做数据增强(比如把多个节假日的数据对齐后平均)。如果某个节假日完全没数据,那就只能人工规则兜底。
6.4 模型过拟合:LSTM 参数越多越容易"背答案"
现象:LSTM 在训练集上 loss 降到 0.001,测试集 loss 一直在 0.1 以上。
原因:模型太复杂,数据量不够。LSTM 的参数量很容易到几十万,而单个站点的训练样本可能只有几千条。
解决:减小 hidden_size(从 128 降到 64 甚至 32),增加 Dropout(从 0.2 提到 0.4),加 L2 正则化。如果还不行,就退回 SARIMA 或 XGBoost,别跟 LSTM 死磕。
6.5 部署后预测值不变:模型加载了但没切换 eval 模式
现象:接口每次返回的预测值都一样,不管输入什么。
原因:PyTorch 模型加载后默认是 train 模式,Dropout 层还在随机丢弃神经元,导致输出不稳定。更隐蔽的情况是:模型加载了但权重没加载成功,用的是随机初始化的权重。
解决:加载后必须调model.eval();加载权重时用strict=True检查键是否完全匹配,不匹配会报错。如果用了 BatchNorm,eval 模式也会改变行为,必须切换。
7. 进阶技巧:用注意力机制提升高峰预测精度
基础 LSTM 对所有时间步一视同仁,但预测早高峰时,显然前一天的早高峰数据比凌晨 3 点的数据更重要。注意力机制就是让模型自己学会"该看哪里"。我试过在 LSTM 后面加一层注意力,高峰 MAE 能降 15% 左右。
实现上,用 PyTorch 的MultiheadAttention或者自己写一个简单的加性注意力。核心是:LSTM 输出所有时间步的隐藏状态,注意力层计算每个时间步的权重,然后加权求和作为最终表示。
class AttentionLSTM(nn.Module): def __init__(self, input_size=1, hidden_size=64, num_layers=2): super().__init__() self.lstm = nn.LSTM(input_size, hidden_size, num_layers, batch_first=True, dropout=0.2) self.attention = nn.Sequential( nn.Linear(hidden_size, 32), nn.Tanh(), nn.Linear(32, 1) ) self.fc = nn.Linear(hidden_size, 1) def forward(self, x): lstm_out, _ = self.lstm(x) # (batch, seq_len, hidden) attn_weights = torch.softmax(self.attention(lstm_out), dim=1) # (batch, seq_len, 1) context = torch.sum(attn_weights * lstm_out, dim=1) # 加权求和 return self.fc(context).squeeze()attention是一个两层全连接,输出每个时间步的分数,softmax 归一化成权重。context是加权后的表示,它更关注重要的时间步。训练时可以把注意力权重可视化出来,看看模型到底在关注哪些时段——如果它关注的是凌晨低峰时段,说明模型没学好,需要检查数据或调整窗口。
验证注意力是否有效,不能只看整体 MAE,要看高峰 MAE 是否下降。如果整体 MAE 降了但高峰 MAE 没降,说明注意力被平峰样本"带偏"了,可以给高峰样本更高权重,或者对高峰单独建模。
最后说个习惯:我每次做完一个客流预测项目,都会把"预测值 vs 实际值"的散点图画出来,按站点、按时段分别看。如果某个站点的点全在对角线下方,说明模型系统性低估,得查查是不是那个站点有特殊事件没被特征捕捉到。这个习惯帮我发现过好几次数据问题,比只看 MAE 有用得多。希望帮到你。
本文还有配套的精品资源,点击获取