Panda’s percentage van totaal met groupby

Dit is natuurlijk eenvoudig, maar als een numpy newbe loop ik vast.

Ik heb een CSV-bestand met drie kolommen, de staat, de kantoor-ID en de verkoop voor dat kantoor.

Ik wil het percentage van de verkoop per kantoor in een bepaalde staat berekenen (het totaal van alle percentages in elke staat is 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'})

Dit geeft als resultaat:

                 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

Ik kan er niet achter komen hoe ik naar het state-niveau van de groupbymoet “bereiken” om de salesop te tellen voor de hele stateom de breuk te berekenen.


Antwoord 1, autoriteit 100%

Het antwoord van Paul His juist dat je een tweede groupby-object moet maken, maar u kunt het percentage op een eenvoudigere manier berekenen — gewoon groupbyde state_officeen de kolom salesdelen door de som. Het begin van het antwoord van Paul H kopiëren:

# 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()))

Retouren:

                    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

Antwoord 2, autoriteit 22%

Je moet een tweede groupby-object maken dat is gegroepeerd op status, en vervolgens de div-methode gebruiken:

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

de level='state'kwarg in divvertelt panda’s om de dataframes uit te zenden/aan te sluiten op basis van de waarden in het state-niveau van de index.


Antwoord 3, autoriteit 19%

(Deze oplossing is geïnspireerd op dit artikel https://pbpython.com/pandas_transform.html)

Ik vind de volgende oplossing de eenvoudigste (en waarschijnlijk de snelste) met behulp van transformation:

Transformatie: hoewel aggregatie een gereduceerde versie van de . moet retourneren
data, transformatie kan een getransformeerde versie van de volledige teruggeven
gegevens opnieuw te combineren. Voor zo’n transformatie is de output hetzelfde
vorm als invoer.

Dus met behulp van transformationis de oplossing 1-liner:

df['%'] = 100 * df['sales'] / df.groupby('state')['sales'].transform('sum')

En als u afdrukt:

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

Antwoord 4, autoriteit 18%

Voor de beknoptheid zou ik de SeriesGroupBy gebruiken:

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

Voor meerdere groepen moet je transform gebruiken (met behulp van Radical’s 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

Dit lijkt iets beter te presteren dan de andere antwoorden (slechts minder dan twee keer de snelheid van Radical’s antwoord, voor mij ~0.08s).


Antwoord 5, autoriteit 14%

Ik denk dat hiervoor benchmarking nodig is. Het originele DataFrame van OP gebruiken,

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

1e Andy Hayden

Zoals commentaar op zijn antwoord, maakt Andy optimaal gebruik van vectorisatie en panda-indexering.

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

3,42 ms± 16,7 µs per lus
(gemiddelde ± standaard ontwikkeling van 7 runs, 100 lussen elk)


2e 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 per lus
(gemiddelde ± standaard ontwikkeling van 7 runs, 100 lussen elk)


3e verkenner

Dit is het langzaamste antwoord omdat het x.sum()berekent voor elke xin niveau 0.

Voor mij is dit nog steeds een nuttig antwoord, hoewel niet in zijn huidige vorm. Voor snelle EDA op kleinere datasets, kunt u met applymethode chaininggebruiken om schrijf dit in een enkele regel. We verwijderen daarom de noodzaak om de naam van een variabele te bepalen, die in feite erg computationeel duuris voor uw meest waardevolle hulpbron (je brein!!).

Hier is de wijziging,

(
    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 per lus
(gemiddelde ± standaard ontwikkeling van 7 runs, 100 lussen elk)


Dus niemand maakt zich druk om 6ms op een kleine dataset. Dit is echter 3x sneller en op een grotere dataset met groupby’s met hoge kardinaliteit zal dit een enorm verschil maken.

Toevoegend aan de bovenstaande code, maken we een DataFrame met vorm (12.000.000, 3) met 14412 statuscategorieën en 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
})

Andy’s gebruiken,

2 s± 10,4 ms per lus
(gemiddelde ± standaard ontwikkeling van 7 runs, elk 1 lus)

en verkenner

19 s± 77,1 ms per lus
(gemiddelde ± standaard ontwikkeling van 7 runs, elk 1 lus)

Dus nu zien we x10 sneller worden bij grote datasets met hoge kardinaliteit.


Zorg ervoor dat je deze drie antwoorden UV gebruikt als je deze UV ​​gebruikt!!


Antwoord 6, autoriteit 8%

Ik realiseer me dat er hier al goede antwoorden zijn.

Toch zou ik mijn eigen bijdrage willen leveren, want ik voel voor een elementaire, eenvoudige vraag als deze, er moet een korte oplossing zijn die in één oogopslag begrijpelijk is.

Het zou ook zo moeten werken dat ik de percentages als een nieuwe kolom kan toevoegen, terwijl de rest van het dataframe ongewijzigd blijft. Last but not least, het zou op een voor de hand liggende manier moeten generaliseren naar het geval waarin er meer dan één groeperingsniveau is (bijv. staat en land in plaats van alleen staat).

Het volgende fragment voldoet aan deze criteria:

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

Merk op dat als je nog steeds Python 2 gebruikt, je de x in de noemer van de lambda-term moet vervangen door float(x).


Antwoord 7, autoriteit 4%

Ik weet dat dit een oude vraag is, maar het antwoord van exp1orer’sis erg traag voor datasets met een groot aantal unieke groepen (waarschijnlijk vanwege de lambda). Ik bouwde voort op hun antwoord om er een matrixberekening van te maken, dus nu is het supersnel! Hieronder staat de voorbeeldcode:

Maak het testdataframe met 50.000 unieke groepen

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

Gegroepeerd ziet het er als volgt uit:

                            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]

Arraymethode om percentage te vinden:

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

Deze methode duurt ongeveer ~0,15 seconden

Beste antwoordmethode (met behulp van de lambda-functie):

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

Deze methode duurt ongeveer ~21 seconden om hetzelfde resultaat te produceren.

Het resultaat:

     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

Antwoord 8, autoriteit 3%

De meest elegante manier om percentages in kolommen of indexen te vinden, is door pd.crosstabte gebruiken.

Voorbeeldgegevens

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

Het uitvoerdataframe ziet er zo uit

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

Geef gewoon de index, kolommen en waarden op om te aggregeren. Het trefwoord normaliseren berekent het % over de index of kolommen, afhankelijk van de context.

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%

Antwoord 9, autoriteit 2%

Je kunt het hele sumDataFrameen delen door het statetotaal:

# 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

Retourneren

   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

Maar houd er rekening mee dat dit alleen werkt omdat alle kolommen behalve statenumeriek zijn, waardoor sommatie van het gehele DataFrame mogelijk is. Als bijvoorbeeld office_ideen teken is, krijg je een foutmelding:

df.office_id = df.office_id.astype(str)
df['sales_ratio'] = (df / df.groupby(['state']).transform(sum))['sales']

TypeError: niet-ondersteunde operandtype(s) voor /: ‘str’ en ‘str’


Antwoord 10, autoriteit 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)]})
df.groupby(['state', 'office_id'])['sales'].sum().rename("weightage").groupby(level = 0).transform(lambda x: x/x.sum())
df.reset_index()

