为 R 中的空间线生成邻居列表对象

Generate Neighbour List object for spatial lines in R

我的目标是生成道路网络中空间线之间邻域关系的对象。如果我的数据是空间多边形,我可以使用 spdep::poly2nb 来执行此操作,但我无法弄清楚如何对空间线执行此操作。

在下面的 reprex 中,我尝试使用 igraph::as_adjacency_matrix 创建邻接矩阵,然后使用 spdep::mat2listw 将其转换为邻居列表对象。但这是正确的方法吗?

有了邻居列表后,我还想使用 road_id 属性进行标记。

library(sfnetworks)
library(sf)
library(spdep)
library(igraph)

net <- as_sfnetwork(roxel, directed = FALSE) %>% 
  activate("edges") %>% 
  mutate(road_id = row_number()+1000)

# Make adjacency matrix
B_net <- igraph::as_adjacency_matrix(net, edges = TRUE, attr = names)

# Make neighbour list
nb_net <- mat2listw(B_net)$neighbours
# Can't use row.names in mat2listw because how do I set row.names in igraph::as_adjacency_matrix

编辑:在此 sfnetworks 问题中使用方法的新方法 https://github.com/luukvdmeer/sfnetworks/issues/10 给出了邻居列表。

net_sf <- st_as_sf(net)

net_neigh <- st_touches(net_sf)

# define ad-hoc function to translate sgbp into nb (as documented in 
# https://r-spatial.github.io/spdep/articles/nb_sf.html#creating-neighbours-using-sf-objects)
as.nb.sgbp <- function(x) {
  attrs <- attributes(x)
  x <- lapply(x, function(i) { if(length(i) == 0L) 0L else i } )
  attributes(x) <- attrs
  class(x) <- "nb"
  x
}

net_nb <- as.nb.sgbp(net_neigh)
net_lw <- nb2listw(net_nb)

已编辑以显示此 sfnetworks 问题的解决方案 https://github.com/luukvdmeer/sfnetworks/issues/10

IMO 有几种方法可以为类似于 spdep::poly2nb 的输出的空间线创建邻域列表对象。不幸的是,还有一些假设(类似于 spdep::poly2nb 的 queen 参数)我将在这里尝试解释。

首先我们加载一些包和数据

library(sf)
#> Linking to GEOS 3.8.0, GDAL 3.0.4, PROJ 6.3.1
library(sfnetworks)
library(igraph)
#> 
#> Attaching package: 'igraph'
#> The following objects are masked from 'package:stats':
#> 
#>     decompose, spectrum
#> The following object is masked from 'package:base':
#> 
#>     union

第一个解决方案基于sf::st_touches函数:

roxel_touches <- stplanr::geo_projected(roxel, st_touches)
roxel_touches
#> Sparse geometry binary predicate list of length 851, where the predicate was `touches'
#> first 10 elements:
#>  1: 101, 146, 493, 577, 742
#>  2: 149, 285, 294, 354, 521
#>  3: 268, 269, 270, 376, 431
#>  4: 8, 9, 276, 735
#>  5: 8, 94, 95, 108, 590
#>  6: 272, 728, 732
#>  7: 274, 275, 276, 734
#>  8: 4, 5, 95, 108, 735
#>  9: 4, 276, 281, 728
#>  10: 273, 281, 729, 733

输出是一个 sgbp 对象,它根据 touches 谓词总结了空间线之间的关系。例如,输出的第一行表示 roxel 对象的第一行触及第 101、146、493、577 和 742 行。stplanr::geo_projected 函数用于应用几何二元谓词函数,无需重新投影数据(否则我们会收到一些警告消息)。

touches 谓词在相应的帮助页面 (?sf::st_touches) 和 here 中定义。简而言之,sf::st_touches 函数匹配共享一个公共点的 LINESTRING 几何图形,当该点位于它们边界的并集(即线的第一个和最后一个点)时。例如,下面的线彼此相交但不相交,因为公共点不在它们的边界内。

es_1 <- st_sfc(
  st_linestring(rbind(c(0, -1), c(0, 1))), 
  st_linestring(rbind(c(-1, 0), c(1, 0)))
)
par(mar = rep(1, 4))
plot(es_1, col = c("red", "blue"), lwd = 2)
legend("topright", legend = c(1, 2), col = c("red", "blue"), lty = 1, lwd = 2)

st_intersects(es_1)
#> Sparse geometry binary predicate list of length 2, where the predicate was `intersects'
#>  1: 1, 2
#>  2: 1, 2
st_touches(es_1)
#> Sparse geometry binary predicate list of length 2, where the predicate was `touches'
#>  1: (empty)
#>  2: (empty)

