{
 "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 soluciones 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",
    "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 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\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* No lo son: 55.97 contra 1.7482. Están relacionados, pero hay un cuadrado y una división en medio.\n",
    "\n",
    "*c)* Sí la tienen, y es exacta. La SVD de los datos y los autovalores de su covarianza son dos formas de calcular lo mismo.\n",
    "\n",
    "*d)* Casi. Falta la división entre n menos uno, que es la que convierte una suma de cuadrados en una varianza.\n",
    "\n",
    "S al cuadrado entre n menos uno da los autovalores, clavados. Por eso las librerías serias calculan PCA con SVD y no con eigh 🐣"
   ]
  },
  {
   "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": [
    "U, S, Vt = np.linalg.svd(Z, full_matrices=False)\n",
    "print('S                :', np.round(S, 4))\n",
    "print('ya venia ordenado:', bool(np.all(np.diff(S) <= 0)))\n",
    "print('todos positivos  :', bool(np.all(S > 0)))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "S                : [55.9718 43.1322 42.2936 41.5137 21.4392]\n",
    "ya venia ordenado: True\n",
    "todos positivos  : True\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con `eigh` había que ordenar a mano. La SVD te los da de mayor a\n",
    "menor y sin signos raros. Es una de las razones por las que se usa más."
   ]
  },
  {
   "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": [
    "U, S, Vt = np.linalg.svd(Z, full_matrices=False)\n",
    "for k in range(1, 6):\n",
    "    Zk = U[:, :k] @ np.diag(S[:k]) @ Vt[:k]\n",
    "    print(k, '-> error', round(float(np.abs(Z - Zk).mean()), 4),\n",
    "          '| guarda', U[:, :k].size + k + Vt[:k].size, 'numeros')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "1 -> error 0.5975 | guarda 1799 numeros\n",
    "2 -> error 0.4905 | guarda 3598 numeros\n",
    "3 -> error 0.3578 | guarda 5397 numeros\n",
    "4 -> error 0.0966 | guarda 7196 numeros\n",
    "5 -> error 0.0 | guarda 8995 numeros\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con k igual a 5 guardas 8.995 números para representar una tabla de 8.965.\n",
    "Guardaste más de lo que tenías. Ese es el chiste de la SVD: solo compensa cuando\n",
    "puedes recortar de verdad, y aquí no se puede."
   ]
  },
  {
   "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": [
    "Zd = np.column_stack([Z, Z[:, 0] * 3])\n",
    "_, Sd, _ = np.linalg.svd(Zd, full_matrices=False)\n",
    "print('valores singulares:', np.round(Sd, 6))\n",
    "print('cuantos no son cero:', int((Sd > 1e-10).sum()))\n",
    "print('matrix_rank dice   :', np.linalg.matrix_rank(Zd))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "valores singulares: [133.913495  55.971759  42.440309  42.136853  21.439308   0.      ]\n",
    "cuantos no son cero: 5\n",
    "matrix_rank dice   : 5\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ese cero del final es la columna que sobra. Y así es exactamente como\n",
    "`matrix_rank` hace su trabajo: cuenta los valores singulares que no\n",
    "son prácticamente cero. No calcula determinantes ni nada de eso, hace una SVD y\n",
    "cuenta. Por eso es fiable donde el determinante del capítulo 8 no lo era."
   ]
  },
  {
   "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
}
