2 votes

Joindre les plages qui se chevauchent de deux trames de données dans r

Note : Cette question a été fermée en tant que "doublon". Les solutions proposées ici y ici n'a pas répondu à ma question. Ils ont montré comment fusionner lorsqu'une entrée unique se situe dans une plage, j'essaie d'identifier les plages qui se chevauchent et de les joindre. Mon titre aurait peut-être pu être meilleur...

J'ai un ensemble de données principal main_df avec une heure de début et de fin (en secondes). Je voudrais voir si la plage de temps en main_df se situe dans une liste de plages dans lookup_df et, si c'est le cas, récupérer la valeur dans lookup_df . En outre, si le main_df se situe dans deux plages de recherche différentes, dupliquez la ligne pour que chaque valeur soit représentée. ***

main_df <- tibble(start = c(30,124,161),
                end = c(80,152,185))

lookup_df <- tibble(start = c(34,73,126,141,174,221),
                       end = c(69,123,136,157,189,267),
                       value = c('a','b','b','b','b','a'))

# Do something here to get the following:

> final_df
# A tibble: 4 x 4
  start   end value notes                                      
  <dbl> <dbl> <chr> <chr>                                      
1    30    80 a     ""                                         
2    30    80 b     "Duplicate because it falls within a and b"
3   124   152 b     "Falls within two lookups but both are b"  
4   161   185 b     ""      

***En regardant la façon dont j'ai structuré le problème...

