Zastosuj wiele funkcji do wielu kolumn grupowania

221

W docs pokazują, jak zastosować wiele funkcji w obiekcie GroupBy naraz przy użyciu dict z nazwami kolumn wyjście jako klucze:

In [563]: grouped['D'].agg({'result1' : np.sum,
   .....:                   'result2' : np.mean})
   .....:
Out[563]: 
      result2   result1
A                      
bar -0.579846 -1.739537
foo -0.280588 -1.402938

Działa to jednak tylko na obiekcie Groupby według grup. Kiedy podobnie dyktafon jest przekazywany do grupy przez DataFrame, oczekuje, że kluczami będą nazwy kolumn, do których funkcja zostanie zastosowana.

Chcę zastosować wiele funkcji do kilku kolumn (ale niektóre kolumny będą obsługiwane wielokrotnie). Ponadto niektóre funkcje będą zależeć od innych kolumn w obiekcie grupowania (takich jak funkcje sumif). Moje obecne rozwiązanie polega na przechodzeniu kolumna po kolumnie i robieniu czegoś podobnego do powyższego kodu, używając lambdas dla funkcji zależnych od innych wierszy. Ale zajmuje to dużo czasu (myślę, że iteracja przez obiekt grupujący zajmuje dużo czasu). Będę musiał to zmienić, aby w jednym przebiegu iterować cały obiekt grupujący według jednego obiektu, ale zastanawiam się, czy w pandach nie ma wbudowanego sposobu, aby zrobić to nieco czysto.

Na przykład próbowałem czegoś takiego

grouped.agg({'C_sum' : lambda x: x['C'].sum(),
             'C_std': lambda x: x['C'].std(),
             'D_sum' : lambda x: x['D'].sum()},
             'D_sumifC3': lambda x: x['D'][x['C'] == 3].sum(), ...)

ale zgodnie z oczekiwaniami otrzymuję KeyError (ponieważ klucze muszą być kolumną, jeśli aggsą wywoływane z DataFrame).

Czy istnieje jakiś sposób na wykonanie tego, co chciałbym zrobić, lub możliwość dodania tej funkcji, czy też będę musiał po prostu ręcznie iterować w grupie?

Dzięki

broda
źródło
2
Jeśli przejdziesz do tego pytania w 2017 roku, zapoznaj się z odpowiedzią poniżej, aby zobaczyć idiomatyczny sposób agregowania wielu kolumn razem. Aktualnie wybrana odpowiedź zawiera wiele przestarzałych danych, a mianowicie, że nie można już używać słownika słowników do zmiany nazw kolumn w wyniku grupowania.
Ted Petrou

Odpowiedzi:

282

Druga połowa obecnie akceptowanej odpowiedzi jest nieaktualna i ma dwie przestarzałe treści. Po pierwsze i najważniejsze, nie można już przekazać słownika słowników do aggmetody grupowania. Po drugie, nigdy nie używaj .ix.

Jeśli chcesz pracować z dwiema osobnymi kolumnami jednocześnie, sugerowałbym użycie applymetody, która domyślnie przekazuje ramkę danych do zastosowanej funkcji. Użyjmy podobnej ramki danych jak ta z góry

df = pd.DataFrame(np.random.rand(4,4), columns=list('abcd'))
df['group'] = [0, 0, 1, 1]
df

          a         b         c         d  group
0  0.418500  0.030955  0.874869  0.145641      0
1  0.446069  0.901153  0.095052  0.487040      0
2  0.843026  0.936169  0.926090  0.041722      1
3  0.635846  0.439175  0.828787  0.714123      1

Słownik odwzorowany z nazw kolumn na funkcje agregujące jest wciąż doskonałym sposobem na agregację.

df.groupby('group').agg({'a':['sum', 'max'], 
                         'b':'mean', 
                         'c':'sum', 
                         'd': lambda x: x.max() - x.min()})

              a                   b         c         d
            sum       max      mean       sum  <lambda>
group                                                  
0      0.864569  0.446069  0.466054  0.969921  0.341399
1      1.478872  0.843026  0.687672  1.754877  0.672401

