Polars に関する Tips#

Polars はデータフレーム操作を効率よく行える強力なライブラリですが、さらに使いこなすにはいくつかのコツを知っておくと役に立ちます。この章では、Polars をよりスムーズに、効率よく使うための実践的な Tips を紹介します。

import panel as pn
pn.extension()
import polars as pl
from helper.jupyter import row

条件分岐#

Polars で条件分岐を書くときは、pl.when().then().when().then().otherwise() のようなメソッドチェーンを使います。ただし、この書き方は煩雑で、読みにくくなりがちです。本書では、これを簡潔に書くために match() を使います。

次のコードでは、rank_expr1 は pl.when() のチェーンでランク付けの条件を書き、rank_expr2 は match() で同じ処理を簡潔に書いています。match() の引数は次のとおりです。

  • 奇数番目の引数 — 条件式。

  • 偶数番目の引数 — 条件に一致したときの結果式。

  • 引数が奇数の場合、最後の引数は otherwise() に渡す既定値として扱われます。

from helper.polars import match

df = pl.DataFrame({
    "name": ["Alice", "Bob", "Charlie", "Diana"],
    "score": [95, 85, 70, 50],
})

rank_expr1 = (
    pl.when(pl.col("score").is_between(90, 100)).then(pl.lit("A"))
    .when(pl.col("score").is_between(80, 89)).then(pl.lit("B"))
    .when(pl.col("score").is_between(70, 79)).then(pl.lit("C"))
    .otherwise(pl.lit("F"))                                    
)

rank_expr2 = match(
    pl.col("score").is_between(90, 100), pl.lit("A"),
    pl.col("score").is_between(80, 89), pl.lit("B"),
    pl.col("score").is_between(70, 79), pl.lit("C"),
    pl.lit("F")
)

df2 = df.with_columns(
    rank_expr1.alias('rank1'),
    rank_expr2.alias("rank2")
)
df2
shape: (4, 4)
namescorerank1rank2
stri64strstr
"Alice"95"A""A"
"Bob"85"B""B"
"Charlie"70"C""C"
"Diana"50"F""F"

このように、match() を使うと読みやすくなり、コードの重複も減らせます。

メソッドチェーン#

Polars ではメソッドチェーンでデータ操作を効率よく書けますが、途中でデータがどう変わるかを把握するのは難しいことがあります。デバッグには次の2つの方法が使えます。

  • helper.polars.DataCapturer — メソッドチェーンの任意の位置でデータを捕まえ、状態を確認できます。

  • helper.polars.PipeLogger — メソッドチェーン全体の入出力と引数を記録し、可視化します。

DataCapturer#

DataCapturer を使うと、メソッドチェーン内の特定の位置でデータの状態を捕まえられます。次は DataCapturer の使い方の例です。

from helper.polars import DataCapturer

df = pl.DataFrame({
    "name": ["Alice", "Bob", "Charlie", "David", "Eve"],
    "age": [25, 30, 35, 40, 45],
    "score": [85, 90, 95, 80, 70],
    "department": ["HR", "IT", "HR", "IT", "HR"]
})

cap = DataCapturer() #❶

result = (
df
.filter(pl.col("age") > 30)  
.with_columns(
  (pl.col("score") * 1.1).alias("adjusted_score")
)
.pipe(cap.before_group) #❷
.group_by("department")
.agg([
  pl.col("adjusted_score").mean().alias("average_score"),
  pl.col("age").max().alias("max_age")
])
.pipe(cap.before_sort) #❷
.sort("average_score", descending=True)
)

row(cap.before_group, cap.before_sort) #❸
shape: (3, 5)
nameagescoredepartmentadjusted_score
stri64i64strf64
"Charlie"3595"HR"104.5
"David"4080"IT"88.0
"Eve"4570"HR"77.0
shape: (2, 3)
departmentaverage_scoremax_age
strf64i64
"HR"90.7545
"IT"88.040

このコードでは、cap.before_group でグループ化の前の、cap.before_sort でソートの前のデータを捕まえています。

