Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
11 changes: 11 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,17 @@ versionado [SemVer](https://semver.org/lang/es/) adaptado a app web:

---

## [Unreleased]

### Added

- Sprint test-1 batch 3: tests para `arma_matriz_transicion`,
`build_tasas_historico`, `regenerar_calidad_panel`, `formato_delta`,
`sankey_label_legible`, `sankey_nodes_orden`. Suite de testthat pasa
de 79 a **149 tests verde**. Cobertura aproximada del Sprint test-1
cerrada salvo `armo_base_panel` legacy (movido a Sprint test-2 para
combinarlo con cobertura de modo runtime via `testServer`).

## [0.9.0] · 2026-05-04

Cierra Sprint A (#44 Tipo de dúo end-to-end). El toggle Interanual
Expand Down
16 changes: 11 additions & 5 deletions ROADMAP.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,11 +21,17 @@ Sprint A). Ver [CHANGELOG.md](CHANGELOG.md) para el detalle.

- [x] Setup `tests/testthat/` + runner + helper-fixtures
- [x] Fixture sintética `panel_mock.rds` (100 individuos × 3 ondas)
- [x] Tests: `agrega_vars_derivadas`, `armo_tabla_sankey`,
`duos_disponibles_por_anio`, `duo_label` → **42 tests PASS**
- [ ] Tests pendientes: `arma_tasas_destacadas`,
`regenerar_panel_historico`, `armo_base_panel` modo legacy
- [ ] CI: GitHub Actions `tests-unit.yml`
- [x] Tests batch 1: `agrega_vars_derivadas`, `armo_tabla_sankey`,
`duos_disponibles_por_anio`, `duo_label` → 42 tests
- [x] Tests batch 2: `arma_tasas_destacadas`, `regenerar_panel_historico`,
`tests/testthat.R` aislado de `00-libraries.R` → 79 tests
- [x] Tests batch 3: `arma_matriz_transicion`, `build_tasas_historico`,
`regenerar_calidad_panel`, `formato_delta`, `sankey_label_legible`,
`sankey_nodes_orden` → **149 tests PASS**
- [x] CI: GitHub Actions `tests-unit.yml` ejecuta en cada push a master/staging
- [ ] Pendiente para Sprint test-2: `armo_base_panel` modo legacy
(requiere `eph::organize_panels()` real, conviene cubrir junto
con runtime mode usando `testServer`).

### Sprint test-2 · Server logic con testServer() (~3-4 hs)

Expand Down
156 changes: 156 additions & 0 deletions tests/testthat/test-arma_matriz_transicion.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,156 @@
### Tests de arma_matriz_transicion() (en R/utils_analisis.R).
###
### Construye una matriz NxN de transición (porcentaje sobre el total t0
### de cada fila) usando preparo_base() con periodo_base = "t_anterior".
###
### Output: tibble con `from` como primera col y una col por categoría
### destino. Valores en porcentaje, deberían sumar ~100 por fila (con
### tolerancia por redondeo a 1 decimal).

test_that("arma_matriz_transicion: estructura del output", {
### Panel mínimo controlado:
### 3 personas Ocupado_t0 → 2 Ocupado_t1 + 1 Desocupado_t1
### 2 personas Desocupado_t0 → 1 Ocupado_t1 + 1 Inactivo_t1
df_panel <- tibble::tibble(
ESTADO = c(1L, 1L, 1L, 2L, 2L),
ESTADO_t1 = c(1L, 1L, 2L, 1L, 3L),
PONDERA = c(500, 500, 500, 500, 500),
PONDERA_t1 = c(500, 500, 500, 500, 500)
)

matriz <- arma_matriz_transicion(
df_panel = df_panel,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar")
)

expect_s3_class(matriz, "tbl_df")
expect_true("from" %in% names(matriz))
### Etiquetas legibles aplicadas: "Ocupado" → "Ocupados", etc.
expect_true(all(c("Ocupados", "Desocupados", "Inactivos",
"Trab. familiares") %in% names(matriz)))
})


test_that("arma_matriz_transicion: filas suman ~100% (tolerancia redondeo)", {
df_panel <- tibble::tibble(
ESTADO = c(1L, 1L, 1L, 2L, 2L),
ESTADO_t1 = c(1L, 1L, 2L, 1L, 3L),
PONDERA = c(500, 500, 500, 500, 500),
PONDERA_t1 = c(500, 500, 500, 500, 500)
)

matriz <- arma_matriz_transicion(
df_panel = df_panel,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar")
)

### Sumar todas las cols numéricas por fila. Tolerancia ±0.5 pp por
### redondeo a 1 decimal en preparo_base.
cols_num <- setdiff(names(matriz), "from")
sumas <- rowSums(matriz[, cols_num])
expect_true(all(abs(sumas - 100) < 1))
})


test_that("arma_matriz_transicion: valores específicos para caso controlado", {
### Panel:
### 2 Ocupado_t0 → Ocupado_t1 (PONDERA 500 c/u → 1000)
### 1 Ocupado_t0 → Desocupado_t1 (PONDERA 500)
### Total Ocupado_t0 = 1500 → 1000/1500 = 66.7%, 500/1500 = 33.3%
df_panel <- tibble::tibble(
ESTADO = c(1L, 1L, 1L),
ESTADO_t1 = c(1L, 1L, 2L),
PONDERA = c(500, 500, 500),
PONDERA_t1 = c(500, 500, 500)
)

matriz <- arma_matriz_transicion(
df_panel = df_panel,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar")
)

fila_ocup <- matriz |> dplyr::filter(from == "Ocupados")
expect_equal(fila_ocup$Ocupados, 66.7)
expect_equal(fila_ocup$Desocupados, 33.3)
expect_equal(fila_ocup$Inactivos, 0)
})


test_that("arma_matriz_transicion: orden de filas y cols sigue 'etiquetas' (no alfabético)", {
### Panel con las 4 categorías presentes para que aparezcan las 4 filas.
df_panel <- tibble::tibble(
ESTADO = c(1L, 2L, 3L, 4L),
ESTADO_t1 = c(1L, 2L, 3L, 4L),
PONDERA = c(500, 500, 500, 500),
PONDERA_t1 = c(500, 500, 500, 500)
)

### Etiquetas en orden NO alfabético: Inactivo, Ocupado, Desocupado, ...
matriz <- arma_matriz_transicion(
df_panel = df_panel,
var = "ESTADO",
etiquetas = c("Inactivo", "Ocupado", "Desocupado", "Trab_familiar")
)

### Filas: el factor con levels = remap(etiquetas) define el orden,
### dplyr::arrange(from, to) lo respeta.
expect_equal(matriz$from, c("Inactivos", "Ocupados", "Desocupados",
"Trab. familiares"))
### Cols (después de "from"): mismo orden.
cols <- setdiff(names(matriz), "from")
expect_equal(cols, c("Inactivos", "Ocupados", "Desocupados",
"Trab. familiares"))
})


test_that("arma_matriz_transicion: funciona con CAT_OCUP", {
### Mismo schema pero variable CAT_OCUP. Códigos 1=Patron, 2=Cuenta_propia,
### 3=Asalariado, 4=TFSR.
df_panel <- tibble::tibble(
CAT_OCUP = c(3L, 3L, 2L),
CAT_OCUP_t1 = c(3L, 3L, 3L),
PONDERA = c(500, 500, 500),
PONDERA_t1 = c(500, 500, 500)
)

matriz <- arma_matriz_transicion(
df_panel = df_panel,
var = "CAT_OCUP",
etiquetas = c("Patron", "Cuenta_propia", "Asalariado", "TFSR")
)

### Etiquetas legibles esperadas según el mapeo de la función.
expect_true(all(c("Patrones", "Cuenta propia", "Asalariados",
"Trab. familiares") %in% names(matriz)))
})


test_that("arma_matriz_transicion: categoría sin presencia → no aparece como fila pero SÍ como columna", {
### Sin Trab_familiar en el panel: la columna existe con 0s gracias a
### names_expand = TRUE + factor con levels en `to`. Pero la fila NO
### se genera (no hay registros que la tengan como `from`).
df_panel <- tibble::tibble(
ESTADO = c(1L, 2L, 3L),
ESTADO_t1 = c(1L, 2L, 3L),
PONDERA = c(500, 500, 500),
PONDERA_t1 = c(500, 500, 500)
)

matriz <- arma_matriz_transicion(
df_panel = df_panel,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar")
)

### Columna "Trab. familiares" presente y todo 0 (no hay transiciones
### hacia esa categoría en el panel).
expect_true("Trab. familiares" %in% names(matriz))
expect_true(all(matriz[["Trab. familiares"]] == 0))

### Pero la fila NO se genera (no hay nadie con ESTADO=4 en t0).
expect_false("Trab. familiares" %in% matriz$from)
expect_equal(nrow(matriz), 3)
})
153 changes: 153 additions & 0 deletions tests/testthat/test-build_tasas_historico.R
Original file line number Diff line number Diff line change
@@ -0,0 +1,153 @@
### Tests de build_tasas_historico() (en ETL/99-functions.R).
###
### Construye un tibble con (periodo, categoria, persistencia, salida,
### entrada) iterando sobre todos los duos válidos del microdato. NO
### escribe a disco — devuelve el tibble in-memory.
###
### Acepta `window`: "trimestral" (default) o "anual" (issue #46).

test_that("build_tasas_historico trimestral: schema y filas > 0", {
df_microdato <- load_panel_mock() |> agrega_vars_derivadas()

out <- build_tasas_historico(
df_microdato = df_microdato,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"),
window = "trimestral"
)

expect_s3_class(out, "tbl_df")
expect_true(all(c("periodo", "categoria", "persistencia", "salida",
"entrada") %in% names(out)))
expect_gt(nrow(out), 0)
})


test_that("trimestral: periodos en formato 'YYYY_tA-tB'", {
df_microdato <- load_panel_mock() |> agrega_vars_derivadas()

out <- build_tasas_historico(
df_microdato = df_microdato,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"),
window = "trimestral"
)

### Fixture tiene 2024-T1, T2, T3 → 2 dúos consecutivos.
expect_true(all(out$periodo %in% c("2024_t1-t2", "2024_t2-t3")))
})


test_that("anual: periodos en formato 'YYYY_tN' (sin '-tM')", {
### Construir microdato extendido a 2025-T1 para tener al menos un dúo
### anual válido (2024-T1 → 2025-T1).
base <- load_panel_mock()
df_2025 <- base |>
dplyr::filter(TRIMESTRE == 1L) |>
dplyr::mutate(ANO4 = 2025L)
df_microdato <- dplyr::bind_rows(base, df_2025) |> agrega_vars_derivadas()

out <- build_tasas_historico(
df_microdato = df_microdato,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"),
window = "anual"
)

### Solo dúo válido: 2024-T1 → 2025-T1, periodo "2024_t1".
expect_true(all(out$periodo == "2024_t1"))
})


test_that("una fila por (periodo, categoria) y todas las etiquetas presentes", {
df_microdato <- load_panel_mock() |> agrega_vars_derivadas()
etiquetas <- c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar")

out <- build_tasas_historico(
df_microdato = df_microdato,
var = "ESTADO",
etiquetas = etiquetas,
window = "trimestral"
)

### 2 dúos x 4 categorías = hasta 8 filas. Puede haber menos si alguna
### categoría no es computable en algún dúo, pero todas deberían estar
### representadas al menos una vez en el panel mock.
expect_setequal(unique(out$categoria), etiquetas)
})


test_that("invariante: persistencia + salida = 100 por fila", {
df_microdato <- load_panel_mock() |> agrega_vars_derivadas()

out <- build_tasas_historico(
df_microdato = df_microdato,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"),
window = "trimestral"
)

### Tolerancia ±0.2 pp por redondeo a 1 decimal en ambas tasas.
deltas <- abs(out$persistencia + out$salida - 100)
expect_true(all(deltas < 0.2))
})


test_that("invariante: tasas en rango [0, 100]", {
df_microdato <- load_panel_mock() |> agrega_vars_derivadas()

out <- build_tasas_historico(
df_microdato = df_microdato,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"),
window = "trimestral"
)

expect_true(all(out$persistencia >= 0 & out$persistencia <= 100))
expect_true(all(out$salida >= 0 & out$salida <= 100))
expect_true(all(out$entrada >= 0 & out$entrada <= 100))
})


test_that("desde_panel filtra periodos previos", {
df_microdato <- load_panel_mock() |> agrega_vars_derivadas()

### Sin filtro: 2 dúos (2024_t1-t2, 2024_t2-t3).
out_full <- build_tasas_historico(
df_microdato = df_microdato,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"),
window = "trimestral"
)

### Con desde_panel = "2024T2": solo 2024_t2-t3.
out_filt <- build_tasas_historico(
df_microdato = df_microdato,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"),
desde_panel = "2024T2",
window = "trimestral"
)

expect_true(all(out_filt$periodo == "2024_t2-t3"))
expect_lt(nrow(out_filt), nrow(out_full))
})


test_that("vars_extra: pasa columnas extra al panel sin afectar el output schema", {
### vars_extra se usa internamente en armo_base_panel para incluir cols
### adicionales en el panel (ej: CH04, CH06 para validaciones), pero el
### output de tasas no las expone.
df_microdato <- load_panel_mock() |> agrega_vars_derivadas()

out <- build_tasas_historico(
df_microdato = df_microdato,
var = "ESTADO",
etiquetas = c("Ocupado", "Desocupado", "Inactivo", "Trab_familiar"),
vars_extra = c("CH04", "CH06"),
window = "trimestral"
)

expect_setequal(names(out), c("periodo", "categoria", "persistencia",
"salida", "entrada"))
})
Loading
Loading