Jeśli nie podoba ci się ta brzydka nazwa kolumny lambda, możesz użyć normalnej funkcji i podać niestandardową nazwę dla specjalnego __name__atrybutu, takiego jak ten:

def max_min(x):
    return x.max() - x.min()

max_min.__name__ = 'Max minus Min'

df.groupby('group').agg({'a':['sum', 'max'], 
                         'b':'mean', 
                         'c':'sum', 
                         'd': max_min})

              a                   b         c             d
            sum       max      mean       sum Max minus Min
group                                                      
0      0.864569  0.446069  0.466054  0.969921      0.341399
1      1.478872  0.843026  0.687672  1.754877      0.672401

Używanie applyi zwracanie serii

Teraz, jeśli masz wiele kolumn, które musiały ze sobą współdziałać, nie możesz ich użyć agg, co niejawnie przekazuje Serię do funkcji agregującej. Gdy używasz applycałej grupy jako DataFrame, zostaje ona przekazana do funkcji.

Zalecam utworzenie pojedynczej funkcji niestandardowej, która zwraca serię wszystkich agregacji. Użyj indeksu serii jako etykiet dla nowych kolumn:

def f(x):
    d = {}
    d['a_sum'] = x['a'].sum()
    d['a_max'] = x['a'].max()
    d['b_mean'] = x['b'].mean()
    d['c_d_prodsum'] = (x['c'] * x['d']).sum()
    return pd.Series(d, index=['a_sum', 'a_max', 'b_mean', 'c_d_prodsum'])

df.groupby('group').apply(f)

         a_sum     a_max    b_mean  c_d_prodsum
group                                           
0      0.864569  0.446069  0.466054     0.173711
1      1.478872  0.843026  0.687672     0.630494

Jeśli jesteś zakochany w MultiIndexes, nadal możesz zwrócić serię taką jak ta:

    def f_mi(x):
        d = []
        d.append(x['a'].sum())
        d.append(x['a'].max())
        d.append(x['b'].mean())
        d.append((x['c'] * x['d']).sum())
        return pd.Series(d, index=[['a', 'a', 'b', 'c_d'], 
                                   ['sum', 'max', 'mean', 'prodsum']])

df.groupby('group').apply(f_mi)

              a                   b       c_d
            sum       max      mean   prodsum
group                                        
0      0.864569  0.446069  0.466054  0.173711
1      1.478872  0.843026  0.687672  0.630494
Ted Petrou
źródło
3
Uwielbiam wzorzec używania funkcji, która zwraca serię. Bardzo schludny.
Stephen McAteer,
2
to jedyny sposób, w jaki udało mi się agregować ramkę danych za pomocą wielu danych wejściowych kolumny jednocześnie (przykład c_d powyżej)
Blake
2
Jestem zdezorientowany wynikami, biorąc pod uwagę podsumowanie aw grupie, 0czy to nie powinno być 0.418500 + 0.446069 = 0.864569? To samo dotyczy innych komórek, liczby nie wydają się sumować. Czy w kolejnych przykładach może być nieco inna podstawowa ramka danych?
slackline
Często używam .size () z groupby, aby zobaczyć liczbę rekordów. Czy można to zrobić za pomocą metody agg: dict? Rozumiem, że mógłbym policzyć określone pole, ale wolę, aby liczenie było niezależne od pola.
Chris Decker
1
@ slackline tak. właśnie to przetestowałem i działa dobrze. Ted musiał właśnie stworzyć ramkę kilka razy, a ponieważ została utworzona za pomocą generowania liczb losowych, dane df do faktycznego wygenerowania danych były inne niż dane ostatecznie wykorzystane w obliczeniach
Lucas H
166

W pierwszej części możesz przekazać dyktę nazw kolumn dla kluczy i listę funkcji dla wartości:

In [28]: df
Out[28]:
          A         B         C         D         E  GRP
