EHRley commited on
Commit
4bdedde
·
verified ·
1 Parent(s): 42a50ff

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +189 -9
app.py CHANGED
@@ -343,7 +343,7 @@ def guardar_participante(edad, talla, sexo, ejercicio_label,
343
  else:
344
  df_nuevo.to_csv(CSV_PATH, mode="w", header=True, index=False)
345
 
346
- return f" Participante {nuevo_id} guardado.", obtener_tabla_csv()
347
 
348
 
349
  def obtener_tabla_csv():
@@ -364,15 +364,135 @@ def obtener_csv_path():
364
  def limpiar_csv():
365
  if os.path.exists(CSV_PATH):
366
  os.remove(CSV_PATH)
367
- return "🗑️ Registros eliminados.", pd.DataFrame(columns=COLUMNAS_CAPTURA)
368
 
369
 
370
  # ─────────────────────────────────────────────
371
  # INTERFAZ
372
  # ─────────────────────────────────────────────
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
374
 
375
- gr.Markdown("# 🏃 Experimento: ¿Cómo afecta el ejercicio físico al estado de ánimo?")
376
  gr.Markdown(
377
  "**Hipótesis:** El nivel de ejercicio físico predice significativamente el cambio en el estado de ánimo, "
378
  "controlando capacidad pulmonar, oximetría, edad, talla y sexo.\n\n"
@@ -388,14 +508,14 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
388
  gr.Markdown("### Carga de datos")
389
  file_input = gr.File(label="Subir CSV/Excel externo (opcional)",
390
  file_types=[".csv", ".xlsx"])
391
- load_btn = gr.Button("🔄 Entrenar con datos base (+ archivo si se subió)", variant="primary")
392
  gr.Markdown("---")
393
  gr.Markdown(
394
  "**¿Ya capturaste participantes?**\n\n"
395
  "Usa el botón de abajo para reentrenar el modelo con los datos base "
396
  "**más** todos los participantes que registraste en la Sección 3."
397
  )
398
- retrain_btn = gr.Button("🧠 Reentrenar con datos base + CSV capturado", variant="secondary")
399
  status_out = gr.Textbox(label="Estado del modelo", value=init_msg, interactive=False)
400
  n_part_out = gr.Number(label="Total de participantes usados", value=init_count, interactive=False)
401
 
@@ -443,7 +563,7 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
443
  with gr.Row():
444
  spo2_a_pred = gr.Number(label="Oximetría ANTES (%)", value=97.0, step=0.5)
445
  spo2_d_pred = gr.Number(label="Oximetría DESPUÉS (%)", value=98.0, step=0.5)
446
- btn_pred = gr.Button("🔮 Calcular ΔÁnimo Predicho", variant="primary")
447
 
448
  with gr.Column():
449
  salida_pred = gr.Markdown(label="Resultado")
@@ -495,8 +615,8 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
495
  )
496
 
497
  with gr.Row():
498
- btn_guardar = gr.Button("💾 Guardar Participante", variant="primary")
499
- btn_limpiar = gr.Button("🗑️ Borrar Todos los Registros")
500
  msg_cap = gr.Textbox(label="Estado del registro", interactive=False)
501
 
502
  with gr.Column():
@@ -507,7 +627,7 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
507
  interactive=False,
508
  wrap=True
509
  )
510
- btn_dl = gr.DownloadButton(label="⬇️ Descargar CSV", value=obtener_csv_path)
511
 
