Python-Pandas Wie ordne ich Spalten im übergeordneten Datenrahmen Groupby-Operationsergebnisse zu?

81

Ich habe den folgenden Datenrahmen in IPython, wobei jede Zeile ein einzelner Bestand ist:

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)

Ich möchte eine Groupby-Operation anwenden, die die kapitalgewichtete durchschnittliche Rendite für jedes Datum in der Spalte "Jahrmonat" berechnet.

Dies funktioniert wie erwartet:

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

Aber dann möchte ich diese Werte zurück zu den Indizes im ursprünglichen Datenrahmen "senden" und sie als konstante Spalten speichern, in denen die Daten übereinstimmen.

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

Mir ist klar, dass diese naive Aufgabe nicht funktionieren sollte. Aber was ist die "richtige" Pandas-Sprache, um das Ergebnis einer Groupby-Operation einer neuen Spalte im übergeordneten Datenrahmen zuzuweisen?

Am Ende möchte ich eine Spalte mit dem Namen "MarketReturn", die ein wiederholter konstanter Wert für alle Indizes ist, deren Datum mit der Ausgabe der groupby-Operation übereinstimmt.

Ein Hack, um dies zu erreichen, wäre der folgende:

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]

Aber das ist langsam, schlecht und unpythonisch.

ely
quelle
Sie weisen Ihrem gruppierten Objekt anstelle Ihres ursprünglichen Rahmens wieder zu.
Wouter Overmeire
2
Ich weiß das und sagte dies direkt unter dem Fehler, wo ich sagte: "Mir ist klar, dass diese naive Zuweisung nicht funktionieren sollte. Aber was ist die" richtige "Pandas-Redewendung, um das Ergebnis einer Groupby-Operation in eine neue Spalte auf dem übergeordneten Element zuzuweisen Datenrahmen? " Das Zuweisen mit meinem ursprünglichen Datenrahmen auf der LHS funktioniert ebenfalls nicht und ist noch weniger intuitiv als das Hinzufügen der Spalte auf GroupBy-Objektebene.
Ely

Antworten:

73
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
quelle
Dies erfordert weiterhin, dass ich die Groupby-Berechnung speichere, anstatt die Zuweisung direkt auf der linken Seite in der Zeile zu haben, in der ich die Groupby-Operation ausführe. Bewerben ist vielleicht etwas besser als die Schleife in meinem Hack am Ende der Frage, aber sie sind im Grunde die gleiche Idee.
ely
Join kann dies tun, aber Sie müssen die hinzugefügte Spalte umbenennen. In diesem Fall ist A_r new_col.
Wouter Overmeire
Das Join-Beispiel unten funktioniert zwar, ist jedoch nicht klar dargestellt. Wenn Sie den ersten Teil der Antwort löschen und den letzten Teil etwas klarer machen möchten, werde ich zusätzlich zur Annahme zustimmen.
Ely
12
Ich habe den ersten Ansatz entfernt. Um ehrlich zu sein, habe ich das Gefühl, dass der Code für sich selbst spricht. Sie können ihn jederzeit bearbeiten, wenn Sie Erklärungen oder Verweise auf die Dokumente hinzufügen möchten. Ich bin nicht wirklich in das Abstimmungssystem verliebt, nur hier, um Pandas ein bisschen zu unterstützen.
Wouter Overmeire
1
Ich habe lange nach dieser Antwort gesucht, ein bisschen wie ein Nekro-Beitrag, aber danke! +1
Dan Carter
50

Während ich noch alle unglaublich intelligenten Methoden untersuche, mit denen applydie angegebenen Teile verkettet werden, gibt es hier eine weitere Möglichkeit, nach einer Groupby-Operation eine neue Spalte im übergeordneten Element hinzuzufügen.

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
quelle
Sie können dies auch tun, ohne die Funktion mit Lambda zu definieren und df.groupby('yearmonth').apply(lambda grp: grp.assign(mkt_return=grp['return'].sum()))
Folgendes
30

