# -*- coding: utf-8 -*-
"""Copia de RemiCash_PoC_Entrega2.ipynb

Automatically generated by Colab.

Original file is located at
    https://colab.research.google.com/drive/1Ui3cosva7HF7c0-JOjRt_6PjzFHbl91M

🏦 RemiCash — Agente de Scoring Crediticio Alternativo
## Entrega 2: Prototipo Funcional y Resultados Iniciales
**Data Science & IA · Máster Fintech, Blockchain y Mercados Financieros · UB 2025–2026**

Equipo: Kilian Martorell · Alison Berbetty · Ronald Rodriguez · Andrés Felipe Vera

---
### Flujo del PoC
```
Datos Open Banking (sintéticos)
        ↓
Preprocesamiento & Feature Engineering
        ↓
Modelo ML (XGBoost) + Métricas
        ↓
Explicabilidad SHAP
        ↓
Agente LLM (Claude API) → Decisión explicable
```

## 0. Instalación de dependencias
"""

# Ejecutar solo si no están instaladas
!pip install xgboost shap pandas numpy scikit-learn matplotlib seaborn anthropic

"""## 1. Importaciones"""

import numpy as np
import pandas as pd
import matplotlib.pyplot as plt
import matplotlib.patches as mpatches
import seaborn as sns
import warnings
warnings.filterwarnings('ignore')

from sklearn.model_selection import train_test_split, StratifiedKFold, cross_val_score
from sklearn.preprocessing import StandardScaler
from sklearn.linear_model import LogisticRegression
from sklearn.ensemble import RandomForestClassifier
from sklearn.metrics import (
    roc_auc_score, classification_report, confusion_matrix,
    f1_score, precision_score, recall_score, roc_curve
)
from xgboost import XGBClassifier
import shap
import anthropic
import json

# Seed para reproducibilidad
SEED = 42
np.random.seed(SEED)

print('✅ Librerías cargadas correctamente')

"""## 2. Generación del Dataset Sintético — Open Banking Simulado

Simulamos perfiles de migrantes latinoamericanos en España con variables de comportamiento financiero accesibles vía PSD2/Open Banking. No se usan datos reales (cumplimiento GDPR).
"""

