C2MV commited on
Commit
518472b
·
verified ·
1 Parent(s): 911746c

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +278 -279
app.py CHANGED
@@ -11,199 +11,204 @@ import gradio as gr
11
  import io
12
  import os
13
  from zipfile import ZipFile
 
 
 
 
 
14
 
15
  class RSM_BoxBehnken:
16
  def __init__(self, data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
17
  self.data = data.copy()
18
  self.model = None
19
  self.model_simplified = None
20
  self.optimized_results = None
21
  self.optimal_levels = None
22
 
 
23
  self.x1_name = x1_name
24
  self.x2_name = x2_name
25
  self.x3_name = x3_name
26
  self.y_name = y_name
27
 
28
- # Niveles originales de las variables
29
  self.x1_levels = x1_levels
30
  self.x2_levels = x2_levels
31
  self.x3_levels = x3_levels
32
 
33
- def get_levels(self, variable_name):
34
- if variable_name == self.x1_name:
35
- return self.x1_levels
36
- elif variable_name == self.x2_name:
37
- return self.x2_levels
38
- elif variable_name == self.x3_name:
39
- return self.x3_levels
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
40
  else:
41
- raise ValueError(f"Variable desconocida: {variable_name}")
42
-
43
- def fit_model(self):
44
- formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
45
- f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2) + ' \
46
- f'{self.x1_name}:{self.x2_name} + {self.x1_name}:{self.x3_name} + {self.x2_name}:{self.x3_name}'
47
- self.model = smf.ols(formula, data=self.data).fit()
48
- print("Modelo Completo:")
49
- print(self.model.summary())
50
- return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
51
-
52
- def fit_simplified_model(self):
53
- formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + ' \
54
- f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
55
- self.model_simplified = smf.ols(formula, data=self.data).fit()
56
- print("\nModelo Simplificado:")
57
- print(self.model_simplified.summary())
58
- return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Modelo Simplificado")
59
 
60
  def optimize(self, method='Nelder-Mead'):
 
 
 
 
 
 
 
 
 
 
 
 
 
61
  if self.model_simplified is None:
62
- print("Error: Ajusta el modelo simplificado primero.")
63
- return
64
 
65
  def objective_function(x):
66
- return -self.model_simplified.predict(pd.DataFrame({self.x1_name: [x[0]], self.x2_name: [x[1]], self.x3_name: [x[2]]}))
 
 
 
 
 
67
 
68
  bounds = [(-1, 1), (-1, 1), (-1, 1)]
69
  x0 = [0, 0, 0]
70
 
71
- self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
 
 
 
 
 
72
  self.optimal_levels = self.optimized_results.x
73
 
 
74
  optimal_levels_natural = [
75
- round(self.coded_to_natural(self.optimal_levels[0], self.x1_name), 3),
76
- round(self.coded_to_natural(self.optimal_levels[1], self.x2_name), 3),
77
- round(self.coded_to_natural(self.optimal_levels[2], self.x3_name), 3)
78
  ]
 
79
  optimization_table = pd.DataFrame({
80
  'Variable': [self.x1_name, self.x2_name, self.x3_name],
81
- 'Nivel Óptimo (Natural)': optimal_levels_natural,
82
- 'Nivel Óptimo (Codificado)': [round(x, 3) for x in self.optimal_levels]
83
  })
84
 
85
  return optimization_table
86
 
87
- def plot_rsm_individual(self, fixed_variable, fixed_level):
88
- if self.model_simplified is None:
89
- print("Error: Ajusta el modelo simplificado primero.")
90
- return None
91
-
92
- varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
93
 
