如何从 pandas groupby().sum() 的输出中创建新列?
- 2024-11-26 08:36:00
- admin 原创
- 163
问题描述:
尝试通过计算创建新列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
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
解决方案 1:
您想使用transform
。这将返回一个与 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
解决方案 2:
如何从 pandas groupby().sum() 的输出中创建新列?
有两种方法——一种方法很简单,另一种方法稍微有趣一些。
大家最喜欢的GroupBy.transform()
:'sum'
@EdChum 的答案可以稍微简化一下。 调用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 上测试过)。transform
如果你改用直接函数GroupBy
并使用 进行广播,似乎可以节省几毫秒的时间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
与上面的输出进行比较df.groupby('Date')['Data3'].transform('sum')
:它们是相同的。
我的测试表明,如果您可以使用直接函数(例如,,,,等map
),速度会更快一些GroupBy
。mean
对于大多数一般情况,速度会更快,最多约 20 万条记录。在此之后,性能实际上取决于数据。min
`max`first
(左: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
)
解决方案 3:
我通常建议使用功能更强大的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 | 苹果 | 15 | 1 | 121 |
7 | 2015-05-05 | 啊啊啊 | 40 | 120 | 121 |
2 | 2015-05-06 | 苹果 | 10 | 6 | 66 |
6 | 2015-05-06 | 啊啊啊 | 100 | 60 | 66 |
1 | 2015-05-07 | 苹果 | 8 | 8 | 108 |
5 | 2015-05-07 | 啊啊啊 | 60 | 100 | 108 |
0 | 2015-05-08 | 苹果 | 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 |
(这种轻松扩展到多个字段分组的可能性就是我喜欢将参数放在列表中的原因groupby
,即使它是一个名称,如前面例子中的“产品”。)
而且,您可以在一个表达式中综合完成所有这些操作。(当然,如果 Python 的 lambda 看起来更美观一些,它会看起来更美观。)
我为什么要讨论一般情况?因为这是在谷歌搜索“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 的窗口函数,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
,但工作方式略有不同,并为内部操作提供了更大的工作范围。有关更多信息,请参见此处
解决方案 4:
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