C2MV commited on
Commit
98c0f1d
1 Parent(s): 981f292

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +822 -301
app.py CHANGED
@@ -1,314 +1,835 @@
1
- # -*- coding: utf-8 -*-
2
- """PrediLectia - Gradio Final v2 with Multiple Y-Axes in Combined Plot.ipynb"""
3
-
4
- # Instalación de librerías necesarias
5
- #!pip install gradio seaborn scipy -q
 
 
 
 
 
6
  import os
7
- os.system('pip install gradio seaborn scipy scikit-learn openpyxl pydantic==1.10.0')
8
 
9
- from pydantic import BaseModel, ConfigDict
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
 
11
- class YourModel(BaseModel):
12
- class Config:
13
- arbitrary_types_allowed = True
 
 
 
 
 
 
 
 
14
 
15
- import numpy as np
16
- import pandas as pd
17
- import matplotlib.pyplot as plt
18
- import seaborn as sns
19
- from scipy.optimize import curve_fit
20
- from sklearn.metrics import mean_squared_error
21
- import gradio as gr
22
- import io
23
- from PIL import Image
24
-
25
- # Definición de la clase CalibrationModel
26
- class CalibrationModel:
27
- def __init__(self):
28
- self.params = {}
29
- self.r2 = {}
30
- self.rmse = {}
31
- self.datax = []
32
- self.datay = []
33
- self.dataxp = []
34
- self.datayp = []
35
- self.datax_std = []
36
- self.datay_std = []
37
-
38
- # Función de ajuste lineal
39
- @staticmethod
40
- def linear(x, m, b):
41
- return m * x + b
42
-
43
- # Métodos de procesamiento y ajuste de datos
44
- def process_data(self, df):
45
- # Obtener todas las columnas que contengan "Concentración" y "Absorbancia"
46
- concentration_cols = [col for col in df.columns if col[1] == 'Concentración']
47
- absorbance_cols = [col for col in df.columns if col[1] == 'Absorbancia']
48
-
49
- # Procesar los datos de concentración
50
- data_concentration = [df[col].values for col in concentration_cols]
51
- data_concentration = np.array(data_concentration) # shape (num_experiments, num_time_points)
52
- self.datax.append(data_concentration)
53
- self.dataxp.append(np.mean(data_concentration, axis=0))
54
- self.datax_std.append(np.std(data_concentration, axis=0, ddof=1))
55
-
56
- # Procesar los datos de absorbancia
57
- data_absorbance = [df[col].values for col in absorbance_cols]
58
- data_absorbance = np.array(data_absorbance)
59
- self.datay.append(data_absorbance)
60
- self.datayp.append(np.mean(data_absorbance, axis=0))
61
- self.datay_std.append(np.std(data_absorbance, axis=0, ddof=1))
62
-
63
- def fit_model(self):
64
- self.fit_calibration = self.fit_calibration_linear
65
-
66
- def fit_calibration_linear(self, concentration, absorbance, bounds):
67
- # Eliminar valores NaN e inf
68
- mask = ~np.isnan(concentration) & ~np.isnan(absorbance) & np.isfinite(concentration) & np.isfinite(absorbance)
69
- concentration = concentration[mask]
70
- absorbance = absorbance[mask]
71
-
72
- if len(concentration) == 0 or len(absorbance) == 0:
73
- raise ValueError("No valid data points for fitting.")
74
-
75
- popt, _ = curve_fit(self.linear, concentration, absorbance, bounds=bounds, maxfev=10000)
76
- self.params['calibration'] = {'m': popt[0], 'b': popt[1]}
77
- y_pred = self.linear(concentration, *popt)
78
- self.r2['calibration'] = 1 - (np.sum((absorbance - y_pred) ** 2) / np.sum((absorbance - np.mean(absorbance)) ** 2))
79
- self.rmse['calibration'] = np.sqrt(mean_squared_error(absorbance, y_pred))
80
- return y_pred
81
-
82
- # Métodos de visualización de resultados
83
- def plot_results(self, concentration, absorbance, y_pred,
84
- concentration_std=None, absorbance_std=None,
85
- experiment_name='', legend_position='best', params_position='upper right',
86
- show_legend=True, show_params=True,
87
- style='whitegrid',
88
- line_color='#0000FF', point_color='#000000', line_style='-', marker_style='o'):
89
- sns.set_style(style) # Establecer el estilo seleccionado
90
-
91
- fig, ax = plt.subplots(figsize=(10, 7))
92
- fig.suptitle(f'{experiment_name}', fontsize=16)
93
-
94
- if absorbance_std is not None:
95
- ax.errorbar(concentration, absorbance, yerr=absorbance_std, fmt=marker_style, color=point_color,
96
- label='Datos experimentales', capsize=5)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
97
  else:
