Python Pandas Jak przypisać wyniki operacji grupowania z powrotem do kolumn w nadrzędnej ramce danych?

83

Mam następującą ramkę danych w IPythonie, gdzie każdy wiersz jest pojedynczą akcją:

In [261]: bdata
Out[261]:
<class 'pandas.core.frame.DataFrame'>
Int64Index: 21210 entries, 0 to 21209
Data columns:
BloombergTicker      21206  non-null values
Company              21210  non-null values
Country              21210  non-null values
MarketCap            21210  non-null values
PriceReturn          21210  non-null values
SEDOL                21210  non-null values
yearmonth            21210  non-null values
dtypes: float64(2), int64(1), object(4)

Chcę zastosować operację grupowania, która oblicza średni zwrot ważony limitem ze wszystkiego, dla każdej daty w kolumnie „yearmonth”.

Działa to zgodnie z oczekiwaniami:

In [262]: bdata.groupby("yearmonth").apply(lambda x: (x["PriceReturn"]*x["MarketCap"]/x["MarketCap"].sum()).sum())
Out[262]:
yearmonth
201204      -0.109444
201205      -0.290546

Ale potem chcę posortować „rozgłaszanie” tych wartości z powrotem do indeksów w oryginalnej ramce danych i zapisać je jako stałe kolumny, w których daty są zgodne.

In [263]: dateGrps = bdata.groupby("yearmonth")

In [264]: dateGrps["MarketReturn"] = dateGrps.apply(lambda x: (x["PriceReturn"]*x["MarketCap"]/x["MarketCap"].sum()).sum())
---------------------------------------------------------------------------
TypeError                                 Traceback (most recent call last)
/mnt/bos-devrnd04/usr6/home/espears/ws/Research/Projects/python-util/src/util/<ipython-input-264-4a68c8782426> in <module>()
----> 1 dateGrps["MarketReturn"] = dateGrps.apply(lambda x: (x["PriceReturn"]*x["MarketCap"]/x["MarketCap"].sum()).sum())

TypeError: 'DataFrameGroupBy' object does not support item assignment

Zdaję sobie sprawę, że to naiwne zadanie nie powinno działać. Ale jaki jest „właściwy” idiom Pandy do przypisywania wyniku operacji grupowania do nowej kolumny w nadrzędnej ramce danych?

Na koniec chcę, aby kolumna o nazwie „MarketReturn” była powtarzalną wartością stałą dla wszystkich indeksów, które mają pasującą datę do danych wyjściowych operacji grupowania.

Jeden hack, aby to osiągnąć, wyglądałby następująco:

marketRetsByDate  = dateGrps.apply(lambda x: (x["PriceReturn"]*x["MarketCap"]/x["MarketCap"].sum()).sum())

bdata["MarketReturn"] = np.repeat(np.NaN, len(bdata))

for elem in marketRetsByDate.index.values:
    bdata["MarketReturn"][bdata["yearmonth"]==elem] = marketRetsByDate.ix[elem]

Ale to jest powolne, złe i nietypowe.

ely
źródło
Przypisujesz z powrotem do zgrupowanego obiektu zamiast oryginalnej ramki.
Wouter Overmeire
2
Wiem o tym i powiedziałem to bezpośrednio pod błędem, gdzie powiedziałem: „Zdaję sobie sprawę, że to naiwne zadanie nie powinno działać. Ale jaki jest„ właściwy ”idiom Pandas do przypisywania wyniku operacji grupowej do nowej kolumny na rodzicu ramka danych?" Wykonywanie przypisania z moją oryginalną ramką danych na LHS też nie działa, a jest jeszcze mniej intuicyjne niż dodanie kolumny na poziomie GroupBy-object.
ely

Odpowiedzi:

74
In [97]: df = pandas.DataFrame({'month': np.random.randint(0,11, 100), 'A': np.random.randn(100), 'B': np.random.randn(100)})

In [98]: df.join(df.groupby('month')['A'].sum(), on='month', rsuffix='_r')
Out[98]:
           A         B  month       A_r
