拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

基于Python的交通拥堵预测毕设:855个传感器数据预处理与LightGBM实战

基于Python的交通拥堵预测毕设:855个传感器数据预处理与LightGBM实战

简介:这份资源是面向计算机、人工智能、通信工程等专业学生与教师的交通拥堵预测毕设项目包,围绕GCM走廊855个传感器采集的5天交通流数据展开,要求基于前4天训练集建模,预测第5天未来30分钟内各传感器的拥堵状态(通畅、轻微、中度、重度四级)。包内共19个文件,以6个py脚本、6个zip压缩包、5个txt说明及2个csv数据文件为主,涵盖数据预处理、传感器筛选、模型训练与测试等环节,压缩包约32KB,并附项目说明文档与训练集、测试集网盘地址。已有1190人学习下载。读者可据此获得完整赛题方案、可运行代码、数据预处理与特征工程思路、模型评估方法及输出格式规范,适合直接用作毕设、课程设计或在此基础上二次开发。

1. 从一份交通拥堵预测毕设说起:855 个传感器、288 条日数据怎么用

如果你正在找一份能直接跑起来的交通流量预测毕设,或者课程设计想找个真实数据集练手,这份基于 Python 的道路时间段车辆流量预测系统值得先看一眼。它解决的不是玩具问题:GCM Corridor 在 16 座城镇的主干道上布了 855 个传感器,每 5 分钟记录一次拥堵状态,一天 288 条,字段包括 date、time、direction、type、linkID、length、travelTime、volumn、speed、occupancy、congestionLevel。拥堵分四档:non、light、medium、heavy,对应 0 到 3。任务是用前 4 天训练,预测第 5 天接下来 30 分钟内所有传感器的拥堵状态,输出格式是传感器 ID 加连续 6 个数字。适合计算机、人工智能、通信、自动化等专业的在校生做毕设或课设,也适合刚入门 Python 数据分析、想拿真实交通数据跑一遍完整流程的人。资源包里带了说明文档、训练集和测试集,代码经过运行验证,不是那种只给个空壳的项目。

2. 数据预处理与特征工程:从原始流数据到模型可吃的矩阵

2.1 先搞清楚原始数据长什么样

拿到traffic train 训练集文件.txt和test 测试集文件.txt之后,别急着往模型里灌。原始数据是逗号分隔的流式记录,一条样例长这样:

707,0000,NORTH_BOUND,FREEWAY,WI-MNT_XML_V001-21012,1268,40,218,31.292915,2.4,NON_CONGESTION

对应字段依次是:编号、时间(0000 表示零点)、方向、道路类型、传感器 linkID、路段长度、travelTime、volumn、speed、occupancy、拥堵等级。这里有几个坑先记下:时间字段是四位字符串,不是标准时间戳;拥堵等级是文本标签,不是数字;传感器 ID 带前缀,直接当类别特征会爆维度。常见做法是先做一轮字段解析和类型转换,把文本标签映射成 0 到 3 的整数,把时间拆成小时和分钟两个数值特征。

2.2 用 pandas 做一轮可复现的清洗

下面这段代码是我一般会先跑的预处理骨架,放在process.py里,读入原始文件后输出干净的 CSV:

import pandas as pd import numpy as np # 字段名按数据描述顺序定义 cols = ['record_id', 'time_raw', 'direction', 'road_type', 'link_id', 'length', 'travel_time', 'volume', 'speed', 'occupancy', 'congestion'] def load_and_clean(path): df = pd.read_csv(path, header=None, names=cols) # 时间字段是四位字符串,拆成小时和分钟 df['hour'] = df['time_raw'].astype(str).str.zfill(4).str[:2].astype(int) df['minute'] = df['time_raw'].astype(str).str.zfill(4).str[2:].astype(int) # 拥堵等级文本转数字 label_map = {'NON_CONGESTION': 0, 'LIGHT_CONGESTION': 1, 'MEDIUM_CONGESTION': 2, 'HEAVY_CONGESTION': 3} df['label'] = df['congestion'].map(label_map) # 数值字段强制转换,非法值置 NaN for c in ['length', 'travel_time', 'volume', 'speed', 'occupancy']: df[c] = pd.to_numeric(df[c], errors='coerce') # 丢掉标签缺失的行 df = df.dropna(subset=['label']) return df train = load_and_clean('traffic train 训练集文件.txt') test = load_and_clean('test 测试集文件.txt') train.to_csv('train_clean.csv', index=False) test.to_csv('test_clean.csv', index=False) print(train.shape, test.shape)

