ARTICLE DETAIL

资讯详情

深耕网站建设、视觉设计与SEO优化的一线实战洞察。

江苏GIS底图实战:三级行政区划与路网矢量包处理

江苏GIS底图实战:三级行政区划与路网矢量包处理

简介:面向GIS空间分析与制图学习者的江苏省基础地理数据包,聚焦省、地市、区县三级行政区划边界及道路网、铁路网矢量数据,可用于区域统计、路网可达性分析、专题图制作和教学演示,适合具备基础GIS操作能力的高校师生与从业者。压缩包共33个文件、约41.72MB,以SHP矢量数据为主体,配套DBF属性表、SHX索引、PRJ坐标系定义及XML元数据等,便于在ArcGIS、QGIS等软件中直接加载、查询与二次处理。已有1973人学习下载,具备一定参考热度。数据覆盖层级较完整,行政区划与交通网络可配合使用,既能支撑省域尺度的空间格局分析,也便于拆分到市县单元开展制图与统计,为课程作业、论文前期数据准备和项目预研提供现成底图与路网基础。

1. 省级 GIS 底图怎么选:一份 2022.07 的江苏矢量包能省掉多少返工

做江苏相关的地理分析,最耗时的往往不是模型,而是底图。省界、市界、县界要能套上,道路网和公路网要能分级渲染,坐标系还得跟业务数据对得上。很多人的做法是临时从不同渠道拼数据:行政区划找一份、路网再找一份,图层叠上去发现边界对不齐、属性字段互不认识,光对齐就耗掉半天。这次拆的这份 2022.07 版江苏矢量包,把行政区划(省、市、县三级)、道路网和公路网放进同一套空间参考里,属于能直接进项目的底图素材。适合做区域规划、交通可达性分析、专题制图的从业者,也适合刚接触 GIS 数据整理、想弄清楚矢量图层怎么组织的新手。下面按「先看清里面有什么,再动手跑通,最后说清坑在哪」展开。

2. 拆开矢量包:三级行政区划与道路网的字段结构

2.1 图层分组与几何类型判断

拿到一个 SHP 包,先别急着往地图里拖。SHP 格式的老规矩是「一个图层一组文件」:.shp存几何、.shx存索引、.dbf存属性表、.prj存坐标系定义,少一个都可能打不开或者在别的软件里丢失坐标信息。江苏省行政区划加路网这类数据,常见组织方式是按要素类别分目录,省、市、县各一层,道路网、铁路网(公路网常与道路网并列存在)各一层。几何类型要提前看清楚:行政区划一般是面(Polygon),道路网、公路网是线(PolyLine),层级属性(省/市/县)落在.dbf的字段里,而不是靠文件名区分。

动手第一步是用命令行把结构摸清,不必打开笨重的桌面软件:

# 列出包内所有 shp 及配套文件,确认没有缺失 .prj find ./jiangsu_2022 -type f \( -name "*.shp" -o -name "*.shx" -o -name "*.dbf" -o -name "*.prj" -o -name "*.cpg" \) | sort # 用 ogrinfo 查看每个图层的几何类型、要素数和坐标系 for f in $(find ./jiangsu_2022 -name "*.shp"); do echo "=== $f ===" ogrinfo -so "$f" "$(basename "$f" .shp)" done

ogrinfo -so只输出摘要(summary only),不加载全部要素,速度快。重点看三行:Geometry是 Polygon 还是 Line String、Feature Count判断数据规模、Layer SRS WKT里的 EPSG 编号。江苏省域矢量底图在国内项目里常见 CGCS2000 或 WGS84,前者 EPSG:4490,后者 EPSG:4326;如果.prj缺失或写成自定义字符串,后面叠图必然错位。我一般会额外确认.cpg文件是否存在,它决定.dbf的中文字段名和属性值按什么编码解析,缺了它,中文地名在部分软件里直接变乱码,这是一个非常高频的翻车点。

2.2 属性表字段:靠什么区分省、市、县

三级行政区划塞在同一批图层里,靠的是属性字段而非几何大小。常见字段包括行政区代码、名称、层级标识、上级代码。做筛选统计时,行政区代码是最稳的键——名称可能重名(同名乡镇在多地存在),代码不会。道路网和公路网则在字段上体现等级:高速、国道、省道、县道、乡道,或者用技术等级、车道数、设计速度等属性描述。把这些字段先读出来,后面的分级渲染和空间连接才有依据。

