Pandas процент от общего числа с groupby
Это, очевидно, просто, но как новый новичок, я застрял.
У меня есть файл CSV, который содержит 3 столбца, состояние, идентификатор Office и Sales для этого офиса.
Я хочу рассчитать процент продаж на офис в определенном состоянии (общее количество всех процентов в каждом состоянии составляет 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
для вычисления доли.
Ответы
Ответ 1
Пол H ответ прав, что вам придется сделать второй объект groupby
, но вы можете рассчитать процент более простым способом - просто groupby
state_office
и разделите столбец sales
на его сумму. Копирование начала ответа Пола Х:
# 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
Ответ 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
level='state'
kwarg в div
сообщает pandas о распространении/объединении базы данных dataframes с значениями уровня state
индекса.
Ответ 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
Для нескольких групп вам необходимо использовать преобразование (используя 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
Это кажется немного более результативным, чем другие ответы (чуть меньше удвоенной скорости радикального ответа, для меня ~ 0.08s).
Ответ 4
Я знаю, что это старый вопрос, но ответ exp1orer очень медленный для наборов данных с большим количеством уникальных групп (вероятно, из-за лямбда). Я построил свой ответ, чтобы превратить его в массивный расчет, поэтому теперь он очень быстрый! Ниже приведен пример кода:
Создайте тестовый фрейм с 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 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 секунд
Метод верхнего ответа (с использованием лямбда-функции):
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
Ответ 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)]
})
Как прокомментировал его ответ, Энди в полной мере использует векторизацию и индексацию панд.
c = df.groupby(['state', 'office_id'])['sales'].sum().rename("count")
c / c.groupby(level=0).sum()
3,42 мс ± 16,7 мкс на цикл
(среднее ± стандартное отклонение от 7 прогонов, 100 петель каждый)
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 петель каждый)
Это самый медленный ответ, так как он вычисляет 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 мс ± 81,5 мкс на цикл
(среднее ± стандартное отклонение от 7 прогонов, 100 петель каждый)
Таким образом, никто не собирается заботиться о 6 мс на небольшом наборе данных. Тем не менее, это в 3 раза быстрее, и для больших массивов данных с большим количеством участников группы это будет иметь огромное значение.
В дополнение к приведенному выше коду мы создаем DataFrame с формой (12 000 000, 3) с 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
})
Используя Энди,
2 с ± 10,4 мс на цикл
(среднее ± стандартное отклонение от 7 прогонов, по 1 петле каждый)
и exp1orer
19 с ± 77,1 мс на цикл
(среднее ± стандартное отклонение от 7 прогонов, по 1 петле каждый)
Так что теперь мы видим ускорение в 10 раз на больших наборах данных с большим количеством элементов.
Обязательно УФ эти три ответа, если вы УФ этот !!
Ответ 6
Вы можете sum
целое DataFrame
и делить на state
total:
# 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'
Ответ 7
Я думаю, что это бы сработало в 1 строку:
df.groupby(['state', 'office_id']).sum().transform(lambda x: x/np.sum(x)*100)
Ответ 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
Я понимаю, что здесь уже есть хорошие ответы.
Я, тем не менее, хотел бы внести свой вклад, потому что я чувствую к элементарному, простому вопросу, как этот, должно быть краткое решение, которое понятно с первого взгляда.
Он также должен работать таким образом, чтобы я мог добавить проценты в качестве нового столбца, оставив остальную часть кадра данных нетронутой. И последнее, но не менее важное: оно должно обобщаться очевидным образом на случай, когда существует более одного уровня группировки (например, штат и страна, а не только штат).
Следующий фрагмент кода соответствует этим критериям:
df['sales_ratio'] = df.groupby(['state'])['sales'].transform(lambda x: x/x.sum())
Обратите внимание, что если вы все еще используете Python 2, вам придется заменить x в знаменателе лямбда-термина на float (x).
Ответ 10
Простой способ, который я использовал, - это объединение после 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
Ответ 11
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