C2MV commited on
Commit
0c6f0a1
·
verified ·
1 Parent(s): e2ca05f

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +366 -341
app.py CHANGED
@@ -5,7 +5,7 @@ import statsmodels.api as sm
5
  import plotly.graph_objects as go
6
  from scipy.optimize import minimize
7
  import plotly.express as px
8
- from scipy.stats import t, f
9
  import gradio as gr
10
  import io
11
  import zipfile
@@ -14,62 +14,92 @@ 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())
65
  return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
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())
@@ -82,34 +112,32 @@ class RSM_BoxBehnken:
82
  if self.model_simplified is None:
83
  print("Error: Ajusta el modelo simplificado primero.")
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
  """
@@ -119,12 +147,16 @@ class RSM_BoxBehnken:
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)
@@ -132,39 +164,27 @@ class RSM_BoxBehnken:
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, :],
@@ -175,7 +195,6 @@ class RSM_BoxBehnken:
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],
@@ -187,18 +206,18 @@ class RSM_BoxBehnken:
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'
@@ -207,11 +226,11 @@ class RSM_BoxBehnken:
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
@@ -226,8 +245,9 @@ class RSM_BoxBehnken:
226
  units = {
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
 
@@ -243,36 +263,21 @@ class RSM_BoxBehnken:
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 usando estadísticos F,
272
  incluyendo la línea de significancia.
273
  """
274
  # Calcular los estadísticos F para cada término
275
- # F = (coef/std_err)^2 = t^2
276
  fvalues = model.tvalues[1:]**2 # Excluir la Intercept y convertir t a F
277
  abs_fvalues = np.abs(fvalues)
278
  sorted_idx = np.argsort(abs_fvalues)[::-1]
@@ -315,21 +320,15 @@ class RSM_BoxBehnken:
315
 
316
  for term, coef in coefficients.items():
317
  if term != 'Intercept':
318
- if term == f'{self.x1_name}':
319
- equation += f" + {coef:.3f}*{self.x1_name}"
320
- elif term == f'{self.x2_name}':
321
- equation += f" + {coef:.3f}*{self.x2_name}"
322
- elif term == f'{self.x3_name}':
323
- equation += f" + {coef:.3f}*{self.x3_name}"
324
- elif term == f'I({self.x1_name} ** 2)':
325
- equation += f" + {coef:.3f}*{self.x1_name}^2"
326
- elif term == f'I({self.x2_name} ** 2)':
327
- equation += f" + {coef:.3f}*{self.x2_name}^2"
328
- elif term == f'I({self.x3_name} ** 2)':
329
- equation += f" + {coef:.3f}*{self.x3_name}^2"
330
-
331
  return equation
332
-
333
  def generate_prediction_table(self):
334
  """
335
  Genera una tabla con los valores actuales, predichos y residuales.
@@ -338,7 +337,7 @@ class RSM_BoxBehnken:
338
  print("Error: Ajusta el modelo simplificado primero.")
339
  return None
340
 
341
- self.data['Predicho'] = self.model_simplified.predict(self.data)
342
  self.data['Residual'] = self.data[self.y_name] - self.data['Predicho']
343
 
344
  return self.data[[self.y_name, 'Predicho', 'Residual']].round(3)
@@ -370,54 +369,13 @@ class RSM_BoxBehnken:
370
 
371
  # Calcular estadísticos F y porcentaje de contribución para cada factor
372
  ms_error = anova_table.loc['Residual', 'sum_sq'] / anova_table.loc['Residual', 'df']
373
-
374
- # Agregar fila para el bloque
375
- block_ss = self.data.groupby('Exp.')[self.y_name].sum().var() * len(self.data)
376
- block_df = 1
377
- block_ms = block_ss / block_df
378
- block_f = block_ms / ms_error
379
- block_p = f.sf(block_f, block_df, anova_table.loc['Residual', 'df'])
380
- block_contribution = (block_ss / ss_total) * 100
381
 
382
- contribution_table = pd.concat([contribution_table, pd.DataFrame({
383
- 'Fuente de Variación': ['Block'],
384
- 'Suma de Cuadrados': [block_ss],
385
- 'Grados de Libertad': [block_df],
386
- 'Cuadrado Medio': [block_ms],
387
- 'F': [block_f],
388
- 'Valor p': [block_p],
389
- '% Contribución': [block_contribution]
390
- })], ignore_index=True)
391
-
392
- # Agregar fila para el modelo
393
- model_ss = anova_table['sum_sq'][:-1].sum() # Suma todo excepto residual
394
- model_df = anova_table['df'][:-1].sum()
395
- model_ms = model_ss / model_df
396
- model_f = model_ms / ms_error
397
- model_p = f.sf(model_f, model_df, anova_table.loc['Residual', 'df'])
398
- model_contribution = (model_ss / ss_total) * 100
399
-
400
- contribution_table = pd.concat([contribution_table, pd.DataFrame({
401
- 'Fuente de Variación': ['Model'],
402
- 'Suma de Cuadrados': [model_ss],
403
- 'Grados de Libertad': [model_df],
404
- 'Cuadrado Medio': [model_ms],
405
- 'F': [model_f],
406
- 'Valor p': [model_p],
407
- '% Contribución': [model_contribution]
408
- })], ignore_index=True)
409
-
410
  # Agregar filas para cada término del modelo
411
  for index, row in anova_table.iterrows():
412
  if index != 'Residual':
413
  factor_name = index
414
- if factor_name == f'I({self.x1_name} ** 2)':
415
- factor_name = f'{self.x1_name}^2'
416
- elif factor_name == f'I({self.x2_name} ** 2)':
417
- factor_name = f'{self.x2_name}^2'
418
- elif factor_name == f'I({self.x3_name} ** 2)':
419
- factor_name = f'{self.x3_name}^2'
420
-
421
  ss_factor = row['sum_sq']
422
  df_factor = row['df']
423
  ms_factor = ss_factor / df_factor
@@ -436,13 +394,10 @@ class RSM_BoxBehnken:
436
  })], ignore_index=True)
437
 
438
  # Agregar fila para Cor Total
439
- cor_total_ss = ss_total
440
- cor_total_df = len(self.data) - 1
441
-
442
  contribution_table = pd.concat([contribution_table, pd.DataFrame({
443
  'Fuente de Variación': ['Cor Total'],
444
- 'Suma de Cuadrados': [cor_total_ss],
445
- 'Grados de Libertad': [cor_total_df],
446
  'Cuadrado Medio': [np.nan],
447
  'F': [np.nan],
448
  'Valor p': [np.nan],
@@ -459,88 +414,60 @@ class RSM_BoxBehnken:
459
  print("Error: Ajusta el modelo simplificado primero.")
460
  return None
461
 
462
- # --- ANOVA detallada ---
463
- # 1. Ajustar un modelo solo con los términos de primer orden y cuadráticos
464
- formula_reduced = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
465
- f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
466
- model_reduced = smf.ols(formula_reduced, data=self.data).fit()
467
-
468
- # 2. ANOVA del modelo reducido
469
- anova_reduced = sm.stats.anova_lm(model_reduced, typ=2)
470
 
471
- # 3. Suma de cuadrados total
472
  ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
473
 
474
- # 4. Grados de libertad totales
475
- df_total = len(self.data) - 1
476
-
477
- # 5. Suma de cuadrados de la regresión
478
- ss_regression = anova_reduced['sum_sq'][:-1].sum() # Sumar todo excepto 'Residual'
479
 
480
- # 6. Grados de libertad de la regresión
481
- df_regression = len(anova_reduced) - 1
482
 
483
- # 7. Suma de cuadrados del error residual
484
- ss_residual = self.model_simplified.ssr
485
- df_residual = self.model_simplified.df_resid
486
 
487
- # 8. Suma de cuadrados del error puro (se calcula a partir de las réplicas)
488
- replicas = self.data[self.data.duplicated(subset=[self.x1_name, self.x2_name, self.x3_name], keep=False)]
489
- if not replicas.empty:
490
- 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
491
- df_pure_error = len(replicas) - replicas.groupby([self.x1_name, self.x2_name, self.x3_name]).ngroups
492
  else:
493
  ss_pure_error = np.nan
494
  df_pure_error = np.nan
495
 
496
- # 9. Suma de cuadrados de la falta de ajuste
497
  ss_lack_of_fit = ss_residual - ss_pure_error if not np.isnan(ss_pure_error) else np.nan
498
  df_lack_of_fit = df_residual - df_pure_error if not np.isnan(df_pure_error) else np.nan
499
 
500
- # 10. Cuadrados medios
501
  ms_regression = ss_regression / df_regression
502
  ms_residual = ss_residual / df_residual
503
  ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit if not np.isnan(ss_lack_of_fit) else np.nan
504
  ms_pure_error = ss_pure_error / df_pure_error if not np.isnan(ss_pure_error) else np.nan
505
 
506
- # 11. Estadísticos F y valores p
507
  f_regression = ms_regression / ms_residual
508
  p_regression = 1 - f.cdf(f_regression, df_regression, df_residual)
509
-
510
  f_lack_of_fit = ms_lack_of_fit / ms_pure_error if not np.isnan(ms_lack_of_fit) else np.nan
511
  p_lack_of_fit = 1 - f.cdf(f_lack_of_fit, df_lack_of_fit, df_pure_error) if not np.isnan(f_lack_of_fit) else np.nan
512
 
513
- # 12. Crear la tabla ANOVA detallada
514
  detailed_anova_table = pd.DataFrame({
515
  'Fuente de Variación': ['Regresión', 'Residual', 'Falta de Ajuste', 'Error Puro', 'Total'],
516
  'Suma de Cuadrados': [ss_regression, ss_residual, ss_lack_of_fit, ss_pure_error, ss_total],
517
- 'Grados de Libertad': [df_regression, df_residual, df_lack_of_fit, df_pure_error, df_total],
518
  'Cuadrado Medio': [ms_regression, ms_residual, ms_lack_of_fit, ms_pure_error, np.nan],
519
  'F': [f_regression, np.nan, f_lack_of_fit, np.nan, np.nan],
520
  'Valor p': [p_regression, np.nan, p_lack_of_fit, np.nan, np.nan]
521
  })
522
-
523
- # Calcular la suma de cuadrados y estadísticos F para la curvatura
524
- ss_curvature = anova_reduced['sum_sq'][f'I({self.x1_name} ** 2)'] + \
525
- anova_reduced['sum_sq'][f'I({self.x2_name} ** 2)'] + \
526
- anova_reduced['sum_sq'][f'I({self.x3_name} ** 2)']
527
- df_curvature = 3
528
- ms_curvature = ss_curvature / df_curvature
529
- f_curvature = ms_curvature / ms_residual
530
- p_curvature = 1 - f.cdf(f_curvature, df_curvature, df_residual)
531
-
532
- # Añadir la fila de curvatura a la tabla ANOVA
533
- detailed_anova_table.loc[len(detailed_anova_table)] = [
534
- 'Curvatura',
535
- ss_curvature,
536
- df_curvature,
537
- ms_curvature,
538
- f_curvature,
539
- p_curvature
540
- ]
541
 
542
  # Reorganizar las filas y resetear el índice
543
- detailed_anova_table = detailed_anova_table.reindex([0, 5, 1, 2, 3, 4]).reset_index(drop=True)
544
 
545
  return detailed_anova_table.round(3)
546
 
@@ -550,12 +477,12 @@ class RSM_BoxBehnken:
550
  """
