---
title: "5. El vacío por diseño"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{5. El vacío por diseño}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r setup, include=FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
# El ancho se restaura al terminar la viñeta: knitr corre cada una en su propia
# sesion, asi que no se filtra, pero el CRAN Cookbook pide devolver toda opcion
# que se cambie y no cuesta nada cumplirlo.
.opciones_previas <- options(width = 90)
library(lupa)
```

¿Por qué un perfil puede decir que falta la mayor parte de una columna completa donde
corresponde? Porque nadie le dijo al paquete dónde corresponde.

> **En esta guía:**
>
> - por qué una celda vacía puede ser correcta, y qué informa el perfil si no se declara;
> - la señal con que `lupa` avisa que hay algo para declarar;
> - cómo declaramos el universo de una columna, y qué error nuevo se vuelve visible;
> - todo lo que se le puede declarar al paquete, en una sola tabla.

## Qué pasa si no se declara nada

Empezamos por el caso mínimo: una encuesta construida acá, con una columna que sólo
corresponde a parte de las filas (`datos_operativos` no tiene ninguna). Pregunta si la
persona tiene auto y, sólo si tiene, de qué marca:

```{r}
tiene_auto <- rep(c("Si", "No"), c(600, 1400))
encuesta <- data.frame(
  tiene_auto = tiene_auto,
  marca_auto = ifelse(tiene_auto == "Si", "Ford", NA),
  stringsAsFactors = FALSE
)

perfil <- perfilar(encuesta, analizar_dependencias = FALSE)
h <- hallazgos(perfil)
h[h$tipo_hallazgo == "faltantes", c("columna", "severidad", "n_afectados", "n_evaluados")]
```

Falta el `r round(100 * mean(is.na(encuesta$marca_auto)))` % de `marca_auto`, con
severidad `error`. Pero la columna está completa en todas las filas donde corresponde que
tenga valor.

## 6 formas de tabla donde el vacío es la estructura {#seis-formas-de-tabla-donde-el-vacío-es-la-estructura}

La encuesta no es un caso raro. En estas formas de tabla, los vacíos son parte del
diseño:

| forma | por qué hay vacíos |
| --- | --- |
| historial con vigencia abierta | `valido_hasta` vacío significa "sigue vigente" |
| encuesta con salto de patrón | quien no tiene auto no responde nada del auto |
| columnas excluyentes por subtipo | una persona jurídica no tiene fecha de nacimiento |
| documento por tipo | quien tiene cédula no tiene pasaporte |
| entidad-atributo-valor | cada fila llena una sola de las columnas de valor |
| formato largo | la misma tabla de arriba, apilada |

## El supuesto que `lupa` hace

Todo perfilador de datos asume una forma de tabla. `lupa` asume 3 cosas a la vez:

1. una fila es un hecho sobre una entidad;
2. una columna es un dominio semántico;
3. una celda vacía debería tener un valor y no lo tiene.

Las 3 son razonables y ninguna es universal. La tercera es la que hace daño: una base
administrativa está llena de vacíos legítimos, celdas donde no corresponde que haya
valor. Sin declarar nada, `lupa` cuenta esos vacíos como ausencia y los informa como
defecto. El conteo es correcto y la lectura es falsa.

## 🔎 La señal que dice que hay algo para declarar {#la-señal-que-dice-que-hay-algo-para-declarar}

El arreglo, que se ve más abajo, exige conocer `aplicabilidad`: el argumento que declara
en qué filas corresponde una columna. Quien no lo conoce recibiría sólo el informe
engañoso. Por eso el mismo perfil trae otra fila:

```{r}
estructural <- h[h$tipo_hallazgo == "posible_ausencia_estructural", ]
estructural[, c("columna", "severidad", "n_afectados")]
writeLines(strwrap(estructural$evidencia, 88))
writeLines(strwrap(estructural$sugerencia, 88))
```

Severidad `ok`: no es un defecto, es una lectura de la estructura.

- La evidencia es un hecho medido sobre los datos. Dice qué proporción de las filas queda
  explicada por el valor de la otra columna. Dice también cuántas ausencias caen donde la
  regla dice que la columna no corresponde: eso cuenta `n_afectados`.
- La sugerencia trae la línea exacta que habría que escribir.

La decisión sigue siendo nuestra. El *universo* de una columna son las filas donde
corresponde que tenga valor (ver el [glosario](leer-un-perfil.html#glosario) de *Leer un
perfil*). El paquete no lo cambia por su cuenta, y las columnas ya declaradas quedan
fuera del examen.

La señal sale por 2 caminos:

- **Una columna determina la presencia de otra**, con un cumplimiento de al menos 0,99.
  Es el caso de la encuesta, y se puede escribir la fórmula.
- **Las filas se reparten entre 2 o más columnas sin pisarse**, y entre todas cubren la
  tabla. Es la forma de los atributos excluyentes por subtipo. No hay fórmula que
  ofrecer, porque falta la columna que diga cuál corresponde. La sugerencia es la
  honesta: si existe esa columna, `aplicabilidad`; si no existe y la ausencia es por
  diseño, `columnas_opcionales`.

Y no dispara por cualquier cosa. Si una parte de las filas rompe el patrón, se calla. La
relación existe pero no es una regla, y ofrecer una fórmula sería afirmar de más. El
sorteo de abajo pone marca de auto a algunas personas que dicen no tener:

```{r}
set.seed(4)
ruidosa <- encuesta
fuera <- sample(which(ruidosa$tiene_auto == "No"), 200)
ruidosa$marca_auto[fuera] <- "Ford"
sum(hallazgos(perfilar(ruidosa, analizar_dependencias = FALSE))$tipo_hallazgo ==
      "posible_ausencia_estructural")
