Pandas: 在 groupby.agg 中弃用重新标记字典会带来许多问题

创建于 2017-11-19  ·  37评论  ·  资料来源: pandas-dev/pandas

此问题是根据 #15931 中的讨论创建的,之后弃用了groupby.agg的重新标记字典。 下面总结的很多内容在前面的讨论中已经讨论过。 我会特别推荐https://github.com/pandas-dev/pandas/pull/15931#issuecomment -336139085,其中也明确说明了问题。

弃用 #15931 背后的动机主要与在 Series 和 Dataframe 之间为agg()带来一致的接口有关(另请参阅 #14668 以了解上下文)。

一些人将使用嵌套字典的重新标记功能描述为过于复杂和/或不一致,因此不推荐使用。

然而,这是有代价的:无法同时聚合和重命名会导致非常烦人的问题和一些向后不兼容,没有合理的解决方法可用:

  • _[烦人]_ 不再控制结果列的名称
  • _[烦人]_ 你需要找到一种方法来重命名 MultiIndex _after_ 执行聚合,需要跟踪代码中两个地方的列顺序......根本不实用,有时甚至完全不可能(下面的情况)。
  • ⚠️ _ [破坏] _ 不能在同一输入列上应用多个具有相同内部名称的可调用对象。 这导致两个子案例:

    • _ [中断] _ 您不能在同一列上再应用两个或多个 lambda 聚合器

    • _ [破坏] _ 你不能再从部分函数中应用两个或多个聚合器,除非你改变它们隐藏的__name__属性

例子

_(请注意,这是一个精心设计的示例,目的是用尽可能短的代码来演示问题,但是自从更改以来,这里所有演示的问题在现实生活中确实让我感到困惑,而且情况并不像这里那么简单)_

输入数据框

mydf = pd.DataFrame(
    {
        'cat': ['A', 'A', 'A', 'B', 'B', 'C'],
        'energy': [1.8, 1.95, 2.04, 1.25, 1.6, 1.01],
        'distance': [1.2, 1.5, 1.74, 0.82, 1.01, 0.6]
    },
    index=range(6)
)
  cat  distance  energy
0   A      1.20    1.80
1   A      1.50    1.95
2   A      1.74    2.04
3   B      0.82    1.25
4   B      1.01    1.60
5   C      0.60    1.01

前:

易于编写和阅读,并按预期工作

import numpy as np
import statsmodels.robust as smrb
from functools import partial

# median absolute deviation as a partial function
# in order to demonstrate the issue with partial functions as aggregators
mad_c1 = partial(smrb.mad, c=1)

# renaming and specifying the aggregators at the same time
# note that I want to choose the resulting column names myself
# for example "total_xxxx" instead of just "sum"
mydf_agg = mydf.groupby('cat').agg({
    'energy': {
        'total_energy': 'sum',
        'energy_p98': lambda x: np.percentile(x, 98),  # lambda
        'energy_p17': lambda x: np.percentile(x, 17),  # lambda
    },
    'distance': {
        'total_distance': 'sum',
        'average_distance': 'mean',
        'distance_mad': smrb.mad,   # original function
        'distance_mad_c1': mad_c1,  # partial function wrapping the original function
    },
})

结果是

          energy                             distance
    total_energy energy_p98 energy_p17 total_distance average_distance distance_mad distance_mad_c1
cat
A           5.79     2.0364     1.8510           4.44            1.480     0.355825           0.240
B           2.85     1.5930     1.3095           1.83            0.915     0.140847           0.095
C           1.01     1.0100     1.0100           0.60            0.600     0.000000           0.000

剩下的就是:

# get rid of the first MultiIndex level in a pretty straightforward way
mydf_agg.columns = mydf_agg.columns.droplevel(level=0)

快乐的舞蹈赞美熊猫💃 🕺!

import numpy as np
import statsmodels.robust as smrb
from functools import partial

# median absolute deviation as a partial function
# in order to demonstrate the issue with partial functions as aggregators
mad_c1 = partial(smrb.mad, c=1)

# no way of choosing the destination's column names...
mydf_agg = mydf.groupby('cat').agg({
    'energy': [
        'sum',
        lambda x: np.percentile(x, 98), # lambda
        lambda x: np.percentile(x, 17), # lambda
    ],
    'distance': [
        'sum',
        'mean',
        smrb.mad, # original function
        mad_c1,   # partial function wrapping the original function
    ],
})

上述中断是因为 lambda 函数都将导致名为<lambda> ,从而导致

SpecificationError: Function names must be unique, found multiple named <lambda>

向后不兼容回归:不能再将两个不同的 lambda 应用于同一原始列。

如果从上面删除lambda x: np.percentile(x, 98) ,我们会得到与从原始函数继承函数名称的部分函数相同的问题:

SpecificationError: Function names must be unique, found multiple named mad

最后,在覆盖部分的__name__属性后(例如使用mad_c1.__name__ = 'mad_c1' ),我们得到:

    energy          distance
       sum <lambda>      sum   mean       mad mad_c1
cat
A     5.79   1.8510     4.44  1.480  0.355825  0.240
B     2.85   1.3095     1.83  0.915  0.140847  0.095
C     1.01   1.0100     0.60  0.600  0.000000  0.000

仍然

  • 缺少一列(第 98 个百分位数)
  • MultiIndex 列的处理
  • 和列的重命名

分步处理。

聚合后无法控制列名,我们可以自动获得的最佳方式是原始列名和 _aggregate 函数的 name_ 的某种组合,如下所示:

mydf_agg.columns = ['_'.join(col) for col in mydf_agg.columns]

