未解決の課題

未解決の課題#

この章では、Polars で実装が難しい、またはまだ解決していない課題をまとめます。多くは GitHub に提案として登録されています。ここで書き出す目的は2つあります。読者が同じところで悩まないようにすることと、現時点での代替手段を残しておくことです。

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 ignore NULL#

rolling_*() は NULL に当たると、結果が NULL になります。ウィンドウの中に 1つでも欠損があれば、そのウィンドウの統計量は正しく出ません。実データではよくあることで、センサーが1回落ちただけで前後のウィンドウが使えなくなります。

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() を計算し、元の NULL と結合します。

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]