#Not actual code
left_join(main_df, lookup_df, by(some_range_join_function) %>% 
  add_rows(through_some_means)

Plutôt que d'ajouter une nouvelle ligne, je pourrais inverser la façon dont je les joins...

semi_join(lookup_df, main_df, by(some_range_join_function))

1voto

jay.sf Points 8160

Vous pourriez faire des comparaisons logiques et ensuite gérer ce qui se passera si tous les éléments sont identiques. 'b' , 'a' y 'b' etc. De cette façon, vous pouvez facilement ajouter d'autres cas, par exemple, les deux sont 'a' l'un est 'a' d'autres sont 'b' que vous n'avez pas déclaré dans l'OP. L'approche donne NULL s'il n'y a pas de correspondance, ce qui est omis lors de l'opération rbind .

f <- \(x, y) {
  w <- which((x[1] >= y[, 1] & x[1] <= y[, 2]) | (x[2] >= y[, 1] & x[1] <= y[, 2]))
  if (length(w) > 0) {
    d <- data.frame(t(x), value=cbind(y[w, 3]), notes='')
    if (length(w) >= 2) {
      if (all(d$value == 'b')) {
        d <- d[!duplicated(d$value), ]
        d$notes[1] <- 'both b'
      }
      else {
        d$notes[nrow(d)] <- 'a & b'
      }
    }
    d
  }
}

apply(main_df, 1, f, lookup_df, simplify=F) |> do.call(what=rbind)
#   start end value  notes
# 1    30  80     a       
# 2    30  80     b  a & b
# 3   124 152     b both b
# 4   161 185     b     

Données :

main_df <- structure(list(start = c(2, 30, 124, 161), end = c(1, 80, 152, 
185)), row.names = c(NA, -4L), class = "data.frame")

lookup_df <- structure(list(start = c(34, 73, 126, 141, 174, 221), end = c(69, 
123, 136, 157, 189, 267), value = c("a", "b", "b", "b", "b", 
"a")), row.names = c(NA, -6L), class = "data.frame")

1voto

Maël Points 644

Une autre option est fuzzyjoin::interval_join :

library(fuzzyjoin)
library(dplyr)

interval_join(main_df, lookup_df, by = c("start", "end"), mode = "inner") %>% 
  group_by(value, start.x, end.x) %>% 
  slice(1) %>% 
  select(start = start.x, end = end.x, value)

# A tibble: 4 × 3
# Groups:   value, start, end [4]
  start   end value
  <dbl> <dbl> <chr>
1    30    80 a    
2    30    80 b    
3   124   152 b    
4   161   185 b

0voto

Merijn van Tilborg Points 1845

Vous pouvez utiliser foverlaps de data.table pour ça.

library(data.table)

setDT(main_df) # make it a data.table if needed
setDT(lookup_df) # make it a data.table if needed

setkey(main_df, start, end) # set the keys of 'y'

foverlaps(lookup_df, main_df, nomatch = NULL) # do the lookup

#    start end i.start i.end value
# 1:    30  80      34    69     a
# 2:    30  80      73   123     b
# 3:   124 152     126   136     b
# 4:   124 152     141   157     b
# 5:   161 185     174   189     b

Ou pour obtenir les résultats nettoyés comme résultat final (final_df du PO)

unique(foverlaps(lookup_df, main_df, nomatch = NULL)[, .(start, end, value)])

   start end value
1:    30  80     a
2:    30  80     b
3:   124 152     b
4:   161 185     b

0voto

Paul Smith Points 406

Une solution possible, basée sur powerjoin :

library(tidyverse)
library(powerjoin)

power_left_join(
  main_df, lookup_df,
  by = ~ (.x$start <= .y$start & .x$end >= .y$end) |
    (.x$start >= .y$start & .x$start <= .y$end) | 
    (.x$start <= .y$start & .x$end >= .y$start), 
  keep = "left") %>% 
  distinct()

#> # A tibble: 4 x 3
#>   start   end value
#>   <dbl> <dbl> <chr>
#> 1    30    80 a    
#> 2    30    80 b    
#> 3   124   152 b    
#> 4   161   185 b

Ou en utilisant tidyr::crossing :

library(tidyverse)

crossing(main_df, lookup_df,
        .name_repair = ~ c("start", "end", "start2", "end2", "value")) %>% 
  filter((start <= start2 & end >= end2) |
         (start >= start2 & start <= end2) | (start <= start2 & end >= start2)) %>% 
  select(-start2, -end2) %>% 
  distinct()

#> # A tibble: 4 x 3
#>   start   end value
#>   <dbl> <dbl> <chr>
#> 1    30    80 a    
#> 2    30    80 b    
#> 3   124   152 b    
#> 4   161   185 b

0voto

shs Points 960

Vous pouvez utiliser le fuzzyjoin à joindre sur la base d'intervalles avec le fuzzyjoin::interval_*_join() fonctions.

Je vais utiliser une jointure interne, car si vous utilisez une jointure semi comme vous le proposez, vous perdrez la colonne de valeur et n'obtiendrez que 3 lignes.

library(tidyverse)
library(fuzzyjoin)

fuzzyjoin::interval_inner_join(lookup_df, main_df, by = c("start", "end"), type = "any")
#> # A tibble: 5 × 5
#>   start.x end.x value start.y end.y
#>     <dbl> <dbl> <chr>   <dbl> <dbl>
#> 1      34    69 a          30    80
#> 2      73   123 b          30    80
#> 3     126   136 b         124   152
#> 4     141   157 b         124   152
#> 5     174   189 b         161   185

Comme vous pouvez le voir, le fuzzy_inner_join() préserve les colonnes by des deux tables, puisqu'elles ne sont pas les mêmes dans une jointure floue. De plus, nous avons toujours des lignes distinctes pour les cas dans la table main_df qui correspondent à plusieurs cas dans lookup_df . Ainsi, nous faisons un peu de nettoyage de la table jointe :

interval_inner_join(lookup_df, main_df, 
                    by = c("start", "end"), 
                    type = "any") |> 
  select(-ends_with(".x")) |> # remove lookup interval cols
  distinct() |> # remove duplicate
  rename_with(str_remove, ends_with(".y"), "\\.y") # remove suffixes from col names
#> # A tibble: 4 × 3
#>   value start   end
#>   <chr> <dbl> <dbl>
#> 1 a        30    80
#> 2 b        30    80
#> 3 b       124   152
#> 4 b       161   185

Enfin, une clarification de la terminologie : Dans votre question, vous dites que vous voulez faire une jointure basée sur l'intervalle de main_df tombant sur l'intervalle entre lookup_df . Cela est possible en utilisant type = "within" en interval_*_join() . Mais d'après les exemples que vous donnez, il semble que vous souhaitiez adhérer sur la base suivante tout chevauchement . Cela peut être fait avec type = "any" mais c'est la valeur par défaut, donc vous n'avez pas besoin de la spécifier.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X