AulaML Python · C++
Python intermedio · 48 min · Python para ciencia de datos

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.

Lección 1 de 5 del módulo
Al terminar esta lección podrás
  • 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
  1. ¿Qué es un ndarray?
  2. ¿Para qué sirve NumPy?
  3. ¿Cómo funciona por dentro? Búfer, forma y vistas
  4. Sintaxis: crear, consultar y operar
  5. Ejemplo básico: operar sobre un vector completo
  6. Ejemplo intermedio: filas, columnas y máscaras
  7. Caso práctico: estandarizar características y predecir
  8. Visualización: la matriz de características X
  9. Errores comunes
  10. Buenas prácticas
  11. Ejercicios
  12. Soluciones explicadas
  13. Resumen
  14. 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

PYTHON Creación y atributos básicos
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ónQué 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.

PYTHON ventas.py
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».

PYTHON indexado.py
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 1

Los 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.

PYTHON modelo.py
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.

Mapa de datos ·Matriz de características X
3 × 3 rango [1, 40] X tiene shape (3, 3): tres muestras (filas) y tres características (columnas).
x1
x2
x3
muestra 0
1
20
5
muestra 1
2
30
3
muestra 2
3
40
7

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.

Ejecución visual ·Centrar una matriz con un vector
1 / 7
1 import numpy as np
2
3 X = np.array([[1.0, 20.0], [2.0, 30.0], [3.0, 40.0]])
4 media = X.mean(axis=0)
5 print(media)
6 centrada = X - media
7 print(centrada)
8 print(centrada.shape)
Se importa NumPy con el alias habitual np.
Variables
np = <module 'numpy'>
Salida
—

Avanza paso a paso: observa la línea resaltada, las variables y la salida.

Errores comunes

Mensaje o síntomaPor qué ocurreCó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 1Revisa shape; quizá falta reshape(-1, 1) o una transpuesta
ValueError: matmul: Input operand 1 has a mismatch in its core dimension 0El número de columnas de la izquierda no coincide con las filas de la derechaAjusta con .T o revisa el orden de @
IndexError: index 3 is out of bounds for axis 0 with size 3Pediste la cuarta fila de una matriz de tresRevisa el índice: NumPy empieza en 0
Cambiar una columna altera toda la matrizEl corte devolvió una vista, no una copiaUsa .copy() si vas a modificar
Un decimal se guarda truncadoEl dtype es entero y NumPy convierte sin avisarCrea el arreglo con dtype=float
El resultado es correcto pero lentísimoHay un for de Python recorriendo un ndarrayReescribe la operación de forma vectorizada
El corte que modifica el original
Incorrecto
PYTHON Vista compartida
X = np.array([[1, 2], [3, 4]])
col = X[:, 0]
col[0] = 99
print(X)
# [[99  2]
#  [ 3  4]]
Correcto
PYTHON Copia independiente
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 shape antes de operar y compárala con la que esperabas; el error se detecta antes de entrenar.
  • Vectoriza: si escribes un for sobre 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 float64 por defecto y baja a float32 solo cuando la memoria sea el límite y hayas medido el efecto.
  • Reserva con np.zeros y 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=0 no es «filas» por casualidad, es la dirección en la que se reduce.

Ejercicios

Ejercicio 1

Analizar notas sin bucles

Crea un ndarray con las notas 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.

Ejercicio 2

Centrar y predecir con producto matricial

Parte de una matriz 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.

Completar código Completa el análisis de una matriz
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.
  • shape es 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

Evaluación 1

¿Cuál es el shape de np.array([[1, 2, 3], [4, 5, 6]])?

Evaluación 2

X tiene shape (4, 3) y media tiene shape (3,). ¿Qué hace X - media?

Evaluación 3

Modificas col = X[:, 0] con col[0] = 99. ¿Qué pasa con X?

Tus notas

¿Ya lo entendiste?

Márcala como completada para llevar tu progreso y desbloquear el siguiente paso de la ruta.