Indice / NumPy / 3.2
Vectorización
La idea entera de NumPy en una línea:
celsius * 9 / 5 + 32 convierte los seis valores, o los 240, sin que
escribas un bucle. Y no es solo velocidad - es que la expresión dice qué se
calcula en vez de cómo se recorre.
Para qué sirve
Es el hábito que hay que adquirir antes de pandas, porque pandas está construido
sobre él: df["importe"] * 1.21 es exactamente esto. Un alumno que sigue
escribiendo for i in range(len(df)) va a pelear con la biblioteca en cada
ejercicio.
ELI5Explicado facil
Un bucle es hacer las fotocopias de a una: poner la hoja, apretar el botón, sacarla, repetir.
Una operación vectorizada es poner las cien hojas en el alimentador y apretar una vez. El mismo trabajo, una sola instrucción, y sobre todo: la instrucción dice «copiar todo esto», no «poner, apretar, sacar».
Cómo se ve
- Punto de partidaSeis lecturas del sensor S01, en grados Celsius. Hay que pasarlas a Fahrenheit: f = c * 9/5 + 32.
celsius = lecturas[0, :6] - Vuelta 1 del buclePython toma el elemento 0, hace 20.8 * 9/5 + 32 y guarda 69.4. Despues pasa al siguiente.
for c in celsius: fahrenheit.append(c * 9 / 5 + 32) - Vuelta 2 del buclePython toma el elemento 1, hace 19.8 * 9/5 + 32 y guarda 67.6. Despues pasa al siguiente.
for c in celsius: fahrenheit.append(c * 9 / 5 + 32) - Vuelta 3 del buclePython toma el elemento 2, hace 19.2 * 9/5 + 32 y guarda 66.6. Despues pasa al siguiente.
for c in celsius: fahrenheit.append(c * 9 / 5 + 32) - Vuelta 4 del buclePython toma el elemento 3, hace 19.0 * 9/5 + 32 y guarda 66.2. Despues pasa al siguiente.
for c in celsius: fahrenheit.append(c * 9 / 5 + 32) - Vuelta 5 del buclePython toma el elemento 4, hace 19.6 * 9/5 + 32 y guarda 67.3. Despues pasa al siguiente.
for c in celsius: fahrenheit.append(c * 9 / 5 + 32) - Vuelta 6 del buclePython toma el elemento 5, hace 20.3 * 9/5 + 32 y guarda 68.5. Despues pasa al siguiente.
for c in celsius: fahrenheit.append(c * 9 / 5 + 32) - Lo mismo, vectorizadoUna sola expresion, y NumPy la aplica a los seis valores a la vez. No hay bucle en Python: el recorrido ocurre adentro de NumPy, compilado.
fahrenheit = celsius * 9 / 5 + 32 - Y con 240 lecturas es igualLa misma linea sirve para la grilla entera de 10 sensores por 24 horas. El bucle de Python pagaria 240 vueltas; la expresion vectorizada, una.
fahrenheit = lecturas * 9 / 5 + 32
Elemento a elemento
import numpy as np
lecturas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
skip_header=1, usecols=range(2, 26))
print("lecturas:", lecturas.shape, lecturas.dtype)
celsius = lecturas[0, :6]
print()
print("las 6 primeras horas del sensor S01, en Celsius:")
print(" ", celsius)
print()
print("A Fahrenheit, sin escribir un solo bucle:")
print(" celsius * 9 / 5 + 32 ->", np.round(celsius * 9 / 5 + 32, 1))
print()
print("Todos los operadores aritmeticos funcionan igual, elemento a elemento:")
print(" + 10 ->", np.round(celsius + 10, 1))
print(" - 20 ->", np.round(celsius - 20, 1))
print(" * 2 ->", np.round(celsius * 2, 1))
print(" / 2 ->", np.round(celsius / 2, 1))
print(" ** 2 ->", np.round(celsius ** 2, 1))
print(" // 5 ->", celsius // 5)
print(" % 5 ->", np.round(celsius % 5, 1))
print()
print("Y entre DOS arrays del mismo tamano, tambien elemento a elemento:")
manana = lecturas[0, 6:12]
tarde = lecturas[0, 12:18]
print(" manana (h06-h11):", manana)
print(" tarde (h12-h17):", tarde)
print(" tarde - manana :", np.round(tarde - manana, 1))
print(" Cada resta usa la lectura de la misma posicion. No hay que")
print(" emparejar nada a mano.")
print()
print("Las funciones de NumPy (las ufunc) hacen lo mismo:")
print(" np.sqrt ->", np.round(np.sqrt(celsius), 2))
print(" np.abs ->", np.abs(np.array([-3.0, 2.0, -1.5])))
print(" np.round ->", np.round(celsius))
print(" np.log ->", np.round(np.log(celsius), 3))
print(" np.minimum(celsius, 20) ->", np.minimum(celsius, 20))
print()
print("Las comparaciones tambien: devuelven un array de booleanos.")
print(" celsius > 20 ->", celsius > 20)
print(" Ese array booleano es la mascara de la leccion 3.4, y es")
print(" exactamente lo mismo que hace df[df.nota > 7] en pandas.")
print()
print("La regla: si escribiste un for para recorrer un array, casi siempre")
print("hay una expresion que lo reemplaza. Y no es solo velocidad: la")
print("expresion dice QUE se calcula, el bucle dice COMO se recorre.")
lecturas: (10, 24) float64
las 6 primeras horas del sensor S01, en Celsius:
[20.8 19.8 19.2 19. 19.6 20.3]
A Fahrenheit, sin escribir un solo bucle:
celsius * 9 / 5 + 32 -> [69.4 67.6 66.6 66.2 67.3 68.5]
Todos los operadores aritmeticos funcionan igual, elemento a elemento:
+ 10 -> [30.8 29.8 29.2 29. 29.6 30.3]
- 20 -> [ 0.8 -0.2 -0.8 -1. -0.4 0.3]
* 2 -> [41.6 39.6 38.4 38. 39.2 40.6]
/ 2 -> [10.4 9.9 9.6 9.5 9.8 10.2]
** 2 -> [432.6 392. 368.6 361. 384.2 412.1]
// 5 -> [4. 3. 3. 3. 3. 4.]
% 5 -> [0.8 4.8 4.2 4. 4.6 0.3]
Y entre DOS arrays del mismo tamano, tambien elemento a elemento:
manana (h06-h11): [20. 21. 21.1 21.7 21.9 22.9]
tarde (h12-h17): [22.6 23.7 23.9 24.3 24.4 23.3]
tarde - manana : [2.6 2.7 2.8 2.6 2.5 0.4]
Cada resta usa la lectura de la misma posicion. No hay que
emparejar nada a mano.
Las funciones de NumPy (las ufunc) hacen lo mismo:
np.sqrt -> [4.56 4.45 4.38 4.36 4.43 4.51]
np.abs -> [3. 2. 1.5]
np.round -> [21. 20. 19. 19. 20. 20.]
np.log -> [3.035 2.986 2.955 2.944 2.976 3.011]
np.minimum(celsius, 20) -> [20. 19.8 19.2 19. 19.6 20. ]
Las comparaciones tambien: devuelven un array de booleanos.
celsius > 20 -> [ True False False False False True]
Ese array booleano es la mascara de la leccion 3.4, y es
exactamente lo mismo que hace df[df.nota > 7] en pandas.
La regla: si escribiste un for para recorrer un array, casi siempre
hay una expresion que lo reemplaza. Y no es solo velocidad: la
expresion dice QUE se calcula, el bucle dice COMO se recorre.
| Forma | Ejemplo | Devuelve |
|---|---|---|
| Array y escalar | celsius * 9 / 5 + 32 | Un array del mismo tamaño |
| Array y array | tarde - manana | Empareja por posición, sin que vos emparejes nada |
| Funciones (ufunc) | np.sqrt, np.abs, np.log, np.minimum | Un array del mismo tamaño |
| Comparaciones | celsius > 20 | Un array de bool - la máscara de la lección 3.4 |
La regla: si escribiste un for para recorrer un array,
casi siempre hay una expresión que lo reemplaza. Y el motivo no es el cronómetro: la
expresión dice qué se calcula y el bucle dice cómo se recorre, que es
un detalle que a nadie le importa.
reshape
import numpy as np
lecturas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
skip_header=1, usecols=range(2, 26))
print("original :", lecturas.shape, "=", lecturas.size, "lecturas")
plano = lecturas.reshape(-1)
print("aplanado :", plano.shape, " <- reshape(-1) siempre da 1 dimension")
print(" primeros 6:", plano[:6])
print(" son las 6 primeras horas de S01: reshape recorre FILA POR FILA.")
print()
por_horas = lecturas.reshape(24, 10)
print("reshape(24, 10):", por_horas.shape)
print(" OJO: esto NO es 'las 24 horas por los 10 sensores'.")
print(" Solo reagrupo los mismos 240 numeros de a 10, en el mismo orden.")
print(" primera fila:", np.round(por_horas[0], 1))
print(" ...que son las 10 primeras horas de S01, no la hora 0 de todos.")
print()
print("Para eso esta la transpuesta, que SI intercambia los ejes:")
transpuesta = lecturas.T
print(" lecturas.T:", transpuesta.shape)
print(" primera fila:", np.round(transpuesta[0], 1))
print(" ahora si: la hora h00 de los 10 sensores.")
print(" Se comprueba contra la columna 0 del original:",
np.array_equal(transpuesta[0], lecturas[:, 0], equal_nan=True))
print()
print("El -1 significa 'calcula vos esta dimension':")
print(" reshape(-1) ->", lecturas.reshape(-1).shape)
print(" reshape(5, -1) ->", lecturas.reshape(5, -1).shape)
print(" reshape(-1, 4) ->", lecturas.reshape(-1, 4).shape)
print(" Solo puede haber UN -1, y el total tiene que dar justo:")
try:
lecturas.reshape(7, -1)
except ValueError as error:
print(" reshape(7, -1) ->", type(error).__name__ + ":", error)
print()
print("Y el detalle que sorprende: reshape devuelve una VISTA, no una copia.")
copia_o_no = lecturas.reshape(24, 10)
copia_o_no[0, 0] = -999
print(" toque copia_o_no[0, 0] = -999")
print(" lecturas[0, 0] ahora vale:", lecturas[0, 0])
print(" Comparten la memoria. Si hace falta independencia: .copy()")
lecturas[0, 0] = 20.8 # dejar el array como estaba
print()
print("Un caso real donde reshape resuelve el problema: pasar la grilla a")
print("formato largo, una fila por (sensor, hora).")
sensores = np.repeat(np.arange(1, 11), 24)
horas = np.tile(np.arange(24), 10)
largo = np.column_stack([sensores, horas, lecturas.reshape(-1)])
print(" shape:", largo.shape)
print(" primeras 3 filas:")
print(largo[:3])
print(" ultimas 2 filas:")
print(largo[-2:])
print(" Es el melt() de la leccion 6.5, en tres lineas de NumPy.")
original : (10, 24) = 240 lecturas
aplanado : (240,) <- reshape(-1) siempre da 1 dimension
primeros 6: [20.8 19.8 19.2 19. 19.6 20.3]
son las 6 primeras horas de S01: reshape recorre FILA POR FILA.
reshape(24, 10): (24, 10)
OJO: esto NO es 'las 24 horas por los 10 sensores'.
Solo reagrupo los mismos 240 numeros de a 10, en el mismo orden.
primera fila: [20.8 19.8 19.2 19. 19.6 20.3 20. 21. 21.1 21.7]
...que son las 10 primeras horas de S01, no la hora 0 de todos.
Para eso esta la transpuesta, que SI intercambia los ejes:
lecturas.T: (24, 10)
primera fila: [20.8 18.5 17.8 21.2 19.9 19.9 23.3 19.1 21.4 16.1]
ahora si: la hora h00 de los 10 sensores.
Se comprueba contra la columna 0 del original: True
El -1 significa 'calcula vos esta dimension':
reshape(-1) -> (240,)
reshape(5, -1) -> (5, 48)
reshape(-1, 4) -> (60, 4)
Solo puede haber UN -1, y el total tiene que dar justo:
reshape(7, -1) -> ValueError: cannot reshape array of size 240 into shape (7,newaxis)
Y el detalle que sorprende: reshape devuelve una VISTA, no una copia.
toque copia_o_no[0, 0] = -999
lecturas[0, 0] ahora vale: -999.0
Comparten la memoria. Si hace falta independencia: .copy()
Un caso real donde reshape resuelve el problema: pasar la grilla a
formato largo, una fila por (sensor, hora).
shape: (240, 3)
primeras 3 filas:
[[ 1. 0. 20.8]
[ 1. 1. 19.8]
[ 1. 2. 19.2]]
ultimas 2 filas:
[[10. 22. 17. ]
[10. 23. 16.7]]
Es el melt() de la leccion 6.5, en tres lineas de NumPy.
reshape(24, 10) no es «las 24 horas por los 10 sensores»
reshape reagrupa los mismos números en el mismo orden,
recorriendo fila por fila. La primera fila del resultado son las 10 primeras horas de
S01, no la hora h00 de los 10 sensores.
Para intercambiar los ejes de verdad está .T, la transpuesta. Se
comprueba en una línea: lecturas.T[0] tiene que ser igual a
lecturas[:, 0].
reshape
-1 significa «calculá vos esta dimensión», y solo puede haber
uno. reshape(5, -1) sobre 240 elementos da (5, 48);
reshape(7, -1) es ValueError, porque 240 no es múltiplo de 7.
Devuelve una vista, no una copia. Tocar el resultado modifica el original: si
hace falta independencia, .copy(). Es el mismo tema que la lección 3.3, y
la causa del SettingWithCopy de pandas.
axis=: el eje que desaparece
import numpy as np
lecturas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
skip_header=1, usecols=range(2, 26))
print("lecturas:", lecturas.shape, " (10 sensores, 24 horas)")
print()
print("Sin axis, la reduccion aplasta TODO a un solo numero:")
print(" np.nanmean(lecturas) ->", round(float(np.nanmean(lecturas)), 2))
print(" shape del resultado :", np.nanmean(lecturas).shape, "(un escalar)")
print()
print("Con axis, el eje que nombras es el que DESAPARECE:")
por_sensor = np.nanmean(lecturas, axis=1)
por_hora = np.nanmean(lecturas, axis=0)
print(" axis=1 -> desaparecen las 24 horas ->", por_sensor.shape,
"un promedio POR SENSOR")
print(" axis=0 -> desaparecen los 10 sensores ->", por_hora.shape,
"un promedio POR HORA")
print()
print("Esa es la regla, y es la unica que hay que recordar:")
print(" (10, 24) con axis=0 -> (24,)")
print(" (10, 24) con axis=1 -> (10,)")
print(" El numero del axis es la POSICION en shape que se elimina.")
print()
print("promedio por sensor (axis=1):")
for i, valor in enumerate(por_sensor, start=1):
print(f" S{i:02d} {valor:6.2f}")
print()
print("promedio por hora (axis=0), las primeras 8:")
print(" ", np.round(por_hora[:8], 2))
print()
print("La forma de saber cual pediste: el resultado tiene que tener tantos")
print("elementos como la dimension que QUEDA.")
print(" 10 sensores -> 10 promedios (axis=1)")
print(" 24 horas -> 24 promedios (axis=0)")
print()
print("Y hay una senal barata: el 999.9 imposible esta en S05.")
print(" S05 con axis=1 ->", round(float(por_sensor[4]), 2),
" <- salta a la vista")
print(" los otros nueve estan entre",
round(float(np.delete(por_sensor, 4).min()), 2), "y",
round(float(np.delete(por_sensor, 4).max()), 2))
print(" Un promedio por eje es tambien una herramienta de diagnostico.")
print()
print("Las reducciones que mas se usan, todas aceptan axis:")
for nombre, funcion in (("sum", np.nansum), ("mean", np.nanmean),
("min", np.nanmin), ("max", np.nanmax),
("std", np.nanstd)):
resultado = funcion(lecturas, axis=1)
print(f" np.nan{nombre:<5}(a, axis=1) -> shape {resultado.shape}, "
f"primer valor {resultado[0]:.2f}")
lecturas: (10, 24) (10 sensores, 24 horas)
Sin axis, la reduccion aplasta TODO a un solo numero:
np.nanmean(lecturas) -> 24.91
shape del resultado : () (un escalar)
Con axis, el eje que nombras es el que DESAPARECE:
axis=1 -> desaparecen las 24 horas -> (10,) un promedio POR SENSOR
axis=0 -> desaparecen los 10 sensores -> (24,) un promedio POR HORA
Esa es la regla, y es la unica que hay que recordar:
(10, 24) con axis=0 -> (24,)
(10, 24) con axis=1 -> (10,)
El numero del axis es la POSICION en shape que se elimina.
promedio por sensor (axis=1):
S01 21.73
S02 19.62
S03 18.75
S04 21.63
S05 60.65
S06 21.08
S07 24.55
S08 20.04
S09 22.78
S10 17.53
promedio por hora (axis=0), las primeras 8:
[19.8 18.83 18.29 17.93 18.27 18.89 19.17 20.12]
La forma de saber cual pediste: el resultado tiene que tener tantos
elementos como la dimension que QUEDA.
10 sensores -> 10 promedios (axis=1)
24 horas -> 24 promedios (axis=0)
Y hay una senal barata: el 999.9 imposible esta en S05.
S05 con axis=1 -> 60.65 <- salta a la vista
los otros nueve estan entre 17.53 y 24.55
Un promedio por eje es tambien una herramienta de diagnostico.
Las reducciones que mas se usan, todas aceptan axis:
np.nansum (a, axis=1) -> shape (10,), primer valor 521.60
np.nanmean (a, axis=1) -> shape (10,), primer valor 21.73
np.nanmin (a, axis=1) -> shape (10,), primer valor 19.00
np.nanmax (a, axis=1) -> shape (10,), primer valor 24.40
np.nanstd (a, axis=1) -> shape (10,), primer valor 1.57
Es la fuente número uno de confusión de NumPy y se resuelve con una sola regla:
| Expresión | Shape del resultado | Qué calculó |
|---|---|---|
np.nanmean(a) | () | Un solo número: el promedio de las 240 |
np.nanmean(a, axis=0) | (24,) | Un promedio por hora: desaparecieron los sensores |
np.nanmean(a, axis=1) | (10,) | Un promedio por sensor: desaparecieron las horas |
El número del axis es la posición de shape que se
elimina. Y la forma de verificar que pediste lo que querías: el resultado
tiene tantos elementos como la dimensión que queda.
Los promedios por sensor dan entre 17,53 y 24,55 - salvo S05, que da
60,65. Ahí está el 999.9 imposible, repartido entre sus 24 horas.
Antes de escribir un validador, una reducción por eje ya señala dónde mirar.
Cuándo el bucle sigue haciendo falta
import numpy as np
lecturas = np.genfromtxt("numpy_sensores.csv", delimiter=",",
skip_header=1, usecols=range(2, 26))
# TAREA: para cada sensor, cuantas horas estuvo por encima de 21 grados,
# ignorando las lecturas ausentes y la imposible.
VALIDO_MAX = 50.0
UMBRAL = 21.0
# --- Version con bucles ------------------------------------------------
con_bucle = []
for fila in range(lecturas.shape[0]):
cuantas = 0
for columna in range(lecturas.shape[1]):
valor = lecturas[fila, columna]
if np.isnan(valor):
continue
if valor > VALIDO_MAX:
continue
if valor > UMBRAL:
cuantas += 1
con_bucle.append(cuantas)
con_bucle = np.array(con_bucle)
# --- Version vectorizada ------------------------------------------------
usable = ~np.isnan(lecturas) & (lecturas <= VALIDO_MAX)
sin_bucle = ((lecturas > UMBRAL) & usable).sum(axis=1)
print("con bucle :", con_bucle)
print("sin bucle :", sin_bucle)
print("¿iguales? :", np.array_equal(con_bucle, sin_bucle))
print()
print("12 lineas contra 2, y el mismo resultado. Pero lo que importa no es")
print("la cantidad de lineas: es que la version de abajo se LEE.")
print(" usable = no es NaN Y no supera el maximo posible")
print(" sin_bucle = de las usables, cuantas superan el umbral, por fila")
print()
print("El detalle que hace que ande: sumar booleanos.")
mascara = (lecturas > UMBRAL) & usable
print(" dtype de la mascara:", mascara.dtype)
print(" mascara.sum(axis=1) cuenta los True, porque True vale 1.")
print(" int(True) ->", int(True), "| int(False) ->", int(False))
print()
print("Y NO siempre se puede vectorizar. El caso tipico es cuando cada")
print("elemento depende del ANTERIOR:")
serie = np.array([3.0, 1.0, 4.0, 1.0, 5.0, 9.0, 2.0])
maximo_hasta_aca = []
tope = -np.inf
for valor in serie:
tope = max(tope, valor)
maximo_hasta_aca.append(tope)
print(" serie :", serie)
print(" maximo acumulado :", np.array(maximo_hasta_aca))
print(" ...pero NumPy tambien tiene eso, y se llama acumulador:")
print(" np.maximum.accumulate ->", np.maximum.accumulate(serie))
print(" np.cumsum ->", np.cumsum(serie))
print(" Antes de escribir el bucle, conviene buscar si ya existe.")
con bucle : [16 8 1 14 1 12 24 7 17 0]
sin bucle : [16 8 1 14 1 12 24 7 17 0]
¿iguales? : True
12 lineas contra 2, y el mismo resultado. Pero lo que importa no es
la cantidad de lineas: es que la version de abajo se LEE.
usable = no es NaN Y no supera el maximo posible
sin_bucle = de las usables, cuantas superan el umbral, por fila
El detalle que hace que ande: sumar booleanos.
dtype de la mascara: bool
mascara.sum(axis=1) cuenta los True, porque True vale 1.
int(True) -> 1 | int(False) -> 0
Y NO siempre se puede vectorizar. El caso tipico es cuando cada
elemento depende del ANTERIOR:
serie : [3. 1. 4. 1. 5. 9. 2.]
maximo acumulado : [3. 3. 4. 4. 5. 9. 9.]
...pero NumPy tambien tiene eso, y se llama acumulador:
np.maximum.accumulate -> [3. 3. 4. 4. 5. 9. 9.]
np.cumsum -> [ 3. 4. 8. 9. 14. 23. 25.]
Antes de escribir el bucle, conviene buscar si ya existe.
Las dos versiones dan [16, 8, 1, 14, 1, 12, 24, 7, 17, 0]: doce líneas
contra dos. Y el truco que hace andar a la corta es que sumar booleanos cuenta
los True, porque int(True) es 1.
Vectorizar no siempre se puede: cuando cada elemento depende del anterior, hay una dependencia real. Pero antes de escribir ese bucle conviene buscar, porque NumPy ya trae los casos frecuentes:
| Lo que querías hacer con un bucle | Ya existe |
|---|---|
| Suma acumulada | np.cumsum(a) |
| Producto acumulado | np.cumprod(a) |
| Máximo hasta acá | np.maximum.accumulate(a) |
| Diferencia con el anterior | np.diff(a) |
| Correr los valores un lugar | np.roll(a, 1) |
| Contar cuántos cumplen | (a > x).sum() |
Dónde se rompe
Causa: el total no es divisible por la dimensión que fijaste.
Arreglo: a.size tiene que ser múltiplo. Si no lo es, el
problema es de los datos, no del reshape.
Causa: dos arrays de formas incompatibles en una operación elemento a elemento.
Arreglo: es el tema de la lección 3.5. La solución casi siempre es
reshape(-1, 1) en el más chico.
nan
Causa: hay al menos un NaN en el eje que se está
reduciendo.
Arreglo: np.nanmean en vez de np.mean. Es la
lección 3.6.
Causa: reshape devuelve una vista: los dos
arrays comparten la memoria.
Arreglo: .copy() si necesitás independencia. Y
a.base is None dice si un array es dueño de sus datos.
Causa: confundiste axis=0 con axis=1.
Arreglo: mirá el shape del resultado antes de seguir. Si
esperabas 10 promedios y tenés 24, invertí el eje.
Recetario
| Expresión | Qué hace | Cuándo |
|---|---|---|
a * 2 + 1 | Elemento a elemento | Siempre, en vez de un bucle |
a - b | Empareja por posición | Dos arrays del mismo shape |
np.sqrt(a), np.abs(a), np.log(a) | Funciones vectorizadas | En vez de math.sqrt en un bucle |
np.minimum(a, 20) | El menor entre cada valor y 20 | Recortar por arriba |
a.reshape(f, c) | Otra forma, misma memoria | Reorganizar |
a.reshape(-1) | Aplanar a una dimensión | Antes de un column_stack |
a.T | Intercambia los ejes | Cuando reshape no es lo que querés |
a.sum(axis=0) | Elimina las filas | Un total por columna |
a.mean(axis=1) | Elimina las columnas | Un promedio por fila |
(a > x).sum(axis=1) | Cuenta por fila | Sumar booleanos es contar |
np.cumsum(a), np.diff(a) | Acumular, diferenciar | Antes de escribir un bucle secuencial |
np.column_stack([a, b, c]) | Pega columnas | Armar una tabla desde vectores |
np.repeat / np.tile | Repite valores / repite el patrón | Construir las claves de una tabla larga |