全国省市区县区划边界数据下载后先算一遍共享边:一半的公共边被画了两遍
拿到一批乡镇街道边界,多数人先看要素数、看字段、看面积合计,很少有人去看相邻两个乡镇之间那条公共边,在文件里到底被存了几次。这个看似无关紧要的细节,决定了后面能不能做拓扑合并、能不能按邻接关系建图、能不能把两个乡镇拼成一个片区而不留缝。 把本站 mapjson/区县/ 下 2901 个有效文件全量扫一遍(42282 个要素、4096 万条边),结论是:唯一化之后剩下 2704 万条边,其中 1391.5 万条恰好出现过两次,48.5% 的边只出现过一次。换句话说,全国乡镇边界数据下载 拿到的文件里,公共边大约有一半是两边各画各的,而不是共用同一组顶点。
一条边出现两次是正常的,出现一次才是要留意的
先用 edge_key 把每条边按两个端点的坐标量化成无序键,同一个文件内统计每个键的出现次数。理想情况下,同一条公共边会被相邻两个要素各引用一次,频次恰好是 2。
全量统计出来的频次分布:
| 边的出现频次 | 边数 | 含义 |
|---|---|---|
| 1 次 | 13,119,960 | 未配对,两侧不共用顶点 |
| 2 次 | 13,915,230 | 正常共享,两要素各一次 |
| 3 次 | 3,346 | 三要素交汇处的短边 |
| 4 次 | 585 | 四要素交汇 |
| 5 次及以上 | 107 | 极罕见,需人工确认 |
| 合计 | 27,039,228 | 唯一化后的边总数 |
按长度而不是条数来算,差距更直观:共享边总长 978,012 千米,未配对边总长 1,083,063 千米,未配对边的长度占比 52.6%——和条数口径的 48.5% 基本一致,说明这不是被少数极短边拉偏的结果。
出问题的不是那些频次 1 的边本身。凡是落在县域外圈、海岸线、国界线上的边,本来就只属于一个要素,出现一次是对的。真正需要留意的是应该配对却没配上的那部分:它们出现在县域内部,是两个相邻乡镇之间的公共边,却各自带一套顶点。
import json, collections
def edge_key(a, b):
"""相邻两点量化到 1e-6 度后取无序键;两个方向视为同一条边"""
p = (round(a[0], 6), round(a[1], 6))
q = (round(b[0], 6), round(b[1], 6))
return (p, q) if p <= q else (q, p)
def scan(path):
d = json.load(open(path, encoding="utf-8"))
cnt = collections.Counter()
for f in d["features"]:
gm = f.get("geometry")
if not gm:
continue
polys = gm["coordinates"] if gm["type"] == "MultiPolygon" else [gm["coordinates"]]
for poly in polys:
ring = poly[0] # 只看外环,内环另算
for i in range(len(ring) - 1):
cnt[edge_key(ring[i], ring[i + 1])] += 1
shared = sum(1 for v in cnt.values() if v >= 2)
return len(cnt), shared, shared / len(cnt) if cnt else 0.0
同一个国家数据,共享比能从 97% 掉到 1.6%
把每个文件的共享边占比单独算出来排序,分布不是"普遍偏低"或"普遍正常",而是两极分化。同一份全国数据里,两种极端同时存在。
| 区县 | 要素数 | 唯一边数 | 共享边数 | 共享占比 |
|---|---|---|---|---|
| 贵州·黔西市 | 31 | 23,926 | 23,212 | 97.0% |
| 贵州·金沙县 | 25 | 18,107 | 17,190 | 94.9% |
| 湖北·蕲春县 | 16 | 13,687 | 12,964 | 94.7% |
| 贵州·大方县 | 37 | 15,800 | 14,794 | 93.6% |
| 湖南·醴陵市 | 38 | 13,315 | 12,384 | 93.0% |
| … | ||||
| 四川·射洪市 | 23 | 6,403 | 101 | 1.6% |
| 四川·大竹县 | 31 | 8,847 | 142 | 1.6% |
| 四川·金堂县 | 16 | 3,137 | 59 | 1.9% |
| 四川·威远县 | 14 | 3,429 | 40 | 1.2% |
| 四川·和布克赛尔蒙古自治县 | 15 | 2,043 | 5 | 0.24% |
黔西市和黔西县这两份文件内容完全一致(31 个要素、23,926 条唯一边、23,212 条共享),是同一轮数据在两个文件名下各存了一份,属于文件名层面的冗余,与拓扑无关。
真正值得看的是四川那一列。大竹县 31 个乡镇,唯一边 8847 条,共享的只有 142 条,占 1.6%;顶点层面同样,8352 个唯一顶点里只有 662 个被两个以上要素共用,占比 7.9%。再对比它逐要素的共享长度占比:四合镇 0.3%、天城镇 0.6%、月华镇 1.0%,最高的团坝镇也只有 7.8%。
这说明大竹县这 31 个乡镇的边界是各自独立矢量化出来的。相邻两个镇之间那条线,在这份文件里真实存在两份几何:一份属于左边那个镇,一份属于右边那个镇,两套顶点在数值上并不完全相同。
独立矢量化带来的三个后果
拼合会出缝或出重叠。 把两个相邻乡镇按 union 合并,如果公共边不是共用顶点,浮点运算下会在接缝处留下长度以米计的细缝,或者留下一条极窄的重叠带。用这条缝去算面积,误差可以忽略;但拿去做进一步的空间运算(缓冲、裁剪、再合并),缝隙会逐级放大,最后出现几何自相交。
邻接关系必须靠几何判定,不能靠顶点相等。 判断"哪两个乡镇相邻"最省事的做法是查是否有共享边。在黔西市这类文件上,这个方法直接可用,一个哈希表就出结果。在大竹县这类文件上,共享边判据几乎全部落空,必须退回 intersects 或按最小距离阈值判定,计算量从 O(n) 涨到 O(n²)。做 全国省市区县geojson数据下载 后续分析的,这一条直接决定代码怎么写。
同一个文件的两种约定不能混用。 更麻烦的是同一批数据里两种约定都存在的县。按要素共享长度占比排序后,一个县内往往前几个要素接近 0%、后几个接近 20%——说明该县一部分乡镇做过拓扑对齐,另一部分没有。这种情况下按统一阈值做邻接判定,会漏掉那些本来相邻但没对齐的要素对。
至于怎么处理,取决于用途:只做展示的,保留原样即可,视觉上看不出差别;要做拓扑运算的,得跑一遍节点对齐(把落入容差范围内的顶点吸附成同一坐标)或直接用拓扑修复工具重建。判据本身很好写——算一遍共享比,低于某个线(经验值 45% 以下)就说明这份文件没做过拓扑对齐,后续运算要走容错路径。这条规则对 GeoJSON下载 回来的任何一批乡镇级数据都成立,与数据出自哪个来源无关。
地域分布上这个比例有规律可循。按地级单位聚合,毕节市 91.5%、黔西南州 82.3%、黄冈市 75.9%、重庆市 73.6% 排在前列;而伊春市 9.0%、双鸭山市 8.0%、克拉玛依市 6.4% 垫底。需要说明的是,这个比值与地形碎度强相关:海岛县和沿海县因为外圈海岸线本身就占掉大量边,共享比天然偏低——烟台长岛县 8 个要素、505 条唯一边,共享边为 0;大连长海县 5 个要素、4353 条唯一边,同样为 0。这类不该被当成"未对齐",判定时要先把面积占比最大的那个要素识别成主岛,只对内部边界计分。
落到流程里的取用方式
如果只是需要某个市或某个区县的地图底图摆进材料,不必在拓扑上花时间。这类需求可以直接取现成的排版件——这里按省、市、区县三级列好了可单独改色的底图,一份 ¥4.99,预览先看不花钱,那里的分块在制作阶段就做过节点对齐,相邻两块拼起来不会有缝。
共享比这个指标的价值在于它几乎不花成本。一次全量扫描只读几何、不建索引,比算一遍面积还快,却能同时给出三个判断:这份 全国省市区县区划边界数据下载 得到的文件能不能直接做拓扑运算、邻接判定该走哪种算法、以及同一个县内部是否存在混合约定。等到逐个县跑完修复再回头定位问题,花的力气要大一个量级。
拓扑约定不统一这件事,在字段和要素数上完全看不出来,只有在算过共享边之后才现形。全国村级geojson数据下载 得到的村界在这一点上更明显——村面的独立矢量化比例通常高于乡镇,聚合到乡镇一级时若没做节点对齐,原先的接缝会原样保留下来。各级行政区划的口径,仍应以国家有关部门发布的为准。