{
 "cells": [
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Práctica: Regresión Lineal con una Variable\n",
    "\n",
    "## 🎯 Objetivos del laboratorio\n",
    "\n",
    "En este notebook implementarás **regresión lineal univariable desde cero**. Al finalizar serás capaz de:\n",
    "\n",
    "- Implementar la función del modelo lineal $f_{w,b}(x) = wx + b$\n",
    "- Calcular la función de costo $J(w,b)$\n",
    "- Calcular el gradiente $\\frac{\\partial J}{\\partial w}$ y $\\frac{\\partial J}{\\partial b}$\n",
    "- Implementar el algoritmo de descenso de gradiente\n",
    "- Visualizar el proceso de entrenamiento y hacer predicciones\n",
    "\n",
    "## 📌 Indicaciones importantes\n",
    "\n",
    "- En cada celda con `### TU CÓDIGO AQUÍ ###` debes completar la implementación.\n",
    "- **No uses operaciones vectorizadas** (operaciones de NumPy sobre arreglos completos). Usa **ciclos `for` explícitos**.\n",
    "- Lee con cuidado las pistas y los comentarios antes de implementar.\n",
    "- Después de cada implementación hay una celda de prueba que valida tu código.\n",
    "- Si una celda falla, revisa tu código antes de continuar (las funciones siguientes dependen de las anteriores)."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 1. Configuración inicial\n",
    "\n",
    "Empezamos importando las librerías necesarias. Solo usaremos NumPy para almacenar los datos y para hacer las gráficas (no para vectorizar operaciones)."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 62,
   "metadata": {},
   "outputs": [],
   "source": [
    "import numpy as np\n",
    "import matplotlib.pyplot as plt\n",
    "import math\n",
    "\n",
    "# Configuración estética para las gráficas\n",
    "plt.rcParams['figure.figsize'] = (8, 5)\n",
    "plt.rcParams['font.size'] = 11"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 2. Planteamiento del problema\n",
    "\n",
    "Trabajaremos con un conjunto de datos sobre **años de experiencia laboral** y **salario mensual** (en miles de quetzales) de profesionales en tecnología.\n",
    "\n",
    "Nuestro objetivo es ajustar un modelo de regresión lineal que nos permita **predecir el salario esperado** dado los años de experiencia de un profesional.\n",
    "\n",
    "### Notación que usaremos\n",
    "\n",
    "| Símbolo | Descripción |\n",
    "|---------|-------------|\n",
    "| $x^{(i)}$ | Años de experiencia del ejemplo $i$ |\n",
    "| $y^{(i)}$ | Salario (en miles de Q) del ejemplo $i$ |\n",
    "| $m$ | Cantidad total de ejemplos |\n",
    "| $w$ | Pendiente del modelo (parámetro) |\n",
    "| $b$ | Intercepto del modelo (parámetro) |"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 63,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Cantidad de ejemplos de entrenamiento: m = 10\n",
      "\n",
      "Datos:\n",
      "  x^(0) =   1.0 años   →   y^(0) =  3.50 miles de Q\n",
      "  x^(1) =   2.0 años   →   y^(1) =  4.20 miles de Q\n",
      "  x^(2) =   3.0 años   →   y^(2) =  5.00 miles de Q\n",
      "  x^(3) =   4.0 años   →   y^(3) =  5.80 miles de Q\n",
      "  x^(4) =   5.0 años   →   y^(4) =  6.50 miles de Q\n",
      "  x^(5) =   6.0 años   →   y^(5) =  7.10 miles de Q\n",
      "  x^(6) =   7.0 años   →   y^(6) =  7.90 miles de Q\n",
      "  x^(7) =   8.0 años   →   y^(7) =  8.60 miles de Q\n",
      "  x^(8) =   9.0 años   →   y^(8) =  9.30 miles de Q\n",
      "  x^(9) =  10.0 años   →   y^(9) = 10.10 miles de Q\n"
     ]
    }
   ],
   "source": [
    "# Datos de entrenamiento\n",
    "# x_train: años de experiencia\n",
    "# y_train: salario en miles de quetzales\n",
    "\n",
    "x_train = np.array([1.0, 2.0, 3.0, 4.0, 5.0, 6.0, 7.0, 8.0, 9.0, 10.0])\n",
    "y_train = np.array([3.5, 4.2, 5.0, 5.8, 6.5, 7.1, 7.9, 8.6, 9.3, 10.1])\n",
    "\n",
    "m = len(x_train)\n",
    "print(f'Cantidad de ejemplos de entrenamiento: m = {m}')\n",
    "print('\\nDatos:')\n",
    "for i in range(m):\n",
    "    print(f'  x^({i}) = {x_train[i]:5.1f} años   →   y^({i}) = {y_train[i]:5.2f} miles de Q')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Visualización inicial de los datos\n",
    "\n",
    "Antes de modelar cualquier cosa, **siempre conviene mirar los datos**."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 64,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 34320 characters]",
      "text/plain": [
       "<Figure size 800x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "💭 ¿Se ve una relación lineal? ¿Tiene sentido modelar esto con una recta?\n"
     ]
    }
   ],
   "source": [
    "plt.scatter(x_train, y_train, marker='x', c='red', s=80, label='Datos reales')\n",
    "plt.title('Salario vs. Años de experiencia')\n",
    "plt.xlabel('Años de experiencia')\n",
    "plt.ylabel('Salario (miles de Q)')\n",
    "plt.legend()\n",
    "plt.grid(True, alpha=0.3)\n",
    "plt.show()\n",
    "\n",
    "print('💭 ¿Se ve una relación lineal? ¿Tiene sentido modelar esto con una recta?')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 3. Función del modelo\n",
    "\n",
    "El modelo de regresión lineal univariable se define como:\n",
    "\n",
    "$$f_{w,b}(x) = wx + b$$\n",
    "\n",
    "Donde:\n",
    "- $w$ es la **pendiente** (cuánto aumenta $y$ por cada unidad de $x$).\n",
    "- $b$ es el **intercepto** (valor de $y$ cuando $x = 0$).\n",
    "\n",
    "### ✏️ Ejercicio 1: Implementa `compute_model_output`\n",
    "\n",
    "Implementa la función que calcula las predicciones del modelo para **todos** los ejemplos de $x$.\n",
    "\n",
    "Para cada ejemplo $i$ debes calcular:\n",
    "\n",
    "$$f_{w,b}(x^{(i)}) = w \\cdot x^{(i)} + b$$\n",
    "\n",
    "🔁 **Recordatorio:** usa un ciclo `for`, no operaciones vectorizadas."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 65,
   "metadata": {},
   "outputs": [],
   "source": [
    "def compute_model_output(x, w, b):\n",
    "    \"\"\"\n",
    "    Calcula la predicción del modelo lineal para cada ejemplo.\n",
    "\n",
    "    Args:\n",
    "        x (ndarray): Arreglo con m ejemplos de entrada\n",
    "        w (float):   Parámetro del modelo (pendiente)\n",
    "        b (float):   Parámetro del modelo (intercepto)\n",
    "\n",
    "    Returns:\n",
    "        f_wb (ndarray): Predicciones del modelo para cada x[i]\n",
    "    \"\"\"\n",
    "    m = len(x)\n",
    "    f_wb = np.zeros(m)\n",
    "\n",
    "    ### TU CÓDIGO AQUÍ ### (≈ 2 líneas)\n",
    "    # Pista 1: itera sobre los m ejemplos con un ciclo for\n",
    "    # Pista 2: para cada i, calcula f_wb[i] = w * x[i] + b\n",
    "    for i in range(m):\n",
    "        f_wb[i] = w * x[i] + b\n",
    "\n",
    "\n",
    "    ### FIN DEL CÓDIGO ###\n",
    "\n",
    "    return f_wb"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 66,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predicciones obtenidas:\n",
      "  f_wb(1.0) = 3.70\n",
      "  f_wb(2.0) = 4.40\n",
      "  f_wb(3.0) = 5.10\n",
      "  f_wb(4.0) = 5.80\n",
      "  f_wb(5.0) = 6.50\n",
      "  f_wb(6.0) = 7.20\n",
      "  f_wb(7.0) = 7.90\n",
      "  f_wb(8.0) = 8.60\n",
      "  f_wb(9.0) = 9.30\n",
      "  f_wb(10.0) = 10.00\n",
      "\n",
      "✅ ¡Correcto! Tu función trabaja como se esperaba.\n"
     ]
    }
   ],
   "source": [
    "# 🧪 Probemos tu implementación con valores conocidos\n",
    "w_prueba = 0.7\n",
    "b_prueba = 3.0\n",
    "\n",
    "predicciones = compute_model_output(x_train, w_prueba, b_prueba)\n",
    "\n",
    "print('Predicciones obtenidas:')\n",
    "for i in range(m):\n",
    "    print(f'  f_wb({x_train[i]:.1f}) = {predicciones[i]:.2f}')\n",
    "\n",
    "# Validación automática\n",
    "esperado = np.array([3.7, 4.4, 5.1, 5.8, 6.5, 7.2, 7.9, 8.6, 9.3, 10.0])\n",
    "if np.allclose(predicciones, esperado):\n",
    "    print('\\n✅ ¡Correcto! Tu función trabaja como se esperaba.')\n",
    "else:\n",
    "    print('\\n❌ Las predicciones no coinciden con lo esperado. Revisa tu implementación.')"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 67,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 49132 characters]",
      "text/plain": [
       "<Figure size 800x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "🤔 ¿Crees que estos valores de w y b son los óptimos?\n",
      "Necesitamos una forma de medir qué tan bueno es el modelo: la función de costo.\n"
     ]
    }
   ],
   "source": [
    "# Visualicemos las predicciones junto con los datos reales\n",
    "plt.scatter(x_train, y_train, marker='x', c='red', s=80, label='Datos reales')\n",
    "plt.plot(x_train, predicciones, c='blue', label=f'Modelo: w={w_prueba}, b={b_prueba}')\n",
    "plt.title('Predicciones del modelo vs. datos reales')\n",
    "plt.xlabel('Años de experiencia')\n",
    "plt.ylabel('Salario (miles de Q)')\n",
    "plt.legend()\n",
    "plt.grid(True, alpha=0.3)\n",
    "plt.show()\n",
    "\n",
    "print('🤔 ¿Crees que estos valores de w y b son los óptimos?')\n",
    "print('Necesitamos una forma de medir qué tan bueno es el modelo: la función de costo.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 4. Función de costo\n",
    "\n",
    "La **función de costo** mide qué tan lejos están las predicciones del modelo de los valores reales. Para regresión lineal usamos el **error cuadrático medio**:\n",
    "\n",
    "$$J(w,b) = \\frac{1}{2m} \\sum_{i=0}^{m-1} \\left( f_{w,b}(x^{(i)}) - y^{(i)} \\right)^2$$\n",
    "\n",
    "Donde:\n",
    "- $f_{w,b}(x^{(i)}) - y^{(i)}$ es el **error** del ejemplo $i$.\n",
    "- Elevamos al cuadrado para que los errores positivos y negativos no se cancelen.\n",
    "- Dividimos entre $2m$ (el 2 es por conveniencia matemática, simplifica las derivadas).\n",
    "\n",
    "### ✏️ Ejercicio 2: Implementa `compute_cost`\n",
    "\n",
    "Sigue estos pasos:\n",
    "1. Inicializa `cost_sum = 0`\n",
    "2. Para cada ejemplo $i$:\n",
    "   - Calcula la predicción: `f_wb_i = w * x[i] + b`\n",
    "   - Calcula el error al cuadrado: `(f_wb_i - y[i]) ** 2`\n",
    "   - Acumula ese valor en `cost_sum`\n",
    "3. Al salir del ciclo, divide `cost_sum` entre $2m$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 68,
   "metadata": {},
   "outputs": [],
   "source": [
    "def compute_cost(x, y, w, b):\n",
    "    \"\"\"\n",
    "    Calcula el costo (error cuadrático medio dividido entre 2)\n",
    "    para los parámetros w y b dados.\n",
    "\n",
    "    Args:\n",
    "        x (ndarray): Arreglo con m ejemplos de entrada\n",
    "        y (ndarray): Arreglo con m valores objetivo\n",
    "        w (float):   Parámetro del modelo (pendiente)\n",
    "        b (float):   Parámetro del modelo (intercepto)\n",
    "\n",
    "    Returns:\n",
    "        total_cost (float): El valor del costo J(w,b)\n",
    "    \"\"\"\n",
    "    m = len(x)\n",
    "    cost_sum = 0\n",
    "\n",
    "    ### TU CÓDIGO AQUÍ ### (≈ 4 líneas)\n",
    "    # Pista 1: itera sobre los m ejemplos con un ciclo for\n",
    "    # Pista 2: en cada iteración:\n",
    "    #   - calcula la predicción: f_wb_i = w * x[i] + b\n",
    "    #   - calcula el error al cuadrado: (f_wb_i - y[i]) ** 2\n",
    "    #   - súmalo a cost_sum\n",
    "    for i in range(m):\n",
    "        f_wb_i = w * x[i] + b\n",
    "        cost_sum += (f_wb_i - y[i]) ** 2\n",
    "\n",
    "\n",
    "    ### FIN DEL CÓDIGO ###\n",
    "\n",
    "    total_cost = cost_sum / (2 * m)\n",
    "    return total_cost"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 69,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Costo con w=0.7, b=3.0: 0.0055\n",
      "Costo con w=0.0, b=0.0: 25.3030\n",
      "Costo con w=1.0, b=5.0: 7.1530\n",
      "\n",
      "✅ ¡Tu función de costo trabaja correctamente!\n",
      "💡 Observa cómo el costo es muy bajo cuando w y b están cerca del óptimo,\n",
      "   y muy alto cuando están lejos (como con w=0, b=0).\n"
     ]
    }
   ],
   "source": [
    "# 🧪 Probemos tu función de costo con varios valores de w y b\n",
    "costo_1 = compute_cost(x_train, y_train, 0.7, 3.0)\n",
    "print(f'Costo con w=0.7, b=3.0: {costo_1:.4f}')\n",
    "\n",
    "costo_2 = compute_cost(x_train, y_train, 0.0, 0.0)\n",
    "print(f'Costo con w=0.0, b=0.0: {costo_2:.4f}')\n",
    "\n",
    "costo_3 = compute_cost(x_train, y_train, 1.0, 5.0)\n",
    "print(f'Costo con w=1.0, b=5.0: {costo_3:.4f}')\n",
    "\n",
    "# Validación automática\n",
    "if math.isclose(costo_1, 0.0055, abs_tol=1e-3) and math.isclose(costo_2, 25.303, abs_tol=1e-2):\n",
    "    print('\\n✅ ¡Tu función de costo trabaja correctamente!')\n",
    "    print('💡 Observa cómo el costo es muy bajo cuando w y b están cerca del óptimo,')\n",
    "    print('   y muy alto cuando están lejos (como con w=0, b=0).')\n",
    "else:\n",
    "    print('\\n❌ Los valores no coinciden con los esperados. Revisa tu implementación.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 5. Cálculo del gradiente\n",
    "\n",
    "Para minimizar la función de costo necesitamos sus derivadas parciales respecto a $w$ y $b$:\n",
    "\n",
    "$$\\frac{\\partial J(w,b)}{\\partial w} = \\frac{1}{m} \\sum_{i=0}^{m-1} \\left( f_{w,b}(x^{(i)}) - y^{(i)} \\right) x^{(i)}$$\n",
    "\n",
    "$$\\frac{\\partial J(w,b)}{\\partial b} = \\frac{1}{m} \\sum_{i=0}^{m-1} \\left( f_{w,b}(x^{(i)}) - y^{(i)} \\right)$$\n",
    "\n",
    "Estas derivadas indican **en qué dirección y con qué magnitud** debemos modificar $w$ y $b$ para reducir el costo.\n",
    "\n",
    "### ✏️ Ejercicio 3: Implementa `compute_gradient`\n",
    "\n",
    "Sigue estos pasos:\n",
    "1. Inicializa `dj_dw = 0` y `dj_db = 0`\n",
    "2. Para cada ejemplo $i$:\n",
    "   - Calcula la predicción: `f_wb_i = w * x[i] + b`\n",
    "   - Calcula el error: `error_i = f_wb_i - y[i]`\n",
    "   - Acumula `error_i * x[i]` en `dj_dw`\n",
    "   - Acumula `error_i` en `dj_db`\n",
    "3. Al salir del ciclo, divide ambas sumas entre $m$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 70,
   "metadata": {},
   "outputs": [],
   "source": [
    "def compute_gradient(x, y, w, b):\n",
    "    \"\"\"\n",
    "    Calcula el gradiente de la función de costo respecto a w y b.\n",
    "\n",
    "    Args:\n",
    "        x (ndarray): Arreglo con m ejemplos de entrada\n",
    "        y (ndarray): Arreglo con m valores objetivo\n",
    "        w (float):   Parámetro del modelo (pendiente)\n",
    "        b (float):   Parámetro del modelo (intercepto)\n",
    "\n",
    "    Returns:\n",
    "        dj_dw (float): Derivada parcial del costo respecto a w\n",
    "        dj_db (float): Derivada parcial del costo respecto a b\n",
    "    \"\"\"\n",
    "    m = len(x)\n",
    "    dj_dw = 0\n",
    "    dj_db = 0\n",
    "\n",
    "    ### TU CÓDIGO AQUÍ ### (≈ 5 líneas)\n",
    "    # Pista 1: itera sobre los m ejemplos con un ciclo for\n",
    "    # Pista 2: en cada iteración:\n",
    "    #   - calcula la predicción: f_wb_i = w * x[i] + b\n",
    "    #   - calcula el error: error_i = f_wb_i - y[i]\n",
    "    #   - acumula  error_i * x[i]  en dj_dw\n",
    "    #   - acumula  error_i         en dj_db\n",
    "    for i in range(m):\n",
    "        f_wb_i = w * x[i] + b\n",
    "        error_i = f_wb_i - y[i]\n",
    "        dj_dw += error_i * x[i]\n",
    "        dj_db += error_i\n",
    "\n",
    "\n",
    "    ### FIN DEL CÓDIGO ###\n",
    "\n",
    "    dj_dw = dj_dw / m\n",
    "    dj_db = dj_db / m\n",
    "\n",
    "    return dj_dw, dj_db"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 71,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Gradiente con w=0, b=0:\n",
      "  dj_dw = -43.4000\n",
      "  dj_db = -6.8000\n",
      "\n",
      "Gradiente con w=0.727, b=2.81 (cerca del óptimo):\n",
      "  dj_dw = 0.0445\n",
      "  dj_db = 0.0085\n",
      "\n",
      "✅ ¡Tu función de gradiente trabaja correctamente!\n",
      "💡 Nota: cerca del óptimo los gradientes son MUY pequeños (cercanos a 0).\n",
      "   Esto indica que ya casi no hay forma de mejorar el modelo.\n"
     ]
    }
   ],
   "source": [
    "# 🧪 Probemos tu función de gradiente\n",
    "dj_dw_1, dj_db_1 = compute_gradient(x_train, y_train, 0.0, 0.0)\n",
    "print(f'Gradiente con w=0, b=0:')\n",
    "print(f'  dj_dw = {dj_dw_1:.4f}')\n",
    "print(f'  dj_db = {dj_db_1:.4f}')\n",
    "\n",
    "dj_dw_2, dj_db_2 = compute_gradient(x_train, y_train, 0.727, 2.81)\n",
    "print(f'\\nGradiente con w=0.727, b=2.81 (cerca del óptimo):')\n",
    "print(f'  dj_dw = {dj_dw_2:.4f}')\n",
    "print(f'  dj_db = {dj_db_2:.4f}')\n",
    "\n",
    "# Validación automática\n",
    "if math.isclose(dj_dw_1, -43.4, abs_tol=0.1) and math.isclose(dj_db_1, -6.8, abs_tol=0.1):\n",
    "    print('\\n✅ ¡Tu función de gradiente trabaja correctamente!')\n",
    "    print('💡 Nota: cerca del óptimo los gradientes son MUY pequeños (cercanos a 0).')\n",
    "    print('   Esto indica que ya casi no hay forma de mejorar el modelo.')\n",
    "else:\n",
    "    print('\\n❌ Los valores no coinciden con los esperados. Revisa tu implementación.')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 6. Descenso de gradiente\n",
    "\n",
    "El algoritmo de descenso de gradiente actualiza los parámetros iterativamente:\n",
    "\n",
    "$$w := w - \\alpha \\frac{\\partial J(w,b)}{\\partial w}$$\n",
    "\n",
    "$$b := b - \\alpha \\frac{\\partial J(w,b)}{\\partial b}$$\n",
    "\n",
    "Donde $\\alpha$ (alpha) es la **tasa de aprendizaje** (learning rate). En cada iteración:\n",
    "1. Calculamos los gradientes con los valores actuales de $w$ y $b$.\n",
    "2. Actualizamos $w$ y $b$ moviéndolos un \"pequeño paso\" en dirección **opuesta** al gradiente.\n",
    "\n",
    "⚠️ **Muy importante**: las actualizaciones deben ser **simultáneas**. Calcula primero ambos gradientes y luego actualiza. Si actualizas $w$ antes de calcular `dj_db`, estarás usando un $w$ que ya cambió.\n",
    "\n",
    "### ✏️ Ejercicio 4: Completa `gradient_descent`\n",
    "\n",
    "La función ya tiene preparada toda la lógica para guardar el historial del costo y los parámetros. Tú solo debes completar **el cuerpo del ciclo**: calcular gradientes y actualizar parámetros."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 72,
   "metadata": {},
   "outputs": [],
   "source": [
    "def gradient_descent(x, y, w_init, b_init, alpha, num_iters):\n",
    "    \"\"\"\n",
    "    Ejecuta el algoritmo de descenso de gradiente para ajustar w y b.\n",
    "\n",
    "    Args:\n",
    "        x (ndarray):   Arreglo con m ejemplos de entrada\n",
    "        y (ndarray):   Arreglo con m valores objetivo\n",
    "        w_init (float): Valor inicial del parámetro w\n",
    "        b_init (float): Valor inicial del parámetro b\n",
    "        alpha (float):  Tasa de aprendizaje\n",
    "        num_iters (int): Cantidad de iteraciones a ejecutar\n",
    "\n",
    "    Returns:\n",
    "        w (float):           Valor final de w\n",
    "        b (float):           Valor final de b\n",
    "        J_history (list):    Historial del costo en cada iteración\n",
    "        params_history (list): Historial de [w, b] en cada iteración\n",
    "    \"\"\"\n",
    "    # Inicialización\n",
    "    w = w_init\n",
    "    b = b_init\n",
    "    J_history = []\n",
    "    params_history = []\n",
    "\n",
    "    for i in range(num_iters):\n",
    "        ### TU CÓDIGO AQUÍ ### (≈ 3 líneas)\n",
    "        # Paso 1: calcula los gradientes usando compute_gradient(...)\n",
    "        # Paso 2: actualiza w  (recuerda: w = w - alpha * dj_dw)\n",
    "        # Paso 3: actualiza b  (recuerda: b = b - alpha * dj_db)\n",
    "        # ⚠️ Calcula AMBOS gradientes ANTES de actualizar cualquier parámetro\n",
    "        dj_dw, dj_db = compute_gradient(x, y, w, b)\n",
    "        w = w - alpha * dj_dw\n",
    "        b = b - alpha * dj_db\n",
    "\n",
    "\n",
    "        ### FIN DEL CÓDIGO ###\n",
    "\n",
    "        # Guardar el costo y los parámetros para análisis posterior\n",
    "        if i < 100000:  # evita uso excesivo de memoria en corridas muy largas\n",
    "            J_history.append(compute_cost(x, y, w, b))\n",
    "            params_history.append([w, b])\n",
    "\n",
    "        # Imprimir el progreso cada 10 % de las iteraciones\n",
    "        if i % math.ceil(num_iters / 10) == 0 or i == num_iters - 1:\n",
    "            print(f'Iteración {i:5}: Costo = {J_history[-1]:8.4f}  |  w = {w:7.4f}, b = {b:7.4f}')\n",
    "\n",
    "    return w, b, J_history, params_history"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 7. Entrenamiento del modelo\n",
    "\n",
    "Ahora ejecutaremos el descenso de gradiente para encontrar los parámetros óptimos. Si todo lo anterior está bien implementado, el costo debería **disminuir progresivamente** y los valores de $w$ y $b$ deberían acercarse al óptimo."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 73,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Iniciando entrenamiento...\n",
      "======================================================================\n",
      "Iteración     0: Costo =   9.7955  |  w =  0.4340, b =  0.0680\n",
      "Iteración   100: Costo =   0.4900  |  w =  1.0341, b =  0.6641\n",
      "Iteración   200: Costo =   0.3222  |  w =  0.9759, b =  1.0690\n",
      "Iteración   300: Costo =   0.2120  |  w =  0.9288, b =  1.3972\n",
      "Iteración   400: Costo =   0.1397  |  w =  0.8906, b =  1.6631\n",
      "Iteración   500: Costo =   0.0921  |  w =  0.8596, b =  1.8787\n",
      "Iteración   600: Costo =   0.0609  |  w =  0.8345, b =  2.0533\n",
      "Iteración   700: Costo =   0.0404  |  w =  0.8142, b =  2.1949\n",
      "Iteración   800: Costo =   0.0270  |  w =  0.7977, b =  2.3096\n",
      "Iteración   900: Costo =   0.0181  |  w =  0.7844, b =  2.4026\n",
      "Iteración   999: Costo =   0.0123  |  w =  0.7736, b =  2.4772\n",
      "======================================================================\n",
      "\n",
      "🎯 Entrenamiento finalizado:\n",
      "   w final     = 0.7736\n",
      "   b final     = 2.4772\n",
      "   costo final = 0.012346\n"
     ]
    }
   ],
   "source": [
    "# Hiperparámetros del entrenamiento\n",
    "w_inicial = 0.0\n",
    "b_inicial = 0.0\n",
    "tasa_aprendizaje = 0.01\n",
    "iteraciones = 1000\n",
    "\n",
    "print('Iniciando entrenamiento...')\n",
    "print('=' * 70)\n",
    "\n",
    "w_final, b_final, J_history, params_history = gradient_descent(\n",
    "    x_train, y_train,\n",
    "    w_inicial, b_inicial,\n",
    "    tasa_aprendizaje, iteraciones\n",
    ")\n",
    "\n",
    "print('=' * 70)\n",
    "print(f'\\n🎯 Entrenamiento finalizado:')\n",
    "print(f'   w final     = {w_final:.4f}')\n",
    "print(f'   b final     = {b_final:.4f}')\n",
    "print(f'   costo final = {J_history[-1]:.6f}')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 8. Visualización de resultados\n",
    "\n",
    "Veamos cómo el costo disminuyó durante el entrenamiento y cómo se ve la línea ajustada sobre los datos."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 74,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 55928 characters]",
      "text/plain": [
       "<Figure size 1400x500 with 2 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Gráfica del costo a lo largo de las iteraciones\n",
    "fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
    "\n",
    "# Toda la curva de costo\n",
    "axes[0].plot(J_history, c='blue')\n",
    "axes[0].set_title('Costo durante el entrenamiento (vista completa)')\n",
    "axes[0].set_xlabel('Iteración')\n",
    "axes[0].set_ylabel('Costo J(w,b)')\n",
    "axes[0].grid(True, alpha=0.3)\n",
    "\n",
    "# Zoom: a partir de la iteración 100\n",
    "axes[1].plot(range(100, len(J_history)), J_history[100:], c='blue')\n",
    "axes[1].set_title('Costo a partir de la iteración 100 (zoom)')\n",
    "axes[1].set_xlabel('Iteración')\n",
    "axes[1].set_ylabel('Costo J(w,b)')\n",
    "axes[1].grid(True, alpha=0.3)\n",
    "\n",
    "plt.tight_layout()\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 75,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 52548 characters]",
      "text/plain": [
       "<Figure size 800x500 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Calcular las predicciones finales con el modelo entrenado\n",
    "predicciones_finales = compute_model_output(x_train, w_final, b_final)\n",
    "\n",
    "# Visualizar el ajuste sobre los datos reales\n",
    "plt.scatter(x_train, y_train, marker='x', c='red', s=80, label='Datos reales')\n",
    "plt.plot(x_train, predicciones_finales, c='blue',\n",
    "         label=f'Modelo ajustado: f(x) = {w_final:.3f}x + {b_final:.3f}')\n",
    "plt.title('Modelo de regresión lineal ajustado')\n",
    "plt.xlabel('Años de experiencia')\n",
    "plt.ylabel('Salario (miles de Q)')\n",
    "plt.legend()\n",
    "plt.grid(True, alpha=0.3)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 9. Hacer predicciones con el modelo\n",
    "\n",
    "Ya tenemos un modelo entrenado. Usémoslo para predecir el salario para nuevos casos.\n",
    "\n",
    "### ✏️ Ejercicio 5: Predicciones para casos nuevos\n",
    "\n",
    "Usa el modelo entrenado (con `w_final` y `b_final`) para estimar el salario de tres profesionales con diferentes años de experiencia."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 76,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "Predicciones del modelo entrenado:\n",
      "-------------------------------------------------------\n",
      "    2.5 años de experiencia  →  Q  4411.32 mensuales\n",
      "    6.5 años de experiencia  →  Q  7505.86 mensuales\n",
      "   12.0 años de experiencia  →  Q 11760.86 mensuales\n"
     ]
    }
   ],
   "source": [
    "# Casos nuevos a predecir\n",
    "casos = [2.5, 6.5, 12.0]  # años de experiencia\n",
    "\n",
    "print('Predicciones del modelo entrenado:')\n",
    "print('-' * 55)\n",
    "\n",
    "for x_nuevo in casos:\n",
    "    ### TU CÓDIGO AQUÍ ### (1 línea)\n",
    "    # Calcula la predicción usando w_final, b_final y x_nuevo\n",
    "    # Pista: salario_predicho = w_final * x_nuevo + b_final\n",
    "    # salario_predicho = None  # ← reemplaza None con tu cálculo\n",
    "    salario_predicho = w_final * x_nuevo + b_final\n",
    "\n",
    "\n",
    "    ### FIN DEL CÓDIGO ###\n",
    "\n",
    "    if salario_predicho is not None:\n",
    "        print(f'  {x_nuevo:5.1f} años de experiencia  →  Q {salario_predicho * 1000:8.2f} mensuales')\n",
    "    else:\n",
    "        print(f'  {x_nuevo:5.1f} años → ⚠️ aún no implementado')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 10. Visualización de la superficie de costo\n",
    "\n",
    "Para terminar, veamos en un mapa cómo $J(w, b)$ se ve sobre el espacio de parámetros y qué trayectoria siguió el descenso de gradiente para llegar al mínimo. **Esta celda ya está completamente escrita** — solo ejecútala y observa."
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 77,
   "metadata": {},
   "outputs": [
    {
     "data": {
      "image/png": "[encoded data, 375392 characters]",
      "text/plain": [
       "<Figure size 1000x700 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    }
   ],
   "source": [
    "# Generamos una malla de valores de w y b para visualizar el costo\n",
    "w_range = np.linspace(0, 1.5, 50)\n",
    "b_range = np.linspace(0, 6, 50)\n",
    "W, B = np.meshgrid(w_range, b_range)\n",
    "\n",
    "# Calculamos el costo en cada punto de la malla (sí, con dos for explícitos)\n",
    "Z = np.zeros_like(W)\n",
    "for i in range(W.shape[0]):\n",
    "    for j in range(W.shape[1]):\n",
    "        Z[i, j] = compute_cost(x_train, y_train, W[i, j], B[i, j])\n",
    "\n",
    "# Extraer la trayectoria del descenso de gradiente\n",
    "ws = [p[0] for p in params_history]\n",
    "bs = [p[1] for p in params_history]\n",
    "\n",
    "# Gráfico de curvas de nivel (contornos)\n",
    "fig, ax = plt.subplots(figsize=(10, 7))\n",
    "contour = ax.contour(W, B, Z, levels=30, cmap='viridis')\n",
    "ax.clabel(contour, inline=True, fontsize=8)\n",
    "ax.plot(ws, bs, 'r.-', markersize=3, linewidth=1, label='Trayectoria del descenso de gradiente')\n",
    "ax.scatter([ws[0]], [bs[0]], c='blue', s=120, marker='o', label='Inicio', zorder=5)\n",
    "ax.scatter([w_final], [b_final], c='red', s=180, marker='*', label='Final', zorder=5)\n",
    "ax.set_xlabel('w')\n",
    "ax.set_ylabel('b')\n",
    "ax.set_title('Curvas de nivel de J(w, b) y trayectoria del descenso de gradiente')\n",
    "ax.legend()\n",
    "ax.grid(True, alpha=0.3)\n",
    "plt.show()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 11. Retos adicionales 🎯\n",
    "\n",
    "Si ya completaste los ejercicios anteriores, intenta los siguientes retos para profundizar.\n",
    "\n",
    "### Reto 1: Efecto de la tasa de aprendizaje\n",
    "\n",
    "Ejecuta el descenso de gradiente con diferentes valores de $\\alpha$ y observa qué ocurre:\n",
    "- $\\alpha = 0.001$ (muy pequeño — convergencia lenta)\n",
    "- $\\alpha = 0.01$ (el que usamos — convergencia razonable)\n",
    "- $\\alpha = 0.1$ (más rápido… ¿siempre estable?)\n",
    "- $\\alpha = 0.3$ (¿qué pasa aquí?)"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 78,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "--- α = 0.001 ---\n",
      "Iteración     0: Costo =  23.4111  |  w =  0.0434, b =  0.0068\n",
      "Iteración    10: Costo =  10.9114  |  w =  0.3937, b =  0.0628\n",
      "Iteración    20: Costo =   5.3025  |  w =  0.6280, b =  0.1021\n",
      "Iteración    30: Costo =   2.7847  |  w =  0.7847, b =  0.1302\n",
      "Iteración    40: Costo =   1.6535  |  w =  0.8894, b =  0.1509\n",
      "Iteración    50: Costo =   1.1444  |  w =  0.9593, b =  0.1665\n",
      "Iteración    60: Costo =   0.9144  |  w =  1.0058, b =  0.1788\n",
      "Iteración    70: Costo =   0.8095  |  w =  1.0367, b =  0.1888\n",
      "Iteración    80: Costo =   0.7608  |  w =  1.0572, b =  0.1973\n",
      "Iteración    90: Costo =   0.7372  |  w =  1.0706, b =  0.2048\n",
      "Iteración    99: Costo =   0.7260  |  w =  1.0786, b =  0.2110\n",
      "\n",
      "--- α = 0.01 ---\n",
      "Iteración     0: Costo =   9.7955  |  w =  0.4340, b =  0.0680\n",
      "Iteración    10: Costo =   0.7152  |  w =  1.0934, b =  0.2187\n",
      "Iteración    20: Costo =   0.6854  |  w =  1.0902, b =  0.2730\n",
      "Iteración    30: Costo =   0.6573  |  w =  1.0827, b =  0.3256\n",
      "Iteración    40: Costo =   0.6303  |  w =  1.0753, b =  0.3770\n",
      "Iteración    50: Costo =   0.6044  |  w =  1.0681, b =  0.4274\n",
      "Iteración    60: Costo =   0.5795  |  w =  1.0610, b =  0.4768\n",
      "Iteración    70: Costo =   0.5557  |  w =  1.0540, b =  0.5251\n",
      "Iteración    80: Costo =   0.5329  |  w =  1.0472, b =  0.5724\n",
      "Iteración    90: Costo =   0.5110  |  w =  1.0406, b =  0.6188\n",
      "Iteración    99: Costo =   0.4921  |  w =  1.0347, b =  0.6596\n",
      "\n",
      "--- α = 0.1 ---\n",
      "Iteración     0: Costo = 211.3711  |  w =  4.3400, b =  0.6800\n",
      "Iteración    10: Costo = 454949371161.8306  |  w = 150634.2168, b = 21637.7431\n",
      "Iteración    20: Costo = 982556045442539323392.0000  |  w = 7000318542.2909, b = 1005529612.5019\n",
      "Iteración    30: Costo = 2122030370041478112231070629888.0000  |  w = 325323123976390.2500, b = 46729592731865.6875\n",
      "Iteración    40: Costo = 4582957798962234990592509569005342687232.0000  |  w = 15118617012789649408.0000, b = 2171646475791153152.0000\n",
      "Iteración    50: Costo = 9897832982785344606234297398244489210952683618304.0000  |  w = 702601701304212398276608.0000, b = 100922095402734481047552.0000\n",
      "Iteración    60: Costo = 21376390979925077622493064214912835934777594017335530749952.0000  |  w = 32651739921579461953955299328.0000, b = 4690113908511765502934122496.0000\n",
      "Iteración    70: Costo = 46166680335116337672813837252696946163438552440657454603690760470528.0000  |  w = 1517411810884372437505464388812800.0000, b = 217961868380108096568550957776896.0000\n",
      "Iteración    80: Costo = 99706371162766156475202217819286495493927295130212137389581139481932108660736.0000  |  w = 70518098249632530179094873076337213440.0000, b = 10129258477396388849898442272622510080.0000\n",
      "Iteración    90: Costo = 215336263692442088508412267204823348280618889557212890684625935226378259279186451496960.0000  |  w = 3277160586911866677600404526338727600979968.0000, b = 470733151924432720512483903059671694442496.0000\n",
      "Iteración    99: Costo = 54209076662330919971499575124747607943995018901367303595895026415028991439135276417449092710400.0000  |  w = -51996623343012274641303498131653400467033030656.0000, b = -7468823619274765311892751605370796094038999040.0000\n",
      "\n",
      "--- α = 0.3 ---\n",
      "Iteración     0: Costo = 2857.7959  |  w = 13.0200, b =  2.0400\n",
      "Iteración    10: Costo = 1300021190306020529799168.0000  |  w = 254632803799.0375, b = 36575596203.2798\n",
      "Iteración    20: Costo = 591520185163725956148739490053632833930395648.0000  |  w = 5431551741957110759424.0000, b = 780191084179200409600.0000\n",
      "Iteración    30: Costo = 269146481661398700604953474507384394954042786846818814675034046464.0000  |  w = 115859990879048702676283529625600.0000, b = 16642192911218891957116996681728.0000\n",
      "Iteración    40: Costo = 122463832017260893760967173084521467475289554877399555200803341050024923293791041880064.0000  |  w = 2471400094157336144444337965661352345206784.0000, b = 354993270892864361287910719547984452780032.0000\n",
      "Iteración    50: Costo = 55722036787460071318033809515662869084912573985821280576284577706976697814408566671598733143095046335102976.0000  |  w = 52717235510376571172095777814191527193663311529902080.0000, b = 7572332747943416901734621637163680295715235950493696.0000\n",
      "Iteración    60: Costo = 25353978661270524623202295226079325289187047230831533747677476462671562824253239343086410902152254788948018140951722597589975040.0000  |  w = 1124507086661777057280457443427482924610791210353691193782566912.0000, b = 161524817361626382217390438715996613158032495428295734801203200.0000\n",
      "Iteración    70: Costo = 11536265919497487124170710906326288137217658784436857138950801138419932571556647717597895653841422853187166756384075330525317819414633525832822292480.0000  |  w = 23986769710328544712132184308344650810949419010584599339367687186674614272.0000, b = 3445472814278091206319458917355033999154590020243285356314599675452719104.0000\n",
      "Iteración    80: Costo = 5249094556060896311134851608657076971199795523547921309982977429417686434836837762788011136979489163925031042318761317255164247702644586809242639068131303862552903024640.0000  |  w = 511659844531855874510675932089011236018414304296426289415472735520125696747488935936.0000, b = 73495101915835116146489057612527305481294880479950281718138056649136304447364792320.0000\n",
      "Iteración    90: Costo = 2388380594790272876133605188056685147474169159282821198938746563423051694596252451941358862298110162657548999431788951652214271959298635215937627767425743141493512189505061678170035353288704.0000  |  w = 10914174758330859703044351313104742480084736579169411035704547509638932845058198122806934241280.0000, b = 1567718074348163466774390067884857824480842049932076971720724930160149226596158302098779275264.0000\n",
      "Iteración    99: Costo = 9339438314311165404458277167070877093334276718653191277225878791784827006678058818413008750354617773121488080324260353611827594742502458638768030173308570097186737749585802624803813419391167387274884958650368.0000  |  w = -21582388974804685764005760170191240688741451827025544490513623028668558871672880135786108773351624278016.0000, b = -3100106240976880499727766495225941812984632897779773714409896275904004776297923610312374302246107611136.0000\n"
     ]
    },
    {
     "data": {
      "image/png": "[encoded data, 64540 characters]",
      "text/plain": [
       "<Figure size 1000x600 with 1 Axes>"
      ]
     },
     "metadata": {},
     "output_type": "display_data"
    },
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "\n",
      "💭 ¿Qué observas? ¿Cuál tasa converge más rápido? ¿Alguna diverge?\n"
     ]
    }
   ],
   "source": [
    "# Reto 1: experimenta con diferentes tasas de aprendizaje\n",
    "tasas = [0.001, 0.01, 0.1, 0.3]\n",
    "\n",
    "fig, ax = plt.subplots(figsize=(10, 6))\n",
    "\n",
    "for alpha in tasas:\n",
    "    print(f'\\n--- α = {alpha} ---')\n",
    "    try:\n",
    "        _, _, J_hist, _ = gradient_descent(x_train, y_train, 0.0, 0.0, alpha, 100)\n",
    "        ax.plot(J_hist, label=f'α = {alpha}')\n",
    "    except Exception as e:\n",
    "        print(f'  ⚠️ Error: {e}')\n",
    "\n",
    "ax.set_xlabel('Iteración')\n",
    "ax.set_ylabel('Costo J(w,b)')\n",
    "ax.set_title('Efecto de la tasa de aprendizaje en el costo')\n",
    "ax.set_yscale('log')  # escala logarítmica para apreciar mejor las diferencias\n",
    "ax.legend()\n",
    "ax.grid(True, alpha=0.3)\n",
    "plt.show()\n",
    "\n",
    "print('\\n💭 ¿Qué observas? ¿Cuál tasa converge más rápido? ¿Alguna diverge?')"
   ]
  },
  {
   "cell_type": "markdown",
   "id": "3cee04b8",
   "metadata": {},
   "source": [
    "La que converge más rápido parece ser la tasa 0.01, porque esta tasa dá pasos más grandes que 0.001 y resuelve más rápido en cambio los números más altos hace que el costo sea mayor y no resuelva, por ello la tasa 0.01 es la que tiene mejor balance ajustado a estos datos."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Reto 2: Implementa una función de evaluación\n",
    "\n",
    "Implementa una función que calcule dos métricas para evaluar tu modelo:\n",
    "\n",
    "- **MAE** (Error Absoluto Medio): $\\;\\;\\dfrac{1}{m} \\sum_{i=0}^{m-1} \\left| f_{w,b}(x^{(i)}) - y^{(i)} \\right|$\n",
    "\n",
    "- **RMSE** (Raíz del Error Cuadrático Medio): $\\;\\;\\sqrt{\\dfrac{1}{m} \\sum_{i=0}^{m-1} \\left( f_{w,b}(x^{(i)}) - y^{(i)} \\right)^2}$"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": 79,
   "metadata": {},
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "MAE  = 0.1356 miles de Q  (≈ Q 135.55 de error promedio)\n",
      "RMSE = 0.1571 miles de Q  (≈ Q 157.13)\n"
     ]
    }
   ],
   "source": [
    "def evaluar_modelo(x, y, w, b):\n",
    "    \"\"\"\n",
    "    Calcula MAE y RMSE para el modelo.\n",
    "\n",
    "    Returns:\n",
    "        mae (float):  Error Absoluto Medio\n",
    "        rmse (float): Raíz del Error Cuadrático Medio\n",
    "    \"\"\"\n",
    "    m = len(x)\n",
    "    suma_abs = 0       # acumulador para MAE\n",
    "    suma_cuadrada = 0  # acumulador para RMSE\n",
    "\n",
    "    ### TU CÓDIGO AQUÍ ###\n",
    "    # Pista 1: usa abs(...) para el valor absoluto\n",
    "    # Pista 2: itera sobre los m ejemplos con un ciclo for\n",
    "    # Pista 3: en cada iteración, calcula la predicción y el error,\n",
    "    #          luego acumula |error| en suma_abs y error**2 en suma_cuadrada\n",
    "\n",
    "    for i in range(m):\n",
    "        f_wb_i = w * x[i] + b\n",
    "        error_i = f_wb_i - y[i]\n",
    "        suma_abs += abs(error_i)\n",
    "        suma_cuadrada += error_i ** 2\n",
    "\n",
    "\n",
    "    ### FIN DEL CÓDIGO ###\n",
    "\n",
    "    mae = suma_abs / m\n",
    "    rmse = math.sqrt(suma_cuadrada / m)\n",
    "    return mae, rmse\n",
    "\n",
    "\n",
    "# 🧪 Probar la función\n",
    "mae, rmse = evaluar_modelo(x_train, y_train, w_final, b_final)\n",
    "print(f'MAE  = {mae:.4f} miles de Q  (≈ Q {mae*1000:.2f} de error promedio)')\n",
    "print(f'RMSE = {rmse:.4f} miles de Q  (≈ Q {rmse*1000:.2f})')"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "### Reto 3: Probar con tus propios datos\n",
    "\n",
    "Modifica `x_train` y `y_train` con datos inventados por ti (por ejemplo: horas de estudio vs. nota del examen, o cualquier otro caso) y vuelve a entrenar el modelo. ¿Qué tan bien ajusta?\n",
    "\n",
    "💡 *Tip:* si los valores de tu nuevo dataset son muy distintos en escala, puede que necesites ajustar la tasa de aprendizaje."
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## 12. Conclusiones\n",
    "\n",
    "¡Felicidades! 🎉 Has implementado desde cero un algoritmo completo de regresión lineal:\n",
    "\n",
    "✅ Función del modelo $f_{w,b}(x) = wx + b$  \n",
    "✅ Función de costo $J(w,b)$  \n",
    "✅ Cálculo del gradiente $\\frac{\\partial J}{\\partial w}$ y $\\frac{\\partial J}{\\partial b}$  \n",
    "✅ Algoritmo de descenso de gradiente  \n",
    "✅ Entrenamiento, predicción y evaluación del modelo  \n",
    "\n",
    "### Preguntas para reflexionar\n",
    "\n",
    "1. ¿Qué pasa si la tasa de aprendizaje es **demasiado grande**? ¿Y si es **demasiado pequeña**?\n",
    "2. ¿Cómo afectan los valores iniciales de $w$ y $b$ al resultado final?\n",
    "3. ¿Por qué dividimos entre $2m$ en la función de costo y no solo entre $m$?\n",
    "4. ¿Qué tan bien crees que generaliza este modelo a años de experiencia muy fuera del rango entrenado (por ejemplo, 30 años)?\n",
    "\n",
    "### Próximos pasos\n",
    "\n",
    "En las siguientes clases veremos:\n",
    "- **Vectorización con NumPy**: el mismo algoritmo, mucho más rápido y conciso (sin ciclos `for`).\n",
    "- **Regresión lineal multivariable**: cuando tenemos más de una característica de entrada.\n",
    "- **Normalización de características**: clave cuando las variables tienen escalas muy distintas."
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",
   "version": "3.10.12"
  }
 },
 "nbformat": 4,
 "nbformat_minor": 5
}
