|
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): |
|
valores_base = [1.00, 0.80, 0.60, 0.40, 0.20, 0.10, 0.05] |
|
|
|
if n_filas <= 7: |
|
solucion_inoculo = valores_base[:n_filas] |
|
agua = [round(1 - x, 2) 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, 2)) |
|
df[f"Concentración Predicha ({unidad_medida})"] = df["Factor de Dilución"].apply( |
|
lambda x: round(concentracion_inicial / x, 0) |
|
) |
|
|
|
df[f"Concentración Real ({unidad_medida})"] = None |
|
|
|
return df |
|
|
|
def generar_datos_sinteticos(df, desviacion_std): |
|
col_predicha = [col for col in df.columns if 'Predicha' in col][0] |
|
col_real = [col for col in df.columns if 'Real' in col][0] |
|
|
|
|
|
valores_predichos = df[col_predicha].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, 2) |
|
|
|
df[col_real] = datos_sinteticos |
|
|
|
return df |
|
|
|
def generar_graficos(df_valid): |
|
col_predicha = [col for col in df_valid.columns if 'Predicha' in col][0] |
|
col_real = [col for col in df_valid.columns if 'Real' in col][0] |
|
|
|
|
|
slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_predicha], df_valid[col_real]) |
|
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)) |
|
|
|
|
|
sns.scatterplot( |
|
data=df_valid, |
|
x=col_predicha, |
|
y=col_real, |
|
ax=ax1, |
|
color='blue', |
|
s=100, |
|
label='Datos Reales', |
|
marker='o' |
|
) |
|
|
|
|
|
sns.lineplot( |
|
x=df_valid[col_predicha], |
|
y=df_valid['Ajuste Lineal'], |
|
ax=ax1, |
|
color='green', |
|
label='Ajuste Lineal', |
|
linewidth=2 |
|
) |
|
|
|
|
|
ax1.plot( |
|
[df_valid[col_predicha].min(), df_valid[col_predicha].max()], |
|
[df_valid[col_predicha].min(), df_valid[col_predicha].max()], |
|
color='red', |
|
linestyle='--', |
|
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', fontsize=12) |
|
|
|
|
|
ax1.annotate( |
|
f'y = {intercept:.2f} + {slope:.2f}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] - df_valid['Ajuste Lineal'] |
|
sns.scatterplot( |
|
data=df_valid, |
|
x=col_predicha, |
|
y=residuos, |
|
ax=ax2, |
|
color='purple', |
|
s=100, |
|
marker='D', |
|
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]].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): |
|
"""Generar un informe completo en formato markdown""" |
|
col_predicha = [col for col in df_valid.columns if 'Predicha' in col][0] |
|
col_real = [col for col in df_valid.columns if 'Real' in col][0] |
|
|
|
|
|
slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_predicha], df_valid[col_real]) |
|
r_squared = r_value ** 2 |
|
rmse = np.sqrt(((df_valid[col_real] - df_valid['Ajuste Lineal']) ** 2).mean()) |
|
cv = (df_valid['Ajuste Lineal'].std() / df_valid['Ajuste Lineal'].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): |
|
if df is None or df.empty: |
|
return "Error en los datos", None, "No se pueden generar análisis" |
|
|
|
col_predicha = [col for col in df.columns if 'Predicha' in col][0] |
|
col_real = [col for col in df.columns if 'Real' in col][0] |
|
|
|
|
|
df[col_predicha] = pd.to_numeric(df[col_predicha], errors='coerce') |
|
df[col_real] = pd.to_numeric(df[col_real], errors='coerce') |
|
|
|
df_valid = df.dropna(subset=[col_predicha, col_real]) |
|
|
|
if len(df_valid) < 2: |
|
return "Se necesitan más datos", None, "Se requieren al menos dos valores reales para el análisis" |
|
|
|
|
|
slope, intercept, r_value, p_value, std_err = stats.linregress(df_valid[col_predicha], df_valid[col_real]) |
|
df_valid['Ajuste Lineal'] = intercept + slope * df_valid[col_predicha] |
|
|
|
fig = generar_graficos(df_valid) |
|
informe, estado = generar_informe_completo(df_valid) |
|
|
|
return estado, fig, informe |
|
|
|
def exportar_informe_word(df_valid, informe_md): |
|
|
|
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 |
|
|
|
|
|
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'] |
|
|
|
|
|
doc.save('informe_calibracion.docx') |
|
return 'informe_calibracion.docx' |
|
|
|
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} |
|
""" |
|
with open('informe_calibracion.tex', 'w') as f: |
|
f.write(informe_tex) |
|
return 'informe_calibracion.tex' |
|
|
|
|
|
def cargar_ejemplo_ufc(): |
|
df = generar_tabla(7, 2000000, "UFC") |
|
valores_reales = [2000000, 1600000, 1200000, 800000, 400000, 200000, 100000] |
|
df[f"Concentración Real (UFC)"] = valores_reales |
|
return 2000000, "UFC", 7, df |
|
|
|
def cargar_ejemplo_od(): |
|
df = generar_tabla(7, 1.0, "OD") |
|
valores_reales = [1.000, 0.800, 0.600, 0.400, 0.200, 0.100, 0.050] |
|
df[f"Concentración Real (OD)"] = valores_reales |
|
return 1.0, "OD", 7, df |
|
|
|
|
|
def actualizar_tabla_evento(n_filas, concentracion, unidad): |
|
df = generar_tabla(n_filas, concentracion, unidad) |
|
return df |
|
|
|
|
|
def limpiar_datos(): |
|
df = generar_tabla(7, 2000000, "UFC") |
|
return ( |
|
2000000, |
|
"UFC", |
|
7, |
|
df, |
|
"", |
|
None, |
|
"" |
|
) |
|
|
|
|
|
def generar_datos_sinteticos_evento(df): |
|
df = df.copy() |
|
desviacion_std = 0.05 * df[df.columns[-2]].mean() |
|
df = generar_datos_sinteticos(df, desviacion_std) |
|
return df |
|
|
|
|
|
def copiar_informe(informe): |
|
return informe |
|
|
|
def exportar_word(df, informe_md): |
|
df_valid = df.copy() |
|
col_predicha = [col for col in df_valid.columns if 'Predicha' in col][0] |
|
col_real = [col for col in df_valid.columns if 'Real' in col][0] |
|
|
|
|
|
df_valid[col_predicha] = pd.to_numeric(df_valid[col_predicha], errors='coerce') |
|
df_valid[col_real] = pd.to_numeric(df_valid[col_real], errors='coerce') |
|
|
|
df_valid = df_valid.dropna(subset=[col_predicha, col_real]) |
|
|
|
if df_valid.empty: |
|
return None |
|
|
|
filename = exportar_informe_word(df_valid, informe_md) |
|
return gr.File.update(value=filename, visible=True) |
|
|
|
def exportar_latex(df, informe_md): |
|
df_valid = df.copy() |
|
col_predicha = [col for col in df_valid.columns if 'Predicha' in col][0] |
|
col_real = [col for col in df_valid.columns if 'Real' in col][0] |
|
|
|
|
|
df_valid[col_predicha] = pd.to_numeric(df_valid[col_predicha], errors='coerce') |
|
df_valid[col_real] = pd.to_numeric(df_valid[col_real], errors='coerce') |
|
|
|
df_valid = df_valid.dropna(subset=[col_predicha, col_real]) |
|
|
|
if df_valid.empty: |
|
return None |
|
|
|
filename = exportar_informe_latex(df_valid, informe_md) |
|
return gr.File.update(value=filename, visible=True) |
|
|
|
|
|
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" |
|
) |
|
|
|
with gr.Row(): |
|
calcular_btn = gr.Button("🔄 Calcular", variant="primary") |
|
limpiar_btn = gr.Button("🗑 Limpiar Datos", 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") |
|
|
|
tabla_output = gr.DataFrame( |
|
row_count=(1, "dynamic"), |
|
col_count=(5, "fixed"), |
|
wrap=True, |
|
label="Tabla de Datos", |
|
interactive=True, |
|
datatype=["number", "number", "number", "number", "number"], |
|
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") |
|
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") |
|
|
|
|
|
input_components = [tabla_output] |
|
output_components = [estado_output, graficos_output, informe_output] |
|
|
|
|
|
calcular_btn.click( |
|
fn=actualizar_analisis, |
|
inputs=tabla_output, |
|
outputs=output_components |
|
) |
|
|
|
|
|
limpiar_btn.click( |
|
fn=limpiar_datos, |
|
inputs=[], |
|
outputs=[concentracion_input, unidad_input, filas_slider, tabla_output, estado_output, graficos_output, informe_output] |
|
) |
|
|
|
|
|
ejemplo_ufc_btn.click( |
|
fn=cargar_ejemplo_ufc, |
|
outputs=[concentracion_input, unidad_input, filas_slider, tabla_output] |
|
) |
|
|
|
ejemplo_od_btn.click( |
|
fn=cargar_ejemplo_od, |
|
outputs=[concentracion_input, unidad_input, filas_slider, tabla_output] |
|
) |
|
|
|
|
|
sinteticos_btn.click( |
|
fn=generar_datos_sinteticos_evento, |
|
inputs=tabla_output, |
|
outputs=tabla_output |
|
) |
|
|
|
|
|
concentracion_input.change( |
|
fn=actualizar_tabla_evento, |
|
inputs=[filas_slider, concentracion_input, unidad_input], |
|
outputs=tabla_output |
|
) |
|
|
|
unidad_input.change( |
|
fn=actualizar_tabla_evento, |
|
inputs=[filas_slider, concentracion_input, unidad_input], |
|
outputs=tabla_output |
|
) |
|
|
|
filas_slider.change( |
|
fn=actualizar_tabla_evento, |
|
inputs=[filas_slider, concentracion_input, unidad_input], |
|
outputs=tabla_output |
|
) |
|
|
|
|
|
copiar_btn.click( |
|
fn=copiar_informe, |
|
inputs=[informe_output], |
|
outputs=[] |
|
) |
|
|
|
|
|
exportar_word_btn.click( |
|
fn=exportar_word, |
|
inputs=[tabla_output, informe_output], |
|
outputs=exportar_word_btn |
|
) |
|
|
|
exportar_latex_btn.click( |
|
fn=exportar_latex, |
|
inputs=[tabla_output, informe_output], |
|
outputs=exportar_latex_btn |
|
) |
|
|
|
|
|
def iniciar_con_ejemplo(): |
|
df = generar_tabla(7, 2000000, "UFC") |
|
valores_reales = [2000000, 1600000, 1200000, 800000, 400000, 200000, 100000] |
|
df[f"Concentración Real (UFC)"] = valores_reales |
|
estado, fig, informe = actualizar_analisis(df) |
|
return ( |
|
2000000, |
|
"UFC", |
|
7, |
|
df, |
|
estado, |
|
fig, |
|
informe |
|
) |
|
|
|
interfaz.load( |
|
fn=iniciar_con_ejemplo, |
|
outputs=[concentracion_input, unidad_input, filas_slider, tabla_output, estado_output, graficos_output, informe_output] |
|
) |
|
|
|
|
|
interfaz.launch() |
|
|