#!/usr/bin/env python3
"""Script to apply required modifications to the Deep Learning Week 8 notebook."""

import json
import copy

NOTEBOOK_PATH = "Deep learning_ejercicio fraude_S8_201021.ipynb"

with open(NOTEBOOK_PATH, "r", encoding="utf-8") as f:
    nb = json.load(f)

cells = nb["cells"]

# ============================================================
# 1. MODIFY TITLE CELL: Add group member names
# ============================================================
for i, cell in enumerate(cells):
    if cell["cell_type"] == "markdown" and any("Autoencoder Para detección de fraude" in s for s in cell["source"]):
        cells[i]["source"] = [
            "# Autoencoder Para detección de fraude\n",
            "\n",
            "---\n",
            "\n",
            "**Integrantes del grupo:**\n",
            "\n",
            "- ALEXANDER OVIEDO FADUL\n",
            "- MARIA FERNANDA RUIZ PAIPILLA\n",
            "- NEHEMAN SAMIR JALLER CERCHIARO\n",
            "- WILLIAM DAVID OBANDO LOPEZ\n",
            "\n",
            "**Asignatura:** Deep Learning (NRC-198)\n",
            "\n",
            "**Docente:** Juan Carlos Valencia\n",
            "\n",
            "**Corporación Universitaria Minuto de Dios – UNIMINUTO**\n",
            "\n",
            "---"
        ]
        print(f"[OK] Title cell modified at index {i}")
        break

# ============================================================
# 2. MODIFY AUTOENCODER ARCHITECTURE CELL
# ============================================================
for i, cell in enumerate(cells):
    if cell["cell_type"] == "code" and any("np.random.seed" in s for s in cell["source"]):
        cells[i]["source"] = [
            "import numpy as np\n",
            "np.random.seed(5)  # MODIFICADO: valor de n = 5 según instrucciones del docente\n",
            "from keras.models import Model, load_model\n",
            "from keras.layers import Input, Dense, Dropout\n",
            "\n",
            "dim_entrada = X_train.shape[1]  # número de elementos de entrada=7 son las 7 características o features\n",
            "capa_entrada = Input(shape=(dim_entrada,))\n",
            "\n",
            "# ========== ARQUITECTURA MODIFICADA ==========\n",
            "# Arquitectura original: 7-6-4-6-7\n",
            "# Arquitectura modificada: 7-256-6-4-6-256-7 (se agrega capa oculta de 256 neuronas)\n",
            "\n",
            "# ENCODER\n",
            "encoder = Dense(256, activation='relu')(capa_entrada)  # NUEVA capa oculta con 256 neuronas\n",
            "encoder = Dropout(0.2)(encoder)  # Dropout del 20% para evitar sobreajuste\n",
            "encoder = Dense(6, activation='tanh')(encoder)  # se reduce a 6 características\n",
            "encoder = Dense(4, activation='relu')(encoder)  # cuello de botella: 4 características\n",
            "\n",
            "# DECODER (simétrico al encoder)\n",
            "decoder = Dense(6, activation='tanh')(encoder)  # aumenta a 6 características\n",
            "decoder = Dense(256, activation='relu')(decoder)  # NUEVA capa simétrica de 256 neuronas\n",
            "decoder = Dropout(0.2)(decoder)  # Dropout del 20%\n",
            "decoder = Dense(7, activation='relu')(decoder)  # salida: 7 (mismas que la entrada)\n",
            "\n",
            "autoencoder = Model(inputs=capa_entrada, outputs=decoder)\n",
            "\n",
            "# ========== OPTIMIZADOR MODIFICADO ==========\n",
            "# Se cambia de SGD/Adam a RMSprop con learning rate de 0.001\n",
            "from tensorflow.keras.optimizers import RMSprop\n",
            "optimizador = RMSprop(learning_rate=0.001)\n",
            "\n",
            "# loss: mse (Mean Squared Error) - error cuadrático medio\n",
            "autoencoder.compile(optimizer=optimizador, loss='mse')\n",
            "\n",
            "# ========== ÉPOCAS MODIFICADAS ==========\n",
            "nits = 150  # MODIFICADO: se aumentan las épocas de 100 a 150\n",
            "tam_lote = 64  # MODIFICADO: se ajusta el tamaño del lote a 64\n",
            "autoencoder.fit(X_train, X_train, epochs=nits, batch_size=tam_lote, shuffle=True, validation_data=(X_test,X_test), verbose=1)"
        ]
        # Clear old outputs since code changed
        cells[i]["outputs"] = []
        cells[i]["execution_count"] = None
        print(f"[OK] Autoencoder architecture cell modified at index {i}")
        break

