第15章 - 矢量空间分析

矢量的”分析”和”显示”是两种完全不同的活。显示只要几何加上样式,分析要回答的是关系:谁在谁里面、谁离谁多远、两期数据在哪儿变了。GeoLibre 把这类问题收在 Processing → GeoLibre Toolbox → Vector 这一个对话框里——34 个工具,分七组,每个工具都有一个引擎选择器。

本章基于 GeoLibre 3.4。工具 id、参数 id、默认值与上限全部取自 packages/processing/src/vector-tools.ts、vector-tool-registry.ts、network-tools.ts、topology-tools.ts 与 statistics-tools.ts,并与 docs/user-guide/processing.md 的表格逐项核对。菜单与控件名保留英文原文。

15.1 两条实现路径与三个引擎

15.1.1 一个工具、三个引擎

同一个工具可以在三套代码上跑,对话框里叫 Engine:

引擎 实际执行者 安装要求 离线可用 处理投影
Client (Turf.js) 浏览器内的 Turf.js,吃图层的 GeoJSON 无 是 不处理,直接在经纬度上算
Sidecar (GeoPandas) 本地 Python sidecar 的 GeoPandas + Shapely 桌面版 + sidecar 的 vector extra 是 会重投影后再算
Python (Pyodide) 浏览器里的 Pyodide,跑同一份 GeoPandas 代码 首次下载几十 MB 的 Python 运行时 首次需联网 会重投影后再算

三条里最实用的判断是:凡是”距离”、”面积”、”周长”这类有量纲的量,就用后两个引擎。Client 引擎在 EPSG:4326 的经纬度上做缓冲区,1 个单位不是 1 米,而是 1 度——纬度 40 度处 1 度经度只有约 85 km,缓冲出来的形状是个被压扁的椭圆,越往北越扁。

注意:sidecar 不可用(没装 vector extra 或进程未启动)时不会静默回退:引擎下拉框保持 sidecar 并给出提示,Run 按钮被禁用。必须用侧车算的工具会提示改用 Pyodide,其余工具提示改用 Client——跑之前先确认引擎与提示,别假定”投影已经处理了”。

15.1.2 Pyodide 为什么值得单独提

Python (Pyodide) 是第三条路:同一份 GeoPandas/Shapely 代码,但完全跑在你的浏览器里。它解决了一个实际矛盾——Web 版没有 sidecar,但用户仍然需要投影感知的缓冲。

三件事要清楚:

  1. 首次运行的下载成本:Python 运行时几十 MB,从 CDN 懒加载。第一次必须有网。
  2. 之后复用:运行时预热后,后续运行不再下载。
  3. 结果与 Sidecar 一致:因为它跑的就是同一份 Python。

还有一个部署层面的取舍,官方文档写得很直白:默认从公共 jsDelivr CDN 加载,而 Pyodide 自己会再去加载 pyodide.asm.js 与 wasm——只对入口脚本做子资源完整性校验是不够的。生产或离线环境应该把 VITE_PYODIDE_INDEX_URL 指向自建的镜像。

15.1.3 250 000 这个数

Client 引擎里所有”两两比较”的工具都卡在同一个上限:

export const MAX_CLIENT_PAIRS = 250_000;

受影响的有 Intersection、Spatial join、Select by location。超限时工具不报错也不静默截断,而是打印:

Error: spatial join needs 1200000 comparisons (limit 250000); use the Sidecar engine for large layers

算一下就知道这个数多容易撞上:5000 个点 × 60 个多边形 = 30 万对,直接超。点对面做空间连接是最典型的撞墙场景。撞上了就换 Sidecar——它的 GeoPandas sjoin 有空间索引,不是朴素两两循环。

15.1.4 结果落在哪

工具类别 输出形态
几何、叠加、连接、选择 新地图图层(客户端是 GeoJSON,sidecar 结果同样加成图层)
Conversion 组 写文件,不加图层(见 第05章 数据添加与格式支持)

每次运行都记进 Processing → History,条目里有 Re-run、Edit & re-run、Copy JSON、Copy Python。

15.2 工具全景

15.2.1 七组 34 个工具

分组 工具
Geometry(17) Buffer、Centroids、Convex hull、Dissolve、Bounding box、Simplify、Reproject、Explode、Aggregate by attribute、Smooth、Extract vertices、Points along geometry、Regular grid、Voronoi / Delaunay、Cell-site coverage、Decode polyline、Encode line to polyline
Overlay(4) Clip、Intersection、Difference、Union
Join(2) Spatial join、Attribute join
Select(3) Select by value、Select by location、Random extract
Movement & time(3) Trajectory speed、Detect stops、Space-time proximity
Data management(1) Merge layers
Data quality(4) Check validity、Fix geometries、Check topology rules、Fix topology

同一份注册表还含 3 个 DGGS 工具(DGGS Generator / DGGS Binning / DGGS Compact),所以 VECTOR_TOOLS 一共 37 条。DGGS 属于离散全球网格体系的专题,本章不展开。

15.2.2 哪些工具挑输入几何

参数定义里带 geometryFilter 的工具会在图层下拉框里过滤——只列出符合条件的图层。选错了根本选不到。

工具 要求的输入几何
Clip / Intersection / Difference / Union 两个输入都必须是 polygon
Voronoi / Delaunay 输入必须是 point
Isochrone / OD matrix / Sequential route 必须都是 point
Aggregate by attribute 输入必须是 polygon

