Skip to content

Goals

First, I want to measure nanoparquet’s speed relative to good quality CSV readers and writers, and also look at the sizes of the Parquet and CSV files.

Second, I want to see how nanoparquet fares relative to other Parquet implementations available from R.

Data sets

I used use three data sets: small, medium and large. The small data set is the nycflights13::flights data set, as is. The medium data set contains 20 copies of the small data set. The large data set contains 200 copies of the small data set. See the gen_data() function in the benchmark-funcs.R file in the nanoparquet GitHub repository.

Some basic information about each data set:

Show the code
if (file.exists(file.path(me, "data-info.parquet"))) {
  info_tab <- nanoparquet::read_parquet(file.path(me, "data-info.parquet"))
} else {
  get_data_info <- function(x) {
    list(dim = dim(x), size = object.size(x))
  }
  info <- lapply(data_sizes, function(s) get_data_info(gen_data(s)))
  info_tab <- data.frame(
    check.names = FALSE,
    name = data_sizes,
    rows = sapply(info, "[[", "dim")[1,],
    columns = sapply(info, "[[", "dim")[2,],
    "size in memory" = sapply(info, "[[", "size")
  )
  nanoparquet::write_parquet(info_tab, file.path(me, "data-info.parquet"))
}
info_tab |>
  gt() |>
  tab_header(title = "Data sets") |>
  tab_options(table.align = "left") |>
  fmt_integer() |>
  fmt_bytes(columns = "size in memory")
Data sets
name rows columns size in memory
small 336,776 19 40.7 MB
medium 6,735,520 19 808.5 MB
large 67,355,200 19 8.1 GB

A quick look at the data:

Show the code
head(nycflights13::flights)
#> # A tibble: 6 × 19
#>    year month   day dep_time sched_dep_time dep_delay arr_time sched_arr_time
#>   <int> <int> <int>    <int>          <int>     <dbl>    <int>          <int>
#> 1  2013     1     1      517            515         2      830            819
#> 2  2013     1     1      533            529         4      850            830
#> 3  2013     1     1      542            540         2      923            850
#> 4  2013     1     1      544            545        -1     1004           1022
#> 5  2013     1     1      554            600        -6      812            837
#> 6  2013     1     1      554            558        -4      740            728
#> # ℹ 11 more variables: arr_delay <dbl>, carrier <chr>, flight <int>,
#> #   tailnum <chr>, origin <chr>, dest <chr>, air_time <dbl>, distance <dbl>,
#> #   hour <dbl>, minute <dbl>, time_hour <dttm>
Show the code
dplyr::glimpse(nycflights13::flights)
#> Rows: 336,776
#> Columns: 19
#> $ year           <int> 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2013, 2…
#> $ month          <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ day            <int> 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1…
#> $ dep_time       <int> 517, 533, 542, 544, 554, 554, 555, 557, 557, 558, 558, …
#> $ sched_dep_time <int> 515, 529, 540, 545, 600, 558, 600, 600, 600, 600, 600, …
#> $ dep_delay      <dbl> 2, 4, 2, -1, -6, -4, -5, -3, -3, -2, -2, -2, -2, -2, -1…
#> $ arr_time       <int> 830, 850, 923, 1004, 812, 740, 913, 709, 838, 753, 849,…
#> $ sched_arr_time <int> 819, 830, 850, 1022, 837, 728, 854, 723, 846, 745, 851,…
#> $ arr_delay      <dbl> 11, 20, 33, -18, -25, 12, 19, -14, -8, 8, -2, -3, 7, -1…
#> $ carrier        <chr> "UA", "UA", "AA", "B6", "DL", "UA", "B6", "EV", "B6", "…
#> $ flight         <int> 1545, 1714, 1141, 725, 461, 1696, 507, 5708, 79, 301, 4…
#> $ tailnum        <chr> "N14228", "N24211", "N619AA", "N804JB", "N668DN", "N394…
#> $ origin         <chr> "EWR", "LGA", "JFK", "JFK", "LGA", "EWR", "EWR", "LGA",…
#> $ dest           <chr> "IAH", "IAH", "MIA", "BQN", "ATL", "ORD", "FLL", "IAD",…
#> $ air_time       <dbl> 227, 227, 160, 183, 116, 150, 158, 53, 140, 138, 149, 1…
#> $ distance       <dbl> 1400, 1416, 1089, 1576, 762, 719, 1065, 229, 944, 733, …
#> $ hour           <dbl> 5, 5, 5, 5, 6, 5, 6, 6, 6, 6, 6, 6, 6, 6, 6, 5, 6, 6, 6…
#> $ minute         <dbl> 15, 29, 40, 45, 0, 58, 0, 0, 0, 0, 0, 0, 0, 0, 0, 59, 0…
#> $ time_hour      <dttm> 2013-01-01 05:00:00, 2013-01-01 05:00:00, 2013-01-01 0…

