C2MV commited on
Commit
f6f4945
verified
1 Parent(s): 30bca94

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +201 -231
app.py CHANGED
@@ -42,297 +42,266 @@ interaction_checkboxes = gr.CheckboxGroup(["x1x2", "x1x3", "x2x3"], label="T茅rm
42
  # --- Clase RSM_BoxBehnken ---
43
  class RSM_BoxBehnken:
44
  def __init__(self, data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels):
 
 
 
45
  self.data = data.copy()
46
  self.model = None
47
  self.model_simplified = None
48
- self.model_personalized = None
49
  self.optimized_results = None
50
  self.optimal_levels = None
51
- self.all_figures_full = []
52
  self.all_figures_simplified = []
53
  self.all_figures_personalized = []
54
  self.x1_name = x1_name
55
  self.x2_name = x2_name
56
  self.x3_name = x3_name
57
  self.y_name = y_name
 
 
58
  self.x1_levels = x1_levels
59
  self.x2_levels = x2_levels
60
  self.x3_levels = x3_levels
61
 
62
  def get_levels(self, variable_name):
 
 
 
63
  levels = {self.x1_name: self.x1_levels, self.x2_name: self.x2_levels, self.x3_name: self.x3_levels}
64
  return levels.get(variable_name)
65
 
66
  def fit_model(self):
67
- 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}'
 
 
 
 
 
68
  self.model = smf.ols(formula, data=self.data).fit()
 
 
69
  return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
70
 
71
  def fit_simplified_model(self):
72
- formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + 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
  return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Modelo Simplificado")
75
 
76
  def optimize(self, method='Nelder-Mead'):
77
- if self.model_simplified is None: return
 
 
 
 
 
 
78
  def objective_function(x):
79
- return -self.model_simplified.predict(pd.DataFrame({self.x1_name: [x[0]], self.x2_name: [x[1]], self.x3_name: [x[2]]})).values[0]
 
 
 
 
 
80
  bounds = [(-1, 1), (-1, 1), (-1, 1)]
81
  x0 = [0, 0, 0]
 
82
  self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
83
- optimal_levels_natural = [self.coded_to_natural(self.optimal_levels[0], self.x1_name), self.coded_to_natural(self.optimal_levels[1], self.x2_name), self.coded_to_natural(self.optimal_levels[2], self.x3_name)]
84
- optimization_table = pd.DataFrame({'Variable': [self.x1_name, self.x2_name, self.x3_name], 'Nivel 脫ptimo (Natural)': optimal_levels_natural, 'Nivel 脫ptimo (Codificado)': self.optimal_levels})
85
- return optimization_table.round(3)
 
 
 
 
 
 
 
 
 
 
 
 
 
86
 
87
  def fit_personalized_model(self, formula):
 
 
 
88
  self.model_personalized = smf.ols(formula, data=self.data).fit()
 
 
89
  return self.model_personalized, self.pareto_chart(self.model_personalized, "Pareto - Modelo Personalizado")
90
 
91
  def generate_all_plots(self):
92
- if self.model_simplified is None: return
93
- self.all_figures_full = []
 
 
 
 
 
 
94
  self.all_figures_simplified = []
95
  self.all_figures_personalized = []
96
- levels_to_plot_natural = {self.x1_name: self.x1_levels, self.x2_name: self.x2_levels, self.x3_name: self.x3_levels}
 
 
 
 
 
 
97
  for fixed_variable in [self.x1_name, self.x2_name, self.x3_name]:
98
  for level in levels_to_plot_natural[fixed_variable]:
99
- fig_full = self.plot_rsm_individual(fixed_variable, level, model_type='full')
100
- if fig_full: self.all_figures_full.append(fig_full)
101
- fig_simplified = self.plot_rsm_individual(fixed_variable, level, model_type='simplified')
102
- if fig_simplified: self.all_figures_simplified.append(fig_simplified)
103
- if self.model_personalized is not None:
104
- fig_personalized = self.plot_rsm_individual(fixed_variable, level, model_type='personalized')
105
- if fig_personalized: self.all_figures_personalized.append(fig_personalized)
106
-
107
- def plot_rsm_individual(self, fixed_variable, fixed_level, model_type='simplified'):
108
- model_to_use = self.model_simplified if model_type == 'simplified' else self.model if model_type == 'full' else self.model_personalized
109
- if model_to_use is None: return None
110
- model_title_suffix = "(Modelo Simplificado)" if model_type == 'simplified' else "(Modelo Completo)" if model_type == 'full' else "(Modelo Personalizado)"
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
111
  varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
 
 
112
  x_natural_levels = self.get_levels(varying_variables[0])
113
  y_natural_levels = self.get_levels(varying_variables[1])
 
 