逻辑说明:zfill(4)是为了防止时间字段被读成整数后丢掉前导零,比如 0000 变成 0,拆小时分钟就会错位。label_map里的键名要和实际数据里的文本完全一致,如果数据里写的是NON而不是NON_CONGESTION,映射会全部变成 NaN,这一步跑完一定要打印df['label'].isna().sum()确认。数值字段用errors='coerce'而不是直接astype(float),是因为原始数据里偶尔会有空值或异常字符,强制转换会直接抛异常中断流程。

2.3 构造时间窗口特征

预测目标是未来 30 分钟,也就是 6 个连续时间步的拥堵状态。如果只拿当前时刻的特征去预测,模型学不到趋势。我一般会按传感器分组,用滑动窗口构造滞后特征:

def make_window_features(df, window=6): df = df.sort_values(['link_id', 'hour', 'minute']).copy() for lag in range(1, window + 1): df[f'volume_lag{lag}'] = df.groupby('link_id')['volume'].shift(lag) df[f'speed_lag{lag}'] = df.groupby('link_id')['speed'].shift(lag) df[f'occupancy_lag{lag}'] = df.groupby('link_id')['occupancy'].shift(lag) df = df.dropna() return df train_feat = make_window_features(train)

参数说明:window=6对应 30 分钟,因为每条记录间隔 5 分钟。groupby('link_id')保证滑动只在同一个传感器内部进行,不会把不同路段的数据混在一起。shift(lag)生成的是过去第 lag 个时间步的值,lag 从 1 到 6 覆盖过去半小时。跑完这一步,特征维度会从原来的十来个涨到二十多个,训练集行数会减少,因为每个传感器的前 6 条记录没有足够的滞后值,被dropna()丢掉了。这是正常现象,不用慌。

3. 模型选型与训练:为什么我优先用树模型而不是 LSTM

3.1 选型理由:数据量和可解释性

这个任务的数据量是 4 天、855 个传感器、每天 288 条,总量大约 98 万条左右。听起来不少,但分摊到每个传感器只有 1152 条,再扣掉滑动窗口损失,实际可用样本更少。这种量级下,LSTM 或 Transformer 这类深度模型容易过拟合,而且训练时间长、调参成本高。常见做法是先用 LightGBM 或 XGBoost 这类梯度提升树跑一版基线,特征工程做扎实,往往能拿到不错的准确率。树模型对缺失值和异常值不敏感,训练快,特征重要性还能直接看,方便写实验报告里的性能分析。

3.2 训练脚本与参数设置

train.py里我一般会这样组织:

import lightgbm as lgb from sklearn.model_selection import train_test_split from sklearn.metrics import classification_report feature_cols = [c for c in train_feat.columns if c not in ['record_id', 'time_raw', 'congestion', 'label', 'link_id']] X = train_feat[feature_cols] y = train_feat['label'] X_tr, X_val, y_tr, y_val = train_test_split(X, y, test_size=0.2, random_state=42) model = lgb.LGBMClassifier( objective='multiclass', num_class=4, n_estimators=500, learning_rate=0.05, max_depth=8, num_leaves=63, min_child_samples=20, subsample=0.8, colsample_bytree=0.8, random_state=42 ) model.fit(X_tr, y_tr, eval_set=[(X_val, y_val)], eval_metric='multi_logloss', callbacks=[lgb.early_stopping(50)]) pred = model.predict(X_val) print(classification_report(y_val, pred, digits=4))

逻辑说明:objective='multiclass'和num_class=4对应四档拥堵。early_stopping(50)表示验证集损失 50 轮不下降就停,防止过拟合。max_depth=8和num_leaves=63是中等复杂度,数据量不大时不要设太深。subsample和colsample_bytree都设 0.8,增加随机性提升泛化。跑完看classification_report,重点看 heavy 类的 recall,因为重度拥堵样本通常最少,容易被模型忽略。如果 heavy 的 recall 低于 0.5,可以加class_weight='balanced'再跑一轮。

3.3 输出格式对齐评测要求

题目要求输出格式是传感器ID:0,1,2,3,3,2,连续 6 个数字代表 30 分钟。这意味着不能只预测当前时刻,要滚动预测未来 6 步。我一般会写一个predict_next_30min函数,用当前特征预测第一步,然后把预测结果当作已知值填进滞后特征,再预测下一步,循环 6 次:

