119 votes

Python Pandas Comment affecter les résultats de l'opération groupby aux colonnes du cadre de données parent ?

J'ai le cadre de données suivant dans IPython, où chaque ligne est une action unique :

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)

Je veux appliquer une opération groupby qui calcule le rendement moyen pondéré par les capitalisations pour tout, pour chaque date dans la colonne "année-mois".

Cela fonctionne comme prévu :

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

Mais ensuite, je veux en quelque sorte "diffuser" ces valeurs vers les indices du cadre de données d'origine, et les enregistrer en tant que colonnes constantes lorsque les dates correspondent.

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

Je réalise que cette mission naïve ne devrait pas fonctionner. Mais quel est le "bon" idiome Pandas pour affecter le résultat d'une opération groupby dans une nouvelle colonne du cadre de données parent ?

Au final, je veux une colonne appelée "MarketReturn" qui sera une valeur constante répétée pour tous les indices qui ont une date correspondante avec la sortie de l'opération groupby.

Un moyen d'y parvenir serait le suivant :

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]

Mais c'est lent, mauvais, et non pythique.

92voto

Wouter Overmeire Points 6676
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

68voto

Garrett Points 5477

Alors que je suis encore en train d'explorer toutes les façons incroyablement intelligentes que apply concatène les morceaux qui lui sont donnés, voici une autre façon d'ajouter une nouvelle colonne dans le parent après une opération groupby.

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

61voto

seeiespi Points 48

En règle générale, lorsque vous utilisez la fonction groupby(), si vous utilisez la fonction .transform(), pandas renverra un tableau de la même longueur que l'original. Si vous utilisez d'autres fonctions comme .sum() ou .first(), pandas retournera un tableau où chaque ligne est un groupe.

Je ne sais pas comment cela fonctionne avec apply, mais la mise en œuvre de fonctions lambda élaborées avec transform peut être assez délicate. La stratégie que je trouve la plus utile est de créer les variables dont j'ai besoin, de les placer dans l'ensemble de données original et d'y effectuer mes opérations.

Si je comprends bien ce que vous essayez de faire, vous pouvez d'abord calculer la capitalisation totale du marché pour chaque groupe :

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

Cela ajoutera une colonne appelée "group_MarketCap" à vos données d'origine, qui contiendra la somme des capitalisations boursières pour chaque groupe. Vous pourrez alors calculer directement les valeurs pondérées :

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

Et enfin, vous calculerez la moyenne pondérée pour chaque groupe en utilisant la même fonction de transformation :

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

J'ai tendance à construire mes variables de cette façon. Parfois, il est possible de tout regrouper en une seule commande, mais cela ne fonctionne pas toujours avec groupby(), car la plupart du temps, pandas doit instancier le nouvel objet pour l'utiliser à l'échelle du jeu de données complet (par exemple, vous ne pouvez pas ajouter deux colonnes ensemble si l'une d'elles n'existe pas encore).

J'espère que cela vous aidera :)

31voto

Wes McKinney Points 17545

Puis-je suggérer le transform (au lieu de l'agrégat) ? Si vous l'utilisez dans votre exemple original, il devrait faire ce que vous voulez (la diffusion).

1voto

Han Zhang Points 41

Je n'ai pas trouvé de moyen d'affecter le cadre de données d'origine. Je me contente donc de stocker les résultats des groupes et de les concaténer. Ensuite, nous trions le dataframe concaténé par index pour obtenir l'ordre original comme dataframe d'entrée. Voici un exemple de code :

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

Cette méthode est assez rapide et extensible. Vous pouvez dériver n'importe quelle fonctionnalité ici.

Note : Si le cadre de données est trop grand, concat peut vous causer une erreur MMO.

Prograide.com

Prograide est une communauté de développeurs qui cherche à élargir la connaissance de la programmation au-delà de l'anglais.
Pour cela nous avons les plus grands doutes résolus en français et vous pouvez aussi poser vos propres questions ou résoudre celles des autres.

Powered by:

X