NumPy: la guía completa del cálculo numérico en Python
Guía completa de NumPy para dominar los arrays ndarray, el broadcasting, las operaciones vectorizadas y el álgebra lineal en Python con ejemplos prácticos.
InSkillCoach
NumPy: la guía completa del cálculo numérico en Python
NumPy (Numerical Python) es la biblioteca fundamental del cálculo numérico en Python. Proporciona un objeto central, el array multidimensional ndarray, así como un amplio conjunto de funciones matemáticas optimizadas escritas en C. Prácticamente todo el ecosistema científico de Python (Pandas, SciPy, scikit-learn, Matplotlib) se apoya en NumPy. Esta guía le presenta los conceptos esenciales para manipular arrays numéricos de forma eficiente: creación, indexación, broadcasting, vectorización y álgebra lineal.
¿Por qué NumPy en lugar de las listas de Python?
Las listas de Python son flexibles, pero resultan lentas para el cálculo numérico. Cada elemento de una lista es un objeto Python completo, con su propia cabecera en memoria, mientras que un array de NumPy almacena sus valores en un bloque de memoria contiguo y homogéneo. Las operaciones se delegan entonces en código C compilado, lo que elimina el coste del intérprete.
import numpy as np
import time
n = 10_000_000
liste = list(range(n))
tableau = np.arange(n)
# Multiplicación por 2 con una lista
debut = time.perf_counter()
resultat_liste = [x * 2 for x in liste]
print(f"Liste : {time.perf_counter() - debut:.3f} s") # Liste : 0.520 s (orden de magnitud)
# Multiplicación por 2 con NumPy
debut = time.perf_counter()
resultat_np = tableau * 2
print(f"NumPy : {time.perf_counter() - debut:.3f} s") # NumPy : 0.015 s (orden de magnitud)
La mejora típica es de un factor de 10 a 100 según la operación.
| Criterio | Lista de Python | Array de NumPy |
|---|---|---|
| Tipos de los elementos | Heterogéneos | Homogéneos (un único dtype) |
| Almacenamiento en memoria | Punteros a objetos dispersos | Bloque contiguo y compacto |
| Operaciones elemento a elemento | Bucle explícito de Python | Vectorizadas en C |
| Consumo de memoria | Alto | Bajo |
| Dimensiones | Anidamiento manual | N dimensiones nativas |
Creación de arrays
A partir de datos existentes
import numpy as np
# Desde una lista
a = np.array([1, 2, 3, 4])
print(a) # [1 2 3 4]
# Array 2D desde una lista de listas
m = np.array([[1, 2, 3], [4, 5, 6]])
print(m)
# [[1 2 3]
# [4 5 6]]
Funciones de generación
# Array de ceros (3 filas, 4 columnas)
zeros = np.zeros((3, 4))
# Array de unos
uns = np.ones((2, 3))
# Array sin inicializar (más rápido, contenido arbitrario)
vide = np.empty((2, 2))
# Valor constante
sept = np.full((2, 2), 7)
# Matriz identidad
identite = np.eye(3)
# Secuencia con un paso (como range, pero devuelve un ndarray)
seq = np.arange(0, 10, 2)
print(seq) # [0 2 4 6 8]
# Número fijo de puntos equiespaciados entre dos límites (límites incluidos)
lin = np.linspace(0, 1, 5)
print(lin) # [0. 0.25 0.5 0.75 1. ]
Prefiera linspace a arange para los valores de coma flotante: el número de puntos está garantizado, mientras que el paso flotante de arange puede producir sorpresas de redondeo.
dtype y shape: el documento de identidad de un array
Cada array posee un tipo de elementos (dtype) y una forma (shape).
a = np.array([[1, 2, 3], [4, 5, 6]])
print(a.shape) # (2, 3) -> 2 filas, 3 columnas
print(a.ndim) # 2 -> número de dimensiones
print(a.size) # 6 -> número total de elementos
print(a.dtype) # int64 (int32 en Windows)
# Imponer un tipo en la creación
b = np.array([1, 2, 3], dtype=np.float64)
print(b.dtype) # float64
# Conversión de tipo (crea un nuevo array)
c = b.astype(np.int32)
print(c) # [1 2 3]
# Cambiar la forma sin copiar los datos
d = np.arange(12).reshape(3, 4)
print(d.shape) # (3, 4)
# Aplanar a 1D
print(d.ravel()) # [ 0 1 2 3 4 5 6 7 8 9 10 11]
Cuidado con los desbordamientos de capacidad: un int8 solo puede contener valores entre -128 y 127. En data science, float64 e int64 son las opciones predeterminadas más seguras; reduzca la precisión únicamente si la memoria se convierte en una limitación.
Indexación y slicing
La indexación de NumPy generaliza la de las listas de Python a los arrays multidimensionales.
a = np.arange(10) # [0 1 2 3 4 5 6 7 8 9]
print(a[0]) # 0
print(a[-1]) # 9
print(a[2:7]) # [2 3 4 5 6]
print(a[::2]) # [0 2 4 6 8]
print(a[::-1]) # [9 8 7 6 5 4 3 2 1 0]
m = np.arange(12).reshape(3, 4)
# [[ 0 1 2 3]
# [ 4 5 6 7]
# [ 8 9 10 11]]
print(m[1, 2]) # 6 -> fila 1, columna 2
print(m[0]) # [0 1 2 3] -> primera fila
print(m[:, 1]) # [1 5 9] -> segunda columna
print(m[1:, 2:]) # [[ 6 7]
# [10 11]]
Punto importante: un slice de NumPy devuelve una vista, no una copia. Modificar la vista modifica el array original.
v = m[0, :]
v[0] = 99
print(m[0, 0]) # 99 -> el array original ha sido modificado
# Para obtener una copia independiente
c = m[0, :].copy()
Máscaras booleanas
El filtrado por condición es uno de los modismos más potentes de NumPy.
notes = np.array([12, 8, 15, 17, 9, 14])
masque = notes >= 10
print(masque) # [ True False True True False True]
# Selección de los elementos que cumplen la condición
print(notes[masque]) # [12 15 17 14]
# Condiciones combinadas: & (y), | (o), ~ (no), con paréntesis obligatorios
print(notes[(notes >= 10) & (notes < 16)]) # [12 15 14]
# Modificación condicional
notes[notes < 10] = 10
print(notes) # [12 10 15 17 10 14]
# np.where: elegir entre dos valores según una condición
mention = np.where(notes >= 14, "admis avec mention", "admis")
print(mention)
# ['admis' 'admis' 'admis avec mention' 'admis avec mention' 'admis' 'admis avec mention']
El broadcasting explicado paso a paso
El broadcasting es el mecanismo que permite a NumPy combinar arrays de formas distintas sin copiar los datos. Las reglas son las siguientes:
- Las formas se alinean por la derecha.
- Dos dimensiones son compatibles si son iguales o si una de las dos vale 1.
- Una dimensión de tamaño 1 se estira virtualmente para coincidir con la otra.
Caso 1: array y escalar
a = np.array([1, 2, 3])
print(a + 10) # [11 12 13]
# El escalar 10 se trata como un array de forma (1,) estirado a (3,)
Caso 2: matriz y vector fila
m = np.arange(6).reshape(2, 3) # forma (2, 3)
v = np.array([10, 20, 30]) # forma (3,)
# Alineación por la derecha: (2, 3) y ( 3) -> compatible
print(m + v)
# [[10 21 32]
# [13 24 35]]
# v se suma a cada fila de m
Caso 3: vector columna y vector fila
col = np.array([[1], [2], [3]]) # forma (3, 1)
lig = np.array([10, 20, 30]) # forma (3,)
# (3, 1) y (3,) -> alineadas como (3, 1) y (1, 3) -> resultado (3, 3)
print(col + lig)
# [[11 21 31]
# [12 22 32]
# [13 23 33]]
Caso incompatible
a = np.ones((2, 3))
b = np.ones((2, 2))
# a + b -> ValueError: operands could not be broadcast together
# (2, 3) y (2, 2): la última dimensión difiere y ninguna vale 1
Para centrar las columnas de una matriz, el broadcasting hace el código muy conciso:
donnees = np.array([[1.0, 200.0], [2.0, 300.0], [3.0, 400.0]])
centrees = donnees - donnees.mean(axis=0)
print(centrees)
# [[-1. -100.]
# [ 0. 0.]
# [ 1. 100.]]
Operaciones vectorizadas y funciones universales
Las funciones universales (ufuncs) se aplican elemento a elemento, sin bucle de Python.
a = np.array([1.0, 4.0, 9.0])
print(np.sqrt(a)) # [1. 2. 3.]
print(np.exp(a)) # [2.71828183e+00 5.45981500e+01 8.10308393e+03]
print(np.log(a)) # [0. 1.38629436 2.19722458]
angles = np.linspace(0, np.pi, 3)
print(np.sin(angles)) # [0.0000000e+00 1.0000000e+00 1.2246468e-16]
# Operadores aritméticos: todos vectorizados
b = np.array([10.0, 20.0, 30.0])
print(a + b) # [11. 24. 39.]
print(a * b) # [ 10. 80. 270.] -> producto elemento a elemento
print(b / a) # [10. 5. 3.33333333]
print(a ** 2) # [ 1. 16. 81.]
# Comparaciones vectorizadas
print(a > 3) # [False True True]
# Redondeos
x = np.array([1.234, 5.678])
print(np.round(x, 1)) # [1.2 5.7]
print(np.floor(x)) # [1. 5.]
print(np.ceil(x)) # [2. 6.]
La regla de oro: si escribe un bucle for sobre los elementos de un array de NumPy, casi siempre existe una formulación vectorizada más rápida y más legible.
Agregaciones: sum, mean y el parámetro axis
m = np.array([[1, 2, 3],
[4, 5, 6]])
# Agregación global
print(m.sum()) # 21
print(m.mean()) # 3.5
print(m.min(), m.max()) # 1 6
print(m.std()) # 1.707825127659933
# axis=0: se reduce a lo largo de las filas -> un resultado por columna
print(m.sum(axis=0)) # [5 7 9]
# axis=1: se reduce a lo largo de las columnas -> un resultado por fila
print(m.sum(axis=1)) # [ 6 15]
# Índice del máximo
print(m.argmax()) # 5 (índice en el array aplanado)
print(m.argmax(axis=0)) # [1 1 1]
# Sumas acumuladas
print(np.cumsum([1, 2, 3, 4])) # [ 1 3 6 10]
Una regla mnemotécnica: axis designa la dimensión que desaparece. Con axis=0 sobre una forma (2, 3), el resultado tiene la forma (3,).
Álgebra lineal
A = np.array([[1, 2],
[3, 4]])
B = np.array([[5, 6],
[7, 8]])
# Producto matricial: operador @ (recomendado) o np.dot
print(A @ B)
# [[19 22]
# [43 50]]
print(np.dot(A, B)) # resultado idéntico
# Atención: * es el producto elemento a elemento, no matricial
print(A * B)
# [[ 5 12]
# [21 32]]
# Transpuesta
print(A.T)
# [[1 3]
# [2 4]]
# Determinante e inversa
print(np.linalg.det(A)) # -2.0000000000000004
print(np.linalg.inv(A))
# [[-2. 1. ]
# [ 1.5 -0.5]]
# Resolución del sistema lineal A x = b (preferible a inv(A) @ b)
b = np.array([5, 11])
x = np.linalg.solve(A, b)
print(x) # [1. 2.]
# Valores propios y vectores propios
valeurs, vecteurs = np.linalg.eig(A)
print(valeurs) # [-0.37228132 5.37228132]
# Norma de un vector
v = np.array([3.0, 4.0])
print(np.linalg.norm(v)) # 5.0
Generación de números aleatorios con default_rng
Desde NumPy 1.17, la API recomendada utiliza un generador explícito creado con np.random.default_rng, más rápido y mejor diseñado que las antiguas funciones np.random.rand y similares.
# Generador con semilla para obtener resultados reproducibles
rng = np.random.default_rng(seed=42)
# Flotantes uniformes en [0, 1)
print(rng.random(3)) # [0.77395605 0.43887844 0.85859792]
# Enteros uniformes en [0, 10)
print(rng.integers(0, 10, size=5)) # [8 6 5 2 3]
# Distribución normal (media 0, desviación típica 1)
print(rng.normal(loc=0, scale=1, size=3)) # [ 0.7611397 -0.86157352 0.36371758]
# Muestreo sin reemplazo en un conjunto
print(rng.choice([10, 20, 30, 40], size=2, replace=False)) # [20 40]
# Mezcla in situ
a = np.arange(5)
rng.shuffle(a)
print(a) # [4 0 3 1 2] (por ejemplo)
Lectura y escritura de arrays
a = np.arange(12).reshape(3, 4)
# Formato binario nativo .npy: rápido, conserva dtype y shape
np.save("tableau.npy", a)
b = np.load("tableau.npy")
print(np.array_equal(a, b)) # True
# Varios arrays en un archivo .npz
np.savez("archive.npz", donnees=a, labels=np.array([0, 1, 2]))
archive = np.load("archive.npz")
print(archive["labels"]) # [0 1 2]
# Formato texto: legible pero más lento y menos preciso
np.savetxt("tableau.csv", a, delimiter=",", fmt="%d")
c = np.loadtxt("tableau.csv", delimiter=",")
print(c.dtype) # float64 (loadtxt lee en float por defecto)
Para los archivos CSV con encabezados, columnas heterogéneas o valores ausentes, prefiera pandas.read_csv, mucho más robusto que loadtxt.
Buenas prácticas
- Vectorice sistemáticamente: sustituya los bucles
forpor operaciones sobre arrays completos; es más rápido y más legible. - Desconfíe de las vistas: los slices comparten la memoria del array original; utilice
.copy()cuando necesite un array independiente. - Fije la semilla aleatoria con
np.random.default_rng(seed)para que sus experimentos sean reproducibles. - Utilice
@para el producto matricial y reserve*para el producto elemento a elemento, a fin de evitar confusiones. - Prefiera
np.linalg.solvea la inversión de matrices para resolver un sistema lineal: es numéricamente más estable. - Compruebe
shapeydtypeen cuanto un resultado le sorprenda: la mayoría de los errores de NumPy provienen de un broadcasting involuntario o de un tipo inesperado. - Elija el formato
.npy/.npzpara guardar arrays entre sesiones de Python, y el CSV únicamente para el intercambio con otras herramientas.
Conclusión
NumPy es la base sobre la que descansa todo el análisis de datos en Python. Al dominar el ndarray, la indexación, las máscaras booleanas, el broadcasting y las operaciones vectorizadas, dispone de las herramientas necesarias para escribir código numérico a la vez conciso y eficiente. Estos conceptos se encuentran directamente en Pandas (cuyas columnas son arrays de NumPy), en SciPy (que extiende NumPy al cálculo científico avanzado) y en las bibliotecas de machine learning. El siguiente paso natural consiste en explorar Pandas para la manipulación de datos tabulares y, después, Matplotlib para la visualización.
Acerca de InSkillCoach
Experto en formación y tecnologías
Coach especializado en tecnologías avanzadas e IA, impulsado por GNeurone Inc.
Certificaciones:
- AWS Certified Solutions Architect – Professional
- Certificaciones de Google Cloud
- Microsoft Certified: DevOps Engineer Expert
- Certified Kubernetes Administrator (CKA)
- CompTIA Security+
Comentarios
Los comentarios funcionan con GitHub Discussions
Inicia sesión con GitHub para participar en la discusión