表达式#

表达式(expression)是操作和转换数据框的核心要素。Polars 的表达式定义“作用于列的计算”,本身不产生结果,只有交给 select()、with_columns() 这类方法时才真正求值。正因为计算被推迟,Polars 才能把整条查询一起优化:合并扫描、裁掉无用列、复用重复计算。这一章先介绍使用表达式的几个方法,再讲表达式如何选列、如何命名、输出多长,最后汇总常用表达式。

import polars as pl
import numpy as np
from polars import selectors as cs
from helper.jupyter import row, capture_except

使用表达式的方法#

这里先介绍三个最常用的方法:select 决定输出哪些列,with_columns 决定在原表上增加什么,filter 决定保留哪些行。三者的区别只在“对结果做什么”,表达式本身的写法相同。

select#

DataFrame.select() 从数据框中选取指定列,或返回表达式计算的结果:传给 select() 的每个表达式都会成为结果中的一列。

select() 和 select_seq() 都用原数据框的数据来计算表达式,区别在于 select() 并行计算各表达式,而 select_seq() 按顺序计算。单个表达式的计算量很小时,并行的调度开销会超过收益,这时应使用 select_seq()。顺序计算还有一个附带效果:中间结果不会同时驻留,峰值内存也更低。

两者的求值对象都是输入表,这层关系不会因为并行或顺序而改变:同一个 select() 里的表达式不能引用彼此的结果,如果写了 pl.col("y"),而 y 只是同一批里的别名,就会直接报 ColumnNotFoundError。需要前后依赖时,应分两次调用 select() 或 with_columns()。

下面的例子演示两者的用法。

df = pl.DataFrame({
    "a": [1, 2, 3],
    "b": [4, 5, 6],
    "c": [7, 8, 9]
})

df1 = df.select(["a", pl.col("c") * 2])
df2 = df.select("a", pl.col("c") * 2)
df3 = df.select_seq(
    a = pl.col("a") * 2,         # "a" 乘以 2
    a_plus_c = pl.col("a") + pl.col("c") #❶
) 
row(df, df1, df2, df3)
shape: (3, 3)
abc
i64i64i64
147
258
369
shape: (3, 2)
ac
i64i64
114
216
318
shape: (3, 2)
ac
i64i64
114
216
318
shape: (3, 2)
aa_plus_c
i64i64
28
410
612

❶ 上面的例子中,select_seq() 计算 a_plus_c 列时使用的 pl.col("a") 引用原始数据的 a 列,而不是 a 列加倍后的结果。

with_columns#

DataFrame.with_columns() 的用法与 DataFrame.select() 类似,但会把新计算出的列和原有列一起返回,因此可以在已有数据框上追加新列。

下面的例子演示如何使用 with_columns() 添加列。df1 新增了 a_plus_b 和 const 两列,df2 更新了原有的 a 列,并新增了 a_plus_c 列。

df1 = df.with_columns(
    a_plus_b = pl.col("a") + pl.col("b"),  # 添加 "a" + "b" 的列
    const = 10  # 添加常数列
)

df2 = df.with_columns(
    a = pl.col("a") * 2,
    a_plus_c = pl.col("a") + pl.col("c")
)

row(df1, df2)
shape: (3, 5)
abca_plus_bconst
i64i64i64i64i32
147510
258710
369910
shape: (3, 4)
abca_plus_c
i64i64i64i64
2478
45810
66912

filter#

DataFrame.filter() 按表达式返回的布尔条件筛选行,只返回条件为 True 的行。下面是一个使用 filter() 的例子。

df1 = df.filter(pl.col("a") > 1)  # 选取 "a" > 1 的行
df2 = df.filter((pl.col("a") > 1) & (pl.col("b") < 6)) # 条件的 AND
df3 = df.filter((pl.col("a") >= 3) |  (pl.col("b") <= 4)) # 条件的 OR
df4 = df.filter(pl.col("a") > 1, pl.col("b") < 6) # 多个表达式等同于用 & 连接
row(df, df1, df2, df3, df4)
shape: (3, 3)
abc
i64i64i64
147
258
369
shape: (2, 3)
abc
i64i64i64
258
369
shape: (1, 3)
abc
i64i64i64
258
shape: (2, 3)
abc
i64i64i64
147
369
shape: (1, 3)
abc
i64i64i64
258

列的选择#

多数表达式的计算都从选择列开始,选列的方法有多种,各自适合不同的场景。

  • pl.col():按列名或数据类型选择列,最常用。

  • pl.nth():按位置选择第 N 列,只适合列顺序固定的场合。

  • pl.selectors:按更复杂的条件选择列,比如“所有整数列”“名字匹配某正则的列”。

下面分别说明。

pl.col()#

pl.col() 用于按列名或列的数据类型选择列。

  • 按列名选择: 指定列名即可选出该列,列名用字符串给出。

df = pl.DataFrame({
    "a": [1], "b": [4.0], "c": [7], "x1":[10.0], "y1":[20.0], "x2":[4], "y2":[10], "xyz":[10.0]})
selected_df = df.select(pl.col("a"))
print(selected_df.columns)
['a']
  • 按多个列名选择: 可以在列表中指定多个列名。

selected_df = df.select(pl.col(["a", "b"]))
print(selected_df.columns)
['a', 'b']
  • 按数据类型选择: 指定数据类型,即可选出该类型的所有列。

selected_df = df.select(pl.col(pl.Int64))
print(selected_df.columns)
['a', 'c', 'x2', 'y2']

pl.nth()#

pl.nth() 用于选择数据框中的第 N 列,索引从 0 开始。

  • 选择第 N 列: 指定索引,选出该索引对应的列。

selected_df = df.select(pl.nth(0))
print(selected_df.columns)
['a']
  • 按多个索引选择: 也可以在列表中指定多个索引,选出这些索引对应的列。

selected_df = df.select(pl.nth([0, 2]))
print(selected_df.columns)
['a', 'c']

pl.all()#

pl.all() 用于选择数据框中的所有列。需要整体操作数据框,或与其他选择条件组合时,它很方便。

selected_df = df.select(pl.all())
print(selected_df.columns)
['a', 'b', 'c', 'x1', 'y1', 'x2', 'y2', 'xyz']

pl.exclude()#

pl.exclude() 用于排除指定的列,可以得到去掉某些列的数据框。

  • 排除指定列:

selected_df = df.select(pl.exclude("b"))
print(selected_df.columns)
['a', 'c', 'x1', 'y1', 'x2', 'y2', 'xyz']
  • 排除多个列:

selected_df = df.select(pl.exclude(["a", "c"]))
print(selected_df.columns)
['b', 'x1', 'y1', 'x2', 'y2', 'xyz']

selectors#

