C2MV commited on
Commit
a6c30a7
·
verified ·
1 Parent(s): 1cf93db

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +109 -170
app.py CHANGED
@@ -8,22 +8,13 @@ from scipy.optimize import minimize
8
  import plotly.express as px
9
  from scipy.stats import t, f
10
  import gradio as gr
 
 
 
11
 
12
  class RSM_BoxBehnken:
13
  def __init__(self, data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels):
14
- """
15
- Inicializa la clase con los datos del diseño Box-Behnken.
16
-
17
- Args:
18
- data (pd.DataFrame): DataFrame con los datos del experimento.
19
- x1_name (str): Nombre de la primera variable independiente.
20
- x2_name (str): Nombre de la segunda variable independiente.
21
- x3_name (str): Nombre de la tercera variable independiente.
22
- y_name (str): Nombre de la variable dependiente.
23
- x1_levels (list): Niveles de la primera variable independiente.
24
- x2_levels (list): Niveles de la segunda variable independiente.
25
- x3_levels (list): Niveles de la tercera variable independiente.
26
- """
27
  self.data = data.copy()
28
  self.model = None
29
  self.model_simplified = None
@@ -41,15 +32,6 @@ class RSM_BoxBehnken:
41
  self.x3_levels = x3_levels
42
 
43
  def get_levels(self, variable_name):
44
- """
45
- Obtiene los niveles para una variable específica.
46
-
47
- Args:
48
- variable_name (str): Nombre de la variable.
49
-
50
- Returns:
51
- list: Niveles de la variable.
52
- """
53
  if variable_name == self.x1_name:
54
  return self.x1_levels
55
  elif variable_name == self.x2_name:
@@ -60,9 +42,6 @@ class RSM_BoxBehnken:
60
  raise ValueError(f"Variable desconocida: {variable_name}")
61
 
62
  def fit_model(self):
63
- """
64
- Ajusta el modelo de segundo orden completo a los datos.
65
- """
66
  formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
67
  f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2) + ' \
68
  f'{self.x1_name}:{self.x2_name} + {self.x1_name}:{self.x3_name} + {self.x2_name}:{self.x3_name}'
@@ -72,9 +51,6 @@ class RSM_BoxBehnken:
72
  return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
73
 
74
  def fit_simplified_model(self):
75
- """
76
- Ajusta el modelo de segundo orden a los datos, eliminando términos no significativos.
77
- """
78
  formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + ' \
79
  f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
80
  self.model_simplified = smf.ols(formula, data=self.data).fit()
@@ -83,12 +59,6 @@ class RSM_BoxBehnken:
83
  return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Modelo Simplificado")
84
 
85
  def optimize(self, method='Nelder-Mead'):
86
- """
87
- Encuentra los niveles óptimos de los factores para maximizar la respuesta usando el modelo simplificado.
88
-
89
- Args:
90
- method (str): Método de optimización a utilizar (por defecto, 'Nelder-Mead').
91
- """
92
  if self.model_simplified is None:
93
  print("Error: Ajusta el modelo simplificado primero.")
94
  return
@@ -102,85 +72,60 @@ class RSM_BoxBehnken:
102
  self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
103
  self.optimal_levels = self.optimized_results.x
104
 
105
- # Convertir niveles óptimos de codificados a naturales
106
  optimal_levels_natural = [
107
- self.coded_to_natural(self.optimal_levels[0], self.x1_name),
108
- self.coded_to_natural(self.optimal_levels[1], self.x2_name),
109
- self.coded_to_natural(self.optimal_levels[2], self.x3_name)
110
  ]
111
- # Crear la tabla de optimización
112
  optimization_table = pd.DataFrame({
113
  'Variable': [self.x1_name, self.x2_name, self.x3_name],
114
  'Nivel Óptimo (Natural)': optimal_levels_natural,
115
- 'Nivel Óptimo (Codificado)': self.optimal_levels
116
  })
117
 
118
  return optimization_table
119
 
120
  def plot_rsm_individual(self, fixed_variable, fixed_level):
121
- """
122
- Genera un gráfico de superficie de respuesta (RSM) individual para una configuración específica.
123
-
124
- Args:
125
- fixed_variable (str): Nombre de la variable a mantener fija.
126
- fixed_level (float): Nivel al que se fija la variable (en unidades naturales).
127
-
128
- Returns:
129
- go.Figure: Objeto de figura de Plotly.
130
- """
131
  if self.model_simplified is None:
132
  print("Error: Ajusta el modelo simplificado primero.")
133
  return None
134
 
135
- # Determinar las variables que varían y sus niveles naturales
136
  varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
137
 
138
- # Establecer los niveles naturales para las variables que varían
139
  x_natural_levels = self.get_levels(varying_variables[0])
140
  y_natural_levels = self.get_levels(varying_variables[1])
141
 
142
- # Crear una malla de puntos para las variables que varían (en unidades naturales)
143
  x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
144
  y_range_natural = np.linspace(y_natural_levels[0], y_natural_levels[-1], 100)
145
  x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
146
 
147
- # Convertir la malla de variables naturales a codificadas
148
  x_grid_coded = self.natural_to_coded(x_grid_natural, varying_variables[0])
149
  y_grid_coded = self.natural_to_coded(y_grid_natural, varying_variables[1])
150
 
