analizar()El perfil dice qué está mal en una tabla. Antes de decidir qué le exigimos, queremos ver qué hay en cada columna: forma, tipo, reparto, asociaciones y período de 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ándoanalizar();- cómo guardamos el análisis y producimos un informe HTML.
Seguimos con datos_operativos, la entrega de 13 registros con defectos sembrados que presenta 1. Empezar en diez minutos.
Fijamos fecha para que el ejemplo dé siempre el mismo resultado.
library(lupa)
analisis <- analizar(
datos_operativos,
nombre = "entrega de ejemplo",
fecha = as.POSIXct("2026-01-15", tz = "UTC")
)
names(analisis)
#> [1] "perfil" "distribuciones" "asociaciones" "temporal"
#> [5] "variables" "propuesta_modelo" "decision_medicion" "tablero"
#> [9] "medicion" "detalle_medicion" "evaluacion" "plan_limpieza"
#> [13] "cobertura" "advertencias" "datos" "meta"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 fecha_evento, la columna que seguimos en toda la guía:
h <- hallazgos(analisis)
h[h$columna %in% "fecha_evento", c("tipo_hallazgo", "severidad", "n_afectados")]
#> tipo_hallazgo severidad n_afectados
#> 4 alta_cardinalidad sospechoso 1
#> 5 faltantes_disfrazados error 1
#> 6 formatos_fecha_mixtos error 5
#> 7 tipo_declarado_distinto sospechoso 1
#> 18 casi_duplicados_vocabulario ok 0Cómo se lee cada hallazgo está en 2. Leer un perfil. Acá miramos lo que hay debajo: los valores que los producen.
Un patrón resume la forma de un valor:
A es una letra mayúscula;a, una minúscula;9, un dígito;+ indica que se repite.descubrir_patrones() recibe un vector, no una tabla, y cuenta cuántos valores tienen cada forma:
descubrir_patrones(datos_operativos$codigo_usuario)
#> patron n proporcion ejemplos
#> 1 A+-9+ 10 0.76923077 USR-001 | USR-002 | USR-004
#> 2 A+-9 1 0.07692308 MAL-5
#> 3 A+9+ 1 0.07692308 USR0003
#> 4 A/A 1 0.07692308 S/DLa 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; los otros 2 habría que revisarlos en el sistema de origen. El análisis guarda la misma tabla para cada columna en analisis$perfil$patrones.
fecha_evento se guarda como texto. Eso no impide proponer un tipo implícito, ni contar los formatos de fecha por separado:
inferir_tipo(datos_operativos$fecha_evento)
#> fecha (84.6%; 11 de 13 valores compatibles)
formatos <- detectar_formatos_fecha(datos_operativos$fecha_evento)
formatos[, c("formato", "n", "proporcion", "estado", "n_ambiguos")]
#> formato n proporcion estado n_ambiguos
#> 1 %Y-%m-%d 4 0.30769231 confirmado 0
#> 2 %d/%m/%Y 4 0.30769231 confirmado 0
#> 3 %Y%m%d 1 0.07692308 confirmado 0
#> 4 %Y/%m/%d 1 0.07692308 confirmado 0
#> 5 %d-%m-%Y 1 0.07692308 confirmado 0De los 13 valores, 11 se leen como fecha, en 5 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 100 % 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:
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]
)
#> columna tipo_inferido proporcion_tipo_inferido estado_tipo_inferido
#> 3 fecha_evento fecha 0.8461538 confirmado
#> 1 fecha fecha 1.0000000 candidatoEn fecha_evento el estado es confirmado aunque haya valores que no se leen: valores como 31/01/2024 sólo admiten una lectura. En la columna construida, la proporción es 1 y el estado es candidato, porque la lectura todavía no está establecida.
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():
analisis$variables[, c(
"columna", "tipo_implicito", "escala_propuesta", "rol", "confianza", "confirmada"
)]
#> columna tipo_implicito escala_propuesta rol confianza confirmada
#> 1 id_registro doble discreta medida 0.65 FALSE
#> 2 codigo_usuario texto nominal categoria 0.55 FALSE
#> 3 fecha_evento fecha temporal fecha 0.85 FALSE
#> 4 canal texto nominal categoria 0.55 FALSE
#> 5 monto doble discreta medida 0.65 FALSE
#> 6 zona texto nominal categoria 0.55 FALSE
#> 7 sistema texto nominal categoria 0.55 FALSE
#> 8 contacto texto nominal categoria 0.55 FALSE
#> 9 id_copia doble discreta medida 0.65 FALSE
#> 10 id_evento identificador nominal identificador 0.90 FALSEconfianza expone la fuerza de cada propuesta; 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).
distribuciones trae las frecuencias de cada columna y los cuantiles de las numéricas:
frecuencias <- analisis$distribuciones$frecuencias
frecuencias[frecuencias$columna == "canal", c("valor", "frecuencia", "proporcion")]
#> valor frecuencia proporcion
#> 35 web 5 0.38461538
#> 36 telefono 2 0.15384615
#> 37 1 0.07692308
#> 38 APP 1 0.07692308
#> 39 S/D 1 0.07692308
#> 40 Web 1 0.07692308
#> 41 presencial 1 0.07692308
#> 42 web 1 0.07692308
cuantiles <- analisis$distribuciones$cuantiles
cuantiles[cuantiles$columna == "monto", c("probabilidad", "valor", "n_analizados")]
#> probabilidad valor n_analizados
#> 6 0.00 -99 13
#> 7 0.25 1250 13
#> 8 0.50 1490 13
#> 9 0.75 1700 13
#> 10 1.00 999999 13Las 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ó.
Cada fila de asociaciones es una medida entre 2 columnas, con el método que se usó:
La columna supuesto dice cómo se trató cada escala.
analisis$asociaciones[, c("columna_1", "columna_2", "metodo", "asociacion")]
#> columna_1 columna_2 metodo asociacion
#> 1 id_registro id_copia pearson_absoluto 1.0000000
#> 2 canal monto eta2 0.9999977
#> 3 canal zona cramer_v 0.8944272
#> 4 id_registro monto pearson_absoluto 0.4301569
#> 5 monto id_copia pearson_absoluto 0.4301569
#> 6 canal id_copia eta2 0.3791539
#> 7 id_registro canal eta2 0.3791539
#> 8 id_registro zona eta2 0.3440367
#> 9 zona id_copia eta2 0.3440367Son medidas, no dependencias ni instrucciones de modelado. 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; el perfil ya lo informó como columnas_duplicadas.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 0,65.Para una relación monótona que no es lineal, metodo_asociacion_numerica = "spearman" cambia Pearson por Spearman.
analizar_tiempo() reconoce fecha_evento por su contenido, aunque se guarde como texto, y resume el período observado:
analisis$temporal$resumen[, c(
"columna", "n_presentes", "fecha_minima", "fecha_maxima", "n_fechas_excluidas_parseo"
)]
#> columna n_presentes fecha_minima fecha_maxima n_fechas_excluidas_parseo
#> 1 fecha_evento 11 2024-01-31 2024-10-23 2
analisis$temporal$propuestas[, c("columna", "frecuencia_dias", "confirmada")]
#> columna frecuencia_dias confirmada
#> 1 fecha_evento 32 FALSELas fechas que no se pudieron leer quedan contadas aparte, en n_fechas_excluidas_parseo. La frecuencia propuesta es la moda de los intervalos observados. 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.
analizar() mide por su cuentaanalizar() no se queda en describir:
proponer_modelo() a partir del perfil;El tablero resume la medición con una fila por métrica y objeto. Su columna orientacion dice cómo leer cada valor.
analisis$propuesta_modelo[, c("metrica", "atributos", "origen", "incluir")]
#> metrica atributos origen incluir
#> 1 EntidadDuplicada hallazgo:filas_duplicadas TRUE
#> 2 NoNulo canal hallazgo:faltantes TRUE
#> 3 NoNulo codigo_usuario hallazgo:faltantes_disfrazados TRUE
#> 4 NoNulo fecha_evento hallazgo:faltantes_disfrazados TRUE
#> 5 NoNulo monto hallazgo:faltantes_disfrazados TRUE
#> 6 Formato contacto perfil:patron_dominante:a+9+@a+.a+ FALSE
#> 7 Formato id_evento perfil:patron_dominante:A+9+ FALSE
#> 8 Formato sistema perfil:patron_dominante:a+ FALSE
#> 9 Formato zona perfil:patron_dominante:Aa+ FALSE
#> 10 ValoresPosiblesPorExtension zona perfil:dominio_observado FALSELas 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. Medirlos contra ella sería circular:
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:
analisis$decision_medicion[, c("metrica", "objeto", "medida")]
#> metrica objeto medida
#> 1 EntidadDuplicada (tabla) TRUE
#> 2 NoNulo canal TRUE
#> 3 NoNulo codigo_usuario TRUE
#> 4 NoNulo fecha_evento TRUE
#> 5 NoNulo monto TRUE
#> 6 Formato contacto FALSE
#> 7 Formato id_evento FALSE
#> 8 Formato sistema FALSE
#> 9 Formato zona FALSE
#> 10 ValoresPosiblesPorExtension zona FALSE
analisis$tablero[, c("metrica_instanciada", "valor", "orientacion")]
#>
#> ── Tablero de calidad ────────────────────────────────────────────────────────────────────
#> metrica_instanciada valor orientacion
#> EntidadDuplicada 0.1538462 defecto
#> NoNulo:canal 0.8461538 conformidad
#> NoNulo:codigo_usuario 0.9230769 conformidad
#> NoNulo:fecha_evento 0.9230769 conformidad
#> NoNulo:monto 0.9230769 conformidadvalor es una proporción. Con orientacion = "conformidad" se lee “más es mejor”; con "defecto", al revés. Por ejemplo, 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ó.
medir_propuesta = FALSE.propuesta_confirmada. Ese camino sigue en 7. Del hallazgo a la métrica.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.
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, en 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 3 valores:
zona <- distribucion_valores(datos_operativos["zona"], max_valores = 3)
zona$frecuencias[, c("valor", "frecuencia")]
#> valor frecuencia
#> 1 Centro 3
#> 2 Norte 3
#> 3 Oeste 3
zona$alcance[, c("columna", "n_distintos_muestra", "n_mostrados", "truncado")]
#> columna n_distintos_muestra n_mostrados truncado
#> 1 zona 5 3 TRUEzona tiene 5 valores distintos y se muestran 3: el recorte queda declarado en truncado.
Las 2 funciones trabajan en memoria, y sobre tablas de millones de filas no son una pasada rápida. A esa escala conviene una de estas vías:
perfilar_dbi(), que calcula los agregados en el motor y deja elegir cuántas filas trae a R (ver 10. Perfilar una base).Los tiempos medidos sobre tablas de ese tamaño, con su fecha y su entorno, están en el artículo Evidencia y rendimiento. La muestra que perfilar() toma por su cuenta es determinista y sin azar —a paso fijo para patrones, tipos y formatos; para las dependencias, una fila por tramo de la tabla en una posición fijada por un hash, que no se alinea con datos periódicos—: sobre los mismos datos, el mismo perfil se rehace igual.
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, guardar_analisis():
meta$persistencia declara qué se guardó y qué no:
archivo_rds <- tempfile(fileext = ".rds")
guardar_analisis(analisis, archivo_rds)
recuperado <- leer_analisis(archivo_rds)
str(recuperado$meta$persistencia)
#> List of 4
#> $ version_esquema : int 1
#> $ datos_incluidos : logi FALSE
#> $ evidencia_protegida : logi FALSE
#> $ funciones_sustituidas: int 0
identical(hallazgos(recuperado), hallazgos(analisis))
#> [1] TRUEreportar() 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:
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("<h2>[^<]*</h2>", html))[[1]]
gsub("</?h2>", "", secciones)
#> [1] "Análisis integral" "Advertencias de alcance"
#> [3] "Perfil de datos: entrega de ejemplo" "Distribuciones y cuantiles"
#> [5] "Asociaciones entre columnas" "Análisis temporal"
#> [7] "Escalas y roles propuestos" "Propuesta de modelo"
#> [9] "Tablero de calidad" "Medidas de calidad"
#> [11] "Plan de limpieza"En una sesión propia, browseURL(archivo_html) lo abre en el navegador; acá borramos los 2 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.
unlink(c(archivo_html, archivo_rds))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.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.4. Estructura no declarada (vignette("estructura-no-declarada")): claves, relaciones y dependencias entre tablas que nadie documentó.