114
  x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
115
  y_range_natural = np.linspace(y_natural_levels[0], y_natural_levels[-1], 100)
116
  x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
 
 
117
  x_grid_coded = self.natural_to_coded(x_grid_natural, varying_variables[0])
118
- y_grid_coded = self.natural_to_coded(y_grid_natural, varying_variables[1])
119
- prediction_data = pd.DataFrame({varying_variables[0]: x_grid_coded.flatten(), varying_variables[1]: y_grid_coded.flatten()})
 
 
 
 
 
120
  prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
121
- z_pred = model_to_use.predict(prediction_data).values.reshape(x_grid_coded.shape)
 
 
 
 
 
 
 
 
 
 
 
 
 
122
  fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable)
123
  subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)]
 
 
124
  valid_levels = [-1, 0, 1]
125
- experiments_data = subset_data[subset_data[varying_variables[0]].isin(valid_levels) & subset_data[varying_variables[1]].isin(valid_levels)]
 
 
 
 
 
126
  experiments_x_natural = experiments_data[varying_variables[0]].apply(lambda x: self.coded_to_natural(x, varying_variables[0]))
127
  experiments_y_natural = experiments_data[varying_variables[1]].apply(lambda x: self.coded_to_natural(x, varying_variables[1]))
128
 
 
129
  fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)])
 
 
 
130
  for i in range(x_grid_natural.shape[0]):
131
- fig.add_trace(go.Scatter3d(x=x_grid_natural[i, :], y=y_grid_natural[i, :], z=z_pred[i, :], mode='lines', line=dict(color='gray', width=2), showlegend=False, hoverinfo='skip'))
 
 
 
 
 
 
 
 
 
132
  for j in range(x_grid_natural.shape[1]):
133
- fig.add_trace(go.Scatter3d(x=x_grid_natural[:, j], y=y_grid_natural[:, j], z=z_pred[:, j], mode='lines', line=dict(color='gray', width=2), showlegend=False, hoverinfo='skip'))
134
-
 
 
 
 
 
 
 
 
 
 
 
135
  colors = px.colors.qualitative.Safe
136
  point_labels = [f"{row[self.y_name]:.3f}" for _, row in experiments_data.iterrows()]
137
- fig.add_trace(go.Scatter3d(x=experiments_x_natural, y=experiments_y_natural, z=experiments_data[self.y_name].round(3), mode='markers+text', marker=dict(size=4, color=colors[:len(experiments_x_natural)]), text=point_labels, textposition='top center', name='Experimentos'))
138
- fig.update_layout(scene=dict(xaxis_title=f"{varying_variables[0]} ({self.get_units(varying_variables[0])})", yaxis_title=f"{varying_variables[1]} ({self.get_units(varying_variables[1])})", zaxis_title=self.y_name), 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)}) {model_title_suffix}</sup>", height=800, width=1000, showlegend=True)
139
- return fig
140
 
141
- def get_units(self, variable_name):
142
- units = {'Glucosa_g_L': 'g/L', 'Proteina_Pescado_g_L': 'g/L', 'Sulfato_Manganeso_g_L': 'g/L', 'Abs_600nm': ''}
143
- return units.get(variable_name, '')
144
-
145
- def coded_to_natural(self, coded_value, variable_name):
146
- levels = self.get_levels(variable_name)
147
- return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2
148
-
149
- def natural_to_coded(self, natural_value, variable_name):
150
- levels = self.get_levels(variable_name)
151
- return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
152
-
153
- def pareto_chart(self, model, title):
154
- tvalues = model.tvalues[1:]
155
- abs_tvalues = np.abs(tvalues)
156
- sorted_idx = np.argsort(abs_tvalues)[::-1]
157
- sorted_tvalues = abs_tvalues[sorted_idx]
158
- sorted_names = tvalues.index[sorted_idx]
159
- alpha = 0.05
160
- dof = model.df_resid
161
- t_critical = t.ppf(1 - alpha / 2, dof)
162
- fig = px.bar(x=sorted_tvalues.round(3), y=sorted_names, orientation='h', labels={'x': 'Efecto Estandarizado', 'y': 'T茅rmino'}, title=title)
163
- fig.update_yaxes(autorange="reversed")
164
- fig.add_vline(x=t_critical, line_dash="dot", annotation_text=f"t cr铆tico = {t_critical:.3f}", annotation_position="bottom right")
165
  return fig
166
 
