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アクセス)
ちなみに、弊社は偉い人たちの意向でFortranとC#が使われています。この業界ではだいぶ珍しいようです。僕としては業界標準に近づけたいので、新しいシステム構築やレポート作成時には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>