尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

用行车轨迹反推信号灯周期:从停车事件到傅里叶分析

用行车轨迹反推信号灯周期:从停车事件到傅里叶分析 简介针对2024年华中杯数学建模B题“使用行车轨迹估计交通信号灯周期问题”的完整解决方案包面向参赛学生、数学建模爱好者及交通数据分析学习者。内含可直接运行的MATLAB源码、最终论文、支撑材料与详细文档覆盖数据清洗、轨迹聚类、信号周期估计与可视化等核心环节可帮助读者按图索骥快速复现赛题结果。包体共95个文件以m脚本、csv/mat数据文件、png/jpg结果图、fig/vsdx流程图与PDF文档为主整体15.66MB目录结构清晰按数据、代码、结果、论文分模块组织。另含B题原始附件、竞赛承诺书及README说明基本无需额外查找资料即可完成训练与测试。目前已有135人学习下载代码均测试通过如遇运行问题可私聊咨询支持远程教学既适合数学建模新手学习完整思路也适合高年级学生在此基础上扩展算法用于课程设计或项目演示。1. 交通信号灯周期估计光靠行车轨迹也能把红灯时间反推出来行车轨迹数据是城市交通里最廉价也最嘈杂的信号源。出租车、网约车、公交车每天产生海量GPS点但没人会专门为红绿灯安装额外传感器。这道题的核心价值在于只用车载轨迹里“停”“走”“等”的行为就能反演出某个路口信号灯的周期、相位差甚至绿信比。原理并不神秘——每辆车在路口经历的停车等待时长本质上是一次对信号配时的随机采样当样本量够多时周期规律会从噪声里浮现出来。适合准备国赛、华为杯或者做城市计算研究的人阅读用Python语言就能复现不需要专用交通仿真软件。2. 轨迹数据预处理坐标系、漂移点与停车事件提取2.1 先把轨迹统一到合理的坐标系拿到一份轨迹数据第一件事不是算周期而是看坐标系和采样频率。常见数据格式是timestamp, lat, lon, direction, speed其中经纬度是WGS84直接算距离会带来误差。我一般会做两步先转成Web Mercator或UTM投影坐标让单位变成米再按vehicle_id和时间戳排序去掉重复点和跳变点。下面的代码演示了从CSV读取到投影转换的最小流程import pandas as pd from pyproj import Transformer df pd.read_csv(trajectory.csv, parse_dates[timestamp]) df df.sort_values([vehicle_id, timestamp]).drop_duplicates( subset[vehicle_id, timestamp]) # WGS84 - UTM zone 50N根据城市所在经度调整 transformer Transformer.from_crs(EPSG:4326, EPSG:32650, always_xyTrue) df[x], df[y] zip(*df.apply(lambda r: transformer.transform(r[lon], r[lat]), axis1)) df[dx] df[x].diff() df[dy] df[y].diff() df[dist] df[[dx, dy]].apply(lambda r: (r[dx]**2 r[dy]**2)**0.5, axis1) df[speed_est] df[dist] / df[timestamp].diff().dt.total_seconds()逻辑说明代码先排序去重避免同一辆车在同一时刻有多个点随后通过投影把经纬度转成平面坐标dist列得到相邻点距离speed_est是估计速度。注意diff()会跨车计算所以需要先按vehicle_id分组再做差分更严谨的写法是用groupby这里为了演示省略。参数说明UTM带号要根据城市所在经度选比如北京是50N广州是49N。如果你的数据自带速度字段可以用df[speed]代替speed_est但要留意车载OBD速度有时会比GPS速度更平滑。2.2 停车事件检测速度阈值与持续时长的双条件判断周期估计依赖的关键特征是“停车事件”的开始和结束时刻。停车不是瞬时速度等于0因为GPS漂移会让静止车辆的速度在0到5 km/h之间抖动。我通常用两个条件叠加速度连续N秒低于一个阈值并且位移不超过一个半径。这个阈值需要针对数据采样频率调整经验值见下表采样频率速度阈值最小持续时长最大位移半径1Hz3 km/h5 s10 m5Hz5 km/h3 s8 m10Hz8 km/h2 s5 m表格里的逻辑是采样率越高单次速度抖动越明显所以阈值要适当放开但持续时长可以缩短因为高频数据能更好确认车辆真的停了。实际操作中我会先用车速列判断再检查这段“疑似停车”期间坐标是否基本不动。下面的代码提取了每辆车的停车片段stopping_mask (df[speed] 3.0) (df[speed].rolling(5, centerTrue).mean() 2.0) df[is_stop] stopping_mask # 合并连续片段 groups (df[is_stop] ! df[is_stop].shift()).cumsum() stop_segments df[df[is_stop]].groupby([vehicle_id, groups]).agg( stop_start(timestamp, min), stop_end(timestamp, max), stop_x(x, mean), stop_y(y, mean), stop_duration(timestamp, lambda s: (s.max() - s.min()).total_seconds()) ).reset_index()这里用到滚动均值和片段合并。rolling(5, centerTrue)可以滤掉单帧的瞬时掉速避免把正常减速误判成停车。注意groupby([vehicle_id, groups])中的groups必须从整个DataFrame维度算不能先切片再算否则不同车辆的片段编号会错位。2.3 轨迹地图匹配把停车点关联到具体车道如果数据里只有经纬度没有车道信息那么停车点可能靠近路口也可能停在离路口几十米的地方。为了准确关联信号灯需要进行地图匹配。常见的开源方案是使用leuvenmapmatching或valhalla但做竞赛题时我更推荐轻量做法预定义路口中心点坐标把停车点按距离归入半径100米范围内的路口。这样做的理由很简单——一个城市的路口数量是有限的而轨迹数据往往集中在干道上不需要完整路网匹配。from scipy.spatial import cKDTree # intersections: [[x1,y1], [x2,y2], ...] tree cKDTree(intersections) stop_segments[nearest_intersection] tree.query( stop_segments[[stop_x, stop_y]].values, k1)[1] stop_segments[distance_to_intersection] tree.query( stop_segments[[stop_x, stop_y]].values, k1)[0] nearby stop_segments[stop_segments[distance_to_intersection] 100]匹配后每个停车事件就有了对应的路口ID。注意一个路口有多个方向后续分析时需要用车辆行驶方向角把停车点再细分到进口道。方向角可以用最后几个GPS点的航向计算也可以用轨迹的斜率和坐标增量算。3. 周期估计核心算法从停车时间序列到傅里叶分析与排队模型3.1 为什么停车事件天然携带周期信息假设一个路口信号周期为T红灯时长为R车辆在红灯期间到达则停车时长服从[0, R]上的截断分布。如果所有车的到达是均匀的那么停车时长序列会呈现以T为间隔的重复模式。但是实际数据中到达率不是恒定的而且存在绿波带、公交站点干扰所以直接看停车时长的直方图可能只有几个模糊的峰。更稳健的做法是看“停车开始时刻”在全天时间轴上的分布然后对这个时间序列做频域分析。因为所有车都受同一个周期调制停车事件密度函数必然在频率1/T处有峰值。3.2 用离散傅里叶变换估计周期把一天时间切成相等的时间片统计每个片内有多少辆车开始停车得到一个事件计数序列。对该序列做FFT找到显著频率。下面是一个完整示例import numpy as np from scipy.fft import fft, fftfreq # 构造时间片单位秒 t_start 0 t_end 24 * 3600 bin_size 10 # 10秒一个bin bins np.arange(t_start, t_end bin_size, bin_size) counts, _ np.histogram( nearby[stop_start].astype(int64) // 10**9 % (t_end), binsbins ) counts counts.astype(float) counts - counts.mean() # 去均值消除直流分量 # FFT N len(counts) fft_vals fft(counts) freqs fftfreq(N, dbin_size) amplitudes np.abs(fft_vals[:N//2]) valid freqs[:N//2] 0 candidate_freq freqs[:N//2][valid][np.argmax(amplitudes[valid])] estimated_period 1 / candidate_freq print(Estimated period (seconds):, estimated_period)逻辑说明np.histogram统计每个10秒窗口内的停车次数形成类似时间序列的计数。去均值后主频分量对应着停车事件的重复周期。注意FFT的频率分辨率是1 / (N * bin_size)如果一天数据量是8640个bin分辨率约为0.000116 Hz换算成周期大约是8640秒的整数倍误差所以周期值只是粗估计还需要局部细化。参数说明bin_size的选择很关键。太小会让很多bin计数为0造成频谱噪声太大会平滑掉短周期信号。对于60到120秒的信号周期10到15秒的bin比较合适。另外如果数据跨天要注意取模时间避免把午夜之后的停车事件算到前一天。3.3 局部周期校正自相关函数找精确峰值FFT得到的周期误差通常有数秒甚至十几秒因为频谱分辨率有限。进一步可以用自相关函数在某段周期范围内寻找峰值。自相关计算量不大适合对单个路口精修from statsmodels.tsa.stattools import acf counts counts.astype(float) lags np.arange(30, 240) # 30~240秒范围内搜索 acf_values acf(counts, nlags240, fftTrue)[30:240] best_lag lags[np.argmax(acf_values)] # 用抛物线插值精修 from scipy.optimize import curve_fit def parabola(x, a, b, c): return a * x**2 b * x c idx np.argmax(acf_values) if 0 idx len(acf_values) - 1: neighbors acf_values[idx-1:idx2] p np.polyfit([-1, 0, 1], neighbors, 2) refined_lag best_lag - p[1] / (2 * p[0])这里的思路是先粗扫30到240秒的滞后找自相关最大值对应的滞后秒数然后用峰值附近三个点的二次插值把峰值位置往小数秒精度逼近。因为信号灯周期一般是整数秒最终取整即可。3.4 结合排队消散的绿灯时长估计如果题目不仅要求周期还要估计绿信比或红灯时长就要用排队论。当信号灯从红变绿时排队车辆以饱和流率依次启动每辆车通过停车线的时间间隔接近一个常数例如2秒/辆。因此绿灯时长可以用“排队车辆数 × 启动时距 首车启动损失时间”来估计。停车的车辆数可以从停车事件中推算也可以用轨迹中静止车辆的累计数量跟踪# 假设某周期内排队长度为n饱和车头时距h首车启动损失l0 green_time n * h l0 # 通常 h2.0s, l03s但这样做有前提排队车辆到达率足够大排队能够在绿灯期间完全消散。如果路口不饱和排队长度不能反映绿灯长短这种情况下需要结合上游检测器的流量数据。竞赛题数据里常常没有真实信号配时做对比所以这个模型主要用于验证傅里叶结果是否合理比如周期估计为80秒绿灯估计为40秒若两个方法对得上说明模型稳定。4. 源代码实现与调参实战仿真验证到数据增强4.1 用合成轨迹验证你的估计算法在没有真实标签的情况下如何确认算法的可靠性首选做法是用SUMO或自定义仿真器生成带真实信号配时的轨迹。如果你没有SUMO环境也可以用简单模型合成假设周期T90红/绿各45秒车辆随机到达并在红灯期间停车。这样生成的轨迹已知答案可以直接评估估计误差。下面是用Python生成合成停车点的代码import numpy as np import pandas as pd np.random.seed(42) T_true 90 green_start 10 # 每次循环中绿灯开始秒数 total_time 2 * 3600 vehicle_count 600 arrivals np.random.uniform(0, total_time, vehicle_count) stop_times [] for arr in arrivals: pos_in_cycle (arr - green_start) % T_true if pos_in_cycle 0: # 红灯 stop_duration T_true - pos_in_cycle stop_start arr stop_times.append(stop_start) # 对这些停车点跑FFT看能恢复出 T_true 吗逻辑说明生成车辆到达时间后计算每个到达时刻在一个周期内的相位如果相位处于红灯窗口记录下停车开始时间。把所有停车点拼成一个时间序列用前面写的FFT代码估计周期。这个流程能帮你快速验证bin_size、阈值等参数对估计结果的影响。4.2 代码结构设计把清洗、匹配、估计拆成三个模块竞赛源代码的交付质量会影响评审印象。我建议代码至少分成preprocess.py、period_estimator.py、main.py三个文件另外配一个config.yaml保存参数这样论文中提到的每个参数都能在配置里对应上。# config.yaml preprocess: speed_threshold: 3.0 # km/h min_stop_duration: 5 # s map_radius: 100 # m period_estimation: bin_size: 10 # s fft_lag_min: 30 fft_lag_max: 240主入口代码负责读取数据、调用三个模块并输出估计结果import yaml, pandas as pd from preprocess import clean_trajectory from period_estimator import estimate_period_with_fft with open(config.yaml) as f: cfg yaml.safe_load(f) df pd.read_csv(data/track.csv) df clean_trajectory(df, cfg[preprocess]) result estimate_period_with_fft(df, cfg[period_estimation]) print(result)参数说明配置文件的好处是论文里可以直接引用“本文参数设置如表1所示”评审如果想复现改一行配置就能重跑。注意clean_trajectory里要返回DataFrame还要记录被过滤掉的样本数方便在论文里写数据清洗比例。4.3 避开三个最常见的坑多周期、漂移和缺失时段第一个坑是实际路口在不同时段信号周期会变比如早晚高峰周期90秒平峰70秒。全天统一做FFT可能得到两个峰。解决办法是分时段估计例如早高峰7-9点、晚高峰17-19点分别跑一次再对比差异。第二个坑是GPS漂移导致停车点被误判为缓慢移动把停车时长算短。这个问题可以通过把速度阈值设小加上位置漂移半径限制来缓解但过小的阈值又会漏掉真实停车。我一般会分别按3、4、5 km/h跑三次看周期估计结果的稳定性。第三个坑是夜间车流量极低停车事件稀疏FFT基本失效。此时需要至少筛选出每个周期内有停车数据的时间段如果连续空白超过3个周期就把这段数据剔除不纳入估计。4.4 用平均绝对误差评估估计结果如果拿合成数据测试你会希望有一个指标来衡量优劣。我常用的是平均绝对误差MAE与误差的累计分布。例如测试10组随机种子每组生成1小时轨迹估计周期分别为88、91、90、89真实值为90那么MAE是1.25秒。这个数据可以写进论文的模型检验部分比单独放一个“结果正确”更有说服力。对于真实数据因为没有真值可以看多天估计值的标准差比如连续7天同一路口估计结果都是45到50秒附近说明信号配时没有大幅调整模型可信。5. 论文与文档交付从参数敏感性到可复现评审的细节5.1 论文结构怎么组织评审才愿意看华中杯这类竞赛的评审重点是问题分析、模型假设、求解结果、分析检验。论文里不需要长篇介绍什么是轨迹数据而要把重点放在“如何证明估计周期是可信的”。我推荐按“数据描述 → 方法设计 → 实验验证 → 结果讨论”来写其中实验验证必须包含合成数据测试和真实数据案例两个部分。用合成的结果说明“算法误差在3秒以内”用真实数据的结果说明“不同方向、不同时段估计的周期一致”。5.2 参数敏感性分析与表格化呈现评审喜欢看你对参数不敏感这意味着模型鲁棒。你可以选取bin_size、速度阈值、FFT峰值搜索范围三个参数逐一扰动后记录估计结果变化。下面是一个示例表格bin_size (s)估计周期 (s)误差 (s)589.7-0.31090.20.21591.01.0表格说明在5到15秒范围内估计周期波动不超过1.5秒说明算法对bin大小不敏感。类似地把速度阈值从2.5调到3.5停车事件数量会变化大约12%但最终周期估计只变化1秒左右。这些数字加到论文里一下就让模型有了说服力。5.3 源代码打包与文档说明的要点文档说明是竞赛题目自带要求通常包括环境依赖、运行步骤、复现结果。我会在压缩包里放一个README.md写出pip install -r requirements.txt之后运行python main.py即可。还需要把原始数据文件名与格式在文档中写明例如“trajectory.csv包含时间戳、车辆ID、经度、纬度、瞬时速度五列”。如果代码量不大可以把核心函数加上docstring并在关键步骤输出日志import logging logging.basicConfig(levellogging.INFO) logger logging.getLogger(__name__) logger.info(Loaded %d raw records, kept %d after cleaning, raw_count, kept_count)日志的作用是让评审和读者看到信息流动也能帮助你自己排查问题。对于傅里叶估计部分可以输出峰值频率和对应的周期候选值方便观察算法挑选过程。5.4 对2026年备赛者的一个具体建议如果你是准备2026年数学建模比赛看到这道题时不要只盯着信号灯周期背后是“从低质量传感器数据中恢复时空周期信号”这一类问题。同样的方法可以迁移到共享单车潮汐、地铁客流波动、甚至空气质量污染周期估计。所以动手时建议把清洗和频域估计封装成独立的工具函数未来换数据只需要改口径。下次遇到“CPI周期估计”“网络流量周期检测”时你可以直接复用同一套clean → FFT → refine流程。这就是这道题最值得带走的资产不是那一个90秒的答案而是一条从原始轨迹到可信结论的标准流水线。本文还有配套的精品资源点击获取
返回列表