Rでndjson形式のログを解析する

by
カテゴリ:

最近、ndjson形式のログをRで解析しました。やはりtidyverseを使える体験のよさは他の追随を許しません。

ただ、ndjson形式を直接読み込む方法を知らずに、jqコマンドを使って通常のJSON形式に変換してから読み込んでいました(cat file.ndjson | jq -c -s . > file.json)。読み込みからRで完結したいと思ったので、方法を調べてみました。

結論としては、用途に応じていくつかのパターンがありそうでした。

  1. jsonlite::stream_in関数でデータフレーム化する
    • 簡単
    • 巨大なファイルの読み込みには時間がかかる
  2. duckdbパッケージを使ってデータフレーム化する
    • 読み込みが高速
    • (a)より一手間かかる
    • メモリより大きなファイルは扱えない
  3. duckdbパッケージを使ってインメモリデータベース化する
    • 読み込み・計算共に高速で省メモリ
    • (a)より一手間かかる
    • 集計に利用可能な関数が限られる

ndjson形式とは

ndjson形式は、1行ごとにJSON形式のデータが記述されたファイル形式です。

{"timestamp": "2024-10-07T00:00:00", "message": "start processing", "task": "1"}
{"timestamp": "2024-10-07T00:00:02", "message": "finish processing", "task": "1"}

いわゆるプレインテキストのログと異なり、行ごとにJSONとしてパースできるため、読み込みやすいです。読み込みの容易さだけであれば通常のJSONでもいいですが、ndjsonであればログの追加は単純な行の追記で済むため、書き込みもしやすいです。

Rでndjsonを読み込む

Rでも、jsonliteパッケージを使って簡単にndjsonを読み込むことができます。 read.csv関数などのように直接ファイルパスを指定するのではなく、file関数で作成したコネクションをstream_in関数に渡して読み込む点に注意してください1

# サンプルデータの作成
ndjson <- tempfile()
writeLines(c(
  '{"timestamp": "2024-10-07T00:00:00Z", "message": "program start"}',
  '{"timestamp": "2024-10-07T00:00:01Z", "message": "start processing", "task": "1"}',
  '{"timestamp": "2024-10-07T00:00:02Z", "message": "start processing", "task": "2"}',
  '{"timestamp": "2024-10-07T00:01:02Z", "message": "finish processing", "task": "2"}',
  '{"timestamp": "2024-10-07T00:02:02Z", "message": "finish processing", "task": "1"}',
  '{"timestamp": "2024-10-07T00:02:03Z", "message": "program end"}'
), ndjson)

# データ読み込み
df <- jsonlite::stream_in(file(ndjson))
#> opening file input connection.
#>  Found 6 records... Imported 6 records. Simplifying...
#> closing file input connection.
df
#>              timestamp           message task
#> 1 2024-10-07T00:00:00Z     program start <NA>
#> 2 2024-10-07T00:00:01Z  start processing    1
#> 3 2024-10-07T00:00:02Z  start processing    2
#> 4 2024-10-07T00:01:02Z finish processing    2
#> 5 2024-10-07T00:02:02Z finish processing    1
#> 6 2024-10-07T00:02:03Z       program end <NA>

読み込み結果はデータフレームなので集計も容易です。たとえば以下のように、タスクごとの処理時間(duration)を求めることができます。

df |>
  # 不要なデータの除去
  dplyr::filter(!is.na(task)) |>
  # タイムスタンプのパース
  dplyr::mutate(timestamp = clock::date_time_parse_RFC_3339(timestamp)) |>
  # タスクごとの開始時間と終了時間の抽出
  dplyr::summarize(
    start = min(timestamp),
    end = max(timestamp),
    .by = "task"
  ) |>
  # タスクごとの処理時間の計算
  dplyr::mutate(duration = end - start)
#>   task               start                 end      duration
#> 1    1 2024-10-07 00:00:01 2024-10-07 00:02:02 2.016667 mins
#> 2    2 2024-10-07 00:00:02 2024-10-07 00:01:02 1.000000 mins

