分类数据和枚举
一个包含只能取有限数量可能值的字符串值的列是包含分类数据的列。通常,可能值的数量远小于列的长度。一些典型的例子包括你的国籍、你计算机的操作系统,或你最喜欢的开源项目使用的许可证。
在处理分类数据时,您可以使用Polars的专用类型Categorical和Enum,以使您的查询更加高效。现在,我们将看到这两种数据类型Categorical和Enum之间的区别,以及何时应该使用其中一种数据类型。我们还在本用户指南部分的末尾包含了一些关于为什么数据类型Categorical和Enum比使用普通字符串值更高效的说明。
Enum 对比 Categorical
简而言之,只要有可能,你应该优先选择Enum而不是Categorical。当类别是固定且事先已知时,使用Enum。当你不知道类别或它们不固定时,你必须使用Categorical。如果你的需求在过程中发生变化,你总是可以从一个转换到另一个。
数据类型 Enum
创建一个Enum
数据类型 Enum 是一种有序的分类数据类型。要使用数据类型 Enum,您必须提前指定类别以创建一个新的数据类型,该数据类型是 Enum 的变体。然后,在创建新系列、新数据框或转换字符串列时,您可以使用该 Enum 变体。
import polars as pl
bears_enum = pl.Enum(["Polar", "Panda", "Brown"])
bears = pl.Series(["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=bears_enum)
print(bears)
shape: (5,)
Series: '' [enum]
[
"Polar"
"Panda"
"Brown"
"Brown"
"Polar"
]
无效值
如果你尝试指定一个数据类型 Enum,但其类别不包含所有存在的值,Polars 将会报错:
from polars.exceptions import InvalidOperationError
try:
bears_kind_of = pl.Series(
["Polar", "Panda", "Brown", "Polar", "Shark"],
dtype=bears_enum,
)
except InvalidOperationError as exc:
print("InvalidOperationError:", exc)
InvalidOperationError: conversion from `str` to `enum` failed in column '' for 1 out of 5 values: ["Shark"]
Ensure that all values in the input column are present in the categories of the enum datatype.
如果您处于无法提前知道所有可能值的情况,并且在未知值上出错在语义上是错误的,您可能需要
使用数据类型 Categorical。
类别排序和比较
数据类型 Enum 是有序的,顺序由您指定类别的顺序决定。下面的示例使用日志级别作为有序 Enum 有用的示例:
log_levels = pl.Enum(["debug", "info", "warning", "error"])
logs = pl.DataFrame(
{
"level": ["debug", "info", "debug", "error"],
"message": [
"process id: 525",
"Service started correctly",
"startup time: 67ms",
"Cannot connect to DB!",
],
},
schema_overrides={
"level": log_levels,
},
)
non_debug_logs = logs.filter(
pl.col("level") > "debug",
)
print(non_debug_logs)
shape: (2, 2)
┌───────┬───────────────────────────┐
│ level ┆ message │
│ --- ┆ --- │
│ enum ┆ str │
╞═══════╪═══════════════════════════╡
│ info ┆ Service started correctly │
│ error ┆ Cannot connect to DB! │
└───────┴───────────────────────────┘
这个例子展示了我们可以将Enum值与字符串进行比较,但这只有在字符串匹配其中一个Enum值时才有效。如果我们将列“level”与除"debug"、"info"、"warning"或"error"之外的任何字符串进行比较,Polars 将会抛出一个异常。
数据类型为Enum的列也可以与其他具有相同数据类型Enum的列或包含字符串的列进行比较,但前提是所有字符串都是有效的Enum值。
数据类型 Categorical
数据类型 Categorical 可以被视为 Enum 的一个更灵活的版本。
创建一个Categorical系列
要使用数据类型 Categorical,你可以将字符串列进行类型转换,或者将 Categorical 指定为系列或数据框列的数据类型:
bears_cat = pl.Series(
["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=pl.Categorical
)
print(bears_cat)
shape: (5,)
Series: '' [cat]
[
"Polar"
"Panda"
"Brown"
"Brown"
"Polar"
]
让Polars为你推断类别听起来可能比事先列出类别更好,但这种推断会带来性能成本。这就是为什么在可能的情况下,你应该使用Enum。你可以通过阅读关于数据类型Categorical及其编码的子章节来了解更多。
字符串的词法比较
当比较一个Categorical列与字符串时,Polars将执行词法比较:
print(bears_cat < "Cat")
shape: (5,)
Series: '' [bool]
[
false
false
true
true
false
]
您还可以将字符串列与您的Categorical列进行比较,比较也将是词汇的:
bears_str = pl.Series(
["Panda", "Brown", "Brown", "Polar", "Polar"],
)
print(bears_cat == bears_str)
shape: (5,)
Series: '' [bool]
[
false
false
true
false
true
]
尽管可以将字符串列与分类列进行比较,但通常更高效的是比较两个分类列。我们接下来将看到如何做到这一点。
比较 Categorical 列和字符串缓存
你被告知,与数据类型为Categorical的列进行比较比其中一个是字符串列更高效。因此,你更改了代码,使第二列也成为分类列,然后你进行了比较...但是Polars抛出了一个异常:
from polars.exceptions import StringCacheMismatchError
bears_cat2 = pl.Series(
["Panda", "Brown", "Brown", "Polar", "Polar"],
dtype=pl.Categorical,
)
try:
print(bears_cat == bears_cat2)
except StringCacheMismatchError as exc:
exc_str = str(exc).splitlines()[0]
print("StringCacheMismatchError:", exc_str)
StringCacheMismatchError: cannot compare categoricals coming from different sources, consider setting a global StringCache.
默认情况下,数据类型为Categorical的列中的值会按照它们在列中出现的顺序进行编码,并且独立于其他列,这意味着Polars无法高效地比较两个独立创建的分类列。
启用Polars字符串缓存并创建启用缓存的列可以解决此问题:
with pl.StringCache():
bears_cat = pl.Series(
["Polar", "Panda", "Brown", "Brown", "Polar"], dtype=pl.Categorical
)
bears_cat2 = pl.Series(
["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=pl.Categorical
)
print(bears_cat == bears_cat2)
shape: (5,)
Series: '' [bool]
[
false
false
true
false
true
]
请注意,使用全局字符串缓存会带来性能成本。
组合 Categorical 列
字符串缓存在任何以任何方式组合或混合两个数据类型为Categorical的列的操作中也很有用。一个例子是当垂直连接两个数据框时:
import warnings
from polars.exceptions import CategoricalRemappingWarning
male_bears = pl.DataFrame(
{
"species": ["Polar", "Brown", "Panda"],
"weight": [450, 500, 110], # 公斤
},
schema_overrides={"species": pl.Categorical},
)
female_bears = pl.DataFrame(
{
"species": ["Brown", "Polar", "Panda"],
"weight": [340, 200, 90], # 公斤
},
schema_overrides={"species": pl.Categorical},
)
with warnings.catch_warnings():
warnings.filterwarnings("ignore", category=CategoricalRemappingWarning)
bears = pl.concat([male_bears, female_bears], how="vertical")
print(bears)
shape: (6, 2)
┌─────────┬────────┐
│ species ┆ weight │
│ --- ┆ --- │
│ cat ┆ i64 │
╞═════════╪════════╡
│ Polar ┆ 450 │
│ Brown ┆ 500 │
│ Panda ┆ 110 │
│ Brown ┆ 340 │
│ Polar ┆ 200 │
│ Panda ┆ 90 │
└─────────┴────────┘
在这种情况下,Polars 会发出警告,抱怨昂贵的重新编码操作可能会导致性能下降。Polars 建议尽可能使用数据类型 Enum,或者使用字符串缓存。要理解此操作的问题以及为什么 Polars 会引发错误,请阅读关于
使用分类数据类型的性能考虑 的最后部分。
Categorical 列之间的比较不是按字典顺序的
当比较两个数据类型为Categorical的列时,Polars默认不会在值之间执行词汇比较。如果你想要词汇排序,你需要在创建列时指定:
with pl.StringCache():
bears_cat = pl.Series(
["Polar", "Panda", "Brown", "Brown", "Polar"],
dtype=pl.Categorical(ordering="lexical"),
)
bears_cat2 = pl.Series(
["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=pl.Categorical
)
print(bears_cat > bears_cat2)
shape: (5,)
Series: '' [bool]
[
true
true
false
false
false
]
否则,顺序将与值一起推断:
with pl.StringCache():
bears_cat = pl.Series(
# 北极熊 < 熊猫 < 棕熊
["Polar", "Panda", "Brown", "Brown", "Polar"],
dtype=pl.Categorical,
)
bears_cat2 = pl.Series(
["Panda", "Brown", "Brown", "Polar", "Polar"], dtype=pl.Categorical
)
print(bears_cat > bears_cat2)
shape: (5,)
Series: '' [bool]
[
false
false
false
true
false
]
分类数据类型的性能考虑
用户指南的这一部分解释了
- 为什么分类数据类型比字符串字面量更高效;以及
- 为什么Polars在处理数据类型
Categorical时需要一个字符串缓存。
编码
分类数据代表字符串数据,其中列中的值具有有限的值集(通常比列的长度小得多)。将这些值存储为纯字符串会浪费内存和性能,因为我们将一遍又一遍地重复相同的字符串。此外,在像连接这样的操作中,我们必须执行昂贵的字符串比较。
像Enum和Categorical这样的分类数据类型允许你以更经济的方式编码字符串值,建立经济编码值和原始字符串字面量之间的关系。
作为一个合理的编码示例,Polars 可以选择将有限的类别集表示为正整数。考虑到这一点,下图显示了一个常规的字符串列和一个可能的 Polars 列表示,该列具有分类数据类型:
| String Column | Categorical Column | ||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
在这种情况下,物理的0编码(或映射)为值'Polar',值1编码为'Panda',值2编码为'Brown'。这种编码的好处是只存储一次字符串值。此外,当我们执行操作(例如排序、计数)时,我们可以直接在物理表示上工作,这比处理字符串数据要快得多。
数据类型 Enum 的编码是全局的
在使用数据类型Enum时,我们预先指定类别。这样,Polars可以确保不同的列甚至不同的数据集具有相同的编码,并且不需要昂贵的重新编码或缓存查找。
数据类型 Categorical 和编码
数据类型Categorical的类别是推断出来的,这一事实是有代价的。这里的主要代价是我们无法控制编码。
考虑以下场景,我们将以下两个分类系列附加在一起:
Polars 按照字符串值出现的顺序对它们进行编码。因此,该系列将如下所示:
| cat_series | cat2_series | ||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
将系列组合起来成为一项非平凡的任务,因为0的物理值在两个系列中代表不同的东西。Polars确实支持这些类型的操作以便利,但由于其较慢的性能,应避免使用这些操作,因为它需要首先使两种编码兼容,然后才能进行任何合并操作。
使用全局字符串缓存
处理这种重新编码问题的一种方法是启用字符串缓存。在字符串缓存下,图表将看起来像这样:
| Series | String cache | ||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
当你启用字符串缓存时,字符串不再按照它们在每列中出现的顺序进行编码。相反,编码在列之间共享。对于在字符串缓存下创建的所有分类列,值'Polar'将始终由相同的值编码。合并操作(例如追加、连接)再次变得廉价,因为不需要首先使编码兼容,从而解决了我们上面遇到的问题。
然而,字符串缓存在构建系列时会带来一些性能上的损失,因为我们需要在缓存中查找或插入字符串值。因此,如果您提前知道您的类别,建议使用数据类型Enum。