NumPy en Python: arrays N-dimensionales, operaciones vectorizadas y broadcasting

NumPy es la base de casi todo el ecosistema científico de Python: pandas, scikit-learn, TensorFlow y PyTorch usan NumPy por debajo. Su aportación central es el ndarray: una estructura de datos N-dimensional con operaciones vectorizadas que ejecuta en C, eliminando la necesidad de bucles for para cálculos sobre grandes colecciones.

Crear arrays

import numpy as np

# Desde listas de Python
v = np.array([1, 2, 3, 4, 5])             # array 1D de enteros
m = np.array([[1, 2, 3], [4, 5, 6]])       # array 2D (matriz 2x3)
c = np.array([1.0, 2.0, 3.0])             # float64 automáticamente

# Arrays con valores predefinidos
print(np.zeros((3, 4)))          # matriz 3x4 de ceros
print(np.ones((2, 3)))           # matriz 2x3 de unos
print(np.eye(3))                 # matriz identidad 3x3
print(np.full((2, 2), 7))       # matriz 2x2 rellena con 7

# Secuencias
print(np.arange(0, 10, 2))      # [0 2 4 6 8] (como range, pero array)
print(np.linspace(0, 1, 5))     # [0.   0.25  0.5   0.75  1. ] (5 puntos equidistantes)

# Aleatorios
rng = np.random.default_rng(seed=42)
print(rng.random((3, 3)))       # floats entre 0 y 1
print(rng.integers(0, 100, size=10))  # enteros entre 0 y 99

Propiedades del array

import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6]])

print(a.shape)    # (2, 3) — filas, columnas
print(a.ndim)     # 2 — número de dimensiones
print(a.size)     # 6 — total de elementos
print(a.dtype)    # int64 — tipo de dato
print(a.itemsize) # 8 — bytes por elemento

# Cambiar forma sin copiar datos
b = a.reshape(3, 2)   # (3, 2)
c = a.flatten()        # [1, 2, 3, 4, 5, 6]
d = a.T                # transpuesta: (3, 2)

Operaciones vectorizadas sin bucles for

import numpy as np

precios = np.array([10.0, 25.0, 8.5, 14.0, 30.0])
cantidades = np.array([3, 1, 5, 2, 1])

# Operaciones elemento a elemento (sin for)
totales = precios * cantidades        # [30.  25.  42.5 28.  30. ]
con_iva = precios * 1.21              # [12.1  30.25  10.285 ...]
descuento = np.where(precios > 20, precios * 0.9, precios)  # 10% dto si precio > 20

# Funciones matemáticas aplicadas a todos los elementos
v = np.array([1.0, 4.0, 9.0, 16.0])
print(np.sqrt(v))    # [1. 2. 3. 4.]
print(np.log(v))     # [0. 1.39 2.2  2.77]
print(np.exp(v))     # [2.72 54.6 8103. ...]

# Comparación: velocidad
import time
n = 1_000_000
lista = list(range(n))
arr = np.arange(n, dtype=float)

inicio = time.perf_counter()
resultado_lista = [x ** 2 for x in lista]
t_lista = time.perf_counter() - inicio

inicio = time.perf_counter()
resultado_np = arr ** 2
t_np = time.perf_counter() - inicio

print(f"Lista: {t_lista:.3f}s  NumPy: {t_np:.3f}s")
# Lista: 0.150s  NumPy: 0.003s  (?50x más rápido)

Indexing y slicing

import numpy as np

a = np.array([[1, 2, 3], [4, 5, 6], [7, 8, 9]])

# Acceso a elementos
print(a[0, 0])   # 1
print(a[2, 1])   # 8

# Slicing: [filas, columnas]
print(a[:, 0])   # [1 4 7] — primera columna
print(a[0, :])   # [1 2 3] — primera fila
print(a[1:, 1:]) # [[5 6] [8 9]] — submatriz

# Indexing booleano (filtrado)
datos = np.array([1, -2, 3, -4, 5, -6])
positivos = datos[datos > 0]
print(positivos)  # [1 3 5]

# Indexing con arrays de índices
indices = np.array([0, 2, 4])
print(datos[indices])  # [1 3 5]

Broadcasting: operaciones entre arrays de distinto tamaño

Broadcasting es la regla que aplica NumPy cuando las dimensiones de dos arrays no coinciden exactamente. En lugar de copiar datos, "expande" virtualmente las dimensiones para que la operación tenga sentido.

import numpy as np

# Sumar un escalar a un array
a = np.array([1, 2, 3])
print(a + 10)    # [11 12 13]  — 10 se "expande" a [10, 10, 10]

# Sumar vector columna a matriz
matriz = np.array([[1, 2, 3], [4, 5, 6]])  # (2, 3)
fila = np.array([10, 20, 30])               # (3,)
print(matriz + fila)
# [[11 22 33]
#  [14 25 36]]

# Suma de columna (2x1) con fila (1x3) ? matriz (2x3)
col = np.array([[100], [200]])   # (2, 1)
fil = np.array([1, 2, 3])        # (3,)
print(col + fil)
# [[101 102 103]
#  [201 202 203]]

# Caso de uso real: normalización por fila
datos = np.array([[1.0, 2.0, 3.0], [4.0, 5.0, 6.0]])
medias = datos.mean(axis=1, keepdims=True)   # (2, 1)
normalizado = datos - medias
print(normalizado)
# [[-1.  0.  1.]
#  [-1.  0.  1.]]

Ufuncs y agregaciones

import numpy as np

ventas = np.array([[120, 250, 180], [90, 310, 200], [150, 280, 160]])
# Filas: trimestres; Columnas: productos

# Agregaciones sobre todo el array
print(ventas.sum())          # total global
print(ventas.mean())         # media global
print(ventas.max())          # máximo global

# Agregaciones por eje
print(ventas.sum(axis=0))    # total por producto (suma columnas)
print(ventas.sum(axis=1))    # total por trimestre (suma filas)
print(ventas.mean(axis=0))   # media por producto
print(ventas.argmax(axis=1)) # índice del producto más vendido por trimestre

# Otras ufuncs útiles
print(np.cumsum(ventas, axis=1))  # suma acumulada por fila
print(np.diff(ventas, axis=0))    # diferencia entre trimestres consecutivos

El principio fundamental de NumPy: si puedes expresar una operación como una transformación sobre todo el array a la vez, hazlo así. Los bucles for de Python son lentos cuando procesan muchos elementos; las operaciones vectorizadas de NumPy trabajan en C con SIMD y son decenas o cientos de veces más rápidas. El cambio de mentalidad —pensar en arrays en lugar de en elementos individuales— es lo que diferencia el código NumPy eficiente del que simplemente funciona.

COMPARTE ESTE ARTÍCULO

COMPARTIR EN FACEBOOK
COMPARTIR EN TWITTER
COMPARTIR EN LINKEDIN
COMPARTIR EN WHATSAPP