这导致:

     energy_sum  energy_<lambda>  distance_sum  distance_mean  distance_mad distance_mad_c1
cat
A          5.79           1.8510          4.44          1.480      0.355825           0.240
B          2.85           1.3095          1.83          0.915      0.140847           0.095
C          1.01           1.0100          0.60          0.600      0.000000           0.000

如果你真的需要有不同的名字,你可以这样做:

mydf_agg.rename({
    "energy_sum": "total_energy",
    "energy_<lambda>": "energy_p17",
    "distance_sum": "total_distance",
    "distance_mean": "average_distance"
    }, inplace=True)

但这意味着您需要小心保持重命名代码(现在必须位于代码中的另一个位置)与定义聚合的代码同步......

悲伤的熊猫用户😢(当然还是喜欢熊猫)


我完全支持一致性,同时我对 _aggregate 和 rename_ 功能的弃用深感遗憾。 我希望上面的例子能把痛点说清楚。


可能的解决方案

  • 取消弃用 dict-of-dict 重新标记功能
  • 提供另一个 API 以便能够做到这一点(但为什么要为相同的主要目的提供两种方法,即聚合?)
  • ??? (开放的建议)

_可选读:_

关于已经进行了几个月的拉取请求中的上述讨论,我最近才意识到我对这种弃用如此困扰的原因之一:“聚合和重命名”是很自然的事情SQL 中的 GROUP BY 聚合,因为在 SQL 中您通常直接在聚合表达式旁边提供目标列名称,例如SELECT col1, avg(col2) AS col2_mean, stddev(col2) AS col2_var FROM mytable GROUP BY col1

_不是

(* 我个人不同意 dict-of-dict 方法很复杂。)

API Design Groupby

最有用的评论

对于它的价值,我也强烈支持不要贬低功能。

对我来说一个重要的原因是,将 Python 的函数命名空间(与特定实现有关)与列名数据(肯定不应该知道实现的东西)混合在一起有一些非常奇怪的事情。 我们看到名为'<lambda>'列(可能是多个列)这一事实导致我严重的认知失调。

重命名方法令人讨厌,因为有这个中间步骤,其中包含不必要的(和暴露的)列名。 此外,它们很难可靠地、系统地重命名,因为对实现存在潜在的依赖性。

除此之外,嵌套的 dict 功能无疑是复杂的,但它是一个正在执行的复杂操作。

TL;DR 请不要贬值。 :)

所有37条评论

@zertrin :谢谢你把它放在一起。 我看到在#15931 中有很多关于这个的讨论。 由于我无法完整阅读本文,我目前无法发表评论。 尽管如此,让我ping:

@jreback @jorisvandenbossche @TomAugspurger @chris-b1

我同意在这个例子中使用当前的agg实现重命名是非常笨拙和损坏的。 嵌套的 dicts 有点复杂,但是按照您的方式编写它们可以非常清楚发生了什么。

我想可能有一个names参数添加到agg ,它将使用字典将聚合列映射到它们的新名称。 您甚至可以添加另一个参数drop_index作为布尔值来确定是否保持较高的索引级别。

所以语法会变成:

agg_dict = {'energy': ['sum',
                       lambda x: np.percentile(x, 98), # lambda
                       lambda x: np.percentile(x, 17), # lambda
                      ],
            'distance': ['sum',
                         'mean',
                         smrb.mad, # original function
                         mad_c1,   # partial function wrapping the original function
                        ]
           }

name_dict = {'energy':['energy_sum', 'energy_p98', 'energy_p17'],
             'distance':['distance_sum', 'distance_mean', 'distance_mad', 'distance_mad_c1']}


mydf.groupby('cat').agg(agg_dict, names=name_dict, drop_index=True)

或者,可以创建一个全新的方法agg_assign ,其工作方式类似于DataFrame.assign

mydf.groupby('cat').agg_assign(energy_sum=lambda x: x.energy.sum(),
                               energy_p98=lambda x: np.percentile(x.energy, 98),
                               energy_p17=lambda x: np.percentile(x.energy, 17),
                               distance_sum=lambda x: x.distance.sum(),
                               distance_mean=lambda x: x.distance.mean(),
                               distance_mad=lambda x: smrb.mad(x.distance),
                               distance_mad_c1=lambda x: mad_c1(x.distance))

我实际上更喜欢这个选项。

对于它的价值,我也强烈支持不要贬低功能。

对我来说一个重要的原因是,将 Python 的函数命名空间(与特定实现有关)与列名数据(肯定不应该知道实现的东西)混合在一起有一些非常奇怪的事情。 我们看到名为'<lambda>'列(可能是多个列)这一事实导致我严重的认知失调。

重命名方法令人讨厌,因为有这个中间步骤,其中包含不必要的(和暴露的)列名。 此外,它们很难可靠地、系统地重命名,因为对实现存在潜在的依赖性。

除此之外,嵌套的 dict 功能无疑是复杂的,但它是一个正在执行的复杂操作。

TL;DR 请不要贬值。 :)

我的贡献有两个动机。

  1. 我知道并同意减少 Pandas 臃肿 API 的动机。 即使我在减少“臃肿”API 元素的感知动机方面被误导,我仍然认为 Pandas 的 API 可以简化。
  2. 我认为拥有一本好的食谱和好的食谱比提供 API 来满足每个人的需要和欲望要好。 我并不是说通过嵌套字典进行重命名是令人满意的,因为它已经存在,我们正在讨论它的弃用。 但它确实位于简化的 API 和其他东西之间的范围内。

