#!/usr/bin/env python3
"""
Script para generar los notebooks completados de la Semana 3 - Deep Learning
Actividad: Descenso del Gradiente y Manejo de DataFrame
Autor: Alexander Oviedo Fadul
"""

import json
import os

# ============================================================
# NOTEBOOK A: Descenso del Gradiente (completado)
# ============================================================

notebook_a = {
    "cells": [
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "# Descenso del Gradiente\n",
                "\n",
                "**Estudiante:** Alexander Oviedo Fadul\n",
                "\n",
                "**Asignatura:** NRC-198 Deep Learning - Semana 3\n",
                "\n",
                "**Fecha:** Marzo 2026"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Introducción\n",
                "\n",
                "El descenso del gradiente es un algoritmo de optimización fundamental en Deep Learning. Se utiliza para minimizar la función de error (pérdida) ajustando iterativamente los pesos y sesgos de una red neuronal.\n",
                "\n",
                "**Concepto clave:** Cuando inicializamos una red neuronal, los pesos y el bias se asignan de forma aleatoria, lo que produce un error inicial alto. El descenso del gradiente calcula la derivada de la función de error respecto a cada peso, y usa esa pendiente para actualizar los pesos en la dirección que reduce el error.\n",
                "\n",
                "### Instrucciones del notebook\n",
                "1. Cargue las librerías que necesite: numpy, matplotlib.pyplot, etc.\n",
                "2. Para cargar las librerías utilice `import` seguido del nombre de la librería y de un alias (`as`). Ejemplo: `import pandas as pd`\n",
                "3. Ejecute de forma secuencial de arriba hacia abajo las celdas de código dando click al botón play o ctrl+enter\n",
                "4. Al ejecutar la celda debe marcar un check en color verde al lado izquierdo de la celda de código\n",
                "5. El símbolo `#` es para comentarios dentro del código\n",
                "6. Para subir un archivo a Google Colab se puede hacer click en la parte izquierda, donde aparece una carpeta"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "**El descenso del gradiente se utiliza para alcanzar el mínimo de la función de error (se produce al dar los valores iniciales a los pesos de las neuronas de la red neuronal y el bias), para actualizar los pesos y el bias o sesgo en el entrenamiento de la red neuronal profunda artificial.**"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": None,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Importamos los módulos a usar.\n",
                "import numpy as np\n",
                "import matplotlib.pyplot as plt\n",
                "%matplotlib inline"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Parte 1: Función base y su gráfica\n",
                "\n",
                "Definimos la función f(x) = x² + 1 y la visualizamos para entender su comportamiento. Esta es una parábola con vértice en (0, 1), lo que significa que el mínimo global está en x = 0."
            ]
        },
        {
            "cell_type": "code",
            "execution_count": None,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Creamos la función a optimizar y la graficamos.\n",
                "# lambda es un tipo de función llamada anónima.\n",
                "\n",
                "funcion = lambda x: x**2 + 1\n",
                "\n",
                "# Se crean 200 valores espaciados entre x=-10 y x=10\n",
                "x = np.linspace(-10, 10, 200)\n",
                "\n",
                "# Graficamos la función.\n",
                "plt.figure(figsize=(8, 5))\n",
                "plt.plot(x, funcion(x), color='steelblue', linewidth=2)\n",
                "plt.title('Función f(x) = x² + 1', fontsize=14)\n",
                "plt.xlabel('x', fontsize=12)\n",
                "plt.ylabel('f(x)', fontsize=12)\n",
                "plt.grid(True, alpha=0.3)\n",
                "plt.show()"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Parte 2: Implementación del Descenso del Gradiente\n",
                "\n",
                "La derivada de f(x) = x² + 1 es f'(x) = 2x. Esta derivada nos indica la pendiente en cada punto, y la usamos para movernos hacia el mínimo."
            ]
        },
        {
            "cell_type": "code",
            "execution_count": None,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Creamos la función de la derivada de x.\n",
                "def derivada(x):\n",
                "    x_derivada = 2*x\n",
                "    return x_derivada  # Regresa la derivada de la función inicial\n",
                "\n",
                "# Creamos la función del gradiente\n",
                "def gradiente(x_inicio, precision, l_r):\n",
                "    \"\"\"\n",
                "    Implementa el descenso del gradiente.\n",
                "    Parámetros:\n",
                "    - x_inicio: valor inicial de x desde donde comienza la búsqueda\n",
                "    - precision: criterio de parada (diferencia mínima entre iteraciones)\n",
                "    - l_r: learning rate (tasa de aprendizaje)\n",
                "    \"\"\"\n",
                "    x_list, y_list = [x_inicio], [funcion(x_inicio)]\n",
                "\n",
                "    while True:\n",
                "        d_x = -derivada(x_inicio)\n",
                "        x_inicio = x_inicio + (l_r * d_x)\n",
                "        x_list.append(x_inicio)\n",
                "        y_list.append(funcion(x_inicio))\n",
                "\n",
                "        if abs(x_list[-1] - x_list[-2]) <= precision:\n",
                "            break\n",
                "\n",
                "    print(\"El mínimo local se produce en: \" + str(x_inicio))\n",
                "    print(\"Valor de f(x) en el mínimo: \" + str(funcion(x_inicio)))\n",
                "    print(\"Número de pasos: \" + str(len(x_list)))\n",
                "\n",
                "    plt.figure(figsize=(8, 5))\n",
                "    plt.scatter(x_list, y_list, c='b', zorder=5, label='Pasos del gradiente')\n",
                "    plt.plot(x_list, y_list, c='b', alpha=0.5)\n",
                "    plt.plot(x, funcion(x), c='y', label='f(x) = x² + 1')\n",
                "    plt.title('Gráfico del descenso del gradiente paso a paso')\n",
                "    plt.xlabel('x')\n",
                "    plt.ylabel('f(x)')\n",
                "    plt.legend()\n",
                "    plt.grid(True, alpha=0.3)\n",
                "    plt.show()"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "**Llamamos la función gradiente pasando los parámetros: punto inicial de x, precisión y learning rate**"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": None,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Prueba 1\n",
                "gradiente(-8, 0.0001, 0.05)"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "**Variemos los 3 valores del Descenso del Gradiente**"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": None,
            "metadata": {},
            "outputs": [],
            "source": [
                "gradiente(-8, 0.01, 0.03)"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": None,
            "metadata": {},
            "outputs": [],
            "source": [
                "gradiente(5, 0.01, 0.03)"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "---\n",
                "## Ejercicio: Descenso del Gradiente para f(x) = x² + 5x + 6\n",
                "\n",
                "**Análisis teórico:**\n",
                "- La función f(x) = x² + 5x + 6 es una parábola.\n",
                "- Su derivada es f'(x) = 2x + 5.\n",
                "- El mínimo se encuentra donde f'(x) = 0, es decir x = -2.5\n",
                "- El valor mínimo es f(-2.5) = 6.25 - 12.5 + 6 = -0.25\n",
                "\n",
                "Desde mi experiencia automatizando procesos en la Rama Judicial, este tipo de optimización es análogo a ajustar parámetros de un modelo de clasificación de tutelas: se parte de valores arbitrarios y se refina hasta encontrar el mínimo error."
            ]
        },
        {
            "cell_type": "code",
            "execution_count": None,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Definimos la nueva función: f(x) = x² + 5x + 6\n",
                "funcion_nueva = lambda x: x**2 + 5*x + 6\n",
                "\n",
                "# Derivada: f'(x) = 2x + 5\n",
                "def derivada_nueva(x):\n",
                "    return 2*x + 5\n",
                "\n",
                "def gradiente_nuevo(x_inicio, precision, l_r):\n",
                "    \"\"\"\n",
                "    Descenso del gradiente para f(x) = x² + 5x + 6\n",
                "    \"\"\"\n",
                "    x_list, y_list = [x_inicio], [funcion_nueva(x_inicio)]\n",
                "\n",
                "    while True:\n",
                "        d_x = -derivada_nueva(x_inicio)\n",
                "        x_inicio = x_inicio + (l_r * d_x)\n",
                "        x_list.append(x_inicio)\n",
                "        y_list.append(funcion_nueva(x_inicio))\n",
                "\n",
                "        if abs(x_list[-1] - x_list[-2]) <= precision:\n",
                "            break\n",
                "\n",
                "    print('='*50)\n",
                "    print(f'Punto de inicio: x = {x_list[0]}')\n",
                "    print(f'Precisión: {precision}')\n",
                "    print(f'Learning rate: {l_r}')\n",
                "    print('-'*50)\n",
                "    print(f'El mínimo se alcanza en x = {x_inicio:.6f}')\n",
                "    print(f'Valor de f(x) en el mínimo: y = {funcion_nueva(x_inicio):.6f}')\n",
                "    print(f'Número de pasos: {len(x_list)}')\n",
                "    print('='*50)\n",
                "\n",
                "    x_rango = np.linspace(-10, 10, 200)\n",
                "    fig, axes = plt.subplots(1, 2, figsize=(14, 5))\n",
                "\n",
                "    # Gráfico 1: Función completa\n",
                "    axes[0].plot(x_rango, funcion_nueva(x_rango), c='green', linewidth=2, label='f(x) = x²+5x+6')\n",
                "    axes[0].axhline(y=0, color='gray', linestyle='--', alpha=0.5)\n",
                "    axes[0].axvline(x=-2.5, color='red', linestyle=':', alpha=0.7, label='Mínimo teórico (x=-2.5)')\n",
                "    axes[0].set_title('Función f(x) = x² + 5x + 6', fontsize=12)\n",
                "    axes[0].set_xlabel('x')\n",
                "    axes[0].set_ylabel('f(x)')\n",
                "    axes[0].legend()\n",
                "    axes[0].grid(True, alpha=0.3)\n",
                "\n",
                "    # Gráfico 2: Descenso del gradiente\n",
                "    axes[1].plot(x_rango, funcion_nueva(x_rango), c='green', alpha=0.5, linewidth=2, label='f(x)')\n",
                "    axes[1].scatter(x_list, y_list, c='red', s=30, zorder=5, label='Pasos')\n",
                "    axes[1].plot(x_list, y_list, c='red', alpha=0.4, linestyle='--')\n",
                "    axes[1].scatter(x_list[-1], y_list[-1], c='darkred', s=100, marker='*', zorder=6, label='Mínimo encontrado')\n",
                "    axes[1].set_title('Descenso del Gradiente - Paso a Paso', fontsize=12)\n",
                "    axes[1].set_xlabel('x')\n",
                "    axes[1].set_ylabel('f(x)')\n",
                "    axes[1].legend()\n",
                "    axes[1].grid(True, alpha=0.3)\n",
                "\n",
                "    plt.tight_layout()\n",
                "    plt.show()"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": None,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Prueba 1: desde x=8\n",
                "gradiente_nuevo(8, 0.0001, 0.05)"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": None,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Prueba 2: desde x=-8\n",
                "gradiente_nuevo(-8, 0.01, 0.03)"
            ]
        },
        {
            "cell_type": "code",
            "execution_count": None,
            "metadata": {},
            "outputs": [],
            "source": [
                "# Prueba 3: desde x=0, mayor learning rate\n",
                "gradiente_nuevo(0, 0.001, 0.1)"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "### Análisis de resultados\n",
                "\n",
                "1. **Convergencia al mínimo:** El algoritmo converge al mínimo teórico en x = -2.5, donde f(-2.5) = -0.25.\n",
                "\n",
                "2. **Efecto del learning rate:** Un learning rate más alto (0.1) llega al mínimo en menos pasos. Sin embargo, un valor demasiado grande podría causar divergencia. Con l_r = 0.03 la convergencia es más estable pero requiere más iteraciones.\n",
                "\n",
                "3. **Efecto de la precisión:** Mayor precisión (0.0001) implica más iteraciones para una respuesta más exacta.\n",
                "\n",
                "4. **Aplicación en redes neuronales:** Este principio se aplica en el entrenamiento de redes neuronales, donde variantes como SGD, Adam y RMSprop ajustan los pesos para minimizar la función de pérdida."
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Conclusiones\n",
                "\n",
                "1. El descenso del gradiente es un algoritmo iterativo que encuentra el mínimo de una función usando la derivada. Es la base de cómo las redes neuronales aprenden.\n",
                "\n",
                "2. Los tres parámetros (punto inicial, precisión y learning rate) impactan directamente la velocidad y calidad de la solución.\n",
                "\n",
                "3. La visualización gráfica permite entender cómo el algoritmo se mueve hacia el mínimo, análogo a lo que ocurre internamente al entrenar modelos con TensorFlow o PyTorch.\n",
                "\n",
                "4. En aplicaciones reales como el sistema MARDUK para análisis de tutelas judiciales, el descenso del gradiente opera sobre miles de parámetros simultáneamente, buscando minimizar el error de predicción.\n",
                "\n",
                "### Referencias\n",
                "- Fandango, A. (2018). Mastering TensorFlow 1.x, pp. 73-111.\n",
                "- Galea, A., y Capelo, L. (2018). Applied Deep Learning with Python, pp. 6-61 y pp. 186-194.\n",
                "- Hodnett, M., y Wiley, J. (2018). R Deep Learning Essentials, pp. 60-96."
            ]
        }
    ],
    "metadata": {
        "colab": {
            "collapsed_sections": [],
            "name": "GradientDescent_Alexander_Oviedo.ipynb",
            "provenance": []
        },
        "kernelspec": {
            "display_name": "Python 3",
            "language": "python",
            "name": "python3"
        },
        "language_info": {
            "name": "python",
            "version": "3.10.12"
        }
    },
    "nbformat": 4,
    "nbformat_minor": 1
}