其余工具(Buffer、Centroids、Spatial join、Select by location、Merge layers 等)接受任意几何。

提示:Buffer 的输入不限几何,但 side 选 inside(内缩)时要求面输入。点或线没有内部可缩,工具会报错。

15.3 几何工具

15.3.1 Buffer:五个参数与五条校验

Buffer 是这个对话框里参数最全的工具,也最能说明”客户端与 Python 引擎必须给出同样的判定”这条设计原则。

参数 id 面板标签 类型 默认 说明
layer Input layer layer — 必填
distance Distance number 1 必填,min: 0,步长 0.1
units Units select kilometers kilometers / meters / miles
side Buffer side select outside Outside (grow) / Inside (shrink) / Both sides
dissolve Dissolve result boolean false 合并成一个要素并融掉重叠;属性会丢

校验顺序被刻意安排成”先单位、再方向、再 dissolve、最后距离”,源码注释解释了原因:

// Validate in the Python engine's order (`_buffer`: units, then side, then
// distance finiteness, then distance sign) so a call with several bad
// parameters at once gets the same first error from both engines, not just
// the same accept/reject verdict.

四种拒绝情形:

输入 日志
未知单位 Error: unknown unit '...'; expected kilometers, meters, miles
未知方向 Error: unknown buffer side '...'; expected outside, inside, both
dissolve 不是布尔 Error: buffer dissolve must be true or false
距离不是有限数或为负 Error: buffer distance must be a finite number

警告:distance 为负会被拒绝,不会当作”内缩”。方向归属 side,这是刻意的分工。程序化调用(Model Builder、历史记录重放、AI 助手)也走同一套校验——表单上的 min: 0 只约束表单,不约束调用方。

dissolve 的语义陷阱:勾上之后所有缓冲区融成一个无属性的要素。后续想按原要素属性分组统计就做不到了。要”按属性融合”,正确顺序是 Buffer(不勾 dissolve)→ 再跑 Dissolve(按字段)。

15.3.2 Centroids / Convex hull / Bounding box

三个”降维”工具,都只有一个输入参数:

工具 id 输入 输出
Centroids centroids 任意几何 每个要素一个中心点
Convex hull convex-hull 任意几何 整个图层一个凸包(不是每个要素一个)
Bounding box bounding-box 任意几何 整个图层一个矩形外包框

后两个的行为很多人第一次会误解:它们把所有要素一起包起来,产出一个要素。要”每个要素各自的外包框”,得先 Explode 或用 SQL 逐要素算。

15.3.3 Dissolve 与 Aggregate by attribute

工具 分组方式 统计量 输出属性
Dissolve 可选按一个字段分组 无 只保留分组字段
Aggregate by attribute 必填一个分组字段 count / sum / mean / min / max / median 分组字段 + 统计字段

Aggregate by attribute 的参数:

参数 id 面板标签 必填 说明
layer Input layer 是 只接受 polygon
group_field Group by field 是 分组字段
statistic Statistic 否 默认 count
stat_field Statistic field 条件必填 statistic 不是 count 时出现

stat_field 用 visibleWhen: { param: "statistic", notIn: ["count"] } 控制可见性——选 Count 时它隐藏,也就跳过了必填校验。这是全对话框里 visibleWhen 用得最典型的一处。

15.3.4 Simplify 与 Smooth:一对反向操作

  Simplify Smooth
算法 Douglas-Peucker Chaikin
顶点数 减少 增加
参数 tolerance(单位是度,默认 0.01)、highQuality(默认 false) iterations(默认值来自工具定义)
用途 减小文件、加快渲染 去掉折线的硬角、改善观感
Z 值 — 保留

tolerance 的单位是度,这是个必须记住的坑。0.01 度在赤道约 1.1 km,在纬度 60 度处经度方向只有约 550 m——同一个容差在不同纬度简化掉的细节量不同。

注意:Simplify 之后不能拿去做面积或长度统计。Douglas-Peucker 会系统性缩小面积(凸角被切掉)。要统计就先统计,要出图才简化。

15.3.5 顶点级工具

工具 id 做什么 参数
Explode explode 多部件几何拆成单部件,每部件一个要素,保留父要素属性 layer
Extract vertices extract-vertices 每个顶点变成一个点要素,保留父要素属性 layer
Points along geometry points-along-geometry 沿线(或面边界)按固定间距或固定数量布点 layer、interval、units
Decode polyline decode-polyline 把编码折线字符串解成 LineString 图层 layer、field、precision(5 或 6)
Encode line to polyline encode-polyline 把线几何编码成字符串写进新字段 layer、precision、targetField

Encode / Decode 这一对是给 Web 地图和路径 API 用的:Google 与 Valhalla 的折线编码能把一条几百个点的线压成几十个字符。precision 必须两边一致,5 和 6 混用会让整条线偏出去几十米。

15.3.6 Regular grid

生成渔网(fishnet),三个来源:

参数 id 面板标签 类型 默认
source Extent source select viewport(Map viewport / Layer extent / Manual bounding box)
layer Input layer layer 仅 source 为 layer 时可见
west / south / east / north West (min lon) 等 number 仅 source 为 bbox 时可见
cell_width Cell width (degrees) number 1,最小 0.0001
cell_height Cell height (degrees) number 留空则等于 cell_width
cell_type Cell type select polygon(Rectangles / Points (cell centers))

