27
クロス集計をbash(awk)だけで実装した話 2012-12-02 @iktakahiro ~ bash最強伝説 : ~

クロス集計をBash(とawk)だけで実装した話

Embed Size (px)

DESCRIPTION

みんな大好きシェル芸シリーズ。 元々社内用に書いていたシェルスクリプトでしたが、面白かったのでまとめました。 cut, paste, sed, awk などを駆使してTSV形式のクロス集計表加工まで行った話。 Github https://github.com/iktakahiro/gis/tree/master/src/crosstabj_3columns

Citation preview

Page 1: クロス集計をBash(とawk)だけで実装した話

クロス集計をbash(とawk)だけで実装した話

2012-12-02@iktakahiro

~ bash最強伝説 : 序 ~

Page 2: クロス集計をBash(とawk)だけで実装した話

自己紹介@iktakahiro

株式会社ALBERT システム開発部

レコメンドエンジンとか作ってるよ!

最近気になっていること: 破とQの間に伊吹マヤに何が起こったのか。

Page 3: クロス集計をBash(とawk)だけで実装した話

このスライドに書いてあること

それなりのデータ量(※1)のログファイルをbashスクリプトでクロス集計表にするまで行ったというお話。

※1 数GBとか、数千万行とか。それ以上は試してない。

Page 4: クロス集計をBash(とawk)だけで実装した話

クロス集計とは?

ピボットテーブルと言うやつです。性別 血液型

男子 A型

女子 A型

男子 O型

女子 B型

男子 AB型

女子 A型

A型 B型 O型 AB型

男子

女子

1 0 1 1

2 1 0 0

Page 5: クロス集計をBash(とawk)だけで実装した話

カウントのパターンもあれば、合算のパターンもある(今回はこっち)。

ユーザー 商品 金額

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

Page 6: クロス集計をBash(とawk)だけで実装した話

Excel => 乙。

Rでやれば良いじゃない => ちょっと前まではやってたけど、メモリ 馬鹿食い&積んでも別の制約にひっかかかる

CとかJAVAでry =>

カジュアルなクロス集計の選択肢

Page 7: クロス集計をBash(とawk)だけで実装した話

bashでやろう。

Page 8: クロス集計をBash(とawk)だけで実装した話

✓Linux/Unix あればおk✓cutでカラム抽出簡単✓catで縦マージpasteで横マージ✓sort超速い <= Point1✓実はJOINできる <= Point2✓S・E・D ! S・E・D ! ※ さすがに一部の処理はしんどいのでawkでフォロー

なぜbashか?

Page 9: クロス集計をBash(とawk)だけで実装した話

テキストファイル処理との親和性が半端じゃないです。

TSVファイルのカラム入れ換えとか抽出とか、正規表現置換とかで苦労している人はいますぐ覚えましょう。

どんなに巨大なファイルでもheadとtailで中身覗ける。(今回は登場しないけど)

シェルスクリプトのTips的な要素もあり。

Page 10: クロス集計をBash(とawk)だけで実装した話

1) 必要なカラムを抽出

2) クロス集計の行と列の要素を取り出す

3) awkでファイル分割する

4) awkで集計する(sum + group by)

5) sort でソート

6) 2)で取り出した行の要素と 5)の結果をJOIN

7) paste と cat でマージしてリストを表に加工

処理の流れ

Page 11: クロス集計をBash(とawk)だけで実装した話

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月

必要なものだけ抽出されていればこの処理は不要だけど、データ量が多いとそれもしんどいので一番差最初にやる。

抽出するのは以降の処理対象のデータを減らしたいというのが理由。

[月] の列は要らない。

Page 12: クロス集計をBash(とawk)だけで実装した話

# 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

Page 13: クロス集計をBash(とawk)だけで実装した話

# クロス集計表の行になる部分を抽出してユニーク化&ソート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)クロス集計の行と列の要素を取り出す

Page 14: クロス集計をBash(とawk)だけで実装した話

# クロス集計表の列になる部分を抽出してユニーク化&ソート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

Page 15: クロス集計をBash(とawk)だけで実装した話

# 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

Page 16: クロス集計をBash(とawk)だけで実装した話

# 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をかけてるよ。

Page 17: クロス集計をBash(とawk)だけで実装した話

6) 2)で取り出した行の要素と 5)の結果をJOIN... 前に一応今回のポイントなので解説。

クロス集計表の作成がなぜめんどくさいか? => レコードがないものを 0 で埋めなければいけないから。

アイス ジュース OREO

Aさん

Bさん

Iさん

310 0 0

130 120 0

0 0 630

[ Aさん x OREO ] とか[ Iさん x アイス] とかがそう。

Page 18: クロス集計をBash(とawk)だけで実装した話

0 で埋めないと(nullでもいいけど)マージがうまくいかなくて残念なことになっちゃう。

ユーザー アイス ジュース OREO

Aさん 310 120 630

Bさん 130

???

アイス買った人 合計金額

Aさん 310

Bさん 130

ジュース買った人 合計金額

Bさん 120

豚 合計金額

Iさん 630

それぞれ行の数がばらばら。分割したファイルに漏れなく要素が存在するわけではない。

Page 19: クロス集計をBash(とawk)だけで実装した話

で、join コマンドです。

Page 20: クロス集計をBash(とawk)だけで実装した話

# 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!

Page 21: クロス集計をBash(とawk)だけで実装した話

# 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

Page 22: クロス集計をBash(とawk)だけで実装した話

# 見出しとなる行、列と、分割処理した集計結果をマージ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

Page 23: クロス集計をBash(とawk)だけで実装した話

できあがり!

アイス ジュース OREO

Aさん

Bさん

Iさん

310 0 0

130 120 0

0 0 630

※ できあがるのはTSVファイル。

Page 24: クロス集計をBash(とawk)だけで実装した話

ベンチマーク

レコード

ファイルサイズ

クロス結果行

クロス結果列

メモリ

処理時間

2,500万

800MB

300万

30

16GB

5分 Githubでスクリプトを公開するのでみんなでベンチマークしてね!

仮想環境上のCentOSでやったのでI/Oはかなり遅いです。

DBで2,500万インサートとかそれだけで死ねる。

Page 25: クロス集計をBash(とawk)だけで実装した話

補足ファイル分割しているので、集計処理を並列化することもできる。並列実行は & とか xargs とかを使うと良いよ!あまり大きい固まりをawkに投げると帰ってこなくなるしね。

joinするには 対象のファイル(のキー)がソート済みである必要があります。

joinには一致しなかった行だけを出力する -v オプションも。

bashでsortする場合は問答無用で export LC_ALL=C しる!

手元では事故ったことないけどソート周りに罠がありそう。

Page 26: クロス集計をBash(とawk)だけで実装した話

まとめ

bash最強!!

AAAヴンダーってなんかもう

Page 27: クロス集計をBash(とawk)だけで実装した話

おわり。