import geopandas as gpd # 读取行政区划面图层与道路网线图层 admin = gpd.read_file("./jiangsu_2022/admin_county.shp", encoding="utf-8") roads = gpd.read_file("./jiangsu_2022/road_network.shp", encoding="utf-8") print("行政区字段:", list(admin.columns)) print("路网字段:", list(roads.columns)) print("坐标系:", admin.crs, "|", roads.crs) # 按层级字段统计要素数量,核对省市县三级是否齐全 if "level" in admin.columns: print(admin["level"].value_counts()) # 道路按等级统计,检查分级字段取值是否规范 if "road_class" in roads.columns: print(roads["road_class"].value_counts().head(10)) # 关键一步:两个图层坐标系必须一致,否则后续空间连接全是空值 assert admin.crs == roads.crs, "行政区与路网坐标系不一致,先统一再分析"

这段脚本的价值在最后一行断言。很多人跳过坐标检查直接做sjoin,结果是所有道路都匹配不上任何行政区,排查半天以为是字段名写错,其实是投影不同。字段名在不同数据源里叫法五花八门,NAME、name、名称都可能出现,用list(columns)先打印再写后续代码,比凭记忆硬写靠谱。另外encoding="utf-8"只是起点,若读出乱码要换gbk或gb18030试,这取决于原始数据的生产环境,没有统一答案,只能试。

2.3 省、市、县三级怎么拆成独立图层

有些包把三级放在一个面图层里用字段区分,有些直接给三个文件。两种都要会处理。字段区分的情况,按层级筛选导出即可;独立文件的情况,重点核对县界拼接后是否严丝合缝覆盖省界,这是检验数据完整性的土办法。下面把混合图层拆开,并做一次面积与覆盖度自检:

import geopandas as gpd admin = gpd.read_file("./jiangsu_2022/admin_all.shp", encoding="utf-8") # 按层级字段拆分三级,字段名以实际打印结果为准 for lv, fname in [("省", "province"), ("市", "city"), ("县", "county")]: sub = admin[admin["level"] == lv] sub.to_file(f"./out/{fname}.shp", encoding="utf-8") print(lv, "要素数:", len(sub), "总面积(km²):", round(sub.to_crs(4527).area.sum() / 1e6, 2)) # 覆盖度自检:县级合并后的总面积应与省级接近 county = gpd.read_file("./out/county.shp") province = gpd.read_file("./out/province.shp") gap = abs(county.to_crs(4527).area.sum() - province.to_crs(4527).area.sum()) print("县合并与省面积差(km²):", round(gap / 1e6, 2))

to_crs(4527)是 CGCS2000 的 3 度带投影坐标,单位是米,算面积必须先投到等面积或投影坐标系,直接在经纬度下算面积会得到毫无意义的平方度。面积差在几十平方公里量级属于正常(边界简化、飞地处理差异),差到几百以上就要怀疑县界有缺口或重复。拆分后单独存盘,一是方便按层级配图,二是避免每次分析都加载全量数据。

3. 把行政区划和路网叠起来:空间连接与可达性统计

3.1 统一坐标系:先定投影再谈分析

所有空间统计的前提是坐标系统一。经纬度坐标(EPSG:4326)适合展示和存储,不适合算长度和面积;做路网密度、可达性这类计算,必须先投影到米制坐标。江苏位于东经 116° 至 121° 之间,CGCS2000 3 度带对应 EPSG:4526(中央经线 117°)和 EPSG:4527(中央经线 120°),跨带数据要么按带分别处理,要么用省级统一的投影参数。我一般把全部图层投到同一个投影再往下走,宁可多一步转换,也不要让后续结果带系统性偏差。

import geopandas as gpd TARGET_CRS = "EPSG:4527" # CGCS2000 / 3-degree Gauss-Kruger CM 120E admin = gpd.read_file("./out/county.shp").to_crs(TARGET_CRS) roads = gpd.read_file("./jiangsu_2022/road_network.shp").to_crs(TARGET_CRS) # 路网长度统计,单位换算成公里 roads["length_km"] = roads.length / 1000 print("路网总里程(km):", round(roads["length_km"].sum(), 1)) # 各等级道路里程汇总,用于判断等级字段是否可用 if "road_class" in roads.columns: print(roads.groupby("road_class")["length_km"].sum().sort_values(ascending=False).head())

roads.length在投影坐标系下返回米,除以 1000 得公里。如果忘记投影,这一步算出的「长度」没有任何物理意义,后续路网密度会整体偏大或偏小,而且偏差随纬度变化,不是常数,很难事后修正。分组汇总那几行不只是看数字,更是校验等级字段:如果某个「等级」的里程异常巨大,很可能里面混了重复要素或把整个面要素的边界当成了线。

3.2 按区县汇总路网密度:sjoin 的正确用法

路网密度是最常见的落地指标,口径是「辖区内道路总里程 ÷ 辖区面积」。做法是先做空间连接把每条道路归属到区县,再分组求和。这里有个容易忽略的点:跨界的道路会被按几何关系拆分或整体归到某一个区县,取决于predicate参数。用intersects会把跨界道路同时算进相邻区县,里程有重复;更严谨的做法是先用overlay做线面相交,把跨界道路在边界处切断,再统计。

