Polars 与 Pandas 对比 - 非基本分组操作

主要是介绍非基本分组操作,看看使用pandas和Polars API进行基本聚合的情况,并讨论探讨非基本聚合,并了解Polars API如何比pandas API实现更多功能。
什么是分组操作?
假设我们有一个(Polars)数据帧,如下所示:
形状: (6, 3)
┌─────┬───────┬───────┐
│ id ┆ sales ┆ views │
│ --- ┆ --- ┆ --- │
│ i64 ┆ i64 ┆ i64 │
╞═════╪═══════╪═══════╡
│ 1 ┆ 4 ┆ 3 │
│ 1 ┆ 1 ┆ 1 │
│ 1 ┆ 2 ┆ 2 │
│ 2 ┆ 7 ┆ 8 │
│ 2 ┆ 6 ┆ 6 │
│ 2 ┆ 7 ┆ 7 │
└─────┴───────┴───────┘
分组操作会为每个组生成一行:
df.group_by('id').agg('sales')
形状: (2, 2)
┌─────┬───────────┐
│ id ┆ sales │
│ --- ┆ --- │
│ i64 ┆ list[i64] │
╞═════╪═══════════╡
│ 1 ┆ [4, 1, 2] │
│ 2 ┆ [7, 6, 7] │
└─────┴───────────┘
如果我们希望每个组有一个单一的标量值,可以使用归约函数(‘mean’、‘sum’、‘std’等):
df.group_by('id').agg(pl.sum('sales'))
形状: (2, 2)
┌─────┬───────┐
│ id ┆ sales │
│ --- ┆ --- │
│ i64 ┆ i64 │
╞═════╪═══════╡
│ 1 ┆ 7 │
│ 2 ┆ 20 │
└─────┴───────┘
在pandas中进行分组操作
如果您来自类似pandas的库,可能习惯将上述示例写成这样:
df.groupby('id')['sales'].sum()
确实,对于这样一个简单的任务,pandas API非常好用。我们只需:
- 选择要分组的列
- 选择要聚合的列
- 指定一个基本聚合函数
让我们尝试另一个任务:“对于每个’id’,找到’sales’大于其平均值时’views’的最大值”。
不幸的是,pandas API无法表达这个操作,这意味着任何复制pandas API的库在一般情况下都无法真正优化这样的操作。
您可能想知道我们是否可以这样做:
df.groupby('id').apply(
lambda df: df[df['sales'] > df['sales'].mean()]['views'].max()
)
然而,这使用了Python的lambda
函数,因此通常效率不高。
另一个可能的解决方案是这样的:
df[df['sales'] > df.groupby('id')['sales'].transform('mean')].groupby('id')['views'].max()
这个解决方案不像上面的apply
解决方案那么糟糕,但仍然看起来过于复杂,并且需要进行两次分组操作。
实际上还有第三种解决方案,它:
- 依赖
GroupBy
缓存其分组 - 对原始数据帧进行原地修改
- 利用
'max'
跳过缺失值的特性
实际上,很少有用户会想到这个解决方案(大多数人会直接使用apply
),但为了完整起见,我们还是给出它:
gb = df.groupby("id")
mask = df["sales"] > gb["sales"].transform("mean")
df["result"] = df["views"].where(mask)
gb["result"].max()
当然,肯定有更好的方法吧?
Polars中的非基本分组操作
Polars API允许我们将表达式传递给GroupBy.agg
。只要您可以将聚合表达为一个表达式,就可以在分组设置中使用它。在这种情况下,我们可以将“‘sales’大于其平均值时’views’的最大值”表达为:
pl.col('views').filter(pl.col('sales') > pl.mean('sales')).max()
然后,我们只需将这个表达式传递给GroupBy.agg
:
df.group_by('id').agg(
pl.col('views').filter(pl.col('sales') > pl.mean('sales')).max()
)
太棒了!这样,我们就可以清晰地表达操作,而无需使用技巧,这意味着任何遵循Polars API的数据帧实现都有可能高效地评估这个操作。
结论及对未来数据帧作者的请求
我们了解了分组操作、pandas和Polars中的基本聚合,以及Polars的语法如何使用户能够清晰地表达非基本聚合。
pandas是一个很棒的工具,为很多人解决了很多实际问题。然而,当新的数据帧库坚持复制其API时,它们正在限制自己的潜力。
如果一个API不允许表达某个操作,而用户最终使用带有自定义Python lambda函数的apply
,那么无论进行多少加速都无法弥补这一点。
另一方面,API方面的创新可以带来新的可能性。这就是为什么“我为速度而来,但为语法而留”成为Polars用户的常见口头禅。