{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Qué es SVD, la que sostiene medio machine learning\n",
    "\n",
    "La descomposición en valores singulares explicada desde PCA, por qué funciona con matrices que no son cuadradas y para qué se usa de verdad.\n",
    "\n",
    "Cuaderno de práctica del capítulo 12 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/que-es-svd/\n",
    "\n",
    "Los ejercicios están al final y traen una celda vacía debajo de cada uno. Las\n",
    "respuestas viven en el cuaderno de soluciones, y merece la pena pelearse un\n",
    "rato antes de abrirlo 💛"
   ]
  },
  {
   "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": [
    "## Antes de empezar\n",
    "\n",
    "Esta celda baja el ayudante que corrige tus ejercicios. Después, en cada\n",
    "ejercicio que se pueda corregir solo, vas a ver `%%revisa` arriba de la celda:\n",
    "escribe tu respuesta debajo, ejecuta, y te digo si te salió 💛"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import urllib.request\n",
    "\n",
    "# El ayudante de los cuadernos. Trae la corrección de los ejercicios y, en los\n",
    "# capítulos de consola, la celda mágica que ejecuta los comandos. Se baja en\n",
    "# vez de venir pegado aquí para que siempre sea el último.\n",
    "urllib.request.urlretrieve(\n",
    "    \"https://missyera.com/static/cuadernos/revisa.py\", \"revisa.py\")\n",
    "import revisa\n",
    "revisa.carga({\n",
    "    1: \"UyAgICAgICAgICAgICAgICA6IFs1NS45NzE4IDQzLjEzMjIgNDIuMjkzNiA0MS41MTM3IDIxLjQzOTJdCnlhIHZlbmlhIG9yZGVuYWRvOiBUcnVlCnRvZG9zIHBvc2l0aXZvcyAgOiBUcnVl\",\n",
    "    2: \"MSAtPiBlcnJvciAwLjU5NzUgfCBndWFyZGEgMTc5OSBudW1lcm9zCjIgLT4gZXJyb3IgMC40OTA1IHwgZ3VhcmRhIDM1OTggbnVtZXJvcwozIC0+IGVycm9yIDAuMzU3OCB8IGd1YXJkYSA1Mzk3IG51bWVyb3MKNCAtPiBlcnJvciAwLjA5NjYgfCBndWFyZGEgNzE5NiBudW1lcm9zCjUgLT4gZXJyb3IgMC4wIHwgZ3VhcmRhIDg5OTUgbnVtZXJvcw==\",\n",
    "    3: \"dmFsb3JlcyBzaW5ndWxhcmVzOiBbMTMzLjkxMzQ5NSAgNTUuOTcxNzU5ICA0Mi40NDAzMDkgIDQyLjEzNjg1MyAgMjEuNDM5MzA4ICAgMC4gICAgICBdCmN1YW50b3Mgbm8gc29uIGNlcm86IDUKbWF0cml4X3JhbmsgZGljZSAgIDogNQ==\",\n",
    "}, lenguaje=\"python\")"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El problema que arregla 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los autovalores del capítulo 10 tienen una limitación\n",
    "grande: solo existen para matrices **cuadradas**. Y tu tabla de\n",
    "datos casi nunca lo es. La mía tiene 1.793 filas y 5 columnas.\n",
    "\n",
    "Por eso en el capítulo anterior no le sacamos los autovectores a la tabla:\n",
    "se los sacamos a su matriz de covarianza, que sí es cuadrada. Fue un rodeo.\n",
    "\n",
    "La SVD quita el rodeo. Funciona con cualquier matriz, del tamaño que sea."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Abrimos el archivo con más columnas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Este bloque necesita una tabla más ancha. Con las tres de siempre no hay nada\n",
    "que descomponer, porque están casi sin relación entre ellas. Así que meto dos\n",
    "más: el descuento y el monto final facturado."
   ]
  },
  {
   "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",
    "\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",
    "M = df[COLS].dropna().to_numpy()\n",
    "\n",
    "Z = (M - M.mean(axis=0)) / M.std(axis=0)\n",
    "print('M.shape:', M.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa `Z` es la tabla escalada: a cada columna le resté su promedio\n",
    "y la dividí por su desviación. Después de eso todas tienen media cero y\n",
    "desviación uno, o sea que ninguna manda por tener números más grandes. En el capítulo 7 viste por qué eso hace falta."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Las tres piezas"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "U, S, Vt = np.linalg.svd(Z, full_matrices=False)\n",
    "print('U ', U.shape)\n",
    "print('S ', S.shape)\n",
    "print('Vt', Vt.shape)\n",
    "print('valores singulares:', np.round(S, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Z=UΣV⊤\n",
    "\n",
    "cualquier matriz, sea del tamaño que sea, se parte en tres: un giro, un estirado por unos números en la diagonal, y otro giro"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Símbolo | Qué es |\n",
    "|---|---|\n",
    "| **Z** | tu tabla, del tamaño que sea |\n",
    "| **U** | un giro del lado de las filas |\n",
    "| **Σ** | la sigma mayúscula: los valores singulares en la diagonal, que son el estirado |\n",
    "| **V⊤** | un giro del lado de las columnas |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Girar, estirar, girar. Cualquier matriz del mundo se puede escribir así, y\n",
    "eso es lo que la hace tan útil.\n",
    "\n",
    "Fíjate en que `S` viene como un vector de 5 números y no como una\n",
    "matriz. NumPy te da solo la diagonal porque el resto son ceros y sería un\n",
    "desperdicio guardarlos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Que sí reconstruye"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(np.allclose(U @ np.diag(S) @ Vt, Z))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nada se perdió. Es la misma tabla escrita de otra manera."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y ahora la conexión con PCA"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "C = np.cov(Z, rowvar=False)\n",
    "vals = np.sort(np.linalg.eigh(C)[0])[::-1]\n",
    "print('S al cuadrado entre n-1:', np.round(S ** 2 / (Z.shape[0] - 1), 4))\n",
    "print('autovalores de C       :', np.round(vals, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los mismos números. No parecidos: los mismos.\n",
    "\n",
    "**SVD y PCA son la misma cosa calculada de dos formas.** Y las\n",
    "librerías de verdad usan SVD, porque hace las cuentas sobre tus datos directos\n",
    "sin tener que construir la covarianza primero, y esa construcción es justamente\n",
    "donde se pierde precisión."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "vals, vecs = np.linalg.eigh(C)\n",
    "orden = np.argsort(vals)[::-1]\n",
    "print('primera fila de Vt:', np.round(Vt[0], 4))\n",
    "print('primer autovector :', np.round(vecs[:, orden][:, 0], 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Los mismos números con el signo cambiado. Y eso **no es un error**:\n",
    "una dirección y su contraria son la misma dirección. Si un autovector apunta al\n",
    "noreste, el mismo con signo cambiado apunta al suroeste, y la recta que definen\n",
    "es idéntica.\n",
    "\n",
    "Te lo digo porque va a pasar: corres el mismo PCA dos veces, o en dos\n",
    "máquinas, y los signos salen cambiados. No te has vuelto loca 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Para qué se usa: quedarse con lo grande"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "k = 2\n",
    "Zk = U[:, :k] @ np.diag(S[:k]) @ Vt[:k]\n",
    "print('rango de Z :', np.linalg.matrix_rank(Z))\n",
    "print('rango de Zk:', np.linalg.matrix_rank(Zk))\n",
    "print('error medio:', round(float(np.abs(Z - Zk).mean()), 4))\n",
    "print('numeros de la tabla entera:', Z.size)\n",
    "print('numeros que hacen falta   :', U[:, :k].size + k + Vt[:k].size)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Me quedé con los dos valores singulares más grandes y tiré los tres chicos.\n",
    "El resultado es una tabla de rango 2 que se guarda con 3.598 números en vez de\n",
    "8.965.\n",
    "\n",
    "Eso se llama **aproximación de rango bajo**, y hay un teorema que\n",
    "dice que de todas las matrices de rango 2 que existen, esta es la más parecida a\n",
    "la original. No es una heurística: es lo mejor que se puede hacer.\n",
    "\n",
    "Con mis datos el error es 0.49 y no compensa, por lo mismo de siempre: acá no\n",
    "había redundancia que exprimir. Pero cámbiale los datos y la historia\n",
    "cambia."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Dónde la vas a encontrar"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- **Sistemas de recomendación.** La tabla de usuarios por\n",
    "productos está casi vacía y es enorme. Quedarse con los k valores singulares\n",
    "grandes es lo que hay detrás de \"a quien vio esto también le gustó aquello\".\n",
    "\n",
    "- **Compresión de imágenes.** Una foto es una matriz de píxeles.\n",
    "Con el 10% de los valores singulares se ve casi igual.\n",
    "\n",
    "- **Buscar temas en textos.** Palabras por documentos, misma\n",
    "receta.\n",
    "\n",
    "- **La pseudoinversa.** Cuando no hay inversa porque la matriz es\n",
    "singular, como en el capítulo 6, la SVD da lo más parecido que\n",
    "existe.\n",
    "\n",
    "En todos los casos la idea es la misma: **casi toda la información está\n",
    "en unas pocas direcciones**, y las demás son ruido 🌟"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- La SVD parte cualquier matriz en giro, estirado y giro.\n",
    "\n",
    "- Funciona con matrices que no son cuadradas, y los autovalores no.\n",
    "\n",
    "- S al cuadrado entre n menos uno son los autovalores de la covarianza.\n",
    "\n",
    "- Las librerías calculan PCA con SVD, y por eso.\n",
    "\n",
    "- Los signos pueden salir cambiados y da igual.\n",
    "\n",
    "- Quedarse con los k más grandes es la mejor aproximación de rango k que\n",
    "existe."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Haces la SVD de tu tabla escalada y los valores singulares son [55.97, 43.13, 42.29, 41.51, 21.44]. ¿Qué relación tienen con los autovalores de la matriz de covarianza?\n",
    "\n",
    "a) Cada autovalor es el valor singular al cuadrado, dividido entre n menos uno\n",
    "\n",
    "b) Son los mismos números\n",
    "\n",
    "c) No tienen relación, son descomposiciones distintas\n",
    "\n",
    "d) Los valores singulares son la raíz de los autovalores"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Los valores singulares vienen ordenados\n",
    "\n",
    "Y siempre son positivos, que es lo que los hace cómodos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 1\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Cuánto pierdes con cada recorte\n",
    "\n",
    "Ve tirando valores singulares y mira el daño."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 2\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. El rango, otra vez, ahora con valores singulares\n",
    "\n",
    "Cómo se calcula el rango por dentro."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "%%revisa 3\n",
    "# tu turno"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 12 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/que-es-svd/\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
}