def generar_dataset_openbanking(n=1000, seed=42):
    rng = np.random.default_rng(seed)

    # Grupos de riesgo: 70% bajo riesgo, 30% alto riesgo
    n_bajo = int(n * 0.70)
    n_alto = n - n_bajo

    # --- Perfil BAJO RIESGO (buenos pagadores) ---
    ingreso_bajo     = rng.normal(2000, 400, n_bajo).clip(1200, 4000)
    antiguedad_bajo  = rng.integers(12, 60, n_bajo)
    ratio_bajo       = rng.beta(2, 5, n_bajo).clip(0.30, 0.65)
    remesas_bajo     = rng.integers(2, 12, n_bajo)
    regular_bajo     = rng.beta(6, 2, n_bajo).clip(0.65, 1.0)
    desc_bajo        = rng.integers(0, 2, n_bajo)
    variab_bajo      = rng.exponential(80, n_bajo).clip(0, 250)
    cuentas_bajo     = rng.integers(1, 4, n_bajo)
    cuotas_bajo      = rng.normal(100, 80, n_bajo).clip(0, 350)

    # --- Perfil ALTO RIESGO (malos pagadores) ---
    ingreso_alto     = rng.normal(1100, 300, n_alto).clip(700, 2000)
    antiguedad_alto  = rng.integers(1, 18, n_alto)
    ratio_alto       = rng.beta(5, 2, n_alto).clip(0.70, 0.97)
    remesas_alto     = rng.integers(0, 4, n_alto)
    regular_alto     = rng.beta(2, 5, n_alto).clip(0.10, 0.50)
    desc_alto        = rng.integers(2, 8, n_alto)
    variab_alto      = rng.exponential(300, n_alto).clip(100, 900)
    cuentas_alto     = rng.integers(1, 3, n_alto)
    cuotas_alto      = rng.normal(300, 100, n_alto).clip(150, 600)

    # Concatenar
    ingreso     = np.concatenate([ingreso_bajo, ingreso_alto])
    antiguedad  = np.concatenate([antiguedad_bajo, antiguedad_alto])
    ratio       = np.concatenate([ratio_bajo, ratio_alto])
    remesas     = np.concatenate([remesas_bajo, remesas_alto])
    regular     = np.concatenate([regular_bajo, regular_alto])
    desc        = np.concatenate([desc_bajo, desc_alto])
    variab      = np.concatenate([variab_bajo, variab_alto])
    cuentas     = np.concatenate([cuentas_bajo, cuentas_alto])
    cuotas      = np.concatenate([cuotas_bajo, cuotas_alto])
    ahorro      = (ingreso * (1 - ratio) - cuotas).clip(0, 1500)

    # Variable objetivo con muy poco ruido
    impago_base = np.concatenate([
        np.zeros(n_bajo, dtype=int),
        np.ones(n_alto, dtype=int)
    ])
    # Pequeño ruido realista (5%)
    flip = rng.random(n) < 0.05
    impago = np.where(flip, 1 - impago_base, impago_base)

    # Shuffle para mezclar los grupos
    idx = rng.permutation(n)

    df = pd.DataFrame({
        'ingreso_mensual_eur':       ingreso[idx].round(2),
        'antiguedad_cuenta_meses':   antiguedad[idx],
        'ratio_gasto_ingreso':       ratio[idx].round(4),
        'frecuencia_remesas_anual':  remesas[idx],
        'regularidad_ingresos':      regular[idx].round(4),
        'descubiertos_12m':          desc[idx],
        'variabilidad_ingresos_eur': variab[idx].round(2),
        'num_cuentas_bancarias':     cuentas[idx],
        'cuotas_deuda_mensual_eur':  cuotas[idx].round(2),
        'ahorro_medio_mensual_eur':  ahorro[idx].round(2),
        'impago':                    impago[idx]
    })
    return df

df = generar_dataset_openbanking(n=1000)
print(f'Dataset generado: {df.shape[0]} perfiles, {df.shape[1]-1} features')
print(f'Tasa de impago en dataset: {df["impago"].mean():.1%}')
df.head()

# Distribución de la variable objetivo
fig, axes = plt.subplots(1, 2, figsize=(12, 4))

# Pie chart
counts = df['impago'].value_counts()
axes[0].pie(counts, labels=['Paga (0)', 'Impago (1)'],
            colors=['#2E86AB', '#E84855'], autopct='%1.1f%%',
            startangle=90, textprops={'fontsize': 12})
axes[0].set_title('Distribución variable objetivo', fontsize=13, fontweight='bold')

# Histograma ingreso por clase
for label, color in [(0, '#2E86AB'), (1, '#E84855')]:
    axes[1].hist(df[df['impago']==label]['ingreso_mensual_eur'],
                 bins=30, alpha=0.6, color=color,
                 label='Paga' if label==0 else 'Impago')
axes[1].set_xlabel('Ingreso mensual (€)')
axes[1].set_ylabel('Frecuencia')
axes[1].set_title('Distribución ingresos por clase', fontsize=13, fontweight='bold')
axes[1].legend()

plt.suptitle('RemiCash PoC — Análisis Exploratorio del Dataset', fontsize=14, y=1.02)
plt.tight_layout()
plt.savefig('eda_distribucion.png', dpi=150, bbox_inches='tight')
plt.show()
print('✅ Figura guardada: eda_distribucion.png')

"""## 3. Preprocesamiento & Feature Engineering"""

FEATURES = [
    'ingreso_mensual_eur',
    'antiguedad_cuenta_meses',
    'ratio_gasto_ingreso',
    'frecuencia_remesas_anual',
    'regularidad_ingresos',
    'descubiertos_12m',
    'variabilidad_ingresos_eur',
    'num_cuentas_bancarias',
    'cuotas_deuda_mensual_eur',
    'ahorro_medio_mensual_eur'
]
TARGET = 'impago'

X = df[FEATURES]
y = df[TARGET]

