表达式#
表达式(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)
|
shape: (3, 2)
|
shape: (3, 2)
|
shape: (3, 2)
|
❶ 上面的例子中,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)
|
shape: (3, 4)
|
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)
|
shape: (2, 3)
|
shape: (1, 3)
|
shape: (2, 3)
|
shape: (1, 3)
|
列的选择#
多数表达式的计算都从选择列开始,选列的方法有多种,各自适合不同的场景。
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('*')
)
| A | B | C | X | Y | AB | BA |
|---|---|---|---|---|---|---|
| i64 | i64 | i64 | i64 | i64 | i64 | i64 |
| 1 | 10 | 100 | 1 | 2 | -9 | 9 |
| 2 | 20 | 200 | 1 | 2 | -18 | 18 |
表达式的输出名#
向 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)
|
shape: (2, 1)
|
shape: (2, 1)
|
shape: (2, 1)
|
输出名必须唯一。一个表达式里出现多列选择器时,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())
| A | B |
|---|---|
| i64 | i64 |
| 11 | 20 |
| 12 | 30 |
.name.suffix() 和 .name.prefix() 在保留根名的基础上添加后缀或前缀,适合给一批计算结果统一标记。它们只改名字,不改计算,所以可以和其他表达式方法自由组合。
这两个方法依赖根名。如果表达式最左边是数值,默认输出名会统一成 literal,加前缀或后缀也无法区分各列。因此,使用时要把列放在最左边。
# 把列放在最左边,名字为 A_calc / B_calc
df.select((pl.col("A", "B") + 10).name.suffix("_calc"))
| A_calc | B_calc |
|---|---|
| i64 | i64 |
| 11 | 20 |
| 12 | 30 |
调整根名的顺序#
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')
)
| j | k | l |
|---|---|---|
| i32 | i32 | i32 |
| 20 | 10 | 10 |
| 10 | 10 | 20 |
指定输出名#
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
| x | y |
|---|---|
| i64 | i64 |
| 2 | 4 |
| 3 | 2 |
| 1 | 8 |
| 6 | 7 |
这个程序用 pl.all() 对数据框 df 的所有列应用 sample(4),从每列随机抽取 4 行。Polars 的表达式对 DataFrame 的各列独立作用,因此 pl.all().sample(4) 会为各列分别随机选取 4 行。虽然对每列执行的操作相同,但各列的执行彼此独立,选中的行不一定一致。通过 seed 参数可以让所有列的随机数使用同一个种子,这样即使各列独立执行,取出的也是同一批行。
df.select(pl.all().sample(4, seed=10))
| x | y |
|---|---|
| i64 | i64 |
| 1 | 1 |
| 6 | 6 |
| 0 | 0 |
| 7 | 7 |
另外,抽取随机行建议直接使用 DataFrame.sample()。
df.sample(4)
| x | y |
|---|---|
| i64 | i64 |
| 1 | 1 |
| 7 | 7 |
| 8 | 8 |
| 0 | 0 |
.sort() 对各个列的值独立排序。
df = pl.DataFrame(
dict(x=[1, 3, 2], y=[9, 8, 7])
)
处理多列的表达式会先拆分为处理单列的表达式,再独立执行。下面的例子分别对 x 列和 y 列排序。
df.select(pl.col('x', 'y').sort())
| x | y |
|---|---|
| i64 | i64 |
| 1 | 7 |
| 2 | 8 |
| 3 | 9 |
要成对排序,应使用 DataFrame.sort()。
row(df.sort('x', 'y'), df.sort('y', 'x'))
shape: (3, 2)
|
shape: (3, 2)
|
在表达式中做成对排序,用 struct。
df.select(
pl.struct('y', 'x').sort().struct.field('*')
)
| y | x |
|---|---|
| i64 | i64 |
| 7 | 2 |
| 8 | 3 |
| 9 | 1 |
或者用 sort_by() 指定排序所依据的列。各列仍然独立运算,但决定顺序的列相同,因此结果中行与行的对应关系得以保留。
df.select(
pl.all().sort_by("y", "x")
)
| x | y |
|---|---|
| i64 | i64 |
| 2 | 7 |
| 3 | 8 |
| 1 | 9 |
表达式的输出数据量#
对长度为 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
| x |
|---|
| i64 |
| 10 |
输出长度为 N#
输出长度为 N 时,表达式对每一行独立运算,返回与原数据框等长的结果,例如对每个元素做标量运算。下面的代码给 x 列每个元素加 1,输出的行数保持不变。
result = df.select(pl.col("x") + 1)
result
| x |
|---|
| i64 |
| 1 |
| 2 |
| 3 |
| 4 |
| 5 |
输出长度为 X#
输出长度为 X 时,计算结果的行数由数据决定,可能与输入的 N 不同,抽样和筛选都属于这一类。下面的代码输出 x 列中大于等于 3 的值。
result = df.select(pl.col('x').filter(pl.col('x') >=3))
result
| 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')
)
| x_mean | x_plus_y |
|---|---|
| f64 | i64 |
| 2.0 | 10 |
| 2.0 | 11 |
| 2.0 | 9 |
下面两个表达式的输出长度分别是 1 和 2。
df.select(
x_mean = pl.col('x').mean(),
x_top2 = pl.col('x').top_k(2)
)
| x_mean | x_top2 |
|---|---|
| f64 | i64 |
| 2.0 | 2 |
| 2.0 | 3 |
一次操作中包含输出长度不同的多个列时,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()
)
| x | y |
|---|---|
| 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()
)
| x | y | y_mult_x | sum_y_mult_x |
|---|---|---|---|
| i64 | list[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()
)
)
| x | x1 | x2 | x3 |
|---|---|---|---|
| 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()) #❺
)
)
| x | x_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
| coords | multiply |
|---|---|
| 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('*')
)
| x | y | dist |
|---|---|---|
| i64 | i64 | f64 |
| 1 | 4 | 41.231056 |
| 4 | 9 | 19.697716 |
| 9 | 16 | 55.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)
)
| x | y | add1 | add2 | cond1 | cond2 |
|---|---|---|---|---|---|
| i64 | i64 | i64 | i64 | bool | bool |
| 1 | 40 | 41 | 41 | false | false |
| 2 | 30 | 32 | 32 | false | false |
| 3 | 20 | 23 | 23 | true | true |
| 4 | 10 | 14 | 14 | true | true |
& 和 | 分别对应 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))
)
| x | y | cond1 | cond2 |
|---|---|---|---|
| i64 | i64 | bool | bool |
| 1 | 40 | false | false |
| 2 | 30 | false | false |
| 3 | 20 | false | false |
| 4 | 10 | true | true |
把值包装成表达式#
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"),
)
| int | str | dict | list | tuple | series | array |
|---|---|---|---|---|---|---|
| i32 | str | struct[2] | list[i64] | list[i64] | i64 | i64 |
| 1 | "hello" | {1,2} | [1, 2, 3] | [1, 2, 3] | 1 | 1 |
| 1 | "hello" | {1,2} | [1, 2, 3] | [1, 2, 3] | 2 | 2 |
| 1 | "hello" | {1,2} | [1, 2, 3] | [1, 2, 3] | 3 | 3 |
要把一个 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(),
)
| x | y | a | b |
|---|---|---|---|
| i64 | i64 | i64 | i64 |
| 1 | 2 | 1 | 2 |
| 2 | 3 | 2 | 3 |
| 3 | 4 | 3 | 4 |
数学函数#
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')),
)
| x | y | sin | sqrt | arctan |
|---|---|---|---|---|
| i64 | i64 | f64 | f64 | f64 |
| 1 | 40 | 0.841471 | 6.324555 | 1.545802 |
| 2 | 30 | 0.909297 | 5.477226 | 1.504228 |
| 3 | 20 | 0.14112 | 4.472136 | 1.421906 |
| 4 | 10 | -0.756802 | 3.162278 | 1.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')
)
| x | y | level |
|---|---|---|
| i64 | i64 | str |
| 1 | 40 | "very bad" |
| 2 | 30 | "bad" |
| 3 | 20 | "fair" |
| 4 | 10 | "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)
)
| 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)
|
shape: (5, 1)
|
.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 bad1 < x <= 2→ bad2 < x <= 3→ fair3 < x <= 4→ good4 < x→ very good
默认左闭,表示左侧闭区间:
x < 1→ very bad1 <= x < 2→ bad2 <= x < 3→ fair3 <= x < 4→ good4 <= 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),
)
| x | y | level_right | level_left |
|---|---|---|---|
| i64 | i64 | enum | enum |
| 1 | 40 | "very bad" | "bad" |
| 2 | 30 | "bad" | "fair" |
| 3 | 20 | "fair" | "good" |
| 4 | 10 | "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)
)
| x | y | y1 | y2 | y3 | y4 |
|---|---|---|---|---|---|
| i64 | i64 | i64 | i64 | i64 | i64 |
| 1 | 40 | 30 | 40 | 20 | 20 |
| 2 | 30 | 30 | 30 | 20 | 30 |
| 3 | 20 | 20 | 30 | 20 | 30 |
| 4 | 10 | 20 | 30 | 10 | 40 |
元素的替换#
.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]),
)
| x | y | x2 | y2 |
|---|---|---|---|
| i64 | i64 | str | i64 |
| 1 | 40 | "very bad" | 40 |
| 2 | 30 | "bad" | 30 |
| 3 | 20 | "fair" | 200 |
| 4 | 10 | "unknown" | 100 |
数据的选取#
用 Polars 的表达式方法可以按条件提取和处理数据,比如取头部和尾部、按位置取、按条件筛选。
下面是常用的方法及其用途。
方法 |
说明 |
|---|---|
|
取前 |
|
取后 |
|
从指定偏移取 |
|
取指定索引的值。 |
|
按指定的索引列表取行。 |
|
按指定步长间隔取行。 |
|
取满足条件的行。 |
|
取值最大的 |
|
按指定列取值最大的 |
|
取值最小的 |
|
按指定列取值最小的 |
|
随机取 |
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)
|
shape: (2, 12)
|
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())
)
| x | y | get |
|---|---|---|
| i64 | i64 | i64 |
| 1 | 40 | 20 |
| 2 | 30 | 10 |
| 3 | 20 | 40 |
| 4 | 10 | 30 |
数据的拼接#
用 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)
|
shape: (6, 3)
|
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)
|
shape: (4, 3)
|
用 .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)
|
shape: (8, 4)
|
顺序调整#
顺序相关的操作(平移、反转、排序)在时序和窗口计算中很常用: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)
|
shape: (4, 2)
|
.reverse() 把列的值反向排列。
df.with_columns(x_reverse=pl.col('x').reverse())
| x | y | x_reverse |
|---|---|---|
| i64 | i64 | i64 |
| 1 | 40 | 4 |
| 2 | 30 | 3 |
| 3 | 20 | 2 |
| 4 | 10 | 1 |
.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')
)
| x | y | x_sort | y_sort | x_sort_by | y_sort_by |
|---|---|---|---|---|---|
| i64 | i64 | i64 | i64 | i64 | i64 |
| 1 | 40 | 4 | 10 | 3 | 20 |
| 2 | 30 | 3 | 20 | 2 | 30 |
| 3 | 20 | 2 | 30 | 4 | 10 |
| 4 | 10 | 1 | 40 | 1 | 40 |
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")
)
| tag | A | B | C | D | E | sum |
|---|---|---|---|---|---|---|
| str | i64 | i64 | i64 | i64 | i64 | i64 |
| "a" | 5 | 3 | 5 | 0 | 7 | 20 |
| "b" | 1 | 1 | 6 | 8 | 7 | 23 |
| "a" | 4 | 1 | 3 | 0 | 6 | 14 |
| "b" | 5 | 6 | 7 | 1 | 2 | 21 |
| "c" | 4 | 4 | 0 | 0 | 2 | 10 |
| "e" | 6 | 9 | 9 | 5 | 8 | 37 |
horizontal 函数不适用时,也可以用 concat_list() 把多个列转换为一个列表列,再用 list.xxx 函数计算。下面的例子计算所有列(tag 列除外)的中位数,添加为新列 median。
df.with_columns(
pl.concat_list(pl.exclude("tag"))
.list.median()
.alias("median")
)
| tag | A | B | C | D | E | median |
|---|---|---|---|---|---|---|
| str | i64 | i64 | i64 | i64 | i64 | f64 |
| "a" | 5 | 3 | 5 | 0 | 7 | 5.0 |
| "b" | 1 | 1 | 6 | 8 | 7 | 6.0 |
| "a" | 4 | 1 | 3 | 0 | 6 | 3.0 |
| "b" | 5 | 6 | 7 | 1 | 2 | 5.0 |
| "c" | 4 | 4 | 0 | 0 | 2 | 2.0 |
| "e" | 6 | 9 | 9 | 5 | 8 | 8.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")
)
| tag | A | B | C | D | E | sum_top_2 |
|---|---|---|---|---|---|---|
| str | i64 | i64 | i64 | i64 | i64 | i64 |
| "a" | 5 | 3 | 5 | 0 | 7 | 12 |
| "b" | 1 | 1 | 6 | 8 | 7 | 15 |
| "a" | 4 | 1 | 3 | 0 | 6 | 10 |
| "b" | 5 | 6 | 7 | 1 | 2 | 13 |
| "c" | 4 | 4 | 0 | 0 | 2 | 8 |
| "e" | 6 | 9 | 9 | 5 | 8 | 18 |