ねこすたっと

ねこの気持ちと統計について悩む筆者の備忘録的ページ。

across関数を使ってデータフレームの列に対して一括で操作する(tidyverseパッケージ)[R]

以前、データフレームの列(=変数)に対して、追加・名前変更・要約などの操作方法についてまとめました。

necostat.hatenablog.jp

necostat.hatenablog.jp

最近は列の操作でacross関数を使うことが多くなってきたので、ここらへんで学び直しておこうと思います。

必要なパッケージとデータの準備

まずはtidyverseパッケージを読み込みます。

library(tidyverse)

今回はggplotでお馴染みのdiamondsデータを使います。diamondsデータに馴染みがなくても、データセットであることがすぐにわかるように、データ名はdataに変更しておきます。

> data <- diamonds
> data
# A tibble: 53,940 × 10
   carat cut       color clarity depth table price     x     y     z
   <dbl> <ord>     <ord> <ord>   <dbl> <dbl> <int> <dbl> <dbl> <dbl>
 1  0.23 Ideal     E     SI2      61.5    55   326  3.95  3.98  2.43
 2  0.21 Premium   E     SI1      59.8    61   326  3.89  3.84  2.31
 3  0.23 Good      E     VS1      56.9    65   327  4.05  4.07  2.31
 4  0.29 Premium   I     VS2      62.4    58   334  4.2   4.23  2.63
 5  0.31 Good      J     SI2      63.3    58   335  4.34  4.35  2.75
 6  0.24 Very Good J     VVS2     62.8    57   336  3.94  3.96  2.48
 7  0.24 Very Good I     VVS1     62.3    57   336  3.95  3.98  2.47
 8  0.26 Very Good H     SI1      61.9    55   337  4.07  4.11  2.53
 9  0.22 Fair      E     VS2      65.1    61   337  3.87  3.78  2.49
10  0.23 Very Good H     VS1      59.4    61   338  4     4.05  2.39
# … with 53,930 more rows

across関数で操作の対象となる変数を指定する

across関数に条件を渡すことで、操作の対象となる変数を指定することができます。across( )は単体では使えません。列に対する操作を行うsummarise( )やmutate( )と合わせて使います。

