dplyr

dplyr速習

dplyrパッケージは主にデータフレームを対象としたデータ加工のための関数を提供します。データ加工のための操作にはいくつかの種類がありますが、dplyrパッケージではこれらの操作を個別の関数として提供しています。dplyrパッケージの関数を組み合わせて利用することで、データ加工の処理を効率的に行うことができます。

dplyrが対象とするデータ操作の種類としては、次のようなものがあります。

  • データの選択・絞り込み… select(), filter()
  • データの並べ替え… arrange()
  • データの加工… mutate()
  • データの集約… summarise(), count()
  • データの結合… *_join(), bind_*()

dplyrはこのほか、データフレーム全体ではなく、データの値、すなわちベクトルに対する操作を行うcase_when()関数、if_else()関数、na_if()関数など、いくつも備わっています。

データ操作を行う上でdplyrパッケージの関数は直感的にわかりやすいため、dplyrの関数をデータベース上のデータに適用したり、data.tableクラスのオブジェクトに対して適用可能にするパッケージも用意されています。

データの用意

dplyrパッケージを使って行う処理を説明するために、南極に生育するペンギン個体の各部位の大きさについてのデータを利用します。このデータ(penguins)は次のパッケージの読み込みと同時に利用できるようになります。

library(palmerpenguins)
penguins |> 
  slice_head(n = 6)
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
Adelie Torgersen 39.1 18.7 181 3750 male 2007
Adelie Torgersen 39.5 17.4 186 3800 female 2007
Adelie Torgersen 40.3 18.0 195 3250 female 2007
Adelie Torgersen NA NA NA NA NA 2007
Adelie Torgersen 36.7 19.3 193 3450 female 2007
Adelie Torgersen 39.3 20.6 190 3650 male 2007

ペンギンデータに対して、以降の処理の説明を簡略化するために、データの件数と変数を減らします。

set.seed(123)
penguins_small <-
  penguins |>
  slice_sample(n = 6) |>
  select(species, island, starts_with("bill"), sex)

penguins_small
species island bill_length_mm bill_depth_mm sex
Gentoo Biscoe 44.5 14.3 NA
Adelie Torgersen 38.6 21.2 male
Gentoo Biscoe 45.3 13.7 female
Chinstrap Dream 52.8 20.0 male
Adelie Torgersen 37.3 20.5 male
Chinstrap Dream 43.2 16.6 female

それではdplyrの関数を使って、このデータを加工していきましょう。

複数の列への処理

across

共通の処理を複数の列へ適用する場合、次のように変数 = 関数(変数)の関係を個別に指定する書き方は効率が悪いです。across()関数を使うことで、このような複数列への一括処理が容易に行えるようになります。

penguins_small |> 
  # stringr::str_to_upper()はアルファベット文字列を大文字に変換します
  mutate(species = str_to_upper(species),
            sex = str_to_upper(sex),
         .keep = "none")
species sex
GENTOO NA
ADELIE MALE
GENTOO FEMALE
CHINSTRAP MALE
ADELIE MALE
CHINSTRAP FEMALE

across()は関数を適用する列を.cols引数で指定し、適用する関数名を.fnsに渡して実行します。 次のコードはstr_to_upper()関数を2つの変数に個別に実行していたものをacross()関数を使った書き方に直したものです。

penguins_small |> 
  mutate(across(.cols = c("species", "sex"), 
                   .fns = str_to_upper),
         .keep = "none")
species sex
GENTOO NA
ADELIE MALE
GENTOO FEMALE
CHINSTRAP MALE
ADELIE MALE
CHINSTRAP FEMALE

結果は出力しませんが、以下のように書くこともできます。

penguins_small |> 
  mutate(across(c(species, sex), 
                   str_to_upper),
         .keep = "none")
penguins_small |> 
  mutate(across(c(species, sex), 
                   ~ str_to_upper(.x)),
         .keep = "none")
penguins_small |> 
  mutate(across(c(species, sex), 
                   function(x) str_to_upper(x)),
         .keep = "none")
penguins_small |> 
  mutate(across(c(species, sex), 
                   \(x) str_to_upper(x)),
         .keep = "none")