551
  prediction_table = self.generate_prediction_table()
552
  contribution_table = self.calculate_contribution_percentage()
553
- detailed_anova_table = self.calculate_detailed_anova()
554
 
555
  return {
556
  'Predicciones': prediction_table,
557
  '% Contribución': contribution_table,
558
- 'ANOVA Detallada': detailed_anova_table
559
  }
560
 
561
  def save_figures_to_zip(self):
@@ -674,58 +601,80 @@ class RSM_BoxBehnken:
674
 
675
  # --- Funciones para la Interfaz de Gradio ---
676
 
677
- def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str):
678
  """
679
- Carga los datos del diseño Box-Behnken desde cajas de texto y crea la instancia de RSM_BoxBehnken.
680
  """
681
  try:
682
- # Convertir los niveles a listas de números
683
- x1_levels = [float(x.strip()) for x in x1_levels_str.split(',')]
684
- x2_levels = [float(x.strip()) for x in x2_levels_str.split(',')]
685
- x3_levels = [float(x.strip()) for x in x3_levels_str.split(',')]
 
 
 
 
 
 
 
 
 
 
686
 
687
  # Crear DataFrame a partir de la cadena de datos
688
  data_list = [row.split(',') for row in data_str.strip().split('\n')]
689
- column_names = ['Exp.', x1_name, x2_name, x3_name, y_name]
690
  data = pd.DataFrame(data_list, columns=column_names)
691
  data = data.apply(pd.to_numeric, errors='coerce') # Convertir a numérico
692
 
693
- # Validar que el DataFrame tenga las columnas correctas
694
- if not all(col in data.columns for col in column_names):
695
- raise ValueError("El formato de los datos no es correcto.")
696
-
697
- # Crear la instancia de RSM_BoxBehnken
698
  global rsm
699
- rsm = RSM_BoxBehnken(data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels)
 
 
 
 
 
 
 
 
 
 
 
700
 
701
- return data.round(3), x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels, gr.update(visible=True)
702
-
703
  except Exception as e:
704
  # Mostrar mensaje de error
705
  error_message = f"Error al cargar los datos: {str(e)}"
706
  print(error_message)
707
- return None, "", "", "", "", [], [], [], gr.update(visible=False)
708
 
709
- def fit_and_optimize_model():
710
  if 'rsm' not in globals():
711
  return [None]*11 # Ajustar el número de outputs
712
 
 
 
 
713
  # Ajustar modelos y optimizar
714
  model_completo, pareto_completo = rsm.fit_model()
715
- model_simplificado, pareto_simplificado = rsm.fit_simplified_model()
 
716
  optimization_table = rsm.optimize()
717
  equation = rsm.get_simplified_equation()
718
  prediction_table = rsm.generate_prediction_table()
719
  contribution_table = rsm.calculate_contribution_percentage()
720
  anova_table = rsm.calculate_detailed_anova()
721
-
722
  # Generar todas las figuras y almacenarlas