151
- # Crear un DataFrame para la predicción con variables codificadas
152
  prediction_data = pd.DataFrame({
153
  varying_variables[0]: x_grid_coded.flatten(),
154
  varying_variables[1]: y_grid_coded.flatten(),
155
  })
156
  prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
157
 
158
- # Calcular los valores predichos
159
  z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape)
160
 
161
- # 1. Identificar los dos factores que varían
162
  varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
163
 
164
- # 2. Filtrar por el nivel de la variable fija (en codificado)
165
  fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable)
166
  subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)]
167
 
168
- # 3. Filtrar por niveles válidos en las variables que varían
169
  valid_levels = [-1, 0, 1]
170
  experiments_data = subset_data[
171
  subset_data[varying_variables[0]].isin(valid_levels) &
172
  subset_data[varying_variables[1]].isin(valid_levels)
173
  ]
174
 
175
- # Convertir coordenadas de experimentos a naturales
176
  experiments_x_natural = experiments_data[varying_variables[0]].apply(lambda x: self.coded_to_natural(x, varying_variables[0]))
177
  experiments_y_natural = experiments_data[varying_variables[1]].apply(lambda x: self.coded_to_natural(x, varying_variables[1]))
178
 
179
- # Crear el gráfico de superficie con variables naturales en los ejes y transparencia
180
  fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)])
181
 
182
- # --- Añadir cuadrícula a la superficie ---
183
- # Líneas en la dirección x
184
  for i in range(x_grid_natural.shape[0]):
185
  fig.add_trace(go.Scatter3d(
186
  x=x_grid_natural[i, :],
@@ -191,7 +136,6 @@ class RSM_BoxBehnken:
191
  showlegend=False,
192
  hoverinfo='skip'
193
  ))
194
- # Líneas en la dirección y
195
  for j in range(x_grid_natural.shape[1]):
196
  fig.add_trace(go.Scatter3d(
197
  x=x_grid_natural[:, j],
@@ -203,10 +147,6 @@ class RSM_BoxBehnken:
203
  hoverinfo='skip'
204
  ))
205
 
206
- # --- Fin de la adición de la cuadrícula ---
207
-
208
- # Añadir los puntos de los experimentos en la superficie de respuesta con diferentes colores y etiquetas
209
- # Crear una lista de colores y etiquetas para los puntos
210
  colors = ['red', 'blue', 'green', 'purple', 'orange', 'yellow', 'cyan', 'magenta']
211
  point_labels = []
212
  for i, row in experiments_data.iterrows():
@@ -217,22 +157,17 @@ class RSM_BoxBehnken:
217
  y=experiments_y_natural,
218
  z=experiments_data[self.y_name],
219
  mode='markers+text',
220
- marker=dict(size=4, color=colors[:len(experiments_x_natural)]), # Usar colores de la lista
221
- text=point_labels, # Usar las etiquetas creadas
222
  textposition='top center',
223
  name='Experimentos'
224
  ))
225
 
226
- # Añadir etiquetas y título con variables naturales
227
  fig.update_layout(
228
  scene=dict(
229
  xaxis_title=varying_variables[0] + " (g/L)",
230
  yaxis_title=varying_variables[1] + " (g/L)",
231
  zaxis_title=self.y_name,
232
- # Puedes mantener la configuración de grid en los planos si lo deseas
233
- # xaxis=dict(showgrid=True, gridwidth=1, gridcolor='lightgray'),
234
- # yaxis=dict(showgrid=True, gridwidth=1, gridcolor='lightgray'),
235
- # zaxis=dict(showgrid=True, gridwidth=1, gridcolor='lightgray')
236
  ),
237
  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>",
238
  height=800,
@@ -255,46 +190,36 @@ class RSM_BoxBehnken:
255
  self.x2_name: self.x2_levels,
256
  self.x3_name: self.x3_levels
257
  }
 
 
258
 
259
  # Generar y mostrar gráficos individuales
260
  for fixed_variable in [self.x1_name, self.x2_name, self.x3_name]:
261
  for level in levels_to_plot_natural[fixed_variable]:
262
  fig = self.plot_rsm_individual(fixed_variable, level)
263
  if fig is not None:
264
- fig.show()
 
265
 
266
  def coded_to_natural(self, coded_value, variable_name):
267
- """Convierte un valor codificado a su valor natural."""
268
  levels = self.get_levels(variable_name)
269
  return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2
270
 
271
  def natural_to_coded(self, natural_value, variable_name):
272
- """Convierte un valor natural a su valor codificado."""
273
  levels = self.get_levels(variable_name)
274
  return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
275
 
276
  def pareto_chart(self, model, title):
277
- """
278
- Genera un diagrama de Pareto para los efectos estandarizados de un modelo,
279
- incluyendo la línea de significancia.
280
-
281
- Args:
282
- model: Modelo ajustado de statsmodels.
283
- title (str): Título del gráfico.
284
- """
285
- # Calcular los efectos estandarizados
286
- tvalues = model.tvalues[1:] # Excluir la Intercept
287
  abs_tvalues = np.abs(tvalues)
288
  sorted_idx = np.argsort(abs_tvalues)[::-1]
289
  sorted_tvalues = abs_tvalues[sorted_idx]
290
  sorted_names = tvalues.index[sorted_idx]
291
 
292
- # Calcular el valor crítico de t para la línea de significancia
293
- alpha = 0.05 # Nivel de significancia
294
- dof = model.df_resid # Grados de libertad residuales
295
  t_critical = t.ppf(1 - alpha / 2, dof)
