NumPy: primer contacto
Python · Lección 6
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.
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
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/:
- Crea
scratch/distancias.pycondistancias_pares(X), que reciba una matriz(n, d)denpuntos enddimensiones 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). - Escribe el test que la compara contra
scipy.spatial.distance.cdisty falla si difieren en más de1e-9. - Mide cuánto tarda tu versión contra una con dos
foranidados paran = 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:
- ¿Qué es el
stridesde un array y cómo permite quereshapey la transpuesta no copien datos? - ¿Qué diferencia hay entre fancy indexing (con una lista de índices) y una rebanada, en cuanto a copias?
- ¿Qué hace
np.wherey por qué reemplaza unif/elsedentro 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
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.
- Python for Data Analysis 3E (McKinney — libro abierto), §4 NumPy Basics: Arrays and Vectorized Computation
Índices verificados el 11-09-2026 contra el índice publicado de cada libro.