Yamamotoの日記

Yale SOM MBA。金融工学、IT関係の記事を書きたいです

PythonでRのgroup_nestを実現する方法

PythonのPolarsでRのgroup_nestを実現する

Polarsでgroup_nestがしたい

R or Python

僕がRで分析するときはいつもこちらの記事を参考にしています。 https://qiita.com/kilometer/items/7184904765fbf0f33f04

tidyverseでデータを読み込んだ後は、なんらかの変数でグループ化して、ネストして、各データセットに対してmapを通じて処理をかけていくと。Rはスクリプト言語でありベクトル化された処理に最適化されているので、大容量のデータを扱う場合はこちらの方が速いですし、何より読みやすいです。

Rはユーザーの活動が活発でTokyoRとかのカンファレンスも多いですし、ネット上でも色々な記事があるので僕にとっては第一(プログラミング)言語なのですが、どうも、金融業界ではそういうわけではないらしいです。Yale大学でも、ファイナンスの教授はみんなPython。一人、統計学出身ぽいBryan Kellyという教授はかつてRを使っていたようなのですが、授業ではPythonを使うよう指定されました。また、彼も含め教授陣の何人かが副業で働いているAQRというヘッジファンドでも、システムはすべてPythonで構築されているそうです(どっちが本業なんでしょうか。。。?)。

個人的には、システムの信頼性やスピードなどを考えると本番トレーディングのシステムはC++かRustで書かれているのではないかと推測していますが、 少なくとも、トレーディング戦略を構築するリサーチ環境はPythonがほとんどだと思います。 と、思いましたがトレーディングシステムを開発するエンジニアもPythonが技術要件になっているので本当にPythonなのかも。。。求人(11/17アクセス)

ちなみに、弊社は偉い人たちの意向でFortranC#が使われています。この業界ではだいぶ珍しいようです。僕としては業界標準に近づけたいので、新しいシステム構築やレポート作成時にはpythonを使うようにしています。

Python: Polars

というわけで、就職してからはPythonに乗り換えようとしてきたわけですが、Pythonはやはりデータ分析というよりはシステム構築がメインなのか、使い勝手が悪いです。その中でも、Polarsというパッケージがtidyverseに似た操作感なので使ってみました。

簡単な使い方は様々なサイトで解説されているので、日本語の記事がほとんどない、group_nestについて記事を記載します。

簡単に解説すると、group_byでグルーピングし、aggにてpl.listとpl.struct型の組み合わせに変換することでデータフレームっぽく扱います。

from palmerpenguins import load_penguins
import polars as pl
df = pl.from_pandas(load_penguins())
# Grouping by code
df.group_by("species").agg(pl.all())
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
str list[str] list[f64] list[f64] list[f64] list[f64] list[str] list[i64]
"Gentoo" ["Biscoe", "Biscoe", … "Biscoe"] [46.1, 50.0, … 49.9] [13.2, 16.3, … 16.1] [211.0, 230.0, … 213.0] [4500.0, 5700.0, … 5400.0] ["female", "male", … "male"] [2007, 2007, … 2009]
"Chinstrap" ["Dream", "Dream", … "Dream"] [46.5, 50.0, … 50.2] [17.9, 19.5, … 18.7] [192.0, 196.0, … 198.0] [3500.0, 3900.0, … 3775.0] ["female", "male", … "female"] [2007, 2007, … 2009]
"Adelie" ["Torgersen", "Torgersen", … "Dream"] [39.1, 39.5, … 41.5] [18.7, 17.4, … 18.5] [181.0, 186.0, … 201.0] [3750.0, 3800.0, … 4000.0] ["male", "female", … "male"] [2007, 2007, … 2009]
nested_df = df.group_by("species").agg([pl.struct(pl.all()).alias("nested_data")])
nested_df
species nested_data
str list[struct[8]]
"Adelie" [{"Adelie","Torgersen",39.1,18.7,181.0,3750.0,"male",2007}, {"Adelie","Torgersen",39.5,17.4,186.0,3800.0,"female",2007}, … {"Adelie","Dream",41.5,18.5,201.0,4000.0,"male",2009}]
"Chinstrap" [{"Chinstrap","Dream",46.5,17.9,192.0,3500.0,"female",2007}, {"Chinstrap","Dream",50.0,19.5,196.0,3900.0,"male",2007}, … {"Chinstrap","Dream",50.2,18.7,198.0,3775.0,"female",2009}]
"Gentoo" [{"Gentoo","Biscoe",46.1,13.2,211.0,4500.0,"female",2007}, {"Gentoo","Biscoe",50.0,16.3,230.0,5700.0,"male",2007}, … {"Gentoo","Biscoe",49.9,16.1,213.0,5400.0,"male",2009}]