# Train / test split estratificado
X_train, X_test, y_train, y_test = train_test_split(
    X, y, test_size=0.2, random_state=SEED, stratify=y
)

print(f'Train: {X_train.shape[0]} muestras | Test: {X_test.shape[0]} muestras')
print(f'Tasa impago train: {y_train.mean():.1%} | test: {y_test.mean():.1%}')

"""## 4. Modelos ML — Entrenamiento y Evaluación Comparativa"""

modelos = {
    'Logistic Regression (baseline)': LogisticRegression(
        max_iter=1000, random_state=SEED, class_weight='balanced'
    ),
    'Random Forest': RandomForestClassifier(
        n_estimators=100, random_state=SEED, class_weight='balanced'
    ),
    'XGBoost': XGBClassifier(
        n_estimators=100, learning_rate=0.1, max_depth=4,
        use_label_encoder=False, eval_metric='logloss',
        random_state=SEED, scale_pos_weight=(y_train==0).sum()/(y_train==1).sum()
    )
}

resultados = []
cv = StratifiedKFold(n_splits=5, shuffle=True, random_state=SEED)

for nombre, modelo in modelos.items():
    modelo.fit(X_train, y_train)
    y_pred = modelo.predict(X_test)
    y_proba = modelo.predict_proba(X_test)[:, 1]

    auc    = roc_auc_score(y_test, y_proba)
    f1     = f1_score(y_test, y_pred)
    prec   = precision_score(y_test, y_pred)
    rec    = recall_score(y_test, y_pred)
    cv_auc = cross_val_score(modelo, X, y, cv=cv, scoring='roc_auc').mean()

    resultados.append({
        'Modelo': nombre,
        'AUC-ROC (test)': round(auc, 4),
        'AUC-ROC (CV-5)': round(cv_auc, 4),
        'F1-Score':       round(f1, 4),
        'Precision':      round(prec, 4),
        'Recall':         round(rec, 4)
    })
    print(f'{nombre:40s} | AUC: {auc:.3f} | F1: {f1:.3f}')

df_resultados = pd.DataFrame(resultados)
print('\n--- Tabla comparativa ---')
df_resultados

# Curvas ROC comparativas
fig, axes = plt.subplots(1, 2, figsize=(14, 5))
colores = ['#7C83FD', '#2E86AB', '#E84855']

for (nombre, modelo), color in zip(modelos.items(), colores):
    y_proba = modelo.predict_proba(X_test)[:, 1]
    fpr, tpr, _ = roc_curve(y_test, y_proba)
    auc = roc_auc_score(y_test, y_proba)
    axes[0].plot(fpr, tpr, color=color, lw=2, label=f'{nombre} (AUC={auc:.3f})')

axes[0].plot([0,1],[0,1], 'k--', alpha=0.4)
axes[0].axhline(y=0.75, color='gray', linestyle=':', alpha=0.7, label='Objetivo mínimo AUC=0.75')
axes[0].set_xlabel('Tasa de Falsos Positivos')
axes[0].set_ylabel('Tasa de Verdaderos Positivos')
axes[0].set_title('Curvas ROC — Comparativa de Modelos', fontweight='bold')
axes[0].legend(fontsize=9)
axes[0].grid(alpha=0.3)

# Bar chart métricas XGBoost (modelo seleccionado)
metricas_xgb = df_resultados[df_resultados['Modelo']=='XGBoost'][['AUC-ROC (test)','F1-Score','Precision','Recall']].values[0]
nombres_m = ['AUC-ROC', 'F1-Score', 'Precision', 'Recall']
objetivos = [0.75, 0.70, 0.70, 0.70]
bars = axes[1].bar(nombres_m, metricas_xgb, color='#2E86AB', alpha=0.8)
axes[1].bar(nombres_m, objetivos, color='#E84855', alpha=0.3, label='Objetivo mínimo')
for bar, val in zip(bars, metricas_xgb):
    axes[1].text(bar.get_x() + bar.get_width()/2, bar.get_height()+0.01,
                 f'{val:.3f}', ha='center', fontsize=10, fontweight='bold')
