This guide is the English version of 1. Empezar en diez minutos.
lupawrites its messages and tables in Spanish, so the output below is in Spanish.
A table arrives and, before using it, we want to know whether we can trust it. What is wrong with it, in which rows are the problems, and what could not be checked?
In this guide:
- we profile a table with
perfilar()and read its summary;- we start with the findings of severity
error;- we go from a finding to the rows of the source system with
clave;- we read in
cobertura()what could not be evaluated;- we save a report and clean a column in a copy.
The data
datos_operativos (operational data) comes with the
package: 13 synthetic records from an event system, with defects planted
on purpose. Before profiling, we look at them.
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 EVT011This is what each column carries (?datos_operativos
describes it), with the finding that flags it:
| column | what it carries on purpose | finding that flags it |
|---|---|---|
id_registro |
1 and 10 repeated; the 2 rows with
10 differ in the rest |
posible_identificador (possible identifier); the
repetition, once declared as a key (guide 2) |
codigo_usuario |
"S/D" instead of a missing value; USR0003
and MAL-5 outside the format |
faltantes_disfrazados (disguised missing values); the
formats, none (see below why) |
fecha_evento |
5 date formats and a "NULL"
|
formatos_fecha_mixtos (mixed date formats),
faltantes_disfrazados
|
canal |
"web", "Web" and "web ";
"S/D" and an empty cell |
mayusculas_inconsistentes (inconsistent
capitalization), espacios_sobrantes (extra spaces),
faltantes_disfrazados
|
monto |
-99, 0, -50 and
999999
|
faltantes_disfrazados for the -99; the
0 and the -50, only if we declare that they
are not allowed (columnas_sin_ceros,
columnas_no_negativas: columns without zeros, non-negative
columns); the 999999, none (see below why) |
sistema |
the same value in every row |
constante (constant) |
contacto |
synthetic emails and one malformed |
dato_personal_posible (possible personal data); the
malformed one, none (for the same reason as the formats of
codigo_usuario) |
id_copia |
a copy of id_registro
|
columnas_duplicadas (duplicated columns) |
id_evento |
the identifier of the event; EVT001 twice |
clave_no_unica (non-unique key), once declared as a
key |
| rows 1 and 12 | identical |
filas_duplicadas (duplicated rows) |
zona carries no defects.
Profiling
perfilar() (profile) reads the table without
modifying it. In clave (key) we pass the column
that identifies each event in the source system. That way, where a
finding points to rows, it says which events they are.
perfil <- perfilar(datos_operativos, clave = "id_evento")
#> Warning: La clave declarada no es única: entre 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.The warning is the first result: the key we declared repeats.
lupa does not take it for granted; it checks it. Printing
the profile shows the summary:
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()`.The first 4 lines count the findings by severity and the diagnostics that could not be evaluated:
-
error: a check crossed its criterion; -
sospechoso(suspicious): something deserves a look; -
ok: an informative reading, not a defect.
Below come the general summary and a few measures per column. The printout also shows the memory the table takes; we leave it out here because it changes with the R installation.
Starting with the errors
The findings that hallazgos() (findings)
returns are a table, and they are filtered like any
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 2There are 6. A columna (column) in
NA marks a finding about the whole table. Rows 1 and 12 are
identical (filas_duplicadas), and that is why
EVT001 appears twice in the key
(clave_no_unica).
From the finding to the row
We take one: the disguised missing values of canal
(channel). The evidencia (evidence) says what was
seen; the sugerencia (suggestion), what could be done.
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.The trazabilidad (traceability) says in which rows it
is, with the values of the key we declared.
eventos <- canal$trazabilidad[[1]]$claves$id_evento
eventos
#> [1] "EVT005" "EVT011"With those identifiers we look up the events in the source system, without opening the whole table. Here we can check them (with quotes, so that the empty cell shows):
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" ""Without clave, the traceability carries only the row
numbers, which work inside R but not in the source system.
⚠️ What could not be evaluated
A check that found nothing is not the same as a check that did not
run. The second case stays in cobertura()
(coverage), with the reason and how to solve it.
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_eventoThe 999999 of monto (amount) is
not among the findings, and not because it is fine. The search for
extreme values did not run on that column:
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.What could not be measured stays on record; it never counts as zero.
The odd formats of codigo_usuario (USR0003
and MAL-5) have no finding either, for another reason: that
check did run. Each format takes 1 row out of 13. By default, a format
is rare when it stays below 5% of the values
(umbral_patron_raro, the rare-pattern threshold). In a
longer table it would be flagged.
📄 A report to share
reportar() (report) writes a single HTML file.
It opens in any browser and needs no network and no other programs.
archivo <- file.path(tempdir(), "calidad-de-la-entrega.html")
reportar(perfil, archivo = archivo, titulo = "Calidad de la entrega")The report has these sections: 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() returns the path of the file; in an interactive
session, browseURL(archivo) opens it in the browser. It
also accepts the full result of analizar(), with the
measurements and the cleaning plan: we see it in 3. Explorar columnas con
analizar() (Exploring columns with
analizar(), in Spanish).
Cleaning a column
lupa does not change the data it profiles. To clean, it
proposes an editable plan; aplicar() (apply) runs
on a copy only the actions we mark.
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 FALSEFor canal, the plan recommends and leaves marked 2
actions:
- turning into
NAthe missing values written as text; - trimming the spaces at the edges.
Between lower case, upper case or a dictionary it does not choose: that decision is ours. Each action carries its justification, which also says what is worth confirming:
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-.For this guide we apply only those 2 actions.
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 ejecutadaWe compare the values before and after in the rows that changed:
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" "" NAThe antes (before) column was read from
datos_operativos after applying the plan: the original
stays as it was. "Web" and "APP" stayed the
same, because we did not choose any capitalization convention. The full
plan, with its groups of actions and its log, is in 6. Limpiar con un plan (Cleaning
with a plan, in Spanish).
Summary
-
perfilar()reads the table without modifying it; its printout counts the findings by severity and what could not be evaluated. -
hallazgos()is a table: we start withseveridad == "error". - With
clave, the traceability of each finding carries the identifiers to go to the source system. -
cobertura()lists the checks that did not run: a missing finding does not prove that the data is fine. -
reportar()saves an HTML report;planificar_limpieza()(plan the cleaning) andaplicar()clean a copy with the actions we choose.
🧭 Next step
2. Leer un perfil (Reading a
profile, in Spanish; vignette("leer-un-perfil"))
explains the severities, each field of a finding, the keys and the
vocabulary of the package.
