在pandas的groupby transform中是否可以访问列名?

3
我的理想问题是“如何在 Pandas 的 DataFrameGroupBy.transform() 函数中访问列?”,但在进行了一些测试后(如下所示),我怀疑这是否可能。
我希望能够像使用 apply 一样访问列名,但是改用 transform 实现。
例如,对于以下示例数据:
import numpy as np
import pandas as pd

np.random.seed(123)
numeric_data = np.random.rand(9, 3)
cat_data = [f'grp_{i}' for i in range(1,4)] * 3
df = pd.DataFrame(numeric_data, columns=list('ABC')).assign(D = cat_data)
print(df)

          A         B         C      D
0  0.696469  0.286139  0.226851  grp_1
1  0.551315  0.719469  0.423106  grp_2
2  0.980764  0.684830  0.480932  grp_3
3  0.392118  0.343178  0.729050  grp_1
4  0.438572  0.059678  0.398044  grp_2
5  0.737995  0.182492  0.175452  grp_3
6  0.531551  0.531828  0.634401  grp_1
7  0.849432  0.724455  0.611024  grp_2
8  0.722443  0.322959  0.361789  grp_3

我能如何使用transform从A中减去B然后乘以C?这是可能的吗?
我知道使用applylambda或传递用户定义函数可以轻松实现这一点,例如:
def customFunc(grp):
    return (grp['A'] - grp['B']) * grp['C']

df.groupby('D').apply(customFunc)

D       
grp_1  0    0.093084
       3    0.035679
       6   -0.000175
grp_2  1   -0.071147
       4    0.150817
       7    0.076364
grp_3  2    0.142324
       5    0.097464
       8    0.144529
dtype: float64

输出的数值是无序的(如您可以在内部索引中看到),所以我不能将该输出直接放置在新列中。一种选择是使用apply事先对数据框进行排序,但说实话,对于具有更复杂的groupby组的大型数据,我不完全有信心这样做会按预期工作。我更愿意使用transform,否则,我认为通过索引将结果合并回df更可靠(假设我们拥有唯一的索引)。
如果我尝试使用transform来使用相同的函数:
df.groupby('D').transform(customFunc)

接着我遇到了一个错误:KeyError: 'A'

为了深入了解使用groupby.applygroupby.transform时发生的情况,我进行了以下操作:

# Select the target-group
grp = df.groupby('D')

grp.apply(lambda x: type(x))

D
grp_1    (<class 'pandas.core.frame.DataFrame'>, 3)
grp_2    (<class 'pandas.core.frame.DataFrame'>, 3)
grp_3    (<class 'pandas.core.frame.DataFrame'>, 3)
dtype: object

grp.transform(lambda x: type(x))
                                     A                                    B                                    C  
0  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  
1  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  
2  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  
3  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  
4  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  
5  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  
6  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  
7  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  
8  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0>  <property object at 0x7f7dbde619f0> 

正如您所看到的,apply 将数据分成子数据框作为组,而我不知道 transform 到底提供了什么(这是我第一次遇到这个属性)。我还进行了进一步的测试:

# Another trials
grp.transform(lambda x: x.shape) # ValueError
grp.transform(lambda x: x['A']) # KeyError
grp.transform(lambda x: x.loc[0]) # KeyError
grp.transform(lambda x: x.iloc[0]) # works (every value get the first value; similar to 'first')

看起来使用.iloc[]可以单独访问每个列的值,但我仍然不知道如何在transform内部访问列(如果有可能的话)。

所以,我的最终问题是:

  • groupby.transform中访问列名以执行跨列计算是否可能?
  • 如果不可能,将apply的输出放回数据框的最佳(可靠)方式是什么?

1
在您的第一个示例中,如果您只是对列进行逐元素操作并将结果附加回数据框,为什么需要按D分组?您可以使用 df["result"] = (df["B"] - df["A"])*df["C"] 来完成这个操作。您能展示一下实际需要分组的示例吗? - Anakhand
1
你说得对!我没有为此提供一个好的例子。但是想象一下,我们将 * C 替换为 * np.mean(C) 以使分组有意义。 - Cainã Max Couto-Silva
2个回答

3

在使用groupby.transform进行计算时,是否可以访问列名以跨列执行计算?

