🐻‍❄️

Polars R パッケージを書き直しました

に公開

r-polars・おぼえていますか

以前、Polars Rパッケージを紹介しました。

https://zenn.dev/eitsupi/articles/r-polars-2023

改めて説明すると、Pythonにデータフレームを持ち込んだpandasよりも高速であると評判のPolarsというRust製のクエリエンジンをバックエンドに持つPythonパッケージがあり、それのR版です。
データフレーム本家であるRとしては見逃せない存在ですよね(?)

前回の紹介から2年、今回は(私が頑張って書き直した)新生Polars Rパッケージを紹介します。

2年間の進捗

前回の記事を書いてから半年くらい経った2024年夏頃、当時のr-polarsとは全く別に、全く新しいPolars Rパッケージを書き始めてみることにしました。

https://github.com/eitsupi/neo-r-polars

というのも、当時のr-polarsの根本的な設計にいくつか不満があったものの、上流の(Rust)Polarsの変更に追従しつつ大規模なリファクタリングを行うよりも最初から書いた方が簡単そうに思えたからです[1]

不満点は以下のようなもので、一時的に書き直しでもの凄く苦労するとしても、メンテナンス性を大幅に向上させられれば総体としては開発の負担は減ると考えました。

  • 参考にしているPython Polarsと内部構造が異なりすぎており、Python Polarsの更新を参考にメンテナンスするのが大変。
  • RとRustの変換が複数箇所で行われており、動作に一貫性がなくメンテナンスも大変。
  • 「依存Rパッケージ0」に対する拘りにより、rlangパッケージを使えば簡単に実現できることを実現できず、メンテナンスも大変。

書き始めたときにはR 4.5.0のリリース(2025年4月)を目標にしていましたが、最終的に旧実装を置換(新実装をR Polars 1.0.0としてリリース)したのは2025年7月でした。

置換してから半年ほど経ちますが、書き直しでPython Polarsと構造を一致させたおかげで狙い通り上流への追従はかなり楽になった気がしており[2]、現在はバージョン1.6.0まで進んでいます。

割とちゃんと変更履歴を書いているので、詳しくはこちらをご覧ください。

https://github.com/pola-rs/r-polars/blob/main/NEWS.md

Python Polarsからの機能移植方法についても結構頑張って書いてあるので、我こそはという方は開発に参加いただけると嬉しいです!

https://github.com/pola-rs/r-polars/blob/main/DEVELOPMENT.md

DeepWikiも設定しているので、色々日本語で質問したりもできます。すごい。

https://deepwiki.com/pola-rs/r-polars

最近追加した独自機能

書き直しによって大きな改修は一段落したもののR独自部分でやりたいことは残っており、最近実現できたものもあるので紹介します。

S7クラスの採用

依存Rパッケージ0を掲げていた旧実装から方針転換しrlangパッケージに依存していた新実装ですが、最近になりS7にも手を出しました。RustベースのRパッケージとしては、初のS7採用ではないかと思われます。

S7をご存じない場合はこちらの記事がおすすめです。

https://qiita.com/Gotoubun_taiwan/items/05b0d345a4af859d1d16

