惯性聚合 高效追踪和阅读你感兴趣的博客、新闻、科技资讯
阅读原文 在惯性聚合中打开

推荐订阅源

爱范儿
爱范儿
Simon Willison's Weblog
Simon Willison's Weblog
K
Kaspersky official blog
P
Palo Alto Networks Blog
Google DeepMind News
Google DeepMind News
www.infosecurity-magazine.com
www.infosecurity-magazine.com
AI
AI
G
GRAHAM CLULEY
O
OpenAI News
Application and Cybersecurity Blog
Application and Cybersecurity Blog
Help Net Security
Help Net Security
L
LINUX DO - 热门话题
S
Schneier on Security
P
Privacy International News Feed
L
Lohrmann on Cybersecurity
SecWiki News
SecWiki News
C
Cybersecurity and Infrastructure Security Agency CISA
T
Threatpost
C
Cyber Attacks, Cyber Crime and Cyber Security
A
Arctic Wolf
C
Cisco Blogs
V2EX - 技术
V2EX - 技术
有赞技术团队
有赞技术团队
Apple Machine Learning Research
Apple Machine Learning Research
月光博客
月光博客
Latest news
Latest news
人人都是产品经理
人人都是产品经理
Schneier on Security
Schneier on Security
Last Week in AI
Last Week in AI
Webroot Blog
Webroot Blog
美团技术团队
N
News and Events Feed by Topic
大猫的无限游戏
大猫的无限游戏
Security Archives - TechRepublic
Security Archives - TechRepublic
奇客Solidot–传递最新科技情报
奇客Solidot–传递最新科技情报
博客园 - 三生石上(FineUI控件)
The Cloudflare Blog
Project Zero
Project Zero
博客园_首页
Cloudbric
Cloudbric
IT之家
IT之家
OSCHINA 社区最新新闻
OSCHINA 社区最新新闻
让小产品的独立变现更简单 - ezindie.com
让小产品的独立变现更简单 - ezindie.com
雷峰网
雷峰网
罗磊的独立博客
Hacker News: Ask HN
Hacker News: Ask HN
The Last Watchdog
The Last Watchdog
cs.CV updates on arXiv.org
cs.CV updates on arXiv.org
T
Tenable Blog
Scott Helme
Scott Helme

博客园 - 我才是银古

第16章:常见问题、排错与最佳实践 第15章:扩展生态、MCAD 与外部集成 第12章:实战案例:机械结构与 3D 打印零件 第14章:构建、测试、调试与贡献流程 第13章:OpenSCAD 源码架构与核心执行流程 第11章:预览、渲染、网格精度与性能优化 第09章:列表推导、递归与算法建模 第08章:参数化零件库与复用设计 第10章:导入导出、命令行与自动化 第06章:CSG 布尔建模方法 第07章:二维图形、拉伸、旋转与投影 第05章:基础几何、坐标系与变换 第04章:参数、变量、函数、模块与作用域 OpenSCAD 教程目录 第03章:OpenSCAD 语言基础 第02章:安装、环境配置与开发工作流 第01章:OpenSCAD 项目全景与学习路线 第02章:源码获取、编译与开发环境配置 第01章:OCCT项目全景与学习路线 第18章:二次开发实战与综合案例 第18章:综合实战案例 第17章:数据交换与协同 第16章:源码架构与二次开发 第15章:插件与自定义工作台开发 第14章:Python脚本宏与自动化 第13章:FEM仿真分析 第12章:CAM数控加工 第11章:SurfaceMesh与逆向工程 第10章:Draft二维绘图与BIM建筑 第09章:工程图TechDraw 第07章:参数化表达式与Spreadsheet 第08章:装配设计Assembly 第06章:Part工作台与几何内核 第05章:PartDesign实体特征建模 第04章:草图Sketcher约束建模 第02章:安装版本与工作环境配置 第03章:界面工作台与基础操作 第01章:项目全景与学习路线 第十二章:插件开发、研究功能与最佳实践 第十章:定时任务与自动化(Cron) 第七章:技能、记忆与自学习闭环 第八章:MCP 集成与上下文文件 第六章:工具系统与终端后端 第五章:模型供应商与配置体系 Hermes Agent 教程目录 第十一章:语音、视觉、浏览器与子代理协作 第四章:CLI/TUI 与会话管理 第十二章:学习路线、实战方案与最佳实践 第十一章:源码结构、开发调试与插件开发 第十章:自动化、远程访问、日志与排障 第九章:Control UI、节点、Canvas 与语音能力 第七章:工具、技能、插件与能力扩展 第八章:安全模型、访问控制与沙箱实践 第六章:Agent 工作区、会话与多智能体路由 第五章:多通道消息接入与聊天平台配置 第四章:配置体系、模型接入与认证管理 第三章:Gateway 架构、协议与运行机制 第二章:安装、环境准备与快速上手 第一章:OpenClaw 项目概览与核心定位 oh-my-openagent 教程目录 09-命令模型回退与配置参考 10-实战案例最佳实践与故障排除 05-工作模式-Ultrawork-Prometheus-Atlas 08-Hooks与MCP系统 06-Category与Skill系统 07-核心工具链 04-智能体全景详解 03-安装与环境配置 02-整体架构与多模型编排机制 01-项目简介与核心理念 01-项目概览与学习路线 02-安装部署与工具适配 03-Skill机制与using-superpowers 05-TDD系统化调试与完成前验证 04-需求澄清方案设计与计划编写 07-并行智能体子智能体与Git-Worktree 第六章:代码审查、反馈处理与分支收尾 08-中国特色Skills与本土团队落地 09-MCP构建工作流执行与自定义Skill 第23章:FreeCAD-Python-API Clipper2 C# 源码解读教程 第19章:PolyTree 多边形树结构 第20章:实际应用与最佳实践 第18章:Minkowski 和与差 第17章:RectClip 矩形裁剪优化 第16章:ClipperOffset 偏移类详解 第15章:填充规则详解 第14章:布尔运算执行流程 第13章:ClipperD 浮点裁剪类 第11章:OutRec 与 OutPt 输出结构 第9章:Active 活动边结构 第10章:Vertex 顶点与 LocalMinima 局部极小值 第12章:Clipper64 裁剪类详解 第7章:高精度运算与128位整数 第8章:ClipperBase 基类详解 第5章:枚举类型与常量定义 第6章:InternalClipper 内部工具类 第2章:核心数据结构 - Point64、PointD 第3章:路径与多边形表示 - Path64、PathD、Paths64、PathsD 第4章:矩形边界 - Rect64、RectD
第10章:几何属性与度量
我才是银古 · 2026-06-22 · via 博客园 - 我才是银古

