C2MV commited on
Commit
070ced0
verified
1 Parent(s): b5cb432

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +316 -392
app.py CHANGED
@@ -4,9 +4,10 @@ import pandas as pd
4
  import statsmodels.formula.api as smf
5
  import statsmodels.api as sm
6
  import plotly.graph_objects as go
7
- from scipy.optimize import minimize
8
  import plotly.express as px
9
- from scipy.stats import f
 
 
10
  import gradio as gr
11
  import io
12
  import zipfile
@@ -15,478 +16,401 @@ from datetime import datetime
15
  import docx
16
  from docx.shared import Pt
17
  from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
18
- import os
19
 
20
- # --- Clase RSM_BoxBehnken (Mejorada) ---
21
  class RSM_BoxBehnken:
22
  def __init__(self, data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels):
23
  self.data = data.copy()
24
- self.model = None
25
- self.model_simplified = None
26
- self.optimized_results = None
27
- self.optimal_levels = None
28
- self.all_figures = [] # Lista para almacenar todas las figuras generadas
29
- self.all_tables = {} # Diccionario para almacenar todas las tablas
30
- self.x1_name = x1_name
31
- self.x2_name = x2_name
32
- self.x3_name = x3_name
33
- self.y_name = y_name
34
- self.x1_levels = x1_levels
35
- self.x2_levels = x2_levels
36
- self.x3_levels = x3_levels
37
-
38
- def get_levels(self, variable_name):
39
- if variable_name == self.x1_name: return self.x1_levels
40
- elif variable_name == self.x2_name: return self.x2_levels
41
- elif variable_name == self.x3_name: return self.x3_levels
42
- else: raise ValueError(f"Variable desconocida: {variable_name}")
43
 
44
- def get_units(self, variable_name):
45
- units = {'Glucosa': 'g/L', 'Extracto_de_Levadura': 'g/L', 'Triptofano': 'g/L', 'AIA_ppm': 'ppm'}
46
- return units.get(variable_name, '')
47
-
48
- def fit_model(self):
49
- formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2) + {self.x1_name}:{self.x2_name} + {self.x1_name}:{self.x3_name} + {self.x2_name}:{self.x3_name}'
50
- self.model = smf.ols(formula, data=self.data).fit()
51
- return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
52
-
53
- def fit_simplified_model(self):
54
- # Determinar t茅rminos significativos del modelo completo (p < 0.05)
55
- pvalues = self.model.pvalues[1:] # Excluir intercepto
56
- significant_terms = pvalues[pvalues < 0.05].index.tolist()
57
 
58
- # Siempre incluir t茅rminos lineales y cuadr谩ticos puros si sus interacciones son significativas
59
- base_terms = [self.x1_name, self.x2_name, self.x3_name,
60
- f'I({self.x1_name} ** 2)', f'I({self.x2_name} ** 2)', f'I({self.x3_name} ** 2)']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
61
 
62
- final_terms = sorted(list(set(base_terms + significant_terms)))
 
 
63
 
64
- formula_simplified = f'{self.y_name} ~ {" + ".join(final_terms)}'
65
 
66
- self.model_simplified = smf.ols(formula_simplified, data=self.data).fit()
67
- return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Modelo Simplificado")
 
 
 
68
 
69
- def optimize(self, method='Nelder-Mead'):
70
- if self.model_simplified is None: return
 
 
 
 
 
71
  def objective_function(x):
72
- return -self.model_simplified.predict(pd.DataFrame({self.x1_name: [x[0]], self.x2_name: [x[1]], self.x3_name: [x[2]]})).values[0]
73
-
 
 
 
74
  bounds = [(-1, 1), (-1, 1), (-1, 1)]
75
- x0 = [0, 0, 0]
76
- self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
77
- self.optimal_levels = self.optimized_results.x
78
- optimal_levels_natural = [self.coded_to_natural(val, name) for val, name in zip(self.optimal_levels, [self.x1_name, self.x2_name, self.x3_name])]
79
 
80
- predicted_max_y = -self.optimized_results.fun
81
-
82
- optimization_table = pd.DataFrame({
83
- 'Variable': [self.x1_name, self.x2_name, self.x3_name, f'**{self.y_name} (Predicho)**'],
84
- 'Nivel 脫ptimo (Natural)': optimal_levels_natural + [f'**{predicted_max_y:.3f}**'],
85
- 'Nivel 脫ptimo (Codificado)': list(self.optimal_levels) + ['-']
 
 
86
  })
87
- self.all_tables['Optimizacion'] = optimization_table.round(3)
88
- return self.all_tables['Optimizacion']
89
-
90
- def plot_rsm_individual(self, fixed_variable, fixed_level):
91
- if self.model_simplified is None: return None
92
- varying_vars = [v for v in [self.x1_name, self.x2_name, self.x3_name] if v != fixed_variable]
93
- x_var, y_var = varying_vars[0], varying_vars[1]
94
-
95
- x_natural_levels = self.get_levels(x_var)
96
- y_natural_levels = self.get_levels(y_var)
97
 
98
- x_range = np.linspace(x_natural_levels[0], x_natural_levels[2], 50)
99
- y_range = np.linspace(y_natural_levels[0], y_natural_levels[2], 50)
100
- x_grid, y_grid = np.meshgrid(x_range, y_range)
101
 
102
- pred_data = pd.DataFrame({
103
- x_var: self.natural_to_coded(x_grid.flatten(), x_var),
104
- y_var: self.natural_to_coded(y_grid.flatten(), y_var)
105
- })
106
- pred_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
 
 
107
 
