calibrate / app.py
C2MV's picture
Update app.py
e84c450 verified
raw
history blame
42.8 kB
import gradio as gr
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns
from scipy import stats
from datetime import datetime
import docx
from docx.shared import Inches, Pt
from docx.enum.text import WD_PARAGRAPH_ALIGNMENT
import os
from matplotlib.colors import to_hex
def safe_color(c):
if isinstance(c, str):
c = c.strip()
if c.lower().startswith('rgba('):
# Extraer valores numéricos del string rgba
vals = c.strip('rgba()').split(',')
vals = [v.strip() for v in vals]
if len(vals) == 4:
r, g, b, a = [float(x) for x in vals]
# Los valores en rgba(...) parecen ser mayores a 1, interpretamos que están en escala 0-255.
# Convertimos a rango [0,1]:
r /= 255.0
g /= 255.0
b /= 255.0
# Convertimos a hex
c = to_hex((r, g, b, a))
return c
def ajustar_decimales_evento(df, decimales):
df = df.copy()
for col in df.columns:
try:
df[col] = pd.to_numeric(df[col], errors='ignore')
df[col] = df[col].round(decimales)
except:
pass
return df
def calcular_promedio_desviacion(df, n_replicas, unidad_medida, decimales):
df = df.copy()
col_replicas = [c for c in df.columns if c.startswith("Concentración Real") and unidad_medida in c and "Promedio" not in c and "Desviación" not in c]
for col in col_replicas:
df[col] = pd.to_numeric(df[col], errors='coerce')
if len(col_replicas) > 0:
df[f"Concentración Real Promedio ({unidad_medida})"] = df[col_replicas].mean(axis=1)
else:
df[f"Concentración Real Promedio ({unidad_medida})"] = np.nan
if len(col_replicas) > 1:
df[f"Desviación Estándar ({unidad_medida})"] = df[col_replicas].std(ddof=1, axis=1)
else:
df[f"Desviación Estándar ({unidad_medida})"] = 0.0
df[f"Concentración Real Promedio ({unidad_medida})"] = df[f"Concentración Real Promedio ({unidad_medida})"].round(decimales)
df[f"Desviación Estándar ({unidad_medida})"] = df[f"Desviación Estándar ({unidad_medida})"].round(decimales)
return df
def generar_graficos(df_valid, n_replicas, unidad_medida,
color_puntos, estilo_puntos,
color_linea_ajuste, estilo_linea_ajuste,
color_linea_ideal, estilo_linea_ideal,
color_barras_error,
mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos):
# Convertir colores a formato válido para matplotlib
color_puntos = safe_color(color_puntos)
color_linea_ajuste = safe_color(color_linea_ajuste)
color_linea_ideal = safe_color(color_linea_ideal)
color_barras_error = safe_color(color_barras_error)
col_predicha_num = "Concentración Predicha Numérica"
col_real_promedio = f"Concentración Real Promedio ({unidad_medida})"
col_desviacion = f"Desviación Estándar ({unidad_medida})"
df_valid[col_predicha_num] = pd.to_numeric(df_valid[col_predicha_num], errors='coerce')
df_valid[col_real_promedio] = pd.to_numeric(df_valid[col_real_promedio], errors='coerce')
df_valid[col_desviacion] = pd.to_numeric(df_valid[col_desviacion], errors='coerce').fillna(0)
if df_valid.empty or df_valid[col_predicha_num].isna().all() or df_valid[col_real_promedio].isna().all():
fig = plt.figure()
plt.text(0.5,0.5,"Datos insuficientes para generar el gráfico",ha='center',va='center')
return fig
slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_predicha_num], df_valid[col_real_promedio])
df_valid['Ajuste Lineal'] = intercept + slope * df_valid[col_predicha_num]
sns.set(style="whitegrid")
plt.rcParams.update({'figure.autolayout': True})
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6))
# Gráfico principal
if mostrar_puntos:
if n_replicas > 1:
ax1.errorbar(
df_valid[col_predicha_num],
df_valid[col_real_promedio],
yerr=df_valid[col_desviacion],
fmt=estilo_puntos,
color=color_puntos,
ecolor=color_barras_error,
elinewidth=2,
capsize=3,
label='Datos Reales'
)
else:
ax1.scatter(
df_valid[col_predicha_num],
df_valid[col_real_promedio],
color=color_puntos,
s=100,
label='Datos Reales',
marker=estilo_puntos
)
if mostrar_linea_ajuste:
ax1.plot(
df_valid[col_predicha_num],
df_valid['Ajuste Lineal'],
color=color_linea_ajuste,
label='Ajuste Lineal',
linewidth=2,
linestyle=estilo_linea_ajuste
)
if mostrar_linea_ideal:
min_predicha = df_valid[col_predicha_num].min()
max_predicha = df_valid[col_predicha_num].max()
ax1.plot(
[min_predicha, max_predicha],
[min_predicha, max_predicha],
color=color_linea_ideal,
linestyle=estilo_linea_ideal,
label='Ideal'
)
ax1.set_title('Correlación entre Concentración Predicha y Real', fontsize=14)
ax1.set_xlabel('Concentración Predicha', fontsize=12)
ax1.set_ylabel('Concentración Real Promedio', fontsize=12)
ax1.annotate(
f'y = {intercept:.3f} + {slope:.3f}x\n$R^2$ = {r_value**2:.4f}',
xy=(0.05, 0.95),
xycoords='axes fraction',
fontsize=12,
backgroundcolor='white',
verticalalignment='top'
)
ax1.legend(loc='lower right', fontsize=10)
# Gráfico de residuos
residuos = df_valid[col_real_promedio] - df_valid['Ajuste Lineal']
ax2.scatter(
df_valid[col_predicha_num],
residuos,
color=color_puntos,
s=100,
marker=estilo_puntos,
label='Residuos'
)
ax2.axhline(y=0, color='black', linestyle='--', linewidth=1)
ax2.set_title('Gráfico de Residuos', fontsize=14)
ax2.set_xlabel('Concentración Predicha', fontsize=12)
ax2.set_ylabel('Residuo', fontsize=12)
ax2.legend(loc='upper right', fontsize=10)
plt.tight_layout()
plt.savefig('grafico.png')
return fig
def evaluar_calidad_calibracion(df_valid, r_squared, rmse, cv_percent):
evaluacion = {
"calidad": "",
"recomendaciones": [],
"estado": "✅" if r_squared >= 0.95 and cv_percent <= 15 else "⚠️"
}
if r_squared >= 0.95:
evaluacion["calidad"] = "Excelente"
elif r_squared >= 0.90:
evaluacion["calidad"] = "Buena"
elif r_squared >= 0.85:
evaluacion["calidad"] = "Regular"
else:
evaluacion["calidad"] = "Deficiente"
if r_squared < 0.95:
evaluacion["recomendaciones"].append("- Considere repetir algunas mediciones para mejorar la correlación")
if cv_percent > 15:
evaluacion["recomendaciones"].append("- La variabilidad es alta. Revise el procedimiento de dilución")
mean_val = df_valid[df_valid.columns[-1]].astype(float).mean() if not df_valid.empty else 1
if mean_val == 0:
mean_val = 1
if rmse > 0.1 * mean_val:
evaluacion["recomendaciones"].append("- El error de predicción es significativo. Verifique la técnica de medición")
return evaluacion
def generar_informe_completo(df_valid, n_replicas, unidad_medida):
col_predicha_num = "Concentración Predicha Numérica"
col_real_promedio = f"Concentración Real Promedio ({unidad_medida})"
df_valid[col_predicha_num] = pd.to_numeric(df_valid[col_predicha_num], errors='coerce')
df_valid[col_real_promedio] = pd.to_numeric(df_valid[col_real_promedio], errors='coerce')
if len(df_valid) < 2:
informe = "# Informe de Calibración ⚠️\nNo hay suficientes datos para calcular la regresión."
return informe, "⚠️"
slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_predicha_num], df_valid[col_real_promedio])
r_squared = r_value ** 2
rmse = np.sqrt(((df_valid[col_real_promedio] - (intercept + slope * df_valid[col_predicha_num])) ** 2).mean())
cv = (df_valid[col_real_promedio].std() / df_valid[col_real_promedio].mean()) * 100 if df_valid[col_real_promedio].mean() != 0 else 0
evaluacion = evaluar_calidad_calibracion(df_valid, r_squared, rmse, cv)
informe = f"""# Informe de Calibración {evaluacion['estado']}
Fecha: {datetime.now().strftime('%d/%m/%Y %H:%M')}
## Resumen Estadístico
- **Ecuación de Regresión**: y = {intercept:.4f} + {slope:.4f}x
- **Coeficiente de correlación (r)**: {r_value:.4f}
- **Coeficiente de determinación ($R^2$)**: {r_squared:.4f}
- **Valor p**: {p_value:.4e}
- **Error estándar de la pendiente**: {std_err:.4f}
- **Error cuadrático medio (RMSE)**: {rmse:.4f}
- **Coeficiente de variación (CV)**: {cv:.2f}%
## Evaluación de Calidad
- **Calidad de la calibración**: {evaluacion['calidad']}
## Recomendaciones
{chr(10).join(evaluacion['recomendaciones']) if evaluacion['recomendaciones'] else "No hay recomendaciones específicas. La calibración cumple con los criterios de calidad."}
## Decisión
{("✅ APROBADO - La calibración cumple con los criterios de calidad establecidos" if evaluacion['estado'] == "✅" else "⚠️ REQUIERE REVISIÓN - La calibración necesita ajustes según las recomendaciones anteriores")}
---
*Nota: Este informe fue generado automáticamente. Por favor, revise los resultados y valide según sus criterios específicos.*
"""
return informe, evaluacion['estado']
def actualizar_analisis(df, n_replicas, unidad_medida, filas_seleccionadas, decimales):
if df is None or df.empty:
return "Error en los datos", None, "No se pueden generar análisis", df
if not filas_seleccionadas:
return "Se necesitan más datos", None, "No se han seleccionado filas para el análisis", df
indices_seleccionados = [int(s.split(' ')[1]) - 1 for s in filas_seleccionadas]
df = calcular_promedio_desviacion(df, n_replicas, unidad_medida, decimales)
col_predicha_num = "Concentración Predicha Numérica"
col_real_promedio = f"Concentración Real Promedio ({unidad_medida})"
df[col_predicha_num] = pd.to_numeric(df[col_predicha_num], errors='coerce')
df[col_real_promedio] = pd.to_numeric(df[col_real_promedio], errors='coerce')
df_valid = df.dropna(subset=[col_predicha_num, col_real_promedio])
df_valid.reset_index(drop=True, inplace=True)
df_valid = df_valid.loc[indices_seleccionados]
if len(df_valid) < 2:
return "Se necesitan más datos", None, "Se requieren al menos dos valores reales para el análisis", df
slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_predicha_num], df_valid[col_real_promedio])
df_valid['Ajuste Lineal'] = intercept + slope * df_valid[col_predicha_num]
# Generar gráfico con colores por defecto (podrán actualizarse con el botón Graficar)
fig = generar_graficos(
df_valid, n_replicas, unidad_medida,
color_puntos="#0000FF", estilo_puntos='o',
color_linea_ajuste="#00FF00", estilo_linea_ajuste='-',
color_linea_ideal="#FF0000", estilo_linea_ideal='--',
color_barras_error="#FFA500",
mostrar_linea_ajuste=True,
mostrar_linea_ideal=False,
mostrar_puntos=True
)
informe, estado = generar_informe_completo(df_valid, n_replicas, unidad_medida)
return estado, fig, informe, df
def exportar_informe_word(df_valid, informe_md, unidad_medida):
doc = docx.Document()
style = doc.styles['Normal']
font = style.font
font.name = 'Times New Roman'
font.size = Pt(12)
titulo = doc.add_heading('Informe de Calibración', 0)
titulo.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
fecha = doc.add_paragraph(f"Fecha: {datetime.now().strftime('%d/%m/%Y %H:%M')}")
fecha.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
if os.path.exists('grafico.png'):
doc.add_picture('grafico.png', width=Inches(6))
ultimo_parrafo = doc.paragraphs[-1]
ultimo_parrafo.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
leyenda = doc.add_paragraph('Figura 1. Gráfico de calibración.')
leyenda_format = leyenda.paragraph_format
leyenda_format.alignment = WD_PARAGRAPH_ALIGNMENT.CENTER
leyenda.style = doc.styles['Caption']
doc.add_heading('Resumen Estadístico', level=1)
for linea in informe_md.split('\n'):
if linea.startswith('##'):
doc.add_heading(linea.replace('##', '').strip(), level=2)
else:
doc.add_paragraph(linea)
doc.add_heading('Tabla de Datos de Calibración', level=1)
tabla_datos = df_valid.reset_index(drop=True)
tabla_datos = tabla_datos.round(4)
columnas = tabla_datos.columns.tolist()
registros = tabla_datos.values.tolist()
tabla = doc.add_table(rows=1 + len(registros), cols=len(columnas))
tabla.style = 'Table Grid'
hdr_cells = tabla.rows[0].cells
for idx, col_name in enumerate(columnas):
hdr_cells[idx].text = col_name
for i, registro in enumerate(registros):
row_cells = tabla.rows[i + 1].cells
for j, valor in enumerate(registro):
row_cells[j].text = str(valor)
for row in tabla.rows:
for cell in row.cells:
for paragraph in cell.paragraphs:
paragraph.style = doc.styles['Normal']
filename = 'informe_calibracion.docx'
doc.save(filename)
return filename
def exportar_informe_latex(df_valid, informe_md):
informe_tex = r"""\documentclass{article}
\usepackage[spanish]{babel}
\usepackage{amsmath}
\usepackage{graphicx}
\usepackage{booktabs}
\begin{document}
"""
informe_tex += informe_md.replace('#', '').replace('**', '\\textbf{').replace('*', '\\textit{')
informe_tex += r"""
\end{document}
"""
filename = 'informe_calibracion.tex'
with open(filename, 'w') as f:
f.write(informe_tex)
return filename
def exportar_word(df, informe_md, unidad_medida, filas_seleccionadas):
df_valid = df.copy()
col_predicha_num = "Concentración Predicha Numérica"
col_real_promedio = [c for c in df_valid.columns if 'Real Promedio' in c][0] if any('Real Promedio' in c for c in df_valid.columns) else None
if col_predicha_num in df_valid.columns:
df_valid[col_predicha_num] = pd.to_numeric(df_valid[col_predicha_num], errors='coerce')
if col_real_promedio:
df_valid[col_real_promedio] = pd.to_numeric(df_valid[col_real_promedio], errors='coerce')
df_valid = df_valid.dropna(subset=[col_predicha_num] if col_predicha_num else df_valid.columns[0], how='all')
df_valid.reset_index(drop=True, inplace=True)
if not filas_seleccionadas:
return None
indices_seleccionados = [int(s.split(' ')[1]) - 1 for s in filas_seleccionadas if s.split(' ')[1].isdigit() and int(s.split(' ')[1]) - 1 < len(df_valid)]
if indices_seleccionados:
df_valid = df_valid.loc[indices_seleccionados]
else:
df_valid = df_valid.iloc[:0]
if df_valid.empty:
return None
filename = exportar_informe_word(df_valid, informe_md, unidad_medida)
return filename
def exportar_latex(df, informe_md, filas_seleccionadas):
df_valid = df.copy()
col_predicha_num = "Concentración Predicha Numérica"
col_real_promedio = [col for col in df_valid.columns if 'Real Promedio' in col][0] if any('Real Promedio' in c for c in df_valid.columns) else None
if col_predicha_num in df_valid.columns:
df_valid[col_predicha_num] = pd.to_numeric(df_valid[col_predicha_num], errors='coerce')
if col_real_promedio:
df_valid[col_real_promedio] = pd.to_numeric(df_valid[col_real_promedio], errors='coerce')
df_valid = df_valid.dropna(subset=[col_predicha_num] if col_predicha_num else df_valid.columns[0], how='all')
df_valid.reset_index(drop=True, inplace=True)
if not filas_seleccionadas:
return None
indices_seleccionados = [int(s.split(' ')[1]) - 1 for s in filas_seleccionadas if s.split(' ')[1].isdigit() and int(s.split(' ')[1]) - 1 < len(df_valid)]
if indices_seleccionados:
df_valid = df_valid.loc[indices_seleccionados]
else:
df_valid = df_valid.iloc[:0]
if df_valid.empty:
return None
filename = exportar_informe_latex(df_valid, informe_md)
return filename
def limpiar_datos(n_replicas):
df = pd.DataFrame({
"Solución": [1/(2**i) for i in range(7)],
"H2O": [1-(1/(2**i)) for i in range(7)],
"Factor de Dilución": [(1/(1/(2**i))) for i in range(7)],
"Concentración Predicha Numérica": [2000000/(1/(1/(2**i))) for i in range(7)],
"Concentración Predicha (mg/L)": [2000000/(1/(1/(2**i))) for i in range(7)]
})
return (
2000000,
"UFC",
7,
df,
"",
None,
""
)
def generar_datos_sinteticos_evento(df, n_replicas, unidad_medida):
df = df.copy()
col_predicha_num = "Concentración Predicha Numérica"
if col_predicha_num in df.columns:
df[col_predicha_num] = pd.to_numeric(df[col_predicha_num], errors='coerce')
for i in range(1, n_replicas + 1):
col_real = f"Concentración Real {i} ({unidad_medida})"
desviacion_std = 0.05 * df[col_predicha_num].mean()
valores_predichos = df[col_predicha_num].dropna().values
if len(valores_predichos) == 0:
continue
datos_sinteticos = valores_predichos + np.random.normal(0, desviacion_std, size=len(valores_predichos))
datos_sinteticos = np.maximum(0, datos_sinteticos)
datos_sinteticos = np.round(datos_sinteticos, 3)
df.loc[df[col_predicha_num].notna(), col_real] = datos_sinteticos
return df
def actualizar_tabla_evento(df, n_filas, conc, unidad, n_replicas, decimales):
df = df.copy()
if len(df) > n_filas:
df = df.iloc[:n_filas].reset_index(drop=True)
else:
for i in range(len(df), n_filas):
df.loc[i, df.columns] = np.nan
df = ajustar_decimales_evento(df, decimales)
return df
def cargar_excel(file):
all_sheets = pd.read_excel(file.name, sheet_name=None)
if len(all_sheets) < 3:
return "El archivo debe tener al menos tres pestañas (Hoja1, Hoja2, Hoja3).", None, None, None, None, None, None, ""
sheet_names = list(all_sheets.keys())
sheet1_name = sheet_names[0]
sheet2_name = sheet_names[1]
sheet3_name = sheet_names[2]
df_sheet1 = all_sheets[sheet1_name]
df_sheet2 = all_sheets[sheet2_name]
df_sheet3 = all_sheets[sheet3_name]
df_base = df_sheet1.iloc[:, :5].copy()
primera_col = df_base.columns[0]
try:
parte_interna = primera_col.split('(')[1].split(')')[0]
partes = parte_interna.split()
unidad_medida = partes[-1]
concentracion_inicial = float("".join(partes[:-1]))
except:
concentracion_inicial = 2000000.0
unidad_medida = "UFC"
n_filas = len(df_base)
n_replicas = 2
df_sistema = df_base.copy()
col_replica_1 = df_sheet2.iloc[:n_filas, 1].values if df_sheet2.shape[1] > 1 else df_sheet2.iloc[:n_filas,0].values
col_replica_2 = df_sheet3.iloc[:n_filas, 1].values if df_sheet3.shape[1] > 1 else df_sheet3.iloc[:n_filas,0].values
df_sistema[f"Concentración Real 1 ({unidad_medida})"] = col_replica_1
df_sistema[f"Concentración Real 2 ({unidad_medida})"] = col_replica_2
return concentracion_inicial, unidad_medida, n_filas, n_replicas, df_sistema, "", None, ""
def actualizar_opciones_filas(df):
if df is None or df.empty:
update = gr.update(choices=[], value=[])
else:
opciones = [f"Fila {i+1}" for i in df.index]
update = gr.update(choices=opciones, value=opciones)
return update, update
def calcular_regresion_tabla_principal(df, unidad_medida, filas_seleccionadas_regresion,
color_puntos, estilo_puntos,
color_linea_ajuste, estilo_linea_ajuste,
mostrar_linea_ajuste, mostrar_puntos,
legend_location, decimales,
titulo_grafico_original, titulo_grafico_personalizado,
eje_x_original, eje_y_original,
eje_x_personalizado, eje_y_personalizado):
if df is None or df.empty:
return "Datos insuficientes", None, None, None
# Convertir colores a formato seguro
color_puntos = safe_color(color_puntos)
color_linea_ajuste = safe_color(color_linea_ajuste)
col_concentracion = "Concentración Predicha Numérica"
col_real_promedio = f"Concentración Real Promedio ({unidad_medida})"
col_desviacion = f"Desviación Estándar ({unidad_medida})"
n_replicas = len([c for c in df.columns if 'Concentración Real' in c and 'Promedio' not in c and 'Desviación' not in c])
df = calcular_promedio_desviacion(df, n_replicas, unidad_medida, decimales)
if col_concentracion not in df.columns or col_real_promedio not in df.columns:
return "Faltan columnas necesarias", None, None, None
df[col_concentracion] = pd.to_numeric(df[col_concentracion], errors='coerce')
df[col_real_promedio] = pd.to_numeric(df[col_real_promedio], errors='coerce')
df[col_desviacion] = pd.to_numeric(df[col_desviacion], errors='coerce').fillna(0)
df_valid = df.dropna(subset=[col_concentracion, col_real_promedio])
df_valid.reset_index(drop=True, inplace=True)
df_original = df_valid.copy()
if not filas_seleccionadas_regresion:
return "Se necesitan más datos", None, None, None
indices_seleccionados = [int(s.split(' ')[1]) - 1 for s in filas_seleccionadas_regresion]
if len(indices_seleccionados) < 2:
return "Se requieren al menos dos puntos para calcular la regresión", None, None, None
df_valid = df_valid.loc[indices_seleccionados]
slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_concentracion], df_valid[col_real_promedio])
sns.set(style="whitegrid")
fig_original, ax_original = plt.subplots(figsize=(8, 6))
# Gráfico Original
ax_original.errorbar(
df_original[col_concentracion],
df_original[col_real_promedio],
yerr=df_original[col_desviacion],
fmt=estilo_puntos,
color=color_puntos,
ecolor='gray',
elinewidth=1,
capsize=3,
label='Datos'
)
slope_all, intercept_all, r_value_all, p_value_all, std_err_all = stats.linregress(df_original[col_concentracion], df_original[col_real_promedio])
ax_original.plot(
df_original[col_concentracion],
intercept_all + slope_all * df_original[col_concentracion],
color=color_linea_ajuste,
linestyle='-',
label='Ajuste Lineal'
)
ax_original.set_xlabel(eje_x_original if eje_x_original else 'Concentración Predicha Numérica')
ax_original.set_ylabel(eje_y_original if eje_y_original else f'Concentración Real Promedio ({unidad_medida})')
ax_original.set_title(titulo_grafico_original if titulo_grafico_original else 'Regresión Lineal: Concentración Real vs Concentración Predicha (Original)')
ax_original.legend(loc=legend_location)
ax_original.annotate(
f'y = {intercept_all:.4f} + {slope_all:.4f}x\n$R^2$ = {r_value_all**2:.4f}',
xy=(0.05, 0.95),
xycoords='axes fraction',
fontsize=12,
backgroundcolor='white',
verticalalignment='top'
)
# Gráfico Personalizado
sns.set(style="whitegrid")
fig_personalizado, ax_personalizado = plt.subplots(figsize=(8, 6))
if mostrar_puntos:
ax_personalizado.errorbar(
df_valid[col_concentracion],
df_valid[col_real_promedio],
yerr=df_valid[col_desviacion],
fmt=estilo_puntos,
color=color_puntos,
ecolor='gray',
elinewidth=1,
capsize=3,
label='Datos'
)
if mostrar_linea_ajuste:
ax_personalizado.plot(
df_valid[col_concentracion],
intercept + slope * df_valid[col_concentracion],
color=color_linea_ajuste,
linestyle=estilo_linea_ajuste,
label='Ajuste Lineal'
)
ax_personalizado.set_xlabel(eje_x_personalizado if eje_x_personalizado else 'Concentración Predicha Numérica')
ax_personalizado.set_ylabel(eje_y_personalizado if eje_y_personalizado else f'Concentración Real Promedio ({unidad_medida})')
ax_personalizado.set_title(titulo_grafico_personalizado if titulo_grafico_personalizado else 'Regresión Lineal Personalizada')
ax_personalizado.legend(loc=legend_location)
ax_personalizado.annotate(
f'y = {intercept:.4f} + {slope:.4f}x\n$R^2$ = {r_value**2:.4f}',
xy=(0.05, 0.95),
xycoords='axes fraction',
fontsize=12,
backgroundcolor='white',
verticalalignment='top'
)
df_resumen = df_valid[[col_concentracion, col_real_promedio, col_desviacion]].copy()
df_resumen.columns = ['Concentración Predicha', 'Absorbancia Promedio', 'Desviación Estándar']
return "Regresión calculada exitosamente", fig_original, fig_personalizado, df_resumen
def iniciar_con_ejemplo():
df = pd.DataFrame({
"Solución": [1.00,0.80,0.67,0.60,0.53,0.47,0.40],
"H2O": [0.00,0.20,0.33,0.40,0.47,0.53,0.60],
"Factor de Dilución": [1.00,1.25,1.50,1.67,1.87,2.14,2.50],
"Concentración Predicha Numérica": [150,120,100,90,80,70,60],
"Concentración Predicha (mg/L)": [150,120,100,90,80,70,60],
"Concentración Real 1 (UFC)": [1.715,1.089,0.941,0.552,0.703,0.801,0.516]
})
n_replicas = 1
estado, fig, informe, df = actualizar_analisis(df, n_replicas, "UFC", [f"Fila {i+1}" for i in df.index], 3)
return (
2000000,
"UFC",
7,
df,
estado,
fig,
informe,
[f"Fila {i+1}" for i in df.index],
3
)
with gr.Blocks(theme=gr.themes.Soft()) as interfaz:
gr.Markdown("""
# 📊 Sistema Avanzado de Calibración con Análisis Estadístico
Configure los parámetros, edite los valores en la tabla y luego presione "Calcular" para obtener el análisis.
""")
with gr.Tab("📝 Datos de Calibración"):
with gr.Row():
concentracion_input = gr.Number(
value=2000000,
label="Concentración Inicial",
precision=0
)
unidad_input = gr.Textbox(
value="UFC",
label="Unidad de Medida",
placeholder="UFC, OD, etc..."
)
filas_slider = gr.Slider(
minimum=1,
maximum=20,
value=7,
step=1,
label="Número de filas"
)
decimales_slider = gr.Slider(
minimum=0,
maximum=5,
value=3,
step=1,
label="Número de Decimales"
)
replicas_slider = gr.Slider(
minimum=1,
maximum=10,
value=1,
step=1,
label="Número de Réplicas"
)
with gr.Row():
calcular_btn = gr.Button("🔄 Calcular", variant="primary")
limpiar_btn = gr.Button("🗑 Limpiar Datos", variant="secondary")
ajustar_decimales_btn = gr.Button("🛠 Ajustar Decimales", variant="secondary")
with gr.Row():
ejemplo_ufc_btn = gr.Button("📋 Cargar Ejemplo UFC", variant="secondary")
ejemplo_od_btn = gr.Button("📋 Cargar Ejemplo OD", variant="secondary")
sinteticos_btn = gr.Button("🧪 Generar Datos Sintéticos", variant="secondary")
cargar_excel_btn = gr.UploadButton("📂 Cargar Excel", file_types=[".xlsx"], variant="secondary")
tabla_output = gr.DataFrame(
wrap=True,
label="Tabla de Datos",
interactive=True,
type="pandas",
)
with gr.Tab("📊 Análisis y Reporte"):
estado_output = gr.Textbox(label="Estado", interactive=False)
graficos_output = gr.Plot(label="Gráficos de Análisis")
filas_seleccionadas = gr.CheckboxGroup(
label="Seleccione las filas a incluir en el análisis",
choices=[],
value=[],
)
with gr.Row():
color_puntos_picker = gr.ColorPicker(label="Color de Puntos", value="#0000FF")
estilo_puntos_dropdown = gr.Dropdown(
choices=["o", "s", "^", "D", "v", "<", ">", "h", "H", "p", "*", "X", "d"],
value="o",
label="Estilo de Punto"
)
color_linea_ajuste_picker = gr.ColorPicker(label="Color de la Línea de Ajuste", value="#00FF00")
estilo_linea_ajuste_dropdown = gr.Dropdown(
choices=["-", "--", "-.", ":"],
value="-",
label="Estilo Línea de Ajuste"
)
with gr.Row():
color_linea_ideal_picker = gr.ColorPicker(label="Color Línea Ideal", value="#FF0000")
estilo_linea_ideal_dropdown = gr.Dropdown(
choices=["--", "-", "-.", ":"],
value="--",
label="Estilo Línea Ideal"
)
color_barras_error_picker = gr.ColorPicker(label="Color Barras de Error", value="#FFA500")
mostrar_linea_ajuste = gr.Checkbox(value=True, label="Mostrar Línea de Ajuste")
mostrar_linea_ideal = gr.Checkbox(value=False, label="Mostrar Línea Ideal")
mostrar_puntos = gr.Checkbox(value=True, label="Mostrar Puntos")
graficar_btn = gr.Button("📊 Graficar", variant="primary")
recalcular_btn = gr.Button("🔄 Calcular Otra Vez", variant="secondary")
with gr.Row():
copiar_btn = gr.Button("📋 Copiar Informe", variant="secondary")
exportar_word_btn = gr.Button("💾 Exportar Informe Word", variant="primary")
exportar_latex_btn = gr.Button("💾 Exportar Informe LaTeX", variant="primary")
with gr.Row():
exportar_word_file = gr.File(label="Informe en Word")
exportar_latex_file = gr.File(label="Informe en LaTeX")
informe_output = gr.Markdown(elem_id="informe_output")
with gr.Tab("📈 Regresión Absorbancia vs Concentración"):
gr.Markdown("## Ajuste de Regresión utilizando datos de la Tabla Principal")
filas_seleccionadas_regresion = gr.CheckboxGroup(
label="Seleccione las filas a incluir en el análisis de regresión",
choices=[],
value=[],
)
with gr.Row():
color_puntos_regresion_picker = gr.ColorPicker(label="Color Puntos Regresión", value="#0000FF")
estilo_puntos_regresion = gr.Dropdown(
choices=["o", "s", "^", "D", "v", "<", ">", "h", "H", "p", "*", "X", "d"],
value="o",
label="Estilo de Puntos (Regresión)"
)
color_linea_ajuste_regresion_picker = gr.ColorPicker(label="Color Línea Ajuste (Regresión)", value="#00FF00")
estilo_linea_ajuste_regresion = gr.Dropdown(
choices=["-", "--", "-.", ":"],
value="-",
label="Estilo Línea de Ajuste (Regresión)"
)
mostrar_linea_ajuste_regresion = gr.Checkbox(value=True, label="Mostrar Línea de Ajuste (Regresión)")
mostrar_puntos_regresion = gr.Checkbox(value=True, label="Mostrar Puntos (Regresión)")
with gr.Row():
legend_location_dropdown = gr.Dropdown(
choices=[
'best', 'upper right', 'upper left', 'lower left', 'lower right',
'right', 'center left', 'center right', 'lower center',
'upper center', 'center'
],
value='lower right',
label='Ubicación de la Leyenda'
)
with gr.Row():
titulo_grafico_original = gr.Textbox(
label="Título del Gráfico Original",
placeholder="Regresión Lineal: Concentración Real vs Concentración Predicha (Original)"
)
titulo_grafico_personalizado = gr.Textbox(
label="Título del Gráfico Personalizado",
placeholder="Regresión Lineal Personalizada"
)
with gr.Row():
eje_x_original = gr.Textbox(
label="Etiqueta del Eje X (Gráfico Original)",
placeholder="Concentración Predicha Numérica"
)
eje_y_original = gr.Textbox(
label="Etiqueta del Eje Y (Gráfico Original)",
placeholder="Concentración Real Promedio (UFC)"
)
with gr.Row():
eje_x_personalizado = gr.Textbox(
label="Etiqueta del Eje X (Gráfico Personalizado)",
placeholder="Concentración Predicha Numérica"
)
eje_y_personalizado = gr.Textbox(
label="Etiqueta del Eje Y (Gráfico Personalizado)",
placeholder="Concentración Real Promedio (UFC)"
)
calcular_regresion_btn = gr.Button("Calcular Regresión")
estado_regresion_output = gr.Textbox(label="Estado de la Regresión", interactive=False)
grafico_original_output = gr.Plot(label="Gráfico Original")
grafico_personalizado_output = gr.Plot(label="Gráfico Personalizado")
tabla_resumen_output = gr.DataFrame(label="Tabla Resumida")
tabla_output.change(
fn=actualizar_opciones_filas,
inputs=[tabla_output],
outputs=[filas_seleccionadas, filas_seleccionadas_regresion]
)
calcular_btn.click(
fn=actualizar_analisis,
inputs=[tabla_output, replicas_slider, unidad_input, filas_seleccionadas, decimales_slider],
outputs=[estado_output, graficos_output, informe_output, tabla_output]
)
def actualizar_graficos_custom(df, n_replicas, unidad_medida,
color_puntos, estilo_puntos,
color_linea_ajuste, estilo_linea_ajuste,
color_linea_ideal, estilo_linea_ideal,
color_barras_error,
mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos,
filas_seleccionadas, decimales):
if df is None or df.empty:
return None
df = calcular_promedio_desviacion(df, n_replicas, unidad_medida, decimales)
col_predicha_num = "Concentración Predicha Numérica"
col_real_promedio = f"Concentración Real Promedio ({unidad_medida})"
df[col_predicha_num] = pd.to_numeric(df[col_predicha_num], errors='coerce')
df[col_real_promedio] = pd.to_numeric(df[col_real_promedio], errors='coerce')
df_valid = df.dropna(subset=[col_predicha_num, col_real_promedio])
df_valid.reset_index(drop=True, inplace=True)
if not filas_seleccionadas:
return None
indices_seleccionados = [int(s.split(' ')[1]) - 1 for s in filas_seleccionadas]
df_valid = df_valid.loc[indices_seleccionados]
if len(df_valid) < 2:
return None
fig = generar_graficos(
df_valid, n_replicas, unidad_medida,
color_puntos, estilo_puntos,
color_linea_ajuste, estilo_linea_ajuste,
color_linea_ideal, estilo_linea_ideal,
color_barras_error,
mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos
)
return fig
graficar_btn.click(
fn=actualizar_graficos_custom,
inputs=[
tabla_output, replicas_slider, unidad_input,
color_puntos_picker, estilo_puntos_dropdown,
color_linea_ajuste_picker, estilo_linea_ajuste_dropdown,
color_linea_ideal_picker, estilo_linea_ideal_dropdown,
color_barras_error_picker,
mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos,
filas_seleccionadas, decimales_slider
],
outputs=graficos_output
)
recalcular_btn.click(
fn=actualizar_analisis,
inputs=[tabla_output, replicas_slider, unidad_input, filas_seleccionadas, decimales_slider],
outputs=[estado_output, graficos_output, informe_output, tabla_output]
)
def resetear_linea_ideal():
return gr.update(value=False)
calcular_btn.click(fn=resetear_linea_ideal, outputs=mostrar_linea_ideal)
limpiar_btn.click(fn=resetear_linea_ideal, outputs=mostrar_linea_ideal)
ajustar_decimales_btn.click(fn=resetear_linea_ideal, outputs=mostrar_linea_ideal)
sinteticos_btn.click(fn=resetear_linea_ideal, outputs=mostrar_linea_ideal)
limpiar_btn.click(
fn=limpiar_datos,
inputs=[replicas_slider],
outputs=[concentracion_input, unidad_input, filas_slider, tabla_output, estado_output, graficos_output, informe_output]
)
def cargar_ejemplo_ufc(n_replicas):
df = pd.DataFrame({
"Solución": [1.00,0.80,0.67,0.60,0.53,0.47,0.40],
"H2O": [0.00,0.20,0.33,0.40,0.47,0.53,0.60],
"Factor de Dilución": [1.00,1.25,1.50,1.67,1.87,2.14,2.50],
"Concentración Predicha Numérica": [150,120,100,90,80,70,60],
"Concentración Predicha (mg/L)": [150,120,100,90,80,70,60]
})
for i in range(1, n_replicas + 1):
df[f"Concentración Real {i} (UFC)"] = np.nan
return 2000000, "UFC", 7, df
def cargar_ejemplo_od(n_replicas):
df = pd.DataFrame({
"Solución": [1.00,0.80,0.60,0.40,0.20,0.10,0.05],
"H2O": [0.00,0.20,0.40,0.60,0.80,0.90,0.95],
"Factor de Dilución": [1.00,1.25,1.67,2.50,5.00,10.00,20.00],
"Concentración Predicha Numérica": [1.0,0.8,0.6,0.4,0.2,0.1,0.05],
"Concentración Predicha (mg/L)": [1.0,0.8,0.6,0.4,0.2,0.1,0.05]
})
for i in range(1, n_replicas + 1):
df[f"Concentración Real {i} (OD)"] = np.nan
return 1.000, "OD", 7, df
ejemplo_ufc_btn.click(
fn=cargar_ejemplo_ufc,
inputs=[replicas_slider],
outputs=[concentracion_input, unidad_input, filas_slider, tabla_output]
)
ejemplo_od_btn.click(
fn=cargar_ejemplo_od,
inputs=[replicas_slider],
outputs=[concentracion_input, unidad_input, filas_slider, tabla_output]
)
sinteticos_btn.click(
fn=generar_datos_sinteticos_evento,
inputs=[tabla_output, replicas_slider, unidad_input],
outputs=tabla_output
)
cargar_excel_btn.upload(
fn=cargar_excel,
inputs=[cargar_excel_btn],
outputs=[concentracion_input, unidad_input, filas_slider, replicas_slider, tabla_output, estado_output, graficos_output, informe_output]
)
ajustar_decimales_btn.click(
fn=ajustar_decimales_evento,
inputs=[tabla_output, decimales_slider],
outputs=tabla_output
)
def actualizar_tabla_wrapper(df, filas, conc, unidad, replicas, decimales):
return actualizar_tabla_evento(df, filas, conc, unidad, replicas, decimales)
concentracion_input.change(
fn=actualizar_tabla_wrapper,
inputs=[tabla_output, filas_slider, concentracion_input, unidad_input, replicas_slider, decimales_slider],
outputs=tabla_output
)
unidad_input.change(
fn=actualizar_tabla_wrapper,
inputs=[tabla_output, filas_slider, concentracion_input, unidad_input, replicas_slider, decimales_slider],
outputs=tabla_output
)
filas_slider.change(
fn=actualizar_tabla_wrapper,
inputs=[tabla_output, filas_slider, concentracion_input, unidad_input, replicas_slider, decimales_slider],
outputs=tabla_output
)
replicas_slider.change(
fn=actualizar_tabla_wrapper,
inputs=[tabla_output, filas_slider, concentracion_input, unidad_input, replicas_slider, decimales_slider],
outputs=tabla_output
)
decimales_slider.change(
fn=ajustar_decimales_evento,
inputs=[tabla_output, decimales_slider],
outputs=tabla_output
)
copiar_btn.click(
None,
[],
[],
js="""
function() {
const informeElement = document.querySelector('#informe_output');
const range = document.createRange();
range.selectNode(informeElement);
window.getSelection().removeAllRanges();
window.getSelection().addRange(range);
document.execCommand('copy');
window.getSelection().removeAllRanges();
alert('Informe copiado al portapapeles');
}
"""
)
exportar_word_btn.click(
fn=exportar_word,
inputs=[tabla_output, informe_output, unidad_input, filas_seleccionadas],
outputs=exportar_word_file
)
exportar_latex_btn.click(
fn=exportar_latex,
inputs=[tabla_output, informe_output, filas_seleccionadas],
outputs=exportar_latex_file
)
interfaz.load(
fn=iniciar_con_ejemplo,
outputs=[concentracion_input, unidad_input, filas_slider, tabla_output, estado_output, graficos_output, informe_output, filas_seleccionadas, decimales_slider]
)
calcular_regresion_btn.click(
fn=calcular_regresion_tabla_principal,
inputs=[
tabla_output, unidad_input, filas_seleccionadas_regresion,
color_puntos_regresion_picker, estilo_puntos_regresion,
color_linea_ajuste_regresion_picker, estilo_linea_ajuste_regresion,
mostrar_linea_ajuste_regresion, mostrar_puntos_regresion,
legend_location_dropdown, decimales_slider,
titulo_grafico_original, titulo_grafico_personalizado,
eje_x_original, eje_y_original,
eje_x_personalizado, eje_y_personalizado
],
outputs=[estado_regresion_output, grafico_original_output, grafico_personalizado_output, tabla_resumen_output]
)
if __name__ == "__main__":
interfaz.launch()