第10章:几何属性与度量

几何属性与度量是空间数据分析的基础。GeoPandas 通过 Shapely 库提供了丰富的几何属性查询和度量计算功能,可以方便地获取几何对象的类型、面积、长度、边界框、质心等信息。本章将全面介绍 GeoPandas 中的几何属性与度量方法。


10.1 几何类型属性

10.1.1 geom_type 属性

geom_type 属性返回每个几何对象的类型名称(字符串):

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

# 创建包含不同几何类型的 GeoDataFrame
gdf = gpd.GeoDataFrame(
    {"name": ["点", "线", "面", "多点"]},
    geometry=[
        Point(0, 0),
        LineString([(0, 0), (1, 1), (2, 0)]),
        Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
        MultiPoint([(0, 0), (1, 1)])
    ]
)

# 查看每个要素的几何类型
print(gdf.geom_type)

输出:

0         Point
1    LineString
2       Polygon
3    MultiPoint
dtype: object

10.1.2 所有几何类型

GeoPandas/Shapely 支持的几何类型:

类型 说明 geom_type 值
单个坐标点 Point
线 有序点序列 LineString
线环 首尾相连的线 LinearRing
多边形 由外环和可选内环组成 Polygon
多点 点的集合 MultiPoint
多线 线的集合 MultiLineString
多面 多边形的集合 MultiPolygon
几何集合 任意几何的集合 GeometryCollection

10.1.3 几何类型判断

# 判断是否为特定类型
is_point = gdf.geom_type == "Point"
is_polygon = gdf.geom_type.isin(["Polygon", "MultiPolygon"])
is_line = gdf.geom_type.isin(["LineString", "MultiLineString"])

print(f"点要素: {is_point.sum()} 个")
print(f"面要素: {is_polygon.sum()} 个")
print(f"线要素: {is_line.sum()} 个")

# 按类型筛选
gdf_points = gdf[gdf.geom_type == "Point"]
gdf_polygons = gdf[gdf.geom_type.isin(["Polygon", "MultiPolygon"])]

10.1.4 统计几何类型分布

# 统计各类型数量
type_counts = gdf.geom_type.value_counts()
print("几何类型分布:")
print(type_counts)

# 检查是否为单一类型
if len(gdf.geom_type.unique()) == 1:
    print(f"数据集为单一类型: {gdf.geom_type.unique()[0]}")
else:
    print("数据集包含混合类型")

10.2 面积计算

10.2.1 area 属性

area 属性返回每个几何对象的面积:

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

gdf = gpd.GeoDataFrame(
    {"name": ["正方形", "三角形", "点", "线"]},
    geometry=[
        Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
        Polygon([(0, 0), (2, 0), (1, 1)]),
        Point(0, 0),
        LineString([(0, 0), (1, 1)])
    ]
)

print(gdf.area)

输出:

0    1.0
1    1.0
2    0.0
3    0.0
dtype: float64

注意: 点和线的面积始终为 0。

10.2.2 投影坐标系的重要性

