Capítulo 20 de 21 14 secciones 8 min

Compartir

El proyecto entero

Los veinte capítulos aplicados de corrido a un problema: desde abrir el archivo hasta interpretar los coeficientes, en diez pasos.

Este es el libro entero aplicado a un problema: predecir el monto final facturado con cuatro columnas. Diez pasos, desde mirar la forma de la matriz hasta leer los coeficientes, y cada uno apunta al capítulo que lo explica.

Todo junto, de una vez 🌸

Veinte capítulos y ni una sola librería de machine learning. Solo NumPy, pandas y lo que fuimos construyendo.

El problema: predecir cuánto se factura de verdad en una venta, a partir de las unidades, el monto, el descuento y la satisfacción.

Cada paso lleva el capítulo que lo explica. Si alguno no te cuadra, ahí está de dónde salió.

Pasos 1 y 2: mirar antes de tocar

import pandas as pd
import numpy as np

COLS = ['unidades', 'monto', 'descuento', 'satisfaccion']
df = pd.read_csv('ventas-miss-yera.csv')
for c in COLS + ['monto_final_facturado']:
    df[c] = pd.to_numeric(df[c], errors='coerce')
d = df[COLS + ['monto_final_facturado']].dropna()

y = d['monto_final_facturado'].to_numpy()
F = d[COLS].to_numpy()

print('forma:', F.shape, '| objetivo:', y.shape)
print('rango:', np.linalg.matrix_rank(F), 'de', F.shape[1])
forma: (1793, 4) | objetivo: (1793,)
rango: 4 de 4

Forma primero, siempre, como en el capítulo 1. Y rango completo, que quiere decir que ninguna columna sobra: el chequeo del capítulo 8 pasa.

Paso 3: escalar

Z = (F - F.mean(axis=0)) / F.std(axis=0)
print('medias      :', np.round(Z.mean(axis=0), 10))
print('desviaciones:', np.round(Z.std(axis=0), 6))
medias      : [-0.  0. -0.  0.]
desviaciones: [1. 1. 1. 1.]

Media cero y desviación uno. Es el paso que en los capítulos 7, 11 y 18 resultó ser el que arreglaba todo, así que va temprano y no se discute.

Paso 4: la matriz que aparece siempre

A = np.column_stack([np.ones(len(Z)), Z])
n = len(y)
G = A.T @ A
print('forma      :', G.shape, '| simetrica:', np.allclose(G, G.T))
print('condicion  :', round(float(np.linalg.cond(G)), 2))
forma      : (5, 5) | simetrica: True
condicion  : 1.11

La columna de unos es el intercepto del capítulo 9. Y condición 1.11 es prácticamente perfecta: no hay nada mal condicionado acá, gracias al paso anterior.

Paso 5: resolver

w_exacto = np.linalg.solve(G, A.T @ y)
print('pesos:', np.round(w_exacto, 4))
pesos: [472.3815   2.2614 478.3356 -20.7077  58.9271]

Las ecuaciones normales del capítulo 6, con solve y no con inv.

Paso 6: comprobar y medir

res = y - A @ w_exacto
print('residuos contra columnas:', np.round(A.T @ res, 6))
print('RMSE modelo:', round(float(np.sqrt((res ** 2).mean())), 2))
print('RMSE media :', round(float(np.sqrt(((y - y.mean()) ** 2).mean())), 2))
residuos contra columnas: [-0.  0.  0.  0. -0.]
RMSE modelo: 433.8
RMSE media : 648.75

Residuos ortogonales, o sea que la proyección del capítulo 9 está bien hecha. Y esta vez, a diferencia de los ejemplos de los capítulos, el modelo sí sirve: baja el error de 648.75 a 433.80, que es un 33% menos.

Te lo señalo porque a lo largo del libro te enseñé varios modelos que no mejoraban nada, a propósito. Este mejora, y la diferencia es que ahora las columnas sí tienen información sobre lo que quiero predecir 🌟

Paso 7: la curvatura y el límite de la tasa

