OptiTec_X3 / app.py
C2MV's picture
Update app.py
fef08f1 verified
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 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
from pyDOE import bbdesign, ccdesign
# --- Clase RSM_ExperimentalDesign ---
class RSM_ExperimentalDesign:
def __init__(self, data, design_type, factor_names, y_name, factor_levels):
"""
Inicializa la clase con los datos del diseño experimental.
Args:
data (pd.DataFrame): Datos del experimento.
design_type (str): Tipo de diseño ('Box-Behnken' o 'Central Compuesto').
factor_names (list): Lista de nombres de los factores.
y_name (str): Nombre de la variable dependiente.
factor_levels (dict): Diccionario con los niveles de cada factor.
"""
self.data = data.copy()
self.design_type = design_type
self.factor_names = factor_names
self.y_name = y_name
self.factor_levels = factor_levels
self.model = None
self.model_simplified = None
self.optimized_results = None
self.optimal_levels = None
self.all_figures = []
self.selected_factors = []
def generate_design(self):
"""
Genera el diseño experimental basado en el tipo especificado y asigna los niveles naturales.
"""
num_factors = len(self.factor_names)
if self.design_type == 'Box-Behnken':
design = bbdesign(num_factors)
elif self.design_type == 'Central Compuesto':
design = ccdesign(num_factors, center=(3, 3)) # Puedes ajustar los puntos centrales si lo deseas
else:
raise ValueError("Tipo de diseño no soportado. Elige 'Box-Behnken' o 'Central Compuesto'.")
# Asignar niveles naturales a las variables
for i, factor in enumerate(self.factor_names):
self.data[factor] = self.coded_to_natural(design[:, i], factor)
return self.data
def coded_to_natural(self, coded_value, variable_name):
"""Convierte un valor codificado a su valor natural."""
levels = self.factor_levels[variable_name]
if len(levels) != 3:
raise ValueError(f"Se requieren exactamente 3 niveles para el factor '{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.factor_levels[variable_name]
return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
def fit_model(self):
"""
Ajusta el modelo de segundo orden completo a los datos.
"""
terms = self.factor_names.copy()
# Términos cuadráticos
terms += [f'I({var}**2)' for var in self.factor_names]
# Términos de interacción
for i in range(len(self.factor_names)):
for j in range(i+1, len(self.factor_names)):
terms.append(f'{self.factor_names[i]}:{self.factor_names[j]}')
formula = f'{self.y_name} ~ ' + ' + '.join(terms)
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, selected_factors):
"""
Ajusta el modelo simplificado basado en los factores seleccionados.
Args:
selected_factors (list): Lista de factores a incluir en el modelo.
"""
self.selected_factors = selected_factors
terms = selected_factors.copy()
# Términos cuadráticos
terms += [f'I({var}**2)' for var in selected_factors]
formula = f'{self.y_name} ~ ' + ' + '.join(terms)
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):
input_data = {var: [x[i]] for i, var in enumerate(self.selected_factors)}
return -self.model_simplified.predict(pd.DataFrame(input_data)).values[0]
# Definir límites codificados para cada factor
bounds = [(-1, 1) for _ in self.selected_factors]
x0 = [0] * len(self.selected_factors)
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[i], self.selected_factors[i])
for i in range(len(self.selected_factors))
]
# Crear la tabla de optimización
optimization_table = pd.DataFrame({
'Variable': self.selected_factors,
'Nivel Óptimo (Natural)': optimal_levels_natural,
'Nivel Óptimo (Codificado)': self.optimal_levels
})
return optimization_table.round(3)
def plot_rsm_individual(self, fixed_variable, fixed_level):
"""
Genera un gráfico de superficie de respuesta (RSM) individual para una configuración específica.
"""
if self.model_simplified is None:
print("Error: Ajusta el modelo simplificado primero.")
return None
varying_variables = [var for var in self.selected_factors if var != fixed_variable]
if len(varying_variables) < 2:
print("Se requieren al menos dos variables para generar un gráfico de superficie.")
return None
var1, var2 = varying_variables[:2] # Solo tomar las dos primeras variables para el gráfico
# Determinar los niveles naturales para las variables que varían
x_natural_levels = self.factor_levels[var1]
y_natural_levels = self.factor_levels[var2]
# 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, var1)
y_grid_coded = self.natural_to_coded(y_grid_natural, var2)
# Crear un DataFrame para la predicción con variables codificadas
prediction_data = pd.DataFrame({
var1: x_grid_coded.flatten(),
var2: y_grid_coded.flatten(),
})
# Asignar valores codificados a las otras variables fijas
for var in self.selected_factors:
if var not in [var1, var2]:
prediction_data[var] = self.natural_to_coded(fixed_level, var)
# Calcular los valores predichos
z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape)
# Crear el gráfico de superficie
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 líneas de cuadrícula
for i in range(x_grid_natural.shape[0]):
fig.add_trace(go.Scatter3d(
x=x_grid_natural[i, :],
y=y_grid_natural[i, :],
z=z_pred[i, :],
mode='lines',
line=dict(color='gray', width=2),
showlegend=False,
hoverinfo='skip'
))
for j in range(x_grid_natural.shape[1]):
fig.add_trace(go.Scatter3d(
x=x_grid_natural[:, j],
y=y_grid_natural[:, j],
z=z_pred[:, j],
mode='lines',
line=dict(color='gray', width=2),
showlegend=False,
hoverinfo='skip'
))
# Añadir los puntos de los experimentos
experiments_data = self.data.copy()
experiments_data['Predicho'] = self.model_simplified.predict(self.data[self.selected_factors])
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_data[var1],
y=experiments_data[var2],
z=experiments_data[self.y_name],
mode='markers+text',
marker=dict(size=4, color=colors[:len(experiments_data)]),
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"{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}<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',
'Triptófano': 'g/L',
'AIA_ppm': 'ppm',
# Agrega más unidades según tus variables
}
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
for fixed_variable in self.selected_factors:
for level in self.factor_levels[fixed_variable]:
fig = self.plot_rsm_individual(fixed_variable, level)
if fig is not None:
self.all_figures.append(fig)
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
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.startswith('I('):
equation += f" + {coef:.3f}*{term[2:-1]}"
elif ':' in term:
equation += f" + {coef:.3f}*{term}"
else:
equation += f" + {coef:.3f}*{term}"
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.selected_factors])
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 filas para cada término del modelo
for index, row in anova_table.iterrows():
if index != 'Residual':
factor_name = index
if factor_name.startswith('I('):
factor_name = factor_name[2:-1] # Quitar 'I(' y ')'
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 fila para Cor 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': [np.nan],
'F': [np.nan],
'Valor p': [np.nan],
'% 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 del modelo simplificado
anova_reduced = sm.stats.anova_lm(self.model_simplified, typ=2)
# Suma de cuadrados total
ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
# Suma de cuadrados de la regresión
ss_regression = anova_reduced['sum_sq'].sum()
# Grados de libertad de la regresión
df_regression = anova_reduced['df'].sum()
# Suma de cuadrados del error residual
ss_residual = anova_reduced.loc['Residual', 'sum_sq']
df_residual = anova_reduced.loc['Residual', 'df']
# Suma de cuadrados del error puro (si hay réplicas)
duplicates = self.data.duplicated(subset=self.selected_factors, keep=False)
if duplicates.any():
ss_pure_error = self.data[duplicates].groupby(self.selected_factors)[self.y_name].var().sum() * self.data[duplicates].groupby(self.selected_factors).ngroups()
df_pure_error = self.data[duplicates].shape[0] - self.data[duplicates].groupby(self.selected_factors).ngroups()
else:
ss_pure_error = np.nan
df_pure_error = np.nan
# 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
# 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
# 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
# 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, len(self.data) - 1],
'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]
})
# Reorganizar las filas y resetear el índice
detailed_anova_table = detailed_anova_table.reindex([0, 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()
anova_table = self.calculate_detailed_anova()
return {
'Predicciones': prediction_table,
'% Contribución': contribution_table,
'ANOVA Detallada': 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_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(design_type, factor_names, factor_levels, y_name, data_str):
"""
Carga los datos del diseño experimental desde las entradas y crea la instancia de RSM_ExperimentalDesign.
"""
try:
# Parsear nombres de factores
factor_names = [fn.strip() for fn in factor_names.split(',')]
num_factors = len(factor_names)
# Parsear niveles de factores
factor_levels_dict = {}
levels = factor_levels.split(';')
if len(levels) != num_factors:
raise ValueError(f"Se esperaban {num_factors} conjuntos de niveles separados por ';'.")
for i, level_str in enumerate(levels):
level_values = [float(x.strip()) for x in level_str.split(',')]
if len(level_values) != 3:
raise ValueError(f"El factor '{factor_names[i]}' requiere exactamente 3 niveles separados por comas.")
factor_levels_dict[factor_names[i]] = level_values
# Crear DataFrame a partir de la cadena de datos
data_list = [row.split(',') for row in data_str.strip().split('\n')]
column_names = ['Exp.'] + factor_names + [y_name]
data = pd.DataFrame(data_list, columns=column_names)
data = data.apply(pd.to_numeric, errors='coerce') # Convertir a numérico
# Crear la instancia de RSM_ExperimentalDesign
global rsm
rsm = RSM_ExperimentalDesign(
data=data,
design_type=design_type,
factor_names=factor_names,
y_name=y_name,
factor_levels=factor_levels_dict
)
# Generar el diseño
rsm.generate_design()
return data.round(3), gr.update(visible=True), factor_names
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(selected_factors):
if 'rsm' not in globals():
return [None]*11 # Ajustar el número de outputs
if not selected_factors:
return [None]*11 # No se han seleccionado factores
# Ajustar modelos y optimizar
model_completo, pareto_completo = rsm.fit_model()
rsm.selected_factors = selected_factors
model_simplificado, pareto_simplificado = rsm.fit_simplified_model(selected_factors)
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
if equation:
equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ")
equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}"
else:
equation_formatted = "No se pudo generar la ecuación del modelo simplificado."
# 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 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"
return (filename, img_bytes)
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:
with open(zip_path, 'rb') as f:
zip_bytes = f.read()
filename = f"Graficos_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
return (filename, zip_bytes)
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:
with open(excel_path, 'rb') as f:
excel_bytes = f.read()
filename = f"Tablas_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
return (filename, excel_bytes)
return None
def exportar_word(tables_dict):
"""
Función para exportar las tablas a un documento de Word.
"""
if not tables_dict:
return None
word_path = rsm.export_tables_to_word(tables_dict)
if word_path and os.path.exists(word_path):
with open(word_path, 'rb') as f:
word_bytes = f.read()
filename = f"Tablas_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.docx"
return (filename, word_bytes)
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")
with gr.Row():
with gr.Column():
gr.Markdown("## Configuración del Diseño")
design_type_input = gr.Dropdown(
label="Tipo de Diseño",
choices=["Box-Behnken", "Central Compuesto"],
value="Box-Behnken"
)
factor_names_input = gr.Textbox(
label="Nombres de los Factores (separados por comas)",
value="Glucosa, Extracto_de_Levadura, Triptófano"
)
factor_levels_input = gr.Textbox(
label="Niveles de los Factores (cada conjunto separado por ';' y niveles separados por comas)",
value="1, 3.5, 5.5; 0.03, 0.2, 0.3; 0.4, 0.65, 0.9"
)
y_name_input = gr.Textbox(
label="Nombre de la Variable Dependiente (ej. AIA_ppm)",
value="AIA_ppm"
)
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()
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 Todas las Tablas")
download_excel_button = gr.DownloadButton(label="Descargar Tablas en Excel")
download_word_button = gr.DownloadButton(label="Descargar Tablas en Word")
with gr.Column():
gr.Markdown("## Selección de Factores para el Modelo Simplificado")
selected_factors_input = gr.CheckboxGroup(
label="Selecciona los Factores a Incluir",
choices=[], # Actualizar dinámicamente
value=[]
)
fit_button_2 = gr.Button("Aplicar Selección de Factores")
gr.Markdown("## Generar Gráficos de Superficie de Respuesta")
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 0", interactive=False)
with gr.Row():
download_plot_button = gr.DownloadButton(label="Descargar Gráfico Actual (PNG)")
download_all_plots_button = gr.DownloadButton(label="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
# Funciones de carga y ajuste
def load_data_interface(design_type, factor_names, factor_levels, y_name, data_str):
data, analysis_visibility, factor_names_list = load_data(design_type, factor_names, factor_levels, y_name, data_str)
if data is not None:
# Actualizar las opciones de selección de factores
selected_factors = factor_names_list
return data, analysis_visibility, selected_factors
else:
return data, analysis_visibility, []
load_button.click(
load_data_interface,
inputs=[design_type_input, factor_names_input, factor_levels_input, y_name_input, data_input],
outputs=[data_output, analysis_row, selected_factors_input]
)
# Ajustar modelo y optimizar
fit_button.click(
fit_and_optimize_model,
inputs=[selected_factors_input],
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
]
)
# Conectar los botones de descarga de tablas
download_excel_button.click(
fn=download_all_tables_excel,
inputs=[],
outputs=download_excel_button
)
download_word_button.click(
fn=exportar_word,
inputs=[gr.State()],
outputs=download_word_button
)
# Generar y mostrar los gráficos
def generate_plots():
if 'rsm' not in globals() or not rsm.all_figures:
return None, "No hay gráficos disponibles.", 0, []
# Mostrar el primer gráfico
return rsm.all_figures[0], f"Gráfico 1 de {len(rsm.all_figures)}", 0, rsm.all_figures
plot_button.click(
generate_plots,
inputs=[],
outputs=[rsm_plot_output, plot_info, current_index_state, all_figures_state]
)
# Navegación de gráficos
def navigate(direction, current_index, all_figures):
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
left_button.click(
navigate,
inputs=[gr.Button("left"), current_index_state, all_figures_state],
outputs=[rsm_plot_output, plot_info, current_index_state]
)
right_button.click(
navigate,
inputs=[gr.Button("right"), 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
)
# Ejemplo de uso
gr.Markdown("## Ejemplo de uso")
gr.Markdown("""
1. **Configura el Diseño:**
- Selecciona el tipo de diseño (Box-Behnken o Central Compuesto).
- Ingresa los nombres de los factores separados por comas.
- Ingresa los niveles para cada factor separados por comas y cada conjunto de niveles por ';'.
- Especifica el nombre de la variable dependiente.
- Proporciona los datos del experimento en formato CSV.
2. **Cargar Datos:**
- Haz clic en 'Cargar Datos' para cargar y visualizar los datos.
3. **Seleccionar Factores para el Modelo Simplificado:**
- Marca los factores que deseas incluir en el modelo simplificado.
4. **Ajustar Modelo y Optimizar:**
- Haz clic en 'Ajustar Modelo y Optimizar' para ajustar los modelos y obtener los resultados.
5. **Generar y Navegar Gráficos:**
- Haz clic en 'Generar Gráficos' para crear las superficies de respuesta.
- Navega entre los gráficos usando los botones '<' y '>'.
- Descarga el gráfico actual en PNG o todos los gráficos en un archivo ZIP.
6. **Descargar Tablas:**
- Descarga todas las tablas generadas en un archivo Excel o Word.
""")
return demo
# --- Función Principal ---
def main():
interface = create_gradio_interface()
interface.launch(share=True)
if __name__ == "__main__":
main()