第三章 空间数据获取与预处理:坐标系统、数据清洗、投影变换

做了这么多年空间数据分析,我最大的体会是:数据准备阶段决定了整个项目的成败。模型再牛,算法再先进,原始数据乱七八糟,结果能好到哪去?今天咱们就聊聊空间数据预处理那几个绕不开的坎——坐标系统、数据清洗、投影变换。说白了,就是把你手里的原始数据,变成能直接喂给模型的好数据。

核心观点: 预处理花掉项目 60% 的时间很正常,但这 60% 决定了剩下 40% 的产出质量。

3.1 坐标系统——空间数据的"地址簿"

坐标系统是什么?就是给地球上的点一个精确的门牌号。问题是,这套门牌号有好多种写法。WGS84、CGCS2000、西安80、北京54……新手经常搞混。

我个人习惯,拿到数据的第一件事——先看坐标系元数据。别急着分析,先搞清楚你的点到底在哪儿。

地理坐标系用经纬度,单位是度;投影坐标系用米,单位是米。两者之间差了投影变换。你想想看,把椭球面的经纬度直接当平面坐标用,距离量出来全是错的。

类型 常见代表 单位 适用场景
地理坐标系 WGS84、CGCS2000 度(°) 全球定位、底图显示
投影坐标系 UTM 50N、Gauss-Kruger 米(m) 面积量算、缓冲区分析

我在项目中遇到过一档子事:合作伙伴给的数据,说是WGS84,我一算距离,偏差了四百多米。后来发现他们把CGCS2000当WGS84用了。两个坐标系在大部分地区差异不到一米,但在高精度场景下,那就是灾难。

我的习惯: 拿到数据先输出一行 print(gdf.crs) 看一眼,确认坐标系全不全、对不对。

下面这张图,就是本章的核心知识体系。你可以把它当作地图,指导后面的学习路径。

空间数据预处理知识体系 空间数据获取与预处理 坐标系统 数据清洗 投影变换 地理坐标系 投影坐标系 基准面/椭球体 缺失坐标值处理 重复点检测与移除 离群点过滤 投影方法选择 坐标系参数设置 重投影计算 数据质量决定分析上限,预处理永远值得花时间

3.2 数据清洗——别让脏数据毁了你的分析

空间数据尤其容易脏。GPS信号漂移、传感器故障、人工录入错误……什么情况都有。我处理过的数据集中,几乎每一批都有问题。

常见的脏数据有三种:

  • 缺失坐标——经纬度字段为空,或者填了 0,0(这个在(0,0)点在非洲西海岸,明显不对)
  • 重复点——同一位置被反复记录,特别是轨迹数据里特别常见
  • 离群点——坐标值突然跳到几百公里外,多半是GPS受到干扰

来看看我是怎么处理的。下面这段代码,基本是我每个项目都会用到的清洗模板。

import pandas as pd
import numpy as np

# 加载原始数据
df = pd.read_csv("raw_poi_data.csv")

# 1. 移除缺失坐标
df = df.dropna(subset=["lat", "lon"])
df = df[(df["lat"] != 0) & (df["lon"] != 0)]

# 2. 重复点检测(保留第一条)
df = df.drop_duplicates(subset=["lat", "lon"])

# 3. 离群点过滤(基于3-sigma原则)
lat_mean, lat_std = df["lat"].mean(), df["lat"].std()
lon_mean, lon_std = df["lon"].mean(), df["lon"].std()

mask_lat = np.abs(df["lat"] - lat_mean) < 3 * lat_std
mask_lon = np.abs(df["lon"] - lon_mean) < 3 * lon_std
df_clean = df[mask_lat & mask_lon]

print(f"原始记录数:{len(df)}")
print(f"清洗后记录数:{len(df_clean)}")
注意: 3-sigma 过滤在高密度城市街区可能误删正常的密集点。建议先做空间分布可视化,观察离群点是否真的异常。

我曾经在一条共享单车轨迹数据上吃过亏——没做离群点过滤,结果模型训练出来,预测的骑行距离平均偏大 40%。后来发现是几个点漂移到了隔壁城市,把整体均值拉偏了。那之后,我每次必做离群点检测。

