成渝城市群矢量数据实战:shp体检、POI空间连接与坐标处理
简介资源聚焦成渝城市群空间数据应用场景面向地理信息系统、城市规划与科研项目中需要学校、医院等POI及道路、建筑轮廓等基础矢量数据的开发者和研究人员。数据年限为2019年POI信息经网络地图爬取并完成导出、裁剪等处理统一采用WGS84椭球投影建筑、铁路与道路数据则源自OpenStreetMap同样经过爬取与裁剪覆盖建筑轮廓、铁路和道路三类要素。压缩包内共155个文件以shp、dbf、shx、prj等矢量数据格式为主其中shp用于空间几何信息dbf保存属性字段prj定义坐标参考整体包体约40.3MB可直接导入ArcGIS或QGIS等工具使用。已有1191人学习下载资源包含医院、幼儿园、中小学、高校、专科医院等细分POI层级以及建筑物和路网数据适合作为城市群分析、设施可达性评估与制图底图等项目的原始数据支撑。1. 成渝城市群矢量数据底图、路网、POI 一次给全做城市群分析时最耗时间的永远是拼数据底图。我最早做成渝城市群基础设施评估时行政边界、道路、建筑轮廓、POI 分了好几个来源下载结果坐标系对不上、路网点位跑到边界外光修数据就修了三天。这套成渝城市群矢量数据把四类图层打包成了 shp市/区县边界、道路、建筑轮廓、POI 数据齐全下载后直接丢进 ArcGIS 或 QGIS 就能用。对做空间分析、选址评估、城市群可视化的人尤其省事——你不需要再到处找底图和路网也不用自己拼接多源数据。做 WebGIS 开发的人也可以拿它当测试数据验证加载性能、跑空间查询都够用。2. 数据体检先行坐标系、图层结构与属性表预检任何 shp 拿到手第一件事都不是急着做分析而是先体检。坐标系错位、字段乱码、缺文件是 shp 数据最常见的三座大山。这一章我把拆包后的检查流程写清楚跟着走一遍就能避掉八成低级问题。2.1 拆包先看图层与配套文件一个完整的 shapefile 不是单个.shp文件而是一组配套文件.shp存几何、.dbf存属性、.shx存索引还有可选的.prj存坐标系、.cpg存编码。下载解压后如果发现.dbf或.prj缺失这个数据基本不能直接信任——几何和属性对不上坐标系也成了黑匣子。我在命令行里常用 Python 快速列一下包里的内容from pathlib import Path data_dir Path(./chengyu_shp) for shp in data_dir.rglob(*.shp): # 统计配套文件是否齐全 shx shp.with_suffix(.shx).exists() dbf shp.with_suffix(.dbf).exists() prj shp.with_suffix(.prj).exists() print(f{shp.name} | shx{shx} dbf{dbf} prj{prj})shx是几何索引少了它很多 GIS 软件打不开dbf保存属性字段少了它图层只剩几何prj记录坐标系少了它后面对接必翻车。我一般的习惯是拆包后先跑这段脚本缺文件的先补补不了的就换数据源。这一步不花时间但能省下后面好几个小时的排查时间。2.2 坐标系先统一再叠加是硬前提成渝城市群这套数据里POI 点位大概率是经纬度坐标WGS84 或 CGCS2000而行政区边界如果做过投影坐标值就会变成以米为单位的平面坐标。这两类数据直接叠加点位会跑到完全无关的位置而且很难通过肉眼察觉——因为范围看起来是“大致对得上”的细看才发现全都偏了。用 Python 检查坐标系是最直接的方式import geopandas as gpd poi gpd.read_file(chengyu_poi.shp, encodingutf-8) boundary gpd.read_file(chengyu_boundary.shp, encodingutf-8) print(POI 坐标系:, poi.crs) print(边界坐标系:, boundary.crs)常见坐标系就三种坐标系EPSG 代码单位适用场景WGS84 经纬度EPSG:4326度全球定位、GPS 采集CGCS2000 经纬度EPSG:4490度国内测绘成果Web MercatorEPSG:3857米Web 地图显示如果边界是用 CGCS2000 投影坐标系做的而 POI 是 WGS84 经纬度叠加前一定要统一。我一般统一到 CGCS2000 的投影坐标系做空间计算出图展示时再转 Web Mercator。注意boundary自带的.prj文件里写的是什么就用什么不要凭文件名猜。2.3 属性表预检编码、字段与空值率shp 的属性表存在.dbf文件里编码问题是最常见的坑。国内很多数据用 GBK 或 CP936 编码保存而geopandas默认按 UTF-8 读结果就是 POI 名称显示成乱码。先读一下字段结构再决定后续处理gdf gpd.read_file(chengyu_poi.shp, encodingutf-8) print(gdf.columns.tolist()) print(gdf.head(3)) print(gdf.isnull().sum())encodingutf-8是显式指定 dbf 的读取编码如果你的数据显示乱码换成encodinggbk再试一次。字段列表决定你能做什么分析——比如 POI 图层有没有“大类/小类”字段边界图层有没有“市/区县”名称字段这直接影响后面的空间挂接和统计。空值率则告诉你数据质量如果一个字段空了大半那统计类分析基本别指望它。属性表这步特别值得认真看。字段名规范、编码正确、空值可控的图层后面每一步都顺畅反之你在半路发现字段不对回头改编码再读一遍等于所有下游分析全部重跑。3. 把 POI 挂到区县边界空间连接与统计输出POI 数据的核心价值是“落到空间里”。单独看一个 POI 点位没有意义把它挂到行政区边界里统计每个区县有多少设施、什么类型占主导这才是城市群分析的基本功。这一章从清洗、空间连接到按区县统计给出完整可复现的操作。3.1 清洗 POI去重、补空、归一类别原始 POI 数据里最常见的问题是重复和空字段。同一个门店可能被采集了两条记录或者“名称”字段有值但“类别”字段空着。不洗直接用统计结果会虚高。我的做法分三步先按“名称地址经纬度”去重再处理空值最后把类别做归一。import pandas as pd import geopandas as gpd from shapely.geometry import Point # 读原始 POI 表可能是 Excel 导出 poi_df pd.read_excel(poi_raw.xlsx) # 去重名称地址完全一致则保留第一条 poi_df poi_df.drop_duplicates(subset[名称, 地址, 经度, 纬度]) # 空值处理没有类别的补“未知” poi_df[类别] poi_df[类别].fillna(未知) # 把 DataFrame 转成 GeoDataFrame poi_gdf gpd.GeoDataFrame( poi_df, geometry[Point(x, y) for x, y in zip(poi_df[经度], poi_df[纬度])], crsEPSG:4326 )Point(x, y)的经纬度顺序别搞反——x是经度、y是纬度。crsEPSG:4326是显式指定 WGS84 经纬度但如果你的原始坐标用的是 GCJ-02 火星坐标这里指定 EPSG:4326 会造成几百米偏移属于坐标系的另一个故事了。去重逻辑里经纬度完全一致才能判定为同一点坐标差一位小数点都不建议合并宁可保留两条也不误删真实存在的不同门店。3.2 空间连接sjoin 让点位落在边界内POI 坐标是经纬度点区县边界是多边形。要把点位挂到所在的区县核心操作是空间连接。做过一次就知道这个操作的本质是“对每一个点找到包含它的那个多边形”性能和数据量直接挂钩——边界图层有几万个面时全量相交会慢到让人怀疑电脑当机。boundary gpd.read_file(chengyu_boundary.shp, encodingutf-8) # 统一坐标系再做连接 boundary boundary.to_crs(EPSG:4326) # 空间连接 joined gpd.sjoin( poi_gdf, boundary, howinner, predicatewithin )两个参数值得展开说。predicatewithin要求点必须完全落在面内落在边界线上或者面外的点都会被丢弃如果你希望保留所有点、没有匹配到的标记为 NaN就用howleft。我的习惯是先跑一次howinner看看命中率如果丢掉的点超过 10%多半是坐标偏移或边界图层缺少辖区范围先排查再继续。joined结果里会带上边界图层的所有字段——尤其是区县名称字段。这时候你可以直接验证一个点位是否挂对了地方随便抽几条记录检查“名称”和“区县名”是否合理匹配。这一步是手动的但非常必要别把空间连接当作可以盲信的黑匣子。3.3 按区县统计并导出制图连接完成之后按区县分组统计是顺手的事result joined.groupby([区县名]).size().reset_index(namepoi_count) result result.sort_values(poi_count, ascendingFalse) result.to_csv(poi_count_by_district.csv, indexFalse, encodingutf-8-sig) print(result.head(10))size()统计每个区县有多少个 POI 点。encodingutf-8-sig值得单独说明带 BOM 的 UTF-8 编码Excel 直接双击打开才不乱码。用默认的utf-8写出来Excel 打开中文列名全是乱码——这是最容易“在最后一步翻车”的地方。导出的 CSV 可以拖进 Tableau 或者直接在地图软件里做专题图。如果要按类别细分把groupby的字段改成[区县名, 类别]就能得到每个区县不同 POI 类别的计数。再往后你可以用这些统计结果做设施密度分析、人均设施覆盖评估或者叠加道路数据做可达性计算——POI 挂接这一步做完下游分析的路就通了。4. 道路与建筑轮廓二次加工拓扑修复、裁剪与简化行政区边界、POI 这些数据拿来就能用的情况比较多道路和建筑轮廓则基本都需要二次加工。原因很简单一套打包好的 shp 数据覆盖范围大直接加载进工程文件会卡顿拓扑错误也多。真实项目里几乎不会全量使用而是按区域裁剪再局部处理。4.1 道路拓扑悬挂点与伪节点排查道路网络的拓扑错误主要分两类悬挂点道路没连到其他道路、变成断头和伪节点一段完整道路被无意义地打断成多段。这些错误在做网络分析时是致命的——最短路径计算可能因为断头路直接算不出结果。在 QGIS 里跑 GRASS 的清理工具是最经济的方式v.clean inputchengyu_road outputchengyu_road_clean \ toolrmdupl,break,rmdangle \ threshold0.01rmdupl删除重复线段break在交叉处打断道路rmdangle删除短于阈值的悬挂线。threshold0.01的单位取决于数据坐标系——如果数据是经纬度0.01 度约等于 1 公里这个阈值会误删大量短线如果你处理的是平面坐标系这个值通常设置在 1~5 米比较合理。我的血泪经验是先确认坐标系单位再设阈值否则清理完发现路网缺了一大块后悔药都没得吃。清理完成不等于万事大吉。我一直保留一个习惯清理后随机抽几条道路手动检查连通性。工具只能处理规则错误路网逻辑上的断裂它看不见。4.2 按区县边界裁剪出你要的片区成渝城市群范围很大全量加载道路和建筑数据会拖慢分析效率。大部分项目只需要其中的几个区县这时裁剪比筛选字段更可靠——裁剪直接按空间范围切片。import geopandas as gpd road gpd.read_file(chengyu_road_clean.shp, encodingutf-8) boundary gpd.read_file(chengyu_boundary.shp, encodingutf-8) # 选出你关心的区县 target boundary[boundary[区县名].isin([渝中区, 锦江区])] # 按目标区县裁剪道路 road_clip gpd.clip(road, target)执行裁剪前必须确认两个图层的坐标系完全一致否则clip的结果可能是一堆空数据或者残缺几何。target直接用了边界图层里的区县名做筛选如果字段名不叫“区县名”要改成实际字段名。裁剪完成后检查一下road_clip的要素数量如果比预期少太多去看是不是坐标系不一致导致的空间范围错位。4.3 建筑轮廓简化精度换顺畅建筑轮廓通常细节极多一个城市可能有几十万个面。直接整个加载到地图里卡顿是必然的。取舍之道是先投影、后简化、再出图。# 先投影到平面坐标系得到米的单位 building building.to_crs(EPSG:3857) # 简化几何容差 5 米保留拓扑结构 building_simple building.simplify(tolerance5, preserve_topologyTrue)tolerance5的含义是简化后的几何与原始几何的最大偏差不超过 5 米。值设得越大、文件越小、细节丢失越多。在经纬度坐标系下直接做simplify是常见误用——单位是度tolerance5代表 5 度基本等于把建筑轮廓画成一个点。先投影成米制坐标再简化这个顺序不能反。preserve_topologyTrue也很关键它保证简化过程不会产生自相交、镂空等拓扑错误。设为False性能更快但可能出现几何错误。对建筑轮廓这种密集面数据我一般优先保拓扑速度慢一点没关系。5. 常见问题与排查坐标错位、属性乱码与拓扑破洞shp 数据用多了翻车场景来来回回就那么几个。这一章把高频问题按“现象 → 原因 → 解决”梳理出来你可以直接对照排查。5.1 坐标错位数据“飘”出边界外现象POI 点叠到区县边界图层上点位整体偏向一侧几百米有些点直接落在边界外。原因最常见的两种情况。一是 POI 用了 GCJ-02 火星坐标而边界是 WGS84 或 CGCS2000两者差几百米二是两个图层的坐标系没有统一直接把经纬度和投影坐标叠加了。解决先看两个图层的crs是否一致。GCJ-02 的问题没有完美的数学转换常见做法是用公开的纠偏算法转成 WGS84 再做分析。坐标系不统一的话用to_crs()统一到同一个 EPSG 代码。我拿到新数据的第一反应永远是先打印crs这已经成了肌肉记忆。5.2 属性表中文乱码读出来全是“口口”现象POI 名称、区县名等中文字段在 QGIS 里显示正常但用 Python 读取后全是“口口”或奇怪符号。原因dbf 文件的编码是 GBK而你读取时用了 UTF-8。国内很多 shp 数据在制作时没有写.cpg文件软件只能靠猜。解决# 优先尝试 GBK gdf gpd.read_file(chengyu_poi.shp, encodinggbk)如果还是乱码就逐个试gb18030、utf-8。定下来之后顺手把数据转成 GeoJSON 保存一份GeoJSON 默认 UTF-8 编码以后不会再出问题。5.3 拓扑破洞裁剪结果缺块现象用边界裁剪道路或建筑时结果里有些区域明显缺失或者生成的面要素中间多了个空洞。原因输入几何存在自相交、重合点等拓扑错误裁剪工具遇到这种错误直接跳过或生成错误结果。解决裁剪前先做一次修复# buffer(0) 是最常用的拓扑修复方式 boundary_clean boundary.buffer(0) road_clip gpd.clip(road, boundary_clean)buffer(0)会重建几何的边界关系消除自相交和重叠。处理后的数据可以再跑一次is_valid检查print(boundary_clean.is_valid.all())输出True说明拓扑没有问题再去裁剪就稳了。5.4 中文路径导致的“找不到文件”现象代码和文件都没问题但read_file报错找不到文件或者 ArcGIS 直接打不开图层。原因部分 GIS 组件和 Python 库对中文路径支持不友好尤其是历史版本的 ArcGIS 工具箱。路径带“重庆”“成都”这类中文字符时底层 C 组件读不了。解决把整套 shp 数据复制到纯英文路径下处理比如D:\gisdata\chengyu\。这个操作很玄学但确实是最稳的解法。项目交付时我会刻意要求所有数据路径只用英文和数字从源头断掉这个坑。6. 按需导出shp 转 WKT、GeoJSON 与 3DTilesshp 是 GIS 桌面端的通用格式但换到 Web 端、数据库或者三维场景里就需要转格式了。常见需求是把 shp 导出成 WKT 文本给 PostGIS 用或者转 GeoJSON 给前端加载。我一般按用途选转换方式——文本交换用 WKTWeb 可视化用 GeoJSON三维场景用 3DTiles。把 shp 转成 WKT 可以直接拿属性加几何拼文本gdf gpd.read_file(chengyu_poi.shp, encodingutf-8) with open(poi.wkt, w, encodingutf-8) as f: for _, row in gdf.iterrows(): f.write(f{row.geometry.wkt}\t{row[名称]}\n)每行一个点几何和名称用制表符分隔。row.geometry.wkt是 Shapely 提供的 WKT 字符串输出导入 PostgreSQL 的 PostGIS 时可以直接用ST_GeomFromText读取。转 GeoJSON 更省事gdf.to_file(chengyu_poi.geojson, driverGeoJSON)GeoJSON 自带坐标系信息Web 端用 MapLibre 或 Leaflet 加载非常顺畅。三维场景里把 shp 转 3DTiles 是另一类需求——我通常先把数据按渔网分割成小块再单独转换避免一次转换整个城市导致生成的文件过大加载崩溃。工具上可以选支持批处理的转换器转换前确认源数据是 WGS84因为 3DTiles 的标准坐标系就是 WGS84。从那以后我每次导出 shp 之前都强制自己先确认三件事坐标系是什么、编码是什么、目标格式要什么。这三件事确认完基本不会再出现导出后数据对不上的情况。希望帮到你。本文还有配套的精品资源点击获取