表达式扩展

表达式扩展是一项功能, 能够编写一个可以扩展为多个不同表达式的表达式, 这可能取决于上下文

下面来展示一下如何使用

数据准备

1import polars as pl
2import polars.selectors as cs
3
4df = pl.DataFrame(
5    {
6        "name":["n1","n2","n3","n4"],
7        "v1":[1,2,3,4],
8        "v2":[5,6,7,8],
9        "v3":[1.1,2.2,3.3,4.4],
10    },
11    schema={"name":pl.String,"v1":pl.UInt16,"v2":pl.Int32,"v3":pl.Float32}
12
13)
14
15print(df)
1shape: (4, 4)
2┌──────┬─────┬─────┬─────┐
3│ name ┆ v1  ┆ v2  ┆ v3  │
4│ ---  ┆ --- ┆ --- ┆ --- │
5│ str  ┆ u16 ┆ i32 ┆ f32 │
6╞══════╪═════╪═════╪═════╡
7│ n1   ┆ 1   ┆ 5   ┆ 1.1 │
8│ n2   ┆ 2   ┆ 6   ┆ 2.2 │
9│ n3   ┆ 3   ┆ 7   ┆ 3.3 │
10│ n4   ┆ 4   ┆ 8   ┆ 4.4 │
11└──────┴─────┴─────┴─────┘

col函数

col函数用来引用(表示) DataFrame 中的一列

向col函数提供多个列名时, 会发生最简单的表达式扩展形式

按列名显式扩展

对列名为"v1"和"v2"使用扩展, 每列的值+10

单个列名无扩展

1res = df.select(
2    pl.col("v1")+10,
3    pl.col("v2")+10
4)
5
6print(res)
1shape: (4, 2)
2┌─────┬─────┐
3│ v1  ┆ v2  │
4│ --- ┆ --- │
5│ i64 ┆ i64 │
6╞═════╪═════╡
7│ 11  ┆ 15  │
8│ 12  ┆ 16  │
9│ 13  ┆ 17  │
10│ 14  ┆ 18  │
11└─────┴─────┘

传递多个列名进行扩展

1res = df.select(
2    pl.col("v1","v2")+10
3)
4
5print(res)
1shape: (4, 2)
2┌─────┬─────┐
3│ v1  ┆ v2  │
4│ --- ┆ --- │
5│ u16 ┆ i32 │
6╞═════╪═════╡
7│ 11  ┆ 15  │
8│ 12  ┆ 16  │
9│ 13  ┆ 17  │
10│ 14  ┆ 18  │
11└─────┴─────┘

按数据类型扩展

col函数传递Polars的数据类型

1# 所有数据类型为Int32的列的值+100
2res = df.select(
3    pl.col(pl.Int32)+100
4)
5print(res)
1shape: (4, 1)
2┌─────┐
3│ v2  │
4│ --- │
5│ i32 │
6╞═════╡
7│ 105 │
8│ 106 │
9│ 107 │
10│ 108 │
11└─────┘

通过模式匹配进行扩展

可以使用正则表达式来指定用于匹配列名的模式

为了区分常规列名和通过模式匹配扩展的列名, 正则表达式以^开头, 以$结尾, 正则表达式可以与常规列名混合使用

1df.select(
2    pl.col("name","^v[0-9]$")
3)

name这一列使用常规列名, v1和v2使用正则表达式进行扩展, ^v[0-9]$ 表示"v开头, 数字0-9结尾", 正好可以匹配到v1和v2

1shape: (4, 4)
2┌──────┬─────┬─────┬─────┐
3│ name ┆ v1  ┆ v2  ┆ v3  │
4│ ---  ┆ --- ┆ --- ┆ --- │
5│ str  ┆ u16 ┆ i32 ┆ f32 │
6╞══════╪═════╪═════╪═════╡
7│ n1   ┆ 1   ┆ 5   ┆ 1.1 │
8│ n2   ┆ 2   ┆ 6   ┆ 2.2 │
9│ n3   ┆ 3   ┆ 7   ┆ 3.3 │
10│ n4   ┆ 4   ┆ 8   ┆ 4.4 │
11└──────┴─────┴─────┴─────┘