0  -0.040710  0.182269      0 -0.331816
1  -0.004867  0.642243      1  2.448232
2  -0.162191  0.442338      4  2.045909
3  -0.979875  1.367018      5 -2.736399
4  -1.126198  0.338946      5 -2.736399
5  -0.992209 -1.343258      1  2.448232
6  -1.450310  0.021290      0 -0.331816
7  -0.675345 -1.359915      9  2.722156
Wouter Overmeire
źródło
To nadal wymaga ode mnie zapisywania obliczeń grupowych, zamiast przypisywania ich bezpośrednio do LHS w linii, w której wykonuję operację grupowania. Zastosuj może być trochę lepsze niż pętla w moim hacku na dole pytania, ale w zasadzie to ten sam pomysł.
ely
Dołącz może to zrobić, ale będziesz musiał zmienić nazwę dodanej kolumny. W tym przypadku A_r to new_col.
Wouter Overmeire
Przykład złączenia na dole działa, ale nie jest jasno przedstawiony. Jeśli masz ochotę usunąć pierwszą część odpowiedzi i uczynić drugą część bardziej zrozumiałą, oprócz akceptacji zagłosuję za głosem.
ely
12
Usunąłem pierwsze podejście. Szczerze mówiąc, wydaje mi się, że kod mówi sam za siebie, nie krępuj się edytować, jeśli chcesz dodać wyjaśnienia lub odniesienia do dokumentów. Nie przepadam za systemem głosowania, tylko po to, żeby trochę wesprzeć pandy.
Wouter Overmeire
1
Długo szukałem tej odpowiedzi, trochę nekro, ale dzięki! +1
Dan Carter,
52

Chociaż wciąż badam wszystkie niewiarygodnie inteligentne sposoby applyłączenia podanych elementów, oto inny sposób dodania nowej kolumny w rodzicu po operacji grupowania.

In [236]: df
Out[236]: 
  yearmonth    return
0    201202  0.922132
1    201202  0.220270
2    201202  0.228856
3    201203  0.277170
4    201203  0.747347

In [237]: def add_mkt_return(grp):
   .....:     grp['mkt_return'] = grp['return'].sum()
   .....:     return grp
   .....: 

In [238]: df.groupby('yearmonth').apply(add_mkt_return)
Out[238]: 
  yearmonth    return  mkt_return
0    201202  0.922132    1.371258
1    201202  0.220270    1.371258
2    201202  0.228856    1.371258
3    201203  0.277170    1.024516
4    201203  0.747347    1.024516
Garrett
źródło
1
Możesz to również zrobić bez definiowania funkcji za pomocą lambdy i przypisać:df.groupby('yearmonth').apply(lambda grp: grp.assign(mkt_return=grp['return'].sum()))
krassowski 22.10.2020
32

Zgodnie z ogólną zasadą podczas korzystania z groupby (), jeśli używasz funkcji .transform (), pandy zwrócą tabelę o takiej samej długości jak oryginał. Gdy używasz innych funkcji, takich jak .sum () lub .first (), pandy zwrócą tabelę, w której każdy wiersz jest grupą.

Nie jestem pewien, jak to działa z zastosuj, ale implementowanie skomplikowanych funkcji lambda z transformacją może być dość trudne, więc strategia, która wydaje mi się najbardziej pomocna, polega na utworzeniu potrzebnych zmiennych, umieszczeniu ich w oryginalnym zbiorze danych, a następnie wykonaniu tam moich operacji.

Jeśli najpierw zrozumiem, co starasz się zrobić poprawnie, możesz obliczyć całkowitą kapitalizację rynkową dla każdej grupy:

bdata['group_MarketCap'] = bdata.groupby('yearmonth')['MarketCap'].transform('sum')

Spowoduje to dodanie kolumny o nazwie „group_MarketCap” do Twoich pierwotnych danych, która będzie zawierać sumę limitów rynkowych dla każdej grupy. Następnie możesz bezpośrednio obliczyć wartości ważone:

bdata['weighted_P'] = bdata['PriceReturn'] * (bdata['MarketCap']/bdata['group_MarketCap'])

Na koniec obliczysz średnią ważoną dla każdej grupy przy użyciu tej samej funkcji przekształcającej:

bdata['MarketReturn'] = bdata.groupby('yearmonth')['weighted_P'].transform('sum')

W ten sposób buduję swoje zmienne. Czasami możesz zrobić wszystko jednym poleceniem, ale to nie zawsze działa z groupby (), ponieważ przez większość czasu pandy muszą utworzyć wystąpienie nowego obiektu, aby operować na nim w pełnej skali zbioru danych (tj. Nie możesz dodaj dwie kolumny razem, jeśli jeszcze jedna nie istnieje).

Mam nadzieję że to pomoże :)

seeiespi
źródło
24

Czy mogę zasugerować transformmetodę (zamiast agregatu)? Jeśli używasz go w swoim oryginalnym przykładzie, powinno robić to, co chcesz (nadawanie).

Wes McKinney
źródło
Zrozumiałem, że transformacja tworzy obiekt, który wygląda jak ten, który został przekazany. Więc jeśli przekształcisz DataFrame, nie odzyskasz po prostu kolumny, odzyskasz DataFrame. Natomiast w moim przypadku chcę dołączyć nowy wynik do oryginalnej ramki danych. A może mówisz, że powinienem napisać oddzielną funkcję, która pobiera ramkę danych, oblicza nową kolumnę i dołącza nową kolumnę, a następnie przekształca ją za pomocą tej funkcji?
ely,
2
Zgadzam się, transformacja jest lepszym wyborem, df ['A-month-sum'] = df.groupby ('month') ['A']. Transform (sum)
Wouter Overmeire
Ale dlaczego miałoby być lepiej? Robi to samo, nie? Czy to jest szybsze?
K.-Michael Aye
1
IMHO, transformwygląda czyściej. Nie mam danych EMS, aby to potwierdzić, ale to może zadziałać (chociaż funkcja lambda może wymagać modyfikacji):bdata['mkt_return'] = bdata.groupby("yearmonth").transform(lambda x: (x["PriceReturn"]*x["MarketCap"]/x["MarketCap"].sum()).sum())
cd98
1
popraw mnie, jeśli się mylę, transformnie pozwala na operowanie na wielu kolumnach po groupby, np. df.groupby('col_3')[['col_1','col_2']].transform(lambda x: ((1-x.col_1.mean()) - x.col_2.std()))wyrzuci błąd narzekając, że 'brak atrybutu XXX'
Jason Goal
0

Nie znalazłem sposobu, aby przypisać do oryginalnej ramki danych. Więc po prostu przechowuję wyniki z grup i łączę je. Następnie sortujemy połączoną ramkę danych według indeksu, aby uzyskać oryginalną kolejność jako wejściową ramkę danych. Oto przykładowy kod:

In [10]: df = pd.DataFrame({'month': np.random.randint(0,11, 100), 'A': np.random.randn(100), 'B': np.random.randn(100)})

In [11]: df.head()
Out[11]:
   month         A         B
0      4 -0.029106 -0.904648
1      2 -2.724073  0.492751
2      7  0.732403  0.689530
3      2  0.487685 -1.017337
4      1  1.160858 -0.025232

In [12]: res = []

In [13]: for month, group in df.groupby('month'):
    ...:     new_df = pd.DataFrame({
    ...:         'A^2+B': group.A ** 2 + group.B,
    ...:         'A+B^2': group.A + group.B**2
    ...:     })
    ...:     res.append(new_df)
    ...:

In [14]: res = pd.concat(res).sort_index()

In [15]: res.head()
Out[15]:
      A^2+B     A+B^2
0 -0.903801  0.789282
1  7.913327 -2.481270
2  1.225944  1.207855
3 -0.779501  1.522660
4  1.322360  1.161495

Ta metoda jest dość szybka i rozszerzalna. Tutaj możesz uzyskać dowolną funkcję.

Han Zhang
źródło