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.
DataFrame
una Series
de cada columna en un DataFrame
? Series
de cada columna en un DataFrame
? Series
de cada columna en un DataFrame
? DataFrame
una Series
de cada columna en un DataFrame
? 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
.
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.
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.
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.
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
¿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
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)