Spaces:
Sleeping
Sleeping
| 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 | |
| # --- Clase RSM_BoxBehnken --- | |
| 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 = [] # Lista para almacenar las figuras | |
| self.x1_name = x1_name | |
| self.x2_name = x2_name | |
| self.x3_name = x3_name | |
| self.y_name = y_name | |
| # Niveles originales de las variables | |
| 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 | |
| # Convertir niveles óptimos de codificados a naturales | |
| 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) | |
| ] | |
| # Crear la tabla de optimización | |
| 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) # Redondear a 3 decimales | |
| 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 | |
| # Determinar las variables que varían y sus niveles naturales | |
| varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable] | |
| # Establecer los niveles naturales para las variables que varían | |
| x_natural_levels = self.get_levels(varying_variables[0]) | |
| y_natural_levels = self.get_levels(varying_variables[1]) | |
| # Crear una malla de puntos para las variables que varían (en unidades naturales) | |
| 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) | |
| # Convertir la malla de variables naturales a codificadas | |
| 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]) | |
| # Crear un DataFrame para la predicción con variables codificadas | |
| 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) | |
| # Calcular los valores predichos | |
| z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape) | |
| # Filtrar por el nivel de la variable fija (en codificado) | |
| fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable) | |
| subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)] | |
| # Filtrar por niveles válidos en las variables que varían | |
| 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) | |
| ] | |
| # Convertir coordenadas de experimentos a naturales | |
| 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])) | |
| # Crear el gráfico de superficie con variables naturales en los ejes y transparencia | |
| fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)]) | |
| # --- Añadir cuadrícula a la superficie --- | |
| # Líneas en la dirección x | |
| 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' | |
| )) | |
| # Líneas en la dirección y | |
| 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' | |
| )) | |
| # --- Fin de la adición de la cuadrícula --- | |
| # Añadir los puntos de los experimentos en la superficie de respuesta con diferentes colores y etiquetas | |
| 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' | |
| )) | |
| # Añadir etiquetas y título con variables naturales | |
| 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 = [] # Resetear la lista de figuras | |
| # Niveles naturales para graficar | |
| levels_to_plot_natural = { | |
| self.x1_name: self.x1_levels, | |
| self.x2_name: self.x2_levels, | |
| self.x3_name: self.x3_levels | |
| } | |
| # Generar y almacenar gráficos individuales | |
| 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. | |
| """ | |
| # Calcular los estadísticos F para cada término | |
| # F = (coef/std_err)^2 = t^2 | |
| fvalues = model.tvalues[1:]**2 # Excluir la Intercept y convertir t a F | |
| abs_fvalues = np.abs(fvalues) | |
| sorted_idx = np.argsort(abs_fvalues)[::-1] | |
| sorted_fvalues = abs_fvalues[sorted_idx] | |
| sorted_names = fvalues.index[sorted_idx] | |
| # Calcular el valor crítico de F para la línea de significancia | |
| alpha = 0.05 # Nivel de significancia | |
| dof_num = 1 # Grados de libertad del numerador (cada término) | |
| dof_den = model.df_resid # Grados de libertad residuales | |
| f_critical = f.ppf(1 - alpha, dof_num, dof_den) | |
| # Crear el diagrama de Pareto | |
| 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") | |
| # Agregar la línea de significancia | |
| 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 del modelo simplificado | |
| anova_table = sm.stats.anova_lm(self.model_simplified, typ=2) | |
| # Suma de cuadrados total | |
| ss_total = anova_table['sum_sq'].sum() | |
| # Crear tabla de contribución | |
| contribution_table = pd.DataFrame({ | |
| 'Fuente de Variación': [], | |
| 'Suma de Cuadrados': [], | |
| 'Grados de Libertad': [], | |
| 'Cuadrado Medio': [], | |
| 'F': [], | |
| 'Valor p': [], | |
| '% Contribución': [] | |
| }) | |
| # Calcular estadísticos F y porcentaje de contribución para cada factor | |
| ms_error = anova_table.loc['Residual', 'sum_sq'] / anova_table.loc['Residual', 'df'] | |
| # Agregar Block (si está disponible en los datos) | |
| block_ss = self.data.groupby('Block')['AIA_ppm'].sum().var() if 'Block' in self.data.columns else 0 | |
| if block_ss > 0: | |
| block_df = len(self.data['Block'].unique()) - 1 if 'Block' in self.data.columns else 1 | |
| block_ms = block_ss / block_df | |
| block_f = block_ms / ms_error | |
| block_p = f.sf(block_f, block_df, anova_table.loc['Residual', 'df']) | |
| contribution_table = pd.concat([contribution_table, pd.DataFrame({ | |
| 'Fuente de Variación': ['Block'], | |
| 'Suma de Cuadrados': [block_ss], | |
| 'Grados de Libertad': [block_df], | |
| 'Cuadrado Medio': [block_ms], | |
| 'F': [block_f], | |
| 'Valor p': [block_p], | |
| '% Contribución': [(block_ss / ss_total) * 100] | |
| })], ignore_index=True) | |
| # Agregar Model (suma de todos los términos del modelo excepto el residual) | |
| model_ss = anova_table['sum_sq'][:-1].sum() # Excluir residual | |
| model_df = anova_table['df'][:-1].sum() | |
| model_ms = model_ss / model_df | |
| model_f = model_ms / ms_error | |
| model_p = f.sf(model_f, model_df, anova_table.loc['Residual', 'df']) | |
| contribution_table = pd.concat([contribution_table, pd.DataFrame({ | |
| 'Fuente de Variación': ['Model'], | |
| 'Suma de Cuadrados': [model_ss], | |
| 'Grados de Libertad': [model_df], | |
| 'Cuadrado Medio': [model_ms], | |
| 'F': [model_f], | |
| 'Valor p': [model_p], | |
| '% Contribución': [(model_ss / ss_total) * 100] | |
| })], ignore_index=True) | |
| # Agregar factores individuales y sus interacciones | |
| 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}²' | |
| elif factor_name == f'I({self.x2_name} ** 2)': | |
| factor_name = f'{self.x2_name}²' | |
| elif factor_name == f'I({self.x3_name} ** 2)': | |
| factor_name = f'{self.x3_name}²' | |
| 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) | |
| # Agregar Residual | |
| residual_ss = anova_table.loc['Residual', 'sum_sq'] | |
| residual_df = anova_table.loc['Residual', 'df'] | |
| residual_ms = residual_ss / residual_df | |
| contribution_table = pd.concat([contribution_table, pd.DataFrame({ | |
| 'Fuente de Variación': ['Residual'], | |
| 'Suma de Cuadrados': [residual_ss], | |
| 'Grados de Libertad': [residual_df], | |
| 'Cuadrado Medio': [residual_ms], | |
| 'F': [None], | |
| 'Valor p': [None], | |
| '% Contribución': [(residual_ss / ss_total) * 100] | |
| })], ignore_index=True) | |
| # Agregar Correlation Total | |
| contribution_table = pd.concat([contribution_table, pd.DataFrame({ | |
| 'Fuente de Variación': ['Cor Total'], | |
| 'Suma de Cuadrados': [ss_total], | |
| 'Grados de Libertad': [len(self.data) - 1], | |
| 'Cuadrado Medio': [None], | |
| 'F': [None], | |
| 'Valor p': [None], | |
| '% Contribución': [100] | |
| })], 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 | |
| # --- ANOVA detallada --- | |
| # 1. Ajustar un modelo solo con los términos de primer orden y cuadráticos | |
| 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() | |
| # 2. ANOVA del modelo reducido | |
| anova_reduced = sm.stats.anova_lm(model_reduced, typ=2) | |
| # 3. Suma de cuadrados total | |
| ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2) | |
| # 4. Grados de libertad totales | |
| df_total = len(self.data) - 1 | |
| # 5. Suma de cuadrados de la regresión | |
| ss_regression = anova_reduced['sum_sq'][:-1].sum() # Sumar todo excepto 'Residual' | |
| # 6. Grados de libertad de la regresión | |
| df_regression = len(anova_reduced) - 1 | |
| # 7. Suma de cuadrados del error residual | |
| ss_residual = self.model_simplified.ssr | |
| df_residual = self.model_simplified.df_resid | |
| # 8. Suma de cuadrados del error puro (se calcula a partir de las réplicas) | |
| 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 | |
| # 9. Suma de cuadrados de la falta de ajuste | |
| 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 | |
| # 10. Cuadrados medios | |
| 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 | |
| # 11. Estadísticos F y valores p | |
| 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 | |
| # 12. Crear la tabla ANOVA detallada | |
| 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] | |
| }) | |
| # Calcular la suma de cuadrados y estadísticos F para la curvatura | |
| 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) | |
| # Añadir la fila de curvatura a la tabla ANOVA | |
| detailed_anova_table.loc[len(detailed_anova_table)] = [ | |
| 'Curvatura', | |
| ss_curvature, | |
| df_curvature, | |
| ms_curvature, | |
| f_curvature, | |
| p_curvature | |
| ] | |
| # Reorganizar las filas y resetear el índice | |
| 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) | |
| # Guardar en un archivo temporal | |
| 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() | |
| # Guardar en un archivo temporal | |
| 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() | |
| # Configurar estilo de fuente | |
| style = doc.styles['Normal'] | |
| font = style.font | |
| font.name = 'Times New Roman' | |
| font.size = Pt(12) | |
| # Título del informe | |
| 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') # Espacio | |
| for sheet_name, table in tables_dict.items(): | |
| # Añadir título de la tabla | |
| doc.add_heading(sheet_name, level=1) | |
| if table.empty: | |
| doc.add_paragraph("No hay datos disponibles para esta tabla.") | |
| continue | |
| # Añadir tabla al documento | |
| table_doc = doc.add_table(rows=1, cols=len(table.columns)) | |
| table_doc.style = 'Light List Accent 1' | |
| # Añadir encabezados | |
| hdr_cells = table_doc.rows[0].cells | |
| for idx, col_name in enumerate(table.columns): | |
| hdr_cells[idx].text = col_name | |
| # Añadir filas de datos | |
| 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') # Espacio entre tablas | |
| # Guardar el documento en un archivo temporal | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".docx") as tmp: | |
| doc.save(tmp.name) | |
| tmp_path = tmp.name | |
| return tmp_path | |
| # --- Funciones para la Interfaz de Gradio --- | |
| 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: | |
| # Convertir los niveles a listas de números | |
| 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(',')] | |
| # Crear DataFrame a partir de la cadena de datos | |
| 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') # Convertir a numérico | |
| # Validar que el DataFrame tenga las columnas correctas | |
| if not all(col in data.columns for col in column_names): | |
| raise ValueError("El formato de los datos no es correcto.") | |
| # Crear la instancia de RSM_BoxBehnken | |
| 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: | |
| # Mostrar mensaje de error | |
| 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 # Ajustar el número de outputs | |
| # Ajustar modelos y optimizar | |
| 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() | |
| # Generar todas las figuras y almacenarlas | |
| rsm.generate_all_plots() | |
| # Formatear la ecuación para que se vea mejor en Markdown | |
| equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ") | |
| equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}" | |
| # Guardar las tablas en Excel temporal | |
| excel_path = rsm.save_tables_to_excel() | |
| # Guardar todas las figuras en un ZIP temporal | |
| 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, # Ruta del ZIP de gráficos | |
| excel_path # Ruta del Excel de tablas | |
| ) | |
| 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" | |
| # Crear un archivo temporal | |
| with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file: | |
| temp_file.write(img_bytes) | |
| temp_path = temp_file.name | |
| return temp_path # Retornar solo la ruta | |
| 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" | |
| # Gradio no permite renombrar directamente, por lo que retornamos la ruta del archivo | |
| 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" | |
| # Gradio no permite renombrar directamente, por lo que retornamos la ruta del archivo | |
| 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 | |
| # --- Crear la interfaz de Gradio --- | |
| 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) | |
| # Sección de análisis visible solo después de cargar los datos | |
| 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) # Estado para el índice actual | |
| all_figures_state = gr.State([]) # Estado para todas las figuras | |
| # Cargar datos | |
| 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] | |
| ) | |
| # Ajustar modelo y optimizar | |
| 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, # Ruta del ZIP de gráficos | |
| download_excel_button # Ruta del Excel de tablas | |
| ] | |
| ) | |
| # Generar y mostrar los gráficos | |
| 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 # Actualizar el estado de todas las figuras | |
| ), | |
| inputs=[fixed_variable_input, fixed_level_input], | |
| outputs=[rsm_plot_output, plot_info, current_index_state, all_figures_state] | |
| ) | |
| # Navegación de gráficos | |
| 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] | |
| ) | |
| # Descargar gráfico actual | |
| download_plot_button.click( | |
| download_current_plot, | |
| inputs=[all_figures_state, current_index_state], | |
| outputs=download_plot_button | |
| ) | |
| # Descargar todos los gráficos en ZIP | |
| download_all_plots_button.click( | |
| download_all_plots_zip, | |
| inputs=[], | |
| outputs=download_all_plots_button | |
| ) | |
| # Descargar todas las tablas en Excel y Word | |
| 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 | |
| ) | |
| # Ejemplo de uso | |
| 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 | |
| # --- Función Principal --- | |
| def main(): | |
| interface = create_gradio_interface() | |
| interface.launch(share=True) | |
| if __name__ == "__main__": | |
| main() |