axes[1].set_ylim(0, 1.1)
axes[1].set_title('XGBoost — Métricas vs Objetivos PoC', fontweight='bold')
axes[1].legend()
axes[1].grid(axis='y', alpha=0.3)

plt.suptitle('RemiCash PoC — Evaluación del Modelo', fontsize=13)
plt.tight_layout()
plt.savefig('metricas_modelos.png', dpi=150, bbox_inches='tight')
plt.show()
print('✅ Figura guardada: metricas_modelos.png')

# Modelo seleccionado: XGBoost
modelo_final = modelos['XGBoost']
y_pred_final = modelo_final.predict(X_test)
y_proba_final = modelo_final.predict_proba(X_test)[:, 1]

# Matriz de confusión
cm = confusion_matrix(y_test, y_pred_final)
fig, ax = plt.subplots(figsize=(5, 4))
sns.heatmap(cm, annot=True, fmt='d', cmap='Blues',
            xticklabels=['Pred: Paga', 'Pred: Impago'],
            yticklabels=['Real: Paga', 'Real: Impago'])
ax.set_title('Matriz de Confusión — XGBoost', fontweight='bold')
plt.tight_layout()
plt.savefig('confusion_matrix.png', dpi=150, bbox_inches='tight')
plt.show()

# Tasa de mora simulada
aprobados = (y_pred_final == 0).sum()
mora_real = ((y_pred_final == 0) & (y_test == 1)).sum()
tasa_mora = mora_real / aprobados if aprobados > 0 else 0
print(f'\n📊 Análisis de mora simulada:')
print(f'   Créditos aprobados:  {aprobados}')
print(f'   Impagos reales:      {mora_real}')
print(f'   Tasa de mora sim.:   {tasa_mora:.1%} (benchmark RemiCash: <3.5%)')
print(f'   ✅ Objetivo cumplido: {"SÍ" if tasa_mora < 0.035 else "NO"}')

"""## 5. Explicabilidad XAI — Análisis SHAP"""

# SHAP explainer para XGBoost
explainer = shap.TreeExplainer(modelo_final)
shap_values = explainer.shap_values(X_test)

# SHAP summary plot
plt.figure(figsize=(10, 6))
shap.summary_plot(shap_values, X_test, feature_names=FEATURES,
                  plot_type='bar', show=False, color='#2E86AB')
plt.title('SHAP — Importancia de Variables (Top Features)', fontsize=13, fontweight='bold')
plt.tight_layout()
plt.savefig('shap_importancia.png', dpi=150, bbox_inches='tight')
plt.show()
print('✅ Figura SHAP guardada')

# Top 5 features por importancia SHAP
mean_shap = np.abs(shap_values).mean(axis=0)
top5_idx = np.argsort(mean_shap)[::-1][:5]
top5_features = [(FEATURES[i], mean_shap[i]) for i in top5_idx]

print('🔍 Top 5 variables más importantes (SHAP):')
for i, (feat, val) in enumerate(top5_features, 1):
    print(f'   {i}. {feat:<35} SHAP: {val:.4f}')

"""## 6. Agente LLM — Justificación de Decisión (Claude API)

El agente recibe el score + SHAP values del perfil y genera una explicación en lenguaje natural: aprobado/rechazado + motivo. Cumple requisito de explicabilidad del AI Act.
"""

def construir_perfil_usuario(idx, X_test, y_proba, y_pred, shap_vals, top_features):
    """
    Construye un dict con el perfil del usuario para enviarlo al agente LLM.
    """
    perfil = X_test.iloc[idx].to_dict()
    score = float(y_proba[idx])
    decision = 'RECHAZADO' if y_pred[idx] == 1 else 'APROBADO'

    # SHAP locales para este perfil
    shap_locales = {
        feat: float(shap_vals[idx][FEATURES.index(feat)])
        for feat, _ in top_features
    }

    return {
        'decision': decision,
        'probabilidad_impago': round(score, 4),
        'perfil_financiero': {k: round(v, 2) if isinstance(v, float) else v
                               for k, v in perfil.items()},
        'factores_decision_shap': shap_locales
    }


