Skip to contents

This guide is the English version of 7. Del hallazgo a la métrica. lupa writes its messages and tables in Spanish, so the output below is in Spanish.

The profile of datos_operativos says that the column canal (channel) has missing values. They are disguised: one is "S/D" and another, an empty text. How does that finding become a measurement that repeats with each delivery and is compared with a requirement?

In this guide:

  • going from the findings to a proposal of metrics with proponer_modelo();
  • deciding what is measured and measuring it with medir();
  • aggregating the measures of each cell up to the column with agregar();
  • evaluating the aggregated measure against a threshold with evaluar();
  • seeing which part of a quality framework was covered with cobertura_analisis().

The starting finding

The table is profiled and the findings of the profile in canal are inspected. The date is fixed so that the example always gives the same result.

library(lupa)

perfil <- perfilar(datos_operativos, fecha = "2026-01-15")
encontrados <- hallazgos(perfil)
encontrados[encontrados$columna %in% "canal", c("tipo_hallazgo", "severidad")]
#>                  tipo_hallazgo  severidad
#> 8                    faltantes sospechoso
#> 9        faltantes_disfrazados      error
#> 10          espacios_sobrantes sospechoso
#> 11   mayusculas_inconsistentes sospechoso
#> 19 casi_duplicados_vocabulario sospechoso

The one with severity error is faltantes_disfrazados (disguised missing values). Its evidence says which ones they are:

disfrazados <- encontrados$columna %in% "canal" &
  encontrados$tipo_hallazgo == "faltantes_disfrazados"
writeLines(encontrados$evidencia[disfrazados])
#> <blanco> (1); S/D (1)

A finding describes this delivery. To know whether the next one is better or worse, and whether it reaches the required level, a metric is needed.

From the findings to a proposal of metrics

A metric is a named way of measuring. It is bound to a table and to one or more columns, and it returns a result for each object it measures. NoNulo (not null), the completeness metric, returns a 1 for each cell with a value and a 0 for each empty cell. What counts as empty is part of its configuration: besides NA, the values declared to it in valores_nulos (null values).

lupa does not decide what to measure: it proposes. proponer_modelo() (propose a model) goes through the findings and returns an editable table, with one row per suggested metric and the finding it came from. It measures nothing.

