Spaces:
Running
Running
Update app.py
Browse files
app.py
CHANGED
|
@@ -11,11 +11,7 @@ import matplotlib.pyplot as plt
|
|
| 11 |
import io
|
| 12 |
|
| 13 |
# ─────────────────────────────────────────────
|
| 14 |
-
#
|
| 15 |
-
# ID, Edad, Talla(m), Sexo(0=M,1=H), Ejercicio(1-5),
|
| 16 |
-
# CapPulm_Antes(L), CapPulm_Despues(L),
|
| 17 |
-
# SpO2_Antes(%), SpO2_Despues(%),
|
| 18 |
-
# Animo_Antes(1-5), Animo_Despues(1-5)
|
| 19 |
# ─────────────────────────────────────────────
|
| 20 |
BASE_DATA = [
|
| 21 |
[1, 17, 1.62, 0, 3, 2.8, 3.1, 97, 98, 3, 5],
|
|
@@ -60,27 +56,25 @@ COLUMNAS_CAPTURA = [
|
|
| 60 |
]
|
| 61 |
|
| 62 |
EJERCICIO_LABELS = {
|
| 63 |
-
"1
|
| 64 |
-
"2
|
| 65 |
-
"3
|
| 66 |
-
"4
|
| 67 |
-
"5
|
| 68 |
}
|
| 69 |
ANIMO_LABELS = {
|
| 70 |
-
"1
|
| 71 |
-
"2
|
| 72 |
-
"3
|
| 73 |
-
"4
|
| 74 |
-
"5
|
| 75 |
}
|
| 76 |
|
| 77 |
EJER_NOMBRE = {1: "Sedentario", 2: "Ocasional", 3: "Moderado", 4: "Activo", 5: "Muy activo"}
|
| 78 |
ANIMO_NOMBRE = {1: "Muy bajo", 2: "Bajo", 3: "Neutral", 4: "Alto", 5: "Muy alto"}
|
| 79 |
COLORES_EJER = {1: "#e74c3c", 2: "#e67e22", 3: "#f1c40f", 4: "#2ecc71", 5: "#27ae60"}
|
| 80 |
|
| 81 |
-
|
| 82 |
-
_DATA_DIR = "/data" if os.path.isdir("/data") else _tf.gettempdir()
|
| 83 |
-
CSV_PATH = os.path.join(_DATA_DIR, "datos_animo_ejercicio.csv")
|
| 84 |
|
| 85 |
estado = {
|
| 86 |
"modelo": None,
|
|
@@ -91,11 +85,9 @@ estado = {
|
|
| 91 |
"intercept": None,
|
| 92 |
}
|
| 93 |
|
| 94 |
-
|
| 95 |
# ─────────────────────────────────────────────
|
| 96 |
# LÓGICA DEL MODELO
|
| 97 |
# ─────────────────────────────────────────────
|
| 98 |
-
|
| 99 |
def calcular_deltas(df):
|
| 100 |
df = df.copy()
|
| 101 |
df["Delta_Animo"] = df["Animo_Despues"] - df["Animo_Antes"]
|
|
@@ -103,7 +95,6 @@ def calcular_deltas(df):
|
|
| 103 |
df["Delta_SpO2"] = df["SpO2_Despues"] - df["SpO2_Antes"]
|
| 104 |
return df
|
| 105 |
|
| 106 |
-
|
| 107 |
def entrenar_modelo(df):
|
| 108 |
df = calcular_deltas(df)
|
| 109 |
X = df[["Ejercicio", "Delta_CapPulm", "Delta_SpO2", "Edad", "Talla_m", "Sexo"]].values
|
|
@@ -112,7 +103,6 @@ def entrenar_modelo(df):
|
|
| 112 |
r2 = r2_score(y, modelo.predict(X))
|
| 113 |
return modelo, r2, df
|
| 114 |
|
| 115 |
-
|
| 116 |
def _construir_salidas_modelo(df, df_delta, modelo, r2, fuente):
|
| 117 |
estado["modelo"] = modelo
|
| 118 |
estado["df"] = df_delta
|
|
@@ -160,14 +150,13 @@ def _construir_salidas_modelo(df, df_delta, modelo, r2, fuente):
|
|
| 160 |
]]
|
| 161 |
|
| 162 |
return (
|
| 163 |
-
f"
|
| 164 |
len(df),
|
| 165 |
tabla,
|
| 166 |
ecuacion,
|
| 167 |
coef_md,
|
| 168 |
)
|
| 169 |
|
| 170 |
-
|
| 171 |
def _csv_capturado_a_df():
|
| 172 |
if not os.path.exists(CSV_PATH):
|
| 173 |
return None, "No hay datos capturados en el CSV todavía."
|
|
@@ -192,7 +181,6 @@ def _csv_capturado_a_df():
|
|
| 192 |
except Exception as e:
|
| 193 |
return None, f"Error leyendo el CSV: {e}"
|
| 194 |
|
| 195 |
-
|
| 196 |
def cargar_y_entrenar(file_obj=None):
|
| 197 |
df = pd.DataFrame(BASE_DATA, columns=COLUMNS)
|
| 198 |
|
|
@@ -217,21 +205,20 @@ def cargar_y_entrenar(file_obj=None):
|
|
| 217 |
df = pd.concat([df, df_new], ignore_index=True)
|
| 218 |
fuente = f"datos base (24 reg.) + archivo subido ({len(df_new)} reg.)"
|
| 219 |
except Exception as e:
|
| 220 |
-
return f"
|
| 221 |
else:
|
| 222 |
fuente = "datos base únicamente"
|
| 223 |
|
| 224 |
modelo, r2, df_delta = entrenar_modelo(df)
|
| 225 |
return _construir_salidas_modelo(df, df_delta, modelo, r2, fuente)
|
| 226 |
|
| 227 |
-
|
| 228 |
def reentrenar_con_csv():
|
| 229 |
df_base = pd.DataFrame(BASE_DATA, columns=COLUMNS)
|
| 230 |
df_csv, err = _csv_capturado_a_df()
|
| 231 |
|
| 232 |
if err:
|
| 233 |
return (
|
| 234 |
-
f"Error {err}",
|
| 235 |
len(df_base),
|
| 236 |
pd.DataFrame(),
|
| 237 |
"— Entrena primero el modelo —",
|
|
@@ -244,15 +231,13 @@ def reentrenar_con_csv():
|
|
| 244 |
fuente = f"datos base (24 reg.) + CSV capturado ({len(df_csv)} reg.)"
|
| 245 |
return _construir_salidas_modelo(df_combined, df_delta, modelo, r2, fuente)
|
| 246 |
|
| 247 |
-
|
| 248 |
# ─────────────────────────────────────────────
|
| 249 |
# PREDICCIÓN
|
| 250 |
# ─────────────────────────────────────────────
|
| 251 |
-
|
| 252 |
def predecir(ejercicio_label, edad, talla, sexo,
|
| 253 |
cap_antes, cap_despues, spo2_antes, spo2_despues):
|
| 254 |
if not estado["entrenado"]:
|
| 255 |
-
return "
|
| 256 |
|
| 257 |
ejer_val = EJERCICIO_LABELS.get(ejercicio_label, 1)
|
| 258 |
sexo_val = 1 if sexo == "Hombre" else 0
|
|
@@ -265,18 +250,18 @@ def predecir(ejercicio_label, edad, talla, sexo,
|
|
| 265 |
b0 = estado["intercept"]
|
| 266 |
|
| 267 |
if pred >= 1.5:
|
| 268 |
-
interp = "
|
| 269 |
elif pred >= 0.5:
|
| 270 |
-
interp = "
|
| 271 |
elif pred >= -0.5:
|
| 272 |
-
interp = "
|
| 273 |
else:
|
| 274 |
-
interp = "
|
| 275 |
|
| 276 |
hip = (
|
| 277 |
-
"
|
| 278 |
if c[0] > 0 else
|
| 279 |
-
"
|
| 280 |
)
|
| 281 |
|
| 282 |
return (
|
|
@@ -298,11 +283,9 @@ def predecir(ejercicio_label, edad, talla, sexo,
|
|
| 298 |
f"**Hipótesis:** {hip}"
|
| 299 |
)
|
| 300 |
|
| 301 |
-
|
| 302 |
# ─────────────────────────────────────────────
|
| 303 |
# CAPTURA DE PARTICIPANTES
|
| 304 |
# ─────────────────────────────────────────────
|
| 305 |
-
|
| 306 |
def obtener_siguiente_id():
|
| 307 |
if os.path.exists(CSV_PATH):
|
| 308 |
try:
|
|
@@ -313,7 +296,6 @@ def obtener_siguiente_id():
|
|
| 313 |
pass
|
| 314 |
return 1
|
| 315 |
|
| 316 |
-
|
| 317 |
def obtener_tabla_csv():
|
| 318 |
if os.path.exists(CSV_PATH):
|
| 319 |
try:
|
|
@@ -322,18 +304,15 @@ def obtener_tabla_csv():
|
|
| 322 |
pass
|
| 323 |
return pd.DataFrame(columns=COLUMNAS_CAPTURA)
|
| 324 |
|
| 325 |
-
|
| 326 |
def obtener_csv_path():
|
| 327 |
if not os.path.exists(CSV_PATH):
|
| 328 |
pd.DataFrame(columns=COLUMNAS_CAPTURA).to_csv(CSV_PATH, index=False)
|
| 329 |
return CSV_PATH
|
| 330 |
|
| 331 |
-
|
| 332 |
def limpiar_csv():
|
| 333 |
if os.path.exists(CSV_PATH):
|
| 334 |
os.remove(CSV_PATH)
|
| 335 |
-
return "
|
| 336 |
-
|
| 337 |
|
| 338 |
def guardar_participante(edad, talla, sexo, ejercicio_label,
|
| 339 |
cap_antes, cap_despues,
|
|
@@ -347,7 +326,7 @@ def guardar_participante(edad, talla, sexo, ejercicio_label,
|
|
| 347 |
if not (0 < cap_antes <= 10): errores.append("Cap. Pulmonar Antes fuera de rango (0-10 L).")
|
| 348 |
if not (0 < cap_despues <= 10): errores.append("Cap. Pulmonar Después fuera de rango (0-10 L).")
|
| 349 |
if errores:
|
| 350 |
-
return "
|
| 351 |
|
| 352 |
sexo_val = 1 if sexo == "Hombre" else 0
|
| 353 |
ejer_val = EJERCICIO_LABELS.get(ejercicio_label, 1)
|
|
@@ -372,20 +351,19 @@ def guardar_participante(edad, talla, sexo, ejercicio_label,
|
|
| 372 |
}
|
| 373 |
|
| 374 |
df_nuevo = pd.DataFrame([nueva_fila], columns=COLUMNAS_CAPTURA)
|
| 375 |
-
|
| 376 |
-
|
| 377 |
-
|
| 378 |
-
|
| 379 |
-
|
| 380 |
-
|
| 381 |
-
|
|
|
|
| 382 |
|
| 383 |
# ─────────────────────────────────────────────
|
| 384 |
# GRÁFICAS
|
| 385 |
# ─────────────────────────────────────────────
|
| 386 |
-
|
| 387 |
def _buf_a_tempfile(buf):
|
| 388 |
-
"""Guarda un BytesIO en un archivo temporal y devuelve la ruta."""
|
| 389 |
if buf is None:
|
| 390 |
return None
|
| 391 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
|
|
@@ -394,25 +372,23 @@ def _buf_a_tempfile(buf):
|
|
| 394 |
tmp.close()
|
| 395 |
return tmp.name
|
| 396 |
|
| 397 |
-
|
| 398 |
def generar_graficas():
|
| 399 |
-
"""Devuelve (path1, path2, path3, resumen_md) con las 3 gráficas."""
|
| 400 |
VACIO = (None, None, None,
|
| 401 |
-
"
|
| 402 |
|
| 403 |
if not os.path.exists(CSV_PATH):
|
| 404 |
return VACIO
|
| 405 |
try:
|
| 406 |
df = pd.read_csv(CSV_PATH)
|
| 407 |
except Exception as e:
|
| 408 |
-
return None, None, None, f"
|
| 409 |
|
| 410 |
if df.empty or "Animo_Antes" not in df.columns:
|
| 411 |
return VACIO
|
| 412 |
|
| 413 |
df = df.dropna(subset=["Animo_Antes", "Animo_Despues", "Delta_Animo", "Ejercicio"])
|
| 414 |
if len(df) < 2:
|
| 415 |
-
return None, None, None, "
|
| 416 |
|
| 417 |
def save_fig(fig):
|
| 418 |
buf = io.BytesIO()
|
|
@@ -421,12 +397,12 @@ def generar_graficas():
|
|
| 421 |
plt.close(fig)
|
| 422 |
return _buf_a_tempfile(buf)
|
| 423 |
|
| 424 |
-
#
|
| 425 |
fig1, ax1 = plt.subplots(figsize=(max(8, len(df) * 0.65), 5))
|
| 426 |
ids = [f"P{int(i)}" for i in df["ID"]]
|
| 427 |
x, w = range(len(df)), 0.35
|
| 428 |
-
ax1.bar([i - w / 2 for i in x], df["Animo_Antes"],
|
| 429 |
-
label="Antes",
|
| 430 |
bars_d = ax1.bar([i + w / 2 for i in x], df["Animo_Despues"], w,
|
| 431 |
label="Después", color="#27ae60", alpha=0.85)
|
| 432 |
for bar, delta in zip(bars_d, df["Delta_Animo"]):
|
|
@@ -438,19 +414,19 @@ def generar_graficas():
|
|
| 438 |
ax1.set_xticks(list(x))
|
| 439 |
ax1.set_xticklabels(ids, rotation=45, ha="right", fontsize=8)
|
| 440 |
ax1.set_yticks([1, 2, 3, 4, 5])
|
| 441 |
-
ax1.set_yticklabels([f"{v}
|
| 442 |
ax1.set_ylim(0, 6.2)
|
| 443 |
ax1.set_title("Estado de Ánimo: Antes vs Después por Participante",
|
| 444 |
fontsize=13, fontweight="bold", pad=12)
|
| 445 |
ax1.set_xlabel("Participante")
|
| 446 |
-
ax1.set_ylabel("Nivel de Ánimo (Likert 1
|
| 447 |
ax1.legend(loc="upper left")
|
| 448 |
ax1.spines[["top", "right"]].set_visible(False)
|
| 449 |
ax1.grid(axis="y", alpha=0.3)
|
| 450 |
fig1.tight_layout()
|
| 451 |
path1 = save_fig(fig1)
|
| 452 |
|
| 453 |
-
#
|
| 454 |
fig2, ax2 = plt.subplots(figsize=(7, 5))
|
| 455 |
niveles = sorted(df["Ejercicio"].dropna().unique().astype(int))
|
| 456 |
data_box = [df[df["Ejercicio"] == n]["Delta_Animo"].values for n in niveles]
|
|
@@ -479,7 +455,7 @@ def generar_graficas():
|
|
| 479 |
fig2.tight_layout()
|
| 480 |
path2 = save_fig(fig2)
|
| 481 |
|
| 482 |
-
#
|
| 483 |
fig3, ax3 = plt.subplots(figsize=(6, 5))
|
| 484 |
total = len(df)
|
| 485 |
mejora = int((df["Delta_Animo"] > 0).sum())
|
|
@@ -487,9 +463,9 @@ def generar_graficas():
|
|
| 487 |
descenso = int((df["Delta_Animo"] < 0).sum())
|
| 488 |
|
| 489 |
vals, colors, labels_pie = [], [], []
|
| 490 |
-
for lbl, val, col in [("Mejora
|
| 491 |
-
("Sin cambio
|
| 492 |
-
("Descenso
|
| 493 |
if val > 0:
|
| 494 |
vals.append(val)
|
| 495 |
colors.append(col)
|
|
@@ -508,18 +484,13 @@ def generar_graficas():
|
|
| 508 |
|
| 509 |
pct = mejora / total * 100 if total > 0 else 0
|
| 510 |
resumen = (
|
| 511 |
-
f"
|
| 512 |
f"**{mejora} mejoraron** ({pct:.0f}%), "
|
| 513 |
f"{igual} sin cambio, {descenso} con descenso. "
|
| 514 |
f"ΔÁnimo promedio: **{df['Delta_Animo'].mean():+.2f} puntos**."
|
| 515 |
)
|
| 516 |
return path1, path2, path3, resumen
|
| 517 |
|
| 518 |
-
|
| 519 |
-
# ─────────────────────────────────────────────
|
| 520 |
-
# FUNCIONES COMBINADAS (guardar + graficar)
|
| 521 |
-
# ─────────────────────────────────────────────
|
| 522 |
-
|
| 523 |
def guardar_y_graficar(edad, talla, sexo, ejercicio_label,
|
| 524 |
cap_antes, cap_despues,
|
| 525 |
spo2_antes, spo2_despues,
|
|
@@ -533,26 +504,23 @@ def guardar_y_graficar(edad, talla, sexo, ejercicio_label,
|
|
| 533 |
p1, p2, p3, resumen = generar_graficas()
|
| 534 |
return msg, tabla, resumen, p1, p2, p3
|
| 535 |
|
| 536 |
-
|
| 537 |
# ─────────────────────────────────────────────
|
| 538 |
# INICIALIZACIÓN
|
| 539 |
# ────────────────────────────���────────────────
|
| 540 |
init_msg, init_count, init_df, init_ec, init_coef = cargar_y_entrenar()
|
| 541 |
|
| 542 |
-
|
| 543 |
# ─────────────────────────────────────────────
|
| 544 |
# INTERFAZ
|
| 545 |
# ─────────────────────────────────────────────
|
| 546 |
with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
|
| 547 |
-
|
| 548 |
-
gr.Markdown("# Experimento: ¿Cómo afecta el ejercicio físico al estado de ánimo?")
|
| 549 |
gr.Markdown(
|
| 550 |
"**Hipótesis:** El nivel de ejercicio físico predice significativamente el cambio "
|
| 551 |
"en el estado de ánimo, controlando capacidad pulmonar, oximetría, edad, talla y sexo.\n\n"
|
| 552 |
-
"**Variable dependiente:** ΔÁnimo = Ánimo_después − Ánimo_antes (escala Likert 1
|
| 553 |
)
|
| 554 |
|
| 555 |
-
#
|
| 556 |
gr.Markdown("---")
|
| 557 |
gr.Markdown("## 1. Modelo de Regresión Lineal Múltiple")
|
| 558 |
|
|
@@ -561,7 +529,7 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
|
|
| 561 |
gr.Markdown("### Carga de datos")
|
| 562 |
file_input = gr.File(label="Subir CSV/Excel externo (opcional)",
|
| 563 |
file_types=[".csv", ".xlsx"])
|
| 564 |
-
load_btn = gr.Button("
|
| 565 |
variant="primary")
|
| 566 |
gr.Markdown("---")
|
| 567 |
gr.Markdown(
|
|
@@ -569,7 +537,7 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
|
|
| 569 |
"Usa el botón de abajo para reentrenar el modelo con los datos base "
|
| 570 |
"**más** todos los participantes registrados en la Sección 3."
|
| 571 |
)
|
| 572 |
-
retrain_btn = gr.Button("
|
| 573 |
variant="secondary")
|
| 574 |
status_out = gr.Textbox(label="Estado del modelo", value=init_msg, interactive=False)
|
| 575 |
n_part_out = gr.Number(label="Total de participantes usados",
|
|
@@ -596,7 +564,7 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
|
|
| 596 |
outputs=[status_out, n_part_out, data_table, ecuacion_out, coef_out]
|
| 597 |
)
|
| 598 |
|
| 599 |
-
#
|
| 600 |
gr.Markdown("---")
|
| 601 |
gr.Markdown("## 2. Realizar una Predicción")
|
| 602 |
gr.Markdown("Ingresa el perfil de un participante para predecir su cambio esperado en el ánimo.")
|
|
@@ -620,7 +588,7 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
|
|
| 620 |
with gr.Row():
|
| 621 |
spo2_a_pred = gr.Number(label="Oximetría ANTES (%)", value=97.0, step=0.5)
|
| 622 |
spo2_d_pred = gr.Number(label="Oximetría DESPUÉS (%)", value=98.0, step=0.5)
|
| 623 |
-
btn_pred = gr.Button("
|
| 624 |
|
| 625 |
with gr.Column():
|
| 626 |
salida_pred = gr.Markdown(label="Resultado")
|
|
@@ -632,7 +600,7 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
|
|
| 632 |
outputs=salida_pred
|
| 633 |
)
|
| 634 |
|
| 635 |
-
#
|
| 636 |
gr.Markdown("---")
|
| 637 |
gr.Markdown("## 3. Registro de Participantes")
|
| 638 |
gr.Markdown("Captura los datos de cada participante antes y después de la actividad física.")
|
|
@@ -675,8 +643,8 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
|
|
| 675 |
value=list(ANIMO_LABELS.keys())[3]
|
| 676 |
)
|
| 677 |
with gr.Row():
|
| 678 |
-
btn_guardar = gr.Button("
|
| 679 |
-
btn_limpiar = gr.Button("
|
| 680 |
msg_cap = gr.Textbox(label="Estado del registro", interactive=False)
|
| 681 |
|
| 682 |
with gr.Column():
|
|
@@ -687,25 +655,40 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
|
|
| 687 |
interactive=False,
|
| 688 |
wrap=True
|
| 689 |
)
|
| 690 |
-
btn_dl = gr.
|
|
|
|
| 691 |
|
| 692 |
-
#
|
| 693 |
gr.Markdown("---")
|
| 694 |
gr.Markdown("## 4. Gráficas de Mejora en el Estado de Ánimo")
|
| 695 |
gr.Markdown(
|
| 696 |
"Las gráficas se actualizan automáticamente al guardar cada participante. "
|
| 697 |
"También puedes generarlas manualmente con el botón."
|
| 698 |
)
|
| 699 |
-
btn_graficas = gr.Button("
|
| 700 |
resumen_graf = gr.Markdown()
|
| 701 |
with gr.Row():
|
| 702 |
graf1 = gr.Image(label="Antes vs Después por participante", type="filepath")
|
| 703 |
-
graf2 = gr.Image(label="ΔÁnimo por nivel de ejercicio",
|
| 704 |
-
graf3 = gr.Image(label="Distribución del cambio (dona)",
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 705 |
|
| 706 |
-
# ── EVENTOS ────────────────────────────────────────────────────
|
| 707 |
-
|
| 708 |
-
# Guardar → actualiza tabla + gráficas automáticamente
|
| 709 |
btn_guardar.click(
|
| 710 |
guardar_y_graficar,
|
| 711 |
inputs=[edad_cap, talla_cap, sexo_cap, ejercicio_cap,
|
|
@@ -714,14 +697,18 @@ with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
|
|
| 714 |
outputs=[msg_cap, tabla_cap, resumen_graf, graf1, graf2, graf3]
|
| 715 |
)
|
| 716 |
|
| 717 |
-
# Borrar registros
|
| 718 |
btn_limpiar.click(
|
| 719 |
limpiar_csv,
|
| 720 |
inputs=None,
|
| 721 |
outputs=[msg_cap, tabla_cap]
|
| 722 |
)
|
| 723 |
|
| 724 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 725 |
btn_graficas.click(
|
| 726 |
generar_graficas,
|
| 727 |
inputs=None,
|
|
|
|
| 11 |
import io
|
| 12 |
|
| 13 |
# ─────────────────────────────────────────────
|
| 14 |
+
# CONFIGURACIÓN Y DATOS BASE
|
|
|
|
|
|
|
|
|
|
|
|
|
| 15 |
# ─────────────────────────────────────────────
|
| 16 |
BASE_DATA = [
|
| 17 |
[1, 17, 1.62, 0, 3, 2.8, 3.1, 97, 98, 3, 5],
|
|
|
|
| 56 |
]
|
| 57 |
|
| 58 |
EJERCICIO_LABELS = {
|
| 59 |
+
"1 - Sedentario (no hace ejercicio)": 1,
|
| 60 |
+
"2 - Ocasional (1 vez/semana)": 2,
|
| 61 |
+
"3 - Moderado (2-3 veces/semana)": 3,
|
| 62 |
+
"4 - Activo (4-5 veces/semana)": 4,
|
| 63 |
+
"5 - Muy activo (diario)": 5,
|
| 64 |
}
|
| 65 |
ANIMO_LABELS = {
|
| 66 |
+
"1 - Muy bajo": 1,
|
| 67 |
+
"2 - Bajo": 2,
|
| 68 |
+
"3 - Neutral": 3,
|
| 69 |
+
"4 - Alto": 4,
|
| 70 |
+
"5 - Muy alto": 5,
|
| 71 |
}
|
| 72 |
|
| 73 |
EJER_NOMBRE = {1: "Sedentario", 2: "Ocasional", 3: "Moderado", 4: "Activo", 5: "Muy activo"}
|
| 74 |
ANIMO_NOMBRE = {1: "Muy bajo", 2: "Bajo", 3: "Neutral", 4: "Alto", 5: "Muy alto"}
|
| 75 |
COLORES_EJER = {1: "#e74c3c", 2: "#e67e22", 3: "#f1c40f", 4: "#2ecc71", 5: "#27ae60"}
|
| 76 |
|
| 77 |
+
CSV_PATH = "datos_animo_ejercicio.csv"
|
|
|
|
|
|
|
| 78 |
|
| 79 |
estado = {
|
| 80 |
"modelo": None,
|
|
|
|
| 85 |
"intercept": None,
|
| 86 |
}
|
| 87 |
|
|
|
|
| 88 |
# ─────────────────────────────────────────────
|
| 89 |
# LÓGICA DEL MODELO
|
| 90 |
# ─────────────────────────────────────────────
|
|
|
|
| 91 |
def calcular_deltas(df):
|
| 92 |
df = df.copy()
|
| 93 |
df["Delta_Animo"] = df["Animo_Despues"] - df["Animo_Antes"]
|
|
|
|
| 95 |
df["Delta_SpO2"] = df["SpO2_Despues"] - df["SpO2_Antes"]
|
| 96 |
return df
|
| 97 |
|
|
|
|
| 98 |
def entrenar_modelo(df):
|
| 99 |
df = calcular_deltas(df)
|
| 100 |
X = df[["Ejercicio", "Delta_CapPulm", "Delta_SpO2", "Edad", "Talla_m", "Sexo"]].values
|
|
|
|
| 103 |
r2 = r2_score(y, modelo.predict(X))
|
| 104 |
return modelo, r2, df
|
| 105 |
|
|
|
|
| 106 |
def _construir_salidas_modelo(df, df_delta, modelo, r2, fuente):
|
| 107 |
estado["modelo"] = modelo
|
| 108 |
estado["df"] = df_delta
|
|
|
|
| 150 |
]]
|
| 151 |
|
| 152 |
return (
|
| 153 |
+
f"Modelo entrenado con {len(df)} participantes. Fuente: {fuente}",
|
| 154 |
len(df),
|
| 155 |
tabla,
|
| 156 |
ecuacion,
|
| 157 |
coef_md,
|
| 158 |
)
|
| 159 |
|
|
|
|
| 160 |
def _csv_capturado_a_df():
|
| 161 |
if not os.path.exists(CSV_PATH):
|
| 162 |
return None, "No hay datos capturados en el CSV todavía."
|
|
|
|
| 181 |
except Exception as e:
|
| 182 |
return None, f"Error leyendo el CSV: {e}"
|
| 183 |
|
|
|
|
| 184 |
def cargar_y_entrenar(file_obj=None):
|
| 185 |
df = pd.DataFrame(BASE_DATA, columns=COLUMNS)
|
| 186 |
|
|
|
|
| 205 |
df = pd.concat([df, df_new], ignore_index=True)
|
| 206 |
fuente = f"datos base (24 reg.) + archivo subido ({len(df_new)} reg.)"
|
| 207 |
except Exception as e:
|
| 208 |
+
return f"Error al procesar el archivo: {e}", 0, pd.DataFrame(), "", ""
|
| 209 |
else:
|
| 210 |
fuente = "datos base únicamente"
|
| 211 |
|
| 212 |
modelo, r2, df_delta = entrenar_modelo(df)
|
| 213 |
return _construir_salidas_modelo(df, df_delta, modelo, r2, fuente)
|
| 214 |
|
|
|
|
| 215 |
def reentrenar_con_csv():
|
| 216 |
df_base = pd.DataFrame(BASE_DATA, columns=COLUMNS)
|
| 217 |
df_csv, err = _csv_capturado_a_df()
|
| 218 |
|
| 219 |
if err:
|
| 220 |
return (
|
| 221 |
+
f"Error: {err}",
|
| 222 |
len(df_base),
|
| 223 |
pd.DataFrame(),
|
| 224 |
"— Entrena primero el modelo —",
|
|
|
|
| 231 |
fuente = f"datos base (24 reg.) + CSV capturado ({len(df_csv)} reg.)"
|
| 232 |
return _construir_salidas_modelo(df_combined, df_delta, modelo, r2, fuente)
|
| 233 |
|
|
|
|
| 234 |
# ─────────────────────────────────────────────
|
| 235 |
# PREDICCIÓN
|
| 236 |
# ─────────────────────────────────────────────
|
|
|
|
| 237 |
def predecir(ejercicio_label, edad, talla, sexo,
|
| 238 |
cap_antes, cap_despues, spo2_antes, spo2_despues):
|
| 239 |
if not estado["entrenado"]:
|
| 240 |
+
return "El modelo no ha sido entrenado. Haz clic en 'Entrenar'."
|
| 241 |
|
| 242 |
ejer_val = EJERCICIO_LABELS.get(ejercicio_label, 1)
|
| 243 |
sexo_val = 1 if sexo == "Hombre" else 0
|
|
|
|
| 250 |
b0 = estado["intercept"]
|
| 251 |
|
| 252 |
if pred >= 1.5:
|
| 253 |
+
interp = "Mejora notable del ánimo — se espera un aumento significativo."
|
| 254 |
elif pred >= 0.5:
|
| 255 |
+
interp = "Ligera mejora del ánimo — cambio positivo moderado."
|
| 256 |
elif pred >= -0.5:
|
| 257 |
+
interp = "Sin cambio relevante — el ánimo se mantiene similar."
|
| 258 |
else:
|
| 259 |
+
interp = "Posible descenso del ánimo — revisar variables de contexto."
|
| 260 |
|
| 261 |
hip = (
|
| 262 |
+
"H1 apoyada: el ejercicio predice positivamente el ánimo."
|
| 263 |
if c[0] > 0 else
|
| 264 |
+
"H1 no apoyada: el coeficiente de ejercicio es negativo en este modelo."
|
| 265 |
)
|
| 266 |
|
| 267 |
return (
|
|
|
|
| 283 |
f"**Hipótesis:** {hip}"
|
| 284 |
)
|
| 285 |
|
|
|
|
| 286 |
# ─────────────────────────────────────────────
|
| 287 |
# CAPTURA DE PARTICIPANTES
|
| 288 |
# ─────────────────────────────────────────────
|
|
|
|
| 289 |
def obtener_siguiente_id():
|
| 290 |
if os.path.exists(CSV_PATH):
|
| 291 |
try:
|
|
|
|
| 296 |
pass
|
| 297 |
return 1
|
| 298 |
|
|
|
|
| 299 |
def obtener_tabla_csv():
|
| 300 |
if os.path.exists(CSV_PATH):
|
| 301 |
try:
|
|
|
|
| 304 |
pass
|
| 305 |
return pd.DataFrame(columns=COLUMNAS_CAPTURA)
|
| 306 |
|
|
|
|
| 307 |
def obtener_csv_path():
|
| 308 |
if not os.path.exists(CSV_PATH):
|
| 309 |
pd.DataFrame(columns=COLUMNAS_CAPTURA).to_csv(CSV_PATH, index=False)
|
| 310 |
return CSV_PATH
|
| 311 |
|
|
|
|
| 312 |
def limpiar_csv():
|
| 313 |
if os.path.exists(CSV_PATH):
|
| 314 |
os.remove(CSV_PATH)
|
| 315 |
+
return "Registros eliminados.", pd.DataFrame(columns=COLUMNAS_CAPTURA)
|
|
|
|
| 316 |
|
| 317 |
def guardar_participante(edad, talla, sexo, ejercicio_label,
|
| 318 |
cap_antes, cap_despues,
|
|
|
|
| 326 |
if not (0 < cap_antes <= 10): errores.append("Cap. Pulmonar Antes fuera de rango (0-10 L).")
|
| 327 |
if not (0 < cap_despues <= 10): errores.append("Cap. Pulmonar Después fuera de rango (0-10 L).")
|
| 328 |
if errores:
|
| 329 |
+
return "Error: " + " | ".join(errores), obtener_tabla_csv()
|
| 330 |
|
| 331 |
sexo_val = 1 if sexo == "Hombre" else 0
|
| 332 |
ejer_val = EJERCICIO_LABELS.get(ejercicio_label, 1)
|
|
|
|
| 351 |
}
|
| 352 |
|
| 353 |
df_nuevo = pd.DataFrame([nueva_fila], columns=COLUMNAS_CAPTURA)
|
| 354 |
+
try:
|
| 355 |
+
modo = "a" if os.path.exists(CSV_PATH) else "w"
|
| 356 |
+
with open(CSV_PATH, mode=modo, newline="", encoding="utf-8") as f:
|
| 357 |
+
df_nuevo.to_csv(f, header=(modo == "w"), index=False)
|
| 358 |
+
return f"Participante {nueva_fila['ID']} guardado.", obtener_tabla_csv()
|
| 359 |
+
except Exception as e:
|
| 360 |
+
print(f"Error guardando CSV: {e}")
|
| 361 |
+
return f"Error al guardar: {e}", obtener_tabla_csv()
|
| 362 |
|
| 363 |
# ─────────────────────────────────────────────
|
| 364 |
# GRÁFICAS
|
| 365 |
# ─────────────────────────────────────────────
|
|
|
|
| 366 |
def _buf_a_tempfile(buf):
|
|
|
|
| 367 |
if buf is None:
|
| 368 |
return None
|
| 369 |
tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".png")
|
|
|
|
| 372 |
tmp.close()
|
| 373 |
return tmp.name
|
| 374 |
|
|
|
|
| 375 |
def generar_graficas():
|
|
|
|
| 376 |
VACIO = (None, None, None,
|
| 377 |
+
"No hay datos capturados todavía. Registra participantes en la Sección 3.")
|
| 378 |
|
| 379 |
if not os.path.exists(CSV_PATH):
|
| 380 |
return VACIO
|
| 381 |
try:
|
| 382 |
df = pd.read_csv(CSV_PATH)
|
| 383 |
except Exception as e:
|
| 384 |
+
return None, None, None, f"Error leyendo CSV: {e}"
|
| 385 |
|
| 386 |
if df.empty or "Animo_Antes" not in df.columns:
|
| 387 |
return VACIO
|
| 388 |
|
| 389 |
df = df.dropna(subset=["Animo_Antes", "Animo_Despues", "Delta_Animo", "Ejercicio"])
|
| 390 |
if len(df) < 2:
|
| 391 |
+
return None, None, None, "Se necesitan al menos 2 participantes para graficar."
|
| 392 |
|
| 393 |
def save_fig(fig):
|
| 394 |
buf = io.BytesIO()
|
|
|
|
| 397 |
plt.close(fig)
|
| 398 |
return _buf_a_tempfile(buf)
|
| 399 |
|
| 400 |
+
# Gráfica 1: Barras Antes vs Después por participante
|
| 401 |
fig1, ax1 = plt.subplots(figsize=(max(8, len(df) * 0.65), 5))
|
| 402 |
ids = [f"P{int(i)}" for i in df["ID"]]
|
| 403 |
x, w = range(len(df)), 0.35
|
| 404 |
+
ax1.bar([i - w / 2 for i in x], df["Animo_Antes"], w,
|
| 405 |
+
label="Antes", color="#5b8dd9", alpha=0.85)
|
| 406 |
bars_d = ax1.bar([i + w / 2 for i in x], df["Animo_Despues"], w,
|
| 407 |
label="Después", color="#27ae60", alpha=0.85)
|
| 408 |
for bar, delta in zip(bars_d, df["Delta_Animo"]):
|
|
|
|
| 414 |
ax1.set_xticks(list(x))
|
| 415 |
ax1.set_xticklabels(ids, rotation=45, ha="right", fontsize=8)
|
| 416 |
ax1.set_yticks([1, 2, 3, 4, 5])
|
| 417 |
+
ax1.set_yticklabels([f"{v} - {ANIMO_NOMBRE[v]}" for v in [1, 2, 3, 4, 5]], fontsize=8)
|
| 418 |
ax1.set_ylim(0, 6.2)
|
| 419 |
ax1.set_title("Estado de Ánimo: Antes vs Después por Participante",
|
| 420 |
fontsize=13, fontweight="bold", pad=12)
|
| 421 |
ax1.set_xlabel("Participante")
|
| 422 |
+
ax1.set_ylabel("Nivel de Ánimo (Likert 1-5)")
|
| 423 |
ax1.legend(loc="upper left")
|
| 424 |
ax1.spines[["top", "right"]].set_visible(False)
|
| 425 |
ax1.grid(axis="y", alpha=0.3)
|
| 426 |
fig1.tight_layout()
|
| 427 |
path1 = save_fig(fig1)
|
| 428 |
|
| 429 |
+
# Gráfica 2: Boxplot ΔÁnimo por nivel de ejercicio
|
| 430 |
fig2, ax2 = plt.subplots(figsize=(7, 5))
|
| 431 |
niveles = sorted(df["Ejercicio"].dropna().unique().astype(int))
|
| 432 |
data_box = [df[df["Ejercicio"] == n]["Delta_Animo"].values for n in niveles]
|
|
|
|
| 455 |
fig2.tight_layout()
|
| 456 |
path2 = save_fig(fig2)
|
| 457 |
|
| 458 |
+
# Gráfica 3: Dona con proporción mejora / igual / descenso
|
| 459 |
fig3, ax3 = plt.subplots(figsize=(6, 5))
|
| 460 |
total = len(df)
|
| 461 |
mejora = int((df["Delta_Animo"] > 0).sum())
|
|
|
|
| 463 |
descenso = int((df["Delta_Animo"] < 0).sum())
|
| 464 |
|
| 465 |
vals, colors, labels_pie = [], [], []
|
| 466 |
+
for lbl, val, col in [("Mejora", mejora, "#27ae60"),
|
| 467 |
+
("Sin cambio", igual, "#f1c40f"),
|
| 468 |
+
("Descenso", descenso, "#e74c3c")]:
|
| 469 |
if val > 0:
|
| 470 |
vals.append(val)
|
| 471 |
colors.append(col)
|
|
|
|
| 484 |
|
| 485 |
pct = mejora / total * 100 if total > 0 else 0
|
| 486 |
resumen = (
|
| 487 |
+
f"Resumen: {total} participantes analizados — "
|
| 488 |
f"**{mejora} mejoraron** ({pct:.0f}%), "
|
| 489 |
f"{igual} sin cambio, {descenso} con descenso. "
|
| 490 |
f"ΔÁnimo promedio: **{df['Delta_Animo'].mean():+.2f} puntos**."
|
| 491 |
)
|
| 492 |
return path1, path2, path3, resumen
|
| 493 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 494 |
def guardar_y_graficar(edad, talla, sexo, ejercicio_label,
|
| 495 |
cap_antes, cap_despues,
|
| 496 |
spo2_antes, spo2_despues,
|
|
|
|
| 504 |
p1, p2, p3, resumen = generar_graficas()
|
| 505 |
return msg, tabla, resumen, p1, p2, p3
|
| 506 |
|
|
|
|
| 507 |
# ─────────────────────────────────────────────
|
| 508 |
# INICIALIZACIÓN
|
| 509 |
# ────────────────────────────���────────────────
|
| 510 |
init_msg, init_count, init_df, init_ec, init_coef = cargar_y_entrenar()
|
| 511 |
|
|
|
|
| 512 |
# ─────────────────────────────────────────────
|
| 513 |
# INTERFAZ
|
| 514 |
# ─────────────────────────────────────────────
|
| 515 |
with gr.Blocks(title="Ejercicio Físico y Estado de Ánimo | CONALEP") as app:
|
| 516 |
+
gr.Markdown("# Experimento: ¿Cómo afecta el ejercicio físico al estado de ánimo?")
|
|
|
|
| 517 |
gr.Markdown(
|
| 518 |
"**Hipótesis:** El nivel de ejercicio físico predice significativamente el cambio "
|
| 519 |
"en el estado de ánimo, controlando capacidad pulmonar, oximetría, edad, talla y sexo.\n\n"
|
| 520 |
+
"**Variable dependiente:** ΔÁnimo = Ánimo_después − Ánimo_antes (escala Likert 1-5)"
|
| 521 |
)
|
| 522 |
|
| 523 |
+
# SECCIÓN 1: MODELO
|
| 524 |
gr.Markdown("---")
|
| 525 |
gr.Markdown("## 1. Modelo de Regresión Lineal Múltiple")
|
| 526 |
|
|
|
|
| 529 |
gr.Markdown("### Carga de datos")
|
| 530 |
file_input = gr.File(label="Subir CSV/Excel externo (opcional)",
|
| 531 |
file_types=[".csv", ".xlsx"])
|
| 532 |
+
load_btn = gr.Button("Entrenar con datos base (+ archivo si se subió)",
|
| 533 |
variant="primary")
|
| 534 |
gr.Markdown("---")
|
| 535 |
gr.Markdown(
|
|
|
|
| 537 |
"Usa el botón de abajo para reentrenar el modelo con los datos base "
|
| 538 |
"**más** todos los participantes registrados en la Sección 3."
|
| 539 |
)
|
| 540 |
+
retrain_btn = gr.Button("Reentrenar con datos base + CSV capturado",
|
| 541 |
variant="secondary")
|
| 542 |
status_out = gr.Textbox(label="Estado del modelo", value=init_msg, interactive=False)
|
| 543 |
n_part_out = gr.Number(label="Total de participantes usados",
|
|
|
|
| 564 |
outputs=[status_out, n_part_out, data_table, ecuacion_out, coef_out]
|
| 565 |
)
|
| 566 |
|
| 567 |
+
# SECCIÓN 2: PREDICCIÓN
|
| 568 |
gr.Markdown("---")
|
| 569 |
gr.Markdown("## 2. Realizar una Predicción")
|
| 570 |
gr.Markdown("Ingresa el perfil de un participante para predecir su cambio esperado en el ánimo.")
|
|
|
|
| 588 |
with gr.Row():
|
| 589 |
spo2_a_pred = gr.Number(label="Oximetría ANTES (%)", value=97.0, step=0.5)
|
| 590 |
spo2_d_pred = gr.Number(label="Oximetría DESPUÉS (%)", value=98.0, step=0.5)
|
| 591 |
+
btn_pred = gr.Button("Calcular ΔÁnimo Predicho", variant="primary")
|
| 592 |
|
| 593 |
with gr.Column():
|
| 594 |
salida_pred = gr.Markdown(label="Resultado")
|
|
|
|
| 600 |
outputs=salida_pred
|
| 601 |
)
|
| 602 |
|
| 603 |
+
# SECCIÓN 3: CAPTURA
|
| 604 |
gr.Markdown("---")
|
| 605 |
gr.Markdown("## 3. Registro de Participantes")
|
| 606 |
gr.Markdown("Captura los datos de cada participante antes y después de la actividad física.")
|
|
|
|
| 643 |
value=list(ANIMO_LABELS.keys())[3]
|
| 644 |
)
|
| 645 |
with gr.Row():
|
| 646 |
+
btn_guardar = gr.Button("Guardar Participante", variant="primary")
|
| 647 |
+
btn_limpiar = gr.Button("Borrar Todos los Registros")
|
| 648 |
msg_cap = gr.Textbox(label="Estado del registro", interactive=False)
|
| 649 |
|
| 650 |
with gr.Column():
|
|
|
|
| 655 |
interactive=False,
|
| 656 |
wrap=True
|
| 657 |
)
|
| 658 |
+
btn_dl = gr.Button("Descargar CSV")
|
| 659 |
+
archivo_dl = gr.File(label="Archivo generado", visible=False)
|
| 660 |
|
| 661 |
+
# SECCIÓN 4: GRÁFICAS
|
| 662 |
gr.Markdown("---")
|
| 663 |
gr.Markdown("## 4. Gráficas de Mejora en el Estado de Ánimo")
|
| 664 |
gr.Markdown(
|
| 665 |
"Las gráficas se actualizan automáticamente al guardar cada participante. "
|
| 666 |
"También puedes generarlas manualmente con el botón."
|
| 667 |
)
|
| 668 |
+
btn_graficas = gr.Button("Generar / Actualizar Gráficas", variant="secondary")
|
| 669 |
resumen_graf = gr.Markdown()
|
| 670 |
with gr.Row():
|
| 671 |
graf1 = gr.Image(label="Antes vs Después por participante", type="filepath")
|
| 672 |
+
graf2 = gr.Image(label="ΔÁnimo por nivel de ejercicio", type="filepath")
|
| 673 |
+
graf3 = gr.Image(label="Distribución del cambio (dona)", type="filepath")
|
| 674 |
+
|
| 675 |
+
# EVENTOS
|
| 676 |
+
def descargar_csv():
|
| 677 |
+
if os.path.exists(CSV_PATH):
|
| 678 |
+
try:
|
| 679 |
+
df = pd.read_csv(CSV_PATH)
|
| 680 |
+
if not df.empty:
|
| 681 |
+
tmp = tempfile.NamedTemporaryFile(
|
| 682 |
+
delete=False, suffix=".csv",
|
| 683 |
+
prefix="datos_animo_ejercicio_"
|
| 684 |
+
)
|
| 685 |
+
df.to_csv(tmp.name, index=False)
|
| 686 |
+
tmp.close()
|
| 687 |
+
return gr.File(value=tmp.name, visible=True)
|
| 688 |
+
except Exception as e:
|
| 689 |
+
print(f"Error descarga: {e}")
|
| 690 |
+
return gr.File(visible=False)
|
| 691 |
|
|
|
|
|
|
|
|
|
|
| 692 |
btn_guardar.click(
|
| 693 |
guardar_y_graficar,
|
| 694 |
inputs=[edad_cap, talla_cap, sexo_cap, ejercicio_cap,
|
|
|
|
| 697 |
outputs=[msg_cap, tabla_cap, resumen_graf, graf1, graf2, graf3]
|
| 698 |
)
|
| 699 |
|
|
|
|
| 700 |
btn_limpiar.click(
|
| 701 |
limpiar_csv,
|
| 702 |
inputs=None,
|
| 703 |
outputs=[msg_cap, tabla_cap]
|
| 704 |
)
|
| 705 |
|
| 706 |
+
btn_dl.click(
|
| 707 |
+
descargar_csv,
|
| 708 |
+
inputs=None,
|
| 709 |
+
outputs=archivo_dl
|
| 710 |
+
)
|
| 711 |
+
|
| 712 |
btn_graficas.click(
|
| 713 |
generar_graficas,
|
| 714 |
inputs=None,
|