znlgis 博客

GIS开发与技术分享

第3章:核心架构与数据模型

深入理解 GeoPandas 的内部架构是高效使用它的前提。本章将系统剖析 GeoPandas 的分层架构、核心类的设计以及数据流转机制,为后续章节的学习打下坚实的理论基础。


3.1 整体架构概览

3.1.1 分层架构

GeoPandas 采用清晰的分层架构设计,每一层各司其职:

┌─────────────────────────────────────────────────────┐
│                   用户接口层                          │
│          (GeoDataFrame / GeoSeries API)              │
├─────────────────────────────────────────────────────┤
│                 GeoPandas 核心层                      │
│   ┌──────────────┬──────────────┬──────────────┐     │
│   │ GeoDataFrame │  GeoSeries   │ GeometryArray│     │
│   │              │              │              │     │
│   │ - 表格数据   │ - 几何序列   │ - 底层存储   │     │
│   │ - 多列管理   │ - 向量操作   │ - 内存管理   │     │
│   └──────┬───────┴──────┬───────┴──────┬───────┘     │
├──────────┼──────────────┼──────────────┼─────────────┤
│          │     依赖库层  │              │             │
│   ┌──────┴──────┐ ┌─────┴─────┐ ┌──────┴──────┐     │
│   │   pandas    │ │  shapely  │ │   pyproj    │     │
│   │   numpy     │ │  (GEOS)   │ │   (PROJ)    │     │
│   └─────────────┘ └───────────┘ └─────────────┘     │
│                        │                             │
│                  ┌─────┴──────┐                      │
│                  │  pyogrio   │                      │
│                  │  (GDAL)    │                      │
│                  └────────────┘                      │
├─────────────────────────────────────────────────────┤
│                     C/C++ 底层库                      │
│          GEOS  ·  PROJ  ·  GDAL/OGR                 │
└─────────────────────────────────────────────────────┘

3.1.2 各层职责

层次 组件 职责
用户接口层 GeoDataFrame, GeoSeries 提供面向用户的高级 API
核心层 GeometryArray, SpatialIndex 数据存储、索引、类型管理
依赖层 pandas, shapely, pyproj, pyogrio 表格处理、几何运算、投影、I/O
底层 C/C++ 库 GEOS, PROJ, GDAL 高性能几何运算、投影变换、数据格式支持

3.1.3 关键设计理念

GeoPandas 的架构体现了以下设计理念:

  1. 继承与扩展:通过继承 pandas 的核心类,最大化代码复用
  2. 组合优于继承:使用 Shapely 对象作为几何列的元素,而非重新实现
  3. 向量化优先:利用 Shapely 2.0 的向量化能力,避免逐行处理
  4. 延迟计算:空间索引等资源按需创建
  5. pandas 兼容:所有 pandas 操作应该在 GeoDataFrame 上无缝工作

3.2 GeoDataFrame 类

3.2.1 定义与继承关系

GeoDataFrame 是 GeoPandas 最核心的类,直接继承自 pandas.DataFrame

class GeoDataFrame(pd.DataFrame):
    """
    一个包含几何列的 pandas DataFrame,
    支持空间操作和地理空间数据处理。
    """
    pass

这意味着 所有 pandas DataFrame 的方法都可以在 GeoDataFrame 上使用

3.2.2 GeoDataFrame 的结构

一个 GeoDataFrame 包含以下核心组件:

GeoDataFrame
├── 索引 (Index)           ← 继承自 pandas
├── 属性列 (Columns)       ← 普通 pandas 列
├── 几何列 (geometry)      ← GeoSeries(核心扩展)
├── 坐标参考系 (CRS)       ← pyproj.CRS 对象
└── 空间索引 (sindex)      ← STRtree 空间索引
import geopandas as gpd
from shapely.geometry import Point

