第23回シェル芸勉強会に参加してきたので、その復習。
今回は初めて来る人が多かったのか、比較的難易度は低めで、かつ(今までのより)実用的、説明も丁寧にされていたような気がする。
テーマがデータ集計ということもあって、全体的にawkが多めになっているようだ。テーマが集計・統計なので、復習では個人的な趣味嗜好によりこないだ知ったdatamashをできる限り使ってみようと思う。問題及び模範解答はこちら

Q1.

年ごとの台風の上陸データについてのCSVデータをダウンロードしてきて、それを年+月の状態に編集する、という問題。
どうもこのCSVファイル、UTF-8のBOMがついちゃってるようなので、用意されているダウンロードのコマンドではnkfを通してファイルに書き出してやる必要がある。

curl http://www.data.jma.go.jp/fcd/yoho/typhoon/statistics/landing/landing.csv | nkf -wLux > landing.csv

最近のディストリだとバンドルで入ってるコマンドはiconvに置き換わってることが多いので、もしnkfがインストールされていないなら便利だし入れておこう。
CentOSならyum、DebianやUbuntuならapt-getで普通にインストール可能だ。

で、本題のCSVファイルの編集。
以下のようにawkコマンドを実行して、年+月ごとに行を分けて出力してやる。

awk -F, 'NR>1{for(i=2;i<NF;++i){printf "%d%02d %d\n",$1,i-1,$i}}' landing.csv > monthly_typhoon

一応、それぞれの処理について分解してコメントも記述すると、以下のような感じ。
(うまく説明できていないかも…)