98
- ax.plot(concentration, absorbance, marker=marker_style, linestyle='', color=point_color,
99
- label='Datos experimentales')
100
- ax.plot(concentration, y_pred, linestyle=line_style, color=line_color, label='Modelo')
101
- ax.set_xlabel('Concentración')
102
- ax.set_ylabel('Absorbancia')
103
- if show_legend:
104
- ax.legend(loc=legend_position)
105
- ax.set_title(f'Curva de Calibrado')
106
-
107
- if show_params:
108
- param_text = '\n'.join([f"{k} = {v:.4f}" for k, v in self.params['calibration'].items()])
109
- text = f"{param_text}\nR² = {self.r2['calibration']:.4f}\nRMSE = {self.rmse['calibration']:.4f}"
110
-
111
- # Si la posición es 'outside right', ajustar la posición del texto
112
- if params_position == 'outside right':
113
- bbox_props = dict(boxstyle='round', facecolor='white', alpha=0.5)
114
- ax.annotate(text, xy=(1.05, 0.5), xycoords='axes fraction',
115
- verticalalignment='center', bbox=bbox_props)
116
- else:
117
- if params_position in ['upper right', 'lower right']:
118
- text_x = 0.95
119
- ha = 'right'
120
- else:
121
- text_x = 0.05
122
- ha = 'left'
123
-
124
- if params_position in ['upper right', 'upper left']:
125
- text_y = 0.95
126
- va = 'top'
127
- else:
128
- text_y = 0.05
129
- va = 'bottom'
130
-
131
- ax.text(text_x, text_y, text, transform=ax.transAxes,
132
- verticalalignment=va, horizontalalignment=ha,
133
- bbox={'boxstyle': 'round', 'facecolor': 'white', 'alpha': 0.5})
134
-
135
- plt.tight_layout()
136
- return fig
137
-
138
- # Función de procesamiento de datos
139
- def process_data(file, legend_position, params_position, experiment_names, lower_bounds, upper_bounds,
140
- style='whitegrid', line_color='#0000FF', point_color='#000000',
141
- line_style='-', marker_style='o', show_legend=True, show_params=True):
142
- # Leer todas las hojas del archivo Excel
143
- xls = pd.ExcelFile(file.name)
144
- sheet_names = xls.sheet_names
145
-
146
- model = CalibrationModel()
147
- model.fit_model()
148
- figures = []
149
-
150
- # Si no se proporcionan suficientes límites, usar valores predeterminados
151
- default_lower_bounds = (0, 0)
152
- default_upper_bounds = (np.inf, np.inf)
153
-
154
- experiment_counter = 0 # Contador global de experimentos
155
-
156
- for sheet_name in sheet_names:
157
- df = pd.read_excel(file.name, sheet_name=sheet_name, header=[0, 1])
158
-
159
- # Procesar datos
160
- model.process_data(df)
161
- concentration = model.dataxp[-1]
162
- absorbance = model.datayp[-1]
163
-
164
- # Si hay replicados en el experimento, calcular la desviación estándar
165
- concentration_std = None
166
- absorbance_std = None
167
- if concentration.ndim > 1:
168
- concentration_std = np.std(concentration, axis=0, ddof=1)
169
- concentration = np.mean(concentration, axis=0)
170
- if absorbance.ndim > 1:
171
- absorbance_std = np.std(absorbance, axis=0, ddof=1)
172
- absorbance = np.mean(absorbance, axis=0)
173
-
174
- # Obtener límites o usar valores predeterminados
175
- lower_bound = lower_bounds[experiment_counter] if experiment_counter < len(lower_bounds) else default_lower_bounds
176
- upper_bound = upper_bounds[experiment_counter] if experiment_counter < len(upper_bounds) else default_upper_bounds
177
- bounds = (lower_bound, upper_bound)
178
-
179
- # Ajustar el modelo
180
- y_pred = model.fit_calibration(concentration, absorbance, bounds)
181
-
182
- # Usar el nombre del experimento proporcionado o un nombre por defecto
183
- experiment_name = experiment_names[experiment_counter] if experiment_counter < len(experiment_names) else f"Tratamiento {experiment_counter + 1}"
184
-
185
- fig = model.plot_results(concentration, absorbance, y_pred,
186
- concentration_std, absorbance_std,
187
- experiment_name, legend_position, params_position,
188
- show_legend, show_params,
189
- style,
190
- line_color, point_color, line_style, marker_style)
191
- figures.append(fig)
192
-
193
- experiment_counter += 1
194
-
195
- return figures
196
-
197
- def create_interface():
198
- with gr.Blocks(theme='upsatwal/mlsc_tiet') as demo:
199
- gr.Markdown("# Ajuste de Curva de Calibrado")
200
- gr.Markdown(
201
- "Sube un archivo Excel con múltiples pestañas. Cada pestaña debe contener columnas 'Concentración' y 'Absorbancia' para cada experimento.")
202
-
203
- file_input = gr.File(label="Subir archivo Excel")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
 
205
  with gr.Row():
206
- with gr.Column():
207
- legend_position = gr.Radio(
208
- choices=["upper left", "upper right", "lower left", "lower right", "best"],
209
- label="Posición de la leyenda",
210
- value="best"
211
- )
212
- show_legend = gr.Checkbox(label="Mostrar Leyenda", value=True)
213
-
214
- with gr.Column():
215
- params_positions = ["upper left", "upper right", "lower left", "lower right", "outside right"]
216
- params_position = gr.Radio(
217
- choices=params_positions,
218
- label="Posición de los parámetros",
219
- value="upper right"
220
- )
221
- show_params = gr.Checkbox(label="Mostrar Parámetros", value=True)
222
-
223
- experiment_names = gr.Textbox(
224
- label="Nombres de los experimentos (uno por línea)",
225
- placeholder="Experimento 1\nExperimento 2\n...",
226
- lines=5
227
  )
228
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
229
  with gr.Row():
230
- with gr.Column():
231
- lower_bounds = gr.Textbox(
232
- label="Lower Bounds (uno por línea, formato: m,b)",
233
- placeholder="0,0\n0,0\n...",
234
- lines=5
235
- )
236
-
237
- with gr.Column():
238
- upper_bounds = gr.Textbox(
239
- label="Upper Bounds (uno por línea, formato: m,b)",
240
- placeholder="inf,inf\ninf,inf\n...",
241
- lines=5
242
- )
243
-
244
- # Añadir un desplegable para seleccionar el estilo del gráfico
245
- styles = ['white', 'dark', 'whitegrid', 'darkgrid', 'ticks']
246
- style_dropdown = gr.Dropdown(choices=styles, label="Selecciona el estilo de gráfico", value='whitegrid')
247
-
248
- # Añadir color pickers para líneas y puntos
249
- line_color_picker = gr.ColorPicker(label="Color de la línea", value='#0000FF')
250
- point_color_picker = gr.ColorPicker(label="Color de los puntos", value='#000000')
251
-
252
- # Añadir listas desplegables para tipo de línea y tipo de punto
253
- line_style_options = ['-', '--', '-.', ':']
254
- line_style_dropdown = gr.Dropdown(choices=line_style_options, label="Estilo de línea", value='-')
255
-
256
- marker_style_options = ['o', 's', '^', 'v', 'D', 'x', '+', '*']
257
- marker_style_dropdown = gr.Dropdown(choices=marker_style_options, label="Estilo de punto", value='o')
258
-
259
- simulate_btn = gr.Button("Simular")
260
-
261
- # Definir un componente gr.Gallery para las salidas
262
- output_gallery = gr.Gallery(label="Resultados", columns=2, height='auto')
263
-
264
- def process_and_plot(file, legend_position, params_position, experiment_names,
265
- lower_bounds, upper_bounds, style,
266
- line_color, point_color, line_style, marker_style,
267
- show_legend, show_params):
268
- # Dividir los nombres de experimentos y límites en listas
269
- experiment_names_list = experiment_names.strip().split('\n') if experiment_names.strip() else []
270
- lower_bounds_list = [tuple(map(float, lb.split(','))) for lb in
271
- lower_bounds.strip().split('\n')] if lower_bounds.strip() else []
272
- upper_bounds_list = [tuple(map(float, ub.split(','))) for ub in
273
- upper_bounds.strip().split('\n')] if upper_bounds.strip() else []
274
-
275
- # Procesar los datos y generar gráficos
276
- figures = process_data(file, legend_position, params_position, experiment_names_list,
277
- lower_bounds_list, upper_bounds_list, style,
278
- line_color, point_color, line_style, marker_style,
279
- show_legend, show_params)
280
-
281
- # Convertir las figuras a imágenes y devolverlas como lista
282
- image_list = []
283
- for fig in figures:
284
- buf = io.BytesIO()
285
- fig.savefig(buf, format='png')
286
- buf.seek(0)
287
- image = Image.open(buf)
288
- image_list.append(image)
289
-
290
- return image_list
291
-
292
- simulate_btn.click(
293
- fn=process_and_plot,
294
- inputs=[file_input,
295
- legend_position,
296
- params_position,
297
- experiment_names,
298
- lower_bounds,
299
- upper_bounds,
300
- style_dropdown,
301
- line_color_picker,
302
- point_color_picker,
303
- line_style_dropdown,
304
- marker_style_dropdown,
305
- show_legend,
306
- show_params],
307
- outputs=output_gallery
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
  )