这是最容易出错的地方之一! 在地理坐标系(如 EPSG:4326)下,.area 返回的值的单位是平方度,这个数值没有实际物理意义。

import geopandas as gpd
from shapely.geometry import box

# 创建一个近似北京市区范围的矩形
beijing_approx = gpd.GeoDataFrame(
    {"name": ["北京近似"]},
    geometry=[box(116.0, 39.6, 116.8, 40.2)],
    crs="EPSG:4326"
)

# ❌ 错误:在地理坐标系下计算面积
area_degrees = beijing_approx.area.iloc[0]
print(f"地理坐标系面积: {area_degrees:.6f} 平方度(无意义)")

# ✅ 正确:转换到投影坐标系后计算
utm_crs = beijing_approx.estimate_utm_crs()
beijing_utm = beijing_approx.to_crs(utm_crs)
area_sqm = beijing_utm.area.iloc[0]
print(f"投影坐标系面积: {area_sqm:,.0f} 平方米")
print(f"投影坐标系面积: {area_sqm / 1e6:,.2f} 平方公里")

10.2.3 面积计算的最佳实践

def calculate_area(gdf, unit="sqm"):
    """
    计算面积,自动处理坐标系转换

    参数:
        gdf: GeoDataFrame
        unit: 面积单位 - "sqm"(平方米), "sqkm"(平方公里),
              "ha"(公顷), "mu"(亩)
    """
    # 如果是地理坐标系,先转换到投影坐标系
    if gdf.crs and gdf.crs.is_geographic:
        utm_crs = gdf.estimate_utm_crs()
        gdf_proj = gdf.to_crs(utm_crs)
    else:
        gdf_proj = gdf

    area_sqm = gdf_proj.area

    # 单位转换
    conversions = {
        "sqm": 1,
        "sqkm": 1e-6,
        "ha": 1e-4,
        "mu": 1 / 666.667,  # 1亩 ≈ 666.667 平方米
        "acre": 1 / 4046.86
    }

    factor = conversions.get(unit, 1)
    return area_sqm * factor

# 使用示例
gdf["area_sqkm"] = calculate_area(gdf, unit="sqkm")
gdf["area_mu"] = calculate_area(gdf, unit="mu")
print(gdf[["name", "area_sqkm", "area_mu"]])

10.3 长度计算

10.3.1 length 属性

length 属性返回几何对象的长度或周长:

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

gdf = gpd.GeoDataFrame(
    {"name": ["线段", "折线", "多边形", "点"]},
    geometry=[
        LineString([(0, 0), (3, 4)]),              # 直线段
        LineString([(0, 0), (1, 0), (1, 1)]),      # 折线
        Polygon([(0, 0), (4, 0), (4, 3), (0, 3)]), # 矩形
        Point(0, 0)                                  # 点
    ]
)

print(gdf.length)

输出:

0     5.0      # sqrt(3² + 4²) = 5
1     2.0      # 1 + 1 = 2
2    14.0      # 矩形周长 = 2*(4+3) = 14
3     0.0      # 点的长度为 0
dtype: float64

10.3.2 线要素与多边形周长

几何类型 length 含义
Point 0
LineString 线的总长度
LinearRing 环的周长
Polygon 外环周长(不含内环)
MultiLineString 所有线段长度之和
MultiPolygon 所有多边形外环周长之和
from shapely.geometry import Polygon

# 带洞的多边形
polygon_with_hole = Polygon(
    [(0, 0), (10, 0), (10, 10), (0, 10)],  # 外环
    [[(2, 2), (4, 2), (4, 4), (2, 4)]]     # 内环(洞)
)

# 注意:Polygon 的 length 返回的是外环的周长
print(f"多边形周长(外环): {polygon_with_hole.length}")
# 40.0

# 如果需要包括内环的总长度
exterior_length = polygon_with_hole.exterior.length
interior_length = sum(ring.length for ring in polygon_with_hole.interiors)
total_length = exterior_length + interior_length
print(f"外环周长: {exterior_length}")
print(f"内环周长: {interior_length}")
print(f"总周长: {total_length}")

10.3.3 长度计算同样需要投影坐标系

# ❌ 地理坐标系下的长度单位是"度",无意义
gdf_geo = gpd.GeoDataFrame(
    geometry=[LineString([(116.0, 39.0), (117.0, 39.0)])],
    crs="EPSG:4326"
)
print(f"地理坐标系长度: {gdf_geo.length.iloc[0]:.4f} 度")

# ✅ 转换到投影坐标系后计算
gdf_utm = gdf_geo.to_crs(gdf_geo.estimate_utm_crs())
print(f"投影坐标系长度: {gdf_utm.length.iloc[0]:,.0f} 米")
# 约 85,394 米(在纬度 39° 处,1° 经度约 85.4 公里)

10.4 边界框

10.4.1 bounds — 每个要素的边界框

