如何避免 Pandas DataFrame 中过多的 lambda 函数分配和应用方法链

tee*_*pee 5 python r python-3.x pandas tidyverse

我正在尝试将 R 中数据帧的操作管道转换为其 Python 等效项。管道的一个基本示例如下,包含一些mutatefilter调用:

library(tidyverse)

calc_circle_area <- function(diam) pi / 4 * diam^2
calc_cylinder_vol <- function(area, length) area * length

raw_data <- tibble(cylinder_name=c('a', 'b', 'c'), length=c(3, 5, 9), diam=c(1, 2, 4))

new_table <- raw_data %>% 
  mutate(area = calc_circle_area(diam)) %>% 
  mutate(vol = calc_cylinder_vol(area, length)) %>% 
  mutate(is_small_vol = vol < 100) %>% 
  filter(is_small_vol)
Run Code Online (Sandbox Code Playgroud)

我可以在熊猫复制这个没有太多的麻烦,但发现它涉及到一些嵌套lambda使用时调用assign做一个apply(第一,其中数据帧来电者是一个参数,随后与数据框行作为参数)。这往往会掩盖赋值调用的含义,如果可能的话,我想在其中指定更切题的内容(如 R 版本)。

import pandas as pd
import math

calc_circle_area = lambda diam: math.pi / 4 * diam**2
calc_cylinder_vol = lambda area, length: area * length

raw_data = pd.DataFrame({'cylinder_name': ['a', 'b', 'c'], 'length': [3, 5, 9], 'diam': [1, 2, 4]})

new_table = (
    raw_data
        .assign(area=lambda df: df.diam.apply(lambda r: calc_circle_area(r.diam), axis=1))
        .assign(vol=lambda df: df.apply(lambda r: calc_cylinder_vol(r.area, r.length), axis=1))
        .assign(is_small_vol=lambda df: df.vol < 100)
        .loc[lambda df: df.is_small_vol]
)
Run Code Online (Sandbox Code Playgroud)

我知道.assign(area=lambda df: df.diam.apply(calc_circle_area))可以写为.assign(area=raw_data.diam.apply(calc_circle_area))但仅因为该diam列已存在于原始数据框中,情况可能并非总是如此。

我也意识到calc_...这里的函数是可向量化的,这意味着我也可以做类似的事情

.assign(area=lambda df: calc_circle_area(df.diam))
.assign(vol=lambda df: calc_cylinder_vol(df.area, df.length))
Run Code Online (Sandbox Code Playgroud)

但同样,由于大多数函数不可矢量化,因此在大多数情况下这不起作用。

TL; DR 我想知道是否有一种更简洁的方法来“变异”不涉及双重嵌套lambda语句的数据帧上的列,例如:

.assign(vol=lambda df: df.apply(lambda r: calc_cylinder_vol(r.area, r.length), axis=1))
Run Code Online (Sandbox Code Playgroud)

这种类型的应用程序是否有最佳实践,或者这是在方法链上下文中可以做的最好的实践吗?

mcs*_*ner 7

最佳实践是向量化操作。

这样做的原因是性能,因为apply非常慢。您已经在 R 代码中利用了矢量化,您应该继续在 Python 中这样做。您会发现,由于这种性能考虑,您需要的大部分功能实际上都是可矢量化的。

这将摆脱你内心的 lambdas。对于 上的外部 lambda df,我认为您拥有的是最干净的模式。另一种方法是反复重新分配给raw_data变量或其他一些中间变量,但这不符合您要求的方法链接样式。

还有像dfply这样的Python 包,旨在模仿dplyrPython 中的感觉。这些不会得到与核心相同级别的支持pandas,所以如果你想走这条路,请记住这一点。


或者,如果您只想节省一点打字的时间,并且所有函数都将只在列上,您可以创建一个粘合函数,为您解压列并传递它们。

def df_apply(col_fn, *col_names):
    def inner_fn(df):
        cols = [df[col] for col in col_names]
        return col_fn(*cols)
    return inner_fn
Run Code Online (Sandbox Code Playgroud)

然后使用最终看起来像这样:

new_table = (
    raw_data
        .assign(area=df_apply(calc_circle_area, 'diam'))
        .assign(vol=df_apply(calc_cylinder_vol, 'area', 'length'))
        .assign(is_small_vol=lambda df: df.vol < 100)
        .loc[lambda df: df.is_small_vol]
)
Run Code Online (Sandbox Code Playgroud)

也可以在不利用矢量化的情况下编写它,以防万一。

def df_apply_unvec(fn, *col_names):
    def inner_fn(df):
        def row_fn(row):
            vals = [row[col] for col in col_names]
            return fn(*vals)
        return df.apply(row_fn, axis=1)
    return inner_fn
Run Code Online (Sandbox Code Playgroud)

为了更加清晰,我使用了命名函数。但是它可以用 lambda 压缩成看起来很像你的原始格式的东西,只是通用的。