# ============================================================
# NOTEBOOK B: DataFrame (completado)
# ============================================================

notebook_b = {
    "nbformat": 4,
    "nbformat_minor": 0,
    "metadata": {
        "colab": {
            "name": "Dataframe_Alexander_Oviedo.ipynb",
            "provenance": []
        },
        "kernelspec": {
            "name": "python3",
            "display_name": "Python 3"
        },
        "language_info": {
            "name": "python"
        }
    },
    "cells": [
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "# DataFrame\n",
                "\n",
                "**Estudiante:** Alexander Oviedo Fadul\n",
                "\n",
                "**Asignatura:** NRC-198 Deep Learning - Semana 3\n",
                "\n",
                "**Fecha:** Marzo 2026"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "### Instrucciones\n",
                "1. Cargue las librerías que necesite: numpy, matplotlib.pyplot, etc.\n",
                "2. Para cargar las librerías utilice `import` seguido del nombre de la librería y de un alias (`as`)\n",
                "3. Ejecute de forma secuencial de arriba hacia abajo las celdas de código\n",
                "4. Al ejecutar la celda debe marcar un check en color verde\n",
                "5. El símbolo `#` es para comentarios dentro del código\n",
                "6. Para subir un archivo a Google Colab se puede hacer click en la carpeta del panel izquierdo"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Carga de librerías y datos"
            ]
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "import pandas as pd\n",
                "import numpy as np"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Ejemplo: Lectura del archivo california_housing_test.csv"
            ]
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Leer el archivo CSV de prueba\n",
                "data_frame = pd.read_csv('sample_data/california_housing_test.csv')\n",
                "data_frame"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "data_frame.head(5)  # Primeras 5 filas"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "data_frame.tail()  # Últimas 5 filas"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "print('Shape: ', data_frame.shape)\n",
                "print('Columnas: ', data_frame.columns)"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "data_frame['median_house_value'].describe()"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "---\n",
                "# Responda las siguientes preguntas:"
            ]
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Pregunta 1\n",
                "Utilice el archivo `california_housing_train.csv`, conviértalo a DataFrame. Muestre las primeras 10 filas y las 10 últimas del DataFrame."
            ]
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Cargar california_housing_train.csv\n",
                "df_train = pd.read_csv('sample_data/california_housing_train.csv')\n",
                "print(f'Archivo cargado. Registros: {len(df_train)}, Columnas: {len(df_train.columns)}')"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Primeras 10 filas\n",
                "df_train.head(10)"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Últimas 10 filas\n",
                "df_train.tail(10)"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Pregunta 2\n",
                "Utilice el archivo `mnist_test.csv`, conviértalo a DataFrame. Muestre las 10 primeras y las 10 últimas filas."
            ]
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Cargar mnist_test.csv\n",
                "df_mnist = pd.read_csv('sample_data/mnist_test.csv')\n",
                "print(f'MNIST cargado. Registros: {len(df_mnist)}, Columnas: {len(df_mnist.columns)}')"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Primeras 10 filas - MNIST\n",
                "df_mnist.head(10)"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Últimas 10 filas - MNIST\n",
                "df_mnist.tail(10)"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Pregunta 3\n",
                "Muestre dimensión, columnas y estadísticos descriptivos de `median_house_value` de `california_housing_train.csv`."
            ]
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Dimensión\n",
                "print(f'Dimensión: {df_train.shape}')\n",
                "print(f'Filas: {df_train.shape[0]}, Columnas: {df_train.shape[1]}')"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Nombres de las columnas\n",
                "print('Columnas:')\n",
                "for i, col in enumerate(df_train.columns, 1):\n",
                "    print(f'  {i}. {col}')"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Estadísticos descriptivos de median_house_value\n",
                "df_train['median_house_value'].describe()"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Pregunta 4\n",
                "Elimine la fila 2999 y la columna `population`, renombre columnas a español."
            ]
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Copia del DataFrame de test\n",
                "df_mod = data_frame.copy()\n",
                "print(f'Original: {df_mod.shape}')"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Eliminar fila 2999\n",
                "df_mod = df_mod.drop(index=2999)\n",
                "print(f'Sin fila 2999: {df_mod.shape}')"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Eliminar columna population\n",
                "df_mod = df_mod.drop(columns=['population'])\n",
                "print(f'Sin columna population: {df_mod.shape}')"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Renombrar columnas a español\n",
                "nombres_es = {\n",
                "    'longitude': 'longitud',\n",
                "    'latitude': 'latitud',\n",
                "    'housing_median_age': 'edad_mediana_vivienda',\n",
                "    'total_rooms': 'total_habitaciones',\n",
                "    'total_bedrooms': 'total_dormitorios',\n",
                "    'households': 'hogares',\n",
                "    'median_income': 'ingreso_mediano',\n",
                "    'median_house_value': 'valor_mediano_vivienda'\n",
                "}\n",
                "df_mod = df_mod.rename(columns=nombres_es)"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "code",
            "metadata": {},
            "source": [
                "# Resultado final\n",
                "print(f'Dimensión final: {df_mod.shape}')\n",
                "print(f'Columnas:')\n",
                "for i, col in enumerate(df_mod.columns, 1):\n",
                "    print(f'  {i}. {col}')\n",
                "\n",
                "print('\\nPrimeras 5 filas:')\n",
                "df_mod.head()"
            ],
            "execution_count": None,
            "outputs": []
        },
        {
            "cell_type": "markdown",
            "metadata": {},
            "source": [
                "## Conclusiones\n",
                "\n",
                "1. **DataFrame como estructura fundamental:** El DataFrame de Pandas organiza información en filas y columnas, facilitando operaciones esenciales antes de alimentar una red neuronal.\n",
                "\n",
                "2. **Comandos de exploración:** `head()`, `tail()`, `shape`, `columns` y `describe()` son herramientas indispensables para comprender la distribución de los datos.\n",
                "\n",
                "3. **Manipulación:** Las operaciones `drop` y `rename` son fundamentales en el preprocesamiento, que representa el 70-80% del trabajo en proyectos de Deep Learning.\n",
                "\n",
                "4. **Conexión con Deep Learning:** Los DataFrames son la primera capa de procesamiento: los datos se cargan, limpian y luego se convierten en tensores para las redes neuronales.\n",
                "\n",
                "### Referencias\n",
                "- Fandango, A. (2018). Mastering TensorFlow 1.x, pp. 73-111.\n",
                "- Galea, A., y Capelo, L. (2018). Applied Deep Learning with Python, pp. 6-61 y pp. 186-194."
            ]
        }
    ]
}


# ============================================================
# Guardar notebooks
# ============================================================
output_dir = os.path.dirname(os.path.abspath(__file__))

path_a = os.path.join(output_dir, "oviedo_alexander_gradiente.ipynb")
path_b = os.path.join(output_dir, "oviedo_alexander_dataframe.ipynb")

with open(path_a, 'w', encoding='utf-8') as f:
    json.dump(notebook_a, f, ensure_ascii=False, indent=2)
print(f"Notebook A guardado: {path_a}")

with open(path_b, 'w', encoding='utf-8') as f:
    json.dump(notebook_b, f, ensure_ascii=False, indent=2)
print(f"Notebook B guardado: {path_b}")

print("\nAmbos notebooks generados exitosamente.")
