データ型#

from helper.python import print_subclasses
from helper.jupyter import row
import numpy as np
import polars as pl

Polarsは、データの効率的な管理と操作を可能にするため、多様なデータ型をサポートしています。この章では、Polarsで利用可能なデータ型を詳細に紹介し、各データ型の特性と用途について説明します。数値型、時系列型、ネスト型など、さまざまなデータ型がどのように構造化され、データ処理にどのように役立つかを理解することで、Polarsの強力なデータ処理機能を最大限に活用できるようになります。

データ型の継承木#

Polarsのデータ型は、非常に多様で柔軟性があります。次の継承木の構造と各データ型の詳細について説明します。この継承木は、各データ型がどのように構造化されているかを示し、各データ型の特性や用途を理解するのに役立ちます。NumericTypeやTemporalTypeのような主要なデータ型は、さらに細かいデータ型に分かれており、それぞれが特定のデータ処理ニーズに応じて最適化されています。NestedTypeは複雑なデータ構造をサポートし、BooleanやStringなどの基本的なデータ型も広範なデータ処理を可能にします。

print_subclasses(pl.DataType)
└──DataType
   ├──NumericType
   │  ├──IntegerType
   │  │  ├──SignedIntegerType
   │  │  │  ├──Int8
   │  │  │  ├──Int16
   │  │  │  ├──Int32
   │  │  │  ├──Int64
   │  │  │  └──Int128
   │  │  └──UnsignedIntegerType
   │  │     ├──UInt8
   │  │     ├──UInt16
   │  │     ├──UInt32
   │  │     └──UInt64
   │  ├──FloatType
   │  │  ├──Float32
   │  │  └──Float64
   │  └──Decimal
   ├──TemporalType
   │  ├──Date
   │  ├──Time
   │  ├──Datetime
   │  └──Duration
   ├──NestedType
   │  ├──List
   │  ├──Array
   │  └──Struct
   ├──ObjectType
   │  └──Object
   ├──Boolean
   ├──String
   ├──Binary
   ├──Categorical
   ├──Enum
   ├──Null
   └──Unknown

数値型

  • NumericType: 数値型の基底クラス。整数型や浮動小数点型などの数値データを扱います。

    • IntegerType: 整数データを表すクラスの基底クラス。

      • SignedIntegerType: 符号付き整数データ(Int8、Int16、Int32、Int64)

      • UnsignedIntegerType: 符号なし整数データ(UInt8、UInt16、UInt32、UInt64)

    • FloatType: 浮動小数点データを表すクラスの基底クラス(Float32、Float64)

    • Decimal: 固定小数点数データを扱います。高精度の数値計算に使用されます。

時系列型

  • TemporalType: 時間に関連するデータ型の基底クラス。

    • Date: 日付データを扱います(例: 2024-07-18)。

    • Time: 時刻データを扱います(例: 12:34:56)。

    • Datetime: 日時データを扱います(例: 2024-07-18 12:34:56)。

    • Duration: 時間の期間を扱います(例: 5分、2時間)。

ネスト型

  • NestedType: ネスト構造を持つデータ型の基底クラス。

    • List: 要素のリストを扱います。リスト内の要素は同じデータ型でなければなりません。

    • Array: 固定長の配列を扱います。

    • Struct: 構造体データを扱います。異なるデータ型を持つ複数のフィールドを持つことができます。

その他のデータ型

  • Boolean: 真偽値データを扱います(TrueまたはFalse)。

  • String: 文字列データを扱います。

  • Binary: バイナリデータを扱います。

  • Categorical: カテゴリカルデータを扱います。効率的にカテゴリ値を管理します。

  • Enum: 列挙型データを扱います。

  • Object: 任意のPythonオブジェクトを扱います。特定のデータ型に適合しないデータを格納するために使用されます。

  • Null: NULL値を扱います。欠損値を示すために使用されます。

  • Unknown: 未知のデータ型を扱います。

データ型変換#

Cast#

Polarsは、データ型の変換を簡単に行うための便利な関数を提供しています。特に、cast()shrink_dtype() の2つの関数は、データ型のキャストや縮小に使用されます。以下に、それぞれの関数について詳しく説明します。

df = pl.DataFrame(
    {
        "a": [3, 3, 3, 4],
        "b": [4, 12, 6, 7],
        "g": ['B', 'A', 'A', 'C']
    }
)
df
shape: (4, 3)
abg
i64i64str
34"B"
312"A"
36"A"
47"C"

cast()は、指定されたデータ型に列をキャストするために使用されます。この関数は、データの表現形式を変える際に非常に便利です。例えば、整数型の列を浮動小数点型に変換したり、文字列型の列を日付型に変換したりすることができます。

df.with_columns(
    pl.col.a.cast(pl.UInt8),
    pl.col.b.cast(pl.String)
)
shape: (4, 3)
abg
u8strstr
3"4""B"
3"12""A"
3"6""A"
4"7""C"

shrink_dtype()は、データ型を必要最小限のサイズに縮小するために使用されます。例えば、Int64 型の列が実際には Int32 で十分な場合、この関数を使用してデータ型を自動的に縮小することができます。これにより、メモリ使用量を削減し、効率的なデータ管理が可能になります。

df.select(pl.all().shrink_dtype())
shape: (4, 3)
abg
i8i8str
34"B"
312"A"
36"A"
47"C"

データ型再定義#

castではなく、NumPyの.view()のように、データをそのままにして、別の型に切り替えたい場合は、次のプログラムのように、.to_arrow()で一旦PyArrowの配列に変換し、その配列の.view()メソッドでデータ型を変更します。最後にその結果をSeriesに変換します。

