#!/usr/bin/env python3
"""
Generador del notebook Portafolio de Evidencias — Semana 8 ML Avanzado (NRC-8772)
==================================================================================
Genera: Oviedo_Alexander_Portafolio_S8_MLA.ipynb

Pipeline completo: Preprocesamiento → Modelado → Validación Cruzada →
Optimización de Hiperparámetros → Evaluación Final
Dataset: Titanic (continuidad con Semana 7)
"""

import json
import os

# ─── Utilidades ────────────────────────────────────────────────────────
def md_cell(source):
    """Crea una celda markdown."""
    return {
        "cell_type": "markdown",
        "metadata": {},
        "source": fix_source(source)
    }

def code_cell(source, outputs=None):
    """Crea una celda de código."""
    return {
        "cell_type": "code",
        "execution_count": None,
        "metadata": {},
        "outputs": outputs or [],
        "source": fix_source(source)
    }

def fix_source(text):
    """Agrega \\n al final de cada línea excepto la última."""
    if isinstance(text, str):
        lines = text.split("\n")
    else:
        lines = text
    result = []
    for i, line in enumerate(lines):
        if i < len(lines) - 1:
            result.append(line + "\n")
        else:
            result.append(line)
    return result


# ─── Celdas del Notebook ──────────────────────────────────────────────
cells = []

# =====================================================================
# CELDA 0: Encabezado
# =====================================================================
COLAB_URL = (
    "https://colab.research.google.com/github/bladealex9848/UNIMINUTO/blob/main/"
    "2do%20CUATRIMESTRE/NRC-8772-Machine%20Learning%20Avanzado/ACTIVIDADES/"
    "MA%20Semana%208/Oviedo_Alexander_Portafolio_S8_MLA.ipynb"
)

cells.append(md_cell(f"""# 📊 Portafolio de Evidencias: Canalización de Proyectos de Machine Learning
## Machine Learning Avanzado — NRC-8772 — Semana 8

[![Open In Colab](https://colab.research.google.com/assets/colab-badge.svg)]({COLAB_URL})

**Grupo:** Grupo 10  
**Integrantes:**  
- Alexander Oviedo Fadul  
- Maria Fernanda Ruiz Paipilla  
- Neheman Samir Jaller Cerchiaro  
- William David Obando Lopez  

**Enlace del Notebook en Google Colab:** [Abrir en Google Colab]({COLAB_URL})

**Programa:** Especialización en Inteligencia Artificial, Corporación Universitaria Minuto de Dios (UNIMINUTO)  
**Curso:** NRC-8772 — Machine Learning Avanzado  
**Profesor:** Leonardo Valderrama García  
**Fecha:** Junio 2026

---

### Descripción

Este portafolio de evidencias integra un proceso completo de **canalización (pipeline)** de Machine Learning
aplicado al dataset del **Titanic**, consolidando los aprendizajes de las semanas previas del curso.
El objetivo central es demostrar cómo la automatización del flujo de trabajo — desde la preparación de datos
hasta la optimización del modelo — maximiza tanto la eficiencia del desarrollo como la precisión de los
resultados.

### Estructura del notebook

| Sección | Contenido |
|---------|-----------|
| 1 | Respuestas a Preguntas Orientadoras |
| 2 | Importación de Librerías |
| 3 | Carga e Ingesta del Dataset |
| 4 | Análisis Exploratorio de Datos (EDA) |
| 5 | Preprocesamiento e Ingeniería de Características |
| 6 | Construcción del Pipeline de Canalización |
| 7 | Evaluación con Validación Cruzada |
| 8 | Optimización de Hiperparámetros (GridSearchCV) |
| 9 | Evaluación Final y Comparativa de Modelos |
| 10 | Consideraciones Éticas |
| 11 | Conclusiones |
| 12 | Referencias |"""))

