Skip to content

カテゴリカルデータ

カテゴリカルデータは、カラムの値が有限のセットの文字列データを表します(通常、カラムの長さよりはるかに小さい)。性別、国、通貨ペアリングなどのカラムを考えることができます。これらの値を単純な文字列として保存すると、同じ文字列を繰り返し保存することになり、メモリとパフォーマンスの無駄になります。さらに、結合操作の際に、コストのかかる文字列比較を行わなければなりません。

そのため、Polarsはディクショナリ形式でストリング値をエンコーディングすることをサポートしています。Polarsでカテゴリカルデータを扱うには、EnumCategoricalの2つの異なるデータ型を使用できます。それぞれに固有の使用例があり、このページでさらに詳しく説明します。 まずは、Polarsにおけるカテゴリカルの定義を見ていきましょう。

Polarsでは、カテゴリカルは、ディクショナリでエンコーディングされた文字列カラムと定義されます。文字列カラムは2つの要素に分割されます:エンコーディングされた整数値と実際の文字列値です。

文字列カラム カテゴリカルカラム
Series
Polar Bear
Panda Bear
Brown Bear
Panda Bear
Brown Bear
Brown Bear
Polar Bear
エンコーディング値
0
1
2
1
2
2
0
カテゴリ
Polar Bear
Panda Bear
Brown Bear

この場合、エンコーディング値の0は'Polar Bear'を表し、値1は'Panda Bear'、値2は'Brown Bear'を表します。このエンコーディングにより、文字列値を1回だけ保存すればよくなります。さらに、ソートやカウントなどの操作をエンコーディング値に対して直接行うことができるため、文字列データを扱うよりも高速です。

Enum vs Categorical

Polarsは、カテゴリカルデータを扱うために2つの異なるデータ型をサポートしています: EnumCategoricalです。カテゴリが事前に分かっている場合は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
Physical
0
1
2
2
0
Categories
Polar
Panda
Brown
Physical
0
1
1
2
2
Categories
Panda
Brown
Polar

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))

しかし、StringCacheSeries の構築時に、キャッシュ内で文字列の検索や挿入を行うため、若干のパフォーマンス低下を招きます。したがって、事前にカテゴリーが分かっている場合は、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
]

EnumString の比較では、語彙順ではなくカテゴリー内の順序が使用されます。比較が有効であるためには、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
]