Expr.reinterpret()Series.reinterpret()もがありますが、これは整数int64とuint64間の切り替えしかできません。

データ型を変更する際に、データそのものは変更せずに型だけを切り替えたい場合があります。NumPyの .view() メソッドのように、データの内容をそのままにして別の型に変更するには、以下の方法を使用します。

  1. to_arrow() を使用してPyArrowの配列に変換

  2. PyArrowの配列の .view() メソッドでデータ型を変更

  3. 変更後の結果を Series に変換

次の例では、UInt64のデータをFloat64として再定義します。

df_wrong = pl.DataFrame({"nums": [4837362400224322580,None,4837362400224322584]}, {"nums":pl.UInt64})
df_fixed = df_wrong.select(pl.col("nums").map_batches(lambda s:pl.Series(s.to_arrow().view("f8"))))
row(df_wrong, df_fixed)
shape: (3, 1)
nums
u64
4837362400224322580
null
4837362400224322584
shape: (3, 1)
nums
f64
2.5000e15
null
2.5000e15

Polarsには Expr.reinterpret() および Series.reinterpret() というメソッドもありますが、これらは主に int64uint64 の間で型を切り替えるためのもので、他のデータ型への変更には使用できません。

バイナリ文字列から同じバイト数のデータ型に変換するには、bin.reinterpret()を使えます。dtype引数で変換後のデータ型を指定し、endianness引数でバイトオーダー(”big” または “little”)を指定できます。次の例では、バイナリデータが4バイトの符号なし整数(UInt32)に変換され、ビッグエンディアン(big-endian)として解釈されます。

df = pl.DataFrame({
    'binary_column': [b'\x00\x00\x00\x01', b'\x00\x00\x00\x02', b'\x00\x00\x00\x03', b'\x00\x00@A']
})

df.select(pl.col('binary_column').bin.reinterpret(dtype=pl.UInt32, endianness="big"))
shape: (4, 1)
binary_column
u32
1
2
3
16449

文字列#

Polarsは文字列データの処理にも強力な機能を備えています。文字列処理関連の演算式関数はネーミングスペース.str の下にあります。

文字列の長さ#

  • str.len_chars(): 文字列のキャラクター数(文字数)を測定します。これは、文字列内に含まれる実際の文字の数を返します。たとえば、マルチバイト文字(日本語のような2バイト文字)も1文字としてカウントされます。

  • str.len_bytes(): 文字列のバイト数を測定します。これは、各文字がエンコードされる際に占めるバイト数の合計を返します。マルチバイト文字は複数バイトとしてカウントされます。

df = pl.DataFrame({
    "lib": ["pandas", "numpy", "polars", "図形"],
    "ver": ["2.0", "1.1.4", "1.2.1", "4.0"]
})

df.select(
    "lib",
    len_char=pl.col("lib").str.len_chars(),
    len_byte=pl.col("lib").str.len_bytes()
)
shape: (4, 3)
liblen_charlen_byte
stru32u32
"pandas"66
"numpy"55
"polars"66
"図形"26

文字列の部分一致検索#

str.contains()str.starts_with()str.ends_with()などの関数で、特定の部分文字列が含まれるか、指定された文字列で始まるか、終わるかをチェックできます。

df.filter(pl.col.lib.str.starts_with("p"))
shape: (2, 2)
libver
strstr
"pandas""2.0"
"polars""1.2.1"
df.filter(pl.col.ver.str.ends_with("0"))
shape: (2, 2)
libver
strstr
"pandas""2.0"
"図形""4.0"
df.filter(pl.col.lib.str.contains('p'))
shape: (3, 2)
libver
strstr
"pandas""2.0"
"numpy""1.1.4"
"polars""1.2.1"

.contains()には正規表現が使えます。次のコードは二つの点があるバージョンの行を取得します。

df.filter(pl.col.ver.str.contains("^\d+\.\d+\.\d+$"))
<>:1: SyntaxWarning: invalid escape sequence '\d'
<>:1: SyntaxWarning: invalid escape sequence '\d'
C:\Users\ruoyu\AppData\Local\Temp\ipykernel_2988\1031424121.py:1: SyntaxWarning: invalid escape sequence '\d'
  df.filter(pl.col.ver.str.contains("^\d+\.\d+\.\d+$"))
shape: (2, 2)
libver
strstr
"numpy""1.1.4"
"polars""1.2.1"

.str.contains_any() は特定の文字列やパターンのリストのいずれかが含まれているかを判定するために使用されます。このメソッドは、複数のキーワードが含まれているかどうかを一度にチェックしたい場合に非常に便利です。

df2 = pl.DataFrame({
    "text": ["hello world", "data process library", "data science", "machine learning", "plot library"]
})

df2.filter(
    pl.col("text").str.contains_any(["science", "machine"])
)
shape: (2, 1)
text
str
"data science"
"machine learning"

.str.contains_any() の引数には、演算式も使用できます。以下のプログラムは、text 列に含まれる単語の中で、出現頻度が上位2つの単語を含む行を抽出します。

expr = pl.col.text.str.split(' ').explode().value_counts(sort=True).head(2).struct.field('text')
row(df2.select(expr), df2.filter(pl.col.text.str.contains_any(expr)))
shape: (2, 1)
text
str
"data"
"library"
shape: (3, 1)
text
str
"data process library"
"data science"
"plot library"

文字列の置換#

.str.replace(), .str.replace_all()で特定の部分文字列を置換します。

  • str.replace(): は最初に見つかったN個の一致のみを置換します。

  • str.replace_all(): はすべての一致を置換します。