nestされたデータフレームを元に戻すには、explodeでリストを解消した後、unnestでstruct型を紐解いてあげればOKです。

nested_df.select(pl.col("nested_data")).explode("nested_data").unnest("nested_data")
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
str str f64 f64 f64 f64 str i64
"Adelie" "Torgersen" 39.1 18.7 181.0 3750.0 "male" 2007
"Adelie" "Torgersen" 39.5 17.4 186.0 3800.0 "female" 2007
"Adelie" "Torgersen" 40.3 18.0 195.0 3250.0 "female" 2007
"Adelie" "Torgersen" null null null null null 2007
"Adelie" "Torgersen" 36.7 19.3 193.0 3450.0 "female" 2007
"Gentoo" "Biscoe" null null null null null 2009
"Gentoo" "Biscoe" 46.8 14.3 215.0 4850.0 "female" 2009
"Gentoo" "Biscoe" 50.4 15.7 222.0 5750.0 "male" 2009
"Gentoo" "Biscoe" 45.2 14.8 212.0 5200.0 "female" 2009
"Gentoo" "Biscoe" 49.9 16.1 213.0 5400.0 "male" 2009

Palmer Penguinsのインストールに思ったより手間がかかったので、今日はここまで。実際の分析作業は次回の記事でやってみます。

余談

Pythonってめんどくさいですね。。。Rならこんな感じで一瞬なのに。。。

library(tidyverse)
── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
✔ dplyr     1.1.4     ✔ readr     2.1.5
✔ forcats   1.0.0     ✔ stringr   1.5.1
✔ ggplot2   3.5.1     ✔ tibble    3.2.1
✔ lubridate 1.9.3     ✔ tidyr     1.3.1
✔ purrr     1.0.2     
── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
✖ dplyr::filter() masks stats::filter()
✖ dplyr::lag()    masks stats::lag()
ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(palmerpenguins)

dat_nested  <- 
    penguins |> 
    group_nest(species)

dat_nested
# A tibble: 3 × 2
  species                 data
  <fct>     <list<tibble[,7]>>
1 Adelie             [152 × 7]
2 Chinstrap           [68 × 7]
3 Gentoo             [124 × 7]
dat_nested |> 
    unnest(data)
# A tibble: 344 × 8
   species island    bill_length_mm bill_depth_mm flipper_length_mm body_mass_g
   <fct>   <fct>              <dbl>         <dbl>             <int>       <int>
 1 Adelie  Torgersen           39.1          18.7               181        3750
 2 Adelie  Torgersen           39.5          17.4               186        3800
 3 Adelie  Torgersen           40.3          18                 195        3250
 4 Adelie  Torgersen           NA            NA                  NA          NA
 5 Adelie  Torgersen           36.7          19.3               193        3450
 6 Adelie  Torgersen           39.3          20.6               190        3650
 7 Adelie  Torgersen           38.9          17.8               181        3625
 8 Adelie  Torgersen           39.2          19.6               195        4675
 9 Adelie  Torgersen           34.1          18.1               193        3475
10 Adelie  Torgersen           42            20.2               190        4250
# ℹ 334 more rows
# ℹ 2 more variables: sex <fct>, year <int>