---
title: "6. Limpiar con un plan"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{6. Limpiar con un plan}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

```{r, setup, include=FALSE}
library(lupa)
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)
```

El perfil encontró problemas. ¿Cómo corregimos los que se pueden corregir sin romper
nada, decidiendo nosotros lo que depende del dominio y dejando registro de cada cambio?

> **En esta guía:**
>
> - qué propone `planificar_limpieza()`, qué recomienda y qué deja para nosotros;
> - cómo elegir entre alternativas excluyentes para un mismo hallazgo;
> - cómo aplicar el plan sobre una copia y comparar los valores antes y después;
> - qué dice el plan de lo que no cubre;
> - cómo imputar un valor desde una dependencia que confirmamos.

Usamos `datos_administrativos`, el otro conjunto de ejemplo del paquete:
`r nrow(datos_administrativos)` registros de personas con
defectos sembrados, como
una cédula `S/D`, un sexo en blanco, fechas de nacimiento en formatos mezclados, un
ingreso `-99`, una columna constante y dos filas idénticas.

```{r}
library(lupa)

datos_administrativos[, c("id_persona", "cedula", "fecha_nacimiento", "sexo", "ingreso")]
```

## Un plan, no un cambio

Diagnosticar no autoriza a modificar datos. `planificar_limpieza()` no toca nada: devuelve
una tabla de acciones posibles, una por fila, que se puede filtrar y editar como cualquier
`data.frame`.

```{r}
perfil <- perfilar(datos_administrativos)
plan <- planificar_limpieza(perfil, datos_administrativos)
plan[, c("id_accion", "columna", "estrategia", "recomendada", "aplicar")]
```

`aplicar` dice qué se ejecutaría si aplicáramos el plan tal como está: por omisión,
sólo lo recomendado. `estado` separa lo que está listo de lo que sólo pide revisar
(`informativa`) y de lo que no se puede ejecutar sin una decisión previa (`bloqueada`):

```{r}
plan[plan$estado != "lista", c("id_accion", "estrategia", "estado")]
```

## Recomendaciones y decisiones de dominio

Una acción se recomienda sólo si es correcta con independencia del dominio: por ejemplo,
convertir un centinela textual inequívoco a `NA` o quitar espacios de borde. Normalizar
mayúsculas, convertir un centinela numérico o eliminar una columna constante exige
contexto y queda desactivado. La justificación acompaña cada alternativa:

```{r}
justificaciones <- plan[plan$columna %in% c("sexo", "ingreso"), ]
for (i in seq_len(nrow(justificaciones))) {
  cat(justificaciones$id_accion[i], justificaciones$estrategia[i], "\n")
  writeLines(strwrap(justificaciones$justificacion[i], 86, indent = 2, exdent = 2))
}
```

Convertir `S/D` en ausencia es seguro en casi cualquier columna, y aun así la
justificación pide confirmarlo. Que `-99` signifique "sin dato" en `ingreso` sólo lo sabe
quien conoce el sistema, así que la acción queda propuesta y sin activar.

Si lo que molesta es sólo la comparación (mayúsculas, acentos, puntuación), no hace falta
limpiar. `normalizacion()` declara qué diferencias ignora el perfil al comparar valores;
cambia la representación de comparación y nunca los datos:

```{r}
normalizacion(acentos = FALSE, puntuacion = TRUE)
```

## Grupos mutuamente excluyentes

Las alternativas para un mismo hallazgo comparten `grupo`, y como máximo una puede tener
`aplicar = TRUE`. `decision_grupo` distingue un grupo pendiente de una decisión explícita
de no actuar: no se inventa una fila ficticia de "no hacer nada".

```{r}
plan[!is.na(plan$grupo), c(
  "grupo", "estrategia", "recomendada", "aplicar", "decision_grupo"
)]
```

En el grupo de las filas duplicadas, por ejemplo, las alternativas son marcarlas,
conservar la primera o conservar la más completa; marcar es lo recomendado porque no
elimina nada.
`decision_grupo` vale `pendiente` donde falta nuestra decisión (el `-99` de `ingreso`) y
`recomendada` donde el plan sigue su propia recomendación, aunque esa recomendación sea no
actuar: la columna constante se deja como está.