参数中不能同时有列名和数据类型

选择所有列

pl.all()是pl.col("*")的语法糖, 不过all语义化更强, 所以推荐使用all

1df.select(pl.all())
1shape: (4, 4)
2┌──────┬─────┬─────┬─────┐
3│ name ┆ v1  ┆ v2  ┆ v3  │
4│ ---  ┆ --- ┆ --- ┆ --- │
5│ str  ┆ u16 ┆ i32 ┆ f32 │
6╞══════╪═════╪═════╪═════╡
7│ n1   ┆ 1   ┆ 5   ┆ 1.1 │
8│ n2   ┆ 2   ┆ 6   ┆ 2.2 │
9│ n3   ┆ 3   ┆ 7   ┆ 3.3 │
10│ n4   ┆ 4   ┆ 8   ┆ 4.4 │
11└──────┴─────┴─────┴─────┘

排除列

选择所有列, 但排除某些列

Polars提供了一种从表达式扩展中排除某些列的机制, 可以使用exclude, 接收与col完全相同类型的参数

1res = df.select(
2    pl.all().exclude("name","^v[1]$")
3)
4print(res)

选择所有列, 但排除列名"name"和列名"v1"的列

1shape: (4, 2)
2┌─────┬─────┐
3│ v2  ┆ v3  │
4│ --- ┆ --- │
5│ i32 ┆ f32 │
6╞═════╪═════╡
7│ 5   ┆ 1.1 │
8│ 6   ┆ 2.2 │
9│ 7   ┆ 3.3 │
10│ 8   ┆ 4.4 │
11└─────┴─────┘

只排除某些列

select上下文默认选择所有列, 可以使用pl.exclude()来排除某些列

1# 排除name这一列
2res = df.select(
3    pl.exclude("name")
4)
5print(res)
1shape: (4, 3)
2┌─────┬─────┬─────┐
3│ v1  ┆ v2  ┆ v3  │
4│ --- ┆ --- ┆ --- │
5│ u16 ┆ i32 ┆ f32 │
6╞═════╪═════╪═════╡
7│ 1   ┆ 5   ┆ 1.1 │
8│ 2   ┆ 6   ┆ 2.2 │
9│ 3   ┆ 7   ┆ 3.3 │
10│ 4   ┆ 8   ┆ 4.4 │
11└─────┴─────┴─────┘

对列重命名

默认情况下, 讲表达式应用于列时, 其结果列名保持不变, 这可能会导致错误, 我们来看代码

1from polars.exceptions import DuplicateError
2try:
3    res = df.select(
4        pl.col("v1")+1,
5        pl.col("v1")+2,
6    )
7except DuplicateError as err:
8    print("column name duplicate error: ", err)
1column name duplicate error:  the name 'v1' is duplicate
2
3It's possible that multiple expressions are returning the same default column name.
4
5If this is the case, try renaming the columns with `.alias("new_name")` to avoid duplicate column names.

上面的错误提示非常清晰, 提示我们v1重复了, 然后如果是这种情况, 考虑使用.alias()来重命名列, 来看下面的解决办法

使用别名重命名单个列

使用.alias()来重命名列, 但是.alias()不能应用于表达式扩展, 因为.alias()被设计用于单个列

1res = df.select(
2    (pl.col("v1")+1).alias("v1+1"),
3    (pl.col("v1")+2).alias("v1+2"),
4)
5print(res)
1shape: (4, 2)
2┌──────┬──────┐
3│ v1+1 ┆ v1+2 │
4│ ---  ┆ ---  │
5│ u16  ┆ u16  │
6╞══════╪══════╡
7│ 2    ┆ 3    │
8│ 3    ┆ 4    │
9│ 4    ┆ 5    │
10│ 5    ┆ 6    │
11└──────┴──────┘