108
- z_pred = self.model_simplified.predict(pred_data).values.reshape(x_grid.shape)
109
-
110
- fig = go.Figure(data=[go.Surface(z=z_pred, x=x_range, y=y_range, colorscale='Viridis', opacity=0.8)])
111
-
112
- # A帽adir puntos experimentales
113
- exp_data = self.data[np.isclose(self.data[fixed_variable], self.natural_to_coded(fixed_level, fixed_variable))]
114
- if not exp_data.empty:
115
- fig.add_trace(go.Scatter3d(
116
- x=self.coded_to_natural(exp_data[x_var], x_var),
117
- y=self.coded_to_natural(exp_data[y_var], y_var),
118
- z=exp_data[self.y_name],
119
- mode='markers',
120
- marker=dict(size=5, color='red', symbol='circle'),
121
- name='Puntos Experimentales'
122
- ))
123
-
124
- fig.update_layout(
125
- title=f"{self.y_name} vs {x_var} y {y_var}<br><sup>{fixed_variable} fijo en {fixed_level:.2f} {self.get_units(fixed_variable)}</sup>",
126
- scene=dict(xaxis_title=f"{x_var} ({self.get_units(x_var)})",
127
- yaxis_title=f"{y_var} ({self.get_units(y_var)})",
128
- zaxis_title=f"{self.y_name} ({self.get_units(self.y_name)})"),
129
- height=600, margin=dict(l=0, r=0, b=0, t=40)
130
- )
131
- return fig
132
-
133
- def generate_all_plots(self):
134
- if self.model_simplified is None: return
135
- self.all_figures.clear()
136
- variables = [self.x1_name, self.x2_name, self.x3_name]
137
- for i in range(3):
138
- fixed_variable = variables[i]
139
- levels_to_plot = self.get_levels(fixed_variable)
140
- for level in levels_to_plot:
141
- fig = self.plot_rsm_individual(fixed_variable, level)
142
- if fig: self.all_figures.append(fig)
143
- return self.all_figures
144
-
145
- def coded_to_natural(self, coded_value, var_name):
146
- levels = self.get_levels(var_name)
147
- return np.interp(coded_value, [-1, 1], [levels[0], levels[2]])
148
-
149
- def natural_to_coded(self, natural_value, var_name):
150
- levels = self.get_levels(var_name)
151
- return np.interp(natural_value, [levels[0], levels[2]], [-1, 1])
152
-
153
- def pareto_chart(self, model, title):
154
- fvalues = model.tvalues[1:]**2
155
- abs_fvalues = np.abs(fvalues)
156
- sorted_idx = np.argsort(abs_fvalues)
157
- sorted_fvalues = abs_fvalues.iloc[sorted_idx]
158
- sorted_names = fvalues.index[sorted_idx]
159
-
160
- f_critical = f.ppf(1 - 0.05, 1, model.df_resid)
161
-
162
- fig = px.bar(x=sorted_fvalues, y=sorted_names, orientation='h',
163
- labels={'x': 'Estad铆stico F', 'y': 'T茅rmino del Modelo'}, title=title)
164
- fig.add_vline(x=f_critical, line_dash="dot", annotation_text=f"F-cr铆tico ({0.05*100}%) = {f_critical:.2f}")
165
- return fig
166
-
167
- def get_simplified_equation(self):
168
- if not self.model_simplified: return "N/A"
169
- params = self.model_simplified.params
170
- eq = f"<b>{self.y_name}</b> = {params['Intercept']:.4f}"
171
- for term, coef in params.items():
172
- if term == 'Intercept': continue
173
- term_name = term.replace('I(', '').replace('**2', '<sup>2</sup>').replace(')', '').replace('_', ' ')
174
- sign = "+" if coef >= 0 else "-"
175
- eq += f" {sign} {abs(coef):.4f} * {term_name}"
176
- return eq
177
-
178
- def generate_prediction_table(self):
179
- if self.model_simplified is None: return pd.DataFrame()
180
- self.data['Predicho'] = self.model_simplified.predict(self.data)
181
- self.data['Residual'] = self.data[self.y_name] - self.data['Predicho']
182
- table = self.data[[self.y_name, 'Predicho', 'Residual']].round(3)
183
- self.all_tables['Predicciones'] = table
184
- return table
185
-
186
- # --- NUEVOS M脡TODOS ESTAD脥STICOS ---
187
- def calculate_contribution_percentage(self):
188
- if self.model_simplified is None: return pd.DataFrame()
189
- anova_table = sm.stats.anova_lm(self.model_simplified, typ=2)
190
- anova_table.loc['Residual', 'sum_sq'] = self.model_simplified.ssr
191
- anova_table.loc['Residual', 'df'] = self.model_simplified.df_resid
192
- ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
193
-
194
- anova_table['% Contribuci贸n'] = (anova_table['sum_sq'] / ss_total) * 100
195
-
196
- # Formatear tabla para presentaci贸n
197
- contribution = anova_table[['sum_sq', 'df', 'F', 'PR(>F)', '% Contribuci贸n']].reset_index()
198
- contribution.rename(columns={'index': 'Fuente', 'sum_sq': 'Suma Cuadrados', 'df': 'GL', 'PR(>F)': 'p-valor'}, inplace=True)
199
- self.all_tables['Contribucion'] = contribution.round(4)
200
- return self.all_tables['Contribucion']
201
-
202
- def calculate_detailed_anova(self):
203
- if self.model_simplified is None: return pd.DataFrame()
204
-
205
- # Calcular Error Puro
206
- replicates = self.data.groupby([self.x1_name, self.x2_name, self.x3_name]).filter(lambda x: len(x) > 1)
207
- if not replicates.empty:
208
- ss_pure_error = np.sum(replicates.groupby([self.x1_name, self.x2_name, self.x3_name])[self.y_name].apply(lambda x: np.sum((x - x.mean())**2)))
209
- df_pure_error = len(replicates) - len(replicates.groupby([self.x1_name, self.x2_name, self.x3_name]))
210
- ms_pure_error = ss_pure_error / df_pure_error if df_pure_error > 0 else 0
211
- else:
212
- ss_pure_error, df_pure_error, ms_pure_error = 0, 0, 0
213
-
214
- ss_residual = self.model_simplified.ssr
215
- df_residual = self.model_simplified.df_resid
216
 
217
  ss_lack_of_fit = ss_residual - ss_pure_error
218
  df_lack_of_fit = df_residual - df_pure_error
219
  ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit if df_lack_of_fit > 0 else 0
220
-
221
  f_lack_of_fit = ms_lack_of_fit / ms_pure_error if ms_pure_error > 0 else np.nan
222
  p_lack_of_fit = f.sf(f_lack_of_fit, df_lack_of_fit, df_pure_error) if ms_pure_error > 0 else np.nan
223
 
224
- # ANOVA del modelo
225
- anova_model = sm.stats.anova_lm(self.model_simplified, typ=1)
226
- ss_regression = anova_model['sum_sq'].sum()
227
- df_regression = anova_model['df'].sum()
228
- ms_regression = ss_regression / df_regression
229
- ms_residual = ss_residual / df_residual
230
- f_regression = ms_regression / ms_residual
231
- p_regression = f.sf(f_regression, df_regression, df_residual)
232
-
233
- ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
234
  df_total = len(self.data) - 1
 
 
 
 
 
235
 
236
  anova_data = {
237
- 'Fuente': ['Regresi贸n', 'Error Residual', ' Falta de Ajuste', ' Error Puro', 'Total'],
238
  'Suma Cuadrados': [ss_regression, ss_residual, ss_lack_of_fit, ss_pure_error, ss_total],
239
  'GL': [df_regression, df_residual, df_lack_of_fit, df_pure_error, df_total],
240
  'Cuadrado Medio': [ms_regression, ms_residual, ms_lack_of_fit, ms_pure_error, np.nan],
241
  'Valor F': [f_regression, np.nan, f_lack_of_fit, np.nan, np.nan],
242
  'p-valor': [p_regression, np.nan, p_lack_of_fit, np.nan, np.nan]
243
  }
