カテゴリカルデータ
カテゴリカルデータは、カラムの値が有限のセットの文字列データを表します(通常、カラムの長さよりはるかに小さい)。性別、国、通貨ペアリングなどのカラムを考えることができます。これらの値を単純な文字列として保存すると、同じ文字列を繰り返し保存することになり、メモリとパフォーマンスの無駄になります。さらに、結合操作の際に、コストのかかる文字列比較を行わなければなりません。
そのため、Polarsはディクショナリ形式でストリング値をエンコーディングすることをサポートしています。Polarsでカテゴリカルデータを扱うには、Enum
とCategorical
の2つの異なるデータ型を使用できます。それぞれに固有の使用例があり、このページでさらに詳しく説明します。
まずは、Polarsにおけるカテゴリカルの定義を見ていきましょう。
Polarsでは、カテゴリカルは、ディクショナリでエンコーディングされた文字列カラムと定義されます。文字列カラムは2つの要素に分割されます:エンコーディングされた整数値と実際の文字列値です。
文字列カラム | カテゴリカルカラム | ||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
この場合、エンコーディング値の0
は'Polar Bear'を表し、値1
は'Panda Bear'、値2
は'Brown Bear'を表します。このエンコーディングにより、文字列値を1回だけ保存すればよくなります。さらに、ソートやカウントなどの操作をエンコーディング値に対して直接行うことができるため、文字列データを扱うよりも高速です。
Enum
vs Categorical
Polarsは、カテゴリカルデータを扱うために2つの異なるデータ型をサポートしています: Enum
とCategorical
です。カテゴリが事前に分かっている場合はEnum
を、カテゴリが分からないか固定されていない場合はCategorical
を使用します。要件が変わった場合は、いつでも片方から他方にキャストできます。
enum_dtype = pl.Enum(["Polar", "Panda", "Brown"])
enum_series = pl.Series(["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=enum_dtype)
cat_series = pl.Series(
["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=pl.Categorical
)
上記のコードブロックから、Enum
データ型は事前にカテゴリを要求するのに対し、Categorical
データ型はカテゴリを推論することがわかります。
Categorical
データ型
Categoricalデータ型は柔軟性があります。Polarsは新しいカテゴリを見つけるたびに追加します。これはEnum
データ型に比べて明らかに優れているように聞こえますが、推論にはコストがかかります。ここでの主なコストは、エンコーディングを制御できないことです。
次のシナリオを考えてみましょう。2つのカテゴリカルSeries
を追加する場合
cat_series = pl.Series(
["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=pl.Categorical
)
cat2_series = pl.Series(
["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=pl.Categorical
)
# Triggers a CategoricalRemappingWarning: Local categoricals have different encodings, expensive re-encoding is done
print(cat_series.append(cat2_series))
Polarsは文字列値を出現順にエンコーディングします。そのため、Series
は次のようになります:
cat_series | cat2_series | ||||||||||||||||||||||||
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
Series
の結合は、両方の Series
における物理的な値0が異なる意味を持つため、非自明で高コストなタスクとなります。Polarsは利便性のためにこの種の操作をサポートしていますが、一般的にはパフォーマンスが低下するため避けるべきです。これは、マージ操作を行う前に両方のエンコーディングを互換性のあるものにする必要があるためです。
グローバルな string cache を使う
この問題を解決する一つの方法は、StringCache
を有効にすることです。StringCache
を有効にすると、文字列は列ごとに出現順にエンコードされるのではなく、各文字列に対して単一のエンコードが保証されます。つまり、StringCache
を使用することで、文字列 Polar
は常に同じ物理的エンコードにマップされます。これにより、マージ操作(例:追加、結合)はエンコードの互換性を事前に確保する必要がなくなるため、高速になります。これにより、上記の問題が解決されます。
with pl.StringCache():
cat_series = pl.Series(
["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=pl.Categorical
)
cat2_series = pl.Series(
["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=pl.Categorical
)
print(cat_series.append(cat2_series))
しかし、StringCache
は Series
の構築時に、キャッシュ内で文字列の検索や挿入を行うため、若干のパフォーマンス低下を招きます。したがって、事前にカテゴリーが分かっている場合は、Enumデータ型を使用することが推奨されます。
Enum
データ型
Enum
データ型では、事前にカテゴリーを指定します。これにより、異なる列や異なるデータセットからのカテゴリカルデータが同じエンコードを持つことが保証され、高コストな再エンコードやキャッシュ検索が不要になります。
dtype = pl.Enum(["Polar", "Panda", "Brown"])
cat_series = pl.Series(["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=dtype)
cat2_series = pl.Series(["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=dtype)
print(cat_series.append(cat2_series))
Polarsは、Enum
で指定されていない値が見つかった場合、OutOfBounds
エラーを発生させます。
dtype = pl.Enum(["Polar", "Panda", "Brown"])
try:
cat_series = pl.Series(["Polar", "Panda", "Brown", "Black"], dtype=dtype)
except Exception as e:
print(e)
conversion from `str` to `enum` failed in column '' for 1 out of 4 values: ["Black"]
Ensure that all values in the input column are present in the categories of the enum datatype.
比較
カテゴリカルデータに対して許可されている比較演算子は次のとおりです:
- Categorical vs Categorical
- Categorical vs String
Categorical
型
Categorical
型の比較は、同じグローバルキャッシュセットを持っている場合、または同じ順序で同じ基礎カテゴリーを持っている場合に有効です。
with pl.StringCache():
cat_series = pl.Series(["Brown", "Panda", "Polar"], dtype=pl.Categorical)
cat_series2 = pl.Series(["Polar", "Panda", "Black"], dtype=pl.Categorical)
print(cat_series == cat_series2)
shape: (3,)
Series: '' [bool]
[
false
true
false
]
CategoricalとStringの比較では、Polarsは語彙順を使用して結果を決定します:
cat_series = pl.Series(["Brown", "Panda", "Polar"], dtype=pl.Categorical)
print(cat_series <= "Cat")
shape: (3,)
Series: '' [bool]
[
true
false
false
]
cat_series = pl.Series(["Brown", "Panda", "Polar"], dtype=pl.Categorical)
cat_series_utf = pl.Series(["Panda", "Panda", "Polar"])
print(cat_series <= cat_series_utf)
shape: (3,)
Series: '' [bool]
[
true
true
true
]
Enum
型
Enum
型の比較は、同じカテゴリーを持っている場合に有効です。
dtype = pl.Enum(["Polar", "Panda", "Brown"])
cat_series = pl.Series(["Brown", "Panda", "Polar"], dtype=dtype)
cat_series2 = pl.Series(["Polar", "Panda", "Brown"], dtype=dtype)
print(cat_series == cat_series2)
shape: (3,)
Series: '' [bool]
[
false
true
false
]
Enum
と String
の比較では、語彙順ではなくカテゴリー内の順序が使用されます。比較が有効であるためには、String
列のすべての値が Enum
のカテゴリーリストに含まれている必要があります。
try:
cat_series = pl.Series(
["Low", "Medium", "High"], dtype=pl.Enum(["Low", "Medium", "High"])
)
cat_series <= "Excellent"
except Exception as e:
print(e)
conversion from `str` to `enum` failed in column '' for 1 out of 1 values: ["Excellent"]
Ensure that all values in the input column are present in the categories of the enum datatype.
dtype = pl.Enum(["Low", "Medium", "High"])
cat_series = pl.Series(["Low", "Medium", "High"], dtype=dtype)
print(cat_series <= "Medium")
shape: (3,)
Series: '' [bool]
[
true
true
false
]
dtype = pl.Enum(["Low", "Medium", "High"])
cat_series = pl.Series(["Low", "Medium", "High"], dtype=dtype)
cat_series2 = pl.Series(["High", "High", "Low"], dtype=dtype)
print(cat_series <= cat_series2)
shape: (3,)
Series: '' [bool]
[
true
true
false
]