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 import os import itertools # --- Clase RSM_BoxBehnken --- class RSM_BoxBehnken: def __init__(self, factor_names, factor_levels, y_name): """ Inicializa la clase con los nombres de factores, sus niveles y la variable dependiente. """ self.factor_names = factor_names # Lista de nombres de factores self.factor_levels = factor_levels # Lista de diccionarios con min y max para cada factor self.y_name = y_name # Nombre de la variable dependiente self.n_factors = len(factor_names) # Número de factores self.data = None # DataFrame con los datos del experimento self.design = None # DataFrame con el diseño Box-Behnken self.model = None # Modelo completo self.model_simplified = None # Modelo simplificado self.optimized_results = None # Resultados de optimización self.optimal_levels = None # Niveles óptimos de factores self.all_figures = [] # Lista para almacenar las figuras generadas def generate_box_behnken_design(self, center_runs=3): """ Genera el diseño Box-Behnken para el número de factores especificado. """ design = [] # Generar todas las combinaciones de tres niveles para cada par de factores for combination in itertools.combinations(self.factor_names, 2): var1, var2 = combination for level1 in [-1, 0, 1]: for level2 in [-1, 0, 1]: # Solo agregar puntos que formen el diseño Box-Behnken if abs(level1) == 1 and abs(level2) == 1: run = {var: 0 for var in self.factor_names} run[var1] = level1 run[var2] = level2 design.append(run) # Añadir corridas centrales for _ in range(center_runs): run = {var: 0 for var in self.factor_names} design.append(run) design_df = pd.DataFrame(design) self.design = design_df # Mapear niveles codificados a naturales for i, factor in enumerate(self.factor_names): design_df[factor] = design_df[factor].apply(lambda x: self.coded_to_natural(x, i)) # Asignar al atributo data self.data = design_df.copy() return self.design def coded_to_natural(self, coded_value, factor_index): """ Convierte un valor codificado (-1, 0, 1) a su valor natural basado en los niveles del factor. """ min_val = self.factor_levels[factor_index]['min'] max_val = self.factor_levels[factor_index]['max'] return min_val + (coded_value + 1) * (max_val - min_val) / 2 def natural_to_coded(self, natural_value, factor_index): """ Convierte un valor natural a su valor codificado (-1, 0, 1) basado en los niveles del factor. """ min_val = self.factor_levels[factor_index]['min'] max_val = self.factor_levels[factor_index]['max'] return -1 + 2 * (natural_value - min_val) / (max_val - min_val) def set_response(self, response_values): """ Establece los valores de respuesta (variable dependiente) en el diseño. """ if len(response_values) != len(self.design): raise ValueError("El número de valores de respuesta no coincide con el número de corridas en el diseño.") self.data[self.y_name] = response_values def fit_model(self): """ Ajusta el modelo de segundo orden completo a los datos. """ formula = self._generate_formula() 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 simplificado a los datos, eliminando términos no significativos. """ formula = self._generate_formula(simplified=True) 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 _generate_formula(self, simplified=False): """ Genera la fórmula del modelo según el número de factores y si es simplificado. """ terms = self.factor_names.copy() if simplified: # Añadir términos cuadráticos terms += [f"I({var}**2)" for var in self.factor_names] else: # Añadir términos cuadráticos e interacciones terms += [f"I({var}**2)" for var in self.factor_names] terms += [f"{var1}:{var2}" for var1, var2 in itertools.combinations(self.factor_names, 2)] formula = f"{self.y_name} ~ " + " + ".join(terms) return formula 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): # Convertir los niveles codificados a naturales natural_values = [self.coded_to_natural(xi, i) for i, xi in enumerate(x)] # Crear un DataFrame para la predicción prediction_df = pd.DataFrame([natural_values], columns=self.factor_names) # Convertir naturales a codificados for i in range(self.n_factors): prediction_df[self.factor_names[i]] = prediction_df[self.factor_names[i]].apply(lambda val: self.natural_to_coded(val, i)) # Predecir la respuesta return -self.model_simplified.predict(prediction_df)[0] # Definir límites en los niveles codificados (-1, 1) bounds = [(-1, 1)] * self.n_factors x0 = [0] * self.n_factors # Punto inicial en el centro 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(xi, i) for i, xi in enumerate(self.optimal_levels) ] # Crear la tabla de optimización optimization_table = pd.DataFrame({ 'Variable': self.factor_names, 'Nivel Óptimo (Natural)': optimal_levels_natural, 'Nivel Óptimo (Codificado)': self.optimal_levels }) return optimization_table.round(3) 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 # Obtener las combinaciones de factores para gráficos for fixed_index, fixed_variable in enumerate(self.factor_names): # Usar los niveles originales para fijar la variable fixed_levels = self.factor_levels[fixed_index]['levels'] for level in fixed_levels: fig = self.plot_rsm_individual(fixed_variable, level) if fig is not None: self.all_figures.append(fig) 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. """ # Determinar las variables que varían varying_variables = [var for var in self.factor_names if var != fixed_variable] if len(varying_variables) < 2: print("Se requieren al menos dos variables que varían para generar el gráfico.") return None var1, var2 = varying_variables[:2] # Seleccionar las dos primeras variables que varían # Niveles naturales para las variables que varían var1_levels = self.factor_levels[self.factor_names.index(var1)]['levels'] var2_levels = self.factor_levels[self.factor_names.index(var2)]['levels'] # Crear una malla de puntos para las variables que varían (en unidades naturales) x_range = np.linspace(min(var1_levels), max(var1_levels), 100) y_range = np.linspace(min(var2_levels), max(var2_levels), 100) x_grid, y_grid = np.meshgrid(x_range, y_range) # Convertir la malla de variables naturales a codificadas x_coded = np.array([self.natural_to_coded(x, self.factor_names.index(var1)) for x in x_grid.flatten()]).reshape(x_grid.shape) y_coded = np.array([self.natural_to_coded(y, self.factor_names.index(var2)) for y in y_grid.flatten()]).reshape(y_grid.shape) # Crear un DataFrame para la predicción con variables codificadas prediction_data = pd.DataFrame({ var1: x_coded.flatten(), var2: y_coded.flatten(), fixed_variable: self.natural_to_coded(fixed_level, self.factor_names.index(fixed_variable)) }) # Añadir las demás variables a 0 (centro) for var in self.factor_names: if var not in [var1, var2, fixed_variable]: prediction_data[var] = 0 # Reordenar las columnas prediction_data = prediction_data[self.factor_names] # Calcular los valores predichos z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid.shape) # Crear el gráfico de superficie fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid, y=y_grid, colorscale='Viridis', opacity=0.7, showscale=True)]) # Añadir puntos de los experimentos experiments = self.data.copy() # Convertir el nivel fijo a codificado para filtrar fixed_level_coded = self.natural_to_coded(fixed_level, self.factor_names.index(fixed_variable)) # Filtrar experimentos donde la variable fija está cerca del nivel seleccionado (tolerancia pequeña) experiments = experiments[np.isclose(experiments[fixed_variable], fixed_level, atol=1e-3)] fig.add_trace(go.Scatter3d( x=experiments[var1], y=experiments[var2], z=experiments[self.y_name], mode='markers+text', marker=dict(size=5, color='red'), text=[f"{val:.2f}" for val in experiments[self.y_name]], textposition='top center', name='Experimentos' )) # Actualizar layout fig.update_layout( scene=dict( xaxis_title=f"{var1} ({self.get_units(var1)})", yaxis_title=f"{var2} ({self.get_units(var2)})", zaxis_title=self.y_name, ), title=f"{self.y_name} vs {var1} y {var2}
{fixed_variable} fijo en {fixed_level} ({self.get_units(fixed_variable)})", 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', 'Tiempo': 'Horas', 'AIA_ppm': 'ppm', # Agrega más unidades según tus variables } return units.get(variable_name, '') def pareto_chart(self, model, title): """ Genera un diagrama de Pareto para los efectos estandarizados de un modelo, incluyendo la línea de significancia. """ # Calcular los efectos estandarizados tvalues = model.tvalues.drop('Intercept') abs_tvalues = tvalues.abs() sorted_idx = abs_tvalues.sort_values(ascending=False).index sorted_tvalues = abs_tvalues[sorted_idx] sorted_names = sorted_idx # Calcular el valor crítico de t para la línea de significancia alpha = 0.05 # Nivel de significancia dof = model.df_resid # Grados de libertad residuales t_critical = t.ppf(1 - alpha / 2, dof) # Crear el diagrama de Pareto fig = px.bar( x=sorted_tvalues.round(3), y=sorted_names, orientation='h', labels={'x': 'Efecto Estandarizado', 'y': 'Término'}, title=title ) fig.update_yaxes(autorange="reversed") # Agregar la línea de significancia fig.add_vline(x=t_critical, line_dash="dot", annotation_text=f"t crítico = {t_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 in self.factor_names: equation += f" + {coef:.3f}*{term}" elif term.startswith("I("): equation += f" + {coef:.3f}*{term[2:-1]}" else: equation += f" + {coef:.3f}*{term.replace(':', '×')}" 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 a la variabilidad de la respuesta (AIA). """ 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({ 'Factor': [], 'Suma de Cuadrados': [], '% Contribución': [] }) # Calcular porcentaje de contribución para cada factor for index, row in anova_table.iterrows(): if index != 'Residual': factor_name = index if 'I(' in factor_name: factor_name = factor_name.replace('I(', '').replace(')', '').replace('** 2', '^2') ss_factor = row['sum_sq'] contribution_percentage = (ss_factor / ss_total) * 100 contribution_table = pd.concat([contribution_table, pd.DataFrame({ 'Factor': [factor_name], 'Suma de Cuadrados': [ss_factor], '% Contribución': [contribution_percentage] })], 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 = self._generate_formula(simplified=True) model_reduced = smf.ols(formula_reduced, data=self.data).fit() # 2. ANOVA del modelo reducido (para obtener la suma de cuadrados de la regresión) 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'].drop('Residual').sum() # 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) # Para simplificar, asumimos que no hay réplicas (no hay corridas duplicadas) 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ístico F y valor p para la falta de ajuste 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': [np.nan, np.nan, f_lack_of_fit, np.nan, np.nan], 'Valor p': [np.nan, np.nan, p_lack_of_fit, np.nan, np.nan] }) # Calcular la suma de cuadrados y grados de libertad para la curvatura ss_curvature = 0 for var in self.factor_names: curvature_term = f"I({var}**2)" if curvature_term in anova_reduced.index: ss_curvature += anova_reduced.loc[curvature_term, 'sum_sq'] df_curvature = self.n_factors # Añadir la fila de curvatura a la tabla ANOVA detailed_anova_table.loc[len(detailed_anova_table)] = ['Curvatura', ss_curvature, df_curvature, ss_curvature / df_curvature, np.nan, np.nan] # Reorganizar las filas para que la curvatura aparezca después de la regresión detailed_anova_table = detailed_anova_table.reindex([0, 5, 1, 2, 3, 4]) # Resetear el índice para que sea consecutivo detailed_anova_table = detailed_anova_table.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 y Word. """ 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 handle_load_design(n_factors, factor_inputs, y_name, load_example): """ Genera el diseño Box-Behnken según la configuración o carga un ejemplo. """ try: if load_example: # Cargar ejemplos predefinidos if n_factors == 3: factor_names = ['Glucosa', 'Extracto_de_Levadura', 'Triptofano'] factor_levels = [ {'min': 1.0, 'max': 5.5, 'levels': [1.0, 3.5, 5.5]}, {'min': 0.03, 'max': 0.3, 'levels': [0.03, 0.2, 0.3]}, {'min': 0.4, 'max': 0.9, 'levels': [0.4, 0.65, 0.9]} ] y_name = 'AIA_ppm' # Crear instancia rsm = RSM_BoxBehnken(factor_names, factor_levels, y_name) design = rsm.generate_box_behnken_design() # Ejemplo de valores de respuesta response_values = [166.594, 177.557, 127.261, 147.573, 188.883, 224.527, 190.238, 226.483, 195.550, 149.493, 187.683, 148.621, 278.951, 297.238, 280.896] rsm.set_response(response_values) return rsm, design elif n_factors == 4: factor_names = ['Glucosa', 'Extracto_de_Levadura', 'Triptofano', 'Tiempo'] factor_levels = [ {'min': 1.0, 'max': 5.5, 'levels': [1.0, 3.5, 5.5]}, {'min': 0.03, 'max': 0.3, 'levels': [0.03, 0.2, 0.3]}, {'min': 0.4, 'max': 0.9, 'levels': [0.4, 0.65, 0.9]}, {'min': 24, 'max': 72, 'levels': [24, 48, 72]} ] y_name = 'AIA_ppm' # Crear instancia rsm = RSM_BoxBehnken(factor_names, factor_levels, y_name) design = rsm.generate_box_behnken_design() # Ejemplo de valores de respuesta (30 corridas para 4 factores) response_values = [200 + np.random.normal(0, 10) for _ in range(len(design))] rsm.set_response(response_values) return rsm, design else: raise ValueError("Ejemplos solo disponibles para 3 y 4 factores.") else: # Cargar según la entrada del usuario factor_names = [] factor_levels = [] for i in range(n_factors): name = factor_inputs[i]['name'] min_val = factor_inputs[i]['min'] max_val = factor_inputs[i]['max'] factor_names.append(name) factor_levels.append({'min': min_val, 'max': max_val, 'levels': [min_val, (min_val + max_val) / 2, max_val]}) # Crear instancia rsm = RSM_BoxBehnken(factor_names, factor_levels, y_name) design = rsm.generate_box_behnken_design() return rsm, design except Exception as e: print(f"Error al cargar el diseño: {str(e)}") return None, None def fit_and_optimize(rsm, response_values): """ Ajusta los modelos, realiza la optimización y genera todas las tablas y gráficos. """ try: rsm.set_response(response_values) 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(" + ", "
+ ").replace("** 2", "^2").replace("*", " × ") equation_formatted = f"### Ecuación del Modelo Simplificado:
{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() # Preparar las tablas para exportación tables_dict = rsm.get_all_tables() 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 tables_dict # Diccionario de tablas para Word ) except Exception as e: print(f"Error en el análisis: {str(e)}") return [None]*12 def export_word(rsm_instance, tables_dict): """ Exporta las tablas a un documento de Word y retorna la ruta del archivo. """ try: word_path = rsm_instance.export_tables_to_word(tables_dict) if word_path and os.path.exists(word_path): return word_path return None except Exception as e: print(f"Error al exportar a Word: {str(e)}") 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 Diseño Box-Behnken") gr.Markdown(""" Esta aplicación te permite generar diseños Box-Behnken con un número variable de factores (mínimo 3, máximo 6), ajustar modelos de respuesta, realizar optimización y exportar los resultados a Excel y Word. """) with gr.Tab("🔧 Configuración"): with gr.Row(): n_factors_input = gr.Slider( minimum=3, maximum=6, step=1, value=3, label="Número de Factores", interactive=True ) load_example_checkbox = gr.Checkbox( label="Cargar Ejemplo", value=False ) with gr.Row(): with gr.Column(): # Factores dinámicos (hasta 6) factor_inputs = [] for i in range(6): with gr.Row(): factor_name = gr.Textbox(label=f"Factor {i+1} Nombre", placeholder=f"Nombre del Factor {i+1}", value=f"Factor_{i+1}") factor_min = gr.Number(label=f"Factor {i+1} Min", value=0.0) factor_max = gr.Number(label=f"Factor {i+1} Max", value=1.0) factor_inputs.append({'name': factor_name, 'min': factor_min, 'max': factor_max}) # Variable dependiente y_name_input = gr.Textbox(label="Variable Dependiente (Ej. AIA_ppm)", value="AIA_ppm") with gr.Row(): load_button = gr.Button("🔄 Generar Diseño") with gr.Tab("📊 Datos del Experimento"): gr.Markdown("### Diseño Box-Behnken") design_output = gr.Dataframe( headers=None, label="Diseño Generado (Completa la Columna de Respuestas)", interactive=True ) submit_response_button = gr.Button("✅ Enviar Respuestas") with gr.Tab("📈 Análisis y Reporte"): with gr.Row(): with gr.Column(): 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() gr.Markdown("**Tabla de Optimización**") optimization_table_output = gr.Dataframe(label="Tabla de Optimización", interactive=False) gr.Markdown("**Tabla de Predicciones**") prediction_table_output = gr.Dataframe(label="Tabla de Predicciones", interactive=False) gr.Markdown("**Tabla de % de Contribución**") contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución", interactive=False) gr.Markdown("**Tabla ANOVA Detallada**") anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada", interactive=False) gr.Markdown("## Descargar Tablas") download_excel_button = gr.DownloadButton("💾 Descargar Tablas en Excel") download_word_button = gr.DownloadButton("📄 Descargar Tablas en Word") with gr.Column(): gr.Markdown("**Gráficos de Superficie de Respuesta**") rsm_plot_output = gr.Plot() plot_info = gr.Textbox(label="Información del Gráfico", value="Gráfico 1 de N", interactive=False) with gr.Row(): left_button = gr.Button("<") right_button = gr.Button(">") 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)") # --- Funciones de la Interfaz --- def handle_load_design_wrapper(n_factors, factor_inputs, y_name, load_example): """ Wrapper para manejar la carga del diseño Box-Behnken. """ # Extraer los valores de los inputs factor_details = [] for i in range(6): name = factor_inputs[i]['name'].value min_val = factor_inputs[i]['min'].value max_val = factor_inputs[i]['max'].value factor_details.append({'name': name, 'min': min_val, 'max': max_val}) # Generar el diseño return handle_load_design(n_factors, factor_details, y_name, load_example) def handle_submit_response(rsm, design_df): """ Obtiene las respuestas ingresadas por el usuario y realiza el análisis. """ try: # Obtener las respuestas de la última columna (AIA_ppm) response_values = design_df[rsm.y_name].tolist() return fit_and_optimize(rsm, response_values) except Exception as e: print(f"Error al procesar las respuestas: {str(e)}") return [None]*12 # Cargar Diseño load_button.click( fn=handle_load_design_wrapper, inputs=[n_factors_input, factor_inputs, y_name_input, load_example_checkbox], outputs=[gr.State(), design_output] ) # Enviar Respuestas y Realizar Análisis submit_response_button.click( fn=handle_submit_response, inputs=[gr.State(), design_output], 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, gr.State() # Para tables_dict ] ) # Navegación de Gráficos (Simplificada) current_plot_state = gr.State(0) def get_current_plot(rsm, current_index): if not rsm.all_figures: return None, "No hay gráficos disponibles.", current_index selected_fig = rsm.all_figures[current_index] plot_info_text = f"Gráfico {current_index + 1} de {len(rsm.all_figures)}" return selected_fig, plot_info_text, current_index def navigate_plot(direction, current_index, rsm): if not rsm.all_figures: return None, "No hay gráficos disponibles.", current_index if direction == 'left': new_index = (current_index - 1) % len(rsm.all_figures) elif direction == 'right': new_index = (current_index + 1) % len(rsm.all_figures) else: new_index = current_index selected_fig = rsm.all_figures[new_index] plot_info_text = f"Gráfico {new_index + 1} de {len(rsm.all_figures)}" return selected_fig, plot_info_text, new_index # Navegación de Gráficos left_button.click( fn=navigate_plot, inputs=[gr.ButtonValue(left_button), current_plot_state, gr.State()], outputs=[rsm_plot_output, plot_info, current_plot_state] ) right_button.click( fn=navigate_plot, inputs=[gr.ButtonValue(right_button), current_plot_state, gr.State()], outputs=[rsm_plot_output, plot_info, current_plot_state] ) # Descargar Gráfico Actual download_plot_button.click( fn=lambda rsm, current_index: rsm.save_fig_to_bytes(rsm.all_figures[current_index]) if rsm and rsm.all_figures else None, inputs=[gr.State(), current_plot_state], outputs=download_plot_button ) # Descargar Todos los Gráficos en ZIP download_all_plots_button.click( fn=lambda rsm: rsm.save_figures_to_zip() if rsm else None, inputs=[gr.State()], outputs=download_all_plots_button ) # Descargar Tablas en Excel download_excel_button.click( fn=lambda excel_path: (excel_path, None) if excel_path else (None, None), inputs=[gr.State()], outputs=[download_excel_button, None] ) # Descargar Tablas en Word download_word_button.click( fn=lambda rsm_instance, tables_dict: export_word(rsm_instance, tables_dict) if rsm_instance and tables_dict else None, inputs=[gr.State(), gr.State()], outputs=[download_word_button] ) return demo # --- Función Principal --- def main(): interface = create_gradio_interface() interface.launch(share=True) if __name__ == "__main__": main()