309
 
310
- return demo
 
 
 
311
 
312
- # Crear y lanzar la interfaz
313
- demo = create_interface()
314
- demo.launch(share=True)
 
1
+ import gradio as gr
2
+ import pandas as pd
3
+ import numpy as np
4
+ import matplotlib.pyplot as plt
5
+ import seaborn as sns
6
+ from scipy import stats
7
+ from datetime import datetime
8
+ import docx
9
+ from docx.shared import Inches, Pt
10
+ from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
11
  import os
 
12
 
13
+ def generar_tabla(n_filas, concentracion_inicial, unidad_medida, n_replicas):
14
+ valores_base = [1.00, 0.80, 0.60, 0.40, 0.20, 0.10, 0.05]
15
+
16
+ if n_filas <= 7:
17
+ solucion_inoculo = valores_base[:n_filas]
18
+ agua = [round(1 - x, 2) for x in solucion_inoculo]
19
+ else:
20
+ solucion_inoculo = valores_base.copy()
21
+ ultimo_valor = valores_base[-1]
22
+ for _ in range(n_filas - 7):
23
+ nuevo_valor = round(ultimo_valor / 2, 3)
24
+ solucion_inoculo.append(nuevo_valor)
25
+ ultimo_valor = nuevo_valor
26
+ agua = [round(1 - x, 3) for x in solucion_inoculo]
27
+
28
+ data = {
29
+ f"Solución de inóculo ({concentracion_inicial} {unidad_medida})": solucion_inoculo,
30
+ "H2O": agua
31
+ }
32
+ df = pd.DataFrame(data)
33
+
34
+ nombre_columna = f"Solución de inóculo ({concentracion_inicial} {unidad_medida})"
35
+ df["Factor de Dilución"] = df[nombre_columna].apply(lambda x: round(1 / x, 2))
36
+ df["Concentración Predicha Numérica"] = df["Factor de Dilución"].apply(
37
+ lambda x: concentracion_inicial / x
38
+ )
39
+ df[f"Concentración Predicha ({unidad_medida})"] = df["Concentración Predicha Numérica"].round(0).astype(str)
40
+
41
+ # Añadir columnas para las réplicas de "Concentración Real"
42
+ for i in range(1, n_replicas + 1):
43
+ df[f"Concentración Real {i} ({unidad_medida})"] = None
44
+
45
+ # Las columnas de promedio y desviación estándar se agregarán durante el análisis
46
+ return df
47
+
48
+ def ajustar_decimales_evento(df, decimales):
49
+ df = df.copy()
50
+ # Ajustar decimales en todas las columnas numéricas
51
+ for col in df.columns:
52
+ try:
53
+ df[col] = pd.to_numeric(df[col], errors='ignore')
54
+ df[col] = df[col].round(decimales)
55
+ except:
56
+ pass
57
+ return df
58
+
59
+ def calcular_promedio_desviacion(df, n_replicas, unidad_medida):
60
+ df = df.copy()
61
+ # Obtener las columnas de réplicas
62
+ col_replicas = [f"Concentración Real {i} ({unidad_medida})" for i in range(1, n_replicas + 1)]
63
+ # Convertir a numérico
64
+ for col in col_replicas:
65
+ df[col] = pd.to_numeric(df[col], errors='coerce')
66
+
67
+ # Calcular el promedio y la desviación estándar
68
+ df[f"Concentración Real Promedio ({unidad_medida})"] = df[col_replicas].mean(axis=1)
69
+
70
+ if n_replicas > 1:
71
+ df[f"Desviación Estándar ({unidad_medida})"] = df[col_replicas].std(ddof=1, axis=1)
72
+ else:
73
+ df[f"Desviación Estándar ({unidad_medida})"] = 0.0
74
+
75
+ return df
76
+
77
+ def generar_graficos(df_valid, n_replicas, unidad_medida, palette_puntos, estilo_puntos,
78
+ palette_linea_ajuste, estilo_linea_ajuste,
79
+ palette_linea_ideal, estilo_linea_ideal,
80
+ palette_barras_error, mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos):
81
+ col_predicha_num = "Concentración Predicha Numérica"
82
+ col_real_promedio = f"Concentración Real Promedio ({unidad_medida})"
83
+ col_desviacion = f"Desviación Estándar ({unidad_medida})"
84
+
85
+ # Convertir a numérico
86
+ df_valid[col_predicha_num] = df_valid[col_predicha_num].astype(float)
87
+ df_valid[col_real_promedio] = df_valid[col_real_promedio].astype(float)
88
+ df_valid[col_desviacion] = df_valid[col_desviacion].fillna(0).astype(float)
89
+
90
+ # Calcular regresión lineal
91
+ slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_predicha_num], df_valid[col_real_promedio])
92
+ df_valid['Ajuste Lineal'] = intercept + slope * df_valid[col_predicha_num]
93
+
94
+ # Configurar estilos
95
+ sns.set(style="whitegrid")
96
+ plt.rcParams.update({'figure.autolayout': True})
97
+
98
+ fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
99
+
100
+ # Obtener colores de las paletas
101
+ colors_puntos = sns.color_palette(palette_puntos, as_cmap=False)
102
+ colors_linea_ajuste = sns.color_palette(palette_linea_ajuste, as_cmap=False)
103
+ colors_linea_ideal = sns.color_palette(palette_linea_ideal, as_cmap=False)
104
+ colors_barras_error = sns.color_palette(palette_barras_error, as_cmap=False)
105
+
106
+ # Seleccionar colores
107
+ color_puntos = colors_puntos[0]
108
+ color_linea_ajuste = colors_linea_ajuste[0]
109
+ color_linea_ideal = colors_linea_ideal[0]
110
+ color_barras_error = colors_barras_error[0]
111
+
112
+ # Gráfico de dispersión con línea de regresión
113
+ if mostrar_puntos:
114
+ if n_replicas > 1:
115
+ # Incluir barras de error
116
+ ax1.errorbar(
117
+ df_valid[col_predicha_num],
118
+ df_valid[col_real_promedio],
119
+ yerr=df_valid[col_desviacion],
120
+ fmt=estilo_puntos,
121
+ color=color_puntos,
122
+ ecolor=color_barras_error,
123
+ elinewidth=2,
124
+ capsize=3,
125
+ label='Datos Reales'
126
+ )
127
+ else:
128
+ ax1.scatter(
129
+ df_valid[col_predicha_num],
130
+ df_valid[col_real_promedio],
131
+ color=color_puntos,
132
+ s=100,
133
+ label='Datos Reales',
134
+ marker=estilo_puntos
135
+ )
136
+
137
+ # Línea de ajuste
138
+ if mostrar_linea_ajuste:
139
+ ax1.plot(
140
+ df_valid[col_predicha_num],
141
+ df_valid['Ajuste Lineal'],
142
+ color=color_linea_ajuste,
143
+ label='Ajuste Lineal',
144
+ linewidth=2,
145
+ linestyle=estilo_linea_ajuste
146
+ )
147
 