94
- x_natural_levels = self.get_levels(varying_variables[0])
95
- y_natural_levels = self.get_levels(varying_variables[1])
96
-
97
- x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
98
- y_range_natural = np.linspace(y_natural_levels[0], y_natural_levels[-1], 100)
99
- x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
100
-
101
- x_grid_coded = self.natural_to_coded(x_grid_natural, varying_variables[0])
102
- y_grid_coded = self.natural_to_coded(y_grid_natural, varying_variables[1])
103
-
104
- prediction_data = pd.DataFrame({
105
- varying_variables[0]: x_grid_coded.flatten(),
106
- varying_variables[1]: y_grid_coded.flatten(),
107
- })
108
- prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
109
-
110
- z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape)
111
-
112
- varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
113
-
114
- fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable)
115
- subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)]
116
-
117
- valid_levels = [-1, 0, 1]
118
- experiments_data = subset_data[
119
- subset_data[varying_variables[0]].isin(valid_levels) &
120
- subset_data[varying_variables[1]].isin(valid_levels)
121
- ]
122
-
123
- experiments_x_natural = experiments_data[varying_variables[0]].apply(lambda x: self.coded_to_natural(x, varying_variables[0]))
124
- experiments_y_natural = experiments_data[varying_variables[1]].apply(lambda x: self.coded_to_natural(x, varying_variables[1]))
125
-
126
- fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)])
127
-
128
- for i in range(x_grid_natural.shape[0]):
129
- fig.add_trace(go.Scatter3d(
130
- x=x_grid_natural[i, :],
131
- y=y_grid_natural[i, :],
132
- z=z_pred[i, :],
133
- mode='lines',
134
- line=dict(color='gray', width=2),
135
- showlegend=False,
136
- hoverinfo='skip'
137
- ))
138
- for j in range(x_grid_natural.shape[1]):
139
- fig.add_trace(go.Scatter3d(
140
- x=x_grid_natural[:, j],
141
- y=y_grid_natural[:, j],
142
- z=z_pred[:, j],
143
- mode='lines',
144
- line=dict(color='gray', width=2),
145
- showlegend=False,
146
- hoverinfo='skip'
147
- ))
148
-
149
- colors = ['red', 'blue', 'green', 'purple', 'orange', 'yellow', 'cyan', 'magenta']
150
- point_labels = []
151
- for i, row in experiments_data.iterrows():
152
- point_labels.append(f"{row[self.y_name]:.2f}")
153
-
154
- fig.add_trace(go.Scatter3d(
155
- x=experiments_x_natural,
156
- y=experiments_y_natural,
157
- z=experiments_data[self.y_name],
158
- mode='markers+text',
159
- marker=dict(size=4, color=colors[:len(experiments_x_natural)]),
160
- text=point_labels,
161
- textposition='top center',
162
- name='Experimentos'
163
- ))
164
-
165
- fig.update_layout(
166
- scene=dict(
167
- xaxis_title=varying_variables[0] + " (g/L)",
168
- yaxis_title=varying_variables[1] + " (g/L)",
169
- zaxis_title=self.y_name,
170
- ),
171
- title=f"{self.y_name} vs {varying_variables[0]} y {varying_variables[1]}<br><sup>{fixed_variable} fijo en {fixed_level:.2f} (g/L) (Modelo Simplificado)</sup>",
172
- height=800,
173
- width=1000,
174
- showlegend=True
175
- )
176
- return fig
177
-
178
- def generate_all_plots(self):
179
- if self.model_simplified is None:
180
- print("Error: Ajusta el modelo simplificado primero.")
181
- return
182
-
183
- levels_to_plot_natural = {
184
- self.x1_name: self.x1_levels,
185
- self.x2_name: self.x2_levels,
186
- self.x3_name: self.x3_levels
187
- }
188
 
189
- figs = []
190
-
191
- for fixed_variable in [self.x1_name, self.x2_name, self.x3_name]:
192
- for level in levels_to_plot_natural[fixed_variable]:
193
- fig = self.plot_rsm_individual(fixed_variable, level)
194
- if fig is not None:
195
- figs.append(fig)
196
- return figs
197
-
198
- def coded_to_natural(self, coded_value, variable_name):
199
- levels = self.get_levels(variable_name)
200
  return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2
201
 
202
  def natural_to_coded(self, natural_value, variable_name):
203
- levels = self.get_levels(variable_name)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
204
  return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
205
 
206
  def pareto_chart(self, model, title):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
207
  tvalues = model.tvalues[1:]
208
  abs_tvalues = np.abs(tvalues)
209
  sorted_idx = np.argsort(abs_tvalues)[::-1]
@@ -218,121 +223,107 @@ class RSM_BoxBehnken:
218
  x=sorted_tvalues,
219
  y=sorted_names,
220
  orientation='h',
221
- labels={'x': 'Efecto Estandarizado', 'y': 'Término'},
222
  title=title
223
  )
224
  fig.update_yaxes(autorange="reversed")
225
-
226
  fig.add_vline(x=t_critical, line_dash="dot",
227
- annotation_text=f"t crítico = {t_critical:.2f}",
228
  annotation_position="bottom right")
229
 
230
  return fig
231
 
232
- def get_simplified_equation(self):
233
- if self.model_simplified is None:
234
- print("Error: Ajusta el modelo simplificado primero.")
235
- return None
236
-
237
- coefficients = self.model_simplified.params
238
- equation = f"{self.y_name} = {coefficients['Intercept']:.3f}"
239
-
240
- for term, coef in coefficients.items():
241
- if term != 'Intercept':
242
- if term == f'{self.x1_name}':
243
- equation += f" + {coef:.3f}*{self.x1_name}"
244
- elif term == f'{self.x2_name}':
245
- equation += f" + {coef:.3f}*{self.x2_name}"
246
- elif term == f'{self.x3_name}':
247
- equation += f" + {coef:.3f}*{self.x3_name}"
248
- elif term == f'I({self.x1_name} ** 2)':
249
- equation += f" + {coef:.3f}*{self.x1_name}^2"
250
- elif term == f'I({self.x2_name} ** 2)':
251
- equation += f" + {coef:.3f}*{self.x2_name}^2"
252
- elif term == f'I({self.x3_name} ** 2)':
253
- equation += f" + {coef:.3f}*{self.x3_name}^2"
254
-
255
- return equation
256
-
257
  def generate_prediction_table(self):