polars.selectors 模块导入为 cs,它提供更高级的列选择:不必逐个列出列名,而是按“类型、模式、前缀”这类条件批量选取。

列较多,或列名会随数据变化时,用选择器比手写列名列表更可靠。下面介绍主要的函数及其用法。

按数据类型选择列#

cs.by_dtype() 按指定的数据类型选择列。

selected_df = df.select(cs.by_dtype(pl.Int64))
print(selected_df.columns)
['a', 'c', 'x2', 'y2']

cs.integer() 选出数据框中所有整数型的列。Polars 的整数型包括 Int8、Int16、Int32、Int64、UInt8、UInt16、UInt32、UInt64 等。

cs.float() 选出数据框中所有浮点型的列。Polars 的浮点型有 Float32 和 Float64。

selected_df = df.select(cs.integer())
print(selected_df.columns)
['a', 'c', 'x2', 'y2']
selected_df = df.select(cs.float())
print(selected_df.columns)
['b', 'x1', 'y1', 'xyz']

cs.matches()#

用正则表达式选择列名。Polars 2.0 的 cs.by_name() 只做精确匹配,按模式选择时应使用 cs.matches()。

# 以 x 开头的列,或以数字结尾的列
selected_df = df.select(cs.matches(r"^x|y\d$"))
print(selected_df.columns)
['x1', 'y1', 'x2', 'y2', 'xyz']

cs.starts_with() 与 cs.ends_with()#

选择具有指定前缀和后缀的列。

selected_df = df.select(cs.starts_with("x"))
print(selected_df.columns)
['x1', 'x2', 'xyz']
selected_df = df.select(cs.ends_with("1"))
print(selected_df.columns)
['x1', 'y1']

集合运算#

把上面介绍的函数组合起来,可以做集合运算:选择器之间支持 |、-、&、~ 四种运算符。

  • 并集(|):合并多个列选择,扩大选择范围。例如选取某些列与所有列的组合。

  • 差集(-):用于排除指定列。例如从所有列中去掉某些列。

  • 交集(&):选取同时满足多个条件的列。例如选取既符合某个模式、又属于某个数据类型的列。

  • 补集(~):选取不满足某个条件的列。例如选取不符合某个模式的列。

这四种运算可以嵌套组合,所以“除了这几列之外的所有数值列”这类需求也能用一个表达式写完。

下面的选择语句选取以 'x' 开头的列,再从中排除以 '2' 结尾的列。

selected_df = df.select(cs.starts_with('x') - cs.ends_with('2'))
print(selected_df.columns)
['x1', 'xyz']

下面的选择语句选取整数列或以 '1' 结尾的列。

selected_df = df.select(cs.integer() | cs.ends_with('1'))
print(selected_df.columns)
['a', 'c', 'x1', 'y1', 'x2', 'y2']

列与列之间的表达式#

下面三个表达式代表三种不同形式的列运算,区别在于“一次作用在几列上”。

  • e1:列与列的运算。A 与 B 逐元素相加,结果只有一列。

  • e2:两列与一列的运算。实际会计算 A + B 和 B + B 两个表达式,所以结果有两列。

  • e3:选择器与表达式的组合。把所有数值列与 B 相加,因此表达式数量取决于数据的列结构,写代码时无法预知会生成几列。

e1 = pl.col("A") + pl.col("B")
e2 = pl.col("A", "B") + pl.col("B")
e3 = cs.numeric() + pl.col("B")  # 选择器与表达式可以直接组合

print(e1)
print(e2)
print(e3)
[(col("A")) + (col("B"))]
[(cs.by_name('A', 'B', require_all=true)) + (col("B"))]
[(cs.numeric()) + (col("B"))]

下面的程序用 LazyFrame.explain() 显示表达式运算的内部结构。

df = pl.DataFrame(dict(A=[1, 2], B=[10, 20], C=[100, 200], X=[1, 1], Y=[2, 2]))
ldf = df.lazy()
query = ldf.with_columns(C=e1)
print(query.explain())
simple π 5/5 ["A", "B", "C", "X", "Y"]
   WITH_COLUMNS:
   [(col("A") + col("B")).alias("C")] 
    DF ["A", "B", "C", "X", ...]; PROJECT["A", "B", "X", "Y"] 4/5 COLUMNS

当存在多列选择器时,会生成多个表达式。

query = ldf.select(e2)
print(query.explain())
SELECT [(col("A") + col("B")), (col("B") + col("B"))]
  DF ["A", "B", "C", "X", ...]; PROJECT["A", "B"] 2/5 COLUMNS
query = ldf.select(e3)
print(query.explain())
SELECT [(col("A") + col("B")), (col("B") + col("B")), (col("C") + col("B")), (col("X") + col("B")), (col("Y") + col("B"))]
  DF ["A", "B", "C", "X", ...]; PROJECT */5 COLUMNS

表达式中出现多个多列选择器时,Polars 按位置一一配对:左边的第 1 列对右边的第 1 列,结果列名取左边选择器的列名。下面的代码用 pl.col('A', 'B') - pl.col('B', 'A') 计算 A - B 和 B - A,得到两列 A、B。长度不一致时,短的一边会广播,例如 pl.col('A', 'B') - pl.col('B') 用同一列 B 分别减去 A 和 B。

query = ldf.select(pl.col('A', 'B') - pl.col('B', 'A'))
print(query.explain())
SELECT [(col("A") - col("B")), (col("B") - col("A"))]
  DF ["A", "B", "C", "X", ...]; PROJECT["A", "B"] 2/5 COLUMNS

配对结果的列名取左边选择器的列名,需要改名时,alias() 只接受字符串,传入列表会抛出 TypeError。这时可以像下面这样,用 pl.struct() 把多列合成一个结构体列再处理,最后给各字段改名。这种写法在旧版本里是绕过“一个表达式只能有一个多列选择器”的办法,现在仍然适合需要自定义列名的场合。

df.with_columns(
    (pl.struct('A', 'B') - pl.struct('B', 'A'))
       .struct.rename_fields(['AB', 'BA'])
       .struct.field('*')
)
shape: (2, 7)
ABCXYABBA
i64i64i64i64i64i64i64
11010012-99
22020012-1818

表达式的输出名#

向 DataFrame 添加新列,或从 select() 取回计算结果时,通常要先给结果起一个名字,这个名字称为“表达式的输出名”。名字看起来只是列的标签,实际却参与结果表的构造:with_columns() 会将其与原表的列名比对,相同就覆盖,不同就新增。

输出名还会影响后续代码。列名是表达式引用列的依据,名字变了,后面的 pl.col() 也要跟着改;同一批表达式里出现两个同名结果时,Polars 无法判断该保留哪一个,会直接抛出 DuplicateError。

