Spaces:
Sleeping
Sleeping
Update app.py
Browse files
app.py
CHANGED
@@ -5,7 +5,7 @@ import statsmodels.api as sm
|
|
5 |
import plotly.graph_objects as go
|
6 |
from scipy.optimize import minimize
|
7 |
import plotly.express as px
|
8 |
-
from scipy.stats import
|
9 |
import gradio as gr
|
10 |
import io
|
11 |
import zipfile
|
@@ -14,62 +14,92 @@ from datetime import datetime
|
|
14 |
import docx
|
15 |
from docx.shared import Inches, Pt
|
16 |
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
|
17 |
-
from matplotlib.colors import to_hex
|
18 |
import os
|
|
|
19 |
|
20 |
-
# --- Clase
|
21 |
-
class
|
22 |
-
def __init__(self, data,
|
23 |
"""
|
24 |
-
Inicializa la clase con los datos del diseño
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
"""
|
26 |
self.data = data.copy()
|
|
|
|
|
|
|
|
|
27 |
self.model = None
|
28 |
self.model_simplified = None
|
29 |
self.optimized_results = None
|
30 |
self.optimal_levels = None
|
31 |
-
self.all_figures = []
|
32 |
-
self.x1_name = x1_name
|
33 |
-
self.x2_name = x2_name
|
34 |
-
self.x3_name = x3_name
|
35 |
-
self.y_name = y_name
|
36 |
-
|
37 |
-
# Niveles originales de las variables
|
38 |
-
self.x1_levels = x1_levels
|
39 |
-
self.x2_levels = x2_levels
|
40 |
-
self.x3_levels = x3_levels
|
41 |
|
42 |
-
def
|
43 |
"""
|
44 |
-
|
45 |
"""
|
46 |
-
|
47 |
-
|
48 |
-
|
49 |
-
|
50 |
-
|
51 |
-
return self.x3_levels
|
52 |
else:
|
53 |
-
raise ValueError(
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
54 |
|
55 |
def fit_model(self):
|
56 |
"""
|
57 |
Ajusta el modelo de segundo orden completo a los datos.
|
58 |
"""
|
59 |
-
|
60 |
-
|
61 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
62 |
self.model = smf.ols(formula, data=self.data).fit()
|
63 |
print("Modelo Completo:")
|
64 |
print(self.model.summary())
|
65 |
return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
|
66 |
|
67 |
-
def fit_simplified_model(self):
|
68 |
"""
|
69 |
-
Ajusta el modelo
|
|
|
|
|
|
|
70 |
"""
|
71 |
-
|
72 |
-
|
|
|
|
|
73 |
self.model_simplified = smf.ols(formula, data=self.data).fit()
|
74 |
print("\nModelo Simplificado:")
|
75 |
print(self.model_simplified.summary())
|
@@ -82,34 +112,32 @@ class RSM_BoxBehnken:
|
|
82 |
if self.model_simplified is None:
|
83 |
print("Error: Ajusta el modelo simplificado primero.")
|
84 |
return
|
85 |
-
|
86 |
def objective_function(x):
|
87 |
-
|
88 |
-
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
x0 = [0, 0, 0]
|
95 |
-
|
96 |
self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
|
97 |
self.optimal_levels = self.optimized_results.x
|
98 |
-
|
99 |
# Convertir niveles óptimos de codificados a naturales
|
100 |
optimal_levels_natural = [
|
101 |
-
self.coded_to_natural(self.optimal_levels[
|
102 |
-
|
103 |
-
self.coded_to_natural(self.optimal_levels[2], self.x3_name)
|
104 |
]
|
|
|
105 |
# Crear la tabla de optimización
|
106 |
optimization_table = pd.DataFrame({
|
107 |
-
'Variable':
|
108 |
'Nivel Óptimo (Natural)': optimal_levels_natural,
|
109 |
'Nivel Óptimo (Codificado)': self.optimal_levels
|
110 |
})
|
111 |
-
|
112 |
-
return optimization_table.round(3)
|
113 |
|
114 |
def plot_rsm_individual(self, fixed_variable, fixed_level):
|
115 |
"""
|
@@ -119,12 +147,16 @@ class RSM_BoxBehnken:
|
|
119 |
print("Error: Ajusta el modelo simplificado primero.")
|
120 |
return None
|
121 |
|
122 |
-
|
123 |
-
|
|
|
|
|
124 |
|
125 |
-
|
126 |
-
|
127 |
-
|
|
|
|
|
128 |
|
129 |
# Crear una malla de puntos para las variables que varían (en unidades naturales)
|
130 |
x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
|
@@ -132,39 +164,27 @@ class RSM_BoxBehnken:
|
|
132 |
x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
|
133 |
|
134 |
# Convertir la malla de variables naturales a codificadas
|
135 |
-
x_grid_coded = self.natural_to_coded(x_grid_natural,
|
136 |
-
y_grid_coded = self.natural_to_coded(y_grid_natural,
|
137 |
|
138 |
# Crear un DataFrame para la predicción con variables codificadas
|
139 |
prediction_data = pd.DataFrame({
|
140 |
-
|
141 |
-
|
142 |
})
|
143 |
-
|
|
|
|
|
|
|
|
|
144 |
|
145 |
# Calcular los valores predichos
|
146 |
z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape)
|
147 |
|
148 |
-
#
|
149 |
-
fixed_level_coded = self.natural_to_coded(fixed_level, fixed_variable)
|
150 |
-
subset_data = self.data[np.isclose(self.data[fixed_variable], fixed_level_coded)]
|
151 |
-
|
152 |
-
# Filtrar por niveles válidos en las variables que varían
|
153 |
-
valid_levels = [-1, 0, 1]
|
154 |
-
experiments_data = subset_data[
|
155 |
-
subset_data[varying_variables[0]].isin(valid_levels) &
|
156 |
-
subset_data[varying_variables[1]].isin(valid_levels)
|
157 |
-
]
|
158 |
-
|
159 |
-
# Convertir coordenadas de experimentos a naturales
|
160 |
-
experiments_x_natural = experiments_data[varying_variables[0]].apply(lambda x: self.coded_to_natural(x, varying_variables[0]))
|
161 |
-
experiments_y_natural = experiments_data[varying_variables[1]].apply(lambda x: self.coded_to_natural(x, varying_variables[1]))
|
162 |
-
|
163 |
-
# Crear el gráfico de superficie con variables naturales en los ejes y transparencia
|
164 |
fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)])
|
165 |
|
166 |
-
#
|
167 |
-
# Líneas en la dirección x
|
168 |
for i in range(x_grid_natural.shape[0]):
|
169 |
fig.add_trace(go.Scatter3d(
|
170 |
x=x_grid_natural[i, :],
|
@@ -175,7 +195,6 @@ class RSM_BoxBehnken:
|
|
175 |
showlegend=False,
|
176 |
hoverinfo='skip'
|
177 |
))
|
178 |
-
# Líneas en la dirección y
|
179 |
for j in range(x_grid_natural.shape[1]):
|
180 |
fig.add_trace(go.Scatter3d(
|
181 |
x=x_grid_natural[:, j],
|
@@ -187,18 +206,18 @@ class RSM_BoxBehnken:
|
|
187 |
hoverinfo='skip'
|
188 |
))
|
189 |
|
190 |
-
#
|
191 |
-
|
192 |
-
|
193 |
colors = px.colors.qualitative.Safe
|
194 |
point_labels = [f"{row[self.y_name]:.3f}" for _, row in experiments_data.iterrows()]
|
195 |
|
196 |
fig.add_trace(go.Scatter3d(
|
197 |
-
x=
|
198 |
-
y=
|
199 |
-
z=experiments_data[self.y_name]
|
200 |
mode='markers+text',
|
201 |
-
marker=dict(size=4, color=colors[:len(
|
202 |
text=point_labels,
|
203 |
textposition='top center',
|
204 |
name='Experimentos'
|
@@ -207,11 +226,11 @@ class RSM_BoxBehnken:
|
|
207 |
# Añadir etiquetas y título con variables naturales
|
208 |
fig.update_layout(
|
209 |
scene=dict(
|
210 |
-
xaxis_title=f"{
|
211 |
-
yaxis_title=f"{
|
212 |
zaxis_title=self.y_name,
|
213 |
),
|
214 |
-
title=f"{self.y_name} vs {
|
215 |
height=800,
|
216 |
width=1000,
|
217 |
showlegend=True
|
@@ -226,8 +245,9 @@ class RSM_BoxBehnken:
|
|
226 |
units = {
|
227 |
'Glucosa': 'g/L',
|
228 |
'Extracto_de_Levadura': 'g/L',
|
229 |
-
'
|
230 |
-
'AIA_ppm': 'ppm'
|
|
|
231 |
}
|
232 |
return units.get(variable_name, '')
|
233 |
|
@@ -243,36 +263,21 @@ class RSM_BoxBehnken:
|
|
243 |
self.all_figures = [] # Resetear la lista de figuras
|
244 |
|
245 |
# Niveles naturales para graficar
|
246 |
-
levels_to_plot_natural =
|
247 |
-
self.x1_name: self.x1_levels,
|
248 |
-
self.x2_name: self.x2_levels,
|
249 |
-
self.x3_name: self.x3_levels
|
250 |
-
}
|
251 |
|
252 |
# Generar y almacenar gráficos individuales
|
253 |
-
for fixed_variable in
|
254 |
-
for level in
|
255 |
fig = self.plot_rsm_individual(fixed_variable, level)
|
256 |
if fig is not None:
|
257 |
self.all_figures.append(fig)
|
258 |
|
259 |
-
def coded_to_natural(self, coded_value, variable_name):
|
260 |
-
"""Convierte un valor codificado a su valor natural."""
|
261 |
-
levels = self.get_levels(variable_name)
|
262 |
-
return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2
|
263 |
-
|
264 |
-
def natural_to_coded(self, natural_value, variable_name):
|
265 |
-
"""Convierte un valor natural a su valor codificado."""
|
266 |
-
levels = self.get_levels(variable_name)
|
267 |
-
return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
|
268 |
-
|
269 |
def pareto_chart(self, model, title):
|
270 |
"""
|
271 |
Genera un diagrama de Pareto para los efectos usando estadísticos F,
|
272 |
incluyendo la línea de significancia.
|
273 |
"""
|
274 |
# Calcular los estadísticos F para cada término
|
275 |
-
# F = (coef/std_err)^2 = t^2
|
276 |
fvalues = model.tvalues[1:]**2 # Excluir la Intercept y convertir t a F
|
277 |
abs_fvalues = np.abs(fvalues)
|
278 |
sorted_idx = np.argsort(abs_fvalues)[::-1]
|
@@ -315,21 +320,15 @@ class RSM_BoxBehnken:
|
|
315 |
|
316 |
for term, coef in coefficients.items():
|
317 |
if term != 'Intercept':
|
318 |
-
if term
|
319 |
-
equation += f" + {coef:.3f}*{
|
320 |
-
elif
|
321 |
-
equation += f" + {coef:.3f}*{
|
322 |
-
|
323 |
-
equation += f" + {coef:.3f}*{
|
324 |
-
|
325 |
-
equation += f" + {coef:.3f}*{self.x1_name}^2"
|
326 |
-
elif term == f'I({self.x2_name} ** 2)':
|
327 |
-
equation += f" + {coef:.3f}*{self.x2_name}^2"
|
328 |
-
elif term == f'I({self.x3_name} ** 2)':
|
329 |
-
equation += f" + {coef:.3f}*{self.x3_name}^2"
|
330 |
-
|
331 |
return equation
|
332 |
-
|
333 |
def generate_prediction_table(self):
|
334 |
"""
|
335 |
Genera una tabla con los valores actuales, predichos y residuales.
|
@@ -338,7 +337,7 @@ class RSM_BoxBehnken:
|
|
338 |
print("Error: Ajusta el modelo simplificado primero.")
|
339 |
return None
|
340 |
|
341 |
-
self.data['Predicho'] = self.model_simplified.predict(self.data)
|
342 |
self.data['Residual'] = self.data[self.y_name] - self.data['Predicho']
|
343 |
|
344 |
return self.data[[self.y_name, 'Predicho', 'Residual']].round(3)
|
@@ -370,54 +369,13 @@ class RSM_BoxBehnken:
|
|
370 |
|
371 |
# Calcular estadísticos F y porcentaje de contribución para cada factor
|
372 |
ms_error = anova_table.loc['Residual', 'sum_sq'] / anova_table.loc['Residual', 'df']
|
373 |
-
|
374 |
-
# Agregar fila para el bloque
|
375 |
-
block_ss = self.data.groupby('Exp.')[self.y_name].sum().var() * len(self.data)
|
376 |
-
block_df = 1
|
377 |
-
block_ms = block_ss / block_df
|
378 |
-
block_f = block_ms / ms_error
|
379 |
-
block_p = f.sf(block_f, block_df, anova_table.loc['Residual', 'df'])
|
380 |
-
block_contribution = (block_ss / ss_total) * 100
|
381 |
|
382 |
-
contribution_table = pd.concat([contribution_table, pd.DataFrame({
|
383 |
-
'Fuente de Variación': ['Block'],
|
384 |
-
'Suma de Cuadrados': [block_ss],
|
385 |
-
'Grados de Libertad': [block_df],
|
386 |
-
'Cuadrado Medio': [block_ms],
|
387 |
-
'F': [block_f],
|
388 |
-
'Valor p': [block_p],
|
389 |
-
'% Contribución': [block_contribution]
|
390 |
-
})], ignore_index=True)
|
391 |
-
|
392 |
-
# Agregar fila para el modelo
|
393 |
-
model_ss = anova_table['sum_sq'][:-1].sum() # Suma todo excepto residual
|
394 |
-
model_df = anova_table['df'][:-1].sum()
|
395 |
-
model_ms = model_ss / model_df
|
396 |
-
model_f = model_ms / ms_error
|
397 |
-
model_p = f.sf(model_f, model_df, anova_table.loc['Residual', 'df'])
|
398 |
-
model_contribution = (model_ss / ss_total) * 100
|
399 |
-
|
400 |
-
contribution_table = pd.concat([contribution_table, pd.DataFrame({
|
401 |
-
'Fuente de Variación': ['Model'],
|
402 |
-
'Suma de Cuadrados': [model_ss],
|
403 |
-
'Grados de Libertad': [model_df],
|
404 |
-
'Cuadrado Medio': [model_ms],
|
405 |
-
'F': [model_f],
|
406 |
-
'Valor p': [model_p],
|
407 |
-
'% Contribución': [model_contribution]
|
408 |
-
})], ignore_index=True)
|
409 |
-
|
410 |
# Agregar filas para cada término del modelo
|
411 |
for index, row in anova_table.iterrows():
|
412 |
if index != 'Residual':
|
413 |
factor_name = index
|
414 |
-
if factor_name
|
415 |
-
factor_name =
|
416 |
-
elif factor_name == f'I({self.x2_name} ** 2)':
|
417 |
-
factor_name = f'{self.x2_name}^2'
|
418 |
-
elif factor_name == f'I({self.x3_name} ** 2)':
|
419 |
-
factor_name = f'{self.x3_name}^2'
|
420 |
-
|
421 |
ss_factor = row['sum_sq']
|
422 |
df_factor = row['df']
|
423 |
ms_factor = ss_factor / df_factor
|
@@ -436,13 +394,10 @@ class RSM_BoxBehnken:
|
|
436 |
})], ignore_index=True)
|
437 |
|
438 |
# Agregar fila para Cor Total
|
439 |
-
cor_total_ss = ss_total
|
440 |
-
cor_total_df = len(self.data) - 1
|
441 |
-
|
442 |
contribution_table = pd.concat([contribution_table, pd.DataFrame({
|
443 |
'Fuente de Variación': ['Cor Total'],
|
444 |
-
'Suma de Cuadrados': [
|
445 |
-
'Grados de Libertad': [
|
446 |
'Cuadrado Medio': [np.nan],
|
447 |
'F': [np.nan],
|
448 |
'Valor p': [np.nan],
|
@@ -459,88 +414,60 @@ class RSM_BoxBehnken:
|
|
459 |
print("Error: Ajusta el modelo simplificado primero.")
|
460 |
return None
|
461 |
|
462 |
-
#
|
463 |
-
|
464 |
-
formula_reduced = f'{self.y_name} ~ {self.x1_name} + {self.x2_name} + {self.x3_name} + ' \
|
465 |
-
f'I({self.x1_name}**2) + I({self.x2_name}**2) + I({self.x3_name}**2)'
|
466 |
-
model_reduced = smf.ols(formula_reduced, data=self.data).fit()
|
467 |
-
|
468 |
-
# 2. ANOVA del modelo reducido
|
469 |
-
anova_reduced = sm.stats.anova_lm(model_reduced, typ=2)
|
470 |
|
471 |
-
#
|
472 |
ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
|
473 |
|
474 |
-
#
|
475 |
-
|
476 |
-
|
477 |
-
# 5. Suma de cuadrados de la regresión
|
478 |
-
ss_regression = anova_reduced['sum_sq'][:-1].sum() # Sumar todo excepto 'Residual'
|
479 |
|
480 |
-
#
|
481 |
-
df_regression =
|
482 |
|
483 |
-
#
|
484 |
-
ss_residual =
|
485 |
-
df_residual =
|
486 |
|
487 |
-
#
|
488 |
-
|
489 |
-
if
|
490 |
-
ss_pure_error =
|
491 |
-
df_pure_error =
|
492 |
else:
|
493 |
ss_pure_error = np.nan
|
494 |
df_pure_error = np.nan
|
495 |
|
496 |
-
#
|
497 |
ss_lack_of_fit = ss_residual - ss_pure_error if not np.isnan(ss_pure_error) else np.nan
|
498 |
df_lack_of_fit = df_residual - df_pure_error if not np.isnan(df_pure_error) else np.nan
|
499 |
|
500 |
-
#
|
501 |
ms_regression = ss_regression / df_regression
|
502 |
ms_residual = ss_residual / df_residual
|
503 |
ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit if not np.isnan(ss_lack_of_fit) else np.nan
|
504 |
ms_pure_error = ss_pure_error / df_pure_error if not np.isnan(ss_pure_error) else np.nan
|
505 |
|
506 |
-
#
|
507 |
f_regression = ms_regression / ms_residual
|
508 |
p_regression = 1 - f.cdf(f_regression, df_regression, df_residual)
|
509 |
-
|
510 |
f_lack_of_fit = ms_lack_of_fit / ms_pure_error if not np.isnan(ms_lack_of_fit) else np.nan
|
511 |
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
|
512 |
|
513 |
-
#
|
514 |
detailed_anova_table = pd.DataFrame({
|
515 |
'Fuente de Variación': ['Regresión', 'Residual', 'Falta de Ajuste', 'Error Puro', 'Total'],
|
516 |
'Suma de Cuadrados': [ss_regression, ss_residual, ss_lack_of_fit, ss_pure_error, ss_total],
|
517 |
-
'Grados de Libertad': [df_regression, df_residual, df_lack_of_fit, df_pure_error,
|
518 |
'Cuadrado Medio': [ms_regression, ms_residual, ms_lack_of_fit, ms_pure_error, np.nan],
|
519 |
'F': [f_regression, np.nan, f_lack_of_fit, np.nan, np.nan],
|
520 |
'Valor p': [p_regression, np.nan, p_lack_of_fit, np.nan, np.nan]
|
521 |
})
|
522 |
-
|
523 |
-
# Calcular la suma de cuadrados y estadísticos F para la curvatura
|
524 |
-
ss_curvature = anova_reduced['sum_sq'][f'I({self.x1_name} ** 2)'] + \
|
525 |
-
anova_reduced['sum_sq'][f'I({self.x2_name} ** 2)'] + \
|
526 |
-
anova_reduced['sum_sq'][f'I({self.x3_name} ** 2)']
|
527 |
-
df_curvature = 3
|
528 |
-
ms_curvature = ss_curvature / df_curvature
|
529 |
-
f_curvature = ms_curvature / ms_residual
|
530 |
-
p_curvature = 1 - f.cdf(f_curvature, df_curvature, df_residual)
|
531 |
-
|
532 |
-
# Añadir la fila de curvatura a la tabla ANOVA
|
533 |
-
detailed_anova_table.loc[len(detailed_anova_table)] = [
|
534 |
-
'Curvatura',
|
535 |
-
ss_curvature,
|
536 |
-
df_curvature,
|
537 |
-
ms_curvature,
|
538 |
-
f_curvature,
|
539 |
-
p_curvature
|
540 |
-
]
|
541 |
|
542 |
# Reorganizar las filas y resetear el índice
|
543 |
-
detailed_anova_table = detailed_anova_table.reindex([0,
|
544 |
|
545 |
return detailed_anova_table.round(3)
|
546 |
|
@@ -550,12 +477,12 @@ class RSM_BoxBehnken:
|
|
550 |
"""
|
551 |
prediction_table = self.generate_prediction_table()
|
552 |
contribution_table = self.calculate_contribution_percentage()
|
553 |
-
|
554 |
|
555 |
return {
|
556 |
'Predicciones': prediction_table,
|
557 |
'% Contribución': contribution_table,
|
558 |
-
'ANOVA Detallada':
|
559 |
}
|
560 |
|
561 |
def save_figures_to_zip(self):
|
@@ -674,58 +601,80 @@ class RSM_BoxBehnken:
|
|
674 |
|
675 |
# --- Funciones para la Interfaz de Gradio ---
|
676 |
|
677 |
-
def load_data(
|
678 |
"""
|
679 |
-
Carga los datos del diseño
|
680 |
"""
|
681 |
try:
|
682 |
-
#
|
683 |
-
|
684 |
-
|
685 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
686 |
|
687 |
# Crear DataFrame a partir de la cadena de datos
|
688 |
data_list = [row.split(',') for row in data_str.strip().split('\n')]
|
689 |
-
column_names = ['Exp.'
|
690 |
data = pd.DataFrame(data_list, columns=column_names)
|
691 |
data = data.apply(pd.to_numeric, errors='coerce') # Convertir a numérico
|
692 |
|
693 |
-
#
|
694 |
-
if not all(col in data.columns for col in column_names):
|
695 |
-
raise ValueError("El formato de los datos no es correcto.")
|
696 |
-
|
697 |
-
# Crear la instancia de RSM_BoxBehnken
|
698 |
global rsm
|
699 |
-
rsm =
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
700 |
|
701 |
-
return data.round(3), x1_name, x2_name, x3_name, y_name, x1_levels, x2_levels, x3_levels, gr.update(visible=True)
|
702 |
-
|
703 |
except Exception as e:
|
704 |
# Mostrar mensaje de error
|
705 |
error_message = f"Error al cargar los datos: {str(e)}"
|
706 |
print(error_message)
|
707 |
-
return None,
|
708 |
|
709 |
-
def fit_and_optimize_model():
|
710 |
if 'rsm' not in globals():
|
711 |
return [None]*11 # Ajustar el número de outputs
|
712 |
|
|
|
|
|
|
|
713 |
# Ajustar modelos y optimizar
|
714 |
model_completo, pareto_completo = rsm.fit_model()
|
715 |
-
|
|
|
716 |
optimization_table = rsm.optimize()
|
717 |
equation = rsm.get_simplified_equation()
|
718 |
prediction_table = rsm.generate_prediction_table()
|
719 |
contribution_table = rsm.calculate_contribution_percentage()
|
720 |
anova_table = rsm.calculate_detailed_anova()
|
721 |
-
|
722 |
# Generar todas las figuras y almacenarlas
|
723 |
rsm.generate_all_plots()
|
724 |
-
|
725 |
# Formatear la ecuación para que se vea mejor en Markdown
|
726 |
-
|
727 |
-
|
728 |
-
|
|
|
|
|
|
|
729 |
# Guardar las tablas en Excel temporal
|
730 |
excel_path = rsm.save_tables_to_excel()
|
731 |
|
@@ -746,32 +695,6 @@ def fit_and_optimize_model():
|
|
746 |
excel_path # Ruta del Excel de tablas
|
747 |
)
|
748 |
|
749 |
-
def show_plot(current_index, all_figures):
|
750 |
-
if not all_figures:
|
751 |
-
return None, "No hay gráficos disponibles.", current_index
|
752 |
-
selected_fig = all_figures[current_index]
|
753 |
-
plot_info_text = f"Gráfico {current_index + 1} de {len(all_figures)}"
|
754 |
-
return selected_fig, plot_info_text, current_index
|
755 |
-
|
756 |
-
def navigate_plot(direction, current_index, all_figures):
|
757 |
-
"""
|
758 |
-
Navega entre los gráficos.
|
759 |
-
"""
|
760 |
-
if not all_figures:
|
761 |
-
return None, "No hay gráficos disponibles.", current_index
|
762 |
-
|
763 |
-
if direction == 'left':
|
764 |
-
new_index = (current_index - 1) % len(all_figures)
|
765 |
-
elif direction == 'right':
|
766 |
-
new_index = (current_index + 1) % len(all_figures)
|
767 |
-
else:
|
768 |
-
new_index = current_index
|
769 |
-
|
770 |
-
selected_fig = all_figures[new_index]
|
771 |
-
plot_info_text = f"Gráfico {new_index + 1} de {len(all_figures)}"
|
772 |
-
|
773 |
-
return selected_fig, plot_info_text, new_index
|
774 |
-
|
775 |
def download_current_plot(all_figures, current_index):
|
776 |
"""
|
777 |
Descarga la figura actual como PNG.
|
@@ -781,12 +704,12 @@ def download_current_plot(all_figures, current_index):
|
|
781 |
fig = all_figures[current_index]
|
782 |
img_bytes = rsm.save_fig_to_bytes(fig)
|
783 |
filename = f"Grafico_RSM_{current_index + 1}.png"
|
784 |
-
|
785 |
# Crear un archivo temporal
|
786 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
|
787 |
temp_file.write(img_bytes)
|
788 |
temp_path = temp_file.name
|
789 |
-
|
790 |
return temp_path # Retornar solo la ruta
|
791 |
|
792 |
def download_all_plots_zip():
|
@@ -797,8 +720,6 @@ def download_all_plots_zip():
|
|
797 |
return None
|
798 |
zip_path = rsm.save_figures_to_zip()
|
799 |
if zip_path:
|
800 |
-
filename = f"Graficos_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.zip"
|
801 |
-
# Gradio no permite renombrar directamente, por lo que retornamos la ruta del archivo
|
802 |
return zip_path
|
803 |
return None
|
804 |
|
@@ -810,16 +731,16 @@ def download_all_tables_excel():
|
|
810 |
return None
|
811 |
excel_path = rsm.save_tables_to_excel()
|
812 |
if excel_path:
|
813 |
-
filename = f"Tablas_RSM_{datetime.now().strftime('%Y%m%d_%H%M%S')}.xlsx"
|
814 |
-
# Gradio no permite renombrar directamente, por lo que retornamos la ruta del archivo
|
815 |
return excel_path
|
816 |
return None
|
817 |
|
818 |
-
def exportar_word(
|
819 |
"""
|
820 |
Función para exportar las tablas a un documento de Word.
|
821 |
"""
|
822 |
-
|
|
|
|
|
823 |
if word_path and os.path.exists(word_path):
|
824 |
return word_path
|
825 |
return None
|
@@ -828,19 +749,32 @@ def exportar_word(rsm_instance, tables_dict):
|
|
828 |
|
829 |
def create_gradio_interface():
|
830 |
with gr.Blocks() as demo:
|
831 |
-
gr.Markdown("# Optimización de la
|
832 |
-
|
833 |
with gr.Row():
|
834 |
with gr.Column():
|
835 |
gr.Markdown("## Configuración del Diseño")
|
836 |
-
|
837 |
-
|
838 |
-
|
839 |
-
|
840 |
-
|
841 |
-
|
842 |
-
|
843 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
844 |
2,1,-1,0,177.557
|
845 |
3,-1,1,0,127.261
|
846 |
4,1,1,0,147.573
|
@@ -854,13 +788,14 @@ def create_gradio_interface():
|
|
854 |
12,0,1,1,148.621
|
855 |
13,0,0,0,278.951
|
856 |
14,0,0,0,297.238
|
857 |
-
15,0,0,0,280.896"""
|
|
|
858 |
load_button = gr.Button("Cargar Datos")
|
859 |
-
|
860 |
with gr.Column():
|
861 |
gr.Markdown("## Datos Cargados")
|
862 |
data_output = gr.Dataframe(label="Tabla de Datos", interactive=False)
|
863 |
-
|
864 |
# Sección de análisis visible solo después de cargar los datos
|
865 |
with gr.Row(visible=False) as analysis_row:
|
866 |
with gr.Column():
|
@@ -873,41 +808,52 @@ def create_gradio_interface():
|
|
873 |
pareto_simplificado_output = gr.Plot()
|
874 |
gr.Markdown("**Ecuación del Modelo Simplificado**")
|
875 |
equation_output = gr.HTML()
|
|
|
876 |
optimization_table_output = gr.Dataframe(label="Tabla de Optimización", interactive=False)
|
|
|
877 |
prediction_table_output = gr.Dataframe(label="Tabla de Predicciones", interactive=False)
|
|
|
878 |
contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución", interactive=False)
|
|
|
879 |
anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada", interactive=False)
|
880 |
gr.Markdown("## Descargar Todas las Tablas")
|
881 |
-
download_excel_button = gr.
|
882 |
-
download_word_button = gr.
|
883 |
-
|
884 |
with gr.Column():
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
885 |
gr.Markdown("## Generar Gráficos de Superficie de Respuesta")
|
886 |
-
fixed_variable_input = gr.Dropdown(label="Variable Fija", choices=["Glucosa", "Extracto_de_Levadura", "Triptofano"], value="Glucosa")
|
887 |
-
fixed_level_input = gr.Slider(label="Nivel de Variable Fija", minimum=-1, maximum=1, step=0.01, value=0.0)
|
888 |
plot_button = gr.Button("Generar Gráficos")
|
889 |
with gr.Row():
|
890 |
left_button = gr.Button("<")
|
891 |
right_button = gr.Button(">")
|
892 |
rsm_plot_output = gr.Plot()
|
893 |
-
plot_info = gr.Textbox(label="Información del Gráfico", value="Gráfico 1 de
|
894 |
with gr.Row():
|
895 |
-
download_plot_button = gr.
|
896 |
-
download_all_plots_button = gr.
|
897 |
current_index_state = gr.State(0) # Estado para el índice actual
|
898 |
all_figures_state = gr.State([]) # Estado para todas las figuras
|
899 |
-
|
900 |
-
#
|
901 |
load_button.click(
|
902 |
load_data,
|
903 |
-
inputs=[
|
904 |
-
outputs=[data_output,
|
905 |
)
|
906 |
-
|
907 |
# Ajustar modelo y optimizar
|
908 |
fit_button.click(
|
909 |
fit_and_optimize_model,
|
910 |
-
inputs=[],
|
911 |
outputs=[
|
912 |
model_completo_output,
|
913 |
pareto_completo_output,
|
@@ -918,23 +864,36 @@ def create_gradio_interface():
|
|
918 |
prediction_table_output,
|
919 |
contribution_table_output,
|
920 |
anova_table_output,
|
921 |
-
download_all_plots_button,
|
922 |
-
download_excel_button
|
923 |
]
|
924 |
)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
925 |
|
|
|
|
|
|
|
|
|
|
|
|
|
926 |
# Generar y mostrar los gráficos
|
927 |
plot_button.click(
|
928 |
-
lambda
|
929 |
-
|
930 |
-
|
931 |
0,
|
932 |
-
|
933 |
),
|
934 |
-
inputs=[
|
935 |
outputs=[rsm_plot_output, plot_info, current_index_state, all_figures_state]
|
936 |
)
|
937 |
-
|
938 |
# Navegación de gráficos
|
939 |
left_button.click(
|
940 |
lambda current_index, all_figures: navigate_plot('left', current_index, all_figures),
|
@@ -946,50 +905,116 @@ def create_gradio_interface():
|
|
946 |
inputs=[current_index_state, all_figures_state],
|
947 |
outputs=[rsm_plot_output, plot_info, current_index_state]
|
948 |
)
|
949 |
-
|
950 |
# Descargar gráfico actual
|
951 |
download_plot_button.click(
|
952 |
download_current_plot,
|
953 |
inputs=[all_figures_state, current_index_state],
|
954 |
outputs=download_plot_button
|
955 |
)
|
956 |
-
|
957 |
# Descargar todos los gráficos en ZIP
|
958 |
download_all_plots_button.click(
|
959 |
download_all_plots_zip,
|
960 |
inputs=[],
|
961 |
outputs=download_all_plots_button
|
962 |
)
|
963 |
-
|
964 |
-
# Descargar todas las tablas en Excel y Word
|
965 |
-
download_excel_button.click(
|
966 |
-
fn=lambda: download_all_tables_excel(),
|
967 |
-
inputs=[],
|
968 |
-
outputs=download_excel_button
|
969 |
-
)
|
970 |
-
|
971 |
-
download_word_button.click(
|
972 |
-
fn=lambda: exportar_word(rsm, rsm.get_all_tables()),
|
973 |
-
inputs=[],
|
974 |
-
outputs=download_word_button
|
975 |
-
)
|
976 |
-
|
977 |
# Ejemplo de uso
|
978 |
gr.Markdown("## Ejemplo de uso")
|
979 |
gr.Markdown("""
|
980 |
-
1.
|
981 |
-
|
982 |
-
|
983 |
-
|
984 |
-
|
985 |
-
|
986 |
-
|
987 |
-
|
988 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
989 |
""")
|
990 |
|
991 |
return demo
|
992 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
993 |
# --- Función Principal ---
|
994 |
|
995 |
def main():
|
@@ -997,4 +1022,4 @@ def main():
|
|
997 |
interface.launch(share=True)
|
998 |
|
999 |
if __name__ == "__main__":
|
1000 |
-
main()
|
|
|
5 |
import plotly.graph_objects as go
|
6 |
from scipy.optimize import minimize
|
7 |
import plotly.express as px
|
8 |
+
from scipy.stats import f
|
9 |
import gradio as gr
|
10 |
import io
|
11 |
import zipfile
|
|
|
14 |
import docx
|
15 |
from docx.shared import Inches, Pt
|
16 |
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
|
|
|
17 |
import os
|
18 |
+
from pyDOE import bbdesign, ccdesign
|
19 |
|
20 |
+
# --- Clase RSM_ExperimentalDesign ---
|
21 |
+
class RSM_ExperimentalDesign:
|
22 |
+
def __init__(self, data, design_type, factor_names, y_name, factor_levels):
|
23 |
"""
|
24 |
+
Inicializa la clase con los datos del diseño experimental.
|
25 |
+
|
26 |
+
Args:
|
27 |
+
data (pd.DataFrame): Datos del experimento.
|
28 |
+
design_type (str): Tipo de diseño ('Box-Behnken' o 'Central Compuesto').
|
29 |
+
factor_names (list): Lista de nombres de los factores.
|
30 |
+
y_name (str): Nombre de la variable dependiente.
|
31 |
+
factor_levels (dict): Diccionario con los niveles de cada factor.
|
32 |
"""
|
33 |
self.data = data.copy()
|
34 |
+
self.design_type = design_type
|
35 |
+
self.factor_names = factor_names
|
36 |
+
self.y_name = y_name
|
37 |
+
self.factor_levels = factor_levels
|
38 |
self.model = None
|
39 |
self.model_simplified = None
|
40 |
self.optimized_results = None
|
41 |
self.optimal_levels = None
|
42 |
+
self.all_figures = []
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
43 |
|
44 |
+
def generate_design(self):
|
45 |
"""
|
46 |
+
Genera el diseño experimental basado en el tipo especificado y asigna los niveles naturales.
|
47 |
"""
|
48 |
+
num_factors = len(self.factor_names)
|
49 |
+
if self.design_type == 'Box-Behnken':
|
50 |
+
design = bbdesign(num_factors)
|
51 |
+
elif self.design_type == 'Central Compuesto':
|
52 |
+
design = ccdesign(num_factors, center=(3, 3)) # Puedes ajustar los puntos centrales si lo deseas
|
|
|
53 |
else:
|
54 |
+
raise ValueError("Tipo de diseño no soportado. Elige 'Box-Behnken' o 'Central Compuesto'.")
|
55 |
+
|
56 |
+
# Asignar niveles naturales a las variables
|
57 |
+
for i, factor in enumerate(self.factor_names):
|
58 |
+
self.data[factor] = self.coded_to_natural(design[:, i], factor)
|
59 |
+
|
60 |
+
return self.data
|
61 |
+
|
62 |
+
def coded_to_natural(self, coded_value, variable_name):
|
63 |
+
"""Convierte un valor codificado a su valor natural."""
|
64 |
+
levels = self.factor_levels[variable_name]
|
65 |
+
if len(levels) != 3:
|
66 |
+
raise ValueError(f"Se requieren exactamente 3 niveles para el factor '{variable_name}'.")
|
67 |
+
return levels[0] + (coded_value + 1) * (levels[-1] - levels[0]) / 2
|
68 |
+
|
69 |
+
def natural_to_coded(self, natural_value, variable_name):
|
70 |
+
"""Convierte un valor natural a su valor codificado."""
|
71 |
+
levels = self.factor_levels[variable_name]
|
72 |
+
return -1 + 2 * (natural_value - levels[0]) / (levels[-1] - levels[0])
|
73 |
|
74 |
def fit_model(self):
|
75 |
"""
|
76 |
Ajusta el modelo de segundo orden completo a los datos.
|
77 |
"""
|
78 |
+
terms = self.factor_names.copy()
|
79 |
+
# Términos cuadráticos
|
80 |
+
terms += [f'I({var}**2)' for var in self.factor_names]
|
81 |
+
# Términos de interacción
|
82 |
+
for i in range(len(self.factor_names)):
|
83 |
+
for j in range(i+1, len(self.factor_names)):
|
84 |
+
terms.append(f'{self.factor_names[i]}:{self.factor_names[j]}')
|
85 |
+
|
86 |
+
formula = f'{self.y_name} ~ ' + ' + '.join(terms)
|
87 |
self.model = smf.ols(formula, data=self.data).fit()
|
88 |
print("Modelo Completo:")
|
89 |
print(self.model.summary())
|
90 |
return self.model, self.pareto_chart(self.model, "Pareto - Modelo Completo")
|
91 |
|
92 |
+
def fit_simplified_model(self, selected_factors):
|
93 |
"""
|
94 |
+
Ajusta el modelo simplificado basado en los factores seleccionados.
|
95 |
+
|
96 |
+
Args:
|
97 |
+
selected_factors (list): Lista de factores a incluir en el modelo.
|
98 |
"""
|
99 |
+
terms = selected_factors.copy()
|
100 |
+
# Términos cuadráticos
|
101 |
+
terms += [f'I({var}**2)' for var in selected_factors]
|
102 |
+
formula = f'{self.y_name} ~ ' + ' + '.join(terms)
|
103 |
self.model_simplified = smf.ols(formula, data=self.data).fit()
|
104 |
print("\nModelo Simplificado:")
|
105 |
print(self.model_simplified.summary())
|
|
|
112 |
if self.model_simplified is None:
|
113 |
print("Error: Ajusta el modelo simplificado primero.")
|
114 |
return
|
115 |
+
|
116 |
def objective_function(x):
|
117 |
+
input_data = {var: [x[i]] for i, var in enumerate(self.selected_factors)}
|
118 |
+
return -self.model_simplified.predict(pd.DataFrame(input_data)).values[0]
|
119 |
+
|
120 |
+
# Definir límites codificados para cada factor
|
121 |
+
bounds = [(-1, 1) for _ in self.selected_factors]
|
122 |
+
x0 = [0] * len(self.selected_factors)
|
123 |
+
|
|
|
|
|
124 |
self.optimized_results = minimize(objective_function, x0, method=method, bounds=bounds)
|
125 |
self.optimal_levels = self.optimized_results.x
|
126 |
+
|
127 |
# Convertir niveles óptimos de codificados a naturales
|
128 |
optimal_levels_natural = [
|
129 |
+
self.coded_to_natural(self.optimal_levels[i], self.selected_factors[i])
|
130 |
+
for i in range(len(self.selected_factors))
|
|
|
131 |
]
|
132 |
+
|
133 |
# Crear la tabla de optimización
|
134 |
optimization_table = pd.DataFrame({
|
135 |
+
'Variable': self.selected_factors,
|
136 |
'Nivel Óptimo (Natural)': optimal_levels_natural,
|
137 |
'Nivel Óptimo (Codificado)': self.optimal_levels
|
138 |
})
|
139 |
+
|
140 |
+
return optimization_table.round(3)
|
141 |
|
142 |
def plot_rsm_individual(self, fixed_variable, fixed_level):
|
143 |
"""
|
|
|
147 |
print("Error: Ajusta el modelo simplificado primero.")
|
148 |
return None
|
149 |
|
150 |
+
varying_variables = [var for var in self.selected_factors if var != fixed_variable]
|
151 |
+
if len(varying_variables) < 2:
|
152 |
+
print("Se requieren al menos dos variables para generar un gráfico de superficie.")
|
153 |
+
return None
|
154 |
|
155 |
+
var1, var2 = varying_variables[:2] # Solo tomar las dos primeras variables para el gráfico
|
156 |
+
|
157 |
+
# Determinar los niveles naturales para las variables que varían
|
158 |
+
x_natural_levels = self.factor_levels[var1]
|
159 |
+
y_natural_levels = self.factor_levels[var2]
|
160 |
|
161 |
# Crear una malla de puntos para las variables que varían (en unidades naturales)
|
162 |
x_range_natural = np.linspace(x_natural_levels[0], x_natural_levels[-1], 100)
|
|
|
164 |
x_grid_natural, y_grid_natural = np.meshgrid(x_range_natural, y_range_natural)
|
165 |
|
166 |
# Convertir la malla de variables naturales a codificadas
|
167 |
+
x_grid_coded = self.natural_to_coded(x_grid_natural, var1)
|
168 |
+
y_grid_coded = self.natural_to_coded(y_grid_natural, var2)
|
169 |
|
170 |
# Crear un DataFrame para la predicción con variables codificadas
|
171 |
prediction_data = pd.DataFrame({
|
172 |
+
var1: x_grid_coded.flatten(),
|
173 |
+
var2: y_grid_coded.flatten(),
|
174 |
})
|
175 |
+
|
176 |
+
# Asignar valores codificados a las otras variables fijas
|
177 |
+
for var in self.selected_factors:
|
178 |
+
if var not in [var1, var2]:
|
179 |
+
prediction_data[var] = self.natural_to_coded(fixed_level, var)
|
180 |
|
181 |
# Calcular los valores predichos
|
182 |
z_pred = self.model_simplified.predict(prediction_data).values.reshape(x_grid_coded.shape)
|
183 |
|
184 |
+
# Crear el gráfico de superficie
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
185 |
fig = go.Figure(data=[go.Surface(z=z_pred, x=x_grid_natural, y=y_grid_natural, colorscale='Viridis', opacity=0.7, showscale=True)])
|
186 |
|
187 |
+
# Añadir líneas de cuadrícula
|
|
|
188 |
for i in range(x_grid_natural.shape[0]):
|
189 |
fig.add_trace(go.Scatter3d(
|
190 |
x=x_grid_natural[i, :],
|
|
|
195 |
showlegend=False,
|
196 |
hoverinfo='skip'
|
197 |
))
|
|
|
198 |
for j in range(x_grid_natural.shape[1]):
|
199 |
fig.add_trace(go.Scatter3d(
|
200 |
x=x_grid_natural[:, j],
|
|
|
206 |
hoverinfo='skip'
|
207 |
))
|
208 |
|
209 |
+
# Añadir los puntos de los experimentos
|
210 |
+
experiments_data = self.data.copy()
|
211 |
+
experiments_data['Predicho'] = self.model_simplified.predict(self.data[self.selected_factors])
|
212 |
colors = px.colors.qualitative.Safe
|
213 |
point_labels = [f"{row[self.y_name]:.3f}" for _, row in experiments_data.iterrows()]
|
214 |
|
215 |
fig.add_trace(go.Scatter3d(
|
216 |
+
x=experiments_data[var1],
|
217 |
+
y=experiments_data[var2],
|
218 |
+
z=experiments_data[self.y_name],
|
219 |
mode='markers+text',
|
220 |
+
marker=dict(size=4, color=colors[:len(experiments_data)]),
|
221 |
text=point_labels,
|
222 |
textposition='top center',
|
223 |
name='Experimentos'
|
|
|
226 |
# Añadir etiquetas y título con variables naturales
|
227 |
fig.update_layout(
|
228 |
scene=dict(
|
229 |
+
xaxis_title=f"{var1} ({self.get_units(var1)})",
|
230 |
+
yaxis_title=f"{var2} ({self.get_units(var2)})",
|
231 |
zaxis_title=self.y_name,
|
232 |
),
|
233 |
+
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>",
|
234 |
height=800,
|
235 |
width=1000,
|
236 |
showlegend=True
|
|
|
245 |
units = {
|
246 |
'Glucosa': 'g/L',
|
247 |
'Extracto_de_Levadura': 'g/L',
|
248 |
+
'Triptófano': 'g/L',
|
249 |
+
'AIA_ppm': 'ppm',
|
250 |
+
# Agrega más unidades según tus variables
|
251 |
}
|
252 |
return units.get(variable_name, '')
|
253 |
|
|
|
263 |
self.all_figures = [] # Resetear la lista de figuras
|
264 |
|
265 |
# Niveles naturales para graficar
|
266 |
+
levels_to_plot_natural = self.factor_levels
|
|
|
|
|
|
|
|
|
267 |
|
268 |
# Generar y almacenar gráficos individuales
|
269 |
+
for fixed_variable in self.selected_factors:
|
270 |
+
for level in self.factor_levels[fixed_variable]:
|
271 |
fig = self.plot_rsm_individual(fixed_variable, level)
|
272 |
if fig is not None:
|
273 |
self.all_figures.append(fig)
|
274 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
275 |
def pareto_chart(self, model, title):
|
276 |
"""
|
277 |
Genera un diagrama de Pareto para los efectos usando estadísticos F,
|
278 |
incluyendo la línea de significancia.
|
279 |
"""
|
280 |
# Calcular los estadísticos F para cada término
|
|
|
281 |
fvalues = model.tvalues[1:]**2 # Excluir la Intercept y convertir t a F
|
282 |
abs_fvalues = np.abs(fvalues)
|
283 |
sorted_idx = np.argsort(abs_fvalues)[::-1]
|
|
|
320 |
|
321 |
for term, coef in coefficients.items():
|
322 |
if term != 'Intercept':
|
323 |
+
if term.startswith('I('):
|
324 |
+
equation += f" + {coef:.3f}*{term[2:-1]}"
|
325 |
+
elif ':' in term:
|
326 |
+
equation += f" + {coef:.3f}*{term}"
|
327 |
+
else:
|
328 |
+
equation += f" + {coef:.3f}*{term}"
|
329 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
330 |
return equation
|
331 |
+
|
332 |
def generate_prediction_table(self):
|
333 |
"""
|
334 |
Genera una tabla con los valores actuales, predichos y residuales.
|
|
|
337 |
print("Error: Ajusta el modelo simplificado primero.")
|
338 |
return None
|
339 |
|
340 |
+
self.data['Predicho'] = self.model_simplified.predict(self.data[self.selected_factors])
|
341 |
self.data['Residual'] = self.data[self.y_name] - self.data['Predicho']
|
342 |
|
343 |
return self.data[[self.y_name, 'Predicho', 'Residual']].round(3)
|
|
|
369 |
|
370 |
# Calcular estadísticos F y porcentaje de contribución para cada factor
|
371 |
ms_error = anova_table.loc['Residual', 'sum_sq'] / anova_table.loc['Residual', 'df']
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
372 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
373 |
# Agregar filas para cada término del modelo
|
374 |
for index, row in anova_table.iterrows():
|
375 |
if index != 'Residual':
|
376 |
factor_name = index
|
377 |
+
if factor_name.startswith('I('):
|
378 |
+
factor_name = factor_name[2:-1] # Quitar 'I(' y ')'
|
|
|
|
|
|
|
|
|
|
|
379 |
ss_factor = row['sum_sq']
|
380 |
df_factor = row['df']
|
381 |
ms_factor = ss_factor / df_factor
|
|
|
394 |
})], ignore_index=True)
|
395 |
|
396 |
# Agregar fila para Cor Total
|
|
|
|
|
|
|
397 |
contribution_table = pd.concat([contribution_table, pd.DataFrame({
|
398 |
'Fuente de Variación': ['Cor Total'],
|
399 |
+
'Suma de Cuadrados': [ss_total],
|
400 |
+
'Grados de Libertad': [len(self.data) - 1],
|
401 |
'Cuadrado Medio': [np.nan],
|
402 |
'F': [np.nan],
|
403 |
'Valor p': [np.nan],
|
|
|
414 |
print("Error: Ajusta el modelo simplificado primero.")
|
415 |
return None
|
416 |
|
417 |
+
# ANOVA del modelo simplificado
|
418 |
+
anova_reduced = sm.stats.anova_lm(self.model_simplified, typ=2)
|
|
|
|
|
|
|
|
|
|
|
|
|
419 |
|
420 |
+
# Suma de cuadrados total
|
421 |
ss_total = np.sum((self.data[self.y_name] - self.data[self.y_name].mean())**2)
|
422 |
|
423 |
+
# Suma de cuadrados de la regresión
|
424 |
+
ss_regression = anova_reduced['sum_sq'].sum()
|
|
|
|
|
|
|
425 |
|
426 |
+
# Grados de libertad de la regresión
|
427 |
+
df_regression = anova_reduced['df'].sum()
|
428 |
|
429 |
+
# Suma de cuadrados del error residual
|
430 |
+
ss_residual = anova_reduced.loc['Residual', 'sum_sq']
|
431 |
+
df_residual = anova_reduced.loc['Residual', 'df']
|
432 |
|
433 |
+
# Suma de cuadrados del error puro (si hay réplicas)
|
434 |
+
duplicates = self.data.duplicated(subset=self.selected_factors, keep=False)
|
435 |
+
if duplicates.any():
|
436 |
+
ss_pure_error = self.data[duplicates].groupby(self.selected_factors)[self.y_name].var().sum() * self.data[duplicates].groupby(self.selected_factors).ngroups()
|
437 |
+
df_pure_error = self.data[duplicates].shape[0] - self.data[duplicates].groupby(self.selected_factors).ngroups()
|
438 |
else:
|
439 |
ss_pure_error = np.nan
|
440 |
df_pure_error = np.nan
|
441 |
|
442 |
+
# Suma de cuadrados de la falta de ajuste
|
443 |
ss_lack_of_fit = ss_residual - ss_pure_error if not np.isnan(ss_pure_error) else np.nan
|
444 |
df_lack_of_fit = df_residual - df_pure_error if not np.isnan(df_pure_error) else np.nan
|
445 |
|
446 |
+
# Cuadrados medios
|
447 |
ms_regression = ss_regression / df_regression
|
448 |
ms_residual = ss_residual / df_residual
|
449 |
ms_lack_of_fit = ss_lack_of_fit / df_lack_of_fit if not np.isnan(ss_lack_of_fit) else np.nan
|
450 |
ms_pure_error = ss_pure_error / df_pure_error if not np.isnan(ss_pure_error) else np.nan
|
451 |
|
452 |
+
# Estadísticos F y valores p
|
453 |
f_regression = ms_regression / ms_residual
|
454 |
p_regression = 1 - f.cdf(f_regression, df_regression, df_residual)
|
455 |
+
|
456 |
f_lack_of_fit = ms_lack_of_fit / ms_pure_error if not np.isnan(ms_lack_of_fit) else np.nan
|
457 |
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
|
458 |
|
459 |
+
# Crear la tabla ANOVA detallada
|
460 |
detailed_anova_table = pd.DataFrame({
|
461 |
'Fuente de Variación': ['Regresión', 'Residual', 'Falta de Ajuste', 'Error Puro', 'Total'],
|
462 |
'Suma de Cuadrados': [ss_regression, ss_residual, ss_lack_of_fit, ss_pure_error, ss_total],
|
463 |
+
'Grados de Libertad': [df_regression, df_residual, df_lack_of_fit, df_pure_error, len(self.data) - 1],
|
464 |
'Cuadrado Medio': [ms_regression, ms_residual, ms_lack_of_fit, ms_pure_error, np.nan],
|
465 |
'F': [f_regression, np.nan, f_lack_of_fit, np.nan, np.nan],
|
466 |
'Valor p': [p_regression, np.nan, p_lack_of_fit, np.nan, np.nan]
|
467 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
468 |
|
469 |
# Reorganizar las filas y resetear el índice
|
470 |
+
detailed_anova_table = detailed_anova_table.reindex([0, 1, 2, 3, 4]).reset_index(drop=True)
|
471 |
|
472 |
return detailed_anova_table.round(3)
|
473 |
|
|
|
477 |
"""
|
478 |
prediction_table = self.generate_prediction_table()
|
479 |
contribution_table = self.calculate_contribution_percentage()
|
480 |
+
anova_table = self.calculate_detailed_anova()
|
481 |
|
482 |
return {
|
483 |
'Predicciones': prediction_table,
|
484 |
'% Contribución': contribution_table,
|
485 |
+
'ANOVA Detallada': anova_table
|
486 |
}
|
487 |
|
488 |
def save_figures_to_zip(self):
|
|
|
601 |
|
602 |
# --- Funciones para la Interfaz de Gradio ---
|
603 |
|
604 |
+
def load_data(design_type, factor_names, factor_levels, y_name, data_str):
|
605 |
"""
|
606 |
+
Carga los datos del diseño experimental desde las entradas y crea la instancia de RSM_ExperimentalDesign.
|
607 |
"""
|
608 |
try:
|
609 |
+
# Parsear nombres de factores
|
610 |
+
factor_names = [fn.strip() for fn in factor_names.split(',')]
|
611 |
+
num_factors = len(factor_names)
|
612 |
+
|
613 |
+
# Parsear niveles de factores
|
614 |
+
factor_levels_dict = {}
|
615 |
+
levels = factor_levels.split(';')
|
616 |
+
if len(levels) != num_factors:
|
617 |
+
raise ValueError(f"Se esperaban {num_factors} conjuntos de niveles separados por ';'.")
|
618 |
+
for i, level_str in enumerate(levels):
|
619 |
+
level_values = [float(x.strip()) for x in level_str.split(',')]
|
620 |
+
if len(level_values) != 3:
|
621 |
+
raise ValueError(f"El factor '{factor_names[i]}' requiere exactamente 3 niveles separados por comas.")
|
622 |
+
factor_levels_dict[factor_names[i]] = level_values
|
623 |
|
624 |
# Crear DataFrame a partir de la cadena de datos
|
625 |
data_list = [row.split(',') for row in data_str.strip().split('\n')]
|
626 |
+
column_names = ['Exp.'] + factor_names + [y_name]
|
627 |
data = pd.DataFrame(data_list, columns=column_names)
|
628 |
data = data.apply(pd.to_numeric, errors='coerce') # Convertir a numérico
|
629 |
|
630 |
+
# Crear la instancia de RSM_ExperimentalDesign
|
|
|
|
|
|
|
|
|
631 |
global rsm
|
632 |
+
rsm = RSM_ExperimentalDesign(
|
633 |
+
data=data,
|
634 |
+
design_type=design_type,
|
635 |
+
factor_names=factor_names,
|
636 |
+
y_name=y_name,
|
637 |
+
factor_levels=factor_levels_dict
|
638 |
+
)
|
639 |
+
|
640 |
+
# Generar el diseño
|
641 |
+
rsm.generate_design()
|
642 |
+
|
643 |
+
return data.round(3), gr.update(visible=True), factor_names
|
644 |
|
|
|
|
|
645 |
except Exception as e:
|
646 |
# Mostrar mensaje de error
|
647 |
error_message = f"Error al cargar los datos: {str(e)}"
|
648 |
print(error_message)
|
649 |
+
return None, gr.update(visible=False), []
|
650 |
|
651 |
+
def fit_and_optimize_model(selected_factors):
|
652 |
if 'rsm' not in globals():
|
653 |
return [None]*11 # Ajustar el número de outputs
|
654 |
|
655 |
+
if not selected_factors:
|
656 |
+
return [None]*11 # No se han seleccionado factores
|
657 |
+
|
658 |
# Ajustar modelos y optimizar
|
659 |
model_completo, pareto_completo = rsm.fit_model()
|
660 |
+
rsm.selected_factors = selected_factors
|
661 |
+
model_simplificado, pareto_simplificado = rsm.fit_simplified_model(selected_factors)
|
662 |
optimization_table = rsm.optimize()
|
663 |
equation = rsm.get_simplified_equation()
|
664 |
prediction_table = rsm.generate_prediction_table()
|
665 |
contribution_table = rsm.calculate_contribution_percentage()
|
666 |
anova_table = rsm.calculate_detailed_anova()
|
667 |
+
|
668 |
# Generar todas las figuras y almacenarlas
|
669 |
rsm.generate_all_plots()
|
670 |
+
|
671 |
# Formatear la ecuación para que se vea mejor en Markdown
|
672 |
+
if equation:
|
673 |
+
equation_formatted = equation.replace(" + ", "<br>+ ").replace(" ** ", "^").replace("*", " × ")
|
674 |
+
equation_formatted = f"### Ecuación del Modelo Simplificado:<br>{equation_formatted}"
|
675 |
+
else:
|
676 |
+
equation_formatted = "No se pudo generar la ecuación del modelo simplificado."
|
677 |
+
|
678 |
# Guardar las tablas en Excel temporal
|
679 |
excel_path = rsm.save_tables_to_excel()
|
680 |
|
|
|
695 |
excel_path # Ruta del Excel de tablas
|
696 |
)
|
697 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
698 |
def download_current_plot(all_figures, current_index):
|
699 |
"""
|
700 |
Descarga la figura actual como PNG.
|
|
|
704 |
fig = all_figures[current_index]
|
705 |
img_bytes = rsm.save_fig_to_bytes(fig)
|
706 |
filename = f"Grafico_RSM_{current_index + 1}.png"
|
707 |
+
|
708 |
# Crear un archivo temporal
|
709 |
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
|
710 |
temp_file.write(img_bytes)
|
711 |
temp_path = temp_file.name
|
712 |
+
|
713 |
return temp_path # Retornar solo la ruta
|
714 |
|
715 |
def download_all_plots_zip():
|
|
|
720 |
return None
|
721 |
zip_path = rsm.save_figures_to_zip()
|
722 |
if zip_path:
|
|
|
|
|
723 |
return zip_path
|
724 |
return None
|
725 |
|
|
|
731 |
return None
|
732 |
excel_path = rsm.save_tables_to_excel()
|
733 |
if excel_path:
|
|
|
|
|
734 |
return excel_path
|
735 |
return None
|
736 |
|
737 |
+
def exportar_word(tables_dict):
|
738 |
"""
|
739 |
Función para exportar las tablas a un documento de Word.
|
740 |
"""
|
741 |
+
if 'rsm' not in globals():
|
742 |
+
return None
|
743 |
+
word_path = rsm.export_tables_to_word(tables_dict)
|
744 |
if word_path and os.path.exists(word_path):
|
745 |
return word_path
|
746 |
return None
|
|
|
749 |
|
750 |
def create_gradio_interface():
|
751 |
with gr.Blocks() as demo:
|
752 |
+
gr.Markdown("# Optimización de la Producción de AIA usando RSM")
|
753 |
+
|
754 |
with gr.Row():
|
755 |
with gr.Column():
|
756 |
gr.Markdown("## Configuración del Diseño")
|
757 |
+
design_type_input = gr.Dropdown(
|
758 |
+
label="Tipo de Diseño",
|
759 |
+
choices=["Box-Behnken", "Central Compuesto"],
|
760 |
+
value="Box-Behnken"
|
761 |
+
)
|
762 |
+
factor_names_input = gr.Textbox(
|
763 |
+
label="Nombres de los Factores (separados por comas)",
|
764 |
+
value="Glucosa, Extracto_de_Levadura, Triptófano"
|
765 |
+
)
|
766 |
+
factor_levels_input = gr.Textbox(
|
767 |
+
label="Niveles de los Factores (cada conjunto separado por ';' y niveles separados por comas)",
|
768 |
+
value="1, 3.5, 5.5; 0.03, 0.2, 0.3; 0.4, 0.65, 0.9"
|
769 |
+
)
|
770 |
+
y_name_input = gr.Textbox(
|
771 |
+
label="Nombre de la Variable Dependiente (ej. AIA_ppm)",
|
772 |
+
value="AIA_ppm"
|
773 |
+
)
|
774 |
+
data_input = gr.Textbox(
|
775 |
+
label="Datos del Experimento (formato CSV)",
|
776 |
+
lines=10,
|
777 |
+
value="""1,-1,-1,0,166.594
|
778 |
2,1,-1,0,177.557
|
779 |
3,-1,1,0,127.261
|
780 |
4,1,1,0,147.573
|
|
|
788 |
12,0,1,1,148.621
|
789 |
13,0,0,0,278.951
|
790 |
14,0,0,0,297.238
|
791 |
+
15,0,0,0,280.896"""
|
792 |
+
)
|
793 |
load_button = gr.Button("Cargar Datos")
|
794 |
+
|
795 |
with gr.Column():
|
796 |
gr.Markdown("## Datos Cargados")
|
797 |
data_output = gr.Dataframe(label="Tabla de Datos", interactive=False)
|
798 |
+
|
799 |
# Sección de análisis visible solo después de cargar los datos
|
800 |
with gr.Row(visible=False) as analysis_row:
|
801 |
with gr.Column():
|
|
|
808 |
pareto_simplificado_output = gr.Plot()
|
809 |
gr.Markdown("**Ecuación del Modelo Simplificado**")
|
810 |
equation_output = gr.HTML()
|
811 |
+
gr.Markdown("**Tabla de Optimización**")
|
812 |
optimization_table_output = gr.Dataframe(label="Tabla de Optimización", interactive=False)
|
813 |
+
gr.Markdown("**Tabla de Predicciones**")
|
814 |
prediction_table_output = gr.Dataframe(label="Tabla de Predicciones", interactive=False)
|
815 |
+
gr.Markdown("**Tabla de % de Contribución**")
|
816 |
contribution_table_output = gr.Dataframe(label="Tabla de % de Contribución", interactive=False)
|
817 |
+
gr.Markdown("**Tabla ANOVA Detallada**")
|
818 |
anova_table_output = gr.Dataframe(label="Tabla ANOVA Detallada", interactive=False)
|
819 |
gr.Markdown("## Descargar Todas las Tablas")
|
820 |
+
download_excel_button = gr.File(label="Descargar Tablas en Excel", visible=False)
|
821 |
+
download_word_button = gr.File(label="Descargar Tablas en Word", visible=False)
|
822 |
+
|
823 |
with gr.Column():
|
824 |
+
gr.Markdown("## Selección de Factores para el Modelo Simplificado")
|
825 |
+
selected_factors_input = gr.CheckboxGroup(
|
826 |
+
label="Selecciona los Factores a Incluir",
|
827 |
+
choices=[], # Actualizar dinámicamente
|
828 |
+
value=[]
|
829 |
+
)
|
830 |
+
fit_button_2 = gr.Button("Aplicar Selección de Factores")
|
831 |
+
gr.Markdown("## Resultados de Optimización")
|
832 |
+
optimization_table_output_2 = gr.Dataframe(label="Tabla de Optimización", interactive=False)
|
833 |
gr.Markdown("## Generar Gráficos de Superficie de Respuesta")
|
|
|
|
|
834 |
plot_button = gr.Button("Generar Gráficos")
|
835 |
with gr.Row():
|
836 |
left_button = gr.Button("<")
|
837 |
right_button = gr.Button(">")
|
838 |
rsm_plot_output = gr.Plot()
|
839 |
+
plot_info = gr.Textbox(label="Información del Gráfico", value="Gráfico 1 de 0", interactive=False)
|
840 |
with gr.Row():
|
841 |
+
download_plot_button = gr.File(label="Descargar Gráfico Actual (PNG)", visible=False)
|
842 |
+
download_all_plots_button = gr.File(label="Descargar Todos los Gráficos (ZIP)", visible=False)
|
843 |
current_index_state = gr.State(0) # Estado para el índice actual
|
844 |
all_figures_state = gr.State([]) # Estado para todas las figuras
|
845 |
+
|
846 |
+
# Funciones de carga y ajuste
|
847 |
load_button.click(
|
848 |
load_data,
|
849 |
+
inputs=[design_type_input, factor_names_input, factor_levels_input, y_name_input, data_input],
|
850 |
+
outputs=[data_output, analysis_row, selected_factors_input]
|
851 |
)
|
852 |
+
|
853 |
# Ajustar modelo y optimizar
|
854 |
fit_button.click(
|
855 |
fit_and_optimize_model,
|
856 |
+
inputs=[selected_factors_input],
|
857 |
outputs=[
|
858 |
model_completo_output,
|
859 |
pareto_completo_output,
|
|
|
864 |
prediction_table_output,
|
865 |
contribution_table_output,
|
866 |
anova_table_output,
|
867 |
+
download_all_plots_button,
|
868 |
+
download_excel_button
|
869 |
]
|
870 |
)
|
871 |
+
|
872 |
+
# Descargar todas las tablas en Excel y Word
|
873 |
+
download_excel_button.click(
|
874 |
+
fn=lambda: download_all_tables_excel(),
|
875 |
+
inputs=[],
|
876 |
+
outputs=download_excel_button
|
877 |
+
)
|
878 |
|
879 |
+
download_word_button.click(
|
880 |
+
fn=lambda: exportar_word(rsm.get_all_tables()),
|
881 |
+
inputs=[],
|
882 |
+
outputs=download_word_button
|
883 |
+
)
|
884 |
+
|
885 |
# Generar y mostrar los gráficos
|
886 |
plot_button.click(
|
887 |
+
lambda: (
|
888 |
+
None, # Placeholder, se actualizará después
|
889 |
+
"No hay gráficos disponibles.",
|
890 |
0,
|
891 |
+
[]
|
892 |
),
|
893 |
+
inputs=[],
|
894 |
outputs=[rsm_plot_output, plot_info, current_index_state, all_figures_state]
|
895 |
)
|
896 |
+
|
897 |
# Navegación de gráficos
|
898 |
left_button.click(
|
899 |
lambda current_index, all_figures: navigate_plot('left', current_index, all_figures),
|
|
|
905 |
inputs=[current_index_state, all_figures_state],
|
906 |
outputs=[rsm_plot_output, plot_info, current_index_state]
|
907 |
)
|
908 |
+
|
909 |
# Descargar gráfico actual
|
910 |
download_plot_button.click(
|
911 |
download_current_plot,
|
912 |
inputs=[all_figures_state, current_index_state],
|
913 |
outputs=download_plot_button
|
914 |
)
|
915 |
+
|
916 |
# Descargar todos los gráficos en ZIP
|
917 |
download_all_plots_button.click(
|
918 |
download_all_plots_zip,
|
919 |
inputs=[],
|
920 |
outputs=download_all_plots_button
|
921 |
)
|
922 |
+
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
923 |
# Ejemplo de uso
|
924 |
gr.Markdown("## Ejemplo de uso")
|
925 |
gr.Markdown("""
|
926 |
+
1. **Configura el Diseño:**
|
927 |
+
- Selecciona el tipo de diseño (Box-Behnken o Central Compuesto).
|
928 |
+
- Ingresa los nombres de los factores separados por comas.
|
929 |
+
- Ingresa los niveles de cada factor separados por comas y cada conjunto de niveles por ';'.
|
930 |
+
- Especifica el nombre de la variable dependiente.
|
931 |
+
- Proporciona los datos del experimento en formato CSV.
|
932 |
+
2. **Cargar Datos:**
|
933 |
+
- Haz clic en 'Cargar Datos' para cargar y visualizar los datos.
|
934 |
+
3. **Ajustar Modelo y Optimizar:**
|
935 |
+
- Selecciona los factores que deseas incluir en el modelo simplificado.
|
936 |
+
- Haz clic en 'Ajustar Modelo y Optimizar' para ajustar los modelos y obtener los resultados.
|
937 |
+
4. **Generar Gráficos:**
|
938 |
+
- Haz clic en 'Generar Gráficos' para crear las superficies de respuesta.
|
939 |
+
- Navega entre los gráficos usando los botones '<' y '>'.
|
940 |
+
- Descarga el gráfico actual en PNG o descarga todos los gráficos en un ZIP.
|
941 |
+
5. **Descargar Tablas:**
|
942 |
+
- Descarga todas las tablas generadas en un archivo Excel o Word.
|
943 |
""")
|
944 |
|
945 |
return demo
|
946 |
|
947 |
+
def navigate_plot(direction, current_index, all_figures):
|
948 |
+
"""
|
949 |
+
Navega entre los gráficos.
|
950 |
+
"""
|
951 |
+
if not all_figures:
|
952 |
+
return None, "No hay gráficos disponibles.", current_index
|
953 |
+
|
954 |
+
if direction == 'left':
|
955 |
+
new_index = (current_index - 1) % len(all_figures)
|
956 |
+
elif direction == 'right':
|
957 |
+
new_index = (current_index + 1) % len(all_figures)
|
958 |
+
else:
|
959 |
+
new_index = current_index
|
960 |
+
|
961 |
+
selected_fig = all_figures[new_index]
|
962 |
+
plot_info_text = f"Gráfico {new_index + 1} de {len(all_figures)}"
|
963 |
+
|
964 |
+
return selected_fig, plot_info_text, new_index
|
965 |
+
|
966 |
+
# --- Funciones de descarga y exportación ---
|
967 |
+
|
968 |
+
def download_current_plot(all_figures, current_index):
|
969 |
+
"""
|
970 |
+
Descarga la figura actual como PNG.
|
971 |
+
"""
|
972 |
+
if not all_figures:
|
973 |
+
return None
|
974 |
+
fig = all_figures[current_index]
|
975 |
+
img_bytes = rsm.save_fig_to_bytes(fig)
|
976 |
+
filename = f"Grafico_RSM_{current_index + 1}.png"
|
977 |
+
|
978 |
+
# Crear un archivo temporal
|
979 |
+
with tempfile.NamedTemporaryFile(delete=False, suffix=".png") as temp_file:
|
980 |
+
temp_file.write(img_bytes)
|
981 |
+
temp_path = temp_file.name
|
982 |
+
|
983 |
+
return temp_path # Retornar solo la ruta
|
984 |
+
|
985 |
+
def download_all_plots_zip():
|
986 |
+
"""
|
987 |
+
Descarga todas las figuras en un archivo ZIP.
|
988 |
+
"""
|
989 |
+
if 'rsm' not in globals():
|
990 |
+
return None
|
991 |
+
zip_path = rsm.save_figures_to_zip()
|
992 |
+
if zip_path:
|
993 |
+
return zip_path
|
994 |
+
return None
|
995 |
+
|
996 |
+
def download_all_tables_excel():
|
997 |
+
"""
|
998 |
+
Descarga todas las tablas en un archivo Excel con múltiples hojas.
|
999 |
+
"""
|
1000 |
+
if 'rsm' not in globals():
|
1001 |
+
return None
|
1002 |
+
excel_path = rsm.save_tables_to_excel()
|
1003 |
+
if excel_path:
|
1004 |
+
return excel_path
|
1005 |
+
return None
|
1006 |
+
|
1007 |
+
def exportar_word(tables_dict):
|
1008 |
+
"""
|
1009 |
+
Función para exportar las tablas a un documento de Word.
|
1010 |
+
"""
|
1011 |
+
if not tables_dict:
|
1012 |
+
return None
|
1013 |
+
word_path = rsm.export_tables_to_word(tables_dict)
|
1014 |
+
if word_path and os.path.exists(word_path):
|
1015 |
+
return word_path
|
1016 |
+
return None
|
1017 |
+
|
1018 |
# --- Función Principal ---
|
1019 |
|
1020 |
def main():
|
|
|
1022 |
interface.launch(share=True)
|
1023 |
|
1024 |
if __name__ == "__main__":
|
1025 |
+
main()
|