H = 2 * G / n
vals = np.linalg.eigvalsh(H)
print('autovalores:', np.round(vals, 4))
print('limite tasa:', round(float(2 / vals.max()), 6))
autovalores: [1.8889 1.9586 2.     2.0571 2.0954]
limite tasa: 0.954485

Todos positivos: convexo, hay un solo fondo. Y ya sé qué tasa puedo usar antes de probar ni una. Los dos son los capítulos 16 y 18.

Paso 8: llegar al mismo sitio por el otro camino

def gradiente(w):
    return -2 * (A.T @ (y - A @ w)) / n

w = np.zeros(A.shape[1])
for _ in range(500):
    w = w - 0.3 * gradiente(w)

print('descenso :', np.round(w, 4))
print('distancia:', round(float(np.linalg.norm(w - w_exacto)), 8))
descenso : [472.3815   2.2614 478.3356 -20.7077  58.9271]
distancia: 0.0

Distancia cero. El descenso de gradiente del capítulo 17 y la fórmula cerrada llegan exactamente al mismo punto.

Este es el momento del libro que más me gusta. Álgebra por un lado, cálculo por el otro, y el mismo vector 💛

Paso 9: mirar la estructura

C = np.cov(Z, rowvar=False)
ev, evec = np.linalg.eigh(C)
orden = np.argsort(ev)[::-1]
print('varianza explicada:', np.round(ev[orden] / ev.sum(), 4))
print('primer autovector :', np.round(evec[:, orden][:, 0], 4))
varianza explicada: [0.2619 0.2571 0.2448 0.2361]
primer autovector : [ 0.4589  0.5758 -0.5253 -0.4265]

26%, 26%, 24%, 24%. Repartido casi perfecto entre las cuatro.

Eso es el capítulo 11 diciéndote que no apliques PCA. No hay redundancia que comprimir: las cuatro columnas aportan cosas distintas. Y eso, para un modelo, es una buena noticia.

Paso 10: regularizar, y comprobar que no hace falta

for al in (0.0, 10.0, 1000.0):
    wr = np.linalg.solve(G + al * np.eye(A.shape[1]), A.T @ y)
    print('alpha', str(al).rjust(6), np.round(wr, 3))
alpha    0.0 [472.382   2.261 478.336 -20.708  58.927]
alpha   10.0 [469.762   2.237 475.677 -20.702  58.561]
alpha 1000.0 [303.251   1.007 306.935 -17.826  36.208]

Con alpha 10 los pesos casi no se mueven. Compáralo con lo que pasaba en el capítulo 19, donde alpha 1 hacía saltar un coeficiente de -565 a 174.

La diferencia es el paso 2: allá había una columna casi duplicada y aquí no. Cuando no hay multicolinealidad, regularizar apenas cambia nada, y con alpha 1000 solo estropea. Que un parámetro no haga nada también es información.

Y ahora la parte que ninguna fórmula hace

Los pesos son 2.26 para unidades, 478.34 para monto, -20.71 para descuento y 58.93 para satisfacción, y como escalamos, todos están medidos en desviaciones y sí se pueden comparar.

El monto manda por goleada, y tiene todo el sentido: lo que se factura sale de lo que se cobra. El descuento resta, que también cuadra.

Y ahora el aviso, que es lo último que quiero que te lleves del libro. Ese -20.71 no dice que dar descuento baje la facturación. Dice que en este archivo, las ventas con más descuento facturaron menos, y eso puede ser porque el descuento baja el monto, o porque los descuentos grandes se dan justamente en las ventas chicas, o por diez cosas más.

El álgebra lineal encuentra relaciones. La causa la pones tú, o no la pone nadie. Toda la matemática de este libro te da coeficientes correctísimos de una pregunta que a lo mejor no era la que querías hacer 🐣

Lo que te llevas

  • Forma y rango antes de tocar nada.
  • Escalar temprano, porque arregla la distancia, PCA y la convergencia.
  • solve, nunca inv.
  • Comparar siempre contra predecir la media.
  • Dos caminos independientes que dan el mismo resultado es la mejor comprobación.
  • PCA repartido parejo quiere decir que no apliques PCA.
  • Que regularizar no cambie nada también es información.
  • Los coeficientes son relaciones, no causas.

