我通常建议使用更强大的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 |
(这种轻松扩展到对多个字段进行分组的可能性是我喜欢将groupby
always 的参数放在一个列表中的原因,即使它是一个名称,例如前面示例中的“产品”。)
您可以在一个表达式中综合完成所有这些操作。(当然,如果 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
但工作方式略有不同,并且为内部操作提供了更大的工作范围。看这里了解更多