読者です 読者をやめる 読者になる 読者になる

bashとgnuplot (時々awk)でデータ整理!

この記事はTUT Advent Calendar 2016の22日目の記事です。

本年度の私は、いろいろなデータをCSVファイルで吐かせて、そのデータ整理を行っていました。
データ整理の際、表計算ソフトが重い、そこまで面倒なことはさせない、といった場合のデータ整理でしたら、シェルスクリプトgnuplot(時々awk)でこなすことができます。
特に最近ではbash on Windowsの登場でWindowsでもbashスクリプトを書くことが可能になりましたので、お勧めしたいと思います。(表計算ソフトが重かったりしますし。。)

以降、今年1年で私が得られた知見について書き連ねていきます。 大まかな流れは以下の通りです。

  • 統計処理
  • ファイル整理
  • グラフ化

統計処理

統計処理として、今回取り上げるのは平均分散最大値(最小値)です。
今回は、CSVファイル(hoge.csv)が以下のような構成をしていることを想定します。

キー(サンプル番号やx軸に対応するもの), データ

つまり、ファイルの2列目を使用したい場合です。

平均

bashで平均値を求めたい場合は、awkを使用して1行で書くことができます。平均値meanは、以下のコードで求めることができます。
awkのオプション、-Fでファイルのデリミタ(区切り文字)である","を指定しています。

mean=$(cat hoge.csv | awk -F,  '{m+=$2} END{print m/NR;}')

awkのオプション、-Fでファイルのデリミタ(区切り文字)である","を指定し、ファイル中の2列目($2)の総和をとり、行数(NR)で割るのみです。

分散

先ほど求めた平均値meanを用いて、分散varianceは容易に求めることができます。

mean=$(cat hoge.csv | awk -F,  '{m+=$2} END{print m/NR;}')
variance=$(cat hoge.csv | awk -F,  "{m+=(\$2-${mean})^2} END{print m/NR;}")

meanの例とは異なり、awkの引数をダブルクォートで囲んでいます。これにより、列数を指定する$はエスケープが必要ですが、代わりにbashの変数展開を用いることができますので、分散を楽に求めることができます。(もっと楽な方法があるかもしれません)

最大値(最小値)

最大値は、awkを用いて次のコードで求めることができます。

max=$(cat hoge.csv | awk '{if(m<$2) m=$2} END{print m}')

最小値はこのコードの不等号を入れ替えるだけで求めることができます。

ただし、hoge.csvの2列目がすべて0の場合、結果として空の文字が返ってくるようです。(比較に失敗し続ける?) そのため、awkのBEGINセクションで初期値を与えてあげるのが無難化と思われます。

ファイル整理

ファイル整理として、例えばファイルの分離にはheadやtailコマンドで行うことができ、ファイルの結合にはcatコマンドでファイルを全て指定することで行うことができます。

本記事で着目するのは、ファイルの列結合です。

catコマンドで行うことができるのはファイルを縦に結合する場合です。ファイルを列で結合するには、joinコマンドを用います。
例えばhoge.csvの同様のファイル形式を持つfuga.csvがある場合、その2つのファイルを同一キーで列結合するには、以下のコマンドで行うことができます。

join -t, hoge.csv fuga.csv >> hogefuga.csv

デリミタを-tオプションで指定し、標準出力に結果が出力されるため、結合後ファイル(hogefuga.csv)に記録します。
joinコマンドはデフォルトでは1列目をキーに、2列目をデータとして処理します。 キーとなる列とデータ列を明示的に指定する場合は、それぞれ-1、-2オプションによって列番号を指定することで処理が可能です。

ただし、このjoinコマンドは同時に2ファイルのみを結合可能という制約がありますので、3ファイル以上を結合したい場合はmktempコマンドなどで一時ファイルを生成してから順番に結合する必要があります。

グラフ化

gnuplotを使う際に多用した機能について述べます。
gnuplotCSVファイルを用いる際は、あらかじめ、

set datafile separator ","

とすることで、読み込むファイルのデリミタを","に指定します。

プロット時の簡単な数値処理

グラフを描画する際、ちょっとした規格化や簡単な単位調整等が必要な場合があります。つまり、ある列のすべての値に対して同じ演算を施したい場合、gnuplotで描画する場合に処理を行うことができます。

通常、hoge.csvの1列目を第1軸に、2列目を第2軸として描画を行う場合、以下のようなコマンドをgnuplotでは実行します。

plot "hoge.csv" using 1:2 

例えばデータを1000倍して表示したい場合、以下のようなコマンドを実行します。

plot "hoge.csv" using 1:($2*1000) 

関数近似

gnuplotでは、関数近似も容易に行うことができます。
例えばhoge.csvの1列目をx軸、2列目をy軸の値として

y=ax2+bx+c

の式で近似したい場合は、以下のコマンドを実行します。

f(x)=a*x**2+b*x+c
fit f(x) "hoge.csv" using 1:2 via a,b,c

このコマンドでは、f(x)としてフィッティング先の関数を、via以降で最適化するパラメータを列挙する必要があります。
フィッティング結果(係数の値、誤差)は、標準出力に出力されるとともに、logファイルが生成され、そこに書き込まれます。

文字列

複数のcsvファイルを同時に描画したい場合があるかと思います。 gnuplotではsprintf関数をサポートしているため、以下のような構文を用いることができます。

i=3
s(n)=sprintf("hoge_n%d.csv",n)
s(i)
# s(i) equals to hoge_n3.csv

また、gnuplot version 5.0以降では、描画時ループ構文をサポートしましたので、以下のような構文を用いることができます。

plot for[i=1:10] s(i) using 1:2

gnuplotではスクリプトファイルを作成し、gnuplot内でloadコマンドを実行することでコマンドをまとめて実行することができますが、 文字列処理や使用ファイル数が煩雑になる場合、bashスクリプトgnuplotスクリプトファイルを自動生成することも手です。

bashでは文字列内の変数展開が可能ですので、ファイル名の指定が容易に可能です。

まとめ

bashgnuplotを用いることで、簡単なデータ整理を行うことができます。

個人的にはjoinコマンドにより、表計算ソフトでの負担が大きく減るように思います。

明日のAdvent Calendarは・・・?

明日23日目のAdvent Calendarは@emaxserさんの担当です。「何か頑張る」とのことなので何やら楽しみです。
無理しない程度に頑張っていただきたいです。