GeoPandas空间连接?Sjoin函数怎么用?

GIS基础理论
Dr.GIS
wowwwai GIS研习社 · 工具流程与项目排障

如果你正在搜索“GeoPandas空间连接?Sjoin函数怎么用?”,大概率是已经有两份矢量数据:一份点、线或面图层,另一份行政区、网格或缓冲区图层,希望把它们按照空间位置关系关联起来。GeoPandas 的 sjoin 函数就是 Python GIS 中最常用的空间连接工具,适合完成“点落在哪个面内”“道路穿过哪些区域”“地块与规划区是否相交”等任务。

GeoPandas空间连接 sjoin函数怎么用 点面空间连接示意图
GeoPandas sjoin 的典型流程:输入两个 GeoDataFrame,根据空间关系生成带属性的连接结果。

引言:GeoPandas空间连接到底解决什么问题

在 GIS 项目中,属性表连接通常依赖共同字段,比如行政区代码、地块编号或道路 ID。但很多空间数据并没有可直接匹配的字段,这时就需要按照几何位置关系进行连接,这就是空间连接

GeoPandas空间连接的核心场景包括:

  • 把点数据匹配到所在行政区,例如门店点属于哪个区县。
  • 统计每个面内包含哪些点,例如每个街道内有哪些 POI。
  • 判断线与面是否相交,例如道路穿过哪些规划管控区。
  • 把缓冲区分析结果与原始对象属性合并。

在 GeoPandas 中,这类工作主要通过 geopandas.sjoin() 完成。它看起来像 Pandas 的 merge,但连接依据不是普通字段,而是几何对象之间的空间关系。

背景:为什么不能直接用 Pandas merge 做空间连接

Pandas 的 merge 只能根据字段值连接表格,例如 idnamecode。而 GIS 数据中的很多关系并不写在字段里,而是隐藏在几何位置中。

例如你有两份数据:

  • points:一批采样点,字段包括 sample_idvalue
  • districts:区县面数据,字段包括 district_namedistrict_code

如果采样点表中没有 district_code 字段,普通属性连接就无法知道每个点属于哪个区县。但只要点坐标落在某个区县面内,GeoPandas sjoin 就可以根据几何关系把区县属性连接到点上。

这也是 GeoPandas空间连接在空间数据清洗、空间统计、制图分区和空间分析自动化中非常常见的原因。

原理:sjoin函数根据空间谓词连接两个GeoDataFrame

sjoin 的基本思想是:输入两个 GeoDataFrame,比较左表几何与右表几何的空间关系,如果满足指定条件,就把右表属性连接到左表结果中。

常用语法如下:

import geopandas as gpd

result = gpd.sjoin(
    left_df,
    right_df,
    how="left",
    predicate="within"
)

几个参数必须理解清楚:

  • left_df:左侧 GeoDataFrame,结果通常以它为主。
  • right_df:右侧 GeoDataFrame,要被连接进来的图层。
  • how:连接方式,可选 leftrightinner
  • predicate:空间谓词,也就是判断几何关系的条件。

常见 predicate 包括:

predicate 含义 常见用途
within 左侧几何在右侧几何内部 点落在哪个行政区内
contains 左侧几何包含右侧几何 面包含哪些点或小面
intersects 两个几何有任意相交 线穿过面、面与面重叠
touches 两个几何边界接触但内部不重叠 相邻地块、行政区邻接判断
crosses 一个几何穿过另一个几何 道路穿过区域
overlaps 同维度几何部分重叠 面与面局部重叠检查

实际使用时,GeoPandas sjoin 的结果还会自动生成 index_right 字段,用来记录匹配到的右表索引。这个字段常用于后续检查连接是否成功。

步骤:GeoPandas sjoin函数怎么用

步骤1:安装并导入GeoPandas

如果还没有安装 GeoPandas,可以使用 conda 或 pip。GIS 初学者更推荐 conda,因为它能更稳定地处理 GEOS、PROJ、GDAL 等底层空间库依赖。

conda install -c conda-forge geopandas

或使用 pip:

pip install geopandas

在 Python 脚本或 Jupyter Notebook 中导入:

import geopandas as gpd

步骤2:读取点图层和面图层

下面以“采样点连接到行政区面”为例。假设点数据是 samples.geojson,区县面数据是 districts.shp

import geopandas as gpd

points = gpd.read_file("data/samples.geojson")
districts = gpd.read_file("data/districts.shp")

print(points.head())
print(districts.head())
print(points.crs)
print(districts.crs)

这里需要特别注意 crs,也就是坐标参考系。空间连接要求两个图层在同一个坐标系下,否则看起来代码能运行,但结果可能全部为空或错位。

步骤3:统一坐标系

如果两个图层坐标系不同,应把其中一个转换到另一个的坐标系。

if points.crs != districts.crs:
    points = points.to_crs(districts.crs)

