|
import numpy as np |
|
import pandas as pd |
|
import statsmodels.formula.api as smf |
|
import statsmodels.api as sm |
|
import plotly.graph_objects as go |
|
from scipy.optimize import minimize |
|
import plotly.express as px |
|
from scipy.stats import t, f |
|
import gradio as gr |
|
import io |
|
import zipfile |
|
import tempfile |
|
from datetime import datetime |
|
import docx |
|
from docx.shared import Inches, Pt |
|
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT |
|
from matplotlib.colors import to_hex |
|
import os |
|
|
|
|
|
class RSM_BoxBehnken: |
|
def __init__(self, data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels): |
|
""" |
|
Inicializa la clase con los datos del diseño Box-Behnken. |
|
""" |
|
self.data = data.copy() |
|
self.model = None |
|
self.model_simplified = None |
|
self.optimized_results = None |
|
self.optimal_levels = None |
|
self.all_figures = [] |
|
self.x1_name = x1_name |
|
self.x2_name = x2_name |
|
self.x3_name = x3_name |
|
self.y_name = y_name |
|
|
|
|
|
self.x1_levels = x1_levels |
|
self.x2_levels = x2_levels |
|
self.x3_levels = x3_levels |
|
|
|
def get_levels(self, variable_name): |
|
""" |
|
Obtiene los niveles para una variable específica. |
|
""" |
|
if variable_name == self.x1_name: |
|
return self.x1_levels |
|
elif variable_name == self.x2_name: |
|
return self.x2_levels |
|
elif variable_name == self.x3_name: |
|
return self.x3_levels |
|
else: |
|
raise ValueError(f"Variable desconocida: {variable_name}") |
|
|
|
def fit_model(self): |
|
""" |
|
Ajusta el modelo de segundo orden completo a los datos. |
|
""" |
|
formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \ |
|
f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2) + ' \ |
|
f'{self.x1_name}:{self.x2_name} + {self.x1_name}:{self.x3_name} + {self.x2_name}:{self.x3_name}' |
|
self.model = smf.ols(formula, data=self.data).fit() |
|
print("Modelo Completo:") |
|
print(self.model.summary()) |
|
return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo") |
|
|
|
def fit_simplified_model(self): |
|
""" |
|
Ajusta el modelo de segundo orden a los datos, eliminando términos no significativos. |
|
""" |
|
formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + ' \ |
|
f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)' |
|
self.model_simplified = smf.ols(formula, data=self.data).fit() |
|
print("\nModelo Simplificado:") |
|
print(self.model_simplified.summary()) |
|
return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Modelo Simplificado") |
|
|
|
def optimize(self, method='Nelder-Mead'): |
|
""" |
|
Encuentra los niveles óptimos de los factores para maximizar la respuesta usando el modelo simplificado. |
|
""" |
|
if self.model_simplified is None: |
|
print("Error: Ajusta el modelo simplificado primero.") |
|
return |
|
|
|
def objective_function(x): |
|
return -self.model_simplified.predict(pd.DataFrame({ |
|
self.x1_name: [x[0]], |
|
self.x2_name: [x[1]], |
|
self.x3_name: [x[2]] |
|
})).values[0] |
|
|
|
bounds = [(-1, 1), (-1, 1), (-1, 1)] |
|
x0 = [0, 0, 0] |
|
|
|
self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds) |
|
self.optimal_levels = self.optimized_results.x |
|
|
|
|
|
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) |
|
] |
|
|
|
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 |
|
}) |
|
|
|
return optimization_table.round(3) |
|
|
|
def plot_rsm_individual(self, fixed_variable, fixed_level): |
|
""" |
|
Genera un gráfico de superficie de respuesta (RSM) individual para una configuración específica. |
|
""" |
|
if self.model_simplified is None: |
|
print("Error: Ajusta el modelo simplificado primero.") |
|
return None |
|
|
|
|
|
varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable] |
|
|
|
|
|
x_natural_levels = self.get_levels(varying_variables[0]) |
|
y_natural_levels = self.get_levels(varying_variables[1]) |
|
|
|
|
|
x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100) |
|
y_range_natural = np.linspace(y_natural_levels[0], y_natural_levels[-1], 100) |
|
x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural) |
|
|
|
|
|
x_grid_coded = self.natural_to_coded(x_grid_natural, varying_variables[0]) |
|
y_grid_coded = self.natural_to_coded(y_grid_natural, varying_variables[1]) |
|
|
|
|
|
prediction_data = pd.DataFrame({ |
|
varying_variables[0]: x_grid_coded.flatten(), |
|
varying_variables[1]: y_grid_coded.flatten(), |
|
}) |
|
prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable) |
|
|
|
|
|
z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape) |
|
|
|
|
|
fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable) |
|
subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)] |
|
|
|
|
|
valid_levels = [-1, 0, 1] |
|
experiments_data = subset_data[ |
|
subset_data[varying_variables[0]].isin(valid_levels) & |
|
subset_data[varying_variables[1]].isin(valid_levels) |
|
] |
|
|
|
|
|
experiments_x_natural = experiments_data[varying_variables[0]].apply(lambda x: self.coded_to_natural(x, varying_variables[0])) |
|
experiments_y_natural = experiments_data[varying_variables[1]].apply(lambda x: self.coded_to_natural(x, varying_variables[1])) |
|
|
|
|
|
fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)]) |
|
|
|
|
|
|
|
for i in range(x_grid_natural.shape[0]): |
|
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' |
|
)) |
|
|
|
for j in range(x_grid_natural.shape[1]): |
|
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' |
|
)) |
|
|
|
|
|
|
|
|
|
colors = px.colors.qualitative.Safe |
|
point_labels = [f"{row[self.y_name]:.3f}" for _, row in experiments_data.iterrows()] |
|
|
|
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' |
|
)) |
|
|
|
|
|
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)}) (Modelo Simplificado)</sup>", |
|
height=800, |
|
width=1000, |
|
showlegend=True |
|
) |
|
return fig |
|
|
|
def get_units(self, variable_name): |
|
""" |
|
Define las unidades de las variables para etiquetas. |
|
Puedes personalizar este método según tus necesidades. |
|
""" |
|
units = { |
|
'Glucosa': 'g/L', |
|
'Extracto_de_Levadura': 'g/L', |
|
'Triptofano': 'g/L', |
|
'AIA_ppm': 'ppm' |
|
} |
|
return units.get(variable_name, '') |
|
|
|
def generate_all_plots(self): |
|
""" |
|
Genera todas las gráficas de RSM, variando la variable fija y sus niveles usando el modelo simplificado. |
|
Almacena las figuras en self.all_figures. |
|
""" |
|
if self.model_simplified is None: |
|
print("Error: Ajusta el modelo simplificado primero.") |
|
return |
|
|
|
self.all_figures = [] |
|
|
|
|
|
levels_to_plot_natural = { |
|
self.x1_name: self.x1_levels, |
|
self.x2_name: self.x2_levels, |
|
self.x3_name: self.x3_levels |
|
} |
|
|
|
|
|
for fixed_variable in [self.x1_name, self.x2_name, self.x3_name]: |
|
for level in levels_to_plot_natural[fixed_variable]: |
|
fig = self.plot_rsm_individual(fixed_variable, level) |
|
if fig is not None: |
|
self.all_figures.append(fig) |
|
|
|
def coded_to_natural(self, coded_value, variable_name): |
|
"""Convierte un valor codificado a su valor natural.""" |
|
levels = self.get_levels(variable_name) |
|
return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2 |
|
|
|
def natural_to_coded(self, natural_value, variable_name): |
|
"""Convierte un valor natural a su valor codificado.""" |
|
levels = self.get_levels(variable_name) |
|
return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0]) |
|
|
|
def pareto_chart(self, model, title): |
|
""" |
|
Genera un diagrama de Pareto para los efectos usando estadísticos F, |
|
incluyendo la línea de significancia. |
|
""" |
|
|
|
|
|
fvalues = model.tvalues[1:]**2 |
|
abs_fvalues = np.abs(fvalues) |
|
sorted_idx = np.argsort(abs_fvalues)[::-1] |
|
sorted_fvalues = abs_fvalues[sorted_idx] |
|
sorted_names = fvalues.index[sorted_idx] |
|
|
|
|
|
alpha = 0.05 |
|
dof_num = 1 |
|
dof_den = model.df_resid |
|
f_critical = f.ppf(1 - alpha, dof_num, dof_den) |
|
|
|
|
|
fig = px.bar( |
|
x=sorted_fvalues.round(3), |
|
y=sorted_names, |
|
orientation='h', |
|
labels={'x': 'Estadístico F', 'y': 'Término'}, |
|
title=title |
|
) |
|
fig.update_yaxes(autorange="reversed") |
|
|
|
|
|
fig.add_vline(x=f_critical, line_dash="dot", |
|
annotation_text=f"F crítico = {f_critical:.3f}", |
|
annotation_position="bottom right") |
|
|
|
return fig |
|
|
|
def get_simplified_equation(self): |
|
""" |
|
Retorna la ecuación del modelo simplificado como una cadena de texto. |
|
""" |
|
if self.model_simplified is None: |
|
print("Error: Ajusta el modelo simplificado primero.") |
|
return None |
|
|
|
coefficients = self.model_simplified.params |
|
equation = f"{self.y_name} = {coefficients['Intercept']:.3f}" |
|
|
|
for term, coef in coefficients.items(): |
|
if term != 'Intercept': |
|
if term == f'{self.x1_name}': |
|
equation += f" + {coef:.3f}*{self.x1_name}" |
|
elif term == f'{self.x2_name}': |
|
equation += f" + {coef:.3f}*{self.x2_name}" |
|
elif term == f'{self.x3_name}': |
|
equation += f" + {coef:.3f}*{self.x3_name}" |
|
elif term == f'I({self.x1_name} ** 2)': |
|
equation += f" + {coef:.3f}*{self.x1_name}^2" |
|
elif term == f'I({self.x2_name} ** 2)': |
|
equation += f" + {coef:.3f}*{self.x2_name}^2" |
|
elif term == f'I({self.x3_name} ** 2)': |
|
equation += f" + {coef:.3f}*{self.x3_name}^2" |
|
|
|
return equation |
|
|
|
def generate_prediction_table(self): |
|
""" |
|
Genera una tabla con los valores actuales, predichos y residuales. |
|
""" |
|
if self.model_simplified is None: |
|
print("Error: Ajusta el modelo simplificado primero.") |
|
return None |
|
|
|
self.data['Predicho'] = self.model_simplified.predict(self.data) |
|
self.data['Residual'] = self.data[self.y_name] - self.data['Predicho'] |
|
|
|
return self.data[[self.y_name, 'Predicho', 'Residual']].round(3) |
|
|
|
def calculate_contribution_percentage(self): |
|
""" |
|
Calcula el porcentaje de contribución de cada factor usando estadísticos F. |
|
""" |
|
if self.model_simplified is None: |
|
print("Error: Ajusta el modelo simplificado primero.") |
|
return None |
|
|
|
|
|
anova_table = sm.stats.anova_lm(self.model_simplified, typ=2) |
|
|
|
|
|
ss_total = anova_table['sum_sq'].sum() |
|
|
|
|
|
contribution_table = pd.DataFrame({ |
|
'Fuente de Variación': [], |
|
'Suma de Cuadrados': [], |
|
'Grados de Libertad': [], |
|
'Cuadrado Medio': [], |
|
'F': [], |
|
'Valor p': [], |
|
'% Contribución': [] |
|
}) |
|
|
|
|
|
ms_error = anova_table.loc['Residual', 'sum_sq'] / anova_table.loc['Residual', 'df'] |
|
|
|
for index, row in anova_table.iterrows(): |
|
if index != 'Residual': |
|
factor_name = index |
|
if factor_name == f'I({self.x1_name} ** 2)': |
|
factor_name = f'{self.x1_name}^2' |
|
elif factor_name == f'I({self.x2_name} ** 2)': |
|
factor_name = f'{self.x2_name}^2' |
|
elif factor_name == f'I({self.x3_name} ** 2)': |
|
factor_name = f'{self.x3_name}^2' |
|
|
|
ss_factor = row['sum_sq'] |
|
df_factor = row['df'] |
|
ms_factor = ss_factor / df_factor |
|
f_stat = ms_factor / ms_error |
|
p_value = f.sf(f_stat, df_factor, anova_table.loc['Residual', 'df']) |
|
contribution_percentage = (ss_factor / ss_total) * 100 |
|
|
|
contribution_table = pd.concat([contribution_table, pd.DataFrame({ |
|
'Fuente de Variación': [factor_name], |
|
'Suma de Cuadrados': [ss_factor], |
|
'Grados de Libertad': [df_factor], |
|
'Cuadrado Medio': [ms_factor], |
|
'F': [f_stat], |
|
'Valor p': [p_value], |
|
'% Contribución': [contribution_percentage] |
|
})], ignore_index=True) |
|
|
|
|
|
f_global = anova_table['sum_sq'][:-1].sum() / anova_table['df'][:-1].sum() / ms_error |
|
p_global = f.sf(f_global, anova_table['df'][:-1].sum(), anova_table.loc['Residual', 'df']) |
|
|
|
|
|
contribution_table = pd.concat([contribution_table, pd.DataFrame({ |
|
'Fuente de Variación': ['F global'], |
|
'Suma de Cuadrados': [np.nan], |
|
'Grados de Libertad': [np.nan], |
|
'Cuadrado Medio': [np.nan], |
|
'F': [f_global], |
|
'Valor p': [p_global], |
|
'% Contribución': [np.nan] |
|
})], ignore_index=True) |
|
|
|
return contribution_table.round(3) |
|
|
|
def calculate_detailed_anova(self): |
|
""" |
|
Calcula la tabla ANOVA detallada con la descomposición del error residual. |
|
""" |
|
if self.model_simplified is None: |
|
print("Error: Ajusta el modelo simplificado primero.") |
|
return None |
|
|
|
|
|
|
|
formula_reduced = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \ |
|
f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)' |
|
model_reduced = smf.ols(formula_reduced, data=self.data).fit() |
|
|
|
|
|
anova_reduced = sm.stats.anova_lm(model_reduced, typ=2) |
|
|
|
|
|
ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2) |
|
|
|
|
|
df_total = len(self.data) - 1 |
|
|
|
|
|
ss_regression = anova_reduced['sum_sq'][:-1].sum() |
|
|
|
|
|
df_regression = len(anova_reduced) - 1 |
|
|
|
|
|
ss_residual = self.model_simplified.ssr |
|
df_residual = self.model_simplified.df_resid |
|
|
|
|
|
replicas = self.data[self.data.duplicated(subset=[self.x1_name, self.x2_name, self.x3_name], keep=False)] |
|
if not replicas.empty: |
|
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 |
|
df_pure_error = len(replicas) - replicas.groupby([self.x1_name, self.x2_name, self.x3_name]).ngroups |
|
else: |
|
ss_pure_error = np.nan |
|
df_pure_error = np.nan |
|
|
|
|
|
ss_lack_of_fit = ss_residual - ss_pure_error if not np.isnan(ss_pure_error) else np.nan |
|
df_lack_of_fit = df_residual - df_pure_error if not np.isnan(df_pure_error) else np.nan |
|
|
|
|
|
ms_regression = ss_regression / df_regression |
|
ms_residual = ss_residual / df_residual |
|
ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit if not np.isnan(ss_lack_of_fit) else np.nan |
|
ms_pure_error = ss_pure_error / df_pure_error if not np.isnan(ss_pure_error) else np.nan |
|
|
|
|
|
f_regression = ms_regression / ms_residual |
|
p_regression = 1 - f.cdf(f_regression, df_regression, df_residual) |
|
|
|
f_lack_of_fit = ms_lack_of_fit / ms_pure_error if not np.isnan(ms_lack_of_fit) else np.nan |
|
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 |
|
|
|
|
|
detailed_anova_table = pd.DataFrame({ |
|
'Fuente de Variación': ['Regresión', 'Residual', 'Falta de Ajuste', 'Error Puro', 'Total'], |
|
'Suma de Cuadrados': [ss_regression, ss_residual, ss_lack_of_fit, ss_pure_error, ss_total], |
|
'Grados de Libertad': [df_regression, df_residual, df_lack_of_fit, df_pure_error, df_total], |
|
'Cuadrado Medio': [ms_regression, ms_residual, ms_lack_of_fit, ms_pure_error, np.nan], |
|
'F': [f_regression, np.nan, f_lack_of_fit, np.nan, np.nan], |
|
'Valor p': [p_regression, np.nan, p_lack_of_fit, np.nan, np.nan] |
|
}) |
|
|
|
|
|
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)'] |
|
df_curvature = 3 |
|
ms_curvature = ss_curvature / df_curvature |
|
f_curvature = ms_curvature / ms_residual |
|
p_curvature = 1 - f.cdf(f_curvature, df_curvature, df_residual) |
|
|
|
|
|
detailed_anova_table.loc[len(detailed_anova_table)] = [ |
|
'Curvatura', |
|
ss_curvature, |
|
df_curvature, |
|
ms_curvature, |
|
f_curvature, |
|
p_curvature |
|
] |
|
|
|
|
|
detailed_anova_table = detailed_anova_table.reindex([0, 5, 1, 2, 3, 4]).reset_index(drop=True) |
|
|
|
return detailed_anova_table.round(3) |
|
|
|
def get_all_tables(self): |
|
""" |
|
Obtiene todas las tablas generadas para ser exportadas a Excel. |
|
""" |
|
prediction_table = self.generate_prediction_table() |
|
contribution_table = self.calculate_contribution_percentage() |
|
detailed_anova_table = self.calculate_detailed_anova() |
|
|
|
return { |
|
'Predicciones': prediction_table, |
|
'% Contribución': contribution_table, |
|
'ANOVA Detallada': detailed_anova_table |
|
} |
|
|
|
def save_figures_to_zip(self): |
|
""" |
|
Guarda todas las figuras almacenadas en self.all_figures a un archivo ZIP en memoria. |
|
""" |
|
if not self.all_figures: |
|
return None |
|
|
|
zip_buffer = io.BytesIO() |
|
with zipfile.ZipFile(zip_buffer, 'w') as zip_file: |
|
for idx, fig in enumerate(self.all_figures, start=1): |
|
img_bytes = fig.to_image(format="png") |
|
zip_file.writestr(f'Grafico_{idx}.png', img_bytes) |
|
zip_buffer.seek(0) |
|
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".zip") as temp_file: |
|
temp_file.write(zip_buffer.read()) |
|
temp_path = temp_file.name |
|
|
|
return temp_path |
|
|
|
def save_fig_to_bytes(self, fig): |
|
""" |
|
Convierte una figura Plotly a bytes en formato PNG. |
|
""" |
|
return fig.to_image(format="png") |
|
|
|
def save_all_figures_png(self): |
|
""" |
|
Guarda todas las figuras en archivos PNG temporales y retorna las rutas. |
|
""" |
|
png_paths = [] |
|
for idx, fig in enumerate(self.all_figures, start=1): |
|
img_bytes = fig.to_image(format="png") |
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file: |
|
temp_file.write(img_bytes) |
|
temp_path = temp_file.name |
|
png_paths.append(temp_path) |
|
return png_paths |
|
|
|
def save_tables_to_excel(self): |
|
""" |
|
Guarda todas las tablas en un archivo Excel con múltiples hojas y retorna la ruta del archivo. |
|
""" |
|
tables = self.get_all_tables() |
|
excel_buffer = io.BytesIO() |
|
with pd.ExcelWriter(excel_buffer, engine='xlsxwriter') as writer: |
|
for sheet_name, table in tables.items(): |
|
table.to_excel(writer, sheet_name=sheet_name, index=False) |
|
excel_buffer.seek(0) |
|
excel_bytes = excel_buffer.read() |
|
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".xlsx") as temp_file: |
|
temp_file.write(excel_bytes) |
|
temp_path = temp_file.name |
|
|
|
return temp_path |
|
|
|
def export_tables_to_word(self, tables_dict): |
|
""" |
|
Exporta las tablas proporcionadas a un documento de Word. |
|
""" |
|
if not tables_dict: |
|
return None |
|
|
|
doc = docx.Document() |
|
|
|
|
|
style = doc.styles['Normal'] |
|
font = style.font |
|
font.name = 'Times New Roman' |
|
font.size = Pt(12) |
|
|
|
|
|
titulo = doc.add_heading('Informe de Optimización de Producción de AIA', 0) |
|
titulo.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER |
|
|
|
doc.add_paragraph(f"Fecha: {datetime.now().strftime('%d/%m/%Y %H:%M')}").alignment = WD_PARAGRAPH_ALIGNMENT.CENTER |
|
|
|
doc.add_paragraph('\n') |
|
|
|
for sheet_name, table in tables_dict.items(): |
|
|
|
doc.add_heading(sheet_name, level=1) |
|
|
|
if table.empty: |
|
doc.add_paragraph("No hay datos disponibles para esta tabla.") |
|
continue |
|
|
|
|
|
table_doc = doc.add_table(rows=1, cols=len(table.columns)) |
|
table_doc.style = 'Light List Accent 1' |
|
|
|
|
|
hdr_cells = table_doc.rows[0].cells |
|
for idx, col_name in enumerate(table.columns): |
|
hdr_cells[idx].text = col_name |
|
|
|
|
|
for _, row in table.iterrows(): |
|
row_cells = table_doc.add_row().cells |
|
for idx, item in enumerate(row): |
|
row_cells[idx].text = str(item) |
|
|
|
doc.add_paragraph('\n') |
|
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp: |
|
doc.save(tmp.name) |
|
tmp_path = tmp.name |
|
|
|
return tmp_path |
|
|
|
|
|
|
|
def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str): |
|
""" |
|
Carga los datos del diseño Box-Behnken desde cajas de texto y crea la instancia de RSM_BoxBehnken. |
|
""" |
|
try: |
|
|
|
x1_levels = [float(x.strip()) for x in x1_levels_str.split(',')] |
|
x2_levels = [float(x.strip()) for x in x2_levels_str.split(',')] |
|
x3_levels = [float(x.strip()) for x in x3_levels_str.split(',')] |
|
|
|
|
|
data_list = [row.split(',') for row in data_str.strip().split('\n')] |
|
column_names = ['Exp.', x1_name, x2_name, x3_name, y_name] |
|
data = pd.DataFrame(data_list, columns=column_names) |
|
data = data.apply(pd.to_numeric, errors='coerce') |
|
|
|
|
|
if not all(col in data.columns for col in column_names): |
|
raise ValueError("El formato de los datos no es correcto.") |
|
|
|
|
|
global rsm |
|
rsm = RSM_BoxBehnken(data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels) |
|
|
|
return data.round(3), x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels, gr.update(visible=True) |
|
|
|
except Exception as e: |
|
|
|
error_message = f"Error al cargar los datos: {str(e)}" |
|
print(error_message) |
|
return None, "", "", "", "", [], [], [], gr.update(visible=False) |
|
|
|
def fit_and_optimize_model(): |
|
if 'rsm' not in globals(): |
|
return [None]*11 |
|
|
|
|
|
model_completo, pareto_completo = rsm.fit_model() |
|
model_simplificado, pareto_simplificado = rsm.fit_simplified_model() |
|
optimization_table = rsm.optimize() |
|
equation = rsm.get_simplified_equation() |
|
prediction_table = rsm.generate_prediction_table() |
|
contribution_table = rsm.calculate_contribution_percentage() |
|
anova_table = rsm.calculate_detailed_anova() |
|
|
|
|
|
rsm.generate_all_plots() |
|
|
|
|
|
equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ") |
|
equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}" |
|
|
|
|
|
excel_path = rsm.save_tables_to_excel() |
|
|
|
|
|
zip_path = rsm.save_figures_to_zip() |
|
|
|
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 |
|
) |
|
|
|
def show_plot(current_index, all_figures): |
|
if not all_figures: |
|
return None, "No hay gráficos disponibles.", current_index |
|
selected_fig = all_figures[current_index] |
|
plot_info_text = f"Gráfico {current_index + 1} de {len(all_figures)}" |
|
return selected_fig, plot_info_text, current_index |
|
|
|
def navigate_plot(direction, current_index, all_figures): |
|
""" |
|
Navega entre los gráficos. |
|
""" |
|
if not all_figures: |
|
return None, "No hay gráficos disponibles.", current_index |
|
|
|
if direction == 'left': |
|
new_index = (current_index - 1) % len(all_figures) |
|
elif direction == 'right': |
|
new_index = (current_index + 1) % len(all_figures) |
|
else: |
|
new_index = current_index |
|
|
|
selected_fig = all_figures[new_index] |
|
plot_info_text = f"Gráfico {new_index + 1} de {len(all_figures)}" |
|
|
|
return selected_fig, plot_info_text, new_index |
|
|
|
def download_current_plot(all_figures, current_index): |
|
""" |
|
Descarga la figura actual como PNG. |
|
""" |
|
if not all_figures: |
|
return None |
|
fig = all_figures[current_index] |
|
img_bytes = rsm.save_fig_to_bytes(fig) |
|
filename = f"Grafico_RSM_{current_index + 1}.png" |
|
|
|
|
|
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file: |
|
temp_file.write(img_bytes) |
|
temp_path = temp_file.name |
|
|
|
return temp_path |
|
|
|
def download_all_plots_zip(): |
|
""" |
|
Descarga todas las figuras en un archivo ZIP. |
|
""" |
|
if 'rsm' not in globals(): |
|
return None |
|
zip_path = rsm.save_figures_to_zip() |
|
if zip_path: |
|
filename = f"Graficos_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip" |
|
|
|
return zip_path |
|
return None |
|
|
|
def download_all_tables_excel(): |
|
""" |
|
Descarga todas las tablas en un archivo Excel con múltiples hojas. |
|
""" |
|
if 'rsm' not in globals(): |
|
return None |
|
excel_path = rsm.save_tables_to_excel() |
|
if excel_path: |
|
filename = f"Tablas_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx" |
|
|
|
return excel_path |
|
return None |
|
|
|
def exportar_word(rsm_instance, tables_dict): |
|
""" |
|
Función para exportar las tablas a un documento de Word. |
|
""" |
|
word_path = rsm_instance.export_tables_to_word(tables_dict) |
|
if word_path and os.path.exists(word_path): |
|
return word_path |
|
return None |
|
|
|
|
|
|
|
def create_gradio_interface(): |
|
with gr.Blocks() as demo: |
|
gr.Markdown("# Optimización de la producción de AIA usando RSM Box-Behnken") |
|
|
|
with gr.Row(): |
|
with gr.Column(): |
|
gr.Markdown("## Configuración del Diseño") |
|
x1_name_input = gr.Textbox(label="Nombre de la Variable X1 (ej. Glucosa)", value="Glucosa") |
|
x2_name_input = gr.Textbox(label="Nombre de la Variable X2 (ej. Extracto de Levadura)", value="Extracto_de_Levadura") |
|
x3_name_input = gr.Textbox(label="Nombre de la Variable X3 (ej. Triptófano)", value="Triptofano") |
|
y_name_input = gr.Textbox(label="Nombre de la Variable Dependiente (ej. AIA (ppm))", value="AIA_ppm") |
|
x1_levels_input = gr.Textbox(label="Niveles de X1 (separados por comas)", value="1, 3.5, 5.5") |
|
x2_levels_input = gr.Textbox(label="Niveles de X2 (separados por comas)", value="0.03, 0.2, 0.3") |
|
x3_levels_input = gr.Textbox(label="Niveles de X3 (separados por comas)", value="0.4, 0.65, 0.9") |
|
data_input = gr.Textbox(label="Datos del Experimento (formato CSV)", lines=10, value="""1,-1,-1,0,166.594 |
|
2,1,-1,0,177.557 |
|
3,-1,1,0,127.261 |
|
4,1,1,0,147.573 |
|
5,-1,0,-1,188.883 |
|
6,1,0,-1,224.527 |
|
7,-1,0,1,190.238 |
|
8,1,0,1,226.483 |
|
9,0,-1,-1,195.550 |
|
10,0,1,-1,149.493 |
|
11,0,-1,1,187.683 |
|
12,0,1,1,148.621 |
|
13,0,0,0,278.951 |
|
14,0,0,0,297.238 |
|
15,0,0,0,280.896""") |
|
load_button = gr.Button("Cargar Datos") |
|
|
|
with gr.Column(): |
|
gr.Markdown("## Datos Cargados") |
|
data_output = gr.Dataframe(label="Tabla de Datos", interactive=False) |
|
|
|
|
|
with gr.Row(visible=False) as analysis_row: |
|
with gr.Column(): |
|
fit_button = gr.Button("Ajustar Modelo y Optimizar") |
|
gr.Markdown("**Modelo Completo**") |
|
model_completo_output = gr.HTML() |
|
pareto_completo_output = gr.Plot() |
|
gr.Markdown("**Modelo Simplificado**") |
|
model_simplificado_output = gr.HTML() |
|
pareto_simplificado_output = gr.Plot() |
|
gr.Markdown("**Ecuación del Modelo Simplificado**") |
|
equation_output = gr.HTML() |
|
optimization_table_output = gr.Dataframe(label="Tabla de Optimización", interactive=False) |
|
prediction_table_output = gr.Dataframe(label="Tabla de Predicciones", interactive=False) |
|
contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución", interactive=False) |
|
anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada", interactive=False) |
|
gr.Markdown("## Descargar Todas las Tablas") |
|
download_excel_button = gr.DownloadButton("Descargar Tablas en Excel") |
|
download_word_button = gr.DownloadButton("Descargar Tablas en Word") |
|
|
|
with gr.Column(): |
|
gr.Markdown("## Generar Gráficos de Superficie de Respuesta") |
|
fixed_variable_input = gr.Dropdown(label="Variable Fija", choices=["Glucosa", "Extracto_de_Levadura", "Triptofano"], value="Glucosa") |
|
fixed_level_input = gr.Slider(label="Nivel de Variable Fija", minimum=-1, maximum=1, step=0.01, value=0.0) |
|
plot_button = gr.Button("Generar Gráficos") |
|
with gr.Row(): |
|
left_button = gr.Button("<") |
|
right_button = gr.Button(">") |
|
rsm_plot_output = gr.Plot() |
|
plot_info = gr.Textbox(label="Información del Gráfico", value="Gráfico 1 de 9", interactive=False) |
|
with gr.Row(): |
|
download_plot_button = gr.DownloadButton("Descargar Gráfico Actual (PNG)") |
|
download_all_plots_button = gr.DownloadButton("Descargar Todos los Gráficos (ZIP)") |
|
current_index_state = gr.State(0) |
|
all_figures_state = gr.State([]) |
|
|
|
|
|
load_button.click( |
|
load_data, |
|
inputs=[x1_name_input, x2_name_input, x3_name_input, y_name_input, x1_levels_input, x2_levels_input, x3_levels_input, data_input], |
|
outputs=[data_output, x1_name_input, x2_name_input, x3_name_input, y_name_input, x1_levels_input, x2_levels_input, x3_levels_input, analysis_row] |
|
) |
|
|
|
|
|
fit_button.click( |
|
fit_and_optimize_model, |
|
inputs=[], |
|
outputs=[ |
|
model_completo_output, |
|
pareto_completo_output, |
|
model_simplificado_output, |
|
pareto_simplificado_output, |
|
equation_output, |
|
optimization_table_output, |
|
prediction_table_output, |
|
contribution_table_output, |
|
anova_table_output, |
|
download_all_plots_button, |
|
download_excel_button |
|
] |
|
) |
|
|
|
|
|
plot_button.click( |
|
lambda fixed_var, fixed_lvl: ( |
|
rsm.plot_rsm_individual(fixed_var, fixed_lvl), |
|
f"Gráfico 1 de {len(rsm.all_figures)}" if rsm.all_figures else "No hay gráficos disponibles.", |
|
0, |
|
rsm.all_figures |
|
), |
|
inputs=[fixed_variable_input, fixed_level_input], |
|
outputs=[rsm_plot_output, plot_info, current_index_state, all_figures_state] |
|
) |
|
|
|
|
|
left_button.click( |
|
lambda current_index, all_figures: navigate_plot('left', current_index, all_figures), |
|
inputs=[current_index_state, all_figures_state], |
|
outputs=[rsm_plot_output, plot_info, current_index_state] |
|
) |
|
right_button.click( |
|
lambda current_index, all_figures: navigate_plot('right', current_index, all_figures), |
|
inputs=[current_index_state, all_figures_state], |
|
outputs=[rsm_plot_output, plot_info, current_index_state] |
|
) |
|
|
|
|
|
download_plot_button.click( |
|
download_current_plot, |
|
inputs=[all_figures_state, current_index_state], |
|
outputs=download_plot_button |
|
) |
|
|
|
|
|
download_all_plots_button.click( |
|
download_all_plots_zip, |
|
inputs=[], |
|
outputs=download_all_plots_button |
|
) |
|
|
|
|
|
download_excel_button.click( |
|
fn=lambda: download_all_tables_excel(), |
|
inputs=[], |
|
outputs=download_excel_button |
|
) |
|
|
|
download_word_button.click( |
|
fn=lambda: exportar_word(rsm, rsm.get_all_tables()), |
|
inputs=[], |
|
outputs=download_word_button |
|
) |
|
|
|
|
|
gr.Markdown("## Ejemplo de uso") |
|
gr.Markdown(""" |
|
1. Introduce los nombres de las variables y sus niveles en las cajas de texto correspondientes. |
|
2. Copia y pega los datos del experimento en la caja de texto 'Datos del Experimento'. |
|
3. Haz clic en 'Cargar Datos' para cargar los datos en la tabla. |
|
4. Haz clic en 'Ajustar Modelo y Optimizar' para ajustar el modelo y encontrar los niveles óptimos de los factores. |
|
5. Selecciona una variable fija y su nivel en los controles deslizantes. |
|
6. Haz clic en 'Generar Gráficos' para generar los gráficos de superficie de respuesta. |
|
7. Navega entre los gráficos usando los botones '<' y '>'. |
|
8. Descarga el gráfico actual en PNG o descarga todos los gráficos en un ZIP. |
|
9. Descarga todas las tablas en un archivo Excel o Word con los botones correspondientes. |
|
""") |
|
|
|
return demo |
|
|
|
|
|
|
|
def main(): |
|
interface = create_gradio_interface() |
|
interface.launch(share=True) |
|
|
|
if __name__ == "__main__": |
|
main() |