Upload
takahiro-ikeuchi
View
9.066
Download
6
Embed Size (px)
DESCRIPTION
みんな大好きシェル芸シリーズ。 元々社内用に書いていたシェルスクリプトでしたが、面白かったのでまとめました。 cut, paste, sed, awk などを駆使してTSV形式のクロス集計表加工まで行った話。 Github https://github.com/iktakahiro/gis/tree/master/src/crosstabj_3columns
Citation preview
クロス集計をbash(とawk)だけで実装した話
2012-12-02@iktakahiro
~ bash最強伝説 : 序 ~
自己紹介@iktakahiro
株式会社ALBERT システム開発部
レコメンドエンジンとか作ってるよ!
最近気になっていること: 破とQの間に伊吹マヤに何が起こったのか。
このスライドに書いてあること
それなりのデータ量(※1)のログファイルをbashスクリプトでクロス集計表にするまで行ったというお話。
※1 数GBとか、数千万行とか。それ以上は試してない。
クロス集計とは?
ピボットテーブルと言うやつです。性別 血液型
男子 A型
女子 A型
男子 O型
女子 B型
男子 AB型
女子 A型
A型 B型 O型 AB型
男子
女子
1 0 1 1
2 1 0 0
カウントのパターンもあれば、合算のパターンもある(今回はこっち)。
ユーザー 商品 金額
Aさん アイス 130
Aさん アイス 180
Bさん ジュース 120
Bさん アイス 130
Iさん OREO 210
Iさん OREO 210
Iさん OREO 210
アイス ジュース OREO
Aさん
Bさん
Iさん
310 0 0
130 120 0
0 0 630
Excel => 乙。
Rでやれば良いじゃない => ちょっと前まではやってたけど、メモリ 馬鹿食い&積んでも別の制約にひっかかかる
CとかJAVAでry =>
カジュアルなクロス集計の選択肢
bashでやろう。
✓Linux/Unix あればおk✓cutでカラム抽出簡単✓catで縦マージpasteで横マージ✓sort超速い <= Point1✓実はJOINできる <= Point2✓S・E・D ! S・E・D ! ※ さすがに一部の処理はしんどいのでawkでフォロー
なぜbashか?
テキストファイル処理との親和性が半端じゃないです。
TSVファイルのカラム入れ換えとか抽出とか、正規表現置換とかで苦労している人はいますぐ覚えましょう。
どんなに巨大なファイルでもheadとtailで中身覗ける。(今回は登場しないけど)
シェルスクリプトのTips的な要素もあり。
1) 必要なカラムを抽出
2) クロス集計の行と列の要素を取り出す
3) awkでファイル分割する
4) awkで集計する(sum + group by)
5) sort でソート
6) 2)で取り出した行の要素と 5)の結果をJOIN
7) paste と cat でマージしてリストを表に加工
処理の流れ
1)必要なカラムを抽出
ユーザー 商品 金額 月
Aさん アイス 130 12月
Aさん アイス 180 11月
Bさん ジュース 120 11月
Bさん アイス 130 12月
Iさん OREO 210 11月
Iさん OREO 210 10月
Iさん OREO 210 2月
必要なものだけ抽出されていればこの処理は不要だけど、データ量が多いとそれもしんどいので一番差最初にやる。
抽出するのは以降の処理対象のデータを減らしたいというのが理由。
[月] の列は要らない。
# 1, 2, 3 カラム目を抽出したいのでcat hoge.txt |awk {print $1, $2, $3}' > tmp_data.txt
ユーザー 商品 金額 月
Aさん アイス 130 12月
Aさん アイス 180 11月
Bさん ジュース 120 11月
Bさん アイス 130 12月
Iさん OREO 210 11月
Iさん OREO 210 10月
Iさん OREO 210 2月
ユーザー 商品 金額
Aさん アイス 130
Aさん アイス 180
Bさん ジュース 120
Bさん アイス 130
Iさん OREO 210
Iさん OREO 210
Iさん OREO 210
# クロス集計表の行になる部分を抽出してユニーク化&ソートcut -f 1 -d , tmp_data.txt |sort -u > rowname.txt
ユーザー 商品 金額
Aさん アイス 130
Aさん アイス 180
Bさん ジュース 120
Bさん アイス 130
Iさん OREO 210
Iさん OREO 210
Iさん OREO 210
ユーザー
Aさん
Bさん
Iさん
MySQLとかの感覚だとこの処理は遅そうですが速いです。
2)クロス集計の行と列の要素を取り出す
# クロス集計表の列になる部分を抽出してユニーク化&ソートcut -f 2 -d , tmp_data.txt |sort -u > tmp_header.txt# 改行をタブに変えて縦横変換tr '\n' '\t' < tmp_header.txt > header.txt
ユーザー 商品 金額
Aさん アイス 130
Aさん アイス 180
Bさん ジュース 120
Bさん アイス 130
Iさん OREO 210
Iさん OREO 210
Iさん OREO 210
商品
アイス
ジュース
OREO
MySQLとかの感覚だとこの処理は遅そうですが速いです。(二回目
商品 アイス ジュース OREO
# tmp_data.txt を 2列目の中身でファイル分割awk -F, '{print > "split_"$2}' tmp_data.txt
ユーザー 商品 金額
Aさん アイス 130
Aさん アイス 180
Bさん ジュース 120
Bさん アイス 130
Iさん OREO 210
Iさん OREO 210
Iさん OREO 210
3)awkでファイル分割する
ユーザー 商品 金額
Aさん アイス 130
Aさん アイス 180
Bさん アイス 130
ユーザー 商品 金額
Bさん ジュース 120
ユーザー 商品 金額
Iさん OREO 210
Iさん OREO 210
Iさん OREO 210
# tmp_data.txt を 2列目の中身でファイル分割awk -F, '{a[$1]+=$3;}END{for(i in a)print i","a[i];}' split_${file} | sort -t 1 > sort_${file}
4)awkで集計する, 5) sortでソートする
ユーザー 商品 金額
Aさん アイス 130
Aさん アイス 180
Bさん アイス 130
ユーザー 商品 金額
Bさん ジュース 120
ユーザー 商品 金額
Iさん OREO 210
Iさん OREO 210
Iさん OREO 210
アイス買った人 合計金額
Aさん 310
Bさん 130
ジュース買った人 合計金額
Bさん 120
豚 合計金額
Iさん 630
このawkの処理は色々使えるので覚えてね!集計結果をパイプで渡してsortをかけてるよ。
6) 2)で取り出した行の要素と 5)の結果をJOIN... 前に一応今回のポイントなので解説。
クロス集計表の作成がなぜめんどくさいか? => レコードがないものを 0 で埋めなければいけないから。
アイス ジュース OREO
Aさん
Bさん
Iさん
310 0 0
130 120 0
0 0 630
[ Aさん x OREO ] とか[ Iさん x アイス] とかがそう。
0 で埋めないと(nullでもいいけど)マージがうまくいかなくて残念なことになっちゃう。
ユーザー アイス ジュース OREO
Aさん 310 120 630
Bさん 130
???
アイス買った人 合計金額
Aさん 310
Bさん 130
ジュース買った人 合計金額
Bさん 120
豚 合計金額
Iさん 630
それぞれ行の数がばらばら。分割したファイルに漏れなく要素が存在するわけではない。
で、join コマンドです。
# 2)で取り出した行の要素と 5)の結果をJOIN からの~ S・E・D !join -t ',' -1 1 -2 1 -a 1 rowname.txt sort_${file} | awk -F, '{print $2}' | sed 's/^$/0/g' > split_sum_${file}
アイス買った人 合計金額
Aさん 310
Bさん 130
ジュース買った人 合計金額
Bさん 120
豚 合計金額
Iさん 630
ユーザー
Aさん
Bさん
Iさん
アイス買った人 合計金額Aさん 310Bさん 130Iさん 0
ジュース買った人 合計金額Aさん 0Bさん 120Iさん 0
OREO買った人 合計金額Aさん 0Bさん 0Iさん 630
JOIN !
Excellent!
# JOIN の解説join -t ',' -1 1 -2 1 -a 1 rowname.txt sort_${file}
join コマンドは、デフォルトでは join 可能だった行のみを出力する。ただし、-a オプションをつけると、「不一致でjoinできなかったキー」も含めて出力してくれる。
Aさん
Bさん
Iさん
JOIN !Bさん 120
AさんBさん 120Iさん
# この結果の2列目を awk で抽出し、空行に sed で 0 を挿入している。awk -F, '{print $2}' | sed 's/^$/0/g' > split_sum_${file}
01200
# 見出しとなる行、列と、分割処理した集計結果をマージcp header.txt sum_result.txtpaste rowname.txt split_sum_* >> sum_result.txt
7)paste で マージしてリストを表に加工
商品 アイス ジュース OREO
ユーザー
Aさん
Bさん
Iさん
310
130
0
0
120
0
0
0
630
できあがり!
アイス ジュース OREO
Aさん
Bさん
Iさん
310 0 0
130 120 0
0 0 630
※ できあがるのはTSVファイル。
ベンチマーク
レコード
ファイルサイズ
クロス結果行
クロス結果列
メモリ
処理時間
2,500万
800MB
300万
30
16GB
5分 Githubでスクリプトを公開するのでみんなでベンチマークしてね!
仮想環境上のCentOSでやったのでI/Oはかなり遅いです。
DBで2,500万インサートとかそれだけで死ねる。
補足ファイル分割しているので、集計処理を並列化することもできる。並列実行は & とか xargs とかを使うと良いよ!あまり大きい固まりをawkに投げると帰ってこなくなるしね。
joinするには 対象のファイル(のキー)がソート済みである必要があります。
joinには一致しなかった行だけを出力する -v オプションも。
bashでsortする場合は問答無用で export LC_ALL=C しる!
手元では事故ったことないけどソート周りに罠がありそう。
まとめ
bash最強!!
AAAヴンダーってなんかもう
おわり。