《QGIS空间数据处理与高级制图》027:空值填充策略(默认值/插值/相邻值)

作者:翰墨之道,毕业于国际知名大学空间信息与计算机专业,获硕士学位,现任国内时空智能领域资深专家、CSDN知名技术博主。多年来深耕地理信息与时空智能核心技术研发,精通 QGIS、GrassGIS、OSG、OsgEarth、UE、Cesium、OpenLayers、Leaflet、MapBox 等主流工具与框架,兼具学术深度与工程实践经验。

专注于时空数据可视化、地理信息系统开发、三维场景搭建等方向,持续在CSDN分享技术干货与实战案例,累计产出多篇高质量原创内容,深受行业开发者认可。诚邀对时空智能、GIS技术、三维技术感兴趣的朋友添加微信(Lucky-Matrix),共探技术前沿、交流实践心得,携手推动相关领域技术落地与创新!
📚 查看《QGIS快速入门与应用基础》系列专栏完整目录

文章目录

  • [1.4.1.2 空值填充策略(默认值/插值/相邻值)](#1.4.1.2 空值填充策略(默认值/插值/相邻值))
    • 一、空值填充策略的核心认知
      • [1.1 为什么需要填充而不是直接删除?](#1.1 为什么需要填充而不是直接删除?)
      • [1.2 三种填充策略的对比](#1.2 三种填充策略的对比)
    • 二、默认值填充策略
      • [2.1 默认值填充的核心方法与原理](#2.1 默认值填充的核心方法与原理)
      • [2.2 QGIS 中默认值填充的实操方法](#2.2 QGIS 中默认值填充的实操方法)
      • [2.3 默认值填充的行业实践](#2.3 默认值填充的行业实践)
      • [2.4 默认值填充的常见问题与解决方案](#2.4 默认值填充的常见问题与解决方案)
      • [2.5 Python 自动化实现:默认值填充脚本](#2.5 Python 自动化实现:默认值填充脚本)
    • 三、空间插值填充策略
      • [3.1 插值填充的核心原理与适用场景](#3.1 插值填充的核心原理与适用场景)
      • [3.2 常用插值方法对比](#3.2 常用插值方法对比)
      • [3.3 QGIS 中插值填充的实操方法](#3.3 QGIS 中插值填充的实操方法)
    • 四、相邻值填充策略
      • [4.1 相邻值填充的核心原理](#4.1 相邻值填充的核心原理)
      • [4.2 三种相邻值填充方法](#4.2 三种相邻值填充方法)
      • [4.3 QGIS 中相邻值填充的实操方法](#4.3 QGIS 中相邻值填充的实操方法)
      • [4.4 相邻值填充的行业应用](#4.4 相邻值填充的行业应用)
    • 五、填充策略的选择原则
      • [5.1 基于数据特征的策略选择](#5.1 基于数据特征的策略选择)
      • [5.2 三种填充策略的综合对比](#5.2 三种填充策略的综合对比)
      • [5.3 填充策略的最佳实践流程](#5.3 填充策略的最佳实践流程)
    • 六、填充后质量验证
      • [6.1 验证指标与方法](#6.1 验证指标与方法)
      • [6.2 Python 自动化验证脚本](#6.2 Python 自动化验证脚本)
    • 七、综合案例:某省国土变更调查数据空值填充
      • [7.1 项目背景](#7.1 项目背景)
      • [7.2 填充策略选择](#7.2 填充策略选择)
      • [7.3 填充流程](#7.3 填充流程)
      • [7.4 验证结果](#7.4 验证结果)
    • 八、常见问题与最佳实践
      • [8.1 填充策略选择常见问题](#8.1 填充策略选择常见问题)
      • [8.2 填充策略最佳实践总结](#8.2 填充策略最佳实践总结)
    • 九、总结

1.4.1.2 空值填充策略(默认值/插值/相邻值)


一、空值填充策略的核心认知

1.1 为什么需要填充而不是直接删除?

在空间数据处理中,空值处理历来有两种思路:

思路 做法 优点 缺点 适用场景
删除法 丢弃含空值的记录 操作简单、不留残迹 样本量锐减、空间分布失衡、可能引入偏差 空值比例 < 1%,且随机分布
填充法 用合理值替换空值 保留样本量、维持空间完整性 可能引入伪数据、需要科学依据 空值比例 ≥ 1%、或空值有特定原因

核心认知

  • 空值填充不是"造假",而是基于已有信息对缺失值进行合理估算
  • 填充的质量取决于策略的选择参数的合理性
  • 填充后必须进行质量验证,否则可能传播错误信息

1.2 三种填充策略的对比

策略 原理 操作难度 对数据的影响 适用数据类型
默认值填充 用固定值(0、均值、中位数、众数)替换空值 ⭐ 简单 可能扭曲分布、不反映空间差异 分类字段、数值字段
插值填充 基于空间邻近原则,用已知点估算未知点 ⭐⭐⭐ 复杂 保留空间连续性、可能平滑极端值 连续型数值字段(高程、温度、降雨)
相邻值填充 用空间相邻要素的值填充空值 ⭐⭐ 中等 保持局部一致性、可能放大噪声 分类字段、序列数据(时间/空间)

二、默认值填充策略

2.1 默认值填充的核心方法与原理

默认值填充是最基础的填充方式,核心是用一个固定值或统计算法计算出的代表值替换所有空值。

填充方法 计算公式/操作 适用场景 局限性
常数填充 所有空值 = 固定值(如 0、-9999) 分类字段的"未知"类别、高程补全 可能扭曲统计分布
均值填充 所有空值 = mean(非空值) 近似正态分布的连续变量 缩小标准差、不反映空间差异
中位数填充 所有空值 = median(非空值) 偏态分布、有极端值 同样不反映空间差异
众数填充 所有空值 = mode(非空值) 分类数据(地类、土壤类型) 可能过度代表某一类别

2.2 QGIS 中默认值填充的实操方法

方法 1:字段计算器 + fill_null() 函数

QGIS 字段计算器提供了 fill_null() 函数,可以一键填充空值:

复制代码
fill_null("elevation", 0)                    -- 用 0 填充高程空值
fill_null("landuse", 'Unknown')              -- 用"Unknown"填充地类空值
fill_null("precipitation", mean("precipitation")) -- 用均值填充

操作步骤

  1. 右键图层 → 打开属性表
  2. 点击"切换编辑模式"(铅笔图标)
  3. 点击"打开字段计算器"
  4. 选择"创建新字段"或"更新现有字段"
  5. 在表达式中输入 fill_null() 函数
  6. 点击"确定"执行填充

图1:QGIS 字段计算器中的 fill_null() 函数

方法 2:使用属性表手动编辑(适用于小量空值)

当空值数量较少(< 50 条)时,可以直接在属性表中手动输入:

  1. 打开属性表
  2. 勾选"切换编辑模式"
  3. 逐个点击空值单元格
  4. 输入默认值

方法 3:模型构建器批量填充

使用模型构建器创建可复用的默认值填充流程:

  1. 打开"模型构建器"(Processing → Model Designer)
  2. 添加"字段计算器"算法
  3. 将图层和填充规则作为参数
  4. 保存模型为 .model3 文件

2.3 默认值填充的行业实践

行业 字段 填充策略 填充值 原因
国土调查 坡度 均值填充 区域平均坡度 坡度空值多为传感器噪声,均值最接近真实值
国土调查 地类编码 众数填充 该乡镇最常见地类 地类空值多为边界模糊,用众数最保险
水利 流量 常数填充 0 河道断流导致流量为空,0 表示无流量
生态 植被覆盖度 中位数填充 区域植被覆盖中位数 覆盖度分布偏态,中位数比均值更稳健
城建 建筑高度 均值填充 同街区建筑平均高度 同街区建筑高度相近,均值合理
农业 土壤有机质 众数填充 该土壤类型常见有机质范围中值 土壤有机质有明确的地带性规律

2.4 默认值填充的常见问题与解决方案

问题 原因 解决方案 最佳实践
填充后标准差显著缩小 均值/中位数填充压缩了变异性 改用空间插值或添加随机扰动 填充值 = 均值 + random(0, ±标准差*0.1)
填充后空间分布均匀化 默认值不反映空间异质性 按空间分区填充(如按乡镇、流域) 先按空间单元分组,再分组填充
分类字段过度代表某一类 众数填充使某类占比虚高 改用相邻值填充或人工判断 对于分类字段,优先用相邻要素的地类
常数 0 被误认为是真实值 0 与有效数据无法区分 使用 -9999 或 NoData 标记填充空值 在数据分析时排除 -9999
填充后统计分布变形 填充方法与原分布不匹配 先用直方图检查分布,再选择填充方法 正态分布用均值,偏态用中位数,多峰用众数

2.5 Python 自动化实现:默认值填充脚本

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
QGIS 空间数据属性空值默认值填充脚本
支持:Shapefile / GeoJSON / CSV / GeoPackage
填充策略:均值 / 中位数 / 众数 / 常数
"""

import argparse
import pandas as pd
import geopandas as gpd
import numpy as np
import sys

def fill_defaults(gdf, strategy='mean', fill_values=None):
    """
    对 GeoDataFrame 的数值和分类字段进行默认值填充
    
    参数:
        gdf : GeoDataFrame
            输入矢量数据
        strategy : str
            填充策略:'mean'(均值)/'median'(中位数)/'mode'(众数)/'constant'(常数)
        fill_values : dict, optional
            当 strategy='constant' 时,指定各字段的填充值
    
    返回:
        filled_gdf : GeoDataFrame
            填充后的矢量数据
    """
    filled_gdf = gdf.copy()
    
    numeric_cols = filled_gdf.select_dtypes(include=[np.number]).columns.tolist()
    categorical_cols = filled_gdf.select_dtypes(include=['object', 'category']).columns.tolist()
    
    # 排除几何列
    categorical_cols = [c for c in categorical_cols if c != 'geometry']
    
    print(f"检测到 {len(numeric_cols)} 个数值字段:{numeric_cols}")
    print(f"检测到 {len(categorical_cols)} 个分类字段:{categorical_cols}")
    
    # 数值字段填充
    for col in numeric_cols:
        null_count = filled_gdf[col].isnull().sum()
        if null_count == 0:
            continue
        
        print(f"\n--- 字段:{col} ---")
        print(f"  空值数量:{null_count} / {len(filled_gdf)}")
        
        if strategy == 'mean':
            fill_val = filled_gdf[col].mean()
            method_name = '均值'
        elif strategy == 'median':
            fill_val = filled_gdf[col].median()
            method_name = '中位数'
        elif strategy == 'constant':
            fill_val = fill_values.get(col, 0) if fill_values else 0
            method_name = f"常数 {fill_val}"
        else:
            continue
        
        filled_gdf[col].fillna(fill_val, inplace=True)
        print(f"  填充方法:{method_name} = {fill_val}")
    
    # 分类字段填充(仅众数)
    if strategy == 'mode':
        for col in categorical_cols:
            null_count = filled_gdf[col].isnull().sum()
            if null_count == 0:
                continue
            
            print(f"\n--- 分类字段:{col} ---")
            print(f"  空值数量:{null_count} / {len(filled_gdf)}")
            
            mode_val = filled_gdf[col].mode()
            fill_val = mode_val[0] if len(mode_val) > 0 else 'Unknown'
            filled_gdf[col].fillna(fill_val, inplace=True)
            print(f"  填充方法:众数 = {fill_val}")
    
    elif strategy == 'constant' and fill_values:
        for col in categorical_cols:
            if col in fill_values:
                null_count = filled_gdf[col].isnull().sum()
                if null_count > 0:
                    fill_val = fill_values[col]
                    filled_gdf[col].fillna(fill_val, inplace=True)
                    print(f"\n分类字段 {col} 用常数 {fill_val} 填充了 {null_count} 条")
    
    return filled_gdf


def main():
    parser = argparse.ArgumentParser(description='QGIS 空间数据默认值填充')
    parser.add_argument('--input', required=True, help='输入文件路径')
    parser.add_argument('--output', required=True, help='输出文件路径')
    parser.add_argument('--strategy', default='mean',
                        choices=['mean', 'median', 'mode', 'constant'],
                        help='填充策略')
    parser.add_argument('--constant-values', default='',
                        help='常数填充值,格式 key=value,key=value')
    args = parser.parse_args()
    
    # 自动识别文件格式
    ext = args.input.lower().split('.')[-1]
    if ext in ['shp', 'geojson', 'json']:
        gdf = gpd.read_file(args.input)
    elif ext == 'csv':
        df = pd.read_csv(args.input)
        gdf = gpd.GeoDataFrame(df, geometry='geometry' if 'geometry' in df.columns else None)
    elif ext == 'gpkg':
        gdf = gpd.read_file(args.input, layer='data')
    else:
        print(f"不支持的文件格式:{ext}")
        sys.exit(1)
    
    print(f"加载文件:{args.input}")
    print(f"记录数:{len(gdf)},字段数:{len(gdf.columns)}")
    
    # 解析常数填充值
    fill_values = {}
    if args.constant_values:
        for pair in args.constant_values.split(','):
            k, v = pair.split('=')
            fill_values[k.strip()] = v.strip()
    
    # 执行填充
    filled_gdf = fill_defaults(gdf, args.strategy, fill_values)
    
    # 导出
    if ext in ['shp']:
        filled_gdf.to_file(args.output, driver='ESRI Shapefile')
    elif ext in ['geojson', 'json']:
        filled_gdf.to_file(args.output, driver='GeoJSON')
    elif ext == 'gpkg':
        filled_gdf.to_file(args.output, layer='data', driver='GPKG')
    elif ext == 'csv':
        filled_gdf.to_csv(args.output, index=False)
    
    # 统计空值剩余量
    null_counts = filled_gdf.isnull().sum()
    remaining = null_counts.sum()
    print(f"\n填充完成!剩余空值:{remaining}")
    print(f"输出文件:{args.output}")


if __name__ == '__main__':
    main()

使用示例

bash 复制代码
# 均值填充
python fill_defaults.py --input survey_data.shp --output filled_mean.shp --strategy mean

# 众数填充(分类字段)
python fill_defaults.py --input survey_data.shp --output filled_mode.shp --strategy mode

# 常数填充
python fill_defaults.py --input survey_data.shp --output filled.shp \
    --strategy constant --constant-values "landuse=Unknown,elevation=0"

三、空间插值填充策略

3.1 插值填充的核心原理与适用场景

空间插值填充是基于**"空间自相关"原理**------空间上邻近的要素往往具有相似属性值。通过已知点的值,可以合理估算未知点的值。

插值填充的核心假设

  • 空间自相关性:邻近要素的属性值具有相似性
  • 距离衰减效应:距离越近,影响越大;距离越远,影响越小
  • 局部连续性:空间现象在局部范围内是连续变化的

3.2 常用插值方法对比

方法 原理 优点 缺点 适用场景
反距离权重(IDW) 邻近点权重的平均值 简单快速、参数少 产生"牛眼"效应、不反映地形 坡度、温度等平滑变化现象
克里金(Kriging) 基于变异函数的最优无偏估计 提供误差估计、统计严谨 参数复杂、计算量大 高程、降雨、矿产储量
样条(Spline) 用最小弯曲的曲面拟合数据 曲面光滑、过控制点 可能出现过冲/振荡 地形表面、重力场
局部多项式(Local Polynomial) 用局部回归曲面拟合 可反映趋势面 边界效应明显 污染浓度梯度、海拔梯度

图2:三种常用空间插值方法对比

3.3 QGIS 中插值填充的实操方法

方法 1:IDW 插值(最简单)

操作步骤:

  1. 打开"处理面板"(Processing → Toolbox)
  2. 搜索"IDW 插值"
  3. 输入参数:
    • 输入图层:含非空值记录的点图层
    • 目标字段:要插值的属性字段
    • 插值倍数:2~5(默认 2,值越大越平滑)
    • 幂次:2(默认 2,值越大邻近点影响越大)
  4. 运行,输出为栅格

方法 2:克里金插值(最严谨)

操作步骤:

  1. 处理面板搜索"克里金插值"
  2. 输入参数:
    • 输入图层:含非空值记录的点图层
    • 目标字段:要插值的属性字段
    • 变异函数模型:球形 / 指数 / 高斯(默认球形)
    • 网格间距:根据研究区域大小设置
  3. 运行

方法 3:基于插值结果回填属性(核心实操)

对于面状矢量数据(如Survey样地、地块),插值填充的核心流程是:

  1. 将含非空值的要素质心提取为点
  2. 用这些点做空间插值生成栅格
  3. 用栅格采样(Zonal Statistics)回填到原矢量要素
python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
QGIS 空间数据插值填充脚本(IDW 方法)
核心流程:质心提取 → IDW 插值 → 栅格采样回填
"""

import argparse
import geopandas as gpd
import numpy as np
from scipy.interpolate import LinearNDInterpolator, NearestNDInterpolator
import sys


def idw_interpolation(filled_gdf, target_field, power=2, k_neighbors=10):
    """
    使用 IDW 方法插值填充空值
    
    参数:
        filled_gdf : GeoDataFrame
            输入矢量数据(含几何信息)
        target_field : str
            要插值的属性字段
        power : float
            幂次参数(默认 2)
        k_neighbors : int
            使用的最近邻点数(默认 10)
    
    返回:
        result_gdf : GeoDataFrame
            填充后的矢量数据
    """
    result_gdf = filled_gdf.copy()
    
    # 分离已知点和空值点
    known_mask = result_gdf[target_field].notna()
    unknown_mask = result_gdf[target_field].isna()
    
    if not known_mask.any():
        print(f"字段 {target_field} 没有已知值,无法插值")
        return result_gdf
    
    if not unknown_mask.any():
        print(f"字段 {target_field} 没有空值,无需插值")
        return result_gdf
    
    known_points = result_gdf[known_mask]
    unknown_points = result_gdf[unknown_mask]
    
    # 提取坐标
    known_coords = np.array([(x, y) for x, y in known_points.geometry.values.centroid.coords])
    known_values = known_points[target_field].values
    
    unknown_coords = np.array([(x, y) for x, y in unknown_points.geometry.values.centroid.coords])
    
    # IDW 插值
    interpolated_values = []
    for ux, uy in unknown_coords:
        # 计算到所有已知点的距离
        distances = np.sqrt(np.sum((known_coords - [ux, uy]) ** 2, axis=1))
        
        # 排除距离为 0 的点(即未知点恰好与已知点重合)
        zero_dist_mask = distances == 0
        if np.any(zero_dist_mask):
            interpolated_values.append(known_values[zero_dist_mask][0])
            continue
        
        # 取最近的 k_neighbors 个点
        k_indices = np.argsort(distances)[:k_neighbors]
        k_distances = distances[k_indices]
        k_values = known_values[k_indices]
        
        # IDW 公式
        weights = 1.0 / (k_distances ** power)
        weight_sum = weights.sum()
        interpolated_val = np.sum(weights * k_values) / weight_sum
        interpolated_values.append(interpolated_val)
    
    # 回填
    unknown_indices = result_gdf[unknown_mask].index
    result_gdf.loc[unknown_indices, target_field] = interpolated_values
    
    print(f"IDW 插值完成:填充了 {len(interpolated_values)} 条记录")
    
    return result_gdf


def main():
    parser = argparse.ArgumentParser(description='QGIS 空间数据 IDW 插值填充')
    parser.add_argument('--input', required=True, help='输入文件路径')
    parser.add_argument('--output', required=True, help='输出文件路径')
    parser.add_argument('--field', required=True, help='要插值的字段名')
    parser.add_argument('--power', type=float, default=2.0, help='IDW 幂次(默认 2)')
    parser.add_argument('--neighbors', type=int, default=10, help='最近邻点数(默认 10)')
    args = parser.parse_args()
    
    gdf = gpd.read_file(args.input)
    print(f"加载文件:{args.input}")
    print(f"记录数:{len(gdf)}")
    
    null_count = gdf[args.field].isnull().sum()
    print(f"空值数量:{null_count} / {len(gdf)}")
    
    result_gdf = idw_interpolation(gdf, args.field, args.power, args.neighbors)
    
    # 导出
    gdf_ext = args.output.lower().split('.')[-1]
    if gdf_ext == 'shp':
        result_gdf.to_file(args.output, driver='ESRI Shapefile')
    elif gdf_ext == 'geojson':
        result_gdf.to_file(args.output, driver='GeoJSON')
    elif gdf_ext == 'gpkg':
        result_gdf.to_file(args.output, driver='GPKG')
    
    remaining = result_gdf[args.field].isnull().sum()
    print(f"填充完成!剩余空值:{remaining}")
    print(f"输出文件:{args.output}")


if __name__ == '__main__':
    main()

使用示例

bash 复制代码
# 使用 IDW 插值填充高程字段
python idw_fill.py --input sample_points.shp --output filled.shp --field elevation --power 2 --neighbors 10

四、相邻值填充策略

4.1 相邻值填充的核心原理

相邻值填充的核心思想是:空间上相邻的要素往往具有相似属性值,尤其是对于分类数据(地类、土壤类型)和具有空间连续性的数据(植被覆盖、土地利用)。

相邻值填充适用场景

  • 分类数据:地类编码、土壤类型、植被类型
  • 空间连续性强的数据:坡度分类、土地利用类型
  • 拓扑关系明确的数据:相邻地块、相邻乡镇

4.2 三种相邻值填充方法

方法 原理 操作 适用场景
最近邻填充 用空间距离最近的非空要素值填充 计算质心距离,取最近非空值 通用场景
** majority 填充** 用相邻 N 个要素中出现频率最高的值填充 取前 N 邻,计算众数 分类数据(地类)
上行填充/下行填充 按属性排序后,用前一个或后一个非空值填充 按字段排序,向前或向后填充 有序数据(如剖面数据、时间序列)

4.3 QGIS 中相邻值填充的实操方法

方法 1:使用 Nearest Neighbour Aggregation 插件

操作步骤:

  1. 处理面板搜索"Nearest Neighbour Aggregation"
  2. 输入参数:
    • 主图层:含空值的矢量图层
    • 连接图层:通常为自身
    • 连接字段:要填充的字段
    • 最近邻数量:3~10(默认 3)
    • 聚合方法:Mean / Median / Mode / First
  3. 运行

方法 2:Python 脚本实现最近邻填充

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
QGIS 空间数据相邻值填充脚本(最近邻 + Majority 方法)
"""

import argparse
import geopandas as gpd
import numpy as np
from scipy.spatial import cKDTree
import sys


def nearest_neighbor_fill(gdf, target_field, k=3):
    """
    使用最近邻方法填充空值
    
    参数:
        gdf : GeoDataFrame
        target_field : str
        k : int
            使用的最近邻数量
    
    返回:
        filled_gdf : GeoDataFrame
    """
    filled_gdf = gdf.copy()
    
    known_mask = filled_gdf[target_field].notna()
    unknown_mask = filled_gdf[target_field].isna()
    
    if not known_mask.any() or not unknown_mask.any():
        return filled_gdf
    
    known_coords = np.array([(x, y) for x, y in filled_gdf[known_mask].geometry.centroid.coords])
    unknown_coords = np.array([(x, y) for x, y in filled_gdf[unknown_mask].geometry.centroid.coords])
    
    # 构建 KD-Tree 加速最近邻搜索
    tree = cKDTree(known_coords)
    
    # 查询每个未知点的最近 k 个已知点
    distances, indices = tree.query(unknown_coords, k=min(k, known_mask.sum()))
    
    # 取最近邻的值
    known_values = filled_gdf[known_mask][target_field].values
    filled_values = known_values[indices[:, 0]]  # 取最近的一个
    
    unknown_indices = filled_gdf[unknown_mask].index
    filled_gdf.loc[unknown_indices, target_field] = filled_values
    
    print(f"最近邻填充完成:填充了 {len(filled_values)} 条记录,k={k}")
    return filled_gdf


def majority_neighbor_fill(gdf, target_field, k=5):
    """
    使用多数邻域方法填充分类数据空值
    取最近 k 个已知点的众数作为填充值
    """
    filled_gdf = gdf.copy()
    
    known_mask = filled_gdf[target_field].notna()
    unknown_mask = filled_gdf[target_field].isna()
    
    if not known_mask.any() or not unknown_mask.any():
        return filled_gdf
    
    known_coords = np.array([(x, y) for x, y in filled_gdf[known_mask].geometry.centroid.coords])
    unknown_coords = np.array([(x, y) for x, y in filled_gdf[unknown_mask].geometry.centroid.coords])
    
    tree = cKDTree(known_coords)
    distances, indices = tree.query(unknown_coords, k=min(k, known_mask.sum()))
    
    known_values = filled_gdf[known_mask][target_field].values
    filled_values = []
    
    for idx in indices:
        neighbor_vals = known_values[idx]
        # 取众数
        unique, counts = np.unique(neighbor_vals, return_counts=True)
        filled_values.append(unique[np.argmax(counts)])
    
    unknown_indices = filled_gdf[unknown_mask].index
    filled_gdf.loc[unknown_indices, target_field] = filled_values
    
    print(f"多数邻域填充完成:填充了 {len(filled_values)} 条记录,k={k}")
    return filled_gdf


def main():
    parser = argparse.ArgumentParser(description='QGIS 空间数据相邻值填充')
    parser.add_argument('--input', required=True)
    parser.add_argument('--output', required=True)
    parser.add_argument('--field', required=True)
    parser.add_argument('--method', default='nearest', choices=['nearest', 'majority'],
                        help='填充方法')
    parser.add_argument('--k', type=int, default=5, help='邻域数量')
    args = parser.parse_args()
    
    gdf = gpd.read_file(args.input)
    
    if args.method == 'nearest':
        result = nearest_neighbor_fill(gdf, args.field, args.k)
    else:
        result = majority_neighbor_fill(gdf, args.field, args.k)
    
    gdf_ext = args.output.lower().split('.')[-1]
    if gdf_ext == 'shp':
        result.to_file(args.output, driver='ESRI Shapefile')
    elif gdf_ext == 'geojson':
        result.to_file(args.output, driver='GeoJSON')
    elif gdf_ext == 'gpkg':
        result.to_file(args.output, driver='GPKG')
    
    remaining = result[args.field].isnull().sum()
    print(f"输出文件:{args.output},剩余空值:{remaining}")


if __name__ == '__main__':
    main()

4.4 相邻值填充的行业应用

行业 字段 填充方法 邻域数量 k 说明
国土调查 地类编码 majority 5~9 相邻地块地类通常一致
生态 植被类型 nearest 3 相邻样地植被相似
水利 土壤类型 majority 5 相邻区域土壤有地带性
城建 建筑年代段 majority 3 同街区建筑年代相近
农业 作物类型 nearest 3 相邻农田作物一致

五、填充策略的选择原则

5.1 基于数据特征的策略选择

数据类型 空值比例 推荐策略 理由
分类数据,空值 < 5% 少量 最近邻填充 保持分类一致性,不引入新类别
分类数据,空值 5%~30% 中等 Majority 填充 利用空间邻域信息,更稳健
分类数据,空值 > 30% 大量 人工判读 + 众数 空值太多时自动填充风险高
连续数值,空值 < 10% 少量 IDW / 克里金插值 利用空间自相关性
连续数值,空值 10%~50% 中等 克里金插值 + 误差估计 提供不确定性评估
连续数值,空值 > 50% 大量 不建议自动填充 样本不足,填充结果不可靠
序列/剖面数据 任意 上行/下行填充 保持序列连续性

5.2 三种填充策略的综合对比

对比维度 默认值填充 插值填充 相邻值填充
操作复杂度 ⭐⭐⭐ ⭐⭐
空间一致性
统计合理性 中等 中等
适用数据类型 所有类型 连续数值 分类 + 有序数据
是否需要空间自相关
计算速度 中等
结果可信度 低(可能引入偏差) 高(有统计依据) 中高(依赖邻域质量)

5.3 填充策略的最佳实践流程

复制代码
1. 空值识别与统计分析(参考第 026 篇)
   ↓
2. 判断空值比例
   ├─ < 1% → 直接删除(或删除法填充)
   └─ ≥ 1% → 进入第 3 步
   ↓
3. 判断数据类型
   ├─ 分类数据 → 相邻值填充(最近邻 / Majority)
   ├─ 连续数值 → 插值填充(IDW / 克里金)
   └─ 无空间自相关 → 默认值填充(均值 / 中位数 / 众数)
   ↓
4. 执行填充
   ↓
5. 质量验证(对比填充前后统计分布、空间分布)

六、填充后质量验证

6.1 验证指标与方法

验证维度 验证方法 验证指标 合格标准
统计分布 填充前后直方图对比 Kolmogorov-Smirnov 检验 p > 0.05(分布无显著差异)
空间分布 填充前后空间自相关对比 Moran's I 填充后 Moran's I 变化 < 10%
交叉验证 留出法验证 RMSE(均方根误差) RMSE < 空值字段标准差的 0.5
业务规则 逻辑校验 业务规则命中次数 0 次冲突

6.2 Python 自动化验证脚本

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
QGIS 空间数据填充后质量验证脚本
"""

import argparse
import geopandas as gpd
import pandas as pd
import numpy as np
from scipy import stats
from pykrige.ok import OrdinaryKriging
import sys


def verify_fill_quality(original_gdf, filled_gdf, field, n_points=100):
    """
    验证填充质量
    
    参数:
        original_gdf : GeoDataFrame
            填充前的原始数据
        filled_gdf : GeoDataFrame
            填充后的数据
        field : str
            验证字段
        n_points : int
            留出验证点数
    
    返回:
        dict : 验证结果
    """
    results = {}
    
    # 1. 统计分布验证(K-S 检验)
    known_original = original_gdf[field].dropna()
    filled_values = filled_gdf.loc[original_gdf[field].isna(), field]
    
    if len(filled_values) > 2 and len(known_original) > 2:
        ks_stat, ks_p = stats.kstest(known_original, filled_values)
        results['ks_stat'] = ks_stat
        results['ks_p'] = ks_p
        results['distribution_ok'] = ks_p > 0.05
    
    # 2. 留出验证(将部分已知点视为空值,验证填充效果)
    known_indices = original_gdf[field].dropna().index
    if len(known_indices) >= 2 * n_points:
        # 随机留出 n_points 个已知点
        validate_indices = np.random.choice(known_indices, n_points, replace=False)
        true_values = original_gdf.loc[validate_indices, field].values
        
        # 模拟:将这些点设为空值
        temp_gdf = original_gdf.copy()
        temp_gdf.loc[validate_indices, field] = np.nan
        
        # 用 IDW 填充
        temp_gdf = idw_interpolation(temp_gdf, field, power=2, k_neighbors=10)
        predicted_values = temp_gdf.loc[validate_indices, field].values
        
        # 计算 RMSE
        rmse = np.sqrt(np.mean((true_values - predicted_values) ** 2))
        std_true = np.std(true_values)
        results['rmse'] = rmse
        results['rmse_ratio'] = rmse / std_true if std_true > 0 else float('inf')
        results['validation_ok'] = rmse / std_true < 0.5 if std_true > 0 else False
    
    # 3. 空间自相关验证
    from statsmodels.sandbox.stats.multitest import multipletests
    
    known_mask = original_gdf[field].notna()
    if known_mask.sum() >= 10:
        coords = np.array([(x, y) for x, y in original_gdf[known_mask].geometry.centroid.coords])
        values = original_gdf.loc[known_mask, field].values
        
        # 简单计算 Moran's I(使用空间权重矩阵)
        # 这里简化为:只检查填充前后空间变异性变化
        results['known_std'] = np.std(values)
        results['filled_std'] = np.std(filled_values) if len(filled_values) > 1 else 0
        results['std_change'] = abs(results['filled_std'] - results['known_std']) / results['known_std'] \
            if results['known_std'] > 0 else float('inf')
        results['spatial_ok'] = results['std_change'] < 0.2  # 标准差变化 < 20%
    
    return results


# 使用示例
def main():
    parser = argparse.ArgumentParser(description='填充后质量验证')
    parser.add_argument('--original', required=True, help='原始文件')
    parser.add_argument('--filled', required=True, help='填充后文件')
    parser.add_argument('--field', required=True, help='验证字段')
    args = parser.parse_args()
    
    original_gdf = gpd.read_file(args.original)
    filled_gdf = gpd.read_file(args.filled)
    
    results = verify_fill_quality(original_gdf, filled_gdf, args.field)
    
    print("=== 填充质量验证报告 ===")
    print(f"字段:{args.field}")
    print(f"K-S 检验统计量:{results.get('ks_stat', 'N/A'):.4f}")
    print(f"K-S 检验 p 值:{results.get('ks_p', 'N/A'):.4f}")
    print(f"分布无显著差异:{'✅ 是' if results.get('distribution_ok') else '❌ 否'}")
    print(f"留出验证 RMSE:{results.get('rmse', 'N/A'):.4f}")
    print(f"RMSE / 标准差:{results.get('rmse_ratio', 'N/A'):.4f}")
    print(f"验证通过:{'✅ 是' if results.get('validation_ok') else '❌ 否'}")
    print(f"标准差变化率:{results.get('std_change', 'N/A'):.4f}")
    print(f"空间一致性:{'✅ 通过' if results.get('spatial_ok') else '⚠️ 需检查'}")


if __name__ == '__main__':
    main()

七、综合案例:某省国土变更调查数据空值填充

7.1 项目背景

某省第三次国土调查数据中,部分图斑的属性字段存在空值:

字段 总记录数 空值数 空值率
地类编码(GBDM) 1,250,000 12,500 1.0%
坡度等级(PDJD) 1,250,000 45,000 3.6%
土层厚度(TCDC) 1,250,000 187,500 15.0%
土壤有机质(YJYXZ) 1,250,000 500,000 40.0%

7.2 填充策略选择

字段 数据类型 空值率 策略 理由
地类编码 分类 1.0% 最近邻填充(k=5) 空值率低,相邻图斑地类通常一致
坡度等级 分类 3.6% Majority 填充(k=7) 中等空值率,利用邻域多数一致
土层厚度 连续数值 15.0% IDW 插值(幂次=2,k=10) 连续变量,空间自相关性强
土壤有机质 连续数值 40.0% 不建议自动填充 空值率过高,需人工补测

7.3 填充流程

bash 复制代码
# 第 1 步:地类编码 --- 最近邻填充
python neighbor_fill.py --input survey.shp --output filled_gbdm.shp \
    --field GBDM --method nearest --k 5

# 第 2 步:坡度等级 --- Majority 填充
python neighbor_fill.py --input survey.shp --output filled_pdjd.shp \
    --field PDJD --method majority --k 7

# 第 3 步:土层厚度 --- IDW 插值
python idw_fill.py --input survey.shp --output filled_tcdc.shp \
    --field TCDC --power 2 --neighbors 10

# 第 4 步:质量验证
python verify_fill.py --original survey.shp --filled filled_combined.shp \
    --field TCDC

7.4 验证结果

字段 K-S p 值 RMSE / 标准差 标准差变化 验证结论
地类编码 0.127 N/A N/A ✅ 分布无显著差异
坡度等级 0.083 N/A N/A ✅ 分布无显著差异
土层厚度 0.215 0.342 8.5% ✅ 通过验证
土壤有机质 --- --- --- ⚠️ 留空,人工补测

八、常见问题与最佳实践

8.1 填充策略选择常见问题

问题 原因 解决方案 最佳实践
填充后数据失去空间变异性 用了均值/常数填充 改用空间插值或相邻值填充 有空间信息的字段优先用空间方法
插值结果出现异常值 幂次设置过高或邻域过小 降低幂次或增大邻域 IDW 幂次在 1~4 之间测试
分类字段填充后某类占比畸高 众数填充或邻域不均衡 改用 majority 填充或增大 k 对于分类字段,k=5~9 最稳健
填充结果与实际业务规则冲突 填充未考虑业务逻辑 填充后做逻辑校验 例如:坡度 = 0 的地类编码不能是"建筑用地"
插值计算太慢 数据量过大 先降采样,或分区块插值 超过 10 万要素建议分块处理

8.2 填充策略最佳实践总结

原则 说明
先分析后填充 必须先做空值统计分析(参考第 026 篇),再选择策略
空间优先 有空间信息的字段优先用空间方法(插值、相邻值),而非默认值
留出一部分验证 保留 20% 的已知数据不参与填充,用于验证填充质量
记录填充过程 记录填充方法、参数、填充量,确保可追溯
谨慎对待高比例空值 空值率 > 30% 时不建议自动填充,应人工补测或标注"数据缺失"

九、总结

本节介绍了三种空值填充策略:

  1. 默认值填充:最简单,适用于无空间自相关的数据或空值率极低的场景
  2. 插值填充:最严谨,适用于连续型空间数据,利用空间自相关原理
  3. 相邻值填充:最实用,适用于分类数据和具有空间一致性的数据

核心原则

  • 填充方法的选择取决于数据类型空值比例空间自相关性
  • 填充后必须进行质量验证,否则可能传播错误信息
  • 对于高比例空值(> 30%),不建议自动填充,应人工补测或标注

下一节将深入探讨更高级的空值处理技术------基于机器学习的缺失值预测填充方法。


相关推荐
大熊背3 天前
ISP模块参数统一对外接口插值逻辑
架构设计·插值·解耦·藕合
551只玄猫2 个月前
【数学建模 matlab 实验报告10】插值
开发语言·数学建模·matlab·课程设计·插值·实验报告
点云SLAM7 个月前
Boost库中Math 模块的插值(interpolation使用和示例
算法·插值·boost库·b-spline·akima 样条·单调三次样条·barycentric 插值
little_fat_sheep9 个月前
【OpenGL ES】光栅化插值原理和射线拾取原理
光栅化·插值·射线拾取·射线与三角形交点·重心
阑梦清川1 年前
关于插值和拟合(数学建模实验课)
数学建模·插值·拟合·三次样条插值
回音谷1 年前
【算法】克里金(Kriging)插值原理及Python应用
python·算法·插值
AomanHao1 年前
【阅读笔记】基于FPGA的红外图像二阶牛顿插值算法的实现
图像处理·笔记·算法·fpga开发·插值·超分
胡西风_foxww1 年前
【es6复习笔记】函数参数的默认值(6)
javascript·笔记·es6·参数·函数·默认值
LabVIEW开发2 年前
LabVIEW界面输入值设为默认值
labview·开发技巧·默认值