dplyr ::inner_join — как включить определенные наблюдения, даже если они не отображаются в обоих фреймах данных

#r #dplyr #merge #inner-join

#r #dplyr #слияние #внутреннее соединение

Вопрос:

У меня есть два фрейма данных, которые я хочу объединить с помощью общей переменной. Некоторые наблюдения отображаются в одних данных, но не в других, и наоборот. При объединении я хочу сохранить только те наблюдения, которые отображаются в обоих фреймах данных, и поэтому dplyr::inner_join() это уместно.

Однако есть исключение. Есть некоторые наблюдения, которые я хотел бы включить в объединенные данные, несмотря ни на что. То есть, даже если они не отображаются в обоих исходных фреймах данных. Указание на то, какие из них являются теми «особыми» наблюдениями, которые следует сохранить, задается в виде определенных значений в определенных столбцах.

Пример

Я хочу объединить следующие фреймы данных ( df_population и df_gdp )

1. df_population

 library(tibble)
library(dplyr)

## helper function
myFun <- function(n = 5000) {
  a <- do.call(paste0, replicate(5, sample(LETTERS, n, TRUE), FALSE))
  paste0(a, sprintf("d", sample(9999, n, TRUE)), sample(LETTERS, n, TRUE))
}

set.seed(2021)

df_population <-
  tribble(~country, ~population,
        "australia", 24.99,
        "united_kingdom", 66.65,
        "france", 66.99,
        "spain", 46.94,
        "canada", 37.59,
        "brazil", 209.5) %>%
  mutate(col_of_strings = c(myFun(5), "dont_leave_me_behind"))

## # A tibble: 6 x 3
##   country        population col_of_strings      
##   <chr>               <dbl> <chr>               
## 1 australia            25.0 GLNWN9968R          
## 2 united_kingdom       66.6 FTELH3426F          
## 3 france               67.0 NFOSZ6335V          
## 4 spain                46.9 ZFGRD8875F          
## 5 canada               37.6 GFICE2875O          
## 6 brazil              210.  dont_leave_me_behind
 

2. df_gdp

 df_gdp <-
  tribble(~country, ~gdp_growth,
        "australia", 2.9,
        "united_kingdom", 1.4,
        "france", 1.7,
        "spain", 2.4,
        "canada", 1.9,
        "greece", 1.9) %>%
  mutate(col_of_strings = sample(c(myFun(5), "dont_leave_me_behind")))


## # A tibble: 6 x 3
##   country        gdp_growth col_of_strings      
##   <chr>               <dbl> <chr>               
## 1 australia             2.9 dont_leave_me_behind
## 2 united_kingdom        1.4 RQHHI9679V          
## 3 france                1.7 PFSZX1552L          
## 4 spain                 2.4 BQTBY7537E          
## 5 canada                1.9 OECIK9698V          
## 6 greece                1.9 VXDQQ4718J 
 

Моя проблема

Обычно я бы выбрал

 dplyr::inner_join(df_population, df_gdp, by = "country")
 

Но:
Хотя мне нужны только страны, которые являются общими для обоих фреймов данных, я все же хочу включить любую страну, которая имеет col_of_strings == dont_leave_me_behind

Я надеюсь, что есть простое решение для этого. Спасибо!

Комментарии:

1. Я бы full_join() , а затем filter() результаты.

2. Возможно, вы могли бы обновить свои данные, чтобы включить строки, которые неправильно соединяются? В нынешнем виде ваши строки «dont_leave_me_behind» сохраняются в inner_join .

3. @M.Viking Это был бы один из способов. Однако учтите, что реальные наборы данных, с которыми я имею дело, более сложны и намного больше по размеру, чем здесь. Это full_join() будет довольно неэффективно (с точки зрения вычислений).

4. Является country ли поле уникальным идентификатором?

5. Приятно, что вы не используете set.seed(2020) … многие предположили, что это ужасная отправная точка любого случайного процесса 😉

Ответ №1:

