如何从pandas groupby().sum()的输出中创建一个新列?

165

尝试从groupby计算创建一个新的列。在下面的代码中,我得到了每个日期的正确计算值(见下面的组),但是当我尝试创建一个新的列(df['Data4'])时,我得到了NaN。因此,我正在尝试在数据帧中创建一个新的列,该列的值为所有日期的Data3之和,并将其应用于每个日期行。例如,2015-05-08在2行中(总计为50 + 5 = 55),在这个新列中我想要在这两行中都有55。

import pandas as pd

df = pd.DataFrame({
    'Date' : ['2015-05-08', '2015-05-07', '2015-05-06', '2015-05-05', '2015-05-08', '2015-05-07', '2015-05-06', '2015-05-05'], 
    'Sym'  : ['aapl', 'aapl', 'aapl', 'aapl', 'aaww', 'aaww', 'aaww', 'aaww'], 
    'Data2': [11, 8, 10, 15, 110, 60, 100, 40],
    'Data3': [5, 8, 6, 1, 50, 100, 60, 120]
})

group = df['Data3'].groupby(df['Date']).sum()

df['Data4'] = group

Date
2015-05-05    121
2015-05-06     66
2015-05-07    108
2015-05-08     55
Name: Data3, dtype: int64

最后的df

         Date   Sym  Data2  Data3  Data4
0  2015-05-08  aapl     11      5    NaN
1  2015-05-07  aapl      8      8    NaN
2  2015-05-06  aapl     10      6    NaN
3  2015-05-05  aapl     15      1    NaN
4  2015-05-08  aaww    110     50    NaN
5  2015-05-07  aaww     60    100    NaN
6  2015-05-06  aaww    100     60    NaN
7  2015-05-05  aaww     40    120    NaN
4个回答

288

你想使用transform。这将返回一个Series,其索引与df对齐,因此您可以将其作为新列添加:

df = pd.DataFrame({
    'Date': ['2015-05-08', '2015-05-07', '2015-05-06', '2015-05-05',
             '2015-05-08', '2015-05-07', '2015-05-06', '2015-05-05'],
    'Sym': ['aapl', 'aapl', 'aapl', 'aapl', 'aaww', 'aaww', 'aaww', 'aaww'],
    'Data2': [11, 8, 10, 15, 110, 60, 100, 40],
    'Data3': [5, 8, 6, 1, 50, 100, 60, 120]
})
​
df['Data4'] = df['Data3'].groupby(df['Date']).transform('sum')
df
         Date   Sym  Data2  Data3  Data4
0  2015-05-08  aapl     11      5     55
1  2015-05-07  aapl      8      8    108
2  2015-05-06  aapl     10      6     66
3  2015-05-05  aapl     15      1    121
4  2015-05-08  aaww    110     50     55
5  2015-05-07  aaww     60    100    108
6  2015-05-06  aaww    100     60     66
7  2015-05-05  aaww     40    120    121

1
如果我们有第二个groupby,会发生什么,就像这里所示:https://dev59.com/3FkS5IYBdhLWcg3wUE90#40067099 - Mr_and_Mrs_D
@Mr_and_Mrs_D 在这种情况下,您需要重置索引并在共同列上执行左合并以添加列。 - EdChum
26
另外一种方法是使用df.groupby('Date')['Data3'].transform('sum')(我觉得稍微更容易记住)。 - Cleb
1
如何使用此模板按两列进行分组?谢谢。 - Z.LI

76
如何从pandas groupby().sum()的输出中创建一个新列?
有两种方法 - 一种是直接的,另一种稍微有趣一些。

每个人都喜欢的: GroupBy.transform()'sum'

@EdChum's answer 可以稍微简化一下。调用 DataFrame.groupby 而不是 Series.groupby。这将导致语法更简单。

df.groupby('Date')['Data3'].transform('sum')

0     55
1    108
2     66
3    121
4     55
5    108
6     66
7    121
Name: Data3, dtype: int64 

它稍微快一点,

df2 = pd.concat([df] * 12345)

%timeit df2['Data3'].groupby(df['Date']).transform('sum')
%timeit df2.groupby('Date')['Data3'].transform('sum')