across( )では次のような引数を指定します。

  • .cols:操作の対象としたい変数。後述のように色々な方法で条件指定可能。デフォルトは.cols = everthing( )、つまり全ての変数。
  • .fns:適用したい関数。
    • = mean:単に関数名を与えるだけでもいい
    • = ~ mean(.x, na.rm = TRUE):ラムダ式で書いてもいい
    • = list(mean = mean, n_miss = ~ sum(is.na(.x)):関数のリストも受け付けてくれる
  • .names:出力される列の名前の付け方。対象とした列名{.col}や適用した関数{.fn}を使って動的に命名できます。

実例は後ほど。

変数名で条件指定する

select( )の使い方でも説明しましたが、変数名の文字列パターンで指定することができます。

  • starts_with( ):変数名が〜で始まるもの
  • ends_with( ):変数名が〜で終わるもの
  • contains( ):変数名に〜

例えば、across(starts_with("a"))とすれば、変数名が"a"から始まる列を対象にすることができます。 検査値を示すものには"lab_"と接頭辞をつけるなど、上手く変数命名のルールを決めると便利ですね。

変数の型で条件指定する

数値型と文字列型では適用できる関数が異なるので、変数の型で条件指定できると便利です。 例えば、across(where(is.numeric))とすれば、数値型の変数のみを対象にすることができます。

複数の条件を組み合わせて指定する

例えば、across(where(is.numeric) & !starts_with("a"))のように、複数の条件を組み合わせて変数を指定することも可能です。この例だと「数値型で、かつ先頭が"a"で始まらない変数」を指定していることになります。

across( )をsummarise( )と一緒に使う

across( )を使わずに、x,y,zを平均値で要約しようとすると、

data %>% summarise(mean_x = mean(x, rm.na=TRUE),
                   mean_y = mean(y, rm.na=TRUE),
                   mean_z = mean(z, rm.na=TRUE))

のように、要約したい変数の個数だけ書き下さないといけなかった。

across( )を使えば、

fun <- fuction(x){
data %>% summarize(across(x:z, ~mean(.x, na.rm=TRUE), .names="mean_{.col}"))

とするだけで、以下の同じ結果が得られる。((ここでは対象となる変数をx:zのように、データフレームでの列の範囲として指定したが、列がどの順序で並んでいるかはちゃんとわからないのであまり使わないかも))

# A tibble: 1 × 3
  mean_x mean_y mean_z
   <dbl>  <dbl>  <dbl>
1   5.73   5.73   3.54

across( )をmutate( )と一緒に使う

summarise( )では要約した値を別のデータフレームとして表示しましたが、元のデータフレームに新たな変数として追加したいときはmutate( )を使います。

例えば、変数x,y,zの平均と標準偏差を、新たな変数mean_x, sd_x, mean_y,...として追加する場合は、

mean_sd <- list(
  mean = ~mean(.x, na.rm = TRUE), 
  sd = ~sd(.x, na.rm = TRUE)
)
data %>% select(x:z) %>%
  mutate(across(x:z, mean_sd, .names="{.fn}_{.col}"))

と書きます。*1 ここでは平均と標準偏差を求める関数をラムダ式で定義し、mean_sdという名前のリストとしてまとめて適用しています。

# A tibble: 53,940 × 9
       x     y     z mean_x  sd_x mean_y  sd_y mean_z  sd_z
   <dbl> <dbl> <dbl>  <dbl> <dbl>  <dbl> <dbl>  <dbl> <dbl>
 1  3.95  3.98  2.43   5.73  1.12   5.73  1.14   3.54 0.706
 2  3.89  3.84  2.31   5.73  1.12   5.73  1.14   3.54 0.706
 3  4.05  4.07  2.31   5.73  1.12   5.73  1.14   3.54 0.706
 4  4.2   4.23  2.63   5.73  1.12   5.73  1.14   3.54 0.706
 5  4.34  4.35  2.75   5.73  1.12   5.73  1.14   3.54 0.706
 6  3.94  3.96  2.48   5.73  1.12   5.73  1.14   3.54 0.706
 7  3.95  3.98  2.47   5.73  1.12   5.73  1.14   3.54 0.706
 8  4.07  4.11  2.53   5.73  1.12   5.73  1.14   3.54 0.706
 9  3.87  3.78  2.49   5.73  1.12   5.73  1.14   3.54 0.706
10  4     4.05  2.39   5.73  1.12   5.73  1.14   3.54 0.706
# … with 53,930 more rows

当たり前ですが、平均・標準偏差は全ての観測値に共通の値が与えられていますね。

もう少し実用的に、例えばcutのグループ毎の平均・標準偏差を追加したい場合は次のようにgroup_by( )を挟んでやります(mean_sdは先程の例と同じです)。

data %>% select(cut,x:z) %>%
  group_by(cut) %>%
  mutate(across(x:z, mean_sd, .names="{.fn}_{.col}"))

出力です。次はcutの水準毎に異なった平均・標準偏差が追加されました。

# A tibble: 53,940 × 10
# Groups:   cut [5]
   cut           x     y     z mean_x  sd_x mean_y  sd_y mean_z  sd_z
   <ord>     <dbl> <dbl> <dbl>  <dbl> <dbl>  <dbl> <dbl>  <dbl> <dbl>
 1 Ideal      3.95  3.98  2.43   5.51 1.06    5.52 1.07    3.40 0.658
 2 Premium    3.89  3.84  2.31   5.97 1.19    5.94 1.26    3.65 0.731
 3 Good       4.05  4.07  2.31   5.84 1.06    5.85 1.05    3.64 0.655
 4 Premium    4.2   4.23  2.63   5.97 1.19    5.94 1.26    3.65 0.731
 5 Good       4.34  4.35  2.75   5.84 1.06    5.85 1.05    3.64 0.655
 6 Very Good  3.94  3.96  2.48   5.74 1.10    5.77 1.10    3.56 0.730
 7 Very Good  3.95  3.98  2.47   5.74 1.10    5.77 1.10    3.56 0.730
 8 Very Good  4.07  4.11  2.53   5.74 1.10    5.77 1.10    3.56 0.730
 9 Fair       3.87  3.78  2.49   6.25 0.964   6.18 0.956   3.98 0.652
10 Very Good  4     4.05  2.39   5.74 1.10    5.77 1.10    3.56 0.730
# … with 53,930 more rows

rename_with( )で列名を変更する

across( )はrename( )と一緒には使えないようです。代わりにrename_with( )と使うようですが、rename_with( )には対象とする変数を指定する引数があるので、across( )を入れ子にする必要はありません(多分)。*2

rename_with( )でもacross( )と同様、.cols.fnを指定しますが、引数の順序が異なるので注意が必要です。例えば、数値型変数に"num_"という接頭辞をつけたい場合は次のように書きます(後で使うので今回は別名で保存しました)。

d <- data %>% rename_with(~paste0("num_", .x), where(is.numeric))
> d
# A tibble: 53,940 × 10
   num_carat cut   color clarity num_depth num_table num_price num_x num_y num_z
       <dbl> <ord> <ord> <ord>       <dbl>     <dbl>     <int> <dbl> <dbl> <dbl>
 1      0.23 Ideal E     SI2          61.5        55       326  3.95  3.98  2.43
 2      0.21 Prem… E     SI1          59.8        61       326  3.89  3.84  2.31
 3      0.23 Good  E     VS1          56.9        65       327  4.05  4.07  2.31
 4      0.29 Prem… I     VS2          62.4        58       334  4.2   4.23  2.63
 5      0.31 Good  J     SI2          63.3        58       335  4.34  4.35  2.75
 6      0.24 Very… J     VVS2         62.8        57       336  3.94  3.96  2.48
 7      0.24 Very… I     VVS1         62.3        57       336  3.95  3.98  2.47
 8      0.26 Very… H     SI1          61.9        55       337  4.07  4.11  2.53
 9      0.22 Fair  E     VS2          65.1        61       337  3.87  3.78  2.49
10      0.23 Very… H     VS1          59.4        61       338  4     4.05  2.39
# … with 53,930 more rows

特定の文字列を置換して変数名を変更したい状況の方が多いような気がします。gsub( )を使うと次のように書けます。

d %>% rename_with(~gsub("num", "cont", .x), where(is.numeric))

gsub( )の引数は以下のとおりです。

  • pattern:検索したい文字列パターン
  • replacement:マッチした場合にこの文字列に置き換えたい
  • x:検索・置換の対象となる文字列(ここではラムダ式で書いているので.xとなっている)

おわりに

  • 自由度が高くなると自分の応用力の無さが際立ちます...
  • ペーストとココナッツミルクでグリーンカレーを作ったら、ネコたちが酔っ払ってしまいました。マタタビに似たものでも入っているんでしょうか。

参考資料

  • 本家の資料。

dplyr.tidyverse.org

  • 新旧の記載方を対応させながら説明されています。

doubtpad.hatenablog.com

  • glueを使った表現の説明が簡潔で分かりやすかったです。

teramonagi.hatenablog.com

*1:見やすさのため必要な変数のみselectしました

*2:rename_with( )では「対象の変数」と「どのように変数名を変えるか(=関数)」を指定します。これはacross( )で「対象の変数」と「関数」を指定したのに対応していて、rename_with( )とacross( )は同格なんだ、と自分の中では噛み砕きました(真偽不明)。