Rで非等価結合 (2)

dplyrが1.1.0になった。これにより、Rで非等価結合 - もうカツ丼はいいよなで少し触れていたdplyrによる非等価結合が正式にサポートされた。

記事で取り上げていた例なら次のように簡潔な記述で結合できるようになった。

# データの準備
library(dplyr)
car <- tibble(
  car_model = c("CarA", "CarB", "CarC"),
  car_price = c(20000, 30000, 50000)
)
boat <- tibble(
  boat_model = c("Boat1", "Boat2", "Boat3"),
  boat_price = c(10000, 40000, 60000)
)

# car_price > boat_priceを条件とした非等価結合
car %>%
  left_join(
    boat,
    by = join_by(car_price > boat_price)
  )

## # A tibble: 4 × 4
##   car_model car_price boat_model boat_price
##   <chr>         <dbl> <chr>           <dbl>
## 1 CarA          20000 Boat1           10000
## 2 CarB          30000 Boat1           10000
## 3 CarC          50000 Boat1           10000
## 4 CarC          50000 Boat2           40000

また、上記のような単純な不等号を使った非等価結合以外にrolling join、overlap joinができるようになっている。これによっておそらく時系列データなどがかなり扱いやすくなるのではないかと思われる。

Rolling join

これは不等号を使った非等価結合であるが、結合の結果を最も「近い」1件に限定する(タイが存在すると1件になるとは限らないが、その場合の挙動は後述)。

例のデータならCarCに対応する部分の結合結果が1行に減る。

car %>% 
  left_join(
    boat,
    by = join_by(closest(car_price > boat_price)) # 条件をclosest()で囲む
  )

## # A tibble: 3 × 4
##   car_model car_price boat_model boat_price
##   <chr>         <dbl> <chr>           <dbl>
## 1 CarA          20000 Boat1           10000
## 2 CarB          30000 Boat1           10000
## 3 CarC          50000 Boat2           40000

Overlap join

結合条件に範囲を使うもの。連続値を離散値に変換するような場合に便利だろう。

price_class <- tibble(
  class = c("A", "B", "C"),
  lower = c(10000, 20000, 50000),
  upper = c(19999, 49999, 100000)
)
car %>% 
  left_join(
    price_class,
    by = join_by(between(car_price, lower, upper)) # 条件をbetween()で記述する
  )

## # A tibble: 3 × 5
##   car_model car_price class lower  upper
##   <chr>         <dbl> <chr> <dbl>  <dbl>
## 1 CarA          20000 B     20000  49999
## 2 CarB          30000 B     20000  49999
## 3 CarC          50000 C     50000 100000

Overlap joinは条件に指定した変数の値次第では複数の行にマッチしてしまい、行が増える可能性がある。それが意図しないものだった場合は困るので、複数行にマッチするケースが存在したらエラーを返すようにすることもできる。

price_class <- tibble(
  class = c("A", "B", "C"),
  lower = c(10000, 20000, 50000),
  upper = c(20000, 50000, 100000) # CarAとCarCが複数のclassに属する
)
car %>% 
  left_join(
    price_class,
    by = join_by(between(car_price, lower, upper)),
    multiple = "error" # 複数のマッチがある場合にエラーを返すようにする。
  )

## Error in `left_join()`:
## ! Each row in `x` must match at most 1 row in `y`.
## ℹ Row 1 of `x` matches multiple rows.

複数行マッチ時のwarning

等価結合の場合とrolling joinの場合、左辺のテーブルの特定の行に右辺のテーブルの複数行がマッチして結果の行が増えてしまうことがあり得るが、それは一般的に意図しないものであろう、ということでそういう場合に警告が出るようになった。

df1 <- tibble(id = c(1, 2, 3), x = c("a", "b", "c"))
df2 <- tibble(id = c(1, 1, 2, 3), y = c("w", "x", "y", "z"))
left_join(df1, df2, by = "id")

## Warning in left_join(df1, df2, by = "id"): Each row in `x` is expected to match at most 1 row in `y`.
## ℹ Row 1 of `x` matches multiple rows.
## ℹ If multiple matches are expected, set `multiple = "all"` to silence this
##   warning.

## # A tibble: 4 × 3
##      id x     y    
##   <dbl> <chr> <chr>
## 1     1 a     w    
## 2     1 a     x    
## 3     2 b     y    
## 4     3 c     z

ちょっと気を利かせすぎな気もするが、実際このようなケースは意図せず生じた場合に検出が難しく間違いにつながることがしばしばある。私も何度かハマった経験がある。なのでデフォルトで警告が出るくらいのが良いのだろう。

警告は先程も使用した引数multiple"all"を指定することでoffにできる。

left_join(df1, df2, by = "id", multiple = "all")

Cross join (交差結合)

他に結合関係の変更として、今まではleft_join(x, y, by = character())というあまり直感的ではない書き方をする必要があった交差結合がcross_join()によってできるようになった(by = character()を指定して交差結合を行うと、cross_join()を使うよう警告が表示される)。

cross_join(car, boat)

## # A tibble: 9 × 4
##   car_model car_price boat_model boat_price
##   <chr>         <dbl> <chr>           <dbl>
## 1 CarA          20000 Boat1           10000
## 2 CarA          20000 Boat2           40000
## 3 CarA          20000 Boat3           60000
## 4 CarB          30000 Boat1           10000
## 5 CarB          30000 Boat2           40000
## 6 CarB          30000 Boat3           60000
## 7 CarC          50000 Boat1           10000
## 8 CarC          50000 Boat2           40000
## 9 CarC          50000 Boat3           60000

その他

  • dplyr 1.1.0での変更点はおそらくこの記事dplyr 1.1.0 is coming soonが現状だとよくまとまっている。一般的にはsummarise().byによる一時的なグループ化ができるようになったことあたりが役立つのではないかと思う。
  • dbplyr 2.3.0においてはまだjoin_by()はサポートされていない。Pull request(Support `join_by()` by mgirlich · Pull Request #1074 · tidyverse/dbplyr · GitHub)が上がっているのでそのうち対応されるだろうと思う。備えよう。