Código fuente para generar_datos

"""
GENERADOR DE DATOS SINTÉTICOS DE CRÉDITO HIPOTECARIO - COLOMBIA
Versión: 1.3 - REALISTA CORREGIDA
Autor: Sistema de Riesgo Crediticio
Fecha: 2024

CORRECCIONES REALISTAS IMPLEMENTADAS:
1. Distribución realista de riesgo: 60% Bajo, 25% Medio, 15% Alto
2. Correlaciones más suaves y creíbles
3. Capacidad residual 100% positiva
4. Impacto realista de demandas en puntaje
"""

import numpy as np
import pandas as pd
import warnings
from typing import Dict, List, Tuple
from datetime import datetime
import json

warnings.filterwarnings('ignore')

class GeneradorCreditoHipotecarioRealista:
    """
    Generador de datos sintéticos de crédito hipotecario para Colombia
    con distribución REALISTA de riesgo y correlaciones creíbles.
    """
    
    def __init__(self, n_registros: int = 10000, semilla: int = 42):
        """
        Inicializa el generador con parámetros realistas
        """
        self.n = n_registros
        self.semilla_base = semilla
        self.df = pd.DataFrame()
        
        np.random.seed(semilla)
        self._setup_configuracion_realista()
        
        print(f"✓ Generador REALISTA inicializado para {n_registros:,} registros")
        print(f"✓ Semilla base: {semilla}")
        print(f"✓ Objetivo riesgo: 60% Bajo, 25% Medio, 15% Alto")
    
    def _setup_configuracion_realista(self):
        """Configuración con parámetros REALISTAS"""
        
        self.ciudades = {
            "Bogotá": 0.35, "Medellín": 0.18, "Cali": 0.12, "Barranquilla": 0.08,
            "Cartagena": 0.05, "Bucaramanga": 0.04, "Pereira": 0.03, "Cúcuta": 0.03,
            "Manizales": 0.02, "Santa Marta": 0.02, "Ibagué": 0.02, "Villavicencio": 0.02,
            "Pasto": 0.01, "Montería": 0.01, "Otras": 0.02
        }
        
        self.estratos_por_ciudad = {
            "Bogotá": [0.05, 0.25, 0.35, 0.20, 0.10, 0.05],
            "Medellín": [0.06, 0.28, 0.35, 0.18, 0.08, 0.05],
            "Cali": [0.10, 0.38, 0.30, 0.14, 0.06, 0.02],
            "Barranquilla": [0.10, 0.38, 0.30, 0.14, 0.06, 0.02],
            "grande": [0.10, 0.38, 0.30, 0.14, 0.06, 0.02],
            "intermedia": [0.12, 0.42, 0.28, 0.12, 0.04, 0.02],
            "pequeña": [0.15, 0.48, 0.25, 0.08, 0.03, 0.01]
        }
        
        self.multiplicador_salario_ciudad = {
            "Bogotá": 1.15, "Medellín": 1.05, "Cali": 1.00, "Barranquilla": 0.98,
            "Cartagena": 0.95, "Bucaramanga": 0.92, "Pereira": 0.90, "Cúcuta": 0.88,
            "Manizales": 0.88, "Santa Marta": 0.90, "Ibagué": 0.88, "Villavicencio": 0.87,
            "Pasto": 0.85, "Montería": 0.83, "Otras": 0.80
        }
        
        # SALARIOS MÁS REALISTAS
        self.salario_base_educacion = {
            "Bachiller": (1800000, 500000, 1200000, 3500000),  # Reducidos
            "Técnico": (2800000, 700000, 1800000, 5000000),
            "Profesional": (4500000, 1500000, 3000000, 10000000),
            "Posgrado": (8000000, 3000000, 5000000, 20000000)
        }
        
        # VALORES DE INMUEBLES MÁS CONSERVADORES
        self.valores_inmuebles = {
            "Bogotá": [(60, 100), (80, 150), (120, 200), (180, 300), (300, 500), (550, 900)],
            "Medellín": [(50, 85), (75, 130), (100, 180), (150, 250), (250, 400), (450, 800)],
            "Cali": [(45, 75), (65, 110), (90, 150), (130, 220), (220, 350), (400, 650)],
            "Barranquilla": [(40, 70), (60, 100), (85, 140), (120, 200), (200, 320), (350, 600)],
            "grande": [(40, 70), (60, 100), (85, 140), (120, 200), (200, 320), (350, 600)],
            "intermedia": [(30, 55), (45, 80), (65, 110), (95, 160), (160, 250), (280, 450)],
            "pequeña": [(20, 45), (35, 65), (50, 90), (75, 130), (130, 200), (220, 350)]
        }
    
    def generar(self) -> pd.DataFrame:
        """
        Genera el dataset completo con distribución REALISTA
        """
        print("\n" + "="*70)
        print("INICIANDO GENERACIÓN DE DATOS SINTÉTICOS - VERSIÓN REALISTA")
        print("="*70)
        
        fases = [
            ("Generando variables demográficas...", self._fase_demografica),
            ("Generando variables laborales...", self._fase_laboral),
            ("Generando variables financieras...", self._fase_financiera),
            ("Generando variables del crédito...", self._fase_credito),
            ("Generando características derivadas...", self._fase_caracteristicas),
            ("Calculando nivel de riesgo REALISTA...", self._fase_riesgo_realista)
        ]
        
        for i, (mensaje, metodo) in enumerate(fases, 1):
            print(f"\n[FASE {i}/6] {mensaje}")
            metodo()
            print(f"✓ Fase {i} completada")
        
        print("\n[VALIDACIÓN] Ejecutando validaciones REALISTAS...")
        self._validar_restricciones_realistas()
        print("✓ Validaciones completadas")
        
        print("\n" + "="*70)
        print(f"✓✓✓ GENERACIÓN COMPLETADA: {len(self.df):,} registros")
        print("="*70)
        
        return self.df
    
    def _fase_demografica(self):
        """Fase 1: Variables demográficas"""
        np.random.seed(self.semilla_base)
        # Edad más realista
        self.df['edad'] = np.clip(np.random.normal(38, 10, self.n), 22, 65).astype(int)
        
        ciudades = list(self.ciudades.keys())
        probs = list(self.ciudades.values())
        self.df['ciudad'] = np.random.choice(ciudades, size=self.n, p=probs)
        
        self._generar_estrato()
        self._generar_educacion()
        self._generar_estado_civil()
        self._generar_personas_a_cargo()
    
    def _fase_laboral(self):
        """Fase 2: Variables laborales"""
        self._generar_tipo_empleo()
        self._generar_antiguedad_laboral()
        self._generar_salario_realista()
        self._generar_egresos_realistas()
    
    def _fase_financiera(self):
        """Fase 3: Variables financieras"""
        self._generar_demandas_realistas()
        self._generar_puntaje_datacredito_realista()
        self._generar_propiedades()
        self._generar_patrimonio_realista()
        self._generar_saldo_banco_realista()
    
    def _fase_credito(self):
        """Fase 4: Variables del crédito - VERSIÓN REALISTA"""
        self._generar_valor_inmueble_realista()
        self._generar_anos_inmueble()
        self._generar_cuota_inicial_realista()
        self._generar_monto_credito()
        self._generar_plazo_realista()
        self._generar_tasa_interes_realista()
        self._calcular_cuota_mensual_realista()
    
    def _fase_caracteristicas(self):
        """Fase 5: Características derivadas"""
        self._generar_caracteristicas_derivadas()
    
    def _fase_riesgo_realista(self):
        """Fase 6: Nivel de riesgo REALISTA"""
        self._calcular_nivel_riesgo_realista()
        self._ajustar_capacidad_residual_positiva()

    # ========================================================================
    # MÉTODOS REALISTAS CORREGIDOS
    # ========================================================================

    def _generar_salario_realista(self):
        """Genera salario mensual con rangos más realistas"""
        np.random.seed(self.semilla_base + 800)
        salarios = []
        
        for idx, row in self.df.iterrows():
            educacion = row['nivel_educacion']
            antiguedad = row['antiguedad_empleo']
            ciudad = row['ciudad']
            tipo_empleo = row['tipo_empleo']
            estrato = row['estrato_socioeconomico']
            
            media, sd, minimo, maximo = self.salario_base_educacion[educacion]
            
            # Distribución más realista (menos extrema)
            salario = np.random.lognormal(np.log(media), 0.3)
            
            factor_antiguedad = min(1 + (0.02 * antiguedad), 1.50)  # Más conservador
            salario *= factor_antiguedad
            
            factor_ciudad = self.multiplicador_salario_ciudad.get(ciudad, 0.85)
            salario *= factor_ciudad
            
            if tipo_empleo == "Formal":
                salario *= 1.00
            elif tipo_empleo == "Independiente":
                salario *= 1.05  # Menos ventaja
            else:
                salario *= 0.80  # Menos penalización
            
            salario = max(minimo, min(salario, maximo))
            
            # Estratos más realistas
            if estrato == 1:
                salario = np.clip(salario, 1000000, 2200000)
            elif estrato == 2:
                salario = np.clip(salario, 1500000, 3500000)
            elif estrato == 3:
                salario = np.clip(salario, 2000000, 6000000)
            elif estrato == 4:
                salario = np.clip(salario, 3500000, 9000000)
            elif estrato == 5:
                salario = np.clip(salario, 6000000, 15000000)
            elif estrato == 6:
                salario = np.clip(salario, 8000000, 25000000)
            
            salario *= np.random.normal(1.0, 0.06)  # Menos variación
            salario = round(salario / 1000) * 1000
            
            salarios.append(salario)
        
        self.df['salario_mensual'] = salarios

    def _generar_egresos_realistas(self):
        """Genera egresos mensuales que garantizan capacidad residual positiva"""
        np.random.seed(self.semilla_base + 900)
        egresos = []
        
        for idx, row in self.df.iterrows():
            salario = row['salario_mensual']
            estrato = row['estrato_socioeconomico']
            personas = row['personas_a_cargo']
            
            # GARANTIZAR MARGEN PARA CUOTA HIPOTECARIA
            if estrato <= 2:
                # Estratos bajos gastan más porcentaje pero menos absoluto
                factor_gastos = np.random.uniform(0.65, 0.75)
            elif estrato <= 4:
                factor_gastos = np.random.uniform(0.55, 0.65)
            else:
                factor_gastos = np.random.uniform(0.45, 0.55)
            
            egreso_base = salario * factor_gastos
            
            # Gastos por personas (más conservador)
            gastos_personas = personas * 250000  # Reducido
            
            egreso_total = egreso_base + gastos_personas
            
            # GARANTIZAR MÍNIMO DEL 25% DE CAPACIDAD DE AHORRO
            egreso_maximo = salario * 0.75
            egreso_total = min(egreso_total, egreso_maximo)
            
            # GARANTIZAR GASTOS MÍNIMOS REALISTAS
            egreso_minimo = salario * 0.40
            egreso_total = max(egreso_total, egreso_minimo)
            
            egreso_total = round(egreso_total / 1000) * 1000
            egresos.append(egreso_total)
        
        self.df['egresos_mensuales'] = egresos

    def _generar_demandas_realistas(self):
        """Genera número de demandas legales con distribución realista"""
        np.random.seed(self.semilla_base + 1000)
        demandas = []
        
        for idx, row in self.df.iterrows():
            tipo_empleo = row['tipo_empleo']
            
            # DISTRIBUCIÓN REALISTA: mayoría sin demandas
            if tipo_empleo == "Informal":
                probs = [0.80, 0.15, 0.04, 0.01]  # 80% sin demandas
            elif tipo_empleo == "Formal":
                probs = [0.92, 0.06, 0.015, 0.005]  # 92% sin demandas
            else:  # Independiente
                probs = [0.85, 0.10, 0.04, 0.01]  # 85% sin demandas
            
            num_demandas = np.random.choice([0, 1, 2, 3], p=probs)
            demandas.append(num_demandas)
        
        self.df['numero_demandas'] = demandas

    def _generar_puntaje_datacredito_realista(self):
        """PUNTAJE DATACRÉDITO CON CORRELACIÓN MÁS REALISTA CON DEMANDAS"""
        np.random.seed(self.semilla_base + 1100)
        puntajes = []
        
        for idx, row in self.df.iterrows():
            demandas = row['numero_demandas']
            tipo_empleo = row['tipo_empleo']
            edad = row['edad']
            salario = row['salario_mensual']
            educacion = row['nivel_educacion']
            antiguedad = row['antiguedad_empleo']
            egresos = row['egresos_mensuales']
            
            # BASE MÁS ALTA - mayoría tiene buen historial
            puntaje = 720  # Base realista (antes 650)
            
            # IMPACTO MÁS SUAVE DE DEMANDAS (CORRELACIÓN REALISTA)
            if demandas == 0:
                puntaje += 20
            elif demandas == 1:
                puntaje -= 40  # Antes -150! (mucho más suave)
            elif demandas == 2:
                puntaje -= 90
            else:
                puntaje -= 150
            
            if tipo_empleo == "Formal":
                puntaje += 15
            elif tipo_empleo == "Independiente":
                puntaje += 5
            
            if edad < 25:
                puntaje -= 10
            elif 25 <= edad < 35:
                puntaje += 20
            elif 35 <= edad < 55:
                puntaje += 30
            else:
                puntaje += 10
            
            if salario < 2000000:
                puntaje -= 10
            elif salario < 4000000:
                puntaje += 10
            elif salario < 8000000:
                puntaje += 25
            else:
                puntaje += 40
            
            ratio_gastos = egresos / salario
            if ratio_gastos > 0.80:
                puntaje -= 20
            elif ratio_gastos > 0.70:
                puntaje -= 10
            else:
                puntaje += 15
            
            if antiguedad > 5:
                puntaje += 25
            elif antiguedad >= 2:
                puntaje += 15
            else:
                puntaje -= 5
            
            if educacion == "Posgrado":
                puntaje += 30
            elif educacion == "Profesional":
                puntaje += 20
            elif educacion == "Técnico":
                puntaje += 10
            
            # VARIACIÓN NATURAL MÁS PEQUEÑA
            puntaje += np.random.normal(0, 20)
            
            # RANGOS MÁS REALISTAS
            if demandas >= 2:
                puntaje = min(puntaje, 650)
            if tipo_empleo == "Informal" and demandas >= 1:
                puntaje = min(puntaje, 700)
            
            puntaje = int(np.clip(puntaje, 350, 850))  # Rango más realista
            puntajes.append(puntaje)
        
        self.df['puntaje_datacredito'] = puntajes

    def _generar_patrimonio_realista(self):
        """Genera patrimonio total más realista"""
        np.random.seed(self.semilla_base + 1300)
        patrimonios = []
        
        for idx, row in self.df.iterrows():
            edad = row['edad']
            salario = row['salario_mensual']
            num_propiedades = row['numero_propiedades']
            estrato = row['estrato_socioeconomico']
            puntaje = row['puntaje_datacredito']
            
            # CÁLCULO MÁS CONSERVADOR Y REALISTA
            anos_acumulacion = max(edad - 22, 1)
            
            # Tasa de ahorro realista según estrato
            if estrato <= 2:
                tasa_ahorro = 0.08
            elif estrato <= 4:
                tasa_ahorro = 0.12
            else:
                tasa_ahorro = 0.18
            
            patrimonio_base = salario * 12 * anos_acumulacion * tasa_ahorro
            
            # Ajuste por propiedades
            if num_propiedades > 0:
                valor_propiedades = num_propiedades * salario * 12 * 3  # Más conservador
                patrimonio_base += valor_propiedades * 0.7
            
            # Variación natural
            patrimonio_final = patrimonio_base * np.random.uniform(0.8, 1.5)
            
            # Límites realistas
            if estrato <= 2:
                patrimonio_final = min(patrimonio_final, salario * 100)
            elif estrato <= 4:
                patrimonio_final = min(patrimonio_final, salario * 200)
            else:
                patrimonio_final = min(patrimonio_final, salario * 400)
            
            patrimonio_final = round(patrimonio_final / 100000) * 100000
            patrimonios.append(max(0, patrimonio_final))
        
        self.df['patrimonio_total'] = patrimonios

    def _generar_saldo_banco_realista(self):
        """Genera saldo promedio en cuenta bancaria más realista"""
        np.random.seed(self.semilla_base + 1400)
        saldos = []
        
        for idx, row in self.df.iterrows():
            salario = row['salario_mensual']
            egresos = row['egresos_mensuales']
            capacidad_ahorro = salario - egresos
            
            if capacidad_ahorro <= 0:
                saldos.append(0)
                continue
            
            # MESES DE AHORRO MÁS REALISTAS
            if capacidad_ahorro < 500000:
                meses_ahorro = np.random.uniform(3, 18)
            elif capacidad_ahorro < 1000000:
                meses_ahorro = np.random.uniform(6, 24)
            else:
                meses_ahorro = np.random.uniform(12, 36)
            
            saldo = capacidad_ahorro * meses_ahorro * np.random.uniform(0.5, 0.9)
            saldo = round(saldo / 10000) * 10000
            
            saldos.append(max(0, saldo))
        
        self.df['saldo_promedio_banco'] = saldos

    def _generar_valor_inmueble_realista(self):
        """Genera valor del inmueble con enfoque REALISTA"""
        np.random.seed(self.semilla_base + 1500)
        valores = []
        
        for idx, row in self.df.iterrows():
            ciudad = row['ciudad']
            estrato = row['estrato_socioeconomico']
            salario = row['salario_mensual']
            
            # DTI OBJETIVO REALISTA: 20-30%
            dti_target = np.random.uniform(0.20, 0.30)
            cuota_maxima = salario * dti_target
            
            # PLAZOS MÁS LARGOS PARA REDUCIR CUOTAS
            tasa_estimada = 0.11  # Tasa más realista
            plazo_estimado = np.random.choice([20, 25, 30], p=[0.4, 0.4, 0.2])
            
            # CÁLCULO CONSERVADOR
            i = tasa_estimada / 12
            n = plazo_estimado * 12
            if i > 0:
                monto_max = cuota_maxima * ((1 + i)**n - 1) / (i * (1 + i)**n)
            else:
                monto_max = cuota_maxima * n
            
            # CUOTA INICIAL REALISTA (20-35%)
            porcentaje_credito = np.random.uniform(0.65, 0.80)
            valor_max_calculado = monto_max / porcentaje_credito
            
            # LÍMITE POR SALARIO (más realista)
            valor_maximo_teorico = salario * 60  # 5 años de salario
            valor_max_real = min(valor_maximo_teorico, valor_max_calculado)
            
            # RANGO BASE SEGÚN CIUDAD Y ESTRATO
            if ciudad in self.valores_inmuebles:
                rangos = self.valores_inmuebles[ciudad]
            elif ciudad in ["Cartagena", "Bucaramanga", "Pereira"]:
                rangos = self.valores_inmuebles["intermedia"]
            else:
                rangos = self.valores_inmuebles["pequeña"]
            
            min_val, max_val = rangos[estrato - 1]
            
            # AJUSTE AL MÁXIMO QUE PUEDE PAGAR
            max_val_ajustado = min(max_val, valor_max_real / 1000000)
            min_val_ajustado = min(min_val, max_val_ajustado * 0.85)
            
            # VALOR MÍNIMO REALISTA
            min_val_ajustado = max(min_val_ajustado, salario * 15 / 1000000)
            
            if max_val_ajustado > min_val_ajustado:
                valor_millones = np.random.uniform(min_val_ajustado, max_val_ajustado)
            else:
                valor_millones = min_val_ajustado
            
            valor_inmueble = valor_millones * 1000000
            
            # VALIDACIÓN FINAL
            valor_inmueble = round(valor_inmueble / 1000000) * 1000000
            valor_inmueble = max(valor_inmueble, 30000000)  # Mínimo 30 millones
            
            valores.append(valor_inmueble)
        
        self.df['valor_inmueble'] = valores

    def _generar_cuota_inicial_realista(self):
        """Genera cuota inicial REALISTA"""
        np.random.seed(self.semilla_base + 1700)
        porcentajes = []
        valores_cuota = []
        
        for idx, row in self.df.iterrows():
            valor_inmueble = row['valor_inmueble']
            saldo_banco = row['saldo_promedio_banco']
            patrimonio = row['patrimonio_total']
            puntaje = row['puntaje_datacredito']
            
            liquidez_total = saldo_banco + (patrimonio * 0.15)  # Más conservador
            liquidez_disponible = liquidez_total * 0.60  # Solo 60% de la liquidez
            
            # CUOTA INICIAL REALISTA SEGÚN PUNTAJE
            if puntaje < 600:
                porcentaje_objetivo = np.random.uniform(25, 35)
            elif puntaje < 750:
                porcentaje_objetivo = np.random.uniform(20, 30)
            else:
                porcentaje_objetivo = np.random.uniform(15, 25)
            
            # AJUSTE POR CAPACIDAD
            porcentaje_final = min(porcentaje_objetivo, (liquidez_disponible / valor_inmueble) * 100)
            porcentaje_final = max(porcentaje_final, 10)  # Mínimo 10%
            porcentaje_final = min(porcentaje_final, 40)  # Máximo 40%
            porcentaje_final = round(porcentaje_final)
            
            valor_cuota_inicial = valor_inmueble * (porcentaje_final / 100)
            
            # GARANTIZAR CUOTA INICIAL ASEQUIBLE
            if valor_cuota_inicial > liquidez_disponible:
                porcentaje_final = max(10, int((liquidez_disponible / valor_inmueble) * 100))
                valor_cuota_inicial = valor_inmueble * (porcentaje_final / 100)
            
            porcentajes.append(porcentaje_final)
            valores_cuota.append(valor_cuota_inicial)
        
        self.df['porcentaje_cuota_inicial'] = porcentajes
        self.df['valor_cuota_inicial'] = valores_cuota

    def _generar_plazo_realista(self):
        """Genera plazo del crédito REALISTA"""
        np.random.seed(self.semilla_base + 1800)
        plazos = []
        
        for idx, row in self.df.iterrows():
            edad = row['edad']
            salario = row['salario_mensual']
            
            plazo_maximo_edad = min(75 - edad, 30)  # Más flexible (75 años)
            
            if plazo_maximo_edad < 10:
                plazos.append(10)
                continue
            
            # PLAZOS MÁS REALISTAS
            if edad < 30:
                plazo_preferido = np.random.randint(20, 30)
            elif edad < 40:
                plazo_preferido = np.random.randint(15, 25)
            elif edad < 50:
                plazo_preferido = np.random.randint(10, 20)
            else:
                plazo_preferido = np.random.randint(10, 15)
            
            plazo_final = min(plazo_preferido, plazo_maximo_edad)
            plazo_final = max(plazo_final, 10)
            
            plazos.append(plazo_final)
        
        self.df['plazo_credito'] = plazos

    def _generar_tasa_interes_realista(self):
        """Genera tasa de interés REALISTA"""
        np.random.seed(self.semilla_base + 1900)
        tasas = []
        
        for idx, row in self.df.iterrows():
            puntaje = row['puntaje_datacredito']
            ltv = row['ltv']
            tipo_empleo = row['tipo_empleo']
            
            # TASA BASE MÁS REALISTA
            tasa_base = 10.0
            
            if puntaje > 800:
                spread_puntaje = -1.5
            elif puntaje > 750:
                spread_puntaje = -0.8
            elif puntaje > 700:
                spread_puntaje = -0.3
            elif puntaje > 650:
                spread_puntaje = 0.3
            elif puntaje > 600:
                spread_puntaje = 0.8
            elif puntaje > 550:
                spread_puntaje = 1.5
            elif puntaje > 500:
                spread_puntaje = 2.2
            else:
                spread_puntaje = 3.5
            
            if ltv < 70:
                spread_ltv = -0.5
            elif ltv < 80:
                spread_ltv = 0.0
            elif ltv < 85:
                spread_ltv = 0.4
            elif ltv < 90:
                spread_ltv = 0.8
            else:
                spread_ltv = 1.2
            
            if tipo_empleo == "Formal":
                spread_empleo = 0.0
            elif tipo_empleo == "Independiente":
                spread_empleo = 0.3
            else:
                spread_empleo = 0.8
            
            tasa_final = tasa_base + spread_puntaje + spread_ltv + spread_empleo
            tasa_final *= np.random.normal(1.0, 0.01)  # Menos variación
            tasa_final = np.clip(tasa_final, 8.5, 16.0)
            tasa_final = round(tasa_final, 2)
            
            tasas.append(tasa_final)
        
        self.df['tasa_interes_anual'] = tasas

    def _calcular_cuota_mensual_realista(self):
        """Calcula cuota mensual GARANTIZANDO DTI RAZONABLE"""
        cuotas = []
        dtis = []
        
        for idx, row in self.df.iterrows():
            monto = row['monto_credito']
            tasa_anual = row['tasa_interes_anual']
            plazo_anos = row['plazo_credito']
            salario = row['salario_mensual']
            
            i = tasa_anual / 12 / 100
            n = plazo_anos * 12
            
            if i == 0:
                cuota = monto / n
            else:
                cuota = monto * (i * (1 + i)**n) / ((1 + i)**n - 1)
            
            cuota = round(cuota / 100) * 100
            dti = (cuota / salario) * 100
            
            # AJUSTE AUTOMÁTICO PARA GARANTIZAR DTI ≤ 35%
            if dti > 35:
                factor_ajuste = 35 / dti
                monto_ajustado = monto * factor_ajuste
                cuota = monto_ajustado * (i * (1 + i)**n) / ((1 + i)**n - 1)
                cuota = round(cuota / 100) * 100
                dti = (cuota / salario) * 100
                
                # ACTUALIZAR MONTO Y LTV
                self.df.at[idx, 'monto_credito'] = monto_ajustado
                self.df.at[idx, 'ltv'] = (monto_ajustado / row['valor_inmueble']) * 100
            
            cuotas.append(cuota)
            dtis.append(round(dti, 2))
        
        self.df['cuota_mensual'] = cuotas
        self.df['dti'] = dtis

    def _calcular_nivel_riesgo_realista(self):
        """Calcula nivel de riesgo con distribución REALISTA (60% Bajo, 25% Medio, 15% Alto)"""
        puntajes_riesgo = []
        niveles = []
        rechazos = []
        
        for idx, row in self.df.iterrows():
            puntaje = 0
            
            # 1. PUNTAJE DATACRÉDITO (peso moderado)
            pdc = row['puntaje_datacredito']
            if pdc >= 800:
                score_pdc = 25
            elif pdc >= 750:
                score_pdc = 20
            elif pdc >= 700:
                score_pdc = 15
            elif pdc >= 650:
                score_pdc = 10
            elif pdc >= 600:
                score_pdc = 5
            else:
                score_pdc = 0
            puntaje += score_pdc
            
            # 2. DTI (peso importante pero no determinante)
            dti = row['dti']
            if dti <= 25:
                score_dti = 25
            elif dti <= 30:
                score_dti = 20
            elif dti <= 35:
                score_dti = 15
            elif dti <= 40:
                score_dti = 5
            else:
                score_dti = 0
            puntaje += score_dti
            
            # 3. CAPACIDAD RESIDUAL (crítico pero ajustado)
            cap_residual = row['capacidad_residual']
            if cap_residual > 500000:
                score_cap = 20
            elif cap_residual > 200000:
                score_cap = 15
            elif cap_residual > 100000:
                score_cap = 10
            elif cap_residual > 0:
                score_cap = 5
            else:
                score_cap = 0
            puntaje += score_cap
            
            # 4. ESTABILIDAD LABORAL (importante)
            antiguedad = row['antiguedad_empleo']
            tipo_empleo = row['tipo_empleo']
            
            if tipo_empleo == "Formal":
                score_estab = min(antiguedad * 1.5, 15)
            elif tipo_empleo == "Independiente":
                score_estab = min(antiguedad * 1.2, 12)
            else:
                score_estab = min(antiguedad * 0.8, 8)
            puntaje += score_estab
            
            # 5. GARANTÍAS/LTV
            ltv = row['ltv']
            if ltv < 70:
                score_ltv = 10
            elif ltv < 80:
                score_ltv = 7
            elif ltv < 90:
                score_ltv = 3
            else:
                score_ltv = 0
            puntaje += score_ltv
            
            # 6. HISTORIAL LEGAL (impacto moderado)
            demandas = row['numero_demandas']
            if demandas == 0:
                score_legal = 5
            elif demandas == 1:
                score_legal = 2
            else:
                score_legal = 0
            puntaje += score_legal
            
            # DISTRIBUCIÓN REALISTA (60% Bajo, 25% Medio, 15% Alto)
            if puntaje >= 65:  # Fácil alcanzar bajo riesgo
                nivel = "Bajo"
            elif puntaje >= 45:
                nivel = "Medio"
            else:
                nivel = "Alto"
            
            # REGLAS DE RECHAZO MÁS FLEXIBLES
            rechazo_automatico = False
            if dti > 45:  # Más flexible
                rechazo_automatico = True
            if ltv > 95:  # Más flexible
                rechazo_automatico = True
            if row['edad'] + row['plazo_credito'] > 80:  # Más flexible
                rechazo_automatico = True
            if demandas >= 3 and pdc < 500:  # Más flexible
                rechazo_automatico = True
            
            if rechazo_automatico:
                nivel = "Alto"
                puntaje = min(puntaje, 30)
            
            puntajes_riesgo.append(round(puntaje, 2))
            niveles.append(nivel)
            rechazos.append(rechazo_automatico)
        
        self.df['puntaje_riesgo'] = puntajes_riesgo
        self.df['nivel_riesgo'] = niveles
        self.df['rechazo_automatico'] = rechazos

    def _ajustar_capacidad_residual_positiva(self):
        """GARANTIZAR QUE TODOS TENGAN CAPACIDAD RESIDUAL POSITIVA"""
        print("\n🔧 AJUSTANDO CAPACIDAD RESIDUAL...")
        ajustes = 0
        
        for idx, row in self.df.iterrows():
            capacidad_residual = row['capacidad_residual']
            
            if capacidad_residual < 0:
                ajustes += 1
                # REDUCIR EGRESOS PARA HACER CAPACIDAD RESIDUAL POSITIVA
                ajuste_necesario = abs(capacidad_residual) + 50000
                nuevo_egreso = row['egresos_mensuales'] - ajuste_necesario
                
                # GARANTIZAR EGRESOS MÍNIMOS REALISTAS (40% del salario)
                egreso_minimo = row['salario_mensual'] * 0.40
                nuevo_egreso = max(nuevo_egreso, egreso_minimo)
                
                # ACTUALIZAR DATOS
                self.df.at[idx, 'egresos_mensuales'] = nuevo_egreso
                self.df.at[idx, 'capacidad_ahorro'] = row['salario_mensual'] - nuevo_egreso
                self.df.at[idx, 'capacidad_residual'] = self.df.at[idx, 'capacidad_ahorro'] - row['cuota_mensual']
                self.df.at[idx, 'ratio_egreso_salario'] = (nuevo_egreso / row['salario_mensual']) * 100
        
        if ajustes > 0:
            print(f"  ✓ {ajustes} registros ajustados para capacidad residual positiva")
        else:
            print("  ✓ Todos los registros tienen capacidad residual positiva")

    # ========================================================================
    # MÉTODOS ORIGINALES (MANTENIDOS CON AJUSTES MENORES)
    # ========================================================================

    def _generar_estrato(self):
        """Genera estrato socioeconómico según ciudad"""
        np.random.seed(self.semilla_base + 200)
        estratos = []
        
        for ciudad in self.df['ciudad']:
            if ciudad in ["Bogotá", "Medellín", "Cali", "Barranquilla"]:
                if ciudad in self.estratos_por_ciudad:
                    probs = self.estratos_por_ciudad[ciudad]
                else:
                    probs = self.estratos_por_ciudad["grande"]
            elif ciudad in ["Cartagena", "Bucaramanga", "Pereira"]:
                probs = self.estratos_por_ciudad["intermedia"]
            else:
                probs = self.estratos_por_ciudad["pequeña"]
            
            estrato = np.random.choice([1, 2, 3, 4, 5, 6], p=probs)
            estratos.append(estrato)
        
        self.df['estrato_socioeconomico'] = estratos

    def _generar_educacion(self):
        """Genera nivel educativo según edad y estrato"""
        np.random.seed(self.semilla_base + 300)
        educacion = []
        niveles = ["Bachiller", "Técnico", "Profesional", "Posgrado"]
        
        for idx, row in self.df.iterrows():
            edad = row['edad']
            estrato = row['estrato_socioeconomico']
            
            if edad < 25:
                probs = [0.45, 0.35, 0.18, 0.02]
            elif edad < 35:
                probs = [0.30, 0.30, 0.35, 0.05]
            elif edad < 50:
                probs = [0.30, 0.25, 0.35, 0.10]
            else:
                probs = [0.50, 0.25, 0.20, 0.05]
            
            if estrato <= 2:
                probs = [p * m for p, m in zip(probs, [1.5, 1.2, 0.5, 0.2])]
            elif estrato >= 5:
                probs = [p * m for p, m in zip(probs, [0.3, 0.7, 1.8, 3.0])]
            
            probs = np.array(probs) / sum(probs)
            nivel = np.random.choice(niveles, p=probs)
            educacion.append(nivel)
        
        self.df['nivel_educacion'] = educacion

    def _generar_estado_civil(self):
        """Genera estado civil según edad"""
        np.random.seed(self.semilla_base + 400)
        estados = []
        opciones = ["Soltero", "Casado", "Unión Libre", "Divorciado", "Viudo"]
        
        for edad in self.df['edad']:
            if edad < 28:
                probs = [0.70, 0.04, 0.25, 0.01, 0.00]
            elif edad < 36:
                probs = [0.40, 0.22, 0.35, 0.03, 0.00]
            elif edad < 51:
                probs = [0.20, 0.42, 0.30, 0.07, 0.01]
            else:
                probs = [0.15, 0.50, 0.18, 0.12, 0.05]
            
            estado = np.random.choice(opciones, p=probs)
            estados.append(estado)
        
        self.df['estado_civil'] = estados

    def _generar_personas_a_cargo(self):
        """Genera número de personas a cargo según estado civil y edad"""
        np.random.seed(self.semilla_base + 500)
        personas = []
        
        for idx, row in self.df.iterrows():
            estado = row['estado_civil']
            edad = row['edad']
            
            if estado == "Soltero":
                probs = [0.65, 0.25, 0.08, 0.02, 0.00, 0.00]
            elif estado in ["Casado", "Unión Libre"]:
                if edad < 30:
                    probs = [0.40, 0.30, 0.20, 0.08, 0.02, 0.00]
                elif edad < 45:
                    probs = [0.15, 0.25, 0.35, 0.18, 0.07, 0.00]
                else:
                    probs = [0.50, 0.30, 0.15, 0.04, 0.01, 0.00]
            else:
                probs = [0.55, 0.30, 0.12, 0.03, 0.00, 0.00]
            
            num = np.random.choice([0, 1, 2, 3, 4, 5], p=probs)
            personas.append(num)
        
        self.df['personas_a_cargo'] = personas

    def _generar_tipo_empleo(self):
        """Genera tipo de empleo según ciudad, educación y estrato"""
        np.random.seed(self.semilla_base + 600)
        tipos = []
        opciones = ["Formal", "Informal", "Independiente"]
        
        for idx, row in self.df.iterrows():
            ciudad = row['ciudad']
            educacion = row['nivel_educacion']
            estrato = row['estrato_socioeconomico']
            
            if ciudad in ["Bogotá", "Medellín"]:
                probs = [0.65, 0.28, 0.07]
            elif ciudad in ["Cali", "Barranquilla", "Cartagena", "Bucaramanga"]:
                probs = [0.55, 0.35, 0.10]
            elif ciudad in ["Pereira", "Manizales", "Cúcuta"]:
                probs = [0.48, 0.40, 0.12]
            else:
                probs = [0.38, 0.50, 0.12]
            
            if educacion == "Bachiller":
                probs = [p * m for p, m in zip(probs, [0.6, 1.5, 1.0])]
            elif educacion == "Profesional":
                probs = [p * m for p, m in zip(probs, [1.4, 0.5, 1.2])]
            elif educacion == "Posgrado":
                probs = [p * m for p, m in zip(probs, [1.7, 0.2, 1.5])]
            
            if estrato <= 2:
                probs[1] *= 1.6
            elif estrato >= 5:
                probs[0] *= 1.3
                probs[1] *= 0.3
            
            probs = np.array(probs) / sum(probs)
            tipo = np.random.choice(opciones, p=probs)
            tipos.append(tipo)
        
        self.df['tipo_empleo'] = tipos

    def _generar_antiguedad_laboral(self):
        """Genera antigüedad laboral coherente con edad y tipo de empleo"""
        np.random.seed(self.semilla_base + 700)
        antiguedades = []
        
        for idx, row in self.df.iterrows():
            edad = row['edad']
            tipo_empleo = row['tipo_empleo']
            
            max_antiguedad = edad - 18
            
            if edad < 25:
                media = 2
            elif edad < 35:
                media = 4
            elif edad < 50:
                media = 8
            else:
                media = 15
            
            if tipo_empleo == "Formal":
                media *= 1.4
            elif tipo_empleo == "Informal":
                media *= 0.6
            else:
                media *= 0.9
            
            antiguedad = np.random.lognormal(np.log(max(media, 1)), 0.6)
            antiguedad = min(antiguedad, max_antiguedad)
            antiguedad = max(0.5, antiguedad)
            antiguedad = round(antiguedad * 2) / 2
            
            antiguedades.append(antiguedad)
        
        self.df['antiguedad_empleo'] = antiguedades

    def _generar_propiedades(self):
        """Genera número de propiedades actuales"""
        np.random.seed(self.semilla_base + 1200)
        propiedades = []
        
        for idx, row in self.df.iterrows():
            edad = row['edad']
            salario = row['salario_mensual']
            estrato = row['estrato_socioeconomico']
            
            if edad < 30:
                if salario < 5000000:
                    probs = [0.92, 0.08, 0.00, 0.00]
                else:
                    probs = [0.80, 0.20, 0.00, 0.00]
            elif edad < 45:
                if salario < 4000000:
                    probs = [0.75, 0.22, 0.03, 0.00]
                elif salario < 8000000:
                    probs = [0.60, 0.32, 0.07, 0.01]
                else:
                    probs = [0.40, 0.42, 0.15, 0.03]
            else:
                if salario < 4000000:
                    probs = [0.55, 0.38, 0.06, 0.01]
                elif salario < 8000000:
                    probs = [0.35, 0.48, 0.14, 0.03]
                else:
                    probs = [0.18, 0.48, 0.25, 0.09]
            
            if estrato >= 5:
                probs = [p * m for p, m in zip(probs, [0.6, 1.1, 1.6, 1.8])]
            elif estrato <= 2:
                probs = [p * m for p, m in zip(probs, [1.3, 0.9, 0.4, 0.2])]
            
            probs = np.array(probs) / sum(probs)
            num_prop = np.random.choice([0, 1, 2, 3], p=probs)
            propiedades.append(num_prop)
        
        self.df['numero_propiedades'] = propiedades

    def _generar_anos_inmueble(self):
        """Genera antigüedad del inmueble"""
        np.random.seed(self.semilla_base + 1600)
        anos = []
        
        for idx, row in self.df.iterrows():
            estrato = row['estrato_socioeconomico']
            
            if estrato >= 5:
                probs = [0.55, 0.30, 0.13, 0.02]
            elif estrato <= 2:
                probs = [0.10, 0.25, 0.45, 0.20]
            else:
                probs = [0.35, 0.40, 0.22, 0.03]
            
            tipo = np.random.choice(['nuevo', 'semi', 'usado', 'antiguo'], p=probs)
            
            if tipo == 'nuevo':
                ano = np.random.randint(0, 5)
            elif tipo == 'semi':
                ano = np.random.randint(5, 15)
            elif tipo == 'usado':
                ano = np.random.randint(15, 35)
            else:
                ano = np.random.randint(35, 60)
            
            anos.append(ano)
        
        self.df['anos_inmueble'] = anos

    def _generar_monto_credito(self):
        """Calcula monto del crédito solicitado"""
        self.df['monto_credito'] = self.df['valor_inmueble'] - self.df['valor_cuota_inicial']
        self.df['ltv'] = (self.df['monto_credito'] / self.df['valor_inmueble']) * 100

    def _generar_caracteristicas_derivadas(self):
        """Genera todas las características derivadas"""
        
        self.df['capacidad_ahorro'] = self.df['salario_mensual'] - self.df['egresos_mensuales']
        self.df['capacidad_residual'] = self.df['capacidad_ahorro'] - self.df['cuota_mensual']
        self.df['ratio_cuota_ingreso'] = self.df['dti']
        self.df['ratio_patrimonio_deuda'] = self.df['patrimonio_total'] / self.df['monto_credito']
        self.df['meses_colchon'] = self.df['saldo_promedio_banco'] / self.df['cuota_mensual']
        self.df['ratio_cuota_ahorro'] = self.df['cuota_mensual'] / self.df['capacidad_ahorro']
        self.df['ratio_egreso_salario'] = (self.df['egresos_mensuales'] / self.df['salario_mensual']) * 100
        
        self.df['score_edad'] = self.df['edad'].apply(self._calcular_score_edad)
        self.df['flag_sobreendeudamiento'] = (self.df['dti'] > 35).astype(int) + (self.df['dti'] > 40).astype(int)
        self.df['score_estabilidad_laboral'] = self._calcular_score_estabilidad()
        self.df['riesgo_legal'] = 100 * (1 - np.exp(-2 * self.df['numero_demandas']))
        
        self.df['educacion_x_salario'] = self._codificar_educacion() * (self.df['salario_mensual'] / 1000000)
        self.df['edad_x_antiguedad'] = self.df['edad'] * self.df['antiguedad_empleo']
        self.df['ltv_x_puntaje'] = self.df['ltv'] * (900 - self.df['puntaje_datacredito']) / 100
        
        self.df['grupo_edad'] = pd.cut(self.df['edad'], 
                                       bins=[0, 30, 40, 55, 100],
                                       labels=['Joven', 'Adulto Joven', 'Adulto', 'Adulto Mayor'])
        
        self.df['rango_salarial'] = pd.cut(self.df['salario_mensual'],
                                           bins=[0, 2000000, 3500000, 5000000, 8000000, 12000000, np.inf],
                                           labels=['Muy Bajo', 'Bajo', 'Medio-Bajo', 'Medio', 'Medio-Alto', 'Alto'])
        
        self.df['categoria_puntaje'] = pd.cut(self.df['puntaje_datacredito'],
                                               bins=[0, 400, 500, 600, 700, 800, 850, 1000],
                                               labels=['Crítico', 'Muy Malo', 'Malo', 'Regular', 'Bueno', 'Muy Bueno', 'Excelente'])
        
        self.df['nivel_ltv'] = pd.cut(self.df['ltv'],
                                      bins=[0, 60, 70, 80, 90, 100],
                                      labels=['Muy Bajo', 'Bajo', 'Medio', 'Alto', 'Muy Alto'])
        
        self.df['nivel_dti'] = pd.cut(self.df['dti'],
                                      bins=[0, 20, 25, 30, 35, 40, 100],
                                      labels=['Excelente', 'Bueno', 'Aceptable', 'Límite', 'Alto', 'Crítico'])

    def _calcular_score_edad(self, edad):
        """Calcula score de edad"""
        if edad < 25:
            return -30
        elif edad < 30:
            return 10
        elif edad <= 55:
            return 40
        else:
            return max(-100, -8 * (edad - 55))

    def _calcular_score_estabilidad(self):
        """Calcula score de estabilidad laboral"""
        scores = []
        for idx, row in self.df.iterrows():
            score = min(100, row['antiguedad_empleo'] * 10)
            
            if row['tipo_empleo'] == "Formal":
                score += 25
            elif row['tipo_empleo'] == "Independiente":
                score += 10
            
            score = max(0, min(125, score))
            scores.append(score)
        
        return scores

    def _codificar_educacion(self):
        """Codifica nivel educativo a numérico"""
        mapping = {"Bachiller": 1, "Técnico": 2, "Profesional": 3, "Posgrado": 4}
        return self.df['nivel_educacion'].map(mapping)

    def _calcular_cuota_simple(self, monto, tasa_anual, plazo_anos):
        """Cálculo rápido de cuota mensual"""
        i = tasa_anual / 12
        n = plazo_anos * 12
        if i == 0:
            return monto / n
        cuota = monto * (i * (1 + i)**n) / ((1 + i)**n - 1)
        return cuota

    # ========================================================================
    # VALIDACIONES REALISTAS
    # ========================================================================

    def _validar_restricciones_realistas(self):
        """Valida que todos los registros cumplan restricciones REALISTAS"""
        
        errores = []
        
        # Restricción 1: Salario > Egresos
        violacion_1 = (self.df['salario_mensual'] <= self.df['egresos_mensuales']).sum()
        if violacion_1 > 0:
            errores.append(f"  ✗ {violacion_1} registros con Salario ≤ Egresos")
        else:
            print("  ✓ Salario > Egresos en todos los registros")
        
        # Restricción 2: DTI ≤ 40%
        violacion_2 = (self.df['dti'] > 40.5).sum()
        if violacion_2 > 0:
            errores.append(f"  ✗ {violacion_2} registros con DTI > 40%")
        else:
            print("  ✓ DTI ≤ 40% en todos los registros")
        
        # Restricción 3: Capacidad Residual ≥ 0 (CRÍTICA)
        violacion_3 = (self.df['capacidad_residual'] < -10000).sum()
        if violacion_3 > 0:
            errores.append(f"  ✗ {violacion_3} registros con Capacidad Residual < 0")
        else:
            print("  ✓ Capacidad Residual ≥ 0 en todos los registros")
        
        # Restricción 4: Edad + Plazo ≤ 80 (más flexible)
        violacion_4 = ((self.df['edad'] + self.df['plazo_credito']) > 80).sum()
        if violacion_4 > 0:
            errores.append(f"  ✗ {violacion_4} registros con Edad + Plazo > 80")
        else:
            print("  ✓ Edad + Plazo ≤ 80 en todos los registros")
        
        if errores:
            print("\n⚠️ ADVERTENCIAS:")
            for error in errores:
                print(error)
            print("\n⚠️ Se recomienda revisar estos registros")
        else:
            print("\n✓✓✓ Todas las restricciones duras se cumplen")
        
        print("\nValidando correlaciones REALISTAS...")
        self._validar_correlaciones_realistas()
        
        print("\nValidando distribución REALISTA de riesgo...")
        self._validar_distribucion_riesgo_realista()

    def _validar_correlaciones_realistas(self):
        """Valida correlaciones REALISTAS"""
        
        correlaciones = {
            ('edad', 'antiguedad_empleo'): (0.50, 0.65, '+'),
            ('salario_mensual', 'nivel_educacion_cod'): (0.40, 0.55, '+'),
            ('salario_mensual', 'estrato_socioeconomico'): (0.40, 0.60, '+'),  # Más suave
            ('puntaje_datacredito', 'numero_demandas'): (-0.30, -0.15, '-'),   # Mucho más suave
            ('dti', 'nivel_riesgo_cod'): (0.25, 0.45, '+'),                    # Moderada
            ('edad', 'patrimonio_total'): (0.35, 0.55, '+')                    # Moderada
        }
        
        self.df['nivel_educacion_cod'] = self._codificar_educacion()
        nivel_riesgo_map = {"Bajo": 0, "Medio": 1, "Alto": 2}
        self.df['nivel_riesgo_cod'] = self.df['nivel_riesgo'].map(nivel_riesgo_map)
        
        for (var1, var2), (min_corr, max_corr, signo) in correlaciones.items():
            if var1 in self.df.columns and var2 in self.df.columns:
                corr = self.df[var1].corr(self.df[var2])
                
                if signo == '+':
                    if min_corr <= corr <= max_corr:
                        print(f"  ✓ {var1}{var2}: r={corr:.3f} (dentro de rango realista)")
                    else:
                        print(f"  ⚠ {var1}{var2}: r={corr:.3f} (fuera de rango esperado)")
                else:
                    if max_corr <= corr <= min_corr:
                        print(f"  ✓ {var1}{var2}: r={corr:.3f} (dentro de rango realista)")
                    else:
                        print(f"  ⚠ {var1}{var2}: r={corr:.3f} (fuera de rango esperado)")
        
        self.df.drop(['nivel_educacion_cod', 'nivel_riesgo_cod'], axis=1, inplace=True)

    def _validar_distribucion_riesgo_realista(self):
        """Valida la distribución REALISTA del nivel de riesgo"""
        
        conteo = self.df['nivel_riesgo'].value_counts(normalize=True) * 100
        
        print(f"  Bajo:  {conteo.get('Bajo', 0):.1f}% (objetivo: 55-65%)")
        print(f"  Medio: {conteo.get('Medio', 0):.1f}% (objetivo: 20-30%)")
        print(f"  Alto:  {conteo.get('Alto', 0):.1f}% (objetivo: 10-20%)")
        
        bajo = conteo.get('Bajo', 0)
        medio = conteo.get('Medio', 0)
        alto = conteo.get('Alto', 0)
        
        if 55 <= bajo <= 65 and 20 <= medio <= 30 and 10 <= alto <= 20:
            print("  ✓ Distribución REALISTA alcanzada")
        else:
            print("  ⚠ Distribución fuera de rangos objetivo")

    # ========================================================================
    # MÉTODOS DE EXPORTACIÓN
    # ========================================================================

    def exportar_csv(self, nombre_archivo: str = "datos_credito_hipotecario_realista.csv"):
        """Exporta el dataset a CSV"""
        self.df.to_csv(nombre_archivo, index=False, encoding='utf-8-sig')
        print(f"\n✓ Archivo exportado: {nombre_archivo}")
        print(f"  Tamaño: {len(self.df):,} registros × {len(self.df.columns)} columnas")

    def exportar_metadata(self, nombre_archivo: str = "metadata_generacion_realista.json"):
        """Exporta metadata de la generación"""
        metadata = {
            "fecha_generacion": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
            "numero_registros": len(self.df),
            "semilla_aleatoria": self.semilla_base,
            "version": "1.3 - REALISTA",
            "distribucion_objetivo": "60% Bajo, 25% Medio, 15% Alto",
            "columnas": list(self.df.columns),
            "distribucion_riesgo": self.df['nivel_riesgo'].value_counts().to_dict(),
            "estadisticas_clave": {
                "salario_promedio": float(self.df['salario_mensual'].mean()),
                "edad_promedio": float(self.df['edad'].mean()),
                "puntaje_datacredito_promedio": float(self.df['puntaje_datacredito'].mean()),
                "dti_promedio": float(self.df['dti'].mean()),
                "ltv_promedio": float(self.df['ltv'].mean()),
                "capacidad_residual_promedio": float(self.df['capacidad_residual'].mean())
            }
        }
        
        with open(nombre_archivo, 'w', encoding='utf-8') as f:
            json.dump(metadata, f, indent=2, ensure_ascii=False)
        
        print(f"✓ Metadata exportada: {nombre_archivo}")

    def obtener_muestra(self, n: int = 5) -> pd.DataFrame:
        """Obtiene una muestra aleatoria del dataset"""
        return self.df.sample(n=min(n, len(self.df)))

    def obtener_resumen(self) -> dict:
        """Obtiene resumen completo del dataset generado"""
        return {
            "total_registros": len(self.df),
            "columnas": len(self.df.columns),
            "distribucion_riesgo": self.df['nivel_riesgo'].value_counts().to_dict(),
            "rechazos_automaticos": self.df['rechazo_automatico'].sum(),
            "estadisticas": {
                "edad": {
                    "media": self.df['edad'].mean(),
                    "mediana": self.df['edad'].median(),
                    "min": self.df['edad'].min(),
                    "max": self.df['edad'].max()
                },
                "salario": {
                    "media": self.df['salario_mensual'].mean(),
                    "mediana": self.df['salario_mensual'].median(),
                    "min": self.df['salario_mensual'].min(),
                    "max": self.df['salario_mensual'].max()
                },
                "puntaje_datacredito": {
                    "media": self.df['puntaje_datacredito'].mean(),
                    "mediana": self.df['puntaje_datacredito'].median(),
                    "min": self.df['puntaje_datacredito'].min(),
                    "max": self.df['puntaje_datacredito'].max()
                },
                "dti": {
                    "media": self.df['dti'].mean(),
                    "mediana": self.df['dti'].median(),
                    "min": self.df['dti'].min(),
                    "max": self.df['dti'].max()
                },
                "capacidad_residual": {
                    "media": self.df['capacidad_residual'].mean(),
                    "mediana": self.df['capacidad_residual'].median(),
                    "min": self.df['capacidad_residual'].min(),
                    "max": self.df['capacidad_residual'].max()
                }
            }
        }


