Pandas按组计算总百分比

304 投票
17 回答
527385 浏览
提问于 2025-04-18 04:49

这看起来很简单,但作为一个刚接触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的层级,以便把整个statesales加起来,来计算比例。

17 个回答

50

我觉得这需要进行性能测试。使用原始的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的基准测试

60

为了简洁起见,我会使用 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 秒)。

73

你需要创建一个新的分组对象,这个对象是按照州来分组的,然后使用 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 这一层的值来进行数据的广播或连接。

97

这个解决方案的灵感来自于这篇文章 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
383

更新 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

撰写回答