コマンドラインでのアドホック性が高い分析は時代の変化とともに、csvからxml, 最近はmsgpack, jsonなどのデータフォーマットが利用されます
jsonはその生い立ちが、設計・開発されたものではなく、JavaScriptのデータフォーマットから偶然発見されたものでした
Apache HadoopやAWS EMR、Google Dataflow, Apache Beamなどで任意のシリアライズ方法が利用できますが、その中でも割と一般的な技術がjsonです。
ビッグデータで利用されてきた知見をローカルでも利用できる一つの手段としてjqと呼ばれるJavaScriptのjsonフォーマット加工に最適化されたインタプリターが利用できます
コードの全体はここに保存されているので、適宜参照利用してほしい
jqだけで全てが完結することをあまり期待しないほうがいいと考えています、
jqはPerlの様に匿名変数が多数利用できて、コードが短くかける代わりに、複雑なコードは書きづらいです
jq(場合によっては、HadoopやBeamなど)で利用するために、CSVのフォーマットをjsonに変換します
そのために今回はRubyを手続きが多い場面に利用しました
$ cat vehicles.csv | ruby csv2json.rb
$ cat ${SOME_ROW_JSONS} | ruby type_infer.rb
$ cat ${ANY_ROW_JSONS} | ruby to_list.rb
PATH=$HOME/jq-ruby-shell-data-analysis/:$PATH
alias conv='csv2json.rb | type_infer.rb | to_list.rb'
これを追記することで、シェルからconvをcsvでパイプで繋ぐと、jqで処理できるようになります
$ cat vehicles.csv | head -n 10
スライシングの指定の仕方では途中を切り取ることもできる
$ cat vehicles.csv | conv | jq '.[:10]'
$ cat vehicles.csv | cut -f1
フィールドをリテラルを指定できる
$ cat vehicles.csv | conv | jq '.[].barrels08
$ cat vehicles.csv | wc -l
$ cat vehicles.csv | conv | jq '. | length'
$ cat vehicles.csv | sort -k,k
$ cat vehicles.csv | conv | jq 'sort_by(.fuelCost08)'
$ cat vehicles.csv | egrep T...ta
$ cat vehicles.csv | conv | jq '.[] | .make | select(test("T....a"))'
よく使うSQLのパターンと等価な例をいくつか示します
maphは入力にList(Array)を期待して、一つ一つの要素に適応する処理を記します 戻り値はListです
$ cat vehicles.csv | conv | jq 'map({"make":.make, "model":.model})'
入力のリストに対してプロパティを指定して評価
$ head -n 1000 vehicles.csv | conv | jq 'select(.[].make == "Toyota")' | less
mapを介して評価する方法もあります
$ cat vehicles.csv | conv | jq 'map(select(.make == "Toyota"))' | less
燃料の全ての和をとります
$ 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ができるかできないかが割と分かれめな気がしていますが、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
$ cat vehicles.csv | conv | jq 'group_by(.make) | map({(.[0].make): length}) | add'
キーが存在しない要素を排除します
$ cat vehicles.csv | conv | jq 'select(.[].fuelCost08)'
例えば、車のメーカごとの燃料の総和を取るとこうなります
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の出番は増えていくものと思われます。
使えると便利ですよ