File size: 4,326 Bytes
1ec1a50
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
import pandas as pd
import shap
import joblib
import numpy as np

def obtener_explicacion_alumno(id_alumno, ruta_modelo, ruta_datos_ml, ruta_datos_maestro):
    """
    Analiza a un estudiante usando SHAP y extrae su perfil sociodemográfico.
    Devuelve un JSON estructurado para alimentar al Agente LLM del tutor.
    """
    try:
        modelo = joblib.load(ruta_modelo)
        df_ml = pd.read_csv(ruta_datos_ml)
        df_maestro = pd.read_csv(ruta_datos_maestro)
    except FileNotFoundError as e:
        return {"error": f"Archivo no encontrado: {e}"}

    # Buscar la fila del alumno
    indice_alumno = df_maestro.index[df_maestro['id_alumno'] == id_alumno].tolist()
    if not indice_alumno:
        return {"error": "Alumno no encontrado en la base de datos."}
    
    idx = indice_alumno[0]
    
   # 1. EXTRACCIÓN Y MAPEO DEL CONTEXTO SOCIODEMOGRÁFICO (Lenguaje Natural para el LLM)
    gen_raw = str(df_maestro.loc[idx, 'genero'])
    disc_raw = str(df_maestro.loc[idx, 'discapacidad'])
    
    # Mapeos simples para claridad del LLM
    texto_genero = "Mujer" if gen_raw == "F" else "Hombre" if gen_raw == "M" else "No especificado"
    texto_discapacidad = "Sí, requiere adaptaciones o atención especial" if disc_raw == "Y" else "No consta"
    
    contexto_alumno = {
        "perfil_demografico": texto_genero,
        "grupo_de_edad": str(df_maestro.loc[idx, 'rango_edad']) + " años",
        "nivel_estudios_previo": str(df_maestro.loc[idx, 'nivel_educativo']),
        "indice_nivel_economico": f"Banda de ingresos {df_maestro.loc[idx, 'nivel_economico']}",
        "veces_matriculado_anteriormente": int(df_maestro.loc[idx, 'intentos_previos']),
        "tiene_discapacidad_reconocida": texto_discapacidad
    }
    
    # 2. PREDICCIÓN Y EXPLICABILIDAD MATEMÁTICA (XAI)
    X = df_ml.drop(columns=['churn', 'code_presentation'], errors='ignore')
    perfil_alumno = X.iloc[[idx]]
    
    prob_abandono = modelo.predict_proba(perfil_alumno)[0][1]
    
    if prob_abandono < 0.50:
        return {
            "alerta": False,
            "id_alumno": id_alumno,
            "mensaje": "Sin riesgo crítico. No requiere intervención del tutor."
        }

    explainer = shap.TreeExplainer(modelo)
    shap_values = explainer.shap_values(perfil_alumno)

# --- CORRECCIÓN DEL ERROR DE DIMENSIONES NUMPY ---
    # Interceptamos la matriz de SHAP y forzamos la extracción de escalares 1D
    if isinstance(shap_values, list):
        valores_shap_clase1 = shap_values[1][0]
    else:
        shap_array = np.array(shap_values)
        if len(shap_array.shape) == 3: # (muestras, features, clases)
            valores_shap_clase1 = shap_array[0, :, 1]
        elif len(shap_array.shape) == 2: # (muestras, features)
            valores_shap_clase1 = shap_array[0, :]
        else:
            valores_shap_clase1 = shap_array.flatten()
            
    # Aplanamos y aseguramos que es un array 1D
    valores_shap_clase1 = np.array(valores_shap_clase1).flatten()
    
    impacto_variables = {}
    for nombre_columna, valor_shap in zip(X.columns, valores_shap_clase1):
        if valor_shap > 0:
            impacto_variables[nombre_columna] = valor_shap
            
    impacto_ordenado = sorted(impacto_variables.items(), key=lambda item: item[1], reverse=True)
    
    top_causas = []
    for var, impacto in impacto_ordenado[:3]:
        top_causas.append({
            "variable_tecnica": var,
            "valor_actual": float(perfil_alumno[var].iloc[0]),
            "peso_en_riesgo": round(impacto, 3)
        })

    # 3. EMPAQUETADO FINAL PARA EL LLM
    return {
        "alerta": True,
        "id_alumno": id_alumno,
        "diagnostico_matematico": {
            "riesgo_abandono_porcentaje": round(prob_abandono * 100, 2),
            "causas_shap": top_causas
        },
        "contexto_alumno": contexto_alumno
    }

if __name__ == "__main__":
    print("Iniciando generación de payload para el tutor...")
    resultado = obtener_explicacion_alumno(
        id_alumno=30268, 
        ruta_modelo='datos_procesados/modelo_rf_optimizado.pkl',
        ruta_datos_ml='datos_procesados/dataset_preparado_ml.csv',
        ruta_datos_maestro='datos_procesados/dataset_maestro_churn.csv'
    )
    import json
    print(json.dumps(resultado, indent=4, ensure_ascii=False))