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á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.
Una llamada, todos los componentes
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 una columna que
vamos a seguir en toda la guía, fecha_evento:
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.
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:
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, 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:
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 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:
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, 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():
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 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).
Distribuciones
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ó.
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.
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, 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 0,65. 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:
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 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.
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, 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:
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”, 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. 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 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:
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 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). 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 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:
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 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.
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_mediciondice qué se midió ymedir_propuesta = FALSElo apaga. -
guardar_analisis()guarda el recorrido sin los datos, yreportar()lo vuelve un HTML autocontenido.
Paso siguiente
4. Estructura no declarada
(vignette("estructura-no-declarada")): claves, relaciones y
dependencias entre tablas que nadie documentó.
