jqでデータ分析

awkの代替としてのjq

コマンドラインでのアドホック性が高い分析は時代の変化とともに、csvからxml, 最近はmsgpack, jsonなどのデータフォーマットが利用されます

jsonはその生い立ちが、設計・開発されたものではなく、JavaScriptのデータフォーマットから偶然発見されたものでした

Apache HadoopやAWS EMR、Google Dataflow, Apache Beamなどで任意のシリアライズ方法が利用できますが、その中でも割と一般的な技術がjsonです。  

ビッグデータで利用されてきた知見をローカルでも利用できる一つの手段としてjqと呼ばれるJavaScriptのjsonフォーマット加工に最適化されたインタプリターが利用できます

コードの全体はここに保存されているので、適宜参照利用してください

jqにcsvを投入する前に前処理

jqだけで全てが完結することをあまり期待しないほうがいいと考えています、

jqはPerlの様に匿名変数が多数利用できて、コードが短くかける代わりに、複雑なコードは書きづらいです

jq(場合によっては、HadoopやBeamなど)で利用するために、CSVのフォーマットをjsonに変換します

そのために今回はRubyを手続きが多い場面に利用しました

CSV to JSON

$ cat vehicles.csv | ruby csv2json.rb 

型がないJSONを適切な型にキャストする

$ cat ${SOME_ROW_JSONS} | ruby type_infer.rb

リストにラップアップしてjqで処理できる様にする

$ cat ${ANY_ROW_JSONS} | ruby to_list.rb

三回も変換をかけるのを面倒なので、bashrcにaliasを設定しておきます

PATH=$HOME/jq-ruby-shell-data-analysis/:$PATH
alias conv='csv2json.rb | type_infer.rb | to_list.rb'

これを追記することで、シェルからconvをcsvでパイプで繋ぐと、jqで処理できるようになります

コマンドラインの基本ツール群をjqで再現する

head vs jq

head

$ cat vehicles.csv | head -n 10  

jq

スライシングの指定の仕方では途中を切り取ることもできる  

$ cat vehicles.csv | conv | jq '.[:10]'

cut vs jq

cut

$ cat vehicles.csv | cut -f1  

jq

フィールドをリテラルを指定できる  

$ cat vehicles.csv | conv | jq '.[].barrels08

wc vs jq

wc

$ cat vehicles.csv | wc -l

jq

$ cat vehicles.csv | conv | jq '. | length'

sort vs jq

sort

$ cat vehicles.csv | sort -k,k

jq

$ cat vehicles.csv | conv | jq 'sort_by(.fuelCost08)'

grep vs jq

grep

$ cat vehicles.csv | egrep T...ta

jq

$ cat vehicles.csv | conv | jq '.[] | .make | select(test("T....a"))'

SQLと等価な操作の例

よく使うSQLのパターンと等価な例をいくつか示します

map

maphは入力にList(Array)を期待して、一つ一つの要素に適応する処理を記します  
戻り値はListです

$ cat vehicles.csv | conv | jq 'map({"make":.make, "model":.model})' 

filter, select

入力のリストに対してプロパティを指定して評価

$ head -n 1000 vehicles.csv | conv | jq 'select(.[].make == "Toyota")' | less

mapを介して評価する方法もあります

$ cat vehicles.csv | conv | jq 'map(select(.make == "Toyota"))' | less

reduce

燃料の全ての和をとります

$ cat vehicles.csv | conv | jq 'reduce .[].fuelCost08 as $fc (0; . + $fc)'

副作用を数字以外にもListの様なオブジェクトを指定することができます  
この例ではmodel(車種)を全てリストアップします

$ cat vehicles.csv  | conv| jq 'reduce .[].model as $model ([]; . + [$model] )'

group by

ほとんどのデータ分析に置いて、group byができるかできないかが割と分かれめな気がしていますが、jqはできます

基本系はこれです

$ head -n 100 vehicles.csv | conv | jq 'group_by(.make)[]'

例えば、group byしたキーをつけてdict型にしたいときなどはこの様にします

$ cat vehicles.csv | conv | jq 'group_by(.make)[] | {(.[0].make): [.[] | .]}' | less 

Examples

Example)特定の要素をカウントする

$ cat vehicles.csv | conv | jq 'group_by(.make) | map({(.[0].make): length}) | add'

Example)Object型から、特定のキーが存在するものを選ぶ

キーが存在しない要素を排除します

$ cat vehicles.csv | conv | jq 'select(.[].fuelCost08)'

Example)GroupByした値に対して複雑なオペレーション

例えば、車のメーカごとの燃料の総和を取るとこうなります

to_entriesってなんのためにあるのかわからなかったのですが、この様なデータ変換して次の処理に渡すときに便利ですね

$ cat vehicles.csv | conv | jq 'group_by(.make) | map({(.[0].make): . }) | add | to_entries' |  jq '[.[] | { (.key): (.value | map(.fuelCost08) | add)} ] | add' | less                            

各メーカの車の燃費の平均値

$ cat vehicles.csv | conv  | jq 'group_by(.make) | map({(.[0].make): . }) | add | to_entries' |  jq '[.[] | { (.key): ((.value | map(.fuelCost08) | add)/(.value | length))} ] | add' | less

まとめ

jqはすごいのですが、データ分析におけるシェルの重要性が何度か語られますが、いずれも入力をCSVとする際の手法ですが、jqは本気で使えばjsonでSQL並みのことはやることができます

awsの標準ツールのawscliやgcpのgclout-toolもレポート機能がjsonで帰ってくることが多いので、jsonの出番は増えていくものと思われます。

使えると便利ですよ

Sign up for free and join this conversation.
Sign Up
If you already have a Qiita account log in.