Pandas按组计算总百分比
这看起来很简单,但作为一个刚接触numpy的新手,我遇到了一些困难。
我有一个CSV文件,里面有三列,分别是州名、办公室ID和该办公室的销售额。
我想计算每个州中每个办公室的销售额占比(每个州的所有办公室销售额占比加起来是100%)。
df = pd.DataFrame({'state': ['CA', 'WA', 'CO', 'AZ'] * 3,
'office_id': list(range(1, 7)) * 2,
'sales': [np.random.randint(100000, 999999)
for _ in range(12)]})
df.groupby(['state', 'office_id']).agg({'sales': 'sum'})
这个代码返回了:
sales
state office_id
AZ 2 839507
4 373917
6 347225
CA 1 798585
3 890850
5 454423
CO 1 819975
3 202969
5 614011
WA 2 163942
4 369858
6 959285
我似乎无法弄明白如何“向上”到达state
的层级,以便把整个state
的sales
加起来,来计算比例。
17 个回答
我觉得这需要进行性能测试。使用原始的DataFrame,
df = pd.DataFrame({
'state': ['CA', 'WA', 'CO', 'AZ'] * 3,
'office_id': list(range(1, 7)) * 2,
'sales': [np.random.randint(100000, 999999) for _ in range(12)]
})
第0个 Caner
新的 Pandas Transform 看起来快了很多。
df['sales'] / df.groupby('state')['sales'].transform('sum')
每次循环 1.32 毫秒 ± 352 微秒
(7次运行的平均值 ± 标准差,每次100次循环)
第1个 Andy Hayden
正如他回答中提到的,Andy充分利用了向量化和Pandas索引。
c = df.groupby(['state', 'office_id'])['sales'].sum().rename("count")
c / c.groupby(level=0).sum()
每次循环 3.42 毫秒 ± 16.7 微秒
(7次运行的平均值 ± 标准差,每次100次循环)
第2个 Paul H
state_office = df.groupby(['state', 'office_id']).agg({'sales': 'sum'})
state = df.groupby(['state']).agg({'sales': 'sum'})
state_office.div(state, level='state') * 100
每次循环 4.66 毫秒 ± 24.4 微秒
(7次运行的平均值 ± 标准差,每次100次循环)
第3个 exp1orer
这是最慢的答案,因为它对每个级别0中的 x
计算 x.sum()
。
对我来说,这仍然是一个有用的答案,尽管目前的形式不太好。对于较小的数据集,apply
允许你使用 方法链 一行写完。这就省去了决定变量名称的麻烦,而这实际上对你最宝贵的资源(你的大脑!!)是非常 耗费计算资源 的。
这是修改后的代码,
(
df.groupby(['state', 'office_id'])
.agg({'sales': 'sum'})
.groupby(level=0)
.apply(lambda x: 100 * x / float(x.sum()))
)
每次循环 10.6 毫秒 ± 81.5 微秒
(7次运行的平均值 ± 标准差,每次100次循环)
所以在小数据集上,没人会在意6毫秒的差别。然而,这在大型数据集上,尤其是有很多分类的情况下,会产生巨大的差异。
在上面的代码基础上,我们创建了一个形状为 (12,000,000, 3) 的DataFrame,包含14412个州类别和600个办公室ID,
import string
import numpy as np
import pandas as pd
np.random.seed(0)
groups = [
''.join(i) for i in zip(
np.random.choice(np.array([i for i in string.ascii_lowercase]), 30000),
np.random.choice(np.array([i for i in string.ascii_lowercase]), 30000),
np.random.choice(np.array([i for i in string.ascii_lowercase]), 30000),
)
]
df = pd.DataFrame({'state': groups * 400,
'office_id': list(range(1, 601)) * 20000,
'sales': [np.random.randint(100000, 999999)
for _ in range(12)] * 1000000
})
使用Caner的方法,
每次循环 0.791 秒 ± 19.4 毫秒 (7次运行的平均值 ± 标准差,每次1次循环)
使用Andy的方法,
每次循环 2 秒 ± 10.4 毫秒
(7次运行的平均值 ± 标准差,每次1次循环)
以及exp1orer的方法
每次循环 19 秒 ± 77.1 毫秒
(7次运行的平均值 ± 标准差,每次1次循环)
所以现在我们看到在大型、高分类数据集上,使用Andy的方法速度提升了10倍,而使用Caner的方法则提升了20倍,非常令人印象深刻。
如果你觉得这个答案有用,记得给这三个答案点赞哦!!
编辑:添加了Caner的基准测试
为了简洁起见,我会使用 SeriesGroupBy:
In [11]: c = df.groupby(['state', 'office_id'])['sales'].sum().rename("count")
In [12]: c
Out[12]:
state office_id
AZ 2 925105
4 592852
6 362198
CA 1 819164
3 743055
5 292885
CO 1 525994
3 338378
5 490335
WA 2 623380
4 441560
6 451428
Name: count, dtype: int64
In [13]: c / c.groupby(level=0).sum()
Out[13]:
state office_id
AZ 2 0.492037
4 0.315321
6 0.192643
CA 1 0.441573
3 0.400546
5 0.157881
CO 1 0.388271
3 0.249779
5 0.361949
WA 2 0.411101
4 0.291196
6 0.297703
Name: count, dtype: float64
如果要处理多个分组,你需要使用 transform(参考 Radical 的 df):
In [21]: c = df.groupby(["Group 1","Group 2","Final Group"])["Numbers I want as percents"].sum().rename("count")
In [22]: c / c.groupby(level=[0, 1]).transform("sum")
Out[22]:
Group 1 Group 2 Final Group
AAHQ BOSC OWON 0.331006
TLAM 0.668994
MQVF BWSI 0.288961
FXZM 0.711039
ODWV NFCH 0.262395
...
Name: count, dtype: float64
这个方法似乎比其他答案稍微快一点(对我来说,速度大约是 Radical 答案的不到两倍,差不多是 0.08 秒)。
你需要创建一个新的分组对象,这个对象是按照州来分组的,然后使用 div
方法:
import numpy as np
import pandas as pd
np.random.seed(0)
df = pd.DataFrame({'state': ['CA', 'WA', 'CO', 'AZ'] * 3,
'office_id': list(range(1, 7)) * 2,
'sales': [np.random.randint(100000, 999999) for _ in range(12)]})
state_office = df.groupby(['state', 'office_id']).agg({'sales': 'sum'})
state = df.groupby(['state']).agg({'sales': 'sum'})
state_office.div(state, level='state') * 100
sales
state office_id
AZ 2 16.981365
4 19.250033
6 63.768601
CA 1 19.331879
3 33.858747
5 46.809373
CO 1 36.851857
3 19.874290
5 43.273852
WA 2 34.707233
4 35.511259
6 29.781508
在 div
方法中,level='state'
这个参数告诉 pandas 根据索引中 state
这一层的值来进行数据的广播或连接。
这个解决方案的灵感来自于这篇文章 https://pbpython.com/pandas_transform.html
我发现以下的解决方案是最简单的(可能也是最快的),使用了 transformation
:
转化(Transformation):聚合(Aggregation)必须返回一个简化版的数据,而转化可以返回一些经过处理的完整数据,以便重新组合。对于这样的转化,输出的数据形状和输入的是一样的。
所以使用 transformation
,这个解决方案只需要一行代码:
df['%'] = 100 * df['sales'] / df.groupby('state')['sales'].transform('sum')
如果你打印出来的话:
print(df.sort_values(['state', 'office_id']).reset_index(drop=True))
state office_id sales %
0 AZ 2 195197 9.844309
1 AZ 4 877890 44.274352
2 AZ 6 909754 45.881339
3 CA 1 614752 50.415708
4 CA 3 395340 32.421767
5 CA 5 209274 17.162525
6 CO 1 549430 42.659629
7 CO 3 457514 35.522956
8 CO 5 280995 21.817415
9 WA 2 828238 35.696929
10 WA 4 719366 31.004563
11 WA 6 772590 33.298509
更新 2022-03
caner 的这个回答使用了 transform
,看起来比我最初的回答要好很多!
df['sales'] / df.groupby('state')['sales'].transform('sum')
感谢 Paul Rougieux 的这个评论,让这个方法被提了出来。
原始回答 (2014)
Paul H 的回答是对的,你确实需要创建一个第二个 groupby
对象,但你可以用更简单的方法来计算百分比——只需对 state_office
进行 groupby
,然后把 sales
列的值除以它的总和。下面是 Paul H 回答的开头部分:
# From Paul H
import numpy as np
import pandas as pd
np.random.seed(0)
df = pd.DataFrame({'state': ['CA', 'WA', 'CO', 'AZ'] * 3,
'office_id': list(range(1, 7)) * 2,
'sales': [np.random.randint(100000, 999999)
for _ in range(12)]})
state_office = df.groupby(['state', 'office_id']).agg({'sales': 'sum'})
# Change: groupby state_office and divide by sum
state_pcts = state_office.groupby(level=0).apply(lambda x:
100 * x / float(x.sum()))
返回结果:
sales
state office_id
AZ 2 16.981365
4 19.250033
6 63.768601
CA 1 19.331879
3 33.858747
5 46.809373
CO 1 36.851857
3 19.874290
5 43.273852
WA 2 34.707233
4 35.511259
6 29.781508