Comprueba que se entendió

Comprueba que lo tienes

En el proyecto, el descenso de gradiente llega al mismo punto que la fórmula exacta con distancia 0.0. ¿Qué te permite concluir?

  • Que el problema estaba bien escalado y bien condicionado, y por eso los dos caminos coinciden
  • Que el modelo predice bien
  • Que el descenso de gradiente siempre llega al mismo sitio
  • Que la fórmula exacta sobra

Ejercicios

1. Quita una columna y mira cuánto costó

Cada columna aporta algo medible.

COLS = ['unidades', 'monto', 'descuento', 'satisfaccion']
for fuera in range(4):
    quedan = [j for j in range(4) if j != fuera]
    Zq = Z[:, quedan]
    Aq = np.column_stack([np.ones(len(Zq)), Zq])
    wq = np.linalg.solve(Aq.T @ Aq, Aq.T @ y)
    rmse = float(np.sqrt(((y - Aq @ wq) ** 2).mean()))
    print('sin', COLS[fuera].ljust(14), '-> RMSE', round(rmse, 2))
sin unidades       -> RMSE 433.81
sin monto          -> RMSE 645.43
sin descuento      -> RMSE 434.3
sin satisfaccion   -> RMSE 437.78

Sin el monto el modelo se va a 645.43, que es casi lo mismo que predecir la media (648.75). Las otras tres, quitándolas, mueven el RMSE entre cero y cuatro puntos.

O sea que el modelo entero es el monto y las otras tres están casi de adorno. Fíjate en que quitar las unidades deja el error en 433.81 cuando con ellas era 433.80: aportan una centésima. Eso no se ve mirando coeficientes, se ve quitando columnas.

2. El chequeo que deberías correr siempre

Cinco líneas que te ahorran tardes enteras.

def revisa(M, nombre='X'):
    print(nombre, 'forma    :', M.shape)
    print(nombre, 'rango    :', np.linalg.matrix_rank(M), 'de', M.shape[1])
    print(nombre, 'nan      :', bool(np.isnan(M).any()))
    print(nombre, 'condicion:', round(float(np.linalg.cond(M.T @ M)), 2))

revisa(Z, 'Z')
Z forma    : (1793, 4)
Z rango    : 4 de 4
Z nan      : False
Z condicion: 1.11

Forma, rango, nan y condición. Cuatro números que resumen los capítulos 1, 8, 18 y 16. Cópialo en tu plantilla de proyectos y córrelo antes de entrenar cualquier cosa.

3. Compruébalo contra scikit-learn

Que lo que escribiste a mano sea lo mismo que usa todo el mundo.

from sklearn.linear_model import LinearRegression

modelo = LinearRegression().fit(Z, y)
print('sklearn intercepto:', round(float(modelo.intercept_), 4))
print('sklearn pesos     :', np.round(modelo.coef_, 4))
print('a mano            :', np.round(w_exacto, 4))
sklearn intercepto: 472.3815
sklearn pesos     : [  2.2614 478.3356 -20.7077  58.9271]
a mano            : [472.3815   2.2614 478.3356 -20.7077  58.9271]

Los mismos números hasta el cuarto decimal. Y eso es lo que quería que supieras al terminar este libro: LinearRegression() no es una caja negra. Es np.linalg.solve con manejo de errores y una API bonita, y tú acabas de escribir lo de adentro 💛

Practica este capítulo 📓

Todo el código de arriba en un cuaderno que corre de principio a fin, y los ejercicios con una celda vacía para que los hagas tú. Se abre en Google Colab de un clic y no hay que instalar nada. Donde veas %%revisa, escribe tu respuesta y el cuaderno te dice si te salió.

¿Prefieres trabajar en tu máquina? Bájate el cuaderno de práctica o el de soluciones. Todos están también en github.com/soymissyera/MissYeraEjercicios.

¿Le sirve a alguien que conoces?

Pásale el libro. Es gratis, está entero y no pide registro 🐣

Instagram y TikTok no dejan compartir enlaces desde la web: esos dos copian la URL para que la pegues en tu historia.

¿Tienes alguna duda o consulta?