258
- if self.model_simplified is None:
259
- print("Error: Ajusta el modelo simplificado primero.")
260
- return None
 
 
 
 
 
 
 
261
 
262
- self.data['Predicho'] = self.model_simplified.predict(self.data)
263
- self.data['Residual'] = self.data[self.y_name] - self.data['Predicho']
264
 
265
- prediction_table = self.data[[self.y_name, 'Predicho', 'Residual']].copy()
266
- prediction_table[self.y_name] = prediction_table[self.y_name].round(3)
267
- prediction_table['Predicho'] = prediction_table['Predicho'].round(3)
268
- prediction_table['Residual'] = prediction_table['Residual'].round(3)
269
 
270
- return prediction_table
271
 
272
  def calculate_contribution_percentage(self):
273
- if self.model_simplified is None:
274
- print("Error: Ajusta el modelo simplificado primero.")
275
- return None
276
-
277
- anova_table = sm.stats.anova_lm(self.model_simplified, typ=2)
278
- ss_total = anova_table['sum_sq'].sum()
279
-
280
- contribution_table = pd.DataFrame({
281
- 'Factor': [],
282
- 'Suma de Cuadrados': [],
283
- '% Contribución': []
284
- })
285
-
286
- for index, row in anova_table.iterrows():
287
- if index != 'Residual':
288
- factor_name = index
289
- if factor_name == f'I({self.x1_name} ** 2)':
290
- factor_name = f'{self.x1_name}^2'
291
- elif factor_name == f'I({self.x2_name} ** 2)':
292
- factor_name = f'{self.x2_name}^2'
293
- elif factor_name == f'I({self.x3_name} ** 2)':
294
- factor_name = f'{self.x3_name}^2'
295
-
296
- ss_factor = row['sum_sq']
297
- contribution_percentage = (ss_factor / ss_total) * 100
298
 
299
- contribution_table = pd.concat([contribution_table, pd.DataFrame({
300
- 'Factor': [factor_name],
301
- 'Suma de Cuadrados': [round(ss_factor, 3)],
302
- '% Contribución': [round(contribution_percentage, 3)]
303
- })], ignore_index=True)
304
 
305
- return contribution_table
 
 
 
 
 
 
 
 
 
 
 
 
 
 
306
 
307
  def calculate_detailed_anova(self):
 
 
 
 
 
 
 
 
308
  if self.model_simplified is None:
309
- print("Error: Ajusta el modelo simplificado primero.")
310
- return None
311
 
 
 
 
 
 
312
  formula_reduced = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
313
  f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
314
  model_reduced = smf.ols(formula_reduced, data=self.data).fit()
315
-
316
  anova_reduced = sm.stats.anova_lm(model_reduced, typ=2)
317
 
318
- ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
319
-
320
- df_total = len(self.data) - 1
321
-
322
  ss_regression = anova_reduced['sum_sq'][:-1].sum()
323
-
324
  df_regression = len(anova_reduced) - 1
325
 
326
  ss_residual = self.model_simplified.ssr
327
  df_residual = self.model_simplified.df_resid
328
 
 
329
  replicas = self.data[self.data.duplicated(subset=[self.x1_name, self.x2_name, self.x3_name], keep=False)]
330
  ss_pure_error = replicas.groupby([self.x1_name, self.x2_name, self.x3_name])[self.y_name].var().sum()
331
  df_pure_error = len(replicas) - len(replicas.groupby([self.x1_name, self.x2_name, self.x3_name]))
332
 
 
333
  ss_lack_of_fit = ss_residual - ss_pure_error
334
  df_lack_of_fit = df_residual - df_pure_error
335
 
 
336
  ms_regression = ss_regression / df_regression
337
  ms_residual = ss_residual / df_residual
338
  ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit
@@ -341,26 +332,29 @@ class RSM_BoxBehnken:
341
  f_lack_of_fit = ms_lack_of_fit / ms_pure_error
342
  p_lack_of_fit = 1 - f.cdf(f_lack_of_fit, df_lack_of_fit, df_pure_error)
343
 
 
344
  detailed_anova_table = pd.DataFrame({
345
- 'Fuente de Variación': ['Regresión', 'Residual', 'Falta de Ajuste', 'Error Puro', 'Total'],
346
- 'Suma de Cuadrados': [round(ss_regression, 3), round(ss_residual, 3), round(ss_lack_of_fit, 3), round(ss_pure_error, 3), round(ss_total, 3)],
347
- 'Grados de Libertad': [df_regression, df_residual, df_lack_of_fit, df_pure_error, df_total],
348
- 'Cuadrado Medio': [round(ms_regression, 3), round(ms_residual, 3), round(ms_lack_of_fit, 3), round(ms_pure_error, 3), np.nan],
 
 
 
 
 
 
 
 
 
 
 
 
349
  'F': [np.nan, np.nan, round(f_lack_of_fit, 3), np.nan, np.nan],
350
- 'Valor p': [np.nan, np.nan, round(p_lack_of_fit, 3), np.nan, np.nan]
351
  })