148
+ # Línea ideal
149
+ if mostrar_linea_ideal:
150
+ min_predicha = df_valid[col_predicha_num].min()
151
+ max_predicha = df_valid[col_predicha_num].max()
152
+ ax1.plot(
153
+ [min_predicha, max_predicha],
154
+ [min_predicha, max_predicha],
155
+ color=color_linea_ideal,
156
+ linestyle=estilo_linea_ideal,
157
+ label='Ideal'
158
+ )
159
 
160
+ ax1.set_title('Correlación entre Concentración Predicha y Real', fontsize=14)
161
+ ax1.set_xlabel('Concentración Predicha', fontsize=12)
162
+ ax1.set_ylabel('Concentración Real Promedio', fontsize=12)
163
+
164
+ # Añadir ecuación y R² en el gráfico
165
+ ax1.annotate(
166
+ f'y = {intercept:.2f} + {slope:.2f}x\n$R^2$ = {r_value**2:.4f}',
167
+ xy=(0.05, 0.95),
168
+ xycoords='axes fraction',
169
+ fontsize=12,
170
+ backgroundcolor='white',
171
+ verticalalignment='top'
172
+ )
173
+
174
+ # Posicionar la leyenda
175
+ ax1.legend(loc='lower right', fontsize=10)
176
+
177
+ # Gráfico de residuos
178
+ residuos = df_valid[col_real_promedio] - df_valid['Ajuste Lineal']
179
+ ax2.scatter(
180
+ df_valid[col_predicha_num],
181
+ residuos,
182
+ color=color_puntos,
183
+ s=100,
184
+ marker=estilo_puntos,
185
+ label='Residuos'
186
+ )
187
+
188
+ ax2.axhline(y=0, color='black', linestyle='--', linewidth=1)
189
+ ax2.set_title('Gráfico de Residuos', fontsize=14)
190
+ ax2.set_xlabel('Concentración Predicha', fontsize=12)
191
+ ax2.set_ylabel('Residuo', fontsize=12)
192
+ ax2.legend(loc='upper right', fontsize=10)
193
+
194
+ plt.tight_layout()
195
+ plt.savefig('grafico.png') # Guardar el gráfico para incluirlo en el informe
196
+ return fig
197
+
198
+ def evaluar_calidad_calibracion(df_valid, r_squared, rmse, cv_percent):
199
+ """Evaluar la calidad de la calibración y proporcionar recomendaciones"""
200
+ evaluacion = {
201
+ "calidad": "",
202
+ "recomendaciones": [],
203
+ "estado": "✅" if r_squared >= 0.95 and cv_percent <= 15 else "⚠️"
204
+ }
205
+
206
+ if r_squared >= 0.95:
207
+ evaluacion["calidad"] = "Excelente"
208
+ elif r_squared >= 0.90:
209
+ evaluacion["calidad"] = "Buena"
210
+ elif r_squared >= 0.85:
211
+ evaluacion["calidad"] = "Regular"
212
+ else:
213
+ evaluacion["calidad"] = "Deficiente"
214
+
215
+ if r_squared < 0.95:
216
+ evaluacion["recomendaciones"].append("- Considere repetir algunas mediciones para mejorar la correlación")
217
+
218
+ if cv_percent > 15:
219
+ evaluacion["recomendaciones"].append("- La variabilidad es alta. Revise el procedimiento de dilución")
220
+
221
+ if rmse > 0.1 * df_valid[df_valid.columns[-1]].astype(float).mean():
222
+ evaluacion["recomendaciones"].append("- El error de predicción es significativo. Verifique la técnica de medición")
223
+
224
+ return evaluacion
225
+
226
+ def generar_informe_completo(df_valid, n_replicas, unidad_medida):
227
+ """Generar un informe completo en formato markdown"""
228
+ col_predicha_num = "Concentración Predicha Numérica"
229
+ col_real_promedio = f"Concentración Real Promedio ({unidad_medida})"
230
+
231
+ # Convertir a numérico
232
+ df_valid[col_predicha_num] = df_valid[col_predicha_num].astype(float)
233
+ df_valid[col_real_promedio] = df_valid[col_real_promedio].astype(float)
234
+
235
+ # Calcular estadísticas
236
+ slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_predicha_num], df_valid[col_real_promedio])
237
+ r_squared = r_value ** 2
238
+ rmse = np.sqrt(((df_valid[col_real_promedio] - (intercept + slope * df_valid[col_predicha_num])) ** 2).mean())
239
+ cv = (df_valid[col_real_promedio].std() / df_valid[col_real_promedio].mean()) * 100 # CV de los valores reales
240
+
241
+ # Evaluar calidad
242
+ evaluacion = evaluar_calidad_calibracion(df_valid, r_squared, rmse, cv)
243
+
244
+ informe = f"""# Informe de Calibración {evaluacion['estado']}
245
+ Fecha: {datetime.now().strftime('%d/%m/%Y %H:%M')}
246
+
247
+ ## Resumen Estadístico
248
+ - **Ecuación de Regresión**: y = {intercept:.4f} + {slope:.4f}x
249
+ - **Coeficiente de correlación (r)**: {r_value:.4f}
250
+ - **Coeficiente de determinación ($R^2$)**: {r_squared:.4f}
251
+ - **Valor p**: {p_value:.4e}
252
+ - **Error estándar de la pendiente**: {std_err:.4f}
253
+ - **Error cuadrático medio (RMSE)**: {rmse:.4f}
254
+ - **Coeficiente de variación (CV)**: {cv:.2f}%
255
+
256
+ ## Evaluación de Calidad
257
+ - **Calidad de la calibración**: {evaluacion['calidad']}
258
+
259
+ ## Recomendaciones
260
+ {chr(10).join(evaluacion['recomendaciones']) if evaluacion['recomendaciones'] else "No hay recomendaciones específicas. La calibración cumple con los criterios de calidad."}
261
+
262
+ ## Decisión
263
+ {("✅ APROBADO - La calibración cumple con los criterios de calidad establecidos" if evaluacion['estado'] == "✅" else "⚠️ REQUIERE REVISIÓN - La calibración necesita ajustes según las recomendaciones anteriores")}
264
+
265
+ ---
266
+ *Nota: Este informe fue generado automáticamente. Por favor, revise los resultados y valide según sus criterios específicos.*
267
+ """
268
+ return informe, evaluacion['estado']
269
+
270
+ def actualizar_analisis(df, n_replicas, unidad_medida):
271
+ if df is None or df.empty:
272
+ return "Error en los datos", None, "No se pueden generar análisis", df
273
+
274
+ # Calcular promedio y desviación estándar dependiendo de las réplicas
275
+ df = calcular_promedio_desviacion(df, n_replicas, unidad_medida)
276
+
277
+ col_predicha_num = "Concentración Predicha Numérica"
278
+ col_real_promedio = f"Concentración Real Promedio ({unidad_medida})"
279
+
280
+ # Convertir columnas a numérico
281
+ df[col_predicha_num] = pd.to_numeric(df[col_predicha_num], errors='coerce')
282
+ df[col_real_promedio] = pd.to_numeric(df[col_real_promedio], errors='coerce')
283
+
284
+ df_valid = df.dropna(subset=[col_predicha_num, col_real_promedio])
285
+
286
+ if len(df_valid) < 2:
287
+ return "Se necesitan más datos", None, "Se requieren al menos dos valores reales para el análisis", df
288
+
289
+ # Calcular la regresión y agregar 'Ajuste Lineal'
290
+ slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_predicha_num], df_valid[col_real_promedio])
291
+ df_valid['Ajuste Lineal'] = intercept + slope * df_valid[col_predicha_num]
292
+
293
+ # Generar gráfico con opciones predeterminadas
294
+ fig = generar_graficos(
295
+ df_valid, n_replicas, unidad_medida,
296
+ palette_puntos='deep', estilo_puntos='o',
297
+ palette_linea_ajuste='muted', estilo_linea_ajuste='-',
298
+ palette_linea_ideal='bright', estilo_linea_ideal='--',
299
+ palette_barras_error='pastel',
300
+ mostrar_linea_ajuste=True, mostrar_linea_ideal=True, mostrar_puntos=True
301
+ )
302
+ informe, estado = generar_informe_completo(df_valid, n_replicas, unidad_medida)
303
+
304
+ return estado, fig, informe, df
305
+
306
+ def actualizar_graficos(df, n_replicas, unidad_medida,
307
+ palette_puntos, estilo_puntos,
308
+ palette_linea_ajuste, estilo_linea_ajuste,
309
+ palette_linea_ideal, estilo_linea_ideal,
310
+ palette_barras_error,
311
+ mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos):
312
+ if df is None or df.empty:
313
+ return None
314
+
315
+ # Asegurarse de que los cálculos estén actualizados
316
+ df = calcular_promedio_desviacion(df, n_replicas, unidad_medida)
317
+
318
+ col_predicha_num = "Concentración Predicha Numérica"
319
+ col_real_promedio = f"Concentración Real Promedio ({unidad_medida})"
320
+
321
+ # Convertir columnas a numérico
322
+ df[col_predicha_num] = pd.to_numeric(df[col_predicha_num], errors='coerce')
323
+ df[col_real_promedio] = pd.to_numeric(df[col_real_promedio], errors='coerce')
324
+
325
+ df_valid = df.dropna(subset=[col_predicha_num, col_real_promedio])
326
+
327
+ if len(df_valid) < 2:
328
+ return None
329
+
330
+ # Generar gráfico con opciones seleccionadas
331
+ fig = generar_graficos(
332
+ df_valid, n_replicas, unidad_medida,
333
+ palette_puntos, estilo_puntos,
334
+ palette_linea_ajuste, estilo_linea_ajuste,
335
+ palette_linea_ideal, estilo_linea_ideal,
336
+ palette_barras_error,
337
+ mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos
338
+ )
339
+
340
+ return fig
341
+
342
+ def exportar_informe_word(df_valid, informe_md, unidad_medida):
343
+ # Crear documento Word
344
+ doc = docx.Document()
345
+
346
+ # Estilos APA 7
347
+ style = doc.styles['Normal']
348
+ font = style.font
349
+ font.name = 'Times New Roman'
350
+ font.size = Pt(12)
351
+
352
+ # Título centrado
353
+ titulo = doc.add_heading('Informe de Calibración', 0)
354
+ titulo.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
355
+
356
+ # Fecha
357
+ fecha = doc.add_paragraph(f"Fecha: {datetime.now().strftime('%d/%m/%Y %H:%M')}")
358
+ fecha.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
359
+
360
+ # Insertar gráfico
361
+ if os.path.exists('grafico.png'):
362
+ doc.add_picture('grafico.png', width=Inches(6))
363
+ ultimo_parrafo = doc.paragraphs[-1]
364
+ ultimo_parrafo.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
365
+
366
+ # Leyenda del gráfico en estilo APA 7
367
+ leyenda = doc.add_paragraph('Figura 1. Gráfico de calibración.')
368
+ leyenda_format = leyenda.paragraph_format
369
+ leyenda_format.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
370
+ leyenda.style = doc.styles['Caption']
371
+
372
+ # Agregar contenido del informe
373
+ doc.add_heading('Resumen Estadístico', level=1)
374
+ for linea in informe_md.split('\n'):
375
+ if linea.startswith('##'):
376
+ doc.add_heading(linea.replace('##', '').strip(), level=2)
377
  else:
378
+ doc.add_paragraph(linea)
379
+
380
+ # Añadir tabla de datos
381
+ doc.add_heading('Tabla de Datos de Calibración', level=1)
382
+
383
+ # Convertir DataFrame a lista de listas
384
+ tabla_datos = df_valid.reset_index(drop=True)
385
+ tabla_datos = tabla_datos.round(4) # Redondear a 4 decimales si es necesario
386
+ columnas = tabla_datos.columns.tolist()
387
+ registros = tabla_datos.values.tolist()
388
+
389
+ # Crear tabla en Word
390
+ tabla = doc.add_table(rows=1 + len(registros), cols=len(columnas))
391
+ tabla.style = 'Table Grid'
392
+
393
+ # Añadir los encabezados
394
+ hdr_cells = tabla.rows[0].cells
395
+ for idx, col_name in enumerate(columnas):
396
+ hdr_cells[idx].text = col_name
397
+
398
+ # Añadir los registros
399
+ for i, registro in enumerate(registros):
400
+ row_cells = tabla.rows[i + 1].cells
401
+ for j, valor in enumerate(registro):
402
+ row_cells[j].text = str(valor)
403
+
404
+ # Formatear fuente de la tabla
405
+ for row in tabla.rows:
406
+ for cell in row.cells:
407
+ for paragraph in cell.paragraphs:
408
+ paragraph.style = doc.styles['Normal']
409
+
410
+ # Guardar documento
411
+ filename = 'informe_calibracion.docx'
412
+ doc.save(filename)
413
+ return filename
414
+
415
+ def exportar_informe_latex(df_valid, informe_md):
416
+ # Generar código LaTeX
417
+ informe_tex = r"""\documentclass{article}
418
+ \usepackage[spanish]{babel}
419
+ \usepackage{amsmath}
420
+ \usepackage{graphicx}
421
+ \usepackage{booktabs}
422
+ \begin{document}
423
+ """
424
+ informe_tex += informe_md.replace('#', '').replace('**', '\\textbf{').replace('*', '\\textit{')
425
+ informe_tex += r"""
426
+ \end{document}
427
+ """
428
+ filename = 'informe_calibracion.tex'
429
+ with open(filename, 'w') as f:
430
+ f.write(informe_tex)
431
+ return filename
432
+
433
+ def exportar_word(df, informe_md, unidad_medida):
434
+ df_valid = df.copy()
435
+ col_predicha_num = "Concentración Predicha Numérica"
436
+ col_real_promedio = f"Concentración Real Promedio ({unidad_medida})"
437
+
438
+ # Convertir columnas a numérico
439
+ df_valid[col_predicha_num] = pd.to_numeric(df_valid[col_predicha_num], errors='coerce')
440
+ df_valid[col_real_promedio] = pd.to_numeric(df_valid[col_real_promedio], errors='coerce')
441
+
442
+ df_valid = df_valid.dropna(subset=[col_predicha_num, col_real_promedio])
443
+
444
+ if df_valid.empty:
445
+ return None
446
+
447
+ filename = exportar_informe_word(df_valid, informe_md, unidad_medida)
448
+
449
+ return filename # Retornamos el nombre del archivo
450
+
451
+ def exportar_latex(df, informe_md):
452
+ df_valid = df.copy()
453
+ col_predicha_num = "Concentración Predicha Numérica"
454
+ col_real_promedio = [col for col in df_valid.columns if 'Real Promedio' in col][0]
455
+
456
+ # Convertir columnas a numérico
457
+ df_valid[col_predicha_num] = pd.to_numeric(df_valid[col_predicha_num], errors='coerce')
458
+ df_valid[col_real_promedio] = pd.to_numeric(df_valid[col_real_promedio], errors='coerce')
459
+
460
+ df_valid = df_valid.dropna(subset=[col_predicha_num, col_real_promedio])
461
+
462
+ if df_valid.empty:
463
+ return None
464
+
465
+ filename = exportar_informe_latex(df_valid, informe_md)
466
+
467
+ return filename # Retornamos el nombre del archivo
468
+
469
+ # Funciones de ejemplo
470
+ def cargar_ejemplo_ufc(n_replicas):
471
+ df = generar_tabla(7, 2000000, "UFC", n_replicas)
472
+ # Valores reales de ejemplo
473
+ for i in range(1, n_replicas + 1):
474
+ valores_reales = [2000000 - (i - 1) * 10000, 1600000 - (i - 1) * 8000, 1200000 - (i - 1) * 6000,
475
+ 800000 - (i - 1) * 4000, 400000 - (i - 1) * 2000, 200000 - (i - 1) * 1000,
476
+ 100000 - (i - 1) * 500]
477
+ df[f"Concentración Real {i} (UFC)"] = valores_reales
478
+ return 2000000, "UFC", 7, df
479
+
480
+ def cargar_ejemplo_od(n_replicas):
481
+ df = generar_tabla(7, 1.0, "OD", n_replicas)
482
+ # Valores reales de ejemplo
483
+ for i in range(1, n_replicas + 1):
484
+ valores_reales = [1.00 - (i - 1) * 0.05, 0.80 - (i - 1) * 0.04, 0.60 - (i - 1) * 0.03,
485
+ 0.40 - (i - 1) * 0.02, 0.20 - (i - 1) * 0.01, 0.10 - (i - 1) * 0.005,
486
+ 0.05 - (i - 1) * 0.002]
487
+ df[f"Concentración Real {i} (OD)"] = valores_reales
488
+ return 1.0, "OD", 7, df
489
+
490
+ def limpiar_datos(n_replicas):
491
+ df = generar_tabla(7, 2000000, "UFC", n_replicas)
492
+ return (
493
+ 2000000, # Concentración Inicial
494
+ "UFC", # Unidad de Medida
495
+ 7, # Número de filas
496
+ df, # Tabla Output
497
+ "", # Estado Output
498
+ None, # Gráficos Output
499
+ "" # Informe Output
500
+ )
501
+
502
+ def generar_datos_sinteticos_evento(df, n_replicas, unidad_medida):
503
+ df = df.copy()
504
+ col_predicha_num = "Concentración Predicha Numérica"
505
+
506
+ # Generar datos sintéticos para cada réplica
507
+ for i in range(1, n_replicas + 1):
508
+ col_real = f"Concentración Real {i} ({unidad_medida})"
509
+ df[col_predicha_num] = pd.to_numeric(df[col_predicha_num], errors='coerce')
510
+ desviacion_std = 0.05 * df[col_predicha_num].mean() # 5% de la media como desviación estándar
511
+ valores_predichos = df[col_predicha_num].astype(float).values
512
+ datos_sinteticos = valores_predichos + np.random.normal(0, desviacion_std, size=len(valores_predichos))
513
+ datos_sinteticos = np.maximum(0, datos_sinteticos) # Asegurar que no haya valores negativos
514
+ datos_sinteticos = np.round(datos_sinteticos, 2)
515
+ df[col_real] = datos_sinteticos
516
+
517
+ return df
518
+
519
+ def actualizar_tabla_evento(df, n_filas, concentracion, unidad, n_replicas):
520
+ # Actualizar tabla sin borrar "Concentración Real"
521
+ df_new = generar_tabla(n_filas, concentracion, unidad, n_replicas)
522
+
523
+ # Mapear columnas
524
+ col_real_new = [col for col in df_new.columns if 'Concentración Real' in col and 'Promedio' not in col and 'Desviación' not in col]
525
+ col_real_old = [col for col in df.columns if 'Concentración Real' in col and 'Promedio' not in col and 'Desviación' not in col]
526
+
527
+ # Reemplazar valores existentes en "Concentración Real"
528
+ for col_new, col_old in zip(col_real_new, col_real_old):
529
+ df_new[col_new] = None
530
+ for idx in df_new.index:
531
+ if idx in df.index:
532
+ df_new.at[idx, col_new] = df.at[idx, col_old]
533
+
534
+ return df_new
535
+
536
+ def cargar_excel(file):
537
+ # Leer el archivo Excel
538
+ df = pd.read_excel(file.name, sheet_name=None)
539
+
540
+ # Verificar que el archivo tenga al menos dos pestañas
541
+ if len(df) < 2:
542
+ return "El archivo debe tener al menos dos pestañas.", None, None, None, None, None, None
543
+
544
+ # Obtener la primera pestaña como referencia
545
+ primera_pestaña = next(iter(df.values()))
546
+ concentracion_inicial = primera_pestaña.iloc[0, 0]
547
+ unidad_medida = primera_pestaña.columns[0].split('(')[-1].split(')')[0]
548
+ n_filas = len(primera_pestaña)
549
+ n_replicas = len(df)
550
+
551
+ # Generar la tabla base
552
+ df_base = generar_tabla(n_filas, concentracion_inicial, unidad_medida, n_replicas)
553
+
554
+ # Llenar la tabla con los datos de cada pestaña
555
+ for i, (sheet_name, sheet_df) in enumerate(df.items(), start=1):
556
+ col_real = f"Concentración Real {i} ({unidad_medida})"
557
+ df_base[col_real] = sheet_df.iloc[:, 1].values
558
+
559
+ return concentracion_inicial, unidad_medida, n_filas, n_replicas, df_base, "", None, ""
560
+
561
+ # Interfaz Gradio
562
+ with gr.Blocks(theme=gr.themes.Soft()) as interfaz:
563
+ gr.Markdown("""
564
+ # 📊 Sistema Avanzado de Calibración con Análisis Estadístico
565
+ Configure los parámetros, edite los valores en la tabla y luego presione "Calcular" para obtener el análisis.
566
+ """)
567
+
568
+ with gr.Tab("📝 Datos de Calibración"):
569
+ with gr.Row():
570
+ concentracion_input = gr.Number(
571
+ value=2000000,
572
+ label="Concentración Inicial",
573
+ precision=0
574
+ )
575
+ unidad_input = gr.Textbox(
576
+ value="UFC",
577
+ label="Unidad de Medida",
578
+ placeholder="UFC, OD, etc..."
579
+ )
580
+ filas_slider = gr.Slider(
581
+ minimum=1,
582
+ maximum=20,
583
+ value=7,
584
+ step=1,
585
+ label="Número de filas"
586
+ )
587
+ decimales_slider = gr.Slider(
588
+ minimum=0,
589
+ maximum=5,
590
+ value=0,
591
+ step=1,
592
+ label="Número de Decimales"
593
+ )
594
+ replicas_slider = gr.Slider(
595
+ minimum=1,
596
+ maximum=10,
597
+ value=1,
598
+ step=1,
599
+ label="Número de Réplicas"
600
+ )
601
 
