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

統計屋のためのAWK入門

はじめに

本稿はAWKという言語を用いて、
ごく簡単にデータ分析用の前処理*1をするための解説記事です。

AWKは短いコマンドを記述するだけで多様なデータ処理を可能にします。
特にデータの抽出に関して恐るべき簡易さを提供します。
具体的には、input.txtというファイルの中から
"fail"という文字列を含む行を抽出したければ次のように書くだけです。

awk /fail/ input.txt

つまり、スラッシュ記号で文字列を指定するだけで
その文字列を含む行を抽出できるのです。
大変簡単ですね!
また、awkLinuxMacには標準で入っており、
Windowsでもawk.exeを一つ用意するだけなので、
面倒なインストール作業や環境構築は不要で誰でも即座に使えるため、
自分で書いた処理を他人に渡したり*2各サーバに仕込むなども簡単に出来ます。

複雑な処理をする場合はPythonRubyなどの言語を用いた方が良いと思いますが、
簡易な処理をするケースではAWKの手っ取り早さが大変便利です。
Excelではちょっと厳しい数百万行以上のデータでも
サクサク処理してくれるのが魅力です。

本稿の対象者はプログラミング未経験~初心者、対象環境はwindows7以降です。
AWKはほんの少し勉強するだけで
プログラミング未経験な文系統計屋でも簡単に使えるようになります。
このドキュメントも15分あれば読めると思います。チャレンジしてみましょう!

なお、本稿ではAWKの言語仕様や文法などについては触れていません。
悪しからず。

下準備

AWKをインストールする

Linux, macであれば標準で入っています。
Windowsの場合は、下記から(2014/09/15時点での)最新版のgawk-4.1.0をダウンロードし、
パスの通ってるフォルダにgawk.exeを置きます。
私はC:\Windows\System32に置いています。
Downloads - gnu-on-windows - GNU tools for win32 - Google Project Hosting
ついにで、exe名をgawk.exeからawk.exeに変えておきます*3
コマンドプロンプトawkと打ち込み、
次のようなヘルプが表示されたらインストール作業終了です。
f:id:AntiBayesian:20140915160301j:plain

サンプルデータ

以下動作確認用のサンプルデータとして
treesというデータセットを一部加工したものを用います。
コピペしてtrees.txtと名前を付けて保存して下さい。
私はc:\直下にawkフォルダを作り、c:\awk\trees.txtとしました。

Girth Volume
8.3 10.3
8.3 10.3
8.8 10.2
10.5 16.4
10.7 18.8
10.8 19.7
11 15.6
11 18.2
11.1 22.6
11.2 19.9
11.3 24.2
11.4 21
11.4 21.4
11.7 21.3
12 19.1
12.9 22.2
12.9 33.8
13.3 27.4
13.7 25.7
13.8 24.9
14 34.5
14.2 31.7
14.5 36.3
16 38.3
16.3 42.6


17.3 55.4
17.5 55.7
17.9 58.3
18
18 51
20.6 77

これで下準備完了です。
ではAWKによる前処理を学びましょう。

AWKによるデータ抽出

指定条件を満たす行を抽出する基本
awk /10/ trees.txt
>8.3     10.3
>8.3     10.3
>8.8     10.2
>10.5    16.4
>10.7    18.8
>10.8    19.7

これで"10"を含む各行を抽出できます。
複数条件で抽出したい場合は以下のように&&や||で条件を連結します。
条件を連結する場合はどこからどこまでが条件なのかを明示するために
""で囲みます*4

# && 両方の条件を満たす行だけを抽出
awk "/10/ && /16/" trees.txt
>10.5    16.4
# || 片方どちらかでも条件を満たした行を抽出
awk "/10/ || /16/" trees.txt
>8.3     10.3
>8.3     10.3
>8.8     10.2
>10.5    16.4
>10.7    18.8
>10.8    19.7
>16      38.3
>16.3    42.6

今までの抽出法では各行の全ての列を対象としていました。
n列目だけを対象にしたければ"$n"と指定します
($0にはその行の全ての列が格納されています)。

awk $1~/10/ trees.txt
>10.5    16.4
>10.7    18.8
>10.8    19.7
# && 両方の条件を満たす行だけを抽出
awk "$1~/10/ && $2~/18/" trees.txt
>10.7    18.8
# || 片方どちらかでも条件を満たした行を抽出
awk "$1~/10/ || $2~/18/" trees.txt
>10.5    16.4
>10.7    18.8
>10.8    19.7
>11      18.2

