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

我有一个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


我似乎无法弄清楚如何“达到” stategroupby水平,从而将整个salesstate总计起来计算分数。

评论

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

#1 楼

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


评论


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

– dhardy
2015年2月6日,9:42

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

–exp1orer
2015年2月8日在15:22



可以,但对我不起作用。 Python 3中的熊猫工作方式有所不同吗?

– dhardy
2015年2月9日在9:59

level = 0是什么意思?

–van_d39
16年11月22日在22:39

@Veenit表示您是按索引的第一级而不是其中一个列进行分组。

–exp1orer
16年11月23日在7:50

#2 楼

您需要制作第二个按状态分组的groupby对象,然后使用div方法:level='state'中的div kwarg告诉熊猫广播/加入数据帧基于索引的state级别中的值。

评论


如果您有3个索引,此方法有效吗?我首先在3列上进行了分组。然后,我仅对2进行了第二次分组,并计算了总和。然后,我尝试使用div,但级别为[[index1“,” index2“],但它告诉我两个MultiIndex对象之间的级别上的Join是不明确的。

– Ger
17年1月4日在13:23



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

– Paul H
17年1月4日在15:20

#3 楼

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

评论


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

–查尔斯
18 Mar 23 '18 at 12:14

这对我也很有效,因为我正在与多个小组合作。谢谢。

–艾琳
18年8月14日在6:01



#4 楼

我认为这需要进行基准测试。使用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)]
})



第一安迪·海顿




矢量化和熊猫索引的优势。

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


每个循环3.42 ms±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 ms±24.4 µs
(平均值±标准运行7次运行,每个循环100次)


第三次exp1orer


这是最慢的答案,因为它为每个x.sum()计算x在0级。

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

这里是修改,

(
    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 )包含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
})


使用Andy's,

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

和exp1orer

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

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


如果您要确保对这三个答案进行UV处理紫外线!!

#5 楼

(此解决方案的灵感来自本文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(不转换为序列),仅添加%列。谢谢

– T.Fung
5月16日13:25

这个答案的变化对我来说使用transform('max')效果很好

–谢尔顿
6月8日13:34



指向描述transform()的帖子的链接非常棒。我认为这是一个比所选解决方案更好的解决方案,但需要学习转换(我认为这是一个积极的方面:)

– pojda
12月1日14:30

#6 楼

我知道这是一个古老的问题,但是对于具有大量唯一组的数据集,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


#7 楼

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

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

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

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

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


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

评论


这是IMO的最佳答案。要添加的唯一内容就是* 100,以百分比表示。

– Bouncner
19年4月4日在11:56

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

–MightyCurious
19年5月5日在7:30

但是,如果您有多个级别,则无法使用。

–艾琳
19年6月13日在9:33

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

–MightyCurious
19年6月14日在12:38

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

– C. Braun
19年11月22日在19:56

#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列一起使用?

– Max Ghenis
17年1月25日在19:18

据我所知:stackoverflow.com/questions/34099684/…

– iggy
17年1月27日在3:22

#10 楼

我认为这可以在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
18-10-9在18:12



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

–MightyCurious
19年4月18日在6:58



#11 楼

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


#12 楼

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


#13 楼

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


,因为这两个数据帧共享一个索引名和一个列名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的部分总计,没有等效值。大熊猫将根据索引和列名匹配位置,在此位置,没有大熊猫会忽略它:

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


#14 楼

单行解决方案:

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


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