*_all(), *_at(), *_if() で終わる関数を使って複数列への処理が可能ですが、これらの関数は2023年8月現在 superseded扱いとなっています。

penguins_small |> 
  transmute_at(c("species", "sex"), 
               .funs = str_to_upper)
species sex
GENTOO NA
ADELIE MALE
GENTOO FEMALE
CHINSTRAP MALE
ADELIE MALE
CHINSTRAP FEMALE

この書き方に慣れている人は、関数を指定する引数名がacross()関数と異なる点に注意が必要です。 具体的にはacross(.fns = )で関数を指定することになります。

また、*_all(), *_at(), *_if()で利用可能だった、処理関数の第一引数以外の引数の指定方法はacross()関数では利用できません。 このあと説明する、across()関数での引数の指定方法を覚えましょう。

penguins_small |> 
  transmute_at(c("species", "sex"), 
               .funs = str_sub, start = 1, end = 3)
species sex
Gen NA
Ade mal
Gen fem
Chi mal
Ade mal
Chi fem

across()関数では、処理を実行する関数(上記の例ではstr_to_upper()関数)の第一引数に.cols引数で指定した変数の値が渡されます。値以外や、第一引数以外に値を渡す必要がある場合などは、次のような書き方で渡される値を明示的に指定します。 )

# species及びsexの値は .x としてstr_sub()の第一引数に渡される
penguins_small |> 
  mutate(across(c(species, sex), ~ str_sub(.x, 1, 2)), .keep = "none")
species sex
Ge NA
Ad ma
Ge fe
Ch ma
Ad ma
Ch fe
# species及びsexの値は 無名関数の引数xとしてstr_sub()の第一引数に渡される
penguins_small |> 
  mutate(across(c(species, sex), function(x) str_sub(x, 1, 2)), .keep = "none")
species sex
Ge NA
Ad ma
Ge fe
Ch ma
Ad ma
Ch fe
# R 4.1.0から利用可能な無名関数の表記も使える
penguins_small |> 
  mutate(across(c(species, sex), \(x) str_sub(x, 1, 2)), .keep = "none")
species sex
Ge NA
Ad ma
Ge fe
Ch ma
Ad ma
Ch fe
penguins_small |> 
  mutate(across(c(species, sex), list(~ str_sub(.x, 1, 2))), .keep = "none")
species_1 sex_1
Ge NA
Ad ma
Ge fe
Ch ma
Ad ma
Ch fe

across()関数を使った複数列への処理では、通常、加工後の列名は元の列名と一致します。 例えば、ペンギンデータのspeciesごとに数値変数の平均値を求める以下の処理とその結果を見てみましょう。

penguins_small |> 
  group_by(species) |> 
  summarise(across(where(is.numeric), \(x) mean(x, na.rm = TRUE)))
species bill_length_mm bill_depth_mm
Adelie 37.95 20.85
Chinstrap 48.00 18.30
Gentoo 44.90 14.00

この挙動が問題となることがあります。それは操作対象の変数に複数の関数を適用する場合です。 次の例はペンギンデータのspeciesごとに、数値変数の平均値と標準偏差を求める処理ですが、計算された平均値が元の変数名に格納されるため、標準偏差の計算結果を正しく求めることができなくなっています。

penguins_small |> 
  group_by(species) |> 
  summarise(across(where(is.numeric), \(x) mean(x, na.rm = TRUE)),
            across(where(is.numeric), \(x) sd(x, na.rm = TRUE)))
species bill_length_mm bill_depth_mm
Adelie NA NA
Chinstrap NA NA
Gentoo NA NA

こうした場合、across()関数の.names引数が役に立ちます。 以下の例では、平均値を求めるacross()関数と標準偏差を求めるacross()関数それぞれに.names引数を指定しています。注目してほしいのは"{.col}_"の部分です。ここにはテンプレートとして元の変数名が使われます。その他、任意の文字列を新たに用意される変数名として利用することができます。

penguins_small |> 
  group_by(species) |> 
  summarise(across(where(is.numeric), \(x) mean(x, na.rm = TRUE), .names = "{.col}_mean"),
            across(where(is.numeric), \(x) sd(x, na.rm = TRUE), .names = "{.col}_sd"))