# 创建一个 GeoDataFrame
gdf = gpd.GeoDataFrame({
    '名称': ['北京', '上海', '广州'],
    '人口': [2189, 2487, 1868],
    'GDP': [41610, 44652, 28231],
    'geometry': [
        Point(116.40, 39.90),
        Point(121.47, 31.23),
        Point(113.26, 23.13)
    ]
}, crs="EPSG:4326")

# 查看结构
print("类型:", type(gdf))
print("列:", gdf.columns.tolist())
print("几何列名:", gdf.geometry.name)
print("CRS:", gdf.crs)
print("空间索引:", type(gdf.sindex))

输出:

类型: <class 'geopandas.geodataframe.GeoDataFrame'>
列: ['名称', '人口', 'GDP', 'geometry']
几何列名: geometry
CRS: EPSG:4326
空间索引: <class 'geopandas.sindex.SpatialIndex'>

3.2.3 活跃几何列概念

GeoDataFrame 可以包含多个几何列,但同一时刻只有一个是活跃几何列(active geometry column)。所有空间操作都作用于活跃几何列。

import geopandas as gpd
from shapely.geometry import Point

gdf = gpd.GeoDataFrame({
    '名称': ['A', 'B', 'C'],
    'geometry': [Point(0, 0), Point(1, 1), Point(2, 2)],
    'centroid_geom': [Point(0.5, 0.5), Point(1.5, 1.5), Point(2.5, 2.5)]
})

# 当前活跃几何列
print("活跃几何列:", gdf.geometry.name)  # 'geometry'

# 切换活跃几何列
gdf = gdf.set_geometry('centroid_geom')
print("切换后活跃几何列:", gdf.geometry.name)  # 'centroid_geom'

# 空间操作现在作用于 centroid_geom
print(gdf.total_bounds)

3.2.4 GeoDataFrame 的核心属性

属性 类型 说明
geometry GeoSeries 活跃几何列
crs pyproj.CRS | None 坐标参考系
sindex SpatialIndex 空间索引(延迟创建)
bounds DataFrame 每个几何对象的边界框
total_bounds ndarray 所有几何对象的总边界框
area Series 面积(依赖 CRS 单位)
length Series 长度(依赖 CRS 单位)

3.2.5 GeoDataFrame 的核心方法

方法 说明 示例
to_crs() 投影转换 gdf.to_crs(epsg=3857)
set_geometry() 设置活跃几何列 gdf.set_geometry('geom2')
rename_geometry() 重命名几何列 gdf.rename_geometry('geom')
to_file() 导出到文件 gdf.to_file('out.shp')
to_json() 导出为 GeoJSON 字符串 gdf.to_json()
to_parquet() 导出为 Parquet gdf.to_parquet('out.parquet')
to_feather() 导出为 Feather gdf.to_feather('out.feather')
to_postgis() 写入 PostGIS gdf.to_postgis('table', engine)
plot() 绘制静态地图 gdf.plot(column='pop')
explore() 交互式地图 gdf.explore()
dissolve() 按属性融合 gdf.dissolve(by='province')
sjoin() 空间连接 gpd.sjoin(gdf1, gdf2)
clip() 裁剪 gpd.clip(gdf, mask)
overlay() 空间叠加 gpd.overlay(gdf1, gdf2)

3.3 GeoSeries 类

3.3.1 定义与特点

GeoSeries 继承自 pandas.Series,是存储 Shapely 几何对象的一维数组。

class GeoSeries(pd.Series):
    """
    存储几何对象的 pandas Series,
    支持空间操作和属性访问。
    """
    pass

核心特点:

  • 每个元素是一个 Shapely 几何对象(或 None)
  • 可以直接进行向量化空间操作
  • 拥有独立的 CRS(坐标参考系)
  • 支持空间索引

3.3.2 创建 GeoSeries

import geopandas as gpd
from shapely.geometry import Point, Polygon, LineString
import numpy as np

# 方法1:从 Shapely 对象列表创建
gs = gpd.GeoSeries([
    Point(0, 0),
    Point(1, 1),
    Point(2, 2)
], crs="EPSG:4326")
print("方法1:", gs)