```

Con el `r round(100 * length(fuera) / nrow(ruidosa))` % de las filas fuera del patrón, la
señal no aparece.

## Declarar el universo

`aplicabilidad` toma una fórmula por columna. Las filas donde el predicado no se cumple
salen del universo de esa columna:

```{r}
declarado <- perfilar(
  encuesta,
  analizar_dependencias = FALSE,
  aplicabilidad = list(marca_auto = ~ tiene_auto == "Si")
)

sum(hallazgos(declarado)$tipo_hallazgo == "faltantes")
columnas(declarado)[
  columnas(declarado)$columna == "marca_auto",
  c("n", "n_aplicables", "n_no_aplica", "n_faltantes", "prop_faltantes")
]
```

El recorte no es silencioso. La regla queda escrita donde se busca lo que no se midió:

```{r}
cobertura_declarada <- cobertura(declarado)
writeLines(strwrap(
  cobertura_declarada$motivo[cobertura_declarada$columna == "marca_auto"], 88
))
```

Cuando la ausencia nunca es defecto y no hay regla que escribir, alcanza con
`columnas_opcionales`. El caso típico es el historial con vigencia abierta:

```{r}
historial <- data.frame(
  id = 1:100,
  valido_hasta = c(rep(NA, 40), 41:100)
)
opcional <- perfilar(
  historial, analizar_dependencias = FALSE,
  columnas_opcionales = "valido_hasta"
)
sum(hallazgos(opcional)$tipo_hallazgo == "faltantes")
```

Con el `r round(100 * mean(is.na(historial$valido_hasta)))` % de la columna vacío, el
perfil suma además un aviso de severidad `ok`: `regla_silencia_ausencia`. Lo explicamos
al final de la guía.

## El error simétrico

Declarar el universo hace visible un diagnóstico imposible sin la declaración: el valor
que aparece donde la regla dice que no corresponde.

```{r}
inconsistente <- data.frame(
  tiene_auto = c("Si", "No", "No", "Si"),
  marca_auto = c("Ford", NA, "Fiat", "VW"),
  stringsAsFactors = FALSE
)
p <- perfilar(
  inconsistente, analizar_dependencias = FALSE,
  aplicabilidad = list(marca_auto = ~ tiene_auto == "Si")
)
fuera_de_universo <- hallazgos(p)[
  hallazgos(p)$tipo_hallazgo == "valor_fuera_de_aplicabilidad",
]
fuera_de_universo[, c("columna", "severidad", "n_afectados")]
writeLines(strwrap(fuera_de_universo$evidencia, 88))
fuera_de_universo$trazabilidad[[1]]$indices_fila
```

La tercera fila declara no tener auto y tiene marca. Es un error en los datos, o en la
regla, y sin el universo declarado `lupa` no puede verlo.

## Cuando la regla no se puede determinar

Si la columna que discrimina también está ausente, no se sabe si la columna aplica. Esas
filas no se cuentan como aplicables ni como no aplicables: se declaran aparte, porque no
saber no es lo mismo que no corresponder.

```{r}
dudoso <- data.frame(
  tiene_auto = c("Si", "No", NA, "Si"),
  marca_auto = c("Ford", NA, NA, "VW"),
  stringsAsFactors = FALSE
)
pd <- perfilar(
  dudoso, analizar_dependencias = FALSE,
  aplicabilidad = list(marca_auto = ~ tiene_auto == "Si")
)
columnas(pd)[
  columnas(pd)$columna == "marca_auto",
  c("n_aplicables", "n_no_aplica", "n_aplicabilidad_indeterminada")
]
```

## El formato largo: `perfilar_por()`

Una tabla en formato largo no es una tabla: son muchas apiladas. Perfilarla entera mezcla
dominios que no tienen nada que ver, y el resultado no describe a ninguno. La tabla de
abajo apila 3 atributos de cada entidad; sus valores se sortean:

```{r}
set.seed(9)
n <- 900
atributo <- rep(c("pais", "edad", "correo"), each = n / 3)
largo <- data.frame(
  entidad = rep(seq_len(n / 3), 3),
  atributo = atributo,
  valor_texto = ifelse(
    atributo == "edad", NA,
    ifelse(atributo == "pais", sample(c("UY", "uy", "AR"), n, TRUE),
           paste0("a", seq_len(n), "@x.uy"))
  ),
  valor_num = ifelse(atributo == "edad", sample(18:80, n, TRUE), NA),
  stringsAsFactors = FALSE
)