species bill_length_mm_mean bill_depth_mm_mean bill_length_mm_sd bill_depth_mm_sd bill_length_mm_mean_sd bill_depth_mm_mean_sd
Adelie 37.95 20.85 0.9192388 0.4949747 NA NA
Chinstrap 48.00 18.30 6.7882251 2.4041631 NA NA
Gentoo 44.90 14.00 0.5656854 0.4242641 NA NA
penguins_small |> 
  group_by(species) |> 
  summarise(across(where(is.numeric), 
                   list(mean = ~ mean(.x, na.rm = TRUE), 
                        sd = ~ sd(.x, na.rm = TRUE))))
species bill_length_mm_mean bill_length_mm_sd bill_depth_mm_mean bill_depth_mm_sd
Adelie 37.95 0.9192388 20.85 0.4949747
Chinstrap 48.00 6.7882251 18.30 2.4041631
Gentoo 44.90 0.5656854 14.00 0.4242641
penguins_small |> 
  group_by(species) |> 
  summarise(across(where(is.numeric), \(x) mean(x, na.rm = TRUE), 
                   .names = "{.col}_mean"))
species bill_length_mm_mean bill_depth_mm_mean
Adelie 37.95 20.85
Chinstrap 48.00 18.30
Gentoo 44.90 14.00

tidyselect::where

列の選択を行うselect()関数にも、複数列への処理として、mutate_if()関数やsummarise_at()関数と同様にselect_if()関数等が利用できました。対象の列が一致する条件式を選択する、というもので、次のような記法を実行するのでした。

# 因子型の列を選択
penguins_small |> 
  select_if(is.factor)
species island sex
Gentoo Biscoe NA
Adelie Torgersen male
Gentoo Biscoe female
Chinstrap Dream male
Adelie Torgersen male
Chinstrap Dream female

ところがselect_if()select_at()関数はやはりsuperseded扱いとなっています。代わりに、where()関数を使った条件式での列の指定が可能となっています。

penguins_small |> 
  select(where(is.factor))
species island sex
Gentoo Biscoe NA
Adelie Torgersen male
Gentoo Biscoe female
Chinstrap Dream male
Adelie Torgersen male
Chinstrap Dream female

rename_with

# すべての列を対象に、列名に対してstr_to_upper()関数が適用される
penguins_small |> 
  rename_with(str_to_upper) |> 
  colnames()
[1] "SPECIES"        "ISLAND"         "BILL_LENGTH_MM" "BILL_DEPTH_MM" 
[5] "SEX"           
# 列名に対してstr_to_upper()関数が適用されるが、適用範囲を因子型の列だけに制限する
penguins_small |> 
  rename_with(str_to_upper, where(is.factor)) |> 
  colnames()
[1] "SPECIES"        "ISLAND"         "bill_length_mm" "bill_depth_mm" 
[5] "SEX"           

グループ

先ほど、「種ごとに処理を適用する」例を示しましたが、ここで使われた種、すなわちグループの指定方法に変更があります。

penguins_small |> 
  group_by(species) |> 
  summarise(n = n())
species n
Adelie 2
Chinstrap 2
Gentoo 2

従来は、処理を行う前にgroup_by()関数で明示的にグループ対象の変数を指定する必要がありました。しかし現在は処理を行うmutate()summarise()関数の中で.by引数によってグループ対象の変数を指定可能です。

penguins_small |> 
  summarise(n = n(), .by = species)
species n
Gentoo 2
Adelie 2
Chinstrap 2
penguins_small |> 
  # speciesごとにbill_length_mmの最大値を計算した値が格納される
  mutate(bill_length_max = max(bill_length_mm, na.rm = TRUE), 
         .by = species) |> 
  # speciesごとにデータを1件ずつ取り出す
  # bill_lenght_maxの値がspeciesごとに異なることがわかる
  slice(1, .by = species)
species island bill_length_mm bill_depth_mm sex bill_length_max
Gentoo Biscoe 44.5 14.3 NA 45.3
Adelie Torgersen 38.6 21.2 male 38.6
Chinstrap Dream 52.8 20.0 male 52.8
penguins_small |> 
  reframe(n = n(), .by = species)