# 方法2:从 WKT 创建
gs_wkt = gpd.GeoSeries.from_wkt([
    'POINT (116.40 39.90)',
    'POINT (121.47 31.23)',
    'POINT (113.26 23.13)'
], crs="EPSG:4326")
print("\n方法2:", gs_wkt)

# 方法3:从 WKB 创建
wkb_data = gs.to_wkb()
gs_wkb = gpd.GeoSeries.from_wkb(wkb_data, crs="EPSG:4326")
print("\n方法3:", gs_wkb)

# 方法4:从坐标数组创建点
gs_xy = gpd.GeoSeries.from_xy(
    x=[116.40, 121.47, 113.26],
    y=[39.90, 31.23, 23.13],
    crs="EPSG:4326"
)
print("\n方法4:", gs_xy)

# 方法5:包含不同类型的几何对象
gs_mixed = gpd.GeoSeries([
    Point(0, 0),
    LineString([(0, 0), (1, 1)]),
    Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])
])
print("\n方法5 (混合类型):", gs_mixed)

3.3.3 GeoSeries 与 pandas Series 的关系

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

gs = gpd.GeoSeries([Point(0, 0), Point(1, 1)], crs="EPSG:4326")

# GeoSeries 是 pandas Series 的子类
print(isinstance(gs, pd.Series))     # True
print(isinstance(gs, gpd.GeoSeries)) # True

# pandas Series 的方法都可以使用
print(gs.index)        # RangeIndex
print(len(gs))         # 2
print(gs.is_empty)     # GeoSeries 特有方法
print(gs.dtype)        # geometry

3.3.4 GeoSeries 的空间属性

GeoSeries 提供了丰富的空间属性访问:

import geopandas as gpd
from shapely.geometry import Polygon

# 创建多边形 GeoSeries
gs = gpd.GeoSeries([
    Polygon([(0,0), (2,0), (2,2), (0,2)]),
    Polygon([(1,1), (4,1), (4,3), (1,3)])
])

# 基本属性
print("几何类型:", gs.geom_type.tolist())    # ['Polygon', 'Polygon']
print("是否为空:", gs.is_empty.tolist())      # [False, False]
print("是否有效:", gs.is_valid.tolist())      # [True, True]
print("是否简单:", gs.is_simple.tolist())     # [True, True]

# 度量属性
print("面积:", gs.area.tolist())              # [4.0, 6.0]
print("长度:", gs.length.tolist())            # [8.0, 10.0]
print("边界框:", gs.bounds)
print("总边界框:", gs.total_bounds)            # [0. 0. 4. 3.]

# 派生几何
print("质心:", gs.centroid.tolist())
print("边界:", gs.boundary.tolist())
print("包络框:", gs.envelope.tolist())
print("凸包:", gs.convex_hull.tolist())

3.4 GeometryArray 类

3.4.1 pandas ExtensionArray 机制

GeoPandas 利用 pandas 的 ExtensionArray 机制来存储几何数据。这是 pandas 提供的扩展点,允许第三方库定义自己的数组类型。

# GeometryArray 的位置
from geopandas.array import GeometryArray

# 它实现了 pandas ExtensionArray 接口
import pandas as pd
print(issubclass(GeometryArray, pd.api.extensions.ExtensionArray))  # True

3.4.2 底层存储结构

GeometryArray 内部使用 NumPy 对象数组 存储 Shapely 几何对象:

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

gs = gpd.GeoSeries([Point(0, 0), Point(1, 1), Point(2, 2)])

# 获取底层 GeometryArray
ga = gs.values
print("类型:", type(ga))                    # GeometryArray
print("底层数据类型:", type(ga._data))       # numpy.ndarray
print("元素类型:", type(ga._data[0]))        # shapely.geometry.point.Point

# GeometryArray 的 dtype
print("dtype:", ga.dtype)                    # geometry
print("dtype name:", ga.dtype.name)          # geometry

3.4.3 GeometryArray 的关键方法

