{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# El proyecto entero\n",
    "\n",
    "Los veinte capítulos aplicados de corrido a un problema: desde abrir el archivo hasta interpretar los coeficientes, en diez pasos.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 20 de **Matemática para machine learning**, de Miss Yera.\n",
    "\n",
    "Corre de arriba abajo. Si lo abres en Google Colab no necesitas instalar nada.\n",
    "\n",
    "Capítulo completo: https://missyera.com/guias/matematica-para-machine-learning/el-proyecto-entero/\n",
    "\n",
    "Este es el cuaderno de **soluciones**. Trae el código de cada ejercicio, la\n",
    "explicación de la trampa y la respuesta del quiz. Si vienes del cuaderno de\n",
    "práctica sin haberlo intentado, vuelve 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Antes de empezar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# Este capitulo abre el archivo por su nombre, sin URL, que es lo que se hace\n",
    "# cuando lo tienes al lado. Aqui se baja primero para que exista.\n",
    "for nombre in ['ventas-miss-yera.csv']:\n",
    "    urllib.request.urlretrieve(f\"https://missyera.com/static/datasets/{nombre}\", nombre)\n",
    "    print(nombre, \"lista\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Todo junto, de una vez 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Veinte capítulos y ni una sola librería de machine learning. Solo NumPy,\n",
    "pandas y lo que fuimos construyendo.\n",
    "\n",
    "El problema: predecir **cuánto se factura de verdad** en una\n",
    "venta, a partir de las unidades, el monto, el descuento y la satisfacción.\n",
    "\n",
    "Cada paso lleva el capítulo que lo explica. Si alguno no te cuadra, ahí está\n",
    "de dónde salió."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Pasos 1 y 2: mirar antes de tocar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "COLS = ['unidades', 'monto', 'descuento', 'satisfaccion']\n",
    "df = pd.read_csv('ventas-miss-yera.csv')\n",
    "for c in COLS + ['monto_final_facturado']:\n",
    "    df[c] = pd.to_numeric(df[c], errors='coerce')\n",
    "d = df[COLS + ['monto_final_facturado']].dropna()\n",
    "\n",
    "y = d['monto_final_facturado'].to_numpy()\n",
    "F = d[COLS].to_numpy()\n",
    "\n",
    "print('forma:', F.shape, '| objetivo:', y.shape)\n",
    "print('rango:', np.linalg.matrix_rank(F), 'de', F.shape[1])"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Forma primero, siempre, como en el capítulo 1. Y rango completo,\n",
    "que quiere decir que ninguna columna sobra: el chequeo del capítulo 8 pasa."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 3: escalar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Z = (F - F.mean(axis=0)) / F.std(axis=0)\n",
    "print('medias      :', np.round(Z.mean(axis=0), 10))\n",
    "print('desviaciones:', np.round(Z.std(axis=0), 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Media cero y desviación uno. Es el paso que en los capítulos\n",
    "7, 11 y 18\n",
    "resultó ser el que arreglaba todo, así que va temprano y no se discute."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 4: la matriz que aparece siempre"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "A = np.column_stack([np.ones(len(Z)), Z])\n",
    "n = len(y)\n",
    "G = A.T @ A\n",
    "print('forma      :', G.shape, '| simetrica:', np.allclose(G, G.T))\n",
    "print('condicion  :', round(float(np.linalg.cond(G)), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La columna de unos es el intercepto del capítulo 9. Y condición 1.11\n",
    "es prácticamente perfecta: no hay nada mal condicionado acá, gracias al paso\n",
    "anterior."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 5: resolver"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "w_exacto = np.linalg.solve(G, A.T @ y)\n",
    "print('pesos:', np.round(w_exacto, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las ecuaciones normales del capítulo 6, con\n",
    "`solve` y no con `inv`."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 6: comprobar y medir"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "res = y - A @ w_exacto\n",
    "print('residuos contra columnas:', np.round(A.T @ res, 6))\n",
    "print('RMSE modelo:', round(float(np.sqrt((res ** 2).mean())), 2))\n",
    "print('RMSE media :', round(float(np.sqrt(((y - y.mean()) ** 2).mean())), 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Residuos ortogonales, o sea que la proyección del capítulo 9 está\n",
    "bien hecha. Y esta vez, a diferencia de los ejemplos de los capítulos, el modelo\n",
    "**sí sirve**: baja el error de 648.75 a 433.80, que es un 33%\n",
    "menos.\n",
    "\n",
    "Te lo señalo porque a lo largo del libro te enseñé varios modelos que no\n",
    "mejoraban nada, a propósito. Este mejora, y la diferencia es que ahora las\n",
    "columnas sí tienen información sobre lo que quiero predecir 🌟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 7: la curvatura y el límite de la tasa"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "H = 2 * G / n\n",
    "vals = np.linalg.eigvalsh(H)\n",
    "print('autovalores:', np.round(vals, 4))\n",
    "print('limite tasa:', round(float(2 / vals.max()), 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Todos positivos: convexo, hay un solo fondo. Y ya sé qué tasa puedo usar antes\n",
    "de probar ni una. Los dos son los capítulos 16 y\n",
    "18."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 8: llegar al mismo sitio por el otro camino"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def gradiente(w):\n",
    "    return -2 * (A.T @ (y - A @ w)) / n\n",
    "\n",
    "w = np.zeros(A.shape[1])\n",
    "for _ in range(500):\n",
    "    w = w - 0.3 * gradiente(w)\n",
    "\n",
    "print('descenso :', np.round(w, 4))\n",
    "print('distancia:', round(float(np.linalg.norm(w - w_exacto)), 8))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Distancia cero. El descenso de gradiente del capítulo 17 y la\n",
    "fórmula cerrada llegan exactamente al mismo punto.\n",
    "\n",
    "Este es el momento del libro que más me gusta. Álgebra por un lado, cálculo\n",
    "por el otro, y el mismo vector 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 9: mirar la estructura"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "C = np.cov(Z, rowvar=False)\n",
    "ev, evec = np.linalg.eigh(C)\n",
    "orden = np.argsort(ev)[::-1]\n",
    "print('varianza explicada:', np.round(ev[orden] / ev.sum(), 4))\n",
    "print('primer autovector :', np.round(evec[:, orden][:, 0], 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "26%, 26%, 24%, 24%. Repartido casi perfecto entre las cuatro.\n",
    "\n",
    "Eso es el capítulo 11 diciéndote **que no apliques PCA**. No\n",
    "hay redundancia que comprimir: las cuatro columnas aportan cosas distintas. Y\n",
    "eso, para un modelo, es una buena noticia."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Paso 10: regularizar, y comprobar que no hace falta"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for al in (0.0, 10.0, 1000.0):\n",
    "    wr = np.linalg.solve(G + al * np.eye(A.shape[1]), A.T @ y)\n",
    "    print('alpha', str(al).rjust(6), np.round(wr, 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "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\n",
    "-565 a 174.\n",
    "\n",
    "La diferencia es el paso 2: allá había una columna casi duplicada y aquí no.\n",
    "**Cuando no hay multicolinealidad, regularizar apenas cambia nada**,\n",
    "y con alpha 1000 solo estropea. Que un parámetro no haga nada también es\n",
    "información."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora la parte que ninguna fórmula hace"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los pesos son 2.26 para unidades, 478.34 para monto, -20.71 para descuento y\n",
    "58.93 para satisfacción, y como escalamos, todos están medidos en desviaciones y\n",
    "sí se pueden comparar.\n",
    "\n",
    "El monto manda por goleada, y tiene todo el sentido: lo que se factura sale\n",
    "de lo que se cobra. El descuento resta, que también cuadra.\n",
    "\n",
    "Y ahora el aviso, que es lo último que quiero que te lleves del libro. Ese\n",
    "-20.71 **no dice que dar descuento baje la facturación**. Dice que\n",
    "en este archivo, las ventas con más descuento facturaron menos, y eso puede ser\n",
    "porque el descuento baja el monto, o porque los descuentos grandes se dan\n",
    "justamente en las ventas chicas, o por diez cosas más.\n",
    "\n",
    "**El álgebra lineal encuentra relaciones. La causa la pones tú, o no la\n",
    "pone nadie.** Toda la matemática de este libro te da coeficientes\n",
    "correctísimos de una pregunta que a lo mejor no era la que querías hacer 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- Forma y rango antes de tocar nada.\n",
    "\n",
    "- Escalar temprano, porque arregla la distancia, PCA y la convergencia.\n",
    "\n",
    "- `solve`, nunca `inv`.\n",
    "\n",
    "- Comparar siempre contra predecir la media.\n",
    "\n",
    "- Dos caminos independientes que dan el mismo resultado es la mejor\n",
    "comprobación.\n",
    "\n",
    "- PCA repartido parejo quiere decir que no apliques PCA.\n",
    "\n",
    "- Que regularizar no cambie nada también es información.\n",
    "\n",
    "- Los coeficientes son relaciones, no causas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "En el proyecto, el descenso de gradiente llega al mismo punto que la fórmula exacta con distancia 0.0. ¿Qué te permite concluir?\n",
    "\n",
    "a) Que el problema estaba bien escalado y bien condicionado, y por eso los dos caminos coinciden\n",
    "\n",
    "b) Que el modelo predice bien\n",
    "\n",
    "c) Que el descenso de gradiente siempre llega al mismo sitio\n",
    "\n",
    "d) Que la fórmula exacta sobra\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Son cosas distintas. Coincidir quiere decir que la optimización funcionó; si esa solución sirve lo dice el RMSE comparado contra la media.\n",
    "\n",
    "*c)* Solo si el problema es convexo y la tasa está bajo el límite. Con las columnas sin escalar, el mismo código explotaba.\n",
    "\n",
    "*d)* Al revés: sirve justamente para comprobar el descenso. En un modelo donde no exista fórmula no vas a tener contra qué comparar.\n",
    "\n",
    "Dos caminos independientes que llegan al mismo punto es la mejor comprobación que hay. Úsala siempre que puedas 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Quita una columna y mira cuánto costó\n",
    "\n",
    "Cada columna aporta algo medible."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "COLS = ['unidades', 'monto', 'descuento', 'satisfaccion']\n",
    "for fuera in range(4):\n",
    "    quedan = [j for j in range(4) if j != fuera]\n",
    "    Zq = Z[:, quedan]\n",
    "    Aq = np.column_stack([np.ones(len(Zq)), Zq])\n",
    "    wq = np.linalg.solve(Aq.T @ Aq, Aq.T @ y)\n",
    "    rmse = float(np.sqrt(((y - Aq @ wq) ** 2).mean()))\n",
    "    print('sin', COLS[fuera].ljust(14), '-> RMSE', round(rmse, 2))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "sin unidades       -> RMSE 433.81\n",
    "sin monto          -> RMSE 645.43\n",
    "sin descuento      -> RMSE 434.3\n",
    "sin satisfaccion   -> RMSE 437.78\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Sin el monto el modelo se va a 645.43, que es casi lo mismo que predecir la\n",
    "media (648.75). Las otras tres, quitándolas, mueven el RMSE entre cero y cuatro\n",
    "puntos.\n",
    "\n",
    "O sea que **el modelo entero es el monto** y las otras tres están\n",
    "casi de adorno. Fíjate en que quitar las unidades deja el error en 433.81 cuando\n",
    "con ellas era 433.80: aportan una centésima. Eso no se ve mirando coeficientes,\n",
    "se ve quitando columnas."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. El chequeo que deberías correr siempre\n",
    "\n",
    "Cinco líneas que te ahorran tardes enteras."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def revisa(M, nombre='X'):\n",
    "    print(nombre, 'forma    :', M.shape)\n",
    "    print(nombre, 'rango    :', np.linalg.matrix_rank(M), 'de', M.shape[1])\n",
    "    print(nombre, 'nan      :', bool(np.isnan(M).any()))\n",
    "    print(nombre, 'condicion:', round(float(np.linalg.cond(M.T @ M)), 2))\n",
    "\n",
    "revisa(Z, 'Z')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "Z forma    : (1793, 4)\n",
    "Z rango    : 4 de 4\n",
    "Z nan      : False\n",
    "Z condicion: 1.11\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Forma, rango, nan y condición. Cuatro números que resumen los capítulos\n",
    "1, 8,\n",
    "18 y 16. Cópialo en tu\n",
    "plantilla de proyectos y córrelo antes de entrenar cualquier cosa."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Compruébalo contra scikit-learn\n",
    "\n",
    "Que lo que escribiste a mano sea lo mismo que usa todo el\n",
    "mundo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "from sklearn.linear_model import LinearRegression\n",
    "\n",
    "modelo = LinearRegression().fit(Z, y)\n",
    "print('sklearn intercepto:', round(float(modelo.intercept_), 4))\n",
    "print('sklearn pesos     :', np.round(modelo.coef_, 4))\n",
    "print('a mano            :', np.round(w_exacto, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "sklearn intercepto: 472.3815\n",
    "sklearn pesos     : [  2.2614 478.3356 -20.7077  58.9271]\n",
    "a mano            : [472.3815   2.2614 478.3356 -20.7077  58.9271]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los mismos números hasta el cuarto decimal. Y eso es lo que quería que\n",
    "supieras al terminar este libro: `LinearRegression()` no es una caja\n",
    "negra. Es `np.linalg.solve` con manejo de errores y una API bonita, y\n",
    "tú acabas de escribir lo de adentro 💛"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 20 de **Matemática para machine learning**. El texto completo, con las salidas de cada bloque, está en https://missyera.com/guias/matematica-para-machine-learning/el-proyecto-entero/\n",
    "\n",
    "Que tengas lindo día! 🌸"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "name": "python",
   "version": "3.11"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