Rで巨大なndjsonを扱う

先にも触れた通り、ndjsonの読み込みはjsonliteパッケージを使えば簡単です。しかし、巨大なファイルになると、読み込みに時間がかかります。

ログデータの代わりに、nycflights13::flightsデータセットをndjson化して実験してみましょう(336776行19列のデータセット)。

# サンプルデータの容易
ndjson <- tempfile()
withr::with_connection(list(con = file(ndjson, "wb")), {
  nycflights13::flights |>
    jsonlite::stream_out(con)
})
#> Processed 500 rows...Processed 1000 rows...Processed 1500 rows...Processed 2000 rows...Processed 2500 rows...Processed 3000 rows...Processed 3500 rows...Processed 4000 rows...Processed 4500 rows...Processed 5000 rows...Processed 5500 rows...Processed 6000 rows...Processed 6500 rows...Processed 7000 rows...Processed 7500 rows...Processed 8000 rows...Processed 8500 rows...Processed 9000 rows...Processed 9500 rows...Processed 10000 rows...Processed 10500 rows...Processed 11000 rows...Processed 11500 rows...Processed 12000 rows...Processed 12500 rows...Processed 13000 rows...Processed 13500 rows...Processed 14000 rows...Processed 14500 rows...Processed 15000 rows...Processed 15500 rows...Processed 16000 rows...Processed 16500 rows...Processed 17000 rows...Processed 17500 rows...Processed 18000 rows...Processed 18500 rows...Processed 19000 rows...Processed 19500 rows...Processed 20000 rows...Processed 20500 rows...Processed 21000 rows...Processed 21500 rows...Processed 22000 rows...Processed 22500 rows...Processed 23000 rows...Processed 23500 rows...Processed 24000 rows...Processed 24500 rows...Processed 25000 rows...Processed 25500 rows...Processed 26000 rows...Processed 26500 rows...Processed 27000 rows...Processed 27500 rows...Processed 28000 rows...Processed 28500 rows...Processed 29000 rows...Processed 29500 rows...Processed 30000 rows...Processed 30500 rows...Processed 31000 rows...Processed 31500 rows...Processed 32000 rows...Processed 32500 rows...Processed 33000 rows...Processed 33500 rows...Processed 34000 rows...Processed 34500 rows...Processed 35000 rows...Processed 35500 rows...Processed 36000 rows...Processed 36500 rows...Processed 37000 rows...Processed 37500 rows...Processed 38000 rows...Processed 38500 rows...Processed 39000 rows...Processed 39500 rows...Processed 40000 rows...Processed 40500 rows...Processed 41000 rows...Processed 41500 rows...Processed 42000 rows...Processed 42500 rows...Processed 43000 rows...Processed 43500 rows...Processed 44000 rows...Processed 44500 rows...Processed 45000 rows...Processed 45500 rows...Processed 46000 rows...Processed 46500 rows...Processed 47000 rows...Processed 47500 rows...Processed 48000 rows...Processed 48500 rows...Processed 49000 rows...Processed 49500 rows...Processed 50000 rows...Processed 50500 rows...Processed 51000 rows...Processed 51500 rows...Processed 52000 rows...Processed 52500 rows...Processed 53000 rows...Processed 53500 rows...Processed 54000 rows...Processed 54500 rows...Processed 55000 rows...Processed 55500 rows...Processed 56000 rows...Processed 56500 rows...Processed 57000 rows...Processed 57500 rows...Processed 58000 rows...Processed 58500 rows...Processed 59000 rows...Processed 59500 rows...Processed 60000 rows...Processed 60500 rows...Processed 61000 rows...Processed 61500 rows...Processed 62000 rows...Processed 62500 rows...Processed 63000 rows...Processed 63500 rows...Processed 64000 rows...Processed 64500 rows...Processed 65000 rows...Processed 65500 rows...Processed 66000 rows...Processed 66500 rows...Processed 67000 rows...Processed 67500 rows...Processed 68000 rows...Processed 68500 rows...Processed 69000 rows...Processed 69500 rows...Processed 70000 rows...Processed 70500 rows...Processed 71000 rows...Processed 71500 rows...Processed 72000 rows...Processed 72500 rows...Processed 73000 rows...Processed 73500 rows...Processed 74000 rows...Processed 74500 rows...Processed 75000 rows...Processed 75500 rows...Processed 76000 rows...Processed 76500 rows...Processed 77000 rows...Processed 77500 rows...Processed 78000 rows...Processed 78500 rows...Processed 79000 rows...Processed 79500 rows...Processed 80000 rows...Processed 80500 rows...Processed 81000 rows...Processed 81500 rows...Processed 82000 rows...Processed 82500 rows...Processed 83000 rows...Processed 83500 rows...Processed 84000 rows...Processed 84500 rows...Processed 85000 rows...Processed 85500 rows...Processed 86000 rows...Processed 86500 rows...Processed 87000 rows...Processed 87500 rows...Processed 88000 rows...Processed 88500 rows...Processed 89000 rows...Processed 89500 rows...Processed 90000 rows...Processed 90500 rows...Processed 91000 rows...Processed 91500 rows...Processed 92000 rows...Processed 92500 rows...Processed 93000 rows...Processed 93500 rows...Processed 94000 rows...Processed 94500 rows...Processed 95000 rows...Processed 95500 rows...Processed 96000 rows...Processed 96500 rows...Processed 97000 rows...Processed 97500 rows...Processed 98000 rows...Processed 98500 rows...Processed 99000 rows...Processed 99500 rows...Processed 1e+05 rows...Processed 100500 rows...Processed 101000 rows...Processed 101500 rows...Processed 102000 rows...Processed 102500 rows...Processed 103000 rows...Processed 103500 rows...Processed 104000 rows...Processed 104500 rows...Processed 105000 rows...Processed 105500 rows...Processed 106000 rows...Processed 106500 rows...Processed 107000 rows...Processed 107500 rows...Processed 108000 rows...Processed 108500 rows...Processed 109000 rows...Processed 109500 rows...Processed 110000 rows...Processed 110500 rows...Processed 111000 rows...Processed 111500 rows...Processed 112000 rows...Processed 112500 rows...Processed 113000 rows...Processed 113500 rows...Processed 114000 rows...Processed 114500 rows...Processed 115000 rows...Processed 115500 rows...Processed 116000 rows...Processed 116500 rows...Processed 117000 rows...Processed 117500 rows...Processed 118000 rows...Processed 118500 rows...Processed 119000 rows...Processed 119500 rows...Processed 120000 rows...Processed 120500 rows...Processed 121000 rows...Processed 121500 rows...Processed 122000 rows...Processed 122500 rows...Processed 123000 rows...Processed 123500 rows...Processed 124000 rows...Processed 124500 rows...Processed 125000 rows...Processed 125500 rows...Processed 126000 rows...Processed 126500 rows...Processed 127000 rows...Processed 127500 rows...Processed 128000 rows...Processed 128500 rows...Processed 129000 rows...Processed 129500 rows...Processed 130000 rows...Processed 130500 rows...Processed 131000 rows...Processed 131500 rows...Processed 132000 rows...Processed 132500 rows...Processed 133000 rows...Processed 133500 rows...Processed 134000 rows...Processed 134500 rows...Processed 135000 rows...Processed 135500 rows...Processed 136000 rows...Processed 136500 rows...Processed 137000 rows...Processed 137500 rows...Processed 138000 rows...Processed 138500 rows...Processed 139000 rows...Processed 139500 rows...Processed 140000 rows...Processed 140500 rows...Processed 141000 rows...Processed 141500 rows...Processed 142000 rows...Processed 142500 rows...Processed 143000 rows...Processed 143500 rows...Processed 144000 rows...Processed 144500 rows...Processed 145000 rows...Processed 145500 rows...Processed 146000 rows...Processed 146500 rows...Processed 147000 rows...Processed 147500 rows...Processed 148000 rows...Processed 148500 rows...Processed 149000 rows...Processed 149500 rows...Processed 150000 rows...Processed 150500 rows...Processed 151000 rows...Processed 151500 rows...Processed 152000 rows...Processed 152500 rows...Processed 153000 rows...Processed 153500 rows...Processed 154000 rows...Processed 154500 rows...Processed 155000 rows...Processed 155500 rows...Processed 156000 rows...Processed 156500 rows...Processed 157000 rows...Processed 157500 rows...Processed 158000 rows...Processed 158500 rows...Processed 159000 rows...Processed 159500 rows...Processed 160000 rows...Processed 160500 rows...Processed 161000 rows...Processed 161500 rows...Processed 162000 rows...Processed 162500 rows...Processed 163000 rows...Processed 163500 rows...Processed 164000 rows...Processed 164500 rows...Processed 165000 rows...Processed 165500 rows...Processed 166000 rows...Processed 166500 rows...Processed 167000 rows...Processed 167500 rows...Processed 168000 rows...Processed 168500 rows...Processed 169000 rows...Processed 169500 rows...Processed 170000 rows...Processed 170500 rows...Processed 171000 rows...Processed 171500 rows...Processed 172000 rows...Processed 172500 rows...Processed 173000 rows...Processed 173500 rows...Processed 174000 rows...Processed 174500 rows...Processed 175000 rows...Processed 175500 rows...Processed 176000 rows...Processed 176500 rows...Processed 177000 rows...Processed 177500 rows...Processed 178000 rows...Processed 178500 rows...Processed 179000 rows...Processed 179500 rows...Processed 180000 rows...Processed 180500 rows...Processed 181000 rows...Processed 181500 rows...Processed 182000 rows...Processed 182500 rows...Processed 183000 rows...Processed 183500 rows...Processed 184000 rows...Processed 184500 rows...Processed 185000 rows...Processed 185500 rows...Processed 186000 rows...Processed 186500 rows...Processed 187000 rows...Processed 187500 rows...Processed 188000 rows...Processed 188500 rows...Processed 189000 rows...Processed 189500 rows...Processed 190000 rows...Processed 190500 rows...Processed 191000 rows...Processed 191500 rows...Processed 192000 rows...Processed 192500 rows...Processed 193000 rows...Processed 193500 rows...Processed 194000 rows...Processed 194500 rows...Processed 195000 rows...Processed 195500 rows...Processed 196000 rows...Processed 196500 rows...Processed 197000 rows...Processed 197500 rows...Processed 198000 rows...Processed 198500 rows...Processed 199000 rows...Processed 199500 rows...Processed 2e+05 rows...Processed 200500 rows...Processed 201000 rows...Processed 201500 rows...Processed 202000 rows...Processed 202500 rows...Processed 203000 rows...Processed 203500 rows...Processed 204000 rows...Processed 204500 rows...Processed 205000 rows...Processed 205500 rows...Processed 206000 rows...Processed 206500 rows...Processed 207000 rows...Processed 207500 rows...Processed 208000 rows...Processed 208500 rows...Processed 209000 rows...Processed 209500 rows...Processed 210000 rows...Processed 210500 rows...Processed 211000 rows...Processed 211500 rows...Processed 212000 rows...Processed 212500 rows...Processed 213000 rows...Processed 213500 rows...Processed 214000 rows...Processed 214500 rows...Processed 215000 rows...Processed 215500 rows...Processed 216000 rows...Processed 216500 rows...Processed 217000 rows...Processed 217500 rows...Processed 218000 rows...Processed 218500 rows...Processed 219000 rows...Processed 219500 rows...Processed 220000 rows...Processed 220500 rows...Processed 221000 rows...Processed 221500 rows...Processed 222000 rows...Processed 222500 rows...Processed 223000 rows...Processed 223500 rows...Processed 224000 rows...Processed 224500 rows...Processed 225000 rows...Processed 225500 rows...Processed 226000 rows...Processed 226500 rows...Processed 227000 rows...Processed 227500 rows...Processed 228000 rows...Processed 228500 rows...Processed 229000 rows...Processed 229500 rows...Processed 230000 rows...Processed 230500 rows...Processed 231000 rows...Processed 231500 rows...Processed 232000 rows...Processed 232500 rows...Processed 233000 rows...Processed 233500 rows...Processed 234000 rows...Processed 234500 rows...Processed 235000 rows...Processed 235500 rows...Processed 236000 rows...Processed 236500 rows...Processed 237000 rows...Processed 237500 rows...Processed 238000 rows...Processed 238500 rows...Processed 239000 rows...Processed 239500 rows...Processed 240000 rows...Processed 240500 rows...Processed 241000 rows...Processed 241500 rows...Processed 242000 rows...Processed 242500 rows...Processed 243000 rows...Processed 243500 rows...Processed 244000 rows...Processed 244500 rows...Processed 245000 rows...Processed 245500 rows...Processed 246000 rows...Processed 246500 rows...Processed 247000 rows...Processed 247500 rows...Processed 248000 rows...Processed 248500 rows...Processed 249000 rows...Processed 249500 rows...Processed 250000 rows...Processed 250500 rows...Processed 251000 rows...Processed 251500 rows...Processed 252000 rows...Processed 252500 rows...Processed 253000 rows...Processed 253500 rows...Processed 254000 rows...Processed 254500 rows...Processed 255000 rows...Processed 255500 rows...Processed 256000 rows...Processed 256500 rows...Processed 257000 rows...Processed 257500 rows...Processed 258000 rows...Processed 258500 rows...Processed 259000 rows...Processed 259500 rows...Processed 260000 rows...Processed 260500 rows...Processed 261000 rows...Processed 261500 rows...Processed 262000 rows...Processed 262500 rows...Processed 263000 rows...Processed 263500 rows...Processed 264000 rows...Processed 264500 rows...Processed 265000 rows...Processed 265500 rows...Processed 266000 rows...Processed 266500 rows...Processed 267000 rows...Processed 267500 rows...Processed 268000 rows...Processed 268500 rows...Processed 269000 rows...Processed 269500 rows...Processed 270000 rows...Processed 270500 rows...Processed 271000 rows...Processed 271500 rows...Processed 272000 rows...Processed 272500 rows...Processed 273000 rows...Processed 273500 rows...Processed 274000 rows...Processed 274500 rows...Processed 275000 rows...Processed 275500 rows...Processed 276000 rows...Processed 276500 rows...Processed 277000 rows...Processed 277500 rows...Processed 278000 rows...Processed 278500 rows...Processed 279000 rows...Processed 279500 rows...Processed 280000 rows...Processed 280500 rows...Processed 281000 rows...Processed 281500 rows...Processed 282000 rows...Processed 282500 rows...Processed 283000 rows...Processed 283500 rows...Processed 284000 rows...Processed 284500 rows...Processed 285000 rows...Processed 285500 rows...Processed 286000 rows...Processed 286500 rows...Processed 287000 rows...Processed 287500 rows...Processed 288000 rows...Processed 288500 rows...Processed 289000 rows...Processed 289500 rows...Processed 290000 rows...Processed 290500 rows...Processed 291000 rows...Processed 291500 rows...Processed 292000 rows...Processed 292500 rows...Processed 293000 rows...Processed 293500 rows...Processed 294000 rows...Processed 294500 rows...Processed 295000 rows...Processed 295500 rows...Processed 296000 rows...Processed 296500 rows...Processed 297000 rows...Processed 297500 rows...Processed 298000 rows...Processed 298500 rows...Processed 299000 rows...Processed 299500 rows...Processed 3e+05 rows...Processed 300500 rows...Processed 301000 rows...Processed 301500 rows...Processed 302000 rows...Processed 302500 rows...Processed 303000 rows...Processed 303500 rows...Processed 304000 rows...Processed 304500 rows...Processed 305000 rows...Processed 305500 rows...Processed 306000 rows...Processed 306500 rows...Processed 307000 rows...Processed 307500 rows...Processed 308000 rows...Processed 308500 rows...Processed 309000 rows...Processed 309500 rows...Processed 310000 rows...Processed 310500 rows...Processed 311000 rows...Processed 311500 rows...Processed 312000 rows...Processed 312500 rows...Processed 313000 rows...Processed 313500 rows...Processed 314000 rows...Processed 314500 rows...Processed 315000 rows...Processed 315500 rows...Processed 316000 rows...Processed 316500 rows...Processed 317000 rows...Processed 317500 rows...Processed 318000 rows...Processed 318500 rows...Processed 319000 rows...Processed 319500 rows...Processed 320000 rows...Processed 320500 rows...Processed 321000 rows...Processed 321500 rows...Processed 322000 rows...Processed 322500 rows...Processed 323000 rows...Processed 323500 rows...Processed 324000 rows...Processed 324500 rows...Processed 325000 rows...Processed 325500 rows...Processed 326000 rows...Processed 326500 rows...Processed 327000 rows...Processed 327500 rows...Processed 328000 rows...Processed 328500 rows...Processed 329000 rows...Processed 329500 rows...Processed 330000 rows...Processed 330500 rows...Processed 331000 rows...Processed 331500 rows...Processed 332000 rows...Processed 332500 rows...Processed 333000 rows...Processed 333500 rows...Processed 334000 rows...Processed 334500 rows...Processed 335000 rows...Processed 335500 rows...Processed 336000 rows...Processed 336500 rows...Complete! Processed total of 336776 rows.
# jsonliteによる読み込みとtidyverseによる集計のベンチマーク
use_jsonlite <- function() {
  file(ndjson) |>
    jsonlite::stream_in(verbose = FALSE) |>
    dplyr::group_by(dest) |>
    dplyr::summarize(delay = mean(dep_time, na.rm = TRUE))
}

