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

资讯详情

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

从Excel经纬度到SHP:合肥公交矢量数据清洗与建库实践

从Excel经纬度到SHP:合肥公交矢量数据清洗与建库实践 简介2020年合肥公交矢量数据集涵盖189条公交线路和4700余个公交站点线路属性包括名称、起止点、首末班时间、上下行方向和总里程站点具备WGS1984经纬度坐标及途经线路信息可直接用于城市公共交通空间分析、GIS制图与站点选址规划。压缩包共12个文件其中shp保存几何图形dbf存储属性字段prj记录坐标系shx辅助索引整体仅828KB适合在ArcGIS、QGIS中直接打开编辑。该数据现已吸引249人学习尤其适合GIS开发者、城市规划人员和交通研究者。借助线路与站点的关联字段可进行公交网络覆盖率评估、最短路径查询、站点密度聚类分析也能结合年份对比观察线路演变为公交线网优化与出行服务改进提供量化支撑。这些数据还可作为城市交通教学与科研的基础样本。1. 为什么一份2020合肥公交矢量数据最后会变成“站点对不上线路”2020合肥公交矢量数据在城市GIS、交通规划和Web可视化项目里被反复下载和使用。它通常包含两个主体公交线路层的线路名称、起止点、首末班时间、方向以及公交站点层的站点名称、经度、纬度。表面上数据字段已经写清楚实际打开往往是一张几百行的时间表、一张几万行的站点明细表甚至只是若干段没有顺序的经纬度文本。问题几乎都出在数据组织方式而不是数据本身站点是离散点线路是顺序连线二者靠“线路名称方向”关联任何一个字段缺失或写法不一致都会出现线不经过站点、站点顺序乱跳、同一线路分出多条悬空线的情况。这篇文章想讲的就是拿到这样一份原始数据后怎样从字段建模、坐标系选择、脚本处理到最终输出SHP把Excel经纬度转换成一套线路与站点能对上、可以进入分析流程的公交矢量数据集。适合正在处理城市公交数据、需要做线网配图和站点覆盖分析的GIS工程师。2. 线路表与站点表合肥公交矢量数据的字段拆解与建模2.1 点线分离两张表解决几何维度不同的问题公交GIS数据的第一个原则是“点线分离”。公交线路在几何上是一条LineString公交站点是一个Point它们共享线路名称但属性维度和使用场景完全不同。如果放在同一张表格里连续堆叠ArcMap和QGIS导出时很容易把路线当成点属性、把站点当成线属性最后一步生成符号系统时彻底失控。我一般会先把数据拆成两张明细表线路明细表和站点明细表。线路表建议保留这样一组字段字段名类型说明line_id文本线路编码建议用数字或拼音缩写不要只用中文名line_name文本公交线路名称如“1路”“B3线”direction文本方向编码统一为“去行/回行”或“1/2”start_stop文本起点站名end_stop文本终点站名first_time文本首班时间统一写“06:00”last_time文本末班时间统一写“21:30”valid_from日期数据生效日期站点表的字段则要精简一些字段名类型说明line_id文本与线路表关联的线路编码direction文本与线路表保持一致的方向编码seq整数从1开始的经停顺序station_name文本站点名称lon浮点经度十进制度lat浮点纬度十进制度为何线路表里不写经纬度如果一条线路有30个站点把经纬度循环写入线路表数据会膨胀到无法维护而且SHP文件的DBF属性表对字段长度也有限制。保留lon和lat只在站点表是公交矢量数据最常见的组织方式后续生成点图层、线图层、做坐标反查都方便。2.2 线路名称加方向才是稳定的关联键公交数据最容易出错的地方是关联键。只用“线路名称”作为键会遇到一个问题同一线路在不同年份可能调整起止点比如2020年合肥公交某条线路因道路改造临时改道起点站从A变到B但站名没变。此时线路表和站点表按名称关联会查出两套不同几何数据。稳妥做法是用“line_id direction”组成复合键。具体做法是给每条行驶方向分配一个稳定的ID如“1路”去行叫“01A”回行叫“01B”。站点表里的每个站点也带上同样的复合键。这样同一条线路出现临时调整时只需要新增一组line_id不影响旧线路的历史查询。处理过全国村界矢量数据的人应该有同感边界不闭合或权属交叉时光靠名称字段根本兜不住必须引入区域编码作为稳定标识公交数据的“边界不闭合”等价问题就是“站点顺序断裂”。2.3 经纬度字段先做范围检查再做类型检查从Excel导入前要对经纬度做两层检查。第一层是取值范围经度应在116.5到117.6之间纬度在31.5到32.1之间。超出这个范围的记录大概率来自手工录入错误或坐标格式混淆。第二层是数据类型很多从网页复制下来的表格经纬度可能是文本甚至带空格和中文括号直接读取会被识别为NULL。我常用的处理方式是在Excel里新增两列用公式清洗E1: VALUE(SUBSTITUTE(TRIM(D2), , .))这里D2是原始经度文本先把中文逗号替换成点再去空格并转成数值。如果Excel报错说明原始数据里有非数字字符需要单独筛出来人工核对。经纬度字段保持在十进制度同时不要混入度分秒格式否则导入ArcMap后坐标系计算会完全错位。3. 用ArcMap和QGIS将Excel经纬度生成SHP文件导入参数与脚本方案3.1 在ArcMap里显示XY数据并导出为SHP把Excel经纬度导入ArcMap并生成SHP文件是这组数据最常用的入口。操作路径是在ArcMap中打开属性表选择“文件 → 添加数据 → 添加XY数据”。关键在参数设置X字段选经度lonY字段选纬度lat。这是新手最容易颠倒的地方经纬度不是数学坐标里的x和y但在ArcMap的字段映射中经度对应X纬度对应Y。坐标系选择“WGS 1984”或“CGCS2000”。2020合肥公交矢量数据如果没有特殊说明默认采用WGS84经纬度。不要选投影坐标系比如UTM或高斯投影否则点会被当作米制单位放大地图直接飘到几千公里外。添加完成后生成的是临时事件图层右下角会显示“Events”。必须右键图层 → “数据” → “导出数据”选择输出要素类保存为点SHP。只停留在事件图层状态关闭ArcMap再打开就会丢失要素这是很多人在导出时卡住的主要原因。如果在属性表里看到经纬度都是数值但图上点全部集中到同一个位置优先检查坐标系是否误选了投影坐标系其次检查经纬度是否被Excel自动改成了科学计数法并丢失小数精度。3.2 用QGIS读取CSV再按线路字段生成点层和线层没有ArcMap许可时QGIS是更轻量的替代方案。操作步骤为先把表格另存为CSV文件编码选择UTF-8在QGIS中选择“图层 → 添加图层 → 添加分隔文本图层”文件格式选CSV几何类型选“点”X字段设为经度Y字段设为纬度几何坐标参考系选“EPSG:4326”。这样得到的是站点点层还需要把同一线路、同一方向下的站点按经停顺序连接成线。这里我一般直接用PyQGIS脚本没必要手动在“点集转线”工具里反复试参数。脚本如下import csv from collections import OrderedDict from qgis.core import ( QgsVectorLayer, QgsProject, QgsFeature, QgsGeometry, QgsPointXY, QgsField ) # 读取CSV字段顺序为 line_id,direction,seq,station_name,lon,lat csv_path rD:/hefei_bus/stops.csv with open(csv_path, r, encodingutf-8) as f: rows list(csv.DictReader(f)) # 站点点层每个站点生成一个Point要素 points_layer QgsVectorLayer(Point?crsEPSG:4326, bus_stops, memory) points_provider points_layer.dataProvider() points_provider.addAttributes([QgsField(line_name, 10), QgsField(seq, 4)]) points_layer.updateFields() for row in rows: feat QgsFeature(points_layer.fields()) feat.setGeometry(QgsGeometry.fromPointXY( QgsPointXY(float(row[lon]), float(row[lat])) )) feat.setAttribute(line_name, row[line_name]) feat.setAttribute(seq, int(row[seq])) points_provider.addFeature(feat) QgsProject.instance().addMapLayer(points_layer) # 线路线层按 line_id direction 分组组内按 seq 排序连成线 grouped OrderedDict() for row in rows: key (row[line_id], row[direction]) grouped.setdefault(key, []).append(row) lines_layer QgsVectorLayer(LineString?crsEPSG:4326, bus_lines, memory) lines_provider lines_layer.dataProvider() lines_provider.addAttributes([QgsField(line_id, 10), QgsField(direction, 10)]) lines_layer.updateFields() for (line_id, direction), stop_rows in grouped.items(): stop_rows.sort(keylambda r: int(r[seq])) geom QgsGeometry.fromPolylineXY([ QgsPointXY(float(r[lon]), float(r[lat])) for r in stop_rows ]) feat QgsFeature(lines_layer.fields()) feat.setGeometry(geom) feat.setAttribute(line_id, line_id) feat.setAttribute(direction, direction) lines_provider.addFeature(feat) QgsProject.instance().addMapLayer(lines_layer)这段脚本的逻辑是先建立一个临时点层再按“line_id direction”分组组内按seq字段从小到大排序最后用一条LineString把同一个方向的所有站点串起来。参数上需要重点确认的是seq字段。seq是整数且从1开始如果CSV里这部分没有顺序信息脚本生成的线就会在原始站点间直接乱连看起来像一大团杂乱折线这时要回到Excel数据源补全经停顺序而不是在脚本里修。EPSG:4326表示WGS84经纬度坐标系代码里同步指定这样加载到QGIS便不会再出现图层坐标系不明的问题。3.3 逗号、编码和表头三个反复出现的导入参数坑处理这类公交数据的CSV时有三个反复出现的坑。第一是CSV文件用逗号做分隔符但站点名称里恰好有中文逗号或英文逗号。一定要给文本字段加上双引号导出否则QGIS会把一个站点名拆成两列。第二是编码问题。国内公交台账常常由老系统导出默认编码是GBK直接导入QGIS会出现中文乱码属性表里线路名变成“B3绂”。建议在代码打开文件时尝试encodinggbk如果报错则改成encodingutf-8。第三是表头名称匹配问题。PyQGIS脚本读取的是字段名的英文名比如line_name、lon、lat如果原表表头写的是“线路名称”“经度”“纬度”脚本会因为没有对应字段而报KeyError。可以先用Python的csv.DictReader打印一行字段名确认后再跑建层逻辑不要盲改字段名。4. 方向字段、起止点与首末班时间公交线网的派生字段处理4.1 方向字段的真实含义与推导方法公交数据里的“方向”字段在不同来源语境下含义差异极大。有些数据提供方记录为“上行/下行”有些记录为“0/1”有些直接记为“东行/西行”。对线路分析而言最重要不是方向的中文叫法而是它是否稳定地区分了往返两条轨迹。2020合肥公交矢量数据中如果原始数据没有明确方向字段我一般用起止点推导把每条线路的第一个站点和最后一个站点取出来与线路表的start_stop和end_stop对比匹配上的一组为去行另一组为回行。具体判断逻辑可以用简单SQL实现SELECT line_id, MAX(CASE WHEN seq 1 THEN station_name END) AS first_stop, MAX(CASE WHEN seq (SELECT MAX(seq) FROM stops s2 WHERE s2.line_id stops.line_id) THEN station_name END) AS last_stop FROM stops GROUP BY line_id;这里用两个聚合条件分别查出每个line_id下的始发站和终点站再与线路表里的start_stop、end_stop比较。如果发现某个line_id的first_stop完全匹配start_stop说明方向正序匹配end_stop则说明方向反序。对于无法匹配的情况说明原始站点表里缺少部分站点或起终点的叫法不一致需要回到上游清洗数据。4.2 首末班时间的排序问题与统一格式首班时间、停运时间这类字段是典型的“看似简单、实际全是字符”的数据。从网页或PDF台账提取出来后经常出现“6:00”“06:00”“600”混杂的情况。直接用文本排序会出现“06:30”排在“6:30”后面的乱象因此我习惯导入时统一转成分钟数并添加一列time_value hour * 60 minute在SQL里可以做如下转换后再参与排序SELECT line_id, direction, MIN(CAST(substr(first_time, 1, 2) AS INTEGER) * 60 CAST(substr(first_time, 4, 2) AS INTEGER)) AS first_minutes, MAX(CAST(substr(last_time, 1, 2) AS INTEGER) * 60 CAST(substr(last_time, 4, 2) AS INTEGER)) AS last_minutes FROM bus_lines GROUP BY line_id, direction;使用substr的前提是时间统一为“HH:mm”五位字符串。如果出现“6:00”这样的短格式先补零为“06:00”再做截取。把时间转成纯数字分钟数还有两个额外好处一是可以和前一天末班车跨天情况做加减判断二是生成GTFS数据时可以直接映射到stop_times中的时间列。4.3 起止点不是看文字而是看几何端点线路表的起止点字段常被当作普通文本处理但公交矢量数据最终要落到几何上。我习惯在生成线层后把每条线路的几何首尾坐标抓出来与start_stop、end_stop做一次空间贴近校验。这里用Shapely写一个小脚本from shapely.geometry import Point, LineString line LineString([(117.278, 31.773), (117.284, 31.778), (117.291, 31.784), (117.299, 31.790)]) start_pt Point(line.coords[0]) end_pt Point(line.coords[-1]) # 和站点表做最邻近匹配0.005度约等于500米 for station in stations: if station.geometry.distance(start_pt) 0.005: print(匹配起点, station.name) if station.geometry.distance(end_pt) 0.005: print(匹配终点, station.name)0.005度的阈值对城市公交比较合适能容忍站点坐标采集时的轻微漂移又不会把相邻车站误匹配进同一端点。如果发现某个线路的几何终点和end_stop相距超过数公里常见原因有两种一是站点顺序表中seq错乱二是Excel里经纬度填反。处理这类问题不要人工逐条排查先把所有匹配失败的线路输出成CSV再集中修正。5. 数据复核清单与一次成型的SHP处理技巧5.1 用六个检查点快速判断SHP是否可用线路与站点都进入SHP后先不要急着做可达性分析花几分钟过一遍检查清单。每项都是可以直接操作的验证步骤线层中每条线路的要素数是否等于“线路数 × 方向数”。如果某条线路只有一个方向多半是线路表里direction字段有NULL。站点表里的线路总数和线路表里是否存在包含关系。正常的站点数量大约是线路数量乘以单线经停站点数相差一个数量级时需要复查关联字段。打开线层属性表确认没有MultiLineString类型的要素。公交线路每方向只允许一条LineString类型为MultiLineString说明数据中混入了不可连续的子段用“多部件转单部件”工具拆分后检查断裂处。计算每个要素的长度过一个城市尺度量级参考。合肥主城区一条公交线路的长度通常在4到20公里之间如果站点坐标错误导致线段跨过数百公里则直接回到坐标范围检查。站点与线层空间位置匹配。在ArcGIS里选中线层要素用“按位置选择”检查是否有超过90%的站点落在缓冲区外。首末班时间字段做一次非空验证。公交“停运时间”为空会直接导致后续所有时间序列计算失败。5.2 把2020合肥公交矢量数据接成GTFS结构如果项目目标是做公交线路查询或行程规划SHP加属性表只是中间形态。更常见的做法是把线路表和站点表转换为GTFS的stops.txt、trips.txt和stop_times.txt三个文件。转换时的核心规则是点序号seq直接映射stop_times的stop_sequence首末班时间字段映射到arrival_time和departure_time。GTFS的时间必须是“HH:MM:SS”格式因此前面统一成分钟数的字段在这一步要重新格式化为字符串。SHP文件中的站点名称字段对应stops.txt的stop_name经纬度对应stop_lat和stop_lon。转换时要注意GTFS中stop_id不能用中文需要把line_id或站点编码映射成纯数字ID。5.3 保留数据快照让下一次更新不再返工2020合肥公交矢量数据的价值不只是静态存档它还是线网分析的历史基准。我会在生成SHP后复制三份副本分别命名为“原始导入”“已清洗”“验证通过”。“原始导入”保持Excel原样绝不修改“已清洗”存放字段类型修复后的中间数据“验证通过”才是进入地图和模型分析的正式数据。后续某个时间点拿到新版本公交数据时只需要写一个脚本把新CSV跑到同一套点线生成流程中再和“验证通过”的版本做站点差异对比就能快速定位到变化线路不需要再从杂乱表格逐行查看哪个站名改了。这套流程的核心不是某个高深算法而是把站点顺序、线路方向、时间格式这三个最容易出问题的环节全部前置到导入阶段处理干净。只要字段建模和验证步骤固定成脚本无论下一份数据来自哪个城市都能在较短时间内输出一套可以直接投入分析的标准公交线网。本文还有配套的精品资源点击获取
返回列表