propuesta <- proponer_modelo(perfil, datos_operativos)
propuesta[, 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   FALSE

The column incluir (include) says which rows stay active. lupa marks them like this:

  • it activates the ones that come from an observed defect: the missing values of 4 columns and the duplicated rows;
  • it leaves off the ones that come from a habit of this delivery, such as a format pattern or the domain of zona (zone).

A single delivery does not show that those habits are requirements. And measuring them on the same delivery they came from would say little.

The row of canal shows the step from the finding to the metric. The values that the profile recognized as disguised missing values move into the configuration of NoNulo:

fila_canal <- which(propuesta$id_sugerencia == "NoNulo:canal")
writeLines(strwrap(propuesta$justificacion[fila_canal], 88))
#> La columna contiene 2 ausentes totales, incluidos 2 disfrazados. Se normalizan: sus
#> valores se declaran en `valores_nulos` y la métrica los cuenta como faltantes.
propuesta$configuracion[[fila_canal]]
#> $valores_nulos
#> [1] ""    "S/D"

Deciding what is measured

The proposal is edited: the decision belongs to whoever uses it. Here the interest is completeness: only the 4 rows of NoNulo stay active, and the one for duplicates is turned off. modelo_desde_propuesta() (model from proposal) turns the active rows into an operational model. It is the list of metrics, already bound to their columns, that medir() (measure) is going to run. It does not measure either.

propuesta$incluir <- propuesta$metrica == "NoNulo"
propuesta[propuesta$incluir, c("metrica", "atributos")]
#>   metrica      atributos
#> 2  NoNulo          canal
#> 3  NoNulo codigo_usuario
#> 4  NoNulo   fecha_evento
#> 5  NoNulo          monto
modelo_completitud <- modelo_desde_propuesta(propuesta)

Measuring

medir() takes the model first and the data after. The identifier and the date of the run stay in each measure; with them, deliveries are compared later.

medicion <- medir(
  modelo_completitud, datos_operativos,
  id_medicion = "entrega-enero",
  fecha = "2026-01-15"
)
nrow(medicion)
#> [1] 52
medicion[medicion$atributo == "canal", c("objeto_medible", "resultado")]
#>                objeto_medible resultado
#> 1   datos_operativos$canal[1]         1
#> 2   datos_operativos$canal[2]         1
#> 3   datos_operativos$canal[3]         1
#> 4   datos_operativos$canal[4]         1
#> 5   datos_operativos$canal[5]         0
#> 6   datos_operativos$canal[6]         1
#> 7   datos_operativos$canal[7]         1
#> 8   datos_operativos$canal[8]         1
#> 9   datos_operativos$canal[9]         1
#> 10 datos_operativos$canal[10]         1
#> 11 datos_operativos$canal[11]         1
#> 12 datos_operativos$canal[12]         1
#> 13 datos_operativos$canal[13]         0

Each row is a measure: a result for an object, here a cell. The 4 columns by 13 rows give 52 measures. In canal, rows 5 and 13 are 0. They are the "S/D" and the empty text, which NoNulo counted as missing because the configuration asked it to.

Aggregating up to the column

One measure per cell does not yet answer the underlying question: how complete each column is. For that, the measures are aggregated. The vocabulary of the levels is the one of the quality framework, and granularidades() (granularities) translates it into the one of tables:

granularidades()[c(1, 2, 4, 5), c("granularidad", "relacional")]
#>        granularidad relacional
#> 1 instanciaAtributo      celda
#> 2          atributo    columna
#> 4  instanciaEntidad      tupla
#> 5           entidad      tabla

In a table:

  • an attribute instance is a cell;
  • an attribute, a column;
  • an entity instance, a row (a tuple);
  • an entity, a table.

These terms are also in the glossary (in Spanish). From the cell to the column the step is ratio, which computes the proportion of ones:

completitud <- agregar(medicion, "atributo", "ratio")
completitud[, c("atributo", "resultado", "agregacion")]
#>         atributo resultado agregacion
#> 1          canal 0.8461538      ratio
#> 2 codigo_usuario 0.9230769      ratio
#> 3   fecha_evento 0.9230769      ratio
#> 4          monto 0.9230769      ratio

agregar() (aggregate) combines measures of a single metric and a single run. It only follows the steps that transiciones_granularidad() (granularity transitions) declares:

  • from the cell to the column or to the row;
  • from the column or the row to the table;
  • from the table upwards.

A cell can be aggregated towards its column or towards its row: they are 2 different directions, not rungs of a single ladder. The available aggregations are:

  • ratio, for boolean results;
  • ratio_umbral (threshold ratio), for real results compared with a threshold;
  • promedio (mean);
  • promedio_ponderado (weighted mean), with weights in [0, 1] that add up to 1 per target.

Unbounded results keep their unit and do not enter these aggregations. They are, for example, the deviation of ErrorEstandar (standard error) or a staleness measured in days.

Evaluating against a threshold

A rule expresses a condition on results. An evaluation profile gathers rules. That profile is not the object perfilar() returns: it is the second argument of evaluar() (evaluate). Each column is required to be at least 90% complete:

regla <- regla_evaluacion(
  "Completitud de al menos 90 %",
  function(x) x >= 0.9
)
publicable <- perfil_evaluacion("Publicable", regla)
evaluacion <- evaluar(completitud, publicable)
evaluacion$perfiles[, c("perfil", "n_reglas", "resultado")]
#>       perfil n_reglas resultado
#> 1 Publicable        1      0.75

The rule was applied to each of the 4 aggregated measures. The result of a rule is the proportion of measures that meet it. The result of a profile is the simple mean of its rules. These are the ones that met it:

cumple <- evaluacion$medidas$resultado[
  match(completitud$id_medida, evaluacion$medidas$id_medida)
]
data.frame(
  columna = completitud$atributo,
  completitud = round(completitud$resultado, 3),
  cumple = cumple
)
#>          columna completitud cumple
#> 1          canal       0.846  FALSE
#> 2 codigo_usuario       0.923   TRUE
#> 3   fecha_evento       0.923   TRUE
#> 4          monto       0.923   TRUE

Every column reaches 90% except canal, which has 0.846: the result is 3 out of 4. The rule really compares the completeness of each column with its threshold. Another threshold gives another result:

resultado_con_umbral <- function(umbral, medidas) {
  regla <- regla_evaluacion("Completitud", function(x) x >= umbral)
  evaluar(medidas, perfil_evaluacion("Publicable", regla))$perfiles$resultado
}
umbrales <- c(0.80, 0.90, 0.95)
data.frame(
  umbral = umbrales,
  agregada = vapply(umbrales, resultado_con_umbral, numeric(1), completitud),
  por_celda = vapply(umbrales, resultado_con_umbral, numeric(1), medicion)
)
#>   umbral agregada por_celda
#> 1   0.80     1.00 0.9038462
#> 2   0.90     0.75 0.9038462
#> 3   0.95     0.00 0.9038462

With the aggregated measure, the result follows the threshold:

  • with 80%, all 4 columns meet it;
  • with 90%, 3 meet it;
  • with 95%, none.

The column por_celda (per cell) shows what happens if the measurement is evaluated without aggregating. The rule asks each cell whether its 0 or its 1 reaches the threshold. The result is the proportion of cells with a value, 0.904, whatever the threshold. That is why the measures are aggregated before evaluating.

The evaluation applies a condition to measures that already exist. It does not read datos_operativos again or replace the measurement.

🔎 What was covered

The absence of findings does not prove that a table is fine: maybe nobody measured what matters. cobertura_analisis() (analysis coverage) crosses the profile and the measurement with a quality framework. It says, factor by factor, what was measured. The third argument is called modelo (model), but it takes a framework such as the one marco_calidad() (quality framework) creates. By default, the AGESIC one.

cobertura <- cobertura_analisis(perfil, medicion, modelo = marco_agesic())
cobertura[, c("dimension", "factor", "estado")]
#>       dimension                        factor           estado
#> 1     Exactitud         Correctitud semántica     no_declarada
#> 2     Exactitud        Correctitud sintáctica     no_declarada
#> 3     Exactitud                     Precisión     no_declarada
#> 4     Exactitud Exactitud posicional absoluta        no_aplica
#> 5     Exactitud Exactitud posicional relativa        no_aplica
#> 6     Exactitud                     Fidelidad fuera_de_alcance
#> 7  Consistencia      Integridad inter-entidad     no_declarada
#> 8  Consistencia      Integridad intra-entidad     no_declarada
#> 9  Consistencia         Integridad de dominio     no_declarada
#> 10 Consistencia       Consistencia topológica        no_aplica
#> 11  Completitud                     Cobertura     no_declarada
#> 12  Completitud                      Densidad           medida
#> 13  Completitud                      Comisión        no_aplica
#> 14     Unicidad                No-duplicación           medida
#> 15     Unicidad              No-contradicción     no_declarada
#> 16     Frescura                    Actualidad     no_declarada
#> 17     Frescura                   Oportunidad     no_declarada

estado (status) tells these cases apart:

  • medida (measured);
  • no_declarada (not declared), when nobody asked to measure it;
  • no_aplica (does not apply), when it does not fit the types of the table, like positional accuracy when there are no geometries;
  • fuera_de_alcance (out of scope), when lupa cannot measure it and says so.

Each row explains why:

medidos <- cobertura[cobertura$estado == "medida", ]
writeLines(paste0(medidos$factor, ": ", medidos$motivo))
#> Densidad: La corrida contiene al menos una métrica instanciada para este factor.
#> No-duplicación: El perfil examinó duplicación de valores, columnas y filas exactas.

Density was backed by the measurement; non-duplication, by what the profile examines on its own. What is missing carries how to solve it:

sintaxis <- cobertura$factor == "Correctitud sintáctica"
writeLines(cobertura$como_resolverlo[sintaxis])
#> Aplicar `especializar()` a `metricas_nucleo()$Formato` con `expresion_regular`, o bien `diccionario`, o bien `validador`; luego `instanciar()` sobre un atributo; después `medir()`.

Summary

  • proponer_modelo() turns the findings into an editable proposal: the disguised missing values the profile found move into the configuration of NoNulo.
  • The column incluir is decided by whoever uses the proposal; what was learned from a single delivery arrives turned off.
  • medir() returns one measure per object, with the id_medicion and the date of the run.
  • Aggregate before evaluating: agregar(medicion, "atributo", "ratio") gives the completeness of each column, and the rule compares that number with its threshold.
  • cobertura_analisis() says which factors of the framework were measured and how to cover the missing ones.

🧭 Next step

8. Marcos y modelos de calidad (Quality frameworks and models, in Spanish) declares a framework of its own and builds custom metrics. Among them is the one that the row of syntactic correctness asks for.