❶ メソッドチェーンを始める前に、DataCapturer() のインスタンス cap を作ります。
❷ メソッドチェーンの途中に .pipe(cap.name) を挟むと、その時点のデータが name として保存されます。
❸ 保存したデータには cap.name でアクセスできます。

PipeLogger#

PipeLogger を使うと、メソッドチェーンの各ステップでの入出力を目で確認できます。データがどう変わるかを見ながら処理を進められるので、デバッグが楽になります。

次のコードでは、PipeLogger でデータフレームを操作し、処理中の入出力を JupyterLab で可視化します。

from helper.polars import PipeLogger

result = (
PipeLogger(df) #❶
.filter(pl.col("age") > 30)
.with_columns(
  (pl.col("score") * 1.1).alias("adjusted_score")
)
.group_by("department")
.agg([
  pl.col("adjusted_score").mean().alias("average_score"),
  pl.col("age").max().alias("max_age")
])
.sort("average_score", descending=True)
)

result #❷

PipeLogger を使うと、メソッドチェーン内のデータの変化を目で追えるので、デバッグがしやすくなります。

❶ 最初の df を PipeLogger() で包みます。すると、結果は DataFrame ではなく、すべての入出力を記録する PipeLogger オブジェクトになります。PipeLogger は各ステップのデータを保存します。

❷ 最後に PipeLogger オブジェクトをセルの末尾に置くと、対話的なウィジェットとして結果が表示され、各メソッドの入出力を確認できます。

ExprCapturer#

式の途中で流れているデータを見たいときは Expr.inspect() を挟みます。値はその場で出力され、式の結果はそのまま後ろに流れます。

# 途中のデータを覗きたいときは inspect() を挟む(1 列ずつ使う)
df.select(
    pl.col("name").str.slice(0, 1).str.to_lowercase().value_counts().inspect().implode()
)
shape: (5,)
Series: 'name' [struct[2]]
[
	{"e",1}
	{"d",1}
	{"a",1}
	{"c",1}
	{"b",1}
]
shape: (1, 1)
name
list[struct[2]]
[{"e",1}, {"d",1}, … {"b",1}]

式のチェーンを流れるデータを保存して、後から調べたいときは、本書の ExprCapturer が便利です。次のコードでは ExprCapturer のインスタンス cap を作り、.pipe(cap.counts) でデータを counts という名前で保存しています。

from helper.polars import ExprCapturer

cap = ExprCapturer()
df_res = df.select(
    pl.col('name', 'department').str.slice(0, 1).str.to_lowercase().value_counts().pipe(cap.counts).implode()
)
df_res
shape: (1, 2)
namedepartment
list[struct[2]]list[struct[2]]
[{"d",1}, {"e",1}, … {"a",1}][{"h",3}, {"i",2}]

複数の列を計算しているため、保存されたデータは複数の Series のリストになります。たとえば、次のようにして中身を確認できます。

row(cap.counts[0], cap.counts[1])
shape: (2,)
name
struct[2]
{"",1}
{"",1}
shape: (2,)
department
struct[2]
{"",1}
{"",1}

複雑な演算式#

同じ中間結果を何度も使うなら、メソッドチェーンの利点は薄れます。そのような場合は、中間結果を変数に保存しておくのが一般的です。たとえば3次元ベクトルを正規化するとき、ベクトルの長さを何度も使います。一見すると length の式が何度も実行されるように見えますが、Polars は演算結果をキャッシュするため、同じ計算を2回実行することはありません。

df = pl.DataFrame(
    dict(x=[1.0, 2.0, 3.0], y=[3.0, 2.0, 1.0], z=[2.0, 1.0, 3.0])
)

x, y, z = pl.col('x'), pl.col('y'), pl.col('z')
length = (x**2 + y**2 + z**2).sqrt()

df1 = df.select(
    x = x / length,
    y = y / length,
    z = z / length
)