import geopandas as gpd TARGET_CRS = "EPSG:4527" county = gpd.read_file("./out/county.shp").to_crs(TARGET_CRS) roads = gpd.read_file("./jiangsu_2022/road_network.shp").to_crs(TARGET_CRS) # 正确做法:线面相交把跨界道路切断,避免同一段路被重复计入 roads_clip = gpd.overlay(roads, county[["name", "geometry"]], how="intersection") roads_clip["length_km"] = roads_clip.length / 1000 # 按区县汇总里程,再除面积得密度 stat = roads_clip.groupby("name")["length_km"].sum().reset_index() county["area_km2"] = county.area / 1e6 stat = stat.merge(county[["name", "area_km2"]], on="name", how="left") stat["density_km_per_km2"] = stat["length_km"] / stat["area_km2"] print(stat.sort_values("density_km_per_km2", ascending=False).head(10))

overlay(how="intersection")会把道路在区县边界处切开,每段只归属一个区县,里程不重不漏,这是密度统计的推荐路径。代价是计算量比sjoin大,数据量大时先按研究区裁剪再 overlay。合并统计结果时用how="left",保证没有道路经过的区县也保留在结果里,否则密度表会悄悄少几行,做图时才发现有的区县是空白。另外面积用投影后的area计算,1e6是把平方米转成平方公里,这一步漏掉的话密度值会差六个数量级。

3.3 分级渲染与专题出图的基本参数

数据算完要出图,分级渲染的关键是分级方法选对。路网密度这类右偏分布,等间距分级会让大部分区县挤在最低一档,图面几乎看不出差异;分位数分级每档数量均衡,但可能把数值接近的区县分到不同档。常用折中是自然断点(Jenks),它让组内方差最小、组间差异最大,适合专题图。下面的代码用 matplotlib 出一张基础密度图:

import matplotlib.pyplot as plt from matplotlib import rcParams rcParams["font.sans-serif"] = ["SimHei"] # 中文字体,缺失时换本机可用字体 rcParams["axes.unicode_minus"] = False fig, ax = plt.subplots(figsize=(10, 8)) # scheme="natural_breaks" 自然断点分级,k 为分级数 stat_gdf = county.merge(stat, on="name") stat_gdf.plot( column="density_km_per_km2", cmap="YlOrRd", scheme="natural_breaks", k=5, legend=True, edgecolor="white", linewidth=0.6, ax=ax, legend_kwds={"title": "路网密度 (km/km²)", "loc": "lower right"}, ) ax.set_title("江苏省区县路网密度分布(2022.07 底图)") ax.set_axis_off() plt.tight_layout() plt.savefig("./out/density_map.png", dpi=300)

k=5是分级数,太多图例看不清,太少抹平差异,五级是国内专题图的常见选择。edgecolor="white"加细描边能让相邻区县边界清晰可辨,不加的话同色块会糊成一片。图例标题必须写清单位和口径,否则读者无法判断这个密度是公路网还是含城市道路的全部路网。出图这一步最容易被轻视,但一张分级方法错误、图例缺失的图,在评审场合基本等于白做。

4. 避坑清单:坐标系、编码与拓扑的五条血泪经验

4.1 图层叠不上:十有八九是坐标系假一致

现象:行政区划和路网拖进同一视图,一个在正确位置,另一个跑到几千公里外或者非洲西海岸附近。原因:两个图层的.prj声明的坐标系不同,或者其中一个.prj是错的(抄了别的数据的定义文件),软件按声明「正确地」渲染到了错误位置。解决:不要信.prj,用ogrinfo -so或 GeoPandas 的.crs实际读出坐标定义,再用to_crs统一到同一个目标投影。若怀疑.prj本身有误,先用gpd.read_file(..., ignore_geometry=False)读几何范围,对照研究区经纬度范围判断,确认无误后再决定是否重写.prj。

4.2 中文地名变乱码:.cpg缺失或缺字段编码声明

现象:属性表里的城市名、区县名显示为问号或方块。原因:.dbf本质是旧式数据库格式,编码不写在文件头里,依赖.cpg声明;.cpg缺失时不同软件按不同默认编码解析,Windows 系软件常用 GBK,跨平台工具默认 UTF-8。解决:读取时显式指定encoding="gb18030"试一遍,能正常显示就说明原数据是 GBK 系;确认编码后再写一个.cpg文件进去,内容写对应的编码名,后续任何人打开都稳定。属性值乱码还能靠重新指定编码救回来,字段名乱码往往不可逆,所以拿到数据第一件事就是打开属性表瞄一眼。

4.3 路网密度虚高:跨界道路被重复计数