244
- detailed_anova_table = pd.DataFrame(anova_data)
245
- self.all_tables['ANOVA_Detallada'] = detailed_anova_table.round(4)
246
- return self.all_tables['ANOVA_Detallada']
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
247
 
248
- # --- Funciones de Exportaci贸n ---
249
- def save_tables_to_excel(self):
250
- if not self.all_tables: return None
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
251
  excel_buffer = io.BytesIO()
252
  with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer:
253
- for sheet_name, table in self.all_tables.items():
254
- table.to_excel(writer, sheet_name=sheet_name, index=False)
 
255
  excel_buffer.seek(0)
256
- with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as temp_file:
257
- temp_file.write(excel_buffer.read())
258
- return temp_file.name
259
 
260
- def save_figures_to_zip(self):
261
- if not self.all_figures: return None
262
  zip_buffer = io.BytesIO()
263
- with zipfile.ZipFile(zip_buffer, 'w') as zip_f:
264
- for i, fig in enumerate(self.all_figures):
265
- img_bytes = fig.to_image(format="png", width=1000, height=800)
266
- zip_f.writestr(f'Grafico_{i+1}.png', img_bytes)
 
267
  zip_buffer.seek(0)
268
- with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as temp_file:
269
- temp_file.write(zip_buffer.read())
270
- return temp_file.name
271
-
272
- def export_to_word(self):
273
- if not self.all_tables: return None
274
- doc = docx.Document()
275
- doc.add_heading('Informe de Optimizaci贸n RSM', 0).alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
276
- doc.add_paragraph(f"Generado el: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}").alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
277
-
278
- for name, table in self.all_tables.items():
279
- doc.add_heading(name.replace('_', ' '), level=1)
280
- if table.empty:
281
- doc.add_paragraph("No hay datos.")
282
- continue
283
 
284
- t = doc.add_table(rows=1, cols=len(table.columns))
285
- t.style = 'Table Grid'
286
- for j, col_name in enumerate(table.columns):
287
- t.cell(0, j).text = str(col_name)
288
- for i, row in table.iterrows():
289
- row_cells = t.add_row().cells
290
- for j, cell_value in enumerate(row):
291
- row_cells[j].text = str(cell_value)
292
- doc.add_paragraph()
293
-
294
- with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp:
295
- doc.save(tmp.name)
296
- return tmp.name
297
-
298
- # --- Instancia global de la clase ---
299
  rsm_analyzer = None
300
 
301
- # --- Funciones de la Interfaz Gradio ---
302
- def process_data(x1, x2, x3, y, l1, l2, l3, data_str):
303
  global rsm_analyzer
304
  try:
305
- x1_levels = [float(x.strip()) for x in l1.split(',')]
306
- x2_levels = [float(x.strip()) for x in l2.split(',')]
307
- x3_levels = [float(x.strip()) for x in l3.split(',')]
308
-
309
- data_io = io.StringIO(data_str)
310
- df = pd.read_csv(data_io, header=None)
311
- df.columns = ['Exp.', x1, x2, x3, y]
312
- df = df.apply(pd.to_numeric, errors='coerce')
313
-
314
- rsm_analyzer = RSM_BoxBehnken(df, x1, x2, x3, y, x1_levels, x2_levels, x3_levels)
315
-
316
- # Correr an谩lisis
317
- model_full, pareto_full = rsm_analyzer.fit_model()
318
- model_simp, pareto_simp = rsm_analyzer.fit_simplified_model()
319
- opt_table = rsm_analyzer.optimize()
320
- equation = rsm_analyzer.get_simplified_equation()
321
- pred_table = rsm_analyzer.generate_prediction_table()
322
- contrib_table = rsm_analyzer.calculate_contribution_percentage()
323
- anova_detail_table = rsm_analyzer.calculate_detailed_anova()
324
 
325
- # Generar gr谩ficos
326
- all_figs = rsm_analyzer.generate_all_plots()
 
 
327
 
328
- # Preparar salidas
329
- initial_plot = all_figs[0] if all_figs else None
330
- plot_info = f"Gr谩fico 1 de {len(all_figs)}" if all_figs else "No hay gr谩ficos"
331
 
332
  return (
333
- df, gr.update(visible=True), model_full.summary().as_html(), pareto_full,
334
- model_simp.summary().as_html(), pareto_simp, equation, opt_table,
335
- pred_table, contrib_table, anova_detail_table,
336
- initial_plot, plot_info, all_figs, 0
 
 
 
 
 
 
 
337
  )
338
  except Exception as e:
339
- gr.Error(f"Error al procesar los datos: {e}")
340
- return None, gr.update(visible=False), None, None, None, None, None, None, None, None, None, None, None, [], 0
341
-
342
- def navigate_plot(direction, current_index, all_figures):
343
- if not all_figures:
344
- return None, "No hay gr谩ficos", current_index
345
-
346
- if direction == 'prev':
347
- new_index = (current_index - 1) % len(all_figures)
348
- else: # 'next'
349
- new_index = (current_index + 1) % len(all_figures)
350
-
351
- selected_fig = all_figures[new_index]
352
- plot_info_text = f"Gr谩fico {new_index + 1} de {len(all_figures)}"
353
-
354
- return selected_fig, plot_info_text, new_index
355
-
356
- def download_zip():
357
- if rsm_analyzer: return rsm_analyzer.save_figures_to_zip()
358
- return None
359
 
360
- def download_excel():
361
- if rsm_analyzer: return rsm_analyzer.save_tables_to_excel()
362
- return None
 
363
 
364
- def download_word():
365
- if rsm_analyzer: return rsm_analyzer.export_to_word()
366
- return None
367
-
368
- # --- Interfaz de Gradio Mejorada ---
369
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
370
- gr.Markdown("# 馃殌 Optimizaci贸n de Procesos con RSM Box-Behnken")
371
- gr.Markdown("Herramienta interactiva para analizar y optimizar dise帽os experimentales Box-Behnken.")
372
-
373
  with gr.Row():
374
  with gr.Column(scale=1):
375
- gr.Markdown("## 1. Configuraci贸n del Experimento")
376
  x1_name = gr.Textbox(label="Nombre Var. X1", value="Glucosa")
377
  x2_name = gr.Textbox(label="Nombre Var. X2", value="Extracto_de_Levadura")
378
  x3_name = gr.Textbox(label="Nombre Var. X3", value="Triptofano")
