C2MV commited on
Commit
e2287bf
1 Parent(s): 8bffe47

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +460 -487
app.py CHANGED
@@ -14,51 +14,87 @@ from datetime import datetime
14
  import docx
15
  from docx.shared import Inches, Pt
16
  from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
17
- from matplotlib.colors import to_hex
18
  import os
 
19
 
20
  # --- Clase RSM_BoxBehnken ---
21
  class RSM_BoxBehnken:
22
- def __init__(self, data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels):
23
- """
24
- Inicializa la clase con los datos del diseño Box-Behnken.
25
- """
26
- self.data = data.copy()
27
- self.model = None
28
- self.model_simplified = None
29
- self.optimized_results = None
30
- self.optimal_levels = None
31
- self.all_figures = [] # Lista para almacenar las figuras
32
- self.x1_name = x1_name
33
- self.x2_name = x2_name
34
- self.x3_name = x3_name
35
- self.y_name = y_name
36
-
37
- # Niveles originales de las variables
38
- self.x1_levels = x1_levels
39
- self.x2_levels = x2_levels
40
- self.x3_levels = x3_levels
41
-
42
- def get_levels(self, variable_name):
43
- """
44
- Obtiene los niveles para una variable específica.
45
- """
46
- if variable_name == self.x1_name:
47
- return self.x1_levels
48
- elif variable_name == self.x2_name:
49
- return self.x2_levels
50
- elif variable_name == self.x3_name:
51
- return self.x3_levels
52
- else:
53
- raise ValueError(f"Variable desconocida: {variable_name}")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
 
55
  def fit_model(self):
56
  """
57
  Ajusta el modelo de segundo orden completo a los datos.
58
  """
59
- formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
60
- f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2) + ' \
61
- f'{self.x1_name}:{self.x2_name} + {self.x1_name}:{self.x3_name} + {self.x2_name}:{self.x3_name}'
62
  self.model = smf.ols(formula, data=self.data).fit()
63
  print("Modelo Completo:")
64
  print(self.model.summary())
@@ -66,15 +102,29 @@ class RSM_BoxBehnken:
66
 
67
  def fit_simplified_model(self):
68
  """
69
- Ajusta el modelo de segundo orden a los datos, eliminando términos no significativos.
70
  """
71
- formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + ' \
72
- f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
73
  self.model_simplified = smf.ols(formula, data=self.data).fit()
74
  print("\nModelo Simplificado:")
75
  print(self.model_simplified.summary())
76
  return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Modelo Simplificado")
77
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
78
  def optimize(self, method='Nelder-Mead'):
79
  """
80
  Encuentra los niveles óptimos de los factores para maximizar la respuesta usando el modelo simplificado.
@@ -84,138 +134,128 @@ class RSM_BoxBehnken:
84
  return
85
 
86
  def objective_function(x):
87
- return -self.model_simplified.predict(pd.DataFrame({
88
- self.x1_name: [x[0]],
89
- self.x2_name: [x[1]],
90
- self.x3_name: [x[2]]
91
- })).values[0]
92
-
93
- bounds = [(-1, 1), (-1, 1), (-1, 1)]
94
- x0 = [0, 0, 0]
 
 
 
 
 
95
 
96
  self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
97
  self.optimal_levels = self.optimized_results.x
98
 
99
  # Convertir niveles óptimos de codificados a naturales
100
  optimal_levels_natural = [
101
- self.coded_to_natural(self.optimal_levels[0], self.x1_name),
102
- self.coded_to_natural(self.optimal_levels[1], self.x2_name),
103
- self.coded_to_natural(self.optimal_levels[2], self.x3_name)
104
  ]
 
105
  # Crear la tabla de optimización
106
  optimization_table = pd.DataFrame({
107
- 'Variable': [self.x1_name, self.x2_name, self.x3_name],
108
  'Nivel Óptimo (Natural)': optimal_levels_natural,
109
  'Nivel Óptimo (Codificado)': self.optimal_levels
110
  })
111
 
112
- return optimization_table.round(3) # Redondear a 3 decimales
113
 
114
- def plot_rsm_individual(self, fixed_variable, fixed_level):
115
  """
116
- Genera un gráfico de superficie de respuesta (RSM) individual para una configuración específica.
 
117
  """
118
  if self.model_simplified is None:
119
  print("Error: Ajusta el modelo simplificado primero.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
120
  return None
121
 
122
- # Determinar las variables que varían y sus niveles naturales
123
- varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
124
-
125
- # Establecer los niveles naturales para las variables que varían
126
- x_natural_levels = self.get_levels(varying_variables[0])
127
- y_natural_levels = self.get_levels(varying_variables[1])
128
 
129
  # Crear una malla de puntos para las variables que varían (en unidades naturales)
130
- x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
131
- y_range_natural = np.linspace(y_natural_levels[0], y_natural_levels[-1], 100)
132
- x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
133
 
134
  # Convertir la malla de variables naturales a codificadas
135
- x_grid_coded = self.natural_to_coded(x_grid_natural, varying_variables[0])
136
- y_grid_coded = self.natural_to_coded(y_grid_natural, varying_variables[1])
137
 
138
  # Crear un DataFrame para la predicción con variables codificadas
139
  prediction_data = pd.DataFrame({
140
- varying_variables[0]: x_grid_coded.flatten(),
141
- varying_variables[1]: y_grid_coded.flatten(),
 
142
  })
143
- prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
144
 
145
- # Calcular los valores predichos
146
- z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape)
 
 
147
 
148
- # Filtrar por el nivel de la variable fija (en codificado)
149
- fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable)
150
- subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)]
151
 
152
- # Filtrar por niveles válidos en las variables que varían
153
- valid_levels = [-1, 0, 1]
154
- experiments_data = subset_data[
155
- subset_data[varying_variables[0]].isin(valid_levels) &
156
- subset_data[varying_variables[1]].isin(valid_levels)
157
- ]
158
 
159
- # Convertir coordenadas de experimentos a naturales
160
- experiments_x_natural = experiments_data[varying_variables[0]].apply(lambda x: self.coded_to_natural(x, varying_variables[0]))
161
- experiments_y_natural = experiments_data[varying_variables[1]].apply(lambda x: self.coded_to_natural(x, varying_variables[1]))
162
-
163
- # Crear el gráfico de superficie con variables naturales en los ejes y transparencia
164
- fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)])
165
-
166
- # --- Añadir cuadrícula a la superficie ---
167
- # Líneas en la dirección x
168
- for i in range(x_grid_natural.shape[0]):
169
- fig.add_trace(go.Scatter3d(
170
- x=x_grid_natural[i, :],
171
- y=y_grid_natural[i, :],
172
- z=z_pred[i, :],
173
- mode='lines',
174
- line=dict(color='gray', width=2),
175
- showlegend=False,
176
- hoverinfo='skip'
177
- ))
178
- # Líneas en la dirección y
179
- for j in range(x_grid_natural.shape[1]):
180
- fig.add_trace(go.Scatter3d(
181
- x=x_grid_natural[:, j],
182
- y=y_grid_natural[:, j],
183
- z=z_pred[:, j],
184
- mode='lines',
185
- line=dict(color='gray', width=2),
186
- showlegend=False,
187
- hoverinfo='skip'
188
- ))
189
-
190
- # --- Fin de la adición de la cuadrícula ---
191
-
192
- # Añadir los puntos de los experimentos en la superficie de respuesta con diferentes colores y etiquetas
193
- colors = px.colors.qualitative.Safe
194
- point_labels = [f"{row[self.y_name]:.3f}" for _, row in experiments_data.iterrows()]
195
 
 
 
 
196
  fig.add_trace(go.Scatter3d(
197
- x=experiments_x_natural,
198
- y=experiments_y_natural,
199
- z=experiments_data[self.y_name].round(3),
200
  mode='markers+text',
201
- marker=dict(size=4, color=colors[:len(experiments_x_natural)]),
202
- text=point_labels,
203
  textposition='top center',
204
  name='Experimentos'
205
  ))
206
 
207
- # Añadir etiquetas y título con variables naturales
208
  fig.update_layout(
209
  scene=dict(
210
- xaxis_title=f"{varying_variables[0]} ({self.get_units(varying_variables[0])})",
211
- yaxis_title=f"{varying_variables[1]} ({self.get_units(varying_variables[1])})",
212
  zaxis_title=self.y_name,
213
  ),
214
- title=f"{self.y_name} vs {varying_variables[0]} y {varying_variables[1]}<br><sup>{fixed_variable} fijo en {fixed_level:.3f} ({self.get_units(fixed_variable)}) (Modelo Simplificado)</sup>",
215
  height=800,
216
  width=1000,
217
  showlegend=True
218
  )
 
219
  return fig
220
 
221
  def get_units(self, variable_name):
@@ -227,56 +267,22 @@ class RSM_BoxBehnken:
227
  'Glucosa': 'g/L',
228
  'Extracto_de_Levadura': 'g/L',
229
  'Triptofano': 'g/L',
230
- 'AIA_ppm': 'ppm'
 
231
  }
232
  return units.get(variable_name, '')
233
 
234
- def generate_all_plots(self):
235
- """
236
- Genera todas las gráficas de RSM, variando la variable fija y sus niveles usando el modelo simplificado.
237
- Almacena las figuras en self.all_figures.
238
- """
239
- if self.model_simplified is None:
240
- print("Error: Ajusta el modelo simplificado primero.")
241
- return
242
-
243
- self.all_figures = [] # Resetear la lista de figuras
244
-
245
- # Niveles naturales para graficar
246
- levels_to_plot_natural = {
247
- self.x1_name: self.x1_levels,
248
- self.x2_name: self.x2_levels,
249
- self.x3_name: self.x3_levels
250
- }
251
-
252
- # Generar y almacenar gráficos individuales
253
- for fixed_variable in [self.x1_name, self.x2_name, self.x3_name]:
254
- for level in levels_to_plot_natural[fixed_variable]:
255
- fig = self.plot_rsm_individual(fixed_variable, level)
256
- if fig is not None:
257
- self.all_figures.append(fig)
258
-
259
- def coded_to_natural(self, coded_value, variable_name):
260
- """Convierte un valor codificado a su valor natural."""
261
- levels = self.get_levels(variable_name)
262
- return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2
263
-
264
- def natural_to_coded(self, natural_value, variable_name):
265
- """Convierte un valor natural a su valor codificado."""
266
- levels = self.get_levels(variable_name)
267
- return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
268
-
269
  def pareto_chart(self, model, title):
270
  """