Parquet implementations

I ran nanoparquet, Arrow and DuckDB. I also ran data.table without and with compression and readr, to read/write CSV files. I used the running time of readr as the baseline.

I ran each benchmark three times and record the results of the third run. This is to make sure that the data and the software is not swapped out by the OS. (Except for readr on the large data set, because it would take too long.)

Show the code
if (file.exists(file.path(me, "results.parquet"))) {
  results <- nanoparquet::read_parquet(file.path(me, "results.parquet"))
} else {
  results <- NULL
  lapply(data_sizes, function(s) {
    lapply(variants, function(v) {
      r <- if (v == "readr" && s == "large") {
        measure(v, s)
      } else {
        measure(v, s)
        measure(v, s)
        measure(v, s)
      }
      results <<- rbind(results, r)
    })
  })
  nanoparquet::write_parquet(results, file.path(me, "results.parquet"))
}

I include the complete raw results at the end of this article.

Parquet vs CSV

The results, focusing on the CSV readers and nanoparquet:

Show the code
csv_tab_read <- results |>
  filter(software %in% c("nanoparquet", "data.table", "data.table.gz", "readr")) |>
  filter(direction == "read") |>
  mutate(software = case_when(
    software ==  "data.table.gz" ~ "data.table (compressed)",
    .default = software
  )) |>
  rename(`data size` = data_size, time = time_elapsed) |>
  mutate(memory = mem_max_after - mem_before) |>
  mutate(base = tail(time, 1), .by = `data size`) |>
  mutate(speedup = base / time, x = round(speedup, digits = 1)) |>
  select(`data size`, software, time, x, speedup, memory) |>
  mutate(rawtime = time, time = prettyunits::pretty_sec(time)) |>
  rename(`speedup from CSV` = speedup)

csv_tab_read |>
  gt() |>
  tab_header(title = "Parquet vs CSV, reading") |>
  tab_options(table.align = "left") |>
  tab_row_group(md("**small data**"), rows = `data size` == "small", "s") |>
  tab_row_group(md("**medium data**"), rows = `data size` == "medium", "m") |>
  tab_row_group(md("**large data**"), rows = `data size` == "large", "l") |>
  row_group_order(c("s", "m", "l")) |>
  cols_hide(columns = c(`data size`, rawtime)) |>
  cols_align(columns = time, align = "right") |>
  fmt_bytes(columns = memory) |>
  gt_plt_bar(column = `speedup from CSV`)
Parquet vs CSV, reading
software time x speedup from CSV memory
small data
nanoparquet 32ms 13.1 69.8 MB
data.table 61ms 6.7 68.1 MB
data.table (compressed) 146ms 2.8 113.3 MB
readr 405ms 1.0 189.6 MB
medium data
nanoparquet 1s 5.2 1.5 GB
data.table 874ms 6.0 1.3 GB
data.table (compressed) 2s 2.6 1.4 GB
readr 5.2s 1.0 2.6 GB
large data
nanoparquet 10.8s 21.7 7.6 GB
data.table 12.5s 18.7 8.9 GB
data.table (compressed) 27.1s 8.6 7.4 GB
readr 3m 54.4s 1.0 8 GB

Notes:

  • The single-threaded nanoparquet Parquet-reader is competitive. It can read a compressed Parquet file just as fast as the state of the art uncompressed CSV reader that uses at least 2 threads.

The nanoparquet vs CSV results when writing Parquet or CSV files:

Show the code
csv_tab_write <- results |>
  filter(software %in% c("nanoparquet", "data.table", "data.table.gz", "readr")) |>
  filter(direction == "write") |>
  mutate(software = case_when(
    software ==  "data.table.gz" ~ "data.table (compressed)",
    .default = software
  )) |>
  rename(`data size` = data_size, time = time_elapsed, `file size` = file_size) |>
  mutate(memory = mem_max_after - mem_before) |>
  mutate(base = tail(time, 1), .by = `data size`) |>
  mutate(speedup = base / time, x = round(speedup, digits = 1)) |>
  select(`data size`, software, time, x, speedup, memory, `file size`) |>
  mutate(rawtime = time, time = prettyunits::pretty_sec(time)) |>
  rename(`speedup from CSV` = speedup)

csv_tab_write |>
  gt() |>
  tab_header(title = "Parquet vs CSV, writing") |>
  tab_options(table.align = "left") |>
  tab_row_group(md("**small data**"), rows = `data size` == "small", "s") |>
  tab_row_group(md("**medium data**"), rows = `data size` == "medium", "m") |>
  tab_row_group(md("**large data**"), rows = `data size` == "large", "l") |>
  row_group_order(c("s", "m", "l")) |>
  cols_hide(columns = c(`data size`, rawtime)) |>
  cols_align(columns = time, align = "right") |>
  fmt_bytes(columns = c(memory, `file size`)) |>
  gt_plt_bar(column = `speedup from CSV`)
Parquet vs CSV, writing
software time x speedup from CSV memory file size
small data
nanoparquet 135ms 5.6 83 MB 5.7 MB
data.table 61ms 12.3 868.4 kB 31 MB
data.table (compressed) 397ms 1.9 1.7 MB 8.3 MB
readr 748ms 1.0 52.6 MB 31.1 MB
medium data
nanoparquet 2.4s 5.5 678.4 MB 111.4 MB
data.table 750ms 18.0 2.6 MB 619.2 MB
data.table (compressed) 7.2s 1.9 5.6 MB 165.2 MB
readr 13.5s 1.0 841.9 MB 621.1 MB
large data
nanoparquet 26.7s 5.3 402.7 MB 1.1 GB
data.table 7.9s 17.9 269.3 MB 6.2 GB
data.table (compressed) 1m 11.7s 2.0 269.3 MB 1.7 GB
readr 2m 20.6s 1.0 1 GB 6.2 GB

Notes:

  • The data.table CSV writer is about 3 times as fast as the nanoparquet Parquet writer, if the CSV file is uncompressed. The CSV writer uses at least 4 threads, the Parquet write is single-threaded.
  • The nanoparquet Parquet writer is 2-5 times faster than the data.table CSV writer if the CSV file is compressed.
  • The Parquet files are about 5-6 times smaller than the uncompressed CSV files and about 30-35% smaller than the compressed CSV files.

Parquet implementations

This is the summary of the Parquet readers, for the same three files.

Show the code
pq_tab_read <- results |>
  filter(software %in% c("nanoparquet", "arrow", "duckdb", "readr")) |>
  filter(direction == "read") |>
  rename(`data size` = data_size, time = time_elapsed) |>
  mutate(memory = mem_max_after - mem_before) |>
  mutate(base = tail(time, 1), .by = `data size`) |>
  mutate(speedup = base / time, x = round(speedup, digits = 1)) |>
  select(`data size`, software, time, x, speedup, memory) |>
  mutate(rawtime = time, time = prettyunits::pretty_sec(time)) |>
  filter(software %in% c("nanoparquet", "arrow", "duckdb")) |>
  mutate(software = case_when(
    software ==  "arrow" ~ "Arrow",
    software == "duckdb" ~ "DuckDB",
    .default = software
  )) |>
  rename(`speedup from CSV` = speedup)

pq_tab_read |>
  gt() |>
  tab_header(title = "Parquet implementations, reading") |>
  tab_options(table.align = "left") |>
  tab_row_group(md("**small data**"), rows = `data size` == "small", "s") |>
  tab_row_group(md("**medium data**"), rows = `data size` == "medium", "m") |>
  tab_row_group(md("**large data**"), rows = `data size` == "large", "l") |>
  row_group_order(c("s", "m", "l")) |>
  cols_hide(columns = c(`data size`, rawtime)) |>
  cols_align(columns = time, align = "right") |>
  fmt_bytes(columns = memory) |>
  gt_plt_bar(column = `speedup from CSV`)