plano <- perfilar(largo, analizar_dependencias = FALSE)
table(as.character(hallazgos(plano)$tipo_hallazgo))
```

Por grupo, cada atributo se perfila contra su propio dominio. Las columnas enteramente
ausentes dentro del grupo se descartan antes de perfilar, y ése es el paso que desactiva
el faltante falso.

```{r}
por_grupo <- perfilar_por(
  largo, "atributo", clave = "entidad", min_filas = 30,
  analizar_dependencias = FALSE
)
sum(por_grupo$tipo_hallazgo == "faltantes")
table(as.character(por_grupo$tipo_hallazgo))
```

El argumento `clave` no es cosmético. Sin la columna de identidad, cada repetición del
valor de un atributo se lee como fila duplicada. Así, el diagnóstico de unicidad informa
la tabla entera. Si se declara, `perfilar()` separa la unicidad de la ausencia de nulos.
`meta$clave` conserva ambos estados cuando alguno no queda verificado, y la trazabilidad
deja explícita su semántica de R cuando agrupa ausentes.

Lo que se descartó queda declarado:

```{r}
head(attr(por_grupo, "cobertura_grupos"), 3)
```

## Lo que `lupa` no hace

No infiere el modelo. No decide por su cuenta que una tabla es entidad-atributo-valor, ni
que 2 columnas son mutuamente excluyentes. Una heurística así fallaría sobre tablas
anchas legítimas. Por la misma razón, el paquete tampoco adivina claves ni jerarquías.

El universo lo declara quien conoce el dato. Sin declaración, toda la columna aplica y el
resultado es el del principio de esta guía: es la única omisión que no supone nada.

Lo que sí hace es medir la evidencia y ofrecerla. La diferencia importa: sugerir con la
evidencia a la vista deja la decisión donde estaba, y adivinar la mueve.

## ⚠️ Y el reverso: cuando la declaración tapa un problema {#y-el-reverso-cuando-la-declaración-tapa-un-problema}

Una regla declarada puede silenciar algo real. Declarar opcional una columna casi vacía
deja el perfil limpio. Quien no lea la cobertura de diagnósticos no se entera de nada:

```{r}
casi_vacia <- data.frame(
  x = c(rep(NA_character_, 160), rep(letters[1:20], 2)),
  stringsAsFactors = FALSE
)
p <- perfilar(casi_vacia, columnas_opcionales = "x",
              analizar_dependencias = FALSE)
silencio <- hallazgos(p)[hallazgos(p)$tipo_hallazgo == "regla_silencia_ausencia", ]
silencio[, c("columna", "severidad", "n_afectados", "n_evaluados")]
writeLines(strwrap(silencio$evidencia, 88))
```

Con el `r round(100 * mean(is.na(casi_vacia$x)))` % de la columna vacío, el aviso es
`regla_silencia_ausencia`, el mismo que apareció en el historial. También es `ok`, y por
la misma razón: la declaración hizo su trabajo. El aviso existe para que eso sea una
decisión y no un efecto lateral. Vale igual cuando el universo se declaró con
`aplicabilidad` y la columna sigue casi vacía *dentro* de ese universo. Es el caso donde
la regla resultó más ancha que el universo real.

## Todo lo que se le declara al paquete

El universo de una columna es una de varias cosas que `lupa` no puede saber mirando los
datos. Todas siguen la misma idea: quien conoce el dato lo declara, y el paquete lo
respeta y deja constancia. Son argumentos de `perfilar()`, salvo la unidad, que viaja con
la columna. `analizar()` y las demás vías de perfilado los reciben por
`argumentos_perfil` o `...`:

| qué declaramos | argumento | qué cambia en el perfil |
| --- | --- | --- |
| en qué filas corresponde una columna | `aplicabilidad` | las filas fuera del universo no cuentan como faltantes; aparece `valor_fuera_de_aplicabilidad`; la regla queda en `cobertura()` |
| que la ausencia nunca es defecto | `columnas_opcionales` | los faltantes no se informan; si la columna sigue muy vacía, `regla_silencia_ausencia` |
| que un número es un código, no una magnitud | `columnas_codigo` | sin media, mediana, desvío, límites de Tukey ni Benford; se conservan mínimo, máximo, moda y conteos; en `analizar()`, escala nominal |
| qué números significan "sin dato" | `sentinelas_numericos` | se cuentan como faltantes disfrazados y salen de media, mediana, mínimo, máximo, desvío y de los cuantiles de `analizar()`; las dependencias y las asociaciones no evalúan la fila que los tiene |
| qué textos significan "sin dato" | `cadenas_ausencia` | se suman a los que el paquete reconoce y se cuentan como faltantes disfrazados |
| dónde un cero o un negativo no es admisible | `columnas_sin_ceros`, `columnas_no_negativas` | aparecen `ceros_no_permitidos` y `negativos_no_permitidos` |
| qué columnas identifican una fila | `clave` | la trazabilidad trae el valor de la clave de cada fila señalada; una clave repetida o con ausentes es un `error` (`clave_no_unica`, `clave_con_ausentes`) |
| qué columnas traen datos personales | `columnas_personales` | se clasifican como datos personales aunque ni el nombre ni la forma los delaten, y con `proteger_datos_personales = TRUE` sus valores salen enmascarados |
| en qué unidad está una magnitud | atributo `units` de la columna | `columnas()` publica la unidad junto a las cifras, y `clasificar_variables()` la usa para proponer la escala |

Conviene ver 2 de ellas, porque cambian lo que el perfil calcula. Primero, un código de
rubro con un valor fuera de catálogo, sin declarar y declarado:

```{r}
rubros <- data.frame(rubro = rep(1:19, length.out = 60))
rubros$rubro[c(5, 9)] <- 99