296
 
297
- # Crear el diagrama de Pareto
298
  fig = px.bar(
299
  x=sorted_tvalues,
300
  y=sorted_names,
@@ -304,7 +229,6 @@ class RSM_BoxBehnken:
304
  )
305
  fig.update_yaxes(autorange="reversed")
306
 
307
- # Agregar la línea de significancia
308
  fig.add_vline(x=t_critical, line_dash="dot",
309
  annotation_text=f"t crítico = {t_critical:.2f}",
310
  annotation_position="bottom right")
@@ -312,37 +236,31 @@ class RSM_BoxBehnken:
312
  return fig
313
 
314
  def get_simplified_equation(self):
315
- """
316
- Imprime la ecuación del modelo simplificado.
317
- """
318
  if self.model_simplified is None:
319
  print("Error: Ajusta el modelo simplificado primero.")
320
  return None
321
 
322
  coefficients = self.model_simplified.params
323
- equation = f"{self.y_name} = {coefficients['Intercept']:.4f}"
324
 
325
  for term, coef in coefficients.items():
326
  if term != 'Intercept':
327
  if term == f'{self.x1_name}':
328
- equation += f" + {coef:.4f}*{self.x1_name}"
329
  elif term == f'{self.x2_name}':
330
- equation += f" + {coef:.4f}*{self.x2_name}"
331
  elif term == f'{self.x3_name}':
332
- equation += f" + {coef:.4f}*{self.x3_name}"
333
  elif term == f'I({self.x1_name} ** 2)':
334
- equation += f" + {coef:.4f}*{self.x1_name}^2"
335
  elif term == f'I({self.x2_name} ** 2)':
336
- equation += f" + {coef:.4f}*{self.x2_name}^2"
337
  elif term == f'I({self.x3_name} ** 2)':
338
- equation += f" + {coef:.4f}*{self.x3_name}^2"
339
 
340
  return equation
341
 
342
  def generate_prediction_table(self):
343
- """
344
- Genera una tabla con los valores actuales, predichos y residuales.
345
- """
346
  if self.model_simplified is None:
347
  print("Error: Ajusta el modelo simplificado primero.")
348
  return None
@@ -350,30 +268,28 @@ class RSM_BoxBehnken:
350
  self.data['Predicho'] = self.model_simplified.predict(self.data)
351
  self.data['Residual'] = self.data[self.y_name] - self.data['Predicho']
352
 
353
- return self.data[[self.y_name, 'Predicho', 'Residual']]
 
 
 
 
 
 
354
 
355
  def calculate_contribution_percentage(self):
356
- """
357
- Calcula el porcentaje de contribución de cada factor a la variabilidad de la respuesta (AIA).
358
- """
359
  if self.model_simplified is None:
360
  print("Error: Ajusta el modelo simplificado primero.")
361
  return None
362
 
363
- # ANOVA del modelo simplificado
364
  anova_table = sm.stats.anova_lm(self.model_simplified, typ=2)
365
-
366
- # Suma de cuadrados total
367
  ss_total = anova_table['sum_sq'].sum()
368
 
369
- # Crear tabla de contribución
370
  contribution_table = pd.DataFrame({
371
  'Factor': [],
372
  'Suma de Cuadrados': [],
373
  '% Contribución': []
374
  })
375
 
376
- # Calcular porcentaje de contribución para cada factor
377
  for index, row in anova_table.iterrows():
378
  if index != 'Residual':
379
  factor_name = index
@@ -389,85 +305,65 @@ class RSM_BoxBehnken:
389
 
390
  contribution_table = pd.concat([contribution_table, pd.DataFrame({
391
  'Factor': [factor_name],
392
- 'Suma de Cuadrados': [ss_factor],
393
- '% Contribución': [contribution_percentage]
394
  })], ignore_index=True)
395
 
396
  return contribution_table
397
 
398
  def calculate_detailed_anova(self):
399
- """
400
- Calcula la tabla ANOVA detallada con la descomposición del error residual.
401
- """
402
  if self.model_simplified is None:
403
  print("Error: Ajusta el modelo simplificado primero.")
404
  return None
405
 
406
- # --- ANOVA detallada ---
407
- # 1. Ajustar un modelo solo con los términos de primer orden y cuadráticos
408
  formula_reduced = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
409
  f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
410
  model_reduced = smf.ols(formula_reduced, data=self.data).fit()
411
 
412
- # 2. ANOVA del modelo reducido (para obtener la suma de cuadrados de la regresión)
413
  anova_reduced = sm.stats.anova_lm(model_reduced, typ=2)
414
 
415
- # 3. Suma de cuadrados total
416
  ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
417
 
418
- # 4. Grados de libertad totales
419
  df_total = len(self.data) - 1
420
 
421
- # 5. Suma de cuadrados de la regresión
422
- ss_regression = anova_reduced['sum_sq'][:-1].sum() # Sumar todo excepto 'Residual'
423
 
424
- # 6. Grados de libertad de la regresión
425
  df_regression = len(anova_reduced) - 1
426
 
427
- # 7. Suma de cuadrados del error residual
428
  ss_residual = self.model_simplified.ssr
429
  df_residual = self.model_simplified.df_resid
430
 
431
- # 8. Suma de cuadrados del error puro (se calcula a partir de las réplicas)
432
  replicas = self.data[self.data.duplicated(subset=[self.x1_name, self.x2_name, self.x3_name], keep=False)]
