GeoJSONcn
GeoJSONcn / 技术专栏 / 全国省市区县区划边界数据下载后的…

全国省市区县区划边界数据下载后的第一道预处理:外接矩形与成本预算

发布于 2026-09-18 · GeoJSONcn 技术团队 · 阅读约 17 分钟

一份中心城区七区的 GeoJSON 摊开看,顶点数最多的是洪山区,13950 个;最少的是江汉区,889 个。两者面积差 20 倍,顶点差 15.7 倍。做全国省市区县区划边界数据下载的人看到这组数字,本能反应是把小数位砍掉、把容差加上去。可真到跑批量任务时才明白,决定一份文件要不要重做的,往往不是顶点数,而是有没有先算过外接矩形。

外接矩形(bbox)是整套预处理流程里最便宜的一个量,也正是最常被跳过的一个量。省掉它,后面每一步都在盲跑。

bbox 的填充率,决定分组方式

把每个单元的边界框面积除以单元实有面积,得到填充率。这个比值低说明轮廓外凸内凹严重,矩形里塞了大量不属于这个单元的空白。

武汉市中心城区七个区的实测如下。实有面积取自站内各区页面,矩形面积按经纬度跨度在 30.6°N 处换算。

单元顶点数实有面积 km²外接矩形 km²填充率
江汉区88928.566.343.0%
江岸区109980.7192.441.9%
硚口区103141.192.644.4%
汉阳区1360112.7218.751.5%
武昌区2150114.0233.148.9%
青山区439498.1168.758.2%
洪山区13950569.71719.533.1%

洪山区 33.1% 的填充率是全组最低。它的边界被长江和多个市辖区切开,轮廓细长且带大量飞地,矩形框里将近七成面积不属于它。这个数一出来很多事情就不用猜了:拿洪山区的 bbox 去铺网格做聚合,六成以上算力会花在空白处。

填充率低于 50% 就值得换策略。利川市 48.0%、襄城区 49.9% 已经接近一半,省级单位动辄 30% 以下——拿全省矩形框一次性铺满网格,是这类任务里最常见的浪费源头。

投影换错,矩形面积能虚增三成

同样一组边界,换投影算出来的矩形面积并不相同。江汉区在等距近似下是 66.3 平方千米,换 EPSG:3857 变成 89.5 平方千米,虚增 35%。洪山区 1719.5 对 2318.1,虚增 34.8%。

原因在墨卡托本身:它的面积畸变随纬度上升放大,在 30.6°N 约为 1/cos(30.6°),正好三成出头。做视觉叠加没问题,一旦拿它算面积、估成本、设阈值,数字全部偏高。全国省市区县geojson数据下载之后若沿用 Web 墨卡托的坐标系做统计,误差还会随纬度一路累积。

下面这段代码同时给出两种口径,差额一眼可见:

import json, math
from pyproj import Transformer

K = 111.32 ** 2 * math.cos(math.radians(30.6))   # 该纬度处 1 度² 折合 km²
t3857 = Transformer.from_crs("EPSG:4326", "EPSG:3857", always_xy=True)
def outer_ring(geom):
    if geom["type"] == "Polygon":
        return geom["coordinates"][0]
    return geom["coordinates"][0][0]            # MultiPolygon 取首部件

def bbox_of(ring):
    xs = [c[0] for c in ring]
    ys = [c[1] for c in ring]
    return min(xs), min(ys), max(xs), max(ys)

gj = json.load(open("mapjson/区县/武汉市-江汉区.json", encoding="utf-8"))
ring = [c for ft in gj["features"] for c in outer_ring(ft["geometry"])]
lo1, la1, lo2, la2 = bbox_of(ring)

approx = (la2 - la1) * 111.32 * (lo2 - lo1) * 111.32 * math.cos(math.radians((la1 + la2) / 2))
x1, y1 = t3857.transform(lo1, la1)
x2, y2 = t3857.transform(lo2, la2)
print(f"等距近似 {approx:.1f} km²   3857 折合 {(x2 - x1) * (y2 - y1) / 1e6:.1f} km²")