# =====================================================================
# CELDA 1: Preguntas orientadoras
# =====================================================================
cells.append(md_cell("""## 1. Respuestas a las Preguntas Orientadoras

### Pregunta 1: ¿Cómo los proyectos de Machine Learning pueden ser canalizados para maximizar su impacto y efectividad?

La canalización (o *pipelining*) de proyectos de ML es, en esencia, la disciplina de **encadenar cada etapa
del flujo de trabajo en una secuencia reproducible y automatizada**. En mi experiencia trabajando con datos
judiciales en la Rama Judicial, me di cuenta rápidamente de que un modelo no vale nada si el proceso que lo
rodea es manual y propenso a errores. Cuando empecé a automatizar la limpieza de datos de tutelas para el
proyecto MARDUK, descubrí que la consistencia del preprocesamiento era tan importante como la arquitectura
del modelo en sí.

La canalización maximiza el impacto y la efectividad a través de varios mecanismos:

1. **Prevención de la fuga de datos (Data Leakage):** Al encapsular transformaciones dentro de un `Pipeline`
   de scikit-learn, las estadísticas (media, mediana, etc.) se calculan **exclusivamente** sobre los datos
   de entrenamiento de cada fold. Esto evita estimaciones de rendimiento artificialmente optimistas
   (Bonaccorso, 2018, p. 15).

2. **Reproducibilidad y consistencia:** El mismo flujo exacto se aplica a entrenamiento, validación y
   producción. Janiesch et al. (2021) enfatizan que esta repetibilidad es lo que distingue un prototipo
   académico de un sistema desplegable.

3. **Optimización integrada:** Herramientas como `GridSearchCV` permiten buscar hiperparámetros óptimos
   *sobre todo el pipeline*, no solo sobre el clasificador. Como mencionó el profesor Valderrama en la
   sesión del 20 de junio: "depende del modelo que estén utilizando, hay diferentes técnicas de
   optimización... hacer iteraciones para configuración de hiperparámetros hasta encontrar el modelo
   adecuado".

4. **Escalabilidad:** Un pipeline bien diseñado se puede desplegar como servicio (API REST, como vimos en
   clase con Flask) o integrar en plataformas de automatización como Make o Zapier.

### Pregunta 2: ¿Cómo canalizar tus proyectos de Machine Learning para obtener los mejores resultados y asegurar el éxito en su implementación?

Para canalizar un proyecto de ML de forma exitosa, recomiendo seguir esta secuencia —la misma que hemos
ido construyendo a lo largo del curso:

1. **Definición clara del problema y recopilación de datos:** Antes de escribir una línea de código, hay que
   entender qué se quiere predecir y con qué datos se cuenta. En el caso del Titanic, nuestra variable
   objetivo es `survived` y contamos con 891 registros con variables demográficas y de viaje.

2. **Preprocesamiento robusto con `ColumnTransformer`:** Separar el tratamiento de variables numéricas
   (imputación + escalado) y categóricas (imputación + one-hot encoding) dentro de un transformador
   unificado garantiza que no se "mezclen" tratamientos incompatibles (Campesato, 2020, p. 8).

3. **Selección y comparación de algoritmos:** No existe un único modelo "mejor" para todos los problemas.
   Comparar al menos 2-3 clasificadores (Random Forest, Gradient Boosting, SVM) mediante validación
   cruzada permite identificar cuál se adapta mejor a la estructura de nuestros datos.

4. **Optimización de hiperparámetros:** Esta es la etapa que marca la diferencia entre un modelo "decente"
   y uno realmente afinado. `GridSearchCV` explora combinaciones de hiperparámetros y selecciona la que
   maximiza la métrica elegida, todo dentro del pipeline para evitar data leakage.

5. **Evaluación rigurosa y despliegue:** Finalmente, evaluar con métricas múltiples (accuracy, F1, AUC-ROC)
   sobre un conjunto de prueba *nunca visto* durante el entrenamiento ni la optimización, y persistir el
   modelo entrenado para su uso en producción."""))

# =====================================================================
# CELDA 2: Importación de librerías
# =====================================================================
cells.append(md_cell("""## 2. Importación de Librerías

Se importan las librerías necesarias para el flujo completo: análisis de datos, visualización,
construcción de pipelines, optimización de hiperparámetros y evaluación multimétrica."""))

cells.append(code_cell("""# ===========================================================================
# 2. IMPORTACIÓN DE LIBRERÍAS
# ===========================================================================
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
import os
import warnings
warnings.filterwarnings('ignore')

# Framework principal de Machine Learning (scikit-learn)
from sklearn.model_selection import (
    train_test_split, cross_validate, GridSearchCV, learning_curve
)
from sklearn.compose import ColumnTransformer
from sklearn.pipeline import Pipeline
from sklearn.impute import SimpleImputer
from sklearn.preprocessing import StandardScaler, OneHotEncoder, FunctionTransformer
from sklearn.feature_selection import SelectKBest, f_classif

# Clasificadores
from sklearn.ensemble import RandomForestClassifier, GradientBoostingClassifier
from sklearn.svm import SVC

# Métricas
from sklearn.metrics import (
    classification_report, confusion_matrix, accuracy_score,
    precision_score, recall_score, f1_score, roc_curve, auc,
    ConfusionMatrixDisplay, RocCurveDisplay
)

# Estilo visual
sns.set_theme(style="whitegrid")
plt.rcParams['figure.figsize'] = (10, 6)
plt.rcParams['font.size'] = 11

print("✅ Librerías cargadas exitosamente.")
print(f"   pandas: {pd.__version__}")
print(f"   numpy: {np.__version__}")
import sklearn
print(f"   scikit-learn: {sklearn.__version__}")"""))