433
  ss_pure_error = replicas.groupby([self.x1_name, self.x2_name, self.x3_name])[self.y_name].var().sum()
434
  df_pure_error = len(replicas) - len(replicas.groupby([self.x1_name, self.x2_name, self.x3_name]))
435
 
436
- # 9. Suma de cuadrados de la falta de ajuste
437
  ss_lack_of_fit = ss_residual - ss_pure_error
438
  df_lack_of_fit = df_residual - df_pure_error
439
 
440
- # 10. Cuadrados medios
441
  ms_regression = ss_regression / df_regression
442
  ms_residual = ss_residual / df_residual
443
  ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit
444
  ms_pure_error = ss_pure_error / df_pure_error
445
 
446
- # 11. Estadístico F y valor p para la falta de ajuste
447
  f_lack_of_fit = ms_lack_of_fit / ms_pure_error
448
- p_lack_of_fit = 1 - f.cdf(f_lack_of_fit, df_lack_of_fit, df_pure_error) # Usar f.cdf de scipy.stats
449
 
450
- # 12. Crear la tabla ANOVA detallada
451
  detailed_anova_table = pd.DataFrame({
452
  'Fuente de Variación': ['Regresión', 'Residual', 'Falta de Ajuste', 'Error Puro', 'Total'],
453
- 'Suma de Cuadrados': [ss_regression, ss_residual, ss_lack_of_fit, ss_pure_error, ss_total],
454
  'Grados de Libertad': [df_regression, df_residual, df_lack_of_fit, df_pure_error, df_total],
455
- 'Cuadrado Medio': [ms_regression, ms_residual, ms_lack_of_fit, ms_pure_error, np.nan],
456
- 'F': [np.nan, np.nan, f_lack_of_fit, np.nan, np.nan],
457
- 'Valor p': [np.nan, np.nan, p_lack_of_fit, np.nan, np.nan]
458
  })
459
 
460
- # Calcular la suma de cuadrados y grados de libertad para la curvatura
461
  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)']
462
  df_curvature = 3
463
 
464
- # Añadir la fila de curvatura a la tabla ANOVA
465
- detailed_anova_table.loc[len(detailed_anova_table)] = ['Curvatura', ss_curvature, df_curvature, ss_curvature / df_curvature, np.nan, np.nan]
466
 
467
- # Reorganizar las filas para que la curvatura aparezca después de la regresión
468
  detailed_anova_table = detailed_anova_table.reindex([0, 5, 1, 2, 3, 4])
469
 
470
- # Resetear el índice para que sea consecutivo
471
  detailed_anova_table = detailed_anova_table.reset_index(drop=True)
472
 
473
  return detailed_anova_table
@@ -475,39 +371,19 @@ class RSM_BoxBehnken:
475
  # --- Funciones para la interfaz de Gradio ---
476
 
477
  def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str):
478
- """
479
- Carga los datos del diseño Box-Behnken desde cajas de texto y crea la instancia de RSM_BoxBehnken.
480
-
481
- Args:
482
- x1_name (str): Nombre de la primera variable independiente.
483
- x2_name (str): Nombre de la segunda variable independiente.
484
- x3_name (str): Nombre de la tercera variable independiente.
485
- y_name (str): Nombre de la variable dependiente.
486
- x1_levels_str (str): Niveles de la primera variable, separados por comas.
487
- x2_levels_str (str): Niveles de la segunda variable, separados por comas.
488
- x3_levels_str (str): Niveles de la tercera variable, separados por comas.
489
- data_str (str): Datos del experimento en formato CSV, separados por comas.
490
-
491
- Returns:
492
- tuple: (pd.DataFrame, str, str, str, str, list, list, list, gr.update)
493
- """
494
  try:
495
- # Convertir los niveles a listas de números
496
  x1_levels = [float(x.strip()) for x in x1_levels_str.split(',')]
497
  x2_levels = [float(x.strip()) for x in x2_levels_str.split(',')]
498
  x3_levels = [float(x.strip()) for x in x3_levels_str.split(',')]
499
 
500
- # Crear DataFrame a partir de la cadena de datos
501
  data_list = [row.split(',') for row in data_str.strip().split('\n')]
502
  column_names = ['Exp.', x1_name, x2_name, x3_name, y_name]
503
  data = pd.DataFrame(data_list, columns=column_names)
504
- data = data.apply(pd.to_numeric, errors='coerce') # Convertir a numérico
505
 
506
- # Validar que el DataFrame tenga las columnas correctas
507
  if not all(col in data.columns for col in column_names):
508
  raise ValueError("El formato de los datos no es correcto.")
509
 
510
- # Crear la instancia de RSM_BoxBehnken
511
  global rsm
512
  rsm = RSM_BoxBehnken(data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels)
513
 
@@ -527,8 +403,7 @@ def fit_and_optimize_model():
527
  prediction_table = rsm.generate_prediction_table()
528
  contribution_table = rsm.calculate_contribution_percentage()
529
  anova_table = rsm.calculate_detailed_anova()
530
-
531
- # Formatear la ecuación para que se vea mejor en Markdown
532
  equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ")
533
  equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}"
534
 
@@ -538,8 +413,64 @@ def fit_and_optimize_model():
538
  def generate_rsm_plot(fixed_variable, fixed_level):
539
  if 'rsm' not in globals():
540
  return None, "Error: Carga los datos primero."
