自定义函数

现在你应该相信,polar表达式是如此的强大和灵活,以至于对自定义python函数的需求比你在其他库中可能需要的要少得多。

尽管如此,你仍然需要有能力将表达式传递给第三方库,或者将你的黑匣子函数应用于polar数据。

为此,我们提供了以下几种表达式:

  • map
  • apply

map

在操作方式上和最终向用户传递的数据上,mapapply函数有重要的区别。

map函数将表达式所支持的Series数据原封不动的传递。

map函数在selectgroupby中遵循相同的规则,这将意味着Series代表DataFrame中的一个列。注意,在groupby情况下,该列还没有被分组!

map函数的用法是将表达式中的Series传递给第三方库。下面我们展示了如何使用map将一个表达式列传递给神经网络模型。

df.with_column([
    pl.col("features").map(lambda s: MyNeuralNetwork.forward(s.to_numpy())).alias("activations")
])

groupby中,map的使用情况很有限。它们只用于性能方面,但很容易导致不正确的结果。让我们来解释一下原因。

df = pl.DataFrame(
    {
        "keys": ["a", "a", "b"],
        "values": [10, 7, 1],
    }
)

out = df.groupby("keys", maintain_order=True).agg(
    [
        pl.col("values").map(lambda s: s.shift()).alias("shift_map"),
        pl.col("values").shift().alias("shift_expression"),
    ]
)
print(df)
shape: (3, 2)
┌──────┬────────┐
│ keys ┆ values │
│ ---  ┆ ---    │
│ str  ┆ i64    │
╞══════╪════════╡
│ a    ┆ 10     │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ a    ┆ 7      │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌┤
│ b    ┆ 1      │
└──────┴────────┘

在上面的片段中,我们按"keys"列分组。这意味着我们有以下几个组。

"a" -> [10, 7]
"b" -> [1]

如果我们再向右应用一个shift操作,我们就会发现。

"a" -> [null, 10]
"b" -> [null]

现在,让我们打印一下得到的结果:

print(out)
shape: (2, 3)
┌──────┬────────────┬──────────────────┐
│ keys ┆ shift_map  ┆ shift_expression │
│ ---  ┆ ---        ┆ ---              │
│ str  ┆ list[i64]  ┆ list[i64]        │
╞══════╪════════════╪══════════════════╡
│ a    ┆ [null, 10] ┆ [null, 10]       │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ b    ┆ [7]        ┆ [null]           │
└──────┴────────────┴──────────────────┘

😯.. 很明显,我们得到了一个错误答案。"b"组甚至从"a"组拿到了一个值7😵.

这是一个可怕的错误,因为map在我们聚合之前就应用了这个函数!这意味着整个列[10, 7, 1]先向右移向到了[null, 10, 7],然后再被聚合。

所以我们的建议是,除非你知道你需要使用map并且知道你在做什么,否则永远不要在groupby时使用map

apply

幸运的是,我们可以用apply来解决之前的例子。apply可以对该操作的最小的逻辑元素起作用。

这就意味着:

  • select-> 单个元素
  • groupby-> 单个分组

因此,我们可以用apply来解决我们上述的问题:

out = df.groupby("keys", maintain_order=True).agg(
    [
        pl.col("values").apply(lambda s: s.shift()).alias("shift_map"),
        pl.col("values").shift().alias("shift_expression"),
    ]
)
print(out)
shape: (2, 3)
┌──────┬────────────┬──────────────────┐
│ keys ┆ shift_map  ┆ shift_expression │
│ ---  ┆ ---        ┆ ---              │
│ str  ┆ list[i64]  ┆ list[i64]        │
╞══════╪════════════╪══════════════════╡
│ a    ┆ [null, 10] ┆ [null, 10]       │
├╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ b    ┆ [null]     ┆ [null]           │
└──────┴────────────┴──────────────────┘

可以看到,我们得到了正确的结果! 🎉

select中的apply

select中,apply表达式将列的元素传递给python函数。

注意,你现在正在运行Python,这将会很慢。

让我们通过一些例子来看看会发生什么。我们将继续使用我们在本节开始时定义的DataFrame,并展示一个使用apply函数的例子和一个使用表达式API实现相同目标的反例。

添加一个计数器

在这个例子中,我们创建了一个全局的 counter (计数器),然后在每处理一个元素时将整数 1 添加到全局状态中。每个迭代的增量结果将被添加到元素值中。

counter = 0


def add_counter(val: int) -> int:
    global counter
    counter += 1
    return counter + val


out = df.select(
    [
        pl.col("values").apply(add_counter).alias("solution_apply"),
        (pl.col("values") + pl.arange(1, pl.count() + 1)).alias("solution_expr"),
    ]
)
print(out)
shape: (3, 2)
┌────────────────┬───────────────┐
│ solution_apply ┆ solution_expr │
│ ---            ┆ ---           │
│ i64            ┆ i64           │
╞════════════════╪═══════════════╡
│ 11             ┆ 11            │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 9              ┆ 9             │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 4              ┆ 4             │
└────────────────┴───────────────┘

合并多列值

如果我们想在一次apply函数调用中访问不同列的值,我们可以创建struct数据类型。这种数据类型将这些列作为字段收集在struct中。因此,如果我们从列"keys""values"中创建一个struct,我们会得到以下结构元素。

[
    {"keys": "a", "values": 10},
    {"keys": "a", "values": 7},
    {"keys": "b", "values": 1},
]

这些将作为dict传递给调用的Python函数,因此可以通过field: str进行索引。

out = df.select(
    [
        pl.struct(["keys", "values"]).apply(lambda x: len(x["keys"]) + x["values"]).alias("solution_apply"),
        (pl.col("keys").str.lengths() + pl.col("values")).alias("solution_expr"),
    ]
)
print(out)
shape: (3, 2)
┌────────────────┬───────────────┐
│ solution_apply ┆ solution_expr │
│ ---            ┆ ---           │
│ i64            ┆ i64           │
╞════════════════╪═══════════════╡
│ 11             ┆ 11            │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 8              ┆ 8             │
├╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┼╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌┤
│ 2              ┆ 2             │
└────────────────┴───────────────┘

返回类型

自定义Python函数对polar而言是黑箱。我们真的不知道你在做什么黑科技,所以我们不得不推断并尽力去理解你的意思。

数据类型是自动推断出来的。我们通过等待第一个非空值来做到这一点。这个值将被用来确定Series的类型。

python类型与polars数据类型的映射如下:

  • int->Int64
  • float->Float64
  • bool->Boolean
  • str->Utf8
  • list[tp]->List[tp](其中内部类型的推断规则相同)
  • dict[str, [tp]]->struct
  • Any->object(在任何时候都要防止这种情况)

作为一个用户,我们希望您能了解我们的工作,以便能更好地利用自定义函数。