使用 groupby 的 Pandas 占总数的百分比
Posted
技术标签:
【中文标题】使用 groupby 的 Pandas 占总数的百分比【英文标题】:Pandas percentage of total with groupby 【发布时间】:2014-06-16 03:00:59 【问题描述】:这显然很简单,但作为一个 numpy 新手,我被卡住了。
我有一个 CSV 文件,其中包含 3 列,即州、办公室 ID 和该办公室的销售额。
我想计算给定州每个办公室的销售额百分比(每个州所有百分比的总和为 100%)。
df = pd.DataFrame('state': ['CA', 'WA', 'CO', 'AZ'] * 3,
'office_id': 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
我似乎不知道如何“达到”groupby
的state
级别,以合计整个state
的sales
来计算分数。
【问题讨论】:
df['sales'] / df.groupby('state')['sales'].transform('sum')
似乎是最明确的答案。
【参考方案1】:
Paul H's answer 是正确的,您必须创建第二个 groupby
对象,但您可以用更简单的方式计算百分比 - 只需 groupby
state_office
并将 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
【讨论】:
这是怎么回事?据我了解,x
是某种表格,所以100 * x
在直觉上没有意义(尤其是当某些单元格包含AZ
之类的字符串时,...)。
@dhardy state_office
是一个具有多索引的系列——所以它只是一个值都是数字的列。执行 groupby 后,每个 x
都是该列的子集。这有意义吗?
它可能,但它对我不起作用。 Python 3 中的 pandas 的工作方式是否有点不同?
level=0
是什么意思?
@Veenit 这意味着您是按索引的第一级分组,而不是按其中一个列。【参考方案2】:
您需要创建第二个按状态分组的 groupby 对象,然后使用 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'
kwarg 告诉 pandas 根据索引的 state
级别中的值广播/加入数据帧。
【讨论】:
如果您有 3 个索引,此方法是否有效?我首先在 3 列上进行了 groupby。然后我只对 2 做了第二个 groupby 并计算总和。然后我尝试使用div
,但使用level=["index1", "index2"]
,但它告诉我Join on level between two MultiIndex objects is ambiguous
。
@Ger 它确实有效,但我无法从该描述中猜出你做错了什么。多在网站上搜索一下。如果您没有找到任何东西,请使用可重现的示例创建一个新问题来演示该问题。 ***.com/questions/20109391/…【参考方案3】:
(这个解决方案的灵感来自这篇文章https://pbpython.com/pandas_transform.html)
我发现以下使用transformation
的解决方案是最简单的(可能也是最快的):
转换:虽然聚合必须返回简化版本的 数据,转换可以返回完整的一些转换版本 数据重组。对于这样的转换,输出是相同的 形状作为输入。
所以使用transformation
,解决方案是1-liner:
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
【讨论】:
@Cancer 这是我最喜欢的答案,因为它将 df 保持为 df(不转换为系列)并且仅添加 % 列。谢谢 这个答案的变体对我来说效果很好transform('max')
指向描述 transform() 的帖子的链接很棒。我认为这是一个比所选解决方案更好的解决方案,但需要学习转换(我认为这是一个优点:)【参考方案4】:
为了简洁起见,我会使用 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
对于多个组,您必须使用转换(使用 Radical's 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 秒)。
【讨论】:
这超级快。我会推荐这是首选的熊猫方法。真正利用了 numpy 的矢量化和 pandas 索引。 这对我也很有效,因为我正在与多个小组一起工作。谢谢。 对于多组,不使用transform也可以做到:c / c.groupby(level=[0, 1]).sum()
【参考方案5】:
我认为这需要进行基准测试。使用OP的原始DataFrame,
df = pd.DataFrame(
'state': ['CA', 'WA', 'CO', 'AZ'] * 3,
'office_id': range(1, 7) * 2,
'sales': [np.random.randint(100000, 999999) for _ in range(12)]
)
第一个Andy Hayden
正如他的回答所评论的那样,Andy 充分利用了矢量化和 pandas 索引。
c = df.groupby(['state', 'office_id'])['sales'].sum().rename("count")
c / c.groupby(level=0).sum()
3.42 毫秒 ± 16.7 µs 每个循环 (平均值±标准差。7 次运行,每次 100 次循环)
第二个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 µs 每个循环 (平均值±标准差。7 次运行,每次 100 次循环)
第三个exp1orer
这是最慢的答案,因为它会为级别 0 中的每个 x
计算 x.sum()
。
对我来说,这仍然是一个有用的答案,尽管不是现在的形式。对于较小数据集的快速 EDA,apply
允许您使用 method chaining 将其写在一行中。因此,我们不再需要决定变量的名称,这实际上是您最宝贵的资源(您的大脑!!)的 computationally expensive。
这里是修改,
(
df.groupby(['state', 'office_id'])
.agg('sales': 'sum')
.groupby(level=0)
.apply(lambda x: 100 * x / float(x.sum()))
)
10.6 毫秒 ± 81.5 µs 每个循环 (平均值±标准差。7 次运行,每次 100 次循环)
所以没有人会关心小数据集上的 6 毫秒。但是,这是 3 倍的速度提升,并且在具有高基数 groupbys 的更大数据集上,这将产生巨大的影响。
添加到上面的代码中,我们制作了一个形状为 (12,000,000, 3) 的 DataFrame,其中包含 14412 个状态类别和 600 个 office_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
)
使用安迪,
2 秒 ± 10.4 毫秒/循环 (平均值±标准偏差。7 次运行,每次 1 个循环)
和探险家
19 秒 ± 77.1 毫秒/循环 (平均值±标准偏差。7 次运行,每次 1 个循环)
所以现在我们看到 x10 在大型、高基数数据集上加速。
如果你对这三个答案进行紫外线,请务必对这三个答案进行紫外线!!
【讨论】:
【参考方案6】:我意识到这里已经有了很好的答案。
尽管如此,我还是想贡献自己的力量,因为我觉得对于这样一个基本的、简单的问题,应该有一个一眼就能理解的简短解决方案。
它还应该以一种我可以将百分比添加为新列的方式工作,而数据框的其余部分保持不变。最后但同样重要的是,它应该以一种明显的方式推广到有多个分组级别的情况(例如,州和国家,而不是只有州)。
以下 sn-p 满足这些条件:
df['sales_ratio'] = df.groupby(['state'])['sales'].transform(lambda x: x/x.sum())
请注意,如果您仍在使用 Python 2,则必须将 lambda 项的分母中的 x 替换为 float(x)。
【讨论】:
这是 IMO 的最佳答案。唯一要添加的是* 100
以使其成为百分比。
@Bouncner:是的,严格来说,您必须乘以 100 才能获得百分比——或者将新变量从“sales_percentage”重命名为“sales_ratio”。就个人而言,我更喜欢后者,并相应地编辑了答案。感谢提及!
如果你有多个级别,这不起作用。
@irene:好点,谢谢!可能在那种情况下 df.reset_index().groupby(['state'])['sales'].transform(lambda x: x/x.sum()) 会起作用。还是我忽略了什么?
这个答案很棒。它不涉及创建一个临时的groupby
对象,非常简洁,从左到右读起来非常合乎逻辑。【参考方案7】:
我知道这是一个老问题,但exp1orer's 对于具有大量唯一组的数据集(可能是因为 lambda)的回答非常慢。我根据他们的答案将其转换为数组计算,所以现在它超级快!下面是示例代码:
创建包含 50,000 个唯一组的测试数据框
import random
import string
import pandas as pd
import numpy as np
np.random.seed(0)
# This is the total number of groups to be created
NumberOfGroups = 50000
# Create a lot of groups (random strings of 4 letters)
Group1 = [''.join(random.choice(string.ascii_uppercase) for _ in range(4)) for x in range(NumberOfGroups/10)]*10
Group2 = [''.join(random.choice(string.ascii_uppercase) for _ in range(4)) for x in range(NumberOfGroups/2)]*2
FinalGroup = [''.join(random.choice(string.ascii_uppercase) for _ in range(4)) for x in range(NumberOfGroups)]
# Make the numbers
NumbersForPercents = [np.random.randint(100, 999) for _ in range(NumberOfGroups)]
# Make the dataframe
df = pd.DataFrame('Group 1': Group1,
'Group 2': Group2,
'Final Group': FinalGroup,
'Numbers I want as percents': NumbersForPercents)
分组后的样子:
Numbers I want as percents
Group 1 Group 2 Final Group
AAAH AQYR RMCH 847
XDCL 182
DQGO ALVF 132
AVPH 894
OVGH NVOO 650
VKQP 857
VNLY HYFW 884
MOYH 469
XOOC GIDS 168
HTOY 544
AACE HNXU RAXK 243
YZNK 750
NOYI NYGC 399
ZYCI 614
QKGK CRLF 520
UXNA 970
TXAR MLNB 356
NMFJ 904
VQYG NPON 504
QPKQ 948
...
[50000 rows x 1 columns]
数组求百分比方法:
# Initial grouping (basically a sorted version of df)
PreGroupby_df = df.groupby(["Group 1","Group 2","Final Group"]).agg('Numbers I want as percents': 'sum').reset_index()
# Get the sum of values for the "final group", append "_Sum" to it's column name, and change it into a dataframe (.reset_index)
SumGroup_df = df.groupby(["Group 1","Group 2"]).agg('Numbers I want as percents': 'sum').add_suffix('_Sum').reset_index()
# Merge the two dataframes
Percents_df = pd.merge(PreGroupby_df, SumGroup_df)
# Divide the two columns
Percents_df["Percent of Final Group"] = Percents_df["Numbers I want as percents"] / Percents_df["Numbers I want as percents_Sum"] * 100
# Drop the extra _Sum column
Percents_df.drop(["Numbers I want as percents_Sum"], inplace=True, axis=1)
此方法大约需要 ~0.15 秒
最佳答案法(使用 lambda 函数):
state_office = df.groupby(['Group 1','Group 2','Final Group']).agg('Numbers I want as percents': 'sum')
state_pcts = state_office.groupby(level=['Group 1','Group 2']).apply(lambda x: 100 * x / float(x.sum()))
此方法需要大约 21 秒才能产生相同的结果。
结果:
Group 1 Group 2 Final Group Numbers I want as percents Percent of Final Group
0 AAAH AQYR RMCH 847 82.312925
1 AAAH AQYR XDCL 182 17.687075
2 AAAH DQGO ALVF 132 12.865497
3 AAAH DQGO AVPH 894 87.134503
4 AAAH OVGH NVOO 650 43.132050
5 AAAH OVGH VKQP 857 56.867950
6 AAAH VNLY HYFW 884 65.336290
7 AAAH VNLY MOYH 469 34.663710
8 AAAH XOOC GIDS 168 23.595506
9 AAAH XOOC HTOY 544 76.404494
【讨论】:
【参考方案8】:在列或索引之间查找百分比的最优雅方法是使用pd.crosstab
。
样本数据
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)])
输出数据框是这样的
print(df)
state office_id sales
0 CA 1 764505
1 WA 2 313980
2 CO 3 558645
3 AZ 4 883433
4 CA 5 301244
5 WA 6 752009
6 CO 1 457208
7 AZ 2 259657
8 CA 3 584471
9 WA 4 122358
10 CO 5 721845
11 AZ 6 136928
只需指定要聚合的索引、列和值。 normalize 关键字将根据上下文计算跨索引或列的百分比。
result = pd.crosstab(index=df['state'],
columns=df['office_id'],
values=df['sales'],
aggfunc='sum',
normalize='index').applymap(':.2f%'.format)
print(result)
office_id 1 2 3 4 5 6
state
AZ 0.00% 0.20% 0.00% 0.69% 0.00% 0.11%
CA 0.46% 0.00% 0.35% 0.00% 0.18% 0.00%
CO 0.26% 0.00% 0.32% 0.00% 0.42% 0.00%
WA 0.00% 0.26% 0.00% 0.10% 0.00% 0.63%
【讨论】:
【参考方案9】:您可以将sum
整个DataFrame
除以state
总数:
# Copying setup from Paul H answer
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)])
# Add a column with the sales divided by state total sales.
df['sales_ratio'] = (df / df.groupby(['state']).transform(sum))['sales']
df
返回
office_id sales state sales_ratio
0 1 405711 CA 0.193319
1 2 535829 WA 0.347072
2 3 217952 CO 0.198743
3 4 252315 AZ 0.192500
4 5 982371 CA 0.468094
5 6 459783 WA 0.297815
6 1 404137 CO 0.368519
7 2 222579 AZ 0.169814
8 3 710581 CA 0.338587
9 4 548242 WA 0.355113
10 5 474564 CO 0.432739
11 6 835831 AZ 0.637686
但请注意,这仅适用于除 state
之外的所有列都是数字的,从而可以对整个 DataFrame 求和。例如,如果 office_id
是字符,则会出现错误:
df.office_id = df.office_id.astype(str)
df['sales_ratio'] = (df / df.groupby(['state']).transform(sum))['sales']
TypeError: /: 'str' 和 'str' 的操作数类型不受支持
【讨论】:
我编辑指出,这仅适用于除groupby
列之外的所有列都是数字的情况。但它在其他方面非常优雅。有没有办法让它与其他 str
列一起使用?
据我所知:***.com/questions/34099684/…【参考方案10】:
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)])
df.groupby(['state', 'office_id'])['sales'].sum().rename("weightage").groupby(level = 0).transform(lambda x: x/x.sum())
df.reset_index()
输出:
state office_id weightage
0 AZ 2 0.169814
1 AZ 4 0.192500
2 AZ 6 0.637686
3 CA 1 0.193319
4 CA 3 0.338587
5 CA 5 0.468094
6 CO 1 0.368519
7 CO 3 0.198743
8 CO 5 0.432739
9 WA 2 0.347072
10 WA 4 0.355113
11 WA 6 0.297815
【讨论】:
【参考方案11】:我认为这可以在 1 行中解决问题:
df.groupby(['state', 'office_id']).sum().transform(lambda x: x/np.sum(x)*100)
【讨论】:
我相信它会占用数据集的所有列。在这种情况下,只有一个。如果您有多个并且想要对单个执行此操作,只需在 groupby 表达式之后指定它: df.groupby(['state', 'office_id'])[[YOUR COLUMN NAME HERE]].etc 如果需要为了保持其他列不变,只需重新分配特定列 @louisD:我非常喜欢你试图保持简短的方法。不幸的是,当我尝试按照您的建议重新分配列时,出现两个错误:“ValueError: Buffer dtype mismatch, expected 'Python object' but got 'long long'”,另外(在处理第一个异常期间):“ TypeError:插入列的索引与框架索引不兼容”我使用的代码如下:df['percent'] = df.groupby(['state', 'office_id']).sum().transform(lambda x: x/np.sum(x)*100) 因此,我将发布一个单独的答案来解决这个问题。【参考方案12】:我使用的简单方法是在 2 个 groupby 之后进行合并,然后进行简单除法。
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'])['sales'].sum().reset_index()
state = df.groupby(['state'])['sales'].sum().reset_index()
state_office = state_office.merge(state, left_on='state', right_on ='state', how = 'left')
state_office['sales_ratio'] = 100*(state_office['sales_x']/state_office['sales_y'])
state office_id sales_x sales_y sales_ratio
0 AZ 2 222579 1310725 16.981365
1 AZ 4 252315 1310725 19.250033
2 AZ 6 835831 1310725 63.768601
3 CA 1 405711 2098663 19.331879
4 CA 3 710581 2098663 33.858747
5 CA 5 982371 2098663 46.809373
6 CO 1 404137 1096653 36.851857
7 CO 3 217952 1096653 19.874290
8 CO 5 474564 1096653 43.273852
9 WA 2 535829 1543854 34.707233
10 WA 4 548242 1543854 35.511259
11 WA 6 459783 1543854 29.781508
【讨论】:
【参考方案13】: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)])
grouped = df.groupby(['state', 'office_id'])
100*grouped.sum()/df[["state","sales"]].groupby('state').sum()
返回:
sales
state office_id
AZ 2 54.587910
4 33.009225
6 12.402865
CA 1 32.046582
3 44.937684
5 23.015735
CO 1 21.099989
3 31.848658
5 47.051353
WA 2 43.882790
4 10.265275
6 45.851935
【讨论】:
【参考方案14】:作为一个也在学习 pandas 的人,我发现其他答案有点含蓄,因为 pandas 将大部分工作隐藏在幕后。即通过自动匹配列和索引名称来操作操作的工作方式。这段代码应该相当于@exp1orer接受的答案的逐步版本
对于df
,我将使用别名state_office_sales
来称呼它:
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_total_sales
是state_office_sales
按index level 0
(最左侧)中的总和分组。
In: state_total_sales = df.groupby(level=0).sum()
state_total_sales
Out:
sales
state
AZ 2448009
CA 2832270
CO 1495486
WA 595859
因为两个数据框共享一个索引名和一个列名,所以 pandas 会通过共享索引找到合适的位置,例如:
In: state_office_sales / state_total_sales
Out:
sales
state office_id
AZ 2 0.448640
4 0.125865
6 0.425496
CA 1 0.288022
3 0.322169
5 0.389809
CO 1 0.206684
3 0.357891
5 0.435425
WA 2 0.321689
4 0.346325
6 0.331986
为了更好地说明这一点,这里是一个带有 XX
的部分总数,没有等价物。 Pandas 会根据索引和列名匹配位置,没有重叠的地方 pandas 会忽略它:
In: partial_total = pd.DataFrame(
data = 'sales' : [2448009, 595859, 99999],
index = ['AZ', 'WA', 'XX' ]
)
partial_total.index.name = 'state'
Out:
sales
state
AZ 2448009
WA 595859
XX 99999
In: state_office_sales / partial_total
Out:
sales
state office_id
AZ 2 0.448640
4 0.125865
6 0.425496
CA 1 NaN
3 NaN
5 NaN
CO 1 NaN
3 NaN
5 NaN
WA 2 0.321689
4 0.346325
6 0.331986
当没有共享索引或列时,这一点变得非常清楚。这里missing_index_totals
等于state_total_sales
,只是它没有索引名称。
In: missing_index_totals = state_total_sales.rename_axis("")
missing_index_totals
Out:
sales
AZ 2448009
CA 2832270
CO 1495486
WA 595859
In: state_office_sales / missing_index_totals
Out: ValueError: cannot join with no overlapping index names
【讨论】:
【参考方案15】:一线解决方案:
df.join(
df.groupby('state').agg(state_total=('sales', 'sum')),
on='state'
).eval('sales / state_total')
这会返回一系列每个办公室的比率——可以单独使用或分配给原始数据框。
【讨论】:
以上是关于使用 groupby 的 Pandas 占总数的百分比的主要内容,如果未能解决你的问题,请参考以下文章