El modo guiado, `guiar_limpieza()`, es una capa opcional. En una consola recorre sólo las
decisiones pendientes o riesgosas, pregunta qué hacer con cada una y devuelve el plan con
nuestras respuestas:

```{r, eval = FALSE}
plan <- guiar_limpieza(plan, datos_administrativos)
```

Al tejer esta guía no hay nadie que conteste, y fuera de una sesión interactiva
`guiar_limpieza()` devuelve el plan sin cambios y sin bloquear; por eso seguimos con las
acciones que marcamos a mano.

```{r, echo = FALSE}
stopifnot(identical(guiar_limpieza(plan, datos_administrativos), plan))
```

## Aplicar sobre una copia

`aplicar()` ejecuta las acciones con `aplicar = TRUE` sobre una copia. El original queda
intacto, y el registro dice qué hizo cada acción:

```{r}
copia <- datos_administrativos
resultado <- aplicar(plan, datos_administrativos)
identical(datos_administrativos, copia)
resultado$registro[, c("id_accion", "estrategia", "estado", "n_cambiadas")]
```

Veamos los valores antes y después en las dos columnas que cambiaron:

```{r}
data.frame(
  cedula_antes = datos_administrativos$cedula,
  cedula_despues = resultado$datos$cedula,
  sexo_antes = datos_administrativos$sexo,
  sexo_despues = resultado$datos$sexo
)
```

`S/D` y la celda en blanco pasaron a `NA`. `MAL-5` y `00000033` siguen igual: corregir una
cédula exige saber cuál era, y eso no sale de los datos.

Las marcas no cambian valores: agregan información. Las filas duplicadas quedan señaladas
en dos columnas nuevas, `.fila_duplicada` (la repetición) y `.grupo_duplicado` (todas las
filas del grupo), y las columnas duplicadas, en un atributo:

```{r}
marcadas <- !is.na(resultado$datos$.grupo_duplicado)
resultado$datos[marcadas, c("id_persona", "cedula", ".fila_duplicada", ".grupo_duplicado")]
attr(resultado$datos, "columnas_duplicadas_marcadas")
```

El plan se puede editar antes de aplicar: quitar una acción, corregir un parámetro o
cambiar `n_afectadas` no altera la obligación de observar qué pasó. Si una acción
seleccionada no cambia ningún valor, el registro dice `fallida` con su motivo, incluso
cuando el estimado está ausente (`NA`).

Las acciones destructivas nunca son recomendadas. Aunque las activemos en el plan,
`aplicar()` exige además `permitir_eliminacion = TRUE`; lo retirado se conserva en
`resultado$eliminados` salvo decisión contraria. Sin ese consentimiento, la llamada se
niega **antes de tocar nada**: no corre ninguna acción del plan, tampoco las que no
eliminan, y el error dice cuáles son los dos caminos.

`n_no_reversibles`, en el registro, no se lee por el nombre de la acción sino por lo que
quedó en el resultado: una normalización que funde dos valores que eran distintos (pasar
`ana` y `ANA` a `ANA`) cuenta esas celdas, porque lo que las separaba no quedó en ningún
lado.

## Lo que el plan no cubre lo dice

Que el plan tenga pocas acciones no significa que lo demás esté bien, así que declara sus
huecos en tres atributos: `cobertura_diagnosticos` (los diagnósticos que el perfil no pudo
evaluar), `hallazgos_sin_accion_por_columna_ambigua` (los hallazgos cuya columna comparte
nombre con otra, así que no hay forma de saber sobre cuál actuaría la limpieza) y
`hallazgos_sin_accion` (los hallazgos que el perfil midió y que ninguna acción atiende,
con el motivo medido de cada uno). La impresión del plan los anuncia y el informe HTML les
da su propia sección.

```{r}
sin_accion <- attr(plan, "hallazgos_sin_accion")
sin_accion[, c("hallazgo", "columna", "severidad")]
```

## Volver a perfilar