这一步是 GeoPandas空间连接中最容易被忽略的步骤。尤其是一个图层是 WGS84 经纬度坐标,另一个图层是投影坐标时,必须先统一坐标系。

步骤4:执行点面空间连接

如果目标是判断每个点落在哪个行政区内,通常使用 predicate="within"

joined = gpd.sjoin(
    points,
    districts[["district_name", "district_code", "geometry"]],
    how="left",
    predicate="within"
)

print(joined.head())

这段代码会把 district_namedistrict_code 连接到点数据上。how="left" 表示保留所有点,即使某些点没有落入任何行政区,也不会被删除。

步骤5:检查连接结果

不要只看代码有没有报错,还要检查结果是否合理。

print(joined[["sample_id", "district_name", "district_code", "index_right"]].head())

unmatched = joined[joined["index_right"].isna()]
print("未匹配点数量:", len(unmatched))

如果未匹配点数量很多,通常说明存在以下问题:

  • 两个图层坐标系不一致。
  • 点坐标本身有偏移或错误。
  • 行政区面边界不完整。
  • 点刚好落在面边界上,within 无法匹配。

步骤6:导出空间连接结果

确认结果无误后,可以导出为 GeoPackage、GeoJSON 或 Shapefile。推荐 GeoPackage,因为它支持较长字段名、中文字段和多图层管理,比 Shapefile 更稳妥。

joined.to_file("output/samples_with_district.gpkg", layer="samples_joined", driver="GPKG")

如果需要导出为 GeoJSON:

joined.to_file("output/samples_with_district.geojson", driver="GeoJSON")

步骤:面内点统计的sjoin用法

除了把行政区属性连接到点上,GeoPandas sjoin 也常用于统计每个面内有多少点。这里可以先完成点面连接,再按区县字段分组统计。

joined = gpd.sjoin(
    points,
    districts[["district_code", "district_name", "geometry"]],
    how="left",
    predicate="within"
)

count_table = joined.groupby("district_code").size().reset_index(name="point_count")

districts_count = districts.merge(count_table, on="district_code", how="left")
districts_count["point_count"] = districts_count["point_count"].fillna(0).astype(int)

districts_count.to_file("output/district_point_count.gpkg", layer="count", driver="GPKG")

这种写法适合制作“每个行政区 POI 数量”“每个网格事件数量”“每个管辖区采样点数量”等统计结果。

步骤:线面相交的sjoin用法

如果左表是道路、河流等线数据,右表是规划区、保护区等面数据,通常使用 predicate="intersects"

roads = gpd.read_file("data/roads.gpkg")
zones = gpd.read_file("data/zones.gpkg")

if roads.crs != zones.crs:
    roads = roads.to_crs(zones.crs)

road_zone = gpd.sjoin(
    roads,
    zones[["zone_name", "zone_type", "geometry"]],
    how="inner",
    predicate="intersects"
)

road_zone.to_file("output/roads_intersect_zones.gpkg", layer="road_zone", driver="GPKG")

how="inner" 表示只保留与规划区相交的道路。如果你希望保留所有道路,并标记哪些道路与规划区相交,则应使用 how="left"

常见坑:GeoPandas空间连接结果为空或不对

坑1:坐标系不同但没有to_crs

这是 GeoPandas空间连接最常见的问题。两个图层坐标系不同,几何坐标值不在同一空间中,连接结果自然不可靠。

print(points.crs)
print(districts.crs)

如果不同,使用:

points = points.to_crs(districts.crs)

注意:to_crs 是坐标转换,不是简单修改坐标系标签。如果数据本身没有 CRS,但你知道它真实坐标系,应先用 set_crs 正确指定,再进行 to_crs

坑2:within和contains方向用反

withincontains 是有方向的。

  • point within polygon:点在面内,常用于点连接面。
  • polygon contains point:面包含点,常用于面查找点。

如果你写的是:

gpd.sjoin(points, districts, predicate="contains")

通常得不到想要的点面匹配结果,因为点不可能包含行政区面。点面连接更常见的写法是:

gpd.sjoin(points, districts, predicate="within")

坑3:点落在边界上导致within匹配不到

within 要求点严格在面内部。如果点刚好落在多边形边界上,可能不满足 within。这时可以考虑使用 intersects

joined = gpd.sjoin(
    points,
    districts,
    how="left",
    predicate="intersects"
)

但要注意,边界点可能同时与两个相邻面相交,从而产生一对多结果。此时需要根据业务规则决定保留哪一个面。

坑4:空间连接出现重复记录

sjoin 并不保证一条左表记录只匹配一条右表记录。如果一个点落在多个重叠面内,或者一条线穿过多个面,结果会出现重复行。

检查重复的方法:

duplicated_count = joined.index.duplicated().sum()
print("重复匹配数量:", duplicated_count)

如果业务上只允许一对一匹配,需要进一步筛选,例如按面积、距离、优先级字段或行政层级选择唯一结果。

坑5:几何无效导致连接异常

