Llega una tabla y, antes de usarla, queremos saber si podemos confiar en ella: qué tiene mal, en qué filas está cada problema y qué no se pudo revisar.
En esta guía:
- perfilamos una tabla con
perfilar()y leemos su resumen;- empezamos por los hallazgos de severidad
error;- vamos de un hallazgo a las filas del sistema de origen con
clave;- leemos en
cobertura()lo que no se pudo evaluar;- guardamos un informe y limpiamos una columna en una copia.
Los datos
datos_operativos viene con el paquete: 13 registros
sintéticos de un sistema de eventos, con defectos sembrados a propósito.
Antes de perfilar, los miramos.
library(lupa)
datos_operativos
#> id_registro codigo_usuario fecha_evento canal monto zona sistema
#> 1 1 USR-001 2024-01-31 web 1250 Norte principal
#> 2 2 USR-002 31/01/2024 telefono 1600 Centro principal
#> 3 3 USR0003 2024/02/15 Web 1425 Sur principal
#> 4 4 USR-004 16-03-2024 presencial -99 Este principal
#> 5 5 MAL-5 2024.04.17 S/D 0 Oeste principal
#> 6 6 USR-006 20240518 web 1750 Norte principal
#> 7 7 USR-007 19/06/2024 telefono 1810 Centro principal
#> 8 8 USR-008 2024-07-20 web 1490 Sur principal
#> 9 9 USR-009 21/08/2024 APP 1660 Este principal
#> 10 10 USR-010 2024-09-22 web 1700 Oeste principal
#> 11 10 USR-010 23/10/2024 web -50 Oeste principal
#> 12 1 USR-001 2024-01-31 web 1250 Norte principal
#> 13 11 S/D NULL 999999 Centro principal
#> contacto id_copia id_evento
#> 1 usuario01@example.invalid 1 EVT001
#> 2 usuario02@example.invalid 2 EVT002
#> 3 usuario03@example.invalid 3 EVT003
#> 4 usuario04@example.invalid 4 EVT004
#> 5 usuario05@example.invalid 5 EVT005
#> 6 usuario06@example.invalid 6 EVT006
#> 7 usuario07@example.invalid 7 EVT007
#> 8 usuario08@example.invalid 8 EVT008
#> 9 usuario09@example.invalid 9 EVT009
#> 10 usuario10@example.invalid 10 EVT010
#> 11 usuario10b@example.invalid 10 EVT012
#> 12 usuario01@example.invalid 1 EVT001
#> 13 contacto-mal 11 EVT011Esto es lo que trae cada columna (lo describe
?datos_operativos) y el hallazgo que lo va a señalar:
| columna | qué trae a propósito | hallazgo que lo señala |
|---|---|---|
id_registro |
1 y 10 repetidos; las dos filas con
10 no coinciden en lo demás |
posible_identificador; la repetición, al declararla
clave (guía 2) |
codigo_usuario |
"S/D" en lugar de un ausente; USR0003 y
MAL-5 fuera del formato |
faltantes_disfrazados; los formatos, ninguno (se ve más
abajo por qué) |
fecha_evento |
cinco formatos de fecha y un "NULL"
|
formatos_fecha_mixtos,
faltantes_disfrazados
|
canal |
"web", "Web" y "web ";
"S/D" y una celda vacía |
mayusculas_inconsistentes,
espacios_sobrantes, faltantes_disfrazados
|
monto |
-99, 0, -50 y
999999
|
faltantes_disfrazados para el -99; el
0 y el -50, sólo si declaramos que no se
admiten (columnas_sin_ceros,
columnas_no_negativas); el 999999, ninguno (se
ve más abajo por qué) |
sistema |
el mismo valor en todas las filas | constante |
contacto |
correos sintéticos y uno mal formado |
dato_personal_posible; el mal formado, ninguno (por la
misma razón que los formatos de codigo_usuario) |
id_copia |
una copia de id_registro
|
columnas_duplicadas |
id_evento |
el identificador del evento; EVT001 dos veces |
clave_no_unica, al declararla clave |
| filas 1 y 12 | idénticas | filas_duplicadas |
zona no trae defectos.
Perfilar
perfilar() lee la tabla y no la modifica. Le pasamos en
clave la columna que identifica cada evento en el sistema
de origen: así cada hallazgo podrá decir en qué eventos está.
perfil <- perfilar(datos_operativos, clave = "id_evento")
#> Warning: La clave declarada no es única: entre las 13 filas con la clave completa, 1 repite su
#> valor (2 filas participan en colisiones). La clave cumple la ausencia de nulos: no tiene
#> valores ausentes.El aviso ya es un resultado: la clave que declaramos se repite.
lupa no la da por buena; la comprueba. Al imprimir el
perfil vemos el resumen:
perfil
#>
#> ── Perfil de datos: datos_operativos ─────────────────────────────────────────────────────
#> ✖ 6 hallazgos con severidad error
#> ! 11 hallazgos sospechosos
#> ✔ 6 hallazgos informativos ok
#> ℹ 9 diagnósticos no evaluados
#>
#> ── Resumen general ──
#>
#> Filas: 13
#> Columnas: 10
#> Celdas: 130
#> Filas completas: 13
#> Filas duplicadas: 1
#>
#> ── Resumen por columna ──
#>
#> columna tipo_inferido prop_faltantes_totales n_distintos n_outliers
#> id_registro doble 0.00000000 11 NA
#> codigo_usuario texto 0.07692308 11 NA
#> fecha_evento fecha 0.07692308 12 NA
#> canal texto 0.15384615 8 NA
#> monto doble 0.07692308 12 NA
#> zona texto 0.00000000 5 NA
#> sistema texto 0.00000000 1 NA
#> contacto texto 0.00000000 12 NA
#> id_copia doble 0.00000000 11 NA
#> id_evento identificador 0.00000000 12 NA
#> ℹ Se muestran 5 de 118 campos; los demás están en `columnas()`.Las cuatro primeras líneas cuentan los hallazgos por severidad y los
diagnósticos que no se pudieron evaluar. error quiere decir
que un control cruzó su criterio; sospechoso, que algo
merece que lo revisemos; ok, que es una lectura
informativa, no un defecto. Debajo vienen el resumen general y unas
pocas medidas por columna. (La impresión trae también la memoria que
ocupa la tabla; acá no la mostramos porque cambia según la instalación
de R).
Empezar por los errores
Los hallazgos son una tabla, y se filtran como cualquier
data.frame.
h <- hallazgos(perfil)
h[h$severidad == "error", c("columna", "tipo_hallazgo", "n_afectados")]
#> columna tipo_hallazgo n_afectados
#> 3 codigo_usuario faltantes_disfrazados 1
#> 5 fecha_evento faltantes_disfrazados 1
#> 6 fecha_evento formatos_fecha_mixtos 5
#> 9 canal faltantes_disfrazados 2
#> 20 <NA> clave_no_unica 2
#> 21 <NA> filas_duplicadas 2Son 6. Una columna en NA quiere decir que
el hallazgo es de la tabla entera: las filas 1 y 12 son idénticas
(filas_duplicadas), y por eso EVT001 aparece
dos veces en la clave (clave_no_unica).
Del hallazgo a la fila
Tomamos uno: los ausentes disfrazados de canal. La
evidencia dice qué se vio, y la sugerencia, qué se podría hacer.
canal <- h[h$columna %in% "canal" & h$tipo_hallazgo == "faltantes_disfrazados", ]
writeLines(canal$evidencia)
#> <blanco> (1); S/D (1)
writeLines(strwrap(canal$sugerencia, 88))
#> Normalizar estas representaciones a NA conservando su significado si fuera necesario.La trazabilidad dice en qué filas está, con los valores de la clave que declaramos.
eventos <- canal$trazabilidad[[1]]$claves$id_evento
eventos
#> [1] "EVT005" "EVT011"Con esos identificadores buscamos los eventos en el sistema de origen, sin abrir la tabla entera. Acá podemos cotejarlos (con comillas, para que se vea la celda vacía):
filas <- datos_operativos$id_evento %in% eventos
print(datos_operativos[filas, c("id_evento", "canal")], quote = TRUE)
#> id_evento canal
#> 5 "EVT005" "S/D"
#> 13 "EVT011" ""Sin clave, la trazabilidad trae sólo los números de
fila, que sirven dentro de R pero no en el sistema de origen.
Lo que no se pudo evaluar
Un control que no encontró nada no es lo mismo que un control que no
corrió. Lo segundo queda en cobertura(), con el motivo y
cómo resolverlo.
no_evaluados <- cobertura(perfil)
no_evaluados[, c("diagnostico", "columna")]
#> diagnostico columna
#> 1 outliers id_registro
#> 2 outliers fecha_evento
#> 3 resumen_cuantitativo fecha_evento
#> 4 patron_raro fecha_evento
#> 5 outliers monto
#> 6 outliers id_copia
#> 7 proximidad_vocabulario zona
#> 8 proximidad_vocabulario contacto
#> 9 proximidad_vocabulario id_eventoEl 999999 de monto no aparece entre los
hallazgos, y no porque esté bien: la búsqueda de valores extremos no
corrió en esa columna.
motivo <- no_evaluados$motivo[
no_evaluados$diagnostico == "outliers" & no_evaluados$columna == "monto"
]
writeLines(strwrap(motivo, 88))
#> No se evaluaron los límites de Tukey: hay 13 valores finitos y se requieren al menos
#> 20. Reunir más observaciones o declarar otro criterio antes de interpretar outliers.Lo que no se pudo medir queda registrado; nunca cuenta como cero.
Los formatos raros de codigo_usuario
(USR0003 y MAL-5) tampoco tienen hallazgo, por
otra razón: ese control sí corrió, pero cada formato ocupa una fila de
13, y por omisión un formato es raro cuando no llega al 5 % de los
valores (umbral_patron_raro). En una tabla más larga lo
señalaría.
Un informe para compartir
reportar() escribe un único archivo HTML, que se abre en
cualquier navegador y no necesita red ni programas aparte.
archivo <- file.path(tempdir(), "calidad-de-la-entrega.html")
reportar(perfil, archivo = archivo, titulo = "Calidad de la entrega")El informe tiene estas secciones: Resumen general; Hallazgos por
severidad; Resumen por columna; Clasificación de posibles datos
personales; Cobertura del análisis; Cobertura de diagnósticos; Patrones
de formato; Formatos de fecha; Dependencias funcionales.
reportar() devuelve la ruta del archivo: en una sesión
interactiva, browseURL(archivo) lo abre en el navegador.
También recibe el resultado completo de analizar(), con las
mediciones y el plan de limpieza; eso lo vemos en 3. Explorar columnas con
analizar().
Limpiar una columna
lupa no cambia los datos que perfila. Para limpiar,
propone un plan editable, y aplicar() ejecuta sobre una
copia sólo las acciones que marcamos.
plan <- planificar_limpieza(perfil, datos_operativos)
plan[plan$columna %in% "canal", c("id_accion", "estrategia", "recomendada", "aplicar")]
#> id_accion estrategia recomendada aplicar
#> 6 accion-0006 convertir_ausencias_textuales TRUE TRUE
#> 7 accion-0007 recortar_espacios TRUE TRUE
#> 8 accion-0008 convertir_minusculas FALSE FALSE
#> 9 accion-0009 convertir_titulo FALSE FALSE
#> 10 accion-0010 convertir_mayusculas FALSE FALSE
#> 11 accion-0011 convertir_segun_diccionario FALSE FALSEPara canal, el plan recomienda y deja marcadas dos
acciones: convertir en NA los ausentes escritos como texto
y recortar los espacios del borde. Entre minúsculas, mayúsculas o un
diccionario no elige: esa decisión es nuestra. Cada acción trae su
justificación, que dice también qué conviene confirmar:
justificacion <- plan$justificacion[
plan$columna %in% "canal" & plan$estrategia == "convertir_ausencias_textuales"
]
writeLines(strwrap(justificacion, 88))
#> Las representaciones textuales del catálogo son marcadores habituales de ausencia. El
#> cambio no es reversible: confirmar que ninguno de esos textos sea un valor legítimo de
#> la columna -`NA` es el código de Namibia, `NULL` puede ser un apellido-.Para esta guía aplicamos sólo esas dos.
plan$aplicar <- plan$columna %in% "canal" & plan$recomendada
limpio <- aplicar(plan, datos_operativos)
limpio$registro[, c("columna", "estrategia", "n_cambiadas", "estado")]
#> columna estrategia n_cambiadas estado
#> 1 canal convertir_ausencias_textuales 2 ejecutada
#> 2 canal recortar_espacios 1 ejecutadaComparamos los valores antes y después en las filas que cambiaron:
antes <- datos_operativos$canal
despues <- limpio$datos$canal
cambiaron <- is.na(despues) | antes != despues
print(data.frame(id_evento = datos_operativos$id_evento, antes, despues)[cambiaron, ],
quote = TRUE)
#> id_evento antes despues
#> 5 "EVT005" "S/D" NA
#> 8 "EVT008" "web " "web"
#> 13 "EVT011" "" NALa columna antes se leyó de
datos_operativos después de aplicar el plan: el original
sigue como estaba. "Web" y "APP" quedaron
igual, porque no elegimos ninguna convención de mayúsculas. El plan
completo, con los grupos de acciones y su registro, está en 6. Limpiar con un plan.
Resumen
-
perfilar()lee la tabla sin modificarla; imprimir el perfil da el recuento por severidad y lo que no se pudo evaluar. -
hallazgos()es una tabla: empezamos porseveridad == "error". - Con
clave, la trazabilidad de cada hallazgo trae los identificadores para ir al sistema de origen. -
cobertura()lista los controles que no corrieron: un hallazgo que falta no prueba que el dato esté bien. -
reportar()guarda un informe HTML;planificar_limpieza()yaplicar()limpian una copia con las acciones que elegimos.
Paso siguiente
2. Leer un perfil
(vignette("leer-un-perfil")) explica las severidades, cada
campo de un hallazgo, las claves y el vocabulario del paquete.