271
  Genera un diagrama de Pareto para los efectos estandarizados de un modelo,
272
  incluyendo la línea de significancia.
273
  """
274
  # Calcular los efectos estandarizados
275
- tvalues = model.tvalues[1:] # Excluir la Intercept
276
- abs_tvalues = np.abs(tvalues)
277
- sorted_idx = np.argsort(abs_tvalues)[::-1]
278
  sorted_tvalues = abs_tvalues[sorted_idx]
279
- sorted_names = tvalues.index[sorted_idx]
280
 
281
  # Calcular el valor crítico de t para la línea de significancia
282
  alpha = 0.05 # Nivel de significancia
@@ -300,34 +306,6 @@ class RSM_BoxBehnken:
300
 
301
  return fig
302
 
303
- def get_simplified_equation(self):
304
- """
305
- Retorna la ecuación del modelo simplificado como una cadena de texto.
306
- """
307
- if self.model_simplified is None:
308
- print("Error: Ajusta el modelo simplificado primero.")
309
- return None
310
-
311
- coefficients = self.model_simplified.params
312
- equation = f"{self.y_name} = {coefficients['Intercept']:.3f}"
313
-
314
- for term, coef in coefficients.items():
315
- if term != 'Intercept':
316
- if term == f'{self.x1_name}':
317
- equation += f" + {coef:.3f}*{self.x1_name}"
318
- elif term == f'{self.x2_name}':
319
- equation += f" + {coef:.3f}*{self.x2_name}"
320
- elif term == f'{self.x3_name}':
321
- equation += f" + {coef:.3f}*{self.x3_name}"
322
- elif term == f'I({self.x1_name} ** 2)':
323
- equation += f" + {coef:.3f}*{self.x1_name}^2"
324
- elif term == f'I({self.x2_name} ** 2)':
325
- equation += f" + {coef:.3f}*{self.x2_name}^2"
326
- elif term == f'I({self.x3_name} ** 2)':
327
- equation += f" + {coef:.3f}*{self.x3_name}^2"
328
-
329
- return equation
330
-
331
  def generate_prediction_table(self):
332
  """
333
  Genera una tabla con los valores actuales, predichos y residuales.
@@ -366,12 +344,8 @@ class RSM_BoxBehnken:
366
  for index, row in anova_table.iterrows():
367
  if index != 'Residual':
368
  factor_name = index
369
- if factor_name == f'I({self.x1_name} ** 2)':
370
- factor_name = f'{self.x1_name}^2'
371
- elif factor_name == f'I({self.x2_name} ** 2)':
372
- factor_name = f'{self.x2_name}^2'
373
- elif factor_name == f'I({self.x3_name} ** 2)':
374
- factor_name = f'{self.x3_name}^2'
375
 
376
  ss_factor = row['sum_sq']
377
  contribution_percentage = (ss_factor / ss_total) * 100
@@ -394,8 +368,7 @@ class RSM_BoxBehnken:
394
 
395
  # --- ANOVA detallada ---
396
  # 1. Ajustar un modelo solo con los términos de primer orden y cuadráticos
397
- formula_reduced = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
398
- f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
399
  model_reduced = smf.ols(formula_reduced, data=self.data).fit()
400
 
401
  # 2. ANOVA del modelo reducido (para obtener la suma de cuadrados de la regresión)
@@ -408,7 +381,7 @@ class RSM_BoxBehnken:
408
  df_total = len(self.data) - 1
409
 
410
  # 5. Suma de cuadrados de la regresión
411
- ss_regression = anova_reduced['sum_sq'][:-1].sum() # Sumar todo excepto 'Residual'
412
 
413
  # 6. Grados de libertad de la regresión
414
  df_regression = len(anova_reduced) - 1
@@ -418,13 +391,9 @@ class RSM_BoxBehnken:
418
  df_residual = self.model_simplified.df_resid
419
 
420
  # 8. Suma de cuadrados del error puro (se calcula a partir de las réplicas)
421
- replicas = self.data[self.data.duplicated(subset=[self.x1_name, self.x2_name, self.x3_name], keep=False)]
422
- if not replicas.empty:
423
- ss_pure_error = replicas.groupby([self.x1_name, self.x2_name, self.x3_name])[self.y_name].var().sum() * replicas.groupby([self.x1_name, self.x2_name, self.x3_name]).ngroups
424
- df_pure_error = len(replicas) - replicas.groupby([self.x1_name, self.x2_name, self.x3_name]).ngroups
425
- else:
426
- ss_pure_error = np.nan
427
- df_pure_error = np.nan
428
 
429
  # 9. Suma de cuadrados de la falta de ajuste
430
  ss_lack_of_fit = ss_residual - ss_pure_error if not np.isnan(ss_pure_error) else np.nan
