演算式#

演算式(またはエクスプレッション)は、データフレームの操作や変換を行うための重要な要素です。Polarsの演算式は、データフレーム内の各列に対して適用される計算や操作を定義します。

import polars as pl
import numpy as np
from polars import selectors as cs
from helper.jupyter import row

演算式を使用するメソッド#

演算式はさまざまな場面で使用されています。本節では、特によく使われる以下の三つのメソッドについて簡単に説明します。演算式の詳細については後の節で解説します。

  • select, select_seq: 必要な列を選択したり、計算結果のみを取得します。

  • with_columns, with_columns_seq: 新しい列を追加したり、既存の列を変換します。

  • filter: 条件を満たす行を抽出します。

これらのメソッドはすべて、元のデータフレームを変更せず、新しいデータフレームを返します。

select#

DataFrame.select() は、データフレームから特定の列を選択したり、新しい列を計算した結果を取得するためのメソッドです。select() の引数として渡した各演算式が、結果のデータフレーム内の列として表現されます。演算式はリスト、位置引数、またはキーワード引数で指定できます。

select()select_seq() の両方とも、元のデータフレームのデータを利用して演算式を計算します。ただし、select() は並列で演算式を計算するのに対し、select_seq() は演算式を順番に計算します。

以下の例では、select() および select_seq() の使用例を示します。

df = pl.DataFrame({
    "a": [1, 2, 3],
    "b": [4, 5, 6],
    "c": [7, 8, 9]
})

df1 = df.select(["a", pl.col("c") * 2])
df2 = df.select("a", pl.col("c") * 2)
df3 = df.select_seq(
    a = pl.col("a") * 2,         # "a" を2倍
    a_plus_c = pl.col("a") + pl.col("c") #❶
) 
row(df, df1, df2, df3)
shape: (3, 3)
abc
i64i64i64
147
258
369
shape: (3, 2)
ac
i64i64
114
216
318
shape: (3, 2)
ac
i64i64
114
216
318
shape: (3, 2)
aa_plus_c
i64i64
28
410
612

❶ 上記の例では、select_seq()a_plus_c列の計算に使用する pl.col("a")は、元データのa列を参照しています。この計算は、a 列を2倍した結果ではなく、元のデータを基に行われます。

with_columns#

DataFrame.with_columns() は、DataFrame.select() と似た使い方をしますが、計算によって生成された新しい列を元の列と一緒に返します。これにより、既存のデータフレームに新しい列を追加するような操作が可能です。

以下の例では、with_columns() を使った列の追加方法を示しています。df1 に新しい列 a_plus_bconst が追加され、df2 では既存の a 列が更新され、新しい列 a_plus_c が追加されています。

df1 = df.with_columns(
    a_plus_b = pl.col("a") + pl.col("b"),  # "a" + "b" の列を追加
    const = 10  # 定数列を追加
)

df2 = df.with_columns(
    a = pl.col("a") * 2,
    a_plus_c = pl.col("a") + pl.col("c")
)

row(df1, df2)
shape: (3, 5)
abca_plus_bconst
i64i64i64i64i32
147510
258710
369910
shape: (3, 4)
abca_plus_c
i64i64i64i64
2478
45810
66912

filter#

DataFrame.filter() は、指定した演算式がブール値を返す条件に基づいて、True に合致する行をフィルタリングして返します。以下は、filter() を使用した例です。

df1 = df.filter(pl.col("a") > 1)  # "a" > 1 の行を選択
df2 = df.filter((pl.col("a") > 1) & (pl.col("b") < 6)) # 条件のAND
df3 = df.filter((pl.col("a") >= 3) |  (pl.col("b") <= 4)) # 条件のOR
df4 = df.filter(pl.col("a") > 1, pl.col("b") < 6) # 複数の式の場合は &で結合と同じ
row(df, df1, df2, df3, df4)
shape: (3, 3)
abc
i64i64i64
147
258
369
shape: (2, 3)
abc
i64i64i64
258
369
shape: (1, 3)
abc
i64i64i64
258
shape: (2, 3)
abc
i64i64i64
147
369
shape: (1, 3)
abc
i64i64i64
258

列の選択#

ほとんどの演算式は列の選択文から始まります。データフレーム内の列を選択するための方法はいくつかあり、それぞれ異なる用途や状況に適しています。以下に、それぞれの方法について詳しく説明します。

  • pl.col(): 列名やデータ型で列を選択します。

  • pl.nth(): N番目の列を選択します。

  • pl.selectors: 複雑なロジックで列を選択します。

pl.col()#

pl.col() は、特定の列名や列のデータ型に基づいて列を選択するために使用されます。

  • 列名での選択: 列名を指定してその列を選択します。列名は文字列として指定します。

df = pl.DataFrame({
    "a": [1], "b": [4.0], "c": [7], "x1":[10.0], "y1":[20.0], "x2":[4], "y2":[10], "xyz":[10.0]})
selected_df = df.select(pl.col("a"))
print(selected_df.columns)
['a']
  • 複数の列名での選択: リストで複数の列名を指定することも可能です。

selected_df = df.select(pl.col(["a", "b"]))
print(selected_df.columns)
['a', 'b']
  • データ型での選択: データ型を指定して、そのデータ型を持つ全ての列を選択することもできます。

selected_df = df.select(pl.col(pl.Int64))
print(selected_df.columns)
['a', 'c', 'x2', 'y2']

pl.nth()#

pl.nth() は、データフレーム内の N 番目の列を選択するために使用されます。インデックスは 0 から始まります。

  • N番目の列の選択: インデックスを指定して、そのインデックスに対応する列を選択します。