# =====================================================================
# CELDA 3: Carga de datos
# =====================================================================
cells.append(md_cell("""## 3. Carga e Ingesta del Dataset

Para asegurar la portabilidad del notebook (ejecución en Google Colab sin dependencias locales),
el dataset del **Titanic** se descarga directamente desde un repositorio público.
Este es el mismo dataset que utilizamos en la Semana 7 para el ejercicio de automatización
con Pipelines, lo que garantiza la continuidad del portafolio."""))

cells.append(code_cell("""# ===========================================================================
# 3. CARGA DE DATOS DESDE LA WEB
# ===========================================================================
URL_TITANIC = "https://raw.githubusercontent.com/datasciencedojo/datasets/master/titanic.csv"
RUTA_LOCAL = "./titanic.csv"

if os.path.exists(RUTA_LOCAL):
    print("📁 Cargando dataset desde la ruta local...")
    df = pd.read_csv(RUTA_LOCAL)
else:
    print("📥 Descargando dataset desde URL pública...")
    try:
        df = pd.read_csv(URL_TITANIC)
        df.to_csv(RUTA_LOCAL, index=False)
        print("💾 Dataset guardado localmente como 'titanic.csv'.")
    except Exception as e:
        print(f"❌ Error al descargar: {e}")

# Estandarizar nombres de columnas a minúsculas
df.columns = df.columns.str.lower()

print(f"\\nDimensiones del dataset: {df.shape[0]} filas × {df.shape[1]} columnas")
print(f"\\nColumnas: {list(df.columns)}")
print(f"\\nPrimeras 5 filas:")
display(df.head())"""))

# =====================================================================
# CELDA 4: EDA
# =====================================================================
cells.append(md_cell("""## 4. Análisis Exploratorio de Datos (EDA)

Antes de construir cualquier modelo, es fundamental entender la distribución de los datos,
identificar valores faltantes y explorar las relaciones entre las variables predictoras y la
variable objetivo (`survived`). Esta etapa es lo que Bonaccorso (2018, p. 10) describe como
"el primer paso indispensable para evitar construir modelos sobre supuestos incorrectos"."""))

cells.append(code_cell("""# ===========================================================================
# 4.1 Información general del dataset
# ===========================================================================
print("=" * 60)
print("RESUMEN ESTADÍSTICO DEL DATASET")
print("=" * 60)
print(f"\\nForma: {df.shape}")
print(f"\\nValores faltantes por columna:")
missing = df.isnull().sum()
missing_pct = (missing / len(df) * 100).round(1)
missing_df = pd.DataFrame({'Faltantes': missing, 'Porcentaje (%)': missing_pct})
display(missing_df[missing_df['Faltantes'] > 0])

print(f"\\nDistribución de la variable objetivo (survived):")
display(df['survived'].value_counts().to_frame('Conteo'))
print(f"\\nTasa de supervivencia: {df['survived'].mean():.1%}")"""))

cells.append(code_cell("""# ===========================================================================
# 4.2 Visualizaciones exploratorias
# ===========================================================================
fig, axes = plt.subplots(2, 2, figsize=(14, 10))
fig.suptitle('Análisis Exploratorio del Dataset Titanic', fontsize=14, fontweight='bold')

# 4.2.1 Distribución de supervivencia
sns.countplot(data=df, x='survived', hue='survived', palette='Set2', ax=axes[0, 0], legend=False)
axes[0, 0].set_title('Distribución de Supervivencia')
axes[0, 0].set_xticklabels(['No Sobrevivió (0)', 'Sobrevivió (1)'])
axes[0, 0].set_ylabel('Frecuencia')

# 4.2.2 Supervivencia por clase
sns.countplot(data=df, x='pclass', hue='survived', palette='Set1', ax=axes[0, 1])
axes[0, 1].set_title('Supervivencia por Clase del Boleto')
axes[0, 1].set_xlabel('Clase')
axes[0, 1].legend(title='Survived', labels=['No', 'Sí'])

# 4.2.3 Distribución de edad por supervivencia
df[df['age'].notna()].boxplot(column='age', by='survived', ax=axes[1, 0])
axes[1, 0].set_title('Distribución de Edad por Supervivencia')
axes[1, 0].set_xlabel('Survived')
axes[1, 0].set_ylabel('Edad')
plt.sca(axes[1, 0])
plt.title('Distribución de Edad por Supervivencia')

# 4.2.4 Supervivencia por género
sns.countplot(data=df, x='sex', hue='survived', palette='coolwarm', ax=axes[1, 1])
axes[1, 1].set_title('Supervivencia por Género')
axes[1, 1].legend(title='Survived', labels=['No', 'Sí'])

plt.tight_layout()
plt.show()"""))

# =====================================================================
# CELDA 5: Preprocesamiento
# =====================================================================
cells.append(md_cell("""## 5. Preprocesamiento e Ingeniería de Características

En esta sección preparamos los datos para el modelado. Siguiendo las buenas prácticas de
canalización que describe Campesato (2020, pp. 8-15), separamos el tratamiento de variables
numéricas y categóricas mediante un `ColumnTransformer`, y añadimos una variable derivada
(`family_size`) que en la Semana 7 demostró mejorar ligeramente el rendimiento.

> **Nota práctica:** El profesor Valderrama insistió en que la normalización y la imputación
> deben hacerse *dentro* del pipeline para evitar la fuga de datos. Eso es exactamente lo que
> implementamos aquí."""))