你可能会问:"过滤掉了会不会丢失信息?" 会。所以我的做法是:把过滤掉的点单独存一个文件,留作核查。别直接扔掉,万一那是新出现的模式呢?

3.3 投影变换——把地球"压平"的艺术

地球是个椭球体。你不可能把橘子皮铺平而不撕裂。投影变换就是解决这个问题的——用一种数学方法,把椭球面转换到平面上。

不同的投影方法,适合不同的区域和分析目的:

投影方法 特点 最佳用途
UTM 投影 分带投影,每带变形小 中等尺度区域分析
Albers 等面积 面积保持准确 面积统计、密度计算
Mercator 角度保真,面积变形大 航海、底图可视化
Lambert 等角 角度保真,适合中纬度 气象、航空

我个人建议:

  • 做距离、缓冲区分析 → 用 UTM 或 Gauss-Kruger(单位是米)
  • 做面积统计 → 用 Albers 等面积投影
  • 只是画个底图看看 → 用 Web Mercator(EPSG:3857)

下面是一个用 geopandas 做投影变换的例子。注意看,代码里指定了源坐标系和目标坐标系。

import geopandas as gpd

# 读取数据(假设是 WGS84 经纬度)
gdf = gpd.read_file("samples.geojson")
print("原始 CRS:", gdf.crs)

# 重投影到 UTM 50N(适用于东经114°-120°区域)
gdf_utm = gdf.to_crs("EPSG:32650")
print("投影后 CRS:", gdf_utm.crs)

# 现在可以算距离了
gdf_utm["distance_km"] = gdf_utm.geometry.length / 1000
print(gdf_utm[["id", "distance_km"]].head())
一个小技巧: 如果你不确定选哪个 UTM 带,可以用公式 utm_zone = int((lon + 180) / 6) % 60 + 1 快速算出来。北半球用 326+带号,南半球用 327+带号。

3.4 综合实战——从原始 CSV 到干净的空间数据集

把前面三个知识点串起来。假设你拿到一份商家 POI 的 CSV 文件,里面只有经纬度,目标是做成一个投影后的空间数据集,用于后续的聚类分析。

import pandas as pd
import geopandas as gpd
from shapely.geometry import Point

# 1. 读取原始数据
df = pd.read_csv("shops.csv")

# 2. 数据清洗
df = df.dropna(subset=["lat", "lon"])
df = df[(df["lat"] != 0) & (df["lon"] != 0)]
df = df.drop_duplicates(subset=["lat", "lon"])

# 3. 转为地理空间数据(WGS84)
geometry = [Point(xy) for xy in zip(df["lon"], df["lat"])]
gdf = gpd.GeoDataFrame(df, geometry=geometry, crs="EPSG:4326")

# 4. 重投影到 UTM 50N
gdf_utm = gdf.to_crs("EPSG:32650")

# 5. 验证投影结果
print(gdf_utm.crs)
print(gdf_utm.head())

# 6. 保存为 GeoPackage
gdf_utm.to_file("shops_clean_utm.gpkg", layer="pois", driver="GPKG")
常见坑: 很多人忘记检查经纬度的顺序——有的数据是 (lat, lon),有的是 (lon, lat)。shapely.geometry.Point 接收的是 (x, y),也就是 (lon, lat)。搞反了,点会跑到海里去。

嗯,这里要注意:上面的代码用的是 Point(lon, lat),先经度后纬度。这是地理空间领域的标准顺序。我见过不少项目因为这个顺序问题,分析做了半天,结果发现点全在非洲。

回顾一下这一章的核心:坐标系统是数据的"语言",数据清洗是"去杂质",投影变换是"换工具"。三者缺一不可。你想想看,如果坐标系搞错了,后面的分析就像用中文翻译器去读法语——结果全是乱码。

一句话总结: 拿到任何空间数据,先问三句话——坐标系是什么?数据干净吗?投影对吗?问清楚再往下走。