{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Regularización vista como geometría: por qué L1 hace ceros y L2 no\n",
    "\n",
    "Ridge y lasso explicados desde las normas L1 y L2, con la diferencia geométrica que hace que una ponga ceros y la otra no.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 19 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/regularizacion-l1-y-l2/\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": [
    "## El capítulo donde se juntan casi todos los anteriores 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Acá vuelven las normas del capítulo 7, la redundancia entre\n",
    "columnas del capítulo 8 y la inversa del capítulo 6. Si algo de eso te quedó suelto, este es el\n",
    "capítulo que lo amarra."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Montamos el desastre a propósito"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Necesito dos columnas casi iguales, así que fabrico una: el monto escalado\n",
    "más un poquito de ruido."
   ]
  },
  {
   "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",
    "        'monto_final_facturado']\n",
    "df = pd.read_csv('ventas-miss-yera.csv')\n",
    "for c in COLS:\n",
    "    df[c] = pd.to_numeric(df[c], errors='coerce')\n",
    "d = df[COLS].dropna()\n",
    "\n",
    "y = d['monto_final_facturado'].to_numpy()\n",
    "F = d[['unidades', 'monto', 'descuento', 'satisfaccion']].to_numpy()\n",
    "Z = (F - F.mean(axis=0)) / F.std(axis=0)\n",
    "\n",
    "rng = np.random.default_rng(0)\n",
    "Z = np.column_stack([Z, Z[:, 1] + rng.normal(0, 0.01, len(Z))])\n",
    "NOMBRES = ['unidades', 'monto', 'descuento', 'satisfaccion',\n",
    "           'monto_casi_igual']\n",
    "yc = y - y.mean()\n",
    "n, dd = Z.shape\n",
    "\n",
    "print('Z.shape:', Z.shape)\n",
    "print('correlacion monto vs monto_casi_igual:',\n",
    "      round(float(np.corrcoef(Z[:, 1], Z[:, 4])[0, 1]), 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "0.99995. Prácticamente la misma columna. En el capítulo 8 te\n",
    "dije que el caso feo no era la copia exacta sino la casi copia, porque no\n",
    "revienta. Acá está."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Sin regularizar"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def ridge(alpha):\n",
    "    return np.linalg.solve(Z.T @ Z + alpha * np.eye(dd), Z.T @ yc)\n",
    "\n",
    "for al in (0.0, 1.0, 10.0, 100.0):\n",
    "    print('alpha', str(al).rjust(6), np.round(ridge(al), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mira la primera fila. El monto tiene coeficiente **-565** y su\n",
    "copia casi idéntica tiene **+1044**.\n",
    "\n",
    "Eso, leído en una reunión, dice \"cuanto más factura una venta, menos factura\".\n",
    "Es un disparate, y sin embargo el modelo predice bien: las dos columnas se\n",
    "cancelan entre sí. El modelo encontró una solución que funciona y no significa\n",
    "nada 🙃\n",
    "\n",
    "Y ahora mira cómo con alpha 1 se convierten en 174 y 305. Los dos positivos,\n",
    "los dos razonables, y sumando parecido a lo que aporta la plata."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "EL2(𝐰)=‖𝐲−X𝐰‖22+α‖𝐰‖22\n",
    "\n",
    "ridge suma al error el cuadrado del largo de los pesos, así que pesos grandes salen caros y el modelo prefiere repartirlos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Le sumas al error el largo de los pesos al cuadrado. A partir de ahí, una\n",
    "solución con pesos de 565 y 1044 **cuesta cara**, así que el modelo\n",
    "prefiere repartir."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for al in (0.0, 1.0, 10.0, 100.0):\n",
    "    print('alpha', str(al).rjust(6), 'norma de los pesos:',\n",
    "          round(float(np.linalg.norm(ridge(al))), 3))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De 1188 a 356 con alpha 1. Eso es lo que hace ridge y no hace nada más:\n",
    "**acortar el vector de pesos**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y fíjate dónde está el alpha"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "𝐰=(X⊤X+αI)−1X⊤𝐲\n",
    "\n",
    "la solución de ridge es la de siempre con alfa sumado en la diagonal, y ese sumando es justo lo que arregla que la matriz no se pueda invertir"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Es la fórmula del capítulo 6 con un\n",
    "αI sumado en la diagonal.\n",
    "\n",
    "Y eso no es un detalle de implementación: es **exactamente** lo\n",
    "que arregla el problema. Ahí donde el determinante estaba pegado a cero y la\n",
    "inversa devolvía basura, sumarle algo a la diagonal lo separa del borde. Por eso\n",
    "ridge funciona con matrices que sin él no se pueden invertir 🌟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ahora la otra: lasso"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "EL1(𝐰)=‖𝐲−X𝐰‖22+α‖𝐰‖1\n",
    "\n",
    "lasso suma la norma L1, que es la suma de valores absolutos, y esa forma es la que hace que algunos pesos acaben exactamente en cero"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def blando(x, t):\n",
    "    return np.sign(x) * np.maximum(np.abs(x) - t, 0.0)\n",
    "\n",
    "def lasso(alpha, iters=3000, tasa=0.001):\n",
    "    w = np.zeros(dd)\n",
    "    for _ in range(iters):\n",
    "        g = -2 * (Z.T @ (yc - Z @ w)) / n\n",
    "        w = blando(w - tasa * g, tasa * alpha)\n",
    "    return w\n",
    "\n",
    "for al in (0.0, 1.0, 10.0, 100.0):\n",
    "    w = lasso(al)\n",
    "    print('alpha', str(al).rjust(6), np.round(w, 3),\n",
    "          '| ceros:', int((np.abs(w) < 1e-8).sum()))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Ceros exactos.** No 0.0001: cero pelado. Con alpha 10 las\n",
    "unidades desaparecen, y con alpha 100 también el descuento.\n",
    "\n",
    "Eso ridge no lo hace nunca. Encoge y encoge y encoge, y no llega. Y la\n",
    "diferencia está en la geometría."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Por qué L1 hace ceros y L2 no"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Acuérdate de las dos normas del capítulo 7. Piensa en el\n",
    "conjunto de todos los pesos que tienen norma 1, con dos pesos para poder\n",
    "imaginarlo:\n",
    "\n",
    "- Con la **L2**, ese conjunto es una **circunferencia**.\n",
    "Cuadrados y raíz: lisa por todos lados.\n",
    "\n",
    "- Con la **L1**, es un **rombo** con las puntas\n",
    "justo sobre los ejes. Suma de valores absolutos, que es lo que hace las\n",
    "esquinas.\n",
    "\n",
    "Ahora: regularizar es encontrar el punto donde el error es más chico\n",
    "**tocando esa figura**. Y una figura con esquinas se toca por la\n",
    "esquina, porque las esquinas sobresalen.\n",
    "\n",
    "Estar en una esquina del rombo quiere decir que **uno de los pesos vale\n",
    "cero**, porque las puntas están sobre los ejes. En la circunferencia no\n",
    "hay esquinas, así que el punto de contacto cae en cualquier sitio y ninguna\n",
    "coordenada tiene motivo para ser cero exacto.\n",
    "\n",
    "Ahí está toda la diferencia. No es que lasso sea más agresivo: es que su\n",
    "figura tiene puntas 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuál usar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "|  | Ridge, L2 | Lasso, L1 |\n",
    "|---|---|---|\n",
    "| Qué hace | encoge todos | pone algunos en cero |\n",
    "| Con columnas parecidas | reparte entre las dos | elige una y tira la otra |\n",
    "| Cuándo | todas aportan un poco | sospechas que muchas sobran |\n",
    "| Solución | fórmula cerrada | hay que iterar |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y las dos a la vez existe, se llama elastic net, y es lo que se usa cuando no\n",
    "sabes cuál de las dos historias es la tuya.\n",
    "\n",
    "Una advertencia sobre lasso con columnas casi iguales: **elige una y no\n",
    "te dice por qué**. Cambia una fila y puede elegir la otra. Si vas a\n",
    "presentar \"estas son las cinco variables que importan\", saber eso te ahorra un\n",
    "mal rato."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- Regularizar es sumarle al error un castigo por pesos grandes.\n",
    "\n",
    "- Sin regularizar, dos columnas parecidas dan coeficientes enormes y de\n",
    "signos contrarios que se cancelan.\n",
    "\n",
    "- Ridge usa L2 y encoge todos; su fórmula es la de siempre con alpha en la\n",
    "diagonal, y por eso arregla la inversa.\n",
    "\n",
    "- Lasso usa L1 y pone ceros exactos.\n",
    "\n",
    "- La razón es geométrica: el rombo de la L1 tiene puntas sobre los ejes.\n",
    "\n",
    "- Lasso elige una entre columnas parecidas y no te dice por qué."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Sin regularizar, dos columnas casi idénticas te dan coeficientes -565 y +1044. Con ridge y alpha=1 se convierten en 174 y 305. ¿Qué pasó?\n",
    "\n",
    "a) El modelo dejó de compensar una columna con la otra, porque ahora los pesos grandes cuestan\n",
    "\n",
    "b) Ridge eliminó una de las dos columnas\n",
    "\n",
    "c) Ridge corrigió los datos, que estaban mal\n",
    "\n",
    "d) El modelo predice peor, y por eso los coeficientes son más chicos\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Ridge no elimina nada: las dos siguen ahí con su coeficiente. Quien pone ceros exactos es lasso, y por su forma, no por ser más agresivo.\n",
    "\n",
    "*c)* Los datos son los mismos. Lo que cambió es qué se considera una solución buena: ahora una con pesos enormes paga un precio.\n",
    "\n",
    "*d)* Predice casi igual, y esa es justo la trampa: sin regularizar los coeficientes eran basura interpretable y el error era parecido.\n",
    "\n",
    "Ridge no arregla la predicción: arregla que los coeficientes se puedan leer. Y en consultoría eso suele valer más 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Predice igual de bien, y eso es lo grave\n",
    "\n",
    "Compara el error de los coeficientes locos contra los\n",
    "sanos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def ridge(alpha):\n",
    "    return np.linalg.solve(Z.T @ Z + alpha * np.eye(dd), Z.T @ yc)\n",
    "\n",
    "for al in (0.0, 1.0, 100.0):\n",
    "    w = ridge(al)\n",
    "    rmse = float(np.sqrt(((yc - Z @ w) ** 2).mean()))\n",
    "    print('alpha', str(al).rjust(6), '| RMSE', round(rmse, 2),\n",
    "          '| peso mayor', round(float(np.abs(w).max()), 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "alpha    0.0 | RMSE 433.68 | peso mayor 1043.9\n",
    "alpha    1.0 | RMSE 433.74 | peso mayor 304.7\n",
    "alpha  100.0 | RMSE 433.96 | peso mayor 233.4\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "El RMSE apenas se mueve: 433.68 contra 433.96. Si solo miraras la métrica,\n",
    "los tres modelos son el mismo y el primero gana por un pelo. Lo que cambia\n",
    "brutalmente es si los coeficientes se pueden leer. Por eso la regularización no\n",
    "se elige mirando solo el error."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. El alpha que arregla la inversa\n",
    "\n",
    "Mira el número de condición según el alpha."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "for al in (0.0, 0.01, 1.0, 100.0):\n",
    "    M = Z.T @ Z + al * np.eye(dd)\n",
    "    print('alpha', str(al).rjust(6), '| condicion',\n",
    "          round(float(np.linalg.cond(M)), 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "alpha    0.0 | condicion 40462.2\n",
    "alpha   0.01 | condicion 36364.7\n",
    "alpha    1.0 | condicion 3299.0\n",
    "alpha  100.0 | condicion 36.9\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "De 40.462 a 36.9. Ese número es el mismo del capítulo 8 y el mismo del capítulo 16: dice\n",
    "cuánto se amplifica el error de redondeo. Ridge lo baja a golpe de diagonal, y\n",
    "de paso deja el problema mucho mejor condicionado para entrenar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Sigue el camino de lasso\n",
    "\n",
    "Sube el alpha poco a poco y mira caer las columnas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def blando(x, t):\n",
    "    return np.sign(x) * np.maximum(np.abs(x) - t, 0.0)\n",
    "\n",
    "def lasso(alpha, iters=3000, tasa=0.001):\n",
    "    w = np.zeros(dd)\n",
    "    for _ in range(iters):\n",
    "        g = -2 * (Z.T @ (yc - Z @ w)) / n\n",
    "        w = blando(w - tasa * g, tasa * alpha)\n",
    "    return w\n",
    "\n",
    "NOMBRES = ['unidades', 'monto', 'descuento', 'satisfaccion',\n",
    "           'monto_casi_igual']\n",
    "for al in (1.0, 5.0, 20.0, 60.0, 200.0):\n",
    "    w = lasso(al)\n",
    "    vivas = [NOMBRES[i] for i in range(dd) if abs(w[i]) > 1e-8]\n",
    "    print('alpha', str(al).rjust(6), '| quedan', len(vivas), ':', vivas)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "alpha    1.0 | quedan 5 : ['unidades', 'monto', 'descuento', 'satisfaccion', 'monto_casi_igual']\n",
    "alpha    5.0 | quedan 4 : ['monto', 'descuento', 'satisfaccion', 'monto_casi_igual']\n",
    "alpha   20.0 | quedan 4 : ['monto', 'descuento', 'satisfaccion', 'monto_casi_igual']\n",
    "alpha   60.0 | quedan 3 : ['monto', 'satisfaccion', 'monto_casi_igual']\n",
    "alpha  200.0 | quedan 2 : ['monto', 'monto_casi_igual']\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Las columnas se van cayendo de menos a más útiles, y eso se llama el camino\n",
    "de lasso. Las unidades caen primero, después el descuento, después la\n",
    "satisfacción.\n",
    "\n",
    "Y ahora mira la última línea, que es la que quiero que te lleves: las dos que\n",
    "sobreviven son `monto` y `monto_casi_igual`, o sea\n",
    "**la misma columna dos veces**. Lasso no las desempató. Se supone\n",
    "que L1 elige una entre columnas parecidas, y con una correlación de 0.99995\n",
    "prefirió quedarse con las dos y repartirse el peso.\n",
    "\n",
    "Si tú hubieras usado esta lista para decir \"estas son las variables que\n",
    "importan\", habrías presentado la misma variable dos veces. El alpha se elige con\n",
    "validación cruzada, y eso está en mi libro de machine learning, pero la lista de\n",
    "variables que sale de lasso siempre hay que mirarla con los ojos abiertos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 19 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/regularizacion-l1-y-l2/\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
}