不可能的,.groupby.transform只能独立地处理每一列,因此无法像groupby.apply那样'看到'其他列。

如果使用print,可以看到它:

print (df.groupby('D').transform(lambda x: print(x)))
2    0.980764
5    0.737995
8    0.722443
Name: A, dtype: float64
2    0.684830
5    0.182492
8    0.322959
Name: B, dtype: float64
2    0.480932
5    0.175452
8    0.361789
Name: C, dtype: float64

如果不是聚合值的函数,那么将apply的输出放回到数据框中的最佳(可靠)方式是什么?
def customFunc(grp):
    return (grp['A'] - grp['B']) * grp['C']

df['new'] = df.groupby('D').apply(customFunc).rename('new').reset_index(level=0, drop=True)

print (df)
          A         B         C      D       new
0  0.696469  0.286139  0.226851  grp_1  0.093084
1  0.551315  0.719469  0.423106  grp_2 -0.071147
2  0.980764  0.684830  0.480932  grp_3  0.142324
3  0.392118  0.343178  0.729050  grp_1  0.035679
4  0.438572  0.059678  0.398044  grp_2  0.150817
5  0.737995  0.182492  0.175452  grp_3  0.097464
6  0.531551  0.531828  0.634401  grp_1 -0.000175
7  0.849432  0.724455  0.611024  grp_2  0.076364
8  0.722443  0.322959  0.361789  grp_3  0.144529

所以工作方式相同:

df['new'] = (df['A'] - df['B']) * df['C']

print (df)
         A         B         C      D       new
0  0.696469  0.286139  0.226851  grp_1  0.093084
1  0.551315  0.719469  0.423106  grp_2 -0.071147
2  0.980764  0.684830  0.480932  grp_3  0.142324
3  0.392118  0.343178  0.729050  grp_1  0.035679
4  0.438572  0.059678  0.398044  grp_2  0.150817
5  0.737995  0.182492  0.175452  grp_3  0.097464
6  0.531551  0.531828  0.634401  grp_1 -0.000175
7  0.849432  0.724455  0.611024  grp_2  0.076364
8  0.722443  0.322959  0.361789  grp_3  0.144529

如果要聚合函数值,请使用DataFrame.joinSeries.map,如果使用一列进行分组:

def customFunc(grp):
    return ((grp['A'] - grp['B']) * grp['C']).mean()

df = df.join(df.groupby('D').apply(customFunc).rename('new'), on='D')

def customFunc(grp):
    return ((grp['A'] - grp['B']) * grp['C']).mean()

df['new'] = df['D'].map(df.groupby('D').apply(customFunc))


print (df)
       A         B         C      D       new
0  0.696469  0.286139  0.226851  grp_1  0.042863
1  0.551315  0.719469  0.423106  grp_2  0.052011
2  0.980764  0.684830  0.480932  grp_3  0.128106
3  0.392118  0.343178  0.729050  grp_1  0.042863
4  0.438572  0.059678  0.398044  grp_2  0.052011
5  0.737995  0.182492  0.175452  grp_3  0.128106
6  0.531551  0.531828  0.634401  grp_1  0.042863
7  0.849432  0.724455  0.611024  grp_2  0.052011
8  0.722443  0.322959  0.361789  grp_3  0.128106

或更改函数:

def customFunc(grp):
    grp['new'] = ((grp['A'] - grp['B']) * grp['C']).mean()
    return grp
    
df = df.groupby('D').apply(customFunc)

print (df)
          A         B         C      D       new
0  0.696469  0.286139  0.226851  grp_1  0.042863
1  0.551315  0.719469  0.423106  grp_2  0.052011
2  0.980764  0.684830  0.480932  grp_3  0.128106
3  0.392118  0.343178  0.729050  grp_1  0.042863
4  0.438572  0.059678  0.398044  grp_2  0.052011
5  0.737995  0.182492  0.175452  grp_3  0.128106
6  0.531551  0.531828  0.634401  grp_1  0.042863
7  0.849432  0.724455  0.611024  grp_2  0.052011
8  0.722443  0.322959  0.361789  grp_3  0.128106

非常感谢您,@jezrael!我真的很感激这个(实际上,是你所有的回答)。只是为了确保我理解正确:在您第一个代码块中的“If function not aggregate values:”处,pandas是否会自动对值进行排序以匹配Series与数据框的索引? - Cainã Max Couto-Silva

