{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Qué es PCA, y ya lo usaste\n",
    "\n",
    "Análisis de componentes principales explicado como lo que es: mirar la misma tabla desde las direcciones que los autovectores encontraron.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 11 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-pca/\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": [
    "## PCA no es un algoritmo nuevo 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Si hiciste el capítulo 10, PCA ya está hecho. Lo único\n",
    "que falta es multiplicar y leer.\n",
    "\n",
    "Y quiero empezar por lo que PCA **no** es, porque hay dos ideas\n",
    "sueltas por ahí que hacen daño. No es \"quitar columnas\": ninguna columna se\n",
    "elimina, todas participan en todas las componentes. Y no es \"quedarse con lo\n",
    "importante\": lo que se mide es dónde hay **variación**, y variar\n",
    "mucho no es lo mismo que importar."
   ]
  },
  {
   "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 direcciones, otra vez"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "C = np.cov(Z, rowvar=False)\n",
    "vals, vecs = np.linalg.eigh(C)\n",
    "orden = np.argsort(vals)[::-1]\n",
    "vals, vecs = vals[orden], vecs[:, orden]\n",
    "print('autovalores:', np.round(vals, 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Y PCA es una multiplicación"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "P = Z @ vecs\n",
    "print('P.shape:', P.shape)\n",
    "print('varianza de cada componente:', np.round(P.var(axis=0, ddof=1), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "P=ZV\n",
    "\n",
    "las componentes principales salen de multiplicar tus datos escalados por la matriz de autovectores, o sea de mirar la misma tabla desde otras direcciones"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Ahí está todo el truco. Las varianzas de las componentes **son**\n",
    "los autovalores, clavados. No se parecen: son los mismos números.\n",
    "\n",
    "Eso es lo que quería decir \"los autovalores reparten la varianza\". Cada\n",
    "dirección se lleva un trozo, y el autovalor es el trozo.\n",
    "\n",
    "Fíjate también en la forma: sigue siendo 1.793 por 5. **No se perdió\n",
    "nada**. Es la misma tabla girada, no una tabla recortada. El recorte\n",
    "viene después y es una decisión aparte."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuánto se lleva cada una"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('proporcion:', np.round(vals / vals.sum(), 4))\n",
    "print('acumulada :', np.round(np.cumsum(vals / vals.sum()), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "proporción explicadai=λiλ1+…+λd\n",
    "\n",
    "qué porcentaje de la variación total se lleva cada dirección es su autovalor dividido entre la suma de todos"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y acá te tengo que dar una noticia mala sobre mis propios datos: **este\n",
    "PCA no sirve de mucho**.\n",
    "\n",
    "Mira la primera componente: 34.95%. Y para llegar al 95% hacen falta cuatro\n",
    "de las cinco. O sea que puedo tirar una columna y poco más.\n",
    "\n",
    "Cuando en un curso te enseñan PCA, el ejemplo siempre tiene la primera\n",
    "componente en 70% u 80%, porque eligieron un dataset donde funciona. En el mío\n",
    "las columnas son bastante independientes entre sí, así que no hay mucho que\n",
    "comprimir.\n",
    "\n",
    "Eso también es un resultado. \"PCA no encontró estructura\" quiere decir \"tus\n",
    "columnas ya estaban aportando cosas distintas\", y eso es una buena noticia para\n",
    "un modelo, aunque sea una mala noticia para el gráfico bonito 🙂"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que sí encontró"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "COLS = ['unidades', 'monto', 'descuento', 'satisfaccion',\n",
    "        'monto_final_facturado']\n",
    "for nombre, peso in zip(COLS, vecs[:, 0]):\n",
    "    print(f'  {nombre:24} {peso:8.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "La componente 1 es **plata**. Monto y monto final facturado con\n",
    "casi el mismo peso, y lo demás en cero.\n",
    "\n",
    "O sea que si tuvieras que resumir cada venta en un solo número, ese número\n",
    "sería \"cuánto factura\". Suena obvio dicho así, y el asunto es que nadie se lo\n",
    "dijo: lo encontró la matriz mirando 1.793 filas.\n",
    "\n",
    "Esa es la utilidad honesta de PCA la mayoría de las veces: **no\n",
    "comprimir, sino entender qué se mueve junto**."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Recortar, que es lo que la gente quiere"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Z2 = P[:, :2] @ vecs[:, :2].T\n",
    "Z4 = P[:, :4] @ vecs[:, :4].T\n",
    "print('error medio con 2 componentes:', round(float(np.abs(Z - Z2).mean()), 4))\n",
    "print('error medio con 4 componentes:', round(float(np.abs(Z - Z4).mean()), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con dos componentes reconstruyo la tabla con un error medio de 0.49 sobre\n",
    "datos que tienen desviación 1. O sea que me equivoco casi media desviación en\n",
    "cada casilla, que es muchísimo.\n",
    "\n",
    "Con cuatro bajo a 0.0966, que ya es razonable. Otra vez lo mismo: acá se puede\n",
    "tirar una columna, no cuatro."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Tres cosas que hay que decir y casi nunca se dicen"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Uno: escalar no es opcional.** Si no escalas, la columna con\n",
    "los números más grandes se lleva la primera componente entera, y no porque\n",
    "importe más. Es el mismo problema del capítulo 7.\n",
    "\n",
    "**Dos: las componentes no se explican.** \"Componente 1\" no es una\n",
    "variable de negocio. Puedes leer sus pesos y ponerle nombre, como hice arriba con\n",
    "\"plata\", pero si le entregas a alguien un modelo entrenado sobre componentes, no\n",
    "vas a poder decirle qué significa ninguna.\n",
    "\n",
    "**Tres: PCA no sabe qué quieres predecir.** Busca dónde hay\n",
    "variación, no dónde hay respuesta. Puede tirar tranquilamente la componente que\n",
    "menos varía y resulta que era justo la que separaba a tus clientes buenos de los\n",
    "malos. Pasa, y cuando pasa no avisa 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- PCA es multiplicar tus datos escalados por los autovectores.\n",
    "\n",
    "- La varianza de cada componente es su autovalor.\n",
    "\n",
    "- PCA gira la tabla, no la recorta. Recortar es una decisión aparte.\n",
    "\n",
    "- Si la varianza se reparte parejo, no hay nada que comprimir, y eso es un\n",
    "resultado.\n",
    "\n",
    "- Sin escalar, PCA solo encuentra la columna más grande.\n",
    "\n",
    "- PCA no sabe qué quieres predecir."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Corres PCA sobre 5 columnas y la varianza explicada acumulada es [0.35, 0.56, 0.76, 0.95, 1.00]. ¿Cuántas componentes te quedas?\n",
    "\n",
    "a) Depende de para qué, y con estos números ninguna opción es buena: hacen falta 4 para llegar al 95%\n",
    "\n",
    "b) Una, porque la primera es la más importante\n",
    "\n",
    "c) Dos, porque es la regla habitual para poder dibujarlo\n",
    "\n",
    "d) Cinco, porque hay que quedarse con el 100%\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* Una sola te deja con el 35%, o sea que tiras dos tercios de la variación. Que sea la más importante no quiere decir que sea suficiente.\n",
    "\n",
    "*c)* Dos sirve para dibujar y ya, y aquí te quedas en 56%. Vale para mirar, no para alimentar un modelo.\n",
    "\n",
    "*d)* Quedarte con todas es no hacer PCA. Si las cinco hacen falta, la respuesta honesta es que ahí no había nada que comprimir.\n",
    "\n",
    "Cuando la varianza se reparte parejo entre las componentes, PCA te está diciendo que tus columnas ya eran independientes. Eso es un resultado, no un fracaso 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Las componentes salen sin correlación entre ellas\n",
    "\n",
    "Esa es la propiedad que hace que PCA sirva de algo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "C = np.cov(Z, rowvar=False)\n",
    "vals, vecs = np.linalg.eigh(C)\n",
    "P = Z @ vecs\n",
    "print(np.round(np.corrcoef(P, rowvar=False), 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "[[ 1.  0.  0.  0. -0.]\n",
    " [ 0.  1.  0. -0.  0.]\n",
    " [ 0.  0.  1.  0.  0.]\n",
    " [ 0. -0.  0.  1.  0.]\n",
    " [-0.  0.  0.  0.  1.]]\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Unos en la diagonal y ceros fuera. Las componentes están descorrelacionadas\n",
    "por construcción, y viene de que los autovectores eran perpendiculares. Por eso\n",
    "PCA se usa a veces antes de una regresión: mata la multicolinealidad del capítulo 8 de un plumazo. El precio es que pierdes la\n",
    "interpretación de los coeficientes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. Sin escalar, PCA solo ve la columna grande\n",
    "\n",
    "Corre lo mismo sobre M sin escalar y compara."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "COLS = ['unidades', 'monto', 'descuento', 'satisfaccion',\n",
    "        'monto_final_facturado']\n",
    "Cm = np.cov(M - M.mean(axis=0), rowvar=False)\n",
    "vm, wm = np.linalg.eigh(Cm)\n",
    "o = np.argsort(vm)[::-1]\n",
    "vm, wm = vm[o], wm[:, o]\n",
    "print('proporcion sin escalar:', np.round(vm / vm.sum(), 4))\n",
    "for nombre, peso in zip(COLS, wm[:, 0]):\n",
    "    print(f'  {nombre:24} {peso:8.4f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "proporcion sin escalar: [0.8715 0.1285 0.     0.     0.    ]\n",
    "  unidades                  -0.0000\n",
    "  monto                     -0.7696\n",
    "  descuento                  0.0000\n",
    "  satisfaccion              -0.0000\n",
    "  monto_final_facturado     -0.6385\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "87% en la primera componente, y las tres ultimas en cero pelado. Parece un resultado espectacular y es una\n",
    "mentira: sale así porque el monto tiene números miles de veces más grandes que\n",
    "la satisfacción. Escalar no es un paso de limpieza, es parte del método."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Reconstruye componente por componente\n",
    "\n",
    "Mira cómo baja el error al ir sumando direcciones."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "C = np.cov(Z, rowvar=False)\n",
    "vals, vecs = np.linalg.eigh(C)\n",
    "orden = np.argsort(vals)[::-1]\n",
    "vals, vecs = vals[orden], vecs[:, orden]\n",
    "P = Z @ vecs\n",
    "\n",
    "for k in range(1, 6):\n",
    "    Zk = P[:, :k] @ vecs[:, :k].T\n",
    "    print(k, 'componentes -> error', round(float(np.abs(Z - Zk).mean()), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "1 componentes -> error 0.5975\n",
    "2 componentes -> error 0.4905\n",
    "3 componentes -> error 0.3578\n",
    "4 componentes -> error 0.0966\n",
    "5 componentes -> error 0.0\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Con las cinco el error es cero exacto, porque no tiraste nada. Y fíjate en\n",
    "que la bajada es bastante pareja hasta la cuarta: no hay ningún codo claro donde\n",
    "cortar. Ese gráfico se llama de codo, y cuando no hay codo la respuesta correcta\n",
    "es no recortar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 11 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-pca/\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
}
