资讯详情

资讯详情

公交行驶路径还原与散点图可视化:从站点坐标到真实轨迹的Python实战

1. 从站点坐标到行驶轨迹为什么有数据不等于有路径很多人做公交网络分析第一步就把所有站点往地图上一撒看着密密麻麻的点位觉得挺像那么回事。但只要你稍微放大一点看就会发现一个问题这些点之间是孤立的你根本不知道一辆公交车是怎么从一个站开到下一个站的。站点坐标只告诉你车停在哪而行驶路径才告诉你车怎么走。这两者之间的差距就是一篇分析报告和一张看起来像那么回事的图之间的差距。我一开始也踩过这个坑。手头拿到一份公交站点数据字段很规整线路编号、站点序号、站点名称、经纬度。我兴冲冲地直接用经纬度画了散点图结果发现同一路段上不同线路的站点重叠在一起完全看不出线路的走向。更尴尬的是有些站点在数据里序号是连续的但实际地理位置却隔了好几条街——因为中间那些不停靠但经过的路段数据里根本没有记录。这就是本篇要解决的核心问题如何从已有的公交站点数据出发还原出一条线路真实的行驶路径并把它可视化出来。关键词就三个公交行驶路径、路径插值、散点图可视化。适合已经拿到公交站点数据、想做线路走向分析但卡在点连不成线这一步的人。如果你连数据都还没拿到建议先去看本系列的第一篇那里讲了数据采集和清洗的基本流程。这篇文章我会按实际操作顺序来讲先搞清楚路径数据到底缺了什么再讲怎么补全然后讲怎么把补全后的路径画成一张能看的散点图最后分享几个我在实操中踩过的坑和优化技巧。全程用Python依赖主要是pandas、numpy和matplotlib不需要额外的地图API密钥纯本地就能跑。2. 公交路径数据的三种来源与各自的坑在动手写代码之前得先想清楚一件事你手里的路径数据到底是什么形态不同来源的数据处理方式完全不同。我大致把它们分成三类每一类的坑我都踩过。2.1 只有站点序列没有中间路径点这是最常见的情况。数据表里只有线路编号、站点序号、站点经纬度比如线路序号站名纬度经度1路1起点站30.1234120.56781路2第二站30.1256120.57011路3第三站30.1289120.5734这种数据的优点是干净、规整缺点是站点之间的实际行驶轨迹完全丢失。你只知道车在第二站和第三站都停过但它是走直线过去的还是绕了一个弯数据里没有任何信息。我一开始的想法很简单既然不知道中间怎么走那就直接用直线连呗。结果画出来的图惨不忍睹——明明是一条沿着主干道走的线路被我用直线一连直接穿过了好几个街区看起来像一条空中航线。这种图拿去做分析结论肯定是错的。2.2 有GPS轨迹点但采样频率不稳定第二种情况是你拿到了车辆的GPS轨迹数据理论上这是最理想的因为每个轨迹点都记录了真实位置。但实际数据往往有几个问题采样间隔不均匀有时1秒一个点有时30秒一个点、存在漂移点信号不好时坐标会跳、以及数据量巨大一条线路一天的轨迹可能有几十万个点。我处理过一份轨迹数据采样间隔从3秒到45秒不等。这意味着在采样间隔大的地方两个轨迹点之间可能隔了好几百米直接连线同样会丢失细节。而且漂移点如果不处理画出来的图会有很多毛刺看起来像心电图。2.3 有路径规划接口返回的折线点第三种是调用路径规划服务输入起点和终点返回一条包含多个折线点的路径。这种数据质量最高因为折线点通常比较密集能较好地还原真实道路走向。但问题在于你需要为每一对相邻站点调用一次接口一条线路如果有30个站就要调用29次。如果有多条线路调用量会非常大而且很多服务有频率限制。另外接口返回的路径是最短路径或最快路径不一定等于公交车的实际行驶路径。公交车可能因为停靠、绕行、单行道等原因走的路线和导航推荐的路线不一样。所以这种数据只能作为近似不能当作绝对真值。提示如果你只有站点序列数据又没有条件调用路径规划服务那么基于路网的最短路径插值是最务实的方案。下面我会重点讲这个方法的实现。3. 用路网最短路径补全站点之间的行驶轨迹既然站点之间的路径缺失最直接的办法就是在路网上找到两个站点之间的最短路径用这条路径上的点来填充中间部分。这听起来简单但实际操作中有几个关键决策点。3.1 路网数据从哪来你需要一份路网数据包含节点交叉口和边路段。常见的来源有公开的路网数据集格式通常是Shapefile或GeoJSON。我用的是一份城市级别的路网数据包含约8万个节点和12万条边。如果你拿不到这么细的路网也可以用简化的网格路网代替精度会差一些但基本走向能看出来。路网数据加载进来后需要构建一个图结构。我用的是networkx库把每个节点作为图的一个顶点每条路段作为一条边边的权重设为路段长度。这样后续就可以直接调用最短路径算法。import networkx as nx import geopandas as gpd # 读取路网数据 road_net gpd.read_file(road_network.geojson) # 构建图结构 G nx.Graph() for idx, row in road_net.iterrows(): start_node row[start_node_id] end_node row[end_node_id] length row[length_m] G.add_edge(start_node, end_node, weightlength)这段代码看起来简单但有一个坑路网数据中的节点ID必须和站点数据中的节点ID能对应上。如果站点数据里只有经纬度没有节点ID你就需要先把每个站点匹配到路网上最近的节点。这个匹配过程叫地图匹配是另一个容易出问题的地方。3.2 站点到路网的匹配最近节点不一定对最朴素的做法是对每个站点在路网中找距离最近的节点把它当作站点在路网上的位置。但这样做有个问题如果站点在一条长路段中间最近的节点可能是路段两端的交叉口而不是站点实际所在的位置。这会导致路径的起点和终点偏移画出来的线会有明显的折角。我的做法是先找最近的路段而不是节点然后把站点投影到这条路段上得到一个投影点。这个投影点就是站点在路网上的精确位置。具体实现时可以用shapely库的投影功能from shapely.geometry import Point, LineString from shapely.ops import nearest_points def match_station_to_road(station_point, road_segments): min_dist float(inf) best_projection None for seg in road_segments: line LineString([seg[start_coord], seg[end_coord]]) proj line.interpolate(line.project(station_point)) dist station_point.distance(proj) if dist min_dist: min_dist dist best_projection proj return best_projection这段代码的逻辑是对每条路段把站点投影上去计算投影距离取距离最小的那个投影点。实测下来这样匹配的精度比直接找最近节点高很多路径的起点和终点基本能落在正确的位置上。3.3 最短路径计算与路径点提取匹配完成后就可以对每一对相邻站点计算最短路径了。networkx提供了现成的shortest_path方法直接调用即可def get_path_between_stations(G, start_node, end_node): try: path nx.shortest_path(G, sourcestart_node, targetend_node, weightweight) return path except nx.NetworkXNoPath: return []这里有个细节需要注意如果两个站点之间在路网上不连通shortest_path会抛异常。这种情况在实际数据中并不少见比如站点匹配到了不同的连通分量上。我的处理方式是如果找不到路径就退化为直线连接并在日志里记录这个情况后续人工检查。拿到路径上的节点序列后还需要把节点坐标提取出来形成一条完整的折线。如果两个节点之间的路段是弯曲的还需要在路段上插值否则画出来的线还是折线。我一般会在每条路段上按固定间隔比如10米插值这样路径点足够密集画出来的线看起来是平滑的。3.4 路径点去重与排序多条线路的路径点合并到一起后会有大量重复点比如同一个交叉口被多条线路经过。如果直接画散点图重复点会导致某些位置颜色过深影响视觉效果。我的做法是先按坐标去重保留唯一的位置点然后再画散点图。这样每个位置只画一个点颜色深浅由该位置的线路数量决定。去重的逻辑很简单用pandas的drop_duplicates即可但要注意经纬度是浮点数直接比较可能会有精度问题。我一般会先把经纬度四舍五入到小数点后5位大约1米精度再去重。path_df[lat_round] path_df[lat].round(5) path_df[lon_round] path_df[lon].round(5) path_df_unique path_df.drop_duplicates(subset[lat_round, lon_round])这样处理之后路径点的数量会大幅减少画图速度也会快很多。4. 散点图绘制的参数调优与视觉表达路径点准备好了接下来就是画图。散点图看起来简单但要画出一张能说明问题的图参数调优很关键。我见过太多人直接用默认参数画散点图结果要么点太小看不清要么颜色太乱没有重点。4.1 点的大小与透明度让密集区域透气散点图最常见的问题是在路径密集的区域点会重叠在一起形成一团黑色。解决方法是调整点的大小和透明度。我的经验值是点的大小设为1到3之间透明度设为0.3到0.6之间。这样即使点很密集也能看出层次感。import matplotlib.pyplot as plt fig, ax plt.subplots(figsize(12, 12)) ax.scatter(path_df_unique[lon], path_df_unique[lat], s1.5, alpha0.4, csteelblue, linewidths0) ax.set_aspect(equal) plt.show()这里有个细节set_aspect(equal)必须加否则经纬度的比例尺不一致画出来的图形会变形。我一开始没加这个结果整个城市被拉长了一倍看起来像被压扁的。4.2 按线路着色用颜色区分不同线路如果你想把不同线路区分开可以按线路编号给点上色。但要注意如果线路很多比如超过20条颜色会很难区分。我的做法是只给重点线路着色其他线路用灰色作为背景。这样既能看到整体路网又能突出你关心的线路。# 背景线路用灰色 ax.scatter(other_lines[lon], other_lines[lat], s1, alpha0.2, cgray, linewidths0) # 重点线路用彩色 colors [red, blue, green, orange] for i, line_id in enumerate(focus_lines): line_data path_df_unique[path_df_unique[line_id] line_id] ax.scatter(line_data[lon], line_data[lat], s2, alpha0.6, ccolors[i], labelf线路{line_id}) ax.legend()这种背景前景的画法在实际报告中非常实用。读者一眼就能看到你关注的线路走向同时也能感知到整体路网的密度。4.3 叠加底图让散点图有地理参照纯散点图虽然能看出路径形状但缺少地理参照读者不知道这条线到底在城市哪个位置。叠加底图能解决这个问题。我一般用两种方式一是用matplotlib的imshow叠加一张静态地图图片二是用contextily库加载在线底图。import contextily as ctx ax.scatter(path_df_unique[lon], path_df_unique[lat], s1.5, alpha0.4, csteelblue, linewidths0) ctx.add_basemap(ax, crsEPSG:4326, sourcectx.providers.CartoDB.Positron)注意contextily需要联网加载底图如果网络不稳定可以提前把底图下载到本地用imshow叠加。另外底图的坐标系要和数据的坐标系一致否则会出现偏移。4.4 输出高分辨率图片别让细节丢失最后一步是保存图片。默认的dpi是100对于路径分析图来说太低了放大后点会糊成一片。我一般把dpi设为300图片尺寸设为12x12英寸这样输出的图片既有足够的细节文件大小也不会太大。plt.savefig(bus_path_scatter.png, dpi300, bbox_inchestight)bbox_inchestight能去掉图片周围的白边让图更紧凑。这个参数在写报告时特别有用因为白边会浪费版面空间。5. 实操中遇到的四个典型问题与排查过程上面讲的是顺利情况下的流程。但实际操作中你一定会遇到各种意外。下面我分享四个我踩过的坑以及完整的排查过程。5.1 路径点全部挤在几个交叉口匹配粒度太粗第一次跑完路径插值后我发现画出来的散点图有几个位置点特别密集而其他路段几乎没有点。排查后发现原因是路网数据的节点太稀疏——两个交叉口之间可能隔了500米而我在路段上插值的间隔是10米导致每个路段上插了50个点但这些点都集中在路段内部交叉口位置反而没有点。解决方案在交叉口位置也补充节点。具体做法是在构建图结构时把每个交叉口作为一个节点同时在路段上按固定间隔插入虚拟节点。这样路径点在整个路网上分布更均匀。5.2 最短路径绕了一大圈权重设置不合理有一次我发现某两个相邻站点之间的路径居然绕了整整一圈才到达。排查后发现是因为我把边的权重设为了路段长度但路网数据中有一条边的长度字段是错的实际100米被记录成了10000米导致最短路径算法选择了绕行。解决方案在使用路网数据前先做一次数据质量检查把长度异常比如超过5公里的边过滤掉或者用坐标重新计算长度。这个检查步骤看起来多余但能避免很多莫名其妙的路径问题。5.3 散点图颜色太乱线路数量太多当我尝试把20条线路用不同颜色画在一张图上时结果是一团糟——颜色太多根本分不清哪条是哪条。解决方案改用灰度背景彩色前景的策略只给3到5条重点线路着色其他线路用浅灰色。这样图面立刻清爽了很多。5.4 图片保存后模糊dpi设置太低这个问题最容易被忽略。默认dpi是100在屏幕上看着还行但插入文档后放大就模糊了。解决方案保存时显式设置dpi300并且用bbox_inchestight去掉白边。如果图片还是太大可以适当降低点的大小和透明度而不是降低dpi。6. 从散点图到线路走向图下一步可以怎么走散点图画出来之后其实还有很大的优化空间。如果你想让图更有表现力可以尝试把散点图升级为线路走向图——也就是用不同粗细和颜色的线来表示不同线路而不是用点。这样线路的走向会更清晰但前提是你的路径点足够密集否则线会显得很折。另一个方向是加入时间维度。如果你有不同时段的路径数据可以画一组散点图用颜色表示不同时段这样能看出公交线路在不同时间的运行差异。我试过用这个方法分析早高峰和晚高峰的线路差异效果挺直观的。还有一个实用的扩展是把路径点和站点数据叠加用大点表示站点小点表示路径点。这样一张图里既能看清线路走向又能看到站点分布信息量更大。不过要注意站点和路径点的颜色要区分开否则会混在一起。最后分享一个小技巧如果你觉得matplotlib的画图速度太慢可以改用datashader库。它专门用于大规模散点图的快速渲染几百万个点也能秒出图。我处理全市公交路径数据时用matplotlib要等十几秒换成datashader后基本是瞬间完成。代价是学习曲线稍陡但如果你经常处理大规模地理数据这个投入是值得的。
觉得有用,分享给同行:

为您的企业打造数字门面

稳重轻奢商务风格,端正雅致视觉,长效耐看不易过时。

立即咨询 →