|
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 |
|
|
|
def generar_tabla(n_filas, concentracion_inicial, unidad_medida, n_replicas): |
|
valores_base = [1.000, 0.800, 0.600, 0.400, 0.200, 0.100, 0.050] |
|
|
|
if n_filas <= 7: |
|
solucion_inoculo = valores_base[:n_filas] |
|
agua = [round(1 - x, 3) for x in solucion_inoculo] |
|
else: |
|
solucion_inoculo = valores_base.copy() |
|
ultimo_valor = valores_base[-1] |
|
for _ in range(n_filas - 7): |
|
nuevo_valor = round(ultimo_valor / 2, 3) |
|
solucion_inoculo.append(nuevo_valor) |
|
ultimo_valor = nuevo_valor |
|
agua = [round(1 - x, 3) for x in solucion_inoculo] |
|
|
|
data = { |
|
f"Solución de inóculo ({concentracion_inicial} {unidad_medida})": solucion_inoculo, |
|
"H2O": agua |
|
} |
|
df = pd.DataFrame(data) |
|
|
|
nombre_columna = f"Solución de inóculo ({concentracion_inicial} {unidad_medida})" |
|
df["Factor de Dilución"] = df[nombre_columna].apply(lambda x: round(1 / x, 3)) |
|
df["Concentración Predicha Numérica"] = df["Factor de Dilución"].apply( |
|
lambda x: concentracion_inicial / x |
|
) |
|
df[f"Concentración Predicha ({unidad_medida})"] = df["Concentración Predicha Numérica"].round(3).astype(str) |
|
|
|
|
|
for i in range(1, n_replicas + 1): |
|
df[f"Concentración Real {i} ({unidad_medida})"] = None |
|
|
|
|
|
return df |
|
|
|
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 = [f"Concentración Real {i} ({unidad_medida})" for i in range(1, n_replicas + 1)] |
|
|
|
for col in col_replicas: |
|
df[col] = pd.to_numeric(df[col], errors='coerce') |
|
|
|
|
|
df[f"Concentración Real Promedio ({unidad_medida})"] = df[col_replicas].mean(axis=1) |
|
|
|
if n_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, palette_puntos, estilo_puntos, |
|
palette_linea_ajuste, estilo_linea_ajuste, |
|
palette_linea_ideal, estilo_linea_ideal, |
|
palette_barras_error, mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos): |
|
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] = df_valid[col_predicha_num].astype(float) |
|
df_valid[col_real_promedio] = df_valid[col_real_promedio].astype(float) |
|
df_valid[col_desviacion] = df_valid[col_desviacion].fillna(0).astype(float) |
|
|
|
|
|
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)) |
|
|
|
|
|
colors_puntos = sns.color_palette(palette_puntos, as_cmap=False) |
|
colors_linea_ajuste = sns.color_palette(palette_linea_ajuste, as_cmap=False) |
|
colors_linea_ideal = sns.color_palette(palette_linea_ideal, as_cmap=False) |
|
colors_barras_error = sns.color_palette(palette_barras_error, as_cmap=False) |
|
|
|
|
|
color_puntos = colors_puntos[0] |
|
color_linea_ajuste = colors_linea_ajuste[0] |
|
color_linea_ideal = colors_linea_ideal[0] |
|
color_barras_error = colors_barras_error[0] |
|
|
|
|
|
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) |
|
|
|
|
|
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): |
|
"""Evaluar la calidad de la calibración y proporcionar recomendaciones""" |
|
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") |
|
|
|
if rmse > 0.1 * df_valid[df_valid.columns[-1]].astype(float).mean(): |
|
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): |
|
"""Generar un informe completo en formato markdown""" |
|
col_predicha_num = "Concentración Predicha Numérica" |
|
col_real_promedio = f"Concentración Real Promedio ({unidad_medida})" |
|
|
|
|
|
df_valid[col_predicha_num] = df_valid[col_predicha_num].astype(float) |
|
df_valid[col_real_promedio] = df_valid[col_real_promedio].astype(float) |
|
|
|
|
|
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 |
|
|
|
|
|
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] |
|
|
|
|
|
fig = generar_graficos( |
|
df_valid, n_replicas, unidad_medida, |
|
palette_puntos='deep', estilo_puntos='o', |
|
palette_linea_ajuste='muted', estilo_linea_ajuste='-', |
|
palette_linea_ideal='bright', estilo_linea_ideal='--', |
|
palette_barras_error='pastel', |
|
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 actualizar_graficos(df, n_replicas, unidad_medida, |
|
palette_puntos, estilo_puntos, |
|
palette_linea_ajuste, estilo_linea_ajuste, |
|
palette_linea_ideal, estilo_linea_ideal, |
|
palette_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, |
|
palette_puntos, estilo_puntos, |
|
palette_linea_ajuste, estilo_linea_ajuste, |
|
palette_linea_ideal, estilo_linea_ideal, |
|
palette_barras_error, |
|
mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos |
|
) |
|
|
|
return fig |
|
|
|
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 = 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') |
|
|
|
df_valid = df_valid.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 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] |
|
|
|
|
|
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 = df_valid.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 df_valid.empty: |
|
return None |
|
|
|
filename = exportar_informe_latex(df_valid, informe_md) |
|
|
|
return filename |
|
|
|
def cargar_ejemplo_ufc(n_replicas): |
|
df = generar_tabla(7, 2000000, "UFC", n_replicas) |
|
|
|
for i in range(1, n_replicas + 1): |
|
valores_reales = [2000000 - (i - 1) * 10000, 1600000 - (i - 1) * 8000, 1200000 - (i - 1) * 6000, |
|
800000 - (i - 1) * 4000, 400000 - (i - 1) * 2000, 200000 - (i - 1) * 1000, |
|
100000 - (i - 1) * 500] |
|
df[f"Concentración Real {i} (UFC)"] = valores_reales |
|
return 2000000, "UFC", 7, df |
|
|
|
def cargar_ejemplo_od(n_replicas): |
|
df = generar_tabla(7, 1.000, "OD", n_replicas) |
|
|
|
for i in range(1, n_replicas + 1): |
|
valores_reales = [1.000 - (i - 1) * 0.050, 0.800 - (i - 1) * 0.040, 0.600 - (i - 1) * 0.030, |
|
0.400 - (i - 1) * 0.020, 0.200 - (i - 1) * 0.010, 0.100 - (i - 1) * 0.005, |
|
0.050 - (i - 1) * 0.002] |
|
df[f"Concentración Real {i} (OD)"] = valores_reales |
|
return 1.000, "OD", 7, df |
|
|
|
def limpiar_datos(n_replicas): |
|
df = generar_tabla(7, 2000000, "UFC", n_replicas) |
|
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" |
|
|
|
|
|
for i in range(1, n_replicas + 1): |
|
col_real = f"Concentración Real {i} ({unidad_medida})" |
|
df[col_predicha_num] = pd.to_numeric(df[col_predicha_num], errors='coerce') |
|
desviacion_std = 0.05 * df[col_predicha_num].mean() |
|
valores_predichos = df[col_predicha_num].astype(float).values |
|
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[col_real] = datos_sinteticos |
|
|
|
return df |
|
|
|
def actualizar_tabla_evento(df, n_filas, concentracion, unidad, n_replicas, decimales): |
|
|
|
df_new = generar_tabla(n_filas, concentracion, unidad, n_replicas) |
|
|
|
|
|
col_real_new = [col for col in df_new.columns if 'Concentración Real' in col and 'Promedio' not in col and 'Desviación' not in col] |
|
col_real_old = [col for col in df.columns if 'Concentración Real' in col and 'Promedio' not in col and 'Desviación' not in col] |
|
|
|
|
|
for col_new, col_old in zip(col_real_new, col_real_old): |
|
df_new[col_new] = None |
|
for idx in df_new.index: |
|
if idx in df.index: |
|
df_new.at[idx, col_new] = df.at[idx, col_old] |
|
|
|
|
|
df_new = ajustar_decimales_evento(df_new, decimales) |
|
|
|
return df_new |
|
|
|
def cargar_excel(file): |
|
|
|
df = pd.read_excel(file.name, sheet_name=None) |
|
|
|
|
|
if len(df) < 2: |
|
return "El archivo debe tener al menos dos pestañas.", None, None, None, None, None, None |
|
|
|
|
|
primera_pestaña = next(iter(df.values())) |
|
concentracion_inicial = primera_pestaña.iloc[0, 0] |
|
unidad_medida = primera_pestaña.columns[0].split('(')[-1].split(')')[0] |
|
n_filas = len(primera_pestaña) |
|
n_replicas = len(df) |
|
|
|
|
|
df_base = generar_tabla(n_filas, concentracion_inicial, unidad_medida, n_replicas) |
|
|
|
|
|
for i, (sheet_name, sheet_df) in enumerate(df.items(), start=1): |
|
col_real = f"Concentración Real {i} ({unidad_medida})" |
|
df_base[col_real] = sheet_df.iloc[:, 1].values |
|
|
|
return concentracion_inicial, unidad_medida, n_filas, n_replicas, df_base, "", None, "" |
|
|
|
def calcular_regresion_tabla_principal(df, unidad_medida, filas_seleccionadas_regresion, |
|
palette_puntos, estilo_puntos, |
|
palette_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 |
|
|
|
col_concentracion = "Concentración Predicha Numérica" |
|
col_absorbancia = f"Concentración Real Promedio ({unidad_medida})" |
|
col_desviacion = f"Desviación Estándar ({unidad_medida})" |
|
|
|
|
|
n_replicas = len([col for col in df.columns if 'Concentración Real' in col and 'Promedio' not in col and 'Desviación' not in col]) |
|
df = calcular_promedio_desviacion(df, n_replicas, unidad_medida, decimales) |
|
|
|
|
|
df[col_concentracion] = pd.to_numeric(df[col_concentracion], errors='coerce') |
|
df[col_absorbancia] = pd.to_numeric(df[col_absorbancia], errors='coerce') |
|
df[col_desviacion] = pd.to_numeric(df[col_desviacion], errors='coerce') |
|
|
|
df_valid = df.dropna(subset=[col_concentracion, col_absorbancia]) |
|
|
|
|
|
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] |
|
|
|
|
|
df_valid = df_valid.loc[indices_seleccionados] |
|
|
|
if len(df_valid) < 2: |
|
return "Se requieren al menos dos puntos para calcular la regresión", None, None, None |
|
|
|
|
|
slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_concentracion], df_valid[col_absorbancia]) |
|
|
|
|
|
sns.set(style="whitegrid") |
|
fig_original, ax_original = plt.subplots(figsize=(8, 6)) |
|
|
|
ax_original.errorbar( |
|
df_original[col_concentracion], |
|
df_original[col_absorbancia], |
|
yerr=df_original[col_desviacion], |
|
fmt='o', |
|
color='blue', |
|
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_absorbancia]) |
|
|
|
ax_original.plot( |
|
df_original[col_concentracion], |
|
intercept_all + slope_all * df_original[col_concentracion], |
|
color='red', |
|
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' |
|
) |
|
|
|
|
|
sns.set(style="whitegrid") |
|
fig_personalizado, ax_personalizado = plt.subplots(figsize=(8, 6)) |
|
|
|
|
|
colors_puntos = sns.color_palette(palette_puntos, as_cmap=False) |
|
colors_linea_ajuste = sns.color_palette(palette_linea_ajuste, as_cmap=False) |
|
|
|
color_puntos = colors_puntos[0] |
|
color_linea_ajuste = colors_linea_ajuste[0] |
|
|
|
if mostrar_puntos: |
|
ax_personalizado.errorbar( |
|
df_valid[col_concentracion], |
|
df_valid[col_absorbancia], |
|
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_absorbancia, 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 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 |
|
|
|
|
|
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(): |
|
|
|
paletas_colores = ["deep", "muted", "pastel", "bright", "dark", "colorblind", "Set1", "Set2", "Set3"] |
|
|
|
palette_puntos_dropdown = gr.Dropdown( |
|
choices=paletas_colores, |
|
value="deep", |
|
label="Paleta para Puntos" |
|
) |
|
estilo_puntos_dropdown = gr.Dropdown( |
|
choices=["o", "s", "^", "D", "v", "<", ">", "h", "H", "p", "*", "X", "d"], |
|
value="o", |
|
label="Estilo de Puntos" |
|
) |
|
palette_linea_ajuste_dropdown = gr.Dropdown( |
|
choices=paletas_colores, |
|
value="muted", |
|
label="Paleta Línea de Ajuste" |
|
) |
|
estilo_linea_ajuste_dropdown = gr.Dropdown( |
|
choices=["-", "--", "-.", ":"], |
|
value="-", |
|
label="Estilo Línea de Ajuste" |
|
) |
|
|
|
with gr.Row(): |
|
palette_linea_ideal_dropdown = gr.Dropdown( |
|
choices=paletas_colores, |
|
value="bright", |
|
label="Paleta Línea Ideal" |
|
) |
|
estilo_linea_ideal_dropdown = gr.Dropdown( |
|
choices=["--", "-", "-.", ":"], |
|
value="--", |
|
label="Estilo Línea Ideal" |
|
) |
|
palette_barras_error_dropdown = gr.Dropdown( |
|
choices=paletas_colores, |
|
value="pastel", |
|
label="Paleta Barras de Error" |
|
) |
|
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") |
|
|
|
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(): |
|
paletas_colores = ["deep", "muted", "pastel", "bright", "dark", "colorblind", "Set1", "Set2", "Set3"] |
|
palette_puntos_regresion = gr.Dropdown( |
|
choices=paletas_colores, |
|
value="deep", |
|
label="Paleta para Puntos" |
|
) |
|
estilo_puntos_regresion = gr.Dropdown( |
|
choices=["o", "s", "^", "D", "v", "<", ">", "h", "H", "p", "*", "X", "d"], |
|
value="o", |
|
label="Estilo de Puntos" |
|
) |
|
palette_linea_ajuste_regresion = gr.Dropdown( |
|
choices=paletas_colores, |
|
value="muted", |
|
label="Paleta Línea de Ajuste" |
|
) |
|
estilo_linea_ajuste_regresion = gr.Dropdown( |
|
choices=["-", "--", "-.", ":"], |
|
value="-", |
|
label="Estilo Línea de Ajuste" |
|
) |
|
mostrar_linea_ajuste_regresion = gr.Checkbox(value=True, label="Mostrar Línea de Ajuste") |
|
mostrar_puntos_regresion = gr.Checkbox(value=True, label="Mostrar Puntos") |
|
|
|
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=f"Concentración Real Promedio ({unidad_input.value})" |
|
) |
|
|
|
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=f"Concentración Real Promedio ({unidad_input.value})" |
|
) |
|
|
|
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] |
|
) |
|
|
|
|
|
graficar_btn.click( |
|
fn=actualizar_graficos, |
|
inputs=[ |
|
tabla_output, replicas_slider, unidad_input, |
|
palette_puntos_dropdown, estilo_puntos_dropdown, |
|
palette_linea_ajuste_dropdown, estilo_linea_ajuste_dropdown, |
|
palette_linea_ideal_dropdown, estilo_linea_ideal_dropdown, |
|
palette_barras_error_dropdown, |
|
mostrar_linea_ajuste, mostrar_linea_ideal, mostrar_puntos, |
|
filas_seleccionadas, decimales_slider |
|
], |
|
outputs=graficos_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] |
|
) |
|
|
|
|
|
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 |
|
) |
|
|
|
|
|
def iniciar_con_ejemplo(): |
|
n_replicas = 1 |
|
df = generar_tabla(7, 2000000, "UFC", n_replicas) |
|
|
|
df[f"Concentración Real 1 (UFC)"] = [2000000, 1600000, 1200000, 800000, 400000, 200000, 100000] |
|
|
|
df = calcular_promedio_desviacion(df, n_replicas, "UFC", 3) |
|
filas_seleccionadas_inicial = [f"Fila {i+1}" for i in df.index] |
|
estado, fig, informe, df = actualizar_analisis(df, n_replicas, "UFC", filas_seleccionadas_inicial, 3) |
|
return ( |
|
2000000, |
|
"UFC", |
|
7, |
|
df, |
|
estado, |
|
fig, |
|
informe, |
|
filas_seleccionadas_inicial, |
|
3 |
|
) |
|
|
|
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, |
|
palette_puntos_regresion, estilo_puntos_regresion, |
|
palette_linea_ajuste_regresion, 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() |
|
|