[R] mutateとacrossでデータの下処理を少しだけエレガントに

まえおき

私はdplyrは5年前くらいから使っていて,自分が扱うようなデータについて自分がやりたいことを(その表現方法のエレガントさは別として)表現することはできていました。ただ,近年dplyrはアップデートを重ねていました。昔覚えたやり方でやろうとしても,その関数は使えませんとか,その表記方法は違いますとか言われることが増えました。分析の下処理でやりたいことは基本的に研究が変わらないので同じです。よって,過去に自分が書いたスクリプトのコピペをすることが多いわけです。それができなくなっていたと。

特に,最近の更新でacross()という関数が導入されたことが変更として大きいなと思います。まだまだこのacross()に関する記事も少なかったので,自分が使うにあたって覚えたことをメモ代わりに書いておきます。ここでは,mutate関数と一緒に使うケースです。つまり,ある特定の列について,ある処理を施して,その処理を施した列をデータフレームに追加するという作業です。単純に列に対して処理を施すだけというのは結構記事があったんですが,列を追加することについては全部列挙するみたいな方法しか見つかりませんでした。そこでacross関数の出番というわけですね。

やりたいこと

例えば,今やってる研究のデータでは,データフレームの中に頻度のデータが入っています。これをログ変換したいとします。すると,これまでは以下のように書いていました。

log(dat$ColFreq)->dat$ColFreq_log #コロケーション頻度
log(dat$AdjFreq)->dat$AdjFreq_log #コロケーション内の形容詞の頻度
log(dat$NounFreq)->dat$NounFreq_log #コロケーション内の名詞の頻度
log(dat$MIScore)->dat$MIScore_log #Mutual Information Score

別にコピペ&書き換えみたいなことをしながらやればいいし,これでだめだってことはないんですけど,複数の列について

  1. 同じ関数を適用
  2. 列を追加する

という同じ動作をしているわけなので,これは一気にできたほうが応用可能性があがります。私は手作業でやるの無理みたいな列数のデータを扱うことはないんですが,もし仮にそういうデータを扱う場合には何十行も使うのは好ましくないし無駄な作業だといえます。そこで列に対して処理を施して追加するという機能があるmutate関数と,それを複数列に適用する際に便利なacross関数を組み合わせます。

やりかた

ちなみに,なんだかんだでdplyrのパッケージのPDFが一番わかりやすかったです(pp. 3-6のacross関数のセクションとpp. 43-46のmutate関数のセクション)。across関数の引数は,列(.cols),関数(.fns),追加する列の名前(.names)という3つの引数があります。よって,今回のケースで言えば列のところで頻度情報が入ってる列を選択し,関数はlogを選べばOKです。ただ,.namesがないと情報を上書きしてしまいます。.namesのところは手書きで全部列名指定してやらなかんのかと思いましたが,そんなことはありません。”{.col}”を使えば,もとの列名を使えます。これにあとは自分で好きなタグのようなものをつけてあげればいいですね。”{.fn}”というのも使えて,これは使った関数名が入ります。

ということで,以下のようにすれば頻度情報にログ変換して列追加という作業ができます。

dat%>%
  mutate(across(c(ColFreq, AdjFreq, NounFreq, MIScore),log,.names = "{.col}_log"))->dat

.namesの部分は”{.col}_log”としていますが,”{.col}_{.fn}”でも同じです。dat$ColFreq_log, dat$AdjFreq_log,dat$NounFreq_log,dat$MIScore_logという4つの列が追加されます。ちなみに,列指定の部分は列の数値(e.g., 1, 2)でも可能です。頻度の情報が5~8列目にあるなら,次のように書くこともできます。ある特定の文字列が含まれる列を選ぶcontains()関数starts_with()関数とかも使えるはずです(こういうのは調べれば結構例があります)。

dat%>%
  mutate(across(5:8,log,.names = "{.col}_log"))->dat

ちょっと応用

さて,mutate関数とacross関数でたいぶすっきりしたコードを書くことができました。そこでふと,私がもう一ついつも下処理で複数列に適用する作業を思い出しました。それは,変数の標準化です。いつもなら次のようにしてました。

dat$z.oqpt <-scale(dat$oqpt)[,1] #Oxford Placement Testの点数の中心化
dat$z.rating <-scale(dat$rating)[,1] #評定値の中心化

これも別に2行だけなので大したことないんですが,やってることは先ほどのログ変換と同じですので,これもmutate関数を使って書き直してみましょう。次のようになります。

dat%>%
  mutate(across(c(oqpt,rating),scale,.names = "z.{.col}"))->dat

これでうまくいっているようにも見えますが,実はscale関数って出力された結果がベクトルではありません(データ型を調べるとmatrix型なのがわかります)。よって,大抵の場合は分析に問題はありませんが,あとで(私の場合だと分析結果の図示とか)ベクトル形式が求められる関数に渡した際に問題が発生することがよくあります。データフレームをただ眺めるだけではそのことはわからないので,次のように工夫してあげる必要があります。

dat%>%
  mutate(across(c(oqpt,rating),~scale(.x)[,1],.names = "z.{.col}"))->dat

さきほどと関数部分の書き方が変わっているのがわかると思います。このように”~”をつける書記法はpurrr-styleと呼ばれるそうですが,これは一般的には関数内の引数を指定する場合によく用いられます。例えば,~mean(.x, na.rm=T) のように使います。”na.rm=T”は欠損値は外して関数を適用するという設定のようなものです。今回は引数の設定ではなく,~scale(.x)[,1]としています。”[,1]”とすることで,行列の1つ目の要素(つまりこれは標準化された数値のベクトル)だけを出力してくれます。ちなみに,この方法で.namesに{.fn}をつかって次のようにすると,出力される列名はoqpt_1, rating_1のようになりました(理由は不明)。

dat%>%
  mutate(across(c(oqpt,rating),~scale(.x)[,1],.names = "{.col}_{.fn}"))->dat

おわりに

というわけで,改良されているんだろうけれども前のやり方に慣れてるこっちからしたらアップデートたびにコードを書き換えるのまじで面倒…って思っていたのですが,調べてみるとやっぱり便利でしたというお話でした。

またこういう系のことで新しく覚えたことがあれば記事に書こうと思います。

なにをゆう たむらゆう。

おしまい。

コメントを残す