如何计算多边形之间的所有成对交互以及 R 中 sf 的百分比覆盖率?

Posted

技术标签:

【中文标题】如何计算多边形之间的所有成对交互以及 R 中 sf 的百分比覆盖率?【英文标题】:How to compute all pairwise interaction between polygons and the the percentage coverage in R with sf? 【发布时间】:2021-12-28 17:26:49 【问题描述】:

我有多边形,我想计算它们之间的重叠百分比。这个想法是,当一个多边形与另一个多边形相交时,可以从一个多边形或另一个多边形的角度计算百分比(即最大值)。因此,我想制作一个脚本,生成多边形之间的百分比覆盖率,获取一个多边形和另一个多边形的重叠百分比,然后将所有结果放在一个数据框中。

这是我目前拥有的代码:

set.seed(131)
library(sf)
library(mapview)
m = rbind(c(0,0), c(1,0), c(1,1), c(0,1), c(0,0))
p = st_polygon(list(m))
n = 5
l = vector("list", n)
for (i in 1:n)
  l[[i]] = p + 2 * runif(2)
s = st_sfc(l)
s.f = st_sf(s)
s.f$id = c(1,1,2,2,3)
s.f.2 = s.f %>% group_by(id) %>%   summarise(geometry = sf::st_union(s)) # %>% summarise(area = st_area(s))

s.f.2$area = st_area(s.f.2)

i = s.f.2 %>% 
  st_intersection(.) %>% 
  mutate(intersect_area = st_area(.)) #%>%

st_intersection(s.f.2) %>% 
  mutate(intersect_area = st_area(.),
         # id.int = sapply(i$origins, function(x) paste0(as.character(hr.pol$id)[x], collapse = ", ")),
         id1 = sapply(i$origins, function(x) paste0(as.character(s.f.2$id)[x][1])),
         id2 = sapply(i$origins, function(x) paste0(as.character(s.f.2$id)[x][2])),
         area.id1 = sapply(i$origins, function(x) s.f.2$area[x][1]),
         area.id2 = sapply(i$origins, function(x) s.f.2$area[x][2]),
         perc1 = as.vector(intersect_area/area.id1),
         perc2 = as.vector(intersect_area/area.id2)) %>%   # create new column with shape area
  filter(n.overlaps ==2) %>% 
  dplyr::select(id, intersect_area, id1, id2, 
                # id.int, 
                perc1,perc2) %>%   # only select columns needed to merge
  st_drop_geometry() %>%  # drop geometry as we don't need it
  select(-id) %>% 
  pivot_longer(#names_prefix = "id", 
    names_to = "perc",
    cols = starts_with("perc"))

这段代码给出了多边形之间重叠的百分比(我只为 2 个重叠做,但如果这可以推广到多个重叠,那就太好了!)

mapview(s.f.2,zcol = "id")

最后,我正在寻找的是这样的:

id   `1`   `2`   `3`
1     100   31.6  0
2     27.0  100   0
3     0     0     100

所以多边形“1”占多边形“2”面积的 31.6%,多边形“2”占多边形“1”面积的 27.0%。

我目前拥有的是(但非常很慢):

data.sp = s.f.2 %>%  
  st_as_sf(.) %>%
  mutate(area.m =  st_area(geometry),
         area.ha = units::set_units(area.m, ha)) %>%
  select(-c(area,area.m))

id.sort = sort(unique(data.sp$id)) # used to reorder columns based on ID

df.fill =data.frame(id1 = NULL, id2=NULL, area =NULL, over1 = NULL, over2 = NULL)

for (k in 1:length(id.sort)) 
  for (op in 1:length(id.sort)) 
    int.out = st_intersection(data.sp[data.sp$id==id.sort[k],], 
                              data.sp[data.sp$id==id.sort[op],])
    # int.out
    if(nrow(int.out) != 0) 
      area.tmp = st_area(int.out)#/10000
      over1 = area.tmp/int.out$area.ha
      over2 = area.tmp/int.out$area.ha.1
     else area.tmp = 0;over1=0;over2=0
    
    df.fill.tmp = data.frame(id1 = id.sort[k], id2=id.sort[op], 
                             area = area.tmp,
                             over1 = over1*100,
                             over2 = over2*100)
    df.fill = rbind(df.fill,df.fill.tmp)
  

df.fill$over1 = as.numeric(df.fill$over1)
df.fill$over2 = as.numeric(df.fill$over2)
df.fill %>% 
  select(-c(area, over2)) %>% 
  pivot_wider(names_from = id2,values_from = over1, 
              values_fill = 0)

