做交通仿真的人基本都经历过这个阶段:路网画好了、交通需求文件配好了、信号配时表也调了好几轮,sumo-gui里车辆跑得欢,结果仿真一结束,面对目录下堆出来的一堆XML文件,人反而懵了。打开文件,密密麻麻全是尖括号,看得脑壳疼,最后只能截一张GUI截图放进报告。这个状态我太熟悉了,因为我也是从这个坑里爬出来的。真正把交通仿真软件SUMO玩明白,恰恰是从仿真结果分析这一步开始的——建模和调参的功夫,都需要在输出数据里兑现成可解释、可比较、可决策的结论。
这篇文章就以SUMO仿真结果分析为主题,围绕“输出文件怎么生成”“每个字段说的是什么”“怎么把XML转成指标和图表”“仿真结论怎么才算可靠”这四件事展开。适合刚跑通第一次仿真、正准备深挖结果数据的读者,也适合已经做了一段时间仿真但总觉得分析环节“差点意思”的朋友。顺便提一句安装相关的事:SUMO官方安装包在官网直接下载就行,Windows版本装完以后sumo和sumo-gui命令会自动进入PATH,Linux发行版一般也自带软件包,装好以后建议先用安装目录里自带的examples练手,别一上来就自己画路网。
1. 用 sumocfg 控制输出:仿真结果文件从哪来、怎么生成
很多人跑完仿真没留下数据,问题就出在启动命令里没写输出参数。sumo-gui打开以后,你看到的车辆运动其实是内存里的实时演算,程序一关,这些数据就全部消失了。想在仿真结束后拿到可分析的结果,必须在仿真开始前,告诉SUMO“我要什么格式的输出、输出到哪个文件”,这一节我们先解决这个前置问题。
1.1 命令行、cfg文件还是脚本:选择适合你的输出配置方式
输出参数有三种常见写法。第一种是直接追加在命令行后面,例如:
sumo -c myproject.sumocfg \ --tripinfo-output tripinfo.xml \ --summary-output summary.xml \ --edge-output edge.xml \ --fcd-output fcd.xml \ --vehroute-output vehroutes.xml \ --emission-output emission.xml这种写法的好处是灵活,临时想看什么就加什么;坏处是不容易复现。我经常做对照实验,上午跑方案A,下午跑方案B,如果每次都要手敲一大串参数,很容易漏掉某一项,最后比较结果时才发现两个方案的数据口径不一样,白跑一趟。
第二种是把参数固化到.sumocfg文件里,这也是我现在的主力用法。在<output>节点里写清楚要哪些输出:
<configuration> <output> <tripinfo-output value="tripinfo.xml"/> <summary-output value="summary.xml"/> <edge-output value="edge.xml"/> <fcd-output value="fcd.xml"/> <vehroute-output value="vehroutes.xml"/> </output> </configuration>这样做的好处是,每个实验方案自成一个项目文件夹,sumo -c 方案名.sumocfg一跑,输出文件的种类、命名、位置全都在配置里写清楚了,复查的时候不会两眼一抹黑。
第三种是写Python脚本用sumo命令行工具批量调用,适合做多轮参数扫描实验(比如同时扫描信号周期和车流规模),脚本里把随机种子、起止时间、输出文件名全部参数化,循环调subprocess运行。这个方法放到后面第四部分讲重复实验时再展开。
1.2 各个输出文件的用途对照表
SUMO的输出选项很多,新手经常面对一堆选项不知道选哪个。我的经验是先按需索取,不要一次全开。输出文件写得越细,磁盘占用越大,分析时处理起来也越慢。下面这张表是我这几年来反复用到的核心输出,按分析目标分好了类:
| 输出选项 | 文件内容 | 典型分析场景 |
|---|---|---|
--tripinfo-output | 每一辆车从出发到抵达的完整记录 | 计算平均行程时间、总延误、排队时间 |
--edge-output | 按时间窗口聚合的路段统计 | 找拥堵路段、评价路网运行状态 |
--summary-output | 每秒/每步的全路网汇总指标 | 看全局车辆数、平均速度、总体延误趋势 |
--fcd-output | 浮动车数据,记录每辆车在每个时刻的位置和速度 | 轨迹回放、精细化路线行为分析 |
--vehroute-output | 车辆实际行驶路径 | 路径选择、绕路行为、OD分配分析 |
--emission-output | 油耗、CO2、污染物排放等数据 | 绿色交通评价、信号优化节能对比 |
--netstate-dump | 每个时间步的路网完整状态 | 需要精确到车道的排队长度与占有率分析 |
这里要提一个很多人忽略的参数:--begin和--end。如果你仿真的总时长是6000秒,但前600秒属于路网加载阶段,统计时想跳过,那么直接把--begin 600传给edge、emission等聚合输出项就行,不用等跑完以后再做数据清洗。我第一次分析时没注意这个点,结果预热阶段的数据混在统计结果里,平均行程时间被拉高了一大截,后面会专门讲这个坑。
2. 带着指标需求读文件:tripinfo、edge、summary 各自能支撑什么分析
有了输出文件,下一步就是读文件。XML格式看着吓人,但只要理解了不同文件里的字段含义,很多分析工作就是“查字段、取数值、做统计”的机械化流程。这一节把最常用的三类文件逐字段过一遍。
2.1 tripinfo:每次出行的“病历本”
tripinfo.xml里一个典型的元素长这样:
<tripinfo id="veh_123" depart="12.00" departLane="edge42_0" departPos="35.20" departSpeed="10.37" arrival="138.50" arrivalLane="edge90_1" arrivalPos="102.10" arrivalSpeed="7.20" duration="126.50" routeLength="2310.40" waitingTime="15.30" timeLoss="38.15" speedFactor="1.05"/>每个<tripinfo>元素就是把一辆车的“一生”摊开了写。depart是出发时刻,arrival是到达时刻,duration是实际行程耗时,routeLength是实际行驶的距离。
这几个字段能做什么?最简单的就是算平均行程速度:routeLength / duration,这个指标比平均瞬时速度更接近驾驶员实际感受到的通行效率。再看waitingTime和timeLoss,waitingTime统计的是车辆由于走走停停、信号灯等原因处于低速状态的总时长,timeLoss则是与“按自由流速度跑完同一段路”相比多花的额外时间。这两个字段经常被拿来做信号优化前后的对比——优化的效果往往不直接体现在总时长上,而是体现在每辆车少等了多少时间。
有一点必须提醒:duration会包含车辆在路网里“动画化”起步后,以默认加速度缓慢起步的时间,所以如果你只对比两个方案的duration差个零点几秒,先不要激动,先确认两个方案的车辆出发分布、车型参数是否完全一致,否则这种微小时差大概率是模型噪声,得不出可靠结论。
2.2 edge 间隔数据:找出拥堵路段的直接工具
--edge-output生成的是按时间段聚合的路段数据。在SUMO里,edge是最小管理单元,一个edge一般代表两个路口之间的一段路,也可以进一步细化到车道。开启输出时可以通过--edge-output begin="600" period="60"这类参数控制间隔切分,比如每60秒输出一段。典型结构如下:
<interval begin="600.00" end="660.00"> <edge id="edge42" traveled="1890.20" density="0.65" avgSpeed="41.30" speedLimit="60"/> </interval>traveled表示该时间段内所有车辆在这个edge上行驶的总距离,density是平均每单位长度上的车辆数,avgSpeed是该间隔内车辆的平均通过速度,speedLimit是这个edge的限速。
要判断一个路段堵不堵,avgSpeed和speedLimit一比就知道:avgSpeed跌到限速的60%以下,基本可以视为出现了拥堵状态;如果density也比较高,说明是因为车辆积压导致的速度下降,而不是个别慢车拉着平均值玩。另外,traveled除以时间段长度可以推出这个edge的总流量,结合平均速度可以用“流量除以路段容量”的饱和度来评估是否有瓶颈风险。
实际做路段缓慢点排查时,我不会一个文件一个文件地翻,而是直接让程序跑一遍所有edge,按时间段把所有avgSpeed低于某个阈值的edge全部列出来,输出成一张“哪段路、哪个时间段、速度为多少”的清单。这种批量筛选写个小脚本就行了,后面会给出实操代码。
2.3 summary:从全局视角看路网是否“健康”
summary.xml记录的是每个仿真步的全路网状态,结构比较扁平:
<step time="120.00" loaded="105" running="82" waiting="31" ended="23" meanTravelTime="56.30" meanSpeed="34.60" meanWaitingTime="4.10" />loaded是累计已出发车辆数,running是当前正在路上的车辆数,waiting是当前因拥堵或信号灯而停车等待的车辆数,ended是已完成行驶的车辆数,meanTravelTime是已经结束行程车辆的平均行程时间,meanSpeed是路网内全部正在运行车辆的平均速度。
summary文件适合看“趋势”。比如信号灯周期调长以后,waiting曲线是不是波动变小了?早高峰车流加载阶段,running是否稳定爬升、没有突然的尖峰?这些宏观趋势判断用summary最快。
但注意,summary里的meanTravelTime是每时每刻对“已经结束行程的车”重新计算的,所以仿真刚开始那一段,只有极少数车跑完,均值波动会非常大。分析时要记住这一点,不要拿着前几十秒的均值对比几百秒后的均值,以为路网性能发生了急剧恶化,其实只是样本数量不同。
3. 从XML到指标与图表:解析脚本怎么写才能不返工
XML文件可以直接看,但数量一多就完全没法人工处理。根本方法其实就那几类:解析XML、提取字段、聚合统计、画图。这一节给出一套可以拿来直接改造使用的Python方案,核心是顺手、稳定、可复现。
3.1 先解析再做统计,别把XML当文本抠
处理XML,xml.etree.ElementTree是标准库,不需要额外安装依赖,处理百万级节点的XML文件也够用。核心用法是ET.parse('xxx.xml')加载文件,再用root.iter()遍历想要的标签。
下面这段代码把tripinfo.xml转成CSV文件,顺便算出几个关键指标的均值,我一般把它存成tripinfo_analyze.py反复用:
import xml.etree.ElementTree as ET import csv tree = ET.parse('tripinfo.xml') root = tree.getroot() rows = [] for trip in root.iter('tripinfo'): rows.append({ 'id': trip.get('id'), 'depart': trip.get('depart'), 'arrival': trip.get('arrival'), 'duration': trip.get('duration'), 'routeLength': trip.get('routeLength'), 'waitingTime': trip.get('waitingTime'), 'timeLoss': trip.get('timeLoss'), 'speedFactor': trip.get('speedFactor') }) with open('tripinfo_cleaned.csv', 'w', newline='', encoding='utf-8-sig') as f: writer = csv.DictWriter(f, fieldnames=rows[0].keys()) writer.writeheader() writer.writerows(rows) # 简单统计 durations = [float(r['duration']) for r in rows if float(r['duration']) > 0] time_loss = [float(r['timeLoss']) for r in rows if float(r['timeLoss']) > 0] if durations: print(f"有效车辆数: {len(durations)}") print(f"平均行程时间: {sum(durations)/len(durations):.2f} s") print(f"平均延误时间: {sum(time_loss)/len(time_loss):.2f} s")这里有个细节我踩过坑:tripinfo元素里可能出现depart和arrival都为空或者duration为0的异常记录,这些通常是在仿真开始前或结束后生成的不完整数据。统计时建议先做一步过滤,比如只保留duration > 0的记录,再去算平均值。
3.2 路段拥堵清单:把edge.xml里的慢速路段抽出来
解析edge-output的思路类似,但要注意嵌套结构:外层是<interval>,里面才是<edge>。我想要找的是“哪个时间段、哪个路段、速度明显低于限速”,所以直接两层循环筛选即可:
import xml.etree.ElementTree as ET tree = ET.parse('edge.xml') root = tree.getroot() THRESHOLD_RATIO = 0.5 # 限速的50%以下视为拥堵 for interval in root.iter('interval'): begin = float(interval.get('begin')) end = float(interval.get('end')) for edge in interval.iter('edge'): avg_speed = edge.get('avgSpeed') speed_limit = edge.get('speedLimit') if avg_speed is None or speed_limit is None: continue avg_speed = float(avg_speed) speed_limit = float(speed_limit) if speed_limit > 0 and avg_speed < speed_limit * THRESHOLD_RATIO: print(f"时间[{begin:.0f}-{end:.0f}] 路段 {edge.get('id')}: " f"平均速度 {avg_speed:.1f} km/h, 限速 {speed_limit:.1f} km/h")跑出来的结果会是一张“拥堵清单”,我在做路网改造方案时,就是靠这张清单找到需要拓宽或者重新配时的路段。
有一个更进阶的用法:给--edge-output加--aggregated-output相关的输出项,或直接用原生的--edge-output加上--period来控制聚合窗口;如果你想看的是高峰期前后15分钟的瞬时状态,把--period改成900即可,窗口越大数据越平滑,窗口越小波动越明显。
3.3 把summary画成折线图,一眼看出全局指标曲线
summary.xml里的数据天生适合画时间序列折线图。用matplotlib画图这段代码很固定:
import xml.etree.ElementTree as ET import matplotlib.pyplot as plt tree = ET.parse('summary.xml') root = tree.getroot() time_series = [] running_series = [] waiting_series = [] for step in root.iter('step'): time_series.append(float(step.get('time'))) running_series.append(float(step.get('running'))) waiting_series.append(float(step.get('waiting'))) fig, ax1 = plt.subplots(figsize=(10, 5)) ax1.plot(time_series, running_series, label='running vehicles', color='blue') ax1.plot(time_series, waiting_series, label='waiting vehicles', color='orange') ax1.set_xlabel('Simulation time (s)') ax1.set_ylabel('Vehicle count') ax1.legend() ax1.grid(True) plt.savefig('network_summary.png', dpi=150)这是最简单的全局曲线。实际工作中,我经常把多个方案的曲线画在同一张图里对比,比如“方案A(现状)”“方案B(信号优化)”的running曲线差异,看起来没有文字表格直观,但一眼能看出哪个方案更早进入平稳态、哪个方案高峰期车辆积压更少。
关于“图表给谁看”有一个经验:如果你是要在技术评审会上向同事汇报,曲线图、散点图都行;如果是要给非交通专业的管理者看,那就尽量把XML转成Excel表格,加上“较现状方案降低了X%”这种结论级描述,而不是把技术分析过程全盘倒出来。
4. 仿真结论才站得住:随机种子、预热期与重复实验的置信度验证
我见过太多人(包括当年的我)跑完一次仿真就拿着一个数字写进报告里:平均行程时间32秒,比现状方案提升15%,然后就没有然后了。这个结论基本站不住脚。交通仿真跟实验科学一样,单次结果是无法代表真实分布的,你必须面对三个核心问题:随机性、预热期、样本量。
4.1 随机种子带来的波动,比想象中大得多
SUMO里的车辆出发时刻、车辆类型分配、路径选择决策等环节都可能引入随机性,影响方式通过--seed参数控制。如果你从没有设置过seed,每次仿真用的可能是随机种子,那两次跑出来的结果细节可能完全不同。
我自己做过一次印象很深的小实验:同一个路网、同样的需求流量,只改种子从1到5,平均行程时间最高和最低之间差了将近9%。这个波动幅度足以左右方案优劣的判断——如果优化方案只比现状好5%,那在随机波动面前,这个“优化”可能根本不存在。
所以,凡是涉及“对比评价”的分析,建议至少跑5次不同种子的仿真,把结果整理成“平均值±标准偏差”的形式再下结论。如果方案差异远大于种子间的波动幅度,这个结论才是可靠的。
实际执行起来,可以用脚本批量跑并自动收集结果:
for seed in 1 2 3 4 5; do sumo -c myproject.sumocfg --seed $seed \ --tripinfo-output tripinfo_seed${seed}.xml \ --summary-output summary_seed${seed}.xml done批量跑完以后,再用第三部分的解析脚本逐文件统计,把所有种子的均值合成一张汇总表。
4.2 预热期不清除,平均数据直接失真
所有基于车辆加载型仿真(demand-driven)的模型,都存在“预热期”的问题。仿真一开始,路网是空的,车辆从起点渐渐驶入,前几百秒甚至半小时,路上车辆数远小于设计通行能力,这时的行程时间、平均速度都过于乐观。如果你把预热期里跑出来的“流畅数据”混进统计,最终均值会被人为拉低,得出比真实运行状态好太多的结论。
解决方法的正确姿势不是在结果文件里做“事后筛选”,而是在仿真阶段就用参数控制统计窗口。SUMO里,像--edge-output、--emission-output、--summary-output这类聚合输出,都可以直接指定统计区间:
sumo -c myproject.sumocfg \ --begin 1200 --end 7200 \ --edge-output edge_busy.xml \ --summary-output summary_busy.xml这里--begin 1200表示从第1200秒开始记录edge统计,前面的预热段直接不输出。对于tripinfo这类全程记录的文件,可以在分析脚本里过滤掉arrival < 1200的车,因为那些车在预热期就已经到达了,不反映稳定期路网状态。
预热期该设多长?没有硬性标准,建议先跑一次仿真,看一眼summary.xml里的running字段——等到“正在运行车辆数”曲线已经进入稳定的上下波动范围,而不是持续单调上升的时候,就说明可以开始统计了。我一般设定为仿真时长的前20%为预热,如果路网规模大、加载慢,要适当延长。
4.3 不用“单次均值”,用“分布和置信区间”表达结论
当你做了多个种子的重复实验,怎么把结论呈现出来?一个直观的办法是计算置信区间。继续用第三部分的tripinfo解析脚本,把5个种子的平均行程时间收集齐:
import statistics avg_durations = [35.2, 33.8, 36.1, 34.5, 35.9] # 5个种子,每个种子算出的平均行程时间 mean = statistics.mean(avg_durations) stdev = statistics.stdev(avg_durations) n = len(avg_durations) # 95% 置信区间近似计算 ci_low = mean - 1.96 * stdev / (n ** 0.5) ci_high = mean + 1.96 * stdev / (n ** 0.5) print(f"平均行程时间: {mean:.2f} s, 95% 置信区间: [{ci_low:.2f}, {ci_high:.2f}]")这个结论表达出来就是:方案A的平均行程时间为35.1秒,95%置信区间为[33.9, 36.3]秒;方案B为31.2秒,置信区间为[30.1, 32.3]秒。两段区间完全不重叠,那方案B的提升是可靠的;如果区间有大量重叠,基本可以断定差距是随机噪声造成的。
最后,识别阈值也要有依据。比如前面示例里拥堵阈值设成限速的50%,不是拍脑袋拍的——我做城市干道评价时习惯参考《城市道路工程设计规范》里对“畅通、缓行、拥堵”等级的速度区间划分,不同等级道路、不同功能定位,阈值应该不同。仿真结论要可信,分析口径就要提前明确定义,并且整个系列实验过程中不随意改动。
说实话,仿真结果分析是SUMO工作流里最容易被低估的一环。跑仿真谁都会,但能把几万个XML节点变成“拥堵出现的时刻与成因”“方案A比方案B可靠快多少”“随机波动之外的规律是什么”,这些才是真正考验积累的地方。我的建议是:不要急着把脚本写得多花哨,先把输出配置固定下来,把字段含义吃透,把重复实验的框架搭起来,之后再逐步细化分析维度。等你习惯用“多轮实验+置信区间+追因分析”的方式看待每一次仿真,那个时候你再回头读输出文件,看到的就不再是尖括号,而是一套完整、可复用的证据链了。