@@ -451,7 +420,9 @@ class RSM_BoxBehnken:
451
  })
452
 
453
  # Calcular la suma de cuadrados y grados de libertad para la curvatura
454
- ss_curvature = anova_reduced['sum_sq'][f'I({self.x1_name} ** 2)'] + anova_reduced['sum_sq'][f'I({self.x2_name} ** 2)'] + anova_reduced['sum_sq'][f'I({self.x3_name} ** 2)']
 
 
455
  df_curvature = 3
456
 
457
  # Añadir la fila de curvatura a la tabla ANOVA
@@ -467,7 +438,7 @@ class RSM_BoxBehnken:
467
 
468
  def get_all_tables(self):
469
  """
470
- Obtiene todas las tablas generadas para ser exportadas a Excel.
471
  """
472
  prediction_table = self.generate_prediction_table()
473
  contribution_table = self.calculate_contribution_percentage()
@@ -595,240 +566,285 @@ class RSM_BoxBehnken:
595
 
596
  # --- Funciones para la Interfaz de Gradio ---
597
 
598
- def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str):
599
  """
600
- Carga los datos del diseño Box-Behnken desde cajas de texto y crea la instancia de RSM_BoxBehnken.
 
601
  """
602
  try:
603
- # Convertir los niveles a listas de números
604
- x1_levels = [float(x.strip()) for x in x1_levels_str.split(',')]
605
- x2_levels = [float(x.strip()) for x in x2_levels_str.split(',')]
606
- x3_levels = [float(x.strip()) for x in x3_levels_str.split(',')]
607
-
608
- # Crear DataFrame a partir de la cadena de datos
609
- data_list = [row.split(',') for row in data_str.strip().split('\n')]
610
- column_names = ['Exp.', x1_name, x2_name, x3_name, y_name]
611
- data = pd.DataFrame(data_list, columns=column_names)
612
- data = data.apply(pd.to_numeric, errors='coerce') # Convertir a numérico
613
-
614
- # Validar que el DataFrame tenga las columnas correctas
615
- if not all(col in data.columns for col in column_names):
616
- raise ValueError("El formato de los datos no es correcto.")
617
-
618
- # Crear la instancia de RSM_BoxBehnken
619
- global rsm
620
- rsm = RSM_BoxBehnken(data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels)
621
-
622
- return data.round(3), x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels, gr.update(visible=True)
623
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
624
  except Exception as e:
625
- # Mostrar mensaje de error
626
- error_message = f"Error al cargar los datos: {str(e)}"
627
- print(error_message)
628
- return None, "", "", "", "", [], [], [], gr.update(visible=False)
629
-
630
- def fit_and_optimize_model():
631
- if 'rsm' not in globals():
632
- return [None]*11 # Ajustar el número de outputs
633
-
634
- # Ajustar modelos y optimizar
635
- model_completo, pareto_completo = rsm.fit_model()
636
- model_simplificado, pareto_simplificado = rsm.fit_simplified_model()
637
- optimization_table = rsm.optimize()
638
- equation = rsm.get_simplified_equation()
639
- prediction_table = rsm.generate_prediction_table()
640
- contribution_table = rsm.calculate_contribution_percentage()
641
- anova_table = rsm.calculate_detailed_anova()
642
-
643
- # Generar todas las figuras y almacenarlas
644
- rsm.generate_all_plots()
645
-
646
- # Formatear la ecuación para que se vea mejor en Markdown
647
- equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ")
648
- equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}"
649
-
650
- # Guardar las tablas en Excel temporal
651
- excel_path = rsm.save_tables_to_excel()
652
-
653
- # Guardar todas las figuras en un ZIP temporal
654
- zip_path = rsm.save_figures_to_zip()
655
-
656
- return (
657
- model_completo.summary().as_html(),
658
- pareto_completo,
659
- model_simplificado.summary().as_html(),
660
- pareto_simplificado,
661
- equation_formatted,
662
- optimization_table,
663
- prediction_table,
664
- contribution_table,
665
- anova_table,
666
- zip_path, # Ruta del ZIP de gráficos
667
- excel_path # Ruta del Excel de tablas
668
- )
669
-
670
- def show_plot(current_index, all_figures):
671
- if not all_figures:
672
- return None, "No hay gráficos disponibles.", current_index
673
- selected_fig = all_figures[current_index]
674
- plot_info_text = f"Gráfico {current_index + 1} de {len(all_figures)}"
675
- return selected_fig, plot_info_text, current_index
676
-
677
- def navigate_plot(direction, current_index, all_figures):
678
- """
679
- Navega entre los gráficos.
680
- """
681
- if not all_figures:
682
- return None, "No hay gráficos disponibles.", current_index
683
-
684
- if direction == 'left':
685
- new_index = (current_index - 1) % len(all_figures)
686
- elif direction == 'right':
687
- new_index = (current_index + 1) % len(all_figures)
688
- else:
689
- new_index = current_index
690
-
691
- selected_fig = all_figures[new_index]
692
- plot_info_text = f"Gráfico {new_index + 1} de {len(all_figures)}"
693
-
694
- return selected_fig, plot_info_text, new_index
695
-
696
- def download_current_plot(all_figures, current_index):
697
  """
698
- Descarga la figura actual como PNG.
699
  """
700
- if not all_figures:
701
- return None
702
- fig = all_figures[current_index]
703
- img_bytes = rsm.save_fig_to_bytes(fig)
704
- filename = f"Grafico_RSM_{current_index + 1}.png"
705
-
706
- # Crear un archivo temporal
707
- with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
708
- temp_file.write(img_bytes)
709
- temp_path = temp_file.name
710
-
711
- return temp_path # Retornar solo la ruta
712
-
713
- def download_all_plots_zip():
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
714
  """
715
- Descarga todas las figuras en un archivo ZIP.
716
  """
717
- if 'rsm' not in globals():
 
 
 
718
  return None
719
- zip_path = rsm.save_figures_to_zip()
720
- if zip_path:
721
- filename = f"Graficos_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
722
- # Gradio no permite renombrar directamente, por lo que retornamos la ruta del archivo
723
- return zip_path
724
- return None
725
-
726
- def download_all_tables_excel():
727
- """
728
- Descarga todas las tablas en un archivo Excel con múltiples hojas.
729
- """
730
- if 'rsm' not in globals():
731
  return None
732
- excel_path = rsm.save_tables_to_excel()
733
- if excel_path:
734
- filename = f"Tablas_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
735
- # Gradio no permite renombrar directamente, por lo que retornamos la ruta del archivo
736
- return excel_path
737
- return None
738
-
739
- def exportar_word(rsm_instance, tables_dict):
740
- """
741
- Función para exportar las tablas a un documento de Word.
742
- """
743
- word_path = rsm_instance.export_tables_to_word(tables_dict)
744
- if word_path and os.path.exists(word_path):
745
- return word_path
746
- return None
747
 
748
  # --- Crear la interfaz de Gradio ---
749
 
750
  def create_gradio_interface():
751
  with gr.Blocks() as demo:
752
- gr.Markdown("# Optimización de la producción de AIA usando RSM Box-Behnken")
753
-
754
- with gr.Row():
755
- with gr.Column():
756
- gr.Markdown("## Configuración del Diseño")
757
- x1_name_input = gr.Textbox(label="Nombre de la Variable X1 (ej. Glucosa)", value="Glucosa")
758
- x2_name_input = gr.Textbox(label="Nombre de la Variable X2 (ej. Extracto de Levadura)", value="Extracto_de_Levadura")
759
- x3_name_input = gr.Textbox(label="Nombre de la Variable X3 (ej. Triptófano)", value="Triptofano")
760
- y_name_input = gr.Textbox(label="Nombre de la Variable Dependiente (ej. AIA (ppm))", value="AIA_ppm")
761
- x1_levels_input = gr.Textbox(label="Niveles de X1 (separados por comas)", value="1, 3.5, 5.5")
762
- x2_levels_input = gr.Textbox(label="Niveles de X2 (separados por comas)", value="0.03, 0.2, 0.3")
763
- x3_levels_input = gr.Textbox(label="Niveles de X3 (separados por comas)", value="0.4, 0.65, 0.9")
764
- data_input = gr.Textbox(label="Datos del Experimento (formato CSV)", lines=10, value="""1,-1,-1,0,166.594
765
- 2,1,-1,0,177.557
766
- 3,-1,1,0,127.261
767
- 4,1,1,0,147.573
768
- 5,-1,0,-1,188.883
769
- 6,1,0,-1,224.527
770
- 7,-1,0,1,190.238
771
- 8,1,0,1,226.483
772
- 9,0,-1,-1,195.550
773
- 10,0,1,-1,149.493
774
- 11,0,-1,1,187.683
775
- 12,0,1,1,148.621
776
- 13,0,0,0,278.951
777
- 14,0,0,0,297.238
778
- 15,0,0,0,280.896""")
779
- load_button = gr.Button("Cargar Datos")
 
 
 
 
 
780
 
781
- with gr.Column():
782
- gr.Markdown("## Datos Cargados")
783
- data_output = gr.Dataframe(label="Tabla de Datos", interactive=False)
784
 
785
- # Sección de análisis visible solo después de cargar los datos
786
- with gr.Row(visible=False) as analysis_row:
787
- with gr.Column():
788
- fit_button = gr.Button("Ajustar Modelo y Optimizar")
789
- gr.Markdown("**Modelo Completo**")
790
- model_completo_output = gr.HTML()
791
- pareto_completo_output = gr.Plot()
792
- gr.Markdown("**Modelo Simplificado**")
793
- model_simplificado_output = gr.HTML()
794
- pareto_simplificado_output = gr.Plot()
795
- gr.Markdown("**Ecuación del Modelo Simplificado**")
796
- equation_output = gr.HTML()
797
- optimization_table_output = gr.Dataframe(label="Tabla de Optimización", interactive=False)
798
- prediction_table_output = gr.Dataframe(label="Tabla de Predicciones", interactive=False)
799
- contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución", interactive=False)
800
- anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada", interactive=False)
801
- gr.Markdown("## Descargar Todas las Tablas")
802
- download_excel_button = gr.DownloadButton("Descargar Tablas en Excel")
803
- download_word_button = gr.DownloadButton("Descargar Tablas en Word")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
804
 
805
- with gr.Column():
806
- gr.Markdown("## Generar Gráficos de Superficie de Respuesta")
807
- fixed_variable_input = gr.Dropdown(label="Variable Fija", choices=["Glucosa", "Extracto_de_Levadura", "Triptofano"], value="Glucosa")
808
- fixed_level_input = gr.Slider(label="Nivel de Variable Fija", minimum=-1, maximum=1, step=0.01, value=0.0)
809
- plot_button = gr.Button("Generar Gráficos")
810
- with gr.Row():
811
- left_button = gr.Button("<")
812
- right_button = gr.Button(">")
813
- rsm_plot_output = gr.Plot()
814
- plot_info = gr.Textbox(label="Información del Gráfico", value="Gráfico 1 de 9", interactive=False)
815
- with gr.Row():
816
- download_plot_button = gr.DownloadButton("Descargar Gráfico Actual (PNG)")
817
- download_all_plots_button = gr.DownloadButton("Descargar Todos los Gráficos (ZIP)")
818
- current_index_state = gr.State(0) # Estado para el índice actual
819
- all_figures_state = gr.State([]) # Estado para todas las figuras
820
-
821
- # Cargar datos
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
822
  load_button.click(
823
- load_data,
824
- inputs=[x1_name_input, x2_name_input, x3_name_input, y_name_input, x1_levels_input, x2_levels_input, x3_levels_input, data_input],
825
- outputs=[data_output, x1_name_input, x2_name_input, x3_name_input, y_name_input, x1_levels_input, x2_levels_input, x3_levels_input, analysis_row]
 
 
 
826
  )
827
-
828
- # Ajustar modelo y optimizar
829
- fit_button.click(
830
- fit_and_optimize_model,
831
- inputs=[],
832
  outputs=[
833
  model_completo_output,
834
  pareto_completo_output,
@@ -839,77 +855,34 @@ def create_gradio_interface():
839
  prediction_table_output,
840
  contribution_table_output,
841
  anova_table_output,
842
- download_all_plots_button, # Ruta del ZIP de gráficos
843
- download_excel_button # Ruta del Excel de tablas
 
844
  ]
845
  )
846
-
847
- # Generar y mostrar los gráficos
848
- plot_button.click(
849
- lambda fixed_var, fixed_lvl: (
850
- rsm.plot_rsm_individual(fixed_var, fixed_lvl),
851
- f"Gráfico 1 de {len(rsm.all_figures)}" if rsm.all_figures else "No hay gráficos disponibles.",
852
- 0,
853
- rsm.all_figures # Actualizar el estado de todas las figuras
854
- ),
855
- inputs=[fixed_variable_input, fixed_level_input],
856
- outputs=[rsm_plot_output, plot_info, current_index_state, all_figures_state]
857
- )
858
-
859
- # Navegación de gráficos
860
- left_button.click(
861
- lambda current_index, all_figures: navigate_plot('left', current_index, all_figures),
862
- inputs=[current_index_state, all_figures_state],
863
- outputs=[rsm_plot_output, plot_info, current_index_state]
864
- )
865
- right_button.click(
866
- lambda current_index, all_figures: navigate_plot('right', current_index, all_figures),
867
- inputs=[current_index_state, all_figures_state],
868
- outputs=[rsm_plot_output, plot_info, current_index_state]
869
- )
870
-
871
- # Descargar gráfico actual
872
- download_plot_button.click(
873
- download_current_plot,
874
- inputs=[all_figures_state, current_index_state],
875
- outputs=download_plot_button
876
  )
877
-
878
- # Descargar todos los gráficos en ZIP
879
  download_all_plots_button.click(
880
- download_all_plots_zip,
881
- inputs=[],
882
- outputs=download_all_plots_button
883
- )
884
-
885
- # Descargar todas las tablas en Excel y Word
886
- download_excel_button.click(
887
- fn=lambda: download_all_tables_excel(),
888
- inputs=[],
889
- outputs=download_excel_button
890
  )
891
-
 
892
  download_word_button.click(
893
- fn=lambda: exportar_word(rsm, rsm.get_all_tables()),
894
- inputs=[],
895
- outputs=download_word_button
896
  )
897
-
898
- # Ejemplo de uso
899
- gr.Markdown("## Ejemplo de uso")
900
- gr.Markdown("""
901
- 1. Introduce los nombres de las variables y sus niveles en las cajas de texto correspondientes.
902
- 2. Copia y pega los datos del experimento en la caja de texto 'Datos del Experimento'.
903
- 3. Haz clic en 'Cargar Datos' para cargar los datos en la tabla.
904
- 4. Haz clic en 'Ajustar Modelo y Optimizar' para ajustar el modelo y encontrar los niveles óptimos de los factores.
905
- 5. Selecciona una variable fija y su nivel en los controles deslizantes.
906
- 6. Haz clic en 'Generar Gráficos' para generar los gráficos de superficie de respuesta.
907
- 7. Navega entre los gráficos usando los botones '<' y '>'.
908
- 8. Descarga el gráfico actual en PNG o descarga todos los gráficos en un ZIP.
909
- 9. Descarga todas las tablas en un archivo Excel o Word con los botones correspondientes.
910
- """)
911
 