方法 说明
_from_sequence() 从序列创建 GeometryArray
_from_factorized() 从因子化数据创建
_concat_same_type() 合并多个 GeometryArray
copy() 复制
isna() 检查缺失值
take() 按索引取值
_values_for_factorize() 为因子化提供值

3.4.4 “geometry” dtype

GeoPandas 注册了一个自定义的 pandas dtype:"geometry"

import geopandas as gpd
from geopandas.array import GeometryDtype

# dtype 信息
dtype = GeometryDtype()
print("名称:", dtype.name)           # geometry
print("类型:", dtype.type)           # <class 'shapely.geometry.base.BaseGeometry'>
print("numpy 类型:", dtype.numpy_dtype)  # object

# 在 DataFrame 中识别
import pandas as pd
from shapely.geometry import Point

gdf = gpd.GeoDataFrame({'geometry': [Point(0, 0)]})
print(gdf.dtypes)
# geometry    geometry
# dtype: object

3.5 GeoPandasBase 基类

3.5.1 公共基类设计

GeoSeriesGeoDataFrame 共享一个公共基类 GeoPandasBase(通过 mixin 模式实现),它定义了两者共有的空间操作方法。

              GeoPandasBase (Mixin)
              ┌─────────────────┐
              │ • area          │
              │ • length        │
              │ • bounds        │
              │ • centroid      │
              │ • buffer()      │
              │ • intersects()  │
              │ • contains()    │
              │ • distance()    │
              │ • ... 100+ 方法 │
              └────────┬────────┘
                       │
           ┌───────────┴───────────┐
           │                       │
    ┌──────┴──────┐         ┌──────┴──────┐
    │  GeoSeries  │         │GeoDataFrame │
    │             │         │             │
    │ (pd.Series) │         │(pd.DataFrame)│
    └─────────────┘         └─────────────┘

3.5.2 共享的空间操作方法

以下方法在 GeoSeries 和 GeoDataFrame 上都可以使用:

属性方法(返回 Series 或标量):

属性/方法 返回类型 说明
area Series 面积
length Series 长度/周长
bounds DataFrame 边界框 (minx, miny, maxx, maxy)
total_bounds ndarray 总边界框
geom_type Series 几何类型名称
is_empty Series 是否为空几何
is_valid Series 是否为有效几何
is_simple Series 是否为简单几何
has_z Series 是否包含 Z 坐标

一元空间操作(返回 GeoSeries):

方法 说明 示例
boundary 边界 gdf.boundary
centroid 质心 gdf.centroid
convex_hull 凸包 gdf.convex_hull
envelope 最小外接矩形 gdf.envelope
exterior 外环(Polygon) gdf.exterior
representative_point() 代表点 gdf.representative_point()
normalize() 标准化几何 gdf.normalize()
make_valid() 修复无效几何 gdf.make_valid()

带参数的空间操作

方法 说明 示例
buffer(distance) 缓冲区 gdf.buffer(100)
simplify(tolerance) 简化 gdf.simplify(0.01)
affine_transform(matrix) 仿射变换 gdf.affine_transform([...])
translate(xoff, yoff) 平移 gdf.translate(1, 2)
rotate(angle) 旋转 gdf.rotate(45)
scale(xfact, yfact) 缩放 gdf.scale(2, 2)

二元空间操作(需要另一个几何参数):

方法 返回类型 说明
intersects(other) Series (bool) 是否相交
contains(other) Series (bool) 是否包含
within(other) Series (bool) 是否在内部
crosses(other) Series (bool) 是否交叉
overlaps(other) Series (bool) 是否重叠
touches(other) Series (bool) 是否相接
disjoint(other) Series (bool) 是否不相交
covers(other) Series (bool) 是否覆盖
covered_by(other) Series (bool) 是否被覆盖
distance(other) Series (float) 距离
intersection(other) GeoSeries 交集
union(other) GeoSeries 并集
difference(other) GeoSeries 差集
symmetric_difference(other) GeoSeries 对称差