Uitvoer:

   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

Antwoord 11

Ik denk dat dit de truc in 1 regel zou doen:

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

Antwoord 12

Een eenvoudige manier die ik heb gebruikt, is een samenvoeging na de 2 groupby’s en vervolgens een eenvoudige verdeling.

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

Antwoord 13

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

Retouren:

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

Antwoord 14

Als iemand die ook panda’s leert, vond ik de andere antwoorden een beetje impliciet, aangezien panda’s het meeste werk achter de schermen verbergen. Namelijk hoe de bewerking werkt door automatisch kolom- en indexnamen op elkaar af te stemmen. Deze code moet gelijk zijn aan een stapsgewijze versie van het geaccepteerde antwoord van @exp1orer

Met de dfnoem ik het onder de alias 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_salesis state_office_salesgegroepeerd op totaalbedragen in index level 0(meest links).

In:   state_total_sales = df.groupby(level=0).sum()
      state_total_sales
Out: 
       sales
state   
AZ     2448009
CA     2832270
CO     1495486
WA     595859

Omdat de twee dataframes een indexnaam en een kolomnaam delen, zullen panda’s de juiste locaties vinden via gedeelde indexen zoals:

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

Om dit nog beter te illustreren, is hier een gedeeltelijk totaal met een XXdie geen equivalent heeft. Panda’s matchen de locatie op basis van index- en kolomnamen, waar er geen overlap is, zullen panda’s deze negeren:

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

Dit wordt heel duidelijk wanneer er geen gedeelde indexen of kolommen zijn. Hier is missing_index_totalsgelijk aan state_total_salesbehalve dat het geen indexnaam heeft.

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

Antwoord 15

One-line oplossing:

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

Dit retourneert een reeks verhoudingen per kantoor — die afzonderlijk kunnen worden gebruikt of aan het oorspronkelijke dataframe kunnen worden toegewezen.

Other episodes