912
- return demo
913
 
914
  # --- Función Principal ---
915
 
 
14
  import docx
15
  from docx.shared import Inches, Pt
16
  from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
 
17
  import os
18
+ import itertools
19
 
20
  # --- Clase RSM_BoxBehnken ---
21
  class RSM_BoxBehnken:
22
+ def __init__(self, factor_names, factor_levels, y_name):
23
+ """
24
+ Inicializa la clase con los nombres de factores, sus niveles y la variable dependiente.
25
+ """
26
+ self.factor_names = factor_names # Lista de nombres de factores
27
+ self.factor_levels = factor_levels # Lista de diccionarios con min y max para cada factor
28
+ self.y_name = y_name # Nombre de la variable dependiente
29
+ self.n_factors = len(factor_names) # Número de factores
30
+ self.data = None # DataFrame con los datos del experimento
31
+ self.design = None # DataFrame con el diseño Box-Behnken
32
+ self.model = None # Modelo completo
33
+ self.model_simplified = None # Modelo simplificado
34
+ self.optimized_results = None # Resultados de optimización
35
+ self.optimal_levels = None # Niveles óptimos de factores
36
+ self.all_figures = [] # Lista para almacenar las figuras generadas
37
+
38
+ def generate_box_behnken_design(self, center_runs=3):
39
+ """
40
+ Genera el diseño Box-Behnken para el número de factores especificado.
41
+ """
42
+ design = []
43
+
44
+ # Generar todas las combinaciones de dos factores
45
+ factor_indices = list(range(self.n_factors))
46
+ for pair in itertools.combinations(factor_indices, 2):
47
+ for levels in [(-1, -1), (-1, 1), (1, -1), (1, 1)]:
48
+ run = [0] * self.n_factors
49
+ run[pair[0]] = levels[0]
50
+ run[pair[1]] = levels[1]
51
+ design.append(run)
52
+
53
+ # Añadir corridas centrales
54
+ for _ in range(center_runs):
55
+ design.append([0] * self.n_factors)
56
+
57
+ design_df = pd.DataFrame(design, columns=self.factor_names)
58
+ self.design = design_df
59
+
60
+ # Mapear niveles codificados a naturales
61
+ for i, factor in enumerate(self.factor_names):
62
+ design_df[factor] = design_df[factor].apply(lambda x: self.coded_to_natural(x, i))
63
+
64
+ # Asignar al atributo data
65
+ self.data = design_df.copy()
66
+
67
+ return self.design
68
+
69
+ def coded_to_natural(self, coded_value, factor_index):
70
+ """
71
+ Convierte un valor codificado (-1, 0, 1) a su valor natural basado en los niveles del factor.
72
+ """
73
+ min_val = self.factor_levels[factor_index]['min']
74
+ max_val = self.factor_levels[factor_index]['max']
75
+ return min_val + (coded_value + 1) * (max_val - min_val) / 2
76
+
77
+ def natural_to_coded(self, natural_value, factor_index):
78
+ """
79
+ Convierte un valor natural a su valor codificado (-1, 0, 1) basado en los niveles del factor.
80
+ """
81
+ min_val = self.factor_levels[factor_index]['min']
82
+ max_val = self.factor_levels[factor_index]['max']
83
+ return -1 + 2 * (natural_value - min_val) / (max_val - min_val)
84
+
85
+ def set_response(self, response_values):
86
+ """
87
+ Establece los valores de respuesta (variable dependiente) en el diseño.
88
+ """
89
+ if len(response_values) != len(self.design):
90
+ raise ValueError("El número de valores de respuesta no coincide con el número de corridas en el diseño.")
91
+ self.data[self.y_name] = response_values
92
 
93
  def fit_model(self):
94
  """
95
  Ajusta el modelo de segundo orden completo a los datos.
96
  """
97
+ formula = self._generate_formula()
 
 
98
  self.model = smf.ols(formula, data=self.data).fit()
99
  print("Modelo Completo:")
100
  print(self.model.summary())
 
102
 
103
  def fit_simplified_model(self):
104
  """
105
+ Ajusta el modelo de segundo orden simplificado a los datos, eliminando términos no significativos.
106
  """
107
+ formula = self._generate_formula(simplified=True)
 
108
  self.model_simplified = smf.ols(formula, data=self.data).fit()
109
  print("\nModelo Simplificado:")
110
  print(self.model_simplified.summary())
111
  return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Modelo Simplificado")
112
 
113
+ def _generate_formula(self, simplified=False):
114
+ """
115
+ Genera la fórmula del modelo según el número de factores y si es simplificado.
116
+ """
117
+ terms = self.factor_names.copy()
118
+ if simplified:
119
+ # Añadir términos cuadráticos
120
+ terms += [f"I({var}**2)" for var in self.factor_names]
121
+ else:
122
+ # Añadir términos cuadráticos e interacciones
123
+ terms += [f"I({var}**2)" for var in self.factor_names]
124
+ terms += [f"{var1}:{var2}" for var1, var2 in itertools.combinations(self.factor_names, 2)]
125
+ formula = f"{self.y_name} ~ " + " + ".join(terms)
126
+ return formula
127
+
128
  def optimize(self, method='Nelder-Mead'):
129
  """
130
  Encuentra los niveles óptimos de los factores para maximizar la respuesta usando el modelo simplificado.
 
134
  return
135
 
136
  def objective_function(x):
137
+ # Convertir los niveles codificados a naturales
138
+ natural_values = [self.coded_to_natural(xi, i) for i, xi in enumerate(x)]
139
+ # Crear un DataFrame para la predicción
140
+ prediction_df = pd.DataFrame([natural_values], columns=self.factor_names)
141
+ # Convertir naturales a codificados
142
+ for i in range(self.n_factors):
143
+ prediction_df[self.factor_names[i]] = prediction_df[self.factor_names[i]].apply(lambda val: self.natural_to_coded(val, i))
144
+ # Predecir la respuesta
145
+ return -self.model_simplified.predict(prediction_df)[0]
146
+
147
+ # Definir límites en los niveles codificados (-1, 1)
148
+ bounds = [(-1, 1)] * self.n_factors
149
+ x0 = [0] * self.n_factors # Punto inicial en el centro
150
 
151
  self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
152
  self.optimal_levels = self.optimized_results.x
153
 
154
  # Convertir niveles óptimos de codificados a naturales
155
  optimal_levels_natural = [
156
+ self.coded_to_natural(xi, i) for i, xi in enumerate(self.optimal_levels)
 
 
157
  ]
158
+
159
  # Crear la tabla de optimización
160
  optimization_table = pd.DataFrame({
161
+ 'Variable': self.factor_names,
162
  'Nivel Óptimo (Natural)': optimal_levels_natural,
163
  'Nivel Óptimo (Codificado)': self.optimal_levels
164
  })
165
 
166
+ return optimization_table.round(3)
167
 
168
+ def generate_all_plots(self):
169
  """