def agente_scoring_llm(perfil_dict, api_key):
    """
    Agente LLM que genera explicación de la decisión crediticia.
    Usa Claude API (claude-sonnet-4-20250514).
    """
    client = anthropic.Anthropic(api_key=api_key)

    system_prompt = """Eres el agente de scoring crediticio de RemiCash, una plataforma fintech
que ofrece microcréditos vinculados a remesas para migrantes latinoamericanos en España.

Tu rol es explicar en lenguaje claro y empático la decisión crediticia generada por el modelo ML,
siguiendo los requisitos de explicabilidad del AI Act (Art. 13 y 86).

REGLAS:
- Máximo 150 palabras
- Tono empático y profesional
- Menciona exactamente los 3 factores más relevantes (positivos o negativos)
- Si es RECHAZADO, ofrece siempre 2 acciones concretas que el usuario puede tomar
- Si es APROBADO, confirma el importe y condición de la remesa
- Nunca uses jerga técnica (no menciones SHAP, modelo, XGBoost, probabilidades)
- Escribe en español, segunda persona ("tú")"""

    user_message = f"""Genera la justificación de la siguiente decisión crediticia:

DECISIÓN: {perfil_dict['decision']}
Probabilidad de impago: {perfil_dict['probabilidad_impago']:.1%}

Perfil financiero del usuario:
{json.dumps(perfil_dict['perfil_financiero'], indent=2, ensure_ascii=False)}

Factores que más influyeron en la decisión (valores positivos = aumentan riesgo):
{json.dumps(perfil_dict['factores_decision_shap'], indent=2, ensure_ascii=False)}

Genera la explicación para el usuario."""

    response = client.messages.create(
        model='claude-sonnet-4-20250514',
        max_tokens=300,
        system=system_prompt,
        messages=[{'role': 'user', 'content': user_message}]
    )

    return response.content[0].text


print('✅ Funciones del agente LLM definidas')

# ⚠️ Reemplaza con tu API key de Anthropic
# Puedes obtenerla en: https://console.anthropic.com
API_KEY = 'CONFIGURAR_EN_VARIABLE_DE_ENTORNO'  # Clave eliminada en la version publica

# --- Demo: 2 perfiles (1 aprobado, 1 rechazado) ---
# Buscar un perfil aprobado y uno rechazado en el test set
idx_aprobado  = np.where(y_pred_final == 0)[0][0]
idx_rechazado = np.where(y_pred_final == 1)[0][0]

for label, idx in [('✅ PERFIL APROBADO', idx_aprobado),
                   ('❌ PERFIL RECHAZADO', idx_rechazado)]:

    perfil = construir_perfil_usuario(
        idx, X_test, y_proba_final, y_pred_final, shap_values, top5_features
    )

    print(f'\n{"="*60}')
    print(f'{label}')
    print(f'{"="*60}')
    print(f'Score impago: {perfil["probabilidad_impago"]:.1%}')
    print(f'Ingreso mensual: {perfil["perfil_financiero"]["ingreso_mensual_eur"]}€')
    print(f'Ratio gasto/ingreso: {perfil["perfil_financiero"]["ratio_gasto_ingreso"]:.1%}')

    if API_KEY and API_KEY != 'CONFIGURAR_EN_VARIABLE_DE_ENTORNO':
        print('\n💬 Explicación del Agente LLM:')
        explicacion = agente_scoring_llm(perfil, API_KEY)
        print(explicacion)
    else:
        print('\n[⚠️ Agrega tu API key para ver la explicación del agente LLM]')

"""## 7. Análisis de Errores & Riesgos"""

# Análisis de errores por tipo
falsos_positivos = ((y_pred_final == 1) & (y_test.values == 0)).sum()
falsos_negativos = ((y_pred_final == 0) & (y_test.values == 1)).sum()
verdaderos_pos   = ((y_pred_final == 1) & (y_test.values == 1)).sum()
verdaderos_neg   = ((y_pred_final == 0) & (y_test.values == 0)).sum()

