大熊猫占大熊猫总数的百分比

这显然很简单,但作为一个麻木的新手,我被卡住了。

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

我似乎无法弄清楚如何“达到”groupbystate级别,将整个statesales加起来以计算分数。

443055 次浏览

你需要创建第二个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级别中的值广播/加入数据框架。

更新2022 - 03

这个答案 by 癌症使用transform看起来比我原来的答案要好得多!

df['sales'] / df.groupby('state')['sales'].transform('sum')

感谢这样的评论通过保罗Rougieux使它浮出水面。

原答案(2014)

Paul H的回答是正确的,你必须创建第二个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

你可以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']

/: 'str'和'str'不支持的操作数类型

我知道这是一个老问题,但是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

为了简洁起见,我使用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(使用激进的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

这似乎比其他答案的性能稍好(对我来说,大约0.08秒,是Radical回答速度的两倍)。

我使用的简单方法是在2组之后合并,然后做简单的除法。

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

我认为这一行就可以做到:

df.groupby(['state', 'office_id']).sum().transform(lambda x: x/np.sum(x)*100)

我认为这需要基准测试。使用OP的原始数据帧,

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 癌症

熊猫变形看起来更快。

df['sales'] / df.groupby('state')['sales'].transform('sum')

1.32毫秒±352µs /环路
(平均值±标准度。dev. of 7次运行,每次100次循环)

1日安迪·海登

正如他的回答所评论的那样,安迪充分利用了向量化和熊猫索引。

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

3.42毫秒±16.7µs /环路
(平均值±标准度。dev. of 7次运行,每次100次循环)


2日保罗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 /环路
(平均值±标准度。dev. of 7次运行,每次100次循环)


3日exp1orer

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

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

这是修改,

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

10.6毫秒±81.5µs /环路
(平均值±标准度。dev. of 7次运行,每次100次循环)


所以没有人会关心小数据集上的6毫秒。然而,这是3倍的速度,在一个更大的数据集上,这将产生巨大的差异。

加上上面的代码,我们创建了一个形状为(12,000,000,3)的DataFrame,其中包含14412个状态类别和600个office_ids,

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
})

使用癌症的,

0.791秒±19.4 ms每个循环 (7次运行,每个循环1次)

使用安迪的,

2 s±10.4 ms per loop
(7次运行,每个循环1次)

和exp1orer

19世纪±77.1 ms per loop
(7次运行,每个循环1次)

所以现在我们看到,用Andy的x10加速了大型、高基数数据集,而用Caner的x20加快了令人印象深刻的速度。


如果你要UV这个答案,一定要UV这三个答案!!

编辑:添加了Caner基准

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

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

我意识到这里已经有了很好的答案。

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

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

下面的代码段满足这些条件:

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

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

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

一行的解决方案:

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

这将返回一系列每个办公室的比率——可以单独使用,也可以分配给原始数据框架。

即通过自动匹配列名和索引名来实现操作。这段代码应该相当于@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是根据index level 0(最左边)中的总数分组的state_office_sales

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,只是它没有index-name。

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


df.groupby('state').office_id.value_counts(normalize = True)

我使用了value_counts方法,但它返回的百分比类似0.700.30,而不是7030