此外,Pandas Series 和 DataFrame 对象具有pipe方法来促进流水线化。 在这个文档段中讨论了我们可以使用pipe代替子类化来代理方法。 本着同样的精神,我们可以使用新的GroupBy.pipe来执行类似的角色,并允许我们为 groupby 对象构建代理方法。

我将使用@zertrin的例子

import numpy as np
import statsmodels.robust as smrb
from functools import partial

# The DataFrame offered up above
mydf = pd.DataFrame(
    {
        'cat': ['A', 'A', 'A', 'B', 'B', 'C'],
        'energy': [1.8, 1.95, 2.04, 1.25, 1.6, 1.01],
        'distance': [1.2, 1.5, 1.74, 0.82, 1.01, 0.6]
    },
    index=range(6)
)

# Identical dictionary passed to `agg`
funcs = {
    'energy': {
        'total_energy': 'sum',
        'energy_p98': lambda x: np.percentile(x, 98),  # lambda
        'energy_p17': lambda x: np.percentile(x, 17),  # lambda
    },
    'distance': {
        'total_distance': 'sum',
        'average_distance': 'mean',
        'distance_mad': smrb.mad,   # original function
        'distance_mad_c1': mad_c1,  # partial function wrapping the original function
    },
}

# Write a proxy method to be passed to `pipe`
def agg_assign(gb, fdict):
    data = {
        (cl, nm): gb[cl].agg(fn)
        for cl, d in fdict.items()
        for nm, fn in d.items()
    }
    return pd.DataFrame(data)

# All the API we need already exists with `pipe`
mydf.groupby('cat').pipe(agg_assign, fdict=funcs)

这导致

            distance                                                 energy                        
    average_distance distance_mad distance_mad_c1 total_distance energy_p17 energy_p98 total_energy
cat                                                                                                
A              1.480     0.355825           0.240           4.44     1.8510     2.0364         5.79
B              0.915     0.140847           0.095           1.83     1.3095     1.5930         2.85
C              0.600     0.000000           0.000           0.60     1.0100     1.0100         1.01

pipe方法在很多情况下不需要添加新的 API。 它还提供了替代我们正在讨论的已弃用功能的方法。 因此,我倾向于继续弃用。

我真的很喜欢 tdpetrou 的想法 - 使用: names=name_dict

这可以使每个人都高兴。 它使我们可以根据需要

并非如此,正如我在最初的帖子中提到的那样,这并不能解决将定义聚合操作的位置与结果列的名称分离的问题,需要额外的努力来确保两者“同步”。

我并不是说这是一个糟糕的解决方案(毕竟它解决了其他问题),但它不会像 dict 方法的 dict 那样简单和清晰。 我的意思是,在编写时,您需要保持列表的两个字典同步,并且在阅读源代码时,读者必须努力将列表的第二个字典中的名称与列表的第一个字典中的聚合定义相匹配。 在每种情况下,这都是两倍的努力。

嵌套的 dicts 有点复杂,但是按照您的方式编写它们可以非常清楚发生了什么。

我还是不明白为什么大家好像都说 dict 的 dict 很复杂。 对我来说,这是最清晰的方法。

也就是说,如果names关键字是 Pandas 团队满意的唯一解决方案,那仍然是对当前情况的改进。

@pirsquared有趣的解决方案与当前的 API。 尽管在我看来不太容易掌握(我真的不明白它是如何工作的 :confused: )

我在数据科学 subreddit 上发起了一个主题 -你讨厌熊猫什么? . 之后有人提出了他们的蔑视多指标返回一个groupby ,并指出dplyr do动词,在实现plydata 。 它恰好和agg_assign所以这很有趣。

@zertrin agg_assign将优于您的 dict of dict 方法,并且与 sql 聚合相同,并允许多个列在聚合中相互交互。 它也与DataFrame.assign

任何想法@jreback @TomAugspurger

...
mydf.groupby('cat').agg(agg_dict, names=name_dict, drop_index=True)

虽然这解决了问题,但需要在两个地方对齐键和值。 我认为不需要这种簿记代码的 API(如.agg_assign所建议的那样)不太容易出错。

还有使用API​​后清理代码的问题。 当groupby操作返回MultiIndex数据帧时,在大多数情况下,用户会撤消MultiIndex 。 使用.agg_assign的直接声明方式表明没有层次结构,没有MultiIndex输出,之后没有清理。

基于使用模式,我认为多索引输出应该严格选择加入而不是选择退出。

我最初对agg_assign提议持怀疑态度,但最后两条评论使我相信这可能是一个很好的解决方案。

特别是考虑以agg_assign(**relabeling_dict)形式使用它的可能性,从而能够像这样定义我的relabeling_dict

relabeling_dict = {
    'energy_sum': lambda x: x.energy.sum(),
    'energy_p98': lambda x: np.percentile(x.energy, 98),
    'energy_p17': lambda x: np.percentile(x.energy, 17),
    'distance_sum': lambda x: x.distance.sum(),
    'distance_mean': lambda x: x.distance.mean(),
    'distance_mad': lambda x: smrb.mad(x.distance),
    'distance_mad_c1': lambda x: mad_c1(x.distance)
}

这将非常灵活,可以解决我的 OP 中提到的所有问题。

@zertrin @has2k1

我更多地考虑了这一点,这个功能已经存在于apply 。 您只需返回一个带有索引作为新列名和值作为聚合的系列。 这允许名称中有空格,并使您能够完全按照您的意愿对列进行排序:

def my_agg(x):
    data = {'energy_sum': x.energy.sum(),
            'energy_p98': np.percentile(x.energy, 98),
            'energy_p17': np.percentile(x.energy, 17),
            'distance sum' : x.distance.sum(),
            'distance mean': x.distance.mean(),
            'distance MAD': smrb.mad(x.distance),
            'distance MAD C1': mad_c1(x.distance)}
    return pd.Series(data, index=list_of_column_order)

mydf.groupby('cat').apply(my_agg)

因此,可能不需要新方法,而只是文档中的更好示例。

@tdpetrou ,你是对的。 由于在快慢路径选择过程中的双重执行,我已经忘记了apply工作的,因为我使用我自己的版本

嗯,确实,我不可能仅仅通过阅读文档就考虑在聚合上下文中使用它......
此外,我仍然发现apply的解决方案有点过于复杂。 agg_assign方法似乎更简单易懂。

由于从来没有关于它的声明, dict-of-dict方法(尽管目前已被弃用,但已经实施并解决了所有这些问题)真的绝对不可能吗?

除了agg_assign方法, dict-of-dict看起来仍然是最简单的方法,不需要任何编码,只是不赞成使用。

agg_assign方法的优点和缺点是它将列选择推入聚合方法。 在所有的例子中, x传递到lambda是一样的东西self.get_group(group)为每个组中的self ,一个DataFrameGroupBy对象。 这很好,因为它干净地将**kwargs命名与函数中的selection分开。

缺点是您漂亮的通用聚合函数现在必须关注列选择。 天下没有免费的午餐! 这意味着你最终会得到很多像lambda x: x[col].min这样的助手。 您还需要注意诸如np.min ,它会减少所有维度,而pd.DataFrame.min会减少axis=0 。 这就是为什么像agg_assign这样的东西不等于applyapply仍然对某些方法按列操作。

我不确定这些权衡与 dict-of-dicts 方法,但我很想听听其他人的想法。 这是agg_assign的粗略草图,我将其称为agg_table以强调函数是通过表而不是列传递的:

from collections import defaultdict

import pandas as pd
import numpy as np
from pandas.core.groupby import DataFrameGroupBy

mydf = pd.DataFrame(
    {
        'cat': ['A', 'A', 'A', 'B', 'B', 'C'],
        'energy': [1.8, 1.95, 2.04, 1.25, 1.6, 1.01],
        'distance': [1.2, 1.5, 1.74, 0.82, 1.01, 0.6]
    },
    index=range(6)
)


def agg_table(self, **kwargs):
    output = defaultdict(dict)
    for group in self.groups:
        for k, v in kwargs.items():
            output[k][group] = v(self.get_group(group))

    return pd.concat([pd.Series(output[k]) for k in output],
                     keys=list(output),
                     axis=1)

DataFrameGroupBy.agg_table = agg_table

用法

>>> gr = mydf.groupby("cat")
>>> gr.agg_table(n=len,
                 foo=lambda x: x.energy.min(),
                 bar=lambda y: y.distance.min())

   n   foo   bar
A  3  1.80  1.20
B  2  1.25  0.82
C  1  1.01  0.60

我怀疑我们可以做一些事情来使这个表现不那么糟糕,但不会像.agg那样多......

来自 Pandas 核心团队的人能否解释在groupby.agg弃用重新标记字典的主要原因是什么?

我可以很容易地理解它是否会导致维护代码的太多问题,但如果它是关于最终用户的复杂性 - 我也会选择将它带回来,因为与所需的解决方法相比它非常清楚......

谢谢!

来自 Pandas 核心团队的人能否解释在 groupby.agg 中弃用重新标记字典的主要原因是什么?

你看到https://github.com/pandas-dev/pandas/pull/15931/files#diff -52364fb643114f3349390ad6bcf24d8fR461了吗?

主要原因是 dict-keys 被重载来做两件事。 对于 Series / SeriesGroupBy,它们用于命名。 对于 DataFrame/DataFrameGroupBy,它们用于选择列。

In [32]: mydf.aggregate({"distance": "min"})
Out[32]:
distance    0.6
dtype: float64

In [33]: mydf.aggregate({"distance": {"foo": "min"}})
/Users/taugspurger/Envs/pandas-dev/bin/ipython:1: FutureWarning: using a dict with renaming is deprecated and will be removed in a future version
  #!/Users/taugspurger/Envs/pandas-dev/bin/python3.6
Out[33]:
     distance
foo       0.6

In [34]: mydf.distance.agg({"foo": "min"})
Out[34]:
foo    0.6
Name: distance, dtype: float64

In [35]: mydf.groupby("cat").agg({"distance": {"foo": "min"}})
/Users/taugspurger/Envs/pandas-dev/lib/python3.6/site-packages/pandas/pandas/core/groupby.py:4201: FutureWarning: using a dict with renaming is deprecated and will be removed in a future version
  return super(DataFrameGroupBy, self).aggregate(arg, *args, **kwargs)
Out[35]:
    distance
         foo
cat
A       1.20
B       0.82
C       0.60

In [36]: mydf.groupby("cat").distance.agg({"foo": "min"})
/Users/taugspurger/Envs/pandas-dev/bin/ipython:1: FutureWarning: using a dict on a Series for aggregation
is deprecated and will be removed in a future version
  #!/Users/taugspurger/Envs/pandas-dev/bin/python3.6
Out[36]:
      foo
cat
A    1.20
B    0.82
C    0.60

这可能不是熊猫中令人困惑的事情,所以也许我们可以重新审视它:) 我大概错过了一些边缘情况。 但即使我们确实删除了 dict-of-dicts 聚合,我们仍然存在命名和列选择之间的不一致:

对于 Series / SeriesGroupBy,字典键始终用于命名输出。

对于 DataFrame / DataFrameGroupby,dict 键始终用于选择。 使用 dict-of-dicts 我们选择一列,然后内部 dict 用于命名输出,就像 Series / SeriesGroupBy 一样。

我们之前简要讨论过这个问题(在关于弃用的长期讨论中),我在这里提出了类似的建议: https :

问题是 dicts 既用于“选择”(您希望在哪个列上应用此函数)和“重命名”(应用此函数时生成的列名应该是什么)。 除了 dicts 之外,另一种语法可以是关键字参数,如agg_assign提案中所讨论的那样。
我仍然赞成探索这种可能性,无论是在agg本身中还是在agg_assign类的新方法中。

我当时提出的类似于agg_assign但是每个关键字使用一个 dict 而不是 lambda 函数。 翻译成这里的例子,这将是这样的:

mydf.groupby('cat').agg(
    energy_sum={'energy': 'sum'},
    energy_p98={'energy': lambda x: np.percentile(x, 98)},
    energy_p17={'energy': lambda x: np.percentile(x, 17)},
    distance_sum={'distance': 'sum'},
    distance_mean={'distance': 'mean'},
    distance_mad={'distance': smrb.mad},
    distance_mad_c1={'distance': mad_c1})

我不确定这是否一定更易读或更容易编写为所有 lambdas 的版本,但是,这个版本可能会更高效,因为 Pandas 仍然可以在您执行的那些列上使用优化的 sum、mean 等实现没有 lambda 或用户指定的函数。

这种方法的一个大问题是df.groupby('cat').agg(foo='mean')是什么意思? 由于您没有进行任何选择(类似于之前的{'col1' : {'foo': 'mean'}, 'col2': {'foo':'mean'}, 'col3': ...} ),这在逻辑上会将“均值”应用于所有列。 但是,这会导致多索引列,而在上面的示例中,我认为最好不要以 MI 列结束。

我认为上述内容可以在现有的agg向后兼容,但问题是是否需要这样做。
我也认为这可以很好地扩展到series情况,例如:

mydf.groupby('cat').distance.agg(
    distance_sum='sum',
    distance_mean='mean',
    distance_mad=smrb.mad,
    distance_mad_c1=mad_c1)

(如果您不喜欢所有的 dicts / lambda,您甚至可以考虑对“距离”执行一次上述操作,对“能量”执行一次并连接结果)

@TomAugspurger在您的agg_table示例简单实现中,迭代要应用的不同函数,而不是迭代组,最后通过 axis=1 连接新列不是更好吗而不是通过 axis=0 连接新形成的行?

顺便说一句, @zertrin @tdpetrou @smcateer @pirsquared和其他人,非常感谢提出这个问题并提供如此详细的反馈。 这种反馈和社区参与非常重要!

我实际上真的很喜欢@tdpetrou建议的模式(将 apply 与返回系列的函数一起使用)——甚至可能比

如果函数返回pd.Series(data, index=data.keys()) ,我们是否保证以正确的顺序获取索引? (只是考虑如何最好地在我的代码中实现模式 - 冒着偏离主题的风险)。

编辑:抱歉,我误解了索引参数的要点(这里是可选的,仅当您想指定列的顺序时才需要 - 返回pd.Series(data)为我完成这项工作)。

@tdpetrou的示例是否适用于firstlast聚合?

我不得不像这样求助于头/尾

def agg_funcs(x):
    data = {'start':x['DATE_TIME'].head(1).values[0],
           'finish':x['DATE_TIME'].tail(1).values[0],
           'events':len(x['DATE_TIME'])}
    return pd.Series(data, index = list(data.keys()))

results = df.groupby('col').apply(agg_funcs)

我仍然想解决这个问题,但我认为它不会在 0.23 中完成。

@tdpetrou的方法可以在不定义我们永远不会在我们的代码中再次使用的函数的情况下工作吗? 来自 Q/Kdb+ 世界(类似于 SQL),我很困惑为什么我们需要为简单的 select 语句创建任何时间变量/函数。

在这里。

老实说,经过这么长时间以及#15931 和这里的大量讨论,我仍然不相信这是弃用重新标记字典的好主意。

最后,这里提出的替代方案对用户来说都比当前的重新标记字典方法更直观,恕我直言。 当它在文档中时,仅通过一个示例就清楚这是如何工作的,并且非常灵活。

当然,pandas 开发人员可能仍然不这么认为,只是站在用户的角度。

即使是重新标记 dict 方法也不是很直观。 在我看来,语法应该类似于 SQL - func(column_name) as new_column_name 。 在 Python 中,我们可以使用三项元组来做到这一点。 (func, column_name, new_column_name) 。 这就是 dexplo 进行 groupby 聚合的方式。

dexplo

@zertrin你对我上面的提议有什么反馈吗: https :
最后,它有点颠倒了 dict 的顺序:而不是“{col: {name: func}}”,它会是“**{name: {col: func}}”

@jorisvandenbossche我已经考虑过你的方法。 问题是,我真的看不出它比当前的方法带来了哪些额外的优势。

说得更直白一点,给出以下选择:

  1. 取消弃用当前运行良好的行为(删除几行弃用代码,重新添加已删除的文档)
  2. 实施您的提议(对代码进行重大更改,继续弃用当前方法,所有用户都需要调整其代码)

我不明白为什么我们应该选择 2,除非它从开发人员和用户的角度带来了有意义和切实的优势。

要解决上述提案中的一些要点:

问题是 dicts 既用于“选择”(您希望在哪个列上应用此函数)和“重命名”(应用此函数时生成的列名应该是什么)。

由于它之前有很好的记录,我认为这对用户来说不是问题。 就个人而言,我在查看文档中的示例后立即明白了这一点。 (编辑:我想:_“耶!非常有用的构造,它完全符合我的要求。很好。”_)

除了 dicts 之外,另一种语法可能是关键字参数

使用 dict-of-dict 方法的吸引力之一是用户可以使用其他代码轻松地动态生成它。 正如您在上面的评论中指出的那样,像在您的命题中一样移动到关键字参数仍然可以通过**{name: {col: func}}构造实现这一点。 所以我不反对你的提议。 当我们已经用当前实施的系统实现了相同级别的功能时,我只是没有看到增加的价值和进行此类更改的必要性。

最后,如果 Pandas 核心开发人员对当前的方法有强烈的感觉,那么你的提议将是 _okay_。 我只是没有看到作为_user_ 的任何好处。 (事实上​​,我看到了更改所有现有用户代码以使其再次与新提议一起工作的缺点)。

@zertrin我们昨天与一些核心开发人员讨论了这个问题,但没有在这里写摘要。 所以现在我要这样做,回答你的评论


首先声明,像 SQL“SELECT avg(col2) as col2_avg”这样的基本功能应该可以工作并且很容易的概念是我们完全同意的,我们真的很想为此找到一个解决方案。

除了我们决定弃用它的最初原因(可能会也可能不会那么强)之外,当前(不推荐使用的)dicts dicts 也不是那么理想,因为这会创建一个您实际上永远不想要的 MultiIndex:

In [1]: df = pd.DataFrame({'A': ['a', 'b', 'a'], 'B': range(3), 'C': [.1, .2, .3]})

In [3]: gr = df.groupby('A')

In [4]: gr.agg({'B': {'b_sum': 'sum'}, 'C': {'c_mean': 'mean', 'c_count': 'count'}})
Out[4]: 
        C            B
  c_count c_mean b_sum
A                     
a       2    0.2     2
b       1    0.2     1

在上面,MultiIndex 的第一级是多余的,因为您已经专门重命名了列(在 OP 中的示例中,这也是直接删除列的第一级之后)。
然而很难改变这一点,因为你也可以做一些事情,比如gr.agg(['sum', 'mean'])或(混合) gr.agg({'B': ['sum', 'mean'], 'C': {'c_mean': 'mean', 'c_count': 'count'}})需要 MultiIndex 并且有意义。

所以在上面的讨论中提到的一个建议是有一种方法来单独指定最终的列名(例如 https://github.com/pandas-dev/pandas/issues/18366#issuecomment-346683449)。
添加例如一个额外的关键字到aggregate以指定列名称,例如

gr.agg({'B': 'sum', 'C': ['mean', 'count']}, columns=['b_sum', 'c_mean', 'c_count'])

将是可能的。
但是,如果我们拆分列/函数规范和新的列名,我们还可以使其比 new 关键字更通用,并执行以下操作:

gr.agg({'B': 'sum', 'C': ['mean', 'count']}).rename(columns=['b_sum', 'c_mean', 'c_count'])

这需要解决https://github.com/pandas-dev/pandas/issues/14829 (我们想要为 0.24.0 做的事情)。
重要提示:为此,我们确实需要修复 lambda 函数的重复名称问题,因此如果我们想支持此解决方案,我们应该对名称进行某种自动重复数据删除。)


然后,我们仍然喜欢重命名关键字参数的方式。 原因如下:

  • 它类似于assign在 Pandas 中的工作方式,也与groupby().aggregate()在 ibis 中的工作方式一致(也类似于它在例如 R 中的 dplyr 中的外观)
  • 它直接为您提供您想要的非分层列名(无 MultiIndex)
  • 对于简单的情况(也例如对于系列情况),我认为它比 dict 的 dict 更简单

我们仍然对它的外观进行了一些讨论。 我上面提出的是(使用与我的第一个示例中相同的列/函数选择):

gr.agg(b_sum={'B': 'sum'}, c_mean={'C': 'mean'}, c_count={'C': 'count'})

您仍然可以将此规范构建为 dict 的 dict,但与当前(已弃用)版本相比,内部和外部级别交换了:

gr.agg(**{'b_sum': {'B': 'sum'}, 'c_mean': {'C': 'mean'}, 'c_count': {'C': 'count'})

(我们可以有一个示例辅助函数,将 dicts 的现有 dicts 转换为此版本)

然而,字典总是只有一个{col: func} ,而那些多个单元素字典看起来有点奇怪。 所以我们想到的另一种方法是使用元组:

gr.agg(b_sum=('B', 'sum'), c_mean=('C', 'mean'), c_count=('C', 'count'))

看起来好一点,但另一方面{'B': 'sum'} dict 与用于指定要应用函数的列的其他 API 一致。


上面的两个建议(之后更容易重命名,以及基于关键字的命名)原则上是正交的,但同时拥有这两个建议(或基于进一步讨论的其他建议)可能会很好

感谢您在此处转发开发人员的当前想法😃

我承认弃用的 dict-of-dict 方法的(在我看来,唯一的)缺点是产生的 MultiIndex。 如果用户传递附加选项(是的 YAO :-/ ),则可能会展平。

如前所述,我不反对第二个版本,只要它仍然可能:

  • 以某种方式动态生成事物并解压它(感谢**{}构造,是的 Python!)
  • 将重命名和聚合规范保持在一起(必须跟踪两个列表以使其顺序保持不变,作为用户恕我直言,这很烦人)
  • 由于(可能缺少或冲突)函数名称,因此无需解决方法即可使用 lambda 或部分函数。

因此,我认为最后一个建议(对于 col>func 映射使用字典或元组)是可以的。

如果您真的愿意,可以实施上一条评论中的第一个建议,但我对此的反馈是,作为用户,我不会选择使用它而不是第二个替代方案,因为在两者之间保持同步的痛苦两个清单。

在今天的开发会议上讨论。

简短的摘要

  1. @jorisvandenbossche将尝试实现gr.agg(b_sum=("B", "sum), ...) ,即当没有arg传递给*GroupBy.agg ,将 kwargs 解释为<output_name>=(<selection>, <aggfunc>)
  2. 正交这个问题,我们想实现MutliIndex.flatten并提供flatten=True关键字.agg

也许这有帮助:我的弃用解决方法是这些辅助函数,它们用正确命名的函数列表替换 alias->aggr 映射:

def aliased_aggr(aggr, name):
    if isinstance(aggr,str):
        def f(data):
            return data.agg(aggr)
    else:
        def f(data):
            return aggr(data)
    f.__name__ = name
    return f

def convert_aggr_spec(aggr_spec):
    return {
        col : [ 
            aliased_aggr(aggr,alias) for alias, aggr in aggr_map.items() 
        ]  
        for col, aggr_map in aggr_spec.items() 
    }

这使旧行为具有:

mydf_agg = mydf.groupby('cat').agg(convert_aggr_spec{
    'energy': {
        'total_energy': 'sum',
        'energy_p98': lambda x: np.percentile(x, 98),  # lambda
        'energy_p17': lambda x: np.percentile(x, 17),  # lambda
    },
    'distance': {
        'total_distance': 'sum',
        'average_distance': 'mean',
        'distance_mad': smrb.mad,   # original function
        'distance_mad_c1': mad_c1,  # partial function wrapping the original function
    },
}))

这与

mydf_agg = mydf.groupby('cat').agg({
    'energy': [ 
        aliased_aggr('sum', 'total_energy'),
        aliased_aggr(lambda x: np.percentile(x, 98), 'energy_p98'),
        aliased_aggr(lambda x: np.percentile(x, 17), 'energy_p17')
    ],
    'distance': [
         aliased_aggr('sum', 'total_distance'),
         aliased_aggr('mean', 'average_distance'),
         aliased_aggr(smrb.mad, 'distance_mad'),
         aliased_aggr(mad_c1, 'distance_mad_c1'),
    ]
})

这对我有用,但在某些极端情况下可能不起作用......

更新:发现不需要重命名,因为聚合规范中的元组被解释为 (alias, aggr)。 所以不需要alias_aggr函数,转换变成:

def convert_aggr_spec(aggr_spec):
    return {
        col : [ 
           (alias,aggr) for alias, aggr in aggr_map.items() 
        ]  
        for col, aggr_map in aggr_spec.items() 
    }

我只是想在这里插一句,作为另一个用户,他真的非常缺少在任何函数上聚合列并立即在同一行中重命名的功能。 我_从来没有_发现自己使用了 Pandas 返回的 MultiIndex - 我要么立即将它展平,要么我实际上想手动指定我的列名,因为它们实际上意味着特定的东西。

我会对这里提出的任何方法感到满意:类似 SQL 的语法(我实际上发现自己已经在 Pandas 中大量使用了.query() ),恢复到折旧的行为,任何其他建议。 目前的方法已经引起了使用 R 的同事的嘲笑。

我最近甚至发现自己使用 PySpark 而不是 Pandas,即使它不是必需的,只是因为我更喜欢它的语法:

df.groupby("whatever").agg(
    F.max("col1").alias("my_max_col"),
    F.avg("age_col").alias("average_age"),
    F.sum("col2").alias("total_yearly_payments")
)

此外,在大多数情况下,PySpark 编写起来比 Pandas 复杂得多,这看起来更简洁! 所以我非常感谢这方面的工作还有待完成:-)

我认为我们对此功能有一个商定的语法; 我们需要有人
实施它。

2019 年 3 月 27 日,星期三,上​​午 9:01,Thomas Kastl通知@github.com
写道:

我只是想作为另一个用户,真的,真的
缺少在任何函数上聚合列的功能,并且
立即在同一行中重命名它。 我从未发现自己
使用 pandas 返回的 MultiIndex - 我要么立即将其展平,
或者我实际上想手动指定我的列名,因为它们
实际上是指一些特定的东西。

我会对这里提出的任何方法感到满意:SQL-like syntax
(实际上我发现自己已经在 Pandas 中大量使用 .query() 了),
恢复到折旧的行为,任何其他建议。 这
目前的方法已经让我遭到使用 R 的同事的嘲笑。

我最近甚至发现自己使用 PySpark 而不是 Pandas 即使
没有必要,只是因为我更喜欢语法:

df.groupby("whatever").agg( F.max("col1").alias("my_max_col"),
F.avg("age_col").alias("average_age"),
F.sum("col2").alias("total_yearly_payments") )

在大多数情况下,PySpark 编写起来也比 Pandas 复杂得多,
这看起来更干净了! 所以我非常感谢这项工作
这仍然要完成:-)


你收到这个是因为你被提到了。
直接回复本邮件,在GitHub上查看
https://github.com/pandas-dev/pandas/issues/18366#issuecomment-477168767
或静音线程
https://github.com/notifications/unsubscribe-auth/ABQHIkCYYsah5siYA4_z0oop_ufIB3h8ks5va3nJgaJpZM4QjSLL
.