另一个略有不同的解决方案是使用 sf::st_relate 函数创建的:

roxel_relate <- stplanr::geo_projected(roxel, st_relate, pattern = "FF*FT****")
roxel_relate
#> Sparse geometry binary predicate list of length 851, where the predicate was `relate_pattern'
#> first 10 elements:
#>  1: 101, 146, 493, 577, 742
#>  2: 149, 285, 294, 354, 521
#>  3: 268, 269, 270, 376, 431
#>  4: 8, 9, 276, 735
#>  5: 8, 94, 95, 108, 590
#>  6: 272, 732
#>  7: 274, 275, 276, 734
#>  8: 4, 5, 95, 108, 735
#>  9: 4, 276, 281, 728
#>  10: 273, 281, 729, 733

输出(再次)是一个 sgbp 对象,它根据具有模式 "FF*FT****" 的 "relate" 谓词总结空间线之间的关系。当且仅当它们在边界上共享至少一个点,它们的内部不相交并且内部和边界之间没有共享点时,才使用这种类型的模式来匹配 LINESTRINGS。例如,以下两行很相关,但根据 "FF*FT****".

模式它们不相关
es_2 <- st_sfc(
  st_linestring(rbind(c(0, -1), c(0, 1)), "red"), 
  st_linestring(rbind(c(0, 0), c(1, 0)), "blue"), 
  crs = 32632
)
plot(es_2, col = c("red", "blue"), lwd = 2)
legend(x = "topright", legend = c(1, 2), lty = 1, col = c("red", "blue"), lwd = 2)

st_touches(es_2)
#> Sparse geometry binary predicate list of length 2, where the predicate was `touches'
#>  1: 2
#>  2: 1
st_relate(es_2, pattern = "FF*FT****")
#> Sparse geometry binary predicate list of length 2, where the predicate was `relate_pattern'
#>  1: (empty)
#>  2: (empty)

您可以在 sf::st_relate 的帮助页面中查看有关 sf::st_relate 函数以及如何构建模式的更多详细信息。当我们比较两个谓词的输出时,我们可以理解为什么 st_touchesst_relate 之间的区别很重要。

roxel_touches
#> Sparse geometry binary predicate list of length 851, where the predicate was `touches'
#> first 10 elements:
#>  1: 101, 146, 493, 577, 742
#>  2: 149, 285, 294, 354, 521
#>  3: 268, 269, 270, 376, 431
#>  4: 8, 9, 276, 735
#>  5: 8, 94, 95, 108, 590
#>  6: 272, 728, 732
#>  7: 274, 275, 276, 734
#>  8: 4, 5, 95, 108, 735
#>  9: 4, 276, 281, 728
#>  10: 273, 281, 729, 733
roxel_relate
#> Sparse geometry binary predicate list of length 851, where the predicate was `relate_pattern'
#> first 10 elements:
#>  1: 101, 146, 493, 577, 742
#>  2: 149, 285, 294, 354, 521
#>  3: 268, 269, 270, 376, 431
#>  4: 8, 9, 276, 735
#>  5: 8, 94, 95, 108, 590
#>  6: 272, 732
#>  7: 274, 275, 276, 734
#>  8: 4, 5, 95, 108, 735
#>  9: 4, 276, 281, 728
#>  10: 273, 281, 729, 733

我们可以看到它们几乎相同,但有少数情况(例如,参见第 6 行)。如果我们绘制相应的线,我们可以更好地理解这种情况:

plot(st_geometry(roxel[c(6, 272, 728, 732), ]), col = c("red", "orange", "blue", "darkgreen"), lwd = 2)
legend(x = "topright", legend = c(6, 272, 728, 732), lty = 1, col = c("red", "orange", "blue", "darkgreen"), lwd = 2)

我们可以看到第 6 行触及所有其他行,但根据模式 "FF*FT****" 它不是 "related" 到第 728 行。这意味着如果您从 st_relate 开始创建邻居列表,那么您会(至少)缺少一个现有的 link。我明确选择了这种模式,因为 sfnetworks 用于从 sf 对象推断图形结构的默认算法与那种模式非常相似(可能相同)。我们可以检查这个事实,创建与 as_sfnetwork:

创建的图形关联的 line graph
roxel_sfnetworks <- as_sfnetwork(roxel, directed = FALSE)
roxel_line_graph <- make_line_graph(roxel_sfnetworks)
roxel_adj_list <- as_adj_list(roxel_line_graph)
str(roxel_adj_list[1:10], give.attr = FALSE)
#> List of 10
#>  $ : 'igraph.vs' int [1:5] 101 146 493 577 742
#>  $ : 'igraph.vs' int [1:5] 149 285 294 354 521
#>  $ : 'igraph.vs' int [1:5] 268 269 270 376 431
#>  $ : 'igraph.vs' int [1:4] 8 9 276 735
#>  $ : 'igraph.vs' int [1:5] 8 94 95 108 590
#>  $ : 'igraph.vs' int [1:2] 272 732
#>  $ : 'igraph.vs' int [1:4] 274 275 276 734
#>  $ : 'igraph.vs' int [1:5] 4 5 95 108 735
#>  $ : 'igraph.vs' int [1:4] 4 276 281 728
#>  $ : 'igraph.vs' int [1:4] 273 281 729 733

并测试它们是否相等:

all(unlist(mapply(function(x, y) unique(x) == unique(y), roxel_adj_list, roxel_relate)))
#> [1] TRUE

我必须使用 unique 函数,因为当两个顶点之间存在多条边时,igraph 输出可能 return 重复索引,即

is.simple(roxel_sfnetworks)
#> [1] FALSE

无论如何,我们可以看到输出与 roxel_relate 相同,这意味着我们不能总是安全地使用 st_relatemake_line_graph 函数,除非对底层街道网络进行特别考虑目的。我不想在这里添加太多细节,因为例子太复杂了,但我们不能总是使用 st_touches 函数来推断邻居列表对象(但是如果你想阅读更多关于这个topic 我最近就这个话题写了一篇paper,目前正在审核中)。

无论如何,那篇论文的总结是我认为我们可以安全地使用 st_touchesst_relate 函数来生成邻居列表,只有在使用 stplanr::rnet_breakup_vertices函数。 (实际上这两种方法之间仍然存在一些非常细微的差异,但我仍然需要正确地弄清楚如何解决它们。目前我会使用st_touches)。

# 1 - Transform the input roxel object
roxel2 <- stplanr::rnet_breakup_vertices(roxel)
#> Splitting rnet object at the intersection points between nodes and internal vertexes

# 2- Apply st_touches function
roxel2_touches <- stplanr::geo_projected(roxel2, st_touches)
roxel2_touches
#> Sparse geometry binary predicate list of length 876, where the predicate was `touches'
#> first 10 elements:
#>  1: 105, 150, 504, 591, 765
#>  2: 153, 291, 300, 363, 533
#>  3: 274, 275, 276, 386, 441
#>  4: 8, 9, 282, 758
#>  5: 8, 98, 99, 112, 604
#>  6: 278, 750, 751, 755
#>  7: 280, 281, 282, 757
#>  8: 4, 5, 99, 112, 758
#>  9: 4, 282, 287, 750
#>  10: 279, 287, 752, 756

然后,如果需要,可以将结果转换为 nb 对象:

as.nb.sgbp <- function(x) {
  attrs <- attributes(x)
  x <- lapply(x, function(i) { if(length(i) == 0L) 0L else i } )
  attributes(x) <- attrs
  class(x) <- "nb"
  x
}

roxel2_nb <- as.nb.sgbp(roxel2_touches)
roxel2_lw <- spdep::nb2listw(roxel2_nb)
roxel2_lw
#> Characteristics of weights list object:
#> Neighbour list object:
#> Number of regions: 876 
#> Number of nonzero links: 3318 
#> Percentage nonzero weights: 0.4323826 
#> Average number of links: 3.787671 
#> 
#> Weights style: W 
#> Weights constants summary:
#>     n     nn  S0       S1       S2
#> W 876 767376 876 494.4606 3580.933

我们也可以使用"line-graph"选项:

str(as_adj_list(make_line_graph(as_sfnetwork(roxel2, directed = FALSE)))[1:10], give.attr = FALSE)
#> List of 10
#>  $ : 'igraph.vs' int [1:5] 105 150 504 591 765
#>  $ : 'igraph.vs' int [1:5] 153 291 300 363 533
#>  $ : 'igraph.vs' int [1:5] 274 275 276 386 441
#>  $ : 'igraph.vs' int [1:4] 8 9 282 758
#>  $ : 'igraph.vs' int [1:5] 8 98 99 112 604
#>  $ : 'igraph.vs' int [1:4] 278 750 751 755
#>  $ : 'igraph.vs' int [1:4] 280 281 282 757
#>  $ : 'igraph.vs' int [1:5] 4 5 99 112 758
#>  $ : 'igraph.vs' int [1:4] 4 282 287 750
#>  $ : 'igraph.vs' int [1:4] 279 287 752 756

reprex package (v0.3.0)

创建于 2020-06-01

我知道我写了很多(可能是不必要的)信息,但我认为它们对于正确提供有关建议解决方案的上下文很有用。如果不清楚,请添加更多问题。