改訂新版 前処理大全6章 集約 with r-polars

R
Polars
Author

statditto

Published

June 6, 2024

はじめに

Warning

2024/06/23、eitsupiさんにコメントを頂いた箇所を修正しました。また、本記事の執筆時に利用したデータの一部は型が正しくないものがありました。本記事上のコードの一部では、型をキャストしていますが、最新のサポートページのデータを利用する場合は基本的に型のキャストを行う必要はありません。

前処理大全6章をRのPolarsを利用して進めていきます。書籍と公式ドキュメントを参照しながら進めていきます。パッケージのインストール周りは前回の記事に記載してあります。

library(polars)

集約

データ全体の集約と代表的な集約関数

キャンセルを除いた集計値の算出

path <- '../../data/reservation.parquet'

df <- pl$read_parquet(path)

Sum関数を利用したときにオーバーフローしても自動でInt64にキャストされないので注意です。Rustでは過去にissueがたてられており1sumなどは自動でInt64にキャストする仕様2に変わったようですが、RのPolarsはそのようになっていないみたいです。 取り消し線のコメントは僕の勘違いでした。パフォーマンスを優先するため、オーバーフローのチェックを行わない仕様とのことです。

単純に合計を取る場合
df$filter(pl$col('status') != 'canceled')$
  select(pl$col('reservation_id')$len()$alias('reservation_cnt'),
         pl$col('total_price')$sum()$alias('sales'))
shape: (1, 2)
┌─────────────────┬────────────┐
│ reservation_cnt ┆ sales      │
│ ---             ┆ ---        │
│ u32             ┆ i32        │
╞═════════════════╪════════════╡
│ 1799589         ┆ -965515632 │
└─────────────────┴────────────┘
合計を取る前に型をキャスト
df$filter(pl$col('status') != 'canceled')$
  select(pl$col('reservation_id')$len()$alias('reservation_cnt'),
         pl$col('total_price')$cast(pl$dtypes$Int64)$sum()$alias('sales'),
         pl$col('total_price')$mean()$alias('mean_sales'),
         pl$col('total_price')$min()$alias('min_sales'),
         pl$col('total_price')$max()$alias('max_sales'),
         pl$col('total_price')$var()$alias('var_sales'),
         pl$col('total_price')$std()$alias('std_sales'))
shape: (1, 7)
┌─────────────────┬─────────────┬──────────────┬───────────┬───────────┬───────────┬──────────────┐
│ reservation_cnt ┆ sales       ┆ mean_sales   ┆ min_sales ┆ max_sales ┆ var_sales ┆ std_sales    │
│ ---             ┆ ---         ┆ ---          ┆ ---       ┆ ---       ┆ ---       ┆ ---          │
│ u32             ┆ i64         ┆ f64          ┆ i32       ┆ i32       ┆ f64       ┆ f64          │
╞═════════════════╪═════════════╪══════════════╪═══════════╪═══════════╪═══════════╪══════════════╡
│ 1799589         ┆ 72048928400 ┆ 40036.324072 ┆ 4000      ┆ 597000    ┆ 1.2237e9  ┆ 34981.716778 │
└─────────────────┴─────────────┴──────────────┴───────────┴───────────┴───────────┴──────────────┘

予約顧客のユニークカウントの算出

ユニークカウントの計算(nullを含む)
df$filter(pl$col('status') != 'canceled')$
  select(pl$col('reservation_id')$n_unique())
shape: (1, 1)
┌────────────────┐
│ reservation_id │
│ ---            │
│ u32            │
╞════════════════╡
│ 1799589        │
└────────────────┘

予約単価の中央値およびパーセンタイル値の算出

中央値およびパーセンタイル値の算出
df$filter(pl$col('status') != 'canceled')$
  select(pl$col('total_price')$median()$alias('median_sales'),
         pl$col('total_price')$quantile(0.25, interpolation = 'linear')$alias('p25_sales'),
         pl$col('total_price')$quantile(0.75, interpolation = 'linear')$alias('p75_sales'))
shape: (1, 3)
┌──────────────┬───────────┬───────────┐
│ median_sales ┆ p25_sales ┆ p75_sales │
│ ---          ┆ ---       ┆ ---       │
│ f64          ┆ f64       ┆ f64       │
╞══════════════╪═══════════╪═══════════╡
│ 30400.0      ┆ 17200.0   ┆ 51000.0   │
└──────────────┴───────────┴───────────┘

