NumPy es, sin exagerar, la piedra angular del ecosistema de ciencia de datos en Python. Sin él no existirían Pandas, Scikit-learn ni TensorFlow tal como los conocemos hoy. La versión 2.4, lanzada en diciembre de 2025, trae mejoras de rendimiento notables: operaciones con escalares hasta 6 veces más rápidas y una gestión de cadenas de texto completamente renovada. En esta guía aprenderás a crear y manipular arrays, eliminar bucles lentos con vectorización, y aplicar broadcasting para operaciones entre arrays de distintas formas.
Honestamente, si llevas tiempo con Python y todavía no dominas NumPy a fondo, este es el mejor momento para hacerlo.
¿Por Qué NumPy es Indispensable para la Ciencia de Datos?
Python por defecto es lento para operaciones numéricas masivas. Un bucle que suma 10 millones de números puede tardar varios segundos. NumPy resuelve esto almacenando datos en bloques de memoria contiguos con un único tipo de dato y delegando la ejecución al código C compilado. El resultado son operaciones 10 a 100 veces más rápidas que los equivalentes en Python puro.
Además, NumPy establece el protocolo de array estándar que utilizan el resto de librerías del ecosistema. Cuando entrenas un modelo con Scikit-learn o creas un DataFrame en Pandas, ambas convierten internamente los datos a arrays NumPy para realizar las operaciones matemáticas. Es, en definitiva, el lenguaje común que hablan todas las herramientas de datos en Python.
Instalación y Configuración de NumPy 2.4
NumPy 2.4.1 es la versión estable actual (enero 2026). Instala o actualiza con pip o Conda — es rápido y sin complicaciones:
pip install "numpy>=2.4"
# O en un entorno Conda:
conda install numpy
# Verificar versión instalada
import numpy as np
print(np.__version__) # 2.4.x
NumPy 2.x requiere Python 3.10 o superior. Si usas Python 3.8 o 3.9, actualiza el intérprete antes de instalar NumPy 2.4 para evitar errores de compatibilidad. (Sí, sé que actualizar el intérprete da pereza, pero vale la pena.)
Arrays de NumPy: La Estructura Fundamental
El objeto central de NumPy es el ndarray (n-dimensional array). A diferencia de las listas de Python, todos los elementos de un ndarray comparten el mismo tipo de dato (dtype), lo que permite operaciones vectorizadas a velocidad de C. Es una diferencia pequeña en el papel, pero enorme en la práctica.
Crear Arrays desde Cero
import numpy as np
# Desde una lista Python
arr1d = np.array([1, 2, 3, 4, 5])
arr2d = np.array([[1, 2, 3], [4, 5, 6]])
# Arrays especiales
zeros = np.zeros((3, 4)) # Matriz 3x4 de ceros
ones = np.ones((2, 3), dtype=float) # Matriz 2x3 de unos
iden = np.eye(4) # Matriz identidad 4x4
# Rangos y secuencias
rango = np.arange(0, 10, 2) # [0, 2, 4, 6, 8]
espaciado = np.linspace(0, 1, 5) # [0.0, 0.25, 0.5, 0.75, 1.0]
# Arrays aleatorios con semilla para reproducibilidad
rng = np.random.default_rng(42)
aleatorio = rng.standard_normal((3, 3))
Atributos Esenciales del ndarray
arr = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
print(arr.shape) # (2, 3) — 2 filas, 3 columnas
print(arr.ndim) # 2 — número de dimensiones
print(arr.dtype) # float64 — tipo de dato
print(arr.size) # 6 — total de elementos
print(arr.nbytes) # 48 — bytes en memoria (6 x 8 bytes)
El dtype determina el uso de memoria y la precisión numérica. Para ciencia de datos, float64 es el estándar, pero usar float32 puede reducir la memoria a la mitad sin pérdida significativa en muchos modelos de machine learning. Merece la pena tenerlo en cuenta cuando trabajas con datasets grandes.
Indexado y Slicing de Arrays
NumPy ofrece un sistema de indexado más potente que las listas de Python, con soporte para indexado multidimensional, booleano y con arrays de índices. Una vez que le coges el truco, no querrás volver a las listas.
Indexado Básico y Slicing
arr = np.array([[10, 20, 30], [40, 50, 60], [70, 80, 90]])
# Elemento específico [fila, columna]
print(arr[1, 2]) # 60
# Fila completa
print(arr[0]) # [10, 20, 30]
# Columna completa
print(arr[:, 1]) # [20, 50, 80]
# Submatriz: filas 0-1, columnas 1-2
print(arr[0:2, 1:3]) # [[20, 30], [50, 60]]
# Reshape: mismos datos, nueva forma
plano = arr.reshape(1, 9) # [[10, 20, 30, 40, 50, 60, 70, 80, 90]]
Indexado Booleano
El indexado booleano es una herramienta esencial para filtrar y transformar datos. Es de esas cosas que parecen magia la primera vez que las ves:
datos = np.array([15, 32, 7, 48, 21, 9, 55, 3])
# Filtrar valores mayores a 20
grandes = datos[datos > 20]
print(grandes) # [32, 48, 21, 55]
# Reemplazar valores negativos con 0 (útil en preprocesamiento)
arr = np.array([-3.0, 1.5, -0.8, 4.2, -1.1])
arr[arr < 0] = 0.0
print(arr) # [0. 1.5 0. 4.2 0. ]
Vectorización: Elimina los Bucles Python
La vectorización es el principio más importante para escribir código NumPy eficiente. Consiste en reemplazar bucles Python explícitos con operaciones que actúan sobre el array completo a la vez, delegando la ejecución a rutinas C compiladas con instrucciones SIMD. Dicho de otro modo: menos código Python, más velocidad real.
Por Qué los Bucles Son Lentos
Python interpreta cada instrucción individualmente. En un bucle de un millón de iteraciones, el intérprete realiza un millón de llamadas de función. NumPy, en cambio, ejecuta la misma operación en una sola llamada a código C altamente optimizado.
import numpy as np
import time
datos = np.random.randn(1_000_000)
# Versión lenta con bucle Python
inicio = time.perf_counter()
resultado = np.empty(len(datos))
for i in range(len(datos)):
resultado[i] = datos[i] ** 2 if datos[i] > 0 else datos[i] ** 3
tiempo_bucle = time.perf_counter() - inicio
# Versión vectorizada con np.where
inicio = time.perf_counter()
resultado_v = np.where(datos > 0, datos ** 2, datos ** 3)
tiempo_vec = time.perf_counter() - inicio
print(f"Bucle Python: {tiempo_bucle:.3f}s")
print(f"Vectorizado: {tiempo_vec:.4f}s")
print(f"Aceleracion: {tiempo_bucle / tiempo_vec:.0f}x mas rapido")
# Aceleracion tipica: 50-100x
Las cifras son consistentes: entre 50x y 100x de aceleración. No es un caso especialmente favorable — es lo habitual.
Funciones Universales (ufuncs)
NumPy proporciona decenas de funciones universales que operan elemento a elemento sobre arrays sin necesidad de bucles:
arr = np.array([1.0, 4.0, 9.0, 16.0, 25.0])
# Operaciones matematicas elementales
print(np.sqrt(arr)) # [1. 2. 3. 4. 5.]
print(np.log(arr)) # logaritmo natural
print(np.exp(np.array([1, 2, 3]))) # [e, e^2, e^3]
# Seleccion condicional vectorizada
x = np.array([3.7, -1.2, 0.5, -4.8, 2.1])
print(np.clip(x, 0.0, 3.0)) # [3. 0. 0.5 0. 2.1]
print(np.sign(x)) # [ 1. -1. 1. -1. 1.]
# np.where con condicion mas compleja
etiquetas = np.where(x > 0, "positivo", "negativo")
print(etiquetas)
Broadcasting: Operaciones con Arrays de Diferente Forma
El broadcasting es el mecanismo que permite a NumPy realizar operaciones entre arrays de formas distintas sin copiar datos. El array más pequeño se "expande" virtualmente para que sea compatible con el más grande. Al principio resulta un poco confuso, pero una vez que lo interioriza se convierte en una herramienta de uso diario.
Las 3 Reglas del Broadcasting
NumPy aplica estas reglas al comparar dimensiones de derecha a izquierda:
- Si los arrays tienen distinto número de dimensiones, el de menor rango se rellena con
1s a la izquierda:(4,)se convierte en(1, 4). - Una dimensión de tamaño
1se "estira" para coincidir con la dimensión correspondiente del otro array. - Si en alguna dimensión los tamaños no coinciden y ninguno es
1, se lanza unValueError.
import numpy as np
# Broadcasting escalar: (4,) + escalar = (4,)
arr = np.array([1, 2, 3, 4])
print(arr + 10) # [11 12 13 14]
# Broadcasting 1D sobre 2D
# matriz (3,3) + vector (3,) -> vector tratado como (1,3) -> (3,3)
matriz = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])
vector = np.array([10, 20, 30])
print(matriz + vector)
# [[11 22 33]
# [14 25 36]
# [17 28 39]]
# Broadcasting columna: np.newaxis crea una dimension de tamanio 1
correcciones = np.array([1.0, -0.5, 2.0]) # forma (3,)
col = correcciones[:, np.newaxis] # forma (3, 1)
print(matriz + col)
# [[ 2. 3. 4. ]
# [ 3.5 4.5 5.5]
# [ 9. 10. 11. ]]
Caso Práctico: Normalización Z-Score
Normalizar datasets es un paso clave antes de entrenar modelos de ML. El broadcasting hace que este preprocesamiento sea elegante y eficiente — y sin una sola línea de bucle:
rng = np.random.default_rng(42)
X = rng.standard_normal((1000, 5)) # 1000 muestras, 5 caracteristicas
# Media y desviacion estandar por columna, shape (5,)
media = X.mean(axis=0)
std = X.std(axis=0)
# Broadcasting: (5,) se expande a (1000, 5) automaticamente
X_normalizado = (X - media) / std
print("Media post-norm:", X_normalizado.mean(axis=0).round(8))
# Valores muy proximos a cero
print("Std post-norm:", X_normalizado.std(axis=0).round(6))
# Valores muy proximos a uno
Caso Práctico: Matriz de Distancias Euclidianas
Con broadcasting se puede calcular todas las distancias entre N puntos en una sola expresión, sin ningún bucle:
puntos = np.array([[0, 0], [3, 4], [1, 1], [6, 8]]) # 4 puntos 2D
# puntos[:, np.newaxis] -> (4, 1, 2)
# puntos[np.newaxis, :] -> (1, 4, 2)
# diferencia resultante -> (4, 4, 2)
diferencias = puntos[:, np.newaxis] - puntos[np.newaxis, :]
distancias = np.sqrt((diferencias ** 2).sum(axis=2))
print(distancias.round(2))
# [[ 0. 5. 1.41 10. ]
# [ 5. 0. 3.61 5. ]
# [ 1.41 3.61 0. 8.6 ]
# [10. 5. 8.6 0. ]]
Funciones Estadísticas y de Álgebra Lineal
Estadísticas Descriptivas
datos = np.array([[4, 7, 2, 8], [1, 9, 3, 6], [5, 2, 8, 4]])
print(datos.mean()) # media de todos los elementos
print(datos.mean(axis=0)) # media por columna, shape (4,)
print(datos.mean(axis=1)) # media por fila, shape (3,)
print(datos.std(ddof=1)) # desviacion estandar muestral
print(np.percentile(datos, [25, 50, 75])) # cuartiles
print(np.median(datos)) # mediana
print(np.corrcoef(datos.T)) # matriz de correlacion entre columnas
Álgebra Lineal con numpy.linalg
A = np.array([[2.0, 1.0], [5.0, 3.0]])
b = np.array([4.0, 7.0])
# Multiplicacion de matrices (operador @)
C = A @ A.T
# Resolver sistema Ax = b
x = np.linalg.solve(A, b)
print("Solucion:", x)
# Descomposicion en valores propios
valores, vectores = np.linalg.eig(A)
# Descomposicion SVD (usada en PCA y recomendadores)
U, s, Vt = np.linalg.svd(A)
print("Valores singulares:", s)
NumPy en el Ecosistema de Ciencia de Datos
NumPy no trabaja en aislamiento: es la infraestructura sobre la que se construyen todas las librerías de datos. Cuando realizas ingeniería de características con Pandas y Scikit-learn, ambas librerías utilizan arrays NumPy internamente para todas sus operaciones numéricas.
import numpy as np
import pandas as pd
# Crear DataFrame desde array NumPy (sin copiar datos)
arr = np.array([[1.0, 2.0], [3.0, 4.0], [5.0, 6.0]])
df = pd.DataFrame(arr, columns=["A", "B"])
# De Pandas a NumPy
arr_desde_df = df.to_numpy()
print(type(arr_desde_df)) #
# Las funciones NumPy operan directamente sobre columnas Pandas
df["distancia"] = np.sqrt(df["A"] ** 2 + df["B"] ** 2)
Cuando construyes pipelines de machine learning con Scikit-learn, todos los estimadores reciben y devuelven arrays NumPy. Dominar NumPy te permite inspeccionar coeficientes, manipular predicciones y depurar modelos con precisión quirúrgica.
from sklearn.preprocessing import StandardScaler
import numpy as np
X = np.random.randn(100, 4)
# Scikit-learn trabaja internamente con NumPy
scaler = StandardScaler()
X_esc = scaler.fit_transform(X)
print(type(X_esc)) # numpy.ndarray
print(scaler.mean_) # numpy.ndarray con medias por columna
print(scaler.scale_) # numpy.ndarray con desviaciones tipicas
Novedades de NumPy 2.4 para 2026
NumPy 2.4.1, lanzado el 10 de enero de 2026, incorpora mejoras que impactan directamente en flujos de trabajo de ciencia de datos. Algunas son notables:
- Escalares 6x más rápidos: las ufuncs de entrada única sobre escalares son aproximadamente 6 veces más rápidas que en versiones anteriores, lo que beneficia cálculos iterativos ligeros.
- StringDType con hash:
np.uniqueusa ahora un algoritmo basado en hash para deduplicación de strings, obteniendo una mejora de ~15x sobre el método anterior (498s vs 33s para mil millones de strings). - Python sin GIL (free-threaded): soporte estable de la ABI para Python 3.13t, permitiendo paralelización real con múltiples hilos sin el Global Interpreter Lock.
- Nuevo casting
same_value: mayor control sobre conversiones de tipo para evitar pérdidas silenciosas de precisión numérica. - Anotaciones de forma mejoradas: los type-checkers pueden inferir el número de dimensiones del array de retorno en operaciones comunes.
# StringDType para arrays de texto de longitud variable
import numpy as np
texto = np.array(
["pandas", "numpy", "scikit-learn", "numpy", "pandas"],
dtype=np.dtypes.StringDType()
)
unicos = np.unique(texto) # ~15x mas rapido en arrays grandes
print(unicos) # ['numpy' 'pandas' 'scikit-learn']
# Verificar soporte free-threaded
import sys
print("Free-threaded:" , "free-threaded" in sys.version.lower())
Errores Comunes y Cómo Evitarlos
Error de Broadcasting por Formas Incompatibles
# Esto falla:
# a = np.array([[1,2,3],[4,5,6]]) # (2, 3)
# b = np.array([1, 2]) # (2,)
# a + b -> ValueError: could not broadcast (2,3) with (2,)
# Solucion: convertir b a vector columna
a = np.array([[1, 2, 3], [4, 5, 6]])
b = np.array([1, 2])
print(a + b[:, np.newaxis]) # b -> (2,1), se expande a (2,3)
# [[2 3 4]
# [6 7 8]]
Vistas vs Copias
Este es uno de esos errores que cuesta caro la primera vez que te lo encuentras en producción:
arr = np.arange(12).reshape(3, 4)
# Las vistas NO copian datos — modificar la vista modifica el original
vista = arr[1:, :]
vista[:] = 0 # arr tambien cambia
# Para una copia independiente:
copia = arr[1:, :].copy()
copia[:] = 99 # arr NO se modifica
Preguntas Frecuentes
¿Cuál es la diferencia entre una lista de Python y un array de NumPy?
Las listas de Python pueden contener cualquier tipo de objeto y son dinámicas, pero lentas para operaciones numéricas. Los arrays NumPy almacenan elementos del mismo tipo en memoria contigua, lo que permite operaciones vectorizadas a velocidad de C, siendo 10 a 100 veces más rápidos en cálculos matemáticos masivos.
¿Qué es el broadcasting en NumPy y cuándo debo usarlo?
Broadcasting es el mecanismo que permite operar arrays de formas distintas sin copiar datos. Úsalo siempre que necesites aplicar una operación a lo largo de un eje: normalizar columnas de una matriz, sumar un vector a cada fila, o calcular distancias entre múltiples puntos simultáneamente.
¿Vale la pena actualizar a NumPy 2.4 desde NumPy 1.x?
Sí, pero con precaución. NumPy 2.x introduce cambios que rompen la compatibilidad con la ABI de NumPy 1.x, por lo que algunos paquetes compilados pueden necesitar actualización. Las mejoras de rendimiento (hasta 15x en ciertas operaciones) y el nuevo StringDType justifican la migración para proyectos nuevos. Para proyectos existentes, revisa primero la guía oficial de migración.
¿Cuándo debería usar Polars o Dask en lugar de NumPy?
NumPy es ideal para arrays numéricos homogéneos que caben en RAM. Si trabajas con datos tabulares de tipos mixtos, usa Pandas o Polars. Si los datos no caben en la memoria de una sola máquina, considera Dask (que usa NumPy internamente) o cuDF para GPU.
¿Cómo sé si mi código NumPy está aprovechando la vectorización al máximo?
Usa %timeit en Jupyter para comparar versiones con bucles versus vectorizadas. Si ves bucles for sobre elementos individuales del array, hay potencial de optimización. Reemplázalos con np.where(), ufuncs como np.add() o np.multiply(), e indexado booleano. Una aceleración de 50–100x es habitual.