3.5.3 方法在 GeoSeries 和 GeoDataFrame 上的行为

import geopandas as gpd
from shapely.geometry import Point, Polygon

# 创建 GeoDataFrame
gdf = gpd.GeoDataFrame({
    '名称': ['A', 'B'],
    'geometry': [
        Polygon([(0,0), (2,0), (2,2), (0,2)]),
        Polygon([(1,1), (3,1), (3,3), (1,3)])
    ]
})

# 在 GeoDataFrame 上调用 - 操作活跃几何列
print("GeoDataFrame.area:")
print(gdf.area)

# 在 GeoSeries 上调用 - 直接操作
gs = gdf.geometry
print("\nGeoSeries.area:")
print(gs.area)

# 两者结果相同
print("\n结果相同:", (gdf.area == gs.area).all())  # True

# 缓冲区 - 返回 GeoSeries
buffered_gdf = gdf.buffer(0.5)
buffered_gs = gs.buffer(0.5)
print("\nbuffer 返回类型 (GeoDataFrame):", type(buffered_gdf))  # GeoSeries
print("buffer 返回类型 (GeoSeries):", type(buffered_gs))       # GeoSeries

3.6 GeometryDtype 类型系统

3.6.1 pandas 扩展类型

GeoPandas 通过 pandas 的扩展类型系统注册了 "geometry" dtype:

from geopandas.array import GeometryDtype
import pandas as pd

# GeometryDtype 继承自 pandas ExtensionDtype
print(issubclass(GeometryDtype, pd.api.extensions.ExtensionDtype))  # True

# 注册为 "geometry"
dtype = GeometryDtype()
print(dtype.name)  # "geometry"

# 可以通过字符串识别
print(pd.api.types.pandas_dtype("geometry"))  # geometry

3.6.2 dtype 的作用

GeometryDtype 在 GeoPandas 中扮演着类型标识的关键角色:

import geopandas as gpd
from shapely.geometry import Point

gdf = gpd.GeoDataFrame({
    '名称': ['A', 'B'],
    'x': [1.0, 2.0],
    'geometry': [Point(0, 0), Point(1, 1)]
})

# dtype 识别
print(gdf.dtypes)
# 名称          object
# x           float64
# geometry    geometry    ← GeometryDtype
# dtype: object

# 通过 dtype 识别几何列
for col in gdf.columns:
    if gdf[col].dtype.name == 'geometry':
        print(f"'{col}' 是几何列")

3.6.3 类型转换与兼容性

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

# GeoSeries 的 dtype 始终是 geometry
gs = gpd.GeoSeries([Point(0, 0), Point(1, 1)])
print(gs.dtype)  # geometry

# 转换为普通 Series 会丢失 geometry dtype
s = pd.Series(gs.values)
print(s.dtype)  # object

# 但仍然包含 Shapely 对象
print(type(s.iloc[0]))  # <class 'shapely.geometry.point.Point'>

3.7 SpatialIndex 空间索引

3.7.1 空间索引的重要性

空间索引是高效空间查询的关键。没有空间索引,每次空间查询都需要与所有几何对象进行比较(O(n)复杂度)。有了空间索引,可以快速缩小候选范围,大幅提升查询性能。

无空间索引:查询 1 个点是否在 10,000 个多边形中
  → 需要 10,000 次几何比较 ❌

有空间索引:查询 1 个点是否在 10,000 个多边形中
  → 先用空间索引过滤到 ~10 个候选
  → 再做 10 次精确几何比较 ✅

3.7.2 STRtree 空间索引

GeoPandas 使用 Shapely 的 STRtree(Sort-Tile-Recursive tree)作为空间索引引擎:

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

# 创建大量随机点
np.random.seed(42)
n = 10000
points = gpd.GeoSeries(
    gpd.points_from_xy(np.random.rand(n) * 100, np.random.rand(n) * 100)
)
gdf = gpd.GeoDataFrame({'id': range(n), 'geometry': points})