352
-
353
- 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)']
354
- df_curvature = 3
355
-
356
- detailed_anova_table.loc[len(detailed_anova_table)] = ['Curvatura', round(ss_curvature, 3), df_curvature, round(ss_curvature / df_curvature, 3), np.nan, np.nan]
357
-
358
- detailed_anova_table = detailed_anova_table.reindex([0, 5, 1, 2, 3, 4])
359
-
360
- detailed_anova_table = detailed_anova_table.reset_index(drop=True)
361
 
362
  return detailed_anova_table
363
-
364
  # --- Funciones para la interfaz de Gradio ---
365
 
366
  def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str):
@@ -396,7 +390,7 @@ def fit_and_optimize_model():
396
  prediction_table = rsm.generate_prediction_table()
397
  contribution_table = rsm.calculate_contribution_percentage()
398
  anova_table = rsm.calculate_detailed_anova()
399
-
400
  equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ")
401
  equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}"
402
 
@@ -407,16 +401,19 @@ def generate_rsm_plot(fixed_variable, fixed_level):
407
  if 'rsm' not in globals():
408
  return None, "Error: Carga los datos primero."
409
 
410
- # Obtener todas las gráficas
411
  all_figs = rsm.generate_all_plots()
412
 
413
- # Convertir la figura seleccionada a bytes
414
- img_bytes = all_figs[0].to_image(format="png")
 
 
 
 
415
 
416
- # Devolver la lista de figuras y la imagen en bytes
417
- return all_figs, img_bytes
418
 
419
- # Función para descargar el Excel con todas las tablas
420
  def download_excel():
421
  if 'rsm' not in globals():
422
  return None, "Error: Carga los datos y ajusta el modelo primero."
@@ -425,31 +422,42 @@ def download_excel():
425
  with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
426
  rsm.data.to_excel(writer, sheet_name='Datos', index=False)
427
  rsm.generate_prediction_table().to_excel(writer, sheet_name='Predicciones', index=False)
428
- rsm.optimize().to_excel(writer, sheet_name='Optimización', index=False)
429
- rsm.calculate_contribution_percentage().to_excel(writer, sheet_name='Contribución', index=False)
430
  rsm.calculate_detailed_anova().to_excel(writer, sheet_name='ANOVA', index=False)
431
 
432
  output.seek(0)
433
-
434
- # Modificar para usar gr.File
435
- return gr.File(value=output, visible=True, filename="resultados_rsm.xlsx")
436
 
437
- # Función para descargar las imágenes
438
  def download_images():
439
  if 'rsm' not in globals():
440
  return None, "Error: Carga los datos y ajusta el modelo primero."
441
 
442
- zip_output = io.BytesIO()
443
- with ZipFile(zip_output, 'w') as zipf:
444
- for fixed_variable in [rsm.x1_name, rsm.x2_name, rsm.x3_name]:
445
- for level in rsm.get_levels(fixed_variable):
446
- fig = rsm.plot_rsm_individual(fixed_variable, level)
447
- img_bytes = fig.to_image(format="png")
448
- img_path = f"{fixed_variable}_{level}.png"
449
- zipf.writestr(img_path, img_bytes)
450
 
451
- zip_output.seek(0)
452
- return gr.File(value=zip_output, visible=True, filename="graficos_rsm.zip")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
453
 
454
  # --- Crear la interfaz de Gradio ---
455
 
@@ -492,6 +500,8 @@ with gr.Blocks() as demo:
492
  with gr.Row(visible=False) as analysis_row:
493
  with gr.Column():
494
  fit_button = gr.Button("Ajustar Modelo y Optimizar")
 
 
495
  gr.Markdown("**Modelo Completo**")
496
  model_completo_output = gr.HTML()
497
  pareto_completo_output = gr.Plot()
@@ -503,22 +513,12 @@ with gr.Blocks() as demo:
503
  prediction_table_output = gr.Dataframe(label="Tabla de Predicciones")
504
  contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución")
505
  anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada")
506
-
507
- # Botones de descarga
508
- with gr.Row():
509
- download_excel_button = gr.Button("Descargar Tablas en Excel")
510
- download_images_button = gr.Button("Descargar Gráficos en ZIP")
511
- excel_file_output = gr.File(label="Descargar Excel")
512
- zip_file_output = gr.File(label="Descargar ZIP")
513
-
514
  with gr.Column():
515
  gr.Markdown("## Generar Gráficos de Superficie de Respuesta")
516
  fixed_variable_input = gr.Dropdown(label="Variable Fija", choices=["Glucosa", "Extracto_de_Levadura", "Triptofano"], value="Glucosa")
517
  fixed_level_input = gr.Slider(label="Nivel de Variable Fija", minimum=0, maximum=1, step=0.01, value=0.5)
518
  plot_button = gr.Button("Generar Gráfico")
519
- # Usar Gallery para mostrar las imágenes
520
- gallery = gr.Gallery(label="Gráficos RSM").style(preview=False, grid=(3,3), height="auto")
521
- image_output = gr.Image(label="Descargar Gráfico")
522
 
