PAYATHON 2026

gtfstools 空间筛选 ATM GTFS 时出现 sfheaders 错误怎么办?

数据小许

优先检查并排序 ATM 的 stop_times:按 trip_id 分组,再按 stop_sequence 排序。仅排序 shapes 无法保证 filter_by_spatial_extent() 所需的班次几何正常生成。

这个判断与现有报错相符,但由于缺少 ATM 数据文件和实际调用栈,还不能断定排序问题是该文件的唯一问题。可以通过下面的分步调用验证。

为什么 shapes 排序后仍会报错?

在核对过的 filter_by_spatial_extent() 实现中,有两条独立的几何生成路径:有 shapes 时调用 convert_shapes_to_sf(gtfs);有 stop_times 时调用 get_trip_geometry(gtfs, file = "stop_times")。因此,shapes 正常时,stop_times 的几何生成仍可能失败。这也解释了没有 shapes.txt 的 feed 为什么可以完成筛选。筛选函数源码

从 stop_times 生成几何时,函数根据 stops 中的坐标,按停靠顺序连接站点。同一 trip_id 的记录如果分散交错,可能触发底层线几何索引错误;班次内的顺序不正确,也可能导致线路走向错误。

部分版本的 get_trip_geometry() 默认将 sort_sequence 设为 FALSE,不会主动整理顺序。不同版本的行为可能有所不同,应以本机帮助文档为准。参数与几何生成说明

先单独定位失败的几何生成步骤

在原始 ATM 对象上分别运行以下调用,观察哪一步出现相同的错误:

# 检查基于 stop_times 的班次几何
atm_trips_sf <- gtfstools::get_trip_geometry(
  gtfs_list[["atm"]],
  file = "stop_times"
)

如果 ATM 对象包含非空 shapes,再单独检查:

atm_shapes_sf <- gtfstools::convert_shapes_to_sf(
  gtfs_list[["atm"]]
)

如果第一步复现报错,而第二步成功,重点检查 stop_times。错误文字中出现 un-ordered data,并不能说明一定是 shapes 未排序。

分别排序两张表,再重新筛选

用下面的代码替换原来只排序 shapes 的代码。两张表分别判断,避免 feed 没有 shapes 时跳过 stop_times 排序。

library(data.table)
library(gtfstools)

gtfs_list <- lapply(gtfs_list, function(gtfs) {
  if (!is.null(gtfs$shapes) && nrow(gtfs$shapes) > 0L) {
    shapes_dt <- copy(as.data.table(gtfs$shapes))
    setorder(shapes_dt, shape_id, shape_pt_sequence)
    gtfs$shapes <- shapes_dt
  }

  if (!is.null(gtfs$stop_times) && nrow(gtfs$stop_times) > 0L) {
    stop_times_dt <- copy(as.data.table(gtfs$stop_times))
    setorder(stop_times_dt, trip_id, stop_sequence)
    gtfs$stop_times <- stop_times_dt
  }

  gtfs
})

# 先确认 ATM 的 stop_times 几何能够生成
atm_trips_sf <- get_trip_geometry(
  gtfs_list[["atm"]],
  file = "stop_times"
)

# 再单独验证 ATM 的空间筛选
atm_stibm <- filter_by_spatial_extent(
  gtfs_list[["atm"]],
  stibm
)

copy() 用于避免 data.table 的引用修改影响其他仍在使用的表对象。还要确认 stop_sequence 和 shape_pt_sequence 的数值类型符合预期:字符类型可能按 "1"、"10"、"2" 的顺序排列。如果类型异常,先检查原值,不要强制转换后忽略新增的 NA。

确认 ATM 可以筛选后,再批量处理:

gtfs_stibm <- lapply(gtfs_list, function(gtfs) {
  filter_by_spatial_extent(gtfs, stibm)
})

直接遍历命名列表会保留运营商名称。筛选失败时会抛出错误,便于在合并前处理异常。

排序无效时继续检查什么?

如果排序后仍然报错,应保留完整报错并检查调用栈。在未被 tryCatch() 捕获的错误发生后,立即运行 traceback(),同时记录包版本:

traceback()

packageVersion("gtfstools")
packageVersion("sfheaders")
packageVersion("sf")

随后根据失败的路径检查数据:

  • stop_times 中是否缺少 trip_id、stop_id 或 stop_sequence,同一班次是否出现重复序号。
  • stop_times$stop_id 是否能关联到 stops$stop_id,相关经纬度是否缺失或异常。
  • 失败的 shape 或班次是否只有一个有效坐标点,无法形成正常线段。
  • 如果错误来自 convert_shapes_to_sf(),检查对应 shape 的标识、坐标和序号,而不只是整张表是否已经排序。

还要检查 sf::st_crs(stibm)。上述筛选函数的实现要求边界采用 WGS 84,即 EPSG

。源 CRS 已正确声明时,可以用 sf::st_transform(stibm, 4326) 转换;源 CRS 缺失时,应先查明坐标来源,不能靠指定 EPSG
猜测。CRS 检查源码

合并前需要注意的结果语义

原代码在筛选失败后返回完整的 gtfs_list[[name]],因此列表中会同时包含已筛选数据和未筛选的 ATM 数据。直接合并后,ATM 的区域外服务仍可能进入结果。排查时应让错误显式中断,或单独记录失败项,处理完成后再合并。

默认空间操作是 sf::st_intersects。筛选会保留与边界相交的 shapes、班次及关联记录,并不意味着线路和所有站点都被裁剪到边界内。如果需要严格限制输出范围,还要明确是保留相交的完整班次,还是只保留区域内站点和线路片段。空间操作定义

日志中的 S2Polyline::Shape with one vertex has no edges 出现在 Trenord 的处理阶段,应单独检查对应的退化几何;它不能作为 ATM 本次错误的直接依据。

备注:内容仅供参考。