bounds 属性返回每个几何对象的最小边界矩形(MBR):

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

gdf = gpd.GeoDataFrame(
    {"name": ["三角形", "折线", "点"]},
    geometry=[
        Polygon([(0, 0), (4, 0), (2, 3)]),
        LineString([(1, 1), (5, 3), (3, 5)]),
        Point(2, 2)
    ]
)

print(gdf.bounds)

输出:

   minx  miny  maxx  maxy
0   0.0   0.0   4.0   3.0
1   1.0   1.0   5.0   5.0
2   2.0   2.0   2.0   2.0

10.4.2 bounds 的返回格式

bounds 返回一个 DataFrame,包含四列:

列名 说明
minx 最小 X 坐标(最西端经度)
miny 最小 Y 坐标(最南端纬度)
maxx 最大 X 坐标(最东端经度)
maxy 最大 Y 坐标(最北端纬度)
# 将边界框信息添加到 GeoDataFrame
bounds = gdf.bounds
gdf["bbox_width"] = bounds["maxx"] - bounds["minx"]
gdf["bbox_height"] = bounds["maxy"] - bounds["miny"]
gdf["bbox_area"] = gdf["bbox_width"] * gdf["bbox_height"]

print(gdf[["name", "bbox_width", "bbox_height", "bbox_area"]])

10.4.3 total_bounds — 整体范围

total_bounds 属性返回整个 GeoDataFrame 的总边界框(numpy 数组):

# 所有要素的总范围
total = gdf.total_bounds
print(f"总范围: {total}")
# [minx, miny, maxx, maxy]

print(f"X 范围: {total[0]:.4f} - {total[2]:.4f}")
print(f"Y 范围: {total[1]:.4f} - {total[3]:.4f}")

# 用途示例:创建覆盖所有数据的边界矩形
from shapely.geometry import box
bbox_polygon = box(*total)
print(f"边界矩形: {bbox_polygon}")

10.4.4 实际应用示例

import geopandas as gpd

# 读取中国省级数据
# gdf = gpd.read_file("china_provinces.shp")

# 获取每个省份的边界范围
# province_bounds = gdf.bounds
# province_bounds["province"] = gdf["name"]
# print(province_bounds.head())

# 获取中国的整体范围
# china_extent = gdf.total_bounds
# print(f"中国范围:")
# print(f"  经度: {china_extent[0]:.2f}° - {china_extent[2]:.2f}°")
# print(f"  纬度: {china_extent[1]:.2f}° - {china_extent[3]:.2f}°")
# 约为: 经度 73.5° - 135.1°, 纬度 18.2° - 53.6°

# 使用总范围进行空间查询
# other_gdf = gpd.read_file("data.shp", bbox=tuple(gdf.total_bounds))

10.5 质心

10.5.1 centroid 属性

centroid 属性返回每个几何对象的几何中心点:

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

gdf = gpd.GeoDataFrame(
    {"name": ["正方形", "三角形", "线段"]},
    geometry=[
        Polygon([(0, 0), (4, 0), (4, 4), (0, 4)]),
        Polygon([(0, 0), (6, 0), (3, 4)]),
        LineString([(0, 0), (10, 0)])
    ]
)

centroids = gdf.centroid
print("质心坐标:")
for name, centroid in zip(gdf["name"], centroids):
    print(f"  {name}: ({centroid.x:.2f}, {centroid.y:.2f})")

输出:

质心坐标:
  正方形: (2.00, 2.00)
  三角形: (3.00, 1.33)
  线段: (5.00, 0.00)

10.5.2 质心的特点与注意事项

from shapely.geometry import Polygon

# 注意:质心可能不在几何内部!
# 例如凹多边形(L 形)
l_shape = Polygon([
    (0, 0), (2, 0), (2, 1), (1, 1),
    (1, 3), (0, 3)
])

centroid = l_shape.centroid
print(f"L 形质心: ({centroid.x:.2f}, {centroid.y:.2f})")
print(f"质心在几何内部: {l_shape.contains(centroid)}")
# 质心可能落在 L 形的凹陷处,不在几何内部

10.5.3 为面要素添加质心标注

# 常见用途:在地图上为面要素添加标注点
gdf["centroid_x"] = gdf.centroid.x
gdf["centroid_y"] = gdf.centroid.y

# 创建质心点图层
gdf_centroids = gdf.copy()
gdf_centroids = gdf_centroids.set_geometry(gdf.centroid)
print(gdf_centroids.geom_type.unique())
# ['Point']

10.6 代表点

10.6.1 representative_point() 方法

representative_point() 返回一个保证在几何对象内部的点。与质心不同,代表点一定在几何内部。

import geopandas as gpd
from shapely.geometry import Polygon

# 创建 L 形(凹多边形)
l_shape = Polygon([
    (0, 0), (4, 0), (4, 2), (2, 2),
    (2, 4), (0, 4)
])