Parquet implementations, reading
software time x speedup from CSV memory
small data
nanoparquet 32ms 13.1 69.8 MB
Arrow 42ms 9.9 110.6 MB
DuckDB 74ms 5.5 114.4 MB
medium data
nanoparquet 1s 5.2 1.5 GB
Arrow 918ms 5.7 2.1 GB
DuckDB 1.2s 4.5 2 GB
large data
nanoparquet 10.8s 21.7 7.6 GB
Arrow 12.3s 19.1 8.8 GB
DuckDB 14.3s 16.4 8.6 GB

Notes:

  • In general, all three implementations perform similarly. nanoparquet is very competitive for these three data sets in terms of speed and also tends to use the least amount of memory.
  • I turned off ALTREP in arrow, so that it reads the data into memory.

The summary for the Parquet writers:

Show the code
pq_tab_write <- results |>
  filter(software %in% c("nanoparquet", "arrow", "duckdb", "readr")) |>
  filter(direction == "write") |>
  rename(`data size` = data_size, time = time_elapsed, `file size` = file_size) |>
  mutate(memory = mem_max_after - mem_before) |>
  mutate(base = tail(time, 1), .by = `data size`) |>
  mutate(speedup = base / time, x = round(speedup, digits = 1)) |>
  select(`data size`, software, time, x, speedup, memory, `file size`) |>
  mutate(rawtime = time, time = prettyunits::pretty_sec(time)) |>
  filter(software %in% c("nanoparquet", "arrow", "duckdb", "readr")) |>
  mutate(software = case_when(
    software ==  "arrow" ~ "Arrow",
    software == "duckdb" ~ "DuckDB",
    .default = software
  )) |>
  rename(`speedup from CSV` = speedup)

pq_tab_write |>
  gt() |>
  tab_header(title = "Parquet implementations, writing") |>
  tab_options(table.align = "left") |>
  tab_row_group(md("**small data**"), rows = `data size` == "small", "s") |>
  tab_row_group(md("**medium data**"), rows = `data size` == "medium", "m") |>
  tab_row_group(md("**large data**"), rows = `data size` == "large", "l") |>
  row_group_order(c("s", "m", "l")) |>
  cols_hide(columns = c(`data size`, rawtime)) |>
  cols_align(columns = time, align = "right") |>
  fmt_bytes(columns = c(memory, `file size`)) |>
  gt_plt_bar(column = `speedup from CSV`)
Parquet implementations, writing
software time x speedup from CSV memory file size
small data
nanoparquet 135ms 5.6 83 MB 5.7 MB
Arrow 144ms 5.2 35.8 MB 5.7 MB
DuckDB 214ms 3.5 159.8 MB 9.7 MB
readr 748ms 1.0 52.6 MB 31.1 MB
medium data
nanoparquet 2.4s 5.5 678.4 MB 111.4 MB
Arrow 2.7s 5.1 291.7 MB 112.1 MB
DuckDB 2.7s 5.0 1.9 GB 193.6 MB
readr 13.5s 1.0 841.9 MB 621.1 MB
large data
nanoparquet 26.7s 5.3 402.7 MB 1.1 GB
Arrow 29.3s 4.8 591 MB 1.1 GB
DuckDB 34s 4.1 621.7 MB 1.9 GB
readr 2m 20.6s 1.0 1 GB 6.2 GB

