# Sys.setenv(NOT_CRAN = "true")
# install.packages("polars", repos = "https://rpolars.r-universe.dev")
はじめに
「改訂新版 前処理大全」が発売されました。写経するだけでは面白くない&Rが載っていないのは寂しいので、RのPolarsパッケージ1を触っていきたいと思います。「RのPolarsパッケージってなにそれ?」という方は先にえいつぴさんの紹介記事(Polars R パッケージについて)を読むと概要が分かって良さそうです。
r-polars
前処理大全4章を進めていきます。書籍と公式ドキュメントを参照しながら進めていきます。
パッケージのインストール
まずはPolarsをインストールしましょう。Rustで書かれたパッケージはCRANに載せるのが難しい2という背景があるらしいので、R-universeを利用することが推奨されているそうです3。
インストール出来たらパッケージを読み込みましょう。今回は実行時間を測定したい箇所があるので、tictocパッケージ4も利用します。
library(polars)
library(tictoc)
Series
とDataFrame
Series
はRのvector
、DataFrame
はdata.frane
に相当するものになります。
<- pl$Series(name = 'col1', 1:3) x
Warning in pl$Series(name = "col1", 1:3): `pl$Series()` will handle unnamed arguments differently as of 0.17.0:
- until 0.17.0, the first argument corresponds to the values and the second argument to the name of the Series.
- as of 0.17.0, the first argument will correspond to the name and the second argument to the values.
Use named arguments in `pl$Series()` or replace `pl$Series(<values>, <name>)` by `as_polars_series(<values>, <name>)` to silence this warning.
x
polars Series: shape: (3,)
Series: 'col1' [i32]
[
1
2
3
]
<- pl$DataFrame(
y 'col1' = 1:3,
'col2' = c(10.0, 20.0, 30.0),
'col3' = c('a', 'b', 'c')
) y
shape: (3, 3)
┌──────┬──────┬──────┐
│ col1 ┆ col2 ┆ col3 │
│ --- ┆ --- ┆ --- │
│ i32 ┆ f64 ┆ str │
╞══════╪══════╪══════╡
│ 1 ┆ 10.0 ┆ a │
│ 2 ┆ 20.0 ┆ b │
│ 3 ┆ 30.0 ┆ c │
└──────┴──────┴──────┘
欠損はnullとして表示されます。
<- pl$Series(name = 'col1', c(1, NA, 3)) x_missing
Warning in pl$Series(name = "col1", c(1, NA, 3)): `pl$Series()` will handle unnamed arguments differently as of 0.17.0:
- until 0.17.0, the first argument corresponds to the values and the second argument to the name of the Series.
- as of 0.17.0, the first argument will correspond to the name and the second argument to the values.
Use named arguments in `pl$Series()` or replace `pl$Series(<values>, <name>)` by `as_polars_series(<values>, <name>)` to silence this warning.
x_missing
polars Series: shape: (3,)
Series: 'col1' [f64]
[
1.0
null
3.0
]
Expression
polarsではデータの変換にExpressionを利用するようです。Expressionをselect()
に渡すことで処理結果を得ることが出来ます。
<- pl$DataFrame(
df 'col1' = 1:3,
'col2' = c(10.0, 20.0, 30.0),
'col3' = c('a', 'b', 'c')
)
# col1の2乗とcol2の和の計算を行うExpressionを定義
<- pl$col("col1")$pow(2) + pl$col("col2")
expr
$select(expr) df
shape: (3, 1)
┌──────┐
│ col1 │
│ --- │
│ f64 │
╞══════╡
│ 11.0 │
│ 24.0 │
│ 39.0 │
└──────┘
$with_columns(col4=expr) df
shape: (3, 4)
┌──────┬──────┬──────┬──────┐
│ col1 ┆ col2 ┆ col3 ┆ col4 │
│ --- ┆ --- ┆ --- ┆ --- │
│ i32 ┆ f64 ┆ str ┆ f64 │
╞══════╪══════╪══════╪══════╡
│ 1 ┆ 10.0 ┆ a ┆ 11.0 │
│ 2 ┆ 20.0 ┆ b ┆ 24.0 │
│ 3 ┆ 30.0 ┆ c ┆ 39.0 │
└──────┴──────┴──────┴──────┘
遅延実行
Polarsは関数を実行するたびに処理が走るeagerモードと、結果が必要になったタイミングで初めて実行されるlazyモードがあります。lazyモードは一連の処理を最適化してくれるそうです。
まずはeagerモードでデータを読み込む例です。
tic()
<- '../../data/reservation.parquet'
path
<- pl$read_parquet(path)
df
$filter(pl$col("reserved_at")$dt$year() >= 2016)$
dffilter(pl$col("people_num") == 1)$
select(pl$col("reservation_id", "total_price"))
shape: (280_847, 2)
┌────────────────┬─────────────┐
│ reservation_id ┆ total_price │
│ --- ┆ --- │
│ i64 ┆ i32 │
╞════════════════╪═════════════╡
│ 595174 ┆ 15500 │
│ 595177 ┆ 7900 │
│ 595183 ┆ 5600 │
│ 595189 ┆ 13200 │
│ 595202 ┆ 8500 │
│ … ┆ … │
│ 1999972 ┆ 9200 │
│ 1999977 ┆ 15200 │
│ 1999997 ┆ 7100 │
│ 1999999 ┆ 17000 │
│ 2000000 ┆ 7800 │
└────────────────┴─────────────┘
toc()
0.22 sec elapsed
次にlazyモードの例です。lazyモードを利用する際は、データの読み込み時にscan_
から始まる関数を利用します。
tic()
<- '../../data/reservation.parquet'
path
<- pl$scan_parquet(path)
df
<- df$
query filter(pl$col("reserved_at")$dt$year() >= 2016)$
filter(pl$col("people_num") == 1)$
select(pl$col("reservation_id", "total_price"))
# この時点ではまだデータは読み込まれておらず、前処理も走っていない
# ここまでの処理を実行して結果を取得
$collect() query
shape: (280_847, 2)
┌────────────────┬─────────────┐
│ reservation_id ┆ total_price │
│ --- ┆ --- │
│ i64 ┆ i32 │
╞════════════════╪═════════════╡
│ 595174 ┆ 15500 │
│ 595177 ┆ 7900 │
│ 595183 ┆ 5600 │
│ 595189 ┆ 13200 │
│ 595202 ┆ 8500 │
│ … ┆ … │
│ 1999972 ┆ 9200 │
│ 1999977 ┆ 15200 │
│ 1999997 ┆ 7100 │
│ 1999999 ┆ 17000 │
│ 2000000 ┆ 7800 │
└────────────────┴─────────────┘
toc()
0.1 sec elapsed
僕の環境では30~40%ほど高速化出来ています。最適化ありとなしでは実行計画がどのように変化するのかを確認してみましょう。
$describe_plan() query
SELECT [col("reservation_id"), col("total_price")] FROM
FILTER [(col("people_num")) == (1.0)] FROM
FILTER [(col("reserved_at").dt.year()) >= (2016.0)] FROM
Parquet SCAN ../../data/reservation.parquet
PROJECT */11 COLUMNS
$describe_optimized_plan() query
SELECT [col("reservation_id"), col("total_price")] FROM
Parquet SCAN ../../data/reservation.parquet
PROJECT 4/11 COLUMNS
SELECTION: [([(col("reserved_at").dt.year().cast(Float64)) >= (2016.0)]) & ([(col("people_num").cast(Float64)) == (1.0)])]
最適化なしの実行計画は特にコメントする箇所はないですね。普通に全データを読み込んで、二回フィルターをかけています。最適化ありの実行計画を確認すると、前処理に利用する4列のみを読み込んでいることが分かります。また、読み込みの時点で抽出条件を適用している事も分かります。かしこい。
DataFrame
のlazy()
を用いると、データを読み込んだ後からでも、その後の処理をlazyモードで行うことが出来ます。
tic()
<- '../../data/reservation.parquet'
path
<- pl$read_parquet(path)
df
<- df$lazy()$
query filter(pl$col("reserved_at")$dt$year() >= 2016)$
filter(pl$col("people_num") == 1)$
select(pl$col("reservation_id", "total_price"))
$collect() query
shape: (280_847, 2)
┌────────────────┬─────────────┐
│ reservation_id ┆ total_price │
│ --- ┆ --- │
│ i64 ┆ i32 │
╞════════════════╪═════════════╡
│ 595174 ┆ 15500 │
│ 595177 ┆ 7900 │
│ 595183 ┆ 5600 │
│ 595189 ┆ 13200 │
│ 595202 ┆ 8500 │
│ … ┆ … │
│ 1999972 ┆ 9200 │
│ 1999977 ┆ 15200 │
│ 1999997 ┆ 7100 │
│ 1999999 ┆ 17000 │
│ 2000000 ┆ 7800 │
└────────────────┴─────────────┘
toc()
0.19 sec elapsed
あんまり速度が変わらないですね。データの読み込みがボトルネックとなるケースもありそうなので、こだわりが無ければscan_
から始まる関数を利用してよさそうです。
おわりに
いつかPolars触るぞ!と決意してからだいぶ月日がた経ってしまいましたが、ようやく重い腰を上げることが出来ました。RとPythonでは、一部関数名が異なるケースもあったので注意して利用したいと思います。
また、Polarsは破壊的変更が頻繁に行われるそうなので、バージョンを明記しておきます。
polars_info()
Polars R package version : 0.16.4
Rust Polars crate version: 0.39.2
Thread pool size: 20
Features:
default TRUE
full_features TRUE
disable_limit_max_threads TRUE
nightly TRUE
sql TRUE
rpolars_debug_print FALSE
Code completion: deactivated