将自定义函数用于 Pandas 取决于 colname 的滚动应用

Using custom function for Pandas Rolling Apply that depends on colname

使用 Pandas 1.1.5,我有一个如下所示的测试 DataFrame:

import numpy as np
import pandas as pd
df = pd.DataFrame({'id': ['a0','a0','a0','a1','a1','a1','a2','a2'],
                   'a': [4,5,6,1,2,3,7,9],
                   'b': [3,4,5,3,2,4,1,3],
                   'c': [7,4,3,8,9,7,4,6],
                   'denom_a': [7,8,9,7,8,9,7,8],
                   'denom_b': [10,11,12,10,11,12,10,11]})

我想在滚动 window 上应用以下自定义聚合函数,其中函数的计算取决于列名:

def custom_func(s, df, colname):
  if 'a' in colname:
    denom = df.loc[s.index, "denom_a"]
    calc = s.sum() / np.max(denom)
  elif 'b' in colname:
    denom = df.loc[s.index, "denom_b"]
    calc = s.sum() / np.max(denom)
  else:
    calc = s.mean()
  return calc

df.groupby('id')\
  .rolling(2, 1)\
  .apply(lambda x: custom_func(x, df, x.name))

这导致 TypeError: argument of type 'NoneType' is not iterable,因为每列的 windowed 子集不保留原始 df 列的名称。也就是说,作为参数传入的 x.name 实际上是传递 None 而不是原始列名的字符串。

是否有某种方法可以使这种方法起作用(例如,保留正在使用 apply 操作的列名并将其传递给函数)?或者有什么改变的建议吗?我查阅了以下参考资料,让自定义函数在同一 window 计算中使用多个列,其中包括:

如果有“更好”的解决方案,我不会感到惊讶,但我认为至少可以是一个“良好的开端”(我并没有用 .rolling(...) 做很多事情)。

对于这个解决方案,我做出两个关键假设:

  1. 所有 denom_<X> 都有相应的 <X> 列。
  2. 你用 (<X>, denom_<X>) 对做的一切都是一样的。 (这应该很容易根据需要进行自定义。)

话虽如此,我在函数内部而不是外部执行 .rolling,部分原因是 RollingGroupBy 上的 .apply(...) 似乎只能按列工作,这在这里不是很有帮助 (imo)。

def cust_fn(df: pd.DataFrame, rolling_args: Tuple) -> pd.Series:
    cols = df.columns
    denom_cols = ["id"]  # the whole dataframe is passed, so place identifiers / uncomputable variables here.

    for denom_col in cols[cols.str.startswith("denom_")]:
        denom_cols += [denom_col, denom_col.replace("denom_", "")]
        col = denom_cols[-1]  # sugar
        df[f"calc_{col}"] = df[col].rolling(*rolling_args).sum() / df[denom_col].max()

    for col in cols[~cols.isin(denom_cols)]:
        print(col, df[col])
        df[f"calc_{col}"] = df[col].rolling(*rolling_args).mean()
    
    return df

那么你运行的方法如下(你会得到相应的输出):

>>> df.groupby("id").apply(cust_fn, rolling_args=(2, 1))
   id  a  b  c  denom_a  denom_b    calc_a    calc_b  calc_c
0  a0  4  3  7        7       10  0.444444  0.250000     7.0
1  a0  5  4  4        8       11  1.000000  0.583333     5.5
2  a0  6  5  3        9       12  1.222222  0.750000     3.5
3  a1  1  3  8        7       10  0.111111  0.250000     8.0
4  a1  2  2  9        8       11  0.333333  0.416667     8.5
5  a1  3  4  7        9       12  0.555556  0.500000     8.0
6  a2  7  1  4        7       10  0.875000  0.090909     4.0
7  a2  9  3  6        8       11  2.000000  0.363636     5.0

如果您需要动态说明存在哪些 non-numeric/computable 列,那么按如下方式定义 cust_fn 可能有意义:

def cust_fn(df: pd.DataFrame, rolling_args: Tuple, index_cols: List = []) -> pd.Series:
    cols = df.columns
    denon_cols = index_cols

    # ... the rest is unchanged

然后您将按如下方式调整 cust_fn 的调用:

>>> df.groupby("id").apply(cust_fn, rolling_args=(2, 1), index_cols=["id"])

当然,如果您 运行 遇到使其适应您的用途的问题,请对此发表评论。