0%
NumPy: la guía completa del cálculo numérico en Python

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.

I

InSkillCoach

· min

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.

CriterioLista de PythonArray de NumPy
Tipos de los elementosHeterogéneosHomogéneos (un único dtype)
Almacenamiento en memoriaPunteros a objetos dispersosBloque contiguo y compacto
Operaciones elemento a elementoBucle explícito de PythonVectorizadas en C
Consumo de memoriaAltoBajo
DimensionesAnidamiento manualN 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:

  1. Las formas se alinean por la derecha.
  2. Dos dimensiones son compatibles si son iguales o si una de las dos vale 1.
  3. 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 for por 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.solve a la inversión de matrices para resolver un sistema lineal: es numéricamente más estable.
  • Compruebe shape y dtype en 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/.npz para 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.

InSkillCoach

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+
1.5k
179

Comentarios

Los comentarios funcionan con GitHub Discussions

Inicia sesión con GitHub para participar en la discusión

¡Enlace copiado!