170
+ Genera todas las gráficas de RSM, variando la variable fija y sus niveles usando el modelo simplificado.
171
+ Almacena las figuras en self.all_figures.
172
  """
173
  if self.model_simplified is None:
174
  print("Error: Ajusta el modelo simplificado primero.")
175
+ return
176
+
177
+ self.all_figures = [] # Resetear la lista de figuras
178
+
179
+ # Obtener las combinaciones de factores para gráficos
180
+ for fixed_index, fixed_variable in enumerate(self.factor_names):
181
+ for level in self.factor_levels[fixed_index]['levels']:
182
+ fig = self.plot_rsm_individual(fixed_variable, level)
183
+ if fig is not None:
184
+ self.all_figures.append(fig)
185
+
186
+ def plot_rsm_individual(self, fixed_variable, fixed_level):
187
+ """
188
+ Genera un gráfico de superficie de respuesta (RSM) individual para una configuración específica.
189
+ """
190
+ # Determinar las variables que varían
191
+ varying_variables = [var for var in self.factor_names if var != fixed_variable]
192
+ if len(varying_variables) < 2:
193
+ print("Se requieren al menos dos variables que varían para generar el gráfico.")
194
  return None
195
 
196
+ var1, var2 = varying_variables[:2] # Seleccionar las dos primeras variables que varían
197
+
198
+ # Niveles naturales para las variables que varían
199
+ var1_levels = self.factor_levels[self.factor_names.index(var1)]['levels']
200
+ var2_levels = self.factor_levels[self.factor_names.index(var2)]['levels']
 
201
 
202
  # Crear una malla de puntos para las variables que varían (en unidades naturales)
203
+ x_range = np.linspace(min(var1_levels), max(var1_levels), 100)
204
+ y_range = np.linspace(min(var2_levels), max(var2_levels), 100)
205
+ x_grid, y_grid = np.meshgrid(x_range, y_range)
206
 
207
  # Convertir la malla de variables naturales a codificadas
208
+ x_coded = np.array([self.natural_to_coded(x, self.factor_names.index(var1)) for x in x_grid.flatten()]).reshape(x_grid.shape)
209
+ y_coded = np.array([self.natural_to_coded(y, self.factor_names.index(var2)) for y in y_grid.flatten()]).reshape(y_grid.shape)
210
 
211
  # Crear un DataFrame para la predicción con variables codificadas
212
  prediction_data = pd.DataFrame({
213
+ var1: x_coded.flatten(),
214
+ var2: y_coded.flatten(),
215
+ fixed_variable: self.natural_to_coded(fixed_level, self.factor_names.index(fixed_variable))
216
  })
 
217
 
218
+ # Añadir las demás variables a 0 (centro)
219
+ for var in self.factor_names:
220
+ if var not in [var1, var2, fixed_variable]:
221
+ prediction_data[var] = 0
222
 
223
+ # Reordenar las columnas
224
+ prediction_data = prediction_data[self.factor_names]
 
225
 
226
+ # Calcular los valores predichos
227
+ z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid.shape)
 
 
 
 
228
 
229
+ # Crear el gráfico de superficie
230
+ fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid, y=y_grid, colorscale='Viridis', opacity=0.7, showscale=True)])
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
231
 
232
+ # Añadir puntos de los experimentos
233
+ experiments = self.data.copy()
234
+ experiments = experiments[(experiments[fixed_variable] == fixed_level)]
235
  fig.add_trace(go.Scatter3d(
236
+ x=experiments[var1],
237
+ y=experiments[var2],
238
+ z=experiments[self.y_name],
239
  mode='markers+text',
240
+ marker=dict(size=5, color='red'),
241
+ text=[f"{val:.2f}" for val in experiments[self.y_name]],
242
  textposition='top center',
243
  name='Experimentos'
244
  ))
245
 
246
+ # Actualizar layout
247
  fig.update_layout(
248
  scene=dict(
249
+ xaxis_title=f"{var1} ({self.get_units(var1)})",
250
+ yaxis_title=f"{var2} ({self.get_units(var2)})",
251
  zaxis_title=self.y_name,
252
  ),
253
+ title=f"{self.y_name} vs {var1} y {var2}<br><sup>{fixed_variable} fijo en {fixed_level} ({self.get_units(fixed_variable)})</sup>",
254
  height=800,
255
  width=1000,
256
  showlegend=True
257
  )
258
+
259
  return fig
260
 
261
  def get_units(self, variable_name):
 
267
  'Glucosa': 'g/L',
268
  'Extracto_de_Levadura': 'g/L',
269
  'Triptofano': 'g/L',
270
+ 'AIA_ppm': 'ppm',
271
+ # Agrega más unidades según tus variables
272
  }
273
  return units.get(variable_name, '')
274
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
275
  def pareto_chart(self, model, title):
276
  """
277
  Genera un diagrama de Pareto para los efectos estandarizados de un modelo,
278
  incluyendo la línea de significancia.
279
  """
280
  # Calcular los efectos estandarizados
281
+ tvalues = model.tvalues.drop('Intercept')
282
+ abs_tvalues = tvalues.abs()
283
+ sorted_idx = abs_tvalues.sort_values(ascending=False).index
284
  sorted_tvalues = abs_tvalues[sorted_idx]
285
+ sorted_names = sorted_idx
286
 
287
  # Calcular el valor crítico de t para la línea de significancia
288
  alpha = 0.05 # Nivel de significancia
 
306
 
307
  return fig
308
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
309
  def generate_prediction_table(self):
310
  """
311
  Genera una tabla con los valores actuales, predichos y residuales.
 
344
  for index, row in anova_table.iterrows():
345
  if index != 'Residual':
346
  factor_name = index
347
+ if 'I(' in factor_name:
348
+ factor_name = factor_name.replace('I(', '').replace(')', '').replace('** 2', '^2')
 
 
 
 
349
 
350
  ss_factor = row['sum_sq']
351
  contribution_percentage = (ss_factor / ss_total) * 100
 
368
 
369
  # --- ANOVA detallada ---
370
  # 1. Ajustar un modelo solo con los términos de primer orden y cuadráticos
371
+ formula_reduced = self._generate_formula(simplified=True)
 
372
  model_reduced = smf.ols(formula_reduced, data=self.data).fit()
373
 
374
  # 2. ANOVA del modelo reducido (para obtener la suma de cuadrados de la regresión)
 
381
  df_total = len(self.data) - 1
382
 
383
  # 5. Suma de cuadrados de la regresión
384
+ ss_regression = anova_reduced['sum_sq'].drop('Residual').sum()
385
 
386
  # 6. Grados de libertad de la regresión
387
  df_regression = len(anova_reduced) - 1
 
391
  df_residual = self.model_simplified.df_resid
392
 
393
  # 8. Suma de cuadrados del error puro (se calcula a partir de las réplicas)
394
+ # Para simplificar, asumimos que no hay réplicas (no hay corridas duplicadas)
395
+ ss_pure_error = np.nan
396
+ df_pure_error = np.nan
 
 
 
 
397
 
398
  # 9. Suma de cuadrados de la falta de ajuste
399
  ss_lack_of_fit = ss_residual - ss_pure_error if not np.isnan(ss_pure_error) else np.nan
 
420
  })
421
 
422
  # Calcular la suma de cuadrados y grados de libertad para la curvatura
423
+ ss_curvature = anova_reduced['sum_sq'].get(f'I({self.factor_names[0]}**2)', 0) + \
424
+ anova_reduced['sum_sq'].get(f'I({self.factor_names[1]}**2)', 0) + \
425
+ anova_reduced['sum_sq'].get(f'I({self.factor_names[2]}**2)', 0)
426
  df_curvature = 3
427
 
428
  # Añadir la fila de curvatura a la tabla ANOVA
 
438
 
439
  def get_all_tables(self):
440
  """
441
+ Obtiene todas las tablas generadas para ser exportadas a Excel y Word.
442
  """
443
  prediction_table = self.generate_prediction_table()
444
  contribution_table = self.calculate_contribution_percentage()
 
566
 
567
  # --- Funciones para la Interfaz de Gradio ---
568
 
