123

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

import pandas as pd
import numpy as np
from pandas import DataFrame

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
4

4 回答 4

239

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

In [74]:

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
Out[74]:
   Data2  Data3        Date   Sym  Data4
0     11      5  2015-05-08  aapl     55
1      8      8  2015-05-07  aapl    108
2     10      6  2015-05-06  aapl     66
3     15      1  2015-05-05  aapl    121
4    110     50  2015-05-08  aaww     55
5     60    100  2015-05-07  aaww    108
6    100     60  2015-05-06  aaww     66
7     40    120  2015-05-05  aaww    121
于 2015-05-14T18:46:07.473 回答
63

How do I create a new column with Groupby().Sum()?

There are two ways - one straightforward and the other slightly more interesting.


Everybody's Favorite: GroupBy.transform() with 'sum'

@Ed Chum's answer can be simplified, a bit. Call DataFrame.groupby rather than Series.groupby. This results in simpler syntax.

# The setup.
df[['Date', 'Data3']]

         Date  Data3
0  2015-05-08      5
1  2015-05-07      8
2  2015-05-06      6
3  2015-05-05      1
4  2015-05-08     50
5  2015-05-07    100
6  2015-05-06     60
7  2015-05-05    120

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 

It's a tad faster,

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)

Unconventional, but Worth your Consideration: GroupBy.sum() + Series.map()

I stumbled upon an interesting idiosyncrasy in the API. From what I tell, you can reproduce this on any major version over 0.20 (I tested this on 0.23 and 0.24). It seems like you consistently can shave off a few milliseconds of the time taken by transform if you instead use a direct function of GroupBy and broadcast it using map:

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

Compare with

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

My tests show that map is a bit faster if you can afford to use the direct GroupBy function (such as mean, min, max, first, etc). It is more or less faster for most general situations upto around ~200 thousand records. After that, the performance really depends on the data.

(Left: v0.23, Right: v0.24)

Nice alternative to know, and better if you have smaller frames with smaller numbers of groups. . . but I would recommend transform as a first choice. Thought this was worth sharing anyway.

Benchmarking code, for reference:

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
)
于 2019-01-29T09:09:47.627 回答
13

我通常建议使用更强大的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)
)

出去:

日期 符号 数据2 数据3 数据4
3 2015-05-05 aapl 15 1 121
7 2015-05-05 啊啊啊 40 120 121
2 2015-05-06 aapl 10 6 66
6 2015-05-06 啊啊啊 100 60 66
1 2015-05-07 aapl 8 8 108
5 2015-05-07 啊啊啊 60 100 108
0 2015-05-08 aapl 11 5 55
4 2015-05-08 啊啊啊 110 50 55

(为什么 python 表达式要放在括号内?这样我们就不需要在代码中到处都是反斜杠,我们可以在表达式代码中添加注释来描述每一步。)

这有什么强大的?这是因为它正在利用“拆分-应用-组合范式”的全部力量。它允许您考虑“将数据帧拆分为块”和“在这些块上运行任意操作”而不减少/聚合,即不减少行数。(并且无需编写显式、冗长的循环,也无需使用昂贵的连接或连接来将结果粘合回来。)

让我们考虑一个更复杂的例子。您的数据框中有多个时间序列的数据。您有一个代表一种产品的列,一个具有时间戳的列,以及一个包含该产品在一年中某个时间售出的商品数量的列。您想按产品分组并获得一个新列,其中包含每个类别销售的商品的累计总数。我们想要一个列,在具有相同产品的每个“块”内,仍然是一个时间序列,并且单调递增(仅在一个块内)。

我们应该怎么做?用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

这种方法的另一个优点是什么?即使我们必须按多个字段分组,它也有效!例如,如果我们'Color'的产品有一个字段,并且我们想要按 分组的累积系列(Product, Color),我们可以:

(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 2021-03-12 黄色 10 90
5 2021-03-13 黄色 120 210

(这种轻松扩展到对多个字段进行分组的可能性是我喜欢将groupbyalways 的参数放在一个列表中的原因,即使它是一个名称,例如前面示例中的“产品”。)

您可以在一个表达式中综合完成所有这些操作。(当然,如果 python 的 lambdas 看起来更好看,它看起来会更好。)


为什么我要讨论一般情况?因为这是在谷歌搜索“pandas new column groupby”之类的内容时出现的第一个 SO 问题。


关于此类操作的 API 的其他想法

基于对组进行的任意计算添加列很像在 SparkSQL 中使用 Windows 上的聚合定义新列的好习惯。

例如,您可以这样想(它是 Scala 代码,但 PySpark 中的等效代码看起来几乎相同):

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

就像(以我们上面看到的方式使用熊猫):

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 的 Window Functions,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,它与 pandas 相似apply但工作方式略有不同,并且为内部操作提供了更大的工作范围。看这里了解更多

于 2021-03-11T14:47:23.530 回答
0
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
于 2021-03-13T13:56:16.383 回答