bench::mark(use_jsonlite(), iterations = 10)
#> Warning: Some expressions had a GC in every iteration; so filtering is
#> disabled.
#> # A tibble: 1 × 6
#>   expression          min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>     <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 use_jsonlite()    11.9s    12.7s    0.0792        NA    0.507

jsonlite::stream_in関数は、ファイルを少しずつ読み込んで、最後にrbind関数で1まとまりのデータフレームに変換しています。おそらく、この部分が処理時間を要しているのでしょう。 pagesize引数を使って一度に読み込む範囲を広くとったり、実装に手を出してdplyr::bind_rows関数を使えば、高速化が望めます。

しかし、パフォーマンスチューニングするのは辛いのと、Rでやることに限界を感じそうです。少し覚えることを増やしてduckdbパッケージを使えば、トータルで幸せになれそうなので、紹介します。

duckdbパッケージを使う

duckdbは列指向のデータベース管理システムです。同名のパッケージにより、Rから簡単に利用できます。

なにやら仰々しいですが、、今回の用途であれば、一部の定型的な呪文を覚えるだけで、実質的には普段のデータフレームと同様に操作できます。

# インメモリデータベースにndjsonを読み込む
# この部分が呪文相当
con <- DBI::dbConnect(duckdb::duckdb())
DBI::dbExecute(
  con,
  paste(
    # duckdbでjsonを使う準備
    "INSTALL json; LOAD json;",
    # ndjsonをtblテーブルに読み込む
    "CREATE OR REPLACE TABLE tbl AS SELECT * FROM read_ndjson('log.ndjson');"
  )
)