cells.append(code_cell("""# ===========================================================================
# 5. PREPROCESAMIENTO E INGENIERÍA DE CARACTERÍSTICAS
# ===========================================================================

# 5.1 Ingeniería de características
df['family_size'] = df['sibsp'] + df['parch'] + 1
df['is_alone'] = (df['family_size'] == 1).astype(int)

# 5.2 Selección de variables
FEATURES_NUM = ['age', 'fare', 'sibsp', 'parch', 'family_size', 'is_alone']
FEATURES_CAT = ['sex', 'embarked', 'pclass']
TARGET = 'survived'

# Convertir pclass a string para que sea tratada como categórica
df['pclass'] = df['pclass'].astype(str)

X = df[FEATURES_NUM + FEATURES_CAT]
y = df[TARGET]

# 5.3 División en entrenamiento y prueba (80/20, estratificada)
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=42, stratify=y
)

print(f"Conjunto de entrenamiento: {X_train.shape[0]} muestras")
print(f"Conjunto de prueba:        {X_test.shape[0]} muestras")
print(f"\\nVariables numéricas:  {FEATURES_NUM}")
print(f"Variables categóricas: {FEATURES_CAT}")
print(f"Variable objetivo:     {TARGET}")"""))

# =====================================================================
# CELDA 6: Pipeline
# =====================================================================
cells.append(md_cell("""## 6. Construcción del Pipeline de Canalización

Aquí es donde todo cobra sentido. El pipeline encapsula **preprocesamiento + modelo** en un
único objeto que se puede entrenar, evaluar y optimizar como una unidad cohesiva.

Construimos tres pipelines, uno para cada clasificador:
- **Random Forest:** Robusto ante outliers, maneja bien variables heterogéneas.
- **Gradient Boosting:** Potente para encontrar patrones complejos, especialmente tras
  optimización de hiperparámetros.
- **SVM (Support Vector Classifier):** Efectivo en espacios de alta dimensión después del
  one-hot encoding.

Esta comparación de múltiples algoritmos es exactamente lo que la rúbrica denomina
*"selección de algoritmos y entrenamiento de modelos"*."""))

cells.append(code_cell("""# ===========================================================================
# 6. CONSTRUCCIÓN DEL PIPELINE DE CANALIZACIÓN
# ===========================================================================

# 6.1 Preprocesador unificado con ColumnTransformer
preprocessor = ColumnTransformer(
    transformers=[
        ('num', Pipeline([
            ('imputer', SimpleImputer(strategy='median')),
            ('scaler', StandardScaler())
        ]), FEATURES_NUM),
        ('cat', Pipeline([
            ('imputer', SimpleImputer(strategy='most_frequent')),
            ('onehot', OneHotEncoder(drop='first', sparse_output=False, handle_unknown='ignore'))
        ]), FEATURES_CAT)
    ]
)

# 6.2 Definición de los tres pipelines
pipelines = {
    'Random Forest': Pipeline([
        ('preprocessor', preprocessor),
        ('classifier', RandomForestClassifier(random_state=42))
    ]),
    'Gradient Boosting': Pipeline([
        ('preprocessor', preprocessor),
        ('classifier', GradientBoostingClassifier(random_state=42))
    ]),
    'SVM (SVC)': Pipeline([
        ('preprocessor', preprocessor),
        ('classifier', SVC(random_state=42, probability=True))
    ])
}

print("✅ Pipelines construidos exitosamente:")
for name in pipelines:
    print(f"   • {name}")

# Mostrar la estructura del pipeline de Random Forest como ejemplo
print("\\n📐 Estructura del pipeline (Random Forest):")
print(pipelines['Random Forest'])"""))

# =====================================================================
# CELDA 7: Validación cruzada
# =====================================================================
cells.append(md_cell("""## 7. Evaluación con Validación Cruzada

Antes de optimizar, necesitamos una **línea base (baseline)** del rendimiento de cada modelo
con sus hiperparámetros por defecto. Usamos validación cruzada estratificada con 5 folds
y evaluamos múltiples métricas simultáneamente.

Como bien señala Janiesch et al. (2021, p. 689): *"la validación cruzada es el estándar
de oro para estimar el rendimiento de generalización de un modelo"*."""))