1

为了补充@jezrael的答案:

是否可以在groupby.transform中访问列名以跨列执行计算?

不可能,.groupby.transform会单独处理每一列,因此无法像groupby.apply那样“看到”其他列。

这是由于transform规定方式(强调我的)所致:

注意事项 每个组都有一个“name”属性,以便您知道正在处理哪个组。 当前实现对f有三个要求: - f必须返回一个与输入子框架具有相同形状的值或可以广播到输入子框架的形状。例如,如果f返回一个标量,它将被广播为与输入子框架具有相同的形状。 - 如果这是一个DataFrame,则f必须支持逐列应用在子框架中。如果f也支持对整个子框架的应用,则从第二个块开始使用快速路径。 - f不能突变组。不支持变异可能会产生意外结果。
即它要求函数能够在单个列上工作。但是,如果它适用于整个数据帧,pandas会自动在一半时切换到应用于整个组(对我来说似乎有点笨拙,但无论如何)。同样,我们可以在转换函数中使用print来查看这一点:
def f(x):
    print(x.name, type(x))
    return x

In [1]: gb.transform(f)
A <class 'pandas.core.series.Series'>
A <class 'pandas.core.series.Series'>
B <class 'pandas.core.series.Series'>
C <class 'pandas.core.series.Series'>
grp_1 <class 'pandas.core.frame.DataFrame'>
grp_2 <class 'pandas.core.frame.DataFrame'>
grp_3 <class 'pandas.core.frame.DataFrame'>
Out[1]:
          A         B         C
0  0.696469  0.286139  0.226851
1  0.551315  0.719469  0.423106
2  0.980764  0.684830  0.480932
3  0.392118  0.343178  0.729050
4  0.438572  0.059678  0.398044
5  0.737995  0.182492  0.175452
6  0.531551  0.531828  0.634401
7  0.849432  0.724455  0.611024
8  0.722443  0.322959  0.361789

在这里,我们可以看到 pandas 在内部执行的操作:

  • 首先,在第一列 A 上进行“试运行”(不确定这是在做什么,但可能是为了检查函数是否兼容或使用最佳性能的方法);
  • 然后,它逐列地在第一组(grp_1)上应用 f
  • 一旦它意识到 f 可以在整个数据帧上工作(我猜测是通过再次尝试在第一组上应用 f,然后将结果与逐列结果进行比较),它就切换到将 f 应用于每个组的所有内容,作为整个数据帧。

实际上,知道了这一点,实际上可以从算法的第二部分开始提取列(一旦它开始在整个数据帧上应用):

def f(x):
    try:
        x["A"]
        print(x.name, type(x), "column retrieval succeeded")
    except KeyError:
        print(x.name, type(x), "column retrieval failed")
    return x

In [59]: gb.transform(f)
A <class 'pandas.core.series.Series'> column retrieval failed
A <class 'pandas.core.series.Series'> column retrieval failed
B <class 'pandas.core.series.Series'> column retrieval failed
C <class 'pandas.core.series.Series'> column retrieval failed
grp_1 <class 'pandas.core.frame.DataFrame'> column retrieval succeeded
grp_2 <class 'pandas.core.frame.DataFrame'> column retrieval succeeded
grp_3 <class 'pandas.core.frame.DataFrame'> column retrieval succeeded
Out[59]:
          A         B         C
0  0.696469  0.286139  0.226851
1  0.551315  0.719469  0.423106
2  0.980764  0.684830  0.480932
3  0.392118  0.343178  0.729050
4  0.438572  0.059678  0.398044
5  0.737995  0.182492  0.175452
6  0.531551  0.531828  0.634401
7  0.849432  0.724455  0.611024
8  0.722443  0.322959  0.361789

当然,在实践中这并不有用,因为文档中指定的转换函数应该能够在单独的列(和可选的整个数据框)上工作,所以你不应该在此函数中引用特定的列。

1
非常感谢您的回答!实际上,打印语句帮助我更好地理解了它。我一直在努力理解 transform 的工作原理。最后,看起来我一直在尝试做一些我不应该做的事情哈哈。 - Cainã Max Couto-Silva

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接