尚未解决的课题

尚未解决的课题#

本章汇总 Polars 里实现起来困难、或者至今还没解决的问题。这些问题大多在 GitHub 上有对应的提案。写出来一是免得读者自己再绕一遍,二是把当前的替代写法记下来。

import polars as pl

scatter#

Series.scatter() 会在原地改写 Series 对象,也就是真正意义上的“按位置批量赋值”。它的限制在于只能作用于 Series:一旦想在表达式链中间做同样的操作,就没有对应的写法。

pola-rs/polars#17332

s = pl.Series([1, 2, 3, 4, 5])
s.scatter(0, 99)
print(s)
shape: (5,)
Series: '' [i64]
[
	99
	2
	3
	4
	5
]

希望有 Expr.scatter(),这样就能把“按掩码替换若干位置的值”直接写进表达式链,不必先取出来、算完再拼回去。有了它,下面的处理会简单很多。

pola-rs/polars#13087

df = pl.DataFrame(dict(
    A=[1, 2, 3, 4, 5],
    B=[0, 5, 9, 2, 10],
))

def set_elements(cols):
    a, b = cols
    return a.scatter((a < b).arg_true(), [100, 210, 320])

df2 = df.with_columns(
    pl.map_batches(['A', 'B'], set_elements).alias('C')
)
df2
shape: (5, 3)
ABC
i64i64i64
101
25100
39210
424
510320

在它出现之前,可以用 gather() 把选中的位置取出来、改完再放回,效果等价但要绕一圈。下面的代码用 gather() 实现了 set_by_mask()。

import numpy as np

def set_by_mask(old_values, cond_expr, new_values):
    if isinstance(new_values, (tuple, list)):
        new_values = pl.lit(new_values).explode()
    elif isinstance(new_values, (np.ndarray, pl.Series)):
        new_values = pl.lit(new_values)
        
    return new_values.gather(pl.when(cond_expr).then(cond_expr.cum_sum()).otherwise(None) - 1).fill_null(old_values)

df.with_columns(C=set_by_mask(pl.col('A'), pl.col('A') < pl.col('B'), np.array([100, 200, 300])))
shape: (5, 3)
ABC
i64i64i64
101
25100
39200
424
510300
arr = np.array([1, 2, 3, 4, 5, 6])
index =[4, 2]
value = [100, 200]
arr[index] = value
arr
array([  1,   2, 200,   4, 100,   6])
[None, None, 1, None, 0, None]
[None, None, 1, None, 0, None]

rolling 忽略 NULL#

rolling_*() 一旦碰到 NULL,结果就整段是 NULL,窗口里只要有一个缺失,整个窗口的统计量都算不准。这在真实数据里很常见:传感器偶尔掉一次数据,前后几个窗口的结果就都用不了。

import polars as pl

df = pl.DataFrame(
    {
        "A": [5, None, 3, 2, 1],
        "B": [5, 3, None, 2, 1],
        "C": [None, None, None, None, None],
    }
)

df.select(pl.col('A').rolling_mean(2))
shape: (5, 1)
A
f64
null
null
null
2.5
1.5

一种变通做法是先把非 NULL 的数据单独拿出来算 rolling_mean(),再按位置拼回原来的表。下面的代码就是这个思路,代价是要多维护一份索引。

df_res = df.with_columns(
    pl.col("A", "B", "C")
      .rolling_mean(2)
      .over(pl.col("A", "B", "C").is_null())
      .name.suffix('.mean')
)
df_res
shape: (5, 6)
ABCA.meanB.meanC.mean
i64i64nullf64f64null
55nullnullnullnull
null3nullnull4.0null
3nullnull4.0nullnull
22null2.52.5null
11null1.51.5null

另一种做法是用 rolling() 把表达式套到窗口上,因为窗口内的 .mean() 可以忽略 NULL。这种情况下需要一个 index 列来界定窗口。

df.with_row_index().select(
    pl.col('A').mean().rolling('index', period='2i')
)
shape: (5, 1)
A
f64
5.0
5.0
3.0
2.5
1.5
        flowchart LR
    A[Hard] -->|Text| B(Round)
    B --> C{Decision}
    C -->|Yes| D[Result 1]
    C -->|No| E[Result 2]