逆に指定した文字列を含まない行のみを抽出する場合は条件の前に!を付けます。

awk !/10/ trees.txt
>Girth   Volume
>11      15.6
>11      18.2
>11.1    22.6
> ... #長いため省略

抽出条件の指定は文字列の一致だけではありません。
数値の範囲指定なども可能です。

awk "$1<10" trees.txt
>8.3     10.3
>8.3     10.3
>8.8     10.2

面白い範囲指定として/start/, /end/という方法があります。
これは/start/条件に一致する行から/end/までの範囲を抽出するというものです。

awk /12/,/14/ trees.txt
>12      19.1
>12.9    22.2
>12.9    33.8
>13.3    27.4
>13.7    25.7
>13.8    24.9
>14      34.5

更に、正規表現と言う複雑な条件指定も可能です。
但し本稿の範囲を超えるため取り扱いません。

次は、このデータ抽出を更に強化するための便利な機能を紹介します。

組込変数を用いたデータ抽出

AWKには様々な組込変数(AWKが事前に用意してくれた便利な変数)があります。
その中でも特に頻繁に用いるモノを紹介します。

# 組込変数
NR #現在読み込んでいる行数
NF #現在読み込んでいる行の列数
-F #ファイルセパレータ指定。デフォルトは半角スペースかタブ。,を指定することでCSVファイルに対応
length #現在読み込んでいる行の文字数

これらをこれまで学んだ抽出条件指定に活用することが出来ます。

# CSVファイルを読む
# -Fオプションを用いてセパレータを,(カンマ)に変更する事で
# CSVファイルのN列目を指定出来る
awk -F , $1~/10/ trees.txt
# 3行目以下を取り出す
awk "NR<=3" trees.txt
>Girth   Volume
>8.3     10.3
>8.3     10.3
# 列数が2未満の行を取り出す
# これを用いれば、「N列ある筈のデータからN列未満しかデータが入っていない行、
# つまり欠損値を含む行が何行目にありどのようなデータかを洗い出す」
# という処理が可能になります。
awk "NF<2 {print NR, $0}" trees.txt
>27
>28
>32 18
# lengthが0より上、つまり空行以外を抽出します。
awk "length>0" trees.txt
>Girth   Volume
>8.3     10.3
>8.3     10.3
> ... 省略

重複行を除外することも可能です。
これはちょっと難しいのでこんなことも出来るんだなー程度に眺めて下さい。

awk "before != $0 {print; before = $0}" trees.txt
>Girth   Volume
>8.3     10.3 # 重複行が消えている
>8.8     10.2
>10.5    16.4

ファイルの行数を出します。

# ENDはファイルを読み終わった後に実行される処理を指定する方法です。
# 他に、BEGINというファイルを読み込む前に実行される処理を指定する方法もあります。
awk "END{print NR}" trees.txt
> 36


欠損値を含んだ行や空行、重複行の抽出や削除の方法を学びました。
現時点でも色々便利なデータ抽出が出来るようになりましたね。
これに加え、さらにデータを加工する手法を学び、
前処理をより簡単にしましょう。

AWKによるデータ加工

AWKは awk /条件/ 入力データ
とするだけで条件に沿ったデータを出力出来ます。
但しこれだけでは条件に合致した行がそのまま出力されるだけです。
printを使ってデータを簡単に加工・整形することが出来ます。

# 1列目だけを出力します
awk "{print $1}" trees.txt
>Girth
>8.3
>8.3
>8.8
> ... 省略

# 1列目と2列目を入れ替えて表示します。
awk "{print $2,$1}" trees.txt
>Volume Girth
>10.3 8.3
>10.3 8.3
>10.2 8.8
> ... 省略

# 1列目と2列目で計算した結果を出力します。
# 掛け算は*, 割算は/で表現します。
awk "{print $0,$1*$2}" trees.txt
# 1行目は文字列同士なので掛け算の結果は0になります。
# 数値以外で計算すると0となることがわかります。
>Girth   Volume 0
>8.3     10.3 85.49
>8.3     10.3 85.49
>8.8     10.2 89.76
> ... 省略

# 消費税を掛けるなどの計算も可能です
awk "{print $1*1.08}" trees.txt
>0
>8.964
>8.964
>9.504
> ... 省略

# 各行に行数を付与する
awk "{print NR, $0}" trees.txt
>1 Girth Volume
>2 8.3   10.3
>3 8.6   10.3
>4 8.8   10.2