row(df, df1)
shape: (3, 3)
xyz
f64f64f64
1.03.02.0
2.02.01.0
3.01.03.0
shape: (3, 3)
xyz
f64f64f64
0.2672610.8017840.534522
0.6666670.6666670.333333
0.6882470.2294160.688247

次のコードでは LazyFrame.explain() で実行プランを表示します。ベクトルの長さを計算する式が、__POLARS_CSER_0x30925ccb05afdfe7 という名前の列にキャッシュされていることが分かります。

print(
    df
    .lazy()
    .with_columns(
        x = x / length,
        y = y / length,
        z = z / length
    )
    .explain()
)
simple π 3/3 ["x", "y", "z"]
   WITH_COLUMNS:
   [(col("x") / col("__POLARS_CSER_0x10000000a")), (col("y") / col("__POLARS_CSER_0x10000000a")), (col("z") / col("__POLARS_CSER_0x10000000a"))] 
     WITH_COLUMNS:
     [(((col("x").pow([dyn int: 2]) + col("y").pow([dyn int: 2])) + col("z").pow([dyn int: 2]))).sqrt().alias("__POLARS_CSER_0x10000000a")] 
      DF ["x", "y", "z"]; PROJECT */3 COLUMNS

式だけで複雑な計算を書くのは手間がかかります。たとえば平方根は sqrt(x) とは書けず、x.sqrt() と書く必要があります。そこで本書では、より数学に近い記法で式を書けるようにするため、polars_exprs デコレータを用意しています。このデコレータを使うと、通常の関数を Polars の式に変換できます。関数の中では式の利点をそのまま使え、.list.first() のようなサブネームスペースの操作も list_first() のような関数として呼べます。また、c_ で始まる変数は列を表す式として解釈され、c_x は pl.col('x') と同じ意味になります。

from helper.polars import polars_exprs

@polars_exprs
def norm():
    length = sqrt(c_x**2 + c_y**2 + c_z**2)
    return dict(x=x / length, y=y / length, z=z / length)

df2 = df.select(**norm())
row(df, df2)
shape: (3, 3)
xyz
f64f64f64
1.03.02.0
2.02.01.0
3.01.03.0
shape: (3, 3)
xyz
f64f64f64
0.2672610.8017840.534522
0.6666670.6666670.333333
0.6882470.2294160.688247

c_ で始まる変数名を使わない場合は、キーワード引数で列名を指定できます。たとえば次のコードのようになります。

@polars_exprs
def norm2():
    length = sqrt(x**2 + y**2 + z**2)
    return dict(x=x / length, y=y / length, z=z / length)

df = pl.DataFrame(
    dict(a=[1.0, 2.0, 3.0], b=[3.0, 2.0, 1.0], c=[2.0, 1.0, 3.0])
)
df1 = df.select(**norm2(x='a', y='b', z='c'))

row(df, df1)
shape: (3, 3)
abc
f64f64f64
1.03.02.0
2.02.01.0
3.01.03.0
shape: (3, 3)
xyz
f64f64f64
0.2672610.8017840.534522
0.6666670.6666670.333333
0.6882470.2294160.688247

polars_exprs でデコレートした関数を実行すると、シンボルは次の順に解釈されます。

  1. キーワード引数 — キーワード引数はそのまま渡されます。値が文字列なら、自動的に pl.col() で列を表す式に置き換えられます。

  2. c_ で始まるシンボル — pl.col() に変換されます。たとえば c_name は pl.col('name') になります。

  3. Polars の式関連の関数やメソッド

  4. 関数定義時のグローバル変数

  5. Python の組み込み関数

次の例では、各シンボルは次のように解釈されます。

  • A と p — キーワード引数で指定した列。pl.col('Amp') と pl.col('p') に対応します。

  • c_f — pl.col('f') に変換されます。

  • math — グローバル変数として扱われます。

  • sin() — Polars の式メソッド sin() が使われます。

  • print() — Python の組み込み関数 print() が使われます。

import math