541
- fig = rsm.plot_rsm_individual(fixed_variable, fixed_level)
542
- return fig
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
543
 
544
  # --- Crear la interfaz de Gradio ---
545
 
@@ -582,6 +513,8 @@ with gr.Blocks() as demo:
582
  with gr.Row(visible=False) as analysis_row:
583
  with gr.Column():
584
  fit_button = gr.Button("Ajustar Modelo y Optimizar")
 
 
585
  gr.Markdown("**Modelo Completo**")
586
  model_completo_output = gr.HTML()
587
  pareto_completo_output = gr.Plot()
@@ -598,7 +531,7 @@ with gr.Blocks() as demo:
598
  fixed_variable_input = gr.Dropdown(label="Variable Fija", choices=["Glucosa", "Extracto_de_Levadura", "Triptofano"], value="Glucosa")
599
  fixed_level_input = gr.Slider(label="Nivel de Variable Fija", minimum=0, maximum=1, step=0.01, value=0.5)
600
  plot_button = gr.Button("Generar Gráfico")
601
- rsm_plot_output = gr.Plot()
602
 
603
  load_button.click(
604
  load_data,
@@ -607,8 +540,12 @@ with gr.Blocks() as demo:
607
  )
608
 
609
  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])
 
610
  plot_button.click(generate_rsm_plot, inputs=[fixed_variable_input, fixed_level_input], outputs=[rsm_plot_output])
611
 
 
 
 
612
  # Ejemplo de uso
613
  gr.Markdown("## Ejemplo de uso")
614
  gr.Markdown("1. Introduce los nombres de las variables y sus niveles en las cajas de texto correspondientes.")
@@ -617,5 +554,7 @@ with gr.Blocks() as demo:
617
  gr.Markdown("4. Haz clic en 'Ajustar Modelo y Optimizar' para ajustar el modelo y encontrar los niveles óptimos de los factores.")
618
  gr.Markdown("5. Selecciona una variable fija y su nivel en los controles deslizantes.")
619
  gr.Markdown("6. Haz clic en 'Generar Gráfico' para generar un gráfico de superficie de respuesta.")
 
 
620
 
621
  demo.launch()
 
8
  import plotly.express as px
9
  from scipy.stats import t, f
10
  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
+ # ... (El código de la clase RSM_BoxBehnken se mantiene igual, solo se modifican las funciones que generan dataframes o strings)
 
 
 
 
 
 
 
 
 
 
 
 
18
  self.data = data.copy()
19
  self.model = None
20
  self.model_simplified = None
 
32
  self.x3_levels = x3_levels
33
 
34
  def get_levels(self, variable_name):
 
 
 
 
 
 
 
 
 
35
  if variable_name == self.x1_name:
36
  return self.x1_levels
37
  elif variable_name == self.x2_name:
 
42
  raise ValueError(f"Variable desconocida: {variable_name}")
43
 
44
  def fit_model(self):
 
 
 
45
  formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
46
  f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2) + ' \
47
  f'{self.x1_name}:{self.x2_name} + {self.x1_name}:{self.x3_name} + {self.x2_name}:{self.x3_name}'
 
51
  return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
52
 
53
  def fit_simplified_model(self):
 
 
 
54
  formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + ' \
55
  f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
56
  self.model_simplified = smf.ols(formula, data=self.data).fit()
 
59
  return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Modelo Simplificado")
60
 
61
  def optimize(self, method='Nelder-Mead'):
 
 
 
 
 
 
62
  if self.model_simplified is None:
63
  print("Error: Ajusta el modelo simplificado primero.")
64
  return
 
72
  self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
73
  self.optimal_levels = self.optimized_results.x
74
 
 
75
  optimal_levels_natural = [
76
+ round(self.coded_to_natural(self.optimal_levels[0], self.x1_name), 3),
77
+ round(self.coded_to_natural(self.optimal_levels[1], self.x2_name), 3),
78
+ round(self.coded_to_natural(self.optimal_levels[2], self.x3_name), 3)
79
  ]
 
80
  optimization_table = pd.DataFrame({
81
  'Variable': [self.x1_name, self.x2_name, self.x3_name],
82
  'Nivel Óptimo (Natural)': optimal_levels_natural,
83
+ 'Nivel Óptimo (Codificado)': [round(x, 3) for x in self.optimal_levels]
84
  })
85
 
86
  return optimization_table
87
 
88
  def plot_rsm_individual(self, fixed_variable, fixed_level):
 
 
 
 
 
 
 
 
 
 
89
  if self.model_simplified is None:
90
  print("Error: Ajusta el modelo simplificado primero.")
91
  return None
92
 
 
93
  varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
94
 
 
95
  x_natural_levels = self.get_levels(varying_variables[0])
96
  y_natural_levels = self.get_levels(varying_variables[1])
97
 
 
98
  x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
99
  y_range_natural = np.linspace(y_natural_levels[0], y_natural_levels[-1], 100)
100
  x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
101
 
 
102
  x_grid_coded = self.natural_to_coded(x_grid_natural, varying_variables[0])
103
  y_grid_coded = self.natural_to_coded(y_grid_natural, varying_variables[1])
104
 
 
105
  prediction_data = pd.DataFrame({
106
  varying_variables[0]: x_grid_coded.flatten(),
107
  varying_variables[1]: y_grid_coded.flatten(),
108
  })
109
  prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
110
 
 
111
  z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape)
112
 
 
113
  varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
114
 
 
115
  fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable)
