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 de replicas por defecto Abs 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, "", # estado None, # graficos "" # informe ) 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 por defecto Abs 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] # Marcamos todas las filas por defecto 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" # Por defecto 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), # mostrar_linea_ajuste gr.update(value=True), # mostrar_puntos gr.update(value="#0000FF"), # color_puntos gr.update(value="o"), # estilo_puntos gr.update(value="#00FF00"), # color_linea_ajuste gr.update(value="-"), # estilo_linea_ajuste gr.update(value="#FFA500") # color_barras_error ) 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)") # Por defecto Abs 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] ) # Cargar ejemplo OD, pero ahora con unidad_replicas = "Abs" def cargar_ejemplo_od_func(n_replicas): unidad_predicha = "mg/L" unidad_replicas = "Abs" # Cambiado a 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()