【问题讨论】:

嗨@M。博索莱伊。你的问题很有趣。我试图向您建议一个解决方案(请参阅下面的答案)。我希望它能满足你的需求。干杯。 【参考方案1】:

简单的问题,但不是一个明显的答案!我建议的解决方案遵循与您的策略略有不同的策略,并且不涉及任何 for 循环。首先,我开发了一个函数(即area_cover_()),它生成一个交叉表,其中只有那些至少有一个交叉点的多边形。然后,在第二步中,我开发了另一个函数(即add_isolated_poly()),它在第一步生成的交叉表的末尾添加了没有交集的多边形。如果您有许多没有交集的多边形,这将更容易阅读最终表格。所以,请在下面找到代表。

注意:reprex 的输入数据对应于您的 sf 对象 s.f.2area

Reprex

1.第一步: 创建一个交叉表,只包含至少有一个交点的多边形(不包括没有交叉点的多边形,这样读取交叉表的效率更高)。为此,我开发了函数area_cover()

area_cover() 函数的代码
library(sf)
library(dplyr)
library(tidyr)


area_cover <- function(x) 
  x %>% 
  st_intersection() %>% 
  filter(n.overlaps>1) %>%
  mutate(area_inter = st_area(.)) %>%  
  unnest(., cols = c(origins, geometry)) %>% 
  left_join(., as.data.frame(x), by = c("origins" = "id")) %>% 
  mutate(cover_percent = area_inter/area.y*100) %>% 
  select(.,origins, area.y, area_inter, cover_percent) %>% 
  rename("id" = "origins", "area" = "area.y") %>% 
  st_drop_geometry() %>%  
  group_by(area_inter) %>% 
  mutate(poly_X_id = rev(id)) %>% 
  relocate(poly_X_id, .before = area_inter) %>% 
  xtabs(cover_percent ~ id + poly_X_id, data = .) %>%  
  replace(.== 0, 100) %>% 
  round(., digits = 1)
  
area_cover() 函数的输出
Results <- area_cover(s.f.2)

Results      # Only polygons with at least one intersection are present in this cross table
#>    poly_X_id
#> id      1     2
#>   1 100.0  31.6
#>   2  27.0 100.0  

class(Results) 
#> [1] "xtabs" "table"  # you can convert 'Results' into matrix with 'as.matrix()' if needed.

2。第二步(可选): 在交叉表“结果”(即上一步的结果)的末尾添加孤立的多边形(即没有交叉点)。为此,我开发了函数add_isolated_poly(),它创建了一个数据框,其中的 n 列对应于孤立多边形的 n 个 id,并用 0 填充

add_isolated_poly() 函数的代码
add_isolated_poly <- function(y, z) # 'y arg.' = s.f.2 and 'z arg.' = result of the function area_cover()

id_isolated_poly <- setdiff(y$id, colnames(z))

df_isolated_poly <- y %>% 
  filter(.,id %in% id_isolated_poly) %>% 
  st_drop_geometry() %>% 
  select(., id) %>% 
  t() %>% 
  as.data.frame() %>% 
  `colnames<-`(., id_isolated_poly) %>% 
  rbind(.,rep(list(rep(0, nrow(y))), length(id_isolated_poly))) %>% 
  slice(-c(1))

cbind.fill <- function(...)
  nm <- list(...)
  nm <- lapply(nm, as.matrix)
  n <- max(sapply(nm, nrow))
  do.call(cbind, lapply(nm, function (x)
    rbind(x, matrix(0, n-nrow(x), ncol(x)))))


Results %>% 
  cbind.fill(., df_isolated_poly) %>%  
  replace(., col(.) == row(.), 100) %>%  
  `rownames<-`(., c(colnames(z), id_isolated_poly))

add_isolated_poly() 函数的输出
Results_2 <- add_isolated_poly(s.f.2, Results)

Results_2
#>     1     2   3
#> 1 100  31.6   0
#> 2  27 100.0   0
#> 3   0   0.0 100

class(Results_2)
#> [1] "matrix" "array

由reprex package (v2.0.1) 于 2021 年 11 月 19 日创建


重要编辑

虽然它们在建议的最小示例中产生了正确的结果,但我上面提出的两个函数不可推广并产生错误的结果。经过大量的试验和错误,这是一个简单,非常快速的功能......而且,这一次,对!所以,请在下面找到代表。

解决方案

area_cover() 函数的代码
library(sf)
library(dplyr)