cells.append(code_cell("""# ===========================================================================
# 7. VALIDACIÓN CRUZADA MULTIMÉTRICA (BASELINE)
# ===========================================================================
SCORING = ['accuracy', 'precision', 'recall', 'f1', 'roc_auc']
CV_FOLDS = 5

results_baseline = {}

print("=" * 70)
print("EVALUACIÓN BASELINE — Validación Cruzada (5-Fold Estratificada)")
print("=" * 70)

for name, pipeline in pipelines.items():
    cv_results = cross_validate(
        pipeline, X_train, y_train,
        cv=CV_FOLDS, scoring=SCORING,
        return_train_score=False, n_jobs=-1
    )

    results_baseline[name] = {
        metric: cv_results[f'test_{metric}'].mean()
        for metric in SCORING
    }

    print(f"\\n🔹 {name}:")
    for metric in SCORING:
        mean_val = cv_results[f'test_{metric}'].mean()
        std_val = cv_results[f'test_{metric}'].std()
        print(f"   {metric:>12s}: {mean_val:.4f} ± {std_val:.4f}")

# Tabla comparativa
print("\\n" + "=" * 70)
print("TABLA COMPARATIVA BASELINE")
print("=" * 70)
baseline_df = pd.DataFrame(results_baseline).T
baseline_df.columns = [m.upper() for m in SCORING]
display(baseline_df.round(4))"""))

# =====================================================================
# CELDA 8: Optimización de hiperparámetros
# =====================================================================
cells.append(md_cell("""## 8. Optimización de Hiperparámetros (GridSearchCV)

Esta es la sección **más importante** del portafolio y la que marca la diferencia con las
actividades anteriores. Como indicó el profesor Valderrama: *"Ojo con el tema de optimización
del modelo, porque depende del modelo que estén utilizando hay diferentes técnicas de
optimización. Una de ellas, por ejemplo, hacer iteraciones para configuración de
hiperparámetros hasta encontrar el modelo adecuado"*.

Usamos `GridSearchCV` dentro del pipeline completo para buscar la mejor combinación de
hiperparámetros. La ventaja de hacerlo así (y no por separado) es que **el preprocesamiento
se recalcula en cada fold**, evitando el data leakage que tanto enfatizamos en la Semana 7.

### Hiperparámetros a explorar

| Modelo | Hiperparámetros | Valores |
|--------|-----------------|---------|
| Random Forest | `n_estimators`, `max_depth`, `min_samples_split` | 50-200, 5-20, 2-5 |
| Gradient Boosting | `n_estimators`, `learning_rate`, `max_depth` | 50-200, 0.01-0.2, 3-7 |
| SVM | `C`, `kernel`, `gamma` | 0.1-10, rbf/linear, scale/auto |"""))

cells.append(code_cell("""# ===========================================================================
# 8. OPTIMIZACIÓN DE HIPERPARÁMETROS CON GridSearchCV
# ===========================================================================

# 8.1 Definición de grids de búsqueda
param_grids = {
    'Random Forest': {
        'classifier__n_estimators': [50, 100, 200],
        'classifier__max_depth': [5, 10, 15, None],
        'classifier__min_samples_split': [2, 5],
        'classifier__min_samples_leaf': [1, 2]
    },
    'Gradient Boosting': {
        'classifier__n_estimators': [50, 100, 200],
        'classifier__learning_rate': [0.01, 0.1, 0.2],
        'classifier__max_depth': [3, 5, 7],
        'classifier__subsample': [0.8, 1.0]
    },
    'SVM (SVC)': {
        'classifier__C': [0.1, 1, 10],
        'classifier__kernel': ['rbf', 'linear'],
        'classifier__gamma': ['scale', 'auto']
    }
}

# 8.2 Ejecución de GridSearchCV para cada pipeline
best_models = {}
results_optimized = {}

print("=" * 70)
print("OPTIMIZACIÓN DE HIPERPARÁMETROS — GridSearchCV (5-Fold)")
print("=" * 70)

for name, pipeline in pipelines.items():
    print(f"\\n🔍 Optimizando {name}...")

    grid_search = GridSearchCV(
        pipeline,
        param_grids[name],
        cv=CV_FOLDS,
        scoring='f1',           # Optimizamos por F1
        n_jobs=-1,
        verbose=0,
        refit=True
    )
    grid_search.fit(X_train, y_train)

    best_models[name] = grid_search.best_estimator_
    results_optimized[name] = {
        'best_score': grid_search.best_score_,
        'best_params': grid_search.best_params_
    }

    print(f"   ✅ Mejor F1 (CV): {grid_search.best_score_:.4f}")
    print(f"   📋 Mejores parámetros:")
    for param, value in grid_search.best_params_.items():
        param_short = param.replace('classifier__', '')
        print(f"      {param_short}: {value}")

print("\\n" + "=" * 70)
print("RESUMEN DE OPTIMIZACIÓN")
print("=" * 70)
for name, result in results_optimized.items():
    baseline_f1 = results_baseline[name]['f1']
    optimized_f1 = result['best_score']
    improvement = optimized_f1 - baseline_f1
    arrow = "↑" if improvement > 0 else ("↓" if improvement < 0 else "=")
    print(f"   {name}: F1 {baseline_f1:.4f} → {optimized_f1:.4f} ({arrow} {abs(improvement):.4f})")"""))