602
  with gr.Row():
603
+ calcular_btn = gr.Button("🔄 Calcular", variant="primary")
604
+ limpiar_btn = gr.Button("🗑 Limpiar Datos", variant="secondary")
605
+ ajustar_decimales_btn = gr.Button("🛠 Ajustar Decimales", variant="secondary")
606
+
607
+ with gr.Row():
608
+ ejemplo_ufc_btn = gr.Button("📋 Cargar Ejemplo UFC", variant="secondary")
609
+ ejemplo_od_btn = gr.Button("📋 Cargar Ejemplo OD", variant="secondary")
610
+ sinteticos_btn = gr.Button("🧪 Generar Datos Sintéticos", variant="secondary")
611
+ cargar_excel_btn = gr.UploadButton("📂 Cargar Excel", file_types=[".xlsx"], variant="secondary")
612
+
613
+ tabla_output = gr.DataFrame(
614
+ wrap=True,
615
+ label="Tabla de Datos",
616
+ interactive=True,
617
+ type="pandas",
 
 
 
 
 
 
618
  )
619
 
620
+ with gr.Tab("📊 Análisis y Reporte"):
621
+ estado_output = gr.Textbox(label="Estado", interactive=False)
622
+ graficos_output = gr.Plot(label="Gráficos de Análisis")
623
+
624
+ # Opciones y botones debajo del gráfico
625
+ with gr.Row():
626
+ # Paletas de colores disponibles en Seaborn
627
+ paletas_colores = ["deep", "muted", "pastel", "bright", "dark", "colorblind", "Set1", "Set2", "Set3"]
628
+
629
+ palette_puntos_dropdown = gr.Dropdown(
630
+ choices=paletas_colores,
631
+ value="deep",
632
+ label="Paleta para Puntos"
633
+ )
634
+ estilo_puntos_dropdown = gr.Dropdown(
635
+ choices=["o", "s", "^", "D", "v", "<", ">", "h", "H", "p", "*", "X", "d"],
636
+ value="o",
637
+ label="Estilo de Puntos"
638
+ )
639
+ palette_linea_ajuste_dropdown = gr.Dropdown(
640
+ choices=paletas_colores,
641
+ value="muted",
642
+ label="Paleta Línea de Ajuste"
643
+ )
644
+ estilo_linea_ajuste_dropdown = gr.Dropdown(
645
+ choices=["-", "--", "-.", ":"],
646
+ value="-",
647
+ label="Estilo Línea de Ajuste"
648
+ )
649
+
650
+ with gr.Row():
651
+ palette_linea_ideal_dropdown = gr.Dropdown(
652
+ choices=paletas_colores,
653
+ value="bright",
654
+ label="Paleta Línea Ideal"
655
+ )
656
+ estilo_linea_ideal_dropdown = gr.Dropdown(
657
+ choices=["--", "-", "-.", ":"],
658
+ value="--",
659
+ label="Estilo Línea Ideal"
660
+ )
661
+ palette_barras_error_dropdown = gr.Dropdown(
662
+ choices=paletas_colores,
663
+ value="pastel",
664
+ label="Paleta Barras de Error"
665
+ )
666
+ mostrar_linea_ajuste = gr.Checkbox(value=True, label="Mostrar Línea de Ajuste")
667
+ mostrar_linea_ideal = gr.Checkbox(value=True, label="Mostrar Línea Ideal")
668
+ mostrar_puntos = gr.Checkbox(value=True, label="Mostrar Puntos")
669
+ graficar_btn = gr.Button("📊 Graficar", variant="primary")
670
+
671
+ with gr.Row():
672
+ copiar_btn = gr.Button("📋 Copiar Informe", variant="secondary")
673
+ exportar_word_btn = gr.Button("💾 Exportar Informe Word", variant="primary")
674
+ exportar_latex_btn = gr.Button("💾 Exportar Informe LaTeX", variant="primary")
675
+
676
  with gr.Row():