569
+ def load_data(n_factors, factor_details, y_name, example=False):
570
  """
571
+ Carga los datos del diseño Box-Behnken según el número de factores y sus detalles.
572
+ Si example=True, carga un ejemplo predefinido.
573
  """
574
  try:
575
+ if example:
576
+ # Ejemplo para 3 factores
577
+ if n_factors == 3:
578
+ factor_names = ['Glucosa', 'Extracto_de_Levadura', 'Triptofano']
579
+ factor_levels = [
580
+ {'min': 1.0, 'max': 5.5, 'levels': [1.0, 3.5, 5.5]},
581
+ {'min': 0.03, 'max': 0.3, 'levels': [0.03, 0.2, 0.3]},
582
+ {'min': 0.4, 'max': 0.9, 'levels': [0.4, 0.65, 0.9]}
583
+ ]
584
+ # Crear instancia
585
+ rsm = RSM_BoxBehnken(factor_names, factor_levels, y_name)
586
+ design = rsm.generate_box_behnken_design()
587
+ # Ejemplo de valores de respuesta
588
+ response_values = [166.594, 177.557, 127.261, 147.573, 188.883, 224.527, 190.238, 226.483, 195.550, 149.493, 187.683, 148.621, 278.951, 297.238, 280.896]
589
+ rsm.set_response(response_values)
590
+ return rsm, design
591
+ # Ejemplo para 4 factores
592
+ elif n_factors == 4:
593
+ factor_names = ['Glucosa', 'Extracto_de_Levadura', 'Triptofano', 'Tiempo']
594
+ factor_levels = [
595
+ {'min': 1.0, 'max': 5.5, 'levels': [1.0, 3.5, 5.5]},
596
+ {'min': 0.03, 'max': 0.3, 'levels': [0.03, 0.2, 0.3]},
597
+ {'min': 0.4, 'max': 0.9, 'levels': [0.4, 0.65, 0.9]},
598
+ {'min': 24, 'max': 72, 'levels': [24, 48, 72]}
599
+ ]
600
+ # Crear instancia
601
+ rsm = RSM_BoxBehnken(factor_names, factor_levels, y_name)
602
+ design = rsm.generate_box_behnken_design()
603
+ # Ejemplo de valores de respuesta (30 corridas para 4 factores)
604
+ response_values = [200 + np.random.normal(0, 10) for _ in range(len(design))]
605
+ rsm.set_response(response_values)
606
+ return rsm, design
607
+ else:
608
+ raise ValueError("Ejemplos solo disponibles para 3 y 4 factores.")
609
+ else:
610
+ # Cargar según la entrada del usuario
611
+ factor_names = [detail['name'] for detail in factor_details]
612
+ factor_levels = [{'min': detail['min'], 'max': detail['max'], 'levels': [detail['min'], (detail['min'] + detail['max']) / 2, detail['max']]} for detail in factor_details]
613
+ # Crear instancia
614
+ rsm = RSM_BoxBehnken(factor_names, factor_levels, y_name)
615
+ design = rsm.generate_box_behnken_design()
616
+ return rsm, design
617
  except Exception as e:
618
+ print(f"Error al cargar los datos: {str(e)}")
619
+ return None, None
620
+
621
+ def fit_and_optimize(rsm, response_values):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
622
  """
623
+ Ajusta los modelos, realiza la optimización y genera todas las tablas y gráficos.
624
  """
625
+ try:
626
+ rsm.set_response(response_values)
627
+ model_completo, pareto_completo = rsm.fit_model()
628
+ model_simplificado, pareto_simplificado = rsm.fit_simplified_model()
629
+ optimization_table = rsm.optimize()
630
+ equation = rsm.get_simplified_equation()
631
+ prediction_table = rsm.generate_prediction_table()
632
+ contribution_table = rsm.calculate_contribution_percentage()
633
+ anova_table = rsm.calculate_detailed_anova()
634
+
635
+ # Generar todas las figuras y almacenarlas
636
+ rsm.generate_all_plots()
637
+
638
+ # Formatear la ecuación para que se vea mejor en Markdown
639
+ equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** 2", "^2").replace("*", " × ")
640
+ equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}"
641
+
642
+ # Guardar las tablas en Excel temporal
643
+ excel_path = rsm.save_tables_to_excel()
644
+
645
+ # Guardar todas las figuras en un ZIP temporal
646
+ zip_path = rsm.save_figures_to_zip()
647
+
648
+ # Preparar las tablas para exportación
649
+ tables_dict = rsm.get_all_tables()
650
+
651
+ return (
652
+ model_completo.summary().as_html(),
653
+ pareto_completo,
654
+ model_simplificado.summary().as_html(),
655
+ pareto_simplificado,
656
+ equation_formatted,
657
+ optimization_table,
658
+ prediction_table,
659
+ contribution_table,
660
+ anova_table,
661
+ zip_path, # Ruta del ZIP de gráficos
662
+ excel_path, # Ruta del Excel de tablas
663
+ tables_dict # Diccionario de tablas para Word
664
+ )
665
+ except Exception as e:
666
+ print(f"Error en el análisis: {str(e)}")
667
+ return [None]*12
668
+
669
+ def export_word(rsm_instance, tables_dict):
670
  """
671
+ Exporta las tablas a un documento de Word y retorna la ruta del archivo.
672
  """
673
+ try:
674
+ word_path = rsm_instance.export_tables_to_word(tables_dict)
675
+ if word_path and os.path.exists(word_path):
676
+ return word_path
677
  return None
678
+ except Exception as e:
679
+ print(f"Error al exportar a Word: {str(e)}")
 
 
 
 
 
 
 
 
 
 
680
  return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
681
 
682
  # --- Crear la interfaz de Gradio ---
683
 
684
  def create_gradio_interface():
685
  with gr.Blocks() as demo:
686
+ gr.Markdown("# 📊 Optimización de la Producción de AIA usando Diseño Box-Behnken")
687
+ gr.Markdown("""
688
+ Esta aplicación te permite generar diseños Box-Behnken con un número variable de factores (mínimo 3, máximo 6), ajustar modelos de respuesta, realizar optimización y exportar los resultados a Excel y Word.
689
+ """)
690
+
691
+ with gr.Tab("🔧 Configuración"):
692
+ with gr.Row():
693
+ n_factors_input = gr.Slider(
694
+ minimum=3,
695
+ maximum=6,
696
+ step=1,
697
+ value=3,
698
+ label="Número de Factores",
699
+ interactive=True
700
+ )
701
+ load_example_checkbox = gr.Checkbox(
702
+ label="Cargar Ejemplo",
703
+ value=False
704
+ )
705
+
706
+ with gr.Row():
707
+ with gr.Column():
708
+ # Factores dinámicos (hasta 6)
709
+ factor_inputs = []
710
+ for i in range(6):
711
+ with gr.Row():
712
+ factor_name = gr.Textbox(label=f"Factor {i+1} Nombre", placeholder=f"Nombre del Factor {i+1}")
713
+ factor_min = gr.Number(label=f"Factor {i+1} Min", value=0.0)
714
+ factor_max = gr.Number(label=f"Factor {i+1} Max", value=1.0)
715
+ factor_inputs.append({'name': factor_name, 'min': factor_min, 'max': factor_max})
716
+
717
+ # Variable dependiente
718
+ y_name_input = gr.Textbox(label="Variable Dependiente (Ej. AIA_ppm)", value="AIA_ppm")
719
 
720
+ with gr.Row():
721
+ load_button = gr.Button("🔄 Generar Diseño")
 
722
 
