NumPy: arrays vectorizados
¿Por qué NumPy?
Python por sí solo es lento para cálculos numéricos. Una operación tan simple como sumar dos vectores de un millón de elementos con una lista pura puede tardar segundos. La misma operación con NumPy tarda milisegundos.
La razón: las listas de Python almacenan objetos genéricos (pueden mezclar números, strings, lo que sea). Cada operación implica buscar el tipo del objeto en runtime y aplicar el método correspondiente. NumPy almacena arrays de tipo homogéneo en memoria contigua y aplica operaciones en código C optimizado.
Eso lo convierte en la base de prácticamente todo el ecosistema de datos en Python: pandas, scikit-learn, matplotlib, PyTorch, todos están construidos sobre NumPy en alguna capa.
import numpy as npnp es el alias convencional. Lo verás en todo código Python de análisis.
ndarray: el tipo base
El objeto principal de NumPy es el ndarray (n-dimensional array). Lo más parecido a un vector de R, pero con más capacidades.
# Crear desde una lista
a = np.array([1, 2, 3, 4, 5])
print(a)
print(a.dtype)
print(a.shape)Salida:
[1 2 3 4 5]
int64
(5,)
Tres propiedades clave:
dtype: tipo de los elementos. Todos los elementos del array son del mismo tipo. Si mezclas, NumPy promociona al más general (int+float→float).shape: forma del array.(5,)es un vector de 5 elementos.(3, 4)es una matriz 3×4.(2, 3, 4)es un tensor 3D.ndim: número de dimensiones. 1 para vector, 2 para matriz, etc.
Creación rápida
np.zeros(5) # array([0., 0., 0., 0., 0.])
np.ones((2, 3)) # matriz 2x3 de unos
np.arange(0, 10, 2) # array([0, 2, 4, 6, 8]), como seq() en R
np.linspace(0, 1, 5) # array([0., 0.25, 0.5, 0.75, 1.]) — 5 puntos equiespaciados
np.random.normal(0, 1, 100) # 100 valores de Normal(0, 1)Estas funciones son el patrón estándar para crear arrays sin escribir los valores a mano.
Vectorización vs loops: el punto crítico
Python tiene tradición de loops for. Para listas, está bien. Para arrays NumPy, es un error de performance.
import time
# Forma "Python pura" — loop explícito
a = list(range(1_000_000))
inicio = time.time()
b = [x ** 2 for x in a]
print(f"Loop: {time.time() - inicio:.3f}s")
# Forma NumPy — vectorización
a_np = np.arange(1_000_000)
inicio = time.time()
b_np = a_np ** 2
print(f"NumPy: {time.time() - inicio:.3f}s")Resultado típico: el loop tarda ~0.05s, NumPy ~0.001s. 50× más rápido, sin esfuerzo.
La regla mental: si vas a hacer la misma operación a muchos elementos, hay una forma vectorizada. Cuando empieces a escribir un for para operar sobre un array, párate y busca la versión vectorizada.
Operadores aritméticos vectorizados:
a = np.array([1, 2, 3, 4])
b = np.array([10, 20, 30, 40])
a + b # array([11, 22, 33, 44])
a * b # array([10, 40, 90, 160]) — element-wise, NO producto matricial
a ** 2 # array([1, 4, 9, 16])
np.sqrt(a)
np.log(a)
np.exp(a)Funciones matemáticas (sqrt, log, exp, sin, cos…) están todas vectorizadas. Si llamas np.log(array_de_1M), NumPy lo procesa entero en C, no en Python.
Broadcasting: la magia
Broadcasting es la regla que permite operar con arrays de formas distintas sin escribir loops:
a = np.array([[1, 2, 3],
[4, 5, 6]]) # shape (2, 3)
b = np.array([10, 20, 30]) # shape (3,)
a + b
# array([[11, 22, 33],
# [14, 25, 36]])NumPy “estiró” b virtualmente para que coincida con cada fila de a, y luego sumó. No se crea una copia del vector en memoria, broadcasting es eficiente.
Las reglas formales:
- Si las dimensiones difieren en número, se prependen
1s a la forma del más pequeño. - Si en alguna dimensión los tamaños son iguales o uno de ellos es 1, son compatibles.
- Si no son compatibles, error.
Ejemplos prácticos:
# Normalizar cada columna restando su media
datos = np.random.normal(0, 1, (100, 5)) # 100 filas, 5 columnas
medias = datos.mean(axis=0) # vector de 5 medias
datos_centrados = datos - medias # broadcasting fila a filaEsto sustituye a un loop sobre las columnas. Es el patrón fundacional de NumPy.
Slicing e indexing
Acceder a partes de un array:
a = np.array([10, 20, 30, 40, 50])
a[0] # 10 (primer elemento, índice 0 — no 1 como en R)
a[-1] # 50 (último)
a[1:4] # array([20, 30, 40]) — slicing exclusivo del final
a[:3] # primeros 3
a[2:] # desde el tercero hasta el finalTrampa clásica para usuarios de R: Python indexa desde 0, R desde 1. El slicing [1:4] en Python da elementos en posiciones 1, 2, 3 (NO 4). En R, [1:4] daría 4 elementos.
Para arrays 2D:
m = np.array([[1, 2, 3],
[4, 5, 6],
[7, 8, 9]])
m[0, 1] # 2 (fila 0, columna 1)
m[:, 0] # array([1, 4, 7]) — toda la columna 0
m[1, :] # array([4, 5, 6]) — toda la fila 1
m[0:2, 1:3] # subbloque 2x2: significa “todos”. Las dos dimensiones se separan por coma.
Boolean indexing
Filtrar arrays con condiciones lógicas:
a = np.array([1, 5, 3, 8, 2, 9])
a > 4
# array([False, True, False, True, False, True])
a[a > 4]
# array([5, 8, 9])a > 4 produce un array booleano del mismo tamaño. Usándolo como índice, extraes solo donde es True.
Patrón muy frecuente:
# Eliminar valores menores que un umbral
datos[datos > 0]
# Combinar condiciones
datos[(datos > 0) & (datos < 10)] # ojo: & y |, no and/orImportante: con arrays NumPy se usan & y | (operadores bit a bit), NO and y or. Los segundos solo funcionan con escalares.
Operaciones agregadas
NumPy tiene un conjunto rico de agregaciones:
a = np.array([1, 2, 3, 4, 5])
a.sum() # 15
a.mean() # 3.0
a.std() # 1.414...
a.min() # 1
a.max() # 5
a.argmin() # 0 (índice del mínimo)
a.argmax() # 4 (índice del máximo)Para arrays 2D, el argumento axis controla sobre qué dimensión agregar:
m = np.array([[1, 2, 3],
[4, 5, 6]])
m.sum() # 21 (todo)
m.sum(axis=0) # array([5, 7, 9]) — suma columnas
m.sum(axis=1) # array([6, 15]) — suma filasRegla mental: axis=0 es la dimensión que colapsas. axis=0 colapsa filas → quedan las columnas. axis=1 colapsa columnas → quedan las filas. Cuesta acostumbrarse. Con la práctica es automático.
Trampas habituales
- Indexar desde 1 (vienes de R). Python es 0-based.
a[1]es el segundo elemento, no el primero. Los slices[1:4]excluyen el índice final (3 elementos, no 4). - Usar
and/orcon arrays booleanos.(a > 0) and (a < 10)falla con error críptico. Tienes que usar(a > 0) & (a < 10). Y los paréntesis son obligatorios por precedencia de operadores. - Loops cuando hay vectorización disponible. Si tu código tiene un
forsobre un array NumPy de tamaño moderado, casi siempre hay versión vectorizada que es 10-100× más rápida. - Confundir copia y vista.
b = a[1:4]es una vista del array original, no una copia. Modificarbmodificaa. Si quieres una copia:b = a[1:4].copy(). Es una fuente común de bugs sutiles cuando vienes de R, donde el slicing siempre copia.
En la siguiente entrega
NumPy te da arrays homogéneos. Pandas te da DataFrame, la estructura que la mayoría usa día a día. Cada columna puede ser de un tipo distinto, hay un índice explícito, y la API está pensada para análisis tabular. Lo siguiente.