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 plotly.subplots import make_subplots
from scipy.optimize import minimize
import plotly.express as px
from scipy.stats import t
import gradio as gr
class RSM_BoxBehnken:
def __init__(self, data):
"""
Inicializa la clase con los datos del diseño Box-Behnken.
Args:
data (pd.DataFrame): DataFrame con los datos del experimento.
"""
self.data = data.copy()
self.data.rename(columns={
'Glucosa': 'Glucosa',
'Extracto de Levadura': 'Extracto_de_Levadura',
'Triptófano': 'Triptofano',
'AIA (ppm)': 'AIA_ppm'
}, inplace=True)
self.model = None
self.model_simplified = None
self.optimized_results = None
self.optimal_levels = None
self.x1_name = 'Glucosa'
self.x2_name = 'Extracto_de_Levadura'
self.x3_name = 'Triptofano'
self.y_name = 'AIA_ppm'
# Niveles originales de las variables
self.x1_levels = [1, 3.5, 5.5] # Glucosa
self.x2_levels = [0.03, 0.2, 0.3] # Extracto de Levadura
self.x3_levels = [0.4, 0.65, 0.9] # Triptófano
def get_levels(self, variable_name):
"""
Obtiene los niveles para una variable específica.
Args:
variable_name (str): Nombre de la variable.
Returns:
list: Niveles de la variable.
"""
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())
self.pareto_chart(self.model, "Pareto - Modelo Completo")
def fit_simplified_model(self):
"""
Ajusta el modelo de segundo orden a los datos, eliminando términos no significativos.
"""
formula = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + ' \
f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
self.model_simplified = smf.ols(formula, data=self.data).fit()
print("\nModelo Simplificado:")
print(self.model_simplified.summary())
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.
Args:
method (str): Método de optimización a utilizar (por defecto, 'Nelder-Mead').
"""
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]]}))
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)
]
print(f"\nNiveles óptimos encontrados (basado en modelo simplificado):")
print(f"{self.x1_name}: {optimal_levels_natural[0]:.4f} g/L")
print(f"{self.x2_name}: {optimal_levels_natural[1]:.4f} g/L")
print(f"{self.x3_name}: {optimal_levels_natural[2]:.4f} g/L")
print(f"Valor máximo de {self.y_name}: {-self.optimized_results.fun:.4f}")
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.
Args:
fixed_variable (str): Nombre de la variable a mantener fija.
fixed_level (float): Nivel al que se fija la variable (en unidades naturales).
Returns:
go.Figure: Objeto de figura de Plotly.
"""
if self.model_simplified is None:
print("Error: Ajusta el modelo simplificado primero.")
return None
# Determinar las variables que varían y sus niveles naturales
varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
# Establecer los niveles naturales para las variables que varían
x_natural_levels = self.get_levels(varying_variables[0])
y_natural_levels = self.get_levels(varying_variables[1])
# Crear una malla de puntos para las variables que varían (en unidades naturales)
x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
y_range_natural = np.linspace(y_natural_levels[0], y_natural_levels[-1], 100)
x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
# Convertir la malla de variables naturales a codificadas
x_grid_coded = self.natural_to_coded(x_grid_natural, varying_variables[0])
y_grid_coded = self.natural_to_coded(y_grid_natural, varying_variables[1])
# Crear un DataFrame para la predicción con variables codificadas
prediction_data = pd.DataFrame({
varying_variables[0]: x_grid_coded.flatten(),
varying_variables[1]: y_grid_coded.flatten(),
})
prediction_data[fixed_variable] = self.natural_to_coded(fixed_level, fixed_variable)
# Calcular los valores predichos
z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape)
# 1. Identificar los dos factores que varían
varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable]
# 2. 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)]
# 3. 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
# Crear una lista de colores y etiquetas para los puntos
colors = ['red', 'blue', 'green', 'purple', 'orange', 'yellow', 'cyan', 'magenta']
point_labels = []
for i, row in experiments_data.iterrows():
point_labels.append(f"{row[self.y_name]:.2f}")
fig.add_trace(go.Scatter3d(
x=experiments_x_natural,
y=experiments_y_natural,
z=experiments_data[self.y_name],
mode='markers+text',
marker=dict(size=4, color=colors[:len(experiments_x_natural)]), # Usar colores de la lista
text=point_labels, # Usar las etiquetas creadas
textposition='top center',
name='Experimentos'
))
# Añadir etiquetas y título con variables naturales
fig.update_layout(
scene=dict(
xaxis_title=varying_variables[0] + " (g/L)",
yaxis_title=varying_variables[1] + " (g/L)",
zaxis_title=self.y_name,
# Puedes mantener la configuración de grid en los planos si lo deseas
# xaxis=dict(showgrid=True, gridwidth=1, gridcolor='lightgray'),
# yaxis=dict(showgrid=True, gridwidth=1, gridcolor='lightgray'),
# zaxis=dict(showgrid=True, gridwidth=1, gridcolor='lightgray')
),
title=f"{self.y_name} vs {varying_variables[0]} y {varying_variables[1]}
{fixed_variable} fijo en {fixed_level:.2f} (g/L) (Modelo Simplificado)",
height=800,
width=1000,
showlegend=True
)
return fig
def generate_all_plots(self):
"""
Genera todas las gráficas de RSM, variando la variable fija y sus niveles usando el modelo simplificado.
"""
if self.model_simplified is None:
print("Error: Ajusta el modelo simplificado primero.")
return
# Niveles naturales para graficar
levels_to_plot_natural = {
self.x1_name: self.x1_levels,
self.x2_name: self.x2_levels,
self.x3_name: self.x3_levels
}
# Generar y mostrar gráficos individuales
for fixed_variable in [self.x1_name, self.x2_name, self.x3_name]:
for level in levels_to_plot_natural[fixed_variable]:
fig = self.plot_rsm_individual(fixed_variable, level)
if fig is not None:
fig.show()
def coded_to_natural(self, coded_value, variable_name):
"""Convierte un valor codificado a su valor natural."""
levels = self.get_levels(variable_name)
return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2
def natural_to_coded(self, natural_value, variable_name):
"""Convierte un valor natural a su valor codificado."""
levels = self.get_levels(variable_name)
return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
def pareto_chart(self, model, title):
"""
Genera un diagrama de Pareto para los efectos estandarizados de un modelo,
incluyendo la línea de significancia.
Args:
model: Modelo ajustado de statsmodels.
title (str): Título del gráfico.
"""
# Calcular los efectos estandarizados
tvalues = model.tvalues[1:] # Excluir la Intercept
abs_tvalues = np.abs(tvalues)
sorted_idx = np.argsort(abs_tvalues)[::-1]
sorted_tvalues = abs_tvalues[sorted_idx]
sorted_names = tvalues.index[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,
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:.2f}",
annotation_position="bottom right")
return fig
# Crear un DataFrame a partir de la tabla
data = pd.DataFrame({
'Exp.': [1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13, 14, 15],
'Glucosa': [-1, 1, -1, 1, -1, 1, -1, 1, 0, 0, 0, 0, 0, 0, 0],
'Extracto de Levadura': [-1, -1, 1, 1, 0, 0, 0, 0, -1, 1, -1, 1, 0, 0, 0],
'Triptófano': [0, 0, 0, 0, -1, -1, 1, 1, -1, -1, 1, 1, 0, 0, 0],
'AIA (ppm)': [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]
})
# Crear una instancia de la clase RSM_BoxBehnken
rsm = RSM_BoxBehnken(data)
# --- Interfaz de Gradio ---
def fit_and_optimize_model():
rsm.fit_model()
rsm.fit_simplified_model()
rsm.optimize()
model_summary = rsm.model_simplified.summary().as_html()
pareto_fig = rsm.pareto_chart(rsm.model_simplified, "Pareto - Modelo Simplificado")
return model_summary, pareto_fig, f"{rsm.x1_name}: {rsm.optimal_levels[0]:.4f} g/L, {rsm.x2_name}: {rsm.optimal_levels[1]:.4f} g/L, {rsm.x3_name}: {rsm.optimal_levels[2]:.4f} g/L, Valor máximo de {rsm.y_name}: {-rsm.optimized_results.fun:.4f}"
def generate_rsm_plot(fixed_variable, fixed_level):
fig = rsm.plot_rsm_individual(fixed_variable, fixed_level)
return fig
# Crear la interfaz de Gradio
with gr.Blocks() as demo:
gr.Markdown("# Optimización de la producción de AIA usando RSM Box-Behnken")
with gr.Row():
with gr.Column():
fit_button = gr.Button("Ajustar Modelo y Optimizar")
model_summary_output = gr.HTML()
pareto_chart_output = gr.Plot()
optimization_results_output = gr.Textbox(label="Resultados de la Optimización")
with gr.Column():
gr.Markdown("## Generar Gráficos de Superficie de Respuesta")
fixed_variable_input = gr.Dropdown(label="Variable Fija", choices=[rsm.x1_name, rsm.x2_name, rsm.x3_name], value=rsm.x1_name)
fixed_level_input = gr.Slider(label="Nivel de Variable Fija", minimum=rsm.x1_levels[0], maximum=rsm.x1_levels[-1], step=0.01, value=rsm.x1_levels[1])
plot_button = gr.Button("Generar Gráfico")
rsm_plot_output = gr.Plot()
fit_button.click(fit_and_optimize_model, inputs=[], outputs=[model_summary_output, pareto_chart_output, optimization_results_output])
plot_button.click(generate_rsm_plot, inputs=[fixed_variable_input, fixed_level_input], outputs=rsm_plot_output)
# Ejemplo de uso
gr.Markdown("## Ejemplo de uso")
gr.Markdown("1. Haz clic en 'Ajustar Modelo y Optimizar' para ajustar el modelo y encontrar los niveles óptimos de los factores.")
gr.Markdown("2. Selecciona una variable fija y su nivel en los controles deslizantes.")
gr.Markdown("3. Haz clic en 'Generar Gráfico' para generar un gráfico de superficie de respuesta.")
demo.launch()