---
title: "3. Explorar columnas con `analizar()`"
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{3. Explorar columnas con `analizar()`}
  %\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 dice qué está mal en una tabla. Antes de decidir qué le exigimos, queremos ver
qué hay en cada columna: forma, tipo, reparto, asociaciones y período de las fechas.
¿Cómo lo vemos todo de una vez, y cómo lo compartimos?

> **En esta guía:**
>
> - qué arma `analizar()` en una sola llamada y dónde queda cada pieza;
> - cómo se leen patrones, tipos, fechas, escalas, distribuciones, asociaciones y tiempo;
> - qué mide `analizar()` por su cuenta y cómo se apaga;
> - cuándo conviene `perfilar()` y cuándo `analizar()`;
> - cómo guardamos el análisis y producimos un informe HTML.

Seguimos con `datos_operativos`, la entrega de `r nrow(datos_operativos)` registros con
defectos sembrados que presenta [1. Empezar en diez minutos](lupa.html).

## Una llamada, todos los componentes

Fijamos `fecha` para que el ejemplo dé siempre el mismo resultado.

```{r}
library(lupa)

analisis <- analizar(
  datos_operativos,
  nombre = "entrega de ejemplo",
  fecha = as.POSIXct("2026-01-15", tz = "UTC")
)
names(analisis)
```

Cada componente es una tabla o una lista de tablas, y casi todos se pueden obtener sueltos
con su propia función:

| componente | qué contiene | función suelta |
| --- | --- | --- |
| `perfil` | hallazgos, resumen por columna, patrones, formatos de fecha, cobertura de diagnósticos | `perfilar()` |
| `variables` | tipo implícito, escala y rol propuestos para cada columna | `clasificar_variables()` |
| `distribuciones` | frecuencias, cuantiles y su alcance | `distribucion_valores()` |
| `asociaciones` | una medida por cada par de columnas que se mueven juntas | `detectar_asociaciones()` |
| `temporal` | período observado, huecos y frecuencia propuesta | `analizar_tiempo()` |
| `propuesta_modelo`, `decision_medicion` | las métricas sugeridas y cuáles se midieron | `proponer_modelo()` |
| `tablero`, `medicion` | la medición agregada | `medir()`, `agregar()`, `tablero_calidad()` |
| `cobertura` | qué factores del marco de calidad quedaron medidos | `cobertura_analisis()` |
| `plan_limpieza` | acciones posibles, ninguna aplicada | `planificar_limpieza()` |
| `advertencias` | lo que el análisis propuso y queda por confirmar | — |
| `meta` | nombre, fecha y qué decisiones tomó la corrida | — |

Los accesores del perfil también leen el análisis: `hallazgos(analisis)` es lo mismo que
`hallazgos(analisis$perfil)`. Miramos los de `fecha_evento`, la columna que seguimos en
toda la guía:

```{r}
h <- hallazgos(analisis)
h[h$columna %in% "fecha_evento", c("tipo_hallazgo", "severidad", "n_afectados")]
```

Cómo se lee cada hallazgo está en [2. Leer un perfil](leer-un-perfil.html). Acá miramos lo
que hay debajo: los valores que los producen.

## Patrones: la forma de los valores

Un patrón resume la forma de un valor:

- `A` es una letra mayúscula;
- `a`, una minúscula;
- `9`, un dígito;
- `+` indica que se repite.

`descubrir_patrones()` recibe un vector, no una tabla, y cuenta cuántos valores tienen
cada forma:

```{r}
descubrir_patrones(datos_operativos$codigo_usuario)
```

```{r, echo = FALSE}
stopifnot(isTRUE(all.equal(
  as.data.frame(descubrir_patrones(datos_operativos$codigo_usuario)),
  as.data.frame(analisis$perfil$patrones$codigo_usuario),
  check.attributes = FALSE
)))
```

La mayoría de los códigos tiene la forma `USR-001`. Se apartan `MAL-5`, `USR0003` y `S/D`.
El último ya figura en el perfil como faltante disfrazado; los otros 2 habría que
revisarlos en el sistema de origen. El análisis guarda la misma tabla para cada columna
en `analisis$perfil$patrones`.

## Tipos y fechas

`fecha_evento` se guarda como texto. Eso no impide proponer un tipo implícito, ni contar
los formatos de fecha por separado:

```{r}
inferir_tipo(datos_operativos$fecha_evento)
formatos <- detectar_formatos_fecha(datos_operativos$fecha_evento)
formatos[, c("formato", "n", "proporcion", "estado", "n_ambiguos")]
```

De los `r attr(formatos, "total")` valores, `r attr(formatos, "compatibles")` se leen
como fecha, en `r nrow(formatos)` formatos distintos. Los que no se leen son `2024.04.17`
y `NULL`. Estas salidas describen compatibilidad y frecuencia: no convierten la columna
ni afirman qué formato debe aceptar el sistema.

Que todos los valores sean compatibles tampoco alcanza. Una columna hecha sólo de fechas
como `01/02/2020` puede leerse al 100 % y seguir siendo ambigua. Sin una convención
externa, no se sabe si el primer número es el día o el mes. El perfil conserva esa
diferencia en `estado_tipo_inferido`:

```{r}
ambigua <- perfilar(
  data.frame(fecha = c("01/02/2020", "02/03/2020", "03/04/2020")),
  muestra = Inf,
  analizar_dependencias = FALSE
)
campos <- c(
  "columna", "tipo_inferido", "proporcion_tipo_inferido", "estado_tipo_inferido"
)
rbind(
  columnas(analisis)[columnas(analisis)$columna == "fecha_evento", campos],
  columnas(ambigua)[, campos]
)
```

En `fecha_evento` el estado es `confirmado` aunque haya valores que no se leen: valores
como `31/01/2024` sólo admiten una lectura. En la columna construida, la proporción es 1
y el estado es `candidato`, porque la lectura todavía no está establecida.

## Escalas y roles propuestos

`variables` separa el tipo de almacenamiento, el tipo implícito y la escala de medición, y
propone un rol para cada columna. Es la salida de `clasificar_variables()`:

```{r}
analisis$variables[, c(
  "columna", "tipo_implicito", "escala_propuesta", "rol", "confianza", "confirmada"
)]
```

`confianza` expone la fuerza de cada propuesta; `confirmada` distingue una declaración de
una inferencia. Sin metadatos, ninguna está confirmada.

`id_registro` sale como una medida discreta: por sus valores, un identificador numérico
no se distingue de una cantidad. Quien conoce la columna lo declara con
`metadatos_variables` o con `columnas_codigo` (ver la tabla final de
[5. El vacío por diseño](vacio-por-diseno.html)).

## Distribuciones

`distribuciones` trae las frecuencias de cada columna y los cuantiles de las numéricas:

```{r}
frecuencias <- analisis$distribuciones$frecuencias
frecuencias[frecuencias$columna == "canal", c("valor", "frecuencia", "proporcion")]

cuantiles <- analisis$distribuciones$cuantiles
cuantiles[cuantiles$columna == "monto", c("probabilidad", "valor", "n_analizados")]
```

Las frecuencias cuentan lo que hay guardado, sin unir variantes. Entre los valores de
`canal` aparecen la celda vacía, `S/D`, `Web` y `"web "` (con un espacio al final).

Los cuantiles de `monto` van de `-99` a `999999`. El perfil señaló `-99` como posible
faltante disfrazado, pero un valor sospechado sólo se informa. No sale de los números
mientras no se lo declare con `sentinelas_numericos`. `analisis$distribuciones$alcance`
dice, columna por columna, cuántas filas se analizaron y si la lista de valores se recortó.

## Asociaciones

Cada fila de `asociaciones` es una medida entre 2 columnas, con el método que se usó:

- Pearson entre numéricas;
- eta cuadrado entre una categoría y un número;
- V de Cramér entre 2 categorías.

La columna `supuesto` dice cómo se trató cada escala.

```{r}
analisis$asociaciones[, c("columna_1", "columna_2", "metodo", "asociacion")]
```

```{r, echo = FALSE}
sin_fila_13 <- detectar_asociaciones(datos_operativos[-13, ])
eta_sin_13 <- sin_fila_13$asociacion[
  sin_fila_13$columna_1 == "canal" & sin_fila_13$columna_2 == "monto"
]
stopifnot(length(eta_sin_13) == 1L, eta_sin_13 < 0.9)
```

Son medidas, no dependencias ni instrucciones de modelado. Conviene leerlas con los datos
a la vista:

- `id_registro` e `id_copia` se asocian al máximo porque una es copia de la otra; el
  perfil ya lo informó como `columnas_duplicadas`.
