Update app.py
Browse files
app.py
CHANGED
@@ -21,8 +21,8 @@ import os
|
|
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 |
-
|
25 |
-
|
26 |
self.data = data.copy()
|
27 |
self.model = None
|
28 |
self.model_simplified = None
|
@@ -41,8 +41,8 @@ class RSM_BoxBehnken:
|
|
41 |
|
42 |
def get_levels(self, variable_name):
|
43 |
"""
|
44 |
-
|
45 |
-
|
46 |
if variable_name == self.x1_name:
|
47 |
return self.x1_levels
|
48 |
elif variable_name == self.x2_name:
|
@@ -54,8 +54,8 @@ class RSM_BoxBehnken:
|
|
54 |
|
55 |
def fit_model(self):
|
56 |
"""
|
57 |
-
|
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}'
|
@@ -66,19 +66,19 @@ class RSM_BoxBehnken:
|
|
66 |
|
67 |
def fit_simplified_model(self):
|
68 |
"""
|
69 |
-
|
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())
|
76 |
return self.model_simplified, self.pareto_chart_f(self.model_simplified, "Pareto - Modelo Simplificado (F)"), self.pareto_chart_t(self.model_simplified, "Pareto - Modelo Simplificado (t)")
|
77 |
-
|
78 |
def optimize(self, method='Nelder-Mead'):
|
79 |
"""
|
80 |
-
|
81 |
-
|
82 |
if self.model_simplified is None:
|
83 |
print("Error: Ajusta el modelo simplificado primero.")
|
84 |
return
|
@@ -113,15 +113,15 @@ class RSM_BoxBehnken:
|
|
113 |
|
114 |
def plot_rsm_individual(self, fixed_variable, fixed_level):
|
115 |
"""
|
116 |
-
|
117 |
-
|
118 |
if self.model_simplified is None:
|
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])
|
@@ -220,9 +220,9 @@ class RSM_BoxBehnken:
|
|
220 |
|
221 |
def get_units(self, variable_name):
|
222 |
"""
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
units = {
|
227 |
'Glucosa': 'g/L',
|
228 |
'Extracto_de_Levadura': 'g/L',
|
@@ -233,9 +233,9 @@ class RSM_BoxBehnken:
|
|
233 |
|
234 |
def generate_all_plots(self):
|
235 |
"""
|
236 |
-
|
237 |
-
|
238 |
-
|
239 |
if self.model_simplified is None:
|
240 |
print("Error: Ajusta el modelo simplificado primero.")
|
241 |
return
|
@@ -268,25 +268,25 @@ class RSM_BoxBehnken:
|
|
268 |
|
269 |
def pareto_chart_f(self, model, title):
|
270 |
"""
|
271 |
-
|
272 |
-
|
273 |
-
|
274 |
# Calcular los estadísticos F para cada término manualmente
|
275 |
# Necesitamos los coeficientes, los errores estándar y los grados de libertad del error
|
276 |
coef = model.params[1:] # Excluir la Intercept
|
277 |
-
stderr = model.bse[1:]
|
278 |
df_resid = model.df_resid
|
279 |
-
|
280 |
# Calcular F = (coef / stderr)^2
|
281 |
fvalues = (coef / stderr) ** 2
|
282 |
-
|
283 |
abs_fvalues = np.abs(fvalues)
|
284 |
sorted_idx = np.argsort(abs_fvalues)[::-1]
|
285 |
sorted_fvalues = abs_fvalues[sorted_idx]
|
286 |
sorted_names = fvalues.index[sorted_idx]
|
287 |
-
|
288 |
# Cambiar I() por "" en los nombres de los terminos
|
289 |
-
sorted_names = sorted_names.str.replace('I(','').str.replace('
|
290 |
|
291 |
# Calcular el valor crítico de F para la línea de significancia
|
292 |
alpha = 0.05 # Nivel de significancia
|
@@ -306,16 +306,16 @@ class RSM_BoxBehnken:
|
|
306 |
|
307 |
# Agregar la línea de significancia
|
308 |
fig.add_vline(x=f_critical, line_dash="dot",
|
309 |
-
|
310 |
-
|
311 |
|
312 |
return fig
|
313 |
-
|
314 |
def pareto_chart_t(self, model, title):
|
315 |
"""
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
# Calcular los estadísticos t para cada término
|
320 |
tvalues = model.tvalues[1:] # Excluir la Intercept
|
321 |
abs_tvalues = np.abs(tvalues)
|
@@ -324,7 +324,7 @@ class RSM_BoxBehnken:
|
|
324 |
sorted_names = tvalues.index[sorted_idx]
|
325 |
|
326 |
# Cambiar I() por "" en los nombres de los terminos
|
327 |
-
sorted_names = sorted_names.str.replace('I(','').str.replace('
|
328 |
|
329 |
# Calcular el valor crítico de t para la línea de significancia
|
330 |
alpha = 0.05 # Nivel de significancia
|
@@ -350,8 +350,8 @@ class RSM_BoxBehnken:
|
|
350 |
|
351 |
def get_simplified_equation(self):
|
352 |
"""
|
353 |
-
|
354 |
-
|
355 |
if self.model_simplified is None:
|
356 |
print("Error: Ajusta el modelo simplificado primero.")
|
357 |
return None
|
@@ -375,11 +375,11 @@ class RSM_BoxBehnken:
|
|
375 |
equation += f" + {coef:.3f}*{self.x3_name}^2"
|
376 |
|
377 |
return equation
|
378 |
-
|
379 |
def generate_prediction_table(self):
|
380 |
"""
|
381 |
-
|
382 |
-
|
383 |
if self.model_simplified is None:
|
384 |
print("Error: Ajusta el modelo simplificado primero.")
|
385 |
return None
|
@@ -391,8 +391,8 @@ class RSM_BoxBehnken:
|
|
391 |
|
392 |
def calculate_anova_table(self):
|
393 |
"""
|
394 |
-
|
395 |
-
|
396 |
if self.model_simplified is None:
|
397 |
print("Error: Ajusta el modelo simplificado primero.")
|
398 |
return None
|
@@ -444,7 +444,7 @@ class RSM_BoxBehnken:
|
|
444 |
# 11. Estadísticos F y valores p
|
445 |
f_regression = ms_regression / ms_residual
|
446 |
p_regression = 1 - f.cdf(f_regression, df_regression, df_residual)
|
447 |
-
|
448 |
f_lack_of_fit = ms_lack_of_fit / ms_pure_error if not np.isnan(ms_lack_of_fit) else np.nan
|
449 |
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
|
450 |
|
@@ -457,11 +457,11 @@ class RSM_BoxBehnken:
|
|
457 |
'F': [f_regression, np.nan, f_lack_of_fit, np.nan, np.nan],
|
458 |
'Valor p': [p_regression, np.nan, p_lack_of_fit, np.nan, np.nan]
|
459 |
})
|
460 |
-
|
461 |
# Calcular la suma de cuadrados y estadísticos F para la curvatura
|
462 |
ss_curvature = anova_reduced['sum_sq'][f'I({self.x1_name} ** 2)'] + \
|
463 |
-
|
464 |
-
|
465 |
df_curvature = 3
|
466 |
ms_curvature = ss_curvature / df_curvature
|
467 |
f_curvature = ms_curvature / ms_residual
|
@@ -469,14 +469,14 @@ class RSM_BoxBehnken:
|
|
469 |
|
470 |
# Añadir la fila de curvatura a la tabla ANOVA
|
471 |
detailed_anova_table.loc[len(detailed_anova_table)] = [
|
472 |
-
'Curvatura',
|
473 |
-
ss_curvature,
|
474 |
-
df_curvature,
|
475 |
ms_curvature,
|
476 |
f_curvature,
|
477 |
p_curvature
|
478 |
]
|
479 |
-
|
480 |
# --- Agregar % de Contribución ---
|
481 |
detailed_anova_table['% Contribución'] = (detailed_anova_table['Suma de Cuadrados'] / ss_total) * 100
|
482 |
|
@@ -487,8 +487,8 @@ class RSM_BoxBehnken:
|
|
487 |
|
488 |
def get_all_tables(self):
|
489 |
"""
|
490 |
-
|
491 |
-
|
492 |
prediction_table = self.generate_prediction_table()
|
493 |
anova_table = self.calculate_anova_table()
|
494 |
|
@@ -499,8 +499,8 @@ class RSM_BoxBehnken:
|
|
499 |
|
500 |
def save_figures_to_zip(self):
|
501 |
"""
|
502 |
-
|
503 |
-
|
504 |
if not self.all_figures:
|
505 |
return None
|
506 |
|
@@ -520,14 +520,14 @@ class RSM_BoxBehnken:
|
|
520 |
|
521 |
def save_fig_to_bytes(self, fig):
|
522 |
"""
|
523 |
-
|
524 |
-
|
525 |
return fig.to_image(format="png")
|
526 |
|
527 |
def save_all_figures_png(self):
|
528 |
"""
|
529 |
-
|
530 |
-
|
531 |
png_paths = []
|
532 |
for idx, fig in enumerate(self.all_figures, start=1):
|
533 |
img_bytes = fig.to_image(format="png")
|
@@ -539,8 +539,8 @@ class RSM_BoxBehnken:
|
|
539 |
|
540 |
def save_tables_to_excel(self):
|
541 |
"""
|
542 |
-
|
543 |
-
|
544 |
tables = self.get_all_tables()
|
545 |
excel_buffer = io.BytesIO()
|
546 |
with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer:
|
@@ -558,8 +558,8 @@ class RSM_BoxBehnken:
|
|
558 |
|
559 |
def export_tables_to_word(self, tables_dict):
|
560 |
"""
|
561 |
-
|
562 |
-
|
563 |
if not tables_dict:
|
564 |
return None
|
565 |
|
@@ -615,8 +615,8 @@ class RSM_BoxBehnken:
|
|
615 |
|
616 |
def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str):
|
617 |
"""
|
618 |
-
|
619 |
-
|
620 |
try:
|
621 |
# Convertir los niveles a listas de números
|
622 |
x1_levels = [float(x.strip()) for x in x1_levels_str.split(',')]
|
|
|
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
|
|
|
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:
|
|
|
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}'
|
|
|
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())
|
76 |
return self.model_simplified, self.pareto_chart_f(self.model_simplified, "Pareto - Modelo Simplificado (F)"), self.pareto_chart_t(self.model_simplified, "Pareto - Modelo Simplificado (t)")
|
77 |
+
|
78 |
def optimize(self, method='Nelder-Mead'):
|
79 |
"""
|
80 |
+
Encuentra los niveles óptimos de los factores para maximizar la respuesta usando el modelo simplificado.
|
81 |
+
"""
|
82 |
if self.model_simplified is None:
|
83 |
print("Error: Ajusta el modelo simplificado primero.")
|
84 |
return
|
|
|
113 |
|
114 |
def plot_rsm_individual(self, fixed_variable, fixed_level):
|
115 |
"""
|
116 |
+
Genera un gráfico de superficie de respuesta (RSM) individual para una configuración específica.
|
117 |
+
"""
|
118 |
if self.model_simplified is None:
|
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])
|
|
|
220 |
|
221 |
def get_units(self, variable_name):
|
222 |
"""
|
223 |
+
Define las unidades de las variables para etiquetas.
|
224 |
+
Puedes personalizar este método según tus necesidades.
|
225 |
+
"""
|
226 |
units = {
|
227 |
'Glucosa': 'g/L',
|
228 |
'Extracto_de_Levadura': 'g/L',
|
|
|
233 |
|
234 |
def generate_all_plots(self):
|
235 |
"""
|
236 |
+
Genera todas las gráficas de RSM, variando la variable fija y sus niveles usando el modelo simplificado.
|
237 |
+
Almacena las figuras en self.all_figures.
|
238 |
+
"""
|
239 |
if self.model_simplified is None:
|
240 |
print("Error: Ajusta el modelo simplificado primero.")
|
241 |
return
|
|
|
268 |
|
269 |
def pareto_chart_f(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 manualmente
|
275 |
# Necesitamos los coeficientes, los errores estándar y los grados de libertad del error
|
276 |
coef = model.params[1:] # Excluir la Intercept
|
277 |
+
stderr = model.bse[1:] # Excluir la Intercept
|
278 |
df_resid = model.df_resid
|
279 |
+
|
280 |
# Calcular F = (coef / stderr)^2
|
281 |
fvalues = (coef / stderr) ** 2
|
282 |
+
|
283 |
abs_fvalues = np.abs(fvalues)
|
284 |
sorted_idx = np.argsort(abs_fvalues)[::-1]
|
285 |
sorted_fvalues = abs_fvalues[sorted_idx]
|
286 |
sorted_names = fvalues.index[sorted_idx]
|
287 |
+
|
288 |
# Cambiar I() por "" en los nombres de los terminos
|
289 |
+
sorted_names = sorted_names.str.replace(r'I\(', '', regex=True).str.replace(r'\*\*2\)', '^2', regex=True).str.replace(r' \* ', '·', regex=True)
|
290 |
|
291 |
# Calcular el valor crítico de F para la línea de significancia
|
292 |
alpha = 0.05 # Nivel de significancia
|
|
|
306 |
|
307 |
# Agregar la línea de significancia
|
308 |
fig.add_vline(x=f_critical, line_dash="dot",
|
309 |
+
annotation_text=f"F crítico = {f_critical:.3f}",
|
310 |
+
annotation_position="bottom right")
|
311 |
|
312 |
return fig
|
313 |
+
|
314 |
def pareto_chart_t(self, model, title):
|
315 |
"""
|
316 |
+
Genera un diagrama de Pareto para los efectos usando estadísticos t,
|
317 |
+
incluyendo la línea de significancia.
|
318 |
+
"""
|
319 |
# Calcular los estadísticos t para cada término
|
320 |
tvalues = model.tvalues[1:] # Excluir la Intercept
|
321 |
abs_tvalues = np.abs(tvalues)
|
|
|
324 |
sorted_names = tvalues.index[sorted_idx]
|
325 |
|
326 |
# Cambiar I() por "" en los nombres de los terminos
|
327 |
+
sorted_names = sorted_names.str.replace(r'I\(', '', regex=True).str.replace(r'\*\*2\)', '^2', regex=True).str.replace(r' \* ', '·', regex=True)
|
328 |
|
329 |
# Calcular el valor crítico de t para la línea de significancia
|
330 |
alpha = 0.05 # Nivel de significancia
|
|
|
350 |
|
351 |
def get_simplified_equation(self):
|
352 |
"""
|
353 |
+
Retorna la ecuación del modelo simplificado como una cadena de texto.
|
354 |
+
"""
|
355 |
if self.model_simplified is None:
|
356 |
print("Error: Ajusta el modelo simplificado primero.")
|
357 |
return None
|
|
|
375 |
equation += f" + {coef:.3f}*{self.x3_name}^2"
|
376 |
|
377 |
return equation
|
378 |
+
|
379 |
def generate_prediction_table(self):
|
380 |
"""
|
381 |
+
Genera una tabla con los valores actuales, predichos y residuales.
|
382 |
+
"""
|
383 |
if self.model_simplified is None:
|
384 |
print("Error: Ajusta el modelo simplificado primero.")
|
385 |
return None
|
|
|
391 |
|
392 |
def calculate_anova_table(self):
|
393 |
"""
|
394 |
+
Calcula la tabla ANOVA detallada, incluyendo la contribución porcentual.
|
395 |
+
"""
|
396 |
if self.model_simplified is None:
|
397 |
print("Error: Ajusta el modelo simplificado primero.")
|
398 |
return None
|
|
|
444 |
# 11. Estadísticos F y valores p
|
445 |
f_regression = ms_regression / ms_residual
|
446 |
p_regression = 1 - f.cdf(f_regression, df_regression, df_residual)
|
447 |
+
|
448 |
f_lack_of_fit = ms_lack_of_fit / ms_pure_error if not np.isnan(ms_lack_of_fit) else np.nan
|
449 |
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
|
450 |
|
|
|
457 |
'F': [f_regression, np.nan, f_lack_of_fit, np.nan, np.nan],
|
458 |
'Valor p': [p_regression, np.nan, p_lack_of_fit, np.nan, np.nan]
|
459 |
})
|
460 |
+
|
461 |
# Calcular la suma de cuadrados y estadísticos F para la curvatura
|
462 |
ss_curvature = anova_reduced['sum_sq'][f'I({self.x1_name} ** 2)'] + \
|
463 |
+
anova_reduced['sum_sq'][f'I({self.x2_name} ** 2)'] + \
|
464 |
+
anova_reduced['sum_sq'][f'I({self.x3_name} ** 2)']
|
465 |
df_curvature = 3
|
466 |
ms_curvature = ss_curvature / df_curvature
|
467 |
f_curvature = ms_curvature / ms_residual
|
|
|
469 |
|
470 |
# Añadir la fila de curvatura a la tabla ANOVA
|
471 |
detailed_anova_table.loc[len(detailed_anova_table)] = [
|
472 |
+
'Curvatura',
|
473 |
+
ss_curvature,
|
474 |
+
df_curvature,
|
475 |
ms_curvature,
|
476 |
f_curvature,
|
477 |
p_curvature
|
478 |
]
|
479 |
+
|
480 |
# --- Agregar % de Contribución ---
|
481 |
detailed_anova_table['% Contribución'] = (detailed_anova_table['Suma de Cuadrados'] / ss_total) * 100
|
482 |
|
|
|
487 |
|
488 |
def get_all_tables(self):
|
489 |
"""
|
490 |
+
Obtiene todas las tablas generadas para ser exportadas a Excel.
|
491 |
+
"""
|
492 |
prediction_table = self.generate_prediction_table()
|
493 |
anova_table = self.calculate_anova_table()
|
494 |
|
|
|
499 |
|
500 |
def save_figures_to_zip(self):
|
501 |
"""
|
502 |
+
Guarda todas las figuras almacenadas en self.all_figures a un archivo ZIP en memoria.
|
503 |
+
"""
|
504 |
if not self.all_figures:
|
505 |
return None
|
506 |
|
|
|
520 |
|
521 |
def save_fig_to_bytes(self, fig):
|
522 |
"""
|
523 |
+
Convierte una figura Plotly a bytes en formato PNG.
|
524 |
+
"""
|
525 |
return fig.to_image(format="png")
|
526 |
|
527 |
def save_all_figures_png(self):
|
528 |
"""
|
529 |
+
Guarda todas las figuras en archivos PNG temporales y retorna las rutas.
|
530 |
+
"""
|
531 |
png_paths = []
|
532 |
for idx, fig in enumerate(self.all_figures, start=1):
|
533 |
img_bytes = fig.to_image(format="png")
|
|
|
539 |
|
540 |
def save_tables_to_excel(self):
|
541 |
"""
|
542 |
+
Guarda todas las tablas en un archivo Excel con múltiples hojas y retorna la ruta del archivo.
|
543 |
+
"""
|
544 |
tables = self.get_all_tables()
|
545 |
excel_buffer = io.BytesIO()
|
546 |
with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer:
|
|
|
558 |
|
559 |
def export_tables_to_word(self, tables_dict):
|
560 |
"""
|
561 |
+
Exporta las tablas proporcionadas a un documento de Word.
|
562 |
+
"""
|
563 |
if not tables_dict:
|
564 |
return None
|
565 |
|
|
|
615 |
|
616 |
def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str):
|
617 |
"""
|
618 |
+
Carga los datos del diseño Box-Behnken desde cajas de texto y crea la instancia de RSM_BoxBehnken.
|
619 |
+
"""
|
620 |
try:
|
621 |
# Convertir los niveles a listas de números
|
622 |
x1_levels = [float(x.strip()) for x in x1_levels_str.split(',')]
|