第三章的核心架构与数据模型有哪些关键要素?

摘要:layout: default title: "第3章:核心架构与数据模型" 第3章:核心架构与数据模型 深入理解 GeoPandas 的内部架构是高效使用它的前提。本章将系统剖析 Geo
第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 的架构体现了以下设计理念: 继承与扩展:通过继承 pandas 的核心类,最大化代码复用 组合优于继承:使用 Shapely 对象作为几何列的元素,而非重新实现 向量化优先:利用 Shapely 2.0 的向量化能力,避免逐行处理 延迟计算:空间索引等资源按需创建 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 公共基类设计 GeoSeries 和 GeoDataFrame 共享一个公共基类 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 空间索引的注意事项 延迟创建:空间索引在第一次访问 sindex 时创建 不可变:修改数据后需要重新创建索引 内存消耗:大数据集的空间索引会占用较多内存 自动使用:sjoin、clip 等高级操作会自动利用空间索引 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 功能 数据处理管道 读取 → 清洗 → 分析 → 可视化 → 输出 关键理解: GeoDataFrame = pandas DataFrame + 几何列 + CRS GeoSeries = pandas Series + Shapely 几何对象 所有 pandas 操作在 GeoPandas 中都可用 空间操作通过 Shapely 2.0 向量化实现高性能 下一章预告:第 4 章将详细讲解 GeoDataFrame 的基础操作,包括创建、查看、修改、过滤等常用操作。 📚 参考资料 GeoPandas 源码 - GeoDataFrame GeoPandas 源码 - GeoSeries pandas ExtensionArray 文档 Shapely 2.0 文档