723
+ with gr.Tab("📊 Datos del Experimento"):
724
+ gr.Markdown("### Diseño Box-Behnken")
725
+ design_output = gr.Dataframe(
726
+ headers=None,
727
+ label="Diseño Generado (Completa y Rellena la Columna de Respuestas)",
728
+ interactive=True
729
+ )
730
+ submit_response_button = gr.Button(" Enviar Respuestas")
731
+
732
+ with gr.Tab("📈 Análisis y Reporte"):
733
+ with gr.Row():
734
+ with gr.Column():
735
+ gr.Markdown("**Modelo Completo**")
736
+ model_completo_output = gr.HTML()
737
+ pareto_completo_output = gr.Plot()
738
+
739
+ gr.Markdown("**Modelo Simplificado**")
740
+ model_simplificado_output = gr.HTML()
741
+ pareto_simplificado_output = gr.Plot()
742
+
743
+ gr.Markdown("**Ecuación del Modelo Simplificado**")
744
+ equation_output = gr.HTML()
745
+
746
+ gr.Markdown("**Tabla de Optimización**")
747
+ optimization_table_output = gr.Dataframe(label="Tabla de Optimización", interactive=False)
748
+
749
+ gr.Markdown("**Tabla de Predicciones**")
750
+ prediction_table_output = gr.Dataframe(label="Tabla de Predicciones", interactive=False)
751
+
752
+ gr.Markdown("**Tabla de % de Contribución**")
753
+ contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución", interactive=False)
754
+
755
+ gr.Markdown("**Tabla ANOVA Detallada**")
756
+ anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada", interactive=False)
757
+
758
+ gr.Markdown("## Descargar Tablas")
759
+ download_excel_button = gr.DownloadButton("💾 Descargar Tablas en Excel")
760
+ download_word_button = gr.DownloadButton("📄 Descargar Tablas en Word")
761
+
762
+ with gr.Column():
763
+ gr.Markdown("**Gráficos de Superficie de Respuesta**")
764
+ rsm_plot_output = gr.Plot()
765
+ plot_info = gr.Textbox(label="Información del Gráfico", value="Gráfico 1 de N", interactive=False)
766
+ with gr.Row():
767
+ left_button = gr.Button("<")
768
+ right_button = gr.Button(">")
769
+ with gr.Row():
770
+ download_plot_button = gr.DownloadButton("💾 Descargar Gráfico Actual (PNG)")
771
+ download_all_plots_button = gr.DownloadButton("💾 Descargar Todos los Gráficos (ZIP)")
772
 
773
+ with gr.Row():
774
+ copiar_btn = gr.Button("📋 Copiar Informe", variant="secondary")
775
+ exportar_word_btn = gr.Button("💾 Exportar Informe Word", variant="primary")
776
+ exportar_excel_btn = gr.Button("💾 Exportar Informe Excel", variant="primary")
777
+
778
+ # --- Funciones de la Interfaz ---
779
+
780
+ def handle_load_design(n_factors, factor_details, y_name, load_example):
781
+ """
782
+ Genera el diseño Box-Behnken según la configuración o carga un ejemplo.
783
+ """
784
+ if load_example:
785
+ # Cargar ejemplos predefinidos
786
+ if n_factors == 3:
787
+ factor_names = ['Glucosa', 'Extracto_de_Levadura', 'Triptofano']
788
+ factor_levels = [
789
+ {'min': 1.0, 'max': 5.5, 'levels': [1.0, 3.5, 5.5]},
790
+ {'min': 0.03, 'max': 0.3, 'levels': [0.03, 0.2, 0.3]},
791
+ {'min': 0.4, 'max': 0.9, 'levels': [0.4, 0.65, 0.9]}
792
+ ]
793
+ y_name = 'AIA_ppm'
794
+ # Crear instancia
795
+ rsm = RSM_BoxBehnken(factor_names, factor_levels, y_name)
796
+ design = rsm.generate_box_behnken_design()
797
+ # Ejemplo de valores de respuesta
798
+ response_values = [166.594, 177.557, 127.261, 147.573, 188.883, 224.527, 190.238, 226.483, 195.550, 149.493, 187.683, 148.621, 278.951, 297.238, 280.896]
799
+ rsm.set_response(response_values)
800
+ return rsm, design
801
+ elif n_factors == 4:
802
+ factor_names = ['Glucosa', 'Extracto_de_Levadura', 'Triptofano', 'Tiempo']
803
+ factor_levels = [
804
+ {'min': 1.0, 'max': 5.5, 'levels': [1.0, 3.5, 5.5]},
805
+ {'min': 0.03, 'max': 0.3, 'levels': [0.03, 0.2, 0.3]},
806
+ {'min': 0.4, 'max': 0.9, 'levels': [0.4, 0.65, 0.9]},
807
+ {'min': 24, 'max': 72, 'levels': [24, 48, 72]}
808
+ ]
809
+ y_name = 'AIA_ppm'
810
+ # Crear instancia
811
+ rsm = RSM_BoxBehnken(factor_names, factor_levels, y_name)
812
+ design = rsm.generate_box_behnken_design()
813
+ # Ejemplo de valores de respuesta (30 corridas para 4 factores)
814
+ response_values = [200 + np.random.normal(0, 10) for _ in range(len(design))]
815
+ rsm.set_response(response_values)
816
+ return rsm, design
817
+ else:
818
+ raise ValueError("Ejemplos solo disponibles para 3 y 4 factores.")
819
+ else:
820
+ # Cargar según la entrada del usuario
821
+ factor_names = [detail['name'] for detail in factor_details]
822
+ factor_levels = [{'min': detail['min'], 'max': detail['max'], 'levels': [detail['min'], (detail['min'] + detail['max']) / 2, detail['max']]} for detail in factor_details]
823
+ # Crear instancia
824
+ rsm = RSM_BoxBehnken(factor_names, factor_levels, y_name)
825
+ design = rsm.generate_box_behnken_design()
826
+ return rsm, design
827
+
828
+ def prepare_download_files(excel_path, zip_path):
829
+ """
830
+ Prepara los archivos para descarga.
831
+ """
832
+ return excel_path, zip_path
833
+
834
+ # Cargar Diseño
835
  load_button.click(
836
+ fn=handle_load_design,
837
+ inputs=[gr.Slider,
838
+ [gr.Row().components for gr.Row in gr.Blocks().__class__],
839
+ y_name_input,
840
+ load_example_checkbox],
841
+ outputs=[gr.State(), design_output]
842
  )
843
+
844
+ # Enviar Respuestas
845
+ submit_response_button.click(
846
+ fn=fit_and_optimize,
847
+ inputs=[gr.State(), design_output],
848
  outputs=[
849
  model_completo_output,
850
  pareto_completo_output,
 
855
  prediction_table_output,
856
  contribution_table_output,
857
  anova_table_output,
858
+ download_all_plots_button,
859
+ download_excel_button,
860
+ tables_dict_output := gr.State()
861
  ]
862
  )
863
+
864
+ # Descargar Tablas en Excel
865
+ download_excel_button.click(
866
+ fn=lambda excel_path: excel_path,
867
+ inputs=[download_excel_button],
868
+ outputs=[download_excel_button]
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
869
  )
870
+
871
+ # Descargar Todos los Gráficos en ZIP
872
  download_all_plots_button.click(
873
+ fn=lambda zip_path: zip_path,
874
+ inputs=[download_all_plots_button],
875
+ outputs=[download_all_plots_button]
 
 
 
 
 
 
 
876
  )
877
+
878
+ # Descargar Tablas en Word
879
  download_word_button.click(
880
+ fn=lambda rsm_instance, tables_dict: export_word(rsm_instance, tables_dict),
881
+ inputs=[gr.State(), gr.State()],
882
+ outputs=[download_word_button]
883
  )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
884
 
885
+ return demo
886
 
887
  # --- Función Principal ---
888