硬上限 GRID_HARD_CAP = 1 000 000 个格子。cell_width 填 0.001 度(约 100 m)去铺一个省,轻松超百万——超了直接拒绝,不会把标签页拖死。

提示:宽高单位都是度。要用米,先确认地图坐标系;这个工具没有投影感知版本,格子永远是经纬度格网。

15.3.7 Voronoi / Delaunay

一个工具、两种产物:

参数 值 输出
type = voronoi 默认 每个点一个多边形,裁剪到点的外包范围
type = delaunay — 三角网

输入必须是点图层(geometryFilter: ["point"])。Voronoi 的”裁剪到外包范围”这一句很重要:不裁剪的 Voronoi 会无限外扩,图上看起来像一张失控的网。裁剪后它才是一个可用的”最近点服务区”近似。

注意:Voronoi 是欧氏距离的划分,不是路网距离。拿它当”最近门店”的近似可以,当”最近门店的可达范围”不行——中间隔着河或高速时结论完全相反。可达范围要用 15.7 的等时圈。

15.3.8 Cell-site coverage

从点站点生成天线扇区面,用于通信覆盖:

参数 说明
layer 站点图层
azimuthField / azimuth 方位角,可从字段读或填固定值
radiusField / radius 半径,同上
beamwidthField / beamwidth 波束宽度,同上
units 长度单位

每一组都是”字段优先、固定值兜底”的结构——字段有值就用字段,字段为空就用固定值。这让你可以在同一批站点里混合使用逐站参数与统一默认值。

15.4 叠加分析:四个工具,三种属性策略

15.4.1 一张表看清区别

工具 几何结果 输入属性 叠加层属性 引擎调用
Clip 输入被叠加层裁掉外面的部分 保留 丢弃 每个输入要素与合并后的叠加几何求交
Intersection 两层的公共部分 保留 合并保留 逐对求交(输入数 × 叠加数)
Difference 输入减去叠加层 保留 丢弃 每个输入要素与合并后的叠加几何求差
Union 两层合并成一个几何 不保留(两个引擎都不保留) 不保留 —

这张表是本章最该记住的东西。Clip 和 Intersection 的差别不在几何,而在属性:Clip 只带输入层的字段,Intersection 把两边的字段合在一起。所以”我需要知道每个碎片属于哪个区”要用 Intersection,”我只要研究区内的地块”用 Clip 就够,还省一半字段。

15.4.2 参数与几何过滤

四个工具的输入参数结构一致:

工具 参数
Clip layer(Input layer)、overlay(Overlay (clip) layer)
Intersection layer(Input layer)、overlay(Overlay layer)
Difference layer(Input layer)、overlay(Overlay layer)
Union layer(Input layer)、overlay(Overlay layer)

四个参数都带 geometryFilter: ["polygon"],两层都必须是面。想用面裁线,得走 Select by location(within/intersects)或先在 SQL 里做。

15.4.3 Clip 与 Difference 的共用引擎

Clip 与 Difference 用的是同一个内部函数 overlay,差别只在传进去的算子:

function overlay(ctx, op, resultName, keepProperties) { ... }
// Clip:      intersect(featureCollection([a, b]))
// Difference: difference(...)

关键在 keepProperties 分支:result.properties = keepProperties ? (feature.properties ?? {}) : {}。Clip 与 Difference 传 true,所以属性来自输入要素。这也解释了为什么叠加层的字段一定丢——合并后的几何根本对应不到任何单个叠加要素。

15.4.4 Intersection 的 1:N 语义

Intersection 不走上面那个共用引擎,因为它要保留两边属性:

for (const a of inputPolys) {
  for (const b of overlayPolys) {
    const piece = intersect(featureCollection([a, b]));
    if (piece?.geometry) {
      piece.properties = { ...(a.properties ?? {}), ...(b.properties ?? {}) };
      results.push(piece);
    }
  }
}

两个后果:

  1. 一个输入要素可能产出多个碎片(如果它横跨多个叠加要素)。输出要素数可以大于输入要素数。
  2. 合并属性时叠加层的同名键会覆盖输入层的(展开顺序决定)。要防止字段打架,跑之前先把重名字段改名。

15.4.5 Union 为什么不保属性

Union 把两层并成一个整体几何,中间的分界线被抹掉。既然分界线没了,”这个碎片原来属于哪一层”这个问题就没有答案了——属性不保留是几何操作的必然结果,不是实现偷懒。需要按来源区分,用 Intersection 再补一层 Difference,或者直接用 Merge layers(它有一个 addSourceField)。

15.5 连接与选择

15.5.1 Spatial join

参数 id 面板标签 类型 默认
layer Input layer layer —
overlay Join layer layer —
predicate Spatial relationship select intersects(Intersects / Within / Contains)
how Join type select inner(Inner / Left)

输入几何不限——这是它和四个叠加工具最大的区别,点、线、面都能连。

