¿Cómo opero un DataFrame con una serie para cada columna?

Objetivo y Motivación.

He visto este tipo de preguntas varias veces y he visto muchas otras preguntas que involucran algún elemento de esto. Más recientemente, tuve que dedicar un poco de tiempo a explicar este concepto en comentarios mientras buscaba las preguntas y respuestas canónicas adecuadas. No encontré uno y entonces pensé escribir uno.

Esta pregunta generalmente surge con respecto a una operación específica, pero se aplica igualmente a la mayoría de las operaciones aritméticas.

  • ¿Cómo DataFrame una Series de cada columna en un DataFrame ?
  • ¿Cómo agrego una Series de cada columna en un DataFrame ?
  • ¿Cómo multiplico una Series de cada columna en un DataFrame ?
  • ¿Cómo DataFrame una Series de cada columna en un DataFrame ?

La pregunta

Dada una Series s y DataFrame df . ¿Cómo opero cada columna de df con s ?

 df = pd.DataFrame( [[1, 2, 3], [4, 5, 6]], index=[0, 1], columns=['a', 'b', 'c'] ) s = pd.Series([3, 14], index=[0, 1]) 

Cuando bash agregarlos, obtengo todos np.nan

 df + s abc 0 1 0 NaN NaN NaN NaN NaN 1 NaN NaN NaN NaN NaN 

Lo que pensé que debería obtener es

  abc 0 4 5 6 1 18 19 20 

Por favor, lleve el preámbulo. Es importante abordar primero algunos conceptos de nivel superior. Como mi motivación es compartir conocimientos y enseñar, quise dejar esto lo más claro posible.


Es útil crear un modelo mental de qué son los objetos Series y DataFrame .

Anatomía de una Series

Una Series debe considerarse como un diccionario mejorado. Esto no siempre es una analogía perfecta, pero comenzaremos aquí. También, hay otras analogías que puedes hacer pero estoy apuntando a un diccionario para demostrar el propósito de esta publicación.

index

Estas son las claves a las que podemos hacer referencia para obtener los valores correspondientes. Cuando los elementos del índice son únicos, la comparación con un diccionario se vuelve muy cercana.

values

Estos son los valores correspondientes que están codificados por el índice.

Anatomía de un DataFrame

Un DataFrame debe considerarse como un diccionario de Series o Series de Series . En este caso, las claves son los nombres de columna y los valores son las columnas en sí mismas como objetos de Series . Cada Series acepta compartir el mismo index que es el índice del DataFrame .

columns

Estas son las claves a las que podemos referirnos para obtener las Series correspondientes.

index

Este es el índice que todos los valores de la Series aceptan compartir.

Nota: RE: columns y objetos de index

Son el mismo tipo de cosas. El index un DataFrame se puede usar como las columns otro DataFrame . De hecho, esto sucede cuando haces df.T para obtener una transposición.

values

Esta es una matriz bidimensional que contiene los datos en un DataFrame . La realidad es que los values NO son lo que se almacena dentro del objeto DataFrame . (Bueno, a veces lo es, pero no intentaré describir el administrador de bloques). El punto es que es mejor pensar esto como acceso a una matriz bidimensional de los datos.


Definir datos de muestra

Estos son ejemplos de objetos de pandas.Index que se pueden usar como el index de una Series o DataFrame o se pueden usar como las columns de un DataFrame de DataFrame

 idx_lower = pd.Index([*'abcde'], name='lower') idx_range = pd.RangeIndex(5, name='range') 

Estos son objetos pandas.Series muestra que usan los objetos pandas.Index anteriores

 s0 = pd.Series(range(10, 15), idx_lower) s1 = pd.Series(range(30, 40, 2), idx_lower) s2 = pd.Series(range(50, 10, -8), idx_range) 

Estos son objetos de muestra pandas.DataFrame que usan los objetos pandas.Index anteriores

 df0 = pd.DataFrame(100, index=idx_range, columns=idx_lower) df1 = pd.DataFrame( np.arange(np.product(df0.shape)).reshape(df0.shape), index=idx_range, columns=idx_lower ) 

Series en Series

Cuando se opera en dos Series , la alineación es obvia. Alineas el index de una Series con el index de la otra.

 s1 + s0 lower a 40 b 43 c 46 d 49 e 52 dtype: int64 

Que es lo mismo que cuando aleatoriamente uno antes de operar. Los índices seguirán alineados.

 s1 + s0.sample(frac=1) lower a 40 b 43 c 46 d 49 e 52 dtype: int64 

Y NO es el caso cuando, en cambio, opero con los valores de la Series barajada. En este caso, Pandas no tiene el index para alinearse y, por lo tanto, opera desde una posición.

 s1 + s0.sample(frac=1).values lower a 42 b 42 c 47 d 50 e 49 dtype: int64 

Añadir un escalar

 s1 + 1 lower a 31 b 33 c 35 d 37 e 39 dtype: int64 

DataFrame en DataFrame

Lo mismo es cierto cuando se opera entre dos DataFrame s
La alineación es obvia y hace lo que pensamos que debería hacer.

 df0 + df1 lower abcde range 0 100 101 102 103 104 1 105 106 107 108 109 2 110 111 112 113 114 3 115 116 117 118 119 4 120 121 122 123 124 

Baraja el segundo DataFrame en ambos ejes. El index y las columns aún se alinearán y nos darán lo mismo.

 df0 + df1.sample(frac=1).sample(frac=1, axis=1) lower abcde range 0 100 101 102 103 104 1 105 106 107 108 109 2 110 111 112 113 114 3 115 116 117 118 119 4 120 121 122 123 124 

