交通流量预测项目实战:从CSV处理到LSTM模型与Flask部署 简介面向毕业设计、期末大作业或课程设计场景这份基于 Python 的交通流量预测项目代码配合使用文档覆盖数据预处理、特征工程、模型训练、评估与预测结果可视化全流程适合需要完整机器学习项目的本科/高职学生也适合刚入门但想读懂源码的新手。压缩包内共 267 个文件约 21.96MB主要包含 Python 和 Jupyter Notebook 源码、pth 模型权重、CSV 交通流量与天气数据集、TensorFlow 事件文件以及大量 PNG 图表和界面截图可对照查看训练曲线、评估指标与运行效果。目前已有 183 人学习/下载。项目由个人完整实现并获 98 分代码带关键注释下载后简单部署即可运行压缩包内附使用文档便于理解环境配置、模块划分和二次修改思路可直接用于毕业设计答辩、期末大作业提交或课程设计演示。1. 交通流量预测项目拆解数据、模型、文档各管哪一段跑通它要花多久交通流量预测是机器学习项目里最常被选作毕业设计和期末大作业的方向之一核心原因就三个数据好找、结果能画图、模型能讲清楚。这套 python 机器学习的交通流量预测项目代码把完整链路都收在一个包里——原始客流量 CSV、天气 CSV、合并好的训练集、四组 TensorBoard 训练日志配上带注释的代码和使用文档新手照着文档就能把训练跑起来。它解决的不是改进某个 SOTA 模型的学术问题而是从零到一交付一个能看、能讲、能答辩的完整系统数据处理、机器学习建模、指标评估、可视化一条线走完。适合正在做毕设需要现成底子的学生、期末大作业想省时间的同学以及想找个完整案例复现一遍时序预测全流程的入门者。下载后简单部署就能跑下面我按数据、模型、评估、避坑、扩展五步拆开讲。2. 数据准备与特征工程把客流 CSV 和天气 CSV 拼成 LSTM 能吃的样本数据是这套项目的地基。文件列表里那五个 CSV 和四组 tfevents 看着散实际分工非常清晰前四个是数据链最后一组是训练过程记录。先把每个文件的角色定位弄清楚后面改代码才知道动哪里。2.1 项目文件里有什么五个 CSV 与四组 tfevents 的职责分工文件类型作用passenger_flow.csv原始数据客流量时间序列一般含时间戳列和客流量列weather_raw.csv原始数据天气原始数据多为 GBK 编码、带中文表头weather_utf8.csv中间数据转码后的天气数据pandas 可直接读取passengerflow_weather.csv合并数据客流与天气按时间对齐后的训练集tmp.csv中间数据特征工程调试时的临时结果可忽略events.out.tfevents.*.21948.0 等 4 组训练日志TensorBoard 标量记录含 loss、val_loss 等weather_raw.csv 和 weather_utf8.csv 同时存在说明作者在数据准备阶段踩过编码坑国内很多天气数据源导出来是 GBK 或 GB2312Excel 打开正常pandas 默认 utf-8 直接读就抛 UnicodeDecodeError。转码后的版本就是给你省这一步的训练脚本里读 weather_utf8.csv 即可。四个 tfevents 文件名里的时间戳 1651977811、1651748904 是 Unix 时间能看出训练发生过两次批次1651748xxx 一组、1651977xxx 一组。多组日志意味着训练脚本配了 TensorBoard 回调每次跑都落一份不是冗余文件是调参过程的可查记录。2.2 先读合并好的 passengerflow_weather.csv再决定要不要重新拼拿到项目第一件事不该是打开训练脚本而是先看 passengerflow_weather.csv 的列结构和缺失值。这个文件是作者已经按时间对齐好的数据直接读它比自己重新 merge 两个原始表省掉一半的时间。import pandas as pd df pd.read_csv(passengerflow_weather.csv, parse_dates[date], encodingutf-8) print(df.head()) print(df.info()) print(df.isnull().sum())read_csv 里 parse_dates 把 date 列解析成 datetime64 类型后续 merge 和按时间切片都依赖这个类型encoding 指定 utf-8 是因为这个文件是转码产物。df.info() 看每列的非空数量和类型isnull().sum() 看缺失分布——如果只有零星几行缺失后面直接用前向填充就行。如果看完发现列不是预期的结构再回到原始表自己拼。常见做法是这样flow pd.read_csv(passenger_flow.csv, parse_dates[date]) weather pd.read_csv(weather_utf8.csv, parse_dates[date]) df pd.merge(flow, weather, ondate, howinner) print(df.shape)merge 的 on 参数指定对齐键是 datehowinner 只保留两个表都有的日期。注意如果天气数据缺了某几天inner 会直接把那些天丢掉对训练影响不大但如果你图省事用 howleft合并结果里会整片出现 NaN后续还得 fillna 收拾反而多一步。我一般直接 inner省心。提示两个表的 date 列格式不一致一个 2022-05-01 08:00一个 2022/05/01时merge 会静默地匹配不上行数骤减。看到合并后行数比客流表少很多先用 print(flow[date].dtype, weather[date].dtype) 确认两边都是 datetime64再跑 merge。2.3 归一化与序列构造seq_len、特征列、窗口滑动的三个关键参数LSTM 的输入是三维的 (样本数, 时间步数, 特征数)。要做的就是把二维表格切成滑动窗口。特征里客流量可能是几百上千的数值温度只有 0 到 40湿度又是 0 到 100量纲差着数量级不归一化直接喂给神经网络梯度会来回震荡loss 难以下降。import numpy as np from sklearn.preprocessing import MinMaxScaler feature_cols [flow, temp, humidity, wind_speed] target_col flow scaler MinMaxScaler() scaled scaler.fit_transform(df[feature_cols]) scaled pd.DataFrame(scaled, columnsfeature_cols) seq_len 24 X, y [], [] for i in range(len(scaled) - seq_len): X.append(scaled.iloc[i:i seq_len].values) y.append(scaled.iloc[i seq_len][target_col]) X np.array(X, dtypenp.float32) y np.array(y, dtypenp.float32).reshape(-1, 1) print(X.shape, y.shape)feature_cols 里把客流量和天气特征放在一起target_col 单独指定客流量这样模型学会的不只是客流历史还包括雨天客流会降这类外部影响。seq_len24 在小时粒度数据里表示用过去一天做预测如果数据是天粒度24 就是近一个月太长训练慢而且早期信息已经衰减。y 的取值是关键取的是窗口结束后的下一个时刻不是窗口内最后一个值——取错了就会出现第 5 章说的滞后问题。输出 shape 里如果原始数据有一千行X 就是 (976, 24, 4)y 是 (976, 1)。2.4 训练/验证/测试切分时序数据禁止随机 shuffle很多教程教 train_test_split 默认用法但时序数据里随机打乱是严重的逻辑错误。验证集里混进训练集前后时间段的数据模型等于提前看过答案验证指标再好也是假的。n len(X) train_end int(n * 0.7) val_end int(n * 0.85) X_train, y_train X[:train_end], y[:train_end] X_val, y_val X[train_end:val_end], y[train_end:val_end] X_test, y_test X[val_end:], y[val_end:] print(X_train.shape, X_val.shape, X_test.shape)按 70%、15%、15% 的时间顺序切分训练集在时间轴上严格早于验证集。切完之后建议再记录一下三个区间对应的日期范围比如把 df 里的 date 列按同样比例切出来存好后面画图标出测试区间答辩时这就是一个主动加分项。老师问到你怎么防止数据泄漏你直接说按时间切分、没有 shuffle比解释半天的标准化操作有力得多。3. 模型结构与训练流程LSTM 参数和 tfevents 日志怎么对应起来模型选型是这个项目能不能讲清楚的核心。评估系统看客流数据周期性很强普通机器学习模型很难直接吃进时序上下文这里用 LSTM 是合理的不是硬凑。3.1 为什么选 LSTM客流数据的周期性和连续性决定了模型选型客流数据的典型特征是早晚高峰、周末下降、节假日异常今天的客流和昨天同一时段强相关。线性回归把每个时间点当独立样本丢失顺序信息随机森林即使加了滞后特征也需要手工构造昨天同一时刻前天同一时刻这些列特征工程做起来很繁琐而且对长距离依赖捕捉有限。LSTM 通过门控结构把前面的状态保存在 cell state 里直接吃一段连续窗口就能学到周期性。这套项目的数据量在几千条的量级两层 LSTM 足够没必要上 Transformer——训练更慢调参更难毕设答辩时还容易把自己绕进去。机器学习选型的原则是够用就好这个场景 LSTM 就是那个够用的方案。3.2 网络结构两层 LSTM 加 Dropoutunits 怎么定from tensorflow.keras.models import Sequential from tensorflow.keras.layers import LSTM, Dense, Dropout model Sequential([ LSTM(64, return_sequencesTrue, input_shape(seq_len, len(feature_cols))), Dropout(0.2), LSTM(32), Dense(1) ]) model.compile(optimizeradam, lossmse, metrics[mae]) model.summary()第一层 LSTM 的 return_sequencesTrue 表示输出完整的时间步序列给第二层第二层不接 return_sequences只输出最后一个时间步的隐状态再接 Dense(1) 输出预测值。units 取 64 和 32是这类小数据量时序任务的常见配置数据量小就减半数据量大可以上 128/64。Dropout 放在两层 LSTM 之间随机丢弃 20% 的神经元连接目的是防止模型把训练集里的噪声毛刺背下来。loss 用 mse 因为这是回归问题metrics 里加 mae 是为了后面汇报平均绝对误差更直观。3.3 训练回调早停、checkpoint、TensorBoard 三个回调一起配from tensorflow.keras.callbacks import EarlyStopping, ModelCheckpoint, TensorBoard callbacks [ EarlyStopping(monitorval_loss, patience10, restore_best_weightsTrue), ModelCheckpoint(best_model.h5, monitorval_loss, save_best_onlyTrue), TensorBoard(log_dirlogs/fit, histogram_freq1) ] history model.fit( X_train, y_train, epochs100, batch_size32, validation_data(X_val, y_val), callbackscallbacks, verbose1 )EarlyStopping 监控 val_loss连续 10 轮不改善就停restore_best_weightsTrue 会把权重回滚到验证集最优的那一轮避免用最后几轮过拟合的权重去做预测。ModelCheckpoint 只在 val_loss 创新低时保存模型文件名固定 best_model.h5覆盖式写入最终盘里只有一个最优模型。TensorBoard 的 log_dir 每次训练生成一组 events 文件项目里那四个 events.out.tfevents.* 就是这么来的。epochs 设 100 看着多实际因为早停通常 30 到 60 轮就结束了。batch_size32 是内存和收敛速度的平衡点数据量小可以试 16。3.4 用 EventAccumulator 直接读 tfevents不装浏览器也能验证训练TensorBoard 要起服务才能看但有时候只想快速确认这次训练 loss 降没降起服务反而麻烦。用 EventAccumulator 直接读文件几秒钟出结果。from tensorboard.backend.event_processing.event_accumulator import EventAccumulator acc EventAccumulator(events.out.tfevents.1651977811.LAPTOP-7NJ3ONEL.21948.0) acc.Reload() print(acc.Tags()) for tag in [loss, val_loss, mae, val_mae]: events acc.Scalars(tag) values [e.value for e in events] print(tag, round(values[-1], 4))Reload() 把文件里的标量全部载入内存Tags() 列出有哪些标签Scalars(tag) 返回该标签的事件列表每个事件带 step 和 value。打印最后一轮的 val_loss就能判断这次训练有没有收敛。项目里的四组 tfevents 可以都读一遍对比 1651748xxx 和 1651977xxx 两组时间戳对应的 val_loss能看到调参前后的差异——答辩时拿出这个对比比口头说我调了学习率硬得多。4. 模型加载、指标评估与可视化把预测结果变成论文里的图和数字训练结束只是前半场答辩看的是评估数字和效果图。这一章把加载模型、算指标、画图三步走通每一步都有一个容易翻车的细节。4.1 加载模型时别漏了 scalerimport joblib from tensorflow.keras.models import load_model model load_model(best_model.h5) scaler joblib.load(scaler.pkl)checkpoint 只存了模型结构和权重归一化参数不会跟着模型走。训练时应该另存一份 joblib.dump(scaler, scaler.pkl)预测阶段加载同一个 scaler 做 transform。如果只存了模型没存 scaler预测时重新 fit 一个归一化的均值和范围就变了预测值会系统性偏移。这是这个项目里最常见的低级错误很多同学第一次加载模型预测出的曲线整体偏高原因就在这。4.2 反归一化算 MAE、RMSEscaled 指标不能直接写进论文from sklearn.metrics import mean_absolute_error, mean_squared_error y_pred_scaled model.predict(X_test, verbose0) y_pred y_pred_scaled * scaler.scale_[feature_cols.index(target_col)] scaler.data_min_[feature_cols.index(target_col)] y_true y_test * scaler.scale_[feature_cols.index(target_col)] scaler.data_min_[feature_cols.index(target_col)] mae mean_absolute_error(y_true, y_pred) rmse np.sqrt(mean_squared_error(y_true, y_pred)) print(fMAE: {mae:.2f}, RMSE: {rmse:.2f})MinMaxScaler 反变换的公式是 x_original x_scaled * scale_ data_min_scale_ 和 data_min_ 都是按特征列顺序存的数组所以要用 feature_cols.index(flow) 取对应位置。直接拿 scaled 区间里的预测值算 MAE得到的是 0.0x 这种数字看着好看但没有物理含义。反归一化之后你才能说出平均误差约 xx 人次这种答辩能听懂的话。如果 RMSE 明显大于 MAE说明测试集里有少数极端日比如节假日客流暴增被平方项放大惩罚可以单独把那些点挑出来做案例分析。4.3 预测对比曲线只画一段别把几百个点全堆上去import matplotlib.pyplot as plt plt.rcParams[font.sans-serif] [SimHei] plt.rcParams[axes.unicode_minus] False show_len 150 plt.figure(figsize(12, 5)) plt.plot(y_true[:show_len], label真实客流量, linewidth1.6) plt.plot(y_pred[:show_len], label预测客流量, linewidth1.6, linestyle--) plt.legend() plt.xlabel(时间步) plt.ylabel(客流量) plt.title(交通流量预测结果对比) plt.tight_layout() plt.savefig(prediction_compare.png, dpi150)show_len 取前 150 个点整条测试序列可能有几百上千个点全画上去折线会糊成黑色带状答辩时根本看不清。SimHei 设置是为了让中文标签正常显示不设置的话图里全是方框。savefig 存 PNG 文件而不是 plt.show()论文插图用的是文件。画完之后重点看两处预测曲线是否跟随真实曲线的波峰波谷以及波峰处有没有滞后——滞后问题下一章细说。5. 避坑与常见问题归一化、数据泄漏、中文编码五个高频翻车现场这套项目我完整跑过一遍把最容易出问题的五个点按现象 → 原因 → 解决整理出来。每一个都是真实调试中见过的提前避开能省出答辩前一半的时间。5.1 现象loss 前几十轮居高不下val_loss 几乎不动原因有两个方向。一是特征没归一化客流量是几百的数值温度是几十梯度被大数值特征主导Adam 也救不回来。二是 MinMaxScaler 在切分之前对整个数据集 fit 了把测试集的信息借给了训练过程虽然这不会让 loss 下不去但会让验证失去意义。解决先归一化再切分或先切分再 fit——我习惯先切分再 fit严格只在训练集上 fit_transform验证和测试集只 transform。确认 feature_cols 里所有列都是数值型没有把日期列混进去。5.2 现象验证集 MAE 低到 0.02测试集一测直接崩原因几乎可以肯定是数据泄漏。最常见的是用了 train_test_split 的默认参数 shuffleTrue随机打乱之后验证集里混进了训练集时间前后的样本模型提前见过了相近的序列片段。时序数据是强相关的切分位置差几十个点预测难度完全不同。解决按第 2.4 节的写法用切片按时间顺序切训练集时间轴严格在验证集和测试集之前。如果一定要用 train_test_split务必传 shuffleFalse。5.3 现象预测曲线整体比真实曲线晚一个时间步波峰对不上原因序列构造时目标取错了。y 取成窗口内最后一个时刻的值等于把输入序列的最后一位复制出来当预测模型学的是延迟复制而不是预测下一步。表现就是预测曲线比真实曲线整体右移一个 step。解决y 的索引严格取 i seq_len也就是窗口结束后的下一个时刻。自查方法打印 X[0] 的最后一行和 y[0]如果 y 等于窗口最后一行里的 flow 值那就是取错了。# 自查y[0] 不应等于 X[0][-1] 位置的 flow print(X[0][-1][feature_cols.index(flow)], y[0][0])5.4 现象pd.read_csv(weather_raw.csv) 直接报 UnicodeDecodeError原因weather_raw.csv 是 GBK 或 GB2312 编码包含中文表头pandas 默认 utf-8 解码失败。作者显然踩过这个坑才转出了一份 weather_utf8.csv。解决直接用 weather_utf8.csv如果非读 raw指定编码再读一次。weather_raw pd.read_csv(weather_raw.csv, encodinggbk, enginepython)5.5 现象TensorBoard 打开一片空白或者四组 events 文件分不清是哪次训练原因log_dir 路径里带中文或反斜杠Windows 下 TensorBoard 解析会出错另一种情况是训练还没结束events 文件没 flush 完。四组 events 文件名只有时间戳差异肉眼难分。解决log_dir 全部用英文路径训练完全结束再开 TensorBoard。分不清训练轮次就用第 3.4 节的 EventAccumulator 逐个读把每个文件的 val_loss 打印出来对比数值低的那份就是调参后的结果。注意如果模型预测结果整体偏移而不是滞后优先检查 4.1 节的 scaler 是否用了训练时的同一份。模型文件丢了可以重训scaler 丢了预测没法修——训练完立刻把 scaler.pkl 和 best_model.h5 放同一个目录。6. 进阶用法把训练好的模型封装成 Flask 接口让答辩演示从跑脚本变成调接口毕设答辩的现场演示环节评委往往没耐心看你在终端里敲命令。把模型封装成一个 HTTP 接口输入一段最近的客流和天气数据直接返回预测值演示效果比跑训练脚本好一个档次。如果配套交付的使用文档里已经带了可视化界面那你直接按文档走没有的话Flask 是成本最低的补法。6.1 Flask 封装预测服务from flask import Flask, request, jsonify import numpy as np import joblib from tensorflow.keras.models import load_model app Flask(__name__) model load_model(best_model.h5) scaler joblib.load(scaler.pkl) seq_len 24 n_features 4 app.route(/predict, methods[POST]) def predict(): data request.get_json()[seq] arr np.array(data, dtypenp.float32).reshape(1, seq_len, n_features) arr_scaled scaler.transform(arr.reshape(-1, n_features)).reshape(1, seq_len, n_features) pred_scaled model.predict(arr_scaled, verbose0) pred pred_scaled * scaler.scale_[0] scaler.data_min_[0] return jsonify({pred_flow: round(float(pred[0][0]), 2)}) if __name__ __main__: app.run(host0.0.0.0, port5000)请求体里传一个 seq 数组形状是 (seq_len, n_features)对应训练时一个样本的形状。reshape(1, seq_len, n_features) 补上 batch 维因为 Keras 预测强制要求四维或三维带 batch。归一化和反归一化用的都是训练时存下的 scaler没有重新 fit。返回的 pred_flow 是原始量纲的预测客流量。6.2 请求格式与批量输入的坑接口写好后用 curl 或 requests 验证一遍。注意 seq 数组的顺序要和训练时 feature_cols 一致顺序错了预测结果完全无意义。curl -X POST http://127.0.0.1:5000/predict \ -H Content-Type: application/json \ -d {seq: [[120, 22, 65, 3.2], [118, 22, 68, 3.0], ...]}请求体里没法手写 24 行数据实际演示时可以写个小脚本从测试集里取一段真实序列自动填充。另一个坑是 batch 预测如果一次传入多组序列arr 要 reshape 成 (batch, seq_len, n_features)反归一化的 scale_ 取值也要对应目标列不要写成硬编码。从那以后我每次做这类时序项目都会在答辩前把数据读取 → 归一化 → 预测 → 反归一化整条链路用接口的方式完整走一遍确认每个环节用的都是同一份 scaler 和同一份模型文件这个习惯帮我挡掉了至少两次现场翻车。希望帮到你。本文还有配套的精品资源点击获取