gdf = gpd.GeoDataFrame(
    {"name": ["L形"]},
    geometry=[l_shape]
)

# 对比质心和代表点
centroid = gdf.centroid.iloc[0]
rep_point = gdf.representative_point().iloc[0]

print(f"质心: ({centroid.x:.2f}, {centroid.y:.2f})")
print(f"代表点: ({rep_point.x:.2f}, {rep_point.y:.2f})")
print(f"质心在内部: {l_shape.contains(centroid)}")
print(f"代表点在内部: {l_shape.contains(rep_point)}")

10.6.2 质心 vs 代表点

特性 centroid representative_point()
计算方式 几何重心 算法保证在内部
是否在内部 不一定 保证在内部
凸几何 一定在内部 在内部
凹几何 可能在外部 在内部
多边形有洞 可能在洞内 在实体部分
适用场景 统计分析 标注、定位
# 实际建议:
# - 统计分析(如加权平均中心)→ 使用 centroid
# - 地图标注 → 使用 representative_point()
# - 空间索引点 → 使用 representative_point()

# 为每个面要素生成标注点
gdf["label_point"] = gdf.representative_point()

10.7 空几何判断

10.7.1 is_empty 属性

is_empty 属性检查几何对象是否为空(没有坐标点的几何):

import geopandas as gpd
from shapely.geometry import Point, LineString
from shapely import wkt

# 创建包含空几何的数据
gdf = gpd.GeoDataFrame(
    {"name": ["正常点", "空几何", "正常线"]},
    geometry=[
        Point(1, 2),
        wkt.loads("GEOMETRYCOLLECTION EMPTY"),
        LineString([(0, 0), (1, 1)])
    ]
)

print(gdf.is_empty)

输出:

0    False
1     True
2    False
dtype: bool

10.7.2 处理空几何

# 过滤掉空几何
gdf_valid = gdf[~gdf.is_empty]
print(f"移除空几何后: {len(gdf_valid)} 条记录")

# 统计空几何数量
empty_count = gdf.is_empty.sum()
print(f"空几何数量: {empty_count}")

# 替换空几何
# gdf.loc[gdf.is_empty, "geometry"] = Point(0, 0)  # 用默认点替代

10.8 有效性判断

10.8.1 is_valid 属性

is_valid 属性检查几何是否满足 OGC Simple Features 规范的有效性规则:

import geopandas as gpd
from shapely.geometry import Polygon

# 创建有效和无效的几何
valid_polygon = Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])

# "蝴蝶结"(自相交多边形)— 无效
invalid_polygon = Polygon([(0, 0), (1, 1), (1, 0), (0, 1)])

gdf = gpd.GeoDataFrame(
    {"name": ["有效", "无效"]},
    geometry=[valid_polygon, invalid_polygon]
)

print(gdf.is_valid)

输出:

0     True
1    False
dtype: bool

10.8.2 is_valid_reason — 获取无效原因

from shapely.validation import explain_validity

# 检查无效原因
for idx, row in gdf.iterrows():
    if not row.geometry.is_valid:
        reason = explain_validity(row.geometry)
        print(f"要素 {row['name']}: {reason}")

# 在 GeoSeries 级别
# GeoPandas 提供了 is_valid_reason 属性(取决于版本)
# print(gdf.geometry.is_valid_reason)

10.8.3 常见的无效几何类型

无效类型 说明 示例
自相交 多边形边界与自身相交 蝴蝶结形状
重复点 连续的相同坐标点 退化的线段
环方向 外环和内环方向不正确 内环应为顺时针
非闭合环 多边形的环首尾不相连
嵌套洞 内环在外环之外
零面积 退化为点或线的多边形

10.8.4 修复无效几何

from shapely.validation import make_valid

# 方法1:使用 make_valid()
gdf["geometry"] = gdf.geometry.make_valid()

# 方法2:使用 buffer(0) 修复(经典方法)
gdf["geometry"] = gdf.geometry.buffer(0)

# 方法3:逐个检查和修复
def fix_geometry(geom):
    """尝试修复无效几何"""
    if geom is None or geom.is_empty:
        return geom
    if not geom.is_valid:
        # 先尝试 make_valid
        fixed = make_valid(geom)
        if fixed.is_valid:
            return fixed
        # 再尝试 buffer(0)
        fixed = geom.buffer(0)
        return fixed
    return geom

gdf["geometry"] = gdf.geometry.apply(fix_geometry)

# 验证修复结果
print(f"所有几何有效: {gdf.is_valid.all()}")

10.9 简单性判断

10.9.1 is_simple 属性

is_simple 检查几何是否是"简单的"(不自相交):

import geopandas as gpd
from shapely.geometry import LineString

# 简单线(不自相交)
simple_line = LineString([(0, 0), (1, 1), (2, 0)])