species n
Gentoo 2
Adelie 2
Chinstrap 2

また、複数のグループを指定した後の処理でグループを維持し続けるかを.groups引数で選択することが可能です。

# group_by()で与えた最初のグループは解除されるが、以降のグループは維持される
# グループ化の処理に対して、どのような振る舞いをするかを.groups引数で指定しないと
# 警告文が出力される
penguins_small |> 
  group_by(species, island, sex) |> 
  summarise(mean_bl = mean(bill_length_mm, na.rm = TRUE))
`summarise()` has grouped output by 'species', 'island'. You can override using
the `.groups` argument.
species island sex mean_bl
Adelie Torgersen male 37.95
Chinstrap Dream female 43.20
Chinstrap Dream male 52.80
Gentoo Biscoe female 45.30
Gentoo Biscoe NA 44.50

.groups引数に”drop”を与えた場合、すべてのグループが解除されます。一方、グループを継続するには”keep”を与えます。

penguins_small |> 
  group_by(species, island, sex) |> 
  summarise(mean_bl = mean(bill_length_mm, na.rm = TRUE), 
            .groups = "drop")
species island sex mean_bl
Adelie Torgersen male 37.95
Chinstrap Dream female 43.20
Chinstrap Dream male 52.80
Gentoo Biscoe female 45.30
Gentoo Biscoe NA 44.50
penguins_small |> 
  group_by(species, island, sex) |> 
  summarise(mean_bl = mean(bill_length_mm, na.rm = TRUE), 
            .groups = "keep")
species island sex mean_bl
Adelie Torgersen male 37.95
Chinstrap Dream female 43.20
Chinstrap Dream male 52.80
Gentoo Biscoe female 45.30
Gentoo Biscoe NA 44.50

データ結合

dplyrパッケージには次の表に示す通り、柔軟なデータ結合関数が提供されています。

関数名 説明
inner_join() キーとして指定した変数から、二つのデータフレームに共通して存在するレコードを結合して返す
left_join() キーとして指定した変数から、左(第一引数)のデータフレームに存在するレコードを結合して返す
right_join() キーとして指定した変数から、右(第二引数)のデータフレームに存在するレコードを結合して返す
full_join() キーとして指定した変数から、二つのデータフレームのいずれかに存在するレコードを結合して返す
semi_join() (絞り込み)
anti_join() キーとして指定した変数から、左(第一引数)のデータフレームに存在しないレコードを結合して返す
cross_join() 二つのデータフレームのすべての組み合わせを結合して返す

実行は以下のように、対象のデータのほか、キーとなる変数をby引数で指定します。

inner_join(データ1, データ2, by = "キーとなる変数")

このby引数の指定方法として、join_by()関数を利用することもできます。この関数により、より柔軟な結合条件の指定が実現します。

penguins_ja <-
 tibble(
  species = c("Adelie", "Gentoo", "Chinstrap"),
  name = c("アデリーペンギン", "ジェンツーペンギン", "ヒゲペンギン"),
  redlist = c("NT", "LC", NA_character_))
penguins_small |> 
  distinct(species, island) |>
  left_join(penguins_ja, by = join_by(species))
species island name redlist
Gentoo Biscoe ジェンツーペンギン LC
Adelie Torgersen アデリーペンギン NT
Chinstrap Dream ヒゲペンギン NA
penguins_small |> 
  distinct(species, island) |>
  left_join(penguins_ja, by = c("species"))
species island name redlist
Gentoo Biscoe ジェンツーペンギン LC
Adelie Torgersen アデリーペンギン NT
Chinstrap Dream ヒゲペンギン NA
penguins_ja2 <-
 tibble(
  name = c("Adelie", "Gentoo", "Chinstrap"),
  jp_name = c("アデリーペンギン", "ジェンツーペンギン", "ヒゲペンギン"),
  redlist = c("NT", "LC", NA_character_))
penguins_small |> 
  distinct(species, island) |>
  left_join(penguins_ja2, by = join_by(species == name))