523
  load_button.click(
524
  load_data,
@@ -528,11 +528,10 @@ with gr.Blocks() as demo:
528
 
529
  fit_button.click(fit_and_optimize_model, outputs=[model_completo_output, pareto_completo_output, model_simplificado_output, pareto_simplificado_output, equation_output, optimization_table_output, prediction_table_output, contribution_table_output, anova_table_output])
530
 
531
- plot_button.click(generate_rsm_plot, inputs=[fixed_variable_input, fixed_level_input], outputs=[gallery, image_output])
532
 
533
- # Asociar las funciones de descarga a los botones
534
- download_excel_button.click(download_excel, outputs=excel_file_output)
535
- download_images_button.click(download_images, outputs=zip_file_output)
536
 
537
  # Ejemplo de uso
538
  gr.Markdown("## Ejemplo de uso")
@@ -542,7 +541,7 @@ with gr.Blocks() as demo:
542
  gr.Markdown("4. Haz clic en 'Ajustar Modelo y Optimizar' para ajustar el modelo y encontrar los niveles óptimos de los factores.")
543
  gr.Markdown("5. Selecciona una variable fija y su nivel en los controles deslizantes.")
544
  gr.Markdown("6. Haz clic en 'Generar Gráfico' para generar un gráfico de superficie de respuesta.")
545
- gr.Markdown("7. Haz clic en 'Descargar Tablas en Excel' para obtener un archivo .xlsx con todas las tablas generadas.")
546
- gr.Markdown("8. Haz clic en 'Descargar Gráficos en ZIP' para obtener un archivo .zip con todas las imágenes de los gráficos.")
547
 
548
  demo.launch()
 
11
  import io
12
  import os
13
  from zipfile import ZipFile
14
+ import warnings
15
+
16
+ # Suppress specific warnings
17
+ warnings.filterwarnings('ignore', category=UserWarning)
18
+ warnings.filterwarnings('ignore', category=RuntimeWarning)
19
 
20
  class RSM_BoxBehnken:
21
  def __init__(self, data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels):
22
+ """
23
+ Initialize the Response Surface Methodology Box-Behnken Design class
24
+
25
+ Parameters:
26
+ -----------
27
+ data : pandas.DataFrame
28
+ Experimental design data
29
+ x1_name, x2_name, x3_name : str
30
+ Names of independent variables
31
+ y_name : str
32
+ Name of dependent variable
33
+ x1_levels, x2_levels, x3_levels : list
34
+ Levels of each independent variable
35
+ """
36
  self.data = data.copy()
37
  self.model = None
38
  self.model_simplified = None
39
  self.optimized_results = None
40
  self.optimal_levels = None
41
 
42
+ # Variable names
43
  self.x1_name = x1_name
44
  self.x2_name = x2_name
45
  self.x3_name = x3_name
46
  self.y_name = y_name
47
 
48
+ # Original levels of variables
49
  self.x1_levels = x1_levels
50
  self.x2_levels = x2_levels
51
  self.x3_levels = x3_levels
52
 
53
+ def _get_levels(self, variable_name):
54
+ """
55
+ Get levels for a specific variable
56
+
57
+ Parameters:
58
+ -----------
59
+ variable_name : str
60
+ Name of the variable
61
+
62
+ Returns:
63
+ --------
64
+ list
65
+ Levels of the variable
66
+ """
67
+ level_map = {
68
+ self.x1_name: self.x1_levels,
69
+ self.x2_name: self.x2_levels,
70
+ self.x3_name: self.x3_levels
71
+ }
72
+
73
+ if variable_name not in level_map:
74
+ raise ValueError(f"Unknown variable: {variable_name}")
75
+
76
+ return level_map[variable_name]
77
+
78
+ def fit_model(self, simplified=False):
79
+ """
80
+ Fit the response surface model
81
+
82
+ Parameters:
83
+ -----------
84
+ simplified : bool, optional
85
+ Whether to fit a simplified model, by default False
86
+
87
+ Returns:
88
+ --------
89
+ tuple
90
+ Fitted model and Pareto chart
91
+ """
92
+ if simplified:
93
+ formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
94
+ f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
95
+ self.model_simplified = smf.ols(formula, data=self.data).fit()
96
+ print("\nSimplified Model:")
97
+ print(self.model_simplified.summary())
98
+ return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Simplified Model")
99
  else:
100
+ formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
101
+ f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2) + ' \
102
+ f'{self.x1_name}:{self.x2_name} + {self.x1_name}:{self.x3_name} + {self.x2_name}:{self.x3_name}'
103
+ self.model = smf.ols(formula, data=self.data).fit()
104
+ print("Full Model:")
105
+ print(self.model.summary())
106
+ return self.model, self.pareto_chart(self.model, "Pareto - Full Model")
 
 
 
 
 
 
 
 
 
 
 
107
 
108
  def optimize(self, method='Nelder-Mead'):