因此,输出名的规则值得先理清。下面先看默认规则,再看如何显式指定和批量变换。

根名与输出名#

不指定输出名时,Polars 取表达式树的第一个操作数作为输出名:第一个操作数是列,就用列名;是数值、字符串这类字面量,就用 literal。这个顺序按表达式树从左到右遍历,而不是按代码的书写顺序。

表达式引用到的所有列名组成“根名”(root names),可以用 .meta.root_names() 查看;.meta.output_name() 则返回当前规则下的输出名。把两者放在一起看,就能判断一个表达式最终会生成哪一列。

下面的 show_names() 把两个值一起打印出来。

def show_names(expr):
    print(f'root_names={expr.meta.root_names()}, output_name={expr.meta.output_name()}')
expr1 = pl.col("A") * pl.col("B") + 1
show_names(expr1)
root_names=['A', 'B'], output_name=A
expr2 = 1 + pl.col("B") * pl.col("A")
show_names(expr2)
root_names=['B', 'A'], output_name=literal
expr3 = (pl.col("A") * pl.col("B") + 1).alias("Z")

如果表达式最左边不是列,而是数值、字符串这类字面量,默认输出名就会变成 literal。例如 1 + pl.col("B") * pl.col("A") 的根名是 B、A,输出名却是 literal,因为表达式树的第一个操作数是常量。

literal 不是一个有意义的列名。用 .name.keep() 可以放弃默认规则,改为取根名列表的第一个,也就是 B。下面的 row() 把几种写法并排显示,便于对照。

row(df.select(expr1), df.select(expr2), df.select(expr2.name.keep()), df.select(expr3))
shape: (2, 1)
A
i64
11
41
shape: (2, 1)
literal
i64
11
41
shape: (2, 1)
B
i64
11
41
shape: (2, 1)
Z
i64
11
41

输出名必须唯一。一个表达式里出现多列选择器时,Polars 会把它拆成多个单列表达式;如果这些表达式的输出名相同,结果表就无法确定该用哪个名字,计算会抛出 DuplicateError。

下面的 10 + pl.col('A', 'B') 会分别对 A 和 B 计算,得到两列。两棵表达式树的最左边都是数值 10,按前面的规则,输出名都是 literal,于是发生冲突。这个例子说明,多列选择器与字面量组合时,不能只看结果对不对,还要看名字是否唯一。

%%capture_except
expr = 10 + pl.col('A', 'B')
df.select(expr)
DuplicateError: projections contained duplicate output name 'literal'. It's possible that multiple expressions are returning the same default column name. If this is the case, try renaming the columns with `.alias("new_name")` to avoid duplicate column names.

解决冲突有两种思路。第一种是调整书写顺序,让列位于最左边,这样每棵子树的根名不同,输出名也就不同;第二种是不改计算,直接用 .name.keep() 把输出名恢复为 root_names[0]。

下面的代码采用第二种写法。expr 本身仍是 10 + pl.col('A', 'B'),但 .name.keep() 会把两列分别命名为 A 和 B,冲突随之消失。

df.select(expr.name.keep())
shape: (2, 2)
AB
i64i64
1120
1230

.name.suffix() 和 .name.prefix() 在保留根名的基础上添加后缀或前缀,适合给一批计算结果统一标记。它们只改名字,不改计算,所以可以和其他表达式方法自由组合。

这两个方法依赖根名。如果表达式最左边是数值,默认输出名会统一成 literal,加前缀或后缀也无法区分各列。因此,使用时要把列放在最左边。

# 把列放在最左边,名字为 A_calc / B_calc
df.select((pl.col("A", "B") + 10).name.suffix("_calc"))
shape: (2, 2)
A_calcB_calc
i64i64
1120
1230

调整根名的顺序#

when().then().otherwise() 的根名顺序和书写顺序不一致,容易让人意外。Polars 收集根名时,then()、otherwise() 的分支列排在 when() 的条件列之前,所以下面的表达式根名顺序是 then()、otherwise()、when()。

例如下面的表达式,root_names 为 ['x', 'y', 'j'],输出名则是第一个根名 x。理解这个顺序,才能解释后面多列条件下为什么会出现重名。

expr = pl.when(pl.col('j') > 50).then('x').otherwise('y')
print(expr.meta.root_names())
['x', 'y', 'j']

when() 的条件里出现多列选择器时,整个条件表达式会按列展开成多份。每一份的 then() 分支都是同一列 x,输出名也都是 x;Polars 无法把多份结果同时放进一张表,于是抛出 DuplicateError。

下面的代码用 pl.col(['j', 'k', 'l']) 作为条件,恰好触发这个错误。

df2 = pl.DataFrame(dict(
    j=np.random.randint(10, 99, 2),
    k=np.random.randint(10, 99, 2),
    l=np.random.randint(10, 99, 2),
    x=10,
    y=20,
))
%%capture_except
cols = pl.col(['j', 'k', 'l'])
expr = pl.when(cols > 50).then('x').otherwise('y')
df2.select(expr)
DuplicateError: projections contained duplicate output name 'x'. It's possible that multiple expressions are returning the same default column name. If this is the case, try renaming the columns with `.alias("new_name")` to avoid duplicate column names.

解决办法是在表达式最前面加一项不影响计算结果、只影响根名顺序的表达式。例如把条件列乘以 0 再加到原表达式上:乘积恒为 0,数值不变,但根名列表会从条件列开始,三份结果的输出名就分别变成 j、k、l。

这种写法看起来像技巧,实际上只是利用了“输出名取第一个根名”的规则;只要不改变计算结果,加什么表达式都可以,关键是让各份结果的根名互不相同。

df2.select(
    cols * 0 + pl.when(cols > 50).then('x').otherwise('y')
)
shape: (2, 3)
jkl
i32i32i32
201010
101020

指定输出名#

alias()#

最直接的命名方式是 alias()。把它接在表达式末尾,传入一个字符串,输出名就被固定下来,不再受根名顺序影响。

alias() 通常写在表达式链的最后,这样读者可以直接看到最终列名。下面的代码把 A + B 命名为 A_plus_B。

result = df.select((pl.col('A') + pl.col('B')).alias("A_plus_B"))
print(result.columns)
['A_plus_B']

关键字参数#

select()、with_columns() 还支持用关键字参数命名:参数名就是输出名,参数值是表达式。这种写法比在表达式末尾接 alias() 更短,列名和计算放在同一行,读起来也更直接。

下面的 A_plus_B=... 与上一节的 alias("A_plus_B") 等价,生成的列名相同。

result = df.select(A_plus_B=pl.col('A') + pl.col('B'))
print(result.columns)
['A_plus_B']

变换输出名#

