znlgis 博客

GIS开发与技术分享

第12章:数据质检(QC)步骤详解

本章全面介绍 GeoPipeAgent 的 10 个数据质检步骤。这些步骤采用独特的”Check and Passthrough”模式——输入数据原样通过,质检问题以 QcIssue 对象形式收集。本章逐一剖析每个 QC 步骤的参数、检测逻辑和源码实现,并通过组合实战演示如何构建完整的质检流水线。


12.1 数据质检概述

12.1.1 QC 步骤清单

GeoPipeAgent 提供 10 个 QC 步骤,覆盖矢量和栅格两大数据类型:

矢量质检步骤(7 个):

步骤 ID 功能 检查目标
qc.geometry_validity 几何有效性检查 自相交、空几何、环方向
qc.crs_check 坐标参考系检查 CRS 存在性和正确性
qc.topology 拓扑关系检查 重叠、缝隙、悬挂节点
qc.attribute_completeness 属性完整性检查 必填字段空值/缺失
qc.attribute_domain 属性值域检查 字段值域和正则匹配
qc.value_range 数值范围检查 数值字段越界
qc.duplicate_check 重复要素检查 几何/属性重复

栅格质检步骤(3 个):

步骤 ID 功能 检查目标
qc.raster_nodata NoData 一致性检查 NoData 值和比例
qc.raster_resolution 分辨率一致性检查 像素尺寸一致性
qc.raster_value_range 栅格值域检查 像素值范围

12.1.2 “Check and Passthrough” 模式

QC 步骤的核心设计模式是“Check and Passthrough”(检查并通过):

                          ┌─────────────────┐
                          │   QC 步骤执行    │
                          │                 │
  输入数据 ──────────────▶│  1. 执行检查     │──────────────▶ 输出数据
  (GeoDataFrame)          │  2. 收集问题     │               (原样输出)
                          │  3. 数据原样通过  │
                          │                 │
                          └───────┬─────────┘
                                  │
                                  ▼
                          ┌─────────────────┐
                          │  $step.issues    │
                          │  (QcIssue 列表)  │
                          └─────────────────┘

关键特征:

特征 说明
数据透传 输入数据不做任何修改(除 auto_fix 为 true 时),直接作为步骤输出
问题收集 所有质检问题存储在 StepResult.issues 属性中
不中断流水线 即使发现问题,流水线也继续执行(除非配合 on_error 使用)
问题可引用 后续步骤可通过 $steps.{step_id}.issues 引用质检结果

12.1.3 _helpers.py 工具函数

QC 步骤共享 _helpers.py 中的工具函数来构建统一格式的步骤结果:

# geopipeagent/steps/qc/_helpers.py

def build_qc_result(gdf, issues, stats=None):
    """构建 QC 步骤的标准返回结果

    Args:
        gdf: 原始输入数据(透传)
        issues: QcIssue 列表
        stats: 额外统计信息

    Returns:
        StepResult: 包含透传数据和质检问题
    """
    issues_gdf = _issues_to_geodataframe(issues, crs=gdf.crs)

    base_stats = {
        "feature_count": len(gdf),
        "issue_count": len(issues),
        "error_count": sum(1 for i in issues if i.severity == "error"),
        "warning_count": sum(1 for i in issues if i.severity == "warning"),
    }
    if stats:
        base_stats.update(stats)

    return StepResult(
        output=gdf,          # 数据原样透传
        stats=base_stats,
        issues=issues,       # 问题列表
    )

12.2 QcIssue 数据模型详解

12.2.1 数据类定义

QcIssue 是所有质检问题的统一数据模型,使用 Python dataclass 定义:

from dataclasses import dataclass, field
from typing import Any

@dataclass
class QcIssue:
    rule_id: str                        # 规则标识
    severity: str                       # 严重级别
    feature_index: int | None = None    # 要素索引
    message: str = ""                   # 问题描述
    geometry: Any = None                # 问题几何
    details: dict = field(default_factory=dict)  # 扩展信息

12.2.2 字段详解

