Pandas的groupby apply和transform在使用特定函数时有何区别?

16

我不理解哪些函数可用于 groupby + transform 操作。通常,我只能猜测、测试、回滚直到有用的东西出现,但我觉得应该有一种系统的方法来确定解决方案是否可行。

这里是一个最简示例。首先,让我们使用 groupby + applyset

df = pd.DataFrame({'a': [1,2,3,1,2,3,3], 'b':[1,2,3,1,2,3,3], 'type':[1,0,1,0,1,0,1]})

g = df.groupby(['a', 'b'])['type'].apply(set)

print(g)

a  b
1  1    {0, 1}
2  2    {0, 1}
3  3    {0, 1}

这个很好用,但我想要通过分组计算set并将结果作为原始数据框中的新列。因此我尝试使用transform

这段代码可以正常工作,但我希望将计算出的 set 按组添加到原始数据框中作为新列。所以我尝试使用 transform

df['g'] = df.groupby(['a', 'b'])['type'].transform(set)

---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
---> 23 df['g'] = df.groupby(['a', 'b'])['type'].transform(set)

TypeError: int() argument must be a string, a bytes-like object or a number, not 'set'

这是我在Pandas v0.19.0中看到的错误。在v0.23.0中,我看到的是TypeError: 'set' type is unordered。当然,我可以映射一个特定定义的索引来实现我的结果:

g = df.groupby(['a', 'b'])['type'].apply(set)
df['g'] = df.set_index(['a', 'b']).index.map(g.get)

print(df)

   a  b  type       g
0  1  1     1  {0, 1}
1  2  2     0  {0, 1}
2  3  3     1  {0, 1}
3  1  1     0  {0, 1}
4  2  2     1  {0, 1}
5  3  3     0  {0, 1}
6  3  3     1  {0, 1}

但是我认为transform的好处是避免这样明确的映射。我错在哪里了?

2个回答

20

首先,我认为在使用这些函数时有一定的直觉空间,因为它们可以非常有意义。

在您的第一个结果中,您实际上并没有尝试转换您的值,而是要汇总它们(这将按您预期的方式工作)。

但是,进入代码,transform文档相当暗示性地表示:

返回与组块大小相同或可广播到组块大小的结果。

当您执行以下操作时:

df.groupby(['a', 'b'])['type'].transform(some_func)

你实际上是在使用some_func函数将每个组中的每个pd.Series对象转换为一个新对象。但问题是,这个新对象应该与该组具有相同的大小或者可广播到块的大小。

因此,如果你使用tuplelist来转换你的序列,那么你基本上就是在转换这个对象。

0    1
1    2
2    3
dtype: int64

进入

[1,2,3]
但请注意,这些值现在已经被重新赋值给它们各自的索引,这就是为什么在transform操作中看不到任何差异的原因。那一行数据中来自pd.Series.iloc[0]值现在将拥有来自transform列表的[1,2,3][0]值(元组也是同样的情况)。请注意,这里的顺序大小很重要,否则您可能会弄乱您的分组,从而使变换无法正常工作(这正是为什么set在这种情况下不是一个合适的函数的原因)。
引用文本的第二部分说:“可以广播到组块的大小”。
这意味着您还可以将您的pd.Series转换为可在所有行中使用的对象。例如:
df.groupby(['a', 'b'])['type'].transform(lambda k: 50)

为什么可以使用集合进行apply操作?因为apply方法没有结果的大小限制。它实际上有三种不同的结果类型,并且它推断出你想要扩展减少广播你的结果。请注意,你不能在转换过程中减少*


默认情况下(result_type=None),最终返回类型是从应用函数的返回类型推断出来的。 result_type: {'expand', 'reduce', 'broadcast', None},默认值为None 这些只在axis=1(列)起作用:

  1. ‘expand’:类似列表的结果将被转换为列。

  2. ‘reduce’:如果可能,返回一个Series而不是扩展类似列表的结果。这与“expand”相反。

  3. ‘broadcast’:结果将被广播到DataFrame的原始形状,原始的索引和列将被保留。

为什么会起作用呢?即使50不可迭代,但通过在初始pd.Series的所有位置重复使用此值,它是可以广播的。


3
谢谢。我忘记了transform方法可以单独使用(不需要groupby),且有特定的要求。尽管如此,在我的经验中,它通常与groupby一起使用,这就是为什么我错误地认为任何与groupbyapply一起使用的内容都可以与transform一起使用。 - jpp

3
转换的结果受到一定类型的限制。【例如,它不能是listsetSeries等——这是不正确的,感谢@RafaelC的评论】我认为这并没有被记录下来,但是当检查groupby.pyseries.py的源代码时,您可以找到这些类型的限制。
groupby文档中。

transform方法返回一个对象,该对象与被分组的对象索引相同(大小相同)。转换函数必须:

  • 返回结果要么与组块大小相同,要么可广播到组块的大小(例如,标量、grouped.transform(lambda x: x.iloc[-1]))。

  • 对组块逐列操作。使用chunk.apply将转换应用于第一个组块。

  • 不对组块执行原地操作。组块应被视为不可变的,对组块的更改可能会产生意外结果。例如,使用fillna时,inplace必须为False(grouped.transform(lambda x: x.fillna(inplace=False)))。

  • (可选)在整个组块上操作。如果支持此操作,则从第二个块开始使用快速路径。

免责声明:我遇到了不同的错误(pandas版本0.23.1):

df['g'] = df.groupby(['a', 'b'])['type'].transform(set)
File "***/lib/python3.6/site-packages/pandas/core/groupby/groupby.py", line 3661, in transform
s = klass(res, indexer)        s = klass(res, indexer)
File "***/lib/python3.6/site-packages/pandas/core/series.py", line 242, in __init__
"".format(data.__class__.__name__))
TypeError: 'set' type is unordered

更新

将组转换为集合后,pandas 无法将其广播到 Series 中,因为它是无序的(并且与组块具有不同的维度)。如果我们强制将其包装在列表中,它将变成与组块相同的大小,并且我们每行只得到单个值。答案是将其包装在某个容器中,使对象的结果大小变为1,然后 pandas 就能够广播它了:

df['g'] = df.groupby(['a', 'b'])['type'].transform(lambda x: np.array(set(x)))
print(df)

   a  b  type       g
0  1  1     1  {0, 1}
1  2  2     0  {0, 1}
2  3  3     1  {0, 1}
3  1  1     0  {0, 1}
4  2  2     1  {0, 1}
5  3  3     0  {0, 1}
6  3  3     1  {0, 1}

我为什么选择np.array作为容器?因为series.py(第205-206行)在不进行进一步检查的情况下传递了这种类型。因此,我相信这种行为将在未来版本中得以保留。


奇怪的是,在v0.23中,tuplelist似乎可以正常工作,但结果并不符合我的预期。我猜我们遇到的问题与“可广播到组块大小”的部分有关。看起来Pandas无法将一个集合/列表等“复制”到组的所有成员中。 - jpp
我同意,这就是为什么我强调了那部分内容。但由于它没有记录,并且代码正在从版本更改,因此我不建议在transform中使用返回标量、numpy向量或字符串以外的任何内容。 - igrinis
事实上,您可以使用“list”或“pd.Series”进行转换。 - rafaelc
2
@RafaelC 你说得对。我会更新我的回答并点赞你的。感谢你和jpp的有趣讨论。 - igrinis

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