Spaces:
Running
Running
| from dataclasses import dataclass | |
| from typing import List, Dict, Tuple | |
| from datetime import datetime | |
| from pathlib import Path | |
| import math | |
| import json | |
| # ============================ | |
| # Núcleo MG360 (24 ítems) | |
| # ============================ | |
| class Item: | |
| code: str | |
| text: str | |
| dimension: str # "COG", "EMO", "REL", "EJE" | |
| reverse: bool | |
| # 24 ítems: 6 por eje (3 directos + 3 inversos) | |
| ITEMS: List[Item] = [ | |
| # Cognitiva (COG) | |
| Item("COG1", "Antes de decidir, evalúo cómo una acción afecta a otras áreas.", "COG", False), | |
| Item("COG2", "Anticipo escenarios y consecuencias más allá del corto plazo.", "COG", False), | |
| Item("COG3", "Identifico patrones y relaciones entre procesos aparentemente aislados.", "COG", False), | |
| Item("COG4", "Prefiero concentrarme solo en mi área aunque desconozca las demás.", "COG", True), | |
| Item("COG5", "Decido principalmente según urgencias del día a día.", "COG", True), | |
| Item("COG6", "Me cuesta ver el impacto sistémico de mis decisiones.", "COG", True), | |
| # Emocional (EMO) | |
| Item("EMO1", "Identifico emociones que influyen en mi trato con otros.", "EMO", False), | |
| Item("EMO2", "Regulo mi respuesta emocional aun bajo presión.", "EMO", False), | |
| Item("EMO3", "Practico la empatía para comprender perspectivas distintas.", "EMO", False), | |
| Item("EMO4", "Suelo reprimir emociones para no mostrar debilidad.", "EMO", True), | |
| Item("EMO5", "Cuando me frustro, reacciono de forma impulsiva.", "EMO", True), | |
| Item("EMO6", "Evito hablar de emociones en el trabajo.", "EMO", True), | |
| # Relacional (REL) | |
| Item("REL1", "Busco acuerdos que integren intereses distintos.", "REL", False), | |
| Item("REL2", "Comunico expectativas de forma clara y verifico entendimiento.", "REL", False), | |
| Item("REL3", "Fomento colaboración efectiva entre áreas y niveles.", "REL", False), | |
| Item("REL4", "Evito confrontar para no generar conflicto.", "REL", True), | |
| Item("REL5", "Prefiero trabajar de forma individual para avanzar más rápido.", "REL", True), | |
| Item("REL6", "Me cuesta adaptar mi comunicación según el interlocutor.", "REL", True), | |
| # Ejecucional (EJE) | |
| Item("EJE1", "Transformo errores en oportunidades de aprendizaje.", "EJE", False), | |
| Item("EJE2", "Cumplo compromisos en tiempo forma de manera consistente.", "EJE", False), | |
| Item("EJE3", "Itero procesos para mejorar resultados de forma continua.", "EJE", False), | |
| Item("EJE4", "Cumplir metas es más importante que desarrollar a las personas.", "EJE", True), | |
| Item("EJE5", "Me desanimo fácilmente cuando surgen obstáculos.", "EJE", True), | |
| Item("EJE6", "Evito revisar resultados para no encontrar fallas.", "EJE", True), | |
| ] | |
| DIMENSIONS = ["COG", "EMO", "REL", "EJE"] | |
| DIMENSION_LABELS = { | |
| "COG": "Cognitiva", | |
| "EMO": "Emocional", | |
| "REL": "Relacional", | |
| "EJE": "Ejecucional", | |
| } | |
| def invert_if_needed(value: float, reverse: bool) -> float: | |
| """Invierte Likert 1–5 cuando el ítem es inverso (6 - v).""" | |
| return 6 - value if reverse else value | |
| def score_responses(responses: Dict[str, float]) -> Dict[str, float]: | |
| """Calcula promedios por dimensión y BALANCE_360.""" | |
| dim_values = {d: [] for d in DIMENSIONS} | |
| for it in ITEMS: | |
| if it.code not in responses: | |
| raise ValueError(f"Falta respuesta para {it.code}") | |
| v = float(responses[it.code]) | |
| if not (1 <= v <= 5): | |
| raise ValueError(f"Respuesta fuera de rango 1-5 en {it.code}: {v}") | |
| dim_values[it.dimension].append(invert_if_needed(v, it.reverse)) | |
| dim_avg = {d: sum(vals)/len(vals) for d, vals in dim_values.items()} | |
| vals = list(dim_avg.values()) | |
| avg = sum(vals)/len(vals) | |
| var = sum((x-avg)**2 for x in vals) / len(vals) | |
| stdev = math.sqrt(var) | |
| balance_360 = 1 - (stdev / avg) if avg > 0 else 0.0 | |
| return {**dim_avg, "BALANCE_360": balance_360} | |
| def dominant_axis(dim_scores: Dict[str, float]) -> Tuple[str, float]: | |
| best_dim = max(DIMENSIONS, key=lambda d: dim_scores[d]) | |
| return best_dim, dim_scores[best_dim] | |
| def interpret(dim_scores: Dict[str, float]) -> Dict[str, str]: | |
| """Interpretación de equilibrio y eje dominante.""" | |
| bal = dim_scores["BALANCE_360"] | |
| if bal > 0.85: | |
| eq = "Mentalidad 360 desarrollada" | |
| elif bal >= 0.70: | |
| eq = "Parcialmente equilibrada" | |
| else: | |
| eq = "Tendencia a sesgo gerencial (un eje domina)" | |
| best, val = dominant_axis(dim_scores) | |
| perfiles = { | |
| "COG": "Estratega analítico (Cognitivo)", | |
| "EMO": "Líder empático (Emocional)", | |
| "REL": "Conector colaborativo (Relacional)", | |
| "EJE": "Gestor ejecutor (Ejecucional)", | |
| } | |
| return { | |
| "equilibrio": eq, | |
| "eje_dominante": f"{perfiles[best]} — {DIMENSION_LABELS[best]} ({val:.2f}/5)" | |
| } | |
| # ============================ | |
| # Radar PRO (matplotlib) | |
| # ============================ | |
| def radar_plot(dim_scores: Dict[str, float], title: str, out_png: str) -> str: | |
| """ | |
| Radar 4D limpio y simétrico (patrón estable): | |
| - Círculo completo (0..2π) | |
| - Cognitiva arriba, sentido horario | |
| - Aros 1..5, etiquetas claras | |
| - Valor numérico en cada eje | |
| """ | |
| import numpy as np | |
| import matplotlib.pyplot as plt | |
| # Orden fijo de dimensiones (coincidirá con tus promedios) | |
| DIM_ORDER = ["COG", "EMO", "REL", "EJE"] | |
| labels = [DIMENSION_LABELS[d] for d in DIM_ORDER] | |
| vals = [float(dim_scores[d]) for d in DIM_ORDER] | |
| # Cerrar polígono (repetimos primer punto al final) | |
| angles = np.linspace(0, 2*np.pi, len(labels), endpoint=False) | |
| angles = np.roll(angles, -0) # no rotamos el orden base | |
| angles_cycle = np.concatenate([angles, [angles[0]]]) | |
| vals_cycle = np.concatenate([vals, [vals[0]]]) | |
| # Lienzo polar | |
| fig = plt.figure(figsize=(8, 8)) | |
| ax = plt.subplot(111, polar=True) | |
| ax.set_facecolor("white") | |
| # Cognitiva arriba (90°) y sentido horario | |
| ax.set_theta_offset(np.pi / 2) | |
| ax.set_theta_direction(-1) | |
| # Ticks de categorías | |
| ax.set_xticks(angles) | |
| ax.set_xticklabels(labels, fontsize=14, fontweight="bold") | |
| # Radial 0–5 con aros visibles | |
| ax.set_ylim(0, 5) | |
| ax.set_yticks([1, 2, 3, 4, 5]) | |
| ax.set_yticklabels(["1", "2", "3", "4", "5"], fontsize=11) | |
| ax.yaxis.grid(True, linewidth=0.8, alpha=0.6) | |
| ax.xaxis.grid(True, linewidth=0.8, alpha=0.6) | |
| # Polígono (borde + relleno suave) — sin especificar colores | |
| ax.plot(angles_cycle, vals_cycle, linewidth=2.2) | |
| ax.fill(angles_cycle, vals_cycle, alpha=0.18) | |
| # Marcadores + etiqueta de valor en cada eje | |
| for ang, v in zip(angles, vals): | |
| ax.plot([ang], [v], marker="o", markersize=6) | |
| ax.text(ang, min(5, v + 0.22), f"{v:.2f}", | |
| ha="center", va="center", fontsize=11, fontweight="bold") | |
| # Títulos | |
| ax.set_title(title, fontsize=22, fontweight="bold", pad=18) | |
| bal = float(dim_scores.get("BALANCE_360", 0.0)) | |
| dom = max(DIM_ORDER, key=lambda d: dim_scores[d]) | |
| subtitle = f"Balance 360: {bal:.3f} · Eje dominante: {DIMENSION_LABELS[dom]} ({dim_scores[dom]:.2f}/5)" | |
| fig.text(0.5, 0.03, subtitle, ha="center", va="center", fontsize=11) | |
| fig.tight_layout() | |
| fig.savefig(out_png, dpi=240, bbox_inches="tight") | |
| plt.close(fig) | |
| return out_png | |
| # ============================ | |
| # Gradio App | |
| # ============================ | |
| def items_schema() -> List[Dict[str, str]]: | |
| return [{"code": it.code, "text": it.text, "dimension": DIMENSION_LABELS[it.dimension], "reverse": it.reverse} for it in ITEMS] | |
| def _ensure_outdir() -> Path: | |
| out_dir = Path("mg360_resultados") | |
| out_dir.mkdir(parents=True, exist_ok=True) | |
| return out_dir | |
| def _evaluate_internal(res_vals: List[int]): | |
| schema = items_schema() | |
| responses = { schema[i]["code"]: int(res_vals[i]) for i in range(len(schema)) } | |
| scores = score_responses(responses) | |
| inter = interpret(scores) | |
| ts = datetime.now().strftime("%Y%m%d_%H%M%S") | |
| out_dir = _ensure_outdir() | |
| out_json = out_dir / f"mg360_reporte_{ts}.json" | |
| out_png = out_dir / f"mg360_radar_{ts}.png" | |
| radar_plot(scores, "Perfil MG360 (1–5)", str(out_png)) | |
| with open(out_json, "w", encoding="utf-8") as f: | |
| json.dump({"responses": responses, "scores": scores, "interpretation": inter}, f, ensure_ascii=False, indent=2) | |
| # Markdown de resultados | |
| md = [ | |
| "**Resultados**", | |
| *(f"- {DIMENSION_LABELS[d]}: {scores[d]:.2f}/5" for d in ["COG","EMO","REL","EJE"]), | |
| f"- **BALANCE 360**: {scores['BALANCE_360']:.3f}", | |
| "", | |
| "**Interpretación**", | |
| f"- Equilibrio: {inter['equilibrio']}", | |
| f"- Eje dominante: {inter['eje_dominante']}", | |
| ] | |
| return str(out_png), "\n".join(md), json.dumps({"responses": responses, "scores": scores, "interpretation": inter}, ensure_ascii=False, indent=2) | |
| # ============ Lanzador Gradio ============ | |
| def main_gradio(): | |
| import gradio as gr | |
| schema = items_schema() | |
| with gr.Blocks() as demo: | |
| gr.Markdown("# Test MG360 (24 ítems) — Versión Avanzada") | |
| gr.Markdown("**Escala 1–5:** 1=**Nunca**, 2=**Rara vez**, 3=**A veces**, 4=**Frecuente**, 5=**Siempre**.") | |
| with gr.Accordion("Cuestionario (24 ítems)", open=True): | |
| gr.Markdown("### Guía de escala: 1=**Nunca** · 2=**Rara vez** · 3=**A veces** · 4=**Frecuente** · 5=**Siempre**") | |
| sliders = [ | |
| gr.Slider(1, 5, step=1, value=3, | |
| label=f"{it['code']} — {it['text']} (1 Nunca · 2 Rara vez · 3 A veces · 4 Frecuente · 5 Siempre)") | |
| for it in schema | |
| ] | |
| btn = gr.Button("Evaluar") | |
| img = gr.Image(type="filepath", label="Radar 4D (1–5)") | |
| md = gr.Markdown() | |
| js = gr.Code(language="json", label="Reporte (JSON)") | |
| # Importante: la función acepta múltiples parámetros (uno por slider) | |
| def evaluate(*vals): | |
| return _evaluate_internal(list(vals)) | |
| btn.click(fn=evaluate, inputs=sliders, outputs=[img, md, js]) | |
| demo.launch() | |
| if __name__ == "__main__": | |
| main_gradio() | |