简介这份资源面向交通工程、城市规划及深度学习入门者提供一套基于Python与Keras的车流量预测完整实现重点解决时间序列数据的建模与预测问题。包内共38个文件以10个ipynb实验笔记、8个csv数据集、6个h5模型权重为主辅以png结果图、py脚本与说明文档压缩包约4.67MB结构清晰便于按模块查阅。内容覆盖数据预处理、滑动窗口构造、LSTM/GRU/BiLSTM及简单RNN、SAEs、岭回归等多模型对比实验并附训练损失记录与预测图像可帮助读者理解循环神经网络处理车流序列的完整流程。目前已有2684人学习下载适合希望快速复现车流量预测方案、对比不同模型效果并积累调参排错经验的学习者参考。1. 车流量预测模型到底在算什么从一段早高峰数据说起早高峰的十字路口摄像头每 5 分钟回传一次过车数一天下来就是 288 个点。车流量预测模型要做的就是拿过去若干天的这串数字去猜未来 15 分钟、1 小时甚至 1 天的流量。它不神秘本质是一个带时间结构的回归问题输入是历史流量序列加上时间特征输出是未来某个时刻的数值。用 Python 做这件事是因为从数据清洗、特征工程到建模评估整条链路都有成熟库支撑不用自己造轮子。这篇文章面向两类人一类是刚学完 Python 基础语法、想找一个能跑通全流程的真实项目练手的人另一类是手头已经有卡口或线圈数据、需要快速搭一个可用预测基线的工程人员。我会把「车流量预测模型」拆成可复现的步骤——数据怎么造、特征怎么搭、模型怎么选、参数怎么调、结果怎么验证中间穿插我踩过的坑。读完你应该能用自己的数据替换掉示例跑出一个能解释、能迭代的预测版本。2. 数据从哪来、长什么样先把手里的流量序列摸清楚2.1 车流量数据的三种常见来源与字段结构做车流量预测第一步不是写模型是搞清楚数据长什么样。常见来源有三类地磁或线圈检测器输出的是固定时间粒度的过车计数视频卡口输出的是每辆车的过车时间戳和车牌需要自己按时间窗聚合公开交通数据集比如一些城市开放平台提供的路段流量 CSV。不管哪种最终都要整理成「时间戳 流量值」两列时间戳要连续缺失的时间点要显式补上不能跳着排。我一般会先做一次字段体检确认这几件事时间列是不是标准 datetime、流量列有没有负数或异常大值、采样间隔是否均匀。下面这段代码就是干这个的用 pandas 读进来后先看结构和统计量。import pandas as pd # 读取原始流量数据假设两列timestamp, flow df pd.read_csv(traffic_flow.csv, parse_dates[timestamp]) df df.sort_values(timestamp).reset_index(dropTrue) # 基础体检 print(df.dtypes) print(df[flow].describe()) print(时间跨度:, df[timestamp].min(), -, df[timestamp].max()) # 检查采样间隔是否均匀 gap df[timestamp].diff().value_counts() print(gap.head())这段代码的关键在parse_dates它把字符串时间转成 datetime后面做重采样和滑窗都依赖这个类型。describe()能快速看出流量有没有离谱的极值比如某条记录是 99999多半是设备故障而不是真实流量。diff().value_counts()看的是相邻时间差如果出现大量非 5 分钟、非 15 分钟的间隔说明数据有断档需要先补全再建模。2.2 用重采样把不规则数据对齐成固定频率真实数据很少是规整的。设备偶尔离线聚合窗口偶尔错位直接丢进模型会让序列错乱。稳妥做法是先设定一个目标频率比如 15 分钟然后用重采样把数据对齐缺失值先标记出来再决定怎么填。# 设定 15 分钟为目标频率聚合方式用求和过车数累加 df df.set_index(timestamp) df_15min df[flow].resample(15min).sum(min_count1) # 统计缺失情况 missing_ratio df_15min.isna().mean() print(f缺失比例: {missing_ratio:.2%}) # 短缺口用线性插值长缺口保留 NaN 后续单独处理 df_15min df_15min.interpolate(methodlinear, limit4) df_15min df_15min.dropna()resample(15min).sum(min_count1)里的min_count1很重要它保证一个窗口内如果一条原始记录都没有结果是 NaN 而不是 0。0 和缺失是两回事凌晨某 15 分钟真的没车流量是 0设备坏了没数据那是缺失。把缺失当 0 填模型会学到假的低谷。interpolate的limit4表示最多连续补 4 个点也就是 1 小时再长的缺口我倾向直接丢掉避免插值造出虚假趋势。2.3 时间特征工程把「几点、周几」变成模型能吃的列流量序列最强的规律是周期性的早高峰、晚高峰、工作日和周末的形态完全不同。模型本身只看数值不会自动知道「这是周一早上 8 点」所以要把时间信息显式拆成特征列。def add_time_features(series): df series.to_frame(flow) df[hour] df.index.hour df[minute] df.index.minute df[dayofweek] df.index.dayofweek df[is_weekend] (df[dayofweek] 5).astype(int) # 把一天内的时刻编码成周期特征避免 23 点和 0 点被当成很远 df[minute_of_day] df[hour] * 60 df[minute] df[sin_time] np.sin(2 * np.pi * df[minute_of_day] / 1440) df[cos_time] np.cos(2 * np.pi * df[minute_of_day] / 1440) return df import numpy as np feat_df add_time_features(df_15min) print(feat_df.head())这里sin_time和cos_time是周期编码把一天 1440 分钟映射到单位圆上让 23:45 和 00:00 在特征空间里也接近。如果只用hour这个整数模型会认为 23 和 0 差得很远实际上它们相邻。is_weekend单独拎出来是因为周末的流量峰值通常比工作日晚一到两小时这个特征能帮模型区分两种模式。3. 车流量预测模型怎么选从移动平均到梯度提升的取舍3.1 先立一个能打的基线历史均值与移动平均上来就上深度学习是新手最容易翻车的地方。我一般先用两个基线把误差范围摸出来一是「昨天同一时刻的值」二是「过去若干天的同时刻均值」。这两个方法实现简单但能告诉你数据本身的规律有多强。如果复杂模型连这两个基线都打不过说明特征或数据有问题不是模型不够深。# 基线1昨天同一时刻 feat_df[lag_1day] feat_df[flow].shift(96) # 96 24h / 15min # 基线2过去7天同时刻均值 feat_df[ma_7day] feat_df[flow].shift(96).rolling(window7).mean() # 评估只用有值的部分 valid feat_df.dropna(subset[lag_1day]) mae_lag (valid[flow] - valid[lag_1day]).abs().mean() mae_ma (valid[flow] - valid[ma_7day]).abs().mean() print(f昨天同时刻 MAE: {mae_lag:.2f}) print(f7天均值 MAE: {mae_ma:.2f})shift(96)是核心96 个 15 分钟正好是一天。这个操作把「昨天的值」搬到今天同一行构造出一个合法的预测特征。注意rolling之前也要先shift否则会把当前时刻的值算进均值里造成数据泄漏。MAE 的单位和流量一致比如 MAE 是 12意思是平均每次预测差 12 辆车这个数字比 R² 更直观跟业务方沟通时更好用。3.2 用 LightGBM 搭一个带滑窗特征的主力模型基线跑通后主力模型我通常选 LightGBM。原因很实际车流量特征里既有数值型的滞后项又有类别型的时间特征树模型对这两类都不挑训练快还能直接输出特征重要性方便排查哪个特征在起作用。构造滑窗特征时把过去几个时间步的流量作为输入列。import lightgbm as lgb from sklearn.model_selection import TimeSeriesSplit # 构造滞后特征过去 1、2、4、96 步 for lag in [1, 2, 4, 96]: feat_df[flag_{lag}] feat_df[flow].shift(lag) # 构造滑动均值特征 feat_df[roll_mean_4] feat_df[flow].shift(1).rolling(4).mean() feat_df[roll_std_4] feat_df[flow].shift(1).rolling(4).std() feat_df feat_df.dropna() feature_cols [c for c in feat_df.columns if c ! flow] X, y feat_df[feature_cols], feat_df[flow] # 时间序列交叉验证不能随机切分 tscv TimeSeriesSplit(n_splits5) for train_idx, test_idx in tscv.split(X): model lgb.LGBMRegressor( n_estimators500, learning_rate0.05, num_leaves31, min_child_samples20, subsample0.8, colsample_bytree0.8, ) model.fit(X.iloc[train_idx], y.iloc[train_idx]) pred model.predict(X.iloc[test_idx]) mae np.abs(pred - y.iloc[test_idx]).mean() print(f验证 MAE: {mae:.2f})这段代码有两个必须守住的点。第一所有滞后和滑窗特征都要shift保证预测 t 时刻时只用到 t 之前的信息否则模型在验证集上表现会好得离谱上线就崩。第二切分必须用TimeSeriesSplit不能train_test_split随机打乱时间序列一旦打乱未来信息会漏进训练集。参数上learning_rate0.05配n_estimators500是比较稳的组合num_leaves31控制树的复杂度流量数据量不大时别调太高容易过拟合。3.3 特征重要性怎么看哪些列真的在驱动预测模型跑完别急着调参先看特征重要性。LightGBM 训练后可以直接拿到每个特征被使用的次数或增益这能帮你判断模型是不是学到了合理的东西。model.fit(X, y) importance pd.Series( model.feature_importances_, indexfeature_cols ).sort_values(ascendingFalse) print(importance.head(10))正常情况下lag_1、lag_96、sin_time、cos_time这几个应该排在前列。lag_1代表最近一个时间步lag_96代表昨天同一时刻两者都强说明流量既有短时惯性又有日周期。如果发现某个特征重要性异常高但业务上说不通比如minute这个原始列压过了周期编码那可能是数据里存在某种采集规律被模型当成了捷径需要回头检查。4. 训练与验证的坑时间序列不能随便切4.1 数据泄漏滑窗特征最容易犯的错数据泄漏是车流量预测里最隐蔽的坑。现象是验证集 MAE 低得反常比如只有 2 到 3上线后实际误差翻好几倍。原因通常是构造特征时忘了shift或者用全量数据的均值去填充缺失把未来信息带进了训练。解决方法是把特征工程写成函数严格保证每个特征在 t 时刻只依赖 t 之前的数据并且用TimeSeriesSplit做验证。我习惯在特征生成后加一句断言检查特征列和标签列的相关性是否高得离谱超过 0.99 就要警惕。4.2 缺失值填充均值填充为什么会让模型变懒现象是模型在缺失较多的时段预测特别平几乎是一条直线。原因是用了全局均值或前后向填充把缺失段填成了同一个值模型学到「这段就是平的」。正确做法是区分短缺口和长缺口短缺口用线性插值长缺口要么丢弃要么加一个is_missing标记列让模型自己决定怎么处理。填充方式不同同一份数据训出来的模型误差能差 20% 以上这个我实测过。4.3 异常值设备故障产生的尖峰怎么处理现象是某天凌晨流量突然出现一个远超正常值的尖峰模型被带偏之后几天的预测都偏高。原因是检测器故障或重复计数产生了物理上不可能的流量。解决方法是先做业务规则过滤比如流量超过车道饱和流量就标记为异常再用中位数或前后均值替换。不要直接删掉整行因为时间序列删行会破坏连续性替换数值更稳妥。4.4 评估指标为什么不能只看 R²现象是 R² 很高但业务方说预测没用。原因是 R² 对极端值不敏感而流量预测里高峰时段的误差才是关键。解决方法是同时看 MAE、RMSE 和高峰时段的分段 MAE。我一般会把预测结果按流量高低分成三档分别算误差高峰档的 MAE 如果明显高于平峰档说明模型在关键时段不够准需要针对性加特征或调整损失函数。4.5 过拟合验证集好、测试集崩的排查顺序现象是交叉验证 MAE 很漂亮换一段新数据就崩。排查顺序我固定为先看特征有没有泄漏再看训练集和验证集的分布是否一致最后才怀疑模型复杂度。很多时候不是模型过拟合是两段数据的采集设备或口径变了。确认是过拟合后优先降num_leaves、加min_child_samples、提高subsample的随机性而不是盲目加数据。5. 让预测更稳的进阶技巧残差修正与滚动更新模型上线后最实际的问题不是再涨零点几个点的精度而是让预测跟着数据变化走。我常用的两个技巧一个是残差修正一个是滚动更新。残差修正是这样的先用主模型预测然后把预测值和真实值的差当成一个新的序列看看这个残差有没有规律。如果残差在特定时段系统性偏正或偏负说明主模型漏掉了某种模式可以再训一个轻量模型去预测残差最后把两部分相加。代码上就是把y - pred作为新标签特征沿用原来的时间特征。# 主模型预测后计算残差 pred model.predict(X) residual y - pred # 用时间特征训一个残差修正模型 res_model lgb.LGBMRegressor(n_estimators200, learning_rate0.05, num_leaves15) res_model.fit(X[[sin_time, cos_time, dayofweek, is_weekend]], residual) res_pred res_model.predict(X[[sin_time, cos_time, dayofweek, is_weekend]]) final_pred pred res_pred mae_final np.abs(final_pred - y).mean() print(f修正后 MAE: {mae_final:.2f})残差模型只用了时间特征是因为残差里剩下的往往就是时间相关的系统性偏差用复杂特征反而容易过拟合。num_leaves15比主模型小也是这个考虑。滚动更新指的是每次拿到新数据后不重新训练整个模型而是把新样本追加进去做增量更新或者每隔固定周期全量重训一次。LightGBM 支持init_model参数做继续训练适合数据流稳定的场景。我的习惯是每天凌晨用最近 30 天数据全量重训一次白天每小时用新数据做一次增量更新这样既跟得上变化又不会频繁重训拖垮资源。验证这套流程是否可靠我固定看三件事一是滚动预测未来 24 小时的 MAE 是否稳定二是高峰时段的误差有没有随更新下降三是特征重要性排名有没有剧烈跳动。如果某次更新后重要性大洗牌多半是数据源出了问题先查数据再查模型。最后说个我自己的习惯每次调完参我都会把当次的特征列表、参数和验证 MAE 记在一个表格里不靠记忆。车流量预测这个方向模型本身不难难的是数据在变、口径在变能复现、能对比、能回滚比单次跑出一个漂亮数字重要得多。希望帮到你。本文还有配套的精品资源点击获取