# データベースの内容をデータフレーム化する
dplyr::tbl(con, "tbl") |>
  dplyr::collect()

最後の2行でデータフレーム化けしてしまえば、あとは普通のデータフレームのように、R上で列選択や集計を行えます。

# R上で列選択
dplyr::tbl(con, "tbl") |>
  dplyr::collect() |>
  dplyr::select(foo)

dbplyrパッケージの力により、dplyr::collect()の前でもtidyverse流のデータ加工を実施できます。この場合は、加工をデータベース側で行うため、更なる高速化や省メモリ化が期待できます。ただし、利用可能な関数が限られる点に注意してください(参考)。

# duckdb上で列選択
dplyr::tbl(con, "tbl") |>
  dplyr::select(foo) |>
  dplyr::collect()

duckdbパッケージでndjsonをデータフレーム化する

ndjsonファイルが巨大でも、データフレームがメモリに乗る程度のサイズであれば、duckdbパッケージをndjsonファイルのデータフレーム化ツールとして使うといいでしょう。ベンチマーク結果もjsonliteパッケージを使う場合に比べて2桁高速です。

use_duckdb2 <- function() {
  # データベースとのコネクションを作成
  con <- withr::local_db_connection(DBI::dbConnect(duckdb::duckdb()))

  # インメモリデータベースにndjsonを読み込む
  DBI::dbExecute(
    con,
    paste(
      "INSTALL json; LOAD json;",
      "CREATE OR REPLACE TABLE tbl AS SELECT * FROM read_ndjson('%s');"
    ) |> sprintf(ndjson)
  )

  # インメモリデータベースをデータフレームに変換する
  df <- dplyr::tbl(con, "tbl") |>
    dplyr::collect()

  # データフレームを集計する
  df |>
    dplyr::group_by(dest) |>
    dplyr::summarize(delay = mean(dep_time, na.rm = TRUE))
}