ホテルごとの宿泊人数の最頻値の算出

グループごとに最頻値を算出
df$filter(pl$col('status') != 'canceled')$
  group_by('hotel_id')$
  agg(pl$col('people_num')$mode()$first()$alias('mode_people_num'))
shape: (4_527, 2)
┌──────────┬─────────────────┐
│ hotel_id ┆ mode_people_num │
│ ---      ┆ ---             │
│ i64      ┆ i32             │
╞══════════╪═════════════════╡
│ 2105     ┆ 3               │
│ 789      ┆ 4               │
│ 3281     ┆ 3               │
│ 673      ┆ 2               │
│ 1837     ┆ 4               │
│ …        ┆ …               │
│ 1694     ┆ 2               │
│ 116      ┆ 3               │
│ 4966     ┆ 3               │
│ 2611     ┆ 3               │
│ 1045     ┆ 3               │
└──────────┴─────────────────┘

グループごとの集約

path <- '../../data/reservation.parquet'

df <- pl$read_parquet(path)

ホテルごとの売り上げの集計

グループごとにデータ数をカウント
df$filter(pl$col('status') != 'canceled')$
  group_by('hotel_id', 'customer_id')$
  agg(pl$len())
shape: (1_798_305, 3)
┌──────────┬─────────────┬─────┐
│ hotel_id ┆ customer_id ┆ len │
│ ---      ┆ ---         ┆ --- │
│ i64      ┆ i64         ┆ u32 │
╞══════════╪═════════════╪═════╡
│ 668      ┆ 82865       ┆ 1   │
│ 783      ┆ 333651      ┆ 1   │
│ 3548     ┆ 451423      ┆ 1   │
│ 891      ┆ 292150      ┆ 1   │
│ 1232     ┆ 334558      ┆ 1   │
│ …        ┆ …           ┆ …   │
│ 1939     ┆ 439107      ┆ 1   │
│ 184      ┆ 156421      ┆ 1   │
│ 1345     ┆ 285625      ┆ 1   │
│ 1179     ┆ 36474       ┆ 1   │
│ 456      ┆ 457964      ┆ 1   │
└──────────┴─────────────┴─────┘

数値の区間ごとの集約

等間隔の価格帯ごとにホテル数を集計

path <- '../../data/hotel.parquet'

df <- pl$read_parquet(path)
価格を区間に丸めて集約
df$group_by((pl$col('unit_price') / 5000)$floor()$cast(pl$Int32) * 5000)$
  agg(pl$len())
shape: (8, 2)
┌────────────┬──────┐
│ unit_price ┆ len  │
│ ---        ┆ ---  │
│ f64        ┆ u32  │
╞════════════╪══════╡
│ 0.0        ┆ 220  │
│ 30000.0    ┆ 6    │
│ 25000.0    ┆ 29   │
│ 5000.0     ┆ 2760 │
│ 20000.0    ┆ 139  │
│ 15000.0    ┆ 456  │
│ 10000.0    ┆ 1389 │
│ 35000.0    ┆ 1    │
└────────────┴──────┘

非等間隔の価格帯ごとにホテル数を集計

価格を区間に丸めて集約
df$group_by(
  pl$when(pl$col('unit_price') < 5000)$then(0)$
    when(pl$col('unit_price') < 10000)$then(5000)$
    when(pl$col('unit_price') < 20000)$then(10000)$
    when(pl$col('unit_price') < 30000)$then(20000)$
    otherwise(30000)$
    alias('unit_price')
  )$
  agg(pl$len())
shape: (5, 2)
┌────────────┬──────┐
│ unit_price ┆ len  │
│ ---        ┆ ---  │
│ f64        ┆ u32  │
╞════════════╪══════╡
│ 0.0        ┆ 220  │
│ 30000.0    ┆ 7    │
│ 5000.0     ┆ 2760 │
│ 20000.0    ┆ 168  │
│ 10000.0    ┆ 1845 │
└────────────┴──────┘

whenは順序が書いた通りではないみたいです。

時間の区間ごとの集約

月ごとの売り上げの集計

path <- '../../data/reservation.parquet'

