Spaces:
Running
Running
Update app.py
Browse files
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"
|
| 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 "
|
| 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("#
|
| 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("
|
| 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("
|
| 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("
|
| 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("
|
| 499 |
-
btn_limpiar = gr.Button("
|
| 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="
|
| 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, "
|