Python Pandas ¿Cómo asignar los resultados de la operación groupby a las columnas en el marco de datos principal?

81

Tengo el siguiente marco de datos en IPython, donde cada fila es una sola acción:

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)

Quiero aplicar una operación groupby que calcule el rendimiento promedio ponderado por capitalización en todo, por cada fecha en la columna "yearmonth".

Esto funciona como se esperaba:

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

Pero luego quiero "transmitir" estos valores a los índices en el marco de datos original y guardarlos como columnas constantes donde coinciden las fechas.

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

Me doy cuenta de que esta ingenua tarea no debería funcionar. Pero, ¿cuál es el idioma "correcto" de Pandas para asignar el resultado de una operación groupby en una nueva columna en el marco de datos principal?

Al final, quiero una columna llamada "MarketReturn" que será un valor constante repetido para todos los índices que tienen una fecha coincidente con la salida de la operación groupby.

Un truco para lograr esto sería el siguiente:

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]

Pero esto es lento, malo y poco pítico.

ely
fuente
Está asignando de nuevo a su objeto agrupado en lugar de su marco original.
Wouter Overmeire
2
Lo sé y lo dije directamente debajo del error, donde dije: "Me doy cuenta de que esta asignación ingenua no debería funcionar. Pero, ¿cuál es el idioma" correcto "de Pandas para asignar el resultado de una operación groupby en una nueva columna en el padre ¿marco de datos?" Hacer la tarea con mi marco de datos original en el LHS tampoco funciona, y es incluso menos intuitivo que agregar la columna a nivel de objeto GroupBy.
Ely

Respuestas:

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
fuente
Esto todavía requiere que guarde el cálculo de grupo por, en lugar de tener la asignación directamente en el LHS en la línea donde realizo la operación de grupo por. Aplicar podría ser un poco mejor que el bucle de mi truco al final de la pregunta, pero son básicamente la misma idea.
ely
Join puede hacer esto, pero deberá cambiar el nombre de la columna agregada. En este caso, A_r es new_col.
Wouter Overmeire
El ejemplo de combinación en la parte inferior funciona, pero no se presenta con claridad. Si desea eliminar la primera parte de la respuesta y dejar la última un poco más clara, votaré a favor además de aceptar.
Ely
12
Eliminé el primer enfoque. Para ser honesto, siento que el código habla por sí mismo, siéntase libre de editar si desea agregar alguna explicación o referencias a los documentos. Realmente no estoy en el sistema de votación so, solo estoy aquí para apoyar un poco a los pandas.
Wouter Overmeire
1
Pasé mucho tiempo buscando esta respuesta, un poco como una publicación necro, ¡pero gracias! +1
Dan Carter
50

Si bien todavía estoy explorando todas las formas increíblemente inteligentes de applyconcatenar las piezas que se le dan, aquí hay otra forma de agregar una nueva columna en el padre después de una operación de grupo.

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
fuente
También puede hacer esto sin definir la función usando lambda y asignar:df.groupby('yearmonth').apply(lambda grp: grp.assign(mkt_return=grp['return'].sum()))
krassowski
30

Como regla general al usar groupby (), si usa la función .transform (), los pandas devolverán una tabla con la misma longitud que la original. Cuando usa otras funciones como .sum () o .first (), los pandas devolverán una tabla en la que cada fila es un grupo.

No estoy seguro de cómo funciona esto con apply, pero implementar funciones lambda elaboradas con transform puede ser bastante complicado, por lo que la estrategia que encuentro más útil es crear las variables que necesito, colocarlas en el conjunto de datos original y luego realizar mis operaciones allí.

Si primero entiendo lo que está tratando de hacer correctamente, puede calcular la capitalización de mercado total para cada grupo:

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

Esto agregará una columna llamada "group_MarketCap" a sus datos originales que contendría la suma de los límites de mercado para cada grupo. Luego, puede calcular los valores ponderados directamente:

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

Y finalmente calcularía el promedio ponderado para cada grupo usando la misma función de transformación:

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

Tiendo a construir mis variables de esta manera. A veces puede lograr ponerlo todo en un solo comando, pero eso no siempre funciona con groupby () porque la mayoría de las veces los pandas necesitan instanciar el nuevo objeto para operar en él a la escala completa del conjunto de datos (es decir, no puede agregue dos columnas juntas si una aún no existe).

Espero que esto ayude :)

Seeiespi
fuente
23

¿Puedo sugerir el transformmétodo (en lugar de agregado)? Si lo usa en su ejemplo original, debería hacer lo que quiera (la transmisión).

Wes McKinney
fuente
Mi entendimiento fue que la transformación produce un objeto que se parece al que se le pasó. Entonces, si transforma un DataFrame, no solo recupera una columna, obtiene un DataFrame. Mientras que en mi caso, quiero agregar un nuevo resultado al marco de datos original. ¿O está diciendo que debería escribir una función separada que tome un marco de datos, calcule la nueva columna y agregue la nueva columna, y luego se transforme con esa función?
Ely
2
Estoy de acuerdo, transform es una mejor opción, df ['A-month-sum'] = df.groupby ('month') ['A']. Transform (sum)
Wouter Overmeire
Pero, ¿por qué sería mejor? Hace lo mismo, ¿no? Es mas rapido?
K.-Michael Aye
1
En mi humilde opinión, transformparece más limpio. No tengo datos de EMS para confirmar esto, pero esto podría funcionar (aunque la función lambda podría tener que modificarse):bdata['mkt_return'] = bdata.groupby("yearmonth").transform(lambda x: (x["PriceReturn"]*x["MarketCap"]/x["MarketCap"].sum()).sum())
cd98
1
corrígeme si me equivoco, transformno se deja uno para operar en múltiples columnas después groupby, por ejemplo, df.groupby('col_3')[['col_1','col_2']].transform(lambda x: ((1-x.col_1.mean()) - x.col_2.std()))se generará un error quejándose de que 'ningún atributo XXX'
Jason Meta
0

No encontré una manera de hacer una asignación al marco de datos original. Así que solo almaceno los resultados de los grupos y los concateno. Luego ordenamos el marco de datos concatenados por índice para obtener el orden original como marco de datos de entrada. Aquí hay un código de muestra:

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

Este método es bastante rápido y extensible. Puede obtener cualquier característica aquí.

Han Zhang
fuente