znlgis 博客

GIS开发与技术分享

第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 中的几何操作方法,包括缓冲区分析、叠加分析等空间分析功能。