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()