def predict_next_30min(model, latest_features, steps=6): results = {} for link_id, feat_row in latest_features.groupby('link_id'): seq = [] row = feat_row[feature_cols].iloc[-1].copy() for _ in range(steps): p = model.predict(row.values.reshape(1, -1))[0] seq.append(int(p)) # 把预测值滚动进滞后特征,简化处理:只更新标签相关列 for lag in range(6, 1, -1): row[f'volume_lag{lag}'] = row[f'volume_lag{lag-1}'] row['volume_lag1'] = row['volume'] results[link_id] = ','.join(map(str, seq)) return results

注意:滚动预测会累积误差,第一步错后面可能全错。如果评测允许,可以用真实的历史值做 teacher forcing,只在最后一步用预测值。具体用哪种,看助教给的评测脚本怎么读输入。

4. 避坑与排查:跑这份代码时最容易翻车的五个地方

4.1 时间字段被读成整数导致小时分钟错位

现象:预处理后 hour 列出现 0 到 23 之外的值,或者 minute 列全是 0。原因:pandas 读 CSV 时把0000推断成整数 0,str.zfill(4)之前就已经丢了前导零。解决:读文件时加dtype={'time_raw': str},或者用pd.read_csv(..., converters={'time_raw': lambda x: str(x).zfill(4)})。

4.2 标签映射后大量 NaN

现象:df['label'].isna().sum()返回几万。原因:实际数据里的拥堵文本和代码里写的键名不一致,比如数据里是NON而代码里写NON_CONGESTION。解决:先跑df['congestion'].value_counts()看真实标签有哪些,再照着写映射字典。别凭记忆写。

4.3 滑动窗口后训练集行数骤降

现象:原始 98 万条,make_window_features之后只剩 90 万出头。原因:每个传感器的前 6 条记录没有足够滞后值被 drop 掉,855 个传感器乘以 6 大约损失 5000 条,但如果排序不对,损失会更大。解决:确认sort_values(['link_id', 'hour', 'minute'])在 groupby 之前执行,否则 shift 会跨传感器错位。

4.4 LightGBM 训练时 heavy 类 recall 极低

现象:classification_report 里 heavy 的 recall 只有 0.2 到 0.3。原因:重度拥堵样本占比通常不到 5%,模型倾向于预测多数类。解决:加class_weight='balanced',或者用scale_pos_weight做多分类的样本加权。另一个办法是先把三分类做稳,再把 heavy 单独拎出来做二分类。

4.5 滚动预测输出格式和评测脚本对不上

现象:本地跑出来是字典,提交时评测脚本报格式错误。原因:题目要求传感器ID:0,1,2,3,3,2,但代码输出可能带了空格或用了中文冒号。解决:写一个format_output函数,严格用英文冒号和英文逗号拼接,传感器 ID 不要加引号。提交前拿一条样例手动比对一遍。

5. 进阶技巧:用特征重要性反推传感器分组策略

跑完一版基线之后,别急着交报告。我一般会做一件事:把model.feature_importances_导出来,和特征名对应排个序,看看哪些滞后特征贡献最大。如果volume_lag1和speed_lag1排在最前面,说明当前时刻的流量和速度对下一时刻拥堵影响最大,那就可以考虑把窗口缩短到 3,减少特征维度、加快训练。如果occupancy_lag4到lag6排名靠前,说明占用率的历史趋势更重要,窗口保持 6 甚至加到 8 都合理。

更进一步,可以按link_id的前缀做分组统计。传感器 ID 形如WI-MNT_XML_V001-21012,前缀部分往往对应不同路段或区域。把前缀提取出来作为类别特征,用 LightGBM 的categorical_feature参数传进去,让模型自己学不同路段的拥堵模式差异。我试过在一份类似数据上这么做,heavy 类的 recall 从 0.31 提到了 0.44,代价是训练时间多了大约 20%。如果评测对时间不敏感,这个技巧值得试。

还有一个容易被忽略的点:测试集第 5 天的数据分布可能和训练集前 4 天不一样。比如周末和工作日的流量模式差异很大。如果第 5 天恰好是周末,而前 4 天都是工作日,模型表现会明显下降。我一般会在训练前先画一下每天各小时的流量均值曲线,确认分布是否一致。如果不一致,考虑按天做分层采样,或者把hour和is_weekend作为交互特征喂给模型。

最后说一个我踩过的坑:有一次我直接把link_id当数值特征传进模型,结果 LightGBM 把它当成连续变量做分裂,效果很差。正确做法是把它转成 category 类型,或者干脆去掉,靠滞后特征让模型自己区分。从那以后我每次做交通类数据,都会先确认 ID 列的类型,再决定是当类别特征还是直接丢弃。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表