01 — numpy

NumPy

Biblioteca para cómputo numérico en Python. Cubre el objeto ndarray, operaciones vectorizadas y las funciones de álgebra lineal relevantes para regresión.

Introducción

Las operaciones sobre listas de Python requieren ciclos explícitos. NumPy evita esto mediante operaciones vectorizadas: el cálculo se aplica simultáneamente sobre todos los elementos del arreglo usando rutinas de C compiladas. Esto reduce el tiempo de ejecución de forma significativa conforme crece el tamaño de los datos.

Concepto clave

Un ndarray es una estructura de datos n-dimensional que almacena elementos del mismo tipo en memoria contigua. Esto lo hace mucho más rápido que una lista de Python para operaciones numéricas.

Arrays

Crear arrays

La forma más directa es pasar una lista a np.array():

import numpy as np

arr = np.array([1, 2, 3, 4, 5])
print(arr.shape)  # (5,)
print(arr.dtype)  # int64

Funciones auxiliares para crear arrays de forma automática:

np.arange(0, 10, 2)      # [0 2 4 6 8]
np.linspace(0, 1, 5)     # [0. 0.25 0.5 0.75 1.]
np.zeros((3, 3))         # matriz de ceros 3x3
np.ones((2, 4))          # matriz de unos 2x4
np.eye(3)                # matriz identidad 3x3

Operaciones elementales

Las operaciones aritméticas entre arrays se aplican elemento a elemento sin necesidad de ciclos:

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

a + b    # [5 7 9]
a * b    # [ 4 10 18]
a ** 2   # [1 4 9]
a / b    # [0.25 0.4  0.5 ]
Ejemplo Broadcasting: operar un array con un escalar

Cuando operas un array con un número, el número se "expande" automáticamente al tamaño del array:

x = np.array([10, 20, 30, 40])
x * 2                         # [20 40 60 80]
x - 5                         # [ 5 15 25 35]

# Estandarización Z-score
(x - x.mean()) / x.std()      # [-1.34 -0.45  0.45  1.34]

Producto punto

El producto punto entre dos vectores es la suma de los productos de sus componentes correspondientes:

\[ \mathbf{a} \cdot \mathbf{b} = \sum_{i=1}^{n} a_i \cdot b_i \]
a = np.array([1, 2, 3])
b = np.array([2, 3, 4])

np.dot(a, b)     # 1*2 + 2*3 + 3*4 = 20
(a * b).sum()    # equivalente
a @ b            # operador @ (producto matricial)
Relevancia en machine learning

El producto punto es la operación central de la regresión lineal: la predicción \(\hat{y} = \mathbf{x} \cdot \mathbf{w}\) es un producto punto entre las características del dato y los pesos del modelo.

Álgebra lineal con np.linalg

NumPy incluye el submódulo np.linalg para resolver sistemas de ecuaciones, calcular determinantes, inversas y descomposiciones matriciales.

Para resolver el sistema \(A\mathbf{x} = \mathbf{b}\):

A = np.array([[ 2,  1, -1],
              [-3, -1,  2],
              [-2,  1,  2]])
b = np.array([8, -11, -3])

x = np.linalg.solve(A, b)
print(x)  # [ 2.  3. -1.]
Profundización Transpuesta, inversa y norma

La transpuesta intercambia filas y columnas. La inversa \(A^{-1}\) cumple \(A \cdot A^{-1} = I\). En regresión lineal la solución analítica exacta es:

\[ \mathbf{w} = (X^T X)^{-1} X^T \mathbf{y} \]
A.T                   # transpuesta
np.linalg.inv(A)      # inversa
np.linalg.det(A)      # determinante
np.linalg.norm(v)     # norma euclidiana del vector v

Error Cuadrático Medio con NumPy

El Error Cuadrático Medio (ECM) mide qué tan lejos están las predicciones de los valores reales. Es la función de pérdida más utilizada en regresión:

\[ ECM = \frac{1}{n} \sum_{i=1}^{n} (y_i - \hat{y}_i)^2 \]

Con NumPy se implementa en una sola línea aprovechando las operaciones vectorizadas:

def ecm(y_real: np.ndarray, y_estimado: np.ndarray) -> float:
    """Calcula el Error Cuadrático Medio entre dos vectores."""
    return ((y_real - y_estimado) ** 2).mean()

y     = np.array([1, 2, 3, 4, 5])
y_hat = np.array([1.1, 2.2, 2.9, 4.1, 5.0])
print(ecm(y, y_hat))  # 0.026
Ejemplo Desglose del cálculo paso a paso
y      = np.array([1, 2, 3])
y_hat  = np.array([2, 4, 6])

diferencia = y - y_hat         # [-1 -2 -3]
cuadrados  = diferencia ** 2   # [1  4  9]
ecm_val    = cuadrados.mean()  # (1+4+9)/3 = 4.67

Pendiente óptima con NumPy

Para una regresión lineal simple \(\hat{y} = mx + b\) con ordenada al origen conocida, la pendiente que minimiza el ECM se obtiene con la fórmula cerrada:

\[ m = \frac{\sum x_i y_i - b \sum x_i}{\sum x_i^2} \]
def pendiente_optima(b: float, x: np.ndarray, y: np.ndarray) -> float:
    return ((x * y).sum() - b * x.sum()) / (x ** 2).sum()

x = np.array([1.0, 2.0, 3.0, 4.0, 5.0])
y = np.array([2.1, 4.0, 5.9, 8.1, 10.0])

m = pendiente_optima(0.0, x, y)
print(f"Pendiente: {m:.4f}")  # aprox. 2.0
Ejercicio Implementa la función de valores estimados

Dado un vector x, una pendiente m y una ordenada al origen b, calcula los valores estimados \(\hat{y} = mx + b\):

def calcular_y(x: np.ndarray, m: float, b: float) -> np.ndarray:
    # Tu código aquí
    pass

# Resultado esperado: [ 2.  4.  6.  8. 10.]
print(calcular_y(np.array([1, 2, 3, 4, 5]), 2.0, 0.0))

Solución:

def calcular_y(x: np.ndarray, m: float, b: float) -> np.ndarray:
    return x * m + b