- La asociación de `canal` con `monto` la produce casi entera la fila 13, donde `canal`
  está vacío y `monto` vale `999999`. Sin esa fila, la medida baja a
  `r format(round(eta_sin_13, 2), nsmall = 2, decimal.mark = ",")`.

Para una relación monótona que no es lineal, `metodo_asociacion_numerica = "spearman"`
cambia Pearson por Spearman.

## Tiempo

`analizar_tiempo()` reconoce `fecha_evento` por su contenido, aunque se guarde como texto,
y resume el período observado:

```{r}
analisis$temporal$resumen[, c(
  "columna", "n_presentes", "fecha_minima", "fecha_maxima", "n_fechas_excluidas_parseo"
)]
analisis$temporal$propuestas[, c("columna", "frecuencia_dias", "confirmada")]
```

Las fechas que no se pudieron leer quedan contadas aparte, en `n_fechas_excluidas_parseo`.
La frecuencia propuesta es la moda de los intervalos observados. Sale con
`confirmada = FALSE`: es evidencia para revisar, no un requisito. `advertencias` lo repite
junto con las escalas sin confirmar, para que no pase inadvertido.

## Lo que `analizar()` mide por su cuenta

`analizar()` no se queda en describir:

1. toma la propuesta de métricas que arma `proponer_modelo()` a partir del perfil;
2. mide lo que esa propuesta deja activo;
3. agrega el resultado en un tablero.

El tablero resume la medición con una fila por métrica y objeto. Su columna `orientacion`
dice cómo leer cada valor.

```{r}
analisis$propuesta_modelo[, c("metrica", "atributos", "origen", "incluir")]
```

Las filas activas (`incluir = TRUE`) salen de hallazgos: los duplicados exactos y los
faltantes, contando como faltante lo que el perfil vio disfrazado.

Los formatos y el dominio de `zona` se aprendieron de esta misma entrega. Medirlos contra
ella sería circular:

- el dominio observado da 1 por construcción;
- cada formato da la proporción del patrón dominante que el perfil ya mostró.

Por eso quedan inactivos hasta que confirmemos que son requisitos. `decision_medicion`
dice, fila por fila, qué se midió, y el tablero muestra el resultado:

```{r}
analisis$decision_medicion[, c("metrica", "objeto", "medida")]
analisis$tablero[, c("metrica_instanciada", "valor", "orientacion")]
```

```{r, echo = FALSE}
# Lo que dice la prosa: se mide exactamente lo activo, y nada aprendido de la entrega.
stopifnot(
  identical(analisis$decision_medicion$medida, analisis$propuesta_modelo$incluir),
  !any(analisis$propuesta_modelo$incluir[
    grepl("^perfil:", analisis$propuesta_modelo$origen)
  ])
)
```

`valor` es una proporción. Con `orientacion = "conformidad"` se lee "más es mejor"; con
`"defecto"`, al revés. Por ejemplo, `EntidadDuplicada` es la proporción de filas que
participan en duplicados. `meta$propuesta_medida_automaticamente` deja constancia de que
la selección la hizo `lupa` y nadie la confirmó.