167
- def get_simplified_equation(self):
168
- if self.model_simplified is None: return None
169
- coefficients = self.model_simplified.params
170
- equation = f"{self.y_name} = {coefficients['Intercept']:.3f}"
171
- for term, coef in coefficients.items():
172
- if term != 'Intercept':
173
- if term == f'{self.x1_name}': equation += f" + {coef:.3f}*{self.x1_name}"
174
- elif term == f'{self.x2_name}': equation += f" + {coef:.3f}*{self.x2_name}"
175
- elif term == f'{self.x3_name}': equation += f" + {coef:.3f}*{self.x3_name}"
176
- elif term == f'I({self.x1_name} ** 2)': equation += f" + {coef:.3f}*{self.x1_name}^2"
177
- elif term == f'I({self.x2_name} ** 2)': equation += f" + {coef:.3f}*{self.x2_name}^2"
178
- elif term == f'I({self.x3_name} ** 2)': equation += f" + {coef:.3f}*{self.x3_name}^2"
179
- return equation
180
-
181
- def generate_prediction_table(self):
182
- if self.model_simplified is None: return None
183
- self.data['Predicho'] = self.model_simplified.predict(self.data)
184
- self.data['Residual'] = self.data[self.y_name] - self.data['Predicho']
185
- return self.data[[self.y_name, 'Predicho', 'Residual']].round(3)
186
-
187
- def calculate_contribution_percentage(self):
188
- if self.model_simplified is None: return None
189
- anova_table = sm.stats.anova_lm(self.model_simplified, typ=2)
190
- ss_total = anova_table['sum_sq'].sum()
191
- contribution_table = pd.DataFrame({'Factor': [], 'Suma de Cuadrados': [], '% Contribuci贸n': []})
192
- for index, row in anova_table.iterrows():
193
- if index != 'Residual':
194
- factor_name = index
195
- if factor_name == f'I({self.x1_name} ** 2)': factor_name = f'{self.x1_name}^2'
196
- elif factor_name == f'I({self.x2_name} ** 2)': factor_name = f'{self.x2_name}^2'
197
- elif factor_name == f'I({self.x3_name} ** 2)': factor_name = f'{self.x3_name}^2'
198
- ss_factor = row['sum_sq']
199
- contribution_percentage = (ss_factor / ss_total) * 100
200
- contribution_table = pd.concat([contribution_table, pd.DataFrame({'Factor': [factor_name], 'Suma de Cuadrados': [ss_factor], '% Contribuci贸n': [contribution_percentage]})], ignore_index=True)
201
- return contribution_table.round(3)
202
-
203
- def calculate_detailed_anova(self):
204
- if self.model_simplified is None: return None
205
- formula_reduced = 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)'
206
- model_reduced = smf.ols(formula_reduced, data=self.data).fit()
207
- anova_reduced = sm.stats.anova_lm(model_reduced, typ=2)
208
- ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
209
- df_total = len(self.data) - 1
210
- ss_regression = anova_reduced['sum_sq'][:-1].sum()
211
- df_regression = len(anova_reduced) - 1
212
- ss_residual = self.model_simplified.ssr
213
- df_residual = self.model_simplified.df_resid
214
- replicas = self.data[self.data.duplicated(subset=[self.x1_name, self.x2_name, self.x3_name], keep=False)]
215
- 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 if not replicas.empty else np.nan
216
- df_pure_error = len(replicas) - replicas.groupby([self.x1_name, self.x2_name, self.x3_name]).ngroups if not replicas.empty else np.nan
217
- ss_lack_of_fit = ss_residual - ss_pure_error if not np.isnan(ss_pure_error) else np.nan
218
- df_lack_of_fit = df_residual - df_pure_error if not np.isnan(df_pure_error) else np.nan
219
- ms_regression = ss_regression / df_regression
220
- ms_residual = ss_residual / df_residual
221
- ms_lack_of_fit = np.nan
222
- if not np.isnan(df_lack_of_fit) and df_lack_of_fit != 0:
223
- ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit
224
- ms_pure_error = ss_pure_error / df_pure_error if not np.isnan(df_pure_error) else np.nan
225
- f_lack_of_fit = ms_lack_of_fit / ms_pure_error if not np.isnan(ms_lack_of_fit) and not np.isnan(ms_pure_error) and ms_pure_error != 0 else np.nan
226
- 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) and not np.isnan(df_lack_of_fit) and not np.isnan(df_pure_error) else np.nan
227
-
228
- detailed_anova_table = pd.DataFrame({
229
- 'Fuente de Variaci贸n': ['Regresi贸n', 'Curvatura', 'Residual', 'Falta de Ajuste', 'Error Puro', 'Total'], # Curvature added here
230
- 'Suma de Cuadrados': [ss_regression, np.nan, ss_residual, ss_lack_of_fit, ss_pure_error, ss_total], # ss_curvature removed from here
231
- 'Grados de Libertad': [df_regression, np.nan, df_residual, df_lack_of_fit, df_pure_error, df_total], # df_curvature removed from here
232
- 'Cuadrado Medio': [ms_regression, np.nan, ms_residual, ms_lack_of_fit, ms_pure_error, np.nan],
233
- 'F': [np.nan, np.nan, np.nan, f_lack_of_fit, np.nan, np.nan],
234
- 'Valor p': [np.nan, np.nan, np.nan, p_lack_of_fit, np.nan, np.nan]
235
- })
236
- ss_curvature = anova_reduced['sum_sq'][f'I({self.x1_name} ** 2)'] + anova_reduced['sum_sq'][f'I({self.x2_name} ** 2)'] + anova_reduced['sum_sq'][f'I({self.x3_name} ** 2)']
237
- df_curvature = 3
238
- detailed_anova_table.loc[1, ['Fuente de Variaci贸n', 'Suma de Cuadrados', 'Grados de Libertad', 'Cuadrado Medio']] = ['Curvatura', ss_curvature, df_curvature, ss_curvature / df_curvature] # Curvature row added here
239
-
240
- return detailed_anova_table.round(3)
241
-
242
- def get_all_tables(self):
243
- prediction_table = self.generate_prediction_table()
244
- contribution_table = self.calculate_contribution_percentage()
245
- detailed_anova_table = self.calculate_detailed_anova()
246
- return {'Predicciones': prediction_table, '% Contribuci贸n': contribution_table, 'ANOVA Detallada': detailed_anova_table}
247
-
248
- def save_figures_to_zip(self):
249
- if not self.all_figures_simplified and not self.all_figures_full and not self.all_figures_personalized: return None
250
- zip_buffer = io.BytesIO()
251
- with zipfile.ZipFile(zip_buffer, 'w') as zip_file:
252
- for idx, fig in enumerate(self.all_figures_simplified, start=1):
253
- img_bytes = fig.to_image(format="png")
254
- zip_file.writestr(f'Grafico_Simplificado_{idx}.png', img_bytes)
255
- for idx, fig in enumerate(self.all_figures_full, start=1):
256
- img_bytes = fig.to_image(format="png")
257
- zip_file.writestr(f'Grafico_Completo_{idx}.png', img_bytes)
258
- for idx, fig in enumerate(self.all_figures_personalized, start=1):
259
- img_bytes = fig.to_image(format="png")
260
- zip_file.writestr(f'Grafico_Personalizado_{idx}.png', img_bytes)
261
- zip_buffer.seek(0)
262
- with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as temp_file:
263
- temp_file.write(zip_buffer.read())
264
- temp_path = temp_file.name
265
- return temp_path
266
-
267
- def save_fig_to_bytes(self, fig):
268
- return fig.to_image(format="png")
269
-
270
- def save_all_figures_png(self):
271
- png_paths = []
272
- for idx, fig in enumerate(self.all_figures_simplified, start=1):
273
- img_bytes = fig.to_image(format="png")
274
- with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
275
- temp_file.write(img_bytes)
276
- png_paths.append(temp_file.name)
277
- for idx, fig in enumerate(self.all_figures_full, start=1):
278
- img_bytes = fig.to_image(format="png")
279
- with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
280
- temp_file.write(img_bytes)
281
- png_paths.append(temp_file.name)
282
- for idx, fig in enumerate(self.all_figures_personalized, start=1):
283
- img_bytes = fig.to_image(format="png")
284
- with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
285
- temp_file.write(img_bytes)
286
- png_paths.append(temp_file.name)
287
- return png_paths
288
-
289
- def save_tables_to_excel(self):
290
- tables = self.get_all_tables()
291
- excel_buffer = io.BytesIO()
292
- with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer:
293
- for sheet_name, table in tables.items():
294
- table.to_excel(writer, sheet_name=sheet_name, index=False)
295
- excel_buffer.seek(0)
296
- excel_bytes = excel_buffer.read()
297
- with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as temp_file:
298
- temp_file.write(excel_bytes)
299
- temp_path = temp_file.name
300
- return temp_path
301
-
302
- def export_tables_to_word(self, tables_dict):
303
- if not tables_dict: return None
304
- doc = docx.Document()
305
- style = doc.styles['Normal']
306
- font = style.font
307
- font.name = 'Times New Roman'
308
- font.size = Pt(12)
309
- titulo = doc.add_heading('Informe de Optimizaci贸n de Producci贸n de Absorbancia', 0)
310
- titulo.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
311
- doc.add_paragraph(f"Fecha: {datetime.now().strftime('%d/%m/%Y %H:%M')}").alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
312
- doc.add_paragraph('\n')
313
- for sheet_name, table in tables_dict.items():
314
- doc.add_heading(sheet_name, level=1)
315
- if table.empty:
316
- doc.add_paragraph("No hay datos disponibles para esta tabla.")
317
- continue
318
- table_doc = doc.add_table(rows=1, cols=len(table.columns))
319
- table_doc.style = 'Light List Accent 1'
320
- hdr_cells = table_doc.rows[0].cells
321
- for idx, col_name in enumerate(table.columns):
322
- hdr_cells[idx].text = col_name
323
- for _, row in table.iterrows():
324
- row_cells = table_doc.add_row().cells
325
- for idx, item in enumerate(row):
326
- row_cells[idx].text = str(item)
327
- doc.add_paragraph('\n')
328
- with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp:
329
- doc.save(tmp.name)
330
- tmp_path = tmp.name
331
- return tmp_path
332
-
333
 