campos <- c("columna", "minimo", "maximo", "media", "n_outliers",
            "estado_resumen_cuantitativo")
rbind(
  columnas(perfilar(rubros, analizar_dependencias = FALSE))[, campos],
  columnas(perfilar(rubros, analizar_dependencias = FALSE,
                    columnas_codigo = "rubro"))[, campos]
)
```

Sin declarar, el código se mide como magnitud: tiene media y el `99` sale como valor
atípico. Declarado, la media no se calcula (`omitidos_codigo`). El `99` sigue a la vista
en el máximo, que es donde aparece un código fuera de catálogo.

Después, una edad donde `999` significa "no informada". `999` está en la lista que el
paquete sospecha por omisión, pero una sospecha sólo se informa: no mueve los números.
Declarado, sale del resumen:

```{r}
edades <- data.frame(edad = c(34, 51, 999, 27, 999, 45, 62, 999, 38, 29))

campos <- c("columna", "n_faltantes_disfrazados", "media", "maximo",
            "n_valores_excluidos_resumen")
rbind(
  columnas(perfilar(edades, analizar_dependencias = FALSE))[, campos],
  columnas(perfilar(edades, analizar_dependencias = FALSE,
                    sentinelas_numericos = 999))[, campos]
)
```

`sentinelas_numericos` reemplaza la lista entera: declaramos exactamente los valores que
valen como "sin dato" en nuestros datos. El paquete trae además `sentinelas_naniar`, una
lista más amplia. Se pide de manera explícita, porque valores como `66`, `77` y `88`
pueden ser datos legítimos:

```{r}
codigos <- data.frame(codigo = c(1, 66, 9999))
c(
  por_omision = columnas(perfilar(codigos, analizar_dependencias = FALSE))$
    n_faltantes_disfrazados,
  naniar = columnas(perfilar(codigos, analizar_dependencias = FALSE,
                             sentinelas_numericos = sentinelas_naniar))$
    n_faltantes_disfrazados
)
```

El artículo
[Alcance y cobertura](https://sebollin.github.io/lupa/articles/alcance-y-cobertura.html)
trae los detalles de cada declaración:

- qué se excluye de cada diagnóstico;
- cómo se reconoce una numeración sin declararla;
- qué se enmascara.

## Resumen

- Sin declarar nada, `lupa` cuenta como faltante toda celda vacía, aunque esté vacía por
  diseño.
- `posible_ausencia_estructural` (severidad `ok`) avisa cuándo pasa y trae la línea para
  declararlo.
- `aplicabilidad` recorta el universo de una columna; `columnas_opcionales` exime la
  columna entera, y `regla_silencia_ausencia` avisa si la declaración tapó algo.
- Declarar el universo hace visible `valor_fuera_de_aplicabilidad`, el error del otro
  lado.
- Códigos, centinelas, claves, datos personales y unidades se declaran igual: el paquete
  no los adivina.

## 🧭 Paso siguiente {#paso-siguiente}

[6. Limpiar con un plan](limpiar-con-un-plan.html)
(`vignette("limpiar-con-un-plan")`): cómo corregir una copia de los datos sin perder el
registro de lo que se hizo.

```{r, cierre, include=FALSE}
options(.opciones_previas)
```