我将尝试达到 0.25.0

我在https://github.com/pandas-dev/pandas/pull/26399 上提出了 PR **kwargs允许这种重命名和特定列聚合的混合,并理解值应该是(selection, aggfunc)元组。

In [2]: df = pd.DataFrame({'kind': ['cat', 'dog', 'cat', 'dog'],
   ...:                    'height': [9.1, 6.0, 9.5, 34.0],
   ...:                    'weight': [7.9, 7.5, 9.9, 198.0]})

In [3]: df
Out[3]:
  kind  height  weight
0  cat     9.1     7.9
1  dog     6.0     7.5
2  cat     9.5     9.9
3  dog    34.0   198.0

In [4]: df.groupby('kind').agg(min_height=('height', 'min'), max_weight=('weight', 'max'))
Out[4]:
      min_height  max_weight
kind
cat          9.1         9.9
dog          6.0       198.0

这有一些限制

  • 这对其余的熊猫来说有些特殊。 sytanx (output_name=(selection, aggfunc))并没有真正出现在其他任何地方(尽管.assign确实使用了output_name=...模式)
  • 不是 python 标识符的输出名称的拼写很难看: .agg(**{'output name': (col, func)})
  • 它只是 Python 3.6+,或者我们需要一些丑陋的 3.5 及更早的 hack,因为**kwargs的顺序以前没有保留
  • aggfunc 需要是一元函数。 如果您的自定义 aggfunc 需要额外的参数,您需要先部分应用它

还有一个实现细节,不支持同一列的多个lambda aggfuncs,尽管可以稍后修复。


我怀疑这里订阅的大多数人都会支持对已弃用行为的某些替代方案。 人们对这个具体有什么看法?

cc @WillAyd如果我错过了您的任何顾虑。

@TomAugspurger

感谢您推动这一进程。

这有一些限制

  • 这对其余的熊猫来说有些特殊。 sytanx (output_name=(selection, aggfunc))并没有真正出现在其他任何地方(尽管.assign确实使用了output_name=...模式)

我不禁觉得这种论点似乎与最初促使弃用现有实现的论点非常相似。

您能否分享为什么我们从这种新方法中比旧方法受益更多_就该特定论点而言_?

我已经想到的一个好处是(对于 py3.6+)我们可以单独选择列的输出顺序。

  • 不是 python 标识符的输出名称的拼写很难看: .agg(**{'output name': (col, func)})

不知何故,旧方法在这方面更好。 但正如我之前所说,只要可以使用**{...}构造来动态构建聚合,我就很高兴了。

  • 它只是 Python 3.6+,或者我们需要一些丑陋的 3.5 及更早的 hack,因为**kwargs的顺序以前没有保留

之前它是如何工作的(现有的 dict-of-dict 功能)? 订单是否以某种方式得到保证?

  • aggfunc 需要是一元函数。 如果您的自定义 aggfunc 需要额外的参数,您需要先部分应用它

只是为了确认我的理解: aggfunc 可以是任何返回有效值的可调用对象,对吗? (除了像'min''max'等“经常使用”的字符串 aggfungs 之外)。 和以前相比有什么不同吗? (即一元限制不是已经存在了吗?)

还有一个实现细节,不支持同一列的多个lambda aggfuncs,尽管可以稍后修复。

是的,这有点烦人,但只要它只是一个临时限制并且可以解决这个问题,那就可以了。

我怀疑这里订阅的大多数人都会支持对已弃用行为的某些替代方案。 人们对这个具体有什么看法?

好吧,无论如何,我认为一步聚合和重命名非常重要。 如果旧的行为真的不是一种选择,那么这个替代方案可以做到。

您能否就该特定论点分享为什么我们从这种新方式中受益比旧方式更多。

我可能记错了,但我相信 SeriesGroupby.agg 和 DataFrameGroupby.agg 在字典中的外键之间有不同的含义(是列选择还是输出命名?)。 使用此语法,我们可以始终使用关键字表示输出名称。

不知何故,旧方法在这方面更好。

区别只是**吗? 否则我认为相同的限制是共享的。

之前它是如何工作的(现有的 dict-of-dict 功能)? 订单是否以某种方式得到保证?

排序键,这就是我现在在 PR 中所做的。

只是为了确认我的理解: aggfunc 可以是任何返回有效值的可调用对象,对吗?

这是区别

In [21]: df = pd.DataFrame({"A": ['a', 'a'], 'B': [1, 2], 'C': [3, 4]})

In [22]: def aggfunc(x, myarg=None):
    ...:     print(myarg)
    ...:     return sum(x)
    ...:

In [23]: df.groupby("A").agg({'B': {'foo': aggfunc}}, myarg='bar')
/Users/taugspurger/sandbox/pandas/pandas/core/groupby/generic.py:1308: FutureWarning: using a dict with renaming is deprecated and will be removed in a future version
  return super().aggregate(arg, *args, **kwargs)
None
Out[23]:
    B
  foo
A
a   3

对于替代提案,我们为输出列名称保留了**kwargs 。 所以你需要functools.partitial(aggfunc, myarg='bar')

好的,谢谢,我认为建议的方法是 👍 用于第一次迭代(并且一旦删除了多个 lambda 实现限制,就可以作为替代品)

此页面是否有帮助?
0 / 5 - 0 等级

相关问题

scls19fr picture scls19fr  ·  3评论

ebran picture ebran  ·  3评论

andreas-thomik picture andreas-thomik  ·  3评论

MatzeB picture MatzeB  ·  3评论

amelio-vazquez-reina picture amelio-vazquez-reina  ·  3评论