面数据如果存在自相交、空几何或无效几何,空间连接可能结果异常。可以先检查:

print(districts.geometry.is_valid.value_counts())
print(districts.geometry.is_empty.value_counts())

常见修复方式:

districts["geometry"] = districts.geometry.buffer(0)

buffer(0) 可以修复部分简单无效面,但不是万能方法。对于复杂拓扑错误,建议在 QGIS 或 ArcGIS Pro 中使用几何修复工具进一步处理。

方法比较:sjoin、overlay、clip和普通merge怎么选

方法 连接依据 是否改变几何 适合场景
merge 字段值 不改变 根据行政代码、ID、名称连接属性表
sjoin 空间关系 通常保留左表几何 点落面、线面相交、面面关系匹配
overlay 空间叠加 会切割并生成新几何 面与面相交、交集、并集、差集分析
clip 裁剪范围 会裁剪几何 按研究区范围裁剪数据

简单判断可以这样记:

  • 只是按字段合并属性,用 merge
  • 想按位置关系把属性带过来,用 sjoin
  • 需要生成相交后的新几何,用 overlay
  • 只想把数据裁到某个范围内,用 clip

所以,GeoPandas sjoin函数适合“空间关系驱动的属性连接”,但不适合替代所有叠加分析。

检查清单:运行sjoin前后要确认什么

为了减少 GeoPandas空间连接结果出错,建议每次按下面清单检查。

运行前检查

  • 两个输入对象都是 GeoDataFrame,并且都有 geometry 列。
  • left_df.crsright_df.crs 已经一致。
  • 几何类型符合空间谓词逻辑,例如点面用 withinintersects
  • 没有大量空几何或无效几何。
  • 右表只保留必要字段,避免结果字段过多。

运行后检查

  • 查看 index_right 是否有大量空值。
  • 检查结果行数是否异常增加。
  • 抽样查看地图位置和属性是否匹配。
  • 检查是否出现重复字段名后缀,例如 name_leftname_right
  • 确认导出格式是否支持字段名、中文和坐标系信息。

一个实用的检查模板如下:

print("左表数量:", len(points))
print("连接结果数量:", len(joined))
print("未匹配数量:", joined["index_right"].isna().sum())
print(joined.columns)

FAQ:GeoPandas空间连接常见问题

Q1:GeoPandas sjoin 和 ArcGIS 空间连接是一个意思吗?

概念上类似,都是根据空间关系把一个图层的属性连接到另一个图层。但 ArcGIS Pro 的空间连接工具提供更多图形界面参数,例如匹配选项、字段映射和汇总规则。GeoPandas sjoin 更适合 Python 自动化流程,灵活但需要自己处理统计、去重和结果验证。

Q2:GeoPandas sjoin 为什么提示 predicate 参数错误?

较老版本 GeoPandas 曾使用 op 参数,较新用法推荐 predicate。如果你的代码来自旧教程,可能会看到:

gpd.sjoin(points, districts, op="within")

现在更推荐写成:

gpd.sjoin(points, districts, predicate="within")

如果环境版本较旧,建议升级 GeoPandas,或查看当前版本文档确认参数名称。

Q3:点面空间连接用 within 还是 intersects?

如果点必须严格落在面内部,用 within。如果边界点也要算匹配,可以用 intersects。但 intersects 可能让边界点同时匹配多个相邻面,需要后续去重或制定归属规则。

Q4:sjoin 能不能直接统计每个面内有多少点?

sjoin 本身返回连接结果,不直接生成汇总表。但可以先用 sjoin 得到点与面的对应关系,再使用 Pandas 的 groupby 统计数量,最后再 merge 回面图层。

Q5:GeoPandas空间连接很慢怎么办?

首先确认已安装支持空间索引的依赖。GeoPandas 会利用空间索引减少几何比较次数。其次可以减少字段、裁剪研究区、过滤无关数据、简化过复杂的面几何。对于超大数据量,可以考虑 PostGIS 的 ST_IntersectsST_Within 与空间索引来完成数据库级空间连接。

Q6:sjoin 后结果为什么比原始点数量多?

因为空间连接可能是一对多关系。一个点如果落在多个重叠面内,就会生成多条结果;一条线穿过多个面,也会重复出现。需要根据业务规则处理重复匹配,例如选择优先级最高的面,或保留全部关系用于统计。

结论:先选对空间谓词,再检查坐标系和结果

GeoPandas空间连接的关键不是只会写 gpd.sjoin(),而是理解它背后的空间关系。点面匹配常用 within,线面和面面关系常用 intersects,而 how 决定结果保留左表、右表还是只保留匹配记录。

在实际项目中,建议按固定流程操作:读取数据、检查 CRS、统一坐标系、选择合适的 predicate、执行 sjoin、检查 index_right 和结果行数,最后再导出成果。只要把这些环节做好,GeoPandas sjoin函数就能稳定完成大部分常见的 Python GIS 空间连接任务。