三条行为细节,全部来自源码:

  1. 一个输入要素匹配多个连接要素时,输出多个要素(与 GeoPandas sjoin 一致)。这就是”1:N 连接会让行数膨胀”。
  2. 同名字段冲突时输入层胜出:{ ...(match.properties ?? {}), ...(feature.properties ?? {}) }——注意展开顺序,输入层在后面。源码注释特别指出 sidecar 的做法不同(它会给重名字段加 _left/_right 后缀)。所以同一个操作在 Client 与 Sidecar 引擎上,重名字段的处理不一样,跨引擎对比结果时要留意。
  3. Left join 会把连接层的所有字段补成 null,而不是干脆不出现。这样输出 schema 与匹配行一致,也和 GeoPandas 的 NaN 行为对齐。

15.5.2 Attribute join

纯表连接,不碰几何。参数比 Spatial join 多:

参数 id 面板标签 必填 默认
layer Target layer 是 —
overlay Join layer (table) 是 —
target_field Target key field 是 —
join_field Join key field 是 —
how Join type 否 left
fields Fields to bring over (optional) 否 空(带全部非键字段)

注意两个默认值与 Spatial join 相反:这里是 left join 为默认,且没有 predicate。

键的匹配规则是最容易出意外的地方,源码注释把边界列得很清楚:

情形 匹配结果
任一侧为 null / undefined / NaN / 空字符串 永不匹配(等价于 SQL/pandas 的 NaN 键)
数值 5 与字符串 "5" 匹配(键统一字符串化)
"01001" 与 "1001" 不匹配(保留前导零)
多条连接行命中同一个键 第一条胜出(一对一)

最后一条是很硬的限制:这是一对一连接,不是”一对多展开”。要一对多,用 Spatial join,或去 SQL 里写 JOIN。

fields 是一个逗号分隔的字段名列表,留空则带入除键字段外的所有连接字段。

15.5.3 Select by value

参数 类型 默认 说明
field field — 必填
operator select eq 见下表
value string — 除两个”空值判断”外必填

十个操作符:eq(=)、neq(≠)、gt(>)、gte(≥)、lt(<)、lte(≤)、contains(文本包含)、starts-with(文本前缀)、is-null(为空)、is-not-null(不为空)。

value 的可见性由 visibleWhen: { param: "operator", notIn: ["is-null", "is-not-null"] } 控制,所以选”为空”时不会要求你填一个值。

比较规则一句话:两边都能解析成数字时按数字比,否则按文本比。所以 "10" > 9 为真(数字比较),但 "10" > "9" 为假(字典序,因为有一位字符更小)。字段类型混杂时这条规则会让人抓狂——先用属性表看清字段类型。

15.5.4 Select by location

参数 可选值
layer 待筛选图层
overlay 筛选用的图层
predicate intersects / within / contains / disjoint(不相交)

比 Spatial join 多一个 disjoint——它选的是”与筛选层完全没有交集”的要素。空筛选层时 disjoint 保留全部,其余三种一个不留,这个边界行为在源码注释里专门写明。

与 Spatial join 的关键区别:Select by location 只筛要素,不增字段。行数不变(除了被筛掉的),schema 不变。想加字段用 Spatial join。

15.5.5 Random extract

参数 默认 说明
method count Number of features / Percentage of features
count 10 超过输入要素数时自动截断
percentage 10 0–100

用途是抽样检查与训练样本抽取。注意它没有随机种子参数——同一份数据跑两次会得到不同的子集。需要可复现的抽样,去 SQL 里用 ORDER BY random() LIMIT n(同样不可复现)或者自带种子字段。

15.6 坐标系、容差与结果偏差

15.6.1 三个容差参数,三种单位

这是本章最容易出错的地方——三个工具都有”容差”,但单位各不相同:

工具 参数 单位 默认
Simplify tolerance 度 0.01
Check topology rules / Fix topology snapTolerance 图层坐标单位(WGS84 时是度) 0.0001
Regular grid cell_width / cell_height 度 1

snapTolerance 的描述写得很明确:in layer coordinate units — degrees for WGS84。如果图层在投影坐标系里,0.0001 就是 0.0001 米——小到几乎不起作用,你得改成米制的合理值(例如 0.5 米的捕捉容差)。

警告:把 WGS84 图层的 snapTolerance 0.0001 直接搬到投影坐标图层上,等于”不捕捉”。拓扑修复会看起来毫无效果。

15.6.2 距离:客户端与 GeoPandas 的分歧点

操作 Client (Turf.js) Sidecar / Pyodide (GeoPandas)
Buffer 距离 在经纬度上按度算(Turf 会做球面近似,但不是真投影) 先重投影到本地 UTM 带(estimate_utm_crs)再算
面积 Turf 用球面公式 投影后平面计算
结果差异 高纬度、大范围时显著 小范围时与真值接近

实践建议:凡是要交付的面积或距离数字,一律用 Sidecar 或 Pyodide。Client 引擎的价值在于”快速看到形状对不对”。

15.6.3 简化与拓扑的先后

顺序错了会做出”看起来对、其实碎了”的数据:

顺序 结果
先 Simplify 再拓扑检查 简化会制造新的自相交与缝隙,检查通过不代表原数据干净
先拓扑检查与修复,再 Simplify 正确顺序
Simplify 后再做叠加 边界不再严丝合缝,Intersection 会出现大量细碎碎片

15.6.4 投影选择速查

分析 推荐投影
缓冲区(米制距离) 所在 UTM 带 / 地方平面坐标
面积统计 Albers 等积
沿线布点(米制间距) 等距投影或 UTM
最近邻 / Voronoi 尽量用等距投影,否则高纬度会偏向东西向
空间连接(拓扑关系) 影响很小,但两层必须同 CRS