@polars_exprs
def my_expr():
    phase = 2 * math.pi * c_f + p
    print(phase)
    value = A * sin(phase)
    return value

df = pl.DataFrame(dict(
    Amp=[1, 2, 3],
    f=[10, 20, 30],
    p=[0.1, 0.2, 0.3]
))

df.select(my_expr(A="Amp", p="p"))
[([(dyn float: 6.283185307179586) * (col("f"))]) + (col("p"))]
shape: (3, 1)
Amp
f64
0.099833
0.397339
0.886561

polars_exprs デコレータの主な利点は次のとおりです。

  1. より直感的な記法で書ける

    • 通常の Python 関数のように書けるので、コードが簡潔で分かりやすくなります。

    • たとえば sqrt(x) のような数学の記法で書けます(通常の Polars では x.sqrt() と書く必要があります)。

  2. 列参照が簡単になる

    • c_ で始まる変数(例: c_x)を使えば、pl.col('x') と書かずに列を参照できます。

    • キーワード引数でも列名を指定できます。たとえば x='column1' のように書けます。

  3. Polars の式の利点をそのまま使える

    • 関数内の演算は Polars の遅延評価や高速なエンジンの恩恵を受けるため、大量データでも効率的です。

    • 式の関数やメソッド(例: sin()、sqrt())を直接使えるので、記述が直感的になります。

  4. サブネームスペースの関数を簡単に使える

    • Polars のサブネームスペース(例: .list.first())に対応する関数(例: list_first())が使えるので、柔軟に書けます。

  5. コードを再利用しやすい

    • 式を Python 関数としてまとめられるので、複雑な計算を繰り返し使うときに再利用しやすくなります。

    • 同じ計算を別の列に適用する場合も、キーワード引数で簡単に対応できます。

  6. Polars の機能と Python の標準機能を組み合わせられる

    • Polars の式に加えて、Python の組み込み関数(例: print())やグローバル変数(例: math.pi)も使えるので、柔軟に処理できます。

  7. 読みやすく保守しやすいコードになる

    • 数学の記法や柔軟な列指定でコードが読みやすくなり、保守しやすくなります。

    • チームで共有したり、後から修正したりするのも容易です。

配列を処理するユーザー関数#

a が b より小さい位置の値を、指定した値に置き換える計算を考えます。式だけで書こうとすると回りくどい一方、NumPy なら 1行で済みます。

次のコードでは、map_batches() 経由で 2つの列を NumPy に渡して処理します。代償としてデータが Python に 1往復するので、「演算式では書きにくいが、データ量は許容できる」場面に向きます。

import numpy as np

df = pl.DataFrame({
    "a": [1, 2, 3, 10],
    "b": [1, 3, 4, 5],
})


def replace_where_smaller(args, values):
    a, b = [s.to_numpy() for s in args]
    c = np.copy(a)
    c[a < b] = values
    return pl.Series(c)


df.with_columns(
    pl_result=pl.map_batches(
        ["a", "b"],
        lambda args: replace_where_smaller(args, [100, 200]),
        return_dtype=pl.Int64,
    )
)
shape: (4, 3)
abpl_result
i64i64i64
111
23100
34200
10510

map_batches() には扱いにくい点が3つあります。

  1. 複数の列を 1つのリストにまとめてユーザー関数へ渡す

  2. ユーザー関数に列以外の引数を渡せない

  3. 入力と出力が Series でないと正しく動かない

Polars 2.0 では、さらに return_dtype を明示する必要があります。出力の型を推定できないためです。

これらの問題をまとめて扱うために、本書では series_batch、pyarrow_batch、numpy_batch の 3つのデコレータを用意しています。デコレートした関数は func(values=values) の形で引数を束ねられ、map_batches() には lambda args: f(args) の形で渡します。

from helper.polars import pyarrow_batch, numpy_batch, series_batch


@series_batch
def se_func(a, b, values):
    return a.scatter((a < b).arg_true(), values)


@pyarrow_batch
def pa_func(a, b, values):
    from pyarrow.compute import less, replace_with_mask

    return replace_with_mask(a, less(a, b), values)