Wenn Sie bei Verwendung von groupby () die Funktion .transform () verwenden, geben Pandas in der Regel eine Tabelle mit der gleichen Länge wie Ihr Original zurück. Wenn Sie andere Funktionen wie .sum () oder .first () verwenden, geben Pandas eine Tabelle zurück, in der jede Zeile eine Gruppe ist.

Ich bin mir nicht sicher, wie dies mit apply funktioniert, aber das Implementieren ausgefeilter Lambda-Funktionen mit Transformation kann ziemlich schwierig sein. Die Strategie, die ich am hilfreichsten finde, besteht darin, die benötigten Variablen zu erstellen, sie in den ursprünglichen Datensatz zu platzieren und dort meine Operationen auszuführen.

Wenn ich verstehe, was Sie zuerst richtig machen wollen, können Sie die Gesamtmarktkapitalisierung für jede Gruppe berechnen:

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

Dadurch wird Ihren Originaldaten eine Spalte mit dem Namen "group_MarketCap" hinzugefügt, die die Summe der Marktkapitalisierungen für jede Gruppe enthält. Dann können Sie die gewichteten Werte direkt berechnen:

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

Und schließlich würden Sie den gewichteten Durchschnitt für jede Gruppe mit derselben Transformationsfunktion berechnen:

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

Ich neige dazu, meine Variablen auf diese Weise zu erstellen. Manchmal können Sie alles in einem einzigen Befehl zusammenfassen, aber das funktioniert nicht immer mit groupby (), da Pandas das neue Objekt die meiste Zeit instanziieren müssen, um es im vollen Datensatzmaßstab zu bearbeiten (dh Sie können es nicht addiere zwei Spalten, falls noch keine existiert).

Hoffe das hilft :)

seeiespi
quelle
23

Darf ich die transformMethode vorschlagen (anstelle von Aggregat)? Wenn Sie es in Ihrem ursprünglichen Beispiel verwenden, sollte es tun, was Sie wollen (die Übertragung).

Wes McKinney
quelle
Mein Verständnis war, dass die Transformation ein Objekt erzeugt, das so aussieht, wie es übergeben wurde. Wenn Sie also einen DataFrame transformieren, erhalten Sie nicht nur eine Spalte zurück, sondern auch einen DataFrame. In meinem Fall möchte ich ein neues Ergebnis an den ursprünglichen Datenrahmen anhängen. Oder sagen Sie, ich sollte eine separate Funktion schreiben, die einen Datenrahmen verwendet, die neue Spalte berechnet, die neue Spalte anfügt und dann mit dieser Funktion transformiert?
Ely
2
Ich stimme zu, Transformation ist eine bessere Wahl, df ['A-Monats-Summe'] = df.groupby ('Monat') ['A']. Transformation (Summe)
Wouter Overmeire
Aber warum sollte es besser sein? Es macht das gleiche, nein? Ist es schneller
K.-Michael Aye
1
IMHO, transformsieht sauberer aus. Ich habe keine EMS-Daten, um dies zu bestätigen, aber dies könnte funktionieren (obwohl die Lambda-Funktion möglicherweise geändert werden muss):bdata['mkt_return'] = bdata.groupby("yearmonth").transform(lambda x: (x["PriceReturn"]*x["MarketCap"]/x["MarketCap"].sum()).sum())
cd98
1
Korrigieren Sie mich, wenn ich falsch transformliege. Erlaubt einem nicht, mehrere Spalten zu bearbeiten groupby, z. B. df.groupby('col_3')[['col_1','col_2']].transform(lambda x: ((1-x.col_1.mean()) - x.col_2.std()))wird ein Fehler ausgegeben, der sich beschwert, dass 'kein Attribut XXX'
Jason Goal
0

Ich habe keine Möglichkeit gefunden, eine Zuordnung zum ursprünglichen Datenrahmen vorzunehmen. Also speichere ich einfach die Ergebnisse aus den Gruppen und verkette sie. Dann sortieren wir den verketteten Datenrahmen nach Index, um die ursprüngliche Reihenfolge als Eingabedatenrahmen zu erhalten. Hier ist ein Beispielcode:

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

Diese Methode ist ziemlich schnell und erweiterbar. Sie können hier jede Funktion ableiten.

Han Zhang
quelle