简介:这份资源提供中国全部地级市行政区域的空间边界数据,采用Shapefile格式,面向GIS学习者、城市规划人员及从事地理、社会、经济研究的师生,可用于城市分布分析、行政边界制图与人口统计等场景。压缩包共11个文件,约1.91MB,包含shp空间几何数据、dbf属性表、prj投影信息,以及shx、sbn、sbx索引文件与xml元数据,各类型分工明确,便于在ArcGIS中直接加载、裁剪与合并。数据年代为2004年,属性表可读取城市名称、代码、面积等信息,适合作为底图开展空间分析与专题制图。目前已有2079人学习下载,说明其在教学与研究中具有一定参考价值;若用于时效性要求较高的项目,建议结合最新行政区划调整进行更新核对。
1. 拿到一份市级行政边界 shp,先想清楚它能干什么
做数据可视化或者空间分析的人,迟早会撞上一个需求:把全国三百多个地级行政单元按某个指标上色,或者按市域范围做统计汇总。这时候最省事的做法不是自己去爬边界数据,而是直接拿一份整理好的市级行政区域 shp 包。这份资源就是干这个的——中国市级行政区域的矢量边界,shp 格式,配套的 dbf、shx、prj 等文件齐全,导入 QGIS、ArcGIS 或者用 GeoPandas 读进来就能直接画图、做空间连接、算面积。
它适合谁?做区域经济看板的前端和数据分析同学、跑空间统计的研究生、需要给业务系统配一张行政区划底图的开发者。不适合谁?需要乡镇级精度、需要实时更新到最新区划调整的场景,这份资源给不了。先把边界和精度预期对齐,后面才不会翻车。
2. 拆包与字段核对:shp 不是单个文件,读之前先看这五件套
2.1 shp 文件组的构成与完整性检查
很多人第一次拿到 shp 会懵:解压出来一堆同名不同后缀的文件,到底哪个才是数据?答案是它们共同构成一份数据,缺一不可。核心是这几个:
| 后缀 | 作用 | 缺失后果 |
|---|---|---|
| .shp | 存储几何图形(点线面坐标) | 直接打不开 |
| .shx | 几何索引,加速空间查询 | 部分软件报错或读取极慢 |
| .dbf | 属性表,存市名、编码等字段 | 只剩图形没有名字 |
| .prj | 坐标系定义(WKT 文本) | 坐标被当成无投影,面积计算全错 |
| .cpg | 属性表字符编码声明 | 中文市名变乱码 |
拿到包先做一件事:确认这五个文件同名同目录。我见过有人只拷了 .shp 和 .dbf 过去,结果在另一台机器上打开发现位置飘到海里去了——因为 .prj 没带,软件默认按经纬度裸坐标处理。用命令行快速列一下:
# 列出目录下所有 shp 相关文件,确认五件套齐全 ls -lh *.shp *.shx *.dbf *.prj *.cpg 2>/dev/null如果 .cpg 缺失,中文属性大概率是 GBK 编码,后面读进来要手动指定编码,这个坑在第 5 章会细说。.prj 里通常写的是地理坐标系(经纬度,常见为 CGCS2000 或 WGS84),这意味着你直接算面积得到的是“平方度”,没有任何物理意义,必须投影后才能算平方公里。
2.2 用 GeoPandas 读入并核对字段与几何类型
Python 侧最顺手的工具是 GeoPandas,底层走 GDAL/OGR,shp 的各种边角情况它基本都能兜住。先装环境,再读:
import geopandas as gpd # 读入 shp,encoding 先按 utf-8 试,乱码再换 gbk gdf = gpd.read_file("china_city.shp", encoding="utf-8") # 看基本信息:几何类型、坐标系、字段 print(gdf.geom_type.unique()) # 期望是 Polygon 或 MultiPolygon print(gdf.crs) # 期望是 EPSG:4326 或 CGCS2000 地理坐标 print(gdf.columns.tolist()) # 看有哪些属性字段 print(len(gdf)) # 市级单元数量,正常在 330~340 上下 print(gdf.head(3))这段代码的重点不在跑通,而在核对三件事。第一,几何类型。市级边界里一定混着 MultiPolygon——沿海城市有岛屿、飞地,单个 Polygon 装不下,如果你的下游代码只处理 Polygon,这里就会炸。第二,crs 是否为 None。为 None 说明 .prj 丢了或没被识别,后面所有空间运算都不可信。第三,记录条数。全国地级行政区数量会随区划调整变化,如果你拿到的只有两百多条,很可能是把省直辖县级单位漏了,或者这份数据本身就不全,得先跟需求方确认口径。
字段方面,常见会带市名(如 name)、市级行政编码(如 adcode,六位)、所属省份(如 province)。编码字段是后续做关联的钥匙,务必确认它是字符串而不是被读成整数——六位编码如果被当数字,前导零会丢,比如编码 010000 变成 10000,跟别的表一 join 全对不上。发现是整数就强制转字符串并补零:
# 编码字段强制转字符串并左补零到 6 位,防止前导零丢失 gdf["adcode"] = gdf["adcode"].astype(str).str.zfill(6)这一步看着琐碎,但它是后面所有“按编码关联统计数据”操作的前提。血泪经验:关联失败十次里有六次是编码类型或前导零的问题,不是数据本身错。
3. 投影与面积计算:经纬度直接算面积为什么是错的
3.1 地理坐标与投影坐标的区别,以及该选哪个投影
shp 里存的如果是经纬度(度为单位),你调用gdf.area得到的是“平方度”。一度经度在赤道约 111 公里,在北纬 40 度只有约 85 公里,所以同样一个“平方度”,在南方和北方代表的实际面积差着一大截。直接拿平方度做面积排名,结论必然是错的,这就是典型的玄学数据。
正确做法是先投影到等积投影再算面积。全国范围常用 Albers 等积圆锥投影,参数一般取中央经线 105°E、双标准纬线 25°N 和 47°N,这套参数在国内做全国尺度制图是通行做法:
# 从地理坐标投影到 Albers 等积圆锥(CGCS2000 基准) albers = "+proj=aea +lat_1=25 +lat_2=47 +lat_0=0 +lon_0=105 +x_0=0 +y_0=0 +ellps=GRS80 +units=m +no_defs" gdf_proj = gdf.to_crs(albers) # 此时面积单位是平方米,换算成平方公里 gdf_proj["area_km2"] = gdf_proj.geometry.area / 1e6 print(gdf_proj[["name", "area_km2"]].sort_values("area_km2", ascending=False).head(10))参数说明:lat_1、lat_2是双标准纬线,选在全国南北跨度内能让形变最小;lon_0是中央经线,取 105°E 大致是全国几何中心;ellps=GRS80对应 CGCS2000 椭球。如果你的数据是 WGS84,用 GRS80 差异在米级,做市级统计可以忽略,但做高精度测量就得换成对应椭球。
投影完再算面积,你会发现排名跟直觉对得上:面积大的市集中在西部和北部,而不是经纬度跨度看起来大的那些。这一步做完,数据才算“能用”。
3.2 空间连接:把点位或栅格统计落到市级单元
市级边界最常见的用法是当“统计容器”。比如你有一批带经纬度的门店、传感器或者事件点,想统计每个市有多少个,用空间连接一步到位:
# points 是带 geometry 的点数据,crs 必须与 gdf 一致 points = points.to_crs(gdf.crs) # 先对齐坐标系,否则连不上 joined = gpd.sjoin(points, gdf[["name", "adcode", "geometry"]], how="left", predicate="within") # 按市聚合计数 count_by_city = joined.groupby(["adcode", "name"]).size().reset_index(name="cnt")关键点在predicate="within"和坐标系对齐。within表示点落在面内部才算命中;如果点正好压在边界线上,within可能判不进去,这时候可以改用intersects,但要注意边界点可能被相邻两个市同时命中,导致重复计数。坐标系不对齐是另一个高频翻车点——点用 WGS84、面用 CGCS2000,肉眼看着差不多,实际差几十米,边界附近的点就会错配到隔壁市。所以to_crs那行不能省。
如果统计对象是栅格(比如夜间灯光、NDVI),思路类似,用rasterstats的zonal_stats按市做分区统计,这里不展开,但前提同样是栅格和矢量坐标系一致。
4. 出图与轻量化:从 shp 到能上网页的 GeoJSON
4.1 用 Matplotlib 快速出图验证数据可用性
在把数据交给前端之前,先自己画一张图,肉眼确认边界没断裂、没重叠、没跑到境外。这是最便宜的质量检查:
import matplotlib.pyplot as plt fig, ax = plt.subplots(figsize=(12, 10)) gdf_proj.plot(ax=ax, edgecolor="gray", facecolor="none", linewidth=0.3) ax.set_axis_off() plt.savefig("city_check.png", dpi=150, bbox_inches="tight")出图后重点看三处:沿海岛屿是否完整、飞地(比如某些市在别的省境内的飞地)是否还在、西部大市边界是否平滑。如果看到某块区域明显缺一块或者多出一块,多半是原始数据在拼接时出了问题,得回去核对源数据,而不是在代码里硬修。
4.2 转 GeoJSON 并做坐标精度与体积压缩
网页端(Leaflet、Mapbox、ECharts)吃的是 GeoJSON,shp 得转一道。直接转出来的文件往往几十兆,加载卡顿,需要做两件事:降精度和简化几何。
# 转回 WGS84(网页底图通用),保留 4 位小数约 10 米精度 gdf_wgs = gdf.to_crs("EPSG:4326") gdf_wgs["geometry"] = gdf_wgs.geometry.simplify(0.01, preserve_topology=True) gdf_wgs.to_file("china_city.geojson", driver="GeoJSON")参数说明:simplify(0.01)里的 0.01 是容差,单位是度,约 1 公里,适合全国尺度的网页展示;如果要做市级细节展示,容差调到 0.001 甚至更小。preserve_topology=True保证简化后相邻市之间不出现缝隙或重叠——这个参数不开,简化完边界会互相穿插,视觉上像拼图错位。坐标保留 4 位小数是网页端的常见折中,再高精度对屏幕显示没意义,只会撑大文件。
转完用浏览器打开 GeoJSON 或者丢进在线校验器看一眼,确认没有自相交的非法几何。有些源数据在拼接时会产生自相交多边形,GeoJSON 校验会报错,前端渲染直接白屏。遇到这种情况用shapely的make_valid修一下:
from shapely.validation import make_valid gdf_wgs["geometry"] = gdf_wgs.geometry.apply(make_valid)5. 避坑与排查:市级边界数据最容易翻车的五个地方
5.1 中文乱码:市名读出来是问号或方块
现象:gdf.head()里 name 字段显示为???或乱码字符。原因:.dbf 的属性表编码是 GBK,而 GeoPandas 默认按 UTF-8 读。解决:读入时显式指定encoding="gbk",或者先确认 .cpg 文件里写的是什么编码再对应设置。如果 .cpg 缺失,GBK 是大概率,试一次不行再试gb18030(GBK 的超集,兼容性更好)。
5.2 坐标系缺失导致位置漂移
现象:数据加载后整体偏移,或者跟底图对不上。原因:.prj 文件丢失,gdf.crs返回 None,软件按无投影处理。解决:先确认原始数据应有的坐标系(国内市级边界多为 CGCS2000 经纬度),手动赋值gdf = gdf.set_crs("EPSG:4490"),再投影。注意set_crs只是“声明”坐标系,不改变坐标值;to_crs才是真正做转换,两者别混用。
5.3 编码前导零丢失导致关联全空
现象:用 adcode 跟统计表 merge,结果一行都对不上。原因:shp 里编码被读成整数,前导零丢了;或者统计表里编码是字符串而 shp 里是整数,类型不匹配。解决:两边都统一成六位字符串再关联,astype(str).str.zfill(6)走一遍,merge 前打印两边 dtype 确认。
5.4 边界点重复计数
现象:按市统计点位数量,总数比实际多。原因:用了intersects做空间连接,落在边界线上的点被相邻两个市同时命中。解决:优先用within;确实需要包含边界点时,连接后按点 ID 去重,只保留一条记录。
5.5 简化后相邻市出现缝隙
现象:网页上放大看,市与市之间有一条白线。原因:几何简化时没开拓扑保持,相邻边界各自简化后不再重合。解决:simplify时加preserve_topology=True;如果已经简化坏了,回到未简化的数据重做,别在简化后的数据上二次修补。
6. 进阶:把市级边界做成可复用的底图服务与校验习惯
做到这一步,数据本身已经能用了,但如果你要在多个项目里反复用,每次都读 shp、投影、简化,效率太低。我的习惯是把它固化成两层:一层是“干净版”的 GeoParquet,一层是带校验的加载函数。
先转 GeoParquet,它比 shp 小、读写快、保留字段类型,适合当本地缓存:
# 投影后的干净数据存成 parquet,下次直接读,省去重复投影 gdf_proj.to_parquet("china_city_albers.parquet") # 下次使用 gdf = gpd.read_parquet("china_city_albers.parquet")再写一个加载函数,把前面几章的检查点串起来,每次调用自动跑一遍:
def load_city_boundary(path): gdf = gpd.read_parquet(path) assert gdf.crs is not None, "坐标系缺失" assert gdf.geom_type.isin(["Polygon", "MultiPolygon"]).all(), "存在非面几何" assert gdf["adcode"].str.len().eq(6).all(), "编码长度异常" assert gdf.geometry.is_valid.all(), "存在非法几何" return gdf这四个断言对应四类高频问题:坐标系、几何类型、编码、几何有效性。跑通它们,基本就排掉了八成翻车场景。熟手可以再加一条面积合理性检查——单个市面积不应为负或为零,也不应大到超过某个阈值,这能抓出坐标单位搞错(比如把米当度)的低级错误。
还有一个容易被忽略的点:区划是会调整的。某两个市合并、某个县升格为市,这些变化会让旧边界和新统计口径对不上。所以我在项目里会记录这份数据的“快照时间”,并在关联统计表时核对编码是否都能匹配上,匹配不上的单独列出来人工确认,而不是静默丢掉。这个习惯帮我挡过好几次“数据看着对、结论却偏”的问题。
从那以后我每次拿到新的边界数据,都强制先跑一遍加载函数里的断言,再画一张全国图肉眼过一遍,最后才进业务逻辑。希望帮到你。
本文还有配套的精品资源,点击获取