723
  rsm.generate_all_plots()
724
-
725
  # Formatear la ecuación para que se vea mejor en Markdown
726
- equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ")
727
- equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}"
728
-
 
 
 
729
  # Guardar las tablas en Excel temporal
730
  excel_path = rsm.save_tables_to_excel()
731
 
@@ -746,32 +695,6 @@ def fit_and_optimize_model():
746
  excel_path # Ruta del Excel de tablas
747
  )
748
 
749
- def show_plot(current_index, all_figures):
750
- if not all_figures:
751
- return None, "No hay gráficos disponibles.", current_index
752
- selected_fig = all_figures[current_index]
753
- plot_info_text = f"Gráfico {current_index + 1} de {len(all_figures)}"
754
- return selected_fig, plot_info_text, current_index
755
-
756
- def navigate_plot(direction, current_index, all_figures):
757
- """
758
- Navega entre los gráficos.
759
- """
760
- if not all_figures:
761
- return None, "No hay gráficos disponibles.", current_index
762
-
763
- if direction == 'left':
764
- new_index = (current_index - 1) % len(all_figures)
765
- elif direction == 'right':
766
- new_index = (current_index + 1) % len(all_figures)
767
- else:
768
- new_index = current_index
769
-
770
- selected_fig = all_figures[new_index]
771
- plot_info_text = f"Gráfico {new_index + 1} de {len(all_figures)}"
772
-
773
- return selected_fig, plot_info_text, new_index
774
-
775
  def download_current_plot(all_figures, current_index):
776
  """
777
  Descarga la figura actual como PNG.
@@ -781,12 +704,12 @@ def download_current_plot(all_figures, current_index):
781
  fig = all_figures[current_index]
782
  img_bytes = rsm.save_fig_to_bytes(fig)
783
  filename = f"Grafico_RSM_{current_index + 1}.png"
784
-
785
  # Crear un archivo temporal
786
  with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
787
  temp_file.write(img_bytes)
788
  temp_path = temp_file.name
789
-
790
  return temp_path # Retornar solo la ruta
791
 
792
  def download_all_plots_zip():
@@ -797,8 +720,6 @@ def download_all_plots_zip():
797
  return None
798
  zip_path = rsm.save_figures_to_zip()
799
  if zip_path:
800
- filename = f"Graficos_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
801
- # Gradio no permite renombrar directamente, por lo que retornamos la ruta del archivo
802
  return zip_path
803
  return None
804
 
@@ -810,16 +731,16 @@ def download_all_tables_excel():
810
  return None
811
  excel_path = rsm.save_tables_to_excel()
812
  if excel_path:
813
- filename = f"Tablas_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
814
- # Gradio no permite renombrar directamente, por lo que retornamos la ruta del archivo
815
  return excel_path
816
  return None
817
 
818
- def exportar_word(rsm_instance, tables_dict):
819
  """
820
  Función para exportar las tablas a un documento de Word.
821
  """
822
- word_path = rsm_instance.export_tables_to_word(tables_dict)
 
 
823
  if word_path and os.path.exists(word_path):
824
  return word_path
825
  return None
@@ -828,19 +749,32 @@ def exportar_word(rsm_instance, tables_dict):
828
 
829
  def create_gradio_interface():
830
  with gr.Blocks() as demo:
831
- gr.Markdown("# Optimización de la producción de AIA usando RSM Box-Behnken")
832
-
833
  with gr.Row():
834
  with gr.Column():
835
  gr.Markdown("## Configuración del Diseño")
836
- x1_name_input = gr.Textbox(label="Nombre de la Variable X1 (ej. Glucosa)", value="Glucosa")
837
- x2_name_input = gr.Textbox(label="Nombre de la Variable X2 (ej. Extracto de Levadura)", value="Extracto_de_Levadura")
838
- x3_name_input = gr.Textbox(label="Nombre de la Variable X3 (ej. Triptófano)", value="Triptofano")
839
- y_name_input = gr.Textbox(label="Nombre de la Variable Dependiente (ej. AIA (ppm))", value="AIA_ppm")
840
- x1_levels_input = gr.Textbox(label="Niveles de X1 (separados por comas)", value="1, 3.5, 5.5")
841
- x2_levels_input = gr.Textbox(label="Niveles de X2 (separados por comas)", value="0.03, 0.2, 0.3")
842
- x3_levels_input = gr.Textbox(label="Niveles de X3 (separados por comas)", value="0.4, 0.65, 0.9")
843
- data_input = gr.Textbox(label="Datos del Experimento (formato CSV)", lines=10, value="""1,-1,-1,0,166.594
 
 
 
 
 
 
 
 
 
 
 
 
 
844
  2,1,-1,0,177.557
845
  3,-1,1,0,127.261
846
  4,1,1,0,147.573
@@ -854,13 +788,14 @@ def create_gradio_interface():
854
  12,0,1,1,148.621
855
  13,0,0,0,278.951
856
  14,0,0,0,297.238