df <- pl$read_parquet(path)
月ごとに総和を計算
df$filter(pl$col('status') != 'canceled')$
  group_by(pl$col('checkout_date')$dt$truncate('1mo'))$
  agg(pl$col('total_price')$cast(pl$dtypes$Int64)$sum())
shape: (60, 2)
┌──────────────────────────┬─────────────┐
│ checkout_date            ┆ total_price │
│ ---                      ┆ ---         │
│ datetime[ns, Asia/Tokyo] ┆ i64         │
╞══════════════════════════╪═════════════╡
│ 2019-06-01 00:00:00 JST  ┆ 696780000   │
│ 2019-03-01 00:00:00 JST  ┆ 1462116000  │
│ 2019-09-01 00:00:00 JST  ┆ 707822000   │
│ 2017-07-01 00:00:00 JST  ┆ 1448508800  │
│ 2018-01-01 00:00:00 JST  ┆ 730334300   │
│ …                        ┆ …           │
│ 2016-07-01 00:00:00 JST  ┆ 1457527800  │
│ 2017-01-01 00:00:00 JST  ┆ 727519600   │
│ 2018-06-01 00:00:00 JST  ┆ 694748200   │
│ 2018-12-01 00:00:00 JST  ┆ 1466301400  │
│ 2017-04-01 00:00:00 JST  ┆ 699264200   │
└──────────────────────────┴─────────────┘

条件を満たす行の存在判定

チェックインの7日以内に予約をキャンセルしたことがある顧客の判定

path <- '../../data/reservation.parquet'

df <- pl$read_parquet(path)
複数の条件で判定
df$group_by('customer_id')$
  agg(
    (
      (pl$col('status') == 'canceled') &
        ((pl$col('checkin_date') - pl$col('canceled_at'))$dt$total_days() <= 7)
    )$any()
  )
shape: (417_368, 2)
┌─────────────┬────────┐
│ customer_id ┆ status │
│ ---         ┆ ---    │
│ i64         ┆ bool   │
╞═════════════╪════════╡
│ 9179        ┆ false  │
│ 85993       ┆ false  │
│ 244870      ┆ false  │
│ 418023      ┆ false  │
│ 143         ┆ false  │
│ …           ┆ …      │
│ 217973      ┆ false  │
│ 102109      ┆ false  │
│ 457847      ┆ false  │
│ 432808      ┆ false  │
│ 9697        ┆ false  │
└─────────────┴────────┘

条件を満たす行のみの集約

顧客ごとの売上とキャンセル率の算出

集約関数とfiterの組み合わせ
df$group_by('customer_id')$
  agg(pl$col('total_price')$filter(pl$col('status') != 'canceled')$sum(),
        (pl$col('reservation_id')$filter(pl$col('status') == 'canceled')$len()$cast(pl$dtypes$Float32)
         / pl$len())$alias('cancel_rate')
  )
shape: (417_368, 3)
┌─────────────┬─────────────┬─────────────┐
│ customer_id ┆ total_price ┆ cancel_rate │
│ ---         ┆ ---         ┆ ---         │
│ i64         ┆ i32         ┆ f64         │
╞═════════════╪═════════════╪═════════════╡
│ 149302      ┆ 15000       ┆ 0.333333    │
│ 463245      ┆ 163800      ┆ 0.0         │
│ 445426      ┆ 337700      ┆ 0.0         │
│ 494828      ┆ 388300      ┆ 0.0         │
│ 367162      ┆ 63000       ┆ 0.2         │
│ …           ┆ …           ┆ …           │
│ 260974      ┆ 222900      ┆ 0.0         │
│ 214433      ┆ 26700       ┆ 0.0         │
│ 23327       ┆ 39500       ┆ 0.0         │
│ 296755      ┆ 285100      ┆ 0.0         │
│ 241696      ┆ 83600       ┆ 0.0         │
└─────────────┴─────────────┴─────────────┘

おわりに

RのPolarsは型のキャストが暗黙的に行われないケースがあるようでした。気付かずにやらかしそうなので注意したいですね。除算の際はfloatに自動でキャストしても良さそうな気もしますがどうなんでしょうか。Pythonの実装と比較してみて差異があったらissueでも投げてみようと思います。また、書籍のサポートページ3のPythonコードをcolabで実行したところ、謎のエラーが出てしまいました。

colab上のPolarsのバージョンが低いことが原因でした。

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