379
  y_name = gr.Textbox(label="Nombre Var. Respuesta (Y)", value="AIA_ppm")
380
-
381
- with gr.Accordion("Niveles Naturales de las Variables (-1, 0, 1)", open=False):
382
- x1_levels = gr.Textbox(label="Niveles de X1 (bajo, medio, alto)", value="1, 3.25, 5.5")
383
- x2_levels = gr.Textbox(label="Niveles de X2 (bajo, medio, alto)", value="0.03, 0.165, 0.3")
384
- x3_levels = gr.Textbox(label="Niveles de X3 (bajo, medio, alto)", value="0.4, 0.65, 0.9")
385
-
386
  with gr.Column(scale=2):
387
- gr.Markdown("## 2. Ingrese los Datos Experimentales")
388
- data_input = gr.Textbox(
389
- label="Pegue los datos (formato CSV: Exp, X1, X2, X3, Y). Los valores de X deben ser codificados (-1, 0, 1).",
390
- lines=10,
391
- value="""1,-1,-1,0,166.594
392
- 2,1,-1,0,177.557
393
- 3,-1,1,0,127.261
394
- 4,1,1,0,147.573
395
- 5,-1,0,-1,188.883
396
- 6,1,0,-1,224.527
397
- 7,-1,0,1,190.238
398
- 8,1,0,1,226.483
399
- 9,0,-1,-1,195.550
400
- 10,0,1,-1,149.493
401
- 11,0,-1,1,187.683
402
- 12,0,1,1,148.621
403
- 13,0,0,0,278.951
404
- 14,0,0,0,297.238
405
- 15,0,0,0,280.896""")
406
- analyze_btn = gr.Button("Analizar Datos", variant="primary")
407
-
408
- # Esta secci贸n aparece despu茅s del an谩lisis
409
  with gr.Tabs(visible=False) as analysis_tabs:
410
  with gr.TabItem("馃搵 Resumen y Optimizaci贸n"):
411
- gr.Markdown("### Tabla de Datos Originales")
412
- data_output = gr.DataFrame(label="Datos Cargados")
413
- gr.Markdown("### Ecuaci贸n del Modelo Simplificado")
414
- equation_output = gr.HTML()
415
- gr.Markdown("### Niveles 脫ptimos para Maximizar la Respuesta")
416
- optimization_output = gr.DataFrame(label="Optimizaci贸n")
417
  with gr.Row():
418
  with gr.Column():
419
- gr.Markdown("### Resumen del Modelo Completo")
420
- model_full_output = gr.HTML()
421
  with gr.Column():
422
- gr.Markdown("### Pareto del Modelo Completo")
423
- pareto_full_output = gr.Plot()
424
  with gr.Row():
425
  with gr.Column():
426
- gr.Markdown("### Resumen del Modelo Simplificado")
427
- model_simp_output = gr.HTML()
 
428
  with gr.Column():
429
- gr.Markdown("### Pareto del Modelo Simplificado")
 
430
  pareto_simp_output = gr.Plot()
431
 
432
- with gr.TabItem("馃搳 An谩lisis de Varianza (ANOVA)"):
433
- gr.Markdown("## Tabla de Contribuci贸n de Factores")
434
- gr.Markdown("Esta tabla muestra qu茅 tan importante es cada t茅rmino del modelo. Un p-valor bajo (<0.05) indica significancia.")
435
- contribution_output = gr.DataFrame(label="% Contribuci贸n")
436
- gr.Markdown("## ANOVA Detallada del Modelo")
437
- gr.Markdown("Esta tabla valida el modelo. Buscamos un p-valor alto (>0.05) para la 'Falta de Ajuste', lo que significa que el modelo se ajusta bien a los datos.")
438
- anova_detail_output = gr.DataFrame(label="ANOVA Detallada")
439
- gr.Markdown("## Tabla de Predicciones y Residuales")
440
- prediction_output = gr.DataFrame(label="Predicciones vs Reales")
441
-
 
 
 
 
 
 
 
 
 
 
 
 
442
  with gr.TabItem("馃搱 Gr谩ficos de Superficie"):
443
- gr.Markdown("## Visor de Gr谩ficos de Superficie de Respuesta")
444
- gr.Markdown("Navegue a trav茅s de todas las combinaciones de variables para visualizar la superficie de respuesta predicha por el modelo.")
445
  with gr.Row():
446
  prev_btn = gr.Button("猬咃笍 Anterior")
447
- plot_info = gr.Textbox(label="Info del Gr谩fico", interactive=False, container=False)
448
  next_btn = gr.Button("Siguiente 鉃★笍")
449
  rsm_plot_output = gr.Plot()
450
- # Estados para manejar la navegaci贸n de gr谩ficos
451
- all_figures_state = gr.State([])
452
- current_index_state = gr.State(0)
453
-
454
- with gr.TabItem("馃摜 Exportar Resultados"):
455
  gr.Markdown("## Descargar Todos los Resultados")
456
  with gr.Row():
457
- download_excel_btn = gr.DownloadButton("Descargar Tablas (Excel)", variant="secondary")
458
- download_word_btn = gr.DownloadButton("Descargar Informe (Word)", variant="secondary")
459
- download_zip_btn = gr.DownloadButton("Descargar Gr谩ficos (ZIP)", variant="secondary")
460
-
461
- # --- L贸gica de los Eventos ---
 
 
 
 
 
 
 
 
 
 
 
 
462
  analyze_btn.click(
463
- fn=process_data,
464
  inputs=[x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels, data_input],
465
- outputs=[
466
- data_output, analysis_tabs, model_full_output, pareto_full_output,
467
- model_simp_output, pareto_simp_output, equation_output, optimization_output,
468
- prediction_output, contribution_output, anova_detail_output,
469
- rsm_plot_output, plot_info, all_figures_state, current_index_state
470
- ]
471
- )
472
-
473
- prev_btn.click(
474
- fn=lambda idx, figs: navigate_plot('prev', idx, figs),
475
- inputs=[current_index_state, all_figures_state],
476
- outputs=[rsm_plot_output, plot_info, current_index_state]
477
- )
478
-
479
- next_btn.click(
480
- fn=lambda idx, figs: navigate_plot('next', idx, figs),
481
- inputs=[current_index_state, all_figures_state],
482
- outputs=[rsm_plot_output, plot_info, current_index_state]
483
  )
484
 
485
- download_excel_btn.click(fn=download_excel, inputs=[], outputs=[download_excel_btn])
486
- download_word_btn.click(fn=download_word, inputs=[], outputs=[download_word_btn])
487
- download_zip_btn.click(fn=download_zip, inputs=[], outputs=[download_zip_btn])
488
-
 
