仿真跑完的那一刻往往是Vadere使用体验里最让人犯嘀咕的环节方案A和方案B明明只改了一个参数可output目录里多出来的那一堆文件到底哪些才是能写进报告、支撑结论的证据Vadere的输出结果解读一直是被问得最多、也最容易踩坑的部分。这篇内容想从实际使用的角度出发把轨迹文件、测量区域数据、图形输出这几类最常见的结果捋一遍再聊聊数据异常时怎么从结果倒推原因以及怎样用Python在Vadere之外做二次分析。刚接触人群仿真的学生、需要把仿真结果整理成分析报告的工程师应该都能从中找到直接能用的东西。1. 先搞清楚Vadere输出到底生成了哪些文件1.1 输出目录是怎么生成的Vadere的默认做法是每次运行模拟后把结果写到场景文件所在目录下的output子目录里。这个路径与场景文件位置绑定换一台机器、换一个目录跑同样的场景输出路径也会跟着变。所以拿到一批结果的第一件事不是急着打开数据而是先确认output里到底有哪些文件、这些文件是哪一次运行产生的。有人会把多个方案反复跑在同一个目录里结果文件混在一起后期根本分不清哪个对应哪个场景。不同版本的Vadere在子目录命名上略有区别。我见过有的版本会为每次运行生成带时间戳的子目录有的则直接把文件覆盖写在output根目录。时间戳目录的好处是多次运行不会互相覆盖但用脚本批量处理时目录遍历逻辑要写对。更稳妥的做法是在每个场景文件里自定义输出文件名前缀跑完立即把整个output目录归档改名避免后续不必要的覆盖。1.2 按类型认识五类常见输出先建立一个整体印象Vadere的输出大体可以分成下面五类输出类型常见文件形式内容概要主要用途轨迹数据txt / csv每个行人在每个输出时刻的位置原始数据几乎所有分析的基础测量区域数据csv指定多边形或线区域内的密度、速度、流量量化某条通道或出口的人群状态标量数据csv / txt全局统计量如疏散总时间、实时人数汇总宏观指标图像快照PNG序列行人位置、密度热力图的可视化帧展示、汇报、定性判断日志文件log运行信息、警告、错误堆栈排查异常和记录运行条件轨迹文件是最底层的原始输出精度最高但加工量也最大测量区域数据属于已经算好的统计量直接可用但要搞懂统计口径标量数据适合做整体对比PNG序列适合肉眼观察日志文件则经常在出了问题之后才被想起来建议运行结束顺手扫一眼。1.3 开始解读前必做的三件事解读任何输出之前我会要求自己先完成三件事这三件事能省掉后面大半的返工。第一步回到场景配置里记录时间步长和输出频率。第二步打开数据文件的头部看注释行和列名确认分隔符。第三步挑一小段数据做手算验证比如取相邻两行轨迹计算速度是否和预期量级一致。很多人会跳过第三步。举一个实际的例子仿真时间步长是0.01秒输出频率设置为每20步输出一次那么轨迹数据里相邻行的时间差是0.2秒。如果在计算速度时误把0.2秒当成0.01秒速度结果会被放大20倍。这种低级错误在报告复核里非常致命提前用手算验证一条速度曲线就可以完全避免。2. 轨迹文件最原始也是最金贵的数据2.1 轨迹文件每一列的含义轨迹数据保存了每个行人在每个输出时刻的位置是所有二次分析的原材料。打开文件后先看表头和注释行。最常见的结构是四列第一列是仿真时间单位秒第二列是行人编号通常从0或1开始第三列和第四列是行人在全局坐标系下的X、Y坐标单位米。一个简化版的轨迹文件片段长这样# time pedestrianId x y 0.00 1 5.0 3.0 0.01 1 5.02 3.01 0.02 1 5.04 3.02 0.03 1 5.06 3.03较新的Vadere版本会在轨迹里附加更多字段比如Z坐标、瞬时速度分量、行人状态标签等。不管列怎么增加前四列的含义基本稳定所以脚本处理时最好按列名读取而不是按位置索引硬编码否则版本一升级脚本就报废。还有一点容易忽略行人编号不一定连续也不一定从1开始。每个行人在被创建时获得一个ID到达出口被移除后这个ID不会再出现。因此按行人分组统计时要使用groupby这就是“分组统计”的命脉同时统计每个ID的存活时长区分“在场景里走完全程”和“中途被移除”两种情况。2.2 用轨迹还原速度与密度的基本算法拿到轨迹后速度是最容易计算的指标。对同一个行人ID把相邻两帧的位置差除以时间差就得到这段时间内的平均速度。写成公式就是[ v_i(t) \frac{\sqrt{(x_{t\Delta t} - x_t)^2 (y_{t\Delta t} - y_t)^2}}{\Delta t} ]这里的时间差 (\Delta t) 是输出间隔不是仿真步长。代码实现很简单import pandas as pd df pd.read_csv(output/trajectories.txt, sepr\s, comment#, names[time, pedestrianId, x, y]) df df.sort_values([pedestrianId, time]) df[dx] df.groupby(pedestrianId)[x].diff() df[dy] df.groupby(pedestrianId)[y].diff() df[dt] df.groupby(pedestrianId)[time].diff() df[speed] (df[dx] ** 2 df[dy] ** 2).pow(0.5) / df[dt]实际算出来的速度曲线通常带有噪声。原因是仿真中行人会产生瞬时加减速和避让抖动差分又放大了这些波动。想要平滑曲线可以先对坐标做窗口平均再求速度或者直接用Vadere测量区域处理器输出的速度统计量。密度的计算比速度复杂。最直观的是网格法把场景划分成较小的格子例如0.5米乘0.5米统计每个格子内的人数再除以格子面积要扣除墙和障碍物占用的面积得到局部密度。这种方法的优点是直观、可复现缺点是格子大小会影响结果边界格子的面积修正也需要处理。更平滑的是核密度法把每个行人看作一个高斯“影响源”向周围扩散后累加。Vadere内部的核密度输出通常采用这类思路但在具体参数上不同版本有差异所以报告里写清楚“密度是怎么算出来的”比写密度的数值更重要。2.3 轨迹坐标的坑单位、原点与旋转轨迹坐标的单位是米这一点几乎不会变但原点和方向就未必了。原点不一定在场景左下角可能会因为底图坐标而有偏移测量区域数据使用同一套全局坐标所以坐标原点偏移本身不是问题。真正容易踩坑的是场景带旋转的情况。当一个大型场景被整体旋转后轨迹输出的是旋转后的模型坐标而你的分析底图可能是未旋转的CAD平面图直接把轨迹画在底图上会出现方向错位。解决方法是先确认场景配置里的全局变换参数把所有坐标统一换算到同一个参考系再做可视化。另一个常被问到的方向问题是Y轴。GUI里一般Y轴向上但输出文件里的坐标方向以场景属性面板为准。判断方法很简单找一个已知坐标的固定障碍物比较它的轨迹坐标和场景面板显示是否一致。不要默认“坐标一定是正的”实测确认后心里才有底。3. 测量区域输出密度、速度、流量到底怎么算的3.1 测量区域文件长什么样测量区域是你在GUI里画出来的几何体用来框定“我想知道这里的人群状态”。Vadere通过输出处理器将测量区域内的统计量写入CSV文件。比较常见的文件结构包含时间步、测量区域名称或ID、测量值等列。处理器不同生成的列也不同有的版本会把每个区域单独输出一个文件有的版本则把所有区域的数据放在同一个文件里。拿到测量区域CSV后先别急着画折线图。把文件加载到脚本里检查每个区域的数据行数、时间轴是否完整。两个相邻测量区域的数据长度未必相同因为行人进入和离开区域的时间不一致统计起止时刻自然也不同。做跨区域比较时必须先把时间轴对齐否则Excel里直接插入折线图会错得离谱。3.2 三种统计口径的物理含义密度是最直观的指标定义为测量区域内当前人数除以区域面积单位是人每平方米。简单密度只考虑总面积不考虑行人分布是否均匀核密度会因行人是否扎堆在区域边缘而给出不同结果。用于瓶颈分析时核密度通常更能反映真实拥挤程度。速度的定义则有讲究这里要区分两种平均。第一种是区域内所有行人瞬时速度的算术平均容易实现但和高密度下的通行效率关系较弱第二种是流率加权平均按通过测量区域的“流量权重”进行平均更接近“单位时间里通过多少个人、这些人的平均速度如何”。Vadere的手册会区分这两类定义做疏散分析时如果目标是评估通道通行效率优先使用第二种口径。指标单位物理含义常见误区密度人/m²区域内人数与面积的比值把简单密度当成核密度速度m/s区域内行人的平均速度混淆算术平均与流率加权平均流量人/(m·s)单位时间、单位宽度通过的人数测量线长度不一致导致对比失效流量定义为单位时间穿过测量边界的人数并按测量线宽度归一化所以单位是人每米每秒。实际使用时要特别注意线状测量区域的长度会直接影响流量数值不同场景对比时尽量用等长测量线否则流量曲线差异会被测量线长度污染。3.3 从测量区域数据画佛利德曼图佛利德曼图是行人动力学研究里最常见的基础图表横轴常用密度纵轴用速度或流量。从测量区域CSV里取密度列和速度列直接画散点图即可。如果要看流量曲线就按“流量密度×速度”计算但要注意这个推导出来的流量要与实际测得的流量保持一致否则两张图表在报告里会自相矛盾。画完图之后检查曲线形状。低密度时速度应该接近自由速度密度增大到临界值后速度逐渐下降流量曲线呈现先升后降的倒U形。如果看到低密度下速度也很低或者高密度下速度突然回升多半是数据里混入了启动阶段或边界效应。先把开头的瞬态数据剔除再把明显异常的点过滤掉曲线会干净很多。4. 图形输出与可视化结果怎么看4.1 PNG序列图的时间步对应关系Vadere能把仿真过程输出为PNG序列每一帧对应一个输出时间点。文件名里的数字表示“第几帧”而不是“第几秒”。举一个实际的例子仿真步长0.01秒输出频率设为每50步输出一帧那么第20帧对应的实际时间就是10秒。如果误把帧号当成秒对“人群在第几秒到达出口”的判断就会完全出错。为了准确对应帧号和时间最简单的办法是同时打开日志或轨迹文件找到帧号对应的时间戳。更省事的做法是在配置阶段让输出处理器在命名模板里携带时间信息某些版本支持这种自定义。我个人的习惯是先用GUI播放功能定位关键时间点再导出对应PNG帧而不是一次性导出几百张图然后一张张翻。4.2 热力图的数值含义热力图本质是密度场的可视化颜色条上的数字就是密度值。读图时第一件事不是看颜色而是看色标范围。同样是一张黄色图如果色标上限是8人每平方米场景已经非常拥挤如果上限只有1人每平方米黄色区域只是正常行走。发布图片时如果省略色标热度图的说服力会大打折扣。第二件事是看热点出现的位置。出口附近容易形成半圆形的高密度堆积这是瓶颈的典型特征如果高密度区域出现在场景中部、周围没有障碍物就要考虑是不是初始化分布不合理或者人群自组织形成了局部拥堵。结合轨迹数据回放能很快判断属于哪种情况。4.3 可视化结果用于报告与展示的经验跨场景对比时务必统一色标和视角。两个方案如果色标不同哪怕第二个方案实际上更拥挤图片上也可能显得更清爽。我的习惯是先把两个方案的密度场标准化到同一个密度区间再生成对比图。还要注意导出图片的分辨率。Vadere默认输出的PNG分辨率可能在放大后出现明显锯齿图中的文字标注也会模糊。可以把画布尺寸调大再导出或者导出轨迹后用外部绘图工具重绘。报告里的每张图最好配一行简短的时间说明例如“疏散开始后第30秒”让审阅者快速理解这张图描述的是哪个时刻的状态。5. 输出结果异常时的排查思路从结果倒推原因5.1 为什么测量区域文件是空的如果你配置了测量区域处理器运行后却得到空文件或全0数据先别怀疑软件出了问题。第一件事是回放轨迹确认行人是否真的穿过了你画的测量区域。很多人把区域画在障碍物后方或被墙体完全挡住的位置行人绕路走自然不会有数据。第二件事是检查处理器配置。输出处理器的配置界面通常有一个下拉框让你选择要对哪个测量区域输出。如果那里保持默认的“无”处理器就不会生成数据。另一种情况是区域形状与处理器不匹配例如想测流量却画了面状区域或者想测密度却选了线状区域。每个处理器有适配的区域类型界面往往有提示对不上时输出自然为空。日志文件在这里非常有价值。运行结束后打开log文件搜索测量区域或处理器相关关键字看有没有警告信息。某些版本在多边形自交、顶点顺序异常等边界情况下会给出警告这些在GUI里肉眼根本看不出来只能靠日志捕捉。5.2 轨迹文件为空或行人数不对轨迹文件为空常见原因有三种。第一种是场景里行人加载源配置成了0仿真启动了但没有任何人出现。第二种是模拟运行了极短时间就被中断轨迹文件只写了开头几行。第三种是输出处理器确实存在但没有被场景运行流程启用这种情况往往伴随日志中的配置警告。行人数变少则通常是正常现象。很多模型规定行人到达出口后被移除模拟后期轨迹里的行人数下降说明大部分人群已经疏散完毕。如果目标是统计疏散总时间应该把“最后一名行人到达出口的时间”作为结束时刻而不是等轨迹文件里完全没有任何行人——因为最后一个行人被移除的瞬间轨迹文件就已经停止写入。5.3 数据跳变与尖峰怎么处理轨迹数据里偶尔会出现一个行人从位置A瞬间跳到很远的B这是碰撞处理或行人步行模型中的局部避让造成的并不代表真实速度。直接用原始轨迹差分时这种跳变会生成一根很高的速度尖峰。处理方法是先对速度做中值滤波或滑动平均或者直接设定一个距离阈值例如2米超过阈值的位移标记为异常记录并剔除。还要检查输出频率是否过低。假设仿真步长是0.01秒行人一步最多移动零点几米但如果输出频率是每100步一次你会看到行人每一秒位移几米甚至十几米差分算出的速度完全不包含中间的真实过程。这种情况不是数据坏了而是观测频率不够。把输出频率加密后重新运行尖峰通常会自行缓解。如果加密后尖峰仍然存在再去检查模型参数里的速度噪声范围是否被设置得过大。6. 用Python对Vadere输出做二次分析的实用套路6.1 加载轨迹文件的代码骨架轨迹文件本质上是普通文本用标准库就能读取。麻烦的是不同版本的分隔符和注释方式不同。最稳妥的做法是先用文本编辑器打开看一眼再用pandas按实际格式读取。典型的加载写法如下import pandas as pd df pd.read_csv( output/trajectories.txt, sepr\s, # 如果列间是空格 comment#, # 跳过注释行 names[time, pedestrianId, x, y], enginepython )如果文件是逗号或分号分隔把sep参数换成,或者;即可列名则根据真实表头调整。读取完成后先打印df.head()和后几行确认时间列是否递增、坐标范围是否合理。这一步成本极低但能避免后续所有基于错误数据Frame的分析白做。6.2 批量统计多个run的数据做参数对比时常常需要跑几十个run。写一个脚本遍历输出目录提取每个run的统计指标并汇总成一张表是效率最高的做法。核心思路是先定义一个函数输入目录路径返回需要的指标字典再循环调用并汇总。import pathlib import pandas as pd def summarize_run(dir_path: pathlib.Path) - dict: traj_file dir_path / trajectories.txt df pd.read_csv(traj_file, sepr\s, comment#, names[time, pedestrianId, x, y]) evacuation_time df.time.max() max_speed df.groupby(pedestrianId)[time].count().max() return { evacuation_time: evacuation_time, max_timesteps_per_ped: max_speed, } rows [] for p in pathlib.Path(output).glob(run_*): rows.append({run: p.name, **summarize_run(p)}) summary pd.DataFrame(rows)批量处理之前务必手工确认两个run的文件结构完全一致尤其是列名和输出频率。如果两个run的输出频率不同直接比较它们导出的“总时间”就会失真。建议在汇总表里额外记录一个“输出间隔”字段需要精确时间时用它做修正。6.3 快速生成密度热力图的脚本思路即便没有配置Vadere自带的密度输出也可以自己用轨迹数据画热力图。思路很简单把场景划分成网格遍历所有轨迹点统计每个网格里的记录次数最后除以网格面积和总时间帧数得到时间平均密度场。import numpy as np import matplotlib.pyplot as plt grid_size 0.5 # 米 x_min, y_min df[[x, y]].min() x_max, y_max df[[x, y]].max() nx int((x_max - x_min) / grid_size) 1 ny int((y_max - y_min) / grid_size) 1 grid np.zeros((ny, nx)) for _, row in df.iterrows(): ix int((row.x - x_min) / grid_size) iy int((row.y - y_min) / grid_size) if 0 ix nx and 0 iy ny: grid[iy, ix] 1 grid / df.time.nunique() * grid_size * grid_size plt.imshow(grid, originlower, cmaphot) plt.colorbar(labelpeople / m2) plt.show()这段代码只是骨架实际使用时要注意两点。第一行人进入场景前和离开后网格会少统计一段时间平均密度会被轻微低估建议只用仿真中间的时间窗口做平均。第二网格法依赖格子大小改一个数字出来的热点位置可能看起来不一样对定量结论要有保留。快速查看定性趋势时网格法够用但发布正式结果时还是建议使用测量区域处理器或核密度算法。实际用过Vadere一段时间的人都会有同感这个工具的输出从来不是为“让你一眼看懂”设计的而是为“让你可信地量化”设计的。所以我自己的体会是解读输出结果前先想清楚这一次仿真想回答什么问题——是疏散时间、瓶颈位置还是人群密度峰值。问题一旦明确要读哪个文件、滤掉哪些时间、用什么口径统计都会变得清晰。还有一个值得坚持的小习惯每次运行前把场景配置复制一份放进output目录和结果放在一起。将来写报告时不需要为确认某个参数而到处翻找原始场景文件这一份配置就是全部答案的来源。
阅读完成 · 觉得有帮助?