--- title: "3. Explorar columnas con `analizar()`" output: rmarkdown::html_vignette vignette: > %\VignetteIndexEntry{3. Explorar columnas con `analizar()`} %\VignetteEngine{knitr::rmarkdown} %\VignetteEncoding{UTF-8} --- ```{r, setup, include=FALSE} library(lupa) knitr::opts_chunk$set(collapse = TRUE, comment = "#>") # El ancho se restaura al terminar la viñeta: 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) ``` El perfil dice qué está mal en una tabla. Antes de decidir qué le exigimos, también queremos saber qué hay en cada columna: qué forma tienen los valores, qué tipo esconden, cómo se reparten, qué columnas se mueven juntas y qué período cubren las fechas. ¿Cómo lo vemos todo de una vez, y cómo lo compartimos? > **En esta guía:** > > - qué arma `analizar()` en una sola llamada y dónde queda cada pieza; > - cómo se leen patrones, tipos, fechas, escalas, distribuciones, asociaciones y tiempo; > - qué mide `analizar()` por su cuenta y cómo se apaga; > - cuándo conviene `perfilar()` y cuándo `analizar()`; > - cómo guardamos el análisis y producimos un informe HTML. Seguimos con `datos_operativos`, la entrega de `r nrow(datos_operativos)` registros con defectos sembrados que presenta [1. Empezar en diez minutos](lupa.html). ## Una llamada, todos los componentes Fijamos `fecha` para que el ejemplo dé siempre el mismo resultado. ```{r} library(lupa) analisis <- analizar( datos_operativos, nombre = "entrega de ejemplo", fecha = as.POSIXct("2026-01-15", tz = "UTC") ) names(analisis) ``` Cada componente es una tabla o una lista de tablas, y casi todos se pueden obtener sueltos con su propia función: | componente | qué contiene | función suelta | | --- | --- | --- | | `perfil` | hallazgos, resumen por columna, patrones, formatos de fecha, cobertura de diagnósticos | `perfilar()` | | `variables` | tipo implícito, escala y rol propuestos para cada columna | `clasificar_variables()` | | `distribuciones` | frecuencias, cuantiles y su alcance | `distribucion_valores()` | | `asociaciones` | una medida por cada par de columnas que se mueven juntas | `detectar_asociaciones()` | | `temporal` | período observado, huecos y frecuencia propuesta | `analizar_tiempo()` | | `propuesta_modelo`, `decision_medicion` | las métricas sugeridas y cuáles se midieron | `proponer_modelo()` | | `tablero`, `medicion` | la medición agregada | `medir()`, `agregar()`, `tablero_calidad()` | | `cobertura` | qué factores del marco de calidad quedaron medidos | `cobertura_analisis()` | | `plan_limpieza` | acciones posibles, ninguna aplicada | `planificar_limpieza()` | | `advertencias` | lo que el análisis propuso y queda por confirmar | — | | `meta` | nombre, fecha y qué decisiones tomó la corrida | — | Los accesores del perfil también leen el análisis: `hallazgos(analisis)` es lo mismo que `hallazgos(analisis$perfil)`. Miramos los de una columna que vamos a seguir en toda la guía, `fecha_evento`: ```{r} h <- hallazgos(analisis) h[h$columna %in% "fecha_evento", c("tipo_hallazgo", "severidad", "n_afectados")] ``` Cómo se lee cada hallazgo está en [2. Leer un perfil](leer-un-perfil.html). Acá miramos lo que hay debajo: los valores que los producen. ## Patrones: la forma de los valores Un patrón resume la forma de un valor: `A` es una letra mayúscula, `a` una minúscula, `9` un dígito, y `+` indica que se repite. `descubrir_patrones()` recibe un vector, no una tabla, y cuenta cuántos valores tienen cada forma: ```{r} descubrir_patrones(datos_operativos$codigo_usuario) ``` ```{r, echo = FALSE} stopifnot(isTRUE(all.equal( as.data.frame(descubrir_patrones(datos_operativos$codigo_usuario)), as.data.frame(analisis$perfil$patrones$codigo_usuario), check.attributes = FALSE ))) ``` La mayoría de los códigos tiene la forma `USR-001`. Se apartan `MAL-5`, `USR0003` y `S/D`: el último ya figura en el perfil como faltante disfrazado, y los otros dos son los que habría que revisar en el sistema de origen. El análisis guarda la misma tabla para cada columna en `analisis$perfil$patrones`. ## Tipos y fechas `fecha_evento` se guarda como texto. Eso no impide proponer un tipo implícito, ni contar los formatos de fecha por separado: ```{r} inferir_tipo(datos_operativos$fecha_evento) formatos <- detectar_formatos_fecha(datos_operativos$fecha_evento) formatos[, c("formato", "n", "proporcion", "estado", "n_ambiguos")] ``` De los `r attr(formatos, "total")` valores, `r attr(formatos, "compatibles")` se leen como fecha, en `r nrow(formatos)` formatos distintos; los que no se leen son `2024.04.17` y `NULL`. Estas salidas describen compatibilidad y frecuencia: no convierten la columna ni afirman qué formato debe aceptar el sistema. Que todos los valores sean compatibles tampoco alcanza. Una columna hecha sólo de fechas como `01/02/2020` puede leerse al cien por ciento y seguir siendo ambigua: sin una convención externa no se sabe si el primer número es el día o el mes. El perfil conserva esa diferencia en `estado_tipo_inferido`: ```{r} ambigua <- perfilar( data.frame(fecha = c("01/02/2020", "02/03/2020", "03/04/2020")), muestra = Inf, analizar_dependencias = FALSE ) campos <- c( "columna", "tipo_inferido", "proporcion_tipo_inferido", "estado_tipo_inferido" ) rbind( columnas(analisis)[columnas(analisis)$columna == "fecha_evento", campos], columnas(ambigua)[, campos] ) ``` En `fecha_evento` el estado es `confirmado` aunque haya valores que no se leen, porque valores como `31/01/2024` sólo admiten una lectura. En la columna construida, la proporción es 1 y el estado es `candidato`: la lectura todavía no está establecida. ## Escalas y roles propuestos `variables` separa el tipo de almacenamiento, el tipo implícito y la escala de medición, y propone un rol para cada columna. Es la salida de `clasificar_variables()`: ```{r} analisis$variables[, c( "columna", "tipo_implicito", "escala_propuesta", "rol", "confianza", "confirmada" )] ``` `confianza` expone la fuerza de cada propuesta y `confirmada` distingue una declaración de una inferencia. Sin metadatos, ninguna está confirmada. `id_registro` sale como una medida discreta: por sus valores, un identificador numérico no se distingue de una cantidad. Quien conoce la columna lo declara, con `metadatos_variables` o con `columnas_codigo` (ver la tabla final de [5. El vacío por diseño](vacio-por-diseno.html)). ## Distribuciones `distribuciones` trae las frecuencias de cada columna y los cuantiles de las numéricas: ```{r} frecuencias <- analisis$distribuciones$frecuencias frecuencias[frecuencias$columna == "canal", c("valor", "frecuencia", "proporcion")] cuantiles <- analisis$distribuciones$cuantiles cuantiles[cuantiles$columna == "monto", c("probabilidad", "valor", "n_analizados")] ``` Las frecuencias cuentan lo que hay guardado, sin unir variantes: entre los valores de `canal` aparecen la celda vacía, `S/D`, `Web` y `"web "`, con un espacio al final. Los cuantiles de `monto` van de `-99` a `999999`. El perfil señaló `-99` como posible faltante disfrazado, pero un valor sospechado sólo se informa: no sale de los números mientras no se lo declare con `sentinelas_numericos`. `analisis$distribuciones$alcance` dice, columna por columna, cuántas filas se analizaron y si la lista de valores se recortó. ## Asociaciones Cada fila de `asociaciones` es una medida entre dos columnas, con el método que se usó: Pearson entre numéricas, eta cuadrado entre una categoría y un número, V de Cramér entre dos categorías. La columna `supuesto` dice cómo se trató cada escala. ```{r} analisis$asociaciones[, c("columna_1", "columna_2", "metodo", "asociacion")] ``` ```{r, echo = FALSE} sin_fila_13 <- detectar_asociaciones(datos_operativos[-13, ]) eta_sin_13 <- sin_fila_13$asociacion[ sin_fila_13$columna_1 == "canal" & sin_fila_13$columna_2 == "monto" ] stopifnot(length(eta_sin_13) == 1L, eta_sin_13 < 0.9) ``` Son medidas, no dependencias ni instrucciones de modelado, y conviene leerlas con los datos a la vista. `id_registro` e `id_copia` se asocian al máximo porque una es copia de la otra, algo que el perfil ya informó como `columnas_duplicadas`. La asociación de `canal` con `monto` la produce casi entera la fila 13, donde `canal` está vacío y `monto` vale `999999`: sin esa fila, la medida baja a `r format(round(eta_sin_13, 2), nsmall = 2, decimal.mark = ",")`. Para una relación monótona que no es lineal, `metodo_asociacion_numerica = "spearman"` cambia Pearson por Spearman. ## Tiempo `analizar_tiempo()` reconoce `fecha_evento` por su contenido, aunque se guarde como texto, y resume el período observado: ```{r} analisis$temporal$resumen[, c( "columna", "n_presentes", "fecha_minima", "fecha_maxima", "n_fechas_excluidas_parseo" )] analisis$temporal$propuestas[, c("columna", "frecuencia_dias", "confirmada")] ``` Las fechas que no se pudieron leer quedan contadas aparte, en `n_fechas_excluidas_parseo`. La frecuencia propuesta es la moda de los intervalos observados y sale con `confirmada = FALSE`: es evidencia para revisar, no un requisito. `advertencias` lo repite junto con las escalas sin confirmar, para que no pase inadvertido. ## Lo que `analizar()` mide por su cuenta `analizar()` no se queda en describir. Toma la propuesta de métricas que arma `proponer_modelo()` a partir del perfil, mide lo que esa propuesta deja activo y agrega el resultado en un tablero: la tabla que resume la medición con una fila por métrica y objeto, y cuya columna `orientacion` dice cómo leer cada valor. ```{r} analisis$propuesta_modelo[, c("metrica", "atributos", "origen", "incluir")] ``` Las filas activas (`incluir = TRUE`) salen de hallazgos: los duplicados exactos y los faltantes, contando como faltante lo que el perfil vio disfrazado. Los formatos y el dominio de `zona` se aprendieron de esta misma entrega, y medirlos contra ella sería circular: el dominio observado da 1 por construcción, y cada formato da la proporción del patrón dominante que el perfil ya mostró. Por eso quedan inactivos hasta que confirmemos que son requisitos. `decision_medicion` dice, fila por fila, qué se midió, y el tablero muestra el resultado: ```{r} analisis$decision_medicion[, c("metrica", "objeto", "medida")] analisis$tablero[, c("metrica_instanciada", "valor", "orientacion")] ``` ```{r, echo = FALSE} # Lo que dice la prosa: se mide exactamente lo activo, y nada aprendido de la entrega. stopifnot( identical(analisis$decision_medicion$medida, analisis$propuesta_modelo$incluir), !any(analisis$propuesta_modelo$incluir[ grepl("^perfil:", analisis$propuesta_modelo$origen) ]) ) ``` `valor` es una proporción. Con `orientacion = "conformidad"` se lee "más es mejor", y con `"defecto"`, al revés: `EntidadDuplicada` es la proporción de filas que participan en duplicados. `meta$propuesta_medida_automaticamente` deja constancia de que la selección la hizo `lupa` y nadie la confirmó. Para quedarnos sólo con la descripción, `medir_propuesta = FALSE`. Para medir lo que decidimos nosotros, editamos la propuesta y la pasamos como `propuesta_confirmada`; ese camino sigue en [7. Del hallazgo a la métrica](https://sebollin.github.io/lupa/articles/del-hallazgo-a-la-metrica.html). `cobertura`, por su parte, dice qué factores del marco de calidad quedaron medidos (por omisión, el de AGESIC); los marcos se explican en [8. Marcos y modelos de calidad](https://sebollin.github.io/lupa/articles/marcos-y-modelos.html). ## ¿`perfilar()` o `analizar()`? `perfilar()` es la pasada que conviene llevar a todos lados: resúmenes por columna, patrones, tipos, hallazgos, cobertura de diagnósticos y relaciones entre columnas. `analizar()` arma alrededor de ese perfil lo que cuesta más y devuelve tablas propias: distribuciones, asociaciones, tiempo, escalas, la propuesta medida, el plan y el tablero. Cada pieza está exportada, así que se puede llamar suelta sin pagar el recorrido entero. Por ejemplo, las frecuencias de una sola columna, con un tope de tres valores: ```{r} zona <- distribucion_valores(datos_operativos["zona"], max_valores = 3) zona$frecuencias[, c("valor", "frecuencia")] zona$alcance[, c("columna", "n_distintos_muestra", "n_mostrados", "truncado")] ``` `zona` tiene `r zona$alcance$n_distintos_muestra` valores distintos y se muestran `r zona$alcance$n_mostrados`: el recorte queda declarado en `truncado`. Las dos funciones trabajan en memoria, y sobre tablas de millones de filas no son una pasada rápida. A esa escala conviene perfilar una muestra y llamar sueltas las piezas que hagan falta, o perfilar dentro de la base con `perfilar_dbi()`, que calcula los agregados en el motor y deja elegir cuántas filas trae a R (ver [10. Perfilar una base](https://sebollin.github.io/lupa/articles/perfilar-una-base.html)). Los tiempos medidos sobre tablas de ese tamaño, con su fecha y su entorno, están en el artículo [Evidencia y rendimiento](https://sebollin.github.io/lupa/articles/evidencia-y-rendimiento.html). La muestra que `perfilar()` toma por su cuenta es sistemática y sin azar: sobre los mismos datos, el mismo perfil se rehace igual. ## Compartir el análisis `guardar_analisis()` escribe el análisis en un archivo RDS con número de esquema, y `leer_analisis()` lo recupera en otra sesión. Por omisión no guarda la tabla de entrada y reemplaza las reglas escritas como funciones por declaraciones pequeñas, para no serializar sus entornos. `meta$persistencia` declara qué se guardó y qué no: ```{r} archivo_rds <- tempfile(fileext = ".rds") guardar_analisis(analisis, archivo_rds) recuperado <- leer_analisis(archivo_rds) str(recuperado$meta$persistencia) identical(hallazgos(recuperado), hallazgos(analisis)) ``` `reportar()` convierte el análisis en un HTML autocontenido, que se abre en cualquier navegador sin instalar nada. También acepta perfiles, mediciones, evaluaciones y planes sueltos; un análisis ya los trae. Estas son las secciones del informe: ```{r} archivo_html <- reportar( analisis, archivo = tempfile(fileext = ".html"), titulo = "Calidad de la entrega de ejemplo", fecha = as.POSIXct("2026-01-15", tz = "UTC") ) html <- readLines(archivo_html, encoding = "UTF-8", warn = FALSE) secciones <- regmatches(html, gregexpr("