- Para quedarnos sólo con la descripción, `medir_propuesta = FALSE`.
- Para medir lo que decidimos nosotros, editamos la propuesta y la pasamos como
  `propuesta_confirmada`. Ese camino sigue en
  [7. Del hallazgo a la métrica](https://sebollin.github.io/lupa/articles/del-hallazgo-a-la-metrica.html).

`cobertura`, por su parte, dice qué factores del marco de calidad quedaron medidos (por
omisión, el de AGESIC). Los marcos se explican en
[8. Marcos y modelos de calidad](https://sebollin.github.io/lupa/articles/marcos-y-modelos.html).

## ⏱ ¿`perfilar()` o `analizar()`? {#perfilar-o-analizar}

- `perfilar()` es la pasada que conviene llevar a todos lados: resúmenes por columna,
  patrones, tipos, hallazgos, cobertura de diagnósticos y relaciones entre columnas.
- `analizar()` arma alrededor de ese perfil lo que cuesta más, en tablas propias:
  distribuciones, asociaciones, tiempo, escalas, la propuesta medida, el plan y el tablero.

Cada pieza está exportada, así que se puede llamar suelta sin pagar el recorrido entero.
Por ejemplo, las frecuencias de una sola columna, con un tope de 3 valores:

```{r}
zona <- distribucion_valores(datos_operativos["zona"], max_valores = 3)
zona$frecuencias[, c("valor", "frecuencia")]
zona$alcance[, c("columna", "n_distintos_muestra", "n_mostrados", "truncado")]
```

`zona` tiene `r zona$alcance$n_distintos_muestra` valores distintos y se muestran
`r zona$alcance$n_mostrados`: el recorte queda declarado en `truncado`.

Las 2 funciones trabajan en memoria, y sobre tablas de millones de filas no son una
pasada rápida. A esa escala conviene una de estas vías:

- perfilar una muestra y llamar sueltas las piezas que hagan falta;
- perfilar dentro de la base con `perfilar_dbi()`, que calcula los agregados en el motor
  y deja elegir cuántas filas trae a R (ver
  [10. Perfilar una base](https://sebollin.github.io/lupa/articles/perfilar-una-base.html)).

Los tiempos medidos sobre tablas de ese tamaño, con su fecha y su entorno, están en el
artículo
[Evidencia y rendimiento](https://sebollin.github.io/lupa/articles/evidencia-y-rendimiento.html).
La muestra que `perfilar()` toma por su cuenta es determinista y sin azar —a paso fijo para
patrones, tipos y formatos; para las dependencias, una fila por tramo de la tabla en una
posición fijada por un hash, que no se alinea con datos periódicos—: sobre los mismos datos,
el mismo perfil se rehace igual.

## 📄 Compartir el análisis {#compartir-el-análisis}

`guardar_analisis()` escribe el análisis en un archivo RDS con número de esquema, y
`leer_analisis()` lo recupera en otra sesión. Por omisión, `guardar_analisis()`:

- no guarda la tabla de entrada;
- reemplaza las reglas escritas como funciones por declaraciones pequeñas, para no
  serializar sus entornos.

`meta$persistencia` declara qué se guardó y qué no:

```{r}
archivo_rds <- tempfile(fileext = ".rds")
guardar_analisis(analisis, archivo_rds)
recuperado <- leer_analisis(archivo_rds)
str(recuperado$meta$persistencia)
identical(hallazgos(recuperado), hallazgos(analisis))
```

`reportar()` convierte el análisis en un HTML autocontenido, que se abre en cualquier
navegador sin instalar nada. También acepta perfiles, mediciones, evaluaciones y planes
sueltos; un análisis ya los trae. Estas son las secciones del informe:

```{r}
archivo_html <- reportar(
  analisis,
  archivo = tempfile(fileext = ".html"),
  titulo = "Calidad de la entrega de ejemplo",
  fecha = as.POSIXct("2026-01-15", tz = "UTC")
)
html <- readLines(archivo_html, encoding = "UTF-8", warn = FALSE)
secciones <- regmatches(html, gregexpr("<h2>[^<]*</h2>", html))[[1]]
gsub("</?h2>", "", secciones)
```

En una sesión propia, `browseURL(archivo_html)` lo abre en el navegador; acá borramos los
2 archivos al terminar. Con `proteger_datos_personales = TRUE`, el informe enmascara los
valores de las columnas que parecen datos personales. El alcance de esa protección está
en el artículo
[Alcance y cobertura](https://sebollin.github.io/lupa/articles/alcance-y-cobertura.html).

```{r}
unlink(c(archivo_html, archivo_rds))
```

## Resumen

- `analizar()` arma en una llamada el perfil y todo lo que lo rodea; cada componente es
  una tabla, y cada pieza se puede llamar suelta.
- Patrones, tipos y fechas describen compatibilidad y frecuencia: no convierten nada ni
  confirman un formato.
- Escalas y frecuencias temporales son propuestas sin confirmar; las asociaciones son
  medidas, y se leen con los datos a la vista.
- Por omisión, `analizar()` mide lo que la propuesta deja activo; `decision_medicion` dice
  qué se midió y `medir_propuesta = FALSE` lo apaga.
- `guardar_analisis()` guarda el recorrido sin los datos, y `reportar()` lo vuelve un HTML
  autocontenido.

## 🧭 Paso siguiente {#paso-siguiente}

[4. Estructura no declarada](estructura-no-declarada.html)
(`vignette("estructura-no-declarada")`): claves, relaciones y dependencias entre tablas
que nadie documentó.

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