# ベンチマーク
bench::mark(use_duckdb2(), iterations = 10)
#> Warning: Some expressions had a GC in every iteration; so filtering is
#> disabled.
#> # A tibble: 1 × 6
#>   expression         min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>    <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 use_duckdb2()    497ms    535ms      1.83        NA     2.92

duckdbパッケージでndjsonをインメモリデータベース化する

duckdbパッケージを単純な形式変換ツールとして使うのではまだ遅いケースでは、duckdb上に読み込んだデータをデータフレーム化するに、データベース上で集計を行うことで、高速化を図ることができます。

今回のデータ程度では劇的な差はないので、集計に利用可能な関数の制限を受けてまで、活用する必要はないでしょう。

require(dbplyr)
use_duckdb <- function() {
  # データベースとのコネクションを作成
  con <- withr::local_db_connection(DBI::dbConnect(duckdb::duckdb()))

  # インメモリデータベースにndjsonを読み込む
  DBI::dbExecute(
    con,
    paste(
      "INSTALL json; LOAD json;",
      "CREATE OR REPLACE TABLE tbl AS SELECT * FROM read_ndjson('%s');"
    ) |> sprintf(ndjson)
  )

  # インメモリデータベースを集計し、結果をデータフレームに変換する
  dplyr::tbl(con, "tbl") |>
    dplyr::group_by(dest) |>
    dplyr::summarize(delay = mean(dep_time, na.rm = TRUE)) |>
    dplyr::collect()
}

bench::mark(use_duckdb(), iterations = 10)
#> # A tibble: 1 × 6
#>   expression        min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>   <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 use_duckdb()    469ms    492ms      2.00        NA     1.33

ENJOY

ndjson形式のログをRで解析する方法として、jsonliteパッケージで読み込む方法、duckdbでデータフレーム化する方法、duckdbで加工してからデータフレーム化する方法を紹介しました。

みなさんのログ解析が捗れば幸いです。


  1. 一般的に、作成したコネクションはclose関数で後処理する必要がありますが、stream_in関数は内部でコネクションを自動的にクローズします。コネクションの有効なスコープを限定する方法にwithr::with_connection関数を使う方法もありますが、stream_in関数の自動クローズ機能と干渉するので、ここでは使いません。↩︎