[^<]*

", html))[[1]] gsub("", "", secciones) ``` En una sesión propia, `browseURL(archivo_html)` lo abre en el navegador; acá borramos los dos archivos al terminar. Con `proteger_datos_personales = TRUE`, el informe enmascara los valores de las columnas que parecen datos personales; el alcance de esa protección está en el artículo [Alcance y cobertura](https://sebollin.github.io/lupa/articles/alcance-y-cobertura.html). ```{r} unlink(c(archivo_html, archivo_rds)) ``` ## Resumen - `analizar()` arma en una llamada el perfil y todo lo que lo rodea; cada componente es una tabla, y cada pieza se puede llamar suelta. - Patrones, tipos y fechas describen compatibilidad y frecuencia: no convierten nada ni confirman un formato. - Escalas y frecuencias temporales son propuestas sin confirmar; las asociaciones son medidas, y se leen con los datos a la vista. - Por omisión, `analizar()` mide lo que la propuesta deja activo; `decision_medicion` dice qué se midió y `medir_propuesta = FALSE` lo apaga. - `guardar_analisis()` guarda el recorrido sin los datos, y `reportar()` lo vuelve un HTML autocontenido. ## Paso siguiente [4. Estructura no declarada](estructura-no-declarada.html) (`vignette("estructura-no-declarada")`): claves, relaciones y dependencias entre tablas que nadie documentó. ```{r, cierre, include=FALSE} options(.opciones_previas) ```