116
  subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)]
117
 
 
118
  valid_levels = [-1, 0, 1]
119
  experiments_data = subset_data[
120
  subset_data[varying_variables[0]].isin(valid_levels) &
121
  subset_data[varying_variables[1]].isin(valid_levels)
122
  ]
123
 
 
124
  experiments_x_natural = experiments_data[varying_variables[0]].apply(lambda x: self.coded_to_natural(x, varying_variables[0]))
125
  experiments_y_natural = experiments_data[varying_variables[1]].apply(lambda x: self.coded_to_natural(x, varying_variables[1]))
126
 
 
127
  fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)])
128
 
 
 
129
  for i in range(x_grid_natural.shape[0]):
130
  fig.add_trace(go.Scatter3d(
131
  x=x_grid_natural[i, :],
 
136
  showlegend=False,
137
  hoverinfo='skip'
138
  ))
 
139
  for j in range(x_grid_natural.shape[1]):
140
  fig.add_trace(go.Scatter3d(
141
  x=x_grid_natural[:, j],
 
147
  hoverinfo='skip'
148
  ))
149
 
 
 
 
 
150
  colors = ['red', 'blue', 'green', 'purple', 'orange', 'yellow', 'cyan', 'magenta']
151
  point_labels = []
152
  for i, row in experiments_data.iterrows():
 
157
  y=experiments_y_natural,
158
  z=experiments_data[self.y_name],
159
  mode='markers+text',
160
+ marker=dict(size=4, color=colors[:len(experiments_x_natural)]),
161
+ text=point_labels,
162
  textposition='top center',
163
  name='Experimentos'
164
  ))
165
 
 
166
  fig.update_layout(
167
  scene=dict(
168
  xaxis_title=varying_variables[0] + " (g/L)",
169
  yaxis_title=varying_variables[1] + " (g/L)",
170
  zaxis_title=self.y_name,
 
 
 
 
171
  ),
172
  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>",
173
  height=800,
 
190
  self.x2_name: self.x2_levels,
191
  self.x3_name: self.x3_levels
192
  }
193
+
194
+ figs = []
195
 
196
  # Generar y mostrar gráficos individuales
197
  for fixed_variable in [self.x1_name, self.x2_name, self.x3_name]:
198
  for level in levels_to_plot_natural[fixed_variable]:
199
  fig = self.plot_rsm_individual(fixed_variable, level)
200
  if fig is not None:
201
+ figs.append(fig)
202
+ return figs
203
 
204
  def coded_to_natural(self, coded_value, variable_name):
 
205
  levels = self.get_levels(variable_name)
206
  return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2
207
 
208
  def natural_to_coded(self, natural_value, variable_name):
 
209
  levels = self.get_levels(variable_name)
210
  return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
211
 
212
  def pareto_chart(self, model, title):
213
+ tvalues = model.tvalues[1:]
 
 
 
 
 
 
 
 
 
214
  abs_tvalues = np.abs(tvalues)
215
  sorted_idx = np.argsort(abs_tvalues)[::-1]
216
  sorted_tvalues = abs_tvalues[sorted_idx]
217
  sorted_names = tvalues.index[sorted_idx]
218
 
219
+ alpha = 0.05
220
+ dof = model.df_resid
 
221
  t_critical = t.ppf(1 - alpha / 2, dof)
222
 
 
223
  fig = px.bar(
224
  x=sorted_tvalues,
225
  y=sorted_names,
 
229
  )
230
  fig.update_yaxes(autorange="reversed")
231
 
 
232
  fig.add_vline(x=t_critical, line_dash="dot",
233
  annotation_text=f"t crítico = {t_critical:.2f}",
234
  annotation_position="bottom right")
 
236
  return fig
237
 
238
  def get_simplified_equation(self):
 
 
 
239
  if self.model_simplified is None:
240
  print("Error: Ajusta el modelo simplificado primero.")
241
  return None
242
 
243
  coefficients = self.model_simplified.params
244
+ equation = f"{self.y_name} = {coefficients['Intercept']:.3f}"
245
 
246
  for term, coef in coefficients.items():
247
  if term != 'Intercept':
248
  if term == f'{self.x1_name}':
249
+ equation += f" + {coef:.3f}*{self.x1_name}"
250
  elif term == f'{self.x2_name}':
251
+ equation += f" + {coef:.3f}*{self.x2_name}"
252
  elif term == f'{self.x3_name}':
253
+ equation += f" + {coef:.3f}*{self.x3_name}"
254
  elif term == f'I({self.x1_name} ** 2)':
255
+ equation += f" + {coef:.3f}*{self.x1_name}^2"
256
  elif term == f'I({self.x2_name} ** 2)':
257
+ equation += f" + {coef:.3f}*{self.x2_name}^2"
258
  elif term == f'I({self.x3_name} ** 2)':
259
+ equation += f" + {coef:.3f}*{self.x3_name}^2"
260
 
261
  return equation
262
 
263
  def generate_prediction_table(self):
 
 
 
264
  if self.model_simplified is None:
265
  print("Error: Ajusta el modelo simplificado primero.")
266
  return None
 
268
  self.data['Predicho'] = self.model_simplified.predict(self.data)
269
  self.data['Residual'] = self.data[self.y_name] - self.data['Predicho']
270
 
