NumPy: vectores, matrices y vectorización
Cambiar bucles por operaciones sobre arreglos completos, con control de formas y tipos
NumPy sustituye listas y bucles por un arreglo con forma, tipo y operaciones completas. Aprenderás a crear ndarrays, indexarlos, aplicar broadcasting y multiplicar matrices para preparar datos y evaluar modelos sin escribir un solo for.
- Crear ndarrays y leer shape, ndim y dtype
- Indexar, cortar y filtrar con máscaras booleanas
- Explicar el broadcasting y predecir la forma del resultado
- Sustituir bucles por operaciones vectorizadas
- Resolver un cálculo de álgebra lineal con @ y comprender su coste
Antes necesitas: Listas y tuplas · Recorridos y comprensiones
En esta lección
- ¿Qué es un ndarray?
- ¿Para qué sirve NumPy?
- ¿Cómo funciona por dentro? Búfer, forma y vistas
- Sintaxis: crear, consultar y operar
- Ejemplo básico: operar sobre un vector completo
- Ejemplo intermedio: filas, columnas y máscaras
- Caso práctico: estandarizar características y predecir
- Visualización: la matriz de características X
- Errores comunes
- Buenas prácticas
- Ejercicios
- Soluciones explicadas
- Resumen
- Evaluación
Una lista de Python guarda objetos dispersos: cada número es un objeto con su tipo, su contador de referencias y su dirección. Cómodo para casi todo, caro para calcular. NumPy cambia el modelo: un ndarray guarda los números uno detrás de otro en memoria, todos del mismo tipo, y opera sobre bloques enteros con código compilado.
Ese cambio parece menor y no lo es. Es la razón por la que una multiplicación de matrices de mil por mil tarda milisegundos en NumPy y segundos en Python puro, y la razón por la que Pandas, scikit-learn, PyTorch y casi todo el ecosistema de datos hablan en ndarray por debajo.
¿Qué es un ndarray?
Un ndarray es un arreglo de N dimensiones con un tipo homogéneo. Un vector es un ndarray de forma (n,); una matriz, de forma (filas, columnas); una imagen en color, de forma (alto, ancho, 3). La forma (shape) es una tupla con el tamaño de cada eje, y el tipo (dtype) describe cómo se guarda cada número: int64, float64, bool.
La forma es lo primero que debes mirar. Un error de formas produce resultados silenciosamente equivocados o un ValueError, y casi siempre se detecta imprimiendo X.shape antes de operar. En Machine Learning, X suele ser (n_muestras, n_características): cada fila es un ejemplo y cada columna una variable.
¿Para qué sirve NumPy?
- Preparar datos: matrices de características, normalización por columnas, máscaras para descartar valores atípicos.
- Álgebra lineal: producto matricial, transpuesta, inversa, autovalores; la base de la regresión y de las redes neuronales.
- Procesar señales e imágenes: recortar, redimensionar, aplicar umbrales a millones de píxeles a la vez.
- Simular y muestrear: generar distribuciones con un generador reproducible y operar sobre lotes completos.
- Servir de puente: Pandas, Matplotlib y PyTorch aceptan o devuelven ndarrays sin fricción.
¿Cómo funciona por dentro? Búfer, forma y vistas
Un ndarray tiene dos partes: un búfer contiguo de bytes y unos metadatos (shape, dtype, strides). Los strides indican cuántos bytes hay que saltar para avanzar una posición en cada eje. Con esa información, NumPy puede interpretar el mismo búfer como una matriz de 3 × 3 o como un vector de 9 elementos sin copiar nada.
De ahí nace la diferencia entre vista y copia. Un corte como X[:, 0] devuelve una vista: comparte el búfer y modificar la vista modifica X. X[:, 0].copy() crea memoria nueva. Entender esto evita el bug más silencioso de NumPy: cambiar una columna «temporal» y descubrir que el dataset original ya no es el mismo.
La vectorización significa que el bucle existe, pero está escrito en C dentro de NumPy. Tú describes la operación sobre el arreglo completo y NumPy recorre los elementos con menos comprobaciones de tipo y mejor uso de la caché. En muchas operaciones también libera el GIL, aunque el paralelismo entre núcleos depende de la biblioteca de álgebra lineal que haya debajo.
El broadcasting es la regla que permite operar formas distintas. Se alinean las dimensiones por la derecha y cada eje debe coincidir o valer 1. Por eso una matriz (3, 2) menos un vector (2,) funciona: el vector se estira sobre las tres filas. Una matriz (3, 2) menos un vector (3,) falla, porque 2 y 3 no coinciden y ninguno vale 1.
Sintaxis: crear, consultar y operar
import numpy as np
a = np.array([1, 2, 3]) # desde una lista
b = np.zeros((2, 3)) # 2 filas, 3 columnas de ceros
c = np.ones(3) # tres unos
d = np.arange(0, 10, 2) # 0, 2, 4, 6, 8
e = np.linspace(0, 1, 5) # 5 valores entre 0 y 1
f = np.eye(3) # identidad 3 x 3
print(a.shape, a.dtype, a.ndim)
print(a * 2 + 1)
print(a.reshape(3, 1).shape)a.shape es (3,), a.dtype es int64 en un equipo de 64 bits y a.ndim es 1. a * 2 + 1 produce [3 5 7] sin bucle: primero multiplica cada elemento y después suma uno a cada uno. reshape no cambia los datos, solo los metadatos: el mismo búfer pasa a leerse como tres filas de un elemento.
| Función | Qué crea |
|---|---|
np.array(lista) | Un arreglo con los datos y el tipo inferido |
np.zeros((f, c)) | Ceros con la forma indicada, útil para reservar |
np.arange(inicio, fin, paso) | Secuencia como range, pero como ndarray |
np.linspace(inicio, fin, n) | n valores equiespaciados, extremos incluidos |
np.eye(n) | Matriz identidad de tamaño n |
X.reshape(f, c) | Misma datos vistos con otra forma |
Ejemplo básico: operar sobre un vector completo
Cuatro ventas diarias. Con una lista escribirías una comprensión o un for; con un ndarray escribes la operación tal como la piensas.
import numpy as np
ventas = np.array([120.0, 80.0, 150.0, 90.0])
print(ventas.shape, ventas.dtype)
print(ventas.sum())
print(ventas * 1.18)
print(ventas.mean())La salida es (4,) float64, 440.0, [141.6 94.4 177. 106.2] y 110.0. El * 1.18 se aplica a los cuatro valores a la vez; no hay bucle visible porque está dentro de NumPy. sum y mean son métodos del arreglo y tampoco construyen listas intermedias.
Ejemplo intermedio: filas, columnas y máscaras
Con una matriz de tres muestras y tres características, X[1] selecciona una fila, X[:, 2] una columna y X[X[:, 1] > 25] las filas que cumplen una condición. La coma separa ejes: antes de la coma, filas; después, columnas. El : significa «todas».
import numpy as np
X = np.array([
[1.0, 20.0, 5.0],
[2.0, 30.0, 3.0],
[3.0, 40.0, 7.0],
])
print(X[1]) # fila 1
print(X[:, 2]) # columna 2
print(X[0, :2]) # fila 0, primeras dos columnas
print(X[X[:, 1] > 25]) # filas con la segunda columna > 25
print(X[1:, 1:].shape) # submatriz desde la fila 1 y columna 1Los resultados son [2. 30. 3.], [5. 3. 7.], [1. 20.], las dos últimas filas completas y (2, 2). La máscara X[:, 1] > 25 produce [False, True, True], un arreglo de booleanos que al indexar deja pasar solo las filas donde es True. No necesitas if ni acumuladores.
Caso práctico: estandarizar características y predecir
Estandarizar es restar la media de cada columna y dividir entre su desviación. Es el primer paso de muchos modelos, y con broadcasting se escribe en dos líneas aunque haya un millón de filas. Después, un producto matricial evalúa un modelo lineal.
import numpy as np
X = np.array([
[1.0, 20.0, 5.0],
[2.0, 30.0, 3.0],
[3.0, 40.0, 7.0],
])
media = X.mean(axis=0)
desviacion = X.std(axis=0)
Z = (X - media) / desviacion
pesos = np.array([1.5, -0.5, 2.0])
sesgo = 0.3
pred = Z @ pesos + sesgo
print(np.round(media, 2))
print(np.round(desviacion, 2))
print(Z.shape, pesos.shape)
print(np.round(pred, 2))axis=0 significa «a lo largo de las filas», es decir, una media por columna. La salida muestra medias [2. 30. 5.], desviaciones [0.82 8.16 1.63], formas (3, 3) y (3,), y predicciones [-0.92 -2.15 3.97]. El operador @ es el producto matricial: (3, 3) @ (3,) devuelve (3,), una predicción por muestra.
Ojo con std: NumPy usa por defecto la desviación poblacional (ddof=0). Si tu curso de estadística usa la muestral, pasa ddof=1 y los números cambiarán. Ninguna de las dos está mal; son definiciones distintas, y mezclarlas entre entrenamiento y producción desplaza las predicciones.
Visualización: la matriz de características X
Recorre los pasos. La misma matriz se lee de tres maneras: completa, por columnas o por filas. Fíjate en la forma del resultado cuando seleccionas una columna y cuando seleccionas una fila.
X tiene shape (3, 3): tres muestras (filas) y tres características (columnas).Recorre los pasos para ver cómo cambian las celdas.
Esta traza muestra por qué (3, 2) - (2,) funciona. La media tiene dos valores, uno por columna; el broadcasting la aplica a las tres filas sin copiarla.
import numpy as npX = np.array([[1.0, 20.0], [2.0, 30.0], [3.0, 40.0]])media = X.mean(axis=0)print(media)centrada = X - mediaprint(centrada)print(centrada.shape)np.—
Avanza paso a paso: observa la línea resaltada, las variables y la salida.
Errores comunes
| Mensaje o síntoma | Por qué ocurre | Cómo se arregla |
|---|---|---|
ValueError: operands could not be broadcast together with shapes (3,) (2,) | Las formas no coinciden ni tienen un eje de tamaño 1 | Revisa shape; quizá falta reshape(-1, 1) o una transpuesta |
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0 | El número de columnas de la izquierda no coincide con las filas de la derecha | Ajusta con .T o revisa el orden de @ |
IndexError: index 3 is out of bounds for axis 0 with size 3 | Pediste la cuarta fila de una matriz de tres | Revisa el índice: NumPy empieza en 0 |
| Cambiar una columna altera toda la matriz | El corte devolvió una vista, no una copia | Usa .copy() si vas a modificar |
| Un decimal se guarda truncado | El dtype es entero y NumPy convierte sin avisar | Crea el arreglo con dtype=float |
| El resultado es correcto pero lentísimo | Hay un for de Python recorriendo un ndarray | Reescribe la operación de forma vectorizada |
X = np.array([[1, 2], [3, 4]])
col = X[:, 0]
col[0] = 99
print(X)
# [[99 2]
# [ 3 4]]X = np.array([[1, 2], [3, 4]])
col = X[:, 0].copy()
col[0] = 99
print(X)
# [[1 2]
# [3 4]]El indexado básico devuelve vistas: `col` apunta al mismo búfer que `X`, así que escribir en ella escribe en el original. `.copy()` reserva memoria nueva. La indexación avanzada con una lista o una máscara, en cambio, sí devuelve copia; por eso conviene saber qué operación tienes delante.
Buenas prácticas
- Imprime
shapeantes de operar y compárala con la que esperabas; el error se detecta antes de entrenar. - Vectoriza: si escribes un
forsobre un ndarray, probablemente haya una operación equivalente. - Usa
@para producto matricial y reserva*para elemento a elemento; confundirlos no siempre da error. - Trabaja con
float64por defecto y baja afloat32solo cuando la memoria sea el límite y hayas medido el efecto. - Reserva con
np.zerosy rellena por índices en lugar de concatenar en un bucle: crecer un arreglo es caro. - Fija la semilla con
np.random.default_rng(42)para que un resultado se pueda reproducir. - Documenta el significado de cada eje:
axis=0no es «filas» por casualidad, es la dirección en la que se reduce.
Ejercicios
Analizar notas sin bucles
10, 14, 18, 8, 16. Calcula la media, la nota máxima y cuántas aprueban con umbral 13 usando una máscara booleana. Después suma 2 puntos solo a las desaprobadas, sin usar for, y muestra el arreglo corregido. Comprueba que las formas se mantienen.Pista
(notas >= 13).sum() cuenta los True. np.where(condicion, valor_si_true, valor_si_false) aplica el ajuste a la vez.
Centrar y predecir con producto matricial
X de 3 × 2, réstale la media de cada columna con broadcasting y calcula y = Xc @ w + b con w = [0.5, -1.0] y b = 2.0. Imprime la matriz centrada, las predicciones y su forma. Verifica a mano la primera predicción.Pista
La media por columna es X.mean(axis=0). El producto (3, 2) @ (2,) devuelve (3,); el sesgo se suma con broadcasting.
import numpy as np X = np.array([[1, 2], [3, 4], [5, 6]]) print(X.) print(X[:, ]) print(X.(axis=0))
La forma se consulta con `shape`. La segunda columna es el índice 1. La media por columna usa `axis=0`.
Soluciones explicadas
En el primer ejercicio, la tentación es recorrer las notas y aplicar un if. La versión vectorizada expresa la misma regla sin estado intermedio: la máscara responde «¿aprueba?», el sum cuenta, y np.where decide. El detalle que suele fallar es el tipo: si el arreglo fuera entero, asignar 10.5 a una posición guardaría 10 sin avisar. Si necesitas decimales, crea el arreglo con dtype=float.
En el segundo, comprobar la primera fila a mano no es un trámite: es la forma de detectar si centraste por filas en lugar de por columnas. Si hubieras usado axis=1, cada fila tendría media 0 y el modelo estaría restando una media que no corresponde a ninguna característica. Las formas seguirían cuadrando y las predicciones serían basura: por eso se verifica un número concreto, no solo que el código corra.
La regla general: las formas se comprueban antes, los valores se comprueban después. X.shape, w.shape y el resultado esperado te dicen si la operación es posible; una fila calculada a mano te dice si es la operación correcta. NumPy no sabe de qué hablan tus columnas, así que esa parte del contrato la pones tú.
Resumen
- Un ndarray es un búfer homogéneo con forma, tipo y strides; no una lista de objetos.
shapees la primera cosa que se revisa;(n,)es un vector y(f, c)una matriz.- El indexado básico y el slicing devuelven vistas;
.copy()reserva memoria nueva. - Las máscaras booleanas filtran filas sin escribir un bucle.
- El broadcasting alinea dimensiones por la derecha y estira los ejes de tamaño 1.
*multiplica elemento a elemento;@hace producto matricial.- La vectorización mueve el bucle a C: menos código y mucho más rendimiento.
Evaluación
¿Cuál es el shape de np.array([[1, 2, 3], [4, 5, 6]])?
X tiene shape (4, 3) y media tiene shape (3,). ¿Qué hace X - media?
Modificas col = X[:, 0] con col[0] = 99. ¿Qué pasa con X?
¿Ya lo entendiste?
Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.