现象:相邻两个区县的路网密度都异常高,加起来超过全省总里程。原因:用sjoin(predicate="intersects")做归属,一条跨界道路同时命中两个区县,各自把整条路的长度计入。解决:改用overlay做线面相交切断,或者在对sjoin结果按道路唯一 ID 去重后再统计,只保留归属关系最长的那个区县。判断方法很简单:把各区县里程加总,和路网总里程对比,超出部分就是重复量,正常应相等或略小(有道路落在省界外)。

4.4 面积和长度单位搞混:经纬度下算出的数字不能用

现象:路网密度算出来是零点零零几或者几万,量级完全不对。原因:在 EPSG:4326 下调用.area和.length,返回的是平方度和度,不是平方米和米。解决:所有量算前先to_crs投到米制投影,江苏用 CGCS2000 对应分带即可。这类错误最隐蔽的地方在于程序不报错,数字照样输出,只有和常识对照才会发现——一个区县的路网密度不可能是 30000 km/km²,看到这种数字先查投影,别查公式。

4.5 边界对不齐:县界拼接存在缝隙或重叠

现象:把县界合并成市界、省界时,渲染图上出现细白缝或深色重影。原因:不同来源的边界数据精度和采集时点不同,相邻县共用边界的节点坐标不完全相同,形成微小的缝隙或重叠。解决:做分析前先做拓扑检查,用shapely的unary_union合并后再buffer(0)修掉自相交;对精度要求高的场景,先按省级边界裁剪,避免县级数据溢出省界。日常制图对这类微缝容忍度高,但做面积统计时,缝隙累积起来会让总面积偏小,必须处理。

5. 让这份底图更耐用的三个进阶技巧

5.1 建立图层索引与元数据登记表

数据一旦进了多个项目,最怕的是「这份 shp 是哪一版、坐标系是什么、字段含义谁定的」没人说得清。我的习惯是在数据目录下放一份metadata.csv,每个图层一行,记录文件名、几何类型、要素数、EPSG 编号、字段说明和更新时间。配合一个极简的 Python 脚本自动生成大半内容,人工只补字段含义,成本很低但回报很高。

import geopandas as gpd import pandas as pd from pathlib import Path rows = [] for shp in Path("./jiangsu_2022").rglob("*.shp"): gdf = gpd.read_file(shp) rows.append({ "file": shp.name, "geom": gdf.geom_type.unique().tolist(), "count": len(gdf), "epsg": gdf.crs.to_epsg() if gdf.crs else None, "fields": ",".join(gdf.columns.drop("geometry")), }) pd.DataFrame(rows).to_csv("./jiangsu_2022/metadata.csv", index=False, encoding="utf-8-sig")

encoding="utf-8-sig"是为了让表格软件打开 CSV 时正确识别中文,不加 BOM 头在部分环境里会乱码。rglob递归扫描子目录,适应按类别分文件夹的组织方式。这份表看起来是杂活,但半年后回来改需求,有没有它就是「五分钟上手」和「半天考古」的区别。

5.2 用 GeoPackage 替代散装 SHP

SHP 的局限在项目变复杂后会很碍事:字段名限 10 个字符、不支持存 NULL 与长文本、一个图层一组文件容易丢。把整理好的图层统一转成 GeoPackage(.gpkg),单文件承载多图层,字段名不再受限,读取速度也更好。

import geopandas as gpd admin = gpd.read_file("./out/county.shp").to_crs("EPSG:4527") roads = gpd.read_file("./jiangsu_2022/road_network.shp").to_crs("EPSG:4527") # 写入同一个 gpkg,layer 参数指定图层名 admin.to_file("./out/jiangsu.gpkg", layer="county", driver="GPKG") roads.to_file("./out/jiangsu.gpkg", layer="road_network", driver="GPKG")

转换时统一投影再写入,避免入库后各图层坐标系混杂。driver="GPKG"必须显式指定,否则 GeoPandas 会按扩展名推断,个别版本推断失败会报错。转完之后原始 SHP 别急着删,留一份归档,交付给只用桌面软件的同事时,SHP 仍然是兼容性最好的格式。

5.3 版本留痕:日期后缀与变更说明

这份数据标题里带 2022.07,说明它是有时点的。行政区划会调整,路网会新建,隔一年再用同一份底图做对比分析,结论可能已经失真。我的做法是目录名带年月,每次基于它做的加工产出都另存新目录,并在变更说明里写清做了什么(裁剪、投影、字段重命名、拓扑修复)。看似笨,但这是唯一能让自己在三个月后回答「这个数字是怎么来的」的办法。有一次做区县合并后的统计,前后两版结果差了百分之十几,翻变更记录才发现是其中一版权重算错了投影,没有留痕的话只能全部重跑。从那以后我每次处理这类底图,都强制先跑一遍坐标系与要素数自检,再动分析代码。希望帮到你。

本文还有配套的精品资源,点击获取

返回列表