334
  # --- Funciones para la Interfaz de Gradio ---
335
 
 
336
  def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str):
337
  try:
338
  x1_levels = [float(x.strip()) for x in x1_levels_str.split(',')]
@@ -340,9 +309,10 @@ def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x
340
  x3_levels = [float(x.strip()) for x in x3_levels_str.split(',')]
341
  data_list = [row.split(',') for row in data_str.strip().split('\n')]
342
  column_names = ['Exp.', x1_name, x2_name, x3_name, y_name]
343
- data = pd.DataFrame(data_list, columns=column_names).apply(pd.to_numeric, errors='coerce')
344
- if not all(col in data.columns for col in column_names): raise ValueError("Data format incorrect.")
345
- global rsm
 
346
  rsm = RSM_BoxBehnken(data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels)
347
  return data.round(3), gr.update(visible=True)
348
  except Exception as e:
@@ -364,7 +334,7 @@ def fit_and_optimize_model():
364
  equation_formatted = f"### Ecuaci贸n del Modelo Simplificado:<br>{equation_formatted}"
365
  excel_path = rsm.save_tables_to_excel()
366
  zip_path = rsm.save_figures_to_zip()
367
- return (model_completo.summary().as_html(), pareto_completo, model_simplificado.summary().as_html(), pareto_simplificado, equation_formatted, optimization_table, prediction_table, contribution_table, anova_table, zip_path, excel_path)
368
 