除了 alias() 和关键字参数,.name 命名空间还提供了一组方法,用于在保留根名信息的前提下批量修改输出名。它们适合“对一批列做同样的改名”这类需求,不必为每一列单独写 alias()。

下面介绍常用的三个:map、prefix 与 suffix。

map

map() 接收一个函数,对根名逐个调用,用返回值作为新的输出名。下面的代码把列名重复三次,得到 AAA 这样的结果。

result_df = df.select(
    (pl.col('A', 'B', 'C') + pl.col('B')).name.map(lambda name: name * 3)
)
print(result_df.columns)
['AAA', 'BBB', 'CCC']

prefix 与 suffix

prefix() 和 suffix() 在根列名的开头或结尾添加固定字符串。前者适合加统一前缀(如 raw_),后者适合加单位或来源标记(如 _calc)。它们只改名字,不改变列的顺序和内容。

下面的代码把 A、B、C 三列与 B 相加,再用 _res 后缀标记结果。

result_df = df.select(
    (pl.col('A', 'B', 'C') + pl.col('B')).name.suffix("_res")
)
print(result_df.columns)
['A_res', 'B_res', 'C_res']

表达式的输出值#

表达式对“一列”运算,但输出可以是一行、与原列等长,也可以是任意长度。理解这层差别,才能解释 over()、agg() 里那些看似奇怪的结果。

表达式独立执行#

用下面的程序说明 Polars 表达式独立执行的含义。

df = pl.DataFrame(
    dict(x=range(10), y=range(10))
)

result = df.select(pl.all().sample(4))
result
shape: (4, 2)
xy
i64i64
24
32
18
67

这个程序用 pl.all() 对数据框 df 的所有列应用 sample(4),从每列随机抽取 4 行。Polars 的表达式对 DataFrame 的各列独立作用,因此 pl.all().sample(4) 会为各列分别随机选取 4 行。虽然对每列执行的操作相同,但各列的执行彼此独立,选中的行不一定一致。通过 seed 参数可以让所有列的随机数使用同一个种子,这样即使各列独立执行,取出的也是同一批行。

df.select(pl.all().sample(4, seed=10))
shape: (4, 2)
xy
i64i64
11
66
00
77

另外,抽取随机行建议直接使用 DataFrame.sample()。

df.sample(4)
shape: (4, 2)
xy
i64i64
11
77
88
00

.sort() 对各个列的值独立排序。

df = pl.DataFrame(
    dict(x=[1, 3, 2], y=[9, 8, 7])
)

处理多列的表达式会先拆分为处理单列的表达式,再独立执行。下面的例子分别对 x 列和 y 列排序。

df.select(pl.col('x', 'y').sort())
shape: (3, 2)
xy
i64i64
17
28
39

要成对排序,应使用 DataFrame.sort()。

row(df.sort('x', 'y'), df.sort('y', 'x'))
shape: (3, 2)
xy
i64i64
19
27
38
shape: (3, 2)
xy
i64i64
27
38
19

在表达式中做成对排序,用 struct。

df.select(
    pl.struct('y', 'x').sort().struct.field('*')
)
shape: (3, 2)
yx
i64i64
72
83
91

或者用 sort_by() 指定排序所依据的列。各列仍然独立运算,但决定顺序的列相同,因此结果中行与行的对应关系得以保留。

df.select(
    pl.all().sort_by("y", "x")
)
shape: (3, 2)
xy
i64i64
27
38
19

表达式的输出数据量#

对长度为 N 的输入,表达式的输出长度有三种:1、N 和 X。

  • 长度 1:聚合类表达式,整列聚合为一个值。

  • 长度 N:逐元素运算,行数不变。

  • 长度 X:筛选、抽样这类操作,行数由数据决定。

同一个 select() 里混用不同长度会抛出错误,所以“聚合结果和原列放一起”时,需要使用 over(),或者先聚合再连接。

下面用例子确认这三种长度的差别。

输出长度为 1#

输出长度为 1 时,表达式对整列做聚合,返回单个结果,例如列的总和或平均值。下面的代码计算 x 列的总和,返回单个值 45。

df = pl.DataFrame({
    "x": range(5),
})

result = df.select(pl.col("x").sum())
result
shape: (1, 1)
x
i64
10

输出长度为 N#

输出长度为 N 时,表达式对每一行独立运算,返回与原数据框等长的结果,例如对每个元素做标量运算。下面的代码给 x 列每个元素加 1,输出的行数保持不变。

result = df.select(pl.col("x") + 1)
result
shape: (5, 1)
x
i64
1
2
3
4
5

输出长度为 X#

输出长度为 X 时,计算结果的行数由数据决定,可能与输入的 N 不同,抽样和筛选都属于这一类。下面的代码输出 x 列中大于等于 3 的值。

result = df.select(pl.col('x').filter(pl.col('x') >=3))
result
shape: (2, 1)
x
i64
3
4

输出长度不同时的行为#

输出长度为 1 的列,其值会被重复,扩展到与其他列相同的长度,这样各列的行数才能保持一致。

df = pl.DataFrame(
    dict(x=[1, 3, 2], y=[9, 8, 7])
)

df.select(
    x_mean = pl.col('x').mean(),
    x_plus_y = pl.col('x') + pl.col('y')
)
shape: (3, 2)
x_meanx_plus_y
f64i64
2.010
2.011
2.09

下面两个表达式的输出长度分别是 1 和 2。

df.select(
    x_mean = pl.col('x').mean(),
    x_top2 = pl.col('x').top_k(2)
)
shape: (2, 2)
x_meanx_top2
f64i64
2.02
2.03

一次操作中包含输出长度不同的多个列时,Polars 会抛出错误。

%%capture_except
df.select(
    x_sq = pl.col('x')**2,
    x_top2 = pl.col('x').top_k(2)
)
ShapeError: Series length 2 doesn't match the DataFrame height of 3

同样的计算式,输出长度也会随列的内容而变化。下面的表达式对 x 列输出长度为 2,对 y 列为 3,因此抛出错误。

%%capture_except
df.select(
    pl.col('x', 'y').filter(pl.col('x', 'y') >= 2)
)
ShapeError: Series length 2 doesn't match the DataFrame height of 3

用 Expr.implode() 把多个值转换为一个列表,所有表达式的输出长度都变成 1,可以避免报错。

df.select(
    pl.col('x', 'y').filter(pl.col('x', 'y') >= 2).implode()
)
shape: (1, 2)
xy
list[i64]list[i64]
[3, 2][9, 8, 7]

表达式的上下文#

同一个表达式,放进不同的方法里,处理的数据并不相同:在 select() 中是整列,在 group_by().agg() 中是一个分组,在 .list.eval() 中是一个列表。表达式能做什么、结果有多长,都由这层“上下文”决定。