0  0.395670  0.219560  0.600644  0.613445  0.242893    0
1  0.323911  0.464584  0.107215  0.204072  0.927325    0
2  0.321358  0.076037  0.166946  0.439661  0.914612    1
3  0.133466  0.447946  0.014815  0.130781  0.268290    1

In [26]: f = {'A':['sum','mean'], 'B':['prod']}

In [27]: df.groupby('GRP').agg(f)
Out[27]:
            A                   B
          sum      mean      prod
GRP
0    0.719580  0.359790  0.102004
1    0.454824  0.227412  0.034060

AKTUALIZACJA 1:

Ponieważ funkcja agregująca działa w Serii, odwołania do innych nazw kolumn są tracone. Aby obejść ten problem, możesz odwołać się do pełnej ramki danych i zindeksować ją za pomocą indeksów grupy w funkcji lambda.

Oto hacky obejście:

In [67]: f = {'A':['sum','mean'], 'B':['prod'], 'D': lambda g: df.loc[g.index].E.sum()}

In [69]: df.groupby('GRP').agg(f)
Out[69]:
            A                   B         D
          sum      mean      prod  <lambda>
GRP
0    0.719580  0.359790  0.102004  1.170219
1    0.454824  0.227412  0.034060  1.182901

Tutaj powstała kolumna „D” składa się ze zsumowanych wartości „E”.

AKTUALIZACJA 2:

Oto metoda, która moim zdaniem zrobi wszystko, o co poprosisz. Najpierw utwórz niestandardową funkcję lambda. Poniżej g odnosi się do grupy. Podczas agregacji g będzie serią. Przejście g.indexdo df.ix[]wybiera bieżącą grupę z df. Następnie sprawdzam, czy kolumna C jest mniejsza niż 0,5. Zwracana seria boolowska jest przekazywana do g[]której wybiera tylko te wiersze, które spełniają kryteria.

In [95]: cust = lambda g: g[df.loc[g.index]['C'] < 0.5].sum()

In [96]: f = {'A':['sum','mean'], 'B':['prod'], 'D': {'my name': cust}}

In [97]: df.groupby('GRP').agg(f)
Out[97]:
            A                   B         D
          sum      mean      prod   my name