489
 
490
- # --- Funci贸n Principal ---
491
  if __name__ == "__main__":
492
  demo.launch(share=True)
 
4
  import statsmodels.formula.api as smf
5
  import statsmodels.api as sm
6
  import plotly.graph_objects as go
 
7
  import plotly.express as px
8
+ import plotly.figure_factory as ff
9
+ from scipy.optimize import minimize
10
+ from scipy.stats import f, probplot
11
  import gradio as gr
12
  import io
13
  import zipfile
 
16
  import docx
17
  from docx.shared import Pt
18
  from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
 
19
 
20
+ # --- Clase RSM_BoxBehnken Optimizada y Enriquecida ---
21
  class RSM_BoxBehnken:
22
  def __init__(self, data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels):
23
  self.data = data.copy()
24
+ # Nombres y niveles de las variables
25
+ self.var_names = {'x1': x1_name, 'x2': x2_name, 'x3': x3_name, 'y': y_name}
26
+ self.levels = {x1_name: x1_levels, x2_name: x2_levels, x3_name: x3_levels}
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
27
 
28
+ # Contenedor para todos los resultados (m谩s organizado)
29
+ self.results = {
30
+ 'models': {},
31
+ 'tables': {},
32
+ 'plots': {'surface': [], 'diagnostic': {}},
33
+ 'data': {'original': self.data}
34
+ }
 
 
 
 
 
 
35
 
36
+ def run_analysis(self, p_threshold=0.05):
37
+ """Orquesta todo el proceso de an谩lisis."""
38
+ try:
39
+ self._fit_full_model()
40
+ self._fit_simplified_model(p_threshold)
41
+
42
+ if 'simplified' not in self.results['models']:
43
+ raise ValueError("El modelo simplificado no pudo ser ajustado.")
44
+
45
+ # Generar todas las tablas
46
+ self._optimize()
47
+ self._generate_prediction_table()
48
+ self._calculate_detailed_anova()
49
+ self._calculate_contribution_percentage()
50
+
51
+ # Generar todos los gr谩ficos
52
+ self._generate_surface_plots()
53
+ self._generate_diagnostic_plots() # Nueva funcionalidad
54
+
55
+ return True
56
+ except Exception as e:
57
+ print(f"Error durante el an谩lisis: {e}")
58
+ return False
59
+
60
+ def _fit_full_model(self):
61
+ formula = f"`{self.var_names['y']}` ~ `{self.var_names['x1']}` + `{self.var_names['x2']}` + `{self.var_names['x3']}` + " \
62
+ f"I(`{self.var_names['x1']}`**2) + I(`{self.var_names['x2']}`**2) + I(`{self.var_names['x3']}`**2) + " \
63
+ f"`{self.var_names['x1']}`:`{self.var_names['x2']}` + `{self.var_names['x1']}`:`{self.var_names['x3']}` + `{self.var_names['x2']}`:`{self.var_names['x3']}`"
64
+ model = smf.ols(formula, data=self.data).fit()
65
+ self.results['models']['full'] = model
66
+ self.results['tables']['pareto_full'] = self._create_pareto_chart(model, "Pareto - Modelo Completo")
67
+
68
+ def _fit_simplified_model(self, p_threshold=0.05):
69
+ full_model = self.results['models']['full']
70
+ pvalues = full_model.pvalues[1:]
71
+ significant_terms = pvalues[pvalues < p_threshold].index.tolist()
72
 
73
+ # Asegurar que los t茅rminos base siempre est茅n si alguna interacci贸n o cuadrado es significativo
74
+ base_terms = [f"`{self.var_names[f'x{i}']}`" for i in range(1, 4)] + \
75
+ [f"I(`{self.var_names[f'x{i}']}` ** 2)" for i in range(1, 4)]
76
 
77
+ final_terms = sorted(list(set(base_terms + significant_terms)))
78
 
79
+ if not final_terms:
80
+ # Si nada es significativo, se usa un modelo con solo el intercepto (modelo medio)
81
+ formula_simplified = f"`{self.var_names['y']}` ~ 1"
82
+ else:
83
+ formula_simplified = f"`{self.var_names['y']}` ~ {' + '.join(final_terms)}"
84
 
85
+ model = smf.ols(formula_simplified, data=self.data).fit()
86
+ self.results['models']['simplified'] = model
87
+ self.results['tables']['pareto_simplified'] = self._create_pareto_chart(model, "Pareto - Modelo Simplificado")
88
+ self.results['tables']['equation'] = self._get_simplified_equation()
89
+
90
+ def _optimize(self, method='Nelder-Mead'):
91
+ model = self.results['models']['simplified']
92
  def objective_function(x):
93
+ df_pred = pd.DataFrame({
94
+ self.var_names['x1']: [x[0]], self.var_names['x2']: [x[1]], self.var_names['x3']: [x[2]]
95
+ })
96
+ return -model.predict(df_pred).iloc[0]
97
+
98
  bounds = [(-1, 1), (-1, 1), (-1, 1)]
99
+ opt_results = minimize(objective_function, x0=[0,0,0], method=method, bounds=bounds)
 
 
 
100
 
101
+ optimal_coded = opt_results.x
102
+ optimal_natural = [self._coded_to_natural(val, self.var_names[f'x{i+1}']) for i, val in enumerate(optimal_coded)]
103
+ predicted_max_y = -opt_results.fun
104
+
105
+ df = pd.DataFrame({
106
+ 'Variable': [self.var_names['x1'], self.var_names['x2'], self.var_names['x3'], f"**{self.var_names['y']} (Predicho)**"],
107
+ 'Nivel 脫ptimo (Natural)': optimal_natural + [f"**{predicted_max_y:.4f}**"],
108
+ 'Nivel 脫ptimo (Codificado)': list(optimal_coded) + ['-']
109
  })
110
+ self.results['tables']['optimization'] = df.round(4)
 
 
 
 
 
 
 
 
 
111
 
112
+ # --- M茅todos de generaci贸n de tablas estad铆sticas (incluyendo los nuevos) ---
 
 
113
 
114
+ def _calculate_detailed_anova(self):
115
+ model = self.results['models']['simplified']
116
+ # Error Puro
117
+ replicates = self.data.groupby([self.var_names['x1'], self.var_names['x2'], self.var_names['x3']]).filter(lambda x: len(x) > 1)
118
+ df_pure_error = len(replicates) - replicates.nunique().iloc[0] if not replicates.empty else 0
119
+ ss_pure_error = np.sum(replicates.groupby([self.var_names['x1'], self.var_names['x2'], self.var_names['x3']])[self.var_names['y']].apply(lambda x: np.sum((x - x.mean())**2))) if df_pure_error > 0 else 0
120
+ ms_pure_error = ss_pure_error / df_pure_error if df_pure_error > 0 else 0
121
 