857
- 15,0,0,0,280.896""")
 
858
  load_button = gr.Button("Cargar Datos")
859
-
860
  with gr.Column():
861
  gr.Markdown("## Datos Cargados")
862
  data_output = gr.Dataframe(label="Tabla de Datos", interactive=False)
863
-
864
  # Sección de análisis visible solo después de cargar los datos
865
  with gr.Row(visible=False) as analysis_row:
866
  with gr.Column():
@@ -873,41 +808,52 @@ def create_gradio_interface():
873
  pareto_simplificado_output = gr.Plot()
874
  gr.Markdown("**Ecuación del Modelo Simplificado**")
875
  equation_output = gr.HTML()
 
876
  optimization_table_output = gr.Dataframe(label="Tabla de Optimización", interactive=False)
 
877
  prediction_table_output = gr.Dataframe(label="Tabla de Predicciones", interactive=False)
 
878
  contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución", interactive=False)
 
879
  anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada", interactive=False)
880
  gr.Markdown("## Descargar Todas las Tablas")
881
- download_excel_button = gr.DownloadButton("Descargar Tablas en Excel")
882
- download_word_button = gr.DownloadButton("Descargar Tablas en Word")
883
-
884
  with gr.Column():
 
 
 
 
 
 
 
 
 
885
  gr.Markdown("## Generar Gráficos de Superficie de Respuesta")
886
- fixed_variable_input = gr.Dropdown(label="Variable Fija", choices=["Glucosa", "Extracto_de_Levadura", "Triptofano"], value="Glucosa")
887
- fixed_level_input = gr.Slider(label="Nivel de Variable Fija", minimum=-1, maximum=1, step=0.01, value=0.0)
888
  plot_button = gr.Button("Generar Gráficos")
889
  with gr.Row():
890
  left_button = gr.Button("<")
891
  right_button = gr.Button(">")
892
  rsm_plot_output = gr.Plot()
893
- plot_info = gr.Textbox(label="Información del Gráfico", value="Gráfico 1 de 9", interactive=False)
894
  with gr.Row():
895
- download_plot_button = gr.DownloadButton("Descargar Gráfico Actual (PNG)")
896
- download_all_plots_button = gr.DownloadButton("Descargar Todos los Gráficos (ZIP)")
897
  current_index_state = gr.State(0) # Estado para el índice actual
898
  all_figures_state = gr.State([]) # Estado para todas las figuras
899
-
900
- # Cargar datos
901
  load_button.click(
902
  load_data,
903
- inputs=[x1_name_input, x2_name_input, x3_name_input, y_name_input, x1_levels_input, x2_levels_input, x3_levels_input, data_input],
904
- 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]
905
  )
906
-
907
  # Ajustar modelo y optimizar
908
  fit_button.click(
909
  fit_and_optimize_model,
910
- inputs=[],
911
  outputs=[
912
  model_completo_output,
913
  pareto_completo_output,
@@ -918,23 +864,36 @@ def create_gradio_interface():
918
  prediction_table_output,
919
  contribution_table_output,
920
  anova_table_output,
921
- download_all_plots_button, # Ruta del ZIP de gráficos
922
- download_excel_button # Ruta del Excel de tablas
923
  ]
924
  )
 
 
 
 
 
 
 
925
 
 
 
 
 
 
 
926
  # Generar y mostrar los gráficos
927
  plot_button.click(
928
- lambda fixed_var, fixed_lvl: (
929
- rsm.plot_rsm_individual(fixed_var, fixed_lvl),
930
- f"Gráfico 1 de {len(rsm.all_figures)}" if rsm.all_figures else "No hay gráficos disponibles.",
931
  0,
932
- rsm.all_figures # Actualizar el estado de todas las figuras
933
  ),
934
- inputs=[fixed_variable_input, fixed_level_input],
935
  outputs=[rsm_plot_output, plot_info, current_index_state, all_figures_state]
936
  )
937
-
938
  # Navegación de gráficos
939
  left_button.click(
940
  lambda current_index, all_figures: navigate_plot('left', current_index, all_figures),
@@ -946,50 +905,116 @@ def create_gradio_interface():
946
  inputs=[current_index_state, all_figures_state],
947
  outputs=[rsm_plot_output, plot_info, current_index_state]
948
  )
949
-
950
  # Descargar gráfico actual
951
  download_plot_button.click(
952
  download_current_plot,
953
  inputs=[all_figures_state, current_index_state],
954
  outputs=download_plot_button
955
  )
956
-
957
  # Descargar todos los gráficos en ZIP
958
  download_all_plots_button.click(
959
  download_all_plots_zip,
960
  inputs=[],
961
  outputs=download_all_plots_button
962
  )
963
-
964
- # Descargar todas las tablas en Excel y Word
965
- download_excel_button.click(
966
- fn=lambda: download_all_tables_excel(),
967
- inputs=[],
968
- outputs=download_excel_button
969
- )
970
-
971
- download_word_button.click(
972
- fn=lambda: exportar_word(rsm, rsm.get_all_tables()),
973
- inputs=[],
974
- outputs=download_word_button
975
- )
976
-
977
  # Ejemplo de uso
978
  gr.Markdown("## Ejemplo de uso")
979
  gr.Markdown("""
980
- 1. Introduce los nombres de las variables y sus niveles en las cajas de texto correspondientes.
981
- 2. Copia y pega los datos del experimento en la caja de texto 'Datos del Experimento'.
982
- 3. Haz clic en 'Cargar Datos' para cargar los datos en la tabla.
983
- 4. Haz clic en 'Ajustar Modelo y Optimizar' para ajustar el modelo y encontrar los niveles óptimos de los factores.
984
- 5. Selecciona una variable fija y su nivel en los controles deslizantes.
985
- 6. Haz clic en 'Generar Gráficos' para generar los gráficos de superficie de respuesta.
986
- 7. Navega entre los gráficos usando los botones '<' y '>'.
987
- 8. Descarga el gráfico actual en PNG o descarga todos los gráficos en un ZIP.
988
- 9. Descarga todas las tablas en un archivo Excel o Word con los botones correspondientes.
 
 
 
 
 
 
 
 
989
  """)
990
 
991
  return demo
992
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
993
  # --- Función Principal ---
994
 
995
  def main():
@@ -997,4 +1022,4 @@ def main():
997
  interface.launch(share=True)
998
 
999
  if __name__ == "__main__":
1000
- main()
 
5
  import plotly.graph_objects as go
6
  from scipy.optimize import minimize
7
  import plotly.express as px
8
+ from scipy.stats import f
9
  import gradio as gr
10
  import io
11
  import zipfile
 
14
  import docx
15
  from docx.shared import Inches, Pt
16
  from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
 
17
  import os
18
+ from pyDOE import bbdesign, ccdesign
19
 
20
+ # --- Clase RSM_ExperimentalDesign ---
21
+ class RSM_ExperimentalDesign:
22
+ def __init__(self, data, design_type, factor_names, y_name, factor_levels):
23
  """
24
+ Inicializa la clase con los datos del diseño experimental.
25
+
26
+ Args:
27
+ data (pd.DataFrame): Datos del experimento.
28
+ design_type (str): Tipo de diseño ('Box-Behnken' o 'Central Compuesto').
29
+ factor_names (list): Lista de nombres de los factores.
30
+ y_name (str): Nombre de la variable dependiente.
31
+ factor_levels (dict): Diccionario con los niveles de cada factor.
32
  """
33
  self.data = data.copy()
34
+ self.design_type = design_type
35
+ self.factor_names = factor_names
36
+ self.y_name = y_name
37
+ self.factor_levels = factor_levels
38
  self.model = None
39
  self.model_simplified = None
40
  self.optimized_results = None
41
  self.optimal_levels = None
42
+ self.all_figures = []
 
 
 
 
 
 
 
 
 
43
 
44
+ def generate_design(self):
45
  """
46
+ Genera el diseño experimental basado en el tipo especificado y asigna los niveles naturales.
47
  """
48
+ num_factors = len(self.factor_names)
49
+ if self.design_type == 'Box-Behnken':
50
+ design = bbdesign(num_factors)
51
+ elif self.design_type == 'Central Compuesto':
52
+ design = ccdesign(num_factors, center=(3, 3)) # Puedes ajustar los puntos centrales si lo deseas
 
53
  else:
54
+ raise ValueError("Tipo de diseño no soportado. Elige 'Box-Behnken' o 'Central Compuesto'.")
55
+
56
+ # Asignar niveles naturales a las variables
57
+ for i, factor in enumerate(self.factor_names):
58
+ self.data[factor] = self.coded_to_natural(design[:, i], factor)
59
+
60
+ return self.data
61
+
62
+ def coded_to_natural(self, coded_value, variable_name):
63
+ """Convierte un valor codificado a su valor natural."""
64
+ levels = self.factor_levels[variable_name]
65
+ if len(levels) != 3:
66
+ raise ValueError(f"Se requieren exactamente 3 niveles para el factor '{variable_name}'.")
67
+ return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2
68
+
69
+ def natural_to_coded(self, natural_value, variable_name):
70
+ """Convierte un valor natural a su valor codificado."""
71
+ levels = self.factor_levels[variable_name]
72
+ return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
73
 
74
  def fit_model(self):
75
  """
76
  Ajusta el modelo de segundo orden completo a los datos.