369
  def fit_custom_model(factor_checkboxes, interaction_checkboxes, model_personalized_output_component, pareto_personalized_output_component):
370
  if 'rsm' not in globals(): return [None]*2
@@ -393,7 +363,7 @@ def navigate_plot(direction, current_index, all_figures, model_type):
393
  new_index = (current_index - 1) % len(figure_list) if direction == 'left' else (current_index + 1) % len(figure_list)
394
  selected_fig = figure_list[new_index]
395
  plot_info_text = f"Gr谩fico {new_index + 1} de {len(figure_list)} (Modelo {model_type.capitalize()})"
396
- return selected_fig, plot_info_text, new_index
397
 
398
  def download_current_plot(all_figures, current_index, model_type):
399
  figure_list = rsm.all_figures_full if model_type == 'full' else rsm.all_figures_simplified if model_type == 'simplified' else rsm.all_figures_personalized
@@ -479,7 +449,7 @@ def create_gradio_interface():
479
  38,1,0,1,1.810
480
  39,0,-1,-1,1.852
481
  40,0,1,-1,1.694
482
- 41,0,-1,1,1.831
483
  42,0,1,1,0.347
484
  43,0,0,0,1.752
485
  44,0,0,0,1.367
@@ -531,7 +501,7 @@ def create_gradio_interface():
531
  rsm_plot_output_comp = rsm_plot_output
532
  plot_info_comp = plot_info
533
  with gr.Row():
534
- download_plot_button_comp = download_plot_button
535
  download_all_plots_button_comp = download_all_plots_button
536
  current_index_state_comp = current_index_state
537
  all_figures_state_comp = all_figures_state
 
42
  # --- Clase RSM_BoxBehnken ---
43
  class RSM_BoxBehnken:
44
  def __init__(self, data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels):
45
+ """
46
+ Inicializa la clase con los datos del dise帽o Box-Behnken.
47
+ """
48
  self.data = data.copy()
49
  self.model = None
50
  self.model_simplified = None
51
+ self.model_personalized = None # For personalized model
52
  self.optimized_results = None
53
  self.optimal_levels = None
54
+ self.all_figures_full = [] # Separate lists for different model plots
55
  self.all_figures_simplified = []
56
  self.all_figures_personalized = []
57
  self.x1_name = x1_name
58
  self.x2_name = x2_name
59
  self.x3_name = x3_name
60
  self.y_name = y_name
61
+
62
+ # Niveles originales de las variables
63
  self.x1_levels = x1_levels
64
  self.x2_levels = x2_levels
65
  self.x3_levels = x3_levels
66
 
