The hardware and bandwidth for this mirror is donated by dogado GmbH, the Webhosting and Full Service-Cloud Provider. Check out our Wordpress Tutorial.
If you wish to report a bug, or if you are interested in having us mirror your free-software or open-source project, please feel free to contact us at mirror[@]dogado.de.

Data profiling and quality measurement with evidence. Every result states its scope and what it could not measure.
lupa examines a table or a database before you trust it.
It finds what summary() misses:
"S/D" (sin dato,
“no data”) or -99;It says which rows each problem is in and which checks it could not run. Then it helps turn what was confirmed into measurements, evaluate them, clean a copy with an editable plan and compare deliveries. It is for anyone who analyzes, produces or publishes tabular data and needs to know how far it can be trusted.
What sets it apart:
Install the released version from CRAN:
install.packages("lupa")or the development version from GitHub:
# install.packages("pak")
pak::pak("sebollin/lupa")It depends only on cli and
data.table.
Suggested packages enable specific capabilities:
DBI with
a driver (RSQLite,
duckdb,
RPostgres
or odbc) to
profile inside a database;stringdist
for approximate text comparison;sf
for geometries.The API, the help pages, the guides and the printed output are in
Spanish, and the public names are part of the contract: the outputs
below appear exactly as lupa writes them.
datos_operativos ships with the package: 13 rows and 10
columns with planted defects. Profile it in one call and start with the
findings of severity error:
library(lupa)
perfil <- perfilar(datos_operativos)
h <- hallazgos(perfil)
errores_primero <- order(h$severidad, decreasing = TRUE)
print(head(h[errores_primero, c("columna", "tipo_hallazgo", "severidad")], 8),
row.names = FALSE)
#> columna tipo_hallazgo severidad
#> codigo_usuario faltantes_disfrazados error
#> fecha_evento faltantes_disfrazados error
#> fecha_evento formatos_fecha_mixtos error
#> canal faltantes_disfrazados error
#> <NA> filas_duplicadas error
#> codigo_usuario alta_cardinalidad sospechoso
#> fecha_evento alta_cardinalidad sospechoso
#> fecha_evento tipo_declarado_distinto sospechosoA columna of <NA> marks a finding
about the whole table, such as the duplicate rows.
What could not be measured does not disappear. In this table 9
diagnostics were left unevaluated, each one counted in every column
where it could not run: the outlier search, for instance, needs more
values than the table has. cobertura(perfil) says which
ones, in which column, why and how to resolve them:
as.data.frame(table(diagnostico = cobertura(perfil)$diagnostico), responseName = "columnas")
#> diagnostico columnas
#> 1 outliers 4
#> 2 patron_raro 1
#> 3 proximidad_vocabulario 3
#> 4 resumen_cuantitativo 1perfilar() profiles a data frame in memory.
perfilar_dbi() profiles a table inside the
database: the aggregates run as SQL on the engine, and only the
rows that fit within the caps come into R. The same surface is measured
against 6 real engines:
| Engine | Version tested | R driver |
|---|---|---|
| DuckDB | 1.5 | duckdb |
| MariaDB | 11 | RMariaDB |
| MySQL | 8 | RMySQL |
| PostgreSQL | 16 | RPostgres |
| SQL Server | 2022 | odbc |
| SQLite | — | RSQLite |
PostgreSQL and MariaDB are measured on every push (engines
CI), and SQLite and DuckDB by the test suite on every CI platform;
MySQL and SQL Server, against local containers before each release.
Oracle is expected to work but has not been measured, and BigQuery is
not documented. requisitos_motor() says what each engine
needs. The versions, the drivers and the date of each run are in Motores
y escala (Engines and scale).
summary() does not
showsummary() describes columns one at a time.
lupa compares representations and cross-checks columns and
rows. It returns its findings as a table: type, severity, evidence,
counts, a suggestion and traceability down to the rows. A finding type
that does not appear is a diagnostic that ran and found nothing; what
did not run is in cobertura().
Among the outputs of that run are:
| What it reveals | tipo_hallazgo |
Evidence (excerpt) |
|---|---|---|
| Missing values hidden behind codes | faltantes_disfrazados |
<blanco> (1); S/D (1) |
| Dates in mixed formats | formatos_fecha_mixtos |
%Y-%m-%d (4); %d/%m/%Y (4); ... |
| Extra whitespace | espacios_sobrantes |
1 valor; ejemplos: "web " |
| Inconsistent capitalization | mayusculas_inconsistentes |
"web"; "Web" |
| Declared and inferred types disagree | tipo_declarado_distinto |
Declarado: texto; inferido: fecha (0.846 compatible); ... |
| A constant column | constante |
Valor: principal; frecuencia: 13 |
| Exact duplicate rows | filas_duplicadas |
2 filas en grupos duplicados (1 excedente) |
| Repeated columns | columnas_duplicadas |
id_registro = id_copia; comparadas sobre 13 filas |
The full vocabulary, with what each finding type means, is in the article Alcance y cobertura (Scope and coverage).
severidad (severity) is an ordered factor:
ok < sospechoso < error.
ok records an informative condition, not an approval: a
column that looks like an identifier is ok.sospechoso (suspicious) asks for a look in context:
alta_cardinalidad (high cardinality) in
codigo_usuario suggests checking whether the column is free
text, an identifier or a poorly normalized category.error means that a check crossed its declared
criterion. It is the only level worth using as an automatic gate, and
only after accepting that criterion.With clave (key), each finding carries the values that
identify its rows, so the case can be looked up in the source system
without opening the table:
con_clave <- 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.
h <- hallazgos(con_clave)
fila <- which(h$tipo_hallazgo == "faltantes_disfrazados" &
h$columna == "fecha_evento")
claves <- h$trazabilidad[[fila]]$claves
claves
#> id_evento
#> 1 EVT011
datos_operativos[datos_operativos$id_evento %in% claves$id_evento,
c("id_evento", "fecha_evento")]
#> id_evento fecha_evento
#> 13 EVT011 NULLThe warning, which says that the declared key is not unique, is part
of the answer: datos_operativos has a duplicated row, so no
key can be unique, and the profile records this as a
clave_no_unica finding of severity error.
What could not be measured is recorded, never counted as zero. That
is why an automated check that looks for error must also
read cobertura(): zero errors with unevaluated diagnostics
is not a clean profile.
Counts (rows, missing values, distinct values, exact duplicates and
summaries) use every row; patterns, types and date formats are inferred
from a systematic sample of up to 100,000 rows (muestra).
The guide Leer
un perfil (Reading a profile) explains each column of the
findings, keys, disagreements between columns and the glossary.
To summarize and plot columns there are skimr
and DataExplorer;
to diagnose a table and write up its report, dlookr
and dataReporter;
to evaluate rules that someone has already written, validate,
pointblank,
assertr
and dataquieR.
lupa works on the stretch before the rule and on the one
after it: it finds what no rule names yet, proposes what to measure
without built-in thresholds and, once a rule is confirmed, measures it
with its scope, cleans a copy and compares deliveries. It is meant to be
used alongside those packages, not instead of them.
| Task | Main functions | Guide |
|---|---|---|
| Look at a table for the first time | perfilar(), hallazgos(),
columnas(), cobertura() |
Empezar en diez minutos, Leer un perfil |
| Explore columns and share the result | analizar(), distribucion_valores(),
detectar_asociaciones(), analizar_tiempo(),
reportar(), guardar_analisis() |
Explorar
columnas con analizar() |
| Find structure and legitimately empty cells | detectar_claves(), sugerir_clave(),
detectar_relaciones(),
detectar_dependencias(),
detectar_discordancias(), perfilar_por() |
Estructura no declarada, El vacío por diseño |
| Clean without touching the original | planificar_limpieza(), guiar_limpieza(),
aplicar(); the reparar_codificacion strategy
for mis-encoded text |
Limpiar con un plan |
| Define, measure and evaluate quality | proponer_modelo(), marco_calidad(),
metrica(), modelo(), medir(),
agregar(), evaluar(),
tablero_calidad() |
Del hallazgo a la métrica, Marcos y modelos de calidad |
| Compare deliveries and track quality over time | comparar_perfiles(),
comparar_equivalencia(), historico_calidad(),
detectar_deriva_calidad() |
Comparar entregas y seguir la calidad |
| Profile a database or a collection of tables | perfilar_dbi(), plan_perfilado_dbi(),
requisitos_motor(), coleccion(),
perfilar_coleccion() |
Perfilar una base, Colecciones |
| Find approximate duplicates | detectar_duplicados_aproximados(),
estimar_costo() |
Escala y duplicados aproximados |
The reference documents every function.
perfilar_dbi() returns two blocks:
resumen_tabla, with the aggregates that the engine computes
in SQL, and perfil_muestra, the row-level profile of what
is read into R. By default the aggregates use the whole table and the
row-level profile tries to bring in all the rows that
fit within the caps: it is not a cheap sample.
con <- DBI::dbConnect(RSQLite::SQLite(), ":memory:")
DBI::dbWriteTable(con, "eventos", datos_operativos)
en_la_base <- perfilar_dbi(con, "eventos")
names(en_la_base)
#> [1] "resumen_tabla" "perfil_muestra"| Option | SQL aggregates over | Brings rows into R? |
|---|---|---|
Default: universo = "tabla_completa",
muestra = Inf |
the whole table | Yes: all that fit within the cell and byte caps |
muestra = n |
the whole table | Yes, up to n: it bounds the read, not the
aggregates |
universo = "muestra_motor",
muestra_motor = n |
a sample taken by the engine | Yes, the rows in that sample |
bloque_muestra = "solo_agregados" |
whatever universo says |
No: aggregates only, no row-level findings |
Without orden_muestra, a muestra = n read
gets the first rows the engine returns, in an order SQL does not
guarantee; with orden_muestra the read can be reproduced.
What was read into R is described in
perfil_muestra$meta$origen_dbi$muestreo;
resumen_tabla$meta$muestreo exists only with
universo = "muestra_motor". There is no snapshot:
resumen_tabla$meta$snapshot is FALSE, because
each aggregate is measured in its own query and the table can change
from one query to the next.
The tested engines are DuckDB, MariaDB, MySQL, PostgreSQL, SQL Server and SQLite. The step-by-step walkthrough is in Perfilar una base (Profiling a database).
max_celdas_muestra) and 512
MiB (max_bytes_muestra), and the cut is recorded in
cobertura() together with the cap that triggered it (Motores
y escala).perfilar_dbi(),
politica_costo = "todas" (the default) computes the mode
and the median in every column; "por_cardinalidad" skips
the mode where the proportion of distinct values reaches
umbral_cardinalidad (0.5), and
plan_perfilado_dbi() shows the queries before running them
(Motores
y escala).detectar_duplicados_aproximados() runs an exact comparison
of up to 50,000,000 pairs (max_pares). With the default
strategy ("auto"), no sample and that cap, more than 10,000
rows take it to MinHash/LSH, which can miss pairs and says so
(candidates, bands and the collision guarantee); with a lower
max_pares or a sample it stays with the exact or sampled
comparison, and estrategia = "lsh" forces it.
estimar_costo() anticipates the switch (Escala
y duplicados aproximados).perfilar() may take; the
estimate relies on an assumption built into the code (10,000 cells per
second), not on a measurement of the machine; the dated measurements are
in Evidencia
y rendimiento.perfilar()
and analizar() take minutes, not seconds: profile a sample
or use perfilar_dbi() instead. The measured figures, with
their dates, are in Evidencia
y rendimiento (Evidence and performance).analizar() gathers the profile, the distributions, the
associations and the temporal analysis into one object, together with a
proposed quality model, its dashboard and a cleaning plan;
reportar() writes it all to a self-contained HTML file for
sharing.
analisis <- analizar(datos_operativos)
reportar(analisis, archivo = "informe-lupa.html")The proposal is only a suggestion: domains and patterns learned from
the same delivery are proposed as inactive and are not measured until
someone confirms them. guardar_analisis() and
leer_analisis() save and restore the whole analysis. The
guide is Explorar
columnas con analizar() (Exploring columns with
analizar()).
lupa does not turn the name of a quality framework into
a measurement. A framework is a taxonomy: a factor is measured when the
data provide evidence for it or when the requirement and the metric that
measures it are declared. This is the coverage of the included
frameworks:
| Framework | Factors | Reachable by declaring | Out of scope | Measured by perfilar()
alone |
|---|---|---|---|---|
| AGESIC | 17 | 12 | 5 | 2 |
| CEPAL | 19 | 6 | 13 | 0 |
| ISO/IEC 25012 | 15 | 15 | 0 | 0 |
The 13 CEPAL principles that are out of scope concern the statistical
system and its institutional process, not the values in a table. ISO/IEC
25012 is fully covered once its requirements and metrics are declared.
cobertura_analisis() reports which factors were measured;
the absence of findings is not a claim that the data are good.
Nor does it certify a dataset or invent a global score:
indice_calidad() computes an index only from the weights
the project declares. And it does not modify the input: no analysis
function alters the table it receives (not its values, types, names or
attributes), not even a data.table. Cleaning returns a copy
and leaves the original intact.
The guides are meant to be read in this order, and all 12 are on the
website. Guides 1 to 6 also ship with the package
(vignette("lupa") opens the first one); 7 to 12 are on the
website only.
clave, read the coverage, save a report and clean a column
in a copy.analizar() (Exploring columns with
analizar()): what analizar() builds in a
single call; how to read patterns, types, dates, distributions,
associations and time; and how to save and report the result.perfilar_dbi(), what each option brings into R and where
the sampling metadata is kept.The detailed reference lives in four articles on the website, in
Spanish. Whatever changes the cost, the memory use or the meaning of a
result is also in the help page of each function, which ships with the
package (?perfilar, ?perfilar_dbi,
?plan_perfilado_dbi,
?detectar_duplicados_aproximados).
| Topic | Where |
|---|---|
| Universes and sampling in SQL, metadata, tested engines with version and date, costs, batches, memory budgets, consistency across queries, dialects and threads | Motores y escala |
What uses every row and what is sampled, no_aplica and
no_disponible, units and traceability, sequential
numberings, sentinels, the full finding vocabulary and what gets
masked |
Alcance y cobertura |
| Evidence benchmarks, performance figures and figures from production data, each with its date and its reproducer | Evidencia y rendimiento |
| Printing in any locale and repairing mis-encoded text | Codificación y locales |
| Every signature, default value and return structure | Reference |
Each piece of evidence states how it is reproduced: whether it regenerates offline, needs a database engine or depends on a downloaded resource, with its date and checksum (Evidencia y rendimiento).
citation("lupa")The conceptual references are Batini and Scannapieco (2016), the Data Quality Management Framework for Digital Government v1.6 by AGESIC, the CEA/CEPAL quality assurance framework (2022) and ISO/IEC 25012:2008.
The public API of lupa may change before 1.0; breaking
changes are announced in NEWS, with
a deprecation warning first whenever possible. The stable contracts are
the declared units, the scope, the protection of personal data and the
audit log; implementation details and timings may change as long as
those contracts still hold.
Bugs, proposals and documentation fixes go to the issue tracker, and CONTRIBUTING.md
explains how to contribute. lupa is released under the GPL-3; inst/COPYRIGHTS
details the Apache-2.0 material translated from ftfy and
the MIT data taken from naniar.
These binaries (installable software) and packages are in development.
They may not be fully stable and should be used with caution. We make no claims about them.
Health stats visible at Monitor.