# 非简单线(自相交,形成8字形)
complex_line = LineString([(0, 0), (1, 1), (1, 0), (0, 1)])

gdf = gpd.GeoDataFrame(
    {"name": ["简单线", "自相交线"]},
    geometry=[simple_line, complex_line]
)

print(gdf.is_simple)

输出:

0     True
1    False
dtype: bool

10.9.2 简单性 vs 有效性

概念 适用类型 说明
简单性(is_simple) 主要针对线 线不自相交
有效性(is_valid) 主要针对面 符合 OGC 规范
# 对于多边形:有效则一定简单
# 对于线:简单不一定有效(线没有有效性概念)
# 点始终是简单的

10.10 环判断

10.10.1 is_ring 属性

is_ring 检查线要素是否构成闭合环(首尾相连且简单):

import geopandas as gpd
from shapely.geometry import LineString

# 闭合环(首尾相连)
ring = LineString([(0, 0), (1, 0), (1, 1), (0, 1), (0, 0)])

# 非闭合线
open_line = LineString([(0, 0), (1, 0), (1, 1)])

# 自相交的闭合线(不是环,因为不简单)
self_intersecting = LineString([(0, 0), (1, 1), (1, 0), (0, 1), (0, 0)])

gdf = gpd.GeoDataFrame(
    {"name": ["闭合环", "非闭合", "自相交闭合"]},
    geometry=[ring, open_line, self_intersecting]
)

print(gdf.is_ring)

输出:

0     True
1    False
2    False
dtype: bool

is_ring 要求同时满足:闭合(is_closed)且简单(is_simple)

10.10.2 is_closed 属性

is_closed 仅检查首尾是否相连,不要求简单性:

# is_closed 只检查首尾是否相连
print("is_closed:")
for name, geom in zip(gdf["name"], gdf.geometry):
    if hasattr(geom, 'is_closed'):
        print(f"  {name}: {geom.is_closed}")

# is_ring = is_closed AND is_simple

10.11 维度信息

10.11.1 has_z 属性

has_z 检查几何是否包含 Z(高程)坐标:

import geopandas as gpd
from shapely.geometry import Point

# 二维点
point_2d = Point(116.4, 39.9)

# 三维点(含高程)
point_3d = Point(116.4, 39.9, 50.0)

gdf = gpd.GeoDataFrame(
    {"name": ["2D点", "3D点"]},
    geometry=[point_2d, point_3d]
)

print(gdf.has_z)

输出:

0    False
1     True
dtype: bool

10.11.2 Z 值的使用场景

# Z 值常用于:
# - 地形高程数据
# - 建筑物高度
# - 地下管线深度
# - 三维城市模型

# 获取 Z 值
point_3d = Point(116.4, 39.9, 50.0)
print(f"X: {point_3d.x}")
print(f"Y: {point_3d.y}")
print(f"Z: {point_3d.z}")

# 注意:大多数二维空间分析会忽略 Z 值
# 面积、距离等计算通常只使用 X、Y 坐标

10.11.3 has_m 属性

M 值(Measure)是除 X、Y、Z 之外的第四个坐标维度,常用于线性参考:

# 注意:Shapely 对 M 值的支持有限
# has_m 属性在较新版本的 Shapely 中可用

# M 值常用于:
# - 道路里程桩
# - 管线测量
# - 时间序列轨迹

# 检查是否支持
# print(gdf.has_m)  # 取决于 Shapely 版本

10.12 坐标计数

10.12.1 count_coordinates()

count_coordinates() 返回每个几何的坐标点总数:

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

# 创建不同复杂度的几何
gdf = gpd.GeoDataFrame(
    {"name": ["点", "线(3点)", "三角形", "矩形"]},
    geometry=[
        Point(0, 0),                                    # 1 个坐标
        LineString([(0, 0), (1, 1), (2, 0)]),           # 3 个坐标
        Polygon([(0, 0), (1, 0), (0, 1)]),              # 4 个坐标(闭合)
        Polygon([(0, 0), (1, 0), (1, 1), (0, 1)])       # 5 个坐标(闭合)
    ]
)

# 使用 shapely 的 count_coordinates 函数
coord_counts = shapely.count_coordinates(gdf.geometry)
print("坐标数量:")
for name, count in zip(gdf["name"], coord_counts):
    print(f"  {name}: {count}")

10.12.2 count_geometries()

count_geometries() 返回多几何中子几何的数量:

from shapely.geometry import MultiPoint, MultiPolygon, Point, Polygon
import shapely

gdf = gpd.GeoDataFrame(
    {"name": ["单点", "3个多点", "2个多面"]},
    geometry=[
        Point(0, 0),
        MultiPoint([(0, 0), (1, 1), (2, 2)]),
        MultiPolygon([
            Polygon([(0, 0), (1, 0), (1, 1), (0, 1)]),
            Polygon([(2, 2), (3, 2), (3, 3), (2, 3)])
        ])
    ]
)

