|
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('): |
|
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] |
|
r = r/255.0 if r > 1 else r |
|
g = g/255.0 if g > 1 else g |
|
b = b/255.0 if b > 1 else b |
|
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_predicha, unidad_replicas, decimales): |
|
df = df.copy() |
|
col_replicas = [c for c in df.columns if c.startswith("Absorbancia Real") and f"({unidad_replicas})" 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"Absorbancia Real Promedio ({unidad_replicas})"] = df[col_replicas].mean(axis=1) |
|
else: |
|
df[f"Absorbancia Real Promedio ({unidad_replicas})"] = np.nan |
|
|
|
if len(col_replicas) > 1: |
|
df[f"Desviación Estándar ({unidad_replicas})"] = df[col_replicas].std(ddof=1, axis=1) |
|
else: |
|
df[f"Desviación Estándar ({unidad_replicas})"] = 0.0 |
|
|
|
df[f"Absorbancia Real Promedio ({unidad_replicas})"] = df[f"Absorbancia Real Promedio ({unidad_replicas})"].round(decimales) |
|
df[f"Desviación Estándar ({unidad_replicas})"] = df[f"Desviación Estándar ({unidad_replicas})"].round(decimales) |
|
|
|
return df |
|
|
|
def generar_graficos(df_valid, n_replicas, unidad_predicha, unidad_replicas, |
|
color_puntos, estilo_puntos, |
|
color_linea_ajuste, estilo_linea_ajuste, |
|
color_barras_error, |
|
mostrar_linea_ajuste, mostrar_puntos): |
|
|
|
color_puntos = safe_color(color_puntos) |
|
color_linea_ajuste = safe_color(color_linea_ajuste) |
|
color_barras_error = safe_color(color_barras_error) |
|
|
|
col_predicha = f"Concentración Predicha ({unidad_predicha})" |
|
col_real_promedio = f"Absorbancia Real Promedio ({unidad_replicas})" |
|
col_desviacion = f"Desviación Estándar ({unidad_replicas})" |
|
|
|
df_valid[col_predicha] = pd.to_numeric(df_valid[col_predicha], 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].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], df_valid[col_real_promedio]) |
|
df_valid['Ajuste Lineal'] = intercept + slope * df_valid[col_predicha] |
|
|
|
sns.set(style="whitegrid") |
|
plt.rcParams.update({'figure.autolayout': True}) |
|
|
|
fig, (ax1, ax2) = plt.subplots(1, 2, figsize=(14, 6)) |
|
|
|
if mostrar_puntos: |
|
if n_replicas > 1: |
|
ax1.errorbar( |
|
df_valid[col_predicha], |
|
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], |
|
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], |
|
df_valid['Ajuste Lineal'], |
|
color=color_linea_ajuste, |
|
label='Ajuste Lineal', |
|
linewidth=2, |
|
linestyle=estilo_linea_ajuste |
|
) |
|
|
|
ax1.set_title('Correlación entre Concentración Predicha y Absorbancia Real', fontsize=14) |
|
ax1.set_xlabel(f'Concentración Predicha ({unidad_predicha})', fontsize=12) |
|
ax1.set_ylabel(f'Absorbancia Real Promedio ({unidad_replicas})', 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) |
|
|
|
residuos = df_valid[col_real_promedio] - df_valid['Ajuste Lineal'] |
|
ax2.scatter( |
|
df_valid[col_predicha], |
|
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(f'Concentración Predicha ({unidad_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_predicha, unidad_replicas): |
|
col_predicha = f"Concentración Predicha ({unidad_predicha})" |
|
col_real_promedio = f"Absorbancia Real Promedio ({unidad_replicas})" |
|
|
|
df_valid[col_predicha] = pd.to_numeric(df_valid[col_predicha], 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], df_valid[col_real_promedio]) |
|
r_squared = r_value ** 2 |
|
rmse = np.sqrt(((df_valid[col_real_promedio] - (intercept + slope * df_valid[col_predicha])) ** 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_predicha, unidad_replicas, 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_predicha, unidad_replicas, decimales) |
|
|
|
col_predicha = f"Concentración Predicha ({unidad_predicha})" |
|
col_real_promedio = f"Absorbancia Real Promedio ({unidad_replicas})" |
|
|
|
df[col_predicha] = pd.to_numeric(df[col_predicha], errors='coerce') |
|
df[col_real_promedio] = pd.to_numeric(df[col_real_promedio], errors='coerce') |
|
|
|
df_valid = df.dropna(subset=[col_predicha, 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], df_valid[col_real_promedio]) |
|
df_valid['Ajuste Lineal'] = intercept + slope * df_valid[col_predicha] |
|
|
|
fig = generar_graficos( |
|
df_valid, n_replicas, unidad_predicha, unidad_replicas, |
|
color_puntos="#0000FF", estilo_puntos='o', |
|
color_linea_ajuste="#00FF00", estilo_linea_ajuste='-', |
|
color_barras_error="#FFA500", |
|
mostrar_linea_ajuste=True, |
|
mostrar_puntos=True |
|
) |
|
informe, estado = generar_informe_completo(df_valid, n_replicas, unidad_predicha, unidad_replicas) |
|
|
|
return estado, fig, informe, df |
|
|
|
def exportar_informe_word(df_valid, informe_md, unidad_predicha, unidad_replicas): |
|
import docx |
|
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_predicha, unidad_replicas, filas_seleccionadas): |
|
df_valid = df.copy() |
|
col_predicha = f"Concentración Predicha ({unidad_predicha})" |
|
col_real_promedio = [c for c in df_valid.columns if 'Absorbancia Real Promedio' in c][0] if any('Absorbancia Real Promedio' in c for c in df_valid.columns) else None |
|
|
|
if col_predicha in df_valid.columns: |
|
df_valid[col_predicha] = pd.to_numeric(df_valid[col_predicha], 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] if col_predicha 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_predicha, unidad_replicas) |
|
return filename |
|
|
|
def exportar_latex(df, informe_md, filas_seleccionadas): |
|
df_valid = df.copy() |
|
col_predicha = [c for c in df_valid.columns if 'Concentración Predicha' in c] |
|
col_predicha = col_predicha[0] if col_predicha else None |
|
col_real_promedio = [col for col in df_valid.columns if 'Absorbancia Real Promedio' in col][0] if any('Absorbancia Real Promedio' in c for c in df_valid.columns) else None |
|
|
|
if col_predicha and col_predicha in df_valid.columns: |
|
df_valid[col_predicha] = pd.to_numeric(df_valid[col_predicha], 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] if col_predicha 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): |
|
unidad_predicha = "mg/L" |
|
|
|
unidad_replicas = "Abs" |
|
df = pd.DataFrame({ |
|
"Solución": [1/(2**i) for i in range(7)], |
|
"H2O": [1-(1/(2**i)) for i in range(7)], |
|
"Dimensión de Dilución": [(1/(1/(2**i))) for i in range(7)], |
|
f"Concentración Predicha ({unidad_predicha})": [150/(1/(1/(2**i))) for i in range(7)] |
|
}) |
|
for i in range(1, n_replicas+1): |
|
df[f"Absorbancia Real {i} ({unidad_replicas})"] = np.nan |
|
|
|
return ( |
|
150, |
|
unidad_predicha, |
|
7, |
|
df, |
|
"", |
|
None, |
|
"" |
|
) |
|
|
|
def generar_datos_sinteticos_evento(df, n_replicas, unidad_predicha, unidad_replicas): |
|
df = df.copy() |
|
col_predicha = f"Concentración Predicha ({unidad_predicha})" |
|
if col_predicha in df.columns: |
|
df[col_predicha] = pd.to_numeric(df[col_predicha], errors='coerce') |
|
if not df[col_predicha].empty: |
|
for i in range(1, n_replicas + 1): |
|
col_real = f"Absorbancia Real {i} ({unidad_replicas})" |
|
desviacion_std = 0.05 * df[col_predicha].mean() |
|
valores_predichos = df[col_predicha].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].notna(), col_real] = datos_sinteticos |
|
return df |
|
|
|
def actualizar_tabla_evento(df, n_filas, conc, unidad_predicha, unidad_replicas, 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[:, :4].copy() |
|
|
|
pred_col = df_base.columns[-1] |
|
unidad_predicha = "mg/L" |
|
try: |
|
if "(" in pred_col and ")" in pred_col: |
|
unidad_predicha = pred_col.split("(")[1].split(")")[0].strip() |
|
except: |
|
unidad_predicha = "mg/L" |
|
|
|
|
|
unidad_replicas = "Abs" |
|
concentracion_inicial = 150.0 |
|
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"Absorbancia Real 1 ({unidad_replicas})"] = col_replica_1 |
|
df_sistema[f"Absorbancia Real 2 ({unidad_replicas})"] = col_replica_2 |
|
|
|
return concentracion_inicial, unidad_predicha, n_filas, n_replicas, df_sistema, "", None, "", [], [] |
|
|
|
def actualizar_opciones_filas(df): |
|
if df is None or df.empty: |
|
update = gr.update(choices=[], value=[]) |
|
update_regresion = gr.update(choices=[], value=[]) |
|
else: |
|
opciones = [f"Fila {i+1}" for i in df.index] |
|
|
|
update = gr.update(choices=opciones, value=opciones) |
|
update_regresion = gr.update(choices=opciones, value=opciones) |
|
return update, update_regresion |
|
|
|
def iniciar_con_ejemplo(): |
|
unidad_predicha = "mg/L" |
|
unidad_replicas = "Abs" |
|
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], |
|
"Dimensión de Dilución": [1.00,1.25,1.50,1.67,1.87,2.14,2.50], |
|
f"Concentración Predicha ({unidad_predicha})": [150,120,100,90,80,70,60], |
|
f"Absorbancia Real 1 ({unidad_replicas})": [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, unidad_predicha, unidad_replicas, [f"Fila {i+1}" for i in df.index], 3) |
|
filas = [f"Fila {i+1}" for i in df.index] |
|
return ( |
|
150, |
|
unidad_predicha, |
|
unidad_replicas, |
|
7, |
|
df, |
|
estado, |
|
fig, |
|
informe, |
|
filas, |
|
3 |
|
) |
|
|
|
def recalcular_y_graficar(df, n_replicas, unidad_predicha, unidad_replicas, filas_seleccionadas, decimales, |
|
color_puntos, estilo_puntos, color_linea_ajuste, estilo_linea_ajuste, |
|
color_barras_error, mostrar_linea_ajuste, mostrar_puntos): |
|
estado, fig_base, informe, df = actualizar_analisis(df, n_replicas, unidad_predicha, unidad_replicas, filas_seleccionadas, decimales) |
|
fig_custom = actualizar_graficos_custom(df, n_replicas, unidad_predicha, unidad_replicas, |
|
color_puntos, estilo_puntos, |
|
color_linea_ajuste, estilo_linea_ajuste, |
|
color_barras_error, |
|
mostrar_linea_ajuste, mostrar_puntos, |
|
filas_seleccionadas, decimales) |
|
return estado, fig_custom, informe |
|
|
|
def actualizar_graficos_custom(df, n_replicas, unidad_predicha, unidad_replicas, |
|
color_puntos, estilo_puntos, |
|
color_linea_ajuste, estilo_linea_ajuste, |
|
color_barras_error, |
|
mostrar_linea_ajuste, mostrar_puntos, |
|
filas_seleccionadas, decimales): |
|
if df is None or df.empty: |
|
return None |
|
|
|
df = calcular_promedio_desviacion(df, n_replicas, unidad_predicha, unidad_replicas, decimales) |
|
col_predicha = f"Concentración Predicha ({unidad_predicha})" |
|
col_real_promedio = f"Absorbancia Real Promedio ({unidad_replicas})" |
|
|
|
df[col_predicha] = pd.to_numeric(df[col_predicha], errors='coerce') |
|
df[col_real_promedio] = pd.to_numeric(df[col_real_promedio], errors='coerce') |
|
|
|
df_valid = df.dropna(subset=[col_predicha, 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 if s.startswith('Fila')] |
|
df_valid = df_valid.loc[indices_seleccionados] |
|
|
|
if len(df_valid) < 2: |
|
return None |
|
|
|
fig = generar_graficos( |
|
df_valid, n_replicas, unidad_predicha, unidad_replicas, |
|
color_puntos, estilo_puntos, |
|
color_linea_ajuste, estilo_linea_ajuste, |
|
color_barras_error, |
|
mostrar_linea_ajuste, mostrar_puntos |
|
) |
|
return fig |
|
|
|
def resetear_ajustes(): |
|
return ( |
|
gr.update(value=True), |
|
gr.update(value=True), |
|
gr.update(value="#0000FF"), |
|
gr.update(value="o"), |
|
gr.update(value="#00FF00"), |
|
gr.update(value="-"), |
|
gr.update(value="#FFA500") |
|
) |
|
|
|
with gr.Blocks(theme=gr.themes.Soft()) as interfaz: |
|
gr.Markdown(""" |
|
# 📊 Sistema Avanzado de Calibración con Análisis Estadístico |
|
|
|
Ahora la unidad de medida por defecto es "Abs", tanto al limpiar datos como al cargar ejemplo OD. |
|
""") |
|
|
|
with gr.Tab("📝 Datos de Calibración"): |
|
with gr.Row(): |
|
with gr.Column(): |
|
concentracion_input = gr.Number(value=150,label="Concentración Inicial",precision=0) |
|
unidad_predicha_input = gr.Textbox(value="mg/L",label="Unidad de Medida (Predicha)") |
|
|
|
unidad_replicas_input = gr.Textbox(value="Abs",label="Unidad de Medida (Absorbancias)") |
|
with gr.Column(): |
|
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(): |
|
with gr.Column(): |
|
calcular_btn = gr.Button("🔄 Calcular", variant="primary") |
|
ajustar_decimales_btn = gr.Button("🛠 Ajustar Decimales", variant="secondary") |
|
limpiar_btn = gr.Button("🗑 Limpiar Datos", variant="secondary") |
|
with gr.Column(): |
|
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"): |
|
filas_seleccionadas = gr.CheckboxGroup(label="Seleccione filas para el análisis",choices=[],value=[]) |
|
with gr.Row(): |
|
with gr.Column(): |
|
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") |
|
|
|
with gr.Column(): |
|
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.Column(): |
|
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_puntos = gr.Checkbox(value=True, label="Mostrar Puntos") |
|
|
|
with gr.Row(): |
|
graficar_btn = gr.Button("📊 Graficar", variant="primary") |
|
resetear_btn = gr.Button("🔄 Resetear Ajustes", variant="secondary") |
|
|
|
estado_output = gr.Textbox(label="Estado del Análisis", interactive=False) |
|
graficos_output = gr.Plot(label="Gráficos de Análisis") |
|
informe_output = gr.Markdown() |
|
|
|
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") |
|
|
|
with gr.Tab("📈 Regresión Absorbancia vs Concentración"): |
|
filas_seleccionadas_regresion = gr.CheckboxGroup(label="Seleccione filas para la regresión",choices=[],value=[]) |
|
|
|
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' |
|
) |
|
|
|
titulo_grafico_original = gr.Textbox(label="Título del Gráfico Original") |
|
titulo_grafico_personalizado = gr.Textbox(label="Título del Gráfico Personalizado") |
|
eje_x_original = gr.Textbox(label="Etiqueta Eje X (Original)") |
|
eje_y_original = gr.Textbox(label="Etiqueta Eje Y (Original)") |
|
eje_x_personalizado = gr.Textbox(label="Etiqueta Eje X (Personalizado)") |
|
eje_y_personalizado = gr.Textbox(label="Etiqueta Eje Y (Personalizado)") |
|
|
|
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]) |
|
|
|
def calcular_y_marcar(df, replicas, up, ur, sel, dec): |
|
estado, fig, informe, df = actualizar_analisis(df, replicas, up, ur, sel, dec) |
|
return estado, fig, informe, df |
|
|
|
calcular_btn.click( |
|
fn=calcular_y_marcar, |
|
inputs=[tabla_output, replicas_slider, unidad_predicha_input, unidad_replicas_input, filas_seleccionadas, decimales_slider], |
|
outputs=[estado_output, graficos_output, informe_output, tabla_output] |
|
) |
|
|
|
ajustar_decimales_btn.click( |
|
fn=ajustar_decimales_evento, |
|
inputs=[tabla_output, decimales_slider], |
|
outputs=tabla_output |
|
) |
|
|
|
def limpiar_todo(n_replicas): |
|
c,u_pred,f_slider,df,est,g,inf = limpiar_datos(n_replicas) |
|
return c,u_pred,"Abs",f_slider,df,"",None,"",[],[],3 |
|
|
|
limpiar_btn.click( |
|
fn=limpiar_todo, |
|
inputs=[replicas_slider], |
|
outputs=[concentracion_input, unidad_predicha_input, unidad_replicas_input, filas_slider, tabla_output, estado_output, graficos_output, informe_output, filas_seleccionadas, filas_seleccionadas_regresion, decimales_slider] |
|
) |
|
|
|
|
|
def cargar_ejemplo_od_func(n_replicas): |
|
unidad_predicha = "mg/L" |
|
unidad_replicas = "Abs" |
|
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], |
|
"Dimensión de Dilución": [1.00,1.25,1.67,2.50,5.00,10.00,20.00], |
|
f"Concentración Predicha ({unidad_predicha})": [1.0,0.8,0.6,0.4,0.2,0.1,0.05] |
|
}) |
|
for i in range(1, n_replicas + 1): |
|
df[f"Absorbancia Real {i} ({unidad_replicas})"] = np.nan |
|
return 150, unidad_predicha, unidad_replicas, 7, df |
|
|
|
ejemplo_od_btn.click( |
|
fn=cargar_ejemplo_od_func, |
|
inputs=[replicas_slider], |
|
outputs=[concentracion_input, unidad_predicha_input, unidad_replicas_input, filas_slider, tabla_output] |
|
) |
|
|
|
sinteticos_btn.click( |
|
fn=generar_datos_sinteticos_evento, |
|
inputs=[tabla_output, replicas_slider, unidad_predicha_input, unidad_replicas_input], |
|
outputs=tabla_output |
|
) |
|
|
|
cargar_excel_btn.upload( |
|
fn=cargar_excel, |
|
inputs=[cargar_excel_btn], |
|
outputs=[concentracion_input, unidad_predicha_input, filas_slider, replicas_slider, tabla_output, estado_output, graficos_output, informe_output, filas_seleccionadas, filas_seleccionadas_regresion] |
|
) |
|
|
|
def actualizar_tabla_wrapper(df, filas, conc, up, ur, rep, dec): |
|
return actualizar_tabla_evento(df, filas, conc, up, ur, rep, dec) |
|
|
|
concentracion_input.change(actualizar_tabla_wrapper,[tabla_output,filas_slider,concentracion_input,unidad_predicha_input,unidad_replicas_input,replicas_slider,decimales_slider],tabla_output) |
|
unidad_predicha_input.change(actualizar_tabla_wrapper,[tabla_output,filas_slider,concentracion_input,unidad_predicha_input,unidad_replicas_input,replicas_slider,decimales_slider],tabla_output) |
|
unidad_replicas_input.change(actualizar_tabla_wrapper,[tabla_output,filas_slider,concentracion_input,unidad_predicha_input,unidad_replicas_input,replicas_slider,decimales_slider],tabla_output) |
|
filas_slider.change(actualizar_tabla_wrapper,[tabla_output,filas_slider,concentracion_input,unidad_predicha_input,unidad_replicas_input,replicas_slider,decimales_slider],tabla_output) |
|
replicas_slider.change(actualizar_tabla_wrapper,[tabla_output,filas_slider,concentracion_input,unidad_predicha_input,unidad_replicas_input,replicas_slider,decimales_slider],tabla_output) |
|
decimales_slider.change(ajustar_decimales_evento,[tabla_output,decimales_slider],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_predicha_input, unidad_replicas_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 |
|
) |
|
|
|
graficar_btn.click( |
|
fn=recalcular_y_graficar, |
|
inputs=[tabla_output, replicas_slider, unidad_predicha_input, unidad_replicas_input, filas_seleccionadas, decimales_slider, |
|
color_puntos_picker, estilo_puntos_dropdown, color_linea_ajuste_picker, estilo_linea_ajuste_dropdown, |
|
color_barras_error_picker, mostrar_linea_ajuste, mostrar_puntos], |
|
outputs=[estado_output, graficos_output, informe_output] |
|
) |
|
|
|
resetear_btn.click(fn=resetear_ajustes, outputs=[mostrar_linea_ajuste, mostrar_puntos, color_puntos_picker, estilo_puntos_dropdown, color_linea_ajuste_picker, estilo_linea_ajuste_dropdown, color_barras_error_picker]) |
|
|
|
def calcular_regresion_tabla_principal(df, up, ur, fsr, |
|
cp, ep, cla, ela, |
|
mla, mp, |
|
ll, dec, |
|
tgo, tgp, |
|
exo, eyo, |
|
exp, eyp): |
|
|
|
if df is None or df.empty: |
|
return "Datos insuficientes", None, None, None |
|
|
|
col_predicha = f"Concentración Predicha ({up})" |
|
col_real_promedio = f"Absorbancia Real Promedio ({ur})" |
|
col_desviacion = f"Desviación Estándar ({ur})" |
|
|
|
n_replicas = len([c for c in df.columns if 'Absorbancia Real ' in c and 'Promedio' not in c and 'Desviación' not in c]) |
|
df = calcular_promedio_desviacion(df, n_replicas, up, ur, dec) |
|
|
|
if col_predicha not in df.columns or col_real_promedio not in df.columns: |
|
return "Faltan columnas necesarias", None, None, None |
|
|
|
df[col_predicha] = pd.to_numeric(df[col_predicha], 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_predicha, col_real_promedio]) |
|
df_valid.reset_index(drop=True, inplace=True) |
|
|
|
df_original = df_valid.copy() |
|
|
|
if not fsr or len(fsr) < 2: |
|
return "Se necesitan más datos", None, None, None |
|
|
|
indices_seleccionados = [int(s.split(' ')[1]) - 1 for s in fsr] |
|
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_predicha], df_valid[col_real_promedio]) |
|
|
|
sns.set(style="whitegrid") |
|
fig_original, ax_original = plt.subplots(figsize=(8, 6)) |
|
ax_original.errorbar( |
|
df_original[col_predicha], |
|
df_original[col_real_promedio], |
|
yerr=df_original[col_desviacion], |
|
fmt=ep, |
|
color=safe_color(cp), |
|
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_predicha], df_original[col_real_promedio]) |
|
ax_original.plot( |
|
df_original[col_predicha], |
|
intercept_all + slope_all * df_original[col_predicha], |
|
color=safe_color(cla), |
|
linestyle='-', |
|
label='Ajuste Lineal' |
|
) |
|
|
|
ax_original.set_xlabel(exo if exo else f'Concentración Predicha ({up})') |
|
ax_original.set_ylabel(eyo if eyo else f'Absorbancia Real Promedio ({ur})') |
|
ax_original.set_title(tgo if tgo else 'Regresión Lineal: Absorbancia Real vs Concentración Predicha (Original)') |
|
ax_original.legend(loc=ll) |
|
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' |
|
) |
|
|
|
sns.set(style="whitegrid") |
|
fig_personalizado, ax_personalizado = plt.subplots(figsize=(8, 6)) |
|
|
|
if mp: |
|
ax_personalizado.errorbar( |
|
df_valid[col_predicha], |
|
df_valid[col_real_promedio], |
|
yerr=df_valid[col_desviacion], |
|
fmt=ep, |
|
color=safe_color(cp), |
|
ecolor='gray', |
|
elinewidth=1, |
|
capsize=3, |
|
label='Datos' |
|
) |
|
|
|
if mla: |
|
ax_personalizado.plot( |
|
df_valid[col_predicha], |
|
intercept + slope * df_valid[col_predicha], |
|
color=safe_color(cla), |
|
linestyle=ela, |
|
label='Ajuste Lineal' |
|
) |
|
|
|
ax_personalizado.set_xlabel(exp if exp else f'Concentración Predicha ({up})') |
|
ax_personalizado.set_ylabel(eyp if eyp else f'Absorbancia Real Promedio ({ur})') |
|
ax_personalizado.set_title(tgp if tgp else 'Regresión Lineal Personalizada') |
|
ax_personalizado.legend(loc=ll) |
|
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_predicha, col_real_promedio, col_desviacion]].copy() |
|
df_resumen.columns = [f'Concentración Predicha ({up})', 'Absorbancia Promedio', 'Desviación Estándar'] |
|
|
|
return "Regresión calculada exitosamente", fig_original, fig_personalizado, df_resumen |
|
|
|
calcular_regresion_btn.click( |
|
fn=calcular_regresion_tabla_principal, |
|
inputs=[tabla_output, unidad_predicha_input, unidad_replicas_input, filas_seleccionadas_regresion, |
|
color_puntos_picker, estilo_puntos_dropdown, |
|
color_linea_ajuste_picker, estilo_linea_ajuste_dropdown, |
|
mostrar_linea_ajuste, mostrar_puntos, |
|
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] |
|
) |
|
|
|
interfaz.load( |
|
fn=iniciar_con_ejemplo, |
|
outputs=[concentracion_input, unidad_predicha_input, unidad_replicas_input, filas_slider, tabla_output, estado_output, graficos_output, informe_output, filas_seleccionadas, decimales_slider] |
|
) |
|
|
|
if __name__ == "__main__": |
|
interfaz.launch() |
|
|