上下文的概念不难,难的是记住每种方法提供的是什么。select() 和 with_columns() 把整列交给表达式,表达式的输出长度可以是 1、N 或 X;group_by().agg() 把每个分组的列交给表达式,每组产生一行;.list.eval() 把每个列表交给表达式,pl.element() 指列表中的元素;.struct.with_fields() 则把结构体的字段交给表达式,pl.field() 指其中一个字段。

写表达式之前先明确当前处于哪个上下文,比事后调试更省力。

后面的例子共用下面这份数据:x 列是分组键,y 列是要计算的值。先创建这个数据框,再分别放进 agg()、list.eval() 和 struct.with_fields() 中观察。

df = pl.DataFrame(dict(x=[1, 2, 1, 2, 3, 3], y=[10, 20, 30, 40, 50, 60]))

agg 上下文#

进入 .agg() 之后,表达式处理的不再是整列,而是每个分组内部的列。下面的 group_by('x') 把数据分成三组,agg() 里的每个表达式都会在三组上各算一次,最后每组输出一行。

同一段表达式在不同分组上的结果长度可能不同。y = pl.col('y') 直接返回组内的值,长度等于该组的行数;sum_y_mult_x = (...).sum() 则把组内值聚合成一个标量。前者以列表形式保存,后者是单个值。

df.group_by('x').agg(
    y = pl.col('y'),
    y_mult_x = pl.col('y') * pl.col('x'),
    sum_y_mult_x = (pl.col('y') * pl.col('x')).sum()
)
shape: (3, 4)
xyy_mult_xsum_y_mult_x
i64list[i64]list[i64]i64
3[50, 60][150, 180]330
2[20, 40][40, 80]120
1[10, 30][10, 30]40

list 上下文#

.list.eval() 的上下文是列表列的每个列表。与 agg() 不同,它不把列表聚合为一行,而是在每个列表内部逐元素计算,输出仍然是列表。

下面的程序用四种写法处理同一个列表列,便于对照:

  • 表达式❶用 list.mean() 计算每个列表的平均值,再从各列表元素中减去该平均值。

  • 表达式❷用 list.eval() 处理 x 列中的每个列表,并应用表达式❸。

  • 表达式❸中 pl.element() 表示当前处理的列表元素,从该元素中减去整个列表的平均值。

  • 表达式❹中 list.agg() 对 x 列的每个列表执行聚合表达式,结果列不再是列表型,而是标量值。这个例子计算每个列表最大值与最小值之差。

df = pl.DataFrame(dict(x=[[1, 2], [3, 4, 5, 6], [7, 8, 9]]))
df.with_columns(
    x1 = pl.col('x') - pl.col('x').list.mean(), #❶
    x2 = pl.col('x').list.eval(                 #❷
        pl.element() - pl.element().mean()      #❸
    ),
    x3 = pl.col('x').list.agg(                  #❹
        pl.element().max() - pl.element().min()
    )
)
shape: (3, 4)
xx1x2x3
list[i64]list[f64]list[f64]i64
[1, 2][-0.5, 0.5][-0.5, 0.5]1
[3, 4, … 6][-1.5, -0.5, … 1.5][-1.5, -0.5, … 1.5]3
[7, 8, 9][-1.0, 0.0, 1.0][-1.0, 0.0, 1.0]2

list.eval() 也可以只保留满足条件的元素。下面的代码从每个列表中选出大于该列表平均值的内容。

这类处理恰好属于 list.eval() 的适用范围:表达式接收的是整个列表,既可以计算列表的统计量(这里是平均值),也可以对每个元素做条件判断。换成普通的列表达式,就只能逐元素比较,拿不到“整个列表的平均值”这个信息。

  • 表达式❺中 pl.element() 表示列表内的各元素。

  • pl.element().mean() 计算当前处理的整个列表的平均值。

  • filter 方法判断每个元素是否大于列表平均值,只保留满足条件的元素。

df.with_columns(
    x_large = pl.col('x').list.eval(
        pl.element().filter(pl.element() > pl.element().mean()) #❺
    )    
)
shape: (3, 2)
xx_large
list[i64]list[i64]
[1, 2][2]
[3, 4, … 6][5, 6]
[7, 8, 9][9]

如果觉得 pl.element() 难以理解,可以把它换成具体的列表。下面的代码对一个普通 Series 做同样的过滤,pl.element() 在这里就是 s 中的每个元素。

以 [7, 8, 9] 为例,先算出平均值,再逐个比较,结果只保留 9。

s = pl.Series([7, 8, 9])
s.filter(s > s.mean()).to_list()
[9]

计算过程分三步:s.mean() 先求出列表的平均值 8.0;s > s.mean() 把每个元素与平均值比较,得到布尔数组 [False, False, True];filter() 只保留布尔值为 True 的元素,于是结果只剩 9。

field 上下文#

最后一类上下文是结构体的字段。.struct.with_fields() 允许在结构体内部按字段计算,表达式里的 pl.field(name) 表示当前结构体的 name 字段,而不是整列。

下面的 coords 列是结构体,含 x、y 两个字段;multiply 是普通列。with_fields() 里的表达式可以用 pl.field() 读取字段,也可以用 pl.col() 读取同一行上的其他列。

df = pl.DataFrame(
    {
        "coords": [{"x": 1, "y": 4}, {"x": 4, "y": 9}, {"x": 9, "y": 16}],
        "multiply": [10, 2, 3],
    }
)
df
shape: (3, 2)
coordsmultiply
struct[2]i64
{1,4}10
{4,9}2
{9,16}3

下面的表达式先算出 x² + y² 的平方根,再乘以同一行的 multiply,结果写回结构体的 dist 字段。pl.field('x') 和 pl.field('y') 只在 with_fields() 内部有效,离开这个上下文就要改用 pl.col('coords').struct.field('x')。

df.select(
    pl.col('coords').struct.with_fields(
        dist = (pl.field('x') ** 2 + pl.field('y') ** 2).sqrt() * pl.col('multiply')      
    ).struct.field('*')
)
shape: (3, 3)
xydist
i64i64f64
1441.231056
4919.697716
91655.072679

常用表达式#

这里汇总介绍常用的表达式:运算符、条件分支、取值、拼接、排序、横向运算。它们大多是一两个方法的组合,先通读一遍有个印象,用到时再回来查阅即可。

运算符#

+、-、*、/ 等运算符都有对应的表达式方法。例如下面的代码用 + 运算符和 add() 方法求 x 列与 y 列之和,用 >= 运算符和 ge() 方法做列与数值的比较。

df = pl.DataFrame(dict(
    x=[1, 2, 3, 4],
    y=[40, 30, 20, 10]
))