Notes:

  • nanoparquet is again very competitive in terms of speed, it is slightly faster than the other two implementations, for these data sets.
  • DuckDB seems to waste space when writing out Parquet files. This could be possibly fine tuned by forcing a different encoding. This behavior has improved somewhat with the 1.2.0 release (https://github.com/duckdb/duckdb/issues/3316).

Conclusions

These results will probably change for a different data sets, or on a different system. In particular, Arrow and DuckDB are probably faster on larger systems, where the data is stored on multiple physical disks.

Both Arrow and DuckDB let you run queries on the data without loading it all into memory first. This is especially important if the data does not fit into memory at all, not even the columns needed for the analysis. nanoparquet cannot do this.

However, in general, based on these benchmarks I have good reasons to trust that the nanoparquet Parquet reader and writer is competitive with the other implementations available from R, both in terms of speed and memory use.

If the limitations of nanoparquet are not prohibitive for your use case, it is a good choice for Parquet I/O.

Raw benchmark results

These are the raw results. You can scroll to the right if your screen is not wide enough for the whole table.

Show the code
#>
#> Attaching package: 'pillar'
#> The following object is masked from 'package:dplyr':
#>
#>     dim_desc
Show the code
print(results, n = Inf)
#> # A data frame: 36 × 10
#>    software      direction data_size time_user time_system time_elapsed mem_before mem_max_before mem_max_after  file_size
#>    <chr>         <chr>     <chr>         <dbl>       <dbl>        <dbl>      <dbl>          <dbl>         <dbl>      <dbl>
#>  1 nanoparquet   read      small        0.0230     0.008         0.0310  161087488      161349632     230916096         NA
#>  2 nanoparquet   write     small        0.112      0.021         0.134   307871744      307970048     390856704    5687765
#>  3 arrow         read      small        0.059      0.024         0.0410  163184640      163446784     273825792         NA
#>  4 arrow         write     small        0.143      0.007         0.143   299401216      299679744     335200256    5690457
#>  5 duckdb        read      small        0.094      0.018         0.0730  163135488      163397632     277512192         NA
#>  6 duckdb        write     small        0.414      0.028         0.214   305135616      305152000     464896000    9723773
#>  7 data.table    read      small        0.141      0.012         0.0600  159612928      159875072     227721216         NA
#>  8 data.table    write     small        0.155      0.00900       0.0610  309542912      309821440     310411264   30960660
#>  9 data.table.gz read      small        0.212      0.026         0.146   159858688      160104448     273154048         NA
#> 10 data.table.gz write     small        1.50       0.017         0.397   298385408      298663936     300105728    8262799
#> 11 readr         read      small        1.05       0.158         0.405   160514048      160776192     350158848         NA
#> 12 readr         write     small        1.69       1.75          0.748   305709056      305987584     358268928   31053850
#> 13 nanoparquet   read      medium       0.864      0.139         1.00    154484736      154746880    1635860480         NA
#> 14 nanoparquet   write     medium       2.15       0.265         2.43   1089323008     1089601536    1767702528  111363391
#> 15 arrow         read      medium       1.30       0.232         0.917   154910720      154910720    2213920768         NA
#> 16 arrow         write     medium       2.70       0.078         2.66   1096105984     1096368128    1387806720  112105412
#> 17 duckdb        read      medium       2.12       0.301         1.16    164790272      165052416    2136080384         NA
#> 18 duckdb        write     medium       6.43       0.612         2.67   1084686336     1084964864    3032383488  193640563
#> 19 data.table    read      medium       2.33       0.135         0.874   158466048      158728192    1453555712         NA
#> 20 data.table    write     medium       2.62       0.103         0.749  1076707328     1076985856    1079279616  619210198
#> 21 data.table.gz read      medium       3.29       0.328         1.99    151683072      151945216    1515175936         NA
#> 22 data.table.gz write     medium      28.3        0.134         7.19   1103118336     1103396864    1108672512  165242566
#> 23 readr         read      medium      18.3        4.92          5.21    157696000      157958144    2803531776         NA
#> 24 readr         write     medium      31.8       29.4          13.5    1082851328     1083129856    1924775936  621073998
#> 25 nanoparquet   read      large        6.65       2.72         10.8     158351360      158613504    7776043008         NA
#> 26 nanoparquet   write     large       21.3        4.36         26.7    8389902336     8658927616    8792555520 1113819170
#> 27 arrow         read      large       12.2        8.07         12.3     151879680      152125440    8983003136         NA
#> 28 arrow         write     large       26.8        2.37         29.3    8385839104     8655028224    8976859136 1120869331
#> 29 duckdb        read      large       19.4        5.10         14.3     158613504      158875648    8774959104         NA
#> 30 duckdb        write     large       66.7       12.0          34.0    7279968256     7901659136    7901659136 1935744269
#> 31 data.table    read      large       21.5        3.80         12.5     152469504      152715264    9010954240         NA
#> 32 data.table    write     large       26.6        1.36          7.87   8382693376     8651997184    8651997184 6192100558
#> 33 data.table.gz read      large       31.1        7.04         27.1     160841728      161103872    7513079808         NA
#> 34 data.table.gz write     large      283.         1.25         71.7    8386412544     8655716352    8655716352 1652420625
#> 35 readr         read      large      148.       185.          234.      153108480      153370624    8192851968         NA
#> 36 readr         write     large      334.       387.          141.     8382644224     8651931648    9416835072 6210738558

Notes:

  • User time (time_user) plus system time (time_system) can be larger than the elapsed time (time_elapsed) for multithreaded implementations and it indeed is for all tools, except for nanoparquet, which is single-threaded.
  • All memory columns are in bytes. mem_before is the RSS size before reading/writing. mem_max_before is the maximum RSS size of the process until then. mem_max_after is the maximum RSS size of the process after the read/write operation.
  • So I can calculate (estimate) the memory usage of the tool by subtracting mem_before from mem_max_after. This could overestimate the memory usage if mem_max_after were the same as mem_max_before, but this never happens in practice.
  • When reading the file, mem_max_after includes the memory needed to store the data set itself. (See data sizes above.)
  • For arrow, I turned off ALTREP using options(arrow.use_altrep = FALSE), see the benchmarks-funcs.R file. Otherwise arrow does not actually read all the data into memory.

About

See the benchmark-funcs.R file in the nanoparquet repository for the code of the benchmarks.

I ran each measurement in its own subprocess, to make it easier to measure memory usage.

I did not include the package loading time in the benchmarks. nanoparquet has no dependencies and loads very quickly. Both the arrow and duckdb packages might take up to 200ms to load on the test system, because they need to load their dependencies and they are also bigger.

Show the code
if (file.exists(file.path(me, "sessioninfo.rds"))) {
  si <- readRDS(file.path(me, "sessioninfo.rds"))
} else {
  si <- sessioninfo::session_info()
  saveRDS(si, file.path(me, "sessioninfo.rds"))
}
# load sessioninfo, for the print method
library(sessioninfo)
si  
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.5.0 (2025-04-11)
#>  os       macOS Sequoia 15.5
#>  system   aarch64, darwin20
#>  ui       X11
#>  language (EN)
#>  collate  en_US.UTF-8
#>  ctype    en_US.UTF-8
#>  tz       Europe/Madrid
#>  date     2025-05-27
#>  pandoc   2.16.2 @ /Users/gaborcsardi/.local/bin/ (via rmarkdown)
#>  quarto   1.5.55 @ /usr/local/bin/quarto
#>
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package      * version    date (UTC) lib source
#>  arrow          20.0.0.2   2025-05-26 [1] CRAN (R 4.5.0)
#>  assertthat     0.2.1      2019-03-21 [1] CRAN (R 4.5.0)
#>  base64enc      0.1-3      2015-07-28 [1] CRAN (R 4.5.0)
#>  bit            4.6.0      2025-03-06 [1] CRAN (R 4.5.0)
#>  bit64          4.6.0-1    2025-01-16 [1] CRAN (R 4.5.0)
#>  cli            3.6.5.9000 2025-04-27 [1] local
#>  colorspace     2.1-1      2024-07-26 [1] CRAN (R 4.5.0)
#>  commonmark     1.9.5      2025-03-17 [1] CRAN (R 4.5.0)
#>  DBI            1.2.3      2024-06-02 [1] CRAN (R 4.5.0)
#>  digest         0.6.37     2024-08-19 [1] CRAN (R 4.5.0)
#>  dplyr        * 1.1.4      2023-11-17 [1] CRAN (R 4.5.0)
#>  duckdb         1.2.2      2025-04-29 [1] CRAN (R 4.5.0)
#>  evaluate       1.0.3      2025-01-10 [1] CRAN (R 4.5.0)
#>  farver         2.1.2      2024-05-13 [1] CRAN (R 4.5.0)
#>  fastmap        1.2.0      2024-05-15 [1] CRAN (R 4.5.0)
#>  fontawesome    0.5.3      2024-11-16 [1] CRAN (R 4.5.0)
#>  generics       0.1.3      2022-07-05 [1] CRAN (R 4.5.0)
#>  ggplot2        3.5.2      2025-04-09 [1] CRAN (R 4.5.0)
#>  glue           1.8.0      2024-09-30 [1] CRAN (R 4.5.0)
#>  gt           * 1.0.0      2025-04-05 [1] CRAN (R 4.5.0)
#>  gtable         0.3.6      2024-10-25 [1] CRAN (R 4.5.0)
#>  gtExtras     * 0.5.0      2023-09-15 [1] CRAN (R 4.5.0)
#>  htmltools      0.5.8.1    2024-04-04 [1] CRAN (R 4.5.0)
#>  htmlwidgets    1.6.4      2023-12-06 [1] CRAN (R 4.5.0)
#>  jsonlite       2.0.0      2025-03-27 [1] CRAN (R 4.5.0)
#>  keyring        1.4.0      2025-05-26 [1] local
#>  knitr          1.50       2025-03-16 [1] CRAN (R 4.5.0)
#>  labeling       0.4.3      2023-08-29 [1] CRAN (R 4.5.0)
#>  lifecycle      1.0.4      2023-11-07 [1] CRAN (R 4.5.0)
#>  litedown       0.7        2025-04-08 [1] CRAN (R 4.5.0)
#>  magrittr       2.0.3      2022-03-30 [1] CRAN (R 4.5.0)
#>  markdown       2.0        2025-03-23 [1] CRAN (R 4.5.0)
#>  munsell        0.5.1      2024-04-01 [1] CRAN (R 4.5.0)
#>  nanoparquet    0.4.2      2025-02-22 [1] CRAN (R 4.5.0)
#>  nycflights13   1.0.2      2021-04-12 [1] CRAN (R 4.5.0)
#>  paletteer      1.6.0      2024-01-21 [1] CRAN (R 4.5.0)
#>  pillar       * 1.10.2     2025-04-05 [1] CRAN (R 4.5.0)
#>  pkgconfig      2.0.3      2019-09-22 [1] CRAN (R 4.5.0)
#>  prettyunits    1.2.0      2023-09-24 [1] CRAN (R 4.5.0)
#>  purrr          1.0.4      2025-02-05 [1] CRAN (R 4.5.0)
#>  R6             2.6.1      2025-02-15 [1] CRAN (R 4.5.0)
#>  ragg           1.4.0      2025-04-10 [1] CRAN (R 4.5.0)
#>  rematch2       2.1.2      2020-05-01 [1] CRAN (R 4.5.0)
#>  rlang          1.1.6      2025-04-11 [1] CRAN (R 4.5.0)
#>  rmarkdown      2.29       2024-11-04 [1] CRAN (R 4.5.0)
#>  sass           0.4.10     2025-04-11 [1] CRAN (R 4.5.0)
#>  scales         1.3.0      2023-11-28 [1] CRAN (R 4.5.0)
#>  sessioninfo    1.2.3      2025-02-05 [1] CRAN (R 4.5.0)
#>  svglite        2.2.1      2025-05-12 [1] CRAN (R 4.5.0)
#>  systemfonts    1.2.3      2025-04-30 [1] CRAN (R 4.5.0)
#>  textshaping    1.0.1      2025-05-01 [1] CRAN (R 4.5.0)
#>  tibble         3.2.1      2023-03-20 [1] CRAN (R 4.5.0)
#>  tidyselect     1.2.1      2024-03-11 [1] CRAN (R 4.5.0)
#>  utf8           1.2.5      2025-05-01 [1] CRAN (R 4.5.0)
#>  vctrs          0.6.5      2023-12-01 [1] CRAN (R 4.5.0)
#>  withr          3.0.2      2024-10-28 [1] CRAN (R 4.5.0)
#>  xfun           0.52       2025-04-02 [1] CRAN (R 4.5.0)
#>  xml2           1.3.8      2025-03-14 [1] CRAN (R 4.5.0)
#>  yaml           2.3.10     2024-07-26 [1] CRAN (R 4.5.0)
#>
#>  [1] /Users/gaborcsardi/Library/R/arm64/4.5/library
#>  [2] /Library/Frameworks/R.framework/Versions/4.5-arm64/Resources/library
#>  * ── Packages attached to the search path.
#>
#> ──────────────────────────────────────────────────────────────────────────────