122
+ ss_residual, df_residual, ms_residual = model.ssr, model.df_resid, model.mse_resid
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
123
 
124
  ss_lack_of_fit = ss_residual - ss_pure_error
125
  df_lack_of_fit = df_residual - df_pure_error
126
  ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit if df_lack_of_fit > 0 else 0
 
127
  f_lack_of_fit = ms_lack_of_fit / ms_pure_error if ms_pure_error > 0 else np.nan
128
  p_lack_of_fit = f.sf(f_lack_of_fit, df_lack_of_fit, df_pure_error) if ms_pure_error > 0 else np.nan
129
 
130
+ ss_total = np.sum((self.data[self.var_names['y']] - self.data[self.var_names['y']].mean())**2)
 
 
 
 
 
 
 
 
 
131
  df_total = len(self.data) - 1
132
+ ss_regression = ss_total - ss_residual
133
+ df_regression = df_total - df_residual
134
+ ms_regression = ss_regression / df_regression
135
+ f_regression = model.fvalue
136
+ p_regression = model.f_pvalue
137
 
138
  anova_data = {
139
+ 'Fuente': ['Regresi贸n', 'Error Residual', ' Falta de Ajuste', ' Error Puro', 'Total Corregido'],
140
  'Suma Cuadrados': [ss_regression, ss_residual, ss_lack_of_fit, ss_pure_error, ss_total],
141
  'GL': [df_regression, df_residual, df_lack_of_fit, df_pure_error, df_total],
142
  'Cuadrado Medio': [ms_regression, ms_residual, ms_lack_of_fit, ms_pure_error, np.nan],
143
  'Valor F': [f_regression, np.nan, f_lack_of_fit, np.nan, np.nan],
144
  'p-valor': [p_regression, np.nan, p_lack_of_fit, np.nan, np.nan]
145
  }
146
+ self.results['tables']['anova_detailed'] = pd.DataFrame(anova_data).round(4)
147
+
148
+ def _calculate_contribution_percentage(self):
149
+ model = self.results['models']['simplified']
150
+ anova_table = sm.stats.anova_lm(model, typ=2)
151
+ ss_total = np.sum((self.data[self.var_names['y']] - self.data[self.var_names['y']].mean())**2)
152
+
153
+ anova_table['% Contribuci贸n'] = (anova_table['sum_sq'] / ss_total) * 100
154
+
155
+ contribution = anova_table[['sum_sq', 'df', 'F', 'PR(>F)', '% Contribuci贸n']].reset_index()
156
+ contribution.rename(columns={'index': 'Fuente', 'sum_sq': 'Suma Cuadrados', 'df': 'GL', 'PR(>F)': 'p-valor'}, inplace=True)
157
+ self.results['tables']['contribution'] = contribution.round(4)
158
+
159
+ def _generate_prediction_table(self):
160
+ model = self.results['models']['simplified']
161
+ self.data['Predicho'] = model.predict(self.data)
162
+ self.data['Residual'] = self.data[self.var_names['y']] - self.data['Predicho']
163
+ table = self.data[[self.var_names['y'], 'Predicho', 'Residual']].round(4)
164
+ self.results['tables']['predictions'] = table
165
+
166
+ # --- M茅todos de generaci贸n de Gr谩ficos (incluyendo los nuevos de diagn贸stico) ---
167
+
168
+ def _generate_surface_plots(self):
169
+ model = self.results['models']['simplified']
170
+ if not model: return
171
+ self.results['plots']['surface'].clear()
172
+ variables = [self.var_names['x1'], self.var_names['x2'], self.var_names['x3']]
173
+ for i in range(3):
174
+ fixed_var = variables[i]
175
+ varying_vars = [v for v in variables if v != fixed_var]
176
+ x_var, y_var = varying_vars[0], varying_vars[1]
177
+
178
+ for level_coded, level_natural in zip([-1, 0, 1], self.levels[fixed_var]):
179
+ x_range = np.linspace(self.levels[x_var][0], self.levels[x_var][2], 40)
180
+ y_range = np.linspace(self.levels[y_var][0], self.levels[y_var][2], 40)
181
+ x_grid, y_grid = np.meshgrid(x_range, y_range)
182
+
183
+ pred_data = pd.DataFrame({
184
+ x_var: self._natural_to_coded(x_grid.flatten(), x_var),
185
+ y_var: self._natural_to_coded(y_grid.flatten(), y_var)
186
+ })
187
+ pred_data[fixed_var] = level_coded
188
+ z_pred = model.predict(pred_data).values.reshape(x_grid.shape)
189
+
190
+ fig = go.Figure(data=[go.Surface(z=z_pred, x=x_range, y=y_range, colorscale='viridis', opacity=0.9)])
191
+
192
+ fig.update_layout(
193
+ title=f"{self.var_names['y']} vs {x_var} & {y_var}<br><sup>{fixed_var} fijo en {level_natural:.2f}</sup>",
194
+ scene=dict(xaxis_title=x_var, yaxis_title=y_var, zaxis_title=self.var_names['y']),
195
+ height=500, margin=dict(l=0, r=0, b=0, t=40)
196
+ )
197
+ self.results['plots']['surface'].append(fig)
198
+
199
+ def _generate_diagnostic_plots(self):
200
+ """Genera un conjunto de gr谩ficos de diagn贸stico para los residuales."""
201
+ model = self.results['models']['simplified']
202
+ residuals = model.resid
203
+ fitted = model.fittedvalues
204
+
205
+ # 1. Normal Q-Q Plot
206
+ qq_data = probplot(residuals, dist="norm", fit=False)
207
+ qq_fig = px.scatter(x=qq_data[0][0], y=qq_data[0][1], labels={'x': 'Cuantiles Te贸ricos', 'y': 'Residuales Ordenados'}, title="Gr谩fico de Probabilidad Normal (Q-Q)")
208
+ qq_fig.add_shape(type='line', x0=qq_data[0][0].min(), y0=qq_data[1][1], x1=qq_data[0][0].max(), y1=qq_data[1][0]*qq_data[0][0].max()+qq_data[1][1], line=dict(color='red'))
209
+ self.results['plots']['diagnostic']['qq'] = qq_fig
210
+
211
+ # 2. Residuals vs. Fitted
212
+ rvf_fig = px.scatter(x=fitted, y=residuals, labels={'x': 'Valores Ajustados (Predichos)', 'y': 'Residuales'}, title="Residuales vs. Ajustados")
213
+ rvf_fig.add_hline(y=0, line_dash="dash", line_color="red")
214
+ self.results['plots']['diagnostic']['rvf'] = rvf_fig
215
+
216
+ # 3. Histogram of Residuals
217
+ hist_fig = px.histogram(x=residuals, nbins=10, title="Histograma de Residuales")
218
+ self.results['plots']['diagnostic']['hist'] = hist_fig
219
+
220
+ # 4. Residuals vs. Order
221
+ run_order = self.data.index
222
+ rvo_fig = px.line(x=run_order, y=residuals, labels={'x': 'Orden de Ejecuci贸n', 'y': 'Residuales'}, title="Residuales vs. Orden de Ejecuci贸n", markers=True)
223
+ rvo_fig.add_hline(y=0, line_dash="dash", line_color="red")
224
+ self.results['plots']['diagnostic']['rvo'] = rvo_fig
225
+
226
+ # --- M茅todos de ayuda y exportaci贸n ---
227
+ def _coded_to_natural(self, coded, name): return np.interp(coded, [-1, 1], [self.levels[name][0], self.levels[name][2]])
228
+ def _natural_to_coded(self, natural, name): return np.interp(natural, [self.levels[name][0], self.levels[name][2]], [-1, 1])
229
 