677
+ exportar_word_file = gr.File(label="Informe en Word")
678
+ exportar_latex_file = gr.File(label="Informe en LaTeX")
679
+
680
+ # Informe al final
681
+ informe_output = gr.Markdown(elem_id="informe_output")
682
+
683
+ # Eventos
684
+ input_components = [tabla_output]
685
+ output_components = [estado_output, graficos_output, informe_output, tabla_output]
686
+
687
+ # Evento al presionar el botón Calcular
688
+ calcular_btn.click(
689
+ fn=actualizar_analisis,
690
+ inputs=[tabla_output, replicas_slider, unidad_input],
691
+ outputs=output_components
692
+ )
693
+
694
+ # Evento para graficar con opciones seleccionadas
695
+ graficar_btn.click(
696
+ fn=actualizar_graficos,
697
+ inputs=[
698
+ tabla_output, replicas_slider, unidad_input,
699
+ palette_puntos_dropdown, estilo_puntos_dropdown,
700
+ palette_linea_ajuste_dropdown, estilo_linea_ajuste_dropdown,
701
+ palette_linea_ideal_dropdown, estilo_linea_ideal_dropdown,
702
+ palette_barras_error_dropdown,
703
+ mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos
704
+ ],
705
+ outputs=graficos_output
706
+ )
707
+
708
+ # Evento para limpiar datos
709
+ limpiar_btn.click(
710
+ fn=limpiar_datos,
711
+ inputs=[replicas_slider],
712
+ outputs=[concentracion_input, unidad_input, filas_slider, tabla_output, estado_output, graficos_output, informe_output]
713
+ )
714
+
715
+ # Eventos de los botones de ejemplo
716
+ ejemplo_ufc_btn.click(
717
+ fn=cargar_ejemplo_ufc,
718
+ inputs=[replicas_slider],
719
+ outputs=[concentracion_input, unidad_input, filas_slider, tabla_output]
720
+ )
721
+
722
+ ejemplo_od_btn.click(
723
+ fn=cargar_ejemplo_od,
724
+ inputs=[replicas_slider],
725
+ outputs=[concentracion_input, unidad_input, filas_slider, tabla_output]
726
+ )
727
+
728
+ # Evento para generar datos sintéticos
729
+ sinteticos_btn.click(
730
+ fn=generar_datos_sinteticos_evento,
731
+ inputs=[tabla_output, replicas_slider, unidad_input],
732
+ outputs=tabla_output
733
+ )
734
+
735
+ # Evento para cargar archivo Excel
736
+ cargar_excel_btn.upload(
737
+ fn=cargar_excel,
738
+ inputs=[cargar_excel_btn],
739
+ outputs=[concentracion_input, unidad_input, filas_slider, replicas_slider, tabla_output, estado_output, graficos_output, informe_output]
740
+ )
741
+
742
+ # Evento al presionar el botón Ajustar Decimales
743
+ ajustar_decimales_btn.click(
744
+ fn=ajustar_decimales_evento,
745
+ inputs=[tabla_output, decimales_slider],
746
+ outputs=tabla_output
747
+ )
748
+
749
+ # Actualizar tabla al cambiar los parámetros (sin borrar "Concentración Real")
750
+ def actualizar_tabla_wrapper(df, filas, conc, unidad, replicas):
751
+ return actualizar_tabla_evento(df, filas, conc, unidad, replicas)
752
+
753
+ concentracion_input.change(
754
+ fn=actualizar_tabla_wrapper,
755
+ inputs=[tabla_output, filas_slider, concentracion_input, unidad_input, replicas_slider],
756
+ outputs=tabla_output
757
+ )
758
+
759
+ unidad_input.change(
760
+ fn=actualizar_tabla_wrapper,
761
+ inputs=[tabla_output, filas_slider, concentracion_input, unidad_input, replicas_slider],
762
+ outputs=tabla_output
763
+ )
764
+
765
+ filas_slider.change(
766
+ fn=actualizar_tabla_wrapper,
767
+ inputs=[tabla_output, filas_slider, concentracion_input, unidad_input, replicas_slider],
768
+ outputs=tabla_output
769
+ )
770
+
771
+ replicas_slider.change(
772
+ fn=actualizar_tabla_wrapper,
773
+ inputs=[tabla_output, filas_slider, concentracion_input, unidad_input, replicas_slider],
774
+ outputs=tabla_output
775
+ )
776
+
777
+ # No agregamos un evento para decimales_slider.change, para evitar borrar la columna "Concentración Real"
778
+
779
+ # Evento de copiar informe utilizando JavaScript
780
+ copiar_btn.click(
781
+ None,
782
+ [],
783
+ [],
784
+ js="""
785
+ function() {
786
+ const informeElement = document.querySelector('#informe_output');
787
+ const range = document.createRange();
788
+ range.selectNode(informeElement);
789
+ window.getSelection().removeAllRanges();
790
+ window.getSelection().addRange(range);
791
+ document.execCommand('copy');
792
+ window.getSelection().removeAllRanges();
793
+ alert('Informe copiado al portapapeles');
794
+ }
795
+ """
796
+ )
797
+
798
+ # Eventos de exportar informes
799
+ exportar_word_btn.click(
800
+ fn=exportar_word,
801
+ inputs=[tabla_output, informe_output, unidad_input],
802
+ outputs=exportar_word_file
803
+ )
804
+
805
+ exportar_latex_btn.click(
806
+ fn=exportar_latex,
807
+ inputs=[tabla_output, informe_output],
808
+ outputs=exportar_latex_file
809
+ )
810
+
811
+ # Inicializar la interfaz con el ejemplo base
812
+ def iniciar_con_ejemplo():
813
+ n_replicas = 1
814
+ df = generar_tabla(7, 2000000, "UFC", n_replicas)
815
+ # Valores reales de ejemplo
816
+ df[f"Concentración Real 1 (UFC)"] = [2000000, 1600000, 1200000, 800000, 400000, 200000, 100000]
817
+ estado, fig, informe, df = actualizar_analisis(df, n_replicas, "UFC")
818
+ return (
819
+ 2000000,
820
+ "UFC",
821
+ 7,
822
+ df,
823
+ estado,
824
+ fig,
825
+ informe
826
  )
827
 
828
+ interfaz.load(
829
+ fn=iniciar_con_ejemplo,
830
+ outputs=[concentracion_input, unidad_input, filas_slider, tabla_output, estado_output, graficos_output, informe_output]
831
+ )
832
 
833
+ # Lanzar la interfaz
834
+ if __name__ == "__main__":
835
+ interfaz.launch()