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

R
Polars
Author

statditto

Published

June 6, 2024

はじめに

前処理大全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             │
╞══════════╪═════════════════╡
│ 4326     ┆ 1               │
│ 4737     ┆ 4               │
│ 2629     ┆ 1               │
│ 3814     ┆ 3               │
│ 405      ┆ 2               │
│ …        ┆ …               │
│ 917      ┆ 4               │
│ 3007     ┆ 2               │
│ 4448     ┆ 4               │
│ 4704     ┆ 4               │
│ 4055     ┆ 1               │
└──────────┴─────────────────┘

グループごとの集約

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 │
╞══════════╪═════════════╪═════╡
│ 1454     ┆ 433986      ┆ 1   │
│ 2996     ┆ 385990      ┆ 1   │
│ 4196     ┆ 369783      ┆ 1   │
│ 2183     ┆ 439413      ┆ 1   │
│ 3193     ┆ 260351      ┆ 1   │
│ …        ┆ …           ┆ …   │
│ 4431     ┆ 34821       ┆ 1   │
│ 1718     ┆ 277347      ┆ 1   │
│ 786      ┆ 138276      ┆ 1   │
│ 3167     ┆ 263892      ┆ 1   │
│ 793      ┆ 303861      ┆ 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  │
╞════════════╪══════╡
│ 30000.0    ┆ 6    │
│ 0.0        ┆ 220  │
│ 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  │
╞════════════╪══════╡
│ 30000.0    ┆ 7    │
│ 0.0        ┆ 220  │
│ 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-09-01 00:00:00 JST  ┆ 707822000   │
│ 2019-03-01 00:00:00 JST  ┆ 1462116000  │
│ 2019-06-01 00:00:00 JST  ┆ 696780000   │
│ 2015-05-01 00:00:00 JST  ┆ 2185718000  │
│ 2016-02-01 00:00:00 JST  ┆ 688327500   │
│ …                        ┆ …           │
│ 2015-02-01 00:00:00 JST  ┆ 650411000   │
│ 2016-10-01 00:00:00 JST  ┆ 717107300   │
│ 2018-09-01 00:00:00 JST  ┆ 711891500   │
│ 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   │
╞═════════════╪════════╡
│ 67394       ┆ false  │
│ 221135      ┆ false  │
│ 268197      ┆ false  │
│ 450145      ┆ false  │
│ 15878       ┆ false  │
│ …           ┆ …      │
│ 21103       ┆ false  │
│ 239481      ┆ true   │
│ 331104      ┆ false  │
│ 379723      ┆ false  │
│ 242616      ┆ 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         │
╞═════════════╪═════════════╪═════════════╡
│ 28457       ┆ 267000      ┆ 0.142857    │
│ 276989      ┆ 232600      ┆ 0.0         │
│ 101341      ┆ 264000      ┆ 0.0         │
│ 442931      ┆ 171100      ┆ 0.2         │
│ 120735      ┆ 297500      ┆ 0.0         │
│ …           ┆ …           ┆ …           │
│ 487879      ┆ 285600      ┆ 0.0         │
│ 400957      ┆ 37000       ┆ 0.0         │
│ 301078      ┆ 83500       ┆ 0.0         │
│ 224142      ┆ 121800      ┆ 0.0         │
│ 296365      ┆ 133900      ┆ 0.0         │
└─────────────┴─────────────┴─────────────┘

おわりに

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

---------------------------------------------------------------------------
AttributeError                            Traceback (most recent call last)
<ipython-input-11-240f1ef56a75> in <cell line: 2>()
      4     .filter(pl.col("status") != "canceled")
      5     # (2)hotel_idとcustomer_idの値の組ごとにデータ数をカウント
----> 6     .group_by(["hotel_id", "customer_id"]).agg(pl.len())
      7 )

AttributeError: module 'polars' has no attribute 'len'

どうして…… 原因がわかったらこちらに関しても追記したいと思います。

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