# 空间索引自动创建(延迟创建)
sindex = gdf.sindex
print("索引类型:", type(sindex))
print("索引大小:", sindex.size)

# 查询范围内的点
query_box = box(10, 10, 20, 20)  # 查询范围
candidates_idx = sindex.query(query_box)
print(f"\n范围查询结果: 在 {n} 个点中找到 {len(candidates_idx)} 个候选")

# 精确过滤
result = gdf.iloc[candidates_idx]
result = result[result.within(query_box)]
print(f"精确过滤后: {len(result)} 个点在范围内")

3.7.3 空间索引的查询方法

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

np.random.seed(42)
points = gpd.GeoSeries(gpd.points_from_xy(np.random.rand(100), np.random.rand(100)))

sindex = points.sindex

# 1. 单个几何对象查询
query_geom = box(0.2, 0.2, 0.4, 0.4)
idx = sindex.query(query_geom)
print(f"方法1 - query(): 找到 {len(idx)} 个候选")

# 2. 批量查询(向量化,高性能)
query_geoms = gpd.GeoSeries([
    box(0, 0, 0.3, 0.3),
    box(0.5, 0.5, 0.8, 0.8)
])
tree_idx, input_idx = sindex.query(query_geoms, predicate='intersects')
print(f"方法2 - 批量 query(): 找到 {len(tree_idx)} 个结果对")

# 3. 最近邻查询
nearest_idx = sindex.nearest(Point(0.5, 0.5))
print(f"方法3 - nearest(): 最近点索引 = {nearest_idx}")

3.7.4 空间索引的性能优势

import geopandas as gpd
from shapely.geometry import box
import numpy as np
import time

# 创建测试数据
np.random.seed(42)
n = 100000
gdf = gpd.GeoDataFrame({
    'geometry': gpd.points_from_xy(np.random.rand(n), np.random.rand(n))
})
query = box(0.3, 0.3, 0.5, 0.5)

# 不使用空间索引(暴力搜索)
start = time.time()
result1 = gdf[gdf.within(query)]
time1 = time.time() - start

# 使用空间索引(两步过滤)
start = time.time()
candidates = gdf.iloc[gdf.sindex.query(query)]
result2 = candidates[candidates.within(query)]
time2 = time.time() - start

print(f"数据量: {n} 条")
print(f"暴力搜索: {time1:.4f} 秒, 结果: {len(result1)} 条")
print(f"空间索引: {time2:.4f} 秒, 结果: {len(result2)} 条")
print(f"加速比: {time1/time2:.1f}x")

3.7.5 空间索引的注意事项

  1. 延迟创建:空间索引在第一次访问 sindex 时创建
  2. 不可变:修改数据后需要重新创建索引
  3. 内存消耗:大数据集的空间索引会占用较多内存
  4. 自动使用sjoinclip 等高级操作会自动利用空间索引

3.8 类继承关系图

3.8.1 完整的类继承关系

pandas.core.base.PandasObject
│
├── pandas.Series
│   └── geopandas.GeoSeries
│       ├── 使用 GeometryArray (ExtensionArray)
│       ├── 使用 GeometryDtype (ExtensionDtype)
│       └── 混入 GeoPandasBase 方法
│
├── pandas.DataFrame
│   └── geopandas.GeoDataFrame
│       ├── 包含一个或多个 GeoSeries 列
│       ├── 维护活跃几何列引用
│       ├── 管理 CRS
│       └── 混入 GeoPandasBase 方法
│
pandas.api.extensions.ExtensionArray
│   └── geopandas.array.GeometryArray
│       ├── 底层: numpy object array of Shapely geometries
│       └── dtype: GeometryDtype
│
pandas.api.extensions.ExtensionDtype
│   └── geopandas.array.GeometryDtype
│       ├── name = "geometry"
│       └── type = shapely.geometry.base.BaseGeometry
│
shapely.geometry.base.BaseGeometry
│   ├── Point
│   ├── LineString
│   │   └── LinearRing
│   ├── Polygon
│   ├── MultiPoint
│   ├── MultiLineString
│   ├── MultiPolygon
│   └── GeometryCollection

3.8.2 关键关系说明

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

# 继承关系验证
gdf = gpd.GeoDataFrame({'geometry': [Point(0, 0)]})
gs = gdf.geometry

# GeoDataFrame 的继承链
print("GeoDataFrame MRO:")
for cls in type(gdf).__mro__[:5]:
    print(f"  → {cls.__name__}")
# GeoDataFrame → DataFrame → NDFrame → PandasObject → object

# GeoSeries 的继承链
print("\nGeoSeries MRO:")
for cls in type(gs).__mro__[:5]:
    print(f"  → {cls.__name__}")
# GeoSeries → Series → NDFrame → PandasObject → object

# 类型检查
print("\n类型检查:")
print(f"isinstance(gdf, pd.DataFrame): {isinstance(gdf, pd.DataFrame)}")  # True
print(f"isinstance(gs, pd.Series): {isinstance(gs, pd.Series)}")          # True
print(f"isinstance(gdf, gpd.GeoDataFrame): {isinstance(gdf, gpd.GeoDataFrame)}")  # True

3.9 数据流与处理管道

3.9.1 典型数据处理管道

一个完整的 GeoPandas 数据处理管道通常包含以下阶段:

┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐    ┌──────────┐
│ 数据读取  │ →  │ 数据清洗  │ →  │ 空间分析  │ →  │  可视化   │ →  │ 数据输出  │
│          │    │          │    │          │    │          │    │          │
│ read_file│    │ 投影转换  │    │ 缓冲区   │    │ plot()   │    │ to_file  │
│ read_pqt │    │ 清理无效  │    │ 叠加分析  │    │ explore()│    │ to_pqt   │
│ from_xy  │    │ 过滤筛选  │    │ 空间连接  │    │          │    │ to_pg    │
└──────────┘    └──────────┘    └──────────┘    └──────────┘    └──────────┘

3.9.2 数据读取阶段

import geopandas as gpd

# 从文件读取
gdf = gpd.read_file("input.shp")

# 从数据库读取
gdf = gpd.read_postgis("SELECT * FROM table", engine)

# 从 Parquet 读取
gdf = gpd.read_parquet("data.parquet")

# 从 pandas DataFrame 转换
import pandas as pd
from shapely.geometry import Point

df = pd.read_csv("data.csv")
gdf = gpd.GeoDataFrame(
    df,
    geometry=gpd.points_from_xy(df.longitude, df.latitude),
    crs="EPSG:4326"
)

3.9.3 数据清洗阶段

# 1. 检查和设置 CRS
if gdf.crs is None:
    gdf = gdf.set_crs("EPSG:4326")

# 2. 投影转换(根据需要)
gdf = gdf.to_crs(epsg=32650)  # 转为 UTM 50N

# 3. 检查和修复无效几何
invalid = gdf[~gdf.is_valid]
print(f"无效几何数量: {len(invalid)}")
gdf['geometry'] = gdf.make_valid()

# 4. 删除空几何
gdf = gdf[~gdf.is_empty]

# 5. 删除重复几何
gdf = gdf.drop_duplicates(subset='geometry')

# 6. 处理缺失值
gdf = gdf.dropna(subset=['geometry'])

3.9.4 空间分析阶段

# 缓冲区分析
buffered = gdf.buffer(1000)

# 空间连接
joined = gpd.sjoin(points_gdf, polygon_gdf, predicate='within')

# 空间叠加
intersection = gpd.overlay(gdf1, gdf2, how='intersection')

# 融合
dissolved = gdf.dissolve(by='category', aggfunc='sum')

# 裁剪
clipped = gpd.clip(gdf, boundary)

3.9.5 可视化与输出阶段