@numpy_batch
def np_func(a, b, values):
    c = a.copy()
    c[a < b] = values
    return c


values = [100, 200]
dtype = pl.Int64  # 2.0 では出力の型を明示する

# 引数を束ねたうえで、map_batches には関数として渡す
se = se_func(values=values)
pa = pa_func(values=values)
npf = np_func(values=values)

df.with_columns(
    se_result=pl.map_batches(["a", "b"], lambda args: se(args), return_dtype=dtype),
    pa_result=pl.map_batches(["a", "b"], lambda args: pa(args), return_dtype=dtype),
    np_result=pl.map_batches(["a", "b"], lambda args: npf(args), return_dtype=dtype),
)
shape: (4, 5)
abse_resultpa_resultnp_result
i64i64i64i64i64
11111
23100100100
34200200200
105101010

演算式の置き換え#

次の calc_rotate_expr() は、3次元座標を回転させる式を作ります。x、y、z は座標、nx、ny、nz は回転軸のベクトル、angle は回転角度(度)です。

def calc_rotate_expr():
    x, y, z, nx, ny, nz, angle = [pl.col(name) for name in 'x y z nx ny nz angle'.split()]
    magnitude = (nx**2 + ny**2 + nz**2).sqrt()
    nx /= magnitude
    ny /= magnitude
    nz /= magnitude

    # Convert angle to radians
    angle_rad = angle.radians()

    # Calculate cosine and sine of the angle
    cos_theta = angle_rad.cos()
    sin_theta = angle_rad.sin()

    # Calculate the rotated coordinates using Rodrigues' rotation formula
    dot_product = nx * x + ny * y + nz * z
    cross_product_x = ny * z - nz * y
    cross_product_y = nz * x - nx * z
    cross_product_z = nx * y - ny * x

    rx = (cos_theta * x + 
          sin_theta * cross_product_x + 
          (1 - cos_theta) * dot_product * nx)

    ry = (cos_theta * y + 
          sin_theta * cross_product_y + 
          (1 - cos_theta) * dot_product * ny)

    rz = (cos_theta * z + 
          sin_theta * cross_product_z + 
          (1 - cos_theta) * dot_product * nz)

    return pl.struct(rx.alias('rx'), ry.alias('ry'), rz.alias('rz'))

この関数が作る式は、次のように使えます。

df = pl.DataFrame(dict(
    x=[1, 2, 3, 4],
    y=[2, 1, 3, 2],
    z=[1, 0, 0, 5],
    nx=[1, 1, 1, 1],
    ny=[0, 0, 0, 0],
    nz=[0, 0, 0, 0],
    angle=[10, 20, 30, 40],
))

rot_expr = calc_rotate_expr()
df_rot = df.select(rot_expr.struct.unnest())
row(df, df_rot)
shape: (4, 7)
xyznxnynzangle
i64i64i64i64i64i64i64
12110010
21010020
33010030
42510040
shape: (4, 3)
rxryrz
f64f64f64
1.01.7959671.332104
2.00.9396930.34202
3.02.5980761.5
4.0-1.6818495.115797

ただし、この式には次の問題があります。

  • 列名が式の中に固定されているため、列名の違うデータフレームをそのまま処理できません。

  • 一部の列を定数として扱いたいときに、処理が複雑になります。

そこで本書の expression_replace() を使うと、式の中の列名を置き換えたり、列を定数などの別の式に置き換えたりできます。

expression_replace(expr, mapper=None, **kw)

この関数は、式 expr の中の列名を mapper かキーワード引数で置き換えます。

次のコードでは、rot_expr の nx、ny、nz、angle を定数に置き換えてから、df2 で計算します。