230
+ def _create_pareto_chart(self, model, title):
231
+ if len(model.pvalues) <= 1: return go.Figure().update_layout(title=f"{title}<br><sup>(No hay t茅rminos para graficar)</sup>")
232
+ fvalues = model.tvalues[1:]**2
233
+ sorted_f = fvalues.sort_values()
234
+ f_critical = f.ppf(1 - 0.05, 1, model.df_resid)
235
+ fig = px.bar(x=sorted_f, y=sorted_f.index, orientation='h', labels={'x': 'Estad铆stico F', 'y': 'T茅rmino'}, title=title)
236
+ fig.add_vline(x=f_critical, line_dash="dot", annotation_text=f"F-cr铆tico (伪=0.05) = {f_critical:.2f}")
237
+ return fig
238
+
239
+ def _get_simplified_equation(self):
240
+ params = self.results['models']['simplified'].params
241
+ eq = f"<b>{self.var_names['y']}</b> = {params.get('Intercept', 0):.4f}"
242
+ for term, coef in params.items():
243
+ if term == 'Intercept': continue
244
+ term_name = term.replace('`', '').replace('I(', '').replace('**2', '<sup>2</sup>').replace(')', '').replace('_', ' ')
245
+ sign = "+" if coef >= 0 else "-"
246
+ eq += f" {sign} {abs(coef):.4f} * <i>{term_name}</i>"
247
+ return eq.replace("+ -", "- ")
248
+
249
+ def export_to_excel(self):
250
  excel_buffer = io.BytesIO()
251
  with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer:
252
+ for name, table in self.results['tables'].items():
253
+ if isinstance(table, pd.DataFrame):
254
+ table.to_excel(writer, sheet_name=name.replace('_', ' ').title(), index=False)
255
  excel_buffer.seek(0)
256
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as f:
257
+ f.write(excel_buffer.read())
258
+ return f.name
259
 
260
+ def export_all_plots_to_zip(self):
 
261
  zip_buffer = io.BytesIO()
262
+ with zipfile.ZipFile(zip_buffer, 'w') as zf:
263
+ for i, fig in enumerate(self.results['plots']['surface']):
264
+ zf.writestr(f"Surface_Plot_{i+1}.png", fig.to_image(format="png"))
265
+ for name, fig in self.results['plots']['diagnostic'].items():
266
+ zf.writestr(f"Diagnostic_{name}.png", fig.to_image(format="png"))
267
  zip_buffer.seek(0)
268
+ with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as f:
269
+ f.write(zip_buffer.read())
270
+ return f.name
 
 
 
 
 
 
 
 
 
 
 
 
271
 
272
+ # --- Instancia Global ---
 
 
 
 
 
 
 
 
 
 
 
 
 
 
273
  rsm_analyzer = None
274
 
275
+ # --- L贸gica de la Interfaz Gradio ---
276
+ def run_full_analysis(x1, x2, x3, y, l1, l2, l3, data_str):
277
  global rsm_analyzer
278
  try:
279
+ x1_l, x2_l, x3_l = [[float(x.strip()) for x in l.split(',')] for l in [l1,l2,l3]]
280
+ df = pd.read_csv(io.StringIO(data_str), header=None, names=['Exp.', x1, x2, x3, y], quotechar='`')
281
+ df = df.apply(pd.to_numeric)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
282
 
283
+ rsm_analyzer = RSM_BoxBehnken(df, x1, x2, x3, y, x1_l, x2_l, x3_l)
284
+ success = rsm_analyzer.run_analysis()
285
+ if not success:
286
+ raise RuntimeError("El an谩lisis fall贸. Verifique los datos y la configuraci贸n.")
287
 
288
+ res = rsm_analyzer.results
289
+ surf_plots = res['plots']['surface']
290
+ diag_plots = res['plots']['diagnostic']
291
 
292
  return (
293
+ df, gr.update(visible=True),
294
+ # Tab 1: Resumen
295
+ res['models']['full'].summary().as_html(), res['tables']['pareto_full'],
296
+ res['models']['simplified'].summary().as_html(), res['tables']['pareto_simplified'],
297
+ res['tables']['equation'], res['tables']['optimization'],
298
+ # Tab 2: ANOVA
299
+ res['tables']['contribution'], res['tables']['anova_detailed'], res['tables']['predictions'],
300
+ # Tab 3: Diagnostico
301
+ diag_plots['qq'], diag_plots['rvf'], diag_plots['hist'], diag_plots['rvo'],
302
+ # Tab 4: Superficies
303
+ surf_plots[0] if surf_plots else None, f"Gr谩fico 1 de {len(surf_plots)}", surf_plots, 0
304
  )
305
  except Exception as e:
306
+ gr.Error(f"Error: {e}")
307
+ return None, gr.update(visible=False), *([None]*16)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
308
 
309
+ def navigate_plot(direction, idx, figs):
310
+ if not figs: return None, "No hay gr谩ficos", idx
311
+ new_idx = (idx + (1 if direction == 'next' else -1)) % len(figs)
312
+ return figs[new_idx], f"Gr谩fico {new_idx + 1} de {len(figs)}", new_idx
313
 
314
+ # --- Construcci贸n de la Interfaz Gradio ---
 
 
 
 
315
  with gr.Blocks(theme=gr.themes.Soft()) as demo:
316
+ gr.Markdown("# 馃殌 Optimizaci贸n Avanzada con RSM Box-Behnken")
317
+ # ... (resto de la interfaz sin cambios en la definici贸n de componentes)
 