# 静态地图
import matplotlib.pyplot as plt
fig, ax = plt.subplots(figsize=(12, 8))
gdf.plot(ax=ax, column='value', legend=True, cmap='YlOrRd')
plt.savefig("output_map.png", dpi=300)

# 交互式地图
m = gdf.explore(column='value', cmap='YlOrRd')
m.save("interactive_map.html")

# 导出数据
gdf.to_file("result.gpkg", driver="GPKG")
gdf.to_parquet("result.parquet")
gdf.to_postgis("result_table", engine)

3.9.6 完整管道示例

import geopandas as gpd
import matplotlib.pyplot as plt

def analyze_poi_coverage(poi_file, boundary_file, buffer_distance=500):
    """分析 POI(兴趣点)的服务覆盖范围"""

    # 1. 读取数据
    pois = gpd.read_file(poi_file)
    boundaries = gpd.read_file(boundary_file)

    # 2. 数据清洗
    pois = pois.set_crs("EPSG:4326", allow_override=True)
    boundaries = boundaries.set_crs("EPSG:4326", allow_override=True)

    # 转为投影坐标系(以便使用米为单位)
    pois = pois.to_crs(epsg=32650)
    boundaries = boundaries.to_crs(epsg=32650)

    # 修复无效几何
    boundaries['geometry'] = boundaries.make_valid()

    # 3. 空间分析
    # 创建服务范围(缓冲区)
    pois['service_area'] = pois.buffer(buffer_distance)

    # 计算每个区域内的 POI 数量
    poi_counts = gpd.sjoin(pois, boundaries, predicate='within')
    poi_summary = poi_counts.groupby('区域名称').size().reset_index(name='POI数量')

    # 合并统计结果
    boundaries = boundaries.merge(poi_summary, on='区域名称', how='left')
    boundaries['POI数量'] = boundaries['POI数量'].fillna(0)

    # 4. 可视化
    fig, axes = plt.subplots(1, 2, figsize=(16, 8))

    boundaries.plot(ax=axes[0], column='POI数量', legend=True, cmap='YlGn')
    pois.plot(ax=axes[0], color='red', markersize=5)
    axes[0].set_title('POI 分布与数量')

    # 服务覆盖率
    service_union = pois.set_geometry('service_area').union_all()
    boundaries.plot(ax=axes[1], color='lightgray', edgecolor='black')
    gpd.GeoSeries([service_union]).plot(ax=axes[1], alpha=0.5, color='blue')
    axes[1].set_title(f'POI 服务覆盖范围({buffer_distance}m)')

    plt.tight_layout()
    plt.savefig("poi_analysis.png", dpi=150)

    # 5. 输出
    boundaries.to_file("poi_analysis_result.gpkg", driver="GPKG")

    return boundaries

3.10 本章小结

本章深入剖析了 GeoPandas 的核心架构与数据模型:

主题 要点
分层架构 用户接口 → 核心层 → 依赖层 → C/C++ 底层
GeoDataFrame 继承 pandas DataFrame,包含几何列、CRS、空间索引
GeoSeries 继承 pandas Series,存储 Shapely 几何对象
GeometryArray pandas ExtensionArray,底层使用 numpy 对象数组
GeoPandasBase GeoSeries/GeoDataFrame 的公共空间操作方法(100+)
GeometryDtype pandas 扩展类型,注册为 “geometry”
空间索引 基于 STRtree,延迟创建,大幅提升空间查询性能
类继承关系 清晰的继承链,最大化复用 pandas 功能
数据处理管道 读取 → 清洗 → 分析 → 可视化 → 输出

关键理解

  1. GeoDataFrame = pandas DataFrame + 几何列 + CRS
  2. GeoSeries = pandas Series + Shapely 几何对象
  3. 所有 pandas 操作在 GeoPandas 中都可用
  4. 空间操作通过 Shapely 2.0 向量化实现高性能

下一章预告:第 4 章将详细讲解 GeoDataFrame 的基础操作,包括创建、查看、修改、过滤等常用操作。


📚 参考资料