# -F … 区切り文字。今回はCSVなので「,」で区切っている。
awk -F, '
# 「NR」で対象の行を指定できる。「NR>1」で、1行目以降の行で処理をするように指定している。
NR>1{
 # 各列をforでまわしていく。「NF」でその行の列数を指定できる。
     for(i=2;i<NF;++i){       # printfで、「"」で囲って表示の仕方を指定している。ここでは、指定した表示方式の後ろに、カンマ区切りでそれぞれ値を代入している。           printf "%d%02d %d\n",$1,i-1,$i           }  }' landing.csv > monthly_typhoon
[root@BS-PUB-CENT7-01 work]# awk -F, 'NR>1{for(i=2;i<NF;++i){printf "%d%02d %d\n",$1,i-1,$i}}' landing.csv > monthly_typhoon
[root@BS-PUB-CENT7-01 work]# head -10 monthly_typhoon
195101 0
195102 0
195103 0
195104 0
195105 0
195106 0
195107 1
195108 0
195109 0
195110 1

Q2.

Q1で作成したファイル「monthly_typhoon」が、元のファイル「landing.csv」とデータとして相違ないか比較するという問題。
とりあえず年間合計があっていれば問題ないと判断していいらしいので、「monthly_typhoon」をすべて合計してやればよい。

この問題では、せっかくなのでdatamashを使用する。

diff <(sed 's/.. / /' monthly_typhoon | datamash -t' ' -g 1 sum 2) <(awk -F, '{printf "%04d %d\n",$1,$NF}' landing.csv | sed 1d)

解説するまでもなさそうだけど…一応、内容について記述。
ここでは、bashのプロセス置換(「<(ほにゃらら)」ってのがそれ)でコマンドの実行結果をdiffに渡している。
なので、それぞれのファイルの内容について同じフォーマットになるように編集して、diffに渡してやればよいのだ。

以下、それぞれのファイルの出力内容について。

1個目のファイル(monthly_typhoon)について

[root@BS-PUB-CENT7-01 work]# # sedで、年+月になってたとこの月を削除する
[root@BS-PUB-CENT7-01 work]# sed 's/.. / /' monthly_typhoon
1951 0
1951 0
1951 0
1951 0
1951 0
~省略~

[root@BS-PUB-CENT7-01 work]# # Datamashでグループ集計(sumで1列目が同じものを合計する)を行う
[root@BS-PUB-CENT7-01 work]# sed 's/.. / /' monthly_typhoon | datamash -t' ' -g 1 sum 2
1951 2
1952 3
1953 2
1954 5
1955 4
1956 3
1957 1
1958 4
~省略~

2個目のファイル(landing.csv)について

[root@BS-PUB-CENT7-01 work]# # awkで、1列目と最終列のみを出力させるようにする
[root@BS-PUB-CENT7-01 work]# awk -F, '{printf "%04d %d\n",$1,$NF}' landing.csv
0000 0
1951 2
1952 3
1953 2
1954 5
1955 4
1956 3
~省略~

[root@BS-PUB-CENT7-01 work]# # 1行目にいらん行が混じってるので削除
[root@BS-PUB-CENT7-01 work]# awk -F, '{printf "%04d %d\n",$1,$NF}' landing.csv | sed 1d
1951 2
1952 3
1953 2
1954 5
1955 4
1956 3
1957 1
~省略~

あとは、それぞれをdiffにプロセス置換で渡してやればよい。

Q3.

各月に台風が上陸する確率を求めるという問題。
これもdatamashを使って求めてみよう。ちょっと勘違いしていたが、"各月に"なので、1か月に2回以上来ていても1回と変わらないので注意。

sed 's/^....//' monthly_typhoon | awk '{if($2>0)$2=1;print $0}' | sort | datamash -t ' ' -g 1 mean 2
[root@BS-PUB-CENT7-01 work]# sed 's/^....//' monthly_typhoon | awk '{if($2>0)$2=1;print $0}' | sort | datamash -t ' ' -g 1 mean 2
01 0
02 0
03 0
04 0.015384615384615
05 0.030769230769231
06 0.13846153846154
07 0.4
08 0.63076923076923
09 0.63076923076923
10 0.2
11 0.015384615384615
12 0

一応、パイプごとの処理を分解。

[root@BS-PUB-CENT7-01 work]# # sedで、1列目の年+月を編集して月のみを表示させる
[root@BS-PUB-CENT7-01 work]# sed 's/^....//' monthly_typhoon
01 0
02 0
03 0
04 0
05 0
06 0
07 1
~省略~

[root@BS-PUB-CENT7-01 work]# # awkで、各月の件数を0以外すべて1にする
[root@BS-PUB-CENT7-01 work]# sed 's/^....//' monthly_typhoon | awk '{if($2>0)$2=1;print $0}'
01 0
02 0
03 0
04 0
05 0
06 0
07 1
~省略~

[root@BS-PUB-CENT7-01 work]# # 月ごとの値にソート
[root@BS-PUB-CENT7-01 work]# sed 's/^....//' monthly_typhoon | awk '{if($2>0)$2=1;print $0}' | sort
01 0
01 0
01 0
01 0
01 0
01 0
01 0
01 0
01 0
~省略~

[root@BS-PUB-CENT7-01 work]# # datamashでグループ集計を行う
[root@BS-PUB-CENT7-01 work]# sed 's/^....//' monthly_typhoon | awk '{if($2>0)$2=1;print $0}' | sort | datamash -t ' ' -g 1 mean 2
01 0
02 0
03 0
04 0.015384615384615
05 0.030769230769231
06 0.13846153846154
07 0.4
08 0.63076923076923
09 0.63076923076923
10 0.2
11 0.015384615384615
12 0

Q4.

各年で最初に台風が上陸した月を抽出し、何月が何回だったかを集計する。
ちゃんと問題読んでなかったので、トンチンカンな回答をtwitterにあげちゃったけど、まぁいいか。

これについては、datamash使わないほうが短く書けるかも。
念のため、どちらのバージョンも記述しておく。

awk '! $2~/0/' monthly_typhoon | uniq -w 4 | sed 's/^..../& /' | sort -k2n | datamash -t' ' -g 2 count 2 # Datamash使う版
awk '! $2~/0/' monthly_typhoon | uniq -w 4 | sed 's/^..../& /' | awk '{print $2}' | sort | uniq -c # Datamash使わない版
[root@BS-PUB-CENT7-01 work]# awk '! $2~/0/' monthly_typhoon | uniq -w 4 | sed 's/^..../& /' | sort -k2n | datamash -t' ' -g 2 count 2 # Datamash使う版
04 1
05 2
06 9
07 21
08 19
09 7
10 2
[root@BS-PUB-CENT7-01 work]# awk '! $2~/0/' monthly_typhoon | uniq -w 4 | sed 's/^..../& /' | awk '{print $2}' | sort | uniq -c # Datamash使わない版
      1 04
      2 05
      9 06
     21 07
     19 08
      7 09
      2 10

以下、パイプごとの処理内容について。

[root@BS-PUB-CENT7-01 work]# # $2が0"以外"のもののみを抽出する
[root@BS-PUB-CENT7-01 work]# awk '! $2~/0/' monthly_typhoon
195107 1
195110 1
195206 1
195207 1
195208 1
195306 1
195309 1
~省略~

[root@BS-PUB-CENT7-01 work]# # uniqコマンドの「-w」オプションで、頭4文字でユニークなもののみを 抽出させる
[root@BS-PUB-CENT7-01 work]# # (この場合、一番上の行のみが出力されるので、必然的に一番最初に台風が来た月だけ残る)
[root@BS-PUB-CENT7-01 work]# awk '! $2~/0/' monthly_typhoon | uniq -w 4
195107 1
195206 1
195306 1
195408 1
195507 1
195604 1
195709 1
~省略~

[root@BS-PUB-CENT7-01 work]# # 月の部分だけ必要なので、一旦「年+月」をスペースで分割する
[root@BS-PUB-CENT7-01 work]# awk '! $2~/0/' monthly_typhoon | uniq -w 4 | sed 's/^..../& /'
1951 07 1
1952 06 1
1953 06 1
1954 08 1
1955 07 1
1956 04 1
1957 09 1
~省略~

[root@BS-PUB-CENT7-01 work]# awk '! $2~/0/' monthly_typhoon | uniq -w 4 | sed 's/^..../& /' | sort -k2n
1956 04 1
1965 05 1
2003 05 1
1952 06 1
1953 06 1
1963 06 1
~省略~

[root@BS-PUB-CENT7-01 work]# # datamashで集計をかける
[root@BS-PUB-CENT7-01 work]# awk '! $2~/0/' monthly_typhoon | uniq -w 4 | sed 's/^..../& /' | sort -k2n | datamash -t' ' -g 2 count 2
04 1
05 2
06 9
07 21
08 19
09 7
10 2

Q5.

台風が上陸しなかった年を抽出する。
これもdatamash使わないほうがシンプルかも。

sed 's/^..../& /' monthly_typhoon | awk '$3~/0/' | datamash -t' ' -g 1 count 1 | grep 12$ | cut -c1-4 # datamash使う版
sed 's/^..../& /' monthly_typhoon | awk '$3~/0/ {count[$1]++}END{for(i in count){if(count[i]>11)print i}}' | sort -n # datamash使わない版
[root@BS-PUB-CENT7-01 work]# sed 's/^..../& /' monthly_typhoon | awk '$3~/0/' | datamash -t' ' -g 1 count 1 | grep 12$ | cut -c1-4 # datamash使う版
1984
1986
2000
2008
[root@BS-PUB-CENT7-01 work]# sed 's/^..../& /' monthly_typhoon | awk '$3~/0/ {count[$1]++}END{for(i in count){if(count[i]>11)print i}}' | sort -n # datamash使わない版
1984
1986
2000
2008

処理を分解したのがこちら。

[root@BS-PUB-CENT7-01 work]# # 年を削除する
[root@BS-PUB-CENT7-01 work]# sed 's/^..../& /' monthly_typhoon
1951 01 0
1951 02 0
1951 03 0
1951 04 0
1951 05 0
1951 06 0
1951 07 1
~省略~

[root@BS-PUB-CENT7-01 work]# # 台風が来ていない月のみを抽出しカウント、12だった場合はその年を表示させる
[root@BS-PUB-CENT7-01 work]# sed 's/^..../& /' monthly_typhoon | awk '$3~/0/ {count[$1]++}END{for(i in count){if(count[i]>11)print i}}'
2000
2008
1984
1986
[root@BS-PUB-CENT7-01 work]# # 数字でソートしておく
[root@BS-PUB-CENT7-01 work]# sed 's/^..../& /' monthly_typhoon | awk '$3~/0/ {count[$1]++}END{for(i in count){if(count[i]>11)print i}}' | sort -n
1984
1986
2000
2008

Q6.

台風から変わって、大阪市のひったくり件数についての集計。
まずはファイルのダウンロードから。

curl http://www.city.osaka.lg.jp/shimin/cmsfiles/contents/0000298/298810/006hittakuri2015.csv | nkf -wLux | tr , ' ' | tail -n +2 > hittakuri

Q6では、一列目のデータが何種類あるかを見る、という問題だ。
普通にawkやsort、uniq -cで回答すればいいし、awkだけでも解ける。ついでにdatamash版も書いておく。

awk '{count[$1]++}END{for(i in count)print i,count[i]"件"}' hittakuri # awkだけで解くパターン
sort hittakuri | datamash -t ' ' -g 1 count 1 # datamashで解くパターン
sort hittakuri | awk '{print $1}' | uniq -c # 模範解答と同じやり方
[root@BS-PUB-CENT7-01 work]# awk '{count[$1]++}END{for(i in count)print i,count[i]"件"}' hittakuri # awkだけで解くパターン
大阪市住之江区 9件
大阪市旭区 8件
大阪市西成区 37件
大阪市中央区 56件
大阪市大正区 7件
大阪市西区 28件
大阪市淀川区 31件
大阪市西淀川区 8件
大阪市此花区 1件
大阪市東住吉区 22件
大阪市城東区 23件
大阪市生野区 31件
大阪市港区 6件
大阪市東淀川区 17件
大阪市阿倍野区 19件
大阪市東成区 18件
大阪市天王寺区 20件
大阪市福島区 9件
大阪市北区 53件
大阪市鶴見区 6件
大阪市平野区 33件
大阪市住吉区 29件
大阪市都島区 15件
大阪市浪速区 22件
[root@BS-PUB-CENT7-01 work]# sort hittakuri | datamash -t ' ' -g 1 count 1 # datamashで解くパターン
大阪市阿倍野区 19
大阪市旭区 8
大阪市港区 6
大阪市此花区 1
大阪市住吉区 29
大阪市住之江区 9
大阪市城東区 23
大阪市生野区 31
大阪市西区 28
大阪市西成区 37
大阪市西淀川区 8
大阪市大正区 7
大阪市中央区 56
大阪市鶴見区 6
大阪市天王寺区 20
大阪市都島区 15
大阪市東住吉区 22
大阪市東成区 18
大阪市東淀川区 17
大阪市福島区 9
大阪市平野区 33
大阪市北区 53
大阪市淀川区 31
大阪市浪速区 22
[root@BS-PUB-CENT7-01 work]# sort hittakuri | awk '{print $1}' | uniq -c # 模範解答と同じやり方
     19 大阪市阿倍野区
      8 大阪市旭区
      6 大阪市港区
      1 大阪市此花区
     29 大阪市住吉区
      9 大阪市住之江区
     23 大阪市城東区
     31 大阪市生野区
     28 大阪市西区
     37 大阪市西成区
      8 大阪市西淀川区
      7 大阪市大正区
     56 大阪市中央区
      6 大阪市鶴見区
     20 大阪市天王寺区
     15 大阪市都島区
     22 大阪市東住吉区
     18 大阪市東成区
     17 大阪市東淀川区
      9 大阪市福島区
     33 大阪市平野区
     53 大阪市北区
     31 大阪市淀川区
     22 大阪市浪速区

これについてはまぁ、特に分解しての解説は不要だろう。

Q7.

各区の人口データ「population_h27sep」と組み合わせて、人口当たりの発生件数が高い区を調べる。
区の表記方法などは同じなので、joinでくっつけてしまい、あとはawkで割ればよい。ただし、awkで割るさいに注意したいのがそのまま割ると桁数が多いために科学表記になってしまうこと。
この場合は、awk内で「OFMT」の値として"%.6f"を与えるとよい。(その他、sprintfで指定してやる方法もある)

join <(awk '{count[$1]++}END{for(i in count)print i,count[i]}' hittakuri|sort) <(sort population_h27sep) | awk '{OFMT="%.6f"}{print $1,$2/$3}' | sort -k2nr

ここでも、Q2で行っていたプロセス置換を用いるのが楽だ。
以下、分解した内容。

[root@BS-PUB-CENT7-01 work]# # joinの左側。Q6のコマンドで各区ごとのひったくり件数を数え、ソートする
[root@BS-PUB-CENT7-01 work]# awk '{count[$1]++}END{for(i in count)print i,count[i]}' hittakuri|sort
大阪市阿倍野区 19
大阪市旭区 8
大阪市港区 6
大阪市此花区 1
大阪市住吉区 29
大阪市住之江区 9
大阪市城東区 23
大阪市生野区 31
大阪市西区 28
大阪市西成区 37
大阪市西淀川区 8
大阪市大正区 7
大阪市中央区 56
大阪市鶴見区 6
大阪市天王寺区 20
大阪市都島区 15
大阪市東住吉区 22
大阪市東成区 18
大阪市東淀川区 17
大阪市福島区 9
大阪市平野区 33
大阪市北区 53
大阪市淀川区 31
大阪市浪速区 22
[root@BS-PUB-CENT7-01 work]# # joinの右側。各区ごとの人口をソートして表示させる
[root@BS-PUB-CENT7-01 work]# sort population_h27sep
大阪市阿倍野区 107791
大阪市旭区 91169
大阪市港区 82391
大阪市此花区 67921
大阪市住吉区 154217
大阪市住之江区 124120
大阪市城東区 167765
大阪市生野区 128122
大阪市西区 90712
大阪市西成区 109328
大阪市西淀川区 97205
大阪市大正区 67338
大阪市中央区 94520
大阪市鶴見区 113073
大阪市天王寺区 73128
大阪市都島区 102768
大阪市東住吉区 130841
大阪市東成区 80972
大阪市東淀川区 171206
大阪市福島区 71054
大阪市平野区 199844
大阪市北区 117384
大阪市淀川区 172994
大阪市浪速区 64099
[root@BS-PUB-CENT7-01 work]# # ↑の2つをjoinでくっつけた結果
[root@BS-PUB-CENT7-01 work]# join <(awk '{count[$1]++}END{for(i in count)print i,count[i]}' hittakuri|sort) <(sort population_h27sep)
大阪市阿倍野区 19 107791
大阪市旭区 8 91169
大阪市港区 6 82391
大阪市此花区 1 67921
大阪市住吉区 29 154217
大阪市住之江区 9 124120
大阪市城東区 23 167765
大阪市生野区 31 128122
大阪市西区 28 90712
大阪市西成区 37 109328
大阪市西淀川区 8 97205
大阪市大正区 7 67338
大阪市中央区 56 94520
大阪市鶴見区 6 113073
大阪市天王寺区 20 73128
大阪市都島区 15 102768
大阪市東住吉区 22 130841
大阪市東成区 18 80972
大阪市東淀川区 17 171206
大阪市福島区 9 71054
大阪市平野区 33 199844
大阪市北区 53 117384
大阪市淀川区 31 172994
大阪市浪速区 22 64099
[root@BS-PUB-CENT7-01 work]# # awkで2列目/3列目を行う
[root@BS-PUB-CENT7-01 work]# join <(awk '{count[$1]++}END{for(i in count)print i,count[i]}' hittakuri|sort) <(sort population_h27sep) | awk '{OFMT="%.6f"}{print $1,$2/$3}'
大阪市阿倍野区 0.000176
大阪市旭区 0.000088
大阪市港区 0.000073
大阪市此花区 0.000015
大阪市住吉区 0.000188
大阪市住之江区 0.000073
大阪市城東区 0.000137
大阪市生野区 0.000242
大阪市西区 0.000309
大阪市西成区 0.000338
大阪市西淀川区 0.000082
大阪市大正区 0.000104
大阪市中央区 0.000592
大阪市鶴見区 0.000053
大阪市天王寺区 0.000273
大阪市都島区 0.000146
大阪市東住吉区 0.000168
大阪市東成区 0.000222
大阪市東淀川区 0.000099
大阪市福島区 0.000127
大阪市平野区 0.000165
大阪市北区 0.000452
大阪市淀川区 0.000179
大阪市浪速区 0.000343
[root@BS-PUB-CENT7-01 work]# # 最後に、一応ランキングにするためソートをかける
[root@BS-PUB-CENT7-01 work]# join <(awk '{count[$1]++}END{for(i in count)print i,count[i]}' hittakuri|sort) <(sort population_h27sep) | awk '{OFMT="%.6f"}{print $1,$2/$3}'|sort -k2nr
大阪市中央区 0.000592
大阪市北区 0.000452
大阪市浪速区 0.000343
大阪市西成区 0.000338
大阪市西区 0.000309
大阪市天王寺区 0.000273
大阪市生野区 0.000242
大阪市東成区 0.000222
大阪市住吉区 0.000188
大阪市淀川区 0.000179
大阪市阿倍野区 0.000176
大阪市東住吉区 0.000168
大阪市平野区 0.000165
大阪市都島区 0.000146
大阪市城東区 0.000137
大阪市福島区 0.000127
大阪市大正区 0.000104
大阪市東淀川区 0.000099
大阪市旭区 0.000088
大阪市西淀川区 0.000082
大阪市港区 0.000073
大阪市住之江区 0.000073
大阪市鶴見区 0.000053
大阪市此花区 0.000015

Q8.

Q8は、ひったくりが2件以上あった住所を調べる、という問題。
以下のコマンドで調べることができる。

awk '{print $1$2$3,$8$9$10}' hittakuri | sort | uniq -d
awk '{print $1$2$3,$8$9$10}' hittakuri | sort | uniq -dc # 件数付き
[root@BS-PUB-CENT7-01 work]# awk '{print $1$2$3,$8$9$10}' hittakuri | sort | uniq -d
大阪市北区角田町付近 2015年11月4日
大阪市北区曾根崎2丁目付近 2015年4月13日
大阪市淀川区十三本町1丁目付近 2015年4月16日
[root@BS-PUB-CENT7-01 work]# awk '{print $1$2$3,$8$9$10}' hittakuri | sort | uniq -dc # 件数付 き
      2 大阪市北区角田町付近 2015年11月4日
      2 大阪市北区曾根崎2丁目付近 2015年4月13日
      2 大阪市淀川区十三本町1丁目付近 2015年4月16日

これも単純な内容なので、特に分解は必要ないだろう。

Q9.

発生したひったくりの手段とその手段ごとの成功率(既遂)を求める問題。
無駄にdatamashにこだわってタイムアップしてしまった…

これは、残念ながら普通にawkで計算するほうがよい。

awk '{a[$7]+=($5~/既遂/);b[$7]++}END{for(i in a)print i,a[i]/b[i]}' hittakuri | sort -nr
[root@BS-PUB-CENT7-01 work]# awk '{a[$7]+=($5~/既遂/);b[$7]++}END{for(i in a)print i,a[i]/b[i]}' hittakuri | sort -nr
徒歩 0.942308
自動二輪 0.954225
自動車 0.904762
自転車 0.92053

全部が全部の問題で使えたわけではないが、多少はdatamashをうまいこと使えたのでよかった。
Q9ではうまく回答にはつなげれなかったけど、グループ対象を2つ指定することでこんなこともできるようだ。

awk '{print $7,$5}' hittakuri | sort | datamash -t ' ' -g 1,2 count 2
[root@BS-PUB-CENT7-01 work]# awk '{print $7,$5}' hittakuri | sort | datamash -t ' ' -g 1,2 count 2
自転車 既遂 139
自転車 未遂 12
自動車 既遂 19
自動車 未遂 2
自動二輪 既遂 271
自動二輪 未遂 13
徒歩 既遂 49
徒歩 未遂 3