geom_counts = shapely.count_geometries(gdf.geometry)
print("子几何数量:")
for name, count in zip(gdf["name"], geom_counts):
    print(f"  {name}: {count}")

输出:

子几何数量:
  单点: 1
  3个多点: 3
  2个多面: 2

10.12.3 count_interior_rings()

count_interior_rings() 返回多边形中内环(洞)的数量:

from shapely.geometry import Polygon
import shapely

# 无洞多边形
simple_poly = Polygon([(0, 0), (10, 0), (10, 10), (0, 10)])

# 有 1 个洞的多边形
one_hole = Polygon(
    [(0, 0), (10, 0), (10, 10), (0, 10)],
    [[(2, 2), (4, 2), (4, 4), (2, 4)]]
)

# 有 2 个洞的多边形
two_holes = Polygon(
    [(0, 0), (10, 0), (10, 10), (0, 10)],
    [
        [(1, 1), (3, 1), (3, 3), (1, 3)],
        [(5, 5), (7, 5), (7, 7), (5, 7)]
    ]
)

gdf = gpd.GeoDataFrame(
    {"name": ["无洞", "1个洞", "2个洞"]},
    geometry=[simple_poly, one_hole, two_holes]
)

ring_counts = shapely.count_interior_rings(gdf.geometry)
print("内环数量:")
for name, count in zip(gdf["name"], ring_counts):
    print(f"  {name}: {count}")

输出:

内环数量:
  无洞: 0
  1个洞: 1
  2个洞: 2

10.13 精度控制

10.13.1 get_precision() — 获取精度

import shapely
from shapely.geometry import Point

# 默认精度为 0(无限精度/浮点精度)
point = Point(116.40723456789, 39.90456789123)
precision = shapely.get_precision(point)
print(f"默认精度: {precision}")
# 0.0

10.13.2 set_precision() — 设置精度

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

# 降低精度(四舍五入坐标)
point = Point(116.40723456789, 39.90456789123)

# 精度为 0.001(约 100 米级别)
point_low = shapely.set_precision(point, grid_size=0.001)
print(f"原始: ({point.x}, {point.y})")
print(f"降精: ({point_low.x}, {point_low.y})")

输出:

原始: (116.40723456789, 39.90456789123)
降精: (116.407, 39.905)

10.13.3 对 GeoSeries 设置精度

# 对整个 GeoSeries 设置精度
gdf = gpd.GeoDataFrame(
    {"name": ["A", "B"]},
    geometry=[
        Point(116.40723456789, 39.90456789123),
        Point(121.47456789012, 31.23056789012)
    ],
    crs="EPSG:4326"
)

# 将坐标精度降低到小数点后 4 位
gdf["geometry"] = shapely.set_precision(gdf.geometry.values, grid_size=0.0001)
print(gdf.geometry)

10.13.4 精度设置的应用场景

# 1. 减小文件大小(较低精度 → 更少的存储空间)
# 地理坐标系下,不同精度对应的近似空间精度:
# 0.1      → 约 11 km
# 0.01     → 约 1.1 km
# 0.001    → 约 110 m
# 0.0001   → 约 11 m
# 0.00001  → 约 1.1 m
# 0.000001 → 约 0.11 m

# 2. 避免浮点数精度问题导致的拓扑错误
# 3. 匹配不同数据源的精度级别

10.14 面积与长度的单位问题

10.14.1 单位取决于坐标系

这是空间分析中最重要的概念之一:面积和长度的单位由坐标参考系统决定

CRS 类型 area 单位 length 单位 是否有实际意义
地理坐标系(度) 平方度 ❌ 无实际意义
投影坐标系(米) 平方米 ✅ 有实际意义
投影坐标系(英尺) 平方英尺 英尺 ✅ 有实际意义

10.14.2 完整的单位处理流程

import geopandas as gpd
from shapely.geometry import Polygon

# 创建数据(WGS84 地理坐标系)
gdf = gpd.GeoDataFrame(
    {"name": ["区域A", "区域B"]},
    geometry=[
        Polygon([(116.0, 39.5), (116.5, 39.5), (116.5, 40.0), (116.0, 40.0)]),
        Polygon([(121.0, 31.0), (121.5, 31.0), (121.5, 31.5), (121.0, 31.5)])
    ],
    crs="EPSG:4326"
)

# 步骤1:检查当前 CRS
print(f"CRS: {gdf.crs}")
print(f"是地理坐标系: {gdf.crs.is_geographic}")

# 步骤2:转换到投影坐标系
# 方法A:使用 estimate_utm_crs()
utm_crs = gdf.estimate_utm_crs()
gdf_utm = gdf.to_crs(utm_crs)

# 方法B:使用等面积投影(更适合面积计算)
# gdf_aea = gdf.to_crs("+proj=aea +lat_1=25 +lat_2=47 +lat_0=0 +lon_0=105 +datum=WGS84 +units=m")