df2.select(pl.col.text.str.replace_all('data', 'DATA'))
shape: (5, 1)
text
str
"hello world"
"DATA process library"
"DATA science"
"machine learning"
"plot library"

文字列の分割#

文字列を特定の区切り文字で分割できます。

df2.with_columns([
    pl.col("text").str.split(" ").alias("split")
])
shape: (5, 2)
textsplit
strlist[str]
"hello world"["hello", "world"]
"data process library"["data", "process", "library"]
"data science"["data", "science"]
"machine learning"["machine", "learning"]
"plot library"["plot", "library"]
df2.with_columns([
    pl.col("text").str.split_exact(" ", 1).alias("split_exact")
])
shape: (5, 2)
textsplit_exact
strstruct[2]
"hello world"{"hello","world"}
"data process library"{"data","process"}
"data science"{"data","science"}
"machine learning"{"machine","learning"}
"plot library"{"plot","library"}

文字列のトリミング#

文字列の前後の空白や特定の文字を除去します。

df3 = pl.DataFrame({
    "text": [" hello ", " world", "polars ", "is ", " awesome"]
})

df3.with_columns([
    pl.col("text").str.strip_chars(" ").alias("stripped"),
    pl.col("text").str.strip_prefix(" ").alias("no_prefix"),
    pl.col("text").str.strip_suffix(" ").alias("no_suffix")
    
])
shape: (5, 4)
textstrippedno_prefixno_suffix
strstrstrstr
" hello ""hello""hello "" hello"
" world""world""world"" world"
"polars ""polars""polars ""polars"
"is ""is""is ""is"
" awesome""awesome""awesome"" awesome"
  • str.strip_chars(): 両端の空白を削除。

  • str.strip_prefix(): 左端の空白を削除。

  • str.strip_suffix(): 右端の空白を削除。

文字列のパディング#

文字列の左右、または片方に特定の文字で埋めてパディングを行います。

df2.with_columns([
    pl.col("text").str.pad_start(20, "-").alias("padded")
])
shape: (5, 2)
textpadded
strstr
"hello world""---------hello world"
"data process library""data process library"
"data science""--------data science"
"machine learning""----machine learning"
"plot library""--------plot library"

文字列の抽出#

文字列データからパターンにマッチした部分を抽出するには、str.extractstr.extract_allstr.extract_groupsstr.extract_many などの関数を使用します。

str.extract(pattern, group_index) は、正規表現に基づいてマッチした 特定のキャプチャグループ を返します。
この関数には以下の2つの引数があります:

  • pattern正規表現パターン を指定します。文字列から特定の部分を抽出するために、() で囲んだ キャプチャグループ を用います。キャプチャグループは、マッチした部分を取得するための括弧で、複数のグループを作ることも可能です。その場合、それぞれのグループには 1, 2, 3… の番号が割り当てられます。

  • group_indexキャプチャグループの番号 を指定します。正規表現内でどのグループを抽出するかを決定します。

以下のコードでは、pattern に2つのキャプチャグループを定義し、group_index を指定して アイテムAアイテムB の数値を抽出します。

df = pl.DataFrame({
    "text": ["Item A: 50, Item B: 30", "Item A: 70, Item B: 45"]
})

pattern = r'Item A: (\d+), Item B: (\d+)'

df.select(
    pl.col("text").str.extract(pattern, 1).alias("A_value"),
    pl.col("text").str.extract(pattern, 2).alias("B_value"),
)
shape: (2, 2)
A_valueB_value
strstr
"50""30"
"70""45"

str.extract_groups() は、正規表現の すべてのキャプチャグループ を抽出し、それらを 構造体(Struct)列 として返します。

  • グループ名が指定されていない場合、グループ番号(1, 2, 3, …)がフィールド名になります。

(
df.select(
    pl.col('text').str.extract_groups(pattern).alias('values')
)
.unnest('values')
)
shape: (2, 2)
12
strstr
"50""30"
"70""45"
  • 正規表現内で 名前付きキャプチャグループ (?P<name>...)を使用した場合、その名前がフィールド名になります。

pattern_named = r'Item A: (?P<A_value>\d+), Item B: (?P<B_value>\d+)'

(
df.select(
    pl.col("text").str.extract_groups(pattern_named).alias('values')
)
.unnest('values')
)
shape: (2, 2)
A_valueB_value
strstr
"50""30"
"70""45"

str.extract_all() は、正規表現に基づいてマッチしたすべての部分をリストとして返します。

df.select(
    pl.col("text").str.extract_all(r'\d+').alias('values')
)
shape: (2, 1)
values
list[str]
["50", "30"]
["70", "45"]

Polarsには、re.split() のように正規表現を使って文字列を分割する関数が用意されていません。その代わりに、以下のコードでは extract_all() を用いて分割パターンを含むすべての部分を抽出し、list.eval() を使用して各文字列から分割パターンを削除することで、同様の機能を実装しています。

次のコードでは、

  1. extract_all() を使って、分割パターン (and|after|or|then) を含むすべての部分を抽出します。

  2. list.eval() を使い、str.replace() で分割パターンを空文字 ("") に置き換えます。

こうすることで、Polars で re.split() のような文字列分割を実現できます。

df = pl.DataFrame(
    {"text": [
        "DOGandCATafterFISHorBIRDthenMOUSE",
        "a or b or c"
    ]})

pattern = "and|after|or|then"

def split(expr, pattern):
    return expr.str.extract_all(rf".*?({pattern}|$)").list.eval(pl.element().str.replace(pattern, ''))

df.select(pl.col('text').pipe(split, pattern)).get_column('text').to_list()
[['DOG', 'CAT', 'FISH', 'BIRD', 'MOUSE'], ['a ', ' b ', ' c']]

