EHRley commited on
Commit
fdc47d6
·
verified ·
1 Parent(s): 0d38dfa

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +85 -98
app.py CHANGED
@@ -11,11 +11,7 @@ import matplotlib.pyplot as plt
11
  import io
12
 
13
  # ─────────────────────────────────────────────
14
- # DATOS BASE (24 participantes hipotéticos)
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 Sedentario (no hace ejercicio)": 1,
64
- "2 Ocasional (1 vez/semana)": 2,
65
- "3 Moderado (2-3 veces/semana)": 3,
66
- "4 Activo (4-5 veces/semana)": 4,
67
- "5 Muy activo (diario)": 5,
68
  }
69
  ANIMO_LABELS = {
70
- "1 Muy bajo": 1,
71
- "2 Bajo": 2,
72
- "3 Neutral": 3,
73
- "4 Alto": 4,
74
- "5 Muy alto": 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
- import tempfile as _tf
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" Modelo entrenado con {len(df)} participantes. Fuente: {fuente}",
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" Error al procesar el archivo: {e}", 0, pd.DataFrame(), "", ""
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 " El modelo no ha sido entrenado. Haz clic en 'Cargar y Entrenar'."
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 = " **Mejora notable del ánimo** — se espera un aumento significativo."
269
  elif pred >= 0.5:
270
- interp = " **Ligera mejora del ánimo** — cambio positivo moderado."
271
  elif pred >= -0.5:
272
- interp = " **Sin cambio relevante** — el ánimo se mantiene similar."
273
  else:
274
- interp = " **Posible descenso del ánimo** — revisar variables de contexto."
275
 
276
  hip = (
277
- " **H₁ apoyada:** el ejercicio predice positivamente el ánimo."
278
  if c[0] > 0 else
279
- " **H₁ no apoyada:** el coeficiente de ejercicio es negativo en este modelo."
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 " Registros eliminados.", pd.DataFrame(columns=COLUMNAS_CAPTURA)
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 "⚠️ " + " | ".join(errores), obtener_tabla_csv()
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
- if os.path.exists(CSV_PATH):
376
- df_nuevo.to_csv(CSV_PATH, mode="a", header=False, index=False)
377
- else:
378
- df_nuevo.to_csv(CSV_PATH, mode="w", header=True, index=False)
379
-
380
- return f" Participante {nueva_fila['ID']} guardado.", obtener_tabla_csv()
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
- " No hay datos capturados todavía. Registra participantes en la Sección 3.")
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" Error leyendo CSV: {e}"
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, " Se necesitan al menos 2 participantes para graficar."
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
- # ── Gráfica 1: Barras Antes vs Después por participante ──────────
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"], w,
429
- label="Antes", color="#5b8dd9", alpha=0.85)
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} {ANIMO_NOMBRE[v]}" for v in [1, 2, 3, 4, 5]], fontsize=8)
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 15)")
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
- # ── Gráfica 2: Boxplot ΔÁnimo por nivel de ejercicio ─────────────
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
- # ── Gráfica 3: Dona con proporción mejora / igual / descenso ─────
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 ", mejora, "#27ae60"),
491
- ("Sin cambio ", igual, "#f1c40f"),
492
- ("Descenso ", descenso, "#e74c3c")]:
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" **Resumen:** {total} participantes analizados — "
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 15)"
553
  )
554
 
555
- # ── SECCIÓN 1: MODELO ─────────────────────────────────────────
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(" Entrenar con datos base (+ archivo si se subió)",
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(" Reentrenar con datos base + CSV capturado",
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
- # ── SECCIÓN 2: PREDICCIÓN ──────────────────────────────────────
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(" Calcular ΔÁnimo Predicho", variant="primary")
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
- # ── SECCIÓN 3: CAPTURA ─────────────────────────────────────────
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(" Guardar Participante", variant="primary")
679
- btn_limpiar = gr.Button(" Borrar Todos los Registros")
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.DownloadButton(label=" Descargar CSV", value=obtener_csv_path)
 
691
 
692
- # ── SECCIÓN 4: GRÁFICAS ────────────────────────────────────────
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(" Generar / Actualizar Gráficas", variant="secondary")
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", type="filepath")
704
- graf3 = gr.Image(label="Distribución del cambio (dona)", type="filepath")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # Generar gráficas manualmente
 
 
 
 
 
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,