为列名添加前缀和后缀

使用.prefix().suffix()来为列名添加前缀和后缀

1# 通过name属性的方法, 可以增加前缀或后缀
2res = df.select(
3    (pl.col("v1")+1).name.prefix("add_1_"),
4    (pl.col("v1")+2).name.suffix("_add_2"),
5)
6print(res)
1shape: (4, 2)
2┌──────────┬──────────┐
3│ add_1_v1 ┆ v1_add_2 │
4│ ---      ┆ ---      │
5│ u16      ┆ u16      │
6╞══════════╪══════════╡
7│ 2        ┆ 3        │
8│ 3        ┆ 4        │
9│ 4        ┆ 5        │
10│ 5        ┆ 6        │
11└──────────┴──────────┘

动态名称替换

如果以上都不能满足需求, 命名空间name还提供了map方法, 该方法接收一个函数, 该函数接受旧的列名返回新的列名

下面的例子展示了如何使用map方法, 我们对v1使用了自定义函数, v2使用了标准库函数str.upper

1def new_name_example(old_name:str) -> str:
2    return old_name+old_name
3
4res = df.select(
5    pl.col("v1").name.map(new_name_example),
6    pl.col("v2").name.map(str.upper)
7)
8
9print(res)
1shape: (4, 2)
2┌──────┬─────┐
3│ v1v1 ┆ V2  │
4│ ---  ┆ --- │
5│ u16  ┆ i32 │
6╞══════╪═════╡
7│ 1    ┆ 5   │
8│ 2    ┆ 6   │
9│ 3    ┆ 7   │
10│ 4    ┆ 8   │
11└──────┴─────┘

以编程的方式生成表达式(动态生成)

如果需求稍微有些复杂, 可以使用动态生成表达式的方式来实现自己的需求, 我们这里使用简单的示例进行演示

官方示例
更直观的示例
1def gen_expr(l: list[int]):
2    for x in l:
3        yield pl.col(f"v{x}")
4res = df.select(gen_expr([1,2]))
5print(res)
1shape: (4, 2)
2┌─────┬─────┐
3│ v1  ┆ v2  │
4│ --- ┆ --- │
5│ u16 ┆ i32 │
6╞═════╪═════╡
7│ 1   ┆ 5   │
8│ 2   ┆ 6   │
9│ 3   ┆ 7   │
10│ 4   ┆ 8   │
11└─────┴─────┘

更灵活的列选择

Polars附带子模块, selectors模块提供了许多函数, 允许编写更灵活的列表达式扩展

可以根据类型选择列, 也可以根据模式选择列

选择所有字符串列和模式选择

使用string()函数和ends_with()函数选择所有字符串列或者以"me"结尾的列

同理还有start_with()等实用函数

1import polars.selectors as cs
2
3res = df.select(cs.string()|cs.ends_with("me"))
4
5print(res)
1shape: (4, 1)
2┌──────┐
3│ name │
4│ ---  │
5│ str  │
6╞══════╡
7│ n1   │
8│ n2   │
9│ n3   │
10│ n4   │
11└──────┘

选择所有的数值列

使用numeric()函数选择所有的数值列, 无论是整数还是浮点数, 省去我们通过col手动指定了

1# 通过schema显式指定类型
2res = df.select(cs.numeric())
3
4print(res)
1shape: (4, 3)
2┌─────┬─────┬─────┐
3│ v1  ┆ v2  ┆ v3  │
4│ --- ┆ --- ┆ --- │
5│ u16 ┆ i32 ┆ f32 │
6╞═════╪═════╪═════╡
7│ 1   ┆ 5   ┆ 1.1 │
8│ 2   ┆ 6   ┆ 2.2 │
9│ 3   ┆ 7   ┆ 3.3 │
10│ 4   ┆ 8   ┆ 4.4 │
11└─────┴─────┴─────┘

将选择器与集合操作组合

A-B: 只在A不在B中, 集合的差集

选择所有包含"v"的列, 但是排除所有浮点数列, 结果为v1和v2列

