{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Rango e independencia: por qué dos columnas parecidas rompen tu modelo\n",
    "\n",
    "Qué es el rango de una matriz, qué es la independencia lineal, y cómo detectar las columnas que sobran antes de que revienten algo.\n",
    "\n",
    "Cuaderno de soluciones del capítulo 8 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/rango-e-independencia/\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 bug que no da error 🌸"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "En el capítulo 6 rompimos la inversa a propósito pegando una\n",
    "columna que eran las unidades por dos. Salió `Singular matrix`.\n",
    "\n",
    "Ese caso es el amable, porque avisa. El que hace daño de verdad es el\n",
    "hermano callado: dos columnas que casi son la misma. Ahí no hay error, hay\n",
    "resultados que parecen bien y no lo están.\n",
    "\n",
    "Este capítulo es para detectarlo antes."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Volvemos a abrir el archivo"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cada capítulo arranca de cero, así que estas líneas se repiten. Son las\n",
    "mismas del capítulo 1 y las vas a ver en todos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "import pandas as pd\n",
    "import numpy as np\n",
    "\n",
    "df = pd.read_csv('ventas-miss-yera.csv')\n",
    "for c in ('unidades', 'monto', 'satisfaccion'):\n",
    "    df[c] = pd.to_numeric(df[c], errors='coerce')\n",
    "X = df[['unidades', 'monto', 'satisfaccion']].dropna().to_numpy()\n",
    "print('X.shape:', X.shape)"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Cuántas columnas aportan de verdad"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print('columnas:', X.shape[1], '| rango:', np.linalg.matrix_rank(X))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Tres columnas, rango tres. Todo bien: ninguna se puede fabricar con las\n",
    "otras. Se dice que son **linealmente independientes**.\n",
    "\n",
    "Ahora le pego una que sí se puede fabricar:"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Xd = np.column_stack([X, X[:, 0] * 2])\n",
    "Xs = np.column_stack([X, X[:, 0] + X[:, 2]])\n",
    "print('con unidades por 2       :', Xd.shape, 'rango', np.linalg.matrix_rank(Xd))\n",
    "print('con unidades+satisfaccion:', Xs.shape, 'rango', np.linalg.matrix_rank(Xs))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Cuatro columnas y rango tres, las dos veces. La tabla creció y la información\n",
    "no.\n",
    "\n",
    "Y mira el segundo caso, que es el interesante: la columna nueva no es un\n",
    "múltiplo de ninguna, es una **suma de dos**. Y aun así sobra."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "𝐯3=α𝐯1+β𝐯2\n",
    "\n",
    "una columna es combinación lineal de otras si se puede fabricar sumándolas y escalándolas, y entonces no trae información nueva"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "| Símbolo | Qué es |\n",
    "|---|---|\n",
    "| **v**3 | la columna que sobra |\n",
    "| **α, β** | dos números cualquiera, las letras alfa y beta |\n",
    "| **v**1, **v**2 | las columnas con las que se puede fabricar |"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Esa es la definición entera de **dependencia lineal**: se puede\n",
    "armar con las otras sumando y escalando, que son las dos operaciones del capítulo 3. El rango es simplemente cuántas quedan\n",
    "cuando quitas las que sobran."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Dónde te va a pasar de verdad"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Nadie pega a mano una columna por dos. Pero esto sí pasa, y pasa mucho:\n",
    "\n",
    "- **El mismo dato en dos unidades.** Precio en soles y precio en\n",
    "dólares con tipo de cambio fijo. Peso en kilos y en libras.\n",
    "\n",
    "- **Un total y sus partes.** Si tienes ventas de Lima, de\n",
    "provincia y el total, el total sobra.\n",
    "\n",
    "- **Dummies sin quitar una.** Si conviertes \"canal\" en tres\n",
    "columnas de ceros y unos, las tres suman siempre uno, y con eso ya sobra una.\n",
    "Por eso `pd.get_dummies` tiene `drop_first`. Es el error\n",
    "más común de los tres.\n",
    "\n",
    "- **Porcentajes que suman cien.** Misma historia."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## El daño, y por qué es callado"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "Xd = np.column_stack([X, X[:, 0] * 2])\n",
    "print('det de X.T @ X   :', round(float(np.linalg.det(X.T @ X)), 2))\n",
    "print('det con la de mas:', round(float(np.linalg.det(Xd.T @ Xd)), 6))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "**Esto revienta a propósito.** Se ejecuta dentro de un `try` para que puedas seguir con \"ejecutar todo\" y aun así ver la queja."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "try:\n",
    "    np.linalg.inv(Xd.T @ Xd)\n",
    "except Exception as e:\n",
    "    print(f'{type(e).__name__}: {e}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Y la queja que tiene que salir es esta:\n",
    "\n",
    "```\n",
    "LinAlgError: Singular matrix\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Determinante cero, no hay inversa, y las ecuaciones normales del capítulo 6 no se pueden resolver. Ese es el caso limpio.\n",
    "\n",
    "El sucio es cuando el determinante no es cero pero está cerca. Ahí\n",
    "`inv` funciona, te devuelve pesos, y esos pesos son basura: enormes,\n",
    "con signos que no tienen sentido, y que cambian del todo si añades una fila.\n",
    "Eso se llama **multicolinealidad**, y es la razón número uno por la\n",
    "que alguien mira los coeficientes de su regresión y dice \"esto no puede ser\".\n",
    "\n",
    "El modelo sigue prediciendo parecido. Lo que se rompe es la interpretación, y\n",
    "si tú vendes la interpretación, se rompe tu trabajo 🙃"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## La correlación no es lo mismo, y hay que decirlo"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "print(np.round(np.corrcoef(X, rowvar=False), 4))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Mis tres columnas están prácticamente sin correlación entre sí, y el rango es\n",
    "tres. Todo coherente.\n",
    "\n",
    "Pero cuidado con la tentación de usar la correlación como detector: la\n",
    "correlación mira las columnas **de dos en dos**. Si una columna es\n",
    "la suma de otras tres, puede tener correlación bajita con cada una por separado\n",
    "y aun así sobrar. El rango lo ve y la correlación no.\n",
    "\n",
    "Por eso: la correlación para mirar, el rango para decidir."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Lo que te llevas"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "- El rango es cuántas columnas aportan información propia.\n",
    "\n",
    "- Columnas menos rango = columnas que sobran.\n",
    "\n",
    "- Dependencia lineal es \"se puede fabricar sumando y escalando las otras\".\n",
    "\n",
    "- Te va a pasar con dummies sin `drop_first`, con totales junto a\n",
    "sus partes y con el mismo dato en dos unidades.\n",
    "\n",
    "- Dependencia exacta revienta; casi dependencia no revienta y arruina la\n",
    "interpretación.\n",
    "\n",
    "- La correlación mira de dos en dos, el rango mira todo junto."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Comprueba que se entendió"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Comprueba que lo tienes\n",
    "\n",
    "Tu tabla tiene 8 columnas y np.linalg.matrix_rank te devuelve 6. ¿Qué quiere decir?\n",
    "\n",
    "a) Que dos columnas se pueden fabricar con las otras seis\n",
    "\n",
    "b) Que hay 6 filas útiles y las demás están repetidas\n",
    "\n",
    "c) Que faltan datos en dos columnas\n",
    "\n",
    "d) Que seis columnas están correlacionadas entre sí\n",
    "\n",
    "---\n",
    "\n",
    "**La correcta es la a.**\n",
    "\n",
    "*b)* El rango de una tabla con muchas más filas que columnas está limitado por las columnas, no por las filas. Aquí habla de columnas.\n",
    "\n",
    "*c)* Los huecos dan nan, y con nan el rango ni se calcula. Esto es un problema de estructura, no de datos que faltan.\n",
    "\n",
    "*d)* Correlación alta y dependencia exacta no son lo mismo. El rango solo baja con la dependencia exacta; la correlación alta hace daño de otra manera, sin que el rango se entere.\n",
    "\n",
    "Rango 6 con 8 columnas quiere decir que dos sobran. Y si eso te pasa con dummies, casi seguro es que no pusiste drop_first 🐣"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Ejercicios"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 1. Cuántas sobran, en una línea\n",
    "\n",
    "El chequeo que deberías correr antes de cada regresión."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "def cuantas_sobran(M):\n",
    "    return M.shape[1] - np.linalg.matrix_rank(M)\n",
    "\n",
    "Xs = np.column_stack([X, X[:, 0] + X[:, 2]])\n",
    "print('X   :', cuantas_sobran(X))\n",
    "print('Xs  :', cuantas_sobran(Xs))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "X   : 0\n",
    "Xs  : 1\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Dos líneas y te ahorras una tarde de mirar coeficientes raros. Ponlo en tu\n",
    "código de preparación de datos, antes de entrenar nada."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 2. El error de las dummies, hecho a propósito\n",
    "\n",
    "Convierte una columna de texto en dummies y mira el rango."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "D = pd.get_dummies(df['canal'], dtype=float).to_numpy()\n",
    "print('solo dummies   :', D.shape[1], 'columnas, rango', np.linalg.matrix_rank(D))\n",
    "print('cada fila suma :', np.unique(D.sum(axis=1)))\n",
    "\n",
    "unos = np.ones((D.shape[0], 1))\n",
    "M = np.hstack([unos, D])\n",
    "print('con intercepto :', M.shape[1], 'columnas, rango', np.linalg.matrix_rank(M))\n",
    "\n",
    "Md = np.hstack([unos, D[:, 1:]])\n",
    "print('con drop_first :', Md.shape[1], 'columnas, rango', np.linalg.matrix_rank(Md))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "solo dummies   : 4 columnas, rango 4\n",
    "cada fila suma : [1.]\n",
    "con intercepto : 5 columnas, rango 4\n",
    "con drop_first : 4 columnas, rango 4\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Lee las tres con calma, porque acá hay un matiz que casi nadie cuenta bien.\n",
    "\n",
    "Las cuatro dummies **solas** tienen rango 4: son independientes.\n",
    "El problema aparece en la segunda línea, cuando entra la columna de unos: las\n",
    "cuatro dummies suman uno en cada fila, así que juntas fabrican esa columna de\n",
    "unos y ahora sobra una. Cinco columnas, rango 4.\n",
    "\n",
    "Y esa columna de unos no la pones tú: la pone el intercepto del modelo, sin\n",
    "avisar. Por eso la trampa de las variables dummy es tan escurridiza, y por eso\n",
    "`pd.get_dummies` trae `drop_first`. La tercera línea lo\n",
    "demuestra: quitando una, vuelve a cuadrar."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### 3. Casi dependiente, que es el caso feo\n",
    "\n",
    "Fabrica una columna casi igual a otra y mira que el rango\n",
    "no se entera."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "casi = X[:, 0] * 2 + np.linspace(0, 0.001, X.shape[0])\n",
    "Xc = np.column_stack([X, casi])\n",
    "print('rango           :', np.linalg.matrix_rank(Xc))\n",
    "print('determinante    :', float(np.linalg.det(Xc.T @ Xc)))\n",
    "print('numero de condicion:', round(float(np.linalg.cond(Xc.T @ Xc)), 1))"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "```\n",
    "rango           : 4\n",
    "determinante    : 1122514662638217.2\n",
    "numero de condicion: 57014881775575.3\n",
    "```"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "Rango 4, o sea que para el rango está todo bien. Y sin embargo el número de\n",
    "condición tiene doce cifras, que es la señal de alarma de verdad: mide cuánto se\n",
    "amplifica cualquier error de redondeo al resolver. Por encima de mil ya\n",
    "conviene mirar; con esto, los coeficientes no valen nada. El rango detecta lo\n",
    "exacto, `np.linalg.cond` detecta lo casi."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "---\n",
    "\n",
    "Ese era el capítulo 8 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/rango-e-independencia/\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
}