selected_df = df.select(pl.nth(0))
print(selected_df.columns)
['a']
  • 複数のインデックスでの選択: 複数のインデックスをリストで指定して、それらに対応する列を選択することも可能です。

selected_df = df.select(pl.nth([0, 2]))
print(selected_df.columns)
['a', 'c']

pl.all()#

pl.all() 関数はデータフレーム内のすべての列を選択するために使用されます。これは、データフレーム全体を操作したい場合や他の選択条件と組み合わせて使用するのに便利です。

selected_df = df.select(pl.all())
print(selected_df.columns)
['a', 'b', 'c', 'x1', 'y1', 'x2', 'y2', 'xyz']

pl.exclude()#

pl.exclude()関数は指定した列を除外するために使用されます。これにより、特定の列を除いたデータフレームを簡単に作成できます。

  • 特定の列を除外:

selected_df = df.select(pl.exclude("b"))
print(selected_df.columns)
['a', 'c', 'x1', 'y1', 'x2', 'y2', 'xyz']
  • 複数の列を除外:

selected_df = df.select(pl.exclude(["a", "c"]))
print(selected_df.columns)
['b', 'x1', 'y1', 'x2', 'y2', 'xyz']

selectors#

polars.selectors (imported as cs) モジュールは、Polarsでデータフレームの列を選択するための高度な機能を提供します。これを使用すると、特定の条件に基づいて列を簡単に選択することができます。以下に、主要な関数とその使用方法について説明します。

データ型で列を選択#

cs.by_dtype()で特定のデータ型に基づいて列を選択します。

selected_df = df.select(cs.by_dtype(pl.Int64))
print(selected_df.columns)
['a', 'c', 'x2', 'y2']

cs.integer() は、データフレーム内のすべての整数型の列を選択するために使用されます。Polars では、整数型には Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64 などが含まれます。

cs.float() は、データフレーム内のすべての浮動小数点型の列を選択するために使用されます。Polars では、浮動小数点型には Float32Float64 があります。

selected_df = df.select(cs.integer())
print(selected_df.columns)
['a', 'c', 'x2', 'y2']
selected_df = df.select(cs.float())
print(selected_df.columns)
['b', 'x1', 'y1', 'xyz']

cs.by_name()#

正規表現で特定の名前パターンに基づいて列を選択します。

selected_df = df.select(cs.by_name("^x|y\d$"))
print(selected_df.columns)
['x1', 'y1', 'x2', 'y2', 'xyz']

cs.starts_with()cs.ends_with()#

特定の接頭辞と接尾辞を持つ列を選択します。

selected_df = df.select(cs.starts_with("x"))
print(selected_df.columns)
['x1', 'x2', 'xyz']
selected_df = df.select(cs.ends_with("1"))
print(selected_df.columns)
['x1', 'y1']

集合演算#

上で説明した関数を組み合わせて、以下のような集合演算を行うことができます。

  • 結合(|): 複数の列選択を結合して、選択範囲を広げます。例えば、特定の列とすべての列の組み合わせを選択することができます。

  • 差集合(-): 特定の列を除外するために使用します。例えば、すべての列から特定の列を除外します。

  • 交差(&): 複数の条件に一致する列を選択します。例えば、特定のパターンに一致し、特定のデータ型を持つ列を選択します。

  • 補集合(~): 特定の条件に一致しない列を選択します。例えば、特定のパターンに一致しない列を選択します。

次の選択文は、’x’で始まる列を選択し、その中から’2’で終わる列を除外しています。

selected_df = df.select(cs.starts_with('x') - cs.ends_with('2'))
print(selected_df.columns)
['x1', 'xyz']

次の選択文は、整数列或いは’1’で終わる列を選択します。

selected_df = df.select(cs.integer() | cs.ends_with('1'))
print(selected_df.columns)
['a', 'c', 'x1', 'y1', 'x2', 'y2']

列同士の演算式#

次の三つの演算式はそれぞれ、

  • e1: 1列と1列同士の演算です。具体的には、列 A と列 B の対応する要素を加算します。

  • e2: 2列と1列の間の演算です。実際に計算するときは、pl.col('A') + pl.col('B'), pl.col('B') + pl.col('B')を同時に計算します。

  • e3: 動的に生成された式で、数値型のすべての列と列 B を加算します。実際のデータによって計算される式の数は未定です。

e1 = pl.col('A') + pl.col('B')
e2 = pl.col('A', 'B') + pl.col('B')
e3 = (cs.numeric() + pl.col('B')).as_expr()

print(e1)
print(e2)
print(e3)
[(col("A")) + (col("B"))]
[(cols(["A", "B"])) + (col("B"))]
[(dtype_columns([Int32, Float64, UInt32, Float32, UInt16, Int64, Int8, Decimal(None, None), UInt64, Int16, UInt8])) + (col("B"))]

次のプログラムは、LazyDataFrame.explain()を使って、式の演算の中身を表示します。

df = pl.DataFrame(dict(A=[1, 2], B=[10, 20], C=[100, 200], X=[1, 1], Y=[2, 2]))
ldf = df.lazy()
query = ldf.with_columns(C=e1)
print(query.explain())
 WITH_COLUMNS:
 [[(col("A")) + (col("B"))].alias("C")] 
  DF ["A", "B", "C", "X"]; PROJECT */5 COLUMNS; SELECTION: None

複数列のセレクタがある場合、複数の式が作成されます。

