
JSONataで集計処理をやってみた
こんにちは。サービス開発室の武田です。
jqコマンドでgroup byやmaxなどの集計処理をやってみたという記事を見かけたので、練習がてら、同じことをJSONataでもやってみました。
参考にさせていただいたのはこちらのブログです。ありがとうございます!
また今回CLIで実行するにあたっては、拙作jt
を使用しました。
AWS CLIと絡めてJSONata使ってみたのはこちら(jt-cli開発前なのでjfqを使っています)。
サンプルのJSON
参考のブログと同じデータを使用します。同じ結果となるようにクエリを書いていきましょう。
JSON=$(cat << EOS
[
{
"user": "alice",
"item": "apple",
"price": 100,
"created_at": "2021-12-25T17:00:00Z"
},
{
"user": "bob",
"item": "orange",
"price": 150,
"created_at": "2021-12-25T17:10:00Z"
},
{
"user": "carol",
"item": "apple",
"price": 100,
"created_at": "2021-12-25T17:20:00Z"
},
{
"user": "alice",
"item": "orange",
"price": 150,
"created_at": "2021-12-25T17:30:00Z"
},
{
"user": "alice",
"item": "banana",
"price": 200,
"created_at": "2021-12-25T17:40:00Z"
},
{
"user": "carol",
"item": "apple",
"price": 100,
"created_at": "2021-12-25T17:50:00Z"
}
]
EOS
)
最大値
created_at
が最大の値を取得します。ですが、実はJSONataにはjqのmax_by
にあたる関数はありません。そのため、愚直に書くなら、次のようになります。
$ echo $JSON | jt '$reduce($, function($acc, $item) { $item.created_at > $acc.created_at ? $item : $acc })'
{
"user": "carol",
"item": "apple",
"price": 100,
"created_at": "2021-12-25T17:50:00Z"
}
計算量は増えますが、よりスマートに書くなら、次のような書き方もできます。created_at
で降順ソートして最初の値を取り出しています。
echo $JSON | jt '$^(>created_at)[0]'
合計値
price
の合計を求めます。JSONataでは$sum
で簡単です。
$echo $JSON | jt '$sum(price)'
800
配列の数
配列の要素数は$count
を使えば簡単です。
$ echo $JSON | jt '$count($)'
6
グループごとの最大値
各ユーザーごとにcreated_at
が最大の値を取得します。jqではgroup_by
やmax_by
など便利な関数が用意されていますが、JSONataでは標準で提供されていません。その代わりパスオペレーターなどを駆使することで同等のことが実現できます。
$ echo $JSON | jt '${user:$^(>created_at)[0]}.*'
[
{
"user": "alice",
"item": "banana",
"price": 200,
"created_at": "2021-12-25T17:40:00Z"
},
{
"user": "bob",
"item": "orange",
"price": 150,
"created_at": "2021-12-25T17:10:00Z"
},
{
"user": "carol",
"item": "apple",
"price": 100,
"created_at": "2021-12-25T17:50:00Z"
}
]
ややこしいので分解して見てみましょう。${}
という記法は要素をグルーピングするものです(jqのgroup_by
に近い)。user
の値をキーにしてグルーピングします。
$ echo $JSON | jt '${user:$}'
{
"alice": [
{
"user": "alice",
"item": "apple",
"price": 100,
"created_at": "2021-12-25T17:00:00Z"
},
{
"user": "alice",
"item": "orange",
"price": 150,
"created_at": "2021-12-25T17:30:00Z"
},
{
"user": "alice",
"item": "banana",
"price": 200,
"created_at": "2021-12-25T17:40:00Z"
}
],
"bob": {
"user": "bob",
"item": "orange",
"price": 150,
"created_at": "2021-12-25T17:10:00Z"
},
"carol": [
{
"user": "carol",
"item": "apple",
"price": 100,
"created_at": "2021-12-25T17:20:00Z"
},
{
"user": "carol",
"item": "apple",
"price": 100,
"created_at": "2021-12-25T17:50:00Z"
}
]
}
各グループごとに最大の値が欲しいので、最初に使ったテクニックを使用します。
$ echo $JSON | jt '${user:$^(>created_at)[0]}'
{
"alice": {
"user": "alice",
"item": "banana",
"price": 200,
"created_at": "2021-12-25T17:40:00Z"
},
"bob": {
"user": "bob",
"item": "orange",
"price": 150,
"created_at": "2021-12-25T17:10:00Z"
},
"carol": {
"user": "carol",
"item": "apple",
"price": 100,
"created_at": "2021-12-25T17:50:00Z"
}
}
グループのキーが残っていますので、.*
で取り除きます。
$ echo $JSON | jt '${user:$^(>created_at)[0]}.*'
[
{
"user": "alice",
"item": "banana",
"price": 200,
"created_at": "2021-12-25T17:40:00Z"
},
{
"user": "bob",
"item": "orange",
"price": 150,
"created_at": "2021-12-25T17:10:00Z"
},
{
"user": "carol",
"item": "apple",
"price": 100,
"created_at": "2021-12-25T17:50:00Z"
}
]
グループごとの合計値
ユーザーごとにprice
の合計を求めます。グループ処理ですので、基本テクニックは先ほどと同じです。
$ echo $JSON | jt '${user:$sum(price)}.$each(function($v, $k) {{"user": $k, "price": $v}})'
[
{
"user": "alice",
"price": 450
},
{
"user": "bob",
"price": 150
},
{
"user": "carol",
"price": 200
}
]
$each
が何をしているのか確認しておきましょう。これがないと次のようになります。
$ echo $JSON | jt '${user:$sum(price)}'
{
"alice": 450,
"bob": 150,
"carol": 200
}
1オブジェクトの各キーと値のペアについて、{"user": key, "price": value}
の形に整形できれば目的の形になります。$each
はまさしく各キーと値のペアに対して処理をする関数です。
ちなみにこの処理はいろいろな書き方ができそうで、次の2パターンを追加で考えてみました。
$ echo $JSON | jt '${user:$sum(price)}.$spread().{"user": $keys(), "price": *}'
$ echo $JSON | jt '${user:$sum(price)} ~> $each(function($v, $k) {{"user": $k, "price": $v}})'
グループごとの配列の数
ユーザーごとに要素数を求めます。ほぼ同じ書き方ができます。
$ echo $JSON | jt '${user:$count($)} ~> $each(function($v, $k) {{"user": $k, "length": $v}})'
[
{
"user": "alice",
"length": 3
},
{
"user": "bob",
"length": 1
},
{
"user": "carol",
"length": 2
}
]
グループごとの平均
ユーザーごとにprice
の平均を求めます。これも書き方はほぼ同じです。
$ echo $JSON | jt '${user:$average(price)} ~> $each(function($v, $k) {{"user": $k, "average": $v}})'
[
{
"user": "alice",
"average": 150
},
{
"user": "bob",
"average": 150
},
{
"user": "carol",
"average": 100
}
]
集計後の並び替え
price
の合計をユーザーごとに求め、その値でソートします。
$ echo $JSON | jt '${user:$sum(price)} ~> $each(function($v, $k) {{"user": $k, "price": $v}})^(price)'
[
{
"user": "bob",
"price": 150
},
{
"user": "carol",
"price": 200
},
{
"user": "alice",
"price": 450
}
]
絞り込んでから集計
フィルターを追加([created_at >= "2021-12-25T17:30:00Z"]
)するだけの簡単なお仕事です。
$ echo $JSON | jt '$[created_at >= "2021-12-25T17:30:00Z"]{user: $count($)} ~> $each(function($v, $k) {{"user": $k, "length": $v}})'
[
{
"user": "alice",
"length": 2
},
{
"user": "carol",
"length": 1
}
]
reduceを使った集計
最初に$reduce
使っていますが、ここでは$sum
相当の処理を$reduce
で書いてみようということですね。
$ echo $JSON | jt '${user:$reduce(price, function($a, $p) { $a + $p })} ~> $each(function($v, $k) {{"user": $k, "price": $v}})'
[
{
"user": "alice",
"price": 450
},
{
"user": "bob",
"price": 150
},
{
"user": "carol",
"price": 200
}
]
まとめ
グルーピングやソート処理などを絡めたクエリを書く練習をしてみました。こういった処理をスムーズに書けると、集計処理も捗りますね!
ちなみにjt-cliでは入力がJSONだけでなくJSON LinesやCSVも対応していますので、きっと役立ってくれるはずです。