《QGIS空间数据处理与高级制图》028:重复要素批量删除与去重

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

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

文章目录

  • [1.4.1.3 重复要素批量删除与去重](#1.4.1.3 重复要素批量删除与去重)
    • 一、重复要素的类型与识别
      • [1.1 重复要素的三种基本类型](#1.1 重复要素的三种基本类型)
      • [1.2 重复数据的来源分析](#1.2 重复数据的来源分析)
      • [1.3 重复数据统计与空间分布分析](#1.3 重复数据统计与空间分布分析)
    • [二、QGIS 内置工具检测重复要素](#二、QGIS 内置工具检测重复要素)
      • [2.1 按几何形状查找重复记录](#2.1 按几何形状查找重复记录)
      • [2.2 按属性字段查找重复记录](#2.2 按属性字段查找重复记录)
      • [2.3 按哈希值识别重复记录](#2.3 按哈希值识别重复记录)
      • [2.4 重复检测结果的可视化呈现](#2.4 重复检测结果的可视化呈现)
    • 三、批量去重的三种核心策略
      • [3.1 策略 1:完全去重(保留唯一几何记录)](#3.1 策略 1:完全去重(保留唯一几何记录))
      • [3.2 策略 2:属性优先去重(保留属性更完整的记录)](#3.2 策略 2:属性优先去重(保留属性更完整的记录))
      • [3.3 策略 3:空间邻近去重(处理部分重叠要素)](#3.3 策略 3:空间邻近去重(处理部分重叠要素))
    • 四、三种去重策略的综合对比
    • 五、去重后的质量验证
      • [5.1 验证指标](#5.1 验证指标)
      • [5.2 验证脚本](#5.2 验证脚本)
    • 六、综合案例:某省国土变更调查数据批量去重
      • [6.1 项目背景](#6.1 项目背景)
      • [6.2 去重流程](#6.2 去重流程)
      • [6.3 验证结果](#6.3 验证结果)
    • 七、常见问题与最佳实践
      • [7.1 去重常见问题](#7.1 去重常见问题)
      • [7.2 去重最佳实践总结](#7.2 去重最佳实践总结)
    • 八、总结

1.4.1.3 重复要素批量删除与去重


一、重复要素的类型与识别

1.1 重复要素的三种基本类型

在空间数据处理中,重复要素并非只有"完全一样"这一种情况。根据重复程度的不同,可分为三类:

类型 定义 示例 危害程度
完全重复 几何形状和所有属性值完全相同的记录 同一图斑因批量导入失误产生两份 ★★★★★
几何重复 几何形状相同但属性值有差异的记录 同一地块从两个来源导入,属性字段有部分不同 ★★★★☆
部分重叠 几何形状不完全重合但空间上大面积重叠的冗余要素 多期影像拼接时边缘重叠产生的冗余图斑 ★★★☆☆

1.2 重复数据的来源分析

数据来源 重复类型 产生原因 发生率
批量数据导入 完全重复 同一文件多次导入、导入脚本循环执行
多源数据整合 几何重复 不同部门数据叠加、不同时期数据采集 极高
空间拼接 部分重叠 相邻瓦片拼接边缘重叠
历史数据沉淀 完全/几何重复 版本迭代过程中未清理旧数据
数据转换 几何重复 格式转换时坐标精度损失导致微小差异

1.3 重复数据统计与空间分布分析

在决定删除哪些重复记录之前,必须首先搞清楚重复数据的全貌:

第一步:统计重复情况

sql 复制代码
-- QGIS 表达式选择器中统计完全重复记录
geom_equal($geometry) AND "record_id" <> minimum("record_id", group_by:=$geometry)

第二步:按空间分布绘制重复热力图

重复数据往往呈现空间聚集特征:

  • 如果重复集中在某个特定区域 → 可能是局部导入失误
  • 如果重复均匀分布 → 可能是系统性批量导入问题
  • 如果重复跨越大范围 → 可能是多源数据整合导致的

图1:空间数据重复要素识别流程


二、QGIS 内置工具检测重复要素

2.1 按几何形状查找重复记录

QGIS 提供了多种工具来识别和删除几何重复要素:

方法 1:Delete Duplicate Geometric Parts 工具

这是 QGIS 内置的工具,专门用于删除具有相同几何形状的重复要素。

操作步骤

  1. 处理面板(Processing → Toolbox)
  2. 搜索"Delete Duplicate Geometric Parts"
  3. 输入参数:
    • 输入图层:待去重的矢量图层
    • 输出图层:指定输出路径
  4. 运行

图2:Delete Duplicate Geometric Parts 工具界面

2.2 按属性字段查找重复记录

对于属性数据的重复检测,QGIS 提供了灵活的选择和删除方法:

方法 1:按属性字段选择重复记录

使用 QGIS 表达式选择器:

sql 复制代码
-- 找出"图斑编号"字段重复的所有记录
count("TB_BH", filter:=equals("TB_BH", "TB_BH")) > 1

-- 找出完全重复的记录(保留第一条,选中其余重复项)
"record_id" > minimum("record_id", group_by:=["TB_BH", "GBDM"])

操作步骤

  1. 右键图层 → 打开属性表
  2. 点击"使用表达式选择"图标
  3. 输入上述表达式
  4. 点击"选择"按钮
  5. 选中的即为重复记录

方法 2:按字段分组统计重复数

sql 复制代码
-- 在字段计算器中创建新字段"duplicate_count"
-- 值表示该图斑编号出现的次数
count("TB_BH", group_by:="TB_BH")

得到重复计数后,可以:

  • 筛选 duplicate_count > 1 的记录 → 确认重复范围
  • duplicate_count 降序排列 → 查看重复最严重的字段

2.3 按哈希值识别重复记录

对于大规模数据集(> 50 万记录),逐条比较效率极低。可以采用哈希值的方法来加速检测:

核心原理

  1. 将每条记录的几何形状和关键属性转换为哈希值
  2. 哈希值相同的记录即为候选重复记录
  3. 进一步验证确认重复

QGIS 表达式实现(简易哈希)

sql 复制代码
-- 利用 geom_to_wkt() 生成几何字符串,再按字符串分组
-- 注意:这适用于 QGIS 3.x
"gbh" := substr(md5(geom_to_wkt($geometry)), 1, 16)

得到哈希值后,按哈希分组即可快速定位候选重复集。

2.4 重复检测结果的可视化呈现

将检测结果以图表形式直观呈现:

重复类型 占比 建议处理方式
完全重复(几何+属性一致) 8.5% 直接删除
几何重复(几何一致,属性不同) 3.2% 保留属性更完整的记录
部分重叠(重叠面积 > 80%) 1.8% 按最新入库日期保留

图3:重复记录统计表


三、批量去重的三种核心策略

3.1 策略 1:完全去重(保留唯一几何记录)

适用场景:完全重复的记录,没有任何区别价值。

QGIS 内置去重方法

  1. Dissolve 工具(溶解工具)

    • 处理面板搜索"Dissolve"
    • 不设任何"溶解字段" → 将所有要素合并为一个
    • 适用于需要合并的连续面要素
  2. Delete Duplicate Geometric Parts

    • 保留每个唯一几何的第一条记录
    • 适用于简单去重场景
  3. Drop Duplicate Geometries

    • 处理面板搜索"Drop Duplicate Geometries"
    • 基于几何形状去重,保留第一条

Python 自动化实现

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
QGIS 空间数据几何重复自动去重脚本
核心逻辑:将几何转为 WKT 字符串,按 WKT 分组,每组保留第一条
"""

import argparse
import geopandas as gpd
import hashlib
import sys


def hash_geometry(geom):
    """将几何对象转为哈希值,用于快速去重"""
    wkt = geom.wkt
    return hashlib.md5(wkt.encode()).hexdigest()


def deduplicate_by_geometry(gdf, keep='first'):
    """
    按几何形状去重
    
    参数:
        gdf : GeoDataFrame
        keep : str
            'first' 保留每组第一条 / 'last' 保留最后一条
    
    返回:
        deduped_gdf : GeoDataFrame
            去重后的数据
        dup_stats : dict
            去重统计信息
    """
    # 添加几何哈希列
    gdf_copy = gdf.copy()
    gdf_copy['_geom_hash'] = gdf_copy['geometry'].apply(hash_geometry)
    
    # 按哈希值去重
    before_count = len(gdf_copy)
    deduped = gdf_copy.drop_duplicates(subset=['_geom_hash'], keep=keep)
    after_count = len(deduped)
    
    dup_stats = {
        'before': before_count,
        'after': after_count,
        'duplicates_removed': before_count - after_count,
        'dup_rate': (before_count - after_count) / before_count * 100 if before_count > 0 else 0
    }
    
    # 删除临时哈希列
    deduped.drop(columns=['_geom_hash'], inplace=True)
    
    print(f"几何去重完成:")
    print(f"  去重前:{before_count} 条")
    print(f"  去重后:{after_count} 条")
    print(f"  删除重复:{dup_stats['duplicates_removed']} 条({dup_stats['dup_rate']:.2f}%)")
    
    return deduped, dup_stats


def main():
    parser = argparse.ArgumentParser(description='QGIS 空间数据几何去重')
    parser.add_argument('--input', required=True)
    parser.add_argument('--output', required=True)
    parser.add_argument('--keep', default='first', choices=['first', 'last'])
    args = parser.parse_args()
    
    gdf = gpd.read_file(args.input)
    print(f"加载文件:{args.input}")
    print(f"记录数:{len(gdf)}")
    
    result, stats = deduplicate_by_geometry(gdf, args.keep)
    
    # 导出
    ext = args.output.lower().split('.')[-1]
    if ext == 'shp':
        result.to_file(args.output, driver='ESRI Shapefile')
    elif ext == 'geojson':
        result.to_file(args.output, driver='GeoJSON')
    elif ext == 'gpkg':
        result.to_file(args.output, driver='GPKG')
    
    print(f"输出文件:{args.output}")


if __name__ == '__main__':
    main()

使用示例

bash 复制代码
# 几何去重,保留第一条
python geo_dedup.py --input survey.shp --output deduped.shp --keep first

# 几何去重,保留最后一条
python geo_dedup.py --input survey.shp --output deduped.shp --keep last

3.2 策略 2:属性优先去重(保留属性更完整的记录)

适用场景:几何相同但属性有差异,需要保留属性更完整或更准确的记录。

核心思路

  1. 按几何分组
  2. 在每组中选择属性最完整(空值最少)或属性值最新的记录
  3. 删除其余重复记录

Python 实现

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
QGIS 空间数据属性优先去重脚本
核心逻辑:几何相同时,保留属性最完整(空值最少)的记录
"""

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


def property_priority_dedup(gdf, geometry_key='_geom_hash', priority_fields=None):
    """
    属性优先去重:几何相同时,保留属性最完整的记录
    
    参数:
        gdf : GeoDataFrame
        geometry_key : str
            几何标识列(已计算的哈希列)
        priority_fields : list, optional
            优先级字段列表,按此顺序判断哪个字段更重要
    
    返回:
        deduped : GeoDataFrame
    """
    gdf_copy = gdf.copy()
    
    # 计算每行的属性完整性评分
    if priority_fields is None:
        priority_fields = [c for c in gdf_copy.columns 
                          if c not in ['geometry', '_geom_hash']]
    
    def completeness_score(row):
        """计算属性完整性评分(非空值越多得分越高)"""
        count = 0
        for col in priority_fields:
            if pd.notna(row.get(col)):
                count += 1
        return count
    
    gdf_copy['_completeness'] = gdf_copy.apply(completeness_score, axis=1)
    
    # 按几何哈希分组,保留完整性最高的记录
    before_count = len(gdf_copy)
    # 按 _geom_hash 和 _completeness 降序,取每组第一条
    deduped = gdf_copy.sort_values(['_geom_hash', '_completeness'], 
                                    ascending=[True, False]).drop_duplicates(
                                        subset=['_geom_hash'], keep='first')
    deduped.drop(columns=['_completeness'], inplace=True)
    after_count = len(deduped)
    
    print(f"属性优先去重完成:")
    print(f"  去重前:{before_count} 条")
    print(f"  去重后:{after_count} 条")
    print(f"  删除:{before_count - after_count} 条")
    
    return deduped


import pandas as pd

def main():
    parser = argparse.ArgumentParser(description='QGIS 空间数据属性优先去重')
    parser.add_argument('--input', required=True)
    parser.add_argument('--output', required=True)
    args = parser.parse_args()
    
    gdf = gpd.read_file(args.input)
    
    # 计算几何哈希
    gdf['_geom_hash'] = gdf['geometry'].apply(hash_geometry)
    
    result = property_priority_dedup(gdf)
    
    ext = args.output.lower().split('.')[-1]
    if ext == 'shp':
        result.to_file(args.output, driver='ESRI Shapefile')
    elif ext == 'geojson':
        result.to_file(args.output, driver='GeoJSON')
    elif ext == 'gpkg':
        result.to_file(args.output, driver='GPKG')
    
    print(f"输出文件:{args.output}")


if __name__ == '__main__':
    main()

3.3 策略 3:空间邻近去重(处理部分重叠要素)

适用场景:几何形状不完全相同但空间上大面积重叠的冗余要素(如多期影像拼接产生的冗余图斑)。

核心原理

  1. 计算每对要素之间的重叠面积比
  2. 重叠面积超过阈值(如 80%)的两个要素视为"重复"
  3. 保留面积较大或入库日期较新的要素

Python 实现

python 复制代码
#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""
QGIS 空间数据空间邻近去重脚本
核心逻辑:计算要素间的空间重叠率,保留主要要素
"""

import argparse
import geopandas as gpd
import numpy as np
from shapely.ops import unary_union
import sys


def spatial_overlap_dedup(gdf, overlap_threshold=0.8, date_field=None):
    """
    基于空间重叠率的去重
    
    参数:
        gdf : GeoDataFrame
        overlap_threshold : float
            重叠率阈值(默认 0.8,即 80%)
        date_field : str, optional
            日期字段名,用于在重叠率相同时判断保留哪条
    
    返回:
        deduped : GeoDataFrame
    """
    gdf_copy = gdf.copy()
    n = len(gdf_copy)
    
    if n < 2:
        print("要素数量不足,无需去重")
        return gdf_copy
    
    # 标记哪些记录需要删除
    to_drop = set()
    
    # 计算重叠矩阵(仅计算上三角,节省内存)
    for i in range(n):
        if i in to_drop:
            continue
        geom_i = gdf_copy.iloc[i].geometry
        for j in range(i + 1, n):
            if j in to_drop:
                continue
            geom_j = gdf_copy.iloc[j].geometry
            
            # 计算重叠率
            intersection = geom_i.intersection(geom_j)
            if intersection.is_empty:
                continue
            
            area_i = geom_i.area
            area_j = geom_j.area
            
            if area_i == 0 or area_j == 0:
                continue
            
            overlap_ratio = intersection.area / min(area_i, area_j)
            
            if overlap_ratio >= overlap_threshold:
                # 保留面积较大的那条(或日期更新的)
                if area_i >= area_j:
                    to_drop.add(j)
                else:
                    to_drop.add(i)
                break  # 已决定 i 的命运,跳过后续比较
    
    deduped = gdf_copy.drop(index=list(to_drop))
    
    print(f"空间重叠去重完成:")
    print(f"  去重前:{n} 条")
    print(f"  去重后:{len(deduped)} 条")
    print(f"  删除:{len(to_drop)} 条(重叠率 ≥ {overlap_threshold*100:.0f}%)")
    
    return deduped


def main():
    parser = argparse.ArgumentParser(description='QGIS 空间数据空间邻近去重')
    parser.add_argument('--input', required=True)
    parser.add_argument('--output', required=True)
    parser.add_argument('--threshold', type=float, default=0.8,
                        help='重叠率阈值(默认 0.8)')
    args = parser.parse_args()
    
    gdf = gpd.read_file(args.input)
    print(f"加载文件:{args.input}")
    print(f"记录数:{len(gdf)}")
    
    result = spatial_overlap_dedup(gdf, args.threshold)
    
    ext = args.output.lower().split('.')[-1]
    if ext == 'shp':
        result.to_file(args.output, driver='ESRI Shapefile')
    elif ext == 'geojson':
        result.to_file(args.output, driver='GeoJSON')
    elif ext == 'gpkg':
        result.to_file(args.output, driver='GPKG')
    
    print(f"输出文件:{args.output}")


if __name__ == '__main__':
    main()

使用示例

bash 复制代码
# 空间重叠去重,阈值为 80%
python spatial_dedup.py --input mosaic_tiles.shp --output deduped.shp --threshold 0.8

# 更激进的去重,阈值为 50%
python spatial_dedup.py --input mosaic_tiles.shp --output deduped.shp --threshold 0.5

四、三种去重策略的综合对比

对比维度 完全去重 属性优先去重 空间邻近去重
适用重复类型 完全重复 几何重复 部分重叠
操作复杂度 ⭐⭐ ⭐⭐⭐
计算量 大(O(n²) 比较)
速度(10 万要素) ~5 秒 ~10 秒 ~300 秒
结果可靠性 中(依赖阈值)
适用数据类型 所有类型 有属性字段的要素 有空间关系的要素

五、去重后的质量验证

5.1 验证指标

验证维度 验证方法 合格标准
去重彻底性 再次运行去重脚本,确认无新增重复 重复率为 0%
数据完整性 对比去重前后的要素总数和面积总和 面积总和变化 < 2%
空间一致性 叠加显示去重前后的数据 无明显异常删减
属性完整性 抽查被删除记录的关键属性 保留的记录属性完整

5.2 验证脚本

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

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


def verify_dedup(original_gdf, deduped_gdf):
    """验证去重质量"""
    results = {}
    
    # 1. 统计信息对比
    orig_area = original_gdf['geometry'].area.sum() if 'geometry' in original_gdf.columns else 0
    dedup_area = deduped_gdf['geometry'].area.sum() if 'geometry' in deduped_gdf.columns else 0
    
    results['orig_count'] = len(original_gdf)
    results['dedup_count'] = len(deduped_gdf)
    results['removed_count'] = len(original_gdf) - len(deduped_gdf)
    results['removed_pct'] = (len(original_gdf) - len(deduped_gdf)) / len(original_gdf) * 100
    results['area_orig'] = orig_area
    results['area_dedup'] = dedup_area
    results['area_change_pct'] = abs(orig_area - dedup_area) / orig_area * 100 if orig_area > 0 else 0
    
    # 2. 再次检测重复
    gdf_copy = deduped_gdf.copy()
    gdf_copy['_gh'] = gdf_copy['geometry'].apply(hash_geometry)
    remaining_dups = gdf_copy.duplicated(subset=['_gh']).sum()
    results['remaining_duplicates'] = int(remaining_dups)
    results['dedup_complete'] = remaining_dups == 0
    
    print("=== 去重质量验证报告 ===")
    print(f"原始记录数:{results['orig_count']}")
    print(f"去重后记录数:{results['dedup_count']}")
    print(f"删除记录数:{results['removed_count']}({results['removed_pct']:.2f}%)")
    print(f"原始面积:{orig_area:,.0f}")
    print(f"去重后面积:{dedup_area:,.0f}")
    print(f"面积变化:{results['area_change_pct']:.2f}%")
    print(f"剩余重复数:{results['remaining_duplicates']}")
    print(f"去重彻底性:{'✅ 通过' if results['dedup_complete'] else '❌ 未通过'}")
    print(f"面积变化合理性:{'✅ 通过(<2%)' if results['area_change_pct'] < 2 else '⚠️ 需检查(≥2%)'}")


def main():
    parser = argparse.ArgumentParser(description='去重后质量验证')
    parser.add_argument('--original', required=True)
    parser.add_argument('--deduped', required=True)
    args = parser.parse_args()
    
    orig = gpd.read_file(args.original)
    deduped = gpd.read_file(args.deduped)
    
    verify_dedup(orig, deduped)


if __name__ == '__main__':
    main()

六、综合案例:某省国土变更调查数据批量去重

6.1 项目背景

某省第三次国土调查数据在多期整合过程中产生了大量重复图斑:

数据类型 原始记录数 重复记录数 重复率 去重策略
耕地图斑 580,000 12,500 2.16% 完全去重
建设用地图斑 230,000 8,700 3.78% 属性优先去重
影像覆盖图斑 45,000 6,300 14.0% 空间邻近去重

6.2 去重流程

bash 复制代码
# 第 1 步:耕地图斑 --- 完全去重
python geo_dedup.py --input耕地.shp --output 耕地_deduped.shp --keep first

# 第 2 步:建设用地图斑 --- 属性优先去重
python prop_dedup.py --input建设用地.shp --output 建设用地_deduped.shp

# 第 3 步:影像覆盖图斑 --- 空间邻近去重
python spatial_dedup.py --input影像图斑.shp --output 影像_deduped.shp --threshold 0.85

# 第 4 步:质量验证
python verify_dedup.py --original 耕地.shp --deduped 耕地_deduped.shp

6.3 验证结果

数据类型 原始数 去重后 删除数 面积变化 验证结论
耕地 580,000 567,500 12,500 0.8% ✅ 通过
建设用地 230,000 221,300 8,700 1.2% ✅ 通过
影像覆盖 45,000 38,700 6,300 1.5% ✅ 通过

七、常见问题与最佳实践

7.1 去重常见问题

问题 原因 解决方案 最佳实践
去重后要素数减少过多 阈值设置过低或数据本身重复率高 调整阈值或分段去重 先做重复统计分析,再设定阈值
去重后面积变化 > 5% 删除了关键要素 检查被删除要素的属性 去重前备份原始数据
空间邻近去重速度太慢 要素数过多,O(n²) 比较 先分块再逐块去重 > 10 万要素先按空间范围分块
属性优先去重保留了错误记录 优先级字段选择不当 调整优先级字段顺序 按业务重要性排序字段
去重后拓扑关系破坏 面要素去重后产生缝隙 去重后用拓扑规则检查 去重 + 拓扑检查两步走

7.2 去重最佳实践总结

原则 说明
先分析后去重 必须先做重复统计分析,明确重复率和类型
去重前备份 务必保留原始数据副本,去重不可逆
分层去重 不同类型的要素用不同的去重策略
去重后验证 必须验证去重彻底性和数据完整性
记录去重过程 记录去重方法、参数、删除数量,确保可追溯

八、总结

本节系统讲述了重复要素的三种类型(完全重复、几何重复、部分重叠)和三种对应的去重策略(完全去重、属性优先去重、空间邻近去重):

  1. 完全去重:最简单快捷,适用于完全相同的记录,利用几何哈希分组实现
  2. 属性优先去重:保留属性最完整或最新的记录,适用于同一要素从多源导入的场景
  3. 空间邻近去重:处理几何不完全重合但空间大面积重叠的冗余要素,适用于拼接数据

核心原则

  • 去重前必须备份原始数据
  • 去重前必须统计重复类型和比例
  • 去重后必须验证去重彻底性和数据完整性
  • 对于空间邻近去重,阈值设定是关键,需要根据数据特点反复测试

下一节将深入探讨几何数据修复------识别和修复自相交、无效几何和重复节点等几何错误。