データ型#
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
│ │ └──UnsignedIntegerType
│ │ ├──UInt8
│ │ ├──UInt16
│ │ ├──UInt32
│ │ └──UInt64
│ ├──FloatType
│ │ ├──Float32
│ │ └──Float64
│ └──Decimal
├──TemporalType
│ ├──Date
│ ├──Time
│ ├──Datetime
│ └──Duration
├──NestedType
│ ├──List
│ ├──Array
│ └──Struct
├──Boolean
├──String
├──Binary
├──Categorical
├──Enum
├──Object
├──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
a | b | g |
---|---|---|
i64 | i64 | str |
3 | 4 | "B" |
3 | 12 | "A" |
3 | 6 | "A" |
4 | 7 | "C" |
cast()
は、指定されたデータ型に列をキャストするために使用されます。この関数は、データの表現形式を変える際に非常に便利です。例えば、整数型の列を浮動小数点型に変換したり、文字列型の列を日付型に変換したりすることができます。
df.with_columns(
pl.col.a.cast(pl.UInt8),
pl.col.b.cast(pl.String)
)
a | b | g |
---|---|---|
u8 | str | str |
3 | "4" | "B" |
3 | "12" | "A" |
3 | "6" | "A" |
4 | "7" | "C" |
shrink_dtype()
は、データ型を必要最小限のサイズに縮小するために使用されます。例えば、Int64 型の列が実際には Int32 で十分な場合、この関数を使用してデータ型を自動的に縮小することができます。これにより、メモリ使用量を削減し、効率的なデータ管理が可能になります。
df.select(pl.all().shrink_dtype())
a | b | g |
---|---|---|
i8 | i8 | str |
3 | 4 | "B" |
3 | 12 | "A" |
3 | 6 | "A" |
4 | 7 | "C" |
データ型再定義#
cast
ではなく、NumPyの.view()
のように、データをそのままにして、別の型に切り替えたい場合は、次のプログラムのように、.to_arrow()
で一旦PyArrowの配列に変換し、その配列の.view()
メソッドでデータ型を変更します。最後にその結果をSeries
に変換します。
※ Expr.reinterpret()
とSeries.reinterpret()
もがありますが、これは整数int64とuint64間の切り替えしかできません。
データ型を変更する際に、データそのものは変更せずに型だけを切り替えたい場合があります。NumPyの .view()
メソッドのように、データの内容をそのままにして別の型に変更するには、以下の方法を使用します。
to_arrow()
を使用してPyArrowの配列に変換PyArrowの配列の
.view()
メソッドでデータ型を変更変更後の結果を
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)
|
shape: (3, 1)
|
Polarsには Expr.reinterpret()
および Series.reinterpret()
というメソッドもありますが、これらは主に int64
と uint64
の間で型を切り替えるためのもので、他のデータ型への変更には使用できません。
次のコードはarrow配列のバッファでbytesデータから整数に変換します。
import polars as pl
import numpy as np
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').map_batches(lambda s:pl.Series(np.frombuffer(s.to_arrow().buffers()[2], dtype=">i4").astype("<i4"))))
binary_column |
---|
i32 |
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()
)
lib | len_char | len_byte |
---|---|---|
str | u32 | u32 |
"pandas" | 6 | 6 |
"numpy" | 5 | 5 |
"polars" | 6 | 6 |
"図形" | 2 | 6 |
文字列の部分一致検索#
str.contains()
、str.starts_with()
、str.ends_with()
などの関数で、特定の部分文字列が含まれるか、指定された文字列で始まるか、終わるかをチェックできます。
df.filter(pl.col.lib.str.starts_with("p"))
lib | ver |
---|---|
str | str |
"pandas" | "2.0" |
"polars" | "1.2.1" |
df.filter(pl.col.ver.str.ends_with("0"))
lib | ver |
---|---|
str | str |
"pandas" | "2.0" |
"図形" | "4.0" |
df.filter(pl.col.lib.str.contains('p'))
lib | ver |
---|---|
str | str |
"pandas" | "2.0" |
"numpy" | "1.1.4" |
"polars" | "1.2.1" |
.contains()
には正規表現が使えます。次のコードは二つの点があるバージョンの行を取得します。
df.filter(pl.col.ver.str.contains("^\d+\.\d+\.\d+$"))
lib | ver |
---|---|
str | str |
"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"])
)
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)
|
shape: (3, 1)
|
文字列の置換#
.str.replace()
, .str.replace_all()
で特定の部分文字列を置換します。
str.replace()
: は最初に見つかったN個の一致のみを置換します。str.replace_all()
: はすべての一致を置換します。
df2.select(pl.col.text.str.replace_all('data', 'DATA'))
text |
---|
str |
"hello world" |
"DATA process library" |
"DATA science" |
"machine learning" |
"plot library" |
文字列の分割#
文字列を特定の区切り文字で分割できます。
df2.with_columns([
pl.col("text").str.split(" ").alias("split")
])
text | split |
---|---|
str | list[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")
])
text | split_exact |
---|---|
str | struct[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")
])
text | stripped | no_prefix | no_suffix |
---|---|---|---|
str | str | str | str |
" 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")
])
text | padded |
---|---|
str | str |
"hello world" | "---------hello world" |
"data process library" | "data process library" |
"data science" | "--------data science" |
"machine learning" | "----machine learning" |
"plot library" | "--------plot library" |
文字列の抽出#
Polarsにおける文字列操作関数のいくつか(str.extract
, str.extract_all
, str.extract_groups
, str.extract_many
)は、正規表現を使ったパターンマッチングによって文字列データから特定の部分を抽出する際に便利です。これらの関数は、使い方や返される結果が異なりますので、順番に説明します。
str.extract(pattern, group_index)
は、正規表現に基づいてマッチしたグループを返します。2つの引数は:
pattern
: 正規表現パターンです。文字列から特定の部分を抽出するために、正規表現を使ってパターンを指定します。このパターンには()
でキャプチャグループを示します。この括弧内に含まれる部分が、マッチした場合に抽出される内容です。複数のキャプチャグループを作ることも可能で、その場合、グループの番号(1, 2, 3…)が割り当てられます。group_index
: キャプチャグループのインデックスです。正規表現パターンの中でどのキャプチャグループを抽出するかを指定します。
次の例のpattern
引数には二つのキャプチャグループがあり、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"),
)
A_value | B_value |
---|---|
str | str |
"50" | "30" |
"70" | "45" |
str.extract_groups()
は、正規表現内のすべてのキャプチャグループを抽出し、一つの構造体列として返します。フィールド名指定する場合は、(?P<A_value>\d+)
のような名前付きキャプチャグループを使用します。
pattern = r'Item A: (?P<A_value>\d+), Item B: (?P<B_value>\d+)'
df.select(
pl.col("text").str.extract_groups(pattern).alias('values')
).unnest('values')
A_value | B_value |
---|---|
str | str |
"50" | "30" |
"70" | "45" |
str.extract_all()
は、正規表現に基づいてマッチしたすべての部分をリストとして返します。
df.select(
pl.col("text").str.extract_all(r'\d+').alias('values')
)
values |
---|
list[str] |
["50", "30"] |
["70", "45"] |
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")
)
matches |
---|
list[str] |
["apple", "banana"] |
["apple"] |
EnumとCategorical#
Enum型(列挙型)は、特定の固定された値の集合から選択されるデータ型です。これは、データの一貫性を確保し、誤った値が入力されるのを防ぐのに役立ちます。Enum型を使用することで、プログラム内で定義された値以外の入力を防ぐことができ、データの整合性を維持することができます。
Categorical型(カテゴリカル型)は、有限個のカテゴリーを持つデータ型で、データの重複を避け、メモリ効率を向上させるために使用されます。Categorical型は、各カテゴリに整数のインデックスを割り当てることで、メモリ使用量を削減します。
Enumは事前に作成され、要素とその順番が固定されています。一方、Categoricalは事前作成の必要がなく、要素の順番はデータ内での出現順になります。
score = pl.Enum(['A', 'B', 'C', 'D'])
df2 = df.select(
pl.col.g.cast(score).alias('g_enum'),
pl.col.g.cast(pl.Categorical).alias('g_cat')
)
df2
g_enum | g_cat |
---|---|
enum | cat |
"B" | "B" |
"A" | "A" |
"A" | "A" |
"C" | "C" |
to_physical()
を使用すると、内部で保存されている番号を取得できます。次の結果では、g_enum
の番号は score
で定義された番号であり、g_cat
の番号は元の列の値が出現する順番に基づいています。
df2.select(pl.all().to_physical())
g_enum | g_cat |
---|---|
u32 | u32 |
1 | 0 |
0 | 1 |
0 | 1 |
2 | 2 |
次のコードは、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'))
c1 | c2 | c1_code | c2_code |
---|---|---|---|
cat | cat | u32 | u32 |
"Polar" | "Panda" | 0 | 0 |
"Panda" | "Brown" | 1 | 1 |
"Brown" | "Brown" | 2 | 1 |
"Brown" | "Polar" | 2 | 2 |
"Polar" | "Polar" | 0 | 2 |
次に、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'))
c1 | c2 | c1_code | c2_code |
---|---|---|---|
cat | cat | u32 | u32 |
"Polar" | "Panda" | 0 | 1 |
"Panda" | "Brown" | 1 | 2 |
"Brown" | "Brown" | 2 | 2 |
"Brown" | "Polar" | 2 | 0 |
"Polar" | "Polar" | 0 | 0 |
DateTime#
ListとArray#
一つの要素に複数の値を保存する場合には、List
列または Array
列を使用します。Array
列では、各要素の長さが一定でなければなりませんが、List
列では要素の長さが異なっても問題ありません。Array
列と List
列の要素を処理するための式関数は、それぞれ array
および list
ネームスペース内にあります。list列のメモリレイアウト 各個リストを処理する式
次のコードは、items
という名前の列を持つDataFrame
を作成しています。この列はリストを含んでおり、各行にはリストが含まれています。list
ネームスペース内のmax()
、mean()
とlen()
を使って、items
列の各リストの最大値、平均値と要素数を計算し、新しい列として追加します。
df = pl.DataFrame(dict(items=[[1, 2], [3, 4], [5, 6, 7]]))
df.with_columns(
max=pl.col("items").list.max(),
mean=pl.col("items").list.mean(),
len=pl.col("items").list.len()
)
items | max | mean | len |
---|---|---|---|
list[i64] | i64 | f64 | u32 |
[1, 2] | 2 | 1.5 | 2 |
[3, 4] | 4 | 3.5 | 2 |
[5, 6, 7] | 7 | 6.0 | 3 |
list.get(index)
で指定されたindexの要素を取り出します。次のコードはitems
列の各個リストの最初の要素と最後の要素の足し算を計算します。
df.with_columns(
first_plus_last =
pl.col('items').list.get(0) +
pl.col('items').list.get(-1)
)
items | first_plus_last |
---|---|
list[i64] | i64 |
[1, 2] | 3 |
[3, 4] | 7 |
[5, 6, 7] | 12 |
concat_list#
pl.concat_list()
関数は、指定された列や式の結果をリストとして結合するために使用されます。この関数を使用すると、複数の列の値をリストとして1つの列にまとめることができます。
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')
)
a | b | g | ab |
---|---|---|---|
i64 | i64 | str | list[i64] |
3 | 4 | "B" | [3, 4, 7] |
3 | 12 | "A" | [3, 12, 15] |
3 | 6 | "A" | [3, 6, 9] |
4 | 7 | "C" | [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')
)
A | B | C | D | E | sum_ACE | sum_ACE2 | median_all |
---|---|---|---|---|---|---|---|
i32 | i32 | i32 | i32 | i32 | i32 | i32 | f64 |
1 | 4 | 3 | 2 | 9 | 13 | 13 | 3.0 |
9 | 9 | 2 | 7 | 3 | 14 | 14 | 7.0 |
2 | 6 | 4 | 6 | 0 | 6 | 6 | 4.0 |
2 | 1 | 1 | 7 | 5 | 8 | 8 | 2.0 |
4 | 8 | 7 | 2 | 7 | 18 | 18 | 7.0 |
2 | 9 | 9 | 2 | 1 | 12 | 12 | 2.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)
|
shape: (3, 5)
|
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())
A | B |
---|---|
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()
)
group | value |
---|---|
str | i64 |
"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')
group | value |
---|---|
str | i64 |
"B" | 3 |
"B" | 4 |
"A" | 1 |
"A" | 2 |
"A" | 6 |
"A" | 1 |
"C" | 2 |
"C" | 3 |
struct列に変換#
list.to_struct()
は、リスト列の各要素を構造化データ(構造体)に変換します。このメソッドでは、リスト内の各値を個別のフィールドとして持つ構造体に変換し、フィールド数は引数n_field_strategy
にn基づいて決定されます。'max_width'
は、最長のリストの長さに基づいてフィールド数を設定するために使用されます。
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'))
row(df, df2)
print(df2.schema)
shape: (3, 1)
|
shape: (3, 1)
|
Schema([('values', Struct({'field_0': Int64, 'field_1': Int64, 'field_2': 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'),
)
x | y | z | x_plus_y | x_plus_z |
---|---|---|---|---|
list[i64] | list[i64] | i64 | list[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] |
Polars 1.17.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]
次の方法を使えば、リスト列の要素同士の計算が可能です。
with_row_index()
各行に一意のインデックスを追加します。このインデックスは後でグループ化に使用されます。explode('x', 'y')
x
列とy
列のリストを展開して、各リストの要素を縦方向に並べます。これにより、リスト要素の対応が維持されます。group_by('index')
行ごとの演算結果を元の形に戻すため、インデックス列を基準にデータをグループ化します。agg()
各グループ内で演算を行います。ここでは、x
列の要素を10倍し、y
列の対応する要素と比較します。drop('index')
最後にインデックス列を削除して不要な列を取り除きます。
(
df
.with_row_index()
.explode('x', 'y')
.group_by('index')
.agg(
pl.col('x') * 10 > pl.col('y')
)
.drop('index')
)
x |
---|
list[bool] |
[false, true, false] |
[false, false, … false] |
本書のhelper.polars
モジュールをインポートすると、list_eval()
メソッドがDataFrame
クラスに追加されます。このメソッドは、リスト列を含むデータフレームに対して要素ごとの演算を簡潔に実行するためのラッパーとして機能します。
import helper.polars #list_evalメソッドを有効化
df.list_eval(
(pl.col('x') * 10 > pl.col('y')).alias('flag')
)
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},
]
})
df
person |
---|
struct[3] |
{"John",30,"ABC"} |
{"Alice",65,"VOX"} |
{"Tom",25,"ABC"} |
{"Bob",45,"KTL"} |
{"Fun",18,null} |
フィールドと列の変換#
構造体列を処理する関数は、struct
ネームスペースにあります。たとえば、.struct.field()
メソッドを使用して、構造体列から複数のフィールドを個別の列として取り出すことができます。
df2 = df.select(
pl.col('person').struct.field('name', 'age')
)
df2
name | age |
---|---|
str | i64 |
"John" | 30 |
"Alice" | 65 |
"Tom" | 25 |
"Bob" | 45 |
"Fun" | 18 |
.struct.field()
の逆の操作は、pl.struct()
を使用することです。pl.struct()
を使うことで、複数の列を構造体列に変換することができます。次のコードでは、name
とage
の2つの列を構造体としてまとめ、person
という名前の構造体列を作成しています。
df2.select(
pl.struct('name', 'age').alias('person')
)
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”に置き換えたます。
df2 = df.select(
pl.col("person").struct.with_fields(
pl.field("name").str.to_uppercase().alias('name_upper'),
pl.field("car").fill_null("Mazda")
)
)
df2
person |
---|
struct[4] |
{"John",30,"ABC","JOHN"} |
{"Alice",65,"VOX","ALICE"} |
{"Tom",25,"ABC","TOM"} |
{"Bob",45,"KTL","BOB"} |
{"Fun",18,"Mazda","FUN"} |
DataFrame.unnest()
は構造体列を複数の列に展開することができます。
df2.unnest('person')
name | age | car | name_upper |
---|---|---|---|
str | i64 | str | str |
"John" | 30 | "ABC" | "JOHN" |
"Alice" | 65 | "VOX" | "ALICE" |
"Tom" | 25 | "ABC" | "TOM" |
"Bob" | 45 | "KTL" | "BOB" |
"Fun" | 18 | "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}))])
car |
---|
struct[2] |
{"ABC",2} |
{"KTL",1} |
{"Mazda",1} |
{"VOX",1} |
複数の列を一緒に処理#
複数の列を一緒に処理するには、構造体を利用すると便利です。たとえば、次のコードでは、A
とB
列の値が同じ行に対して、最初の行だけを残します。
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()
)
A | B | C |
---|---|---|
i64 | i64 | i64 |
1 | 2 | 1 |
2 | 4 | 2 |
2 | 5 | 4 |
上のコードは、DataFrame.unique()
と同じ結果を得られます。
df.unique(['A', 'B'], maintain_order=True, keep='first')
A | B | C |
---|---|---|
i64 | i64 | i64 |
1 | 2 | 1 |
2 | 4 | 2 |
2 | 5 | 4 |
pl.when().then().otherwise()
で同じ条件に基づいて複数の演算式を計算する際は、次のように構造体を使うことでコードを短くできます。
次のコードでは、A
列の値に基づいてB
とC
列の値を操作します。
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')
)
A | B | C |
---|---|---|
i64 | i64 | i64 |
1 | 12 | 101 |
2 | -6 | -98 |
1 | 12 | 103 |
2 | -5 | -96 |
1 | 12 | 105 |
2 | -6 | -94 |