271
+ # Redondear a 3 decimales en la tabla de predicciones
272
+ prediction_table = self.data[[self.y_name, 'Predicho', 'Residual']].copy()
273
+ prediction_table[self.y_name] = prediction_table[self.y_name].round(3)
274
+ prediction_table['Predicho'] = prediction_table['Predicho'].round(3)
275
+ prediction_table['Residual'] = prediction_table['Residual'].round(3)
276
+
277
+ return prediction_table
278
 
279
  def calculate_contribution_percentage(self):
 
 
 
280
  if self.model_simplified is None:
281
  print("Error: Ajusta el modelo simplificado primero.")
282
  return None
283
 
 
284
  anova_table = sm.stats.anova_lm(self.model_simplified, typ=2)
 
 
285
  ss_total = anova_table['sum_sq'].sum()
286
 
 
287
  contribution_table = pd.DataFrame({
288
  'Factor': [],
289
  'Suma de Cuadrados': [],
290
  '% Contribución': []
291
  })
292
 
 
293
  for index, row in anova_table.iterrows():
294
  if index != 'Residual':
295
  factor_name = index
 
305
 
306
  contribution_table = pd.concat([contribution_table, pd.DataFrame({
307
  'Factor': [factor_name],
308
+ 'Suma de Cuadrados': [round(ss_factor, 3)],
309
+ '% Contribución': [round(contribution_percentage, 3)]
310
  })], ignore_index=True)
311
 
312
  return contribution_table
313
 
314
  def calculate_detailed_anova(self):
 
 
 
315
  if self.model_simplified is None:
316
  print("Error: Ajusta el modelo simplificado primero.")
317
  return None
318
 
 
 
319
  formula_reduced = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
320
  f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
321
  model_reduced = smf.ols(formula_reduced, data=self.data).fit()
322
 
 
323
  anova_reduced = sm.stats.anova_lm(model_reduced, typ=2)
324
 
 
325
  ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
326
 
 
327
  df_total = len(self.data) - 1
328
 
329
+ ss_regression = anova_reduced['sum_sq'][:-1].sum()
 
330
 
 
331
  df_regression = len(anova_reduced) - 1
332
 
 
333
  ss_residual = self.model_simplified.ssr
334
  df_residual = self.model_simplified.df_resid
335
 
 
336
  replicas = self.data[self.data.duplicated(subset=[self.x1_name, self.x2_name, self.x3_name], keep=False)]
337
  ss_pure_error = replicas.groupby([self.x1_name, self.x2_name, self.x3_name])[self.y_name].var().sum()
338
  df_pure_error = len(replicas) - len(replicas.groupby([self.x1_name, self.x2_name, self.x3_name]))
339
 
 
340
  ss_lack_of_fit = ss_residual - ss_pure_error
341
  df_lack_of_fit = df_residual - df_pure_error
342
 
 
343
  ms_regression = ss_regression / df_regression
344
  ms_residual = ss_residual / df_residual
345
  ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit
346
  ms_pure_error = ss_pure_error / df_pure_error
347
 
 
348
  f_lack_of_fit = ms_lack_of_fit / ms_pure_error
349
+ p_lack_of_fit = 1 - f.cdf(f_lack_of_fit, df_lack_of_fit, df_pure_error)
350
 
 
351
  detailed_anova_table = pd.DataFrame({
352
  'Fuente de Variación': ['Regresión', 'Residual', 'Falta de Ajuste', 'Error Puro', 'Total'],
353
+ '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)],
354
  'Grados de Libertad': [df_regression, df_residual, df_lack_of_fit, df_pure_error, df_total],
355
+ 'Cuadrado Medio': [round(ms_regression, 3), round(ms_residual, 3), round(ms_lack_of_fit, 3), round(ms_pure_error, 3), np.nan],
356
+ 'F': [np.nan, np.nan, round(f_lack_of_fit, 3), np.nan, np.nan],
357
+ 'Valor p': [np.nan, np.nan, round(p_lack_of_fit, 3), np.nan, np.nan]
358
  })
359
 
 
360
  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)']
361
  df_curvature = 3
362
 
363
+ 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]
 
364
 
 
365
  detailed_anova_table = detailed_anova_table.reindex([0, 5, 1, 2, 3, 4])
366
 
 
367
  detailed_anova_table = detailed_anova_table.reset_index(drop=True)
368
 
369
  return detailed_anova_table
 
371
  # --- Funciones para la interfaz de Gradio ---
372
 
373
  def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
374
  try:
 
375
  x1_levels = [float(x.strip()) for x in x1_levels_str.split(',')]
376
  x2_levels = [float(x.strip()) for x in x2_levels_str.split(',')]
377
  x3_levels = [float(x.strip()) for x in x3_levels_str.split(',')]
378
 
 
379
  data_list = [row.split(',') for row in data_str.strip().split('\n')]
380
  column_names = ['Exp.', x1_name, x2_name, x3_name, y_name]
381
  data = pd.DataFrame(data_list, columns=column_names)
382
+ data = data.apply(pd.to_numeric, errors='coerce')
383
 
 
384
  if not all(col in data.columns for col in column_names):
385
  raise ValueError("El formato de los datos no es correcto.")
386
 
 
387
  global rsm
388
  rsm = RSM_BoxBehnken(data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels)
389
 
 
403
  prediction_table = rsm.generate_prediction_table()
404
  contribution_table = rsm.calculate_contribution_percentage()
405
  anova_table = rsm.calculate_detailed_anova()
406
+
 
407
  equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ")
408
  equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}"
