yokaのblog

湖で微生物の研究してます

シェルのパイプからRを使って最大値や平均値を簡単に得る方法

 シェルでデータテーブルを触るのにawkがよく使われるけど、最大値・最小値・平均値などを計算しようとすると結構めんどい。Rでやれば2語で済むような処理も、awkだとifやforを使って複数行を使って書かなければできなかったりする。自分はRのほうが得意なので、データが複雑になるとすぐにRに切り替えて解析するのだけど、概ねシェルで完結できそうな作業の中で一部だけRを使いたいときに、いちいち環境を切り替えるのが面倒で頑張ってawkで書いて時間を浪費することになっていた。なんとかならないかな、と思って色々と調べていたら、この方法を見つけて感動したので備忘で書いておく。 

例えば以下のようなtsvがあって、これの最大値を求めたい。

$cat data.txt
1	2	3
4	5	6
7	8	9

欲しい数値は9だ。awkでやろうとすると、列ごとに最大値を求めるのもまあまあめんどいのに、このケースでは列を横断して最大値を求めようとしているので余計にめんどい。
Rなら

max(data.txt)

の一行で済む。で、以下のようにすれば、これをシェルスクリプトの中で直接実行できる。

$cat data.txt | xargs Rscript -e 'max(as.numeric(commandArgs(T)))'
[1] 9

パイプからの出力をxargsでRscriptに引数として渡して、それをcommandArgs()で読み込んで実行するという流れ。-e' '内のコマンドをRスクリプトとして直接実行するために必要なオプション。commandArgs()内のTはtrailingOnly=TRUEの省略形で、余計な引数をRscriptに渡さないようにするために必要な呪文(デフォルトでTRUEになってないのは何故なのだろうか)。注意点としては、この方法では行列の形で渡しても、Rにはベクトルとして渡っているということと、渡された引数は文字列として認識されてしまうために数値として扱うにはas.numeric()をかませる必要があるということだ。
例えば以下のようになる。

$cat data.txt | xargs Rscript -e 'commandArgs(T)'
[1] "1" "2" "3" "4" "5" "6" "7" "8" "9"

入力を行列として受け取りたい場合は、R内でmatrix()を使って整形するなどの対応が必要になる。
また、デフォルトのRの出力では行頭に要素番号[1]がついていて、シェル内で変数として代入したい時など、数値だけが欲しいときには不都合だ。Rのcat()関数を用いれば要素番号を消せるのだけど、cat()はデフォルトでは最後に改行を返してくれないので不都合が起こることがある。そこで、paste0()で改行を行末に足すことで、数値だけを取り出すことができる。

$cat data.txt | xargs Rscript -e 'cat(paste0(max(as.numeric(commandArgs(T))),"\n"))'
9

で、これを応用すれば、最小値も平均値も中央値も合計も自在に求めることができる。

#最小値
$cat data.txt | xargs Rscript -e 'cat(paste0(min(as.numeric(commandArgs(T))),"\n"))'
1
#平均値
$cat data.txt | xargs Rscript -e 'cat(paste0(mean(as.numeric(commandArgs(T))),"\n"))'
5
#中央値
$cat data.txt | xargs Rscript -e 'cat(paste0(median(as.numeric(commandArgs(T))),"\n"))'
5
#合計
$cat data.txt | xargs Rscript -e 'cat(paste0(sum(as.numeric(commandArgs(T))),"\n"))'
45

行や列ごとの最大値が欲しいときは、シェルの段階で処理してからRに渡す。

#2列目の最大値を求める
cat data.txt | awk '{print $2}' | xargs Rscript -e 'cat(paste0(sum(as.numeric(commandArgs(T))),"\n"))'
8
#3行目の合計を求める
cat data.txt | sed -n '3p' | xargs Rscript -e 'cat(paste0(sum(as.numeric(commandArgs(T))),"\n"))'
24

もう少しデータが複雑になったら素直に一旦ファイルに出力してからR上でread.delimで読み込んだほうが早そうだけど、「Rなら1行でできるのに!」がシェル上でも1行でできるようになってとても快適になった。

参考にした記事:
immanacling63.rssing.com