NumPy: primer contacto

Python · Lección 6

L3 From scratch Fase F0 Libro McKinney cap. 4 Prereqs Entorno

La idea

Una lista de Python guarda punteros a objetos dispersos en memoria. Un array de NumPy guarda los números mismos, contiguos, todos del mismo tipo. De esa sola diferencia sale todo: la velocidad, el broadcasting, y la razón por la que toda la ciencia de datos en Python se apoya en NumPy.

Esta es la última lección de la Fase 0 y el puente hacia la matemática: a partir de la semana 3, cada cosa que derives la vas a implementar aquí.

Vectorización: por qué es rápido

De dónde sale la diferencia

En la lista, cada elemento es un objeto int de Python completo: un encabezado con el tipo, un contador de referencias y el valor. Están esparcidos por la memoria, y la lista guarda punteros a ellos.

Multiplicar recorre la lista elemento por elemento. En cada paso el intérprete tiene que: seguir el puntero, mirar el tipo, decidir qué __mul__ llamar, crear un objeto nuevo. Ese despacho por tipo ocurre un millón de veces.

El array guarda un millón de int64 pegados, en un solo bloque contiguo, con el tipo declarado una vez en el array, no en cada elemento.

array * 2 baja a un bucle en C que ya sabe el tipo: sin despacho, sin objetos intermedios, y con los datos contiguos la CPU los trae en bloque a la caché y usa instrucciones SIMD que procesan varios números por ciclo.

La lección práctica: si escribes un for sobre un array de NumPy, casi siempre hay una forma vectorizada, y es de 10 a 100 veces más rápida.

El array y sus atributos

shape es lo que vas a mirar mil veces cuando algo no cuadre. El 90 % de los errores en NumPy y en cualquier framework de ML son errores de forma.

TipUsa default_rng, no np.random.seed

np.random.default_rng(semilla) crea un generador independiente. La API vieja (np.random.seed + np.random.normal) usa estado global: dos partes de tu código pueden pisarse el estado sin que te enteres, y tus resultados dejan de ser reproducibles.

Indexado: vistas y máscaras

WarningUna rebanada es una VISTA, no una copia

Esto conecta directo con la Lección 1, pero con un giro: en listas, lista[:] copia. En NumPy, array[:] no copia — te da una ventana al mismo bloque de memoria.

Indexado booleano — la herramienta que más vas a usar:

Broadcasting

Cuando operas dos arrays de formas distintas, NumPy intenta estirar el más chico. La regla: compara las formas de derecha a izquierda; dos dimensiones son compatibles si son iguales o si una de ellas es 1.

axis=0 recorre hacia abajo (colapsa filas, deja una por columna). axis=1 recorre a lo ancho. keepdims=True conserva la dimensión como 1 en vez de eliminarla, que es lo que permite el broadcasting por filas. Memoriza esos tres: son la fuente eterna de confusión.

Ejercicio 1 — Vectorizar un bucle

Reescribe el cálculo sin ningún for, usando operaciones de NumPy.

La división entre dos arrays del mismo shape es elemento a elemento. No necesitas zip ni bucle: es una sola operación.

Ejercicio 2 — Máscara booleana

Calcula la venta total de las sucursales cuyo ticket promedio supera 32, sin usar bucles.

Primero construye la máscara: (ventas / trx) > 32. Después úsala para indexar ventas y suma el resultado con .sum().

Ejercicio 3 — Broadcasting con keepdims

Centra la matriz por filas: a cada elemento réstale el promedio de su propia fila.

m.mean(axis=1) da shape (2,), que no alinea para restar por filas. Necesitas keepdims=True para obtener shape (2, 1), que sí hace broadcasting a lo ancho.

Reto

En proyectos/notebooks/F0-retos.ipynb. Este es además el primer archivo de scratch/:

  1. Crea scratch/distancias.py con distancias_pares(X), que reciba una matriz (n, d) de n puntos en d dimensiones y devuelva la matriz (n, n) de distancias euclidianas entre todos los pares — sin ningún bucle, solo broadcasting. Pista: X[:, None, :] - X[None, :, :] tiene shape (n, n, d).
  2. Escribe el test que la compara contra scipy.spatial.distance.cdist y falla si difieren en más de 1e-9.
  3. Mide cuánto tarda tu versión contra una con dos for anidados para n = 500. Anota la razón en tu nota.

Esto es exactamente el patrón de todo el resto del plan: derivar, implementar sin librería, verificar contra la librería.

Para el Cerebro

Lee McKinney cap. 4 completo con estas preguntas escritas antes:

  1. ¿Qué es el strides de un array y cómo permite que reshape y la transpuesta no copien datos?
  2. ¿Qué diferencia hay entre fancy indexing (con una lista de índices) y una rebanada, en cuanto a copias?
  3. ¿Qué hace np.where y por qué reemplaza un if/else dentro de un bucle?
¿Por qué un array de NumPy es más rápido que una lista?::Datos contiguos de un solo tipo; el bucle baja a C sin despacho por tipo ni objetos intermedios
¿Una rebanada de array de NumPy es copia o vista?::Vista: comparte memoria con el original
¿Cómo se fuerza una copia en NumPy?::Con el método .copy()
¿Qué recorre axis=0 y qué recorre axis=1?::axis=0 colapsa filas (resultado por columna); axis=1 colapsa columnas (resultado por fila)
¿Para qué sirve keepdims=True?::Conserva la dimensión colapsada como 1, permitiendo broadcasting por filas
Regla de broadcasting::Comparar shapes de derecha a izquierda; compatibles si son iguales o si una es 1
¿Por qué default_rng y no np.random.seed?::default_rng crea un generador independiente; la API vieja usa estado global compartido
NoteFin de la Fase 0

Con esto cierra el refresco de Python. La semana 3 arranca Matemática · Álgebra lineal y Estadística, intercaladas. Pídelas el viernes.

Fuentes

Lo que esta página demuestra sola. Los ejercicios corren en tu navegador con NumPy real vía Pyodide.

Lo que viene de los libros.

Índices verificados el 11-09-2026 contra el índice publicado de cada libro.