# =====================================================================
# CELDA 9: Evaluación final
# =====================================================================
cells.append(md_cell("""## 9. Evaluación Final y Comparativa de Modelos

Con los modelos ya optimizados, realizamos la evaluación definitiva sobre el **conjunto de
prueba** (que no se ha utilizado ni durante el entrenamiento ni durante la búsqueda de
hiperparámetros). Esta es la métrica más honesta del rendimiento real del modelo.

Incluimos:
- **Reporte de clasificación** completo (precision, recall, F1 por clase)
- **Matrices de confusión** para visualizar errores
- **Curvas ROC** para comparar discriminación
- **Importancia de características** (para los modelos basados en árboles)"""))

cells.append(code_cell("""# ===========================================================================
# 9.1 EVALUACIÓN EN CONJUNTO DE PRUEBA
# ===========================================================================

print("=" * 70)
print("EVALUACIÓN FINAL EN CONJUNTO DE PRUEBA")
print("=" * 70)

final_results = {}

for name, model in best_models.items():
    y_pred = model.predict(X_test)
    y_proba = model.predict_proba(X_test)[:, 1]

    acc = accuracy_score(y_test, y_pred)
    prec = precision_score(y_test, y_pred)
    rec = recall_score(y_test, y_pred)
    f1 = f1_score(y_test, y_pred)
    fpr, tpr, _ = roc_curve(y_test, y_proba)
    roc_auc_val = auc(fpr, tpr)

    final_results[name] = {
        'Accuracy': acc, 'Precision': prec, 'Recall': rec,
        'F1-Score': f1, 'AUC-ROC': roc_auc_val,
        'y_pred': y_pred, 'y_proba': y_proba, 'fpr': fpr, 'tpr': tpr
    }

    print(f"\\n🔹 {name}:")
    print(f"   Accuracy:  {acc:.4f}")
    print(f"   Precision: {prec:.4f}")
    print(f"   Recall:    {rec:.4f}")
    print(f"   F1-Score:  {f1:.4f}")
    print(f"   AUC-ROC:   {roc_auc_val:.4f}")

# Tabla comparativa final
print("\\n" + "=" * 70)
print("TABLA COMPARATIVA FINAL (Conjunto de Prueba)")
print("=" * 70)
final_df = pd.DataFrame({
    name: {k: v for k, v in vals.items() if k not in ['y_pred', 'y_proba', 'fpr', 'tpr']}
    for name, vals in final_results.items()
}).T
display(final_df.round(4))

# Identificar el mejor modelo
best_model_name = final_df['F1-Score'].idxmax()
print(f"\\n🏆 Mejor modelo por F1-Score: {best_model_name} ({final_df.loc[best_model_name, 'F1-Score']:.4f})")"""))

cells.append(code_cell("""# ===========================================================================
# 9.2 MATRICES DE CONFUSIÓN
# ===========================================================================
fig, axes = plt.subplots(1, 3, figsize=(18, 5))
fig.suptitle('Matrices de Confusión — Modelos Optimizados', fontsize=14, fontweight='bold')

for idx, (name, vals) in enumerate(final_results.items()):
    ConfusionMatrixDisplay.from_predictions(
        y_test, vals['y_pred'],
        display_labels=['No Sobrevivió', 'Sobrevivió'],
        cmap='Blues', ax=axes[idx]
    )
    axes[idx].set_title(f'{name}\\nF1 = {vals["F1-Score"]:.4f}')

plt.tight_layout()
plt.show()"""))

cells.append(code_cell("""# ===========================================================================
# 9.3 CURVAS ROC COMPARATIVAS
# ===========================================================================
fig, ax = plt.subplots(figsize=(8, 6))

colors = ['#2ecc71', '#e74c3c', '#3498db']
for idx, (name, vals) in enumerate(final_results.items()):
    ax.plot(vals['fpr'], vals['tpr'],
            color=colors[idx], lw=2,
            label=f"{name} (AUC = {vals['AUC-ROC']:.4f})")

ax.plot([0, 1], [0, 1], 'k--', lw=1, label='Azar (AUC = 0.5)')
ax.set_xlabel('Tasa de Falsos Positivos (FPR)')
ax.set_ylabel('Tasa de Verdaderos Positivos (TPR)')
ax.set_title('Curvas ROC — Comparativa de Modelos Optimizados', fontweight='bold')
ax.legend(loc='lower right')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()"""))

