在我们发布的一篇社交媒体帖子中,我们向您提出了一个问题:表达式.struct.unnest
是否违反了 Polars 最基本的原则之一,即一个表达式必须只生成一列输出?虽然本文将详细回答这个问题,但我们可以提前告诉您最终结论:最终一切都会好起来的。
TL;DR
如果您赶时间,简短的回答是.struct.unnest
使用了表达式扩展。通过使用此功能,.struct.unnest
会针对结构体中的每个字段扩展成一个表达式,这就是为什么它看起来像一个表达式生成了多个列作为输出。
问题
Polars 查询遵循一个简单规则:查询中的一个表达式在输出中只生成一个且仅一个列。
import polars as pl
df = pl.DataFrame(
{
"name": ["Anne", "Abe", "Anastasia", "Anton"],
"age": [16, 23, 62, 8],
}
)
print(
df.select(
pl.col("name").str.len_chars().alias("name_length"), # 1
(pl.col("age") > 18).alias("is_adult"), # 2
(2024 - pl.col("age")).alias("birthyear"), # 3
(pl.col("name").str.len_chars() * pl.col("age")).alias("hun?"), # 4
)
)
shape: (4, 4)
# 1 # 2 # 3 # 4
┌─────────────┬──────────┬───────────┬──────┐
│ name_length ┆ is_adult ┆ birthyear ┆ hun? │
│ --- ┆ --- ┆ --- ┆ --- │
│ u32 ┆ bool ┆ i64 ┆ i64 │
╞═════════════╪══════════╪═══════════╪══════╡
│ 4 ┆ false ┆ 2008 ┆ 64 │
│ 3 ┆ true ┆ 2001 ┆ 69 │
│ 9 ┆ true ┆ 1962 ┆ 558 │
│ 5 ┆ false ┆ 2016 ┆ 40 │
└─────────────┴──────────┴───────────┴──────┘
“一个表达式生成一列”的规则是一个设计决策,它使得推理复杂查询结果数据框的模式(schema)变得更容易。然而,当使用表达式.struct.unnest
时,它看起来像一个单一的表达式正在生成多个列。
df = pl.DataFrame(
{
"structs": [
{"a": 1, "b": 2, "c": 3},
{"a": 4, "b": 5, "c": 6},
{"a": 7, "b": 8, "c": 9},
]
}
)
print(
df.select(
pl.col("structs").struct.unnest(), # 1
)
)
shape: (3, 3)
# 1 # 2 # 3
┌─────┬─────┬─────┐
│ a ┆ b ┆ c │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞═════╪═════╪═════╡
│ 1 ┆ 2 ┆ 3 │
│ 4 ┆ 5 ┆ 6 │
│ 7 ┆ 8 ┆ 9 │
└─────┴─────┴─────┘
一个表达式必须生成一列的规则似乎与上述示例相悖,但事实并非如此。要理解原因,您需要了解表达式扩展。
表达式扩展
表达式扩展是 Polars 的一个功能,它允许您编写简短但功能强大的表达式,同时避免结构上的重复。当您想为多个列编写相同的表达式时,您可以编写一个单一的表达式,Polars 会将其扩展为多个并行表达式。
此代码片段显示了两个结构上相同的表达式
df = pl.DataFrame(
{
"first_name": ["Anne", "Abe", "Anastasia", "Anton"],
"last_name": ["Holmes", "Watson", "Kent", "Wayne"],
}
)
df.select(
pl.col("first_name").str.len_chars(), # 1.
pl.col("last_name").str.len_chars(), # 2.
)
我们可以使用带有函数pl.col
的表达式扩展来避免重复
df.select(
pl.col("first_name", "last_name").str.len_chars(),
)
在这个特殊的例子中,我们使用了一种表达式扩展类型,在这种类型中,我们事先知道 Polars 将创建多少个并行表达式。这是因为函数pl.col
明确列出了我们想要计算字符串长度的所有列。
表达式扩展也可以以这样一种方式使用:Polars 执行的并行表达式数量将取决于表达式所作用的数据框的模式。例如,考虑以下表达式
import polars.selectors as cs
plus_one = cs.integer() + 1
表达式plus_one
使用 Polars 的列选择器来引用“所有”整数列。然而,由于表达式只是计算的一种表示,表达式plus_one
需要 Polars 上下文来准确确定哪些是整数列。在此上下文中,该表达式引用了零列
df = pl.DataFrame(
{
"first_name": ["Anne", "Abe", "Anastasia", "Anton"],
"last_name": ["Holmes", "Watson", "Kent", "Wayne"],
}
)
print(df.select(plus_one))
shape: (0, 0)
┌┐
╞╡
└┘
如果回到本文中使用的第一个数据框,那么plus_one
会扩展成一个单独的列
df = pl.DataFrame(
{
"name": ["Anne", "Abe", "Anastasia", "Anton"],
"age": [16, 23, 62, 8],
}
)
print(df.select(plus_one))
shape: (4, 1)
┌─────┐
│ age │
│ --- │
│ i64 │
╞═════╡
│ 17 │
│ 24 │
│ 63 │
│ 9 │
└─────┘
而使用另一个数据框,我们可以看到plus_one
扩展成五个并行表达式
df = pl.DataFrame({"one": 1, "two": 2, "three": 3, "four": 4, "five": 5})
print(df.select(plus_one))
shape: (1, 5)
┌─────┬─────┬───────┬──────┬──────┐
│ one ┆ two ┆ three ┆ four ┆ five │
│ --- ┆ --- ┆ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 ┆ i64 ┆ i64 │
╞═════╪═════╪═══════╪══════╪══════╡
│ 2 ┆ 3 ┆ 4 ┆ 5 ┆ 6 │
└─────┴─────┴───────┴──────┴──────┘
结构体(struct)的表达式扩展
到目前为止,我们已经了解了表达式扩展如何与函数pl.col
和模块selectors
一起工作。我们只是粗略介绍了这两种工具提供的功能,但现在我们将注意力转向结构体。
结构体数据类型,您可以将其想象为类似于 Python 字典,由任意数量的命名字段组成。在此实例中,表达式value_counts
生成一个包含两个字段的结构体
df = pl.DataFrame(
{
"ballot_id": [6145, 176723, 345623, 77234, 75246],
"best_movie": ["Cars", "Toy Story", "Toy Story", "Cars", "Toy Story"],
}
)
votes = df.select(pl.col("best_movie").value_counts())
print(votes)
shape: (2, 1)
┌─────────────────┐
│ best_movie │
│ --- │
│ struct[2] │
╞═════════════════╡
│ {"Cars",2} │
│ {"Toy Story",3} │
└─────────────────┘
表达式value_counts
生成结构体作为结果,因为投票计数和电影名称需要配对在一起,但value_counts
是一个单一的表达式,因此它必须生成一个单一的列作为结果。
可以使用表达式.struct.field
访问结构体的字段,该表达式需要提取的字段名称。投票计数位于名为"count"
的字段中,因此我们可以使用.struct.field("count")
来提取它。
print(votes.select(pl.col("best_movie").struct.field("count")))
shape: (2, 1)
┌───────┐
│ count │
│ --- │
│ u32 │
╞═══════╡
│ 2 │
│ 3 │
└───────┘
然而,与函数pl.col
非常相似,表达式.struct.field
接受任意数量的字段名称进行提取
print(votes.select(pl.col("best_movie").struct.field("best_movie", "count")))
shape: (2, 2)
┌────────────┬───────┐
│ best_movie ┆ count │
│ --- ┆ --- │
│ str ┆ u32 │
╞════════════╪═══════╡
│ Cars ┆ 2 │
│ Toy Story ┆ 3 │
└────────────┴───────┘
pl.col
和.struct.field
还支持通过正则表达式模式进行表达式扩展。这种模式匹配通常支持正则表达式语法,但也支持一个特殊的通配符参数"*"
,它匹配所有名称。
df = pl.DataFrame(
{
"ballot_id": [6145, 176723, 345623, 77234, 75246],
"best_movie": ["Cars", "Toy Story", "Toy Story", "Cars", "Toy Story"],
}
)
print(df.select(pl.col("*").name.to_uppercase()))
shape: (5, 2)
┌───────────┬────────────┐
│ BALLOT_ID ┆ BEST_MOVIE │
│ --- ┆ --- │
│ i64 ┆ str │
╞═══════════╪════════════╡
│ 6145 ┆ Cars │
│ 176723 ┆ Toy Story │
│ 345623 ┆ Toy Story │
│ 77234 ┆ Cars │
│ 75246 ┆ Toy Story │
└───────────┴────────────┘
print(votes.select(pl.col("best_movie").struct.field("*")))
shape: (2, 2)
┌────────────┬───────┐
│ best_movie ┆ count │
│ --- ┆ --- │
│ str ┆ u32 │
╞════════════╪═══════╡
│ Cars ┆ 2 │
│ Toy Story ┆ 3 │
└────────────┴───────┘
这谜题的最后一块是 Polars 为pl.col("*")
和.struct.field("*")
提供了别名。Polars 更倾向于使用可读性更高的pl.all()
来代替pl.col("*")
。对于.struct.field("*")
,Polars 更倾向于使用别名.struct.unnest()
。因此,表达式.struct.unnest
看似从一个单一表达式中生成多个列的原因是,它利用了带有通配符匹配的表达式扩展功能。当应用于结构体列时,Polars 会将该表达式扩展为与字段数量相同的并行表达式,将每个字段提取到其自己的列中。
延伸阅读
表达式扩展的内容远不止本博客文章所涵盖的范围,因此我们邀请您前往用户指南阅读关于表达式扩展的部分。在那里,您将了解更多关于使用函数pl.col
进行表达式扩展、如何从模式中排除列、如何在表达式扩展中重命名列、模块selectors
提供的功能以及如何以编程方式生成表达式。