要算真实面积,用等积投影而不是等距近似。ESRI:54034 在这批数据上给出的结果与等距近似几乎一致,江汉区 66.1 对 66.3,洪山区 1713.9 对 1719.5,偏差都在 0.4% 以内,拿它做交叉校验足够。

命中口径差一成,报表就会对不上

外接矩形算完,下一步通常是网格化。上一轮我用质心落点判定单元归属,这次把口径换成矩形相交重新数了一遍武汉市中心城区。

网格边长全网格数质心命中矩形相交命中差值
1000 米315712201398178
2000 米81930139291
5000 米144548228

1000 米档两个口径差 178 个单元,占质心命中数的 14.6%。同一份数据、同一套网格,换一个判定写法结果就差出一成半。跨口径对账时出现这种偏差,先查判定写法,不必怀疑数据。

做统计报表用质心法,单元唯一归属、不重复计数;做覆盖范围展示用相交法,边缘的格子不会漏。两者都不算错,混用才会出事。折中做法是相交法命中的格子按重叠面积比例分摊权重,代价是每格都要跑一次几何求交,比质心判定慢一倍以上。

抽稀之前先确认 bbox 没变

回到开头那个直觉:要不要抽稀。用 shapely 的 simplify 在中心城区七区上跑了两档,取 preserve_topology=True。

56 米容差(约 0.0005 度)下,江岸区顶点从 1099 降到 209,留存 19.0%,Hausdorff 位移 49.5 米;江汉区 889 降到 162,留存 18.2%,位移 35.2 米。整组七区的字节数从 616.9 KB 压到 100.6 KB,gzip 后 149.3 KB 压到 24.6 KB。

代价在小单元上更明显。江汉区实有面积只有 28.5 平方千米,56 米容差带来的面积变化是 -0.259%;洪山区 569.7 平方千米,同样容差下只变 -0.035%。容差是绝对值,单元越小,它占的比重越大。江汉、硚口这类面积不足 50 平方千米的市辖区,56 米一档已经能吃出可见的形状变化。

十一米档(约 0.0001 度)保守得多。江汉区位移 10.2 米、面积变化 -0.048%,洪山区位移 12.0 米、面积变化 +0.001%。留存率也不一样,洪山区 34.1%、江岸区 36.6%、江汉区 31.2%,省下的体积远不如 56 米档。

值得一提的是,抽稀不改 bbox。边界框由极值点决定,容差再大也只删中间点。靠 bbox 做分区、做缓存键、做增量比对的流程,抽稀前后完全一致,不必重算。这条性质让预处理能拆成两步:先按原始数据算 bbox 并落盘,后续无论抽稀到哪一档,索引都不用动。

反过来,如果外接矩形在两次运行之间变了,那说明面本身改了,不是精度问题。这时候该查源数据版本,而不是回去调容差。

把这几步收进构建脚本

全国省市区县区划边界数据下载完成之后,预处理阶段至少该产出三件东西:每个单元的 bbox、填充率、以及按填充率分档的建议容差。三者都在几十毫秒内算完,却能省掉后面整轮返工。

武汉这批数据我按填充分了三档。填充率 55% 以上的走标准容差;45% 到 55% 之间降一档;低于 45% 的改用分组处理,不再拿单个矩形铺满。洪山区 33.1% 落在第三档,也是这一组里唯一需要拆开跑的单元。

区县这一级数据量不算大,真正吃资源的是往下的乡镇街道层。把全国村级geojson数据下载全量拉回本地,动辄几十万个要素,每一步的系数都会被放大,省在预处理上的时间会成倍体现出来。把 bbox 和填充率算在构建期、跟数据一起发布,前端只读结果,是最省事的分工。

顺带提一句,这几项指标对做汇报底图也有用。填充率低的单元形状细长、飞地多,落到版面上容易显得碎,需要重新安排取景和分页。拿省、市、区县单独做 PPT 底图的话,可以去 地图 PPT 模板选购页 从省到市逐级挑一份可编辑底图,预览确认范围后再决定下载,单份 ¥4.99,省掉自己在脚本里拼图形这一步。

边界框这类前置指标,做的是把后面的选择提前收敛。算一次,管很久。