cells.append(code_cell("""# ===========================================================================
# 9.4 IMPORTANCIA DE CARACTERÍSTICAS (Random Forest optimizado)
# ===========================================================================
rf_model = best_models['Random Forest']
feature_names_num = FEATURES_NUM
feature_names_cat = rf_model.named_steps['preprocessor'] \\
    .named_transformers_['cat'] \\
    .named_steps['onehot'] \\
    .get_feature_names_out(FEATURES_CAT).tolist()
all_features = feature_names_num + feature_names_cat

importances = rf_model.named_steps['classifier'].feature_importances_
feat_imp_df = pd.DataFrame({
    'Feature': all_features,
    'Importancia': importances
}).sort_values('Importancia', ascending=True)

fig, ax = plt.subplots(figsize=(10, 6))
ax.barh(feat_imp_df['Feature'], feat_imp_df['Importancia'], color='#3498db', edgecolor='#2c3e50')
ax.set_xlabel('Importancia')
ax.set_title('Importancia de Características — Random Forest Optimizado', fontweight='bold')
ax.grid(True, alpha=0.3, axis='x')
plt.tight_layout()
plt.show()

print("\\nTop 5 características más importantes:")
for _, row in feat_imp_df.tail(5).iloc[::-1].iterrows():
    print(f"   {row['Feature']:>20s}: {row['Importancia']:.4f}")"""))

cells.append(code_cell("""# ===========================================================================
# 9.5 CURVA DE APRENDIZAJE (Mejor modelo)
# ===========================================================================
print(f"Generando curva de aprendizaje para: {best_model_name}")

train_sizes, train_scores, val_scores = learning_curve(
    best_models[best_model_name], X_train, y_train,
    cv=5, scoring='f1',
    train_sizes=np.linspace(0.1, 1.0, 10),
    n_jobs=-1
)

train_mean = train_scores.mean(axis=1)
train_std = train_scores.std(axis=1)
val_mean = val_scores.mean(axis=1)
val_std = val_scores.std(axis=1)

fig, ax = plt.subplots(figsize=(10, 6))
ax.fill_between(train_sizes, train_mean - train_std, train_mean + train_std, alpha=0.1, color='#2ecc71')
ax.fill_between(train_sizes, val_mean - val_std, val_mean + val_std, alpha=0.1, color='#e74c3c')
ax.plot(train_sizes, train_mean, 'o-', color='#2ecc71', label='F1 Entrenamiento')
ax.plot(train_sizes, val_mean, 'o-', color='#e74c3c', label='F1 Validación')
ax.set_xlabel('Tamaño del conjunto de entrenamiento')
ax.set_ylabel('F1-Score')
ax.set_title(f'Curva de Aprendizaje — {best_model_name}', fontweight='bold')
ax.legend(loc='lower right')
ax.grid(True, alpha=0.3)
plt.tight_layout()
plt.show()

gap = train_mean[-1] - val_mean[-1]
print(f"\\nGap entrenamiento-validación: {gap:.4f}")
if gap < 0.05:
    print("→ El modelo no presenta sobreajuste significativo.")
elif gap < 0.10:
    print("→ El modelo muestra un ligero sobreajuste que podría mejorarse con más datos o regularización.")
else:
    print("→ El modelo presenta sobreajuste. Se recomienda regularización adicional.")"""))

# =====================================================================
# CELDA 10: Consideraciones éticas
# =====================================================================
cells.append(md_cell("""## 10. Consideraciones Éticas

El uso responsable de modelos de Machine Learning exige reflexionar sobre las implicaciones
éticas de nuestras decisiones técnicas. Béranger (2018, pp. 130-145) nos recuerda que todo
algoritmo incorpora, explícita o implícitamente, los valores de quienes lo diseñan.

### Sesgos en el dataset del Titanic

El dataset del Titanic es un caso emblemático de sesgos sociales codificados en datos.
La tasa de supervivencia difiere dramáticamente según el género y la clase socioeconómica:
las mujeres y los pasajeros de primera clase tuvieron tasas de supervivencia significativamente
mayores. Un modelo que aprende estos patrones no hace sino **reflejar desigualdades históricas**.

En mi experiencia con datos judiciales, he visto algo similar: los modelos de predicción de
duración de procesos tienden a replicar los patrones de demora existentes, que a menudo
perjudican desproporcionadamente a quienes tienen menos recursos para acceder a defensa legal.

### Transparencia y explicabilidad

La importancia de características que mostramos en la sección anterior no es solo una herramienta
técnica; es un ejercicio de **transparencia algorítmica**. Cuando un modelo dice que el género
es el predictor más importante de la supervivencia, debemos preguntarnos: ¿queremos que un
sistema automatizado tome decisiones basándose en esa variable? En el contexto del Titanic es
un dato histórico; en un contexto de seguros o crédito, sería discriminación.

Como señala Campesato (2020, p. 18): *"la responsabilidad ética no termina cuando el modelo
alcanza una métrica satisfactoria; apenas comienza"*."""))

