OptiTec-L2 / app.py
C2MV's picture
Update app.py
c121bf6 verified
raw
history blame
25.4 kB
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
# --- Data definition in global scope ---
data_dict = {
'Tratamiento': ['T1', 'T2', 'T3', 'T4', 'T5', 'T6', 'T7', 'T8', 'T9', 'T10', 'T11', 'T12', 'T13', 'T14', 'T15'] * 3,
'Tiempo_fermentacion_h': [16] * 15 + [23] * 15 + [40] * 15,
'pH': [6.02, 5.39, 6.27, 4.82, 6.25, 4.87, 4.76, 4.68, 4.64, 6.35, 4.67, 6.43, 4.58, 4.60, 6.96,
5.17, 5.95, 6.90, 5.50, 5.08, 4.95, 5.41, 5.52, 4.98, 7.10, 5.36, 6.91, 5.21, 4.66, 7.10,
5.42, 5.60, 7.36, 5.36, 4.66, 4.93, 5.18, 5.26, 4.92, 7.28, 5.26, 6.84, 5.19, 4.58, 7.07],
'Abs_600nm': [1.576, 1.474, 1.293, 1.446, 1.537, 1.415, 1.481, 1.419, 1.321, 1.224, 1.459, 0.345, 1.279, 1.181, 0.662,
1.760, 1.690, 1.485, 1.658, 1.728, 1.594, 1.673, 1.607, 1.531, 1.424, 1.595, 0.344, 1.477, 1.257, 0.660,
1.932, 1.780, 1.689, 1.876, 1.885, 1.824, 1.913, 1.810, 1.852, 1.694, 1.831, 0.347, 1.752, 1.367, 0.656],
'Glucosa_g_L': [5,10,0,5,10,5,10,5,10,0,5,0,5,5,0] * 3,
'Proteina_Pescado_g_L': [1.4,1.4,3.2,3.2,3.2,3.2,3.2,5,5,5,5,0,5,5,0] * 3,
'Sulfato_Manganeso_g_L': [0.75,0.5,0.75,0.5,0.75,0.5,0.75,0.5,0.25,0.75,0.5,0.25,0.5,0.25,0.5] * 3
}
data = pd.DataFrame(data_dict)
# --- End of data definition in global scope ---
# --- Clase RSM_BoxBehnken ---
class RSM_BoxBehnken:
def __init__(self, data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels):
"""
Inicializa la clase con los datos del diseño Box-Behnken.
"""
self.data = data.copy()
self.model = None
self.model_simplified = None
self.model_personalized = None # For personalized model
self.optimized_results = None
self.optimal_levels = None
self.all_figures_full = [] # Separate lists for different model plots
self.all_figures_simplified = []
self.all_figures_personalized = []
self.x1_name = x1_name
self.x2_name = x2_name
self.x3_name = x3_name
self.y_name = y_name
# Niveles originales de las variables
self.x1_levels = x1_levels
self.x2_levels = x2_levels
self.x3_levels = x3_levels
def get_levels(self, variable_name):
"""
Obtiene los niveles para una variable específica.
"""
if variable_name == self.x1_name:
return self.x1_levels
elif variable_name == self.x2_name:
return self.x2_levels
elif variable_name == self.x3_name:
return self.x3_levels
else:
raise ValueError(f"Variable desconocida: {variable_name}")
def fit_model(self):
"""
Ajusta el modelo de segundo orden completo a los datos.
"""
formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2) + ' \
f'{self.x1_name}:{self.x2_name} + {self.x1_name}:{self.x3_name} + {self.x2_name}:{self.x3_name}'
self.model = smf.ols(formula, data=self.data).fit()
print("Modelo Completo:")
print(self.model.summary())
return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
def fit_simplified_model(self):
"""
Ajusta el modelo de segundo orden a los datos, eliminando términos no significativos.
"""
formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + ' \
f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)' # Adjusted formula to include x3^2
self.model_simplified = smf.ols(formula, data=self.data).fit()
print("\nModelo Simplificado:")
print(self.model_simplified.summary())
return self.model_simplified, self.pareto_chart(self.model_simplified, "Pareto - Modelo Simplificado")
def optimize(self, method='Nelder-Mead'):
"""
Encuentra los niveles óptimos de los factores para maximizar la respuesta usando el modelo simplificado.
"""
if self.model_simplified is None:
print("Error: Ajusta el modelo simplificado primero.")
return
def objective_function(x):
return -self.model_simplified.predict(pd.DataFrame({
self.x1_name: [x[0]],
self.x2_name: [x[1]],
self.x3_name: [x[2]]
})).values[0]
bounds = [(-1, 1), (-1, 1), (-1, 1)]
x0 = [0, 0, 0]
self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
self.optimal_levels = self.optimized_results.x
# Convertir niveles óptimos de codificados a naturales
optimal_levels_natural = [
self.coded_to_natural(self.optimal_levels[0], self.x1_name),
self.coded_to_natural(self.optimal_levels[1], self.x2_name),
self.coded_to_natural(self.optimal_levels[2], self.x3_name)
]
# Crear la tabla de optimización
optimization_table = pd.DataFrame({
'Variable': [self.x1_name, self.x2_name, self.x3_name],
'Nivel Óptimo (Natural)': optimal_levels_natural,
'Nivel Óptimo (Codificado)': self.optimal_levels
})
return optimization_table.round(3) # Redondear a 3 decimales
def fit_personalized_model(self, formula):
"""
Ajusta un modelo personalizado de segundo orden a los datos, usando la formula dada.
"""
self.model_personalized = smf.ols(formula, data=self.data).fit()
print("\nModelo Personalizado:")
print(self.model_personalized.summary())
return self.model_personalized, self.pareto_chart(self.model_personalized, "Pareto - Modelo Personalizado")
def generate_all_plots(self):
"""
Genera todas las gráficas de RSM para todos los modelos.
"""
if self.model_simplified is None:
print("Error: Ajusta el modelo simplificado primero.")
return
self.all_figures_full = [] # Reset lists for each model type
self.all_figures_simplified = []
self.all_figures_personalized = []
levels_to_plot_natural = { # Levels from data, as before
self.x1_name: sorted(list(set(self.data[self.x1_name]))),
self.x2_name: sorted(list(set(self.data[self.x2_name]))),
self.x3_name: sorted(list(set(self.data[self.x3_name])))
}
for fixed_variable in [self.x1_name, self.x2_name, self.x3_name]:
for level in levels_to_plot_natural[fixed_variable]:
fig_full = self.plot_rsm_individual(fixed_variable, level, model_type='full') # Pass model_type
if fig_full is not None:
self.all_figures_full.append(fig_full)
fig_simplified = self.plot_rsm_individual(fixed_variable, level, model_type='simplified') # Pass model_type
if fig_simplified is not None:
self.all_figures_simplified.append(fig_simplified)
if self.model_personalized is not None: # Generate personalized plots only if model exists
fig_personalized = self.plot_rsm_individual(fixed_variable, level, model_type='personalized') # Pass model_type
if fig_personalized is not None:
self.all_figures_personalized.append(fig_personalized)
def plot_rsm_individual(self, fixed_variable, fixed_level, model_type='simplified'): # Added model_type parameter
"""
Genera un gráfico de superficie de respuesta (RSM) individual para una configuración específica y modelo.
"""
model_to_use = self.model_simplified # Default to simplified model
model_title_suffix = "(Modelo Simplificado)"
if model_type == 'full':
model_to_use = self.model
model_title_suffix = "(Modelo Completo)"
elif model_type == 'personalized':
if self.model_personalized is None:
print("Error: Modelo personalizado no ajustado.")
return None
model_to_use = self.model_personalized
model_title_suffix = "(Modelo Personalizado)"
if model_to_use is None: # Use model_to_use instead of self.model_simplified
print(f"Error: Ajusta el modelo {model_type} primero.") # More informative error message
return None
# Determinar las variables que varían y sus niveles naturales
varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
# Establecer los niveles naturales para las variables que varían
x_natural_levels = self.get_levels(varying_variables[0])
y_natural_levels = self.get_levels(varying_variables[1])
# Crear una malla de puntos para las variables que varían (en unidades naturales)
x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
y_range_natural = np.linspace(y_natural_levels[0], y_natural_levels[-1], 100)
x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
# Convertir la malla de variables naturales a codificadas
x_grid_coded = self.natural_to_coded(x_grid_natural, varying_variables[0])
y_grid_coded = self.natural_to_coded(y_range_natural, varying_variables[1])
# Crear un DataFrame para la predicción con variables codificadas
prediction_data = pd.DataFrame({
varying_variables[0]: x_grid_coded.flatten(),
varying_variables[1]: y_grid_coded.flatten(),
})
prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
# Fijar la variable fija en el DataFrame de predicción
fixed_var_levels = self.get_levels(fixed_variable)
if len(fixed_var_levels) == 3: # Box-Behnken design levels
prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
elif len(fixed_var_levels) > 0: # Use the closest level if not Box-Behnken
closest_level_coded = self.natural_to_coded(min(fixed_var_levels, key=lambda x:abs(x-fixed_level)), fixed_variable)
prediction_data[fixed_variable] = closest_level_coded
# Calcular los valores predichos
z_pred = model_to_use.predict(prediction_data).values.reshape(x_grid_coded.shape) # Use model_to_use here
# Filtrar por el nivel de la variable fija (en codificado)
fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable)
subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)]
# Filtrar por niveles válidos en las variables que varían
valid_levels = [-1, 0, 1]
experiments_data = subset_data[
subset_data[varying_variables[0]].isin(valid_levels) &
subset_data[varying_variables[1]].isin(valid_levels)
]
# Convertir coordenadas de experimentos a naturales
experiments_x_natural = experiments_data[varying_variables[0]].apply(lambda x: self.coded_to_natural(x, varying_variables[0]))
experiments_y_natural = experiments_data[varying_variables[1]].apply(lambda x: self.coded_to_natural(x, varying_variables[1]))
# Crear el gráfico de superficie con variables naturales en los ejes y transparencia
fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)])
# --- Añadir cuadrícula a la superficie ---
# Líneas en la dirección x
for i in range(x_grid_natural.shape[0]):
fig.add_trace(go.Scatter3d(
x=x_grid_natural[i, :],
y=y_grid_natural[i, :],
z=z_pred[i, :],
mode='lines',
line=dict(color='gray', width=2),
showlegend=False,
hoverinfo='skip'
))
# Líneas en la dirección y
for j in range(x_grid_natural.shape[1]):
fig.add_trace(go.Scatter3d(
x=x_grid_natural[:, j],
y=y_grid_natural[:, j],
z=z_pred[:, j],
mode='lines',
line=dict(color='gray', width=2),
showlegend=False,
hoverinfo='skip'
))
# --- Fin de la adición de la cuadrícula ---
# Añadir los puntos de los experimentos en la superficie de respuesta con diferentes colores y etiquetas
colors = px.colors.qualitative.Safe
point_labels = [f"{row[self.y_name]:.3f}" for _, row in experiments_data.iterrows()]
fig.add_trace(go.Scatter3d(
x=experiments_x_natural,
y=experiments_y_natural,
z=experiments_data[self.y_name].round(3),
mode='markers+text',
marker=dict(size=4, color=colors[:len(experiments_x_natural)]),
text=point_labels,
textposition='top center',
name='Experimentos'
))
# Añadir etiquetas y título con variables naturales
fig.update_layout(
scene=dict(
xaxis_title=f"{varying_variables[0]} ({self.get_units(varying_variables[0])})",
yaxis_title=f"{varying_variables[1]} ({self.get_units(varying_variables[1])})",
zaxis_title=self.y_name,
),
title=f"{self.y_name} vs {varying_variables[0]} y {varying_variables[1]}<br><sup>{fixed_variable} fijo en {fixed_level:.3f} ({self.get_units(fixed_variable)}) {model_title_suffix}</sup>", # Updated title
height=800,
width=1000,
showlegend=True
)
return fig
# --- Funciones para la Interfaz de Gradio ---
model_completo_output = gr.HTML()
pareto_completo_output = gr.Plot()
model_simplificado_output = gr.HTML()
pareto_simplificado_output = gr.Plot()
equation_output = gr.HTML()
optimization_table_output = gr.Dataframe(label="Tabla de Optimización", interactive=False)
prediction_table_output = gr.Dataframe(label="Tabla de Predicciones", interactive=False)
contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución", interactive=False)
anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada", interactive=False)
download_all_plots_button = gr.DownloadButton("Descargar Todos los Gráficos (ZIP)")
download_excel_button = gr.DownloadButton("Descargar Tablas en Excel")
rsm_plot_output = gr.Plot()
plot_info = gr.Textbox(label="Información del Gráfico", value="Gráfico 1 de 9", interactive=False)
current_index_state = gr.State(0)
all_figures_state = gr.State([])
current_model_type_state = gr.State('simplified')
model_personalized_output = gr.HTML() # Output for personalized model summary
pareto_personalized_output = gr.Plot() # Pareto for personalized model
factor_checkboxes = gr.CheckboxGroup(["factors", "x1_sq", "x2_sq", "x3_sq"], label="Términos de Factores", value=["factors", "x1_sq", "x2_sq", "x3_sq"]) # Factor Checkboxes
interaction_checkboxes = gr.CheckboxGroup(["x1x2", "x1x3", "x2x3"], label="Términos de Interacción") # Interaction Checkboxes
def create_gradio_interface():
global 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, rsm_plot_output, plot_info, current_index_state, all_figures_state, current_model_type_state, model_personalized_output, pareto_personalized_output, factor_checkboxes, interaction_checkboxes
with gr.Blocks() as demo:
gr.Markdown("# Optimización de la Absorbancia usando RSM")
with gr.Row():
with gr.Column():
gr.Markdown("## Configuración del Análisis")
data_input = gr.Textbox(label="Datos del Experimento (formato CSV - Ignored, Data is Hardcoded)", lines=5, interactive=False, value="Data is pre-loaded, ignore input.")
load_button = gr.Button("Cargar Datos")
data_dropdown = gr.Dropdown(["All Data"], value="All Data", label="Seleccionar Datos") # Data Selection Dropdown - currently only 'All Data'
with gr.Column():
gr.Markdown("## Datos Cargados")
data_output = gr.Dataframe(label="Tabla de Datos", interactive=False)
with gr.Row(visible=False) as analysis_row:
with gr.Column():
fit_button = gr.Button("Ajustar Modelo Simplificado y Completo") # Button label changed
gr.Markdown("**Modelo Completo**")
model_completo_output = model_completo_output # gr.HTML() # output_components are now global
pareto_completo_output = pareto_completo_output # gr.Plot()
gr.Markdown("**Modelo Simplificado**")
model_simplificado_output = model_simplificado_output # gr.HTML()
pareto_simplificado_output = pareto_simplificado_output # gr.Plot()
gr.Markdown("## Modelo Personalizado") # Personalized Model Section
factor_checkboxes_comp = factor_checkboxes # gr.CheckboxGroup(["factors", "x1_sq", "x2_sq", "x3_sq"], label="Términos de Factores", value=["factors", "x1_sq", "x2_sq", "x3_sq"]) # Factor Checkboxes
interaction_checkboxes_comp = interaction_checkboxes # gr.CheckboxGroup(["x1x2", "x1x3", "x2x3"], label="Términos de Interacción") # Interaction Checkboxes
custom_model_button = gr.Button("Ajustar Modelo Personalizado") # Fit Custom Model Button
model_personalized_output_comp = model_personalized_output # gr.HTML() # Output for personalized model summary
pareto_personalized_output_comp = pareto_personalized_output # gr.Plot() # Pareto for personalized model
gr.Markdown("**Ecuación del Modelo Simplificado**")
equation_output = equation_output # gr.HTML()
optimization_table_output = optimization_table_output # gr.Dataframe(label="Tabla de Optimización", interactive=False)
prediction_table_output = prediction_table_output # gr.Dataframe(label="Tabla de Predicciones", interactive=False)
contribution_table_output = contribution_table_output # gr.Dataframe(label="Tabla de % de Contribución", interactive=False)
anova_table_output = anova_table_output # gr.Dataframe(label="Tabla ANOVA Detallada", interactive=False)
gr.Markdown("## Descargar Todas las Tablas")
download_excel_button_comp = 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")
model_type_radio = gr.Radio(["simplified", "full", "personalized"], value="simplified", label="Tipo de Modelo para Gráficos") # Model Type Radio
fixed_variable_input = gr.Dropdown(label="Variable Fija", choices=["Glucosa_g_L", "Proteina_Pescado_g_L", "Sulfato_Manganeso_g_L"], value="Glucosa_g_L")
fixed_level_input = gr.Slider(label="Nivel de Variable Fija (Natural Units)", minimum=min(data['Glucosa_g_L']), maximum=max(data['Glucosa_g_L']), step=0.1, value=5.0)
plot_button = gr.Button("Generar Gráficos")
with gr.Row():
left_button = gr.Button("<")
right_button = gr.Button(">")
rsm_plot_output = rsm_plot_output # gr.Plot()
plot_info = plot_info # gr.Textbox(label="Información del Gráfico", value="Gráfico 1 de 9", interactive=False)
with gr.Row():
download_plot_button_comp = download_plot_button # gr.DownloadButton("Descargar Gráfico Actual (PNG)")
download_all_plots_button_comp = download_all_plots_button # gr.DownloadButton("Descargar Todos los Gráficos (ZIP)")
current_index_state = current_index_state # gr.State(0)
all_figures_state = all_figures_state # gr.State([])
current_model_type_state = current_model_type_state # gr.State('simplified')
# Cargar datos
load_button.click(
load_data,
inputs=[data_input],
outputs=[data_output, analysis_row]
)
# Ajustar modelo y optimizar (Simplified and Full)
fit_button.click(
fit_and_optimize_model,
inputs=[],
outputs=[
model_completo_output,
pareto_completo_output,
model_simplificado_output,
pareto_simplificado_output,
equation_output,
optimization_table_output,
prediction_table_output,
contribution_table_output,
anova_table_output,
download_all_plots_button,
download_excel_button
]
)
# Ajustar modelo personalizado
custom_model_button.click( # New event for custom model fitting
fit_custom_model,
inputs=[factor_checkboxes_comp, interaction_checkboxes_comp],
outputs=[model_personalized_output_comp, pareto_personalized_output_comp] # pass output components
)
# Generar y mostrar los gráficos
plot_button.click(
lambda fixed_var, fixed_lvl, model_type: show_plot(0, [], model_type) if not hasattr(rsm, 'all_figures_full') or not rsm.all_figures_full else show_plot(0, [], model_type) if model_type == 'full' and not rsm.all_figures_full else show_plot(0, [], model_type) if model_type == 'simplified' and not rsm.all_figures_simplified else show_plot(0, [], model_type) if model_type == 'personalized' and not rsm.all_figures_personalized else show_plot(0, rsm.all_figures_full if model_type == 'full' else rsm.all_figures_simplified if model_type == 'simplified' else rsm.all_figures_personalized, model_type),
inputs=[fixed_variable_input, fixed_level_input, model_type_radio],
outputs=[rsm_plot_output, plot_info, current_index_state, current_model_type_state]
)
# Navegación de gráficos
left_button.click(
lambda current_index, all_figures, model_type: navigate_plot('left', current_index, all_figures, model_type),
inputs=[current_index_state, all_figures_state, current_model_type_state],
outputs=[rsm_plot_output, plot_info, current_index_state]
)
right_button.click(
lambda current_index, all_figures, model_type: navigate_plot('right', current_index, all_figures, model_type),
inputs=[current_index_state, all_figures_state, current_model_type_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, current_model_type_state],
outputs=download_plot_button
)
# Descargar todos los gráficos en ZIP
download_all_plots_button.click(
lambda model_type: download_all_plots_zip(model_type),
inputs=[current_model_type_state],
outputs=download_all_plots_button
)
# Descargar todas las tablas en Excel y Word
download_excel_button.click(
fn=lambda: download_all_tables_excel(),
inputs=[],
outputs=download_excel_button
)
download_word_button.click(
fn=lambda: exportar_word(rsm, rsm.get_all_tables()),
inputs=[],
outputs=download_word_button
)
# Ejemplo de uso
gr.Markdown("## Instrucciones:")
gr.Markdown("""
1. Click 'Cargar Datos' para usar los datos precargados.
2. Click 'Ajustar Modelo Simplificado y Completo'.
3. Opcional: Define un Modelo Personalizado seleccionando términos y haz clic en 'Ajustar Modelo Personalizado'.
4. Selecciona el 'Tipo de Modelo para Gráficos' (Simplificado, Completo o Personalizado).
5. Select 'Variable Fija' and 'Nivel de Variable Fija'.
6. Click 'Generar Gráficos'.
7. Navega entre los gráficos usando los botones '<' y '>'.
8. Descarga el gráfico actual en PNG o descarga todos los gráficos en un ZIP.
9. Descarga todas las tablas en un archivo Excel o Word con los botones correspondientes.
""")
return demo
# --- Función Principal ---
def main():
interface = create_gradio_interface()
interface.launch(share=True)
if __name__ == "__main__":
main()