На ум приходят три способа.

  1. Согласно предложению @M.Viking, full_join сначала, а затем фильтруйте.
     dplyr::full_join(df_population, df_gdp, by = "country") %>%
      dplyr::filter(
        col_of_strings.y == "dont_leave_me_behind" | !is.na(col_of_strings.x),
        col_of_strings.x == "dont_leave_me_behind" | !is.na(col_of_strings.y)
      )
    # # A tibble: 6 x 5
    #   country        population col_of_strings.x     gdp_growth col_of_strings.y    
    #   <chr>               <dbl> <chr>                     <dbl> <chr>               
    # 1 australia            25.0 LQMPB3662R                  2.9 VKBCE2969H          
    # 2 united_kingdom       66.6 WDXVX4684T                  1.4 FMAKF4470M          
    # 3 france               67.0 VJHBH0078U                  1.7 dont_leave_me_behind
    # 4 spain                46.9 XFJPD7687T                  2.4 RMPYK2467U          
    # 5 canada               37.6 AQRCR0724P                  1.9 JXMMZ3736X          
    # 6 brazil              210.  dont_leave_me_behind       NA   <NA>                
     
  2. Выполните внутреннее объединение, извлеките недостающие строки из каждого фрейма и bind_rows верните их обратно. Эти шаги требуют некоторого переименования из-за .x / .y names после объединения.
     tmp1 <- dplyr::inner_join(df_population, df_gdp, by = "country")
    
    missing_pop <- df_population %>%
      dplyr::filter(
        col_of_strings == "dont_leave_me_behind",
        !country %in% tmp1$country
      ) %>%
      dplyr::rename(col_of_strings.x = col_of_strings)
    missing_pop
    # # A tibble: 1 x 3
    #   country population col_of_strings.x    
    #   <chr>        <dbl> <chr>               
    # 1 brazil        210. dont_leave_me_behind
    
    missing_gdp <- df_gdp %>%
      dplyr::filter(
        col_of_strings == "dont_leave_me_behind",
        !country %in% tmp1$country
      ) %>%
      dplyr::rename(col_of_strings.y = col_of_strings)
    missing_gdp
    # # A tibble: 0 x 3
    # # ... with 3 variables: country <chr>, gdp_growth <dbl>, col_of_strings.y <chr>
    
    out <- dplyr::bind_rows(tmp1, missing_pop, missing_gdp)
    out
    # # A tibble: 6 x 5
    #   country        population col_of_strings.x     gdp_growth col_of_strings.y    
    #   <chr>               <dbl> <chr>                     <dbl> <chr>               
    # 1 australia            25.0 LQMPB3662R                  2.9 VKBCE2969H          
    # 2 united_kingdom       66.6 WDXVX4684T                  1.4 FMAKF4470M          
    # 3 france               67.0 VJHBH0078U                  1.7 dont_leave_me_behind
    # 4 spain                46.9 XFJPD7687T                  2.4 RMPYK2467U          
    # 5 canada               37.6 AQRCR0724P                  1.9 JXMMZ3736X          
    # 6 brazil              210.  dont_leave_me_behind       NA   <NA>                
     
  3. Аналогично 2, но с использованием anti_join :
     tmp1 <- dplyr::inner_join(df_population, df_gdp, by = "country")
    out <- dplyr::bind_rows(
      tmp1,
      dplyr::filter(df_population, col_of_strings == "dont_leave_me_behind") %>%
        dplyr::anti_join(., tmp1, by = "country") %>%
        dplyr::rename(col_of_strings.x = col_of_strings),
      dplyr::filter(df_gdp, col_of_strings == "dont_leave_me_behind") %>%
        anti_join(., tmp1, by = "country") %>%
        dplyr::rename(col_of_strings.y = col_of_strings)
    )
     

Последние два выполняют примерно то же самое с бенчмаркингом:

 bench::mark(full1=..., inner2=..., inner3=...)
# # A tibble: 3 x 13
#   expression     min  median `itr/sec` mem_alloc `gc/sec` n_itr  n_gc total_time result memory
#   <bch:expr> <bch:t> <bch:t>     <dbl> <bch:byt>    <dbl> <int> <dbl>   <bch:tm> <list> <list>
# 1 full1       5.83ms  7.72ms     127.     1.36KB     4.32    59     2      463ms <tibb~ <Rpro~
# 2 inner2      9.54ms 11.46ms      84.9   11.28KB     2.07    41     1      483ms <tibb~ <Rpro~
# 3 inner3     13.95ms 14.92ms      62.9   11.28KB     4.49    28     2      445ms <tibb~ <Rpro~
# # ... with 2 more variables: time <list>, gc <list>
 

full_join в этом случае работает намного лучше. Большие данные могут работать существенно по-разному, я еще не тестировал это.