熊猫与groupby占总数的百分比


147

这显然很简单,但是作为一个笨拙的新手,我陷入了困境。

我有一个包含3列的CSV文件,分别是该办公室的州,办公室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

我似乎无法弄清楚如何“高达”的state水平groupby与总起来sales对整个state计算分数。


3
df['sales'] / df.groupby('state')['sales'].transform('sum')似乎是最明确的答案。
Paul Rougieux

Answers:


207

Paul H的答案是正确的,您将不得不创建第二个groupby对象,但是您可以以一种更简单的方式来计算百分比-只需groupbystate_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

1
这里发生了什么?据我了解,它 x是某种类型的表,因此100 * x从直观上讲是没有意义的(尤其是当某些单元格包含诸如AZ...的字符串时)。
dhardy 2015年

5
@dhardy state_office是具有多重索引的系列-因此它只是一列,其值都是数字。进行分组后,每个分组x都是该列的子集。那有意义吗?
exp1orer

2
可以,但是对我不起作用。Python 3中的熊猫工作是否有所不同?
dhardy 2015年

1
什么level=0意思
van_d39 '16

3
@Veenit表示您正在按索引的第一级而不是按任一列进行分组。
exp1orer

54

您需要创建另一个按状态分组的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

level='state'在kwarg div告诉大熊猫广播/加入关于该值的dataframes基地state索引的水平。


4
如果您有3个索引,此方法有效吗?我首先在3列上进行了分组。然后,我仅对2进行了第二次分组,并计算了总和。然后我尝试使用divbut,level=["index1", "index2"]但它告诉我Join on level between two MultiIndex objects is ambiguous
Ger

@Ger它确实起作用,但是我无法从该描述中得知您在做什么错。在网站上搜索更多。如果找不到任何内容,请创建一个新的问题,并用一个可复制的示例演示该问题。stackoverflow.com/questions/20109391/...
保罗^ h

34

为简洁起见,我将使用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.08s)。


5
这是超级快。我建议将其作为首选的熊猫方法。真正利用了numpy的向量化和熊猫索引功能。
查尔斯

这对我也很有效,因为我正在与多个小组一起工作。谢谢。
艾琳

27

我认为这需要进行基准测试。使用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充分利用了矢量化和熊猫索引的优势。

c = df.groupby(['state', 'office_id'])['sales'].sum().rename("count")
c / c.groupby(level=0).sum()

每个循环3.42 ms ±16.7 µs
(平均±标准偏差,共运行7次,每个循环100个)


第二保罗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 ms ±24.4 µs
(平均±标准偏差,共运行7次,每个循环100个)


第三exp1orer

这是最慢的答案,因为它x.sum()针对x级别0中的每个答案进行计算。

对我来说,尽管不是目前的形式,这仍然是一个有用的答案。为了在较小的数据集上apply实现快速EDA,允许您使用方法链接将其写在一行中。因此,我们无需决定变量的名称,而实际上这在计算上非常昂贵对于您最宝贵的资源(您的大脑!)来说。

这是修改,

(
    df.groupby(['state', 'office_id'])
    .agg({'sales': 'sum'})
    .groupby(level=0)
    .apply(lambda x: 100 * x / float(x.sum()))
)

每个循环10.6 ms ±81.5 µs
(平均±标准偏差,共运行7次,每个循环100个)


因此,在小型数据集上,没有人会关心6ms。但是,这是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 s ±10.4毫秒
(平均±标准偏差,共运行7次,每个循环1次)

和exp1orer

每个循环19 s ±77.1 ms
(平均±标准偏差,共运行7次,每个循环1次)

因此,现在我们看到x10在大型,高基数的数据集上速度加快。


如果要紫外线这三个答案,一定要紫外线这三个答案!


17

(此解决方案的灵感来自本文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

3
@Cancer这是我最喜欢的答案,因为它使df保持为df(不转换为序列),仅添加%列。谢谢
冯德

这个答案的变化工作非常出色,我用transform('max')
Sheldore

11

我知道这是一个古老的问题,但是对于具有大量唯一组的数据集,exp1orer的答案非常慢(可能是由于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

9

我知道这里已经有了很好的答案。

尽管如此,我还是想贡献自己的力量,因为我觉得这样的基本问题很简单,因此应该有一个简短的解决方案,一目了然。

它也应该以可以将百分比添加为新列的方式工作,而其余数据框保持不变。最后但并非最不重要的一点是,它应该以明显的方式推广到存在多个分组级别的情况(例如,州和国家而不是仅州)。

以下代码段满足这些条件:

df['sales_ratio'] = df.groupby(['state'])['sales'].transform(lambda x: x/x.sum())

请注意,如果您仍在使用Python 2,则必须用float(x)替换lambda项的分母中的x。


这是IMO的最佳答案。唯一要添加的就是将其* 100设为一个百分比。
Bouncner,

1
@Bouncner:是的,严格来说,您必须乘以100才能得到百分比-或将新变量从“ sales_percentage”重命名为“ sales_ratio”。就个人而言,我更喜欢后者,并据此编辑了答案。感谢您的提及!
MightyCurious

2
但是,如果您有多个级别,则无法使用。
艾琳(Irene)

@irene:好点,谢谢!可能在这种情况下,df.reset_index()。groupby(['state'])['sales']。transform(lambda x:x / x.sum())会起作用。还是我忽略了什么?
MightyCurious19年

1
这个答案很好。它不涉及创建临时groupby对象,非常简洁,并且从逻辑上从左到右读取。
C. Braun

7

查找跨列或索引百分比的最优雅的方法是使用 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%

3

你可以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列一起使用?
Max Ghenis


2

我认为这可以在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]]。etcetc为了不影响其他列,只需重新分配特定的列
louisD

@louisD:我非常喜欢您尝试使其简短的方法。不幸的是,当我尝试按照您的建议重新分配列时,出现两个错误:“ ValueError:缓冲区dtype不匹配,预期为'Python object'但为'long long'”,以及(在处理第一个异常期间): TypeError:插入的索引与框架索引的索引不兼容”我使用的代码如下:df ['percent'] = df.groupby(['state','office_id'])。sum()。transform(lambda x: x / np.sum(x)* 100)因此,我将发布一个单独的答案来解决此问题。
MightyCurious

1

我使用的简单方法是在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

1
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

0

作为一个也在学习熊猫的人,我发现其他答案有些隐含,因为熊猫将大部分工作隐藏在幕后。即通过自动匹配列名和索引名来确定操作的工作方式。此代码应等效于@ 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_salesstate_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

因为这两个数据帧共享一个索引名和一个列名,熊猫将通过共享索引找到合适的位置,例如:

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其中没有等效项。大熊猫将根据索引和列名称匹配位置,在没有重叠的大熊猫将忽略它:

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

-1

一线解决方案:

df.join(
    df.groupby('state').agg(state_total=('sales', 'sum')),
    on='state'
).eval('sales / state_total')

这将返回一系列按办公室使用的比率-可以单独使用或分配给原始数据框。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.