まだ採用はごく一部のクラスだけですが、将来的にはDataFrameなどの主要なクラスもS7で書き換え、多重ディスパッチのサポートを狙っています。
[のメソッドなんか、現在はifだらけでかなり複雑になってしまっていますが、ダブルディスパッチすればかなりすっきりするはず。

Python Polarsとのデータ交換

今更ですけれど、PolarsってPythonファーストなんですよね。確かにクエリエンジンはRustで書かれているのですが、Pythonからじゃないと使えない機能が結構あります[3]
Polarsの開発者の所属しているPolars社の商品であるPolars Cloudも、Pythonからじゃないとアクセスできません。

書き直し始めたころから私の念頭にはこれまたPythonからしか利用できないGPUエンジンがあり、「クエリプラン(LazyFrame)をPythonに送ってPython側でGPUエンジンを実行し最終結果をApache Arrow C Streamインターフェースを使いゼロコピーでR側に返す」ということをやりたかったのですが、最近ようやくできるようになりました[4]
とは言っても私はGPUエンジンを使える環境にないので試したことはなく、実際に有用かどうかは謎ですが……。

もし使われる方がいらしたら、感想をいただけるとありがたいです。

ちなみに、この機能を実装する過程でPython PolarsのWindows用ビルドから他のプラットフォームにLazyFrameを送信できないバグに気付き、上流を修正しました[5]。悪いのが自分の書いたR側じゃなくてPython側だと気付くまで何時間も時間を浪費したので、エラーメッセージはちゃんと読んだ方が良かったです……。

で、Polarsって速いの?

Polars界隈では「速さのために使い始め、構文を理由に使い続ける」みたいなことがよく言われているようで、当然皆さん速さが気になるわけです。

ただ、私は開発に参加しているものの使っているわけではないので、ぶっちゃけると分かりません!

「Polarsって速いんですか?」と聞かれたら「DuckDBの方がオススメです!」と回答するようにしています。

今回はせっかくなので、簡単にベンチマークしてみましょう。
4年前にarrowパッケージとduckdbパッケージのベンチマークをとったのと同じやつをやってみます。

https://qiita.com/eitsupi/items/ce3e1b1fb0e45e0d45e3

比較対象として、4年前から続投のarrowパッケージ(クエリエンジンはAceroという名前)と、DuckDB、加えて新たにDataFusion[6]の代打として、内部的にDataFusionを使用しているsedonadbパッケージの最新開発バージョンを選びました(sedonadb 0.2.0として近々リリースされそう)。
DuckDBはSQLに加えてdbplyr経由とduckplyr経由でdplyr構文のものも試してみましょう。

polarsはPolars構文そのままと、tidypolarsパッケージ経由でdplyr構文のものを使います。

polarsパッケージはas_tibble()のS3メソッドを実装しており、パフォーマンスが向上するかも知れないのでengine = "streaming"オプションを指定してストリーミングエンジンを使用します[7]

R
bench::mark(
  acero_dplyr = {
    arrow::open_dataset("lineitemsf1.snappy.parquet") |>
      dplyr::summarise(revenue = sum(l_extendedprice * l_discount, na.rm = TRUE)) |>
      dplyr::collect()
  },
  duckdb_dbplyr = {
    dplyr::tbl(duckdb::default_conn(), "lineitemsf1.snappy.parquet") |>
      dplyr::summarise(revenue = sum(l_extendedprice * l_discount, na.rm = TRUE)) |>
      dplyr::collect()
  },
  duckdb_duckplyr = {
    duckplyr::read_parquet_duckdb("lineitemsf1.snappy.parquet") |>
      dplyr::summarise(revenue = sum(l_extendedprice * l_discount, na.rm = TRUE)) |>
      dplyr::collect()
  },
  duckdb = {
    duckdb::sql_query("
      from read_parquet('lineitemsf1.snappy.parquet')
      select revenue: sum(l_extendedprice * l_discount)
    ") |>
      tibble::as_tibble()
  },
  sedonadb = {
    sedonadb::sd_sql("
      select sum(l_extendedprice * l_discount) as revenue
      from 'lineitemsf1.snappy.parquet'
    ") |>
      tibble::as_tibble()
  },
  polars_tidypolars = {
    tidypolars::scan_parquet_polars("lineitemsf1.snappy.parquet") |>
      dplyr::summarise(revenue = sum(l_extendedprice * l_discount, na.rm = TRUE)) |>
      tibble::as_tibble(engine = "streaming") # tidypolars 0.15.1 段階ではcollect() だとtibbleを返してくれず
  },
  polars = {
    polars::pl$scan_parquet("lineitemsf1.snappy.parquet")$select(
      revenue = (polars::pl$col("l_extendedprice") * polars::pl$col("l_discount"))$sum()
    ) |>
      tibble::as_tibble(engine = "streaming")
  }
)
結果
#> # A tibble: 7 × 6
#>   expression             min   median `itr/sec` mem_alloc `gc/sec`
#>   <bch:expr>        <bch:tm> <bch:tm>     <dbl> <bch:byt>    <dbl>
#> 1 acero_dplyr        149.7ms  156.1ms      6.25   41.83MB     0
#> 2 duckdb_dbplyr      109.4ms  113.7ms      8.35   24.18MB     2.09
#> 3 duckdb_duckplyr     62.8ms   69.1ms     14.4     3.71MB     0
#> 4 duckdb              59.6ms   61.5ms     14.9      4.9KB     0
#> 5 sedonadb            56.8ms   73.1ms     13.6      6.7MB     0
#> 6 polars_tidypolars   62.4ms   67.2ms     13.5    13.15MB     2.26
#> 7 polars                50ms   53.2ms     18.8    99.51KB     0

どうやら今回はPolarsが一番でしたね! tidypolarsも十分速いようです。

この手のベンチマークは実行環境やユースケースによって結果は大きく変わるのであくまで参考ですが、DuckDBに負けないパフォーマンスが出ているのを見るのは嬉しいです。

Aceroは速度について熱心に開発されているわけではないため[8]、上に貼った4年前の記事の頃と比べると相対的に遅くなっていると思われ、結果に表れていそうです。
DuckDB、Polars、DataFusionはバチバチに競い合っている仲なので、パフォーマンス拮抗していそうですね。

duckplyrとtidypolarsの比較をもっと見たい方は以下の記事がオススメです。

https://ginolhac.github.io/posts/2025-07-17_polar-duckdb/

なお、実行環境は以下の通りでした。

セッション情報
R
sessioninfo::session_info()
#> ─ Session info ───────────────────────────────────────────────────────────────
#>  setting  value
#>  version  R version 4.5.1 (2025-06-13)
#>  os       Ubuntu 24.04.2 LTS
#>  system   x86_64, linux-gnu
#>  ui       X11
#>  language (EN)
#>  collate  en_US.UTF-8
#>  ctype    en_US.UTF-8
#>  tz       Etc/UTC
#>  date     2025-11-30
#>  pandoc   3.7.0.2 @ /usr/bin/ (via rmarkdown)
#>  quarto   1.8.26 @ /opt/quarto/bin/quarto
#>
#> ─ Packages ───────────────────────────────────────────────────────────────────
#>  package     * version    date (UTC) lib source
#>  arrow         22.0.0     2025-10-29 [1] RSPM
#>  assertthat    0.2.1      2019-03-21 [1] RSPM
#>  bench         1.1.4      2025-01-16 [1] RSPM
#>  bit           4.6.0      2025-03-06 [1] RSPM
#>  bit64         4.6.0-1    2025-01-16 [1] RSPM
#>  blob          1.2.4      2023-03-17 [1] RSPM
#>  cachem        1.1.0      2024-05-16 [1] RSPM
#>  cli           3.6.5      2025-04-23 [1] RSPM
#>  collections   0.3.9      2025-08-18 [1] RSPM
#>  DBI           1.2.3      2024-06-02 [1] RSPM
#>  dbplyr        2.5.1      2025-09-10 [1] RSPM
#>  digest        0.6.37     2024-08-19 [1] RSPM
#>  dplyr         1.1.4      2023-11-17 [1] RSPM
#>  duckdb        1.4.2      2025-11-17 [1] RSPM
#>  duckplyr      1.1.3      2025-11-04 [1] RSPM
#>  evaluate      1.0.3      2025-01-10 [1] RSPM
#>  fastmap       1.2.0      2024-05-15 [1] RSPM
#>  fs            1.6.6      2025-04-12 [1] RSPM
#>  generics      0.1.4      2025-05-09 [1] RSPM
#>  geoarrow      0.4.1      2025-11-19 [1] RSPM
#>  glue          1.8.0      2024-09-30 [1] RSPM
#>  htmltools     0.5.8.1    2024-04-04 [1] RSPM
#>  knitr         1.50       2025-03-16 [1] RSPM
#>  lifecycle     1.0.4      2023-11-07 [1] RSPM
#>  magrittr      2.0.4      2025-09-12 [1] RSPM
#>  memoise       2.0.1      2021-11-26 [1] RSPM
#>  nanoarrow     0.7.0.9000 2025-11-21 [1] https://apache.r-universe.dev (R 4.5.2)
#>  pillar        1.11.1     2025-09-17 [1] RSPM
#>  pkgconfig     2.0.3      2019-09-22 [1] RSPM
#>  polars        1.6.0      2025-11-15 [1] https://r~
#>  profmem       0.7.0      2025-05-02 [1] RSPM
#>  purrr         1.2.0      2025-11-04 [1] RSPM
#>  R6            2.6.1      2025-02-15 [1] RSPM
#>  reprex        2.1.1      2024-07-06 [1] RSPM
#>  rlang         1.1.6      2025-04-11 [1] RSPM
#>  rmarkdown     2.29       2024-11-04 [1] RSPM
#>  S7            0.2.1      2025-11-14 [1] RSPM
#>  sedonadb      0.1.0.9000 2025-11-30 [1] https://apache.r-universe.dev (R 4.5.2)
#>  sessioninfo   1.2.3      2025-02-05 [1] RSPM
#>  tibble        3.3.0      2025-06-08 [1] RSPM
#>  tidypolars    0.15.1     2025-11-16 [1] https://r-multiverse.r-universe.dev (R 4.5.1)
#>  tidyr         1.3.1      2024-01-24 [1] RSPM
#>  tidyselect    1.2.1      2024-03-11 [1] RSPM
#>  utf8          1.2.6      2025-06-08 [1] RSPM
#>  vctrs         0.6.5      2023-12-01 [1] RSPM
#>  withr         3.0.2      2024-10-28 [1] RSPM
#>  wk            0.9.4      2024-10-11 [1] RSPM
#>  xfun          0.52       2025-04-02 [1] RSPM
#>  yaml          2.3.10     2024-07-26 [1] RSPM
#>
#>  [1] /usr/local/lib/R/site-library
#>  [2] /usr/local/lib/R/library
#>
#> ──────────────────────────────────────────────────────────────────────────────

まとめ

趣味で開発した、新生Polars Rパッケージについて好き勝手に紹介しました。
もしかしたら皆さんのユースケースを高速化できるかも知れないので、頭の片隅にでも置いておいてもらえれば幸いです。

Rustに興味あるそこのあなたは、ぜひ開発に参加してみてください!

脚注
  1. tokeiでr-polarsの合計行数を計ってみると、Rファイルは53,623行でRustファイルは10,244行でした。参考として、ggplot2はRファイル64,106行、dplyrはRファイル39,345行でした。 ↩︎

  2. 全然楽ではなく、直近ではR Polars 1.6.0の参照先であるPython Polars 1.35.2 から 1.36.0(今日時点未リリース)のRust部分の差分がデカすぎて泣きそうになっていました。 ↩︎

  3. Rustのfeatureフラグでpythonfeatureを有効にしないと使えない機能がRustコード内に偏在している等。 ↩︎

  4. 現時点では開発版のnanoarrowパッケージに頼っています。nanoarrow 0.8.0が年内にリリースされそうなので、その後ドキュメントを書こうと考えています。 ↩︎

  5. 「Polars CloudはWindowsから一切使われていないのか……?」と不安になりました。 ↩︎

  6. Apache DataFusionはPolarsと同じくRust製のクエリエンジンですが、PythonファーストのPolarsと違いRustファーストで、カスタマイズしながら何かの製品に組み込んで使われることが多いようです。参考: Apache DataFusion とは ↩︎

  7. Polarsの開発状況を追っていると、そろそろデフォルトのエンジンがストリーミングエンジンの方になりそうな感じに見えています。 ↩︎

  8. "While Acero isn’t intended to be a cutting-edge query engine competing with systems like DuckDB or Velox" https://github.com/apache/arrow/discussions/47331#discussioncomment-14142992 ↩︎

Discussion