77
  """
78
+ terms = self.factor_names.copy()
79
+ # Términos cuadráticos
80
+ terms += [f'I({var}**2)' for var in self.factor_names]
81
+ # Términos de interacción
82
+ for i in range(len(self.factor_names)):
83
+ for j in range(i+1, len(self.factor_names)):
84
+ terms.append(f'{self.factor_names[i]}:{self.factor_names[j]}')
85
+
86
+ formula = f'{self.y_name} ~ ' + ' + '.join(terms)
87
  self.model = smf.ols(formula, data=self.data).fit()
88
  print("Modelo Completo:")
89
  print(self.model.summary())
90
  return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
91
 
92
+ def fit_simplified_model(self, selected_factors):
93
  """
94
+ Ajusta el modelo simplificado basado en los factores seleccionados.
95
+
96
+ Args:
97
+ selected_factors (list): Lista de factores a incluir en el modelo.
98
  """
99
+ terms = selected_factors.copy()
100
+ # Términos cuadráticos
101
+ terms += [f'I({var}**2)' for var in selected_factors]
102
+ formula = f'{self.y_name} ~ ' + ' + '.join(terms)
103
  self.model_simplified = smf.ols(formula, data=self.data).fit()
104
  print("\nModelo Simplificado:")
105
  print(self.model_simplified.summary())
 
112
  if self.model_simplified is None:
113
  print("Error: Ajusta el modelo simplificado primero.")
114
  return
115
+
116
  def objective_function(x):
117
+ input_data = {var: [x[i]] for i, var in enumerate(self.selected_factors)}
118
+ return -self.model_simplified.predict(pd.DataFrame(input_data)).values[0]
119
+
120
+ # Definir límites codificados para cada factor
121
+ bounds = [(-1, 1) for _ in self.selected_factors]
122
+ x0 = [0] * len(self.selected_factors)
123
+
 
 
124
  self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
125
  self.optimal_levels = self.optimized_results.x
126
+
127
  # Convertir niveles óptimos de codificados a naturales
128
  optimal_levels_natural = [
129
+ self.coded_to_natural(self.optimal_levels[i], self.selected_factors[i])
130
+ for i in range(len(self.selected_factors))
 
131
  ]
132
+
133
  # Crear la tabla de optimización
134
  optimization_table = pd.DataFrame({
135
+ 'Variable': self.selected_factors,
136
  'Nivel Óptimo (Natural)': optimal_levels_natural,
137
  'Nivel Óptimo (Codificado)': self.optimal_levels
138
  })
139
+
140
+ return optimization_table.round(3)
141
 
142
  def plot_rsm_individual(self, fixed_variable, fixed_level):
143
  """
 
147
  print("Error: Ajusta el modelo simplificado primero.")
148
  return None
149
 
150
+ varying_variables = [var for var in self.selected_factors if var != fixed_variable]
151
+ if len(varying_variables) < 2:
152
+ print("Se requieren al menos dos variables para generar un gráfico de superficie.")
153
+ return None
154
 
155
+ var1, var2 = varying_variables[:2] # Solo tomar las dos primeras variables para el gráfico
156
+
157
+ # Determinar los niveles naturales para las variables que varían
158
+ x_natural_levels = self.factor_levels[var1]
159
+ y_natural_levels = self.factor_levels[var2]
160
 
161
  # Crear una malla de puntos para las variables que varían (en unidades naturales)
162
  x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
 
164
  x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
165
 
166
  # Convertir la malla de variables naturales a codificadas
167
+ x_grid_coded = self.natural_to_coded(x_grid_natural, var1)
168
+ y_grid_coded = self.natural_to_coded(y_grid_natural, var2)
169
 
170
  # Crear un DataFrame para la predicción con variables codificadas
171
  prediction_data = pd.DataFrame({
172
+ var1: x_grid_coded.flatten(),
173
+ var2: y_grid_coded.flatten(),
174
  })
175
+
176
+ # Asignar valores codificados a las otras variables fijas
177
+ for var in self.selected_factors:
178
+ if var not in [var1, var2]:
179
+ prediction_data[var] = self.natural_to_coded(fixed_level, var)
180
 
181
  # Calcular los valores predichos
182
  z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape)
183
 
184
+ # Crear el gráfico de superficie
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
185
  fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)])
186
 
187
+ # Añadir líneas de cuadrícula
 
188
  for i in range(x_grid_natural.shape[0]):
189
  fig.add_trace(go.Scatter3d(
190
  x=x_grid_natural[i, :],
 
195
  showlegend=False,
196
  hoverinfo='skip'
197
  ))
 
198
  for j in range(x_grid_natural.shape[1]):
199
  fig.add_trace(go.Scatter3d(
200
  x=x_grid_natural[:, j],
 
206
  hoverinfo='skip'
207
  ))
208
 
209
+ # Añadir los puntos de los experimentos
210
+ experiments_data = self.data.copy()
211
+ experiments_data['Predicho'] = self.model_simplified.predict(self.data[self.selected_factors])
212
  colors = px.colors.qualitative.Safe
213
  point_labels = [f"{row[self.y_name]:.3f}" for _, row in experiments_data.iterrows()]
214
 