area_cover <- function(x)
  Results <- x %>% 
    st_intersection(.,.) %>% 
    mutate(area_inter = st_area(.),
           cover = area_inter/area.1*100) %>% 
    st_drop_geometry() %>% 
    xtabs(cover ~ id.1 + id, data = ., sparse = TRUE) %>%  
    round(., digits = 1) %>% 
    as.matrix(.) 
  
  names(dimnames(Results)) <- NULL
  
  return(Results)

area_cover() 函数的输出
area_cover(s.f.2)

#>     1     2   3
#> 1 100  31.6   0
#> 2  27 100.0   0
#> 3   0   0.0 100

由reprex package (v2.0.1) 于 2021 年 11 月 22 日创建

基准测试

我根据@M 提供的新数据集将我的函数与@wibeasley 的经过验证的解决方案进行了比较。 Beausoleil(见下面的 cmets)。

为了使比较有效,我稍微修改了@wibeasley 的函数,以便输出是一个包含百分比的矩阵(即与我的函数相同的输出)

数据
set.seed(1234)
m = rbind(c(0,0), c(1,0), c(1,1), c(0,1), c(0,0))
p = st_polygon(list(m))
n = 100
l = vector("list", n)
for (i in 1:n)
  l[[i]] = p + 7 * runif(2)
s = st_sfc(l)
s.f = st_sf(s)
s.f$id = c(1,1,2,2,3,4,5,5,6,7,7,8,9,8,7,3,4,5,5,6)
s.f.2 = s.f %>% group_by(id) %>%   summarise(geometry = sf::st_union(s)) # %>% summarise(area = st_area(s))
s.f.2$area = st_area(s.f.2)
比较两种解决方案的代码
library(sf)
library(dplyr)
library(tidyr)
library(bench)

bench_results <- bench::mark(
  "wibeasley_validated_answer" =   #!!!!!  NB: lighlty modified function for the comparison to be valid!!!!!
    wibeasley <- sf::st_intersection(s.f.2, s.f.2) %>% 
      dplyr::mutate(
        area       = sf::st_area(.),
        proportion = area / area.1 * 100
      ) %>%
      tibble::as_tibble() %>%
      dplyr::select(
        id_1 = id,
        id_2 = id.1,
        proportion,
      ) %>% 
      # tidyr::complete(id_1, id_2, fill = list(proportion = 0))
      tidyr::pivot_wider(
        names_from = id_1,
        values_from = proportion,
        values_fill = 0
      ) %>% 
      as.matrix(.,rownames.force = TRUE) %>% 
      `<-`(., .[,-c(1)]) %>%  
      round(.,1)
    ,  
  "lovalery_answer" = 
    lovalery <- area_cover(s.f.2)
  ,
  min_iterations = 1000,
  relative = TRUE, 
  check = TRUE)
基准测试结果(相对值)
bench_results
#> # A tibble: 2 x 6
#>   expression                   min median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>                 <dbl>  <dbl>     <dbl>     <dbl>    <dbl>
#> 1 wibeasley_validated_answer  1.13   1.10      1          1       1.16
#> 2 lovalery_answer             1      1         1.10      24.1     1
基准测试结果(绝对值)
bench_results
#> # A tibble: 2 x 6
#>   expression                      min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>                 <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 wibeasley_validated_answer   46.4ms   49.1ms      20.2    2.54MB    1.11 
#> 2 lovalery_answer              41.5ms   43.7ms      22.7   61.09MB    0.969
最终检查两个函数是否产生相同的结果
all.equal(wibeasley, lovalery)
#> [1] TRUE

wibeasley
#>       1     2     3     4     5     6     7     8     9
#> 1 100.0  19.0  13.3   4.6  22.6  21.2  12.8   3.7  11.6
#> 2  18.3 100.0  28.7  31.9  33.0  14.3  32.2  25.1   5.1
#> 3  14.6  32.7 100.0  23.3  35.5   7.2  28.8   7.0  13.7
#> 4   5.1  36.4  23.3 100.0  20.3  23.7  26.2  26.8  14.7
#> 5  13.0  19.8  18.7  10.7 100.0  10.3  27.0  15.6  12.4
#> 6  21.3  15.0   6.6  21.8  18.0 100.0  26.9  24.4  15.8
#> 7   9.5  24.8  19.5  17.7  34.7  19.9 100.0  11.8  17.9
#> 8   3.8  26.8   6.6  25.1  27.7  24.9  16.3 100.0   1.3
#> 9  22.1  10.0  23.7  25.5  41.0  30.0  46.0   2.3 100.0