512
  btn_guardar.click(
513
  guardar_participante,
@@ -521,7 +641,67 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
521
  inputs=None,
522
  outputs=[msg_cap, tabla_cap]
523
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
524
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
525
  gr.Markdown("---")
526
  gr.Markdown(
527
  "*Basado en: Ruiz Cruz, EH (2026). Pensamiento científico auténtico mediante STEAM, "
 
343
  else:
344
  df_nuevo.to_csv(CSV_PATH, mode="w", header=True, index=False)
345
 
346
+ return f" Participante {nuevo_id} guardado.", obtener_tabla_csv()
347
 
348
 
349
  def obtener_tabla_csv():
 
364
  def limpiar_csv():
365
  if os.path.exists(CSV_PATH):
366
  os.remove(CSV_PATH)
367
+ return " Registros eliminados.", pd.DataFrame(columns=COLUMNAS_CAPTURA)
368
 
369
 
370
  # ─────────────────────────────────────────────
371
  # INTERFAZ
372
  # ─────────────────────────────────────────────
373
+ def generar_graficas():
374
+ """Genera gráficas de mejora del estado de ánimo desde el CSV capturado."""
375
+ import matplotlib
376
+ matplotlib.use("Agg")
377
+ import matplotlib.pyplot as plt
378
+ import matplotlib.patches as mpatches
379
+ import io, base64
380
+
381
+ if not os.path.exists(CSV_PATH):
382
+ return None, None, None, "⚠️ No hay datos capturados todavía. Registra participantes primero."
383
+
384
+ try:
385
+ df = pd.read_csv(CSV_PATH)
386
+ except Exception as e:
387
+ return None, None, None, f"❌ Error leyendo CSV: {e}"
388
+
389
+ if df.empty or "Animo_Antes" not in df.columns:
390
+ return None, None, None, "⚠️ El CSV no tiene datos de ánimo."
391
+
392
+ df = df.dropna(subset=["Animo_Antes","Animo_Despues","Delta_Animo","Ejercicio"])
393
+ if len(df) < 2:
394
+ return None, None, None, "⚠️ Se necesitan al menos 2 participantes para graficar."
395
+
396
+ EJER_NOMBRE = {1:"Sedentario", 2:"Ocasional", 3:"Moderado", 4:"Activo", 5:"Muy activo"}
397
+ ANIMO_NOMBRE = {1:"Muy bajo", 2:"Bajo", 3:"Neutral", 4:"Alto", 5:"Muy alto"}
398
+ COLORES = {1:"#e74c3c", 2:"#e67e22", 3:"#f1c40f", 4:"#2ecc71", 5:"#27ae60"}
399
+
400
+ def fig_to_img(fig):
401
+ buf = io.BytesIO()
402
+ fig.savefig(buf, format="png", dpi=130, bbox_inches="tight")
403
+ buf.seek(0)
404
+ plt.close(fig)
405
+ return buf
406
+
407
+ # ── GRÁFICA 1: Antes vs Después por participante (barras agrupadas) ──
408
+ fig1, ax1 = plt.subplots(figsize=(max(8, len(df)*0.6), 5))
409
+ ids = [f"P{int(i)}" for i in df["ID"]]
410
+ x = range(len(df))
411
+ w = 0.35
412
+ bars_a = ax1.bar([i - w/2 for i in x], df["Animo_Antes"], w, label="Antes", color="#5b8dd9", alpha=0.85)
413
+ bars_d = ax1.bar([i + w/2 for i in x], df["Animo_Despues"], w, label="Después", color="#27ae60", alpha=0.85)
414
+ for bar, delta in zip(bars_d, df["Delta_Animo"]):
415
+ color = "#27ae60" if delta > 0 else ("#e74c3c" if delta < 0 else "#888")
416
+ ax1.text(bar.get_x() + bar.get_width()/2,
417
+ bar.get_height() + 0.05,
418
+ f"{delta:+.0f}", ha="center", va="bottom",
419
+ fontsize=8, color=color, fontweight="bold")
420
+ ax1.set_xticks(list(x))
421
+ ax1.set_xticklabels(ids, rotation=45, ha="right", fontsize=8)
422
+ ax1.set_yticks([1,2,3,4,5])
423
+ ax1.set_yticklabels([f"{v} – {ANIMO_NOMBRE[v]}" for v in [1,2,3,4,5]], fontsize=8)
424
+ ax1.set_ylim(0, 6)
425
+ ax1.set_title("Estado de Ánimo: Antes vs Después por Participante", fontsize=13, fontweight="bold", pad=12)
426
+ ax1.set_xlabel("Participante")
427
+ ax1.set_ylabel("Nivel de Ánimo (Likert 1–5)")
428
+ ax1.legend(loc="upper left")
429
+ ax1.spines[["top","right"]].set_visible(False)
430
+ ax1.grid(axis="y", alpha=0.3)
431
+ fig1.tight_layout()
432
+
433
+ # ── GRÁFICA 2: ΔÁnimo por nivel de ejercicio (boxplot + puntos) ──
434
+ fig2, ax2 = plt.subplots(figsize=(7, 5))
435
+ niveles_presentes = sorted(df["Ejercicio"].dropna().unique().astype(int))
436
+ data_box = [df[df["Ejercicio"] == n]["Delta_Animo"].values for n in niveles_presentes]
437
+ etiquetas = [f"{n}\n{EJER_NOMBRE.get(n,'')}" for n in niveles_presentes]
438
+ bp = ax2.boxplot(data_box, patch_artist=True, widths=0.45,
439
+ medianprops=dict(color="black", linewidth=2))
440
+ for patch, niv in zip(bp["boxes"], niveles_presentes):
441
+ patch.set_facecolor(COLORES.get(niv, "#aaa"))
442
+ patch.set_alpha(0.7)
443
+ for i, (niv, datos) in enumerate(zip(niveles_presentes, data_box), 1):
444
+ jitter = np.random.uniform(-0.15, 0.15, size=len(datos))
445
+ ax2.scatter([i + j for j in jitter], datos,
446
+ color=COLORES.get(niv, "#aaa"), s=50, zorder=5, edgecolors="white", linewidths=0.5)
447
+ ax2.axhline(0, color="#e74c3c", linestyle="--", linewidth=1.2, alpha=0.7, label="Sin cambio (Δ=0)")
448
+ ax2.set_xticks(range(1, len(niveles_presentes)+1))
449
+ ax2.set_xticklabels(etiquetas, fontsize=9)
450
+ ax2.set_title("Cambio en Ánimo (ΔÁnimo) según Nivel de Ejercicio", fontsize=13, fontweight="bold", pad=12)
451
+ ax2.set_xlabel("Nivel de Ejercitación")
452
+ ax2.set_ylabel("ΔÁnimo (puntos Likert)")
453
+ ax2.legend(fontsize=9)
454
+ ax2.spines[["top","right"]].set_visible(False)
455
+ ax2.grid(axis="y", alpha=0.3)
456
+ fig2.tight_layout()
457
+
458
+ # ── GRÁFICA 3: Proporción de mejora / sin cambio / descenso ──
459
+ fig3, ax3 = plt.subplots(figsize=(6, 5))
460
+ mejora = (df["Delta_Animo"] > 0).sum()
461
+ igual = (df["Delta_Animo"] == 0).sum()
462
+ descenso = (df["Delta_Animo"] < 0).sum()
463
+ total = len(df)
464
+ cats = []
465
+ vals = []
466
+ colors = []
467
+ labels_pie = []
468
+ for label, val, col in [("Mejora ", mejora, "#27ae60"),
469
+ ("Sin cambio ", igual, "#f1c40f"),
470
+ ("Descenso ", descenso, "#e74c3c")]:
471
+ if val > 0:
472
+ cats.append(label); vals.append(val)
473
+ colors.append(col)
474
+ labels_pie.append(f"{label}\n{val} participantes\n({val/total*100:.0f}%)")
475
+ wedges, texts = ax3.pie(vals, colors=colors, startangle=90,
476
+ wedgeprops=dict(width=0.55, edgecolor="white", linewidth=2))
477
+ ax3.legend(wedges, labels_pie, loc="lower center",
478
+ bbox_to_anchor=(0.5, -0.18), ncol=len(vals), fontsize=9)
479
+ ax3.set_title("Distribución del Cambio en Estado de Ánimo", fontsize=13, fontweight="bold", pad=12)
480
+ centro = dict(ha="center", va="center", fontsize=14, fontweight="bold", color="#333")
481
+ ax3.text(0, 0, f"n={total}", **centro)
482
+ fig3.tight_layout()
483
+
484
+ pct_mejora = mejora / total * 100 if total > 0 else 0
485
+ resumen = (
486
+ f" **Resumen:** {total} participantes analizados — "
487
+ f"**{mejora} mejoraron** ({pct_mejora:.0f}%), "
488
+ f"{igual} sin cambio, {descenso} con descenso. "
489
+ f"ΔÁnimo promedio: **{df['Delta_Animo'].mean():+.2f} puntos**."
490
+ )
491
+
492
+ return fig_to_img(fig1), fig_to_img(fig2), fig_to_img(fig3), resumen
493
  with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
494
 
495
+ gr.Markdown("# Experimento: ¿Cómo afecta el ejercicio físico al estado de ánimo?")
496
  gr.Markdown(
497
  "**Hipótesis:** El nivel de ejercicio físico predice significativamente el cambio en el estado de ánimo, "
498
  "controlando capacidad pulmonar, oximetría, edad, talla y sexo.\n\n"
 
508
  gr.Markdown("### Carga de datos")
509
  file_input = gr.File(label="Subir CSV/Excel externo (opcional)",
510
  file_types=[".csv", ".xlsx"])
511
+ load_btn = gr.Button(" Entrenar con datos base (+ archivo si se subió)", variant="primary")
512
  gr.Markdown("---")
513
  gr.Markdown(
514
  "**¿Ya capturaste participantes?**\n\n"
515
  "Usa el botón de abajo para reentrenar el modelo con los datos base "
516
  "**más** todos los participantes que registraste en la Sección 3."
517
  )
518
+ retrain_btn = gr.Button(" Reentrenar con datos base + CSV capturado", variant="secondary")
519
  status_out = gr.Textbox(label="Estado del modelo", value=init_msg, interactive=False)
520
  n_part_out = gr.Number(label="Total de participantes usados", value=init_count, interactive=False)
521
 
 
563
  with gr.Row():
564
  spo2_a_pred = gr.Number(label="Oximetría ANTES (%)", value=97.0, step=0.5)
565
  spo2_d_pred = gr.Number(label="Oximetría DESPUÉS (%)", value=98.0, step=0.5)
566
+ btn_pred = gr.Button(" Calcular ΔÁnimo Predicho", variant="primary")
567
 
568
  with gr.Column():
569
  salida_pred = gr.Markdown(label="Resultado")
 
615
  )
616
 
617
  with gr.Row():
618
+ btn_guardar = gr.Button(" Guardar Participante", variant="primary")
619
+ btn_limpiar = gr.Button(" Borrar Todos los Registros")
620
  msg_cap = gr.Textbox(label="Estado del registro", interactive=False)
621
 
622
  with gr.Column():
 
627
  interactive=False,
628
  wrap=True
629
  )
630
+ btn_dl = gr.DownloadButton(label=" Descargar CSV", value=obtener_csv_path)
631
 
632
  btn_guardar.click(
633
  guardar_participante,
 
641
  inputs=None,
642
  outputs=[msg_cap, tabla_cap]
643
  )
644
+ # ── SECCIÓN 4: GRÁFICAS ────────────────────────────────────────
645
+ gr.Markdown("---")
646
+ gr.Markdown("## 4. Gráficas de Mejora en el Estado de Ánimo")
647
+ gr.Markdown(
648
+ "Genera las gráficas con los participantes ya capturados. "
649
+ "Puedes actualizar en cualquier momento después de agregar más registros."
650
+ )
651
+ btn_graficas = gr.Button(" Generar Gráficas", variant="primary")
652
+ resumen_graf = gr.Markdown()
653
+ with gr.Row():
654
+ graf1 = gr.Image(label="Antes vs Después por participante", type="filepath")
655
+ graf2 = gr.Image(label="ΔÁnimo por nivel de ejercicio", type="filepath")
656
+ graf3 = gr.Image(label="Distribución del cambio (dona)", type="filepath")
657
+ def guardar_y_graficar(*args):
658
+ """Guarda participante y actualiza gráficas automáticamente."""
659
+ msg, tabla = guardar_participante(*args)
660
+ f1, f2, f3, res = generar_graficas()
661
+ imgs = []
662
+ for buf in [f1, f2, f3]:
663
+ if buf:
664
+ path = f"/tmp/graf_{len(imgs)}.png"
665
+ with open(path, "wb") as fout:
666
+ fout.write(buf.read())
667
+ imgs.append(path)
668
+ else:
669
+ imgs.append(None)
670
+ return msg, tabla, res, imgs[0], imgs[1], imgs[2]
671
 
672
+ btn_graficas.click(
673
+ lambda: _run_graficas(),
674
+ inputs=None,
675
+ outputs=[resumen_graf, graf1, graf2, graf3]
676
+ )
677
+
678
+ def _run_graficas():
679
+ f1, f2, f3, res = generar_graficas()
680
+ imgs = []
681
+ for buf in [f1, f2, f3]:
682
+ if buf:
683
+ import tempfile
684
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
685
+ tmp.write(buf.read()); tmp.flush()
686
+ imgs.append(tmp.name)
687
+ else:
688
+ imgs.append(None)
689
+ return res, imgs[0], imgs[1], imgs[2]
690
+
691
+ btn_graficas.click(
692
+ _run_graficas,
693
+ inputs=None,
694
+ outputs=[resumen_graf, graf1, graf2, graf3]
695
+ )
696
+
697
+ # Reemplaza también el btn_guardar.click existente con este:
698
+ btn_guardar.click(
699
+ guardar_y_graficar,
700
+ inputs=[edad_cap, talla_cap, sexo_cap, ejercicio_cap,
701
+ cap_a_cap, cap_d_cap, spo2_a_cap, spo2_d_cap,
702
+ animo_a_cap, animo_d_cap],
703
+ outputs=[msg_cap, tabla_cap, resumen_graf, graf1, graf2, graf3]
704
+ )
705
  gr.Markdown("---")
706
  gr.Markdown(
707
  "*Basado en: Ruiz Cruz, EH (2026). Pensamiento científico auténtico mediante STEAM, "