67
  def get_levels(self, variable_name):
68
+ """
69
+ Obtiene los niveles para una variable espec铆fica.
70
+ """
71
  levels = {self.x1_name: self.x1_levels, self.x2_name: self.x2_levels, self.x3_name: self.x3_levels}
72
  return levels.get(variable_name)
73
 
74
  def fit_model(self):
75
+ """
76
+ Ajusta el modelo de segundo orden completo a los datos.
77
+ """
78
+ formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
79
+ f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2) + ' \
80
+ f'{self.x1_name}:{self.x2_name} + {self.x1_name}:{self.x3_name} + {self.x2_name}:{self.x3_name}'
81
  self.model = smf.ols(formula, data=self.data).fit()
82
+ print("Modelo Completo:")
83
+ print(self.model.summary())
84
  return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
85
 
86
  def fit_simplified_model(self):
87
+ """
88
+ Ajusta el modelo de segundo orden a los datos, eliminando t茅rminos no significativos.
89
+ """
90
+ formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + ' \
91
+ f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)' # Adjusted formula to include x3^2
92
  self.model_simplified = smf.ols(formula, data=self.data).fit()
93
+ print("\nModelo Simplificado:")
94
+ print(self.model_simplified.summary())
95
  return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Modelo Simplificado")
96
 
97
  def optimize(self, method='Nelder-Mead'):
98
+ """
99
+ Encuentra los niveles 贸ptimos de los factores para maximizar la respuesta usando el modelo simplificado.
100
+ """
101
+ if self.model_simplified is None:
102
+ print("Error: Ajusta el modelo simplificado primero.")
103
+ return
104
+
105
  def objective_function(x):
106
+ return -self.model_simplified.predict(pd.DataFrame({
107
+ self.x1_name: [x[0]],
108
+ self.x2_name: [x[1]],
109
+ self.x3_name: [x[2]]
110
+ })).values[0]
111
+
112
  bounds = [(-1, 1), (-1, 1), (-1, 1)]
113
  x0 = [0, 0, 0]
114
+
115
  self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
116
+ self.optimal_levels = self.optimized_results.x
117
+
118
+ # Convertir niveles 贸ptimos de codificados a naturales
119
+ optimal_levels_natural = [
120
+ self.coded_to_natural(self.optimal_levels[0], self.x1_name),
121
+ self.coded_to_natural(self.optimal_levels[1], self.x2_name),
122
+ self.coded_to_natural(self.optimal_levels[2], self.x3_name)
123
+ ]
124
+ # Crear la tabla de optimizaci贸n
125
+ optimization_table = pd.DataFrame({
126
+ 'Variable': [self.x1_name, self.x2_name, self.x3_name],
127
+ 'Nivel 脫ptimo (Natural)': optimal_levels_natural,
128
+ 'Nivel 脫ptimo (Codificado)': self.optimal_levels
129
+ })
130
+
131
+ return optimization_table.round(3) # Redondear a 3 decimales
132
 
133
  def fit_personalized_model(self, formula):
134
+ """
135
+ Ajusta un modelo personalizado de segundo orden a los datos, usando la formula dada.
136
+ """
137
  self.model_personalized = smf.ols(formula, data=self.data).fit()
138
+ print("\nModelo Personalizado:")
139
+ print(self.model_personalized.summary())
140
  return self.model_personalized, self.pareto_chart(self.model_personalized, "Pareto - Modelo Personalizado")
141
 
142
  def generate_all_plots(self):
143
+ """
144
+ Genera todas las gr谩ficas de RSM para todos los modelos.
145
+ """
146
+ if self.model_simplified is None:
147
+ print("Error: Ajusta el modelo simplificado primero.")
148
+ return
149
+
150
+ self.all_figures_full = [] # Reset lists for each model type
151
  self.all_figures_simplified = []
152
  self.all_figures_personalized = []
153
+
154
+ levels_to_plot_natural = { # Levels from data, as before
155
+ self.x1_name: sorted(list(set(self.data[self.x1_name]))),
156
+ self.x2_name: sorted(list(set(self.data[self.x2_name]))),
157
+ self.x3_name: sorted(list(set(self.data[self.x3_name])))
158
+ }
159
+
160
  for fixed_variable in [self.x1_name, self.x2_name, self.x3_name]:
161
  for level in levels_to_plot_natural[fixed_variable]:
162
+ fig_full = self.plot_rsm_individual(fixed_variable, level, model_type='full') # Pass model_type
163
+ if fig_full is not None:
164
+ self.all_figures_full.append(fig_full)
165
+ fig_simplified = self.plot_rsm_individual(fixed_variable, level, model_type='simplified') # Pass model_type
166
+ if fig_simplified is not None:
167
+ self.all_figures_simplified.append(fig_simplified)
168
+ if self.model_personalized is not None: # Generate personalized plots only if model exists
169
+ fig_personalized = self.plot_rsm_individual(fixed_variable, level, model_type='personalized') # Pass model_type
170
+ if fig_personalized is not None:
171
+ self.all_figures_personalized.append(fig_personalized)
172
+
173
+ def plot_rsm_individual(self, fixed_variable, fixed_level, model_type='simplified'): # Added model_type parameter
174
+ """
175
+ Genera un gr谩fico de superficie de respuesta (RSM) individual para una configuraci贸n espec铆fica y modelo.
176
+ """
177
+ model_to_use = self.model_simplified # Default to simplified model
178
+ model_title_suffix = "(Modelo Simplificado)"
179
+ if model_type == 'full':
180
+ model_to_use = self.model
181
+ model_title_suffix = "(Modelo Completo)"
182
+ elif model_type == 'personalized':
183
+ if self.model_personalized is None:
184
+ print("Error: Modelo personalizado no ajustado.")
185
+ return None
186
+ model_to_use = self.model_personalized
187
+ model_title_suffix = "(Modelo Personalizado)"
188
+
189
+ if model_to_use is None: # Use model_to_use instead of self.model_simplified
190
+ print(f"Error: Ajusta el modelo {model_type} primero.") # More informative error message
191
+ return None
192
+
193
+ # Determinar las variables que var铆an y sus niveles naturales
194
  varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
195
+
196
+ # Establecer los niveles naturales para las variables que var铆an
197
  x_natural_levels = self.get_levels(varying_variables[0])
198
  y_natural_levels = self.get_levels(varying_variables[1])
199
+
200
+ # Crear una malla de puntos para las variables que var铆an (en unidades naturales)
201
  x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
202
  y_range_natural = np.linspace(y_natural_levels[0], y_natural_levels[-1], 100)
203
  x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
204
+
205
+ # Convertir la malla de variables naturales a codificadas
206
  x_grid_coded = self.natural_to_coded(x_grid_natural, varying_variables[0])
207
+ y_grid_coded = self.natural_to_coded(y_range_natural, varying_variables[1])
208
+
209
+ # Crear un DataFrame para la predicci贸n con variables codificadas
210
+ prediction_data = pd.DataFrame({
211
+ varying_variables[0]: x_grid_coded.flatten(),
212
+ varying_variables[1]: y_grid_coded.flatten(),
213
+ })
214
  prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
215
+
216
+ # Fijar la variable fija en el DataFrame de predicci贸n
217
+ fixed_var_levels = self.get_levels(fixed_variable)
218
+ if len(fixed_var_levels) == 3: # Box-Behnken design levels
219
+ prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
220
+ elif len(fixed_var_levels) > 0: # Use the closest level if not Box-Behnken
221
+ closest_level_coded = self.natural_to_coded(min(fixed_var_levels, key=lambda x:abs(x-fixed_level)), fixed_variable)
222
+ prediction_data[fixed_variable] = closest_level_coded
223
+
224
+
225
+ # Calcular los valores predichos
226
+ z_pred = model_to_use.predict(prediction_data).values.reshape(x_grid_coded.shape) # Use model_to_use here
227
+
228
+ # Filtrar por el nivel de la variable fija (en codificado)
229
  fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable)
230
  subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)]
231
+
232
+ # Filtrar por niveles v谩lidos en las variables que var铆an
233
  valid_levels = [-1, 0, 1]
234
+ experiments_data = subset_data[
235
+ subset_data[varying_variables[0]].isin(valid_levels) &
236
+ subset_data[varying_variables[1]].isin(valid_levels)
237
+ ]
238
+
239
+ # Convertir coordenadas de experimentos a naturales
240
  experiments_x_natural = experiments_data[varying_variables[0]].apply(lambda x: self.coded_to_natural(x, varying_variables[0]))
241
  experiments_y_natural = experiments_data[varying_variables[1]].apply(lambda x: self.coded_to_natural(x, varying_variables[1]))
242
 
243
+ # Crear el gr谩fico de superficie con variables naturales en los ejes y transparencia
244
  fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)])
245
+
246
+ # --- A帽adir cuadr铆cula a la superficie ---
247
+ # L铆neas en la direcci贸n x
248
  for i in range(x_grid_natural.shape[0]):
249
+ fig.add_trace(go.Scatter3d(
250
+ x=x_grid_natural[i, :],
251
+ y=y_grid_natural[i, :],
252
+ z=z_pred[i, :],
253
+ mode='lines',
254
+ line=dict(color='gray', width=2),
255
+ showlegend=False,
256
+ hoverinfo='skip'
257
+ ))
258
+ # L铆neas en la direcci贸n y
259
  for j in range(x_grid_natural.shape[1]):
