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))