もう、周知のネタ感はあるけど、dplyr
でグループ単位にデータフレームを操作できるのは便利だなと思うので、いくつか例をあげてみる。
summarize
, mutate
, filter
などの関数の.by
引数を使うと、グループごとに計算ができて非常に便利。
group_by
関数でも同じことができるけど、使い方をミスると思わぬ挙動に繋がることもある。
このあたりのことをいくつか例に出してみる。
.by
によるグループ単位の操作例
summarizeでグループ単位の集計
これが一番よく使われる例かなと思う。たとえば、ペンギンさんの体型に関するデータの平均値を、を種や島ごとに集計する。
palmerpenguins::penguins |>
dplyr::summarize(
dplyr::across(dplyr::where(is.numeric), function(x) mean(x, na.rm = TRUE)),
.by = c(species, island)
)
#> # A tibble: 5 × 7
#> species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
#> <fct> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 Adelie Torgersen 39.0 18.4 191. 3706.
#> 2 Adelie Biscoe 39.0 18.4 189. 3710.
#> 3 Adelie Dream 38.5 18.3 190. 3688.
#> 4 Gentoo Biscoe 47.5 15.0 217. 5076.
#> 5 Chinstrap Dream 48.8 18.4 196. 3733.
#> # ℹ 1 more variable: year <dbl>
mutateでグループ単位の計算
データの偏差を求めたい場合、summarize
した結果を元のデータフレームと比較するのは一手。
しかし、mutate
にも.by
引数があり、これを使うとグループ単位の計算ができるので、偏差を一発で出せる。
palmerpenguins::penguins |>
dplyr::mutate(
dplyr::across(
dplyr::where(is.numeric),
function(x) x - mean(x, na.rm = TRUE)
),
.by = c(species, island)
)
#> # A tibble: 344 × 8
#> species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
#> <fct> <fct> <dbl> <dbl> <dbl> <dbl>
#> 1 Adelie Torgersen 0.149 0.271 -10.2 43.6
#> 2 Adelie Torgersen 0.549 -1.03 -5.20 93.6
#> 3 Adelie Torgersen 1.35 -0.429 3.80 -456.
#> 4 Adelie Torgersen NA NA NA NA
#> 5 Adelie Torgersen -2.25 0.871 1.80 -256.
#> 6 Adelie Torgersen 0.349 2.17 -1.20 -56.4
#> 7 Adelie Torgersen -0.0510 -0.629 -10.2 -81.4
#> 8 Adelie Torgersen 0.249 1.17 3.80 969.
#> 9 Adelie Torgersen -4.85 -0.329 1.80 -231.
#> 10 Adelie Torgersen 3.05 1.77 -1.20 544.
#> # ℹ 334 more rows
#> # ℹ 2 more variables: sex <fct>, year <dbl>
filterでグループ単位の行選択
同じ発想で、filter
もグループ単位で実行できる。たとえば、グループごとに中央値以上の体重を持つペンギンのデータだけを抽出なんてことができる。
palmerpenguins::penguins |>
dplyr::filter(
body_mass_g >= median(body_mass_g, na.rm = TRUE),
.by = c(species, island)
)
#> # A tibble: 180 × 8
#> species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
#> <fct> <fct> <dbl> <dbl> <int> <int>
#> 1 Adelie Torgersen 39.1 18.7 181 3750
#> 2 Adelie Torgersen 39.5 17.4 186 3800
#> 3 Adelie Torgersen 39.2 19.6 195 4675
#> 4 Adelie Torgersen 42 20.2 190 4250
#> 5 Adelie Torgersen 37.8 17.3 180 3700
#> 6 Adelie Torgersen 38.6 21.2 191 3800
#> 7 Adelie Torgersen 34.6 21.1 198 4400
#> 8 Adelie Torgersen 36.6 17.8 185 3700
#> 9 Adelie Torgersen 42.5 20.7 197 4500
#> 10 Adelie Torgersen 46 21.5 194 4200
#> # ℹ 170 more rows
#> # ℹ 2 more variables: sex <fct>, year <int>
group_by
してみる
今は、.by
という引数を使えるようになったが、昔は group_by
を使っていた。最後にungroup
しておかないと事故のもとで、一癖あるものの、同じグループに対して複数の操作をしたい時に便利だ。たとえば先程までの例の、偏差を求める操作と、体重が中央値以上のデータを抽出する操作を同時に行うならこんな感じ。
palmerpenguins::penguins |>
dplyr::group_by(species, island) |>
dplyr::mutate(
dplyr::across(
dplyr::where(is.numeric),
function(x) x - mean(x, na.rm = TRUE),
.names = "deviation_{.col}"
)
) |>
dplyr::filter(body_mass_g >= median(body_mass_g, na.rm = TRUE)) |>
dplyr::ungroup()
#> # A tibble: 180 × 13
#> species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
#> <fct> <fct> <dbl> <dbl> <int> <int>
#> 1 Adelie Torgersen 39.1 18.7 181 3750
#> 2 Adelie Torgersen 39.5 17.4 186 3800
#> 3 Adelie Torgersen 39.2 19.6 195 4675
#> 4 Adelie Torgersen 42 20.2 190 4250
#> 5 Adelie Torgersen 37.8 17.3 180 3700
#> 6 Adelie Torgersen 38.6 21.2 191 3800
#> 7 Adelie Torgersen 34.6 21.1 198 4400
#> 8 Adelie Torgersen 36.6 17.8 185 3700
#> 9 Adelie Torgersen 42.5 20.7 197 4500
#> 10 Adelie Torgersen 46 21.5 194 4200
#> # ℹ 170 more rows
#> # ℹ 7 more variables: sex <fct>, year <int>, deviation_bill_length_mm <dbl>,
#> # deviation_bill_depth_mm <dbl>, deviation_flipper_length_mm <dbl>,
#> # deviation_body_mass_g <dbl>, deviation_year <dbl>
ちなみにgroup_by
したままのデータを表示すると、# Groups: species[3]
のようにグループ分けされたデータフレームであることが分かる。
palmerpenguins::penguins |>
dplyr::group_by(species, island)
#> # A tibble: 344 × 8
#> # Groups: species, island [5]
#> species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
#> <fct> <fct> <dbl> <dbl> <int> <int>
#> 1 Adelie Torgersen 39.1 18.7 181 3750
#> 2 Adelie Torgersen 39.5 17.4 186 3800
#> 3 Adelie Torgersen 40.3 18 195 3250
#> 4 Adelie Torgersen NA NA NA NA
#> 5 Adelie Torgersen 36.7 19.3 193 3450
#> 6 Adelie Torgersen 39.3 20.6 190 3650
#> 7 Adelie Torgersen 38.9 17.8 181 3625
#> 8 Adelie Torgersen 39.2 19.6 195 4675
#> 9 Adelie Torgersen 34.1 18.1 193 3475
#> 10 Adelie Torgersen 42 20.2 190 4250
#> # ℹ 334 more rows
#> # ℹ 2 more variables: sex <fct>, year <int>
ungroup
忘れに起因する想定外の挙動の例として、purrr::map
でプログレスバーを出したいのだけれど、なぜか出ないという相談を最近受けた。最小構成は以下のような感じ
data.frame(x = 1:2) |>
dplyr::group_by(x) |> # ここをコメントアウトするとプログレスバーが見れる
dplyr::mutate(y = purrr::map(x, \(...) Sys.sleep(1), .progress = TRUE))
#> # A tibble: 2 × 2
#> # Groups: x [2]
#> x y
#> <int> <list>
#> 1 1 <NULL>
#> 2 2 <NULL>
group_by
をコメントアウトする(あるいはungroup
しておく)と、プログレスバーが出る。
purrr::map(.progress = TRUE)
のプログレスバーは処理に一定以上の時間がかかる場合でかつ、2つ以上の要素を処理する場合に発生するようだ。
先の例だと、group_by
のせいで、1行ごとにグループを作ってしまっていた。つまり、実質的には行数が1のデータフレームごとにmap
をかけてから、結合したような処理だ。すると、.progress = TRUE
を指定していても、要素数が1しかないため、プログレスバーが出なかったというわけ。
イメージとしては以下のような感じ。
list(data.frame(x = 1), data.frame(x = 2)) |>
purrr::map(
\(df) {
dplyr::mutate(
df,
y = purrr::map(x, \(...) Sys.sleep(1), .progress = TRUE)
)
}
) |>
dplyr::bind_rows() |>
tibble::as_tibble()
#> # A tibble: 2 × 2
#> x y
#> <dbl> <list>
#> 1 1 <NULL>
#> 2 2 <NULL>