409
 
 
413
  def generate_rsm_plot(fixed_variable, fixed_level):
414
  if 'rsm' not in globals():
415
  return None, "Error: Carga los datos primero."
416
+
417
+ # Generar todas las gráficas
418
+ all_figs = rsm.generate_all_plots()
419
+
420
+ # Crear una lista de figuras para la salida
421
+ plot_outputs = []
422
+ for fig in all_figs:
423
+ # Convertir la figura a una imagen en formato PNG
424
+ img_bytes = fig.to_image(format="png")
425
+ plot_outputs.append(img_bytes)
426
+
427
+ # Retornar la lista de imágenes
428
+ return plot_outputs
429
+
430
+ def download_excel():
431
+ if 'rsm' not in globals():
432
+ return None, "Error: Carga los datos y ajusta el modelo primero."
433
+
434
+ output = io.BytesIO()
435
+ with pd.ExcelWriter(output, engine='xlsxwriter') as writer:
436
+ rsm.data.to_excel(writer, sheet_name='Datos', index=False)
437
+ rsm.generate_prediction_table().to_excel(writer, sheet_name='Predicciones', index=False)
438
+ rsm.optimize().to_excel(writer, sheet_name='Optimizacion', index=False)
439
+ rsm.calculate_contribution_percentage().to_excel(writer, sheet_name='Contribucion', index=False)
440
+ rsm.calculate_detailed_anova().to_excel(writer, sheet_name='ANOVA', index=False)
441
+
442
+ output.seek(0)
443
+ return gr.File.update(value=output, visible=True, filename="resultados_rsm.xlsx")
444
+
445
+ def download_images():
446
+ if 'rsm' not in globals():
447
+ return None, "Error: Carga los datos y ajusta el modelo primero."
448
+
449
+ # Crear un directorio temporal para guardar las imágenes
450
+ temp_dir = "temp_images"
451
+ os.makedirs(temp_dir, exist_ok=True)
452
+
453
+ # Generar todas las gráficas y guardarlas como imágenes PNG
454
+ all_figs = rsm.generate_all_plots()
455
+ for i, fig in enumerate(all_figs):
456
+ img_path = os.path.join(temp_dir, f"plot_{i}.png")
457
+ fig.write_image(img_path)
458
+
459
+ # Comprimir las imágenes en un archivo ZIP
460
+ zip_buffer = io.BytesIO()
461
+ with ZipFile(zip_buffer, "w") as zip_file:
462
+ for filename in os.listdir(temp_dir):
463
+ file_path = os.path.join(temp_dir, filename)
464
+ zip_file.write(file_path, arcname=filename)
465
+
466
+ # Eliminar el directorio temporal
467
+ for filename in os.listdir(temp_dir):
468
+ file_path = os.path.join(temp_dir, filename)
469
+ os.remove(file_path)
470
+ os.rmdir(temp_dir)
471
+
472
+ zip_buffer.seek(0)
473
+ return gr.File.update(value=zip_buffer, visible=True, filename="graficos_rsm.zip")
474
 
475
  # --- Crear la interfaz de Gradio ---
476
 
 
513
  with gr.Row(visible=False) as analysis_row:
514
  with gr.Column():
515
  fit_button = gr.Button("Ajustar Modelo y Optimizar")
516
+ download_excel_button = gr.Button("Descargar Tablas en Excel")
517
+ download_images_button = gr.Button("Descargar Gráficos en ZIP")
518
  gr.Markdown("**Modelo Completo**")
519
  model_completo_output = gr.HTML()
520
  pareto_completo_output = gr.Plot()
 
531
  fixed_variable_input = gr.Dropdown(label="Variable Fija", choices=["Glucosa", "Extracto_de_Levadura", "Triptofano"], value="Glucosa")
532
  fixed_level_input = gr.Slider(label="Nivel de Variable Fija", minimum=0, maximum=1, step=0.01, value=0.5)
533
  plot_button = gr.Button("Generar Gráfico")
534
+ rsm_plot_output = gr.Gallery(label="Gráficos RSM", columns=3, preview=True, height="auto")
535
 
536
  load_button.click(
537
  load_data,
 
540
  )
541
 
542
  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])
543
+
544
  plot_button.click(generate_rsm_plot, inputs=[fixed_variable_input, fixed_level_input], outputs=[rsm_plot_output])
545
 
546
+ download_excel_button.click(download_excel, outputs=[gr.File()])
547
+ download_images_button.click(download_images, outputs=[gr.File()])
548
+
549
  # Ejemplo de uso
550
  gr.Markdown("## Ejemplo de uso")
551
  gr.Markdown("1. Introduce los nombres de las variables y sus niveles en las cajas de texto correspondientes.")
 
554
  gr.Markdown("4. Haz clic en 'Ajustar Modelo y Optimizar' para ajustar el modelo y encontrar los niveles óptimos de los factores.")
555
  gr.Markdown("5. Selecciona una variable fija y su nivel en los controles deslizantes.")
556
  gr.Markdown("6. Haz clic en 'Generar Gráfico' para generar un gráfico de superficie de respuesta.")
557
+ gr.Markdown("7. Haz clic en 'Descargar Tablas en Excel' para obtener un archivo Excel con todas las tablas generadas.")
558
+ gr.Markdown("8. Haz clic en 'Descargar Gráficos en ZIP' para obtener un archivo ZIP con todos los gráficos generados.")
559
 
560
  demo.launch()