我建议通常使用更强大的apply
,您可以使用它在单个表达式中编写查询,即使是更复杂的用法,例如定义一个新列,其值被定义为对组进行操作,并且可以在同一组内具有不同的值!
这比简单情况更通用,即为每个组定义具有相同值的列(如此问题中的sum
,它按组变化但在同一组内相同)。
简单情况(在组内具有相同值的新列,在组之间具有不同的值):
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
.groupby(['Date'])
.apply(lambda gdf: gdf.assign(Data4=lambda gdf: gdf['Data3'].sum()))
.droplevel(0)
)
(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_values('Date')
.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'],
'ItemsSold': [300, 400, 234, 80, 10, 120,
123, 84, 923, 0, 220, 94],
})
.groupby(['Product', 'Color'])
.apply(lambda gdf: (gdf
.sort_values('Date')
.assign(CumulativeItemsSold=lambda df: df['ItemsSold'].cumsum())))
.droplevel([0,1])
输出:
|
日期 |
产品 |
颜色 |
销售量 |
累计销售量 |
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
.groupby(['depName'])
.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 ,但工作方式略有不同,并为内部操作提供了更大的作用范围。更多信息请参见此处。
|
df.groupby('Date')['Data3'].transform('sum')
(我觉得稍微更容易记住)。 - Cleb