lovalery
#>       1     2     3     4     5     6     7     8     9
#> 1 100.0  19.0  13.3   4.6  22.6  21.2  12.8   3.7  11.6
#> 2  18.3 100.0  28.7  31.9  33.0  14.3  32.2  25.1   5.1
#> 3  14.6  32.7 100.0  23.3  35.5   7.2  28.8   7.0  13.7
#> 4   5.1  36.4  23.3 100.0  20.3  23.7  26.2  26.8  14.7
#> 5  13.0  19.8  18.7  10.7 100.0  10.3  27.0  15.6  12.4
#> 6  21.3  15.0   6.6  21.8  18.0 100.0  26.9  24.4  15.8
#> 7   9.5  24.8  19.5  17.7  34.7  19.9 100.0  11.8  17.9
#> 8   3.8  26.8   6.6  25.1  27.7  24.9  16.3 100.0   1.3
#> 9  22.1  10.0  23.7  25.5  41.0  30.0  46.0   2.3 100.0

由reprex package (v2.0.1) 于 2021 年 11 月 22 日创建

【讨论】:

嗨@wibeasley,我不是OP,但是,是的,我认为你误解了目标。所需的结果是您在评论中描述的结果:它对应于相交区域的面积除以每个多边形的总面积。最后,我认为您应该将您的评论放在 OP 的问题或您的答案下,而不是我的 ;-) 干杯。 确实,@lovalery。这种计算背后的想法是重叠区域可能意味着不同的东西,具体取决于所比较的多边形。例如,如果我有 2 只动物的领土(多边形)重叠。根据每只动物的领土大小,这两个领土之间重叠的面积有不同的百分比。因此,有趣的是看看一种动物和另一种动物覆盖了多少(也许一种动物完全重叠了另一种动物的领土,但第二种动物只重叠了另一种动物的一小部分。这有意义吗? 您好 Beausoleil 先生和@wibeasley,非常感谢您的反馈。说实话,我很难重现你得到的错误,因为在我的电脑上一切正常。也就是说,我想我已经找到了一个解决方法,让它在所有情况下都能顺利运行!我认为@wibeasley 的假设是正确的:问题在于replace_na() 函数需要一个向量或数据帧而不是矩阵,这是cbind.fill() 函数返回的。 所以我通过修改cbind.fill() 函数来编辑我上面的答案,然后不使用replace_na() 函数。请让我知道这现在是否也适合您。干杯。 (注意:我建议的解决方案应该快速运行(只要它有效!)) 现在为我工作。【参考方案2】:

如果没有一个真实的示例来进行基准测试,我不确定它是否比您的解决方案更快。但它更简单、更容易理解(至少对我的大脑而言)。

sf::st_intersection() 是矢量化的。所以它会为你找到并返回第一个和第二个参数的所有交集。在这种情况下,这两个参数是同一组多边形。

sf::st_intersection(s.f.2, s.f.2) %>% 
  dplyr::mutate(
    area       = sf::st_area(.),
    proportion = area / area.1
  ) %>%
  tibble::as_tibble() %>%
  dplyr::select(
    id_1 = id,
    id_2 = id.1,
    proportion,
  ) %>% 
  # tidyr::complete(id_1, id_2, fill = list(proportion = 0))
  tidyr::pivot_wider(
    names_from = id_1,
    values_from = proportion,
    values_fill = 0
  )

输出:

# A tibble: 3 x 4
   id_2   `1`   `2`   `3`
  <dbl> <dbl> <dbl> <dbl>
1     1 1     0.316     0
2     2 0.270 1         0
3     3 0     0         1

需要考虑的事项:

将面积保留为比例,而不是百分比。通常以后计算比较好。 待久一点,不要转身。通常以后计算会更好,因为您可以加入 id_1id_2。 如果您旋转宽度,您可能希望它作为一个矩阵,而不是一个 data.frame。

【讨论】:

我没有这方面的基准,但我可以与以前的基准进行比较,这样更快!谢谢!如果 area 列中有一些单位,请小心:由于某种原因,pivot_wider 不能使用单位...【参考方案3】:

我会优化一些东西。我不能真正评估只有三个多边形的优化,我猜交叉计算是唯一真正昂贵的部分,所以我会从那里开始。

如果您不计算 (1) 多边形 A 和 B,然后 (2) 多边形 B 和 A,您将立即减少约 50%。从某种意义上说,只计算 upper triangle,并且重用/反映 lower 三角形的值。


# I think you wanted to create 5 empty columns.  Use `numeric(0)` instead of `NULL`
df.fill=data.frame(id1=numeric(0), id2=numeric(0), area=numeric(0), over1=numeric(0), over2=numeric(0))

for (k in seq_along(id.sort)) 
  for (op in seq(from = k, to = length(id.sort), by = 1))  # Avoid the lower triangle
    int.out = st_intersection(
      data.sp[data.sp$id==id.sort[k],], 
      data.sp[data.sp$id==id.sort[op],]
    )
    
    if(nrow(int.out) != 0) 
      area.tmp = st_area(int.out)#/10000
      over1 = area.tmp/int.out$area.ha
      over2 = area.tmp/int.out$area.ha.1 
     else area.tmp = 0;over1=0;over2=0
    
    df.fill.tmp.upper = data.frame(id1 = id.sort[k], id2=id.sort[op], 
                                   area = area.tmp,
                                   over1 = over1,
                                   over2 = over2)
    df.fill.tmp.lower = data.frame(id1 = id.sort[op], id2=id.sort[k], 
                                   area = area.tmp,
                                   over1 = over2,
                                   over2 = over1)
    df.fill <- 
      if (k == op) rbind(df.fill, df.fill.tmp.upper)
      else         rbind(df.fill, df.fill.tmp.upper, df.fill.tmp.lower)
  

df.fill %>% 
  dplyr::mutate(
    over1 = as.numeric(over1) * 100
    over2 = as.numeric(over2) * 100
  ) %>%
  select(-area, -over2) %>% 
  pivot_wider(
    names_from = id2,
    values_from = over1, 
    values_fill = 0
  )

输出:

# A tibble: 3 x 4
    id1   `1`   `2`   `3`
  <dbl> <dbl> <dbl> <dbl>
1     1 100    31.6     0
2     2  27.0 100       0
3     3   0     0     100

【讨论】:

不要介意我在@lovalery 的解决方案上的cmets。我的生产相同的不对称矩阵。我把昨晚的事情搞糊涂了。谢谢你的耐心。交集只需要计算一次,除法有两次。一次由多边形 A,一次由多边形 B。【参考方案4】:

如果与其余代码相比,交集计算确实很昂贵,则此解决方案可能比我的第二个解决方案更快。与@lovalery 的解决方案一样,只有一个data.frame 被传递给sf:st_intersection()。根据我的解释,主要区别在于该解决方案会保持较长时间,并使用交叉连接来枚举所有可能的组合,并在最后一步中广泛使用。当事物长而不是宽时(对我来说)更容易操作。

intersections <-
  s.f.2 %>% 
  sf::st_intersection() %>% 
  dplyr::filter(2L <= n.overlaps) %>%
  dplyr::mutate(
    numerator = sf::st_area(.)
  ) %>%
  tibble::as_tibble() %>%
  tidyr::unnest_wider(origins) %>%
  dplyr::select(
    id_1 = `...1`, 
    id_2 = `...2`, 
    numerator
  )

denominators <- 
  s.f.2 %>% 
  tibble::as_tibble() %>% 
  dplyr::select(id, area) 
  
denominators %>% 
  dplyr::full_join(denominators, by = character()) %>% 
  dplyr::select(
    id_1 = id.x,
    id_2 = id.y,
    area = area.y
  ) %>% 
  dplyr::left_join(intersections, by = c("id_1" = "id_1",  "id_2" = "id_2")) %>%
  dplyr::left_join(intersections, by = c("id_1" = "id_2",  "id_2" = "id_1")) %>% 
  dplyr::mutate(
    numerator   = dplyr::coalesce(numerator.x, numerator.y, 0),
    proportion  = dplyr::if_else(id_1 == id_2, 1, numerator / area),
  ) %>% 
  dplyr::select(id_1, id_2, proportion) %>% 
  tidyr::pivot_wider(
    names_from  = id_1,
    values_from = proportion,
    values_fill = 0
  )

输出:

# A tibble: 3 × 4
   id_2   `1`   `2`   `3`
  <dbl> <dbl> <dbl> <dbl>
1     1 1     0.316     0
2     2 0.270 1         0
3     3 0     0         1

【讨论】:

以上是关于如何计算多边形之间的所有成对交互以及 R 中 sf 的百分比覆盖率?的主要内容,如果未能解决你的问题,请参考以下文章

如何计算二维的所有成对距离

将函数应用于 R 中列表元素的所有成对组合

如何在 Pyspark Dataframe 中创建多列的所有成对组合?

从 R sf 中的多边形中删除孔

计算 shapefile 中每个多边形之间的最大/(或最小)距离

将栅格裁剪为 sf 集合中的多边形 [R sf]