
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も対応していますので、きっと役立ってくれるはずです。