最后一格是硬要求:空间连接与叠加的两个输入图层必须在同一个 CRS 里。GeoLibre 的图层通常统一显示在 Web Mercator,但工具读的是图层自身的 GeoJSON 坐标——两层 CRS 不一致时,几何根本对不上,结果会是”什么都没匹配上”,而不是报错。

15.7 网络分析

15.7.1 等时圈 / 服务区

参数 id 面板标签 类型 默认
layer Origin points layer(仅点) —
mode Travel mode select auto(Driving / Walking / Cycling)
metric Metric select time(Travel time (minutes) / Distance (km))
contours Contours string "5,10,15"
endpoint Routing server (Valhalla) string 空(运行时取全局配置)

contours 是逗号分隔的数值:metric 为时间时单位是分钟,为距离时单位是公里。

三条硬上限:

常量 值 含义
MAX_ISOCHRONE_POINTS 25 单次最多 25 个起点,超出只取前 25 个并打日志
MAX_ISOCHRONE_CONTOURS 4 Valhalla 单请求最多 4 个等值线
MAX_ROUTE_POINTS 20 顺序路径最多 20 个途经点(公共 FOSSGIS 服务的 max_locations 就是 20)

15.7.2 OD 成本矩阵

参数 说明
origins 起点图层(仅点)
destinations 终点图层(仅点)
mode 出行方式
endpoint Valhalla 地址

输出是连接线,每条线带该 OD 对的行程时间与距离。

上限是 MAX_MATRIX_CELLS = 2500,即 起点数 × 终点数 ≤ 2500。超了报:Error: 60 × 60 = 3600 pairs exceeds the 2500-cell limit. Reduce the input sizes.

提示:2500 这个数决定了用法——它适合”20 个门店 × 100 个小区”,不适合”全城网格”。大规模可达性分析应该自己做路网分区,或者用等时圈叠加代替。

15.7.3 顺序路径

参数 说明
layer 途经点图层(仅点)
order_field 决定顺序的字段
mode 出行方式
endpoint Valhalla 地址

输出每条 leg 只有 leg_index、from_id、to_id、time_s、distance_km、mode,不带逐段导航指令;需要转向提示时用单独的 Navigation(turn-by-turn)工具。超过 20 个点会被截断并打日志——不是报错,是静默降级,留意日志。

15.7.4 谁在算这笔账

网络分析不在浏览器里算,它请求一个 Valhalla 服务器。默认是公共 FOSSGIS 实例(valhalla1.openstreetmap.de)。

两件事必须知道:

  1. 你提交的坐标会离开你的设备。 官方文档在右键快捷入口处明确提示了这一点,第一次使用会请求同意。
  2. 自托管可以改。 部署时可指向自己的 Valhalla 服务,见 第22章 服务端、自托管与部署运维。

15.7.5 右键快捷入口

菜单项 行为
Buffer … here 三个距离预设;预设值跟随 Settings 里的 Scale bar units:米制是 500 m / 1 km / 5 km,英制是 0.25 mi / 1 mi / 5 mi
Drive time from here 5 / 10 / 15 分钟驾车等时圈
Walk time from here 同样三条等值线,步行
Open in Processing… 打开完整对话框,暴露预设没给的参数

同一套 Quick analysis 也出现在图层行菜单里,但对整层操作,只有四项:Buffer all features by …、Centroids、Convex hull、Bounding box。

15.8 空间统计

15.8.1 七个工具

Processing → GeoLibre Toolbox → Spatial Statistics:

工具 回答的问题
Global Moran’s I 这个数值属性在整层上是聚集、离散还是随机?
Local Moran’s I (LISA) 逐个要素看:高高聚集、低低聚集、还是空间离群点?
Getis-Ord Gi* hotspots 逐个要素看热点与冷点,带显著性等级
Average nearest neighbor 点格局比随机更聚集还是更分散?
Kernel density (heatmap) 从点图层生成连续密度面
Emerging Hot Spot (space-time cube) 时空立方体上的热点趋势
Composite score (suitability index) 多个数值字段归一化加权成一个指数

15.8.2 Composite score 的六个决策

这是唯一一个”参数本身就是方法论”的工具。它的六个选择直接决定结果的含义:

决策 选项 影响
归一化方式 min-max / z-score / rank / quantile 决定原始值如何映射到可比尺度
相对权重 任意正数 界面上同时显示”占总权重的比例”,权重效果可读
好的一端 每字段各自指定 有的指标越大越好,有的越小越好
组合方式 加权算术平均 / 加权几何平均 算术平均允许一项强项补偿弱项;几何平均不允许
缺失值处理 丢弃该要素 / 在已有字段上重新归一化权重 无默认值,必须明确选
保留归一化分量 开 / 关 开启后每个字段的归一化值单独成列,评分可审计

输出刻度两个选项:0–100(默认,适合直接做图例)与 0–1(分数还要参与后续计算时用)。字段名由 Score field name 决定。

为什么缺失值没有默认值:官方文档写得很直接——两种做法都会产出看起来合理的图,但只有一种是你要的那个指数。丢弃会让某些区域成空洞,重归一化会让同一地区的评分随缺失组合变化。

