|
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, 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. |
|
|
|
Args: |
|
data (pd.DataFrame): DataFrame con los datos del experimento. |
|
x1_name (str): Nombre de la primera variable independiente. |
|
x2_name (str): Nombre de la segunda variable independiente. |
|
x3_name (str): Nombre de la tercera variable independiente. |
|
y_name (str): Nombre de la variable dependiente. |
|
x1_levels (list): Niveles de la primera variable independiente. |
|
x2_levels (list): Niveles de la segunda variable independiente. |
|
x3_levels (list): Niveles de la tercera variable independiente. |
|
""" |
|
self.data = data.copy() |
|
|
|
|
|
self.model = None |
|
self.model_simplified = None |
|
self.optimized_results = None |
|
self.optimal_levels = None |
|
|
|
self.x1_name = x1_name |
|
self.x2_name = x2_name |
|
self.x3_name = x3_name |
|
self.y_name = y_name |
|
|
|
|
|
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. |
|
|
|
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()) |
|
return 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()) |
|
return 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 |
|
|
|
|
|
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 |
|
|
|
|
|
varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable] |
|
|
|
|
|
x_natural_levels = self.get_levels(varying_variables[0]) |
|
y_natural_levels = self.get_levels(varying_variables[1]) |
|
|
|
|
|
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) |
|
|
|
|
|
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]) |
|
|
|
|
|
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) |
|
|
|
|
|
z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape) |
|
|
|
|
|
varying_variables = [var for var in [self.x1_name, self.x2_name, self.x3_name] if var != fixed_variable] |
|
|
|
|
|
fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable) |
|
subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)] |
|
|
|
|
|
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) |
|
] |
|
|
|
|
|
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])) |
|
|
|
|
|
fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)]) |
|
|
|
|
|
|
|
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' |
|
)) |
|
|
|
|
|
|
|
|
|
|
|
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)]), |
|
text=point_labels, |
|
textposition='top center', |
|
name='Experimentos' |
|
)) |
|
|
|
|
|
fig.update_layout( |
|
scene=dict( |
|
xaxis_title=varying_variables[0] + " (g/L)", |
|
yaxis_title=varying_variables[1] + " (g/L)", |
|
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:.2f} (g/L) (Modelo Simplificado)</sup>", |
|
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 |
|
|
|
|
|
levels_to_plot_natural = { |
|
self.x1_name: self.x1_levels, |
|
self.x2_name: self.x2_levels, |
|
self.x3_name: self.x3_levels |
|
} |
|
|
|
|
|
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. |
|
""" |
|
|
|
tvalues = model.tvalues[1:] |
|
abs_tvalues = np.abs(tvalues) |
|
sorted_idx = np.argsort(abs_tvalues)[::-1] |
|
sorted_tvalues = abs_tvalues[sorted_idx] |
|
sorted_names = tvalues.index[sorted_idx] |
|
|
|
|
|
alpha = 0.05 |
|
dof = model.df_resid |
|
t_critical = t.ppf(1 - alpha / 2, dof) |
|
|
|
|
|
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") |
|
|
|
|
|
fig.add_vline(x=t_critical, line_dash="dot", |
|
annotation_text=f"t crítico = {t_critical:.2f}", |
|
annotation_position="bottom right") |
|
|
|
return fig |
|
|
|
|
|
|
|
def load_data(x1_name, x2_name, x3_name, y_name, x1_levels_str, x2_levels_str, x3_levels_str, data_str): |
|
""" |
|
Carga los datos del diseño Box-Behnken desde cajas de texto y crea la instancia de RSM_BoxBehnken. |
|
|
|
Args: |
|
x1_name (str): Nombre de la primera variable independiente. |
|
x2_name (str): Nombre de la segunda variable independiente. |
|
x3_name (str): Nombre de la tercera variable independiente. |
|
y_name (str): Nombre de la variable dependiente. |
|
x1_levels_str (str): Niveles de la primera variable, separados por comas. |
|
x2_levels_str (str): Niveles de la segunda variable, separados por comas. |
|
x3_levels_str (str): Niveles de la tercera variable, separados por comas. |
|
data_str (str): Datos del experimento en formato CSV, separados por comas. |
|
|
|
Returns: |
|
tuple: (pd.DataFrame, str, str, str, str, list, list, list, gr.update) |
|
""" |
|
try: |
|
|
|
x1_levels = [float(x.strip()) for x in x1_levels_str.split(',')] |
|
x2_levels = [float(x.strip()) for x in x2_levels_str.split(',')] |
|
x3_levels = [float(x.strip()) for x in x3_levels_str.split(',')] |
|
|
|
|
|
data_list = [row.split(',') for row in data_str.strip().split('\n')] |
|
column_names = ['Exp.', x1_name, x2_name, x3_name, y_name] |
|
data = pd.DataFrame(data_list, columns=column_names) |
|
data = data.apply(pd.to_numeric, errors='coerce') |
|
|
|
|
|
if not all(col in data.columns for col in column_names): |
|
raise ValueError("El formato de los datos no es correcto.") |
|
|
|
|
|
global rsm |
|
rsm = RSM_BoxBehnken(data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels) |
|
|
|
return data, x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels, gr.update(visible=True) |
|
|
|
except Exception as e: |
|
return None, "", "", "", "", [], [], [], gr.update(visible=False), f"Error: {e}" |
|
|
|
def fit_and_optimize_model(): |
|
if 'rsm' not in globals(): |
|
return None, None, "Error: Carga los datos primero." |
|
|
|
pareto_completo = rsm.fit_model() |
|
pareto_simplificado = rsm.fit_simplified_model() |
|
rsm.optimize() |
|
model_summary = rsm.model_simplified.summary().as_html() |
|
|
|
return model_summary, pareto_completo, pareto_simplificado, f"{rsm.x1_name}: {rsm.optimal_levels[0]:.4f}, {rsm.x2_name}: {rsm.optimal_levels[1]:.4f}, {rsm.x3_name}: {rsm.optimal_levels[2]:.4f}, Valor máximo de {rsm.y_name}: {-rsm.optimized_results.fun:.4f}" |
|
|
|
def generate_rsm_plot(fixed_variable, fixed_level): |
|
if 'rsm' not in globals(): |
|
return None, "Error: Carga los datos primero." |
|
fig = rsm.plot_rsm_individual(fixed_variable, fixed_level) |
|
return fig |
|
|
|
|
|
|
|
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(): |
|
gr.Markdown("## Configuración del Diseño") |
|
x1_name_input = gr.Textbox(label="Nombre de la Variable X1 (ej. Glucosa)", value="Glucosa") |
|
x2_name_input = gr.Textbox(label="Nombre de la Variable X2 (ej. Extracto de Levadura)", value="Extracto_de_Levadura") |
|
x3_name_input = gr.Textbox(label="Nombre de la Variable X3 (ej. Triptófano)", value="Triptofano") |
|
y_name_input = gr.Textbox(label="Nombre de la Variable Dependiente (ej. AIA (ppm))", value="AIA_ppm") |
|
x1_levels_input = gr.Textbox(label="Niveles de X1 (separados por comas)", value="1, 3.5, 5.5") |
|
x2_levels_input = gr.Textbox(label="Niveles de X2 (separados por comas)", value="0.03, 0.2, 0.3") |
|
x3_levels_input = gr.Textbox(label="Niveles de X3 (separados por comas)", value="0.4, 0.65, 0.9") |
|
data_input = gr.Textbox(label="Datos del Experimento (formato CSV)", lines=5, 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") |
|
|
|
|
|
with gr.Row(visible=False) as analysis_row: |
|
with gr.Column(): |
|
fit_button = gr.Button("Ajustar Modelo y Optimizar") |
|
model_summary_output = gr.HTML() |
|
pareto_chart_completo = gr.Plot() |
|
pareto_chart_simplificado = 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=["Glucosa", "Extracto_de_Levadura", "Triptófano"], value="Glucosa") |
|
fixed_level_input = gr.Slider(label="Nivel de Variable Fija", minimum=0, maximum=1, step=0.01, value=0.5) |
|
plot_button = gr.Button("Generar Gráfico") |
|
rsm_plot_output = gr.Plot() |
|
|
|
load_button.click( |
|
load_data, |
|
inputs=[x1_name_input, x2_name_input, x3_name_input, y_name_input, x1_levels_input, x2_levels_input, x3_levels_input, data_input], |
|
outputs=[data_output, x1_name_input, x2_name_input, x3_name_input, y_name_input, x1_levels_input, x2_levels_input, x3_levels_input, analysis_row] |
|
) |
|
|
|
fit_button.click(fit_and_optimize_model, outputs=[model_summary_output, pareto_chart_completo, pareto_chart_simplificado, optimization_results_output]) |
|
plot_button.click(generate_rsm_plot, inputs=[fixed_variable_input, fixed_level_input], outputs=[rsm_plot_output]) |
|
|
|
|
|
gr.Markdown("## Ejemplo de uso") |
|
gr.Markdown("1. Introduce los nombres de las variables y sus niveles en las cajas de texto correspondientes.") |
|
gr.Markdown("2. Copia y pega los datos del experimento en la caja de texto 'Datos del Experimento'.") |
|
gr.Markdown("3. Haz clic en 'Cargar Datos' para cargar los datos en la tabla.") |
|
gr.Markdown("4. Haz clic en 'Ajustar Modelo y Optimizar' para ajustar el modelo y encontrar los niveles óptimos de los factores.") |
|
gr.Markdown("5. Selecciona una variable fija y su nivel en los controles deslizantes.") |
|
gr.Markdown("6. Haz clic en 'Generar Gráfico' para generar un gráfico de superficie de respuesta.") |
|
|
|
demo.launch() |