query = ldf.select(e2)
print(query.explain())
 SELECT [[(col("A")) + (col("B"))], [(col("B")) + (col("B"))]] FROM
  DF ["A", "B", "C", "X"]; PROJECT 2/5 COLUMNS; SELECTION: None
query = ldf.select(e3)
print(query.explain())
 SELECT [[(col("A")) + (col("B"))], [(col("B")) + (col("B"))], [(col("C")) + (col("B"))], [(col("X")) + (col("B"))], [(col("Y")) + (col("B"))]] FROM
  DF ["A", "B", "C", "X"]; PROJECT 5/5 COLUMNS; SELECTION: None

式の中には複数列のセレクタは一つしか存在することができません。例えば、以下の文はA - BB - Aを計算しようとしますが、エラーが発生します。

%%capture_except
query = ldf.select(pl.col('A', 'B') - pl.col('B', 'A'))
print(query.explain())
ComputeError: expanding more than one `col` is not allowed

Resolved plan until failure:

	---> FAILED HERE RESOLVING 'select' <---
DF ["A", "B", "C", "X"]; PROJECT */5 COLUMNS; SELECTION: None

この問題について、GitHubに以下の提案があります。

現段階は、次のように、pl.struct()を使って複数の列を一つのstruct列に変更して処理すれば、複数列同士の計算をすることができます。

df.with_columns(
    (pl.struct('A', 'B') - pl.struct('B', 'A'))
       .struct.rename_fields(['AB', 'BA'])
       .struct.field('*')
)
shape: (2, 7)
ABCXYABBA
i64i64i64i64i64i64i64
11010012-99
22020012-1818

式の出力名#

DataFrameに新しい列を追加する場合や列の計算結果を取得する場合、通常その結果に対して名前を付けることができます。この名前を「式の出力名」と呼びます。式の出力名を指定することで、計算結果を明示的に識別し、後で参照したり、他の操作に使用したりすることができます。

ルート名と出力名#

式の出力名が指定されていない場合は、式で最初に使用された項の名を出力名とします。式で使用した列名のリストは、式のルート名として知られています。ルート名と出力名はそれぞれ.meta.root_names().meta.output_name()で取得することができます。

def show_names(expr):
    print(f'root_names={expr.meta.root_names()}, output_name={expr.meta.output_name()}')
expr1 = pl.col("A") * pl.col("B") + 1
show_names(expr1)
root_names=['A', 'B'], output_name=A
expr2 = 1 + pl.col("B") * pl.col("A")
show_names(expr2)
root_names=['B', 'A'], output_name=literal
expr3 = (pl.col("A") * pl.col("B") + 1).alias("Z")

output_nameliteralの場合は、.name.keep()root_namesの初めての名前に変更できます。

row(df.select(expr1), df.select(expr2), df.select(expr2.name.keep()), df.select(expr3))
shape: (2, 1)
A
i64
11
41
shape: (2, 1)
literal
i64
11
41
shape: (2, 1)
B
i64
11
41
shape: (2, 1)
Z
i64
11
41

複数の式の出力名が同じ場合、計算時にエラーが発生します。例えば、次の pl.all() は df 中の A と B 列に対して式を計算します。これにより、両方の式の出力名はが同じ”literal”になるため、エラーが発生しました。

%%capture_except
expr = 10 + pl.col('A', 'B')
df.select(expr)
DuplicateError: the name 'literal' is duplicate

It's possible that multiple expressions are returning the same default column name. If this is the case, try renaming the columns with `.alias("new_name")` to avoid duplicate column names.

式中の列の現れる順番を変更するか、或いは次のように.name.keep()で名前をroot_names[0]に変更します。

df.select(expr.name.keep())
shape: (2, 2)
AB
i64i64
1120
1230

或いは、.name.suffix()などを使って、各個列の名前をベースに、新しい列名を算出します。

df.select(expr.name.suffix("_calc"))
shape: (2, 2)
A_calcB_calc
i64i64
1120
1230

ルート名の順番を変更#

次のpl.when().then().otherwise()の式では、root_names中の列名の表す順番はthen(), otherwise(), when()の順になります。例えば、以下のような式では、expr.meta.root_names['x', 'y', 'j'] となります。

expr = pl.when(pl.col('j') > 50).then('x').otherwise('y')
print(expr.meta.root_names())
['x', 'y', 'j']

when()中に複数の列がある場合は、すべての式の出力名は’x’なので、実際の計算を行うと、名前重複のDuplicateErrorエラーが発生します。

%%capture_except
cols = pl.col(['j', 'k', 'l'])
expr = pl.when(cols > 50).then('x').otherwise('y')
df.select(expr)
ColumnNotFoundError: x

Resolved plan until failure:

	---> FAILED HERE RESOLVING 'select' <---
DF ["A", "B", "C", "X"]; PROJECT */5 COLUMNS; SELECTION: None

この場合、式の先頭に計算結果を影響しない、ルート名の順番だけを影響する式を追加すれば、解決できます。

df2 = pl.DataFrame(dict(
    j=np.random.randint(10, 99, 2),
    k=np.random.randint(10, 99, 2),
    l=np.random.randint(10, 99, 2),
    x=10,
    y=20,
))

df2.select(
    cols * 0 + pl.when(cols > 50).then('x').otherwise('y')
)
shape: (2, 3)
jkl
i32i32i32
202020
101010

出力名を指定する#

alias()#

alias() メソッドを使用して、計算結果に名前を付けることができます。

result = df.select((pl.col('A') + pl.col('B')).alias("A_plus_B"))
print(result.columns)
['A_plus_B']

キーワード引数#