字段 类型 说明 示例
rule_id str 质检规则标识,对应步骤 ID "geometry_validity"
severity str 严重级别:error/warning/info "error"
feature_index int\|None 出错要素的行索引,栅格检查时为 None 42
message str 人类可读的问题描述 "Self-intersection at (116.3, 39.9)"
geometry Any 问题所在位置的几何,可为点/线/面 Point(116.3, 39.9)
details dict 扩展详情字典,存放额外的诊断信息 {"field": "name", "value": None}

12.2.3 severity 级别含义

严重级别层次:

  ┌──────────┐
  │  error   │  ← 数据错误,必须修复
  ├──────────┤     例:自相交多边形、CRS 缺失
  │ warning  │  ← 数据异常,建议检查
  ├──────────┤     例:属性空值、值接近边界
  │   info   │  ← 提示信息,仅供参考
  └──────────┘     例:重复要素(可能是合理的)
级别 含义 对流水线的影响
error 严重数据错误 可配合 on_error: fail 终止流水线
warning 潜在问题 记录但不终止
info 信息提示 仅记录到报告

12.2.4 QcIssue 序列化

QcIssue 支持 to_dict() 方法,用于 JSON 报告输出:

issue = QcIssue(
    rule_id="geometry_validity",
    severity="error",
    feature_index=7,
    message="Self-intersection detected",
    geometry=Point(116.39, 39.91),
    details={"type": "self_intersection"},
)

issue.to_dict()
# {
#     "rule_id": "geometry_validity",
#     "severity": "error",
#     "feature_index": 7,
#     "message": "Self-intersection detected",
#     "geometry": "POINT (116.39 39.91)",
#     "details": {"type": "self_intersection"}
# }

12.3 qc.geometry_validity 几何有效性检查

12.3.1 功能说明

qc.geometry_validity 检查矢量数据中每个要素的几何是否有效。这是最基础的质检步骤,通常作为质检流水线的第一步。

12.3.2 参数定义

参数 类型 必需 默认值 说明
input GeoDataFrame 输入矢量数据
auto_fix bool False 是否自动修复无效几何
severity str "error" 问题严重级别

12.3.3 检测项目

geometry_validity 检查以下三类几何问题:

检测项 说明 检测方法 修复方法
自相交 (Self-intersection) 多边形边界与自身相交 geometry.is_valid make_valid()
空几何 (Empty geometry) 几何对象为空或 None geometry.is_empty 移除或标记
环方向错误 (Ring orientation) 外环非逆时针/内环非顺时针 shapely.is_ccw orient()
几何问题示例:

自相交多边形:        空几何:           环方向错误:
    ╱╲                                   外环应逆时针
   ╱  ╲             EMPTY               ╭───→───╮
  ╱  ╳ ╲   ← 交叉点                     │       │
 ╱  ╱ ╲  ╲                              ↑       ↓  ← 正确
╱__╱   ╲__╲                             │       │
                                         ╰───←───╯

12.3.4 源码分析

@step(id="qc.geometry_validity", ...)
def geometry_validity(ctx):
    gdf = ctx.input("input")
    auto_fix = ctx.param("auto_fix") or False
    severity = ctx.param("severity") or "error"

    issues = []
    result_gdf = gdf.copy()

    for idx, row in gdf.iterrows():
        geom = row.geometry

        # 检查空几何
        if geom is None or geom.is_empty:
            issues.append(QcIssue(
                rule_id="geometry_validity",
                severity=severity,
                feature_index=idx,
                message="Empty or null geometry",
                details={"type": "empty_geometry"},
            ))
            continue

        # 检查几何有效性(含自相交)
        if not geom.is_valid:
            reason = shapely.validation.explain_validity(geom)
            issues.append(QcIssue(
                rule_id="geometry_validity",
                severity=severity,
                feature_index=idx,
                message=f"Invalid geometry: {reason}",
                geometry=geom,
                details={"type": "invalid", "reason": reason},
            ))

            # 自动修复
            if auto_fix:
                from shapely.validation import make_valid
                result_gdf.at[idx, "geometry"] = make_valid(geom)

    output = result_gdf if auto_fix else gdf
    return build_qc_result(output, issues)

12.3.5 auto_fix 模式

auto_fix=true 时,步骤尝试自动修复无效几何:

问题类型 修复方法 说明
自相交 shapely.validation.make_valid() 分割或清理自相交区域
环方向 shapely.geometry.polygon.orient() 统一为 OGC 标准方向
空几何 不修复 仅记录问题

⚠️ 注意:auto_fix=true 是唯一会修改输出数据的 QC 模式。修复后的数据作为步骤输出,而原始问题仍记录在 issues 中。

12.3.6 使用示例

- id: check_geometry
  step: qc.geometry_validity
  params:
    input: "$steps.read_parcels"
    auto_fix: true
    severity: "error"

12.4 qc.crs_check 坐标参考系检查

12.4.1 功能说明

qc.crs_check 验证输入数据的坐标参考系(CRS)是否存在且符合预期。CRS 错误会导致所有空间计算结果失误,因此该检查至关重要。

12.4.2 参数定义

参数 类型 必需 默认值 说明
input GeoDataFrame 输入矢量数据
expected_crs str None 期望的 CRS(如 "EPSG:4326"

12.4.3 验证逻辑

@step(id="qc.crs_check", ...)
def crs_check(ctx):
    gdf = ctx.input("input")
    expected_crs = ctx.param("expected_crs")

    issues = []

    # 检查 CRS 是否存在
    if gdf.crs is None:
        issues.append(QcIssue(
            rule_id="crs_check",
            severity="error",
            message="CRS is not defined",
        ))
    elif expected_crs:
        # 检查 CRS 是否匹配
        from pyproj import CRS
        expected = CRS.from_user_input(expected_crs)
        if gdf.crs != expected:
            issues.append(QcIssue(
                rule_id="crs_check",
                severity="error",
                message=f"CRS mismatch: expected {expected_crs}, "
                        f"got {gdf.crs.to_epsg() or gdf.crs}",
                details={
                    "expected": expected_crs,
                    "actual": str(gdf.crs),
                },
            ))

    return build_qc_result(gdf, issues)

检查分两个阶段:

阶段 检查内容 问题级别
1. CRS 存在性 gdf.crs is None → CRS 未定义 error
2. CRS 正确性 gdf.crs != expected_crs → CRS 不匹配 error

12.4.4 使用示例

- id: check_crs
  step: qc.crs_check
  params:
    input: "$steps.read_data"
    expected_crs: "EPSG:4326"

12.5 qc.topology 拓扑关系检查

12.5.1 功能说明

qc.topology 检查矢量数据要素之间的空间拓扑关系,确保数据满足特定的拓扑规则。常用于土地利用数据、行政区划等需要严格拓扑完整性的场景。

12.5.2 参数定义

参数 类型 必需 默认值 说明
input GeoDataFrame 输入矢量数据
rules list[str] 拓扑规则列表
tolerance float 0.0 容差值

12.5.3 支持的拓扑规则

规则 说明 检测方法
no_overlaps 要素之间不得重叠 检查任意两个多边形的交集面积
no_gaps 要素之间不得有缝隙 检查合并后多边形的孔洞
no_dangles 线要素不得有悬挂端点 检查线端点的连通性
拓扑问题示例:

no_overlaps(无重叠):   no_gaps(无缝隙):     no_dangles(无悬挂):

 ┌────┐                  ┌────┐ ┌────┐          ───┬───
 │ A ┌┼───┐              │ A  │ │ B  │             │
 │   ││ B │  ← 重叠区域   │    │ │    │          ───┤
 └───┼┘   │              └────┘ └────┘             │
     └────┘                ↑ 缝隙                   ╵ ← 悬挂端点

12.5.4 使用示例

- id: check_topology
  step: qc.topology
  params:
    input: "$steps.read_parcels"
    rules:
      - no_overlaps
      - no_gaps
    tolerance: 0.001

12.6 qc.attribute_completeness 属性完整性检查

12.6.1 功能说明

qc.attribute_completeness 检查指定的必填字段是否存在空值(NoneNaN)或空字符串。确保数据表中的关键字段均已填写。

12.6.2 参数定义

参数 类型 必需 默认值 说明
input GeoDataFrame 输入矢量数据
required_fields list[str] 必填字段名列表
severity str "error" 问题严重级别

12.6.3 源码分析

@step(id="qc.attribute_completeness", ...)
def attribute_completeness(ctx):
    gdf = ctx.input("input")
    required_fields = ctx.param("required_fields")
    severity = ctx.param("severity") or "error"

    issues = []

    for field_name in required_fields:
        # 检查字段是否存在
        if field_name not in gdf.columns:
            issues.append(QcIssue(
                rule_id="attribute_completeness",
                severity=severity,
                message=f"Required field '{field_name}' not found in data",
                details={"field": field_name, "type": "missing_field"},
            ))
            continue

        # 检查空值
        for idx, value in gdf[field_name].items():
            if value is None or (isinstance(value, float) and
                                  pd.isna(value)) or value == "":
                issues.append(QcIssue(
                    rule_id="attribute_completeness",
                    severity=severity,
                    feature_index=idx,
                    message=f"Field '{field_name}' is null or empty",
                    geometry=gdf.geometry.iloc[idx]
                        if idx < len(gdf) else None,
                    details={
                        "field": field_name,
                        "value": str(value),
                    },
                ))

    return build_qc_result(gdf, issues)

12.6.4 使用示例

- id: check_completeness
  step: qc.attribute_completeness
  params:
    input: "$steps.read_buildings"
    required_fields:
      - "building_id"
      - "name"
      - "address"
      - "category"
    severity: "error"

12.7 qc.attribute_domain 属性值域检查

12.7.1 功能说明

qc.attribute_domain 验证字段值是否在允许的值域内,支持两种模式:枚举值列表和正则表达式匹配。

12.7.2 参数定义

参数 类型 必需 默认值 说明
input GeoDataFrame 输入矢量数据
field str 待检查的字段名
allowed_values list None 允许的枚举值列表
pattern str None 正则表达式模式

allowed_valuespattern 二选一,至少指定一个。

12.7.3 两种检查模式

模式 1:枚举值列表

- id: check_landuse_type
  step: qc.attribute_domain
  params:
    input: "$steps.read_parcels"
    field: "land_use"
    allowed_values:
      - "residential"
      - "commercial"
      - "industrial"
      - "agricultural"
      - "public"

模式 2:正则表达式

- id: check_parcel_id
  step: qc.attribute_domain
  params:
    input: "$steps.read_parcels"
    field: "parcel_id"
    pattern: "^[A-Z]{2}\\d{6}$"  # 例如 BJ123456

12.7.4 源码分析

@step(id="qc.attribute_domain", ...)
def attribute_domain(ctx):
    import re

    gdf = ctx.input("input")
    field_name = ctx.param("field")
    allowed_values = ctx.param("allowed_values")
    pattern = ctx.param("pattern")

    issues = []

    for idx, value in gdf[field_name].items():
        if value is None or (isinstance(value, float) and pd.isna(value)):
            continue  # 空值由 attribute_completeness 检查

        if allowed_values and value not in allowed_values:
            issues.append(QcIssue(
                rule_id="attribute_domain",
                severity="error",
                feature_index=idx,
                message=f"Value '{value}' not in allowed values "
                        f"for field '{field_name}'",
                details={
                    "field": field_name,
                    "value": value,
                    "allowed": allowed_values,
                },
            ))

        if pattern and not re.match(pattern, str(value)):
            issues.append(QcIssue(
                rule_id="attribute_domain",
                severity="error",
                feature_index=idx,
                message=f"Value '{value}' does not match pattern "
                        f"'{pattern}' for field '{field_name}'",
                details={
                    "field": field_name,
                    "value": value,
                    "pattern": pattern,
                },
            ))

    return build_qc_result(gdf, issues)

12.8 qc.value_range 数值范围检查

12.8.1 功能说明

qc.value_range 检查数值字段的值是否在指定的最小值和最大值范围内。

12.8.2 参数定义

参数 类型 必需 默认值 说明
input GeoDataFrame 输入矢量数据
field str 待检查的数值字段名
min float None 最小允许值(含)
max float None 最大允许值(含)
severity str "error" 问题严重级别

12.8.3 检查逻辑

@step(id="qc.value_range", ...)
def value_range(ctx):
    gdf = ctx.input("input")
    field_name = ctx.param("field")
    min_val = ctx.param("min")
    max_val = ctx.param("max")
    severity = ctx.param("severity") or "error"

    issues = []

    for idx, value in gdf[field_name].items():
        if value is None or (isinstance(value, float) and pd.isna(value)):
            continue

        numeric_val = float(value)
        if min_val is not None and numeric_val < min_val:
            issues.append(QcIssue(
                rule_id="value_range",
                severity=severity,
                feature_index=idx,
                message=f"Value {numeric_val} < min {min_val} "
                        f"for field '{field_name}'",
                details={"field": field_name, "value": numeric_val,
                         "min": min_val},
            ))
        if max_val is not None and numeric_val > max_val:
            issues.append(QcIssue(
                rule_id="value_range",
                severity=severity,
                feature_index=idx,
                message=f"Value {numeric_val} > max {max_val} "
                        f"for field '{field_name}'",
                details={"field": field_name, "value": numeric_val,
                         "max": max_val},
            ))

    return build_qc_result(gdf, issues)

12.8.4 使用示例

# 检查建筑层数合理性
- id: check_floors
  step: qc.value_range
  params:
    input: "$steps.read_buildings"
    field: "floor_count"
    min: 1
    max: 200
    severity: "warning"

# 检查人口密度
- id: check_population
  step: qc.value_range
  params:
    input: "$steps.read_districts"
    field: "population_density"
    min: 0
    max: 100000

12.9 qc.duplicate_check 重复要素检查

12.9.1 功能说明

qc.duplicate_check 检测数据中的重复要素,支持基于几何和/或属性字段的重复判定。

12.9.2 参数定义

参数 类型 必需 默认值 说明
input GeoDataFrame 输入矢量数据
check_geometry bool True 是否检查几何重复
check_fields list[str] None 用于判重的属性字段列表
tolerance float 0.0 几何比较的容差值

12.9.3 重复判定逻辑

重复检查有三种组合模式:

check_geometry check_fields 判重条件
True None 几何完全相同(考虑容差)
False ["id", "name"] 指定字段值完全相同
True ["id"] 几何相同 字段值相同
@step(id="qc.duplicate_check", ...)
def duplicate_check(ctx):
    gdf = ctx.input("input")
    check_geometry = ctx.param("check_geometry")
    if check_geometry is None:
        check_geometry = True
    check_fields = ctx.param("check_fields")
    tolerance = ctx.param("tolerance") or 0.0

    issues = []
    seen = {}  # 用于跟踪已见过的要素

    for idx, row in gdf.iterrows():
        key_parts = []

        if check_geometry and row.geometry is not None:
            if tolerance > 0:
                geom_key = row.geometry.simplify(tolerance).wkt
            else:
                geom_key = row.geometry.wkt
            key_parts.append(geom_key)

        if check_fields:
            field_key = tuple(row[f] for f in check_fields)
            key_parts.append(str(field_key))

        key = "|".join(key_parts)

        if key in seen:
            issues.append(QcIssue(
                rule_id="duplicate_check",
                severity="warning",
                feature_index=idx,
                message=f"Duplicate of feature {seen[key]}",
                geometry=row.geometry,
                details={
                    "duplicate_of": seen[key],
                    "key": key[:100],
                },
            ))
        else:
            seen[key] = idx

    return build_qc_result(gdf, issues)

12.9.4 使用示例

# 检查几何和属性重复
- id: check_duplicates
  step: qc.duplicate_check
  params:
    input: "$steps.read_poi"
    check_geometry: true
    check_fields:
      - "poi_name"
      - "category"
    tolerance: 0.0001

12.10 qc.raster_nodata NoData 一致性检查

12.10.1 功能说明

qc.raster_nodata 检查栅格数据的 NoData 值是否与预期一致,并验证 NoData 像素占比是否超出阈值。

12.10.2 参数定义

参数 类型 必需 默认值 说明
input raster_info 输入栅格数据
expected_nodata float None 期望的 NoData 值
max_nodata_ratio float None NoData 像素最大占比(0~1)

12.10.3 检查逻辑

@step(id="qc.raster_nodata", ...)
def raster_nodata(ctx):
    raster = ctx.input("input")  # raster_info dict
    expected_nodata = ctx.param("expected_nodata")
    max_nodata_ratio = ctx.param("max_nodata_ratio")

    issues = []
    profile = raster["profile"]
    data = raster["data"]
    actual_nodata = profile.get("nodata")

    # 检查 NoData 值一致性
    if expected_nodata is not None and actual_nodata != expected_nodata:
        issues.append(QcIssue(
            rule_id="raster_nodata",
            severity="error",
            message=f"NoData mismatch: expected {expected_nodata}, "
                    f"got {actual_nodata}",
            details={
                "expected": expected_nodata,
                "actual": actual_nodata,
            },
        ))

    # 检查 NoData 像素占比
    if max_nodata_ratio is not None and actual_nodata is not None:
        total_pixels = data.size
        nodata_pixels = (data == actual_nodata).sum()
        ratio = nodata_pixels / total_pixels

        if ratio > max_nodata_ratio:
            issues.append(QcIssue(
                rule_id="raster_nodata",
                severity="warning",
                message=f"NoData ratio {ratio:.2%} exceeds "
                        f"threshold {max_nodata_ratio:.2%}",
                details={
                    "nodata_ratio": float(ratio),
                    "threshold": max_nodata_ratio,
                    "nodata_pixels": int(nodata_pixels),
                    "total_pixels": int(total_pixels),
                },
            ))

    return build_qc_result(raster, issues)

12.10.4 使用示例

- id: check_nodata
  step: qc.raster_nodata
  params:
    input: "$steps.read_dem"
    expected_nodata: -9999
    max_nodata_ratio: 0.1  # NoData 不超过 10%

12.11 qc.raster_resolution 分辨率一致性检查

12.11.1 功能说明

qc.raster_resolution 检查栅格数据的像素分辨率是否与预期一致。

12.11.2 参数定义

参数 类型 必需 默认值 说明
input raster_info 输入栅格数据
expected_x float 期望的 X 方向分辨率
expected_y float 期望的 Y 方向分辨率
tolerance float 0.0 允许的误差范围

12.11.3 检查逻辑

@step(id="qc.raster_resolution", ...)
def raster_resolution(ctx):
    raster = ctx.input("input")
    expected_x = ctx.param("expected_x")
    expected_y = ctx.param("expected_y")
    tolerance = ctx.param("tolerance") or 0.0

    issues = []
    transform = raster["transform"]

    # 从仿射变换矩阵获取实际分辨率
    actual_x = abs(transform.a)   # X 方向像素大小
    actual_y = abs(transform.e)   # Y 方向像素大小

    if abs(actual_x - expected_x) > tolerance:
        issues.append(QcIssue(
            rule_id="raster_resolution",
            severity="error",
            message=f"X resolution mismatch: expected {expected_x}, "
                    f"got {actual_x}",
            details={"axis": "x", "expected": expected_x,
                     "actual": actual_x},
        ))

    if abs(actual_y - expected_y) > tolerance:
        issues.append(QcIssue(
            rule_id="raster_resolution",
            severity="error",
            message=f"Y resolution mismatch: expected {expected_y}, "
                    f"got {actual_y}",
            details={"axis": "y", "expected": expected_y,
                     "actual": actual_y},
        ))

    return build_qc_result(raster, issues)

12.11.4 使用示例

- id: check_resolution
  step: qc.raster_resolution
  params:
    input: "$steps.read_dem"
    expected_x: 30.0   # 30 米分辨率
    expected_y: 30.0
    tolerance: 0.5     # 允许 0.5 米误差

12.12 qc.raster_value_range 栅格值域检查

12.12.1 功能说明

qc.raster_value_range 检查栅格数据的像素值是否在合理范围内。

12.12.2 参数定义

参数 类型 必需 默认值 说明
input raster_info 输入栅格数据
min float None 最小允许值
max float None 最大允许值
severity str "error" 问题严重级别
band int 1 波段索引(从 1 开始)

12.12.3 源码分析

@step(id="qc.raster_value_range", ...)
def raster_value_range(ctx):
    raster = ctx.input("input")
    min_val = ctx.param("min")
    max_val = ctx.param("max")
    severity = ctx.param("severity") or "error"
    band = ctx.param("band") or 1

    issues = []
    data = raster["data"]
    nodata = raster["profile"].get("nodata")

    # 提取指定波段(band 索引从 1 开始)
    band_data = data[band - 1]

    # 排除 NoData 值
    if nodata is not None:
        valid_data = band_data[band_data != nodata]
    else:
        valid_data = band_data.ravel()

    actual_min = float(valid_data.min())
    actual_max = float(valid_data.max())

    if min_val is not None and actual_min < min_val:
        issues.append(QcIssue(
            rule_id="raster_value_range",
            severity=severity,
            message=f"Band {band}: min value {actual_min} < {min_val}",
            details={"band": band, "actual_min": actual_min,
                     "expected_min": min_val},
        ))

    if max_val is not None and actual_max > max_val:
        issues.append(QcIssue(
            rule_id="raster_value_range",
            severity=severity,
            message=f"Band {band}: max value {actual_max} > {max_val}",
            details={"band": band, "actual_max": actual_max,
                     "expected_max": max_val},
        ))

    return build_qc_result(raster, issues, stats={
        "band": band,
        "actual_min": actual_min,
        "actual_max": actual_max,
    })

12.12.4 使用示例

# DEM 高程值域检查
- id: check_dem_values
  step: qc.raster_value_range
  params:
    input: "$steps.read_dem"
    min: -500     # 最低高程(死海 -431m)
    max: 9000     # 最高高程(珠峰 8849m)
    band: 1

# NDVI 值域检查
- id: check_ndvi
  step: qc.raster_value_range
  params:
    input: "$steps.compute_ndvi"
    min: -1.0
    max: 1.0
    severity: "error"
    band: 1

12.13 QC 步骤组合实战

12.13.1 矢量数据质检流水线

以下流水线对土地利用矢量数据执行完整的质检链:

# vector-qc.yaml
name: 土地利用数据质量检查
description: 对土地利用矢量数据执行全链路质量检查

steps:
  # ─── 数据读取 ───
  - id: read_parcels
    step: io.read_vector
    params:
      path: "data/land_use_parcels.gpkg"

  # ─── 基础检查 ───
  - id: check_crs
    step: qc.crs_check
    params:
      input: "$steps.read_parcels"
      expected_crs: "EPSG:4326"

  - id: check_geometry
    step: qc.geometry_validity
    params:
      input: "$steps.read_parcels"
      auto_fix: true
      severity: "error"

  # ─── 拓扑检查 ───
  - id: check_topo
    step: qc.topology
    params:
      input: "$steps.check_geometry"
      rules:
        - no_overlaps
        - no_gaps
      tolerance: 0.0001

  # ─── 属性检查 ───
  - id: check_completeness
    step: qc.attribute_completeness
    params:
      input: "$steps.check_geometry"
      required_fields:
        - "parcel_id"
        - "land_use"
        - "area_sqm"
        - "owner"
      severity: "error"

  - id: check_landuse_domain
    step: qc.attribute_domain
    params:
      input: "$steps.check_geometry"
      field: "land_use"
      allowed_values:
        - "residential"
        - "commercial"
        - "industrial"
        - "agricultural"
        - "public"
        - "green_space"

  - id: check_parcel_id_format
    step: qc.attribute_domain
    params:
      input: "$steps.check_geometry"
      field: "parcel_id"
      pattern: "^[A-Z]{2}\\d{8}$"

  - id: check_area
    step: qc.value_range
    params:
      input: "$steps.check_geometry"
      field: "area_sqm"
      min: 1
      max: 10000000
      severity: "warning"

  # ─── 重复检查 ───
  - id: check_dup
    step: qc.duplicate_check
    params:
      input: "$steps.check_geometry"
      check_geometry: true
      check_fields:
        - "parcel_id"
      tolerance: 0.0001

12.13.2 栅格数据质检流水线

# raster-qc.yaml
name: DEM 栅格数据质量检查
description: 对 DEM 高程数据执行质量检查

steps:
  # ─── 读取栅格 ───
  - id: read_dem
    step: io.read_raster
    params:
      path: "data/srtm_dem.tif"

  # ─── NoData 检查 ───
  - id: check_nodata
    step: qc.raster_nodata
    params:
      input: "$steps.read_dem"
      expected_nodata: -9999
      max_nodata_ratio: 0.05

  # ─── 分辨率检查 ───
  - id: check_resolution
    step: qc.raster_resolution
    params:
      input: "$steps.read_dem"
      expected_x: 30.0
      expected_y: 30.0
      tolerance: 1.0

  # ─── 值域检查 ───
  - id: check_elevation
    step: qc.raster_value_range
    params:
      input: "$steps.read_dem"
      min: -500
      max: 9000
      band: 1
      severity: "error"

12.13.3 质检流水线数据流

矢量质检数据流:

read_parcels
     │
     ├──▶ check_crs ──▶ [CRS 问题]
     │
     ├──▶ check_geometry ──(auto_fix)──▶ 修复后的数据
     │                                        │
     │    ┌───────────────────────────────────┘
     │    │
     │    ├──▶ check_topo ──▶ [拓扑问题]
     │    ├──▶ check_completeness ──▶ [完整性问题]
     │    ├──▶ check_landuse_domain ──▶ [值域问题]
     │    ├──▶ check_parcel_id_format ──▶ [格式问题]
     │    ├──▶ check_area ──▶ [范围问题]
     │    └──▶ check_dup ──▶ [重复问题]
     │
     ▼
所有 issues 汇总 → QC 报告

12.14 QC 报告解读

12.14.1 qc_summary 结构

执行引擎的 reporter.py 会汇总所有 QC 步骤的问题到 qc_summary 中:

{
  "pipeline": "土地利用数据质量检查",
  "status": "completed",
  "qc_summary": {
    "total_issues": 23,
    "by_severity": {
      "error": 5,
      "warning": 15,
      "info": 3
    },
    "by_rule": {
      "geometry_validity": 2,
      "attribute_completeness": 8,
      "attribute_domain": 3,
      "value_range": 5,
      "duplicate_check": 2,
      "topology": 3
    },
    "steps": {
      "check_geometry": {
        "issue_count": 2,
        "errors": 2,
        "warnings": 0
      },
      "check_completeness": {
        "issue_count": 8,
        "errors": 8,
        "warnings": 0
      }
    }
  },
  "steps": [...]
}

12.14.2 报告字段说明

字段 说明
total_issues 所有 QC 步骤发现的问题总数
by_severity 按严重级别分组统计
by_rule 按规则 ID 分组统计
steps 每个 QC 步骤的独立统计

12.14.3 利用报告做质量决策

# 在质检之后根据问题数量决定是否继续
- id: process_data
  step: vector.buffer
  params:
    input: "$steps.check_geometry"
    distance: 100
  when: "$steps.check_geometry.stats.error_count == 0"

通过 when 条件表达式结合 QC 步骤的统计信息,可以实现条件化流水线执行:仅在质检通过时才执行后续处理步骤。


12.15 本章小结

本章全面介绍了 GeoPipeAgent 的 10 个数据质检步骤:

核心概念:

  • “Check and Passthrough” 模式:数据原样通过,问题以 QcIssue 对象收集到 $step.issues
  • QcIssue 数据模型:包含 rule_idseverityfeature_indexmessagegeometrydetails 六个字段
  • 三级严重性error(错误)、warning(警告)、info(提示)

矢量质检步骤(7 个):

步骤 核心功能
qc.geometry_validity 几何有效性检查,支持 auto_fix 自动修复
qc.crs_check CRS 存在性和正确性验证
qc.topology 拓扑规则检查(no_overlaps / no_gaps / no_dangles)
qc.attribute_completeness 必填字段空值检查
qc.attribute_domain 枚举值/正则表达式值域检查
qc.value_range 数值字段范围检查
qc.duplicate_check 几何和属性重复检查

栅格质检步骤(3 个):

步骤 核心功能
qc.raster_nodata NoData 值和占比检查
qc.raster_resolution 像素分辨率一致性检查
qc.raster_value_range 像素值域范围检查

通过组合这些 QC 步骤,可以构建针对矢量和栅格数据的全链路质量检查流水线,并利用 JSON 报告中的 qc_summary 进行质量评估和决策。


下一章:Backend 多后端系统 →