215
  fig.add_trace(go.Scatter3d(
216
+ x=experiments_data[var1],
217
+ y=experiments_data[var2],
218
+ z=experiments_data[self.y_name],
219
  mode='markers+text',
220
+ marker=dict(size=4, color=colors[:len(experiments_data)]),
221
  text=point_labels,
222
  textposition='top center',
223
  name='Experimentos'
 
226
  # Añadir etiquetas y título con variables naturales
227
  fig.update_layout(
228
  scene=dict(
229
+ xaxis_title=f"{var1} ({self.get_units(var1)})",
230
+ yaxis_title=f"{var2} ({self.get_units(var2)})",
231
  zaxis_title=self.y_name,
232
  ),
233
+ title=f"{self.y_name} vs {var1} y {var2}<br><sup>{fixed_variable} fijo en {fixed_level:.3f} ({self.get_units(fixed_variable)}) (Modelo Simplificado)</sup>",
234
  height=800,
235
  width=1000,
236
  showlegend=True
 
245
  units = {
246
  'Glucosa': 'g/L',
247
  'Extracto_de_Levadura': 'g/L',
248
+ 'Triptófano': 'g/L',
249
+ 'AIA_ppm': 'ppm',
250
+ # Agrega más unidades según tus variables
251
  }
252
  return units.get(variable_name, '')
253
 
 
263
  self.all_figures = [] # Resetear la lista de figuras
264
 
265
  # Niveles naturales para graficar
266
+ levels_to_plot_natural = self.factor_levels
 
 
 
 
267
 
268
  # Generar y almacenar gráficos individuales
269
+ for fixed_variable in self.selected_factors:
270
+ for level in self.factor_levels[fixed_variable]:
271
  fig = self.plot_rsm_individual(fixed_variable, level)
272
  if fig is not None:
273
  self.all_figures.append(fig)
274
 
 
 
 
 
 
 
 
 
 
 
275
  def pareto_chart(self, model, title):
276
  """
277
  Genera un diagrama de Pareto para los efectos usando estadísticos F,
278
  incluyendo la línea de significancia.
279
  """
280
  # Calcular los estadísticos F para cada término
 
281
  fvalues = model.tvalues[1:]**2 # Excluir la Intercept y convertir t a F
282
  abs_fvalues = np.abs(fvalues)
283
  sorted_idx = np.argsort(abs_fvalues)[::-1]
 
320
 
321
  for term, coef in coefficients.items():
322
  if term != 'Intercept':
323
+ if term.startswith('I('):
324
+ equation += f" + {coef:.3f}*{term[2:-1]}"
325
+ elif ':' in term:
326
+ equation += f" + {coef:.3f}*{term}"
327
+ else:
328
+ equation += f" + {coef:.3f}*{term}"
329
+
 
 
 
 
 
 
330
  return equation
331
+
332
  def generate_prediction_table(self):
333
  """
334
  Genera una tabla con los valores actuales, predichos y residuales.
 
337
  print("Error: Ajusta el modelo simplificado primero.")
338
  return None
339
 
340
+ self.data['Predicho'] = self.model_simplified.predict(self.data[self.selected_factors])
341
  self.data['Residual'] = self.data[self.y_name] - self.data['Predicho']
342
 
343
  return self.data[[self.y_name, 'Predicho', 'Residual']].round(3)
 
369
 
370
  # Calcular estadísticos F y porcentaje de contribución para cada factor
371
  ms_error = anova_table.loc['Residual', 'sum_sq'] / anova_table.loc['Residual', 'df']
 
 
 
 
 
 
 
 
372
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
373
  # Agregar filas para cada término del modelo
374
  for index, row in anova_table.iterrows():
375
  if index != 'Residual':
376
  factor_name = index
377
+ if factor_name.startswith('I('):
378
+ factor_name = factor_name[2:-1] # Quitar 'I(' y ')'
 
 
 
 
 
379
  ss_factor = row['sum_sq']
380
  df_factor = row['df']
381
  ms_factor = ss_factor / df_factor
 
394
  })], ignore_index=True)
395
 
396
  # Agregar fila para Cor Total
 
 
 
397
  contribution_table = pd.concat([contribution_table, pd.DataFrame({
398
  'Fuente de Variación': ['Cor Total'],
399
+ 'Suma de Cuadrados': [ss_total],
400
+ 'Grados de Libertad': [len(self.data) - 1],
401
  'Cuadrado Medio': [np.nan],
402
  'F': [np.nan],
403
  'Valor p': [np.nan],
 
414
  print("Error: Ajusta el modelo simplificado primero.")
415
  return None
416
 
417
+ # ANOVA del modelo simplificado
418
+ anova_reduced = sm.stats.anova_lm(self.model_simplified, typ=2)
 
 
 
 
 
 
419
 
420
+ # Suma de cuadrados total
421
  ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
422
 
423
+ # Suma de cuadrados de la regresión
424
+ ss_regression = anova_reduced['sum_sq'].sum()
 
 
 
425
 
426
+ # Grados de libertad de la regresión
427
+ df_regression = anova_reduced['df'].sum()
428
 
429
+ # Suma de cuadrados del error residual
430
+ ss_residual = anova_reduced.loc['Residual', 'sum_sq']
431
+ df_residual = anova_reduced.loc['Residual', 'df']
432
 
433
+ # Suma de cuadrados del error puro (si hay réplicas)
434
+ duplicates = self.data.duplicated(subset=self.selected_factors, keep=False)
435
+ if duplicates.any():
436
+ ss_pure_error = self.data[duplicates].groupby(self.selected_factors)[self.y_name].var().sum() * self.data[duplicates].groupby(self.selected_factors).ngroups()
437
+ df_pure_error = self.data[duplicates].shape[0] - self.data[duplicates].groupby(self.selected_factors).ngroups()
438
  else:
439
  ss_pure_error = np.nan
440
  df_pure_error = np.nan
441
 
442
+ # Suma de cuadrados de la falta de ajuste
443
  ss_lack_of_fit = ss_residual - ss_pure_error if not np.isnan(ss_pure_error) else np.nan
444
  df_lack_of_fit = df_residual - df_pure_error if not np.isnan(df_pure_error) else np.nan
445
 
446
+ # Cuadrados medios
447
  ms_regression = ss_regression / df_regression
448
  ms_residual = ss_residual / df_residual
449
  ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit if not np.isnan(ss_lack_of_fit) else np.nan
450
  ms_pure_error = ss_pure_error / df_pure_error if not np.isnan(ss_pure_error) else np.nan
451
 
452
+ # Estadísticos F y valores p
453
  f_regression = ms_regression / ms_residual
454
  p_regression = 1 - f.cdf(f_regression, df_regression, df_residual)
455
+
456
  f_lack_of_fit = ms_lack_of_fit / ms_pure_error if not np.isnan(ms_lack_of_fit) else np.nan
457
  p_lack_of_fit = 1 - f.cdf(f_lack_of_fit, df_lack_of_fit, df_pure_error) if not np.isnan(f_lack_of_fit) else np.nan
458
 
459
+ # Crear la tabla ANOVA detallada
460
  detailed_anova_table = pd.DataFrame({
461
  'Fuente de Variación': ['Regresión', 'Residual', 'Falta de Ajuste', 'Error Puro', 'Total'],
462
  'Suma de Cuadrados': [ss_regression, ss_residual, ss_lack_of_fit, ss_pure_error, ss_total],
463
+ 'Grados de Libertad': [df_regression, df_residual, df_lack_of_fit, df_pure_error, len(self.data) - 1],
464
  'Cuadrado Medio': [ms_regression, ms_residual, ms_lack_of_fit, ms_pure_error, np.nan],
465
  'F': [f_regression, np.nan, f_lack_of_fit, np.nan, np.nan],
466
  'Valor p': [p_regression, np.nan, p_lack_of_fit, np.nan, np.nan]
467
  })
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
468
 
469
  # Reorganizar las filas y resetear el índice
470
+ detailed_anova_table = detailed_anova_table.reindex([0, 1, 2, 3, 4]).reset_index(drop=True)
471
 
472
  return detailed_anova_table.round(3)
473
 
 
477
  """
478
  prediction_table = self.generate_prediction_table()
479
  contribution_table = self.calculate_contribution_percentage()
480
+ anova_table = self.calculate_detailed_anova()
481
 
482
  return {
483
  'Predicciones': prediction_table,
484
  '% Contribución': contribution_table,
485
+ 'ANOVA Detallada': anova_table
486
  }
487
 
488
  def save_figures_to_zip(self):
 
601
 
602
  # --- Funciones para la Interfaz de Gradio ---
603
 
604
+ def load_data(design_type, factor_names, factor_levels, y_name, data_str):
605
  """
606
+ Carga los datos del diseño experimental desde las entradas y crea la instancia de RSM_ExperimentalDesign.
607
  """
608
  try:
609
+ # Parsear nombres de factores
610
+ factor_names = [fn.strip() for fn in factor_names.split(',')]
611
+ num_factors = len(factor_names)
612
+
613
+ # Parsear niveles de factores
614
+ factor_levels_dict = {}
615
+ levels = factor_levels.split(';')
616
+ if len(levels) != num_factors:
617
+ raise ValueError(f"Se esperaban {num_factors} conjuntos de niveles separados por ';'.")
618
+ for i, level_str in enumerate(levels):
619
+ level_values = [float(x.strip()) for x in level_str.split(',')]
620
+ if len(level_values) != 3:
621
+ raise ValueError(f"El factor '{factor_names[i]}' requiere exactamente 3 niveles separados por comas.")
622
+ factor_levels_dict[factor_names[i]] = level_values
623
 
624
  # Crear DataFrame a partir de la cadena de datos
625
  data_list = [row.split(',') for row in data_str.strip().split('\n')]
626
+ column_names = ['Exp.'] + factor_names + [y_name]
627
  data = pd.DataFrame(data_list, columns=column_names)
628
  data = data.apply(pd.to_numeric, errors='coerce') # Convertir a numérico
629
 
630
+ # Crear la instancia de RSM_ExperimentalDesign
 
 
 
 
631
  global rsm
632
+ rsm = RSM_ExperimentalDesign(
633
+ data=data,
634
+ design_type=design_type,
635
+ factor_names=factor_names,
636
+ y_name=y_name,
637
+ factor_levels=factor_levels_dict
638
+ )
639
+
640
+ # Generar el diseño
641
+ rsm.generate_design()
642
+
643
+ return data.round(3), gr.update(visible=True), factor_names
644
 
 
 
645
  except Exception as e:
646
  # Mostrar mensaje de error
647
  error_message = f"Error al cargar los datos: {str(e)}"
648
  print(error_message)
649
+ return None, gr.update(visible=False), []
650
 
651
+ def fit_and_optimize_model(selected_factors):
652
  if 'rsm' not in globals():
653
  return [None]*11 # Ajustar el número de outputs
654
 
655
+ if not selected_factors:
656
+ return [None]*11 # No se han seleccionado factores
657
+
658
  # Ajustar modelos y optimizar
659
  model_completo, pareto_completo = rsm.fit_model()
660
+ rsm.selected_factors = selected_factors
661
+ model_simplificado, pareto_simplificado = rsm.fit_simplified_model(selected_factors)
662
  optimization_table = rsm.optimize()
663
  equation = rsm.get_simplified_equation()
664
  prediction_table = rsm.generate_prediction_table()
665
  contribution_table = rsm.calculate_contribution_percentage()
666
  anova_table = rsm.calculate_detailed_anova()
667
+
668
  # Generar todas las figuras y almacenarlas
669
  rsm.generate_all_plots()
670
+
671
  # Formatear la ecuación para que se vea mejor en Markdown
672
+ if equation:
673
+ equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ")
674
+ equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}"
675
+ else:
676
+ equation_formatted = "No se pudo generar la ecuación del modelo simplificado."
677
+
678
  # Guardar las tablas en Excel temporal
679
  excel_path = rsm.save_tables_to_excel()
680
 
 
695
  excel_path # Ruta del Excel de tablas
696
  )
697
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
698
  def download_current_plot(all_figures, current_index):
699
  """
700
  Descarga la figura actual como PNG.
 
704
  fig = all_figures[current_index]
705
  img_bytes = rsm.save_fig_to_bytes(fig)
706
  filename = f"Grafico_RSM_{current_index + 1}.png"
707
+
708
  # Crear un archivo temporal
709
  with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
710
  temp_file.write(img_bytes)
711
  temp_path = temp_file.name
712
+
713
  return temp_path # Retornar solo la ruta
714
 
715
  def download_all_plots_zip():
 
720
  return None
721
  zip_path = rsm.save_figures_to_zip()
722
  if zip_path:
 
 
723
  return zip_path
724
  return None
725
 
 
731
  return None
732
  excel_path = rsm.save_tables_to_excel()
733
  if excel_path:
 
 
734
  return excel_path
735
  return None
736
 
737
+ def exportar_word(tables_dict):
738
  """
739
  Función para exportar las tablas a un documento de Word.
740
  """
741
+ if 'rsm' not in globals():
742
+ return None
743
+ word_path = rsm.export_tables_to_word(tables_dict)
744
  if word_path and os.path.exists(word_path):
745
  return word_path
746
  return None
 
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")
753
+
754
  with gr.Row():
755
  with gr.Column():
756
  gr.Markdown("## Configuración del Diseño")
757
+ design_type_input = gr.Dropdown(
758
+ label="Tipo de Diseño",
759
+ choices=["Box-Behnken", "Central Compuesto"],
760
+ value="Box-Behnken"
761
+ )
762
+ factor_names_input = gr.Textbox(
763
+ label="Nombres de los Factores (separados por comas)",
764
+ value="Glucosa, Extracto_de_Levadura, Triptófano"
765
+ )
766
+ factor_levels_input = gr.Textbox(
767
+ label="Niveles de los Factores (cada conjunto separado por ';' y niveles separados por comas)",
768
+ value="1, 3.5, 5.5; 0.03, 0.2, 0.3; 0.4, 0.65, 0.9"
769
+ )
770
+ y_name_input = gr.Textbox(
771
+ label="Nombre de la Variable Dependiente (ej. AIA_ppm)",
772
+ value="AIA_ppm"
773
+ )
774
+ data_input = gr.Textbox(
775
+ label="Datos del Experimento (formato CSV)",
776
+ lines=10,
777
+ value="""1,-1,-1,0,166.594
778
  2,1,-1,0,177.557
779
  3,-1,1,0,127.261
780
  4,1,1,0,147.573
 
788
  12,0,1,1,148.621
789
  13,0,0,0,278.951
790
  14,0,0,0,297.238
791
+ 15,0,0,0,280.896"""
792
+ )
793
  load_button = gr.Button("Cargar Datos")
794
+
795
  with gr.Column():
796
  gr.Markdown("## Datos Cargados")
797
  data_output = gr.Dataframe(label="Tabla de Datos", interactive=False)
798
+
799
  # Sección de análisis visible solo después de cargar los datos
800
  with gr.Row(visible=False) as analysis_row:
801
  with gr.Column():
 
808
  pareto_simplificado_output = gr.Plot()
809
  gr.Markdown("**Ecuación del Modelo Simplificado**")
810
  equation_output = gr.HTML()
811
+ gr.Markdown("**Tabla de Optimización**")
812
  optimization_table_output = gr.Dataframe(label="Tabla de Optimización", interactive=False)
813
+ gr.Markdown("**Tabla de Predicciones**")
814
  prediction_table_output = gr.Dataframe(label="Tabla de Predicciones", interactive=False)
815
+ gr.Markdown("**Tabla de % de Contribución**")
816
  contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución", interactive=False)
817
+ gr.Markdown("**Tabla ANOVA Detallada**")
818
  anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada", interactive=False)
819
  gr.Markdown("## Descargar Todas las Tablas")
820
+ download_excel_button = gr.File(label="Descargar Tablas en Excel", visible=False)
821
+ download_word_button = gr.File(label="Descargar Tablas en Word", visible=False)
822
+
823
  with gr.Column():
824
+ gr.Markdown("## Selección de Factores para el Modelo Simplificado")
825
+ selected_factors_input = gr.CheckboxGroup(
826
+ label="Selecciona los Factores a Incluir",
827
+ choices=[], # Actualizar dinámicamente
828
+ value=[]
829
+ )
830
+ fit_button_2 = gr.Button("Aplicar Selección de Factores")
831
+ gr.Markdown("## Resultados de Optimización")
832
+ optimization_table_output_2 = gr.Dataframe(label="Tabla de Optimización", interactive=False)
833
  gr.Markdown("## Generar Gráficos de Superficie de Respuesta")
 
 
834
  plot_button = gr.Button("Generar Gráficos")
835
  with gr.Row():
836
  left_button = gr.Button("<")
837
  right_button = gr.Button(">")
838
  rsm_plot_output = gr.Plot()
839
+ plot_info = gr.Textbox(label="Información del Gráfico", value="Gráfico 1 de 0", interactive=False)
840
  with gr.Row():
841
+ download_plot_button = gr.File(label="Descargar Gráfico Actual (PNG)", visible=False)
842
+ download_all_plots_button = gr.File(label="Descargar Todos los Gráficos (ZIP)", visible=False)
843
  current_index_state = gr.State(0) # Estado para el índice actual
844
  all_figures_state = gr.State([]) # Estado para todas las figuras
845
+
846
+ # Funciones de carga y ajuste
847
  load_button.click(
848
  load_data,
849
+ inputs=[design_type_input, factor_names_input, factor_levels_input, y_name_input, data_input],
850
+ outputs=[data_output, analysis_row, selected_factors_input]
851
  )
852
+
853
  # Ajustar modelo y optimizar
854
  fit_button.click(
855
  fit_and_optimize_model,
856
+ inputs=[selected_factors_input],
857
  outputs=[
858
  model_completo_output,
859
  pareto_completo_output,
 
864
  prediction_table_output,
865
  contribution_table_output,
866
  anova_table_output,
867
+ download_all_plots_button,
868
+ download_excel_button
869
  ]
870
  )
871
+
872
+ # Descargar todas las tablas en Excel y Word
873
+ download_excel_button.click(
874
+ fn=lambda: download_all_tables_excel(),
875
+ inputs=[],
876
+ outputs=download_excel_button
877
+ )
878
 
879
+ download_word_button.click(
880
+ fn=lambda: exportar_word(rsm.get_all_tables()),
881
+ inputs=[],
882
+ outputs=download_word_button
883
+ )
884
+
885
  # Generar y mostrar los gráficos
886
  plot_button.click(
887
+ lambda: (
888
+ None, # Placeholder, se actualizará después
889
+ "No hay gráficos disponibles.",
890
  0,
891
+ []
892
  ),
893
+ inputs=[],
894
  outputs=[rsm_plot_output, plot_info, current_index_state, all_figures_state]
895
  )
896
+
897
  # Navegación de gráficos
898
  left_button.click(
899
  lambda current_index, all_figures: navigate_plot('left', current_index, all_figures),
 
905
  inputs=[current_index_state, all_figures_state],
906
  outputs=[rsm_plot_output, plot_info, current_index_state]
907
  )
908
+
909
  # Descargar gráfico actual
910
  download_plot_button.click(
911
  download_current_plot,
912
  inputs=[all_figures_state, current_index_state],
913
  outputs=download_plot_button
914
  )
915
+
916
  # Descargar todos los gráficos en ZIP
917
  download_all_plots_button.click(
918
  download_all_plots_zip,
919
  inputs=[],
920
  outputs=download_all_plots_button
921
  )
922
+
 
 
 
 
 
 
 
 
 
 
 
 
 
923
  # Ejemplo de uso
924
  gr.Markdown("## Ejemplo de uso")
925
  gr.Markdown("""
926
+ 1. **Configura el Diseño:**
927
+ - Selecciona el tipo de diseño (Box-Behnken o Central Compuesto).
928
+ - Ingresa los nombres de los factores separados por comas.
929
+ - Ingresa los niveles de cada factor separados por comas y cada conjunto de niveles por ';'.
930
+ - Especifica el nombre de la variable dependiente.
931
+ - Proporciona los datos del experimento en formato CSV.
932
+ 2. **Cargar Datos:**
933
+ - Haz clic en 'Cargar Datos' para cargar y visualizar los datos.
934
+ 3. **Ajustar Modelo y Optimizar:**
935
+ - Selecciona los factores que deseas incluir en el modelo simplificado.
936
+ - Haz clic en 'Ajustar Modelo y Optimizar' para ajustar los modelos y obtener los resultados.
937
+ 4. **Generar Gráficos:**
938
+ - Haz clic en 'Generar Gráficos' para crear las superficies de respuesta.
939
+ - Navega entre los gráficos usando los botones '<' y '>'.
940
+ - Descarga el gráfico actual en PNG o descarga todos los gráficos en un ZIP.
941
+ 5. **Descargar Tablas:**
942
+ - Descarga todas las tablas generadas en un archivo Excel o Word.
943
  """)
944
 
945
  return demo
946
 
947
+ def navigate_plot(direction, current_index, all_figures):
948
+ """
949
+ Navega entre los gráficos.
950
+ """
951
+ if not all_figures:
952
+ return None, "No hay gráficos disponibles.", current_index
953
+
954
+ if direction == 'left':
955
+ new_index = (current_index - 1) % len(all_figures)
956
+ elif direction == 'right':
957
+ new_index = (current_index + 1) % len(all_figures)
958
+ else:
959
+ new_index = current_index
960
+
961
+ selected_fig = all_figures[new_index]
962
+ plot_info_text = f"Gráfico {new_index + 1} de {len(all_figures)}"
963
+
964
+ return selected_fig, plot_info_text, new_index
965
+
966
+ # --- Funciones de descarga y exportación ---
967
+
968
+ def download_current_plot(all_figures, current_index):
969
+ """
970
+ Descarga la figura actual como PNG.
971
+ """
972
+ if not all_figures:
973
+ return None
974
+ fig = all_figures[current_index]
975
+ img_bytes = rsm.save_fig_to_bytes(fig)
976
+ filename = f"Grafico_RSM_{current_index + 1}.png"
977
+
978
+ # Crear un archivo temporal
979
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
980
+ temp_file.write(img_bytes)
981
+ temp_path = temp_file.name
982
+
983
+ return temp_path # Retornar solo la ruta
984
+
985
+ def download_all_plots_zip():
986
+ """
987
+ Descarga todas las figuras en un archivo ZIP.
988
+ """
989
+ if 'rsm' not in globals():
990
+ return None
991
+ zip_path = rsm.save_figures_to_zip()
992
+ if zip_path:
993
+ return zip_path
994
+ return None
995
+
996
+ def download_all_tables_excel():
997
+ """
998
+ Descarga todas las tablas en un archivo Excel con múltiples hojas.
999
+ """
1000
+ if 'rsm' not in globals():
1001
+ return None
1002
+ excel_path = rsm.save_tables_to_excel()
1003
+ if excel_path:
1004
+ return excel_path
1005
+ return None
1006
+
1007
+ def exportar_word(tables_dict):
1008
+ """
1009
+ Función para exportar las tablas a un documento de Word.
1010
+ """
1011
+ if not tables_dict:
1012
+ return None
1013
+ word_path = rsm.export_tables_to_word(tables_dict)
1014
+ if word_path and os.path.exists(word_path):
1015
+ return word_path
1016
+ return None
1017
+
1018
  # --- Función Principal ---
1019
 
1020
  def main():
 
1022
  interface.launch(share=True)
1023
 
1024
  if __name__ == "__main__":
1025
+ main()