关于 rank 与 quantile 归一化有一个细节:并列值共享秩(mid-rank 约定),所以相等的输入一定得到相等的分数。

提示:结果落地时已经按分数做了分级渲染。要复核,打开 Keep normalized components,然后在属性表里对几个要素手工算一遍加权和——如果对不上,多半是某个字段的”好的一端”设反了。

15.8.3 空间统计的适用范围

方法 前提假设 违反会怎样
Global / Local Moran’s I 空间权重矩阵合理;属性有意义的邻接关系 用不合适的邻接定义会得到”显著但无意义”的结果
Getis-Ord Gi* 同上 同上
Average nearest neighbor 研究区边界明确 边界不闭合时结论不可靠
Kernel density 带宽选择 带宽过小成噪声,过大抹平结构
时空立方体 时间切片等间隔 不等间隔会让趋势判断失真

15.9 数据质量与拓扑

15.9.1 两级质量检查

工具 id 查什么 输出
Check validity check-validity 几何本身是否合法(自相交的环、洞跑到壳外…) 在地图上标出问题要素
Fix geometries fix-geometries 用 ST_MakeValid 修复 合法要素原样通过
Check topology rules check-topology-rules 要素之间的拓扑规则 违规要素图层 + 按规则分类的计数
Fix topology fix-topology 自动修复可修的违规 修复后的图层(或 dry run 报告)

区别很重要:Check validity 看单个要素,Check topology rules 看要素之间。一块多边形可以自身完全合法,但它和邻居重叠了 3 米——只有后者能发现。

15.9.2 六条拓扑规则

规则 id 面板标签 默认
line_must_not_self_intersect Lines must not self-intersect 开
polygon_must_not_overlap Polygons must not overlap 开
polygon_must_not_have_gaps Polygons must not have gaps 开
line_must_not_have_dangles Lines must not have dangles 关
point_must_be_covered_by_line Points must be covered by a line 关
line_endpoints_must_snap_within_tolerance Line endpoints must connect within tolerance 关

前三条默认开,后三条默认关,原因在源码注释里:连通性规则必须手动开启,否则任意图层上每一个自由端点、每一个孤立点都会被报成违规——一个完全正常的道路网会有成百上千个自由端。

snapTolerance 默认 0.0001,只对端点规则生效。

15.9.3 Fix topology 只能修三种

FIXABLE_TOPOLOGY_RULES 明确只有三条:

面板标签 规则 默认
Snap nearly-connected line endpoints line_endpoints_must_snap_within_tolerance 开
Repair dangles (snap or project onto nearby geometry) line_must_not_have_dangles 开
Project points onto the nearest line point_must_be_covered_by_line 关

而 polygon_must_not_have_gaps 被引擎接受但从不产生任何修改,所以刻意不在 Fix topology 的选项里出现——它的注释写得很清楚:accepted by the engine but never produces changes, so it is not offered here。

还有一个 Preview only (dry run) 选项:只报告会做哪些修复,不产出修复后的图层。大范围修复前先 dry run 一次,否则你会在一个 10 万要素的图层上点下去然后等很久。

修复的原则是保守的:附近没有东西的自由端会被原样留下。它不会为了”修好”而把线拉到很远的地方。修复后要把结果和原图层叠一遍确认没有意外位移——日志会输出变更数量的尾部(最多 20 行)。

15.10 批处理与模型串联

需求 工具 说明
同一工具套多个图层 Batch tools 的 Batch 模式 客户端引擎
多工具串成链并随项目保存 Batch tools 的 Models 模式 客户端引擎
有分叉的可视化流程 Model Builder 见 第13章 模型构建器与自动化工作流
关系运算与聚合 SQL Workspace 见 第11章 SQL 工作区与 DuckDB 空间分析
循环与条件 Python 控制台 / Notebook 见 第19章 Python 控制台、Notebook 与 Jupyter 集成

Model Builder 的矢量工具节点取的就是本章这 37 个工具。意味着本章的任何工具都可以变成模型里的一个节点,且 type: "layer" 参数会自动变成输入端口(见第 13 章 13.5.2)。

15.11 精度、性能与结果校验

15.11.1 性能上限总表

限制 值 影响工具
MAX_CLIENT_PAIRS 250 000 Intersection、Spatial join、Select by location
GRID_HARD_CAP 1 000 000 格 Regular grid
MAX_ISOCHRONE_POINTS 25 Isochrone
MAX_ISOCHRONE_CONTOURS 4 Isochrone
MAX_MATRIX_CELLS 2 500 OD cost matrix
MAX_ROUTE_POINTS 20 Sequential route

15.11.2 七个校验动作

检查 怎么做 不通过说明
要素数是否合理 属性表底部计数,与输入对比 1:N 连接会让数变大,叠加会让数变成碎片
几何类型 图层信息 Clip 出 MultiPolygon 很正常,出 GeometryCollection 说明有退化几何
面积总和 与输入面积比 Difference 后应减小,Intersection 应小于两者中较小者
空几何 Select by value 里用条件筛 null 空几何会在导出时炸掉
字段完整性 属性表列头 同名字段被覆盖是 Spatial join 的已知行为
空间范围 Zoom to layer 结果跑到另一个大陆多半是 CRS 不一致
引擎一致性 同一操作分别用 Client 与 Sidecar 跑 差异过大说明单位或投影问题