# 組込変数を用いてデータを加工する。
# 今回は各行の先頭にファイル名を付与します。
# よくあるケースとして、ファイル名に日付は入っているが
# データの中身に日付が入っておらず、
# 後でマージしようとして困る時があります。そういう時に便利です。
awk "{print FILENAME, $0}" trees.txt
>trees.txt Girth Volume
>trees.txt 8.3   10.3
>trees.txt 8.3   10.3
>trees.txt 8.8   10.2

# 組込関数を用いたデータ加工。
# gsubという関数を用いて文字列の置換が可能です。
# gsubで置換する場合は
# gsub(置換対象の文字列,置換後の文字列,置換対象の列)
# と指定します。
# ここでは$0(行全体)の8をxxxという文字列に置換し、
# その後変換済みのデータを表示しています。
# 複数の処理を行う場合は;で繋ぎます。
# この文字列の置換は指定が複雑ですが、
# 強力かつ本当に良く使うため是非覚えて下さい。
awk "{gsub(8,\"xxx\",$0);print $0}" trees.txt
>Girth   Volume
>xxx.3   10.3
>xxx.3   10.3
>xxx.xxx 10.2

AWKでデータを整形・加工する方法を学びました。
ここまでの内容を組み合わせるだけで
本当に多様な前処理が可能になります。
皆さんの前処理ライフが幸あるものになりますよう!






落穂広い

AWKのあんまりドキュメントに載って無さそうなことをつらつらと。

#標準入力を用いる
awk "{print $2}" -
#とすると、標準入力に打ち込んだ2列目だけ表示されます。
# 組込変数いろいろ
# ARGC:コマンドライン引数の数
" ARGV:コマンドライン引数の配列
awk "BEGIN{print ARGV[3]}" p 100 200 300
>200

# FNRとNRの違い。複数ファイルを入力としたときに挙動が異なる
# FNR:読み込んでる現ファイルのみの現在行
# NR:読み込んだ全てのファイルの現在行

# ファイル入出力のあれこれ
# FS:入力区切り文字(デフォ=半角スペースかタブ)
# OFS:出力区切り文字(デフォ=半角スペース)
# RS:入力行区切り(デフォ:復帰改行=\n)
# ORS:出力行区切り(デフォ:復帰改行=\n)
# 配列
a[i] = x
a[i, j] = x
# で多次元配列定義可能。
awk "BEGIN{a[0,1,2,3,4,5,6,7,8,9]=100}{print a[0,1,2,3,4,5,6,7,8,9]}" p
# と何次元でもOK
delete a[0]
# で配列の中身を消せる

AWKでなんか色々統計処理をやってみた

AWKと言えば1ライナー。
1ライナーで統計処理やりたいなーと思ってたので書いてみた。