from helper.polars import expression_replace
rot_expr2 = expression_replace(rot_expr, nx=pl.lit(1), ny=pl.lit(0), nz=pl.lit(0), angle=pl.lit(45))
df2 = df.select('x', 'y', 'z')
df2_rot = df2.select(rot_expr2.struct.unnest())
row(df2, df2_rot)
shape: (4, 3)
xyz
i64i64i64
121
210
330
425
shape: (4, 3)
rxryrz
f64f64f64
1.00.7071072.12132
2.00.7071070.707107
3.02.121322.12132
4.0-2.121324.949747

次のコードは、列名を大文字に置き換えます。

rot_expr3 = expression_replace(
    rot_expr, 
    x='X', y='Y', z='Z', 
    angle=pl.lit(30)
)

df3 = df.select(pl.col('x', 'y', 'z').name.to_uppercase(), pl.col('nx', 'ny', 'nz'))
df3_rot = df3.select(rot_expr3.struct.unnest())
row(df3, df3_rot)
shape: (4, 6)
XYZnxnynz
i64i64i64i64i64i64
121100
210100
330100
425100
shape: (4, 3)
rxryrz
f64f64f64
1.01.2320511.866025
2.00.8660250.5
3.02.5980761.5
4.0-0.7679495.330127

SymPy の式から Polars の演算式への変換#

本書の helper.polars.sympy_to_polars を使うと、SymPy の数式を Polars の式に自動で変換できます。たとえば次のコードは、SymPy の関数で3次元座標 \((x, y, z)\) を Z 軸、Y 軸、X 軸のまわりに \(\gamma\)、\(\beta\)、\(\alpha\) 度ずつ回転させるものです。得られる数式は複雑になります。

import sympy as sp
x, y, z, a, b, r = sp.symbols("x y z alpha beta gamma")
xr, yr, zr = sp.rot_axis1(sp.rad(a)) * sp.rot_axis1(sp.rad(b)) * sp.rot_axis2(sp.rad(r)) * sp.Matrix([x, y, z])
yr
\[\displaystyle x \left(\sin{\left(\frac{\pi \alpha}{180} \right)} \cos{\left(\frac{\pi \beta}{180} \right)} + \sin{\left(\frac{\pi \beta}{180} \right)} \cos{\left(\frac{\pi \alpha}{180} \right)}\right) \sin{\left(\frac{\pi \gamma}{180} \right)} + y \left(- \sin{\left(\frac{\pi \alpha}{180} \right)} \sin{\left(\frac{\pi \beta}{180} \right)} + \cos{\left(\frac{\pi \alpha}{180} \right)} \cos{\left(\frac{\pi \beta}{180} \right)}\right) + z \left(\sin{\left(\frac{\pi \alpha}{180} \right)} \cos{\left(\frac{\pi \beta}{180} \right)} + \sin{\left(\frac{\pi \beta}{180} \right)} \cos{\left(\frac{\pi \alpha}{180} \right)}\right) \cos{\left(\frac{\pi \gamma}{180} \right)}\]

次に、sympy_to_polars() を使用して、数式 \(x_r\)、\(y_r\)、\(z_r\) を Polars の演算式に変換します。列名をカスタマイズしたい場合は、symbol_map 引数に SymPy のシンボルと対応する列名の辞書を渡します。

from helper.polars import sympy_to_polars
xr_expr, yr_expr, zr_expr = [sympy_to_polars(expr) for expr in (xr, yr, zr)]
print(yr_expr)
[([([(col("y")) * ([([([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("alpha"))].cos()) * ([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("beta"))].cos())]) + ([([(dyn float: -1) * ([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("alpha"))].sin())]) * ([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("beta"))].sin())])])]) + ([([(col("x")) * ([([([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("alpha"))].cos()) * ([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("beta"))].sin())]) + ([([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("beta"))].cos()) * ([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("alpha"))].sin())])])]) * ([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("gamma"))].sin())])]) + ([([(col("z")) * ([([([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("alpha"))].cos()) * ([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("beta"))].sin())]) + ([([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("beta"))].cos()) * ([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("alpha"))].sin())])])]) * ([([(dyn float: 0.005555555555555556) * (dyn float: 3.141592653589793)]) * (col("gamma"))].cos())])]