df.with_columns(
    add1=pl.col('x') + pl.col('y'),
    add2=pl.col('x').add(pl.col('y')),
    cond1=pl.col('x') >= 3,
    cond2=pl.col('x').ge(3)
)
shape: (4, 6)
xyadd1add2cond1cond2
i64i64i64i64boolbool
1404141falsefalse
2303232falsefalse
3202323truetrue
4101414truetrue

& 和 | 分别对应 and_ 与 or_ 方法。下面的代码用 | 运算符和 or_() 方法计算两个条件的逻辑或(OR)。

df.with_columns(
    cond1=(pl.col('x') > 3) | (pl.col('y') < 10),
    cond2=pl.col('x').gt(3).or_(pl.col('y').lt(10))
)
shape: (4, 4)
xycond1cond2
i64i64boolbool
140falsefalse
230falsefalse
320falsefalse
410truetrue

把值包装成表达式#

pl.lit() 用于把值包装成表达式。

传入 Series 或 NumPy 数组时,会被解释为含多个值的列,其长度决定结果的行数。其他数据类型都按标量处理,按结果行数自动重复同一个值。因此下面的例子会生成 3 行的 DataFrame,行数来自 Series 和 NumPy 数组的长度,而 int、str、dict 在每一行都重复同一个值。

另外,Python 的 list 和 tuple 不按“列方向的多值”处理,而是解释为“作为单个元素的列表”,也就是 list 型,因此每行都放入同一个列表。dict 则按 struct 型处理。这样 Series 和 NumPy 数组沿行方向展开,而 list 和 tuple 保留为“每行都有同一列表的列”,这个差异在下面的输出中可以看得很清楚。

pl.select(
    pl.lit(1).alias("int"),
    pl.lit("hello").alias("str"),
    pl.lit(dict(x=1, y=2)).alias("dict"),
    pl.lit([1, 2, 3]).alias("list"),
    pl.lit((1, 2, 3)).alias("tuple"),
    pl.lit(pl.Series([1, 2, 3])).alias("series"),
    pl.lit(np.array([1, 2, 3])).alias("array"),
)
shape: (3, 7)
intstrdictlisttupleseriesarray
i32strstruct[2]list[i64]list[i64]i64i64
1"hello"{1,2}[1, 2, 3][1, 2, 3]11
1"hello"{1,2}[1, 2, 3][1, 2, 3]22
1"hello"{1,2}[1, 2, 3][1, 2, 3]33

要把一个 pl.lit() 变成多列,通常有两种做法。

  • 使用字典列表:把字典列表转成结构体(struct)型的 Series,再传给 pl.lit(),然后调用 .struct.unnest() 把结构体各字段展开为独立的列。

  • 使用列表的列表:把列表的列表转成 list 型的 Series,用 .list.to_struct(fields=...) 转成结构体列,最后用 .struct.unnest() 一次生成多列。

这样经由 struct 型再调用 .struct.unnest(),即使只有一个 pl.lit() 也能高效生成多列。把外部数据或计算结果整体作为列追加时尤其方便。

pl.select(
    # 字典列表 → struct Series → 多列
    pl.lit(
        pl.Series(
            [{"x": 1, "y": 2}, {"x": 2, "y": 3}, {"x": 3, "y": 4}]
        )
    )
    .struct.unnest(),

    # 列表的列表 → list Series → struct Series → 多列
    pl.lit(
        pl.Series([[1, 2], [2, 3], [3, 4]])
    )
    .list.to_struct(fields=["a", "b"])
    .struct.unnest(),
)
shape: (3, 4)
xyab
i64i64i64i64
1212
2323
3434

数学函数#

sin、sqrt 这类一元函数用对应的方法计算,arctan2 这类二元函数则用全局函数计算。

df.with_columns(
    sin=pl.col('x').sin(),
    sqrt=pl.col('y').sqrt(),
    arctan=pl.arctan2(pl.col('y'), pl.col('x')),
)
shape: (4, 5)
xysinsqrtarctan
i64i64f64f64f64
1400.8414716.3245551.545802
2300.9092975.4772261.504228
3200.141124.4721361.421906
410-0.7568023.1622781.19029

条件分支#

条件分支以 pl.when(cond) 为起点,之后用 .then()、.when()、.otherwise() 等方法组合而成。下面这段 Python 代码表达的就是同样的条件分支。

if x > 4:
    result = 'very good'
elif x > 3:
    result = 'good'
elif x > 2:
    result = 'fair'
elif x > 1:
    result = 'bad'
else:
    result = 'very bad'
df.with_columns(
    pl.when(pl.col('x') > 4)
      .then(pl.lit('very good'))
      .when(pl.col('x') > 3)
      .then(pl.lit('good'))
      .when(pl.col('x') > 2)    
      .then(pl.lit('fair'))
      .when(pl.col('x') > 1)    
      .then(pl.lit('bad'))    
      .otherwise(pl.lit('very bad'))
      .alias('level')
)
shape: (4, 3)
xylevel
i64i64str
140"very bad"
230"bad"
320"fair"
410"good"

when-then-otherwise 只对满足条件的行计算 then 分支,不满足条件的行走 otherwise 分支。下面的代码把以 "D" 开头的字符串的后续部分转成整数。"HAB"、"HFF" 这类值不满足条件,不会进入 then 分支,所以不会触发转换错误,结果是 null。

反过来,如果条件覆盖了无法转换的行,比如把条件写成 pl.lit(True),同样的表达式就会抛出 ComputeError: strict integer parsing failed。

df_s = pl.DataFrame(dict(val=["D12", "D32", "HAB", "HFF", "D100"]))

df_s.select(
    pl.when(pl.col("val").str.starts_with("D"))
    .then(pl.col("val").str.slice(1).str.to_integer())
    .otherwise(None)
)
shape: (5, 1)
val
i64
12
32
null
null
100

传给 str.to_integer() 的 strict=False 让无法转换的值转为 null,而不是抛出错误。条件可能覆盖到无法转换的行时,用它比依赖条件本身更稳妥。

df_s1 = df_s.select(
    pl.when(pl.col("val").str.starts_with("D"))
    .then(pl.col("val").str.slice(1).str.to_integer(strict=False))
    .otherwise(None)
)

或者先用 when-then-otherwise 把不满足条件的值转为 null,再做数据转换。

df_s2 = df_s.select(
    pl.when(pl.col("val").str.starts_with("D"))
    .then("val")
    .otherwise(None)
    .str.slice(1)
    .str.to_integer()
)

row(df_s1, df_s2)
shape: (5, 1)
val
i64
12
32
null
null
100
shape: (5, 1)
val
i64
12
32
null
null
100