109
+ """
110
+ Optimize the response surface model
111
+
112
+ Parameters:
113
+ -----------
114
+ method : str, optional
115
+ Optimization method, by default 'Nelder-Mead'
116
+
117
+ Returns:
118
+ --------
119
+ pandas.DataFrame
120
+ Optimization results table
121
+ """
122
  if self.model_simplified is None:
123
+ raise ValueError("Fit the simplified model first.")
 
124
 
125
  def objective_function(x):
126
+ """Objective function for optimization"""
127
+ return -self.model_simplified.predict(pd.DataFrame({
128
+ self.x1_name: [x[0]],
129
+ self.x2_name: [x[1]],
130
+ self.x3_name: [x[2]]
131
+ }))
132
 
133
  bounds = [(-1, 1), (-1, 1), (-1, 1)]
134
  x0 = [0, 0, 0]
135
 
136
+ self.optimized_results = minimize(
137
+ objective_function,
138
+ x0,
139
+ method=method,
140
+ bounds=bounds
141
+ )
142
  self.optimal_levels = self.optimized_results.x
143
 
144
+ # Convert to natural levels
145
  optimal_levels_natural = [
146
+ round(self.coded_to_natural(self.optimal_levels[i], var), 3)
147
+ for i, var in enumerate([self.x1_name, self.x2_name, self.x3_name])
 
148
  ]
149
+
150
  optimization_table = pd.DataFrame({
151
  'Variable': [self.x1_name, self.x2_name, self.x3_name],
152
+ 'Optimal Level (Natural)': optimal_levels_natural,
153
+ 'Optimal Level (Coded)': [round(x, 3) for x in self.optimal_levels]
154
  })
155
 
156
  return optimization_table
157
 
158
+ def coded_to_natural(self, coded_value, variable_name):
159
+ """
160
+ Convert coded value to natural level
 
 
 
161
 
162
+ Parameters:
163
+ -----------
164
+ coded_value : float
165
+ Coded value of the variable
166
+ variable_name : str
167
+ Name of the variable
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
168
 
169
+ Returns:
170
+ --------
171
+ float
172
+ Natural level of the variable
173
+ """
174
+ levels = self._get_levels(variable_name)
 
 
 
 
 
175
  return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2
176
 
177
  def natural_to_coded(self, natural_value, variable_name):
178
+ """
179
+ Convert natural level to coded value
180
+
181
+ Parameters:
182
+ -----------
183
+ natural_value : float
184
+ Natural level of the variable
185
+ variable_name : str
186
+ Name of the variable
187
+
188
+ Returns:
189
+ --------
190
+ float
191
+ Coded value of the variable
192
+ """
193
+ levels = self._get_levels(variable_name)
194
  return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
195
 
196
  def pareto_chart(self, model, title):
197
+ """
198
+ Create Pareto chart of standardized effects
199
+
200
+ Parameters:
201
+ -----------
202
+ model : statsmodels.regression.linear_model.RegressionResultsWrapper
203
+ Fitted regression model
204
+ title : str
205
+ Title of the Pareto chart
206
+
207
+ Returns:
208
+ --------
209
+ plotly.graph_objects.Figure
210
+ Pareto chart
211
+ """
212
  tvalues = model.tvalues[1:]
213
  abs_tvalues = np.abs(tvalues)
214
  sorted_idx = np.argsort(abs_tvalues)[::-1]
 
223
  x=sorted_tvalues,
224
  y=sorted_names,
225
  orientation='h',
226
+ labels={'x': 'Standardized Effect', 'y': 'Term'},
227
  title=title
228
  )
229
  fig.update_yaxes(autorange="reversed")
 
230
  fig.add_vline(x=t_critical, line_dash="dot",
231
+ annotation_text=f"Critical t = {t_critical:.2f}",
232
  annotation_position="bottom right")
233
 
234
  return fig
235
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
236
  def generate_prediction_table(self):
237
+ """
238
+ Generate prediction table with predicted and residual values
239
+
240
+ Returns:
241
+ --------
242
+ pandas.DataFrame
243
+ Prediction table
244
+ """
245
+ if self.model_simplified is None:
246
+ raise ValueError("Fit the simplified model first.")
247
 
248
+ predictions = self.model_simplified.predict(self.data)
249
+ residuals = self.data[self.y_name] - predictions
250
 
251
+ prediction_table = self.data.copy()
252
+ prediction_table['Predicted'] = predictions.round(3)
253
+ prediction_table['Residual'] = residuals.round(3)
 
254
 
255
+ return prediction_table[[self.y_name, 'Predicted', 'Residual']]
256
 
257
  def calculate_contribution_percentage(self):