select()with_columns()などのメソッドを使用して、新しい列を追加する際に、キーワード引数その列に名前を付けることができます。

result = df.select(A_plus_B=pl.col('A') + pl.col('B'))
print(result.columns)
['A_plus_B']

出力名を変換#

.nameの下には出力名を操作するためのメソッドがいくつか提供されています。以下はよく使う三つの関数を紹介します。

map

ルート名に関数を適用して式の出力名を変更します。

result_df = df.select(
    (pl.col('A', 'B', 'C') + pl.col('B')).name.map(lambda name: name * 3)
)
print(result_df.columns)
['AAA', 'BBB', 'CCC']

prefixsuffix

ルート列名の先頭と末尾に接頭辞を追加します。

result_df = df.select(
    (pl.col('A', 'B', 'C') + pl.col('B')).name.suffix("_res")
)
print(result_df.columns)
['A_res', 'B_res', 'C_res']

式の出力値#

式は独立で実行する#

Polars の式が独立して実行されることについて、以下のプログラムを使用して説明します。

df = pl.DataFrame(
    dict(x=range(10), y=range(10))
)

result = df.select(pl.all().sample(4))
result
shape: (4, 2)
xy
i64i64
46
87
93
02

このプログラムでは、pl.all() を使ってデータフレーム df のすべての列に対して sample(4) を適用しています。これにより、各列からランダムに4行が選択されます。Polarsの式は、DataFrameの各列に対して独立して適用されます。このため、pl.all().sample(4) を使った場合、各列から独立して4行がランダムに選ばれます。各列に対して同じ操作が適用されますが、操作は独立して実行されるため、異なる列で選ばれる行は必ずしも一致しません。

seed引数で、すべての列の乱数は同じシードを使います。独立で実行しても、同じ行のデータを取り出すことができます。

df.select(pl.all().sample(4, seed=10))
shape: (4, 2)
xy
i64i64
33
55
00
22

ちなみに、ランダム行を抽出するにはDataFrame.sample()を使うのは一番進めです。

df.sample(4)
shape: (4, 2)
xy
i64i64
66
11
55
44

.sort()は各個列の値を独立で並び替えます。

df = pl.DataFrame(
    dict(x=[1, 3, 2], y=[9, 8, 7])
)

複数の列を処理する式は各列を処理する式に変更されてから独立で実行されいます。下の例はx列とy列をそれぞれ並び替えます。

df.select(pl.col('x', 'y').sort())
shape: (3, 2)
xy
i64i64
17
28
39

ペアで並び替えするには、DataFrame.sort()を使います。

row(df.sort('x', 'y'), df.sort('y', 'x'))
shape: (3, 2)
xy
i64i64
19
27
38
shape: (3, 2)
xy
i64i64
27
38
19

式でペア並び替えの場合は、structを使います。

df.select(
    pl.struct('y', 'x').sort().struct.field('*')
)
shape: (3, 2)
yx
i64i64
72
83
91

或いはsort_by()でソートする列を指定します。各個列は独立で演算しますが、順番を決める列は同じなので、結果には行の関係は残ります。

df.select(
    pl.all().sort_by("y", "x")
)
shape: (3, 2)
xy
i64i64
27
38
19

式の出力データ数#

長さNの入力に対して、式の出力長さは以下の3種類になります:1、NおよびX。それぞれの例を使って説明します。

出力長さが1#

出力長さが1の場合、式はデータフレーム全体に対して集計操作を行い、単一の結果を返します。例えば、列の合計や平均などです。次のコードでは、x 列の合計が計算され、単一の値 45 が返されます。

df = pl.DataFrame({
    "x": range(5),
})

result = df.select(pl.col("x").sum())
result
shape: (1, 1)
x
i64
10

出力長さがN#

出力長さがNの場合、式は各行に対して独立に操作を行い、元のデータフレームと同じ長さの結果を返します。例えば、各列の要素に対するスカラー操作などです。次のコードでは、x 列の各要素に 1 が加えられ、元のデータフレームと同じ長さの結果が返されます。

result = df.select(pl.col("x") + 1)
result
shape: (5, 1)
x
i64
1
2
3
4
5

出力長さがX#

出力長さがXの場合、特定の操作により元の長さNと異なる長さの結果が返されます。例えば、サンプリングやフィルタリングなどです。次のコードでは、X列の3以上の値を出力します。

result = df.select(pl.col('x').filter(pl.col('x') >=3))
result
shape: (2, 1)
x
i64
3
4

異なる長さの出力がある場合の動作#

長さが1の列は、その値がリピートされ、他の列と同じ長さに拡張されます。これにより、DataFrameの整合性が保たれます。

df = pl.DataFrame(
    dict(x=[1, 3, 2], y=[9, 8, 7])
)

df.select(
    x_mean = pl.col('x').mean(),
    x_plus_y = pl.col('x') + pl.col('y')
)
shape: (3, 2)
x_meanx_plus_y
f64i64
2.010
2.011
2.09

次の二つ式の出力長さは1と2です。

df.select(
    x_mean = pl.col('x').mean(),
    x_top2 = pl.col('x').top_k(2)
)
shape: (2, 2)
x_meanx_top2
f64i64
2.02
2.03

出力の長さが異なる複数の列を含む操作を行うと、Polarsはエラーを発生させます。

%%capture_except
df.select(
    x_sq = pl.col('x')**2,
    x_top2 = pl.col('x').top_k(2)
)
ShapeError: Series length 2 doesn't match the DataFrame height of 3

同じ計算式でも、列の中身によって出力の長さは変わります。次の式ではx列に対しての出力長さは2、y列の出力長さは3になるので、エラーになります。