1res = df.select(cs.contains("v")-cs.float())
2
3print(res)
1shape: (4, 2)
2┌─────┬─────┐
3│ v1  ┆ v2  │
4│ --- ┆ --- │
5│ u16 ┆ i32 │
6╞═════╪═════╡
7│ 1   ┆ 5   │
8│ 2   ┆ 6   │
9│ 3   ┆ 7   │
10│ 4   ┆ 8   │
11└─────┴─────┘

A|B: 满足表达式A或者B的列, 集合的并集

1res = df.select(cs.string() | cs.float())
2print(res)
1shape: (4, 2)
2┌──────┬─────┐
3│ name ┆ v3  │
4│ ---  ┆ --- │
5│ str  ┆ f32 │
6╞══════╪═════╡
7│ n1   ┆ 1.1 │
8│ n2   ┆ 2.2 │
9│ n3   ┆ 3.3 │
10│ n4   ┆ 4.4 │
11└──────┴─────┘

A&B: 满足表达式A和B的列, 集合的交集

选择包含"v"并且是整数的列(排除了浮点数)

1res = df.select(cs.contains("v") & cs.integer())
2print(res)
1

~A: 集合的补集

1res = df.select(~cs.integer())
2print(res)
1shape: (4, 2)
2┌──────┬─────┐
3│ name ┆ v3  │
4│ ---  ┆ --- │
5│ str  ┆ f32 │
6╞══════╪═════╡
7│ n1   ┆ 1.1 │
8│ n2   ┆ 2.2 │
9│ n3   ┆ 3.3 │
10│ n4   ┆ 4.4 │
11└──────┴─────┘

解决运算符歧义

~符号针对布尔表达式含义是取反, 针对选择器是取补集, 为了解决歧义, 可以使用方法: as_expr, 看下面官方代码

我们想把所有以"has"开头的列的值取反, 但结果是选择了所有不以"has"开头的列

1import polars as pl
2import polars.selectors as cs
3people = pl.DataFrame(
4    {
5        "name": ["Anna", "Bob"],
6        "has_partner": [True, False],
7        "has_kids": [False, False],
8        "has_tattoos": [True, False],
9        "is_alive": [True, True],
10    }
11)
12
13wrong_result = people.select((~cs.starts_with("has_")))
14print(wrong_result)
1shape: (2, 2)
2┌──────┬──────────┐
3│ name ┆ is_alive │
4│ ---  ┆ ---      │
5│ str  ┆ bool     │
6╞══════╪══════════╡
7│ Anna ┆ true     │
8│ Bob  ┆ true     │
9└──────┴──────────┘

正确的写法是使用as_expr()方法, as_expr方法将选择器转换为表达式, 然后取反, 我们看下面代码

1res = people.select((~cs.starts_with("has_").as_expr()))
2print(res)

可以细致对比一下结果, 是否按照我们的预期, 针对布尔值进行取反了

1shape: (2, 3)
2┌─────────────┬──────────┬─────────────┐
3│ has_partner ┆ has_kids ┆ has_tattoos │
4│ ---         ┆ ---      ┆ ---         │
5│ bool        ┆ bool     ┆ bool        │
6╞═════════════╪══════════╪═════════════╡
7│ false       ┆ true     ┆ false       │
8│ true        ┆ true     ┆ true        │
9└─────────────┴──────────┴─────────────┘

调试选择器

当不确定是否有一个Polars选择器时, 可以使用函数: cs.is_selector来检测

print(cs.is_selector(~cs.starts_with("has_").as_expr())) : False print(cs.is_selector(~cs.starts_with("has_"))): True, 这是一个选择器

另一个有用的调试器函数是: expand_selector, 对于给定的目标DataFrame或者选择器, 可以检查扩展为哪些列

1print(
2    cs.expand_selector(
3        people,
4        cs.starts_with("has_"),
5    )
6)

('has_partner', 'has_kids', 'has_tattoos')

完整参考

详见官方文档