返回博客

用表达式扩展打破规则

2024年12月5日,星期四

在我们发布的一篇社交媒体帖子中,我们向您提出了一个问题:表达式.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提供的功能以及如何以编程方式生成表达式。

1
2
4
3
5
6
7
8
9
10
11
12