15.11.3 五个常见错误

现象 原因 修法
缓冲区形状被压扁、越北越扁 用 Client 引擎在经纬度上按度缓冲 换 Sidecar / Pyodide
“needs N comparisons (limit 250000)” 两两比较超限 换 Sidecar,或先聚合减少要素数
空间连接全都没匹配上 两层 CRS 不一致 统一 CRS 后重跑
Intersection 产出大量微碎片 两层边界不严丝合缝 先做拓扑修复与捕捉
属性连接大量未匹配 一侧有前导零/数值与文本混用 统一键字段类型

15.12 两个完整案例

15.12.1 案例一:商圈覆盖分析

问题:某连锁有 12 家门店,想知道每家门店 15 分钟步行范围内覆盖了多少个居住小区、多少人口。

输入:stores(点,12 个)、communities(面,带 population 字段)。

步骤:

  1. 等时圈。Processing → GeoLibre Toolbox → Network → Isochrone / service area。
    • layer = stores
    • mode = Walking
    • metric = Travel time (minutes)
    • contours = 15
    • Run → 得到 Isochrones 图层,12 个面。

    注意:12 个起点在 25 的上限内,没问题。但如果门店超过 25 家,工具只会取前 25 个并打一行日志——它不是分批自动跑。

  2. 空间连接。Processing → GeoLibre Toolbox → Vector → Spatial join。
    • layer = communities(要保留小区属性)
    • overlay = 等时圈图层
    • predicate = Intersects
    • how = Inner(只要被覆盖的)
    • Engine = Sidecar (GeoPandas)(小区数 × 12 很容易撞 25 万上限)

    提示:一个小区可能同时落在两个门店的等时圈里。Inner join 会为每个匹配产出一行,所以这是预期的重复,不是 bug——但它意味着你不能直接把 population 求和,否则重叠区会被算两次。

  3. 去重。这一步是很多人漏掉的。用一个能区分门店的字段(等时圈图层会带来源点 id)分组:
    • 若只需”被任意门店覆盖的小区”,用 Dissolve 或 SQL 的 SELECT DISTINCT 按小区 id 去重。
    • 若要”每个门店各覆盖多少”,则保留重复,按门店 id 分组求和。
  4. 统计。Processing → GeoLibre Toolbox → Vector → Aggregate by attribute:
    • group_field = 门店 id 字段
    • statistic = Sum
    • stat_field = population

    或在 SQL Workspace 里:

    SELECT store_id,
           count(DISTINCT community_id) AS communities,
           sum(population)              AS population
    FROM joined
    GROUP BY store_id
    ORDER BY population DESC;
    
  5. 验证。把去重后的小区总数与 communities 总数对比。如果”被覆盖小区数”超过了总小区数,说明第 3 步没做。

这条案例的三个坑:

坑 表现 原因
人口被重复计 12 家门店覆盖人口之和远大于全城人口 重叠区在 1:N 连接里出现多次
步行等时圈奇形怪状 出现飞地、跨越河道 Valhalla 用的是真实路网与人行桥,形状本来就不规则
小区只被部分覆盖 边界上的小区被整块计入 空间连接是按”是否相交”判断,不做面积加权

第三条要更精确的话,得走 Intersection(小区 ∩ 等时圈)再用 population × 面积占比 加权——但那需要先算面积,属于下一节的技能。

15.12.2 案例二:土地利用变化叠加

问题:有两期土地利用面(lu_2015、lu_2020),都带 code 字段。要产出”变化图斑”(2020 与 2015 类型不同的地方),并统计各转换类型的面积。

步骤:

  1. 统一 CRS。确认两期数据在同一投影坐标系(这是后续面积统计的前提)。不一致就先 Reproject(需要 Sidecar 或 Python 引擎)。

  2. 求公共区域。Intersection:
    • layer = lu_2020
    • overlay = lu_2015
    • Engine = Sidecar(两期各几千个面,两两相乘轻松超 25 万)

    输出每个碎片同时带两期的属性。注意同名字段 code 会冲突——Client 引擎下输入层(2020)胜出,Sidecar 引擎下会加 _left/_right 后缀。跑之前先把字段改名为 code_2020 与 code_2015,这是最省事的做法。

  3. 筛出变化。Select by value:
    • field = code_2020
    • operator = neq
    • value = … 这里有个问题:neq 只能和一个固定值比,不能比两个字段。

    两个字段互比需要走 SQL Workspace:

    CREATE TABLE changed AS
    SELECT *, ST_Area(geometry) AS area_m2
    FROM lu_overlay
    WHERE code_2020 <> code_2015;
    

    或者用 Edit → Select by Expression(按表达式选),再另存为新图层。

  4. 算变化方向的面积。按转换类型分组:

    SELECT code_2015, code_2020,
           count(*)          AS patches,
           sum(ST_Area(geometry)) AS area_m2
    FROM changed
    GROUP BY code_2015, code_2020
    ORDER BY area_m2 DESC;
    
  5. 做变化图。把 changed 建成图层,按 code_2015 || '→' || code_2020 做一个分类着色,或者只高亮”耕地转建设用地”这类重点转换。

  6. 验证:
    • 变化图斑总面积应远小于研究区总面积(通常 5%–20%)。
    • 各转换类型的面积之和应等于变化总面积(误差小于 1%)。
    • 随机抽 10 个图斑,回到影像上目视核对。