# ランダムサンプリング
awk "BEGIN{srand()} {line[NR] = $0} END{for(;i<100;i++){print line[1+int(rand()*NR)]}}" p
# シャッフル
awk "BEGIN{srand()} {line[NR] = $0} END{for(;i<1000;i++){r=1+int(rand()*NR);s=1+int(rand()*NR);tmp=line[r];line[r]=line[s];line[s]=tmp};for(t in line)print line[t]}" p
# シャッフルを用いた重複なしランダムサンプリング
awk "BEGIN{srand()} {line[NR] = $0} END{for(;i<1000;i++){r=1+int(rand()*NR);s=1+int(rand()*NR);tmp=line[r];line[r]=line[s];line[s]=tmp};for(t in line)print line[t]}" p | awk "NR <= 5"
# max, min
awk "NR==1{min=$1;max=$1}{if(max<$1)max=$1;if(min>$1)min=$1}END{print max,min}" p
# 算術平均、中央値(偶数考慮無し)、最頻値
awk "{avg+=$1;median[NR]=$1;mode[$1]++}END{asort(mode, keys);asort(median);max=keys[length(keys)];print \"mean\",avg/NR;print \"median\",median[int(NR/2)];for(i in mode){if(max==mode[i])print \"mode\",i}}" p
# 五数要約(箱ひげ図用)
awk "NR==1{min=$1;max=$1}{if(max<$1)max=$1;if(min>$1)min=$1;percentile[NR]=$1}END{asort(percentile);print max,percentile[int(NR*0.75)],percentile[int(NR*0.5)],percentile[int(NR*0.25)],min}" p
# 偏差平方和
awk "{d[NR]=$1;avg+=$1}END{a=avg/NR;for(i in d)s+=(d[i]-a)^2;print s}" p
# 分散
awk "{d[NR]=$1;avg+=$1}END{a=avg/NR;for(i in d)s+=(d[i]-a)^2;print s/(NR-1)}" p
# 偏差積和
awk "{dx[NR]=$1;dy[NR]=$2;avg_x+=$1;avg_y+=$2}END{ax=avg_x/NR;ay=avg_y/NR;for(i=1;i<=NR;i++){s+=(dx[i]-ax)*(dy[i]-ay)};print s}" p
# 共分散
awk "{dx[NR]=$1;dy[NR]=$2;avg_x+=$1;avg_y+=$2}END{ax=avg_x/NR;ay=avg_y/NR;for(i=1;i<=NR;i++){s+=(dx[i]-ax)*(dy[i]-ay)};print s/(NR-1)}" p
# 標準偏差
awk "{d[NR]=$1;avg+=$1}END{a=avg/NR;for(i in d)s+=(d[i]-a)^2;print sqrt(s/(NR-1))}" p
# ピアソンの積率相関係数
awk "{dx[NR]=$1;dy[NR]=$2;avg_x+=$1;avg_y+=$2}END{ax=avg_x/NR;ay=avg_y/NR;for(i=1;i<=NR;i++){s+=(dx[i]-ax)*(dy[i]-ay);sx+=(dx[i]-ax)^2;sy+=(dy[i]-ay)^2};print s/(NR-1)/sqrt((sx/(NR-1))*(sy/(NR-1)))}" p
# 度数分布。binを適当に調整してね
awk "BEGIN{bin=1.0}{f[int($1/bin)]++}END{for(i in f)print i*bin,f[i]}" p
# 単回帰
awk "{dx[NR]=$1;dy[NR]=$2;avg_x+=$1;avg_y+=$2}END{ax=avg_x/NR;ay=avg_y/NR;for(i=1;i<=NR;i++){s+=(dx[i]-ax)*(dy[i]-ay);sx+=(dx[i]-ax)^2;sy+=(dy[i]-ay)^2};print \"回帰係数\",s/sx,\"切片\",ay-s/sx*ax,\"決定係数\",(s/(NR-1)/sqrt((sx/(NR-1))*(sy/(NR-1))))^2}" p
# ワードカウント
awk "{for(i=NF;i;i--)a[$i]++}END{for(w in a)print w,a[w]}" p

感想。
統計処理1ライナーで書くなアホか見辛い。

参考文献

AWKの古典的なテキストです。
AWKの詳細な文法だけではなく、
ランダムな文章を生成したり、ワードカウントやKWIC検索など
テキストマイニング的な処理を実装したり、
何とインタプリタを実装したりします!
流石に内容古いですが、今読んでも面白い記述が沢山あります。

プログラミング言語AWK

プログラミング言語AWK

  • 作者: A.V.エイホ,P.J.ワインバーガー,B.W.カーニハン,足立高徳
  • 出版社/メーカー: USP研究所
  • 発売日: 2010/01/01
  • メディア: 単行本(ソフトカバー)
  • クリック: 1回
  • この商品を含むブログを見る
AWKについてまとまったページ数割いた解説記事があります。
安くて入手しやすくて分かり易いの3拍子揃っています。
AWKで3Dグラフィックを描画するとか凄まじい内容もあります。日本語で読めるAWKまとめサイトです。
有益な情報が沢山掲載されているので、
基本的な使い方を理解してAWKの深淵を覗きたくなったら
隅々まで読んで感嘆しましょう。
AWK Users JP :: 日本の AWK ユーザのためのハブサイト
sed & awkプログラミング 改訂版 (A nutshell handbook)

sed & awkプログラミング 改訂版 (A nutshell handbook)

アルゴリズムのはなし
AWKの覚書き -
https://www.gnu.org/software/gawk/manual/gawk.pdf
AWK Users JP :: ある平均値の正規分布のサンプル
AWK Users JP :: 重みを付けて乱択する
AWK Users JP :: 簡易ダウンローダー
AWK Users JP :: 中置記法から S 式への変換
AWK Users JP :: awk でアスキーアート画像を生成する

*1:データ抽出、加工

*2:他人にexeとスクリプトぽいっと渡すだけで実行して貰えるようになるの、かなり重要なことです。Windows環境ですと、エンジニアでもないとRubyPythonなどの処理系がインストールされてないため、手元で便利なスクリプトを作っても他人に利用して貰いにくい場合があります。Webツール化する程のものでもない使い捨ての処理する場合特に有益です

*3:この改名は別にする必要は全く無いのですが、単にタイプ数減らしたいので私はそうしてます

*4:実はawkにも色々あって(今回使ってるgawk以外にnawkなど)、"ではなく'で囲むケースもあります