在Pandas的聚合函数中如何命名返回的列?

208
我在使用Pandas的groupby功能时遇到了问题。我已经阅读了文档,但是我无法弄清楚如何对多个列应用聚合函数并为这些列设置自定义名称。
这个方法非常接近,但返回的数据结构具有嵌套的列标题:
data.groupby("Country").agg({
  "column1": {"foo": sum()},
  "column2": {"mean": np.mean, "std": np.std}
})

我想计算第二列的平均值和标准差,但将这些列命名为"mean"和"std"。我有什么遗漏吗?

嗨@david_chouinard,实际上我有同样的问题。您认为在今天的pandas 17.1中,所提出的解决方案仍然是最佳选择吗? - ℕʘʘḆḽḘ
6个回答

352

针对pandas版本大于等于0.25

重新引入了命名返回的聚合列的功能,其已在主分支中实现,并已定于pandas 0.25发布。新的语法是.agg(new_col_name=('col_name', 'agg_func'),可参考上述链接中的PR示例。

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

根据 这个 PR ,使用此语法和我之前建议的两步重命名语法,还可以使用多个 lambda 表达式。再次强调,从 PR 中的示例复制:

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

In [3]: df.groupby("A").agg({'B': [lambda x: 0, lambda x: 1]})
Out[3]:
         B
  <lambda> <lambda 1>
A
a        0          1

然后使用.rename(),或者一次性完成:

In [4]: df.groupby("A").agg(b=('B', lambda x: 0), c=('B', lambda x: 1))
Out[4]:
   b  c
A
a  0  0

对于Pandas < 0.25版本

目前被接受的答案由unutbu提供,这是在Pandas版本<= 0.20中完成此操作的一种很好的方法。然而,从Pandas 0.20开始,使用此方法会引发警告,指示将来的Pandas版本中将不再支持该语法。

Series:

FutureWarning: 使用字典对Series进行聚合已过时,并将在将来的版本中删除

DataFrames:

FutureWarning:使用带有重命名的字典已过时,并将在将来的版本中删除

根据Pandas 0.20变更日志,在聚合时推荐重命名列的方式如下。

# Create a sample data frame
df = pd.DataFrame({'A': [1, 1, 1, 2, 2],
                   'B': range(5),
                   'C': range(5)})

# ==== SINGLE COLUMN (SERIES) ====
# Syntax soon to be deprecated
df.groupby('A').B.agg({'foo': 'count'})
# Recommended replacement syntax
df.groupby('A').B.agg(['count']).rename(columns={'count': 'foo'})

# ==== MULTI COLUMN ====
# Syntax soon to be deprecated
df.groupby('A').agg({'B': {'foo': 'sum'}, 'C': {'bar': 'min'}})
# Recommended replacement syntax
df.groupby('A').agg({'B': 'sum', 'C': 'min'}).rename(columns={'B': 'foo', 'C': 'bar'})
# As the recommended syntax is more verbose, parentheses can
# be used to introduce line breaks and increase readability
(df.groupby('A')
    .agg({'B': 'sum', 'C': 'min'})
    .rename(columns={'B': 'foo', 'C': 'bar'})
)

请参阅0.20版本更新日志以获取更多详细信息。

回应@JunkMechanic的评论,更新于2017年01月03日。

在旧格式字典语法中,由于这些将根据传递的字典中的键重命名,因此可以向.agg传递多个lambda函数:

>>> df.groupby('A').agg({'B': {'min': lambda x: x.min(), 'max': lambda x: x.max()}})

    B    
  max min
A        
1   2   0
2   4   3

多个函数也可以作为一个列表传递给单个列:

>>> df.groupby('A').agg({'B': [np.min, np.max]})

     B     
  amin amax
A          
1    0    2
2    3    4

然而,这种方法不能应用于lambda函数,因为它们是匿名的并且都返回<lambda>,这会导致名称冲突:

>>> df.groupby('A').agg({'B': [lambda x: x.min(), lambda x: x.max]})
SpecificationError: Function names must be unique, found multiple named <lambda>

为了避免SpecificationError,可以事先定义具名函数而不是使用lambda。合适的函数名称也可以避免在之后对数据框调用.rename。这些函数可以使用与上面相同的列表语法进行传递:

>>> def my_min(x):
>>>     return x.min()

>>> def my_max(x):
>>>     return x.max()

>>> df.groupby('A').agg({'B': [my_min, my_max]})

       B       
  my_min my_max
A              
1      0      2
2      3      4

4
谢谢您的更新。我经常使用这种模式:df = df.groupby('col_to_grpd_by').agg({'quantity': { 'mu': lambda series: stats.norm.fit(series)[0], 'sigma': lambda series: stats.norm.fit(series)[1], 'active': 'count', }})。在以后的处理中,应如何处理它?我所能想到的唯一方法是定义两个单独的函数,以返回元组中的相应元素,从stats.norm.fit中。请忽略我正在使用norm的事实。它可能是不同的分布。 - JunkMechanic
@JunkMechanic 我认为你需要先定义它们,然后将它们作为列表传递。我已经更新了答案,并提供了一个示例。 - joelostblom
5
他们为什么要废除.agg({'B': {'min': lambda x: x.min(), 'max': lambda x: x.max()}})语法?它看起来非常有用,而且比仅用于此目的时定义命名函数更加简洁。 - sheridp
从上面链接的0.20版本更改日志中可以看到:“然而,.agg(..) 还可以接受一个允许对结果列进行‘重命名’的字典。这是一个复杂和混乱的语法,而且在Series和DataFrame之间也不一致。我们正在废除这种‘重命名’功能。” - joelostblom
1
@joelostblom - 看起来你在更新部分的选项中使用了这个代码:df.groupby('A').agg({'B': [lambda x: x.min(), lambda x: x.max()]}),现在它会自动为 lambda 列名添加后缀。 @JunkMechanic, @sheridp - 可以通过在字典中定义命名聚合,然后在 agg 方法中解包来获得类似于旧行为的选项。例如:df.groupby('A').agg(**{"min": ("B", lambda x: x.min()),"max": ("B", lambda x: x.max())}) - Mad
显示剩余2条评论

148
这将从分层列索引中删除最外层级别:
df = data.groupby(...).agg(...)
df.columns = df.columns.droplevel(0)

如果您想保留最外层级别,可以对多级列使用ravel()函数以形成新标签:

df.columns = ["_".join(x) for x in df.columns.ravel()]
< p > 更新:在较新的pandas中,不再使用.ravel(),而是使用.tolist().to_numpy()。使用df.columns = ["_".join(x) for x in df.columns.tolist()]来将列名中的空格替换为下划线。


例如:

import pandas as pd
import pandas.rpy.common as com
import numpy as np

data = com.load_data('Loblolly')
print(data.head())
#     height  age Seed
# 1     4.51    3  301
# 15   10.89    5  301
# 29   28.72   10  301
# 43   41.74   15  301
# 57   52.70   20  301

df = data.groupby('Seed').agg(
    {'age':['sum'],
     'height':['mean', 'std']})
print(df.head())
#       age     height           
#       sum        std       mean
# Seed                           
# 301    78  22.638417  33.246667
# 303    78  23.499706  34.106667
# 305    78  23.927090  35.115000
# 307    78  22.222266  31.328333
# 309    78  23.132574  33.781667

df.columns = df.columns.droplevel(0)
print(df.head())

产量
      sum        std       mean
Seed                           
301    78  22.638417  33.246667
303    78  23.499706  34.106667
305    78  23.927090  35.115000
307    78  22.222266  31.328333
309    78  23.132574  33.781667

或者,保留索引的第一级:

df = data.groupby('Seed').agg(
    {'age':['sum'],
     'height':['mean', 'std']})
df.columns = ["_".join(x) for x in df.columns.ravel()]

产量
      age_sum   height_std  height_mean
Seed                           
301        78    22.638417    33.246667
303        78    23.499706    34.106667
305        78    23.927090    35.115000
307        78    22.222266    31.328333
309        78    23.132574    33.781667

2
可以运行,但是会去掉分组列,因为它在第0级别 :( - Mugen
4
请注意,这种语法在未来的 pandas 版本中将被弃用。有关详细信息,请参阅0.20 更改日志,我已在我的答案中进行了总结。 - joelostblom
2
@Mugen 抱歉回复晚了(非常晚),但这很容易解决,你只需要执行 df.columns = ['_'.join(x) if isinstance(x,tuple) else x for x in df.columns.ravel()] 即可。这利用了聚合列唯一是元组的事实,所以如果你的列名中有其他元组,请谨慎操作。 - Lucas H

13

我同意原帖中的观点,即在同一位置命名和定义输出列似乎更加自然和一致(例如像R语言中 tidyverse 的 summarize 那样),但目前在 pandas 中的解决方法是先使用 assign 方法创建具有所需名称的新列,然后再进行聚合:

data.assign(
    f=data['column1'],
    mean=data['column2'],
    std=data['column2']
).groupby('Country').agg(dict(f=sum, mean=np.mean, std=np.std)).reset_index()

使用reset_index方法可以将'Country', 'f', 'mean', 和 'std'这些列变成常规列,并分配一个独立的整数索引。


8
如果您想要与JMP类似的行为,创建列标题以保留来自多重索引的所有信息,您可以使用以下方法:
newidx = []
for (n1,n2) in df.columns.ravel():
    newidx.append("%s-%s" % (n1,n2))
df.columns=newidx

它将会改变您的数据框架,例如:
    I                       V
    mean        std         first
V
4200.0  25.499536   31.557133   4200.0
4300.0  25.605662   31.678046   4300.0
4400.0  26.679005   32.919996   4400.0
4500.0  26.786458   32.811633   4500.0

to

    I-mean      I-std       V-first
V
4200.0  25.499536   31.557133   4200.0
4300.0  25.605662   31.678046   4300.0
4400.0  26.679005   32.919996   4400.0
4500.0  26.786458   32.811633   4500.0

你也可以添加一个条件检查,以便在没有第二行的情况下获取相同的列名。if n2 == '': new_col_name.append("%s" % n1) else: new_col_name.append("%s_%s" % (n1, n2)) - Adarsh Madrecha
您不需要使用.ravel(),我认为df.columns就足够了! - Yas

1

受@Joel Ostblom的启发

对于那些已经拥有可用于仅聚合的字典的人,您可以使用/修改以下代码进行新版本聚合,将聚合和重命名部分分开。如果有多个项,请注意嵌套字典。

def agg_translate_agg_rename(input_agg_dict):
    agg_dict = {}
    rename_dict = {}
    for k, v in input_agg_dict.items():
        if len(v) == 1:
            agg_dict[k] = list(v.values())[0]
            rename_dict[k] = list(v.keys())[0]
        else:
            updated_index = 1
            for nested_dict_k, nested_dict_v in v.items():
                modified_key = k + "_" + str(updated_index)
                agg_dict[modified_key] = nested_dict_v
                rename_dict[modified_key] = nested_dict_k
                updated_index += 1
    return agg_dict, rename_dict

one_dict = {"column1": {"foo": 'sum'}, "column2": {"mean": 'mean', "std": 'std'}}
agg, rename = agg_translator_aa(one_dict)

我们得到

agg = {'column1': 'sum', 'column2_1': 'mean', 'column2_2': 'std'}
rename = {'column1': 'foo', 'column2_1': 'mean', 'column2_2': 'std'}

如果有更聪明的方法,请告诉我。谢谢。


似乎是四年后唯一可行的解决方案。 - Ufos
1
或者这个也可以工作:df.columns = ['_'.join(a) for a in df.columns.to_flat_index()] - Ufos

1

比如这种数据框,列名有两个层级:

 shop_id  item_id   date_block_num item_cnt_day       
                                  target              
0   0       30          1            31               

我们可以使用以下代码: df.columns = [col[0] if col[-1]=='' else col[-1] for col in df.columns.values] 结果为:
 shop_id  item_id   date_block_num target              
0   0       30          1            31 

我最喜欢这种方法。稍加修改后,我得到了以下结果:[col[0] if col[-1]=='' else col[-1]+'_'+col[0] for col in path_info.columns.values] - Moein

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