python - tablas - ¿Cómo opero un DataFrame con una serie para cada columna?
seleccionar columnas en pandas (2)
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
unaSeries
de cada columna en unDataFrame
? -
¿Cómo agrego una
Series
de cada columna en unDataFrame
? -
¿Cómo multiplico una
Series
de cada columna en unDataFrame
? -
¿Cómo
DataFrame
unaSeries
de cada columna en unDataFrame
?
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 intento agregarlos, obtengo todos
np.nan
df + s
a b c 0 1
0 NaN NaN NaN NaN NaN
1 NaN NaN NaN NaN NaN
Lo que pensé que debería obtener es
a b c
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 a b c d e
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 a b c d e
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 a b c d e
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 a b c d e
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 a b c d e
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 a b c d e
10 11 12 13 14
df0:
lower a b c d e
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 a b c d e
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 a b c d e
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
a b c d e 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 a b c d e
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 a b c d e
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 a b c d e
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 a b c d e
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 a b c d e
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 a b c d e
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 a b c d e
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 marco de datos:
>>>> df.apply(lambda col: col + s)
a b c
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)