print('📋 ANÁLISIS DE ERRORES — XGBoost en test set')
print(f'{'='*50}')
print(f'Verdaderos Negativos (paga, pred. paga):     {verdaderos_neg:4d} ✅')
print(f'Verdaderos Positivos (impago, pred. impago): {verdaderos_pos:4d} ✅')
print(f'Falsos Positivos (paga, pred. impago):       {falsos_positivos:4d} ⚠️  (excluye buenos pagadores)')
print(f'Falsos Negativos (impago, pred. paga):       {falsos_negativos:4d} 🚨 (riesgo financiero real)')
print(f'{'='*50}')
print(f'\nCobertura de usuarios (decisión emitida):    {(1 - (falsos_negativos/len(y_test))):.1%}')
print(f'Tasa mora simulada (FN/aprobados):           {tasa_mora:.1%}')

riesgos = [
    {
        'ID': 'R01',
        'Riesgo': 'Sesgo en datos sintéticos',
        'Impacto': 'Alto',
        'Probabilidad': 'Media',
        'Mitigación': 'Validar con datos reales anonimizados en producción; test de fairness por origen'
    },
    {
        'ID': 'R02',
        'Riesgo': 'Overfitting del modelo XGBoost',
        'Impacto': 'Alto',
        'Probabilidad': 'Baja',
        'Mitigación': 'Cross-val 5-fold implementada; regularización L2; monitoreo AUC en prod'
    },
    {
        'ID': 'R03',
        'Riesgo': 'Hallucination del agente LLM',
        'Impacto': 'Alto',
        'Probabilidad': 'Media',
        'Mitigación': 'Prompting estructurado con reglas rígidas; validación de output antes de mostrar al usuario'
    },
    {
        'ID': 'R04',
        'Riesgo': 'Incumplimiento GDPR / PSD2',
        'Impacto': 'Crítico',
        'Probabilidad': 'Baja',
        'Mitigación': 'Solo datos sintéticos en PoC; consentimiento explícito en arquitectura productiva'
    },
    {
        'ID': 'R05',
        'Riesgo': 'Alta tasa de FP (exclusión de buenos pagadores)',
        'Impacto': 'Medio',
        'Probabilidad': 'Media',
        'Mitigación': 'Ajustar umbral de decisión; añadir variable de comportamiento de remesas'
    }
]

df_riesgos = pd.DataFrame(riesgos)
print('\n\n📋 MATRIZ DE RIESGOS:')
df_riesgos

"""## 8. Resumen de Resultados"""

xgb_row = df_resultados[df_resultados['Modelo']=='XGBoost'].iloc[0]

print('='*60)
print('📊 RESUMEN DE RESULTADOS — RemiCash PoC Entrega 2')
print('='*60)
print(f'Modelo seleccionado: XGBoost')
print(f'Dataset: {len(df)} perfiles sintéticos (Open Banking simulado)')
print()
print('MÉTRICAS:')
print(f'  AUC-ROC (test):   {xgb_row["AUC-ROC (test)"]:.3f}   (objetivo ≥0.75 ✅)'
      if xgb_row['AUC-ROC (test)'] >= 0.75 else
      f'  AUC-ROC (test):   {xgb_row["AUC-ROC (test)"]:.3f}   (objetivo ≥0.75 ❌)')
print(f'  F1-Score:         {xgb_row["F1-Score"]:.3f}   (objetivo ≥0.70 {"✅" if xgb_row["F1-Score"]>=0.70 else "❌"})')
print(f'  Tasa mora sim.:   {tasa_mora:.1%}   (objetivo <3.5% {"✅" if tasa_mora<0.035 else "❌"})')
print()
print('COMPONENTES IMPLEMENTADOS:')
print('  ✅ Dataset Open Banking sintético (1000 perfiles)')
print('  ✅ Modelo XGBoost + comparativa LR / RF')
print('  ✅ Validación cruzada k-fold (k=5)')
print('  ✅ Análisis SHAP (Top-5 features identificadas)')
print('  ✅ Agente LLM (Claude API) — decisiones explicables')
print('  ✅ Matriz de riesgos + plan de mitigación')
print()
print('TOP 5 FEATURES (SHAP):')
for i, (feat, val) in enumerate(top5_features, 1):
    print(f'  {i}. {feat}')
print('='*60)