str.extract_many() は、Aho-Corasickアルゴリズムを使用して、文字列内から指定されたパターンに一致するすべての部分を抽出するための関数です。このメソッドは、正規表現ではなく、文字列リテラルのみに基づいてマッチングを行います。このメソッドは、文字列検索に対して非常に効率的であり、大量のデータセットでもパフォーマンスを発揮します。引数は以下のようです。

  • patterns: 検索する文字列パターンのリストを指定します。["apple", "banana", "orange"] のように、探したい特定の単語や文字列をリスト形式で与えます。

  • ascii_case_insensitive: 大文字・小文字を区別しないマッチングを有効にするかどうかを指定します。

  • overlapping: オーバーラップするマッチを許可するかどうかを指定します。True にすると、同じ文字列内で複数のパターンが重複している場合、それぞれを独立したマッチとして扱います。

df = pl.DataFrame({
    "text": ["I like apples and bananas", "I prefer oranges and pineapples"]
})

# "apple"と"banana"を検索
df.select(
    pl.col("text").str.extract_many(["apple", "banana"]).alias("matches")
)
shape: (2, 1)
matches
list[str]
["apple", "banana"]
["apple"]

EnumとCategorical#

Enum型(列挙型)は、特定の固定された値の集合から選択されるデータ型です。これは、データの一貫性を確保し、誤った値が入力されるのを防ぐのに役立ちます。Enum型を使用することで、プログラム内で定義された値以外の入力を防ぐことができ、データの整合性を維持することができます。

Categorical型(カテゴリカル型)は、有限個のカテゴリーを持つデータ型で、データの重複を避け、メモリ効率を向上させるために使用されます。Categorical型は、各カテゴリに整数のインデックスを割り当てることで、メモリ使用量を削減します。

Enumは事前に作成され、要素とその順番が固定されています。一方、Categoricalは事前作成の必要がなく、要素の順番はデータ内での出現順になります。

score = pl.Enum(['A', 'B', 'C', 'D'])
df = pl.DataFrame(
    {
        "a": [3, 3, 3, 4],
        "b": [4, 12, 6, 7],
        "g": ['B', 'A', 'A', 'C']
    }
)
df2 = df.select(
    pl.col.g.cast(score).alias('g_enum'),
    pl.col.g.cast(pl.Categorical).alias('g_cat')
)
df2
shape: (4, 2)
g_enumg_cat
enumcat
"B""B"
"A""A"
"A""A"
"C""C"

to_physical()を使用すると、内部で保存されている番号を取得できます。次の結果では、g_enum の番号は score で定義された番号であり、g_catの番号は元の列の値が出現する順番に基づいています。

df2.select(pl.all().to_physical())
shape: (4, 2)
g_enumg_cat
u32u32
10
01
01
22

次のコードは、Categorical型の列で異なるコードが使用される場合の違いを示しています。異なるコードが使用される二つのCategorical列同士で計算を行うと、処理が遅くなる可能性があります。

