此问题是根据 #15931 中的讨论创建的,之后弃用了groupby.agg
的重新标记字典。 下面总结的很多内容在前面的讨论中已经讨论过。 我会特别推荐https://github.com/pandas-dev/pandas/pull/15931#issuecomment -336139085,其中也明确说明了问题。
弃用 #15931 背后的动机主要与在 Series 和 Dataframe 之间为agg()
带来一致的接口有关(另请参阅 #14668 以了解上下文)。
一些人将使用嵌套字典的重新标记功能描述为过于复杂和/或不一致,因此不推荐使用。
然而,这是有代价的:无法同时聚合和重命名会导致非常烦人的问题和一些向后不兼容,没有合理的解决方法可用:
__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
仍然
分步处理。
聚合后无法控制列名,我们可以自动获得的最佳方式是原始列名和 _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_ 功能的弃用深感遗憾。 我希望上面的例子能把痛点说清楚。
_可选读:_
关于已经进行了几个月的拉取请求中的上述讨论,我最近才意识到我对这种弃用如此困扰的原因之一:“聚合和重命名”是很自然的事情SQL 中的 GROUP BY 聚合,因为在 SQL 中您通常直接在聚合表达式旁边提供目标列名称,例如SELECT col1, avg(col2) AS col2_mean, stddev(col2) AS col2_var FROM mytable GROUP BY col1
。
我_不是
(* 我个人不同意 dict-of-dict 方法很复杂。)
@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 请不要贬值。 :)
我的贡献有两个动机。
此外,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: )
...
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
这样的东西不等于apply
。 apply
仍然对某些方法按列操作。
我不确定这些权衡与 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的示例是否适用于first
和last
聚合?
我不得不像这样求助于头/尾
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 聚合的方式。
@zertrin你对我上面的提议有什么反馈吗: https :
最后,它有点颠倒了 dict 的顺序:而不是“{col: {name: func}}”,它会是“**{name: {col: func}}”
@jorisvandenbossche我已经考虑过你的方法。 问题是,我真的看不出它比当前的方法带来了哪些额外的优势。
说得更直白一点,给出以下选择:
我不明白为什么我们应该选择 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 中的外观)我们仍然对它的外观进行了一些讨论。 我上面提出的是(使用与我的第一个示例中相同的列/函数选择):
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!)因此,我认为最后一个建议(对于 col>func 映射使用字典或元组)是可以的。
如果您真的愿意,可以实施上一条评论中的第一个建议,但我对此的反馈是,作为用户,我不会选择使用它而不是第二个替代方案,因为在两者之间保持同步的痛苦两个清单。
在今天的开发会议上讨论。
简短的摘要
gr.agg(b_sum=("B", "sum), ...)
,即当没有arg
传递给*GroupBy.agg
,将 kwargs 解释为<output_name>=(<selection>, <aggfunc>)
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
这有一些限制
(output_name=(selection, aggfunc))
并没有真正出现在其他任何地方(尽管.assign
确实使用了output_name=...
模式).agg(**{'output name': (col, func)})
**kwargs
的顺序以前没有保留还有一个实现细节,不支持同一列的多个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 实现限制,就可以作为替代品)
最有用的评论
对于它的价值,我也强烈支持不要贬低功能。
对我来说一个重要的原因是,将 Python 的函数命名空间(与特定实现有关)与列名数据(肯定不应该知道实现的东西)混合在一起有一些非常奇怪的事情。 我们看到名为
'<lambda>'
列(可能是多个列)这一事实导致我严重的认知失调。重命名方法令人讨厌,因为有这个中间步骤,其中包含不必要的(和暴露的)列名。 此外,它们很难可靠地、系统地重命名,因为对实现存在潜在的依赖性。
除此之外,嵌套的 dict 功能无疑是复杂的,但它是一个正在执行的复杂操作。
TL;DR 请不要贬值。 :)