258
+ """
259
+ Calculate percentage contribution of model terms
260
+
261
+ Returns:
262
+ --------
263
+ pandas.DataFrame
264
+ Contribution percentage table
265
+ """
266
+ if self.model_simplified is None:
267
+ raise ValueError("Fit the simplified model first.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
268
 
269
+ anova_table = sm.stats.anova_lm(self.model_simplified, typ=2)
270
+ ss_total = anova_table['sum_sq'].sum()
 
 
 
271
 
272
+ contribution_table = []
273
+
274
+ for index, row in anova_table.iterrows():
275
+ if index != 'Residual':
276
+ factor_name = index.replace('I(', '').replace('**2)', '^2')
277
+ ss_factor = row['sum_sq']
278
+ contribution_percentage = (ss_factor / ss_total) * 100
279
+
280
+ contribution_table.append({
281
+ 'Factor': factor_name,
282
+ 'Sum of Squares': round(ss_factor, 3),
283
+ '% Contribution': round(contribution_percentage, 3)
284
+ })
285
+
286
+ return pd.DataFrame(contribution_table)
287
 
288
  def calculate_detailed_anova(self):
289
+ """
290
+ Perform detailed ANOVA analysis
291
+
292
+ Returns:
293
+ --------
294
+ pandas.DataFrame
295
+ Detailed ANOVA table
296
+ """
297
  if self.model_simplified is None:
298
+ raise ValueError("Fit the simplified model first.")
 
299
 
300
+ # Preparar datos para ANOVA detallado
301
+ ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
302
+ df_total = len(self.data) - 1
303
+
304
+ # ANOVA para modelo reducido
305
  formula_reduced = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
306
  f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
307
  model_reduced = smf.ols(formula_reduced, data=self.data).fit()
 
308
  anova_reduced = sm.stats.anova_lm(model_reduced, typ=2)
309
 
310
+ # Calcular componentes de variación
 
 
 
311
  ss_regression = anova_reduced['sum_sq'][:-1].sum()
 
312
  df_regression = len(anova_reduced) - 1
313
 
314
  ss_residual = self.model_simplified.ssr
315
  df_residual = self.model_simplified.df_resid
316
 
317
+ # Error puro
318
  replicas = self.data[self.data.duplicated(subset=[self.x1_name, self.x2_name, self.x3_name], keep=False)]
319
  ss_pure_error = replicas.groupby([self.x1_name, self.x2_name, self.x3_name])[self.y_name].var().sum()
320
  df_pure_error = len(replicas) - len(replicas.groupby([self.x1_name, self.x2_name, self.x3_name]))
321
 
322
+ # Falta de ajuste
323
  ss_lack_of_fit = ss_residual - ss_pure_error
324
  df_lack_of_fit = df_residual - df_pure_error
325
 
326
+ # Calcular cuadrados medios y estadísticos F
327
  ms_regression = ss_regression / df_regression
328
  ms_residual = ss_residual / df_residual
329
  ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit
 
332
  f_lack_of_fit = ms_lack_of_fit / ms_pure_error
333
  p_lack_of_fit = 1 - f.cdf(f_lack_of_fit, df_lack_of_fit, df_pure_error)
334
 
335
+ # Crear tabla de ANOVA detallada
336
  detailed_anova_table = pd.DataFrame({
337
+ 'Source of Variation': ['Regression', 'Residual', 'Lack of Fit', 'Pure Error', 'Total'],
338
+ 'Sum of Squares': [
339
+ round(ss_regression, 3),
340
+ round(ss_residual, 3),
341
+ round(ss_lack_of_fit, 3),
342
+ round(ss_pure_error, 3),
343
+ round(ss_total, 3)
344
+ ],
345
+ 'Degrees of Freedom': [df_regression, df_residual, df_lack_of_fit, df_pure_error, df_total],
346
+ 'Mean Square': [
347
+ round(ms_regression, 3),
348
+ round(ms_residual, 3),
349
+ round(ms_lack_of_fit, 3),
350
+ round(ms_pure_error, 3),
351
+ np.nan
352
+ ],
353
  'F': [np.nan, np.nan, round(f_lack_of_fit, 3), np.nan, np.nan],
354
+ 'p-value': [np.nan, np.nan, round(p_lack_of_fit, 3), np.nan, np.nan]
355
  })
 
 
 
 
 
 
 
 
 
356
 
357
  return detailed_anova_table
 
358
  # --- Funciones para la interfaz de Gradio ---
359
 
360
  def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str):
 
390
  prediction_table = rsm.generate_prediction_table()
391
  contribution_table = rsm.calculate_contribution_percentage()
392
  anova_table = rsm.calculate_detailed_anova()
393
+
394
  equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ")
395
  equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}"
396
 
 
401
  if 'rsm' not in globals():
402
  return None, "Error: Carga los datos primero."
403
 
404
+ # Generar todas las gráficas
405
  all_figs = rsm.generate_all_plots()
406
 
407
+ # Crear una lista de figuras para la salida
408
+ plot_outputs = []
409
+ for fig in all_figs:
410
+ # Convertir la figura a una imagen en formato PNG
411
+ img_bytes = fig.to_image(format="png")
412
+ plot_outputs.append(img_bytes)
413
 
414
+ # Retornar la lista de imágenes
415
+ return plot_outputs
416
 
 
417
  def download_excel():
418
  if 'rsm' not in globals():
