Porcentaje de pandas del total con groupby

Resuelto erikcw asked hace 10 años • 17 respuestas

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 statenivel del groupbytotal salespara statecalcular la fracción.

erikcw avatar Apr 30 '14 06:04 erikcw
Aceptado

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 groupbyobjeto, pero puedes calcular el porcentaje de una manera más sencilla: simplemente groupbydivide state_officela salescolumna 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
exp1orer avatar Apr 29 '2014 23:04 exp1orer

(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
Caner avatar Aug 05 '2019 13:08 Caner

Necesita crear un segundo objeto groupby que agrupe por estados y luego usar el divmé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 divle dice a los pandas que transmitan/se unan a los marcos de datos en función de los valores en el statenivel del índice.

Paul H avatar Apr 29 '2014 23:04 Paul H

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

Andy Hayden avatar Nov 12 '2017 19:11 Andy Hayden