这条案例的四个坑:

坑 表现 原因
同名字段互相覆盖 只剩一个 code Intersection 合并属性的展开顺序
出现大量 1 平方米的碎片 图斑数是预期的几十倍 两期边界不严丝合缝,Intersection 在缝隙处切出微片
面积对不上 分组求和与总数不等 混合了不同 CRS 或不同面积单位
变化图斑包含”伪变化” 实际没变的地方被标成变化 两期分类体系不一致(三期分类 vs 二期分类)

第二条的修法:先对两期数据做 Check topology rules(开 polygon_must_not_overlap 与 polygon_must_not_have_gaps),用 Fix topology 的 snapTolerance 把边界对齐;或者在 Intersection 之后用 SQL 过滤掉面积小于某个阈值的碎片。

警告:“伪变化”是土地利用变化分析最大的陷阱。两期数据的分类体系、最小成图单元、边界勾绘精度只要有一项不同,就会产生大量”技术性变化”。做统计之前先看两期的分类代码表——如果 2015 有 12 类而 2020 有 8 类,那么大部分”变化”其实是分类口径变了。

15.13 本章小结

  • 矢量工具集中在 Processing → GeoLibre Toolbox → Vector,七组共 34 个(加上 3 个 DGGS 工具共 37 条注册表条目)。
  • 三个引擎:Client (Turf.js) 不做投影、Sidecar (GeoPandas) 会重投影、Python (Pyodide) 在浏览器里跑同一份 GeoPandas 代码。凡有量纲的量用后两个。
  • Sidecar 不可用时不会自动回退:提示后 Run 被禁用,需手动改选 Client 或 Pyodide。
  • 250 000 是 Client 引擎两两比较的硬上限,影响 Intersection、Spatial join、Select by location。撞上就换 Sidecar。
  • 四个叠加工具的区别主要在属性:Clip 保留输入属性、Intersection 合并两边属性、Difference 保留输入属性、Union 两边都不保留。
  • Spatial join:1:N 会膨胀行数;同名字段输入层胜出(Client)而 Sidecar 加 _left/_right 后缀;Left join 把连接字段补 null。
  • Attribute join 是一对一(首行胜出),键的规则是”空值永不匹配、数值与数字字符串互通、保留前导零”。
  • 三个”容差”单位不同:Simplify 的 tolerance 是度,拓扑工具的 snapTolerance 是图层坐标单位,Regular grid 的格子是度。
  • 网络分析靠外部 Valhalla 服务(默认公共 FOSSGIS 实例,坐标会离开设备)。上限:25 个起点、4 条等值线、2500 个 OD 格、20 个途经点。
  • 网络分析的三条语义:起点只接受点图层;contours 用逗号分隔(时间=分钟,距离=公里);OD 输出是带时间与距离的连接线。
  • Composite score 的六个决策里,缺失值处理没有默认值——必须明确选”丢弃”还是”重归一化”,两者产出的图都”看起来合理”但含义不同。
  • 拓扑检查分两级:Check validity 看单个几何,Check topology rules 看要素之间。六条规则里前三条默认开,三条连通性规则默认关。
  • Fix topology 只能修三种(端点捕捉、悬挂点、点投影到线);polygon_must_not_have_gaps 被接受但从不产生修改,所以没被提供。
  • 顺序铁律:先拓扑修复,再简化与叠加。反过来会制造新的拓扑问题与大量碎片。
  • 两个输入图层必须在同一 CRS。不一致时的表现是”一个都没匹配上”,而不是报错。

15.14 实践练习

  1. 三引擎对照:对同一个面图层做 1 公里缓冲区,分别用 Client (Turf.js)、Sidecar (GeoPandas)、Python (Pyodide) 各跑一次。把三个结果叠在一起,量出最大偏差出现在哪个纬度、哪个方向上,并用自己的话解释原因。

  2. 撞上限:造一个 800 点的点图层与一个 500 面的面图层,跑 Spatial join(Client 引擎),记录报错文本。再跑一次 Select by location(Client),看它是否也在同一处失败。最后换 Sidecar 重跑,记录耗时。

  3. 属性策略验证:用同一对图层分别跑 Clip、Intersection、Difference、Union,列出四次输出的字段列表。特别确认 Union 是否真的一个属性字段都不剩,并解释为什么这在几何上是必然的。

  4. 键匹配实验:造一个小表,键字段里混入 5(数值)、"5"(文本)、"05"、"01001"、空字符串、null。用 Attribute join 连到另一张只有 "5" 与 "01001" 的表上,记录哪些行匹配成功,并对照 15.5.2 的规则表验证。

  5. 容差单位陷阱:取一个 WGS84 的道路线图层和它的投影版本(例如 UTM),对两者都跑 Check topology rules(只开端点捕捉规则),snapTolerance 都用默认 0.0001。比较两次报出的违规数量,并算出要让投影版本得到相同捕捉效果,容差应该是多少。

  6. 完整商圈分析:用 15.12.1 的流程,但改成 Drive time 10 分钟,并且要求”每个小区只计入最近的那家门店”。写出你的去重规则(提示:需要先算小区中心到门店的距离,或者用 Voronoi 做最近门店划分,再与等时圈取交集),并说明这个规则在什么情况下会给出错误结论。

15.15 延伸阅读