空间数据清洗与预处理

做空间数据分析这些年,我踩过最多的坑不在建模环节,反而在数据准备阶段。说白了一句话:垃圾进,垃圾出。你算法再漂亮,拿到一份缺胳膊断腿、拓扑关系全乱的数据,结果也是废的。

这一章咱们就专门聊聊数据清洗的四个硬骨头:缺失值处理、异常值检测、数据一致性检查、拓扑错误修复。我个人的习惯是,拿到任何空间数据集,先按这四个维度过一遍,再谈后续分析。这样能省掉至少一半的返工时间。

空间数据清洗与预处理流程图 空间数据清洗与预处理流程 原始空间数据集 缺失值处理 异常值检测 一致性检查 拓扑错误修复 删除法 插补法 IQR 空间Z值 属性校验 几何校验 悬线处理 缝隙闭合 四项工作流程互有重叠,建议按顺序执行

缺失值处理

空间数据里的缺失值,和普通表格数据不一样。它可能是某个栅格像元没有值(NoData),也可能是矢量数据的属性字段是空的。我记得有一次做城市绿地分析,到手的数据有一半的绿地斑块缺少“面积”属性,当时差点崩溃。

处理缺失值,常用的方法就这么几类:

  • 删除法:直接扔掉有缺失的记录或像元。如果你缺失比例小于5%,且缺失是随机的,这招最省事。
  • 均值/中位数插补:用周围邻居的均值填进去。对栅格数据尤其好用,像 ArcGIS 的 Nibble 工具就是干这个的。
  • 克立金插值(Kriging):适合有空间自相关特征的数据。我曾为土壤采样点的缺失值做过一次普通克立金插值,效果比简单均值好太多。

我个人的经验法则:缺失比例超过20%时,不要无脑插补。先搞清楚缺失原因——是传感器故障还是采样设计问题,否则插出来的数据会带偏后续分析。

下面是一个 QGIS 中使用栅格计算器做简单邻域插补的脚本片段:

# QGIS Python 控制台:栅格 NoData 填充
import processing
from qgis.core import QgsRasterLayer

# 假设你已经加载了一个名为 "dem.tif" 的 DEM 图层
input_raster = "dem.tif"
output_raster = "dem_filled.tif"

# 使用 Fill NoData 工具,基于周围有效像元
processing.run("gdal:fillnodata", {
    'INPUT': input_raster,
    'BAND': 1,
    'DISTANCE': 10,
    'OUTPUT': output_raster
})

异常值检测

空间数据里的异常值往往不按常理出牌。你想想看,一个高程点突然比周围高出100米,或者一个湖泊的周长面积比离谱到爆——这些都要揪出来。

我常用的检测手段有以下几种:

方法适用场景说明
箱线图(IQR)属性值异常超过1.5倍四分位距的视为异常
空间局部Z值空间分布异常计算每个点与其邻居的标准化差值
几何规则检查矢量图形异常如狭长多边形、自相交环
领域上下文检查专题数据土地利用类型与周围不协调

记得有一次做土地利用变化分析,发现一块“森林”斑块的大小在5年内暴涨了20倍,结合遥感影像一看,原来是当年分类错误——把新造林也算进去了。这就是典型的异常值藏在属性里的案例。

避坑提醒:不要孤立地看一个异常值。我曾经直接删掉了一个看起来“离谱”的高程点,后来发现那个点是雷达回波的真实峰值,删了反而让地形分析失真。先做空间可视化再做决策,是个好习惯。

用 Python 检测空间数据的局部异常,可以这么写:

import geopandas as gpd
import numpy as np
from shapely.geometry import Point

# 读取空间数据
gdf = gpd.read_file("building_footprints.shp")

# 计算每个面要素的面积
gdf['area'] = gdf.geometry.area

# 用 IQR 方法检测面积异常值
Q1 = gdf['area'].quantile(0.25)
Q3 = gdf['area'].quantile(0.75)
IQR = Q3 - Q1

lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR

outliers = gdf[(gdf['area'] < lower_bound) | (gdf['area'] > upper_bound)]
print(f"检测到 {len(outliers)} 个面积异常要素")

数据一致性检查

一致性检查说白了就是看数据内部的“逻辑”对不对。我给你列几个最常见的检查项:

  • 属性域一致性:字段取值是否在合理范围内,比如“土地利用类型”不能填“3.14”这类浮点数。
  • 几何合法性:多边形的环方向是否正确?有没有自相交?
  • 时空一致性:同一位置不同时段的属性变化是否合理,比如1990年的城区面积不可能比2020年还大。

这里有一张我整理的检查清单表格,大家可以照着做:

检查项检查方式典型问题
字段类型对比 Schema 定义数字字段存了字符串
唯一值检查统计 distinct countID 字段重复
空间参考对比 EPSG 编码多个图层 CRS 不一致
范围检查Min/Max 统计高程出现负数
逻辑关系字段间比对面积小于0或大于行政边界总面积

小技巧:用 FME 或 QGIS 的 Processing Toolbox 可以批量建检查流程。我这里有一个简化版的检查脚本,专门查属性域一致性:

# 用 GeoPandas 检查属性域
valid_landuse = [1, 2, 3, 4, 5]  # 假设有效编码
gdf = gpd.read_file("landuse.shp")

inconsistent = gdf[~gdf['landuse_code'].isin(valid_landuse)]
print(f"发现 {len(inconsistent)} 条无效土地利用编码")

# 输出异常记录到独立文件
inconsistent.to_file("landuse_inconsistent.shp")

拓扑错误修复

拓扑错误是矢量数据的老大难。面重叠、缝隙、悬线、伪节点——这些错误不修掉,后续的叠加分析、空间连接全是错的。

我经历过的惨痛教训:有一次做城市路网分析,拓扑检查做了三遍才把悬线全清掉。为什么?因为部分路段的端点差了0.001个度,肉眼根本看不出来,但网络分析就是跑不通。

常见的拓扑错误类型:

  1. 悬线(Dangles):线要素端点没有与其他线连接。多出现在交通网络数据。
  2. 伪节点(Pseudo Nodes):一个节点只有两条线连接,这通常是数字化错误。
  3. 缝隙(Gaps):相邻面之间有空隙,多出现在行政区或地块数据。
  4. 重叠(Overlaps):面要素交叉覆盖,常见于多源数据融合时。

下面是一个用 PostGIS 修复拓扑错误的通用流程:

-- 1. 创建拓扑结构
SELECT CreateTopology('my_topology', 4326);

-- 2. 将要素加入拓扑层
SELECT TopoGeo_AddPolygon('my_topology', 'land_parcels', 'geom', 'gid');

-- 3. 检查拓扑错误
SELECT * FROM my_topology.edge WHERE ST_IsValid(geom) = false;

-- 4. 自动修复悬线和缝隙
-- 注意:ST_Snap 可以将不匹配的节点吸附到容差范围内
UPDATE land_parcels
SET geom = ST_Snap(geom, ref_geom, 0.001)
FROM (SELECT ST_Collect(geom) AS ref_geom FROM land_parcels) AS ref;

我个人特别推荐:QGIS 的 v.clean 工具(来自 GRASS GIS 集成)功能强大,可以一次性处理大部分拓扑问题。你只要设置一个合理的 snap tolerance 和 threshold,剩下的交给它自动跑。但要注意——每次跑完一定要手动抽检几个局部区域,因为自动算法有时会过度修改。

做个总结:数据清洗没有捷径,但有流程可依。我的习惯是:miss-check-consistency-fix,四步走一遍,再用空间可视化扫一眼。如果你赶时间,至少要把拓扑错误修掉——这是很多后续分析出错的根本原因。

最后提醒一句:永远保留一份原始数据的备份。数据清洗是迭代过程,一次过的清洗方案很少存在。我保留下来的原则是“不改原数据,只改副本”,这样万一出了偏差还能回退。