.cut() 是按边界值把数据分段并添加标签的表达式,Polars 2.0 中已经弃用,由 bin_intervals() 承担同样的作用。三个要点分别是:

  • breaks,即 bin_intervals() 的第 1 个参数,给出 N 个边界值。

  • labels 给出 N+1 个标签。

  • 区间的闭合方式,cut() 的 left_closed=False 表示右闭区间,对应 bin_intervals() 在默认左闭时的 right_closed=True。

下面的代码给 x 列添加标签。

  • right_closed=True,相当于 cut(left_closed=False),表示右侧闭区间:

    • x <= 1 → very bad

    • 1 < x <= 2 → bad

    • 2 < x <= 3 → fair

    • 3 < x <= 4 → good

    • 4 < x → very good

  • 默认左闭,表示左侧闭区间:

    • x < 1 → very bad

    • 1 <= x < 2 → bad

    • 2 <= x < 3 → fair

    • 3 <= x < 4 → good

    • 4 <= x → very good

labels = ["very bad", "bad", "fair", "good", "very good"]
df.with_columns(
    level_right=pl.col("x").bin_intervals([1, 2, 3, 4], labels=labels, right_closed=True),
    level_left=pl.col("x").bin_intervals([1, 2, 3, 4], labels=labels),
)
shape: (4, 4)
xylevel_rightlevel_left
i64i64enumenum
140"very bad""bad"
230"bad""fair"
320"fair""good"
410"good""very good"

.clip() 以指定的最小值和最大值为边界,把超出范围的值改为边界值。下面的例子对 y 列做不同的边界裁剪。

  • y1 列:把 y 列的值限制在 20 到 30 之间。

  • y2 列:只设下界 30,比 30 小的值被裁剪为 30。

  • y3 列:只设上界 20,比 20 大的值被裁剪为 20。

  • y4 列:上下界都用表达式给出。此时每个元素各自计算边界值,并据此裁剪。

df.with_columns(
    y1=pl.col('y').clip(20, 30),
    y2=pl.col('y').clip(lower_bound=30),
    y3=pl.col('y').clip(upper_bound=20),
    y4=pl.col('y').clip(pl.col('x') * 10, pl.col('x') * 20)
)
shape: (4, 6)
xyy1y2y3y4
i64i64i64i64i64i64
14030402020
23030302030
32020302030
41020301040

元素的替换#

.replace() 和 .replace_strict() 可以替换元素。替换前后的值可以传入两个列表,也可以传入一个字典。两者的区别如下:

  • .replace():只替换部分元素,结果保持原数据类型。

  • .replace_strict():替换所有元素,结果变为替换后的数据类型。

下面的代码中,x 列的值替换后变为字符串型(x2 列),y 列的值保持原数据类型(y2 列)。

df.with_columns(
    x2=pl.col('x').replace_strict({1:'very bad', 2:'bad', 3:'fair'}, default='unknown'),
    y2=pl.col('y').replace([10, 20], [100, 200]),
)
shape: (4, 4)
xyx2y2
i64i64stri64
140"very bad"40
230"bad"30
320"fair"200
410"unknown"100

数据的选取#

用 Polars 的表达式方法可以按条件提取和处理数据,比如取头部和尾部、按位置取、按条件筛选。

下面是常用的方法及其用途。

方法

说明

.head(n)

取前 n 行。

.tail(n)

取后 n 行。

.slice(offset, n)

从指定偏移取 n 行。

.get(index)

取指定索引的值。

.gather(indices)

按指定的索引列表取行。

.gather_every(n, offset=0)

按指定步长间隔取行。

.filter(condition)

取满足条件的行。

.top_k(k)

取值最大的 k 行。

.top_k_by(col, k)

按指定列取值最大的 k 行。

.bottom_k(k)

取值最小的 k 行。

.bottom_k_by(col, k)

按指定列取值最小的 k 行。

.sample(n)

随机取 n 行。