%%capture_except
df.select(
    pl.col('x', 'y').filter(pl.col('x', 'y') >= 2)
)
ShapeError: Series length 2 doesn't match the DataFrame height of 3

Expr.implode() を使って複数の値を一つのリストに変換すれば、式の出力長さはすべて1になり、エラーを防ぐことができます。

df.select(
    pl.col('x', 'y').filter(pl.col('x', 'y') >= 2).implode()
)
shape: (1, 2)
xy
list[i64]list[i64]
[3, 2][9, 8, 7]

式のコンテキスト#

式の実行環境(コンテキスト)によって、式は処理するデータが異なります。select(), with_columns()の場合、1列のすべてのデータを式に渡します。group_by().agg()の場合は、各個グループの列を式に渡します。

df = pl.DataFrame(dict(x=[1, 2, 1, 2, 3, 3], y=[10, 20, 30, 40, 50, 60]))

aggコンテキスト#

次の.agg()中の式はx列で分けられた三つのグループのデータに対して処理します。式の出力は複数の値がある場合、listで保存します。

df.group_by('x').agg(
    y = pl.col('y'),
    y_mult_x = pl.col('y') * pl.col('x'),
    sum_y_mult_x = (pl.col('y') * pl.col('x')).sum()
)
shape: (3, 4)
xyy_mult_xsum_y_mult_x
i64list[i64]list[i64]i64
3[50, 60][150, 180]330
2[20, 40][40, 80]120
1[10, 30][10, 30]40

listコンテキスト#

.list.eval() の式のコンテキストは、処理対象の各リストとなります。以下のプログラムでは、x列中の各リストの平均値を計算し、それを用いて各リストを調整します。

  • 式❶では、list.mean() を使って各リストの平均値を計算し、その平均値を各リストの要素から引き算します。

  • 式❷では、list.eval() を使って x列の各リストを処理し、式❸を適用します。

  • 式❸では、pl.element() が現在処理中のリストの各要素を表し、その要素からリスト全体の平均値を引き算します。

df = pl.DataFrame(dict(x=[[1, 2], [3, 4, 5, 6], [7, 8, 9]]))
df.with_columns(
    x1 = pl.col('x') - pl.col('x').list.mean(), #❶
    x2 = pl.col('x').list.eval(                 #❷
        pl.element() - pl.element().mean()      #❸
    )
)
shape: (3, 3)
xx1x2
list[i64]list[f64]list[f64]
[1, 2][-0.5, 0.5][-0.5, 0.5]
[3, 4, … 6][-1.5, -0.5, … 1.5][-1.5, -0.5, … 1.5]
[7, 8, 9][-1.0, 0.0, 1.0][-1.0, 0.0, 1.0]

次のコードは、各リストの要素の中から、そのリストの平均値より大きい値を抽出します。このような処理は、list.eval() を使用することで簡潔に実装できます。list.eval() を使わない場合、リスト全体に対する要素ごとの条件処理を実現するのが難しくなります。

  • 式❹では、pl.element() がリスト内の各要素を表します。

  • pl.element().mean() は現在処理中のリスト全体の平均値を計算します。

  • filter メソッドにより、各要素がリストの平均値より大きいかどうかを判定し、条件を満たす要素だけを抽出します。

df.with_columns(
    x_large = pl.col('x').list.eval(
        pl.element().filter(pl.element() > pl.element().mean()) #❹
    )    
)
shape: (3, 2)
xx_large
list[i64]list[i64]
[1, 2][2]
[3, 4, … 6][5, 6]
[7, 8, 9][9]

式❹のpl.element()を具体的なリストに置き換えることで、この計算の仕組みをより直感的に理解できます。たとえば、リスト[7, 8, 9]に対して、次のような計算を行います。

s = pl.Series([7, 8, 9])
s.filter(s > s.mean()).to_list()
[9]
  • この場合、s.mean() はリストの平均値を計算します (8.0)。

  • s > s.mean() は各要素が平均値より大きいかどうかを判定し、ブール値の配列 [False, False, True] を生成します。

  • filter メソッドを使うと、このブール条件を満たす要素(9)だけが抽出されます。

fieldコンテキスト#

.struct.with_fields()中の式は、struct列の各個fieldに対して計算を行います。

df = pl.DataFrame(
    {
        "coords": [{"x": 1, "y": 4}, {"x": 4, "y": 9}, {"x": 9, "y": 16}],
        "multiply": [10, 2, 3],
    }
)
df
shape: (3, 2)
coordsmultiply
struct[2]i64
{1,4}10
{4,9}2
{9,16}3

次の.struct.with_fields()の中の式では、pl.field(name)でstruct列のnameフィールドのデータを表します。

df.select(
    pl.col('coords').struct.with_fields(
        dist = (pl.field('x') ** 2 + pl.field('y') ** 2).sqrt() * pl.col('multiply')      
    ).struct.field('*')
)
shape: (3, 3)
xydist
i64i64f64
1441.231056
4919.697716
91655.072679

よく使われる演算式#

Polarsでは、グローバル関数や演算式メソッドを使って演算式を作成します。本節では、よく使われる演算式について説明します。

演算子#

+, -, *, /などの演算子には、対応する演算式のメソッドも用意されています。たとえば、次のコードでは、+ 演算子と add() メソッドを使用して x 列と y 列の和を求めています。また、>= 演算子と ge() メソッドを使用して列と数値の比較を行っています。

df = pl.DataFrame(dict(
    x=[1, 2, 3, 4],
    y=[40, 30, 20, 10]
))

