Porcentaje de pandas del total con groupby
Obviamente, esto es simple, pero como novato me estoy estancando.
Tengo un archivo CSV que contiene 3 columnas, el Estado, el ID de la oficina y las Ventas de esa oficina.
Quiero calcular el porcentaje de ventas por oficina en un estado determinado (el total de todos los porcentajes en cada estado es 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'})
Esto devuelve:
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
Parece que no puedo entender cómo "alcanzar" el state
nivel del groupby
total sales
para state
calcular la fracción.
Actualización 2022-03
¡ Esta respuesta del uso de Canertransform
se ve mucho mejor que mi respuesta original!
df['sales'] / df.groupby('state')['sales'].transform('sum')
Gracias a este comentario de Paul Rougieux por sacarlo a la luz.
Respuesta original (2014)
La respuesta de Paul H es correcta: tendrás que hacer un segundo groupby
objeto, pero puedes calcular el porcentaje de una manera más sencilla: simplemente groupby
divide state_office
la sales
columna por su suma. Copiando el comienzo de la respuesta de 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()))
Devoluciones:
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
(Esta solución está inspirada en este artículo https://pbpython.com/pandas_transform.html )
Considero que la siguiente solución es la más simple (y probablemente la más rápida) usando transformation
:
Transformación: si bien la agregación debe devolver una versión reducida de los datos, la transformación puede devolver una versión transformada de los datos completos para recombinarlos. Para tal transformación, la salida tiene la misma forma que la entrada.
Entonces, usando transformation
, la solución es de 1 línea:
df['%'] = 100 * df['sales'] / df.groupby('state')['sales'].transform('sum')
Y si imprimes:
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
Necesita crear un segundo objeto groupby que agrupe por estados y luego usar el div
método:
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
el level='state'
kwarg in div
le dice a los pandas que transmitan/se unan a los marcos de datos en función de los valores en el state
nivel del índice.
Para ser conciso, usaría 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
Para múltiples grupos tienes que usar transform (usando df de Radical ):
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
Esto parece tener un rendimiento ligeramente mayor que las otras respuestas (un poco menos del doble de la velocidad de la respuesta de Radical, para mí ~0,08 s).