260
+ fig.add_trace(go.Scatter3d(
261
+ x=x_grid_natural[:, j],
262
+ y=y_grid_natural[:, j],
263
+ z=z_pred[:, j],
264
+ mode='lines',
265
+ line=dict(color='gray', width=2),
266
+ showlegend=False,
267
+ hoverinfo='skip'
268
+ ))
269
+
270
+ # --- Fin de la adici贸n de la cuadr铆cula ---
271
+
272
+ # A帽adir los puntos de los experimentos en la superficie de respuesta con diferentes colores y etiquetas
273
  colors = px.colors.qualitative.Safe
274
  point_labels = [f"{row[self.y_name]:.3f}" for _, row in experiments_data.iterrows()]
 
 
 
275
 
276
+ fig.add_trace(go.Scatter3d(
277
+ x=experiments_x_natural,
278
+ y=experiments_y_natural,
279
+ z=experiments_data[self.y_name].round(3),
280
+ mode='markers+text',
281
+ marker=dict(size=4, color=colors[:len(experiments_x_natural)]),
282
+ text=point_labels,
283
+ textposition='top center',
284
+ name='Experimentos'
285
+ ))
286
+
287
+ # A帽adir etiquetas y t铆tulo con variables naturales
288
+ fig.update_layout(
289
+ scene=dict(
290
+ xaxis_title=f"{varying_variables[0]} ({self.get_units(varying_variables[0])})",
291
+ yaxis_title=f"{varying_variables[1]} ({self.get_units(varying_variables[1])})",
292
+ zaxis_title=self.y_name,
293
+ ),
294
+ 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)}) {model_title_suffix}</sup>", # Updated title
295
+ height=800,
296
+ width=1000,
297
+ showlegend=True
298
+ )
 
299
  return fig
300
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
301
 
302
  # --- Funciones para la Interfaz de Gradio ---
303
 
304
+
305
  def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str):
306
  try:
307
  x1_levels = [float(x.strip()) for x in x1_levels_str.split(',')]
 
309
  x3_levels = [float(x.strip()) for x in x3_levels_str.split(',')]
310
  data_list = [row.split(',') for row in data_str.strip().split('\n')]
311
  column_names = ['Exp.', x1_name, x2_name, x3_name, y_name]
312
+ data_loaded = pd.DataFrame(data_list, columns=column_names).apply(pd.to_numeric, errors='coerce')
313
+ if not all(col in data_loaded.columns for col in column_names): raise ValueError("Data format incorrect.")
314
+ global rsm, data
315
+ data = data_loaded # Assign loaded data to global data variable
316
  rsm = RSM_BoxBehnken(data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels)
317
  return data.round(3), gr.update(visible=True)
318
  except Exception as e:
 
334
  equation_formatted = f"### Ecuaci贸n del Modelo Simplificado:<br>{equation_formatted}"
335
  excel_path = rsm.save_tables_to_excel()
336
  zip_path = rsm.save_figures_to_zip()
337
+ return (model_completo.summary().as_html(), pareto_completo, model_simplificado_output, pareto_simplificado, equation_formatted, optimization_table, prediction_table, contribution_table, anova_table, zip_path, excel_path)
338
 
339
  def fit_custom_model(factor_checkboxes, interaction_checkboxes, model_personalized_output_component, pareto_personalized_output_component):
340
  if 'rsm' not in globals(): return [None]*2
 
363
  new_index = (current_index - 1) % len(figure_list) if direction == 'left' else (current_index + 1) % len(figure_list)
364
  selected_fig = figure_list[new_index]
365
  plot_info_text = f"Gr谩fico {new_index + 1} de {len(figure_list)} (Modelo {model_type.capitalize()})"
366
+ return selected_fig, plot_info_text, current_index
367
 
368
  def download_current_plot(all_figures, current_index, model_type):
369
  figure_list = rsm.all_figures_full if model_type == 'full' else rsm.all_figures_simplified if model_type == 'simplified' else rsm.all_figures_personalized
 
449
  38,1,0,1,1.810
450
  39,0,-1,-1,1.852
451
  40,0,1,-1,1.694
452
+ 41,0,1,1,1.831
453
  42,0,1,1,0.347
454
  43,0,0,0,1.752
455
  44,0,0,0,1.367
 
501
  rsm_plot_output_comp = rsm_plot_output
502
  plot_info_comp = plot_info
503
  with gr.Row():
504
+ download_plot_button_comp = download_plot_button # Correctly assigned here now
505
  download_all_plots_button_comp = download_all_plots_button
506
  current_index_state_comp = current_index_state
507
  all_figures_state_comp = all_figures_state