# 步骤3:计算面积和周长
gdf["area_sqm"] = gdf_utm.area
gdf["area_sqkm"] = gdf_utm.area / 1e6
gdf["perimeter_m"] = gdf_utm.length
gdf["perimeter_km"] = gdf_utm.length / 1000

print(gdf[["name", "area_sqkm", "perimeter_km"]])

10.14.3 不同投影下面积的差异

import geopandas as gpd
from shapely.geometry import Polygon

# 创建一个近似北京市区的矩形
beijing = gpd.GeoDataFrame(
    {"name": ["北京"]},
    geometry=[Polygon([
        (116.0, 39.6), (116.8, 39.6),
        (116.8, 40.2), (116.0, 40.2)
    ])],
    crs="EPSG:4326"
)

# 使用不同投影计算面积
projections = {
    "UTM 50N": "EPSG:32650",
    "Web Mercator": "EPSG:3857",
    "Albers 等面积": "+proj=aea +lat_1=25 +lat_2=47 +lon_0=105 +datum=WGS84 +units=m"
}

print("不同投影下的面积对比:")
for name, crs in projections.items():
    area = beijing.to_crs(crs).area.iloc[0] / 1e6
    print(f"  {name}: {area:,.2f} 平方公里")

# 建议:
# - 面积计算优先使用等面积投影
# - UTM 在小范围内也足够准确
# - Web Mercator 不适合面积计算(面积变形严重)

10.14.4 单位转换参考表

转换 系数
平方米 → 平方公里 × 0.000001 (÷ 1,000,000)
平方米 → 公顷 × 0.0001 (÷ 10,000)
平方米 → 亩 ÷ 666.667
平方米 → 英亩 ÷ 4,046.86
平方米 → 平方英里 ÷ 2,589,988
米 → 公里 ÷ 1,000
米 → 英里 ÷ 1,609.34
米 → 海里 ÷ 1,852
# 单位转换工具函数
class AreaConverter:
    """面积单位转换器"""
    FACTORS = {
        "sqm": 1,
        "sqkm": 1e-6,
        "ha": 1e-4,
        "mu": 1 / 666.667,
        "acre": 1 / 4046.86,
        "sqmi": 1 / 2589988
    }

    @staticmethod
    def convert(value_sqm, to_unit):
        """将平方米转换为目标单位"""
        factor = AreaConverter.FACTORS.get(to_unit)
        if factor is None:
            raise ValueError(f"不支持的单位: {to_unit}")
        return value_sqm * factor

class LengthConverter:
    """长度单位转换器"""
    FACTORS = {
        "m": 1,
        "km": 0.001,
        "mi": 1 / 1609.34,
        "nmi": 1 / 1852,
        "ft": 3.28084
    }

    @staticmethod
    def convert(value_m, to_unit):
        factor = LengthConverter.FACTORS.get(to_unit)
        if factor is None:
            raise ValueError(f"不支持的单位: {to_unit}")
        return value_m * factor

# 使用示例
area_sqm = 16400000  # 16.4 平方公里
print(f"面积: {AreaConverter.convert(area_sqm, 'sqkm'):.2f} 平方公里")
print(f"面积: {AreaConverter.convert(area_sqm, 'ha'):.0f} 公顷")
print(f"面积: {AreaConverter.convert(area_sqm, 'mu'):.0f} 亩")

10.15 本章小结

本章全面介绍了 GeoPandas 中的几何属性与度量功能,涵盖了以下核心内容:

主题 属性/方法 关键要点
几何类型 geom_type 返回字符串类型名,支持类型过滤
面积 area 单位取决于 CRS,必须使用投影坐标系
长度 length 线的长度或面的周长,同样需要投影坐标系
边界框 bounds / total_bounds 单个要素或整体的最小外接矩形
质心 centroid 几何中心,可能不在几何内部
代表点 representative_point() 保证在几何内部,适合标注
空几何 is_empty 检查几何是否为空
有效性 is_valid OGC 规范有效性,可用 make_valid() 修复
简单性 is_simple 检查线是否自相交
is_ring 闭合且简单的线
Z/M 维度 has_z / has_m 是否包含高程或度量维度
坐标计数 count_coordinates() 统计坐标点数量
精度控制 set_precision() 控制坐标精度,减小存储

核心要诀:

  • 面积和长度计算必须在投影坐标系下进行,地理坐标系下的值无意义
  • 面积计算推荐使用等面积投影或 UTM 投影
  • 标注定位使用 representative_point() 而非 centroid
  • 处理数据前检查有效性(is_valid),必要时修复(make_valid()
  • 不同投影下的面积/长度结果会有差异,选择合适的投影很重要

下一章我们将学习 GeoPandas 中的几何操作方法,包括缓冲区分析、叠加分析等空间分析功能。