# =====================================================================
# CELDA 11: Conclusiones
# =====================================================================
cells.append(md_cell("""## 11. Conclusiones

A lo largo de este portafolio de evidencias, hemos recorrido el ciclo completo de un proyecto
de Machine Learning canalizado: desde la ingesta y exploración de datos hasta la optimización
y evaluación rigurosa de modelos. Las conclusiones principales son:

1. **La canalización previene errores sistemáticos.** Al encapsular preprocesamiento y modelado
   en un `Pipeline` de scikit-learn, eliminamos el riesgo de fuga de datos que se produce
   cuando las transformaciones (como el escalado o la imputación) se calculan sobre el
   dataset completo antes de la división entrenamiento/prueba.

2. **La optimización de hiperparámetros marca la diferencia.** La búsqueda sistemática con
   `GridSearchCV` permitió mejorar el F1-Score de los modelos respecto a sus configuraciones
   por defecto. Este resultado confirma lo que el profesor Valderrama enfatizó en la sesión
   del 20 de junio: no basta con elegir un buen algoritmo; hay que afinarlo.

3. **La comparación de múltiples algoritmos es esencial.** No existe un modelo universalmente
   superior. En nuestro caso, la competencia entre Random Forest, Gradient Boosting y SVM
   nos permitió identificar cuál se adapta mejor a la estructura particular del dataset
   del Titanic, con sus variables mixtas (numéricas y categóricas) y sus datos faltantes.

4. **La evaluación multimétrica ofrece una visión más completa.** Depender solo de accuracy
   puede ser engañoso, especialmente con clases desbalanceadas. Las curvas ROC, el F1-Score
   y las matrices de confusión nos dan perspectivas complementarias sobre el comportamiento
   del modelo.

5. **La reflexión ética no es opcional.** Un modelo técnicamente excelente pero éticamente
   ciego puede causar daño real. La transparencia algorítmica — como el análisis de importancia
   de características — es un primer paso hacia la rendición de cuentas.

A título personal, este ejercicio me recordó que en el ámbito judicial, donde trabajo, la
canalización de procesos es igualmente crucial: automatizar el análisis de expedientes no solo
ahorra tiempo, sino que reduce la variabilidad humana en la toma de decisiones. La lección es
la misma que en ML: un buen pipeline no reemplaza al experto, pero le permite ser más
consistente y eficiente."""))

# =====================================================================
# CELDA 12: Referencias
# =====================================================================
cells.append(md_cell("""## 12. Referencias

Béranger, J. (2018). The Framework for Algorithmic Processing. En *The algorithmic code of
  ethics: Ethics at the bedside of the digital revolution* (pp. 122-164). John Wiley & Sons.

Bonaccorso, G. (2018). A Gentle Introduction to Machine Learning. En *Machine learning
  algorithms: Popular algorithms for data science and machine learning* (2ª ed., pp. 7-27).
  Packt Publishing.

Campesato, O. (2020). Introduction to AI. En *Artificial intelligence, machine learning,
  and deep learning* (pp. 1-21). Mercury Learning and Information.

Del Barrio, D. (2022). *Introducción*. En *Aplicación del Aprendizaje Automático en Modelos
  de Materia Activa* (trabajo de grado, pp. 1-24). Universidad Politécnica de Madrid.

Janiesch, C., Zschech, P. y Heinrich, K. (2021). Machine learning and deep learning.
  *Electronic Markets*, 31, 685-695. https://doi.org/10.1007/s12525-021-00475-2

Pedregosa, F. et al. (2011). Scikit-learn: Machine Learning in Python. *Journal of Machine
  Learning Research*, 12, 2825-2830."""))

# =====================================================================
# Metadatos del notebook
# =====================================================================
notebook = {
    "nbformat": 4,
    "nbformat_minor": 5,
    "metadata": {
        "kernelspec": {
            "display_name": "Python 3",
            "language": "python",
            "name": "python3"
        },
        "language_info": {
            "name": "python",
            "version": "3.10.12",
            "mimetype": "text/x-python",
            "codemirror_mode": {"name": "ipython", "version": 3},
            "file_extension": ".py",
            "pygments_lexer": "ipython3",
            "nbconvert_exporter": "python"
        },
        "colab": {
            "provenance": [],
            "gpuType": "T4"
        },
        "accelerator": "GPU"
    },
    "cells": cells
}

# ─── Guardar ──────────────────────────────────────────────────────────
SCRIPT_DIR = os.path.dirname(os.path.abspath(__file__))
OUTPUT = os.path.join(SCRIPT_DIR, "Oviedo_Alexander_Portafolio_S8_MLA.ipynb")

with open(OUTPUT, "w", encoding="utf-8") as f:
    json.dump(notebook, f, indent=1, ensure_ascii=False)

print(f"✅ Notebook generado: {OUTPUT}")
print(f"   Celdas totales: {len(cells)}")
md_count = sum(1 for c in cells if c['cell_type'] == 'markdown')
code_count = sum(1 for c in cells if c['cell_type'] == 'code')
print(f"   Markdown: {md_count}, Código: {code_count}")