df_res = df.select(
    head=pl.col("x").head(2),                       # 前 2 行
    tail=pl.col("x").tail(2),                       # 后 2 行
    slice=pl.col("x").slice(1, 2),                  # 从偏移 1 起取 2 行
    get=pl.col("x").get(3),                         # 索引 3 的值
    gather=pl.col("x").gather([0, 3]),              # 索引 0 和 3 的行
    gather_every=pl.col("x").gather_every(2, offset=1), # 偏移 1、每 2 步取一行
    filter=pl.col("x").filter(pl.col("y") // 10 % 2 == 0), # 按条件筛选
    top_k=pl.col("x").top_k(2),                     # 取前 2 个
    top_k_by=pl.col("x").top_k_by(pl.col("y"), 2),  # 按 y 列取前 2 个
    bottom_k=pl.col("x").bottom_k(2),               # 取后 2 个
    bottom_k_by=pl.col("x").bottom_k_by(pl.col("y"), 2), # 按 y 列取后 2 个
    sample=pl.col("x").sample(2),                   # 随机 2 行
)
row(df, df_res)
shape: (4, 2)
xy
i64i64
140
230
320
410
shape: (2, 12)
headtailslicegetgathergather_everyfiltertop_ktop_k_bybottom_kbottom_k_bysample
i64i64i64i64i64i64i64i64i64i64i64i64
132412141144
243444332233

Polars 的许多方法不仅能接受普通值,也能接受表达式作为参数,因此参数的取值可以随数据变化。例如给 .get() 传入表达式,就能按每行的值动态取出元素。下面的代码中:

  • pl.len() 获取数据框的行数。

  • (pl.col("x") + 1) % pl.len() 给 x 列每个元素加 1,再对行数取余,作为索引。

  • .get() 用上面算出的索引取元素。

df.with_columns(
    get=pl.col('y').get((pl.col('x') + 1) % pl.len())
)
shape: (4, 3)
xyget
i64i64i64
14020
23010
32040
41030

数据的拼接#

用 concat() 或 .append() 可以把多个运算结果纵向拼接,用 .extend_constant() 则可以把常数重复追加多次。

下面的程序演示如何把 x 列与 y 列的一部分或常数拼接。

df2 = df.select(
    append=pl.col('x').append(pl.col('y').slice(1, 2)),
    concat=pl.concat([pl.col('x'), pl.col('y').slice(1, 2)]),
    extend=pl.col('x').extend_constant(99, 2),
)
row(df, df2)
shape: (4, 2)
xy
i64i64
140
230
320
410
shape: (6, 3)
appendconcatextend
i64i64i64
111
222
333
444
303099
202099

concat_list()、concat_arr() 和 struct() 可以把多个运算结果横向合并,分别生成 List 型、Array 型和 Struct 型的列。下面的程序把 x 列、y 列以及 x 与 y 的商横向合并。

  • List 型和 Array 型要求所有元素的数据类型一致。

    • concat_list() 会自动向上转型。

    • 用 concat_arr() 时需要手动转换为相同的数据类型。

  • Struct 型中各字段可以有不同的数据类型。

df3 = df.select(
    list=pl.concat_list('x', pl.col('y') / pl.col('x')),
    arr=pl.concat_arr(pl.col('x').cast(pl.Float64), pl.col('y') / pl.col('x')),
    struct=pl.struct('x', pl.col('y') / pl.col('x')),
)

row(df, df3)
shape: (4, 2)
xy
i64i64
140
230
320
410
shape: (4, 3)
listarrstruct
list[f64]array[f64, 2]struct[2]
[1.0, 40.0][1.0, 40.0]{1,40.0}
[2.0, 15.0][2.0, 15.0]{2,15.0}
[3.0, 6.666667][3.0, 6.666667]{3,6.666667}
[4.0, 2.5][4.0, 2.5]{4,2.5}

用 .explode() 可以把 List 型和 Array 型列的元素按行优先纵向拼接,.list.explode() 和 .arr.explode() 做同样的处理。

下面的程序把上面结果中的 list 列和 arr 列的元素纵向拼接。要把 Struct 型列按行优先拼接时,先用 concat_list() 把 .struct.unnest() 的结果转换为 List 型,再用 .explode() 纵向拼接。按列优先拼接则用 concat() 合并 .struct.unnest() 的结果。

df4 = df3.select(
    pl.col('list').explode(), 
    pl.col('arr').explode(),
    struct_row=pl.concat_list(pl.col('struct').struct.unnest()).explode(),
    struct_col=pl.concat([pl.col('struct').struct.unnest()])
)
row(df3, df4)
shape: (4, 3)
listarrstruct
list[f64]array[f64, 2]struct[2]
[1.0, 40.0][1.0, 40.0]{1,40.0}
[2.0, 15.0][2.0, 15.0]{2,15.0}
[3.0, 6.666667][3.0, 6.666667]{3,6.666667}
[4.0, 2.5][4.0, 2.5]{4,2.5}
shape: (8, 4)
listarrstruct_rowstruct_col
f64f64f64f64
1.01.01.01.0
40.040.040.02.0
2.02.02.03.0
15.015.015.04.0
3.03.03.040.0
6.6666676.6666676.66666715.0
4.04.04.06.666667
2.52.52.52.5

顺序调整#

顺序相关的操作(平移、反转、排序)在时序和窗口计算中很常用:shift() 构造“上一期”的值,sort() 决定聚合的先后,reverse() 常用于配合累计计算。

.shift() 把一列的值向上或向下移动 N 个位置。N 为正时向下移,为负时向上移。空出的位置用 fill_value 参数指定的值填充,默认是 NULL。要做循环移位时,先用 .append() 把开头或结尾的值拼接上,再移位。

df1 = df.with_columns(
    x_shift1=pl.col("x").shift(1),
    x_shift2=pl.col("x").shift(-2),
)

df2 = df.select(
    x_shift1=pl.col('x').tail(1).append(pl.col('x')).shift(1).tail(pl.len()),
    x_shift2=pl.col('x').append(pl.col('x').head(2)).shift(-2).head(pl.len())
)

row(df1, df2)
shape: (4, 4)
xyx_shift1x_shift2
i64i64i64i64
140null3
23014
3202null
4103null
shape: (4, 2)
x_shift1x_shift2
i64i64
43
14
21
32

.reverse() 把列的值反向排列。

df.with_columns(x_reverse=pl.col('x').reverse())
shape: (4, 3)
xyx_reverse
i64i64i64
1404
2303
3202
4101

.sort() 和 .sort_by() 按大小关系排列列的值。descending=True 时降序,默认升序(False)。

  • .sort():对指定的列按升序或降序排列。

  • .sort_by():可以指定多个作为排序依据的表达式。

下面的代码把 x 列降序排列的结果添加为 x_sort 列,把 y 列升序排列的结果添加为 y_sort 列。再以 y 列与 24 的距离为基准,把 x 和 y 列升序排列,添加为 x_sort_by 和 y_sort_by 列。

df.with_columns(
    pl.col('x').sort(descending=True).alias('x_sort'),
    pl.col('y').sort().alias('y_sort'),
    pl.all().sort_by((pl.col('y') - 24).abs()).name.suffix('_sort_by')
)
shape: (4, 6)
xyx_sorty_sortx_sort_byy_sort_by
i64i64i64i64i64i64
140410320
230320230
320230410
410140140

NULL 处理#

与 NULL 相关的表达式在 NULL 处理 中详细说明。

横向运算#

函数名中含 horizontal 的函数在多个列之间做横向运算,下面的代码列出这些函数。

for name in dir(pl):
    if 'horizontal' in name:
        print(name)
all_horizontal
any_horizontal
cum_sum_horizontal
max_horizontal
mean_horizontal
min_horizontal
sum_horizontal

下面的代码计算 tag 列以外所有列的总和,并把结果添加为新列 sum。

df = pl.DataFrame(
    {
     'tag': ['a', 'b', 'a', 'b', 'c', 'e'],        
     'A': [5, 1, 4, 5, 4, 6],
     'B': [3, 1, 1, 6, 4, 9],
     'C': [5, 6, 3, 7, 0, 9],
     'D': [0, 8, 0, 1, 0, 5],
     'E': [7, 7, 6, 2, 2, 8],
    }
)

df.with_columns(
    pl.sum_horizontal(pl.exclude("tag")).alias("sum")
)
shape: (6, 7)
tagABCDEsum
stri64i64i64i64i64i64
"a"5350720
"b"1168723
"a"4130614
"b"5671221
"c"4400210
"e"6995837

horizontal 函数不适用时,也可以用 concat_list() 把多个列转换为一个列表列,再用 list.xxx 函数计算。下面的例子计算所有列(tag 列除外)的中位数,添加为新列 median。

df.with_columns(
    pl.concat_list(pl.exclude("tag"))
    .list.median()
    .alias("median")
)
shape: (6, 7)
tagABCDEmedian
stri64i64i64i64i64f64
"a"535075.0
"b"116876.0
"a"413063.0
"b"567125.0
"c"440022.0
"e"699588.0

list.xxx 无法表达的计算可以用 list.eval() 编写。下面的代码取每行值中最大的两个值,把它们的和存入 sum_top_2。

list.eval() 中的 .sum() 把整个列表的和作为 1 个元素的列表返回,所以用 .list.first() 取出。

df.with_columns(
    pl.concat_list(pl.exclude("tag"))
    .list.eval(pl.element().sort(descending=True).head(2).sum())
    .list.first()
    .alias("sum_top_2")
)
shape: (6, 7)
tagABCDEsum_top_2
stri64i64i64i64i64i64
"a"5350712
"b"1168715
"a"4130610
"b"5671213
"c"440028
"e"6995818