df.with_columns(
    add1=pl.col('x') + pl.col('y'),
    add2=pl.col('x').add(pl.col('y')),
    cond1=pl.col('x') >= 3,
    cond2=pl.col('x').ge(3)
)
shape: (4, 6)
xyadd1add2cond1cond2
i64i64i64i64boolbool
1404141falsefalse
2303232falsefalse
3202323truetrue
4101414truetrue

&| に対しては、それぞれ and_or_ メソッドが対応します。次のコードでは、| 演算子と or_() メソッドを使って2つの条件の論理和 (OR) を計算しています。

df.with_columns(
    cond1=(pl.col('x') > 3) | (pl.col('y') < 10),
    cond2=pl.col('x').gt(3).or_(pl.col('y').lt(10))
)
shape: (4, 4)
xycond1cond2
i64i64boolbool
140falsefalse
230falsefalse
320falsefalse
410truetrue

数学関数#

sinsqrt といった単項関数は、対応するメソッドを使用して計算します。一方、arctan2 のような二項関数は、グローバル関数を使用して計算します。

df.with_columns(
    sin=pl.col('x').sin(),
    sqrt=pl.col('y').sqrt(),
    arctan=pl.arctan2(pl.col('y'), pl.col('x')),
)
shape: (4, 5)
xysinsqrtarctan
i64i64f64f64f64
1400.8414716.3245551.545802
2300.9092975.4772261.504228
3200.141124.4721361.421906
410-0.7568023.1622781.19029

条件分岐#

条件分岐は、pl.when(cond) を起点とし、その後に .then().when().otherwise() などのメソッドを組み合わせて表現します。たとえば、次のコードは以下の条件分岐を実現します。

if x > 4:
    result = 'very good'
elif x > 3:
    result = 'good'
elif x > 2:
    result = 'fair'
elif x > 1:
    result = 'bad'
else:
    result = 'very bad'
df.with_columns(
    pl.when(pl.col('x') > 4)
      .then(pl.lit('very good'))
      .when(pl.col('x') > 3)
      .then(pl.lit('good'))
      .when(pl.col('x') > 2)    
      .then(pl.lit('fair'))
      .when(pl.col('x') > 1)    
      .then(pl.lit('bad'))    
      .otherwise(pl.lit('very bad'))
      .alias('level')
)
shape: (4, 3)
xylevel
i64i64str
140"very bad"
230"bad"
320"fair"
410"good"

.cut() は、指定された境界値に基づいてデータを切り分け、各区間にラベルを付けることができる関数です。

  • breaks 引数には、N 個の境界値を指定します。

  • labels 引数には、N+1 個のラベルを指定します。

  • left_closed 引数で、区間が左側閉区間右側閉区間かを指定します。

以下のコードは、x 列を cut() を使ってラベル付けする例です

  • left_closed=False: 右側が閉区間の場合

    • x <= 1 → very bad

    • 1 < x <= 2 → bad

    • 2 < x <= 3 → fair

    • 3 < x <= 4 → good

    • 4 < x → very good

  • left_closed=True: 左側が閉区間の場合

    • x < 1 → very bad

    • 1 <= x < 2 → bad

    • 2 <= x < 3 → fair

    • 3 <= x < 4 → good

    • 4 <= x → very good

labels = ['very bad', 'bad', 'fair', 'good', 'very good']
df.with_columns(
    level_right=pl.col('x').cut(breaks=[1, 2, 3, 4], labels=labels, left_closed=False),
    leve_left=pl.col('x').cut(breaks=[1, 2, 3, 4], labels=labels, left_closed=True),
)
shape: (4, 4)
xylevel_rightleve_left
i64i64catcat
140"very bad""bad"
230"bad""fair"
320"fair""good"
410"good""very good"

.clip() は、指定した最小値や最大値を境界として、値がその範囲を超えた場合に境界値へ変換することができる関数です。以下の例では、y 列を様々な境界条件でクリップ処理しています。

  • y1 列: y 列の値を 20 ~ 30 の範囲内に収めます。

  • y2 列: 下限値のみを 30 に設定し、それより小さい値を 30 にクリップします。

  • y3 列: 上限値のみを 20 に設定し、それより大きい値を 20 にクリップします。

  • y4 列: 上限値と下限値を演算式で指定します。この場合、各要素ごとに個別の境界値が計算され、それを基準にクリップされます。

df.with_columns(
    y1=pl.col('y').clip(20, 30),
    y2=pl.col('y').clip(lower_bound=30),
    y3=pl.col('y').clip(upper_bound=20),
    y4=pl.col('y').clip(pl.col('x') * 10, pl.col('x') * 20)
)
shape: (4, 6)
xyy1y2y3y4
i64i64i64i64i64i64
14030402020
23030302030
32020302030
41020301040

要素の置き換え#

.replace().replace_strict() を使用すると、要素を置き換えることができます。置き換え前後の値を、二つのリストまたは一つの辞書として渡すことができます。両者の違いは以下の通りです:

  • .replace(): 一部の要素を置き換えます。結果は元のデータ型のままとなります。

  • .replace_strict(): すべての要素を置き換えます。結果は置き換え後のデータ型に変わります。

次のコードでは、x 列の値は置き換え後に文字列型 (x2列) に変わり、y 列の値は元のデータ型のまま (y2列) です。

df.with_columns(
    x2=pl.col('x').replace_strict({1:'very bad', 2:'bad', 3:'fair'}, default='unknown'),
    y2=pl.col('y').replace([10, 20], [100, 200]),
)
shape: (4, 4)
xyx2y2
i64i64stri64
140"very bad"40
230"bad"30
320"fair"200
410"unknown"100