Es lo mismo, pero agrega la matriz y no el DataFrame . Ya no está alineado y obtendrá diferentes resultados.

 df0 + df1.sample(frac=1).sample(frac=1, axis=1).values lower abcde range 0 123 124 121 122 120 1 118 119 116 117 115 2 108 109 106 107 105 3 103 104 101 102 100 4 113 114 111 112 110 

Añadir una matriz dimensional. Se alineará con las columnas y se transmitirá a través de filas.

 df0 + [*range(2, df0.shape[1] + 2)] lower abcde range 0 102 103 104 105 106 1 102 103 104 105 106 2 102 103 104 105 106 3 102 103 104 105 106 4 102 103 104 105 106 

Añadir un escalar. Nada para alinearse con las transmisiones a todo.

 df0 + 1 lower abcde range 0 101 101 101 101 101 1 101 101 101 101 101 2 101 101 101 101 101 3 101 101 101 101 101 4 101 101 101 101 101 

DataFrame de DataFrame en Series

Si los DataFrame s deben considerarse como los diccionarios de Series y Series se deben considerar como diccionarios de valores, entonces es natural que cuando se opera entre un DataFrame y Series se deben alinear con sus “claves”.

 s0: lower abcde 10 11 12 13 14 df0: lower abcde range 0 100 100 100 100 100 1 100 100 100 100 100 2 100 100 100 100 100 3 100 100 100 100 100 4 100 100 100 100 100 

Y cuando operamos, el 10 en s0['a'] se agrega a la columna completa de df0['a']

 df0 + s0 lower abcde range 0 110 111 112 113 114 1 110 111 112 113 114 2 110 111 112 113 114 3 110 111 112 113 114 4 110 111 112 113 114 

Corazón del tema y punto del post.

¿Qué pasa si quiero s2 y df0 ?

 s2: df0: | lower abcde range | range 0 50 | 0 100 100 100 100 100 1 42 | 1 100 100 100 100 100 2 34 | 2 100 100 100 100 100 3 26 | 3 100 100 100 100 100 4 18 | 4 100 100 100 100 100 

Cuando opero, obtengo todos los np.nan tal como se citan en la pregunta

 df0 + s2 abcde 0 1 2 3 4 range 0 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 1 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 2 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 3 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 4 NaN NaN NaN NaN NaN NaN NaN NaN NaN NaN 

Esto no produce lo que queríamos. Porque Pandas está alineando el index de s2 con las columns de df0 . Las columns del resultado incluyen una unión del index de s2 y las columns de df0 .

Podríamos fingir con una transposición difícil

 (df0.T + s2).T lower abcde range 0 150 150 150 150 150 1 142 142 142 142 142 2 134 134 134 134 134 3 126 126 126 126 126 4 118 118 118 118 118 

Pero resulta que Pandas tiene una mejor solución. Existen métodos de operación que nos permiten pasar un argumento de axis para especificar el eje con el que se alineará.

- sub
+ add
* mul
/ div
** pow

Y así, la respuesta es simple.

 df0.add(s2, axis='index') lower abcde range 0 150 150 150 150 150 1 142 142 142 142 142 2 134 134 134 134 134 3 126 126 126 126 126 4 118 118 118 118 118 

Resulta que axis='index' es sinónimo de axis=0 .
Como es axis='columns' sinónimo de axis=1

 df0.add(s2, axis=0) lower abcde range 0 150 150 150 150 150 1 142 142 142 142 142 2 134 134 134 134 134 3 126 126 126 126 126 4 118 118 118 118 118 

Resto de las operaciones.

 df0.sub(s2, axis=0) lower abcde range 0 50 50 50 50 50 1 58 58 58 58 58 2 66 66 66 66 66 3 74 74 74 74 74 4 82 82 82 82 82 

 df0.mul(s2, axis=0) lower abcde range 0 5000 5000 5000 5000 5000 1 4200 4200 4200 4200 4200 2 3400 3400 3400 3400 3400 3 2600 2600 2600 2600 2600 4 1800 1800 1800 1800 1800 

 df0.div(s2, axis=0) lower abcde range 0 2.000000 2.000000 2.000000 2.000000 2.000000 1 2.380952 2.380952 2.380952 2.380952 2.380952 2 2.941176 2.941176 2.941176 2.941176 2.941176 3 3.846154 3.846154 3.846154 3.846154 3.846154 4 5.555556 5.555556 5.555556 5.555556 5.555556 

 df0.pow(1 / s2, axis=0) lower abcde range 0 1.096478 1.096478 1.096478 1.096478 1.096478 1 1.115884 1.115884 1.115884 1.115884 1.115884 2 1.145048 1.145048 1.145048 1.145048 1.145048 3 1.193777 1.193777 1.193777 1.193777 1.193777 4 1.291550 1.291550 1.291550 1.291550 1.291550 

Prefiero el método mencionado por @piSquared (es decir, df.add (s, axis = 0)), pero otros métodos se apply junto con lambda para realizar una acción en cada columna en el dataframe:

 >>>> df.apply(lambda col: col + s) abc 0 4 5 6 1 18 19 20 

Para aplicar la función lambda a las filas, use axis=1 :

 >>> df.T.apply(lambda row: row + s, axis=1) 0 1 a 4 18 b 5 19 c 6 20 

Este método podría ser útil cuando la transformación es más compleja, por ejemplo:

 df.apply(lambda col: 0.5 * col ** 2 + 2 * s - 3)