df3 = pl.DataFrame(dict(
    c1 = pl.Series(["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=pl.Categorical),
    c2 = pl.Series(["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=pl.Categorical),
))

df3.with_columns(pl.all().to_physical().name.suffix('_code'))
shape: (5, 4)
c1c2c1_codec2_code
catcatu32u32
"Polar""Panda"00
"Panda""Brown"11
"Brown""Brown"21
"Brown""Polar"22
"Polar""Polar"02

次に、pl.StringCache()を使用して、同じコードが使われるCategorical列を作成します。

with pl.StringCache() as s:
    df4 = pl.DataFrame(dict(
        c1 = pl.Series(["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=pl.Categorical),
        c2 = pl.Series(["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=pl.Categorical),
    ))
    
df4.with_columns(pl.all().to_physical().name.suffix('_code'))
shape: (5, 4)
c1c2c1_codec2_code
catcatu32u32
"Polar""Panda"01
"Panda""Brown"12
"Brown""Brown"22
"Brown""Polar"20
"Polar""Polar"00

DateTime#

ListとArray#

一つの要素に複数の値を保存する場合は、List 列または Array 列を使用します。Array 列では、各要素の長さが一定でなければなりませんが、List 列では要素の長さが異なっても問題ありません。Array 列と List 列の要素を処理するための式関数は、それぞれ array および list ネームスペース内にあります。

次のコードでは、a_listb_arr という列を持つ DataFrame を作成し、それぞれのデータ型を schemapl.List(pl.Int8)pl.Array(pl.Int8, 3) に定義します。List 列のデータ型は pl.List(element_dtype) で、Array 列のデータ型は pl.Array(element_dtype, length) で定義されます。element_dtype は要素のデータ型を、length は各配列の長さを表します。

df = pl.DataFrame(
    dict(
        a_list=[[1, 2], [3, 4], [5, 6, 7]],
        b_arr=[[1, 2, 3], [3, 4, 5], [5, 6, 7]]
    ),
    schema=dict(a_list=pl.List(pl.Int8), b_arr=pl.Array(pl.Int8, 3))
)
df
shape: (3, 2)
a_listb_arr
list[i8]array[i8, 3]
[1, 2][1, 2, 3]
[3, 4][3, 4, 5]
[5, 6, 7][5, 6, 7]

次のコードでは、list ネームスペース内の max()mean()len() を使用して、a_list 列の各リストの最大値、平均値、要素数を計算し、新しい列として追加します。 また、b_arr 列の各配列の最大値を arr.max() で計算します。arr ネームスペースは比較的新しく、バージョン 1.24.0 では arr.len()arr.mean() はまだサポートされていません。

df.with_columns(
    a_max=pl.col("a_list").list.max(),
    a_mean=pl.col("a_list").list.mean(),
    a_len=pl.col("a_list").list.len(),
    b_max=pl.col("b_arr").arr.max(),
)
shape: (3, 6)
a_listb_arra_maxa_meana_lenb_max
list[i8]array[i8, 3]i8f64u32i8
[1, 2][1, 2, 3]21.523
[3, 4][3, 4, 5]43.525
[5, 6, 7][5, 6, 7]76.037

getgather#

list.get(index) または arr.get(index) を使用すると、指定した index の要素を取得できます。次のコードでは、a_list 列の各リストの最初の要素と最後の要素の合計を計算します。b_arr 列に対しても同様の処理を行います。

df.with_columns(
    a_first_plus_last = pl.col('a_list').list.get(0) + pl.col('a_list').list.get(-1),
    b_first_plus_last =pl.col('b_arr').arr.get(0) + pl.col('b_arr').arr.get(-1), 
)
shape: (3, 4)
a_listb_arra_first_plus_lastb_first_plus_last
list[i8]array[i8, 3]i8i8
[1, 2][1, 2, 3]34
[3, 4][3, 4, 5]78
[5, 6, 7][5, 6, 7]1212

list.gather(indices) を使用すると、複数のインデックスを指定して要素を取得し、新しいリスト列に保存できます。null_on_oob 引数のデフォルト値は False で、この場合、範囲外のインデックスを指定するとエラーが発生します。一方、True に設定すると、範囲外のインデックスは Null で埋められます。次のコードは、リスト a_list の各要素に対して、インデックス [2, 1, 0] の順に要素を取得し、範囲外のインデックスがある場合は Null で埋める処理を行います。

df.select(
    'a_list',
    gather=pl.col('a_list').list.gather([2, 1, 0], null_on_oob=True)
)
shape: (3, 2)
a_listgather
list[i8]list[i8]
[1, 2][null, 2, 1]
[3, 4][null, 4, 3]
[5, 6, 7][7, 6, 5]

concat_listconcat_arr#

pl.concat_list() 関数は、指定した列や式の結果をリストとして結合するために使用されます。この関数を使用すると、複数の列の値を 1 つのリスト列にまとめることができます。一方、pl.concat_arr() は、複数の式の結果を配列として結合します。

df = pl.DataFrame(
    {
        "a": [3, 3, 3, 4],
        "b": [4, 12, 6, 7],
        "g": ['B', 'A', 'A', 'C']
    }
)

df.with_columns(
    pl.concat_list(['a', 'b', pl.col('a') + pl.col('b')]).alias('ab_list'),
    pl.concat_arr(['a', 'b', pl.col('a') + pl.col('b')]).alias('ab_arr'),
)
shape: (4, 5)
abgab_listab_arr
i64i64strlist[i64]array[i64, 3]
34"B"[3, 4, 7][3, 4, 7]
312"A"[3, 12, 15][3, 12, 15]
36"A"[3, 6, 9][3, 6, 9]
47"C"[4, 7, 11][4, 7, 11]

pl.concat_list()で複数の列を一つのリスト列に変換した後、.list下のメソッドを使って様々な計算を行うことができます。例えば、次のプログラムでは、この方法で次の計算を行います。

  • A, C, E列の行毎の総和。行毎の総和を計算する場合は、pl.sum_horizontal()も使えます。

  • 行毎の中間値

df_numbers = pl.DataFrame(
    np.random.randint(0, 10, size=(6, 5)), schema=list("ABCDE")
)
df_numbers.with_columns(
    pl.concat_list('A', 'C', 'E').list.sum().alias('sum_ACE'),
    pl.sum_horizontal(pl.col('A', 'C', 'E')).alias('sum_ACE2'),
    pl.concat_list('*').list.median().alias('median_all')
)
shape: (6, 8)
ABCDEsum_ACEsum_ACE2median_all
i32i32i32i32i32i32i32f64
6199823238.0
2684111114.0
5806510105.0
8157316165.0
5886922228.0
7028110102.0

reshape#

Expr.reshape()numpy.reshape()と似ています。1つの列の要素を指定した数で横向きに並べる関数です。以下の例では、reshape()を使ってデータフレームの列の形状を変更しています。

row(
    df_numbers.select(pl.col('*').reshape((-1, 3))),
    df_numbers.select(pl.col('*').reshape((-1, 2))),
)
shape: (2, 5)
ABCDE
array[i32, 3]array[i32, 3]array[i32, 3]array[i32, 3]array[i32, 3]
[6, 2, 5][1, 6, 8][9, 8, 0][9, 4, 6][8, 1, 5]
[8, 5, 7][1, 8, 0][5, 8, 2][7, 6, 8][3, 9, 1]
shape: (3, 5)
ABCDE
array[i32, 2]array[i32, 2]array[i32, 2]array[i32, 2]array[i32, 2]
[6, 2][1, 6][9, 8][9, 4][8, 1]
[5, 8][8, 1][0, 5][6, 7][5, 3]
[5, 7][8, 0][8, 2][6, 8][9, 1]

implode と explode#

Expr.implode()は、式の出力を1つのリスト列に変換します。以下のプログラムは、各列の最初の3行のデータをリストとして保存します。

df = pl.DataFrame(dict(A=[1, 2, 3, 4], B=["X", "Y", "X", "Z"]))
df.select(pl.col('*').head(3).implode())
shape: (1, 2)
AB
list[i64]list[str]
[1, 2, 3]["X", "Y", "X"]

Expr.explode()は、リスト内の要素を縦に展開します。次のプログラムでは、リスト列valueの要素を縦に並べ、group列の各要素をExpr.repeat_by()を使ってvalueの各リストの長さ回繰り返します。

  • pl.col("group").repeat_by(pl.col("value").list.lengths()) は、group列の各要素をvalue列の各リストの長さ回繰り返します。

  • pl.col("value").explode() は、value列のリストを縦に展開します。

結果として、group列の各要素は対応するvalue列の要素に合わせて繰り返されます。

df = pl.DataFrame(dict(group=["B", "A", "A", "C"], value=[[3, 4], [1, 2, 6], [1], [2, 3]]))

df.select(
    pl.col.group.repeat_by(pl.col.value.list.len()).explode(),
    pl.col.value.list.explode()
)
shape: (8, 2)
groupvalue
stri64
"B"3
"B"4
"A"1
"A"2
"A"6
"A"1
"C"2
"C"3

DataFrame.explode()は、Expr.explode()と同じ処理を行いますが、他の列のデータを自動的に繰り返します。次のプログラムでは、リスト列valueの要素を縦に並べ、他の列(group列)のデータを自動的に繰り返します。

df.explode('value')
shape: (8, 2)
groupvalue
stri64
"B"3
"B"4
"A"1
"A"2
"A"6
"A"1
"C"2
"C"3

List列とArray列同士の変換#

list.to_array()arr.to_list() を使用すると、List列とArray列同士の変換ができます。arr.to_list() はそのまま変換できますが、list.to_array() の場合、要素の長さを引数として渡す必要があり、List列の要素の長さが一致しない場合はエラーになります。

df = pl.DataFrame(
    dict(
        a=[[1, 2], [3, 4], [5, 6]],
        b=[[1, 2, 3], [3, 4, 5], [5, 6, 7]]
    ),
    schema=dict(a=pl.List(pl.Int8), b=pl.Array(pl.Int8, 3))
)

df.with_columns(
    a_to_arr=pl.col('a').list.to_array(2),
    b_to_list=pl.col('b').arr.to_list()
)
shape: (3, 4)
aba_to_arrb_to_list
list[i8]array[i8, 3]array[i8, 2]list[i8]
[1, 2][1, 2, 3][1, 2][1, 2, 3]
[3, 4][3, 4, 5][3, 4][3, 4, 5]
[5, 6][5, 6, 7][5, 6][5, 6, 7]

長さがバラバラのList列をArray列に変換するには、次の方法で list.gather() を使用してNullを埋め値として長さを揃え、その後Array列に変換します。

df = pl.DataFrame(
    dict(
        a=[[1, 2], [3, 4], [5, 6, 7], [8, 9, 10, 11]],
    ),
    schema=dict(a=pl.List(pl.Int8))
)
df.with_columns(
    a_arr=pl.col('a').list.gather(pl.int_range(end=4), null_on_oob=True).list.to_array(4)
)
shape: (4, 2)
aa_arr
list[i8]array[i8, 4]
[1, 2][1, 2, … null]
[3, 4][3, 4, … null]
[5, 6, 7][5, 6, … null]
[8, 9, … 11][8, 9, … 11]

Struct列への変換#

list.to_struct()は、リスト列の各要素を構造化データ(構造体)に変換します。このメソッドでは、リスト内の各値を個別のフィールドとして持つ構造体に変換し、フィールド数は引数n_field_strategyにn基づいて決定されます。'max_width'は、最長のリストの長さに基づいてフィールド数を設定するために使用されます。fields引数でフィールド名を指定します。

df = pl.DataFrame(dict(values=[[1, 2], [2, 3, 4], [1, 10]]))
df2 = df.select(pl.col.values.list.to_struct(n_field_strategy='max_width', fields=["a", "b"]))
row(df, df2)
print(df2.schema)
shape: (3, 1)
values
list[i64]
[1, 2]
[2, 3, 4]
[1, 10]
shape: (3, 1)
values
struct[2]
{1,2}
{2,3}
{1,10}
Schema({'values': Struct({'a': Int64, 'b': Int64})})

リスト列同士の計算#

Polarsでは、リスト列同士、またはリスト列と数値列の間で算術演算を行うことができます。

  • リスト列同士の演算: 各行において、対応するリストの要素ごとに演算が行われます。

  • リスト列と数値列の演算: 数値列の値が各リストの全要素に適用されます。

以下は具体的な例です。このデータフレームでは、x列とy列の要素がリストで、各行のリストの長さは一致しています。また、z列は数値列です。

df = pl.DataFrame(dict(
    x=[[1, 3, 3], [4, 5, 7, 8]], 
    y=[[10, 20, 30], [40, 50, 60, 80]],
    z=[10, 20]    
))

df.with_columns(
    x_plus_y = pl.col('x') + pl.col('y'),
    x_plus_z = pl.col('x') + pl.col('z'),
)
shape: (2, 5)
xyzx_plus_yx_plus_z
list[i64]list[i64]i64list[i64]list[i64]
[1, 3, 3][10, 20, 30]10[11, 23, 33][11, 13, 13]
[4, 5, … 8][40, 50, … 80]20[44, 55, … 88][24, 25, … 28]

ビットのリストから整数に変換#

リスト型の列内の各リストの要素と、単一のリストの要素を対応させて計算するために、単一のリストを持つ Series を作成するか、またはリストを直接計算する演算式を使用します。以下は、ビットのリストを整数に変換するプログラムで、この計算方法について説明します。

このコードでは、df_bits に格納されたビットのリストを整数値に変換します。ビット列 [b7, b6, ..., b0]b7 * 2**7 + b6 * 2**6 + ... + b0 * 2**0 のように計算し、対応する整数値を求めます。

❶事前に 2**i のリストを Series に変換し、それを bits の各要素と掛け算した後、.list.sum() で合計を求めます。
pl.int_range(7, -1, -1) を使って 2**i の列を生成し、それを implode() で単一のリストを持つ列に変換した後、bits と掛け算して合計を求めます。

df_bits = pl.DataFrame(dict(
    bits=[
        [0, 1, 1, 0, 1, 1, 0, 0],
        [1, 0, 0, 1, 1, 0, 0, 1],
        [1, 1, 1, 1, 1, 1, 1, 1]
    ]
))

df_val1 = df_bits.select(
    value=(pl.col('bits') * pl.Series(values=[[2**i for i in range(7, -1, -1)]])).list.sum() #❶
)

df_val2 = df_bits.select(
    value=(pl.col('bits') * (2 ** pl.int_range(7, -1, -1)).implode()).list.sum() #❷
)
with pl.Config() as conf:
    conf.set_fmt_table_cell_list_len(10)
    row(df_bits, df_val1, df_val2)
shape: (3, 1)
bits
list[i64]
[0, 1, 1, 0, 1, 1, 0, 0]
[1, 0, 0, 1, 1, 0, 0, 1]
[1, 1, 1, 1, 1, 1, 1, 1]
shape: (3, 1)
value
i64
108
153
255
shape: (3, 1)
value
i64
108
153
255

次のように、各ビットのリストの長さが異なる場合、int_ranges() を用いて各リストの長さに対応する指数のリストを作成し、ビット列を整数に変換します。

  • pl.col('bits').list.len() - 1: 各リストの長さを取得し、最上位ビットの指数を決定します。

  • pl.int_ranges(..., -1, -1): 各リストの長さに対応する2**iの指数部分のリストを作成します。

  • .list.eval(2 ** pl.element()): 各要素を2**iに変換します。

df_bits = pl.DataFrame(dict(
    bits=[
        [0, 1, 1, 0, 1, 1, 0, 0],
        [1, 0, 0, 1, 1, 0, 0],
        [1, 1, 1, 1]
    ]
))

df_val = df_bits.select(
    (
        (
            pl.col('bits') * 
            pl.int_ranges(pl.col('bits').list.len() - 1, -1, -1).list.eval(2 ** pl.element())
        )
    ).list.sum()
)
with pl.Config() as conf:
    conf.set_fmt_table_cell_list_len(10)
    row(df_bits, df_val)
shape: (3, 1)
bits
list[i64]
[0, 1, 1, 0, 1, 1, 0, 0]
[1, 0, 0, 1, 1, 0, 0]
[1, 1, 1, 1]
shape: (3, 1)
bits
i64
108
76
15

サポートされていない演算#

Polars 1.24.0時点では、リスト列間の演算として算術演算(加算、減算、乗算、除算など)はサポートされていますが、それ以外の演算(例: 比較演算や論理演算)はサポートされていません。そのため、次のコードはエラーになります。

%%capture_except
df.select(
    pl.col('x') * 10 > pl.col('y')
)
InvalidOperationError: cannot perform '>' comparison between series 'x' of dtype: list[i64] and series 'y' of dtype: list[i64]

次の方法を使えば、リスト列の要素同士の計算が可能です。

  1. with_row_index(): 各行に一意のインデックスを追加します。このインデックスは後でグループ化に使用されます。

  2. explode('x', 'y'): x列とy列のリストを展開して、各リストの要素を縦方向に並べます。これにより、リスト要素の対応が維持されます。

  3. group_by('index'): 行ごとの演算結果を元の形に戻すため、インデックス列を基準にデータをグループ化します。

  4. agg(): 各グループ内で演算を行います。ここでは、x列の要素を10倍し、y列の対応する要素と比較します。

  5. drop('index'): 最後にインデックス列を削除して不要な列を取り除きます。

(
df
.with_row_index()
.explode('x', 'y')
.group_by('index')
.agg(
    pl.col('x') * 10 > pl.col('y')
)
.drop('index')
)
shape: (2, 1)
x
list[bool]
[false, true, false]
[false, false, … false]

本書のhelper.polarsモジュールをインポートすると、list_eval()メソッドがDataFrameクラスに追加されます。このメソッドは、リスト列を含むデータフレームに対して要素ごとの演算を簡潔に実行するためのラッパーとして機能します。

import helper.polars

df.list_eval(
    (pl.col('x') * 10 > pl.col('y')).alias('flag')
)
shape: (2, 1)
flag
list[bool]
[false, true, false]
[false, false, … false]

構造体(struct)#

polarsの構造体列は、複数のフィールドを持つ複合データ型で、複数の異なる型のデータを1つの列にまとめて格納することができます。構造体列は、データを整理して、より複雑なデータ構造を表現するのに役立ちます。データフレームを作成する際に、Pythonの辞書のリストを構造体列に変換することができます。

df = pl.DataFrame({
    "person": [
        {"name": "John", "age": 30, "car": "ABC"},
        {"name": "Alice", "age": 65, "car": "VOX"},
        {"name": "Tom", "age": 25, "car": "ABC"},
        {"name": "Bob", "age": 45, "car": "KTL"},
        {"name": "Fun", "age": 18, "car": None},
    ],
    "num": [1, 2, 3, 4, 5],
})

df
shape: (5, 2)
personnum
struct[3]i64
{"John",30,"ABC"}1
{"Alice",65,"VOX"}2
{"Tom",25,"ABC"}3
{"Bob",45,"KTL"}4
{"Fun",18,null}5

フィールドと列の変換#

構造体列を処理する関数は、structネームスペースにあります。たとえば、.struct.field()メソッドを使用して、構造体列から複数のフィールドを個別の列として取り出すことができます。

df2 = df.select(
    pl.col('person').struct.field('name', 'age')
)

df2
shape: (5, 2)
nameage
stri64
"John"30
"Alice"65
"Tom"25
"Bob"45
"Fun"18

.struct.field()の逆の操作は、pl.struct()を使用することです。pl.struct()を使うことで、複数の列を構造体列に変換することができます。次のコードでは、nameageの2つの列を構造体としてまとめ、personという名前の構造体列を作成しています。

df2.select(
    pl.struct('name', 'age').alias('person')
)
shape: (5, 1)
person
struct[2]
{"John",30}
{"Alice",65}
{"Tom",25}
{"Bob",45}
{"Fun",18}

フィールドの演算式#

.struct.with_fields() メソッドを使うと、構造体列に対してフィールドの追加や変換ができます。演算式では、pl.col() の代わりに pl.field() を使ってフィールドを指定します。これにより、構造体内の特定のフィールドに対して操作を実行できます。

次の例では、person 列内の name フィールドの文字列を大文字に変換した新しい name_upper フィールドを追加し、car フィールドの null 値を "Mazda" に置き換えます。また、age フィールドと num 列の値を足し算して、新しい age フィールドを更新します。

df2 = df.select(
    pl.col("person").struct.with_fields(
        pl.field("name").str.to_uppercase().alias('name_upper'),
        pl.field("car").fill_null("Mazda"),
        pl.field("age") + pl.col("num")
    )
)
df2
shape: (5, 1)
person
struct[4]
{"John",31,"ABC","JOHN"}
{"Alice",67,"VOX","ALICE"}
{"Tom",28,"ABC","TOM"}
{"Bob",49,"KTL","BOB"}
{"Fun",23,"Mazda","FUN"}

DataFrame.unnest()は構造体列を複数の列に展開することができます。

df2.unnest('person')
shape: (5, 4)
nameagecarname_upper
stri64strstr
"John"31"ABC""JOHN"
"Alice"67"VOX""ALICE"
"Tom"28"ABC""TOM"
"Bob"49"KTL""BOB"
"Fun"23"Mazda""FUN"

出力は構造体の演算式#

一部の演算式の出力は構造体になることがあります。例えば、value_count() は:

df_car_count = df2.select(pl.col("person").struct.field("car").value_counts())
print(df_car_count.schema)
df_car_count
Schema({'car': Struct({'car': String, 'count': UInt32})})
shape: (4, 1)
car
struct[2]
{"Mazda",1}
{"KTL",1}
{"ABC",2}
{"VOX",1}

List または Array 列への変換#

次の方法で、構造体列を List 列または Array 列に変換できます。

  1. まず、struct.field('*') を使って全フィールドを選択するか、フィールド名のリストを指定して一部のフィールドを選択します。

  2. 各フィールドのデータ型が異なる場合は、必要に応じて cast() を使い、最も上位のデータ型に変換します。

  3. concat_list() または concat_arr() を使用して、1 つの List 列または Array 列に変換します。

df = pl.DataFrame(dict(
    point=[{"x":1.0, "y":2, "z":3}, {"x":4.0, "y":5, "z":6}]
))

df2 = df.with_columns(
    point_list=pl.concat_list(pl.col('point').struct.field('*').cast(pl.Float64)),
    point_list_2d=pl.concat_list(pl.col('point').struct.field('x', 'y').cast(pl.Float64)),
    point_arr=pl.concat_arr(pl.col('point').struct.field('*').cast(pl.Float64)),
)
df2
shape: (2, 4)
pointpoint_listpoint_list_2dpoint_arr
struct[3]list[f64]list[f64]array[f64, 3]
{1.0,2,3}[1.0, 2.0, 3.0][1.0, 2.0][1.0, 2.0, 3.0]
{4.0,5,6}[4.0, 5.0, 6.0][4.0, 5.0][4.0, 5.0, 6.0]

複数の列を一緒に処理#

複数の列を一緒に処理するには、構造体を利用すると便利です。たとえば、次のコードでは、AB列の値が同じ行に対して、最初の行だけを残します。

df = pl.DataFrame(
    dict(
        A = [1, 2, 1, 2, 1, 2],
        B = [2, 4, 2, 5, 2, 4],
        C = [1, 2, 3, 4, 5, 6]
    )
)

df.filter(
    pl.struct('A', 'B').is_first_distinct()
)
shape: (3, 3)
ABC
i64i64i64
121
242
254

上のコードは、DataFrame.unique()と同じ結果を得られます。

df.unique(['A', 'B'], maintain_order=True, keep='first')
shape: (3, 3)
ABC
i64i64i64
121
242
254

pl.when().then().otherwise() で同じ条件に基づいて複数の演算式を計算する際は、次のように構造体を使うことでコードを短くできます。

次のコードでは、A列の値に基づいてBC列の値を操作します。

  • A列の値は1の場合は、B列の値に10を加算し、C列の値に100を加算します。

  • A列の値は1ではない場合は、B列から10を減算し、C列から100を減算します。

pl.struct()で複数の演算式を一つの構造体に纏め、最後に.struct.field()で構造体のフィールドを列に戻ります。

df.with_columns(
    pl.when(pl.col('A') == 1)
    .then(
        pl.struct(
            pl.col('B') + 10, 
            pl.col('C') + 100
        )
    )
    .otherwise(
         pl.struct(
             pl.col('B') - 10, 
             pl.col('C') - 100
         )   
    )
    .struct.field('B', 'C')
)
shape: (6, 3)
ABC
i64i64i64
112101
2-6-98
112103
2-5-96
112105
2-6-94