El ciclo termina volviendo a perfilar el resultado y comparándolo con el perfil de
partida. `comparar_perfiles()` dice qué hallazgos se resolvieron y cuáles aparecieron:

```{r}
perfil_despues <- perfilar(resultado$datos)
cambios <- comparar_perfiles(perfil, perfil_despues)
cambios[cambios$aspecto == "hallazgo", c("columna", "cambio", "valor_anterior", "valor_actual")]
```

Los faltantes disfrazados de `cedula` y `sexo` quedaron `resuelto`. Los hallazgos
`aparecido` en `.grupo_duplicado` son la huella de la marca: esa columna sólo tiene valor
en las filas duplicadas, así que el perfil la encuentra casi vacía y con un único valor.
Y `filas_duplicadas` queda `no_comparable`: la tabla ganó las columnas de marca, y con
otro conjunto de columnas no se puede decidir si las filas repetidas se resolvieron. La
comparación entre entregas se explica en [9. Comparar entregas y seguir la calidad](https://sebollin.github.io/lupa/articles/historico-y-deriva.html).

## Imputar por una dependencia

Una dependencia funcional exacta, como las que muestra
[4. Estructura no declarada](estructura-no-declarada.html), permite deducir un valor desde
los propios datos. Pero sigue siendo una regularidad observada en una entrega: puede
representar una regla real o un error sistemático. Por eso la acción se ofrece con su
mapa y su soporte, pero no queda recomendada ni activa.

En esta tabla de trámites, `codigo` determina `descripcion`, y faltan algunas
descripciones:

```{r}
tramites <- data.frame(
  id_tramite = 101:112,
  codigo = rep(1:3, each = 4),
  descripcion = rep(c("Alta", "Baja", "Cambio"), each = 4),
  stringsAsFactors = FALSE
)
tramites$descripcion[c(2, 6)] <- NA

perfil_fd <- perfilar(tramites, muestra = Inf)
plan_fd <- planificar_limpieza(perfil_fd, tramites)
imputacion <- grep("^imputar_dependencia_funcional", plan_fd$estrategia)
plan_fd[imputacion, c("id_accion", "estrategia", "recomendada", "aplicar")]
writeLines(strwrap(plan_fd$justificacion[imputacion], 88))
plan_fd$parametros[[imputacion]]$mapa
```

El mapa es la regla observada: cada código con su descripción. Después de confirmar que es
una regla del sistema, activamos la acción y registramos la decisión. La dependencia
vuelve a validarse al aplicar el plan:

```{r}
plan_fd$aplicar[imputacion] <- TRUE
plan_fd$decision_grupo[imputacion] <- "elegida"
resultado_fd <- aplicar(plan_fd, tramites)
resultado_fd$registro[, c("id_accion", "estrategia", "estado", "n_cambiadas")]
data.frame(
  codigo = tramites$codigo,
  antes = tramites$descripcion,
  despues = resultado_fd$datos$descripcion
)
```

Las descripciones que faltaban se completaron desde el mapa (`n_cambiadas` las cuenta),
y nada más cambió.

## Resumen

- `planificar_limpieza()` propone acciones y no toca los datos; sólo activa lo que es
  correcto con independencia del dominio.
- Las alternativas para un mismo hallazgo forman un grupo, y como máximo una se aplica.
- `aplicar()` trabaja sobre una copia, registra cada acción y exige consentimiento
  explícito para eliminar.
- El plan declara lo que no cubre, y volver a perfilar muestra qué se resolvió.
- Imputar desde una dependencia exige confirmarla antes como regla.

Si la limpieza es parte de entregas periódicas y la pregunta es qué cambió entre una y
otra, se puede saltar directamente a
[9. Comparar entregas y seguir la calidad](https://sebollin.github.io/lupa/articles/historico-y-deriva.html).

## Paso siguiente

[7. Del hallazgo a la métrica](https://sebollin.github.io/lupa/articles/del-hallazgo-a-la-metrica.html),
en el sitio del paquete: cómo pasar de lo que el perfil encontró a
métricas que se miden y se evalúan contra un umbral.

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