GRP
0    0.719580  0.359790  0.102004  0.204072
1    0.454824  0.227412  0.034060  0.570441
Zelazny7
źródło
Co ciekawe, mogę również przekazać listę {funcname: func}jako wartości zamiast list, aby zachować moje niestandardowe nazwy. Ale w obu przypadkach nie mogę przejść, lambdaktóry używa innych kolumn (jak lambda x: x['D'][x['C'] < 3].sum()wyżej: „KeyError: 'D” ”). Masz pomysł, jeśli to możliwe?
beardc
Próbowałem zrobić dokładnie to i dostaję błądKeyError: 'D'
Zelazny7
Fajnie, mam to do pracy df['A'].ix[g.index][df['C'] < 0].sum(). Zaczyna się to jednak robić dość niechlujnie - myślę, że ze względu na czytelność bardziej pożądane może być ręczne zapętlenie, a ponadto nie jestem pewien, czy istnieje sposób, aby nadać mu moją preferowaną nazwę w aggargumencie (zamiast <lambda>). Będę miał nadzieję, że ktoś może poznać prostszy sposób ...
beardc
3
Możesz przekazać dykt dla wartości kolumny, {'D': {'my name':lambda function}}a to spowoduje, że wewnętrzny dykta wprowadzi nazwę kolumny.
Zelazny7
1
Uważam, że pandy obsługują teraz wiele funkcji zastosowanych w ramce danych pogrupowanej według: pandas.pydata.org/pandas-docs/stable/…
IanS
22

Jako alternatywę (głównie w zakresie estetyki) do odpowiedzi Teda Petrou, uznałem, że wolę nieco bardziej zwartą listę. Proszę nie rozważać akceptacji, to po prostu bardziej szczegółowy komentarz do odpowiedzi Teda oraz kod / dane. Python / pandy nie jest moim pierwszym / najlepszym, ale znalazłem to, aby dobrze czytać:

df.groupby('group') \
  .apply(lambda x: pd.Series({
      'a_sum'       : x['a'].sum(),
      'a_max'       : x['a'].max(),
      'b_mean'      : x['b'].mean(),
      'c_d_prodsum' : (x['c'] * x['d']).sum()
  })
)

          a_sum     a_max    b_mean  c_d_prodsum
group                                           
0      0.530559  0.374540  0.553354     0.488525
1      1.433558  0.832443  0.460206     0.053313

Uważam, że bardziej przypomina dplyrrury i data.tablepowiązane łańcuchy poleceń. Nie mówiąc już, że są lepsze, po prostu bardziej mi znane. (Z pewnością doceniam siłę i, dla wielu, preferencję używania bardziej sformalizowanych deffunkcji dla tego typu operacji. Jest to po prostu alternatywa, niekoniecznie lepsza.)


Wygenerowałem dane w taki sam sposób jak Ted, dodam ziarno dla odtwarzalności.

import numpy as np
np.random.seed(42)
df = pd.DataFrame(np.random.rand(4,4), columns=list('abcd'))
df['group'] = [0, 0, 1, 1]
df

          a         b         c         d  group
0  0.374540  0.950714  0.731994  0.598658      0
1  0.156019  0.155995  0.058084  0.866176      0
2  0.601115  0.708073  0.020584  0.969910      1
3  0.832443  0.212339  0.181825  0.183405      1
r2evans
źródło
2
Najbardziej podoba mi się ta odpowiedź. Jest to podobne do rur dplyr w R.
Renhuai,
18

Pandas >= 0.25.0, nazwane agregacje

Od wersji pandy 0.25.0lub wyższej odchodzimy od agregacji i zmiany nazw opartych na słowniku i przechodzimy w kierunku nazwanych agregacji, które akceptują a tuple. Teraz możemy jednocześnie agregować + zmienić nazwę na bardziej pouczającą nazwę kolumny:

Przykład :

df = pd.DataFrame(np.random.rand(4,4), columns=list('abcd'))
df['group'] = [0, 0, 1, 1]

          a         b         c         d  group
0  0.521279  0.914988  0.054057  0.125668      0
1  0.426058  0.828890  0.784093  0.446211      0
2  0.363136  0.843751  0.184967  0.467351      1
3  0.241012  0.470053  0.358018  0.525032      1

Zastosuj GroupBy.aggz nazwaną agregacją:

df.groupby('group').agg(
             a_sum=('a', 'sum'),
             a_mean=('a', 'mean'),
             b_mean=('b', 'mean'),
             c_sum=('c', 'sum'),
             d_range=('d', lambda x: x.max() - x.min())
)

          a_sum    a_mean    b_mean     c_sum   d_range
group                                                  
0      0.947337  0.473668  0.871939  0.838150  0.320543
1      0.604149  0.302074  0.656902  0.542985  0.057681
Erfan
źródło
Lubię te nazwane agregacje, ale nie widziałem, jak mamy ich używać z wieloma kolumnami?
Simon Woodhead
Dobre pytanie, nie mogłem tego rozgryźć, wątpię, że jest to możliwe (jeszcze). Otworzyłem bilet na to. Zachowa moje pytanie, a ty będziesz na bieżąco. Dzięki za wskazanie @ SimonWoodhead
Erfan
4

Nowości w wersji 0.25.0.

Aby wesprzeć agregację specyficzną dla kolumny z kontrolą nazw kolumn wyjściowych, pandy akceptują specjalną składnię w GroupBy.agg () , znaną jako „agregacja nazwana” , gdzie

  • Słowa kluczowe to wyjściowe nazwy kolumn
  • Wartości to krotki, których pierwszym elementem jest kolumna do wybrania, a drugim elementem jest agregacja, którą należy zastosować do tej kolumny. Panda udostępnia pandas.NamedAgg o nazwie temple z polami [„kolumna”, „aggfunc”], aby wyjaśnić, jakie są argumenty. Jak zwykle agregacja może być wywoływalnym lub aliasem ciągu.
    In [79]: animals = pd.DataFrame({'kind': ['cat', 'dog', 'cat', 'dog'],
       ....:                         'height': [9.1, 6.0, 9.5, 34.0],
       ....:                         'weight': [7.9, 7.5, 9.9, 198.0]})
       ....: 

    In [80]: animals
    Out[80]: 
      kind  height  weight
    0  cat     9.1     7.9
    1  dog     6.0     7.5
    2  cat     9.5     9.9
    3  dog    34.0   198.0

    In [81]: animals.groupby("kind").agg(
       ....:     min_height=pd.NamedAgg(column='height', aggfunc='min'),
       ....:     max_height=pd.NamedAgg(column='height', aggfunc='max'),
       ....:     average_weight=pd.NamedAgg(column='weight', aggfunc=np.mean),
       ....: )
       ....: 
    Out[81]: 
          min_height  max_height  average_weight
    kind                                        
    cat          9.1         9.5            8.90
    dog          6.0        34.0          102.75

pandas.NamedAgg jest tylko imieniem. Krotki zwykłe są również dozwolone.

    In [82]: animals.groupby("kind").agg(
       ....:     min_height=('height', 'min'),
       ....:     max_height=('height', 'max'),
       ....:     average_weight=('weight', np.mean),
       ....: )
       ....: 
    Out[82]: 
          min_height  max_height  average_weight
    kind                                        
    cat          9.1         9.5            8.90
    dog          6.0        34.0          102.75

Dodatkowe argumenty słów kluczowych nie są przekazywane do funkcji agregujących. Tylko pary (column, aggfunc) powinny być przekazywane jako ** kwargs. Jeśli funkcje agregujące wymagają dodatkowych argumentów, częściowo zastosuj je za pomocą funkcji funools.partial ().

Nazwana agregacja obowiązuje również w przypadku agregacji grupowych według serii. W tym przypadku nie ma wyboru kolumny, więc wartości są tylko funkcjami.

    In [84]: animals.groupby("kind").height.agg(
       ....:     min_height='min',
       ....:     max_height='max',
       ....: )
       ....: 
    Out[84]: 
          min_height  max_height
    kind                        
    cat          9.1         9.5
    dog          6.0        34.0
exan
źródło
3

Odpowiedź Teda jest niesamowita. Skończyło się na użyciu mniejszej wersji tego na wypadek, gdyby ktoś był zainteresowany. Przydatne, gdy szukasz jednej agregacji, która zależy od wartości z wielu kolumn:

utwórz ramkę danych

df=pd.DataFrame({'a': [1,2,3,4,5,6], 'b': [1,1,0,1,1,0], 'c': ['x','x','y','y','z','z']})


   a  b  c
0  1  1  x
1  2  1  x
2  3  0  y
3  4  1  y
4  5  1  z
5  6  0  z

grupowanie i agregowanie z zastosowaniem (za pomocą wielu kolumn)

df.groupby('c').apply(lambda x: x['a'][(x['a']>1) & (x['b']==1)].mean())

c
x    2.0
y    4.0
z    5.0

grupowanie i agregowanie za pomocą agregacji (przy użyciu wielu kolumn)

Podoba mi się to podejście, ponieważ nadal mogę używać agregacji. Być może ludzie poinformują mnie, dlaczego zastosowanie jest konieczne, aby uzyskać dostęp do wielu kolumn podczas agregacji na grupach.

Teraz wydaje się to oczywiste, ale dopóki nie wybierzesz interesującej kolumny bezpośrednio po grupie , będziesz mieć dostęp do wszystkich kolumn ramki danych z poziomu funkcji agregacji.

tylko dostęp do wybranej kolumny

df.groupby('c')['a'].aggregate(lambda x: x[x>1].mean())

dostęp do wszystkich kolumn, ponieważ wybór jest przecież magią

df.groupby('c').aggregate(lambda x: x[(x['a']>1) & (x['b']==1)].mean())['a']

lub podobnie

df.groupby('c').aggregate(lambda x: x['a'][(x['a']>1) & (x['b']==1)].mean())

Mam nadzieję, że to pomoże.

Campo
źródło