species island jp_name redlist
Gentoo Biscoe ジェンツーペンギン LC
Adelie Torgersen アデリーペンギン NT
Chinstrap Dream ヒゲペンギン NA
penguins_small |> 
  distinct(species, island) |>
  left_join(penguins_ja2, by = c("species" = "name"))
species island jp_name redlist
Gentoo Biscoe ジェンツーペンギン LC
Adelie Torgersen アデリーペンギン NT
Chinstrap Dream ヒゲペンギン NA
penguins_name <-
 tibble(
  name = c("adeliae", "Gentoo", "antarctica"),
  redlist = c("NT", "LC", NA_character_))

キー変数の項目が一致しないものがあるとエラーを発生させます。

penguins_small |> 
  distinct(species, island) |>
  left_join(penguins_name, by = join_by(species == name), unmatched = "error")
Error in `left_join()`:
! Each row of `y` must be matched by `x`.
ℹ Row 1 of `y` was not matched.

キー変数の項目が一致しないものがあると、そのレコードを除外します。これはデフォルトの挙動です。

penguins_small |> 
  distinct(species, island) |>
  left_join(penguins_name, by = join_by(species == name), unmatched = "drop")
species island redlist
Gentoo Biscoe LC
Adelie Torgersen NA
Chinstrap Dream NA

もう一つのオプション

df_pgid <-
  tibble(id = seq.int(4),
       species = c("Adelie", "Chinstrap", "Gentoo", "Chinstrap"))
       
df_pgid
id species
1 Adelie
2 Chinstrap
3 Gentoo
4 Chinstrap
df_pgname <-
tibble(
      species = c("Adelie", "Chinstrap", "Chinstrap"),
      name = c("アデリーペンギン", "ヒゲペンギン", "ナンキョクペンギン"))

df_pgname
species name
Adelie アデリーペンギン
Chinstrap ヒゲペンギン
Chinstrap ナンキョクペンギン
# multiple = "all"の状態
df_pgid |> 
  left_join(
    df_pgname,
    by = join_by(species))
Warning in left_join(df_pgid, df_pgname, by = join_by(species)): Detected an unexpected many-to-many relationship between `x` and `y`.
ℹ Row 2 of `x` matches multiple rows in `y`.
ℹ Row 2 of `y` matches multiple rows in `x`.
ℹ If a many-to-many relationship is expected, set `relationship =
  "many-to-many"` to silence this warning.
id species name
1 Adelie アデリーペンギン
2 Chinstrap ヒゲペンギン
2 Chinstrap ナンキョクペンギン
3 Gentoo NA
4 Chinstrap ヒゲペンギン
4 Chinstrap ナンキョクペンギン

結合するデータの件数が多い場合、問題がどこかに混ざる危険があります。 新しい*_join()関数では、このような問題を回避するために、結合先のレコードが複数ある場合、デフォルトではエラーを返却します。データや目的に応じて、ユーザー自身が複数の値への対処法を決定することができます。

# 結合先が複数ある状態で、最初のレコードのみを結合する
# multiple = "last" にすると最後のレコードのみを結合する
df_pgid |> 
  left_join(
    df_pgname,
    by = join_by(species),
    multiple = "first")
id species name
1 Adelie アデリーペンギン
2 Chinstrap ヒゲペンギン
3 Gentoo NA
4 Chinstrap ヒゲペンギン
df_pgid |> 
  left_join(
    df_pgname,
    by = join_by(species),
    multiple = "all",
    relationship = "many-to-many")
id species name
1 Adelie アデリーペンギン
2 Chinstrap ヒゲペンギン
2 Chinstrap ナンキョクペンギン
3 Gentoo NA
4 Chinstrap ヒゲペンギン
4 Chinstrap ナンキョクペンギン

非等価結合、ローリング結合等はスキップ…

penguins_name <-
  tibble(
    name = c("Adelie", "Gentoo", "Chinstrap"),
    preview_n = c(160, 121, 60),
    redlist = c("NT", "LC", NA_character_))

# Adelieはn >= preview_nの条件に合わないので結合から除外される
penguins_small |> 
  count(species) |> 
  left_join(penguins_name, by = join_by(species == name, n >= preview_n))
species n preview_n redlist
Adelie 2 NA NA
Chinstrap 2 NA NA
Gentoo 2 NA NA