10.4 ms ± 367 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)
8.58 ms ± 559 µs per loop (mean ± std. dev. of 7 runs, 100 loops each)

非传统方法,但值得您考虑:GroupBy.sum() + Series.map()

我偶然发现了API中一个有趣的怪癖。据我所知,您可以在任何大于0.20的主要版本上复制这一点(我在0.23和0.24上测试过)。如果您改为使用GroupBy的直接函数并使用map广播,似乎您可以始终削减掉transform所需的时间。

df['Date'].map(df.groupby('Date')['Data3'].sum())

0     55
1    108
2     66
3    121
4     55
5    108
6     66
7    121
Name: Date, dtype: int64

与上面的输出 df.groupby('Date')['Data3'].transform('sum') 相比较:它们是相同的。

我的测试表明,如果你可以使用直接的 GroupBy 函数(如 meanminmaxfirst 等),那么 map 会稍微快一些。对于大多数一般情况,它或多或少比 ~200,000 条记录更快。之后,性能就真的取决于数据了。

(左:v0.23,右:v0.24)

这是一个不错的选择,如果你有较小的框架和较少的组数,那么它会更好,但我建议首选transform。无论如何,我认为这值得分享。

参考基准代码:

import perfplot

perfplot.show(
    setup=lambda n: pd.DataFrame({'A': np.random.choice(n//10, n), 'B': np.ones(n)}),
    kernels=[
        lambda df: df.groupby('A')['B'].transform('sum'),
        lambda df:  df.A.map(df.groupby('A')['B'].sum()),
    ],
    labels=['GroupBy.transform', 'GroupBy.sum + map'],
    n_range=[2**k for k in range(5, 20)],
    xlabel='N',
    logy=True,
    logx=True
)

1
这是很好的了解!您是否介意在未来的perfplot中包含版本号?性能差异很有趣,但毕竟这些都是实现细节,可能会在未来得到解决。特别是如果开发人员注意到您的帖子。 - jpp
@jpp 没错,很公平!已添加版本。这是在0.23上测试的,但我认为只要你有任何超过0.20的版本,就会看到差异。 - cs95

26

我建议通常使用更强大的apply,您可以使用它在单个表达式中编写查询,即使是更复杂的用法,例如定义一个新列,其值被定义为对组进行操作,并且可以在同一组内具有不同的值!

这比简单情况更通用,即为每个组定义具有相同值的列(如此问题中的sum,它按组变化但在同一组内相同)。

简单情况(在组内具有相同值的新列,在组之间具有不同的值):

# I'm assuming the name of your dataframe is something long, like
# `my_data_frame`, to show the power of being able to write your
# data processing in a single expression without multiple statements and
# multiple references to your long name, which is the normal style
# that the pandas API naturally makes you adopt, but which make the
# code often verbose, sparse, and a pain to generalize or refactor

my_data_frame = pd.DataFrame({
    'Date': ['2015-05-08', '2015-05-07', '2015-05-06', '2015-05-05', '2015-05-08', '2015-05-07', '2015-05-06', '2015-05-05'], 
    'Sym': ['aapl', 'aapl', 'aapl', 'aapl', 'aaww', 'aaww', 'aaww', 'aaww'], 
    'Data2': [11, 8, 10, 15, 110, 60, 100, 40],
    'Data3': [5, 8, 6, 1, 50, 100, 60, 120]})
​
(my_data_frame
    # create groups by 'Date'
    .groupby(['Date'])
    # for every small Group DataFrame `gdf` with the same 'Date', do:
    # assign a new column 'Data4' to it, with the value being
    # the sum of 'Data3' for the small dataframe `gdf`
    .apply(lambda gdf: gdf.assign(Data4=lambda gdf: gdf['Data3'].sum()))
    # after groupby operations, the variable(s) you grouped by on
    # are set as indices. In this case, 'Date' was set as an additional
    # level for the (multi)index. But it is still also present as a
    # column. Thus, we drop it from the index:
    .droplevel(0)
)

### OR

# We don't even need to define a variable for our dataframe.
# We can chain everything in one expression

(pd
    .DataFrame({
        'Date': ['2015-05-08', '2015-05-07', '2015-05-06', '2015-05-05', '2015-05-08', '2015-05-07', '2015-05-06', '2015-05-05'], 
        'Sym': ['aapl', 'aapl', 'aapl', 'aapl', 'aaww', 'aaww', 'aaww', 'aaww'], 
        'Data2': [11, 8, 10, 15, 110, 60, 100, 40],
        'Data3': [5, 8, 6, 1, 50, 100, 60, 120]})
    .groupby(['Date'])
    .apply(lambda gdf: gdf.assign(Data4=lambda gdf: gdf['Data3'].sum()))
    .droplevel(0)
)

输出:

日期 Sym Data2 Data3 Data4
3 2015-05-05 aapl 15 1 121
7 2015-05-05 aaww 40 120 121
2 2015-05-06 aapl 10 6 66
6 2015-05-06 aaww 100 60 66
1 2015-05-07 aapl 8 8 108
5 2015-05-07 aaww 60 100 108
0 2015-05-08 aapl 11 5 55
4 2015-05-08 aaww 110 50 55
为什么Python表达式需要用括号括起来?这是因为我们不需要在代码中到处添加反斜杠,我们可以在表达式代码中放置注释来描述每个步骤。
这样的强大之处在于它利用了“split-apply-combine范式”的全部威力。它允许您以“将数据框拆分成块”和“在这些块上运行任意操作”的方式进行思考,而不需要减少/聚合,即不需要减少行数。(而且不需要编写显式、冗长的循环,并且不需要使用昂贵的连接或串联来粘合结果。)
让我们考虑一个更复杂的例子。其中您有多个时间序列的数据在您的数据框中。您有一个表示某种产品的列,一个具有时间戳的列,以及一个包含在某年某个时间销售该产品的商品数量的列。您想按产品分组,并获得一个新列,其中包含每个类别销售的商品的累计总数。我们希望一个列,在每个具有相同产品的“块”内部仍然是时间序列,并且在块内单调递增。
我们如何做到这一点?通过使用groupby+apply
(pd
     .DataFrame({
        'Date': ['2021-03-11','2021-03-12','2021-03-13','2021-03-11','2021-03-12','2021-03-13'], 
        'Product': ['shirt','shirt','shirt','shoes','shoes','shoes'], 
        'ItemsSold': [300, 400, 234, 80, 10, 120],
        })
    .groupby(['Product'])
    .apply(lambda gdf: (gdf
        # sort by date within a group
        .sort_values('Date')
        # create new column
        .assign(CumulativeItemsSold=lambda df: df['ItemsSold'].cumsum())))
    .droplevel(0)
)

输出:

日期 产品 销售数量 累计销售数量
0 2021-03-11 衬衫 300 300
1 2021-03-12 衬衫 400 700
2 2021-03-13 衬衫 234 934
3 2021-03-11 鞋子 80 80
4 2021-03-12 鞋子 10 90
5 2021-03-13 鞋子 120 210

这种方法的另一个优点是,即使我们需要按多个字段分组,它也能正常工作!例如,如果我们的产品有一个“颜色”字段,并且我们希望按(产品,颜色)分组进行累计汇总,则可以:

(pd
     .DataFrame({
        'Date': ['2021-03-11','2021-03-12','2021-03-13','2021-03-11','2021-03-12','2021-03-13',
                 '2021-03-11','2021-03-12','2021-03-13','2021-03-11','2021-03-12','2021-03-13'], 
        'Product': ['shirt','shirt','shirt','shoes','shoes','shoes',
                    'shirt','shirt','shirt','shoes','shoes','shoes'], 
        'Color': ['yellow','yellow','yellow','yellow','yellow','yellow',
                  'blue','blue','blue','blue','blue','blue'], # new!
        'ItemsSold': [300, 400, 234, 80, 10, 120,
                      123, 84, 923, 0, 220, 94],
        })
    .groupby(['Product', 'Color']) # We group by 2 fields now
    .apply(lambda gdf: (gdf
        .sort_values('Date')
        .assign(CumulativeItemsSold=lambda df: df['ItemsSold'].cumsum())))
    .droplevel([0,1]) # We drop 2 levels now

输出:

日期 产品 颜色 销售量 累计销售量
6 2021-03-11 衬衫 蓝色 123 123
7 2021-03-12 衬衫 蓝色 84 207
8 2021-03-13 衬衫 蓝色 923 1130
0 2021-03-11 衬衫 黄色 300 300
1 2021-03-12 衬衫 黄色 400 700
2 2021-03-13 衬衫 黄色 234 934
9 2021-03-11 鞋子 蓝色 0 0
10 2021-03-12 鞋子 蓝色 220 220
11 2021-03-13 鞋子 蓝色 94 314
3 2021-03-11 鞋子 黄色 80 80
4 {{在多个字段上轻松扩展分组的可能性是我喜欢始终将groupby的参数放在列表中的原因,即使它只是一个单独的名称,如前面示例中的' Product'。}}
{{您可以在单个表达式中合成所有这些内容。 (当然,如果Python的lambda函数看起来更好一些,那就会看起来更好。)}}

为什么要涉及一般情况?因为这是在搜索“pandas新列groupby”等内容时出现的最初的stackoverflow问题之一。


关于此类操作的API的附加想法

根据分组进行任意计算添加列,很像在SparkSQL中使用窗口聚合定义新列的好习惯defining new column using aggregations over Windows in SparkSQL

例如,您可以考虑以下内容(这是Scala代码,但在PySpark中的等效代码看起来几乎相同):

val byDepName = Window.partitionBy('depName)
empsalary.withColumn("avg", avg('salary) over byDepName)

作为类似以下方式的内容(使用我们之前看到的pandas):

empsalary = pd.DataFrame(...some dataframe...)
(empsalary
    # our `Window.partitionBy('depName)`
    .groupby(['depName'])
    # our 'withColumn("avg", avg('salary) over byDepName)
    .apply(lambda gdf: gdf.assign(avg=lambda df: df['salary'].mean()))
    .droplevel(0)
)

请注意Spark示例的合成和美观程度。相比之下,pandas等效方法看起来有些笨拙。pandas API不易于编写这种“流畅”操作。
这种习惯用法反过来源自SQL的窗口函数,PostgreSQL文档对其进行了非常好的定义:(强调是我的)
窗口函数执行跨一组与当前行相关的表行的计算。这类似于可以使用聚合函数进行的计算。但与常规聚合函数不同,使用窗口函数不会导致行成为单个输出行 - 行保留其单独的标识。在幕后,窗口函数能够访问查询结果的不止当前行。
并提供了一个漂亮的SQL一行代码示例:(在组内排名)
SELECT depname, empno, salary, rank() OVER (PARTITION BY depname ORDER BY salary DESC) FROM empsalary;
部门名称 员工编号 薪资 等级
开发部 8 6000 1
开发部 10 5200 2
开发部 11 5200 2
开发部 9 4500 4
开发部 7 4200 5
人事部 2 3900 1
人事部 5 3500 2
销售部 1 5000 1
销售部 4 4800 2
销售部 3 4800 2

最后一件事:您可能也对pandas的pipe感兴趣,它类似于apply,但工作方式略有不同,并为内部操作提供了更大的作用范围。更多信息请参见此处


2
df = pd.DataFrame({
'Date' : ['2015-05-08', '2015-05-07', '2015-05-06', '2015-05-05', '2015-05-08', '2015-05-07', '2015-05-06', '2015-05-05'], 
'Sym'  : ['aapl', 'aapl', 'aapl', 'aapl', 'aaww', 'aaww', 'aaww', 'aaww'], 
'Data2': [11, 8, 10, 15, 110, 60, 100, 40],
'Data3': [5, 8, 6, 1, 50, 100, 60, 120]
})
print(pd.pivot_table(data=df,index='Date',columns='Sym',     aggfunc={'Data2':'sum','Data3':'sum'}))

输出

Data2      Data3     
Sym         aapl aaww  aapl aaww
Date                            
2015-05-05    15   40     1  120
2015-05-06    10  100     6   60
2015-05-07     8   60     8  100
2015-05-08    11  110     5   50

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