次のコードは、自動生成された演算式と SymPy の数式の結果を比較します。

df = pl.DataFrame(dict(
    x=[1, 2, 3, 4],
    y=[2, 1, 3, 2],
    z=[1, 0, 0, 5],
    alpha=[30, 40, 50, 60],
    beta=[10, 20, -30, -60],
    gamma=[5, -5, 5, -5],
)).select(pl.all().cast(pl.Float64))

df_pl = df.select(xr=xr_expr, yr=yr_expr, zr=zr_expr)
df_sp = pl.DataFrame([
    {
        'xr':xr.evalf(subs=row),
        'yr':yr.evalf(subs=row),
        'zr':zr.evalf(subs=row)
    }
    for row in df.rows(named=True)
])

row(df_sp, df_pl)
shape: (4, 3)
xryrzr
f64f64f64
0.9090392.228453-0.455681
1.9923890.349042-0.953181
2.9885842.908505-0.780362
4.4205582.04.632351
shape: (4, 3)
xryrzr
f64f64f64
0.9090392.228453-0.455681
1.9923890.349042-0.953181
2.9885842.908505-0.780362
4.4205582.04.632351

Polars は複数の式を計算するとき、自動的に最適化するので、同じ計算が重複して実行されることはありません。たとえば次のコードは、最適化後の式を出力します。中間結果の一部は計算されて一時的な列に保存され、その後の計算で使い回されます。この最適化は CSE(Common Subexpression Elimination、共通部分式の除去)と呼ばれます。

print(df.lazy().select(xr=xr_expr, yr=yr_expr, zr=zr_expr).explain())
SELECT [col("x").fma([col("__POLARS_CSER_0x100000009").alias("literal"), (-1.0 * col("z")) * col("__POLARS_CSER_0x100000004")]).alias("xr"), (col("z") * col("__POLARS_CSER_0x100000015")).fma([col("__POLARS_CSER_0x100000009").alias("literal"), col("y").fma([col("__POLARS_CSER_0x10000001a").alias("literal"), (col("x") * col("__POLARS_CSER_0x100000015")) * col("__POLARS_CSER_0x100000004")])]).alias("yr"), (col("z") * col("__POLARS_CSER_0x10000001a")).fma([col("__POLARS_CSER_0x100000009").alias("literal"), col("y").fma([(-1.0 * col("__POLARS_CSER_0x100000014")).fma([col("__POLARS_CSER_0x100000013").alias("literal"), (-1.0 * col("__POLARS_CSER_0x100000011")) * col("__POLARS_CSER_0x10000000e")]), (col("x") * col("__POLARS_CSER_0x10000001a")) * col("__POLARS_CSER_0x100000004")])]).alias("zr")]
   WITH_COLUMNS:
   [((0.017453 * col("gamma"))).sin().alias("__POLARS_CSER_0x100000004"), ((0.017453 * col("gamma"))).cos().alias("__POLARS_CSER_0x100000009"), ((0.017453 * col("alpha"))).cos().fma([((0.017453 * col("beta"))).sin(), (((0.017453 * col("beta"))).cos() * ((0.017453 * col("alpha"))).sin())]).alias("__POLARS_CSER_0x100000015"), ((0.017453 * col("alpha"))).cos().fma([((0.017453 * col("beta"))).cos(), (-1.0 * ((0.017453 * col("alpha"))).sin()) * ((0.017453 * col("beta"))).sin()]).alias("__POLARS_CSER_0x10000001a"), ((0.017453 * col("alpha"))).sin().alias("__POLARS_CSER_0x10000000e"), ((0.017453 * col("beta"))).cos().alias("__POLARS_CSER_0x100000011"), ((0.017453 * col("beta"))).sin().alias("__POLARS_CSER_0x100000013"), ((0.017453 * col("alpha"))).cos().alias("__POLARS_CSER_0x100000014")] 
    DF ["x", "y", "z", "alpha", ...]; PROJECT */6 COLUMNS