318
  with gr.Row():
319
  with gr.Column(scale=1):
320
+ gr.Markdown("## 1. Configuraci贸n")
321
  x1_name = gr.Textbox(label="Nombre Var. X1", value="Glucosa")
322
  x2_name = gr.Textbox(label="Nombre Var. X2", value="Extracto_de_Levadura")
323
  x3_name = gr.Textbox(label="Nombre Var. X3", value="Triptofano")
324
  y_name = gr.Textbox(label="Nombre Var. Respuesta (Y)", value="AIA_ppm")
325
+ with gr.Accordion("Niveles Naturales (-1, 0, 1)", open=False):
326
+ x1_levels = gr.Textbox(label="Niveles de X1", value="1, 3.25, 5.5")
327
+ x2_levels = gr.Textbox(label="Niveles de X2", value="0.03, 0.165, 0.3")
328
+ x3_levels = gr.Textbox(label="Niveles de X3", value="0.4, 0.65, 0.9")
 
 
329
  with gr.Column(scale=2):
330
+ gr.Markdown("## 2. Datos Experimentales")
331
+ data_input = gr.Textbox(label="Pegue datos CSV (Exp, X1, X2, X3, Y). X deben ser codificados (-1, 0, 1).", lines=10, value="""1,-1,-1,0,166.594\n2,1,-1,0,177.557\n3,-1,1,0,127.261\n4,1,1,0,147.573\n5,-1,0,-1,188.883\n6,1,0,-1,224.527\n7,-1,0,1,190.238\n8,1,0,1,226.483\n9,0,-1,-1,195.550\n10,0,1,-1,149.493\n11,0,-1,1,187.683\n12,0,1,1,148.621\n13,0,0,0,278.951\n14,0,0,0,297.238\n15,0,0,0,280.896""")
332
+ analyze_btn = gr.Button("Analizar y Optimizar", variant="primary")
333
+
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
334
  with gr.Tabs(visible=False) as analysis_tabs:
335
  with gr.TabItem("馃搵 Resumen y Optimizaci贸n"):
 
 
 
 
 
 
336
  with gr.Row():
337
  with gr.Column():
338
+ gr.Markdown("### Ecuaci贸n del Modelo")
339
+ equation_output = gr.HTML()
340
  with gr.Column():
341
+ gr.Markdown("### Optimizaci贸n")
342
+ optimization_output = gr.DataFrame(label="Niveles 脫ptimos")
343
  with gr.Row():
344
  with gr.Column():
345
+ gr.Markdown("#### Modelo Completo")
346
+ model_full_output = gr.HTML()
347
+ pareto_full_output = gr.Plot()
348
  with gr.Column():
349
+ gr.Markdown("#### Modelo Simplificado")
350
+ model_simp_output = gr.HTML()
351
  pareto_simp_output = gr.Plot()
352
 
353
+ with gr.TabItem("馃搳 ANOVA y Contribuci贸n"):
354
+ gr.Markdown("## Tablas de An谩lisis de Varianza")
355
+ with gr.Row():
356
+ with gr.Column(scale=2):
357
+ gr.Markdown("### % de Contribuci贸n de Factores")
358
+ contribution_output = gr.DataFrame()
359
+ with gr.Column(scale=3):
360
+ gr.Markdown("### ANOVA Detallada (Prueba de Falta de Ajuste)")
361
+ anova_detail_output = gr.DataFrame()
362
+ gr.Markdown("### Tabla de Predicciones vs. Valores Reales")
363
+ prediction_output = gr.DataFrame()
364
+
365
+ with gr.TabItem("馃攳 Diagn贸stico del Modelo"):
366
+ gr.Markdown("## An谩lisis de Residuales para Validar Supuestos del Modelo")
367
+ gr.Markdown("Un buen modelo tendr谩 residuales que se asemejen al ruido aleatorio. Buscamos puntos cercanos a la l铆nea roja en el Q-Q Plot y sin patrones claros en el gr谩fico de Residuales vs. Ajustados.")
368
+ with gr.Row():
369
+ qq_plot_output = gr.Plot()
370
+ rvf_plot_output = gr.Plot()
371
+ with gr.Row():
372
+ hist_plot_output = gr.Plot()
373
+ rvo_plot_output = gr.Plot()
374
+
375
  with gr.TabItem("馃搱 Gr谩ficos de Superficie"):
376
+ # ... (sin cambios)
 
377
  with gr.Row():
378
  prev_btn = gr.Button("猬咃笍 Anterior")
379
+ plot_info = gr.Textbox(label="Info", interactive=False, container=False)
380
  next_btn = gr.Button("Siguiente 鉃★笍")
381
  rsm_plot_output = gr.Plot()
382
+
383
+ with gr.TabItem("馃摜 Exportar"):
 
 
 
384
  gr.Markdown("## Descargar Todos los Resultados")
385
  with gr.Row():
386
+ download_excel_btn = gr.DownloadButton("Tablas (Excel)")
387
+ download_zip_btn = gr.DownloadButton("Gr谩ficos (ZIP)")
388
+
389
+ # Estados para la navegaci贸n de gr谩ficos
390
+ all_figures_state = gr.State([])
391
+ current_index_state = gr.State(0)
392
+
393
+ # --- L贸gica de Eventos ---
394
+ outputs_list = [
395
+ data_input, analysis_tabs,
396
+ model_full_output, pareto_full_output, model_simp_output, pareto_simp_output,
397
+ equation_output, optimization_output,
398
+ contribution_output, anova_detail_output, prediction_output,
399
+ qq_plot_output, rvf_plot_output, hist_plot_output, rvo_plot_output,
400
+ rsm_plot_output, plot_info, all_figures_state, current_index_state
401
+ ]
402
+
403
  analyze_btn.click(
404
+ fn=run_full_analysis,
405
  inputs=[x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels, data_input],
406
+ outputs=outputs_list
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
407
  )
408
 
409
+ prev_btn.click(lambda i, f: navigate_plot('prev', i, f), [current_index_state, all_figures_state], [rsm_plot_output, plot_info, current_index_state])
410
+ next_btn.click(lambda i, f: navigate_plot('next', i, f), [current_index_state, all_figures_state], [rsm_plot_output, plot_info, current_index_state])
411
+
412
+ download_excel_btn.click(lambda: rsm_analyzer.export_to_excel() if rsm_analyzer else None, [], download_excel_btn)
413
+ download_zip_btn.click(lambda: rsm_analyzer.export_all_plots_to_zip() if rsm_analyzer else None, [], download_zip_btn)
414
 
 
415
  if __name__ == "__main__":
416
  demo.launch(share=True)