# ============================================================
# 3. ADD THEORETICAL ANSWERS CELL AT THE END
# ============================================================
theoretical_cell = {
    "cell_type": "markdown",
    "metadata": {
        "id": "resp_teoricas_grupo5"
    },
    "source": [
        "---\n",
        "\n",
        "# Respuestas del Grupo\n",
        "\n",
        "## 1. ¿Cuáles son las aplicaciones de un autoencoder?\n",
        "\n",
        "Un autoencoder es una arquitectura de red neuronal no supervisada que aprende a comprimir (codificar) datos en una representación latente y luego reconstruirlos (decodificar). Entre sus principales aplicaciones se encuentran:\n",
        "\n",
        "- **Reducción de dimensionalidad:** similar al análisis de componentes principales (PCA), pero capaz de capturar relaciones no lineales entre variables, lo que permite representar datos complejos en espacios de menor dimensión.\n",
        "- **Eliminación de ruido (denoising):** los autoencoders aprenden a filtrar el ruido de los datos de entrada, reconstruyendo versiones limpias de señales, imágenes o registros corruptos.\n",
        "- **Detección de anomalías:** al entrenarse exclusivamente con datos normales, el autoencoder genera un alto error de reconstrucción ante entradas atípicas, lo que permite identificar fraudes, fallas industriales o intrusiones en redes.\n",
        "- **Generación de datos sintéticos:** los autoencoders variacionales (VAE) permiten muestrear nuevos datos del espacio latente, útil para aumentar datasets o crear contenido artificial.\n",
        "- **Compresión de datos:** la representación latente puede servir como una forma eficiente de almacenar información con menor costo computacional.\n",
        "- **Imputación de datos faltantes:** el modelo puede aprender a predecir valores ausentes a partir de los patrones aprendidos en el espacio latente.\n",
        "- **Segmentación y agrupamiento semántico:** las representaciones latentes permiten agrupar datos similares sin necesidad de etiquetas previas.\n",
        "\n",
        "## 2. ¿Cómo se realiza la detección de fraudes mediante autoencoder en Python?\n",
        "\n",
        "La detección de fraudes bancarios con autoencoders en Python sigue un flujo bien definido:\n",
        "\n",
        "1. **Preparación de datos:** se carga el dataset de transacciones, se seleccionan las características relevantes (monto, frecuencia, hora, categoría, etc.) y se aplica escalado (por ejemplo, con `StandardScaler` de sklearn) para normalizar las variables.\n",
        "2. **División del dataset:** se separan los datos en conjunto de entrenamiento y prueba. Idealmente, el entrenamiento se realiza solo con transacciones normales para que el modelo aprenda exclusivamente el comportamiento legítimo.\n",
        "3. **Construcción del autoencoder:** se define la arquitectura usando Keras/TensorFlow, estableciendo capas de codificación (encoder) que comprimen la entrada a un espacio latente de menor dimensión, y capas de decodificación (decoder) simétricas que reconstruyen la entrada original.\n",
        "4. **Entrenamiento:** se entrena el modelo minimizando el error cuadrático medio (MSE) entre la entrada y la salida reconstruida, usando un optimizador como RMSprop o Adam.\n",
        "5. **Cálculo del error de reconstrucción:** una vez entrenado, se pasan las transacciones de prueba por el autoencoder y se calcula el MSE para cada registro.\n",
        "6. **Definición del umbral:** se establece un umbral de anomalía (por ejemplo, usando un percentil del error de reconstrucción). Las transacciones cuyo error supere este umbral se clasifican como sospechosas.\n",
        "7. **Evaluación:** se analiza la matriz de confusión, la precisión (precision), la sensibilidad (recall) y el F1-score para evaluar el desempeño del modelo en la detección de fraudes.\n",
        "\n",
        "### Modificaciones realizadas en este notebook:\n",
        "\n",
        "| Parámetro | Valor original | Valor modificado |\n",
        "| --- | --- | --- |\n",
        "| Semilla (n) | No especificada | **5** |\n",
        "| Optimizador | SGD | **RMSprop** |\n",
        "| Learning rate | 0.01 | **0.001** |\n",
        "| Épocas | 100 | **150** |\n",
        "| Batch size | 32 | **64** |\n",
        "| Arquitectura | 7-6-4-6-7 | **7-256-6-4-6-256-7** |\n",
        "| Dropout | No aplicaba | **0.2 (20%)** |\n",
        "| Funciones de activación | tanh-relu-tanh-relu | **relu-tanh-relu-tanh-relu-relu** |\n",
        "\n",
        "Estas modificaciones buscan mejorar la capacidad del modelo para capturar patrones complejos gracias a la capa adicional de 256 neuronas, prevenir el sobreajuste mediante Dropout, y optimizar la convergencia con RMSprop y un learning rate más adecuado."
    ]
}

cells.append(theoretical_cell)
print(f"[OK] Theoretical answers cell added at index {len(cells)-1}")

# ============================================================
# SAVE MODIFIED NOTEBOOK
# ============================================================
with open(NOTEBOOK_PATH, "w", encoding="utf-8") as f:
    json.dump(nb, f, ensure_ascii=False, indent=2)

print("\n[DONE] Notebook modifications applied successfully.")
print(f"Total cells: {len(cells)}")