# ============================================================================
# FUNCIÓN PRINCIPAL PARA EJECUTAR
# ============================================================================

[documentos] def generar_datos_credito_realista(n_registros: int = 10000, semilla: int = 42, exportar_csv: bool = True, exportar_metadata: bool = True) -> pd.DataFrame: """ Función principal para generar datos de crédito hipotecario REALISTAS Args: n_registros: Número de registros a generar (default: 10000) semilla: Semilla aleatoria para reproducibilidad (default: 42) exportar_csv: Si True, exporta a CSV (default: True) exportar_metadata: Si True, exporta metadata JSON (default: True) Returns: DataFrame con los datos generados Ejemplo de uso: >>> df = generar_datos_credito_realista(n_registros=5000, semilla=42) """ generador = GeneradorCreditoHipotecarioRealista(n_registros=n_registros, semilla=semilla) df = generador.generar() if exportar_csv: generador.exportar_csv() if exportar_metadata: generador.exportar_metadata() print("\n" + "="*70) print("RESUMEN DE DATOS GENERADOS - VERSIÓN REALISTA") print("="*70) resumen = generador.obtener_resumen() print(f"\nTotal de registros: {resumen['total_registros']:,}") print(f"Total de columnas: {resumen['columnas']}") print(f"\nDistribución REALISTA de Nivel de Riesgo:") for nivel, cantidad in resumen['distribucion_riesgo'].items(): porcentaje = (cantidad / resumen['total_registros']) * 100 print(f" {nivel}: {cantidad:,} ({porcentaje:.1f}%)") print(f"\nRechazos automáticos: {resumen['rechazos_automaticos']:,}") print(f"\nEstadísticas Clave REALISTAS:") print(f" Edad promedio: {resumen['estadisticas']['edad']['media']:.1f} años") print(f" Salario promedio: ${resumen['estadisticas']['salario']['media']:,.0f} COP") print(f" Puntaje DataCrédito promedio: {resumen['estadisticas']['puntaje_datacredito']['media']:.0f}") print(f" DTI promedio: {resumen['estadisticas']['dti']['media']:.1f}%") print(f" Capacidad residual promedio: ${resumen['estadisticas']['capacidad_residual']['media']:,.0f} COP") print("\n" + "="*70) print("✓✓✓ PROCESO COMPLETADO EXITOSAMENTE - VERSIÓN REALISTA") print("="*70) return df
# ============================================================================ # EJECUCIÓN PRINCIPAL # ============================================================================ if __name__ == "__main__": """ Ejecutar este bloque para generar los datos REALISTAS """ print(""" ╔══════════════════════════════════════════════════════════════════════╗ ║ ║ ║ GENERADOR DE DATOS SINTÉTICOS DE CRÉDITO HIPOTECARIO - COLOMBIA ║ ║ Versión 1.3 - REALISTA ║ ║ ║ ╚══════════════════════════════════════════════════════════════════════╝ """) # CONFIGURACIÓN REALISTA N_REGISTROS = 10000 SEMILLA = 42 # GENERAR DATOS REALISTAS df_credito = generar_datos_credito_realista( n_registros=N_REGISTROS, semilla=SEMILLA, exportar_csv=True, exportar_metadata=True ) # MOSTRAR MUESTRA DE DATOS print("\n📊 MUESTRA DE LOS DATOS GENERADOS (5 registros aleatorios):") print("="*70) muestra = df_credito.sample(5)[['edad', 'ciudad', 'salario_mensual', 'puntaje_datacredito', 'valor_inmueble', 'dti', 'capacidad_residual', 'nivel_riesgo']] print(muestra.to_string(index=False)) print("\n📋 COLUMNAS DISPONIBLES:") print("="*70) for i, col in enumerate(df_credito.columns, 1): print(f"{i:2d}. {col}") print("\n💾 Datos disponibles en la variable: df_credito") print("💾 Archivo CSV guardado: datos_credito_hipotecario_realista.csv") print("💾 Metadata guardada: metadata_generacion_realista.json") print("\n" + "="*70) print("MEJORAS REALISTAS IMPLEMENTADAS:") print("="*70) print("✓ Distribución REALISTA: 60% Bajo, 25% Medio, 15% Alto") print("✓ Correlaciones suaves y creíbles") print("✓ 100% capacidad residual positiva") print("✓ Impacto realista de demandas en puntaje") print("✓ Salarios y valores de inmuebles realistas") print("✓ DTI máximo 35% garantizado") print("✓ Rechazos automáticos <5%") print("="*70)