简介:这份资源面向地理信息、城市规划与人口统计等方向的学习者和从业者,提供2017年中国县级行政区划的矢量边界数据,采用1:100万比例尺,适合大范围空间分析与制图实践。压缩包共7个文件、约46.33MB,以shp主文件承载几何图形,dbf保存县名与代码等属性,prj定义CGS_WGS_1984坐标系,sbn、sbx、shx等索引文件加速检索,xml记录元数据,配套结构完整,可直接在主流GIS软件中加载使用。已有615人学习下载,说明其在教学与项目练习中具有一定参考价值。读者可借助该数据完成行政区划底图制作、人口密度与地形气候等专题叠加分析,并理解Shapefile各组成文件的分工与坐标系统一要点,为后续空间统计与规划决策打下数据基础。
1. 拿到一份 2017 县级行政区 shp,先别急着往地图里拖
一份标注为 2017 年、覆盖县级行政区划的 shp 文件,落到手里第一件事往往不是打开 QGIS 看效果,而是判断它到底能不能用、该怎么用。基础地理数据库里的县级行政区图层,本质是一份带属性表的矢量面数据,承载的是行政区名称、代码、几何边界三样东西。它最常见的用途是给统计数据做空间挂接、给业务系统做底图、给分析任务做区域裁剪。但 2017 这个年份很关键:区划调整年年有,撤县设区、合并、更名都会让边界和代码对不上今天的口径。所以真正要问的是三件事——坐标系是什么、属性字段能不能对上你的业务主键、几何有没有拓扑问题。这篇笔记按我实际处理这类数据的顺序走一遍,从字段结构、坐标转换、入库挂接,到避坑和验证,新手能照着复现,熟手能直接跳到参数和边界那几节。
2. 拆开 shp 看结构:字段、编码与坐标系的三个确认点
shp 从来不是单个文件,而是一组同名文件的集合。拿到压缩包先解压,用命令行把目录列清楚,比在图形界面里点开更可靠,因为你能立刻发现缺了 .prj 或 .dbf 这种致命问题。
2.1 先确认文件组是否完整
一个能正常读取的 shapefile 至少要有 .shp(几何)、.shx(索引)、.dbf(属性)三个文件,.prj(投影定义)和 .cpg(编码声明)属于强烈建议存在的补充文件。缺 .prj 意味着坐标系未知,缺 .cpg 意味着中文字段可能乱码。
# 列出解压目录下所有同名文件组,确认完整性 ls -lh ./county_2017/ | awk '{print $9, $5}' # 期望看到成对出现的 .shp .shx .dbf .prj .cpg这段命令只是把文件名和大小打出来,重点看两件事:同名文件是否成套、.dbf 大小是否合理。如果 .dbf 只有几 KB 而 .shp 有几十 MB,说明属性表几乎是空的,这种数据只能当底图用,做不了属性挂接。参数上没什么可调的,-lh只是让人眼可读,awk取第 9 列文件名和第 5 列大小,不同系统ls列序略有差异,看不准就直接ls -l人工核对。
2.2 用 Python 读出字段清单和坐标系
图形界面能看,但批量判断和留痕要靠代码。用 geopandas 读进来,先打印 CRS 和字段类型,这一步决定了后面所有转换动作。
import geopandas as gpd # 读取时显式指定编码,中文属性表常见 GBK/GB18030 gdf = gpd.read_file("./county_2017/county_2017.shp", encoding="gb18030") print("要素数量:", len(gdf)) print("坐标系:", gdf.crs) print("字段与类型:") print(gdf.dtypes) print("前两行属性:") print(gdf.drop(columns="geometry").head(2))逻辑说明:encoding参数是这类国内基础地理数据最容易翻车的地方,默认 UTF-8 读 GBK 编码的 .dbf 会抛 UnicodeDecodeError 或读出乱码字段名。先试gb18030,它兼容 GBK 且覆盖更全。gdf.crs如果打印出None,说明 .prj 缺失或损坏,必须手动补坐标系,不能靠猜。字段类型里要特别关注行政区代码字段——如果它被读成 int64,前导零会丢,比如110101变成110101还好,但010101这种就会变成10101,挂接时全对不上。
2.3 行政区代码字段的三种常见命名与清洗
不同来源的县级图层,代码字段可能叫ADCODE、CODE、XZQDM、AREA_CODE,名称字段可能叫NAME、XZQMC、COUNTY。不要假设,先看实际列名再写映射。清洗的核心是把代码统一成 6 位字符串,并去掉可能存在的空格。
# 按实际列名调整,这里假设代码列叫 ADCODE,名称列叫 NAME code_col = "ADCODE" name_col = "NAME" # 统一转 6 位字符串,补前导零,去空格 gdf[code_col] = gdf[code_col].astype(str).str.strip().str.zfill(6) gdf[name_col] = gdf[name_col].astype(str).str.strip() # 检查代码长度分布,异常长度说明数据有问题 print(gdf[code_col].str.len().value_counts()) # 正常应几乎全是 6;出现 5 或 7 要逐条排查参数说明:zfill(6)是补零的关键,astype(str)必须在它之前,否则对数值型补零无效。str.strip()处理的是从 Excel 或数据库导出时混入的首尾空格,这类空格肉眼看不见,但会让merge静默失败——这是血泪经验里最常见的一类。长度分布打印出来如果出现非 6 位,不要急着删,先定位是哪些行,往往是某个特殊区划或数据录入错误。
3. 坐标系与投影:从地理坐标到可计算面积的转换路径
县级行政区数据最常见的坐标系是 CGCS2000 地理坐标(单位度)或某种高斯-克吕格投影(单位米)。前者适合存储和展示,后者适合算面积、算距离。选错坐标系不会报错,但会让面积统计差出几十倍,这是最隐蔽的坑。
3.1 判断当前是地理坐标还是投影坐标
看gdf.crs的输出:如果unit是degree,就是地理坐标;如果是metre,就是投影坐标。也可以直接看几何坐标数值范围,经度在 73 到 135 之间、纬度在 3 到 54 之间,基本可判定为地理坐标。
# 打印 CRS 详细信息和几何范围 print(gdf.crs.to_string() if gdf.crs else "CRS 缺失") print("几何边界:", gdf.total_bounds) # 地理坐标下 total_bounds 大致是 [73, 3, 135, 54] 量级如果total_bounds出现几百万量级的数值,说明已经是投影坐标,单位大概率是米。这一步的判断直接决定下一步要不要做投影转换。注意:total_bounds返回的是[minx, miny, maxx, maxy],别把经纬顺序搞反。
3.2 面积计算必须切到等积投影
在地理坐标下直接调.area,得到的是平方度,没有任何物理意义。正确做法是先投影到适合中国区域的等积坐标系,再算面积。常见选择是 Albers 等积投影,参数按中国范围设定。
# 定义中国区域 Albers 等积投影 albers_crs = "+proj=aea +lat_1=25 +lat_2=47 +lat_0=0 +lon_0=105 +x_0=0 +y_0=0 +datum=WGS84 +units=m +no_defs" gdf_proj = gdf.to_crs(albers_crs) gdf_proj["area_km2"] = gdf_proj.geometry.area / 1e6 print(gdf_proj[["NAME", "area_km2"]].sort_values("area_km2", ascending=False).head(5))参数说明:lat_1和lat_2是标准纬线,取 25 和 47 覆盖中国大部分纬度带,能压低面积变形;lon_0=105是中央经线,取中国中部经度。这套参数是业内常用配置,不是唯一解,但对全国尺度统计足够。/1e6是把平方米转平方公里。如果只做单省分析,可以把标准纬线和中央经线收窄到该省范围,面积精度会更好。注意to_crs会重算几何,原始gdf不受影响,别在转换后的对象上再调原始字段做混用。
3.3 投影转换后的几何有效性检查
投影转换本身可能放大原本就存在的几何问题,比如自相交、重复点。转换后必须做一次有效性检查,否则后续空间叠加会报拓扑错误。
# 检查无效几何并尝试修复 invalid = gdf_proj[~gdf_proj.geometry.is_valid] print("无效几何数量:", len(invalid)) if len(invalid) > 0: gdf_proj["geometry"] = gdf_proj.geometry.buffer(0) print("修复后无效数量:", (~gdf_proj.geometry.is_valid).sum())buffer(0)是修复自相交的常用手段,原理是把几何做一次零距离缓冲,多数自相交会被规整。但它不是万能药,对严重破损的几何可能产生空几何或碎片,修复后要再查一次is_valid和is_empty。如果无效数量占比很高,说明源数据质量差,应该回到源头核对,而不是硬修。
4. 入库与挂接:把县级行政区接到业务统计表上
数据清洗完,真正的落地动作是入库和挂接。这一步的目标是让县级行政区图层能和你的统计表按行政区代码一一对上,对不上的要能定位原因,而不是靠肉眼找。
4.1 导出为 GeoPackage 或 PostGIS 的取舍
shp 有字段名长度限制(10 字符)和编码问题,长期使用建议转成 GeoPackage 或直接入 PostGIS。GeoPackage 单文件、无字段名限制、UTF-8 原生,适合中小规模;PostGIS 适合多用户并发和空间查询。
# 导出 GeoPackage,字段名不受 10 字符限制 gdf_proj.to_file("./county_2017.gpkg", layer="county", driver="GPKG") # 如需入 PostGIS,用 sqlalchemy 引擎 # from sqlalchemy import create_engine # engine = create_engine("postgresql://user:pass@host:5432/gisdb") # gdf_proj.to_postgis("county_2017", engine, if_exists="replace", index=False)参数说明:driver="GPKG"必须显式指定,否则 geopandas 可能按扩展名推断失败。if_exists="replace"会覆盖同名表,生产环境慎用,建议先append再校验。入 PostGIS 前确认目标库已启用 PostGIS 扩展,否则建表会失败。字段名在 GeoPackage 里可以保留中文,但跨系统交换时仍建议用英文列名加中文别名,减少编码依赖。
4.2 按行政区代码做左连接并统计匹配率
挂接的核心是左连接:以业务统计表为主表,县级图层为右表,按代码连接,统计匹配率。匹配率低于预期时,问题一定出在代码格式或区划变更上。
import pandas as pd # 模拟业务统计表,代码为 6 位字符串 stats = pd.DataFrame({ "ADCODE": ["110101", "130102", "310101", "999999"], "value": [120, 340, 210, 50] }) merged = stats.merge( gdf_proj[["ADCODE", "NAME", "geometry"]], on="ADCODE", how="left" ) matched = merged["NAME"].notna().sum() print(f"匹配率: {matched}/{len(merged)}") print("未匹配代码:", merged.loc[merged["NAME"].isna(), "ADCODE"].tolist())逻辑说明:how="left"保证业务表所有行保留,未匹配的NAME为 NaN,便于定位。匹配率是判断数据可用性的硬指标,低于 95% 就要停下来查。未匹配代码常见原因有三类:业务表代码带前导零丢失、图层是 2017 口径而业务表用了新代码、代码字段混入了空格或全角字符。逐类排查比盲目重试有效。
4.3 区划变更导致的代码对不上怎么处理
2017 年的县级图层和今天的业务数据之间,必然存在撤县设区、合并更名的情况。处理方式不是改图层,而是建一张代码映射表,把旧代码映射到新代码,挂接前先过一遍映射。
# 维护一张变更映射表,旧代码 -> 新代码 code_map = pd.DataFrame({ "old_code": ["130102", "310101"], "new_code": ["130102", "310101"] # 示例占位,实际按真实变更填写 }) stats = stats.merge(code_map, left_on="ADCODE", right_on="old_code", how="left") stats["ADCODE"] = stats["new_code"].fillna(stats["ADCODE"]) stats = stats.drop(columns=["old_code", "new_code"])这张映射表要作为独立资产维护,每次区划调整后更新,而不是散落在脚本里。参数上注意fillna的顺序:先用新代码填充,没有映射的保留原代码,这样不会把未变更的记录误改。映射表本身要有来源记录,否则半年后没人说得清某条映射的依据。
5. 避坑与排查:县级行政区 shp 处理中最容易翻车的五件事
5.1 中文属性乱码,字段名读出来是问号
现象:gdf.columns打印出一堆\uXXXX或问号,NAME列的值也是乱码。原因:.dbf 的编码声明缺失或与 .cpg 不一致,geopandas 按默认 UTF-8 解码 GBK 内容。解决:读取时显式传encoding="gb18030",如果仍乱码,用dbfread单独读 .dbf 确认原始编码,再决定是否用iconv转码后重写 .cpg。不要反复用不同编码试读后直接覆盖原文件,先备份。
5.2 面积算出来大得离谱或小得离谱
现象:某县面积算出几十万平方公里。原因:在地理坐标下直接调.area,得到的是平方度,或者投影参数的标准纬线设错。解决:先确认gdf.crs的unit,地理坐标必须先to_crs到等积投影再算面积。投影参数按分析范围设定,全国尺度用 Albers 标准纬线 25/47,单省分析收窄参数。算完拿几个已知面积的县做交叉验证,偏差超过 5% 就回头查参数。
5.3 merge 之后匹配率莫名很低
现象:代码肉眼看着一样,merge后大量 NaN。原因:一侧是 int 一侧是 str,或一侧带前导零一侧没有,或混入了不可见空格。解决:连接前两侧都做astype(str).str.strip().str.zfill(6),并在 merge 前各自打印str.len().value_counts()确认长度一致。这类问题不会报错,只能靠匹配率统计发现,所以每次挂接都要打印匹配率,别省这一步。
5.4 投影转换后几何报拓扑错误
现象:to_crs后做空间叠加抛TopologyException。原因:源数据本身有自相交或重复点,投影放大了问题。解决:转换后立即做is_valid检查,对无效几何用buffer(0)修复,修复后再查is_empty。如果无效比例超过 1%,不要硬修,回到源数据核对,硬修出来的边界可能已经偏离实际。
5.5 用 2017 数据直接对今天的业务表
现象:挂接后某些区县永远匹配不上,或匹配上了但边界明显不对。原因:2017 至今的区划调整没有处理。解决:建代码映射表,挂接前先映射;对边界敏感的分析,明确标注数据年份,不要默认它代表现状。年份不是元数据里的一个装饰字段,它决定了这份数据能回答什么问题、不能回答什么问题。
6. 验证与进阶:用抽样核对和版本留痕把数据用稳
数据能不能信,不靠感觉,靠可重复的验证。我一般会做两层核对:一层是抽样人工核对,挑几个熟悉的县,把算出的面积和公开的行政区面积对比;另一层是内部一致性核对,检查代码唯一性、几何是否重叠、名称是否有重复。
# 内部一致性三项检查 print("代码重复:", gdf_proj["ADCODE"].duplicated().sum()) print("名称为空:", gdf_proj["NAME"].isna().sum()) print("几何为空:", gdf_proj.geometry.is_empty.sum()) # 抽样核对:取几个县打印面积,人工比对 sample = gdf_proj.sample(5, random_state=42)[["NAME", "area_km2"]] print(sample)duplicated().sum()应为 0,出现重复说明同一行政区有多条记录,挂接时会产生笛卡尔积式的行数膨胀。is_empty检查能抓出投影修复后产生的空几何。抽样用固定random_state保证可复现,方便下次对比。
进阶用法上,我习惯把整个处理流程固化成一条可重跑的脚本链:读取 → 编码清洗 → 代码标准化 → 投影转换 → 有效性修复 → 导出 → 匹配率报告。每一步的输出都落盘,中间产物带日期后缀,这样任何一次结果异常都能回溯到具体环节。参数不写死在代码里,而是集中到一个配置字典,投影参数、编码、代码字段名都从配置读,换一批数据只改配置不改逻辑。
| 检查项 | 合格标准 | 不合格时的动作 |
|---|---|---|
| 代码唯一性 | 重复数为 0 | 定位重复记录,确认是否多版本混入 |
| 代码长度 | 全部为 6 位 | 逐条排查前导零或录入错误 |
| 几何有效性 | 无效占比低于 1% | 低于阈值修复,超过则回源核对 |
| 挂接匹配率 | 高于 95% | 查代码格式与区划变更映射 |
| 面积抽样偏差 | 与参考值偏差低于 5% | 复查投影参数与单位换算 |
这张表我每次处理新批次数据都会过一遍,五项全绿才敢往业务里推。最后说个习惯:任何一份带年份的行政区数据,我都会在文件名和图层名里保留年份,并在属性表加一列data_year,不是为了好看,是为了半年后有人问“这个边界怎么和现在不一样”时,能一句话说清是数据口径问题而不是处理错误。希望帮到你。
本文还有配套的精品资源,点击获取