データの選択#

Polars の演算式メソッドを使用して、特定の条件に基づいたデータの抽出や処理が可能です。以下は代表的なメソッドとその用途を示します。

メソッド

説明

.head(n)

最初の n 行を取得します。

.tail(n)

最後の n 行を取得します。

.slice(offset, n)

指定したオフセットから n 行を取得します。

.get(index)

指定したインデックスの値を取得します。

.gather(indices)

指定したインデックスリストに基づいて行を取得します。

.gather_every(n, offset=0)

指定したステップ間隔で行を取得します。

.filter(condition)

条件を満たす行を取得します。

.top_k(k)

値が上位 k 個の行を取得します。

.top_k_by(col, k)

指定した列に基づいて値が上位 k 個の行を取得します。

.bottom_k(k)

値が下位 k 個の行を取得します。

.bottom_k_by(col, k)

指定した列に基づいて値が下位 k 個の行を取得します。

.sample(n)

ランダムに n 行を取得します。

df_res = df.select(
    head=pl.col("x").head(2),                       # 最初の2行
    tail=pl.col("x").tail(2),                       # 最後の2行
    slice=pl.col("x").slice(1, 2),                  # オフセット1から2行
    get=pl.col("x").get(3),                         # インデックス3の値
    gather=pl.col("x").gather([0, 3]),              # インデックス0と3の行
    gather_every=pl.col("x").gather_every(2, offset=1), # 1オフセットで2ステップごと
    filter=pl.col("x").filter(pl.col("y") // 10 % 2 == 0), # 条件でフィルタ
    top_k=pl.col("x").top_k(2),                     # 上位2件
    top_k_by=pl.col("x").top_k_by(pl.col("y"), 2),  # y列に基づく上位2件
    bottom_k=pl.col("x").bottom_k(2),               # 下位2件
    bottom_k_by=pl.col("x").bottom_k_by(pl.col("y"), 2), # y列に基づく下位2件
    sample=pl.col("x").sample(2),                   # ランダム2行
)
row(df, df_res)
shape: (4, 2)
xy
i64i64
140
230
320
410
shape: (2, 12)
headtailslicegetgathergather_everyfiltertop_ktop_k_bybottom_kbottom_k_bysample
i64i64i64i64i64i64i64i64i64i64i64i64
132412141142
243444332231

Polars の多くのメソッドでは、単なる値だけでなく、演算式を引数として渡すことができます。これにより、柔軟で動的なデータ操作が可能になります。例えば、.get() メソッドに演算式を渡すことで、複数の要素を動的に取得することができます。

次のコードでは、

  • pl.len() でデータフレームの行数を取得します。

  • (pl.col("x") + 1) % pl.len()で、列 x の各要素に 1 を足し、データフレームの行数で割った余りをインデックスとします。

  • .get() で上の式で計算したインデックスを使用し、要素を取得します。

df.with_columns(
    get=pl.col('y').get((pl.col('x') + 1) % pl.len())
)
shape: (4, 3)
xyget
i64i64i64
14020
23010
32040
41030

データの結合#

concat().append() を使用すると、複数の演算結果を縦方向に結合することができます。一方、.extend_constant() を使用すると、定数を複数回追加することができます。

以下のプログラムは、x 列と y 列の一部或いは定数を結合する例を示しています。

df2 = df.select(
    append=pl.col('x').append(pl.col('y').slice(1, 2)),
    concat=pl.concat([pl.col('x'), pl.col('y').slice(1, 2)]),
    extend=pl.col('x').extend_constant(99, 2),
)
row(df, df2)
shape: (4, 2)
xy
i64i64
140
230
320
410
shape: (6, 3)
appendconcatextend
i64i64i64
111
222
333
444
303099
202099

concat_list()concat_arr()、および struct() を使用して、複数の演算結果を横方向に結合します。それぞれ、List 型、Array 型、Struct 型の列を作成します。以下のプログラムは、x 列、y 列、そして x 列と y 列の商を横方向に結合する例です。

  • List 型と Array 型では、すべての要素のデータ型が一致している必要があります。

    • concat_list() は自動的に上位のデータ型にキャストします。

    • concat_arr() を使用する場合は、手動で同じデータ型にキャストする必要があります。

  • Struct 型では、各フィールドが独自のデータ型を持つことができます。

df3 = df.select(
    list=pl.concat_list('x', pl.col('y') / pl.col('x')),
    arr=pl.concat_arr(pl.col('x').cast(pl.Float64), pl.col('y') / pl.col('x')),
    struct=pl.struct('x', pl.col('y') / pl.col('x')),
)

row(df, df3)
shape: (4, 2)
xy
i64i64
140
230
320
410
shape: (4, 3)
listarrstruct
list[f64]array[f64, 2]struct[2]
[1.0, 40.0][1.0, 40.0]{1,40.0}
[2.0, 15.0][2.0, 15.0]{2,15.0}
[3.0, 6.666667][3.0, 6.666667]{3,6.666667}
[4.0, 2.5][4.0, 2.5]{4,2.5}

.explode() を使用すると、List 型および Array 型の列の要素を行優先で縦方向に結合することができます。また、.list.explode().arr.explode() は同じ処理を行います。

次のプログラムは、上記の結果で作成した list 列と arr 列の要素を縦方向に結合する例を示しています。又、Struct 型の列を行優先で結合する場合は、まず、concat_list() を使用して .struct.unnest() の結果を List 型に変換します。その後、.explode() を適用して縦方向に結合します。列優先で結合する場合は.struct.unnest() の結果を concat() を用いて結合します。

df4 = df3.select(
    pl.col('list').explode(), 
    pl.col('arr').explode(),
    struct_row=pl.concat_list(pl.col('struct').struct.unnest()).explode(),
    struct_col=pl.concat([pl.col('struct').struct.unnest()])
)
row(df3, df4)
shape: (4, 3)
listarrstruct
list[f64]array[f64, 2]struct[2]
[1.0, 40.0][1.0, 40.0]{1,40.0}
[2.0, 15.0][2.0, 15.0]{2,15.0}
[3.0, 6.666667][3.0, 6.666667]{3,6.666667}
[4.0, 2.5][4.0, 2.5]{4,2.5}
shape: (8, 4)
listarrstruct_rowstruct_col
f64f64f64f64
1.01.01.01.0
40.040.040.02.0
2.02.02.03.0
15.015.015.04.0
3.03.03.040.0
6.6666676.6666676.66666715.0
4.04.04.06.666667
2.52.52.52.5

順番替え#

.shift() は、1 列の値を上向きまたは下向きに N 個ずらす関数です。N が正数の場合は下向きにシフトし、負数の場合は上向きにシフトします。元の位置には、fill_value 引数で指定した値で埋められます。デフォルトでは NULL が使われます。また、循環シフトを行いたい場合は、.append() で先頭または最後の値を結合してからシフトを行います。

df1 = df.with_columns(
    x_shift1=pl.col("x").shift(1),
    x_shift2=pl.col("x").shift(-2),
)

df2 = df.select(
    x_shift1=pl.col('x').tail(1).append(pl.col('x')).shift(1).tail(pl.len()),
    x_shift2=pl.col('x').append(pl.col('x').head(2)).shift(-2).head(pl.len())
)

row(df1, df2)
shape: (4, 4)
xyx_shift1x_shift2
i64i64i64i64
140null3
23014
3202null
4103null
shape: (4, 2)
x_shift1x_shift2
i64i64
43
14
21
32

.reverse() は、列の値を逆向きに並べ替える関数です。

df.with_columns(x_reverse=pl.col('x').reverse())
shape: (4, 3)
xyx_reverse
i64i64i64
1404
2303
3202
4101

.sort().sort_by() は、列の値を大小関係で並べ替える関数です。 descending=True の場合は降順で並べ替えます。デフォルトでは昇順(False)になります。

  • .sort(): 指定した列を昇順または降順で並べ替えます。

  • .sort_by():並べ替えの基準となる演算式を複数指定できます。

次のコードでは、x 列を降順で並べ替えた結果を x_sort 列として追加し、y 列を昇順で並べ替えた結果を y_sort 列として追加しています。また、xy 列を、y 列と 24 の距離を基準に昇順で並べ替えた結果を x_sort_byy_sort_by 列として追加します。

df.with_columns(
    pl.col('x').sort(descending=True).alias('x_sort'),
    pl.col('y').sort().alias('y_sort'),
    pl.all().sort_by((pl.col('y') - 24).abs()).name.suffix('_sort_by')
)
shape: (4, 6)
xyx_sorty_sortx_sort_byy_sort_by
i64i64i64i64i64i64
140410320
230320230
320230410
410140140

NULL処理#

NULLに関するの演算式はNULL処理で詳しく説明します。

横向き演算#

関数名に horizontal が含まれる関数は、複数の列間で横方向の演算を行います。次のコードは、これらの関数をすべて出力します。

for name in dir(pl):
    if 'horizontal' in name:
        print(name)
all_horizontal
any_horizontal
cum_sum_horizontal
max_horizontal
mean_horizontal
min_horizontal
sum_horizontal

次のコードは、tag 列以外のすべての列の総和を計算し、その結果を新しい列 sum として追加します。

df = pl.DataFrame(
    {
     'tag': ['a', 'b', 'a', 'b', 'c', 'e'],        
     'A': [5, 1, 4, 5, 4, 6],
     'B': [3, 1, 1, 6, 4, 9],
     'C': [5, 6, 3, 7, 0, 9],
     'D': [0, 8, 0, 1, 0, 5],
     'E': [7, 7, 6, 2, 2, 8],
    }
)

df.with_columns(
    pl.sum_horizontal(pl.exclude("tag")).alias("sum")
)

horizontal 関数が利用できない場合、concat_list() を使用して複数の列を 1 つのリスト列に変換し、その後 list.xxx 関数を使って計算する方法もあります。以下の例では、すべての列の中央値を計算し、新しい列 median として追加しています(tag 列を除く)。

df.with_columns(
    pl.concat_list(pl.exclude("tag"))
    .list.median()
    .alias("median")
)
shape: (6, 7)
tagABCDEmedian
stri64i64i64i64i64f64
"a"535075.0
"b"116876.0
"a"413063.0
"b"567125.0
"c"440022.0
"e"699588.0

list.xxx で実現できない複雑な計算は、list.eval() を使用して柔軟に計算できます。以下のコードは、各行のトップ 2 の値を抽出し、それらの総和を計算して新しい列 sum_top_2 として追加する例です。

df.with_columns(
    pl.concat_list(pl.exclude("tag"))
    .list.eval(
        pl.element()
        .sort(descending=True)
        .head(2)
        .sum())
    .flatten().alias("sum_top_2")
)
shape: (6, 7)
tagABCDEsum_top_2
stri64i64i64i64i64i64
"a"5350712
"b"1168715
"a"4130610
"b"5671213
"c"440028
"e"6995818