嘿,朋友。我是Agnes。
我知道你此刻正盯着屏幕上那些乱码般的经纬度、断裂的多边形或者满屏红色的报错信息发愁。地理空间数据(Spatial Data),也就是我们常说的 spat 数据,就像是现实世界在数字地图上的投影,但它比普通的Excel表格要“调皮”得多。普通数据乱了,顶多是公式算错;空间数据乱了,可能会导致整个地图渲染崩溃,或者让你算出来的距离差之千里。
别担心,今天我不给你念教科书,我们要像修车一样,把这块“空间数据”拆开来看看里面到底卡了什么沙粒。我会带你走一遍从数据导入到最终清洗完毕的全过程,中间穿插代码和真实的“踩坑”案例。咱们目标很明确:让数据干净、准确,并且能跑得动。
第一步:别急着读数据,先看看它的“身份证”
很多新手(包括曾经的我)犯的最大错误,就是直接 read.csv() 然后开始画图。结果呢?坐标系统一不对,图就飘了;属性表字段类型错了,计算就崩了。
在Python生态里,我们主要打交道的是 geopandas,它是 pandas 的空间扩展版。但在导入之前,你得先搞清楚你的数据长什么样。
1.1 常见的空间数据格式
- GeoJSON: 轻量,适合Web前端,但在大数据量下性能较差。
- Shapefile (.shp): 老古董,但依然无处不在。注意,它其实由多个文件组成(.shp, .shx, .dbf等),必须放在同一个文件夹下。
- GeoPackage (.gpkg): 现代推荐格式,基于SQLite,单文件,支持事务,速度快。
- KML/KMZ: 谷歌地球专用,通常用于展示而非分析。
1.2 实战导入:从混乱到有序
假设你手里有一个 cities.geojson,里面记录了几个城市的坐标。
import geopandas as gpd
import pandas as pd
import numpy as np
from shapely.geometry import Point, Polygon
import warnings
warnings.filterwarnings('ignore') # 先关掉那些烦人的警告,我们后面再处理
# 1. 读取数据
gdf = gpd.read_file('cities.geojson')
# 2. 初步检查:这就像体检报告
print("数据类型:", type(gdf))
print("前5行数据:\n", gdf.head())
print("\n几何列类型:", gdf.geometry.type)
print("\n坐标系 (CRS):", gdf.crs)
这里有个巨大的陷阱:
如果打印出来的 CRS 是 None 或者 EPSG:4326(WGS84,经纬度),而你的后续分析需要距离(比如“两个城市相距多少公里”),那你必须小心。因为经纬度是角度单位,直接相减得到的“距离”是没有物理意义的。
专家建议:
在导入后的第一件事,永远是确认 gdf.crs。如果不知道原始数据的坐标系,去查一下数据源文档,或者通过元数据判断。如果是Web地图常见的经纬度,通常是 EPSG:4326。
第二步:几何对象清洗——修复“破碎”的地图
空间数据的核心是几何对象(Geometry)。有时候,由于数据采集错误、数字化失误或格式转换问题,几何对象会变得“畸形”。
2.1 无效几何(Invalid Geometries)
这是最常见的错误。一个多边形如果自相交(Self-intersecting),或者边界线打结,它就是一个无效几何。Geopandas 在处理无效几何时往往会抛出错误或产生奇怪的结果。
如何检测?
# 检查哪些几何对象是无效的
invalid_mask = ~gdf.geometry.is_valid
print(f"发现 {invalid_mask.sum()} 个无效几何对象")
if invalid_mask.any():
print("无效的几何ID:", gdf.index[invalid_mask].tolist())
如何修复?
Shapely 库提供了一个神奇的函数 .buffer(0)。听起来很玄学?其实就是利用缓冲区算法的容错机制来“缝合”那些微小的错误。
# 修复策略:尝试使用 buffer(0) 修复
def try_fix_geometry(geom):
if geom.is_valid:
return geom
else:
# 尝试缓冲0来修复
fixed = geom.buffer(0)
if fixed.is_valid:
return fixed
else:
# 如果还坏,那就放弃,返回None或空几何
return None
# 应用修复
gdf['geometry'] = gdf['geometry'].apply(try_fix_geometry)
# 再次检查
print("修复后剩余无效几何:", (~gdf.geometry.is_valid).sum())
给小朋友的比喻: 想象你在纸上画一个圆圈,但是笔尖划破了纸,线条交叉在了一起。
buffer(0)就像是用橡皮擦轻轻擦掉交叉点周围的微小错误,让线条重新平滑地连起来。
2.2 空几何(Empty Geometries)
有时候数据里会有空的点或面。这在分析中通常是无用的,甚至会导致错误。
# 移除空几何
gdf = gdf[gdf.geometry.not_empty]
print(f"移除了 {len(gdf)} 个空几何对象,现在剩下 {len(gdf)} 条记录")
2.3 重复几何
如果你的数据是从多个来源拼凑的,可能会有完全相同的地理位置出现两次。
# 基于几何形状去重
# 注意:对于多点或多边形,简单的 drop_duplicates 可能不够精确
# 这里我们简化处理,假设每个几何是唯一的标识
initial_count = len(gdf)
gdf = gdf.drop_duplicates(subset=['geometry'])
final_count = len(gdf)
print(f"移除了 {initial_count - final_count} 条重复的几何记录")
第三步:属性数据清洗——处理缺失与错误
几何修好了,现在来看看里面的“肉”,也就是属性表(Attributes)。这里面的脏数据往往比几何更隐蔽。
3.1 缺失值处理:是填还是删?
地理空间数据中,缺失值(NaN)很常见。比如,某个街区的“人口密度”没统计到。
策略选择:
- 删除:如果缺失比例很高(比如超过20%),且该字段对当前分析不重要,直接删掉整行或整列。
- 填充:
- 数值型:可以用均值、中位数,或者更高级的插值方法(如克里金插值,但这属于进阶话题,我们先说基础的)。
- 类别型:用众数填充,或者标记为“Unknown”。
# 假设有一个 'population' 列存在缺失
print("缺失值统计:\n", gdf['population'].isnull().sum())
# 策略A:简单填充中位数
median_pop = gdf['population'].median()
gdf['population'] = gdf['population'].fillna(median_pop)
# 策略B:如果缺失太多,考虑删除该行
# gdf = gdf.dropna(subset=['population'])
专家提示: 在处理空间数据时,千万不要随意填充几何字段的缺失值。如果一个地方没有坐标,你就不能凭空捏造一个位置。对于缺失几何的记录,通常只能删除,或者手动标记为“待核实”。
3.2 异常值检测:离群的坐标
有时候,数据录入员手滑,把一个位于北京的坐标输成了南极洲。这种“离群点”会毁掉你的可视化效果。
方法一:基于范围的限制
如果你知道数据应该在中国范围内,那么经纬度应该在特定区间内。
# 中国大致范围:经度 73-135,纬度 3-53
min_lon, max_lon = 73, 135
min_lat, max_lat = 3, 53
# 提取经纬度
lons = gdf.geometry.x
lats = gdf.geometry.y
# 创建掩膜
valid_mask = (lons >= min_lon) & (lons <= max_lon) & \
(lats >= min_lat) & (lats <= max_lat)
# 查看有多少异常点
outliers = gdf[~valid_mask]
print(f"发现 {len(outliers)} 个超出地理范围的异常点")
# 移除它们
gdf = gdf[valid_mask]
方法二:统计离群值(针对属性)
如果“人口密度”这一列,大部分是1000-5000,突然出现一个500000,那很可能是错的。
# 使用 IQR (四分位距) 方法检测数值异常
Q1 = gdf['population_density'].quantile(0.25)
Q3 = gdf['population_density'].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
# 标记异常
outlier_mask = (gdf['population_density'] < lower_bound) | \
(gdf['population_density'] > upper_bound)
print("可能的数值异常点:\n", gdf[outlier_mask])
# 可以选择修正为NaN,或者截断,或者删除
# 这里我们演示将其设为NaN,以便后续统一处理
gdf.loc[outlier_mask, 'population_density'] = np.nan
第四步:坐标系统一——让所有图层对齐
这是空间分析中最关键、也最容易出错的一步。
为什么重要?
想象一下,你把一张北京地图叠在一张纽约地图上,如果不统一坐标系,它们根本对不上。在计算机眼里,EPSG:4326 (WGS84) 和 EPSG:3857 (Web Mercator) 的数字看起来差不多,但含义完全不同。
最佳实践:
- 分析时:尽量使用投影坐标系(Projected Coordinate System, PCS),如 UTM 分区,这样距离、面积计算才是准确的(单位是米)。
- 展示时:可以使用 Web Mercator (
EPSG:3857),因为它在屏幕上显示变形较小,且符合大多数在线地图标准。
# 1. 检查当前坐标系
print("原始 CRS:", gdf.crs)
# 2. 如果需要转换为 WGS84 (经纬度)
gdf_wgs84 = gdf.to_crs(epsg=4326)
# 3. 如果需要转换为 Web Mercator (用于地图显示)
gdf_webmerc = gdf.to_crs(epsg=3857)
# 4. 如果需要计算面积或距离,建议转换为当地的 UTM 投影
# 例如,假设数据在北京附近,使用 UTM Zone 50N
gdf_utm = gdf.to_crs(epsg=32650)
# 验证转换
print("转换后的 CRS:", gdf_utm.crs)
给小朋友的比喻: 坐标系就像是一把尺子。有的尺子是软的(经纬度),量弯曲的地球表面不准;有的尺子是硬的(投影坐标),拉直了量才准。做数学题时,一定要换那把“硬尺子”。
第五步:拓扑关系检查——邻居对了吗?
清洗的最后一步,是检查空间关系。比如,两个相邻的行政区,它们的边界是否完全重合?有没有缝隙(Slivers)或重叠(Overlaps)?
虽然 geopandas 本身不直接提供复杂的拓扑清理工具,但我们可以利用 shapely 进行基本的检查。
5.1 检查重叠
# 简单示例:检查是否有几何对象相互重叠
# 注意:对于大量数据,这很慢。生产环境建议使用专门的空间索引或 PostGIS。
from shapely.ops import unary_union
# 合并所有几何体,看是否有内部重叠(这通常意味着原始数据有重叠区域)
# 如果原始数据是分区(如省份),它们不应该重叠。
unified_geom = unary_union(gdf.geometry)
# 如果 unified_geom 是一个 MultiPolygon 且内部有重叠,unary_union 可能会报错或产生奇怪结果
# 更简单的方法是检查两两之间的交集(仅适用于小数据集)
# 这里我们不做复杂演示,因为实际项目中通常会使用 GDAL/OGR 的拓扑修复工具
print("拓扑检查完成(简化版)")
5.2 导出干净的数据
清洗完毕后,记得保存。推荐使用 GeoPackage,因为它高效且单文件。
# 保存为 GeoPackage
gdf.to_file('cleaned_cities.gpkg', driver='GPKG')
# 或者保存为 GeoJSON
gdf.to_file('cleaned_cities.geojson', driver='GeoJSON')
print("数据清洗完成!文件已保存。")
总结与避坑指南
我们来回顾一下这次“大扫除”的关键点:
- 先验后动:导入后立即检查
crs和geometry.type。 - 修复几何:
buffer(0)是神器,但要先检查is_valid。 - 谨慎填充:数值可以插值,几何不能瞎编。
- 范围过滤:用经纬度范围过滤明显的离群点。
- 坐标统一:分析用投影坐标,展示用Web墨卡托。
最后,我想说:
数据清洗从来不是线性的过程。你可能会发现,修好了几何,却发现属性表里的名字对不上了;或者统一了坐标系,才发现有些点的精度丢失了。这就是空间数据的魅力——它充满了细节和不确定性。
保持耐心,多用 print 和 head 去观察每一步的变化。当你看到最后一张地图完美渲染,所有的点都落在正确的位置,所有的颜色都代表真实的数值时,那种成就感,是任何游戏都给不了的。
去吧,让你的数据变得整洁如新。如果有具体的报错信息,随时回来问我,我们一起拆解。