--- title: "Histórico y monitoreo de deriva" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{Histórico y monitoreo de deriva} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, setup, include=FALSE} knitr::opts_chunk$set(collapse = TRUE, comment = "#>") # El ancho se restaura al terminar la vinneta: knitr corre cada una en su propia # sesion, asi que no se filtra, pero el CRAN Cookbook pide devolver toda opcion # que se cambie y no cuesta nada cumplirlo. .opciones_previas <- options(width = 90) ``` Una medición aislada describe una entrega. Varias corridas permiten saber si la calidad mejora y si cambió la estructura que el modelo esperaba. ```{r} library(lupa) ``` ## Serie de evaluaciones El identificador y la fecha se fijan al medir. Aquí se evalúa la completitud de la misma columna en dos meses. ```{r} nucleo <- metricas_nucleo() instancia <- instanciar( especializar(nucleo$NoNulo, nombre_especifico = "NoNuloDato"), "entrega", "dato" ) modelo_calidad <- modelo(instancia) enero <- medir( modelo_calidad, data.frame(dato = c("A", NA, "C", NA)), id_medicion = "enero", fecha = as.POSIXct("2026-01-31", tz = "UTC") ) febrero <- medir( modelo_calidad, data.frame(dato = c("A", "B", "C", NA)), id_medicion = "febrero", fecha = as.POSIXct("2026-02-28", tz = "UTC") ) enero <- agregar(enero, "atributo", "ratio") febrero <- agregar(febrero, "atributo", "ratio") regla <- regla_evaluacion("Completitud mayor al 60 %", function(x) x > 0.6) perfil <- perfil_evaluacion("Operativo", regla) evaluacion_enero <- evaluar(enero, perfil) evaluacion_febrero <- evaluar(febrero, perfil) comparar_evaluaciones(evaluacion_enero, evaluacion_febrero) ``` `historico_calidad()` normaliza las corridas en un `data.frame` plano y versionado. Puede escribirse como CSV o llevarse a una tabla institucional sin desarmar listas anidadas. La configuración de cada corrida —la tabla, el marco, los tipos de resultado— viaja en las columnas `identidad_tabla` y `configuracion_*` de las filas `evaluacion_perfil`, así que un histórico releído de un CSV separa las tablas y corta los cambios de marco igual que el objeto. `comparar_evaluaciones()` publica el mismo delta que la deriva, y en `comparacion` dice por qué falta cuando las corridas no se pueden comparar. ```{r} historico <- historico_calidad(evaluacion_enero, evaluacion_febrero) historico[, c("id_medicion", "fecha", "nivel", "resultado")] detectar_deriva_calidad(historico, umbral = 0.05) ``` `acumular_historico()` existe para el flujo incremental: amplía una serie ya guardada y no duplica una corrida idéntica. ```{r} historico_incremental <- acumular_historico( historico_calidad(evaluacion_enero), evaluacion_febrero ) identical(historico, historico_incremental) ``` La persistencia usa RDS y no agrega dependencias. El archivo no se sobrescribe sin consentimiento. ```{r} archivo <- tempfile(fileext = ".rds") guardar_historico(historico, archivo) recuperado <- leer_historico(archivo) identical(historico, recuperado) unlink(archivo) ``` ## Deriva estructural entre entregas `comparar_perfiles()` distingue cambios de esquema y cambios sobre columnas comparables. Informa columnas nuevas o retiradas, tipos, ausencias, cardinalidad, rangos, patrones y hallazgos. ```{r} data(datos_administrativos) entrega_enero <- datos_administrativos entrega_febrero <- datos_administrativos entrega_febrero$cedula[1] <- "12345678" entrega_febrero$nueva_columna <- "nuevo" perfil_enero <- perfilar( entrega_enero, fecha = as.POSIXct("2026-01-31", tz = "UTC") ) perfil_febrero <- perfilar( entrega_febrero, fecha = as.POSIXct("2026-02-28", tz = "UTC") ) deriva <- comparar_perfiles(perfil_enero, perfil_febrero) deriva[, c("columna", "aspecto", "cambio", "severidad")] ``` Una comparación contra el mismo perfil produce cero cambios. Las columnas que sólo existen en un lado se informan como cambios estructurales; no hacen fallar la comparación de las demás. Lo que la protección de datos personales tapa no se compara, pero tampoco se calla: el rango de `cedula` o de `correo` queda declarado, por columna y lado, en el atributo `campos_protegidos`. Si la protección de una columna cambia entre las entregas, ese cambio sale con su propia fila, `aspecto = "proteccion"`. ```{r} attr(deriva, "campos_protegidos") ``` Las configuraciones que contienen nombres o texto del usuario se convierten en claves por bytes, no por la marca de codificación ni por la intercalación del locale. Por eso la comparación sigue siendo estable si el perfil se guarda con `saveRDS()` y se relee bajo otro locale. La misma regla alcanza la clave interna que agrupa hallazgos: dos perfiles idénticos no emiten avisos espurios bajo LC_CTYPE = C. La misma separación se usa al redactar sugerencias: los nombres de la tabla se conservan tal como llegaron y sólo la copia destinada al texto se marca como UTF-8. Así el código publicado no cambia al pasar de un locale UTF-8 a `C`. ```{r} nrow(comparar_perfiles(perfil_enero, perfil_enero)) ``` ## Equivalencia de resúmenes `comparar_equivalencia()` devuelve una fila por columna y campo compartidos. Compara con igualdad exacta los conteos y usa la tolerancia declarada por quien llama sólo para los campos flotantes. ```{r equivalencia} set.seed(20260831) datos_equivalencia <- data.frame( monto = c(10, 20, 30, 40), stringsAsFactors = FALSE ) perfil_equivalencia_anterior <- perfilar( datos_equivalencia, muestra = Inf, analizar_dependencias = FALSE ) perfil_equivalencia_actual <- perfilar( datos_equivalencia, muestra = Inf, analizar_dependencias = FALSE ) fila_monto <- match( "monto", perfil_equivalencia_actual$columnas$columna ) perfil_equivalencia_actual$columnas$media[fila_monto] <- perfil_equivalencia_actual$columnas$media[fila_monto] + 0.001 perfil_equivalencia_actual$columnas$n[fila_monto] <- perfil_equivalencia_actual$columnas$n[fila_monto] + 1L tolerancia_llamador <- 0.01 equivalencia <- comparar_equivalencia( perfil_equivalencia_anterior, perfil_equivalencia_actual, tolerancia = tolerancia_llamador ) campos_mostrados <- c("n", "n_faltantes", "media") equivalencia_monto <- equivalencia[ equivalencia$columna == "monto" & equivalencia$campo %in% campos_mostrados, , drop = FALSE ] equivalencia_monto <- equivalencia_monto[ match(campos_mostrados, equivalencia_monto$campo), c("campo", "valor_anterior", "valor_actual", "veredicto", "motivo", "tipo_eje", "tolerancia") ] equivalencia_monto ``` La salida muestra los tres veredictos y el motivo de cada fila; la tolerancia es del llamador y viaja ecoada en `tolerancia`. En los ejes exactos, `equivalente` es inalcanzable: el conteo cambiado permanece `materialmente_distinto` con cualquier tolerancia válida. ```{r equivalencia-eje-exacto} tolerancias <- c(0, tolerancia_llamador, 1e12) veredictos_conteo <- vapply(tolerancias, function(tolerancia) { prueba <- comparar_equivalencia( perfil_equivalencia_anterior, perfil_equivalencia_actual, tolerancia = tolerancia ) as.character(prueba$veredicto[ prueba$columna == "monto" & prueba$campo == "n" ]) }, character(1)) data.frame(tolerancia = tolerancias, veredicto_n = veredictos_conteo) stopifnot(all(veredictos_conteo == "materialmente_distinto")) ``` El invariante es deliberado: devuelve datos, no decisiones. ```{r, cierre, include=FALSE} options(.opciones_previas) ```