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

尝试从 groupby计算中创建新列。在下面的代码中,我得到了每个日期的正确计算值(参见下面的组) ,但是当我尝试用它创建一个新列(df['Data4'])时,我得到了 NaN。因此,我试图在数据框中创建一个新列,其中所有日期的总和为 Data3,并将其应用到每个日期行。例如,2015-05-08在两行中(total 是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
216176 次浏览

你想要使用 transform,这将返回一个系列,其索引与 df 对齐,这样你就可以将它添加为一个新列:

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

如何使用 Groupby ()创建新列?

有两种方法——一种直截了当,另一种稍微有点意思。


每个人的最爱: GroupBy.transform()'sum'

@ Ed Chum 的答案可以简化一点,调用 DataFrame.groupby而不是 Series.groupby,这样语法更简单。

# 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

要快一点,

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')


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

我的测试表明,如果能够使用直接的 GroupBy函数(如 meanminmaxfirst等) ,那么 map会稍微快一些。对于大多数一般情况,它或多或少地快一些,最多可达约20万条记录。在此之后,性能真正取决于数据。

(左: 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
)

我一般建议使用功能更强大的 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 七百
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 七百
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

(这种容易扩展到多个字段的分组的可能性就是为什么我喜欢将 groupby的参数总是放在一个列表中的原因,即使它是一个单一的名称,如前面示例中的‘ Product’。)

你可以在一个表达式中综合完成所有这些。(当然,如果蟒蛇的 lambda 看起来更漂亮一点,它看起来会更漂亮。)


我为什么要看一般的案子?因为这是搜索“熊猫新栏目 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 的例子是多么的合成和美好。熊猫看起来有点笨重。熊猫 API 并没有让编写这些“流畅”的操作变得容易)。

这个习惯用法依次来自 SQL 的窗口函数,PostgreSQL 文档给出了一个非常好的定义: (重点是我的定义)

窗口函数执行计算 跨一组以某种方式与当前行相关的表行。这与可以使用聚合函数进行的计算类型相当。但是,与常规聚合函数不同,使用窗口函数不会导致将行分组为单个输出行ーー 这些行保留着它们各自的身份。在幕后,window 函数不仅能够访问查询结果的当前行。

并给出了一个漂亮的 SQL 一行程序示例: (在组中排名)

SELECT depname, empno, salary, rank() OVER (PARTITION BY depname ORDER BY salary DESC) FROM empsalary;
名字 喝吧 薪水 军衔
开发 8 六千 1
开发 10 5200 2
开发 11 5200 2
开发 9 4500 4
开发 7 4200 5
人事部 2 3900 1
人事部 5 3500 2
销售 1 五千 1
销售 4 4800 2
销售 3 4800 2

最后一件事: 你可能也对大熊猫的 pipe感兴趣,它与 apply类似,但是工作原理有点不同,并且给了内部操作更大的工作空间。有关更多信息,请参见 给你

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