419
  return None, "Error: Carga los datos y ajusta el modelo primero."
 
422
  with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
423
  rsm.data.to_excel(writer, sheet_name='Datos', index=False)
424
  rsm.generate_prediction_table().to_excel(writer, sheet_name='Predicciones', index=False)
425
+ rsm.optimize().to_excel(writer, sheet_name='Optimizacion', index=False)
426
+ rsm.calculate_contribution_percentage().to_excel(writer, sheet_name='Contribucion', index=False)
427
  rsm.calculate_detailed_anova().to_excel(writer, sheet_name='ANOVA', index=False)
428
 
429
  output.seek(0)
430
+ return gr.File.update(value=output, visible=True, filename="resultados_rsm.xlsx")
 
 
431
 
 
432
  def download_images():
433
  if 'rsm' not in globals():
434
  return None, "Error: Carga los datos y ajusta el modelo primero."
435
 
436
+ # Crear un directorio temporal para guardar las imágenes
437
+ temp_dir = "temp_images"
438
+ os.makedirs(temp_dir, exist_ok=True)
 
 
 
 
 
439
 
440
+ # Generar todas las gráficas y guardarlas como imágenes PNG
441
+ all_figs = rsm.generate_all_plots()
442
+ for i, fig in enumerate(all_figs):
443
+ img_path = os.path.join(temp_dir, f"plot_{i}.png")
444
+ fig.write_image(img_path)
445
+
446
+ # Comprimir las imágenes en un archivo ZIP
447
+ zip_buffer = io.BytesIO()
448
+ with ZipFile(zip_buffer, "w") as zip_file:
449
+ for filename in os.listdir(temp_dir):
450
+ file_path = os.path.join(temp_dir, filename)
451
+ zip_file.write(file_path, arcname=filename)
452
+
453
+ # Eliminar el directorio temporal
454
+ for filename in os.listdir(temp_dir):
455
+ file_path = os.path.join(temp_dir, filename)
456
+ os.remove(file_path)
457
+ os.rmdir(temp_dir)
458
+
459
+ zip_buffer.seek(0)
460
+ return gr.File.update(value=zip_buffer, visible=True, filename="graficos_rsm.zip")
461
 
462
  # --- Crear la interfaz de Gradio ---
463
 
 
500
  with gr.Row(visible=False) as analysis_row:
501
  with gr.Column():
502
  fit_button = gr.Button("Ajustar Modelo y Optimizar")
503
+ download_excel_button = gr.Button("Descargar Tablas en Excel")
504
+ download_images_button = gr.Button("Descargar Gráficos en ZIP")
505
  gr.Markdown("**Modelo Completo**")
506
  model_completo_output = gr.HTML()
507
  pareto_completo_output = gr.Plot()
 
513
  prediction_table_output = gr.Dataframe(label="Tabla de Predicciones")
514
  contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución")
515
  anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada")
 
 
 
 
 
 
 
 
516
  with gr.Column():
517
  gr.Markdown("## Generar Gráficos de Superficie de Respuesta")
518
  fixed_variable_input = gr.Dropdown(label="Variable Fija", choices=["Glucosa", "Extracto_de_Levadura", "Triptofano"], value="Glucosa")
519
  fixed_level_input = gr.Slider(label="Nivel de Variable Fija", minimum=0, maximum=1, step=0.01, value=0.5)
520
  plot_button = gr.Button("Generar Gráfico")
521
+ rsm_plot_output = gr.Gallery(label="Gráficos RSM", columns=3, preview=True, height="auto")
 
 
522
 
523
  load_button.click(
524
  load_data,
 
528
 
529
  fit_button.click(fit_and_optimize_model, outputs=[model_completo_output, pareto_completo_output, model_simplificado_output, pareto_simplificado_output, equation_output, optimization_table_output, prediction_table_output, contribution_table_output, anova_table_output])
530
 
531
+ plot_button.click(generate_rsm_plot, inputs=[fixed_variable_input, fixed_level_input], outputs=[rsm_plot_output])
532
 
533
+ download_excel_button.click(download_excel, outputs=[gr.File()])
534
+ download_images_button.click(download_images, outputs=[gr.File()])
 
535
 
536
  # Ejemplo de uso
537
  gr.Markdown("## Ejemplo de uso")
 
541
  gr.Markdown("4. Haz clic en 'Ajustar Modelo y Optimizar' para ajustar el modelo y encontrar los niveles óptimos de los factores.")
542
  gr.Markdown("5. Selecciona una variable fija y su nivel en los controles deslizantes.")
543
  gr.Markdown("6. Haz clic en 'Generar Gráfico' para generar un gráfico de superficie de respuesta.")
544
+ gr.Markdown("7. Haz clic en 'Descargar Tablas en Excel' para obtener un archivo Excel con todas las tablas generadas.")
545
+ gr.Markdown("8. Haz clic en 'Descargar Gráficos en ZIP' para obtener un archivo ZIP con todos los gráficos generados.")
546
 
547
  demo.launch()