1 Propósito

Este documento reproduce la caracterización definitiva de la alfabetización física realizada con el CAPL-2 versión Colombia en la muestra multicéntrica nacional de escolares de 8 a 12 años.

El flujo reproduce:

  1. disponibilidad de los puntajes finales;
  2. descriptivos nacionales y regionales;
  3. categorías interpretativas oficiales del CAPL-2;
  4. distribución de categorías a nivel nacional y por región;
  5. asociación entre región y categoría mediante chi-cuadrado de Pearson y V de Cramer;
  6. verificación mediante simulación Monte Carlo para Comportamiento diario;
  7. comparación regional de puntajes mediante Kruskal-Wallis y epsilon cuadrado;
  8. comparaciones post hoc de Dunn con ajuste de Holm;
  9. controles de correspondencia con los resultados definitivos de la tesis.

2 1. Entorno reproducible

paquetes <- c(
  "readxl",
  "dplyr",
  "tidyr",
  "tibble",
  "purrr",
  "capl",
  "knitr"
)

faltantes <- paquetes[
  !vapply(
    paquetes,
    requireNamespace,
    logical(1),
    quietly = TRUE
  )
]

if (length(faltantes) > 0) {
  stop(
    paste0(
      "Instale antes de continuar los paquetes: ",
      paste(faltantes, collapse = ", ")
    )
  )
}

library(readxl)
library(dplyr)
library(tidyr)
library(tibble)
library(purrr)
library(capl)
library(knitr)

set.seed(20260907)

3 2. Lectura de la base analítica

localizar_archivo <- function(nombre_archivo) {

  if (file.exists(nombre_archivo)) {
    return(normalizePath(nombre_archivo))
  }

  entrada <- tryCatch(
    knitr::current_input(dir = TRUE),
    error = function(e) ""
  )

  if (nzchar(entrada)) {

    candidato <- file.path(
      dirname(entrada),
      basename(nombre_archivo)
    )

    if (file.exists(candidato)) {
      return(normalizePath(candidato))
    }
  }

  archivos <- list.files(
    path = getwd(),
    recursive = TRUE,
    full.names = TRUE
  )

  coincidencias <- archivos[
    tolower(basename(archivos)) ==
      tolower(basename(nombre_archivo))
  ]

  if (length(coincidencias) >= 1) {
    return(normalizePath(coincidencias[1]))
  }

  stop(
    paste0(
      "No se encontró el archivo '",
      basename(nombre_archivo),
      "'. Guarde el Excel y el Rmd en la misma carpeta ",
      "o ajuste params$archivo_excel."
    )
  )
}

ruta_base <- localizar_archivo(
  params$archivo_excel
)

cat(
  "Archivo localizado en:\n",
  ruta_base,
  "\n"
)
## Archivo localizado en:
##  C:\Users\Personal\Documents\análisis doctorado\Analisis\Datos_publicos_CAPL2_REVISION\CAPL2_Colombia_BASE_PUBLICA_REPRODUCIBLE.xlsx
hojas <- readxl::excel_sheets(
  ruta_base
)

if (!params$hoja_excel %in% hojas) {
  stop(
    paste0(
      "No se encontró la hoja '",
      params$hoja_excel,
      "'."
    )
  )
}

base <- readxl::read_excel(
  ruta_base,
  sheet = params$hoja_excel
)

cat(
  "Dimensiones de la base:",
  nrow(base),
  "filas x",
  ncol(base),
  "columnas\n"
)
## Dimensiones de la base: 843 filas x 46 columnas

4 3. Variables requeridas y auditoría inicial

variables_requeridas <- c(
  "age",
  "gender",
  "region",
  "grade",
  "pc_score_final",
  "db_score_final",
  "mc_score_final",
  "ku_score_final",
  "capl_total_final"
)

faltan_variables <- setdiff(
  variables_requeridas,
  names(base)
)

if (length(faltan_variables) > 0) {
  stop(
    paste0(
      "Faltan variables necesarias: ",
      paste(
        faltan_variables,
        collapse = ", "
      )
    )
  )
}

if (nrow(base) != 843) {
  stop(
    paste0(
      "Se esperaban 843 escolares y se encontraron ",
      nrow(base),
      "."
    )
  )
}
normalizar_sexo_capl <- function(x) {

  x2 <- tolower(
    iconv(
      as.character(x),
      from = "",
      to = "ASCII//TRANSLIT"
    )
  )

  dplyr::case_when(
    x2 %in% c(
      "boy",
      "male",
      "nino",
      "masculino",
      "m",
      "1"
    ) ~ "boy",

    x2 %in% c(
      "girl",
      "female",
      "nina",
      "femenino",
      "f",
      "0"
    ) ~ "girl",

    TRUE ~ NA_character_
  )
}

car_nacional <- base %>%
  transmute(
    edad = as.numeric(age),
    sexo_original = as.character(gender),
    sexo_capl = normalizar_sexo_capl(gender),
    sexo = case_when(
      sexo_capl == "boy" ~ "Niños",
      sexo_capl == "girl" ~ "Niñas",
      TRUE ~ NA_character_
    ),
    region = as.character(region),
    grado = as.numeric(grade),
    competencia_fisica =
      as.numeric(pc_score_final),
    comportamiento_diario =
      as.numeric(db_score_final),
    motivacion_confianza =
      as.numeric(mc_score_final),
    conocimiento_comprension =
      as.numeric(ku_score_final),
    capl_total =
      as.numeric(capl_total_final)
  )

cat("N total =", nrow(car_nacional), "\n")
## N total = 843
cat("\nSexo:\n")
## 
## Sexo:
print(
  table(
    car_nacional$sexo,
    useNA = "ifany"
  )
)
## 
## Niñas Niños 
##   425   418
cat("\nEdad:\n")
## 
## Edad:
print(
  table(
    car_nacional$edad,
    useNA = "ifany"
  )
)
## 
##   8   9  10  11  12 
## 168 170 171 170 164
cat("\nRegión:\n")
## 
## Región:
print(
  table(
    car_nacional$region,
    useNA = "ifany"
  )
)
## 
##                 Caribe         Centro-Oriente    Centro sur-Amazonía 
##                    140                    140                    141 
## Eje cafetero-Antioquia       Llanos-Orinoquía               Pacífico 
##                    141                    141                    140

5 4. Disponibilidad de los puntajes

tabla_disponibilidad_nacional <- tibble(
  dominio = c(
    "Competencia física",
    "Comportamiento diario",
    "Motivación y confianza",
    "Conocimiento y comprensión",
    "CAPL-2 total"
  ),

  n_valido = c(
    sum(!is.na(
      car_nacional$competencia_fisica
    )),
    sum(!is.na(
      car_nacional$comportamiento_diario
    )),
    sum(!is.na(
      car_nacional$motivacion_confianza
    )),
    sum(!is.na(
      car_nacional$conocimiento_comprension
    )),
    sum(!is.na(
      car_nacional$capl_total
    ))
  )
) %>%
  mutate(
    n_total = nrow(
      car_nacional
    ),
    porcentaje_valido =
      100 * n_valido / n_total,
    n_sin_puntaje =
      n_total - n_valido,
    porcentaje_sin_puntaje =
      100 * n_sin_puntaje / n_total,
    across(
      c(
        porcentaje_valido,
        porcentaje_sin_puntaje
      ),
      ~ round(.x, 1)
    )
  )

knitr::kable(
  tabla_disponibilidad_nacional,
  caption = "Disponibilidad de puntuaciones del CAPL-2"
)
Disponibilidad de puntuaciones del CAPL-2
dominio n_valido n_total porcentaje_valido n_sin_puntaje porcentaje_sin_puntaje
Competencia física 843 843 100.0 0 0.0
Comportamiento diario 840 843 99.6 3 0.4
Motivación y confianza 842 843 99.9 1 0.1
Conocimiento y comprensión 842 843 99.9 1 0.1
CAPL-2 total 838 843 99.4 5 0.6
n_esperados <- c(
  843,
  840,
  842,
  842,
  838
)

control_disponibilidad <- tabla_disponibilidad_nacional %>%
  mutate(
    n_esperado = n_esperados,
    coincide =
      n_valido ==
      n_esperado
  )

knitr::kable(
  control_disponibilidad,
  caption = "Control de disponibilidad frente a los resultados definitivos"
)
Control de disponibilidad frente a los resultados definitivos
dominio n_valido n_total porcentaje_valido n_sin_puntaje porcentaje_sin_puntaje n_esperado coincide
Competencia física 843 843 100.0 0 0.0 843 TRUE
Comportamiento diario 840 843 99.6 3 0.4 840 TRUE
Motivación y confianza 842 843 99.9 1 0.1 842 TRUE
Conocimiento y comprensión 842 843 99.9 1 0.1 842 TRUE
CAPL-2 total 838 843 99.4 5 0.6 838 TRUE
if (!all(control_disponibilidad$coincide)) {
  stop(
    "La disponibilidad de los puntajes no coincide con la versión definitiva."
  )
}

6 5. Descriptivos nacionales y regionales

puntajes_nacionales_largo <- car_nacional %>%
  select(
    edad,
    sexo,
    region,
    grado,
    competencia_fisica,
    comportamiento_diario,
    motivacion_confianza,
    conocimiento_comprension,
    capl_total
  ) %>%
  pivot_longer(
    cols = c(
      competencia_fisica,
      comportamiento_diario,
      motivacion_confianza,
      conocimiento_comprension,
      capl_total
    ),
    names_to = "variable",
    values_to = "puntaje"
  ) %>%
  mutate(
    dominio = case_when(
      variable ==
        "competencia_fisica" ~
        "Competencia física",

      variable ==
        "comportamiento_diario" ~
        "Comportamiento diario",

      variable ==
        "motivacion_confianza" ~
        "Motivación y confianza",

      variable ==
        "conocimiento_comprension" ~
        "Conocimiento y comprensión",

      variable ==
        "capl_total" ~
        "CAPL-2 total"
    ),

    maximo_teorico = case_when(
      variable %in% c(
        "competencia_fisica",
        "comportamiento_diario",
        "motivacion_confianza"
      ) ~ 30,

      variable ==
        "conocimiento_comprension" ~ 10,

      variable ==
        "capl_total" ~ 100
    )
  )
tabla_puntajes_nacional <- puntajes_nacionales_largo %>%
  group_by(
    dominio,
    maximo_teorico
  ) %>%
  summarise(
    n = sum(!is.na(puntaje)),
    media = mean(
      puntaje,
      na.rm = TRUE
    ),
    DE = sd(
      puntaje,
      na.rm = TRUE
    ),
    P25 = quantile(
      puntaje,
      0.25,
      na.rm = TRUE
    ),
    mediana = median(
      puntaje,
      na.rm = TRUE
    ),
    P75 = quantile(
      puntaje,
      0.75,
      na.rm = TRUE
    ),
    minimo = min(
      puntaje,
      na.rm = TRUE
    ),
    maximo = max(
      puntaje,
      na.rm = TRUE
    ),
    .groups = "drop"
  ) %>%
  mutate(
    mediana_porcentaje_maximo =
      100 *
      mediana /
      maximo_teorico,
    across(
      c(
        media,
        DE,
        P25,
        mediana,
        P75,
        minimo,
        maximo,
        mediana_porcentaje_maximo
      ),
      ~ round(.x, 2)
    )
  )

knitr::kable(
  tabla_puntajes_nacional,
  caption = "Descriptivos nacionales de los puntajes del CAPL-2"
)
Descriptivos nacionales de los puntajes del CAPL-2
dominio maximo_teorico n media DE P25 mediana P75 minimo maximo mediana_porcentaje_maximo
CAPL-2 total 100 838 60.37 9.46 53.83 59.77 66.71 30.70 88.86 59.77
Competencia física 30 843 14.94 4.29 12.00 14.71 17.50 5.29 29.29 49.05
Comportamiento diario 30 840 17.19 4.60 14.00 17.00 20.00 3.00 28.00 56.67
Conocimiento y comprensión 10 842 5.40 2.55 3.00 5.00 7.00 0.00 10.00 50.00
Motivación y confianza 30 842 22.85 4.23 19.62 23.10 26.40 9.60 30.00 77.00
tabla_puntajes_region <- puntajes_nacionales_largo %>%
  group_by(
    dominio,
    maximo_teorico,
    region
  ) %>%
  summarise(
    n = sum(!is.na(puntaje)),
    media = mean(
      puntaje,
      na.rm = TRUE
    ),
    DE = sd(
      puntaje,
      na.rm = TRUE
    ),
    P25 = quantile(
      puntaje,
      0.25,
      na.rm = TRUE
    ),
    mediana = median(
      puntaje,
      na.rm = TRUE
    ),
    P75 = quantile(
      puntaje,
      0.75,
      na.rm = TRUE
    ),
    .groups = "drop"
  ) %>%
  mutate(
    mediana_porcentaje_maximo =
      100 *
      mediana /
      maximo_teorico,
    across(
      c(
        media,
        DE,
        P25,
        mediana,
        P75,
        mediana_porcentaje_maximo
      ),
      ~ round(.x, 2)
    )
  )

tabla_medianas_region <- tabla_puntajes_region %>%
  select(
    dominio,
    region,
    mediana
  ) %>%
  pivot_wider(
    names_from = region,
    values_from = mediana
  )

knitr::kable(
  tabla_medianas_region,
  caption = "Puntajes medianos del CAPL-2 según región"
)
Puntajes medianos del CAPL-2 según región
dominio Caribe Centro sur-Amazonía Centro-Oriente Eje cafetero-Antioquia Llanos-Orinoquía Pacífico
CAPL-2 total 56.98 57.81 62.96 56.53 66.46 60.87
Competencia física 14.36 12.43 16.46 17.14 16.71 12.43
Comportamiento diario 15.00 17.00 18.00 18.00 18.00 17.00
Conocimiento y comprensión 4.00 4.00 4.00 4.00 8.00 7.00
Motivación y confianza 23.50 24.50 23.00 18.20 23.60 24.80
control_nacional <- tribble(
  ~dominio, ~mediana_esperada, ~P25_esperado, ~P75_esperado,
  "CAPL-2 total", 59.8, 53.8, 66.7,
  "Competencia física", 14.7, 12.0, 17.5,
  "Comportamiento diario", 17.0, 14.0, 20.0,
  "Motivación y confianza", 23.1, 19.6, 26.4,
  "Conocimiento y comprensión", 5.0, 3.0, 7.0
)

control_nacional <- control_nacional %>%
  left_join(
    tabla_puntajes_nacional %>%
      select(
        dominio,
        mediana,
        P25,
        P75
      ),
    by = "dominio"
  ) %>%
  mutate(
    coincide_mediana =
      abs(
        mediana -
        mediana_esperada
      ) < 0.11,
    coincide_P25 =
      abs(
        P25 -
        P25_esperado
      ) < 0.11,
    coincide_P75 =
      abs(
        P75 -
        P75_esperado
      ) < 0.11
  )

knitr::kable(
  control_nacional,
  caption = "Control de descriptivos nacionales"
)
Control de descriptivos nacionales
dominio mediana_esperada P25_esperado P75_esperado mediana P25 P75 coincide_mediana coincide_P25 coincide_P75
CAPL-2 total 59.8 53.8 66.7 59.77 53.83 66.71 TRUE TRUE TRUE
Competencia física 14.7 12.0 17.5 14.71 12.00 17.50 TRUE TRUE TRUE
Comportamiento diario 17.0 14.0 20.0 17.00 14.00 20.00 TRUE TRUE TRUE
Motivación y confianza 23.1 19.6 26.4 23.10 19.62 26.40 TRUE TRUE TRUE
Conocimiento y comprensión 5.0 3.0 7.0 5.00 3.00 7.00 TRUE TRUE TRUE
if (
  !all(
    control_nacional$coincide_mediana &
    control_nacional$coincide_P25 &
    control_nacional$coincide_P75
  )
) {
  warning(
    "Algún descriptivo nacional no coincide con la versión definitiva."
  )
}

7 6. Categorías interpretativas oficiales del CAPL-2

Las categorías se obtienen mediante los criterios oficiales del CAPL-2 considerando la edad, el sexo y el puntaje correspondiente.

car_nacional_categorias <- car_nacional %>%
  mutate(
    categoria_pc =
      capl::get_capl_interpretation(
        age = edad,
        gender = sexo_capl,
        score = competencia_fisica,
        protocol = "pc"
      ),

    categoria_db =
      capl::get_capl_interpretation(
        age = edad,
        gender = sexo_capl,
        score = comportamiento_diario,
        protocol = "db"
      ),

    categoria_mc =
      capl::get_capl_interpretation(
        age = edad,
        gender = sexo_capl,
        score = motivacion_confianza,
        protocol = "mc"
      ),

    categoria_ku =
      capl::get_capl_interpretation(
        age = edad,
        gender = sexo_capl,
        score = conocimiento_comprension,
        protocol = "ku"
      ),

    categoria_total =
      capl::get_capl_interpretation(
        age = edad,
        gender = sexo_capl,
        score = capl_total,
        protocol = "capl"
      )
  )
auditoria_categorias <- tibble(
  dominio = c(
    "Competencia física",
    "Comportamiento diario",
    "Motivación y confianza",
    "Conocimiento y comprensión",
    "CAPL-2 total"
  ),

  n_puntaje = c(
    sum(!is.na(
      car_nacional_categorias$competencia_fisica
    )),
    sum(!is.na(
      car_nacional_categorias$comportamiento_diario
    )),
    sum(!is.na(
      car_nacional_categorias$motivacion_confianza
    )),
    sum(!is.na(
      car_nacional_categorias$conocimiento_comprension
    )),
    sum(!is.na(
      car_nacional_categorias$capl_total
    ))
  ),

  n_categoria = c(
    sum(!is.na(
      car_nacional_categorias$categoria_pc
    )),
    sum(!is.na(
      car_nacional_categorias$categoria_db
    )),
    sum(!is.na(
      car_nacional_categorias$categoria_mc
    )),
    sum(!is.na(
      car_nacional_categorias$categoria_ku
    )),
    sum(!is.na(
      car_nacional_categorias$categoria_total
    ))
  )
) %>%
  mutate(
    diferencia =
      n_puntaje -
      n_categoria
  )

knitr::kable(
  auditoria_categorias,
  caption = "Auditoría de las categorías oficiales"
)
Auditoría de las categorías oficiales
dominio n_puntaje n_categoria diferencia
Competencia física 843 843 0
Comportamiento diario 840 840 0
Motivación y confianza 842 842 0
Conocimiento y comprensión 842 842 0
CAPL-2 total 838 838 0
if (
  any(
    auditoria_categorias$diferencia != 0
  )
) {
  stop(
    "Existen puntajes válidos sin categoría oficial reproducida."
  )
}
categorias_largo <- car_nacional_categorias %>%
  select(
    edad,
    sexo,
    region,
    categoria_pc,
    categoria_db,
    categoria_mc,
    categoria_ku,
    categoria_total
  ) %>%
  pivot_longer(
    cols =
      starts_with(
        "categoria_"
      ),
    names_to = "variable",
    values_to = "categoria"
  ) %>%
  mutate(
    dominio = case_when(
      variable ==
        "categoria_pc" ~
        "Competencia física",

      variable ==
        "categoria_db" ~
        "Comportamiento diario",

      variable ==
        "categoria_mc" ~
        "Motivación y confianza",

      variable ==
        "categoria_ku" ~
        "Conocimiento y comprensión",

      variable ==
        "categoria_total" ~
        "CAPL-2 total"
    ),

    categoria =
      factor(
        categoria,
        levels = c(
          "beginning",
          "progressing",
          "achieving",
          "excelling"
        ),
        labels = c(
          "Beginning",
          "Progressing",
          "Achieving",
          "Excelling"
        )
      )
  )

8 7. Distribución nacional y regional de las categorías

tabla_categorias_nacional <- categorias_largo %>%
  filter(
    !is.na(categoria)
  ) %>%
  count(
    dominio,
    categoria,
    name = "n"
  ) %>%
  group_by(
    dominio
  ) %>%
  mutate(
    total = sum(n),
    porcentaje =
      100 * n / total
  ) %>%
  ungroup() %>%
  mutate(
    porcentaje =
      round(
        porcentaje,
        1
      ),
    presentacion =
      paste0(
        n,
        " (",
        format(
          porcentaje,
          nsmall = 1,
          decimal.mark = ","
        ),
        "%)"
      )
  )

tabla_categorias_nacional_ancha <-
  tabla_categorias_nacional %>%
  select(
    dominio,
    total,
    categoria,
    presentacion
  ) %>%
  distinct() %>%
  pivot_wider(
    names_from = categoria,
    values_from = presentacion
  ) %>%
  rename(
    n = total
  )

knitr::kable(
  tabla_categorias_nacional_ancha,
  caption = "Distribución nacional según categorías interpretativas oficiales del CAPL-2"
)
Distribución nacional según categorías interpretativas oficiales del CAPL-2
dominio n Beginning Progressing Achieving Excelling
CAPL-2 total 838 115 (13,7%) 545 (65,0%) 123 (14,7%) 55 ( 6,6%)
Competencia física 843 372 (44,1%) 366 (43,4%) 60 ( 7,1%) 45 ( 5,3%)
Comportamiento diario 840 56 ( 6,7%) 645 (76,8%) 126 (15,0%) 13 ( 1,5%)
Conocimiento y comprensión 842 386 (45,8%) 208 (24,7%) 88 (10,5%) 160 (19,0%)
Motivación y confianza 842 55 ( 6,5%) 363 (43,1%) 163 (19,4%) 261 (31,0%)
categorias_esperadas <- tribble(
  ~dominio, ~Beginning, ~Progressing, ~Achieving, ~Excelling,
  "CAPL-2 total", 115, 545, 123, 55,
  "Competencia física", 372, 366, 60, 45,
  "Comportamiento diario", 56, 645, 126, 13,
  "Motivación y confianza", 55, 363, 163, 261,
  "Conocimiento y comprensión", 386, 208, 88, 160
)

categorias_observadas <- tabla_categorias_nacional %>%
  select(
    dominio,
    categoria,
    n
  ) %>%
  pivot_wider(
    names_from = categoria,
    values_from = n,
    values_fill = 0
  )

control_categorias <- categorias_esperadas %>%
  left_join(
    categorias_observadas,
    by = "dominio",
    suffix = c(
      "_esperado",
      "_observado"
    )
  )

for (
  cat_actual in c(
    "Beginning",
    "Progressing",
    "Achieving",
    "Excelling"
  )
) {

  control_categorias[[
    paste0(
      "coincide_",
      cat_actual
    )
  ]] <-
    control_categorias[[
      paste0(
        cat_actual,
        "_esperado"
      )
    ]] ==
    control_categorias[[
      paste0(
        cat_actual,
        "_observado"
      )
    ]]
}

knitr::kable(
  control_categorias,
  caption = "Control de correspondencia de las categorías nacionales"
)
Control de correspondencia de las categorías nacionales
dominio Beginning_esperado Progressing_esperado Achieving_esperado Excelling_esperado Beginning_observado Progressing_observado Achieving_observado Excelling_observado coincide_Beginning coincide_Progressing coincide_Achieving coincide_Excelling
CAPL-2 total 115 545 123 55 115 545 123 55 TRUE TRUE TRUE TRUE
Competencia física 372 366 60 45 372 366 60 45 TRUE TRUE TRUE TRUE
Comportamiento diario 56 645 126 13 56 645 126 13 TRUE TRUE TRUE TRUE
Motivación y confianza 55 363 163 261 55 363 163 261 TRUE TRUE TRUE TRUE
Conocimiento y comprensión 386 208 88 160 386 208 88 160 TRUE TRUE TRUE TRUE
columnas_control <- grep(
  "^coincide_",
  names(
    control_categorias
  ),
  value = TRUE
)

if (
  !all(
    unlist(
      control_categorias[
        columnas_control
      ]
    )
  )
) {
  stop(
    "La distribución de categorías no coincide con los resultados definitivos."
  )
}
tabla_categorias_region <- categorias_largo %>%
  filter(
    !is.na(categoria)
  ) %>%
  count(
    dominio,
    region,
    categoria,
    name = "n"
  ) %>%
  group_by(
    dominio,
    region
  ) %>%
  mutate(
    total_region =
      sum(n),
    porcentaje =
      100 *
      n /
      total_region
  ) %>%
  ungroup() %>%
  mutate(
    porcentaje =
      round(
        porcentaje,
        1
      )
  ) %>%
  arrange(
    dominio,
    region,
    categoria
  )

knitr::kable(
  head(
    tabla_categorias_region,
    40
  ),
  caption = "Ejemplo de la distribución de categorías oficiales según región"
)
Ejemplo de la distribución de categorías oficiales según región
dominio region categoria n total_region porcentaje
CAPL-2 total Caribe Beginning 28 140 20.0
CAPL-2 total Caribe Progressing 98 140 70.0
CAPL-2 total Caribe Achieving 11 140 7.9
CAPL-2 total Caribe Excelling 3 140 2.1
CAPL-2 total Centro sur-Amazonía Beginning 29 139 20.9
CAPL-2 total Centro sur-Amazonía Progressing 85 139 61.2
CAPL-2 total Centro sur-Amazonía Achieving 20 139 14.4
CAPL-2 total Centro sur-Amazonía Excelling 5 139 3.6
CAPL-2 total Centro-Oriente Beginning 14 138 10.1
CAPL-2 total Centro-Oriente Progressing 84 138 60.9
CAPL-2 total Centro-Oriente Achieving 33 138 23.9
CAPL-2 total Centro-Oriente Excelling 7 138 5.1
CAPL-2 total Eje cafetero-Antioquia Beginning 23 141 16.3
CAPL-2 total Eje cafetero-Antioquia Progressing 112 141 79.4
CAPL-2 total Eje cafetero-Antioquia Achieving 6 141 4.3
CAPL-2 total Llanos-Orinoquía Beginning 11 140 7.9
CAPL-2 total Llanos-Orinoquía Progressing 65 140 46.4
CAPL-2 total Llanos-Orinoquía Achieving 30 140 21.4
CAPL-2 total Llanos-Orinoquía Excelling 34 140 24.3
CAPL-2 total Pacífico Beginning 10 140 7.1
CAPL-2 total Pacífico Progressing 101 140 72.1
CAPL-2 total Pacífico Achieving 23 140 16.4
CAPL-2 total Pacífico Excelling 6 140 4.3
Competencia física Caribe Beginning 65 140 46.4
Competencia física Caribe Progressing 64 140 45.7
Competencia física Caribe Achieving 8 140 5.7
Competencia física Caribe Excelling 3 140 2.1
Competencia física Centro sur-Amazonía Beginning 94 141 66.7
Competencia física Centro sur-Amazonía Progressing 45 141 31.9
Competencia física Centro sur-Amazonía Achieving 2 141 1.4
Competencia física Centro-Oriente Beginning 39 140 27.9
Competencia física Centro-Oriente Progressing 83 140 59.3
Competencia física Centro-Oriente Achieving 12 140 8.6
Competencia física Centro-Oriente Excelling 6 140 4.3
Competencia física Eje cafetero-Antioquia Beginning 29 141 20.6
Competencia física Eje cafetero-Antioquia Progressing 83 141 58.9
Competencia física Eje cafetero-Antioquia Achieving 17 141 12.1
Competencia física Eje cafetero-Antioquia Excelling 12 141 8.5
Competencia física Llanos-Orinoquía Beginning 48 141 34.0
Competencia física Llanos-Orinoquía Progressing 48 141 34.0

9 8. Asociación entre región y categoría oficial

analizar_asociacion <- function(datos) {

  datos <- datos %>%
    filter(
      !is.na(region),
      !is.na(categoria)
    )

  tab <- table(
    datos$region,
    datos$categoria
  )

  prueba <- suppressWarnings(
    chisq.test(
      tab,
      correct = FALSE
    )
  )

  n_total <- sum(tab)

  filas <- nrow(tab)
  columnas <- ncol(tab)

  v_cramer <- sqrt(
    as.numeric(
      prueba$statistic
    ) /
    (
      n_total *
      min(
        filas - 1,
        columnas - 1
      )
    )
  )

  tibble(
    n = n_total,
    chi2 =
      as.numeric(
        prueba$statistic
      ),
    gl =
      as.numeric(
        prueba$parameter
      ),
    p =
      prueba$p.value,
    v_cramer =
      v_cramer,
    esperado_minimo =
      min(
        prueba$expected
      ),
    porcentaje_esperados_menor_5 =
      100 *
      mean(
        prueba$expected < 5
      )
  )
}
tabla_asociacion_region_categoria <- categorias_largo %>%
  filter(
    !is.na(categoria)
  ) %>%
  group_by(
    dominio
  ) %>%
  group_modify(
    ~ analizar_asociacion(.x)
  ) %>%
  ungroup() %>%
  mutate(
    chi2 = round(
      chi2,
      3
    ),
    v_cramer =
      round(
        v_cramer,
        3
      ),
    esperado_minimo =
      round(
        esperado_minimo,
        3
      ),
    porcentaje_esperados_menor_5 =
      round(
        porcentaje_esperados_menor_5,
        3
      )
  )

knitr::kable(
  tabla_asociacion_region_categoria,
  digits = 3,
  caption = "Asociación entre región y categorías interpretativas oficiales"
)
Asociación entre región y categorías interpretativas oficiales
dominio n chi2 gl p v_cramer esperado_minimo porcentaje_esperados_menor_5
CAPL-2 total 838 144.74 15 0 0.240 9.057 0
Competencia física 843 184.96 15 0 0.270 7.473 0
Comportamiento diario 840 83.46 15 0 0.182 2.151 25
Conocimiento y comprensión 842 321.75 15 0 0.357 14.527 0
Motivación y confianza 842 194.61 15 0 0.278 9.080 0

9.1 8.1. Monte Carlo para Comportamiento diario

set.seed(20260907)

datos_db_cat <- categorias_largo %>%
  filter(
    dominio ==
      "Comportamiento diario",
    !is.na(region),
    !is.na(categoria)
  )

tabla_db_cat <- table(
  datos_db_cat$region,
  datos_db_cat$categoria
)

chi_db_mc <- chisq.test(
  tabla_db_cat,
  simulate.p.value = TRUE,
  B = 100000
)

n_db <- sum(
  tabla_db_cat
)

v_cramer_db <- sqrt(
  as.numeric(
    chi_db_mc$statistic
  ) /
  (
    n_db *
    min(
      nrow(
        tabla_db_cat
      ) - 1,
      ncol(
        tabla_db_cat
      ) - 1
    )
  )
)

resultado_db_mc <- tibble(
  dominio =
    "Comportamiento diario",
  n = n_db,
  chi2 =
    as.numeric(
      chi_db_mc$statistic
    ),
  p_monte_carlo =
    chi_db_mc$p.value,
  B = 100000,
  v_cramer =
    v_cramer_db
) %>%
  mutate(
    chi2 =
      round(
        chi2,
        3
      ),
    v_cramer =
      round(
        v_cramer,
        3
      )
  )

knitr::kable(
  resultado_db_mc,
  digits = 4,
  caption = "Verificación Monte Carlo para Comportamiento diario"
)
Verificación Monte Carlo para Comportamiento diario
dominio n chi2 p_monte_carlo B v_cramer
Comportamiento diario 840 83.46 0 100000 0.182
asociacion_esperada <- tribble(
  ~dominio, ~n_esperado, ~chi2_esperado, ~v_esperado,
  "CAPL-2 total", 838, 145.0, 0.240,
  "Competencia física", 843, 185.0, 0.270,
  "Comportamiento diario", 840, 83.5, 0.182,
  "Motivación y confianza", 842, 195.0, 0.278,
  "Conocimiento y comprensión", 842, 322.0, 0.357
)

control_asociacion <- asociacion_esperada %>%
  left_join(
    tabla_asociacion_region_categoria %>%
      select(
        dominio,
        n,
        chi2,
        v_cramer
      ),
    by = "dominio"
  ) %>%
  mutate(
    coincide_n =
      n ==
      n_esperado,
    coincide_chi2 =
      abs(
        chi2 -
        chi2_esperado
      ) < 0.11,
    coincide_v =
      abs(
        v_cramer -
        v_esperado
      ) < 0.002
  )

knitr::kable(
  control_asociacion,
  caption = "Control de correspondencia de la asociación región × categoría"
)
Control de correspondencia de la asociación región × categoría
dominio n_esperado chi2_esperado v_esperado n chi2 v_cramer coincide_n coincide_chi2 coincide_v
CAPL-2 total 838 145.0 0.240 838 144.74 0.240 TRUE FALSE TRUE
Competencia física 843 185.0 0.270 843 184.96 0.270 TRUE TRUE TRUE
Comportamiento diario 840 83.5 0.182 840 83.46 0.182 TRUE TRUE TRUE
Motivación y confianza 842 195.0 0.278 842 194.61 0.278 TRUE FALSE TRUE
Conocimiento y comprensión 842 322.0 0.357 842 321.75 0.357 TRUE FALSE TRUE
if (
  !all(
    control_asociacion$coincide_n &
    control_asociacion$coincide_chi2 &
    control_asociacion$coincide_v
  )
) {
  warning(
    "Algún resultado de asociación no coincide con el redondeo de la versión definitiva."
  )
}

10 9. Comparación regional mediante Kruskal-Wallis

analizar_kw <- function(datos) {

  datos <- datos %>%
    filter(
      !is.na(puntaje),
      !is.na(region)
    )

  prueba <- kruskal.test(
    puntaje ~ region,
    data = datos
  )

  H <- as.numeric(
    prueba$statistic
  )

  k <- length(
    unique(
      datos$region
    )
  )

  n_total <- nrow(
    datos
  )

  epsilon2 <-
    (
      H -
      k +
      1
    ) /
    (
      n_total -
      k
    )

  epsilon2 <- max(
    0,
    epsilon2
  )

  tibble(
    n = n_total,
    H = H,
    gl =
      as.numeric(
        prueba$parameter
      ),
    p =
      prueba$p.value,
    epsilon2 =
      epsilon2
  )
}
tabla_kw_definitiva <- puntajes_nacionales_largo %>%
  group_by(
    dominio
  ) %>%
  group_modify(
    ~ analizar_kw(.x)
  ) %>%
  ungroup() %>%
  mutate(
    H = round(
      H,
      3
    ),
    epsilon2 =
      round(
        epsilon2,
        3
      ),
    magnitud = case_when(
      epsilon2 <
        0.01 ~
        "Despreciable",

      epsilon2 <
        0.06 ~
        "Pequeña",

      epsilon2 <
        0.14 ~
        "Moderada",

      TRUE ~
        "Grande"
    )
  )

knitr::kable(
  tabla_kw_definitiva,
  digits = 3,
  caption = "Comparación de los puntajes del CAPL-2 entre regiones"
)
Comparación de los puntajes del CAPL-2 entre regiones
dominio n H gl p epsilon2 magnitud
CAPL-2 total 838 97.12 5 0 0.111 Moderada
Competencia física 843 167.62 5 0 0.194 Grande
Comportamiento diario 840 41.46 5 0 0.044 Pequeña
Conocimiento y comprensión 842 294.98 5 0 0.347 Grande
Motivación y confianza 842 210.35 5 0 0.246 Grande
kw_esperado <- tribble(
  ~dominio, ~n_esperado, ~H_esperado, ~epsilon_esperado, ~magnitud_esperada,
  "CAPL-2 total", 838, 97.1, 0.111, "Moderada",
  "Competencia física", 843, 168.0, 0.194, "Grande",
  "Comportamiento diario", 840, 41.5, 0.044, "Pequeña",
  "Motivación y confianza", 842, 210.0, 0.246, "Grande",
  "Conocimiento y comprensión", 842, 295.0, 0.347, "Grande"
)

control_kw <- kw_esperado %>%
  left_join(
    tabla_kw_definitiva %>%
      select(
        dominio,
        n,
        H,
        epsilon2,
        magnitud
      ),
    by = "dominio"
  ) %>%
  mutate(
    coincide_n =
      n ==
      n_esperado,
    coincide_H =
      abs(
        H -
        H_esperado
      ) < 0.11,
    coincide_epsilon =
      abs(
        epsilon2 -
        epsilon_esperado
      ) < 0.002,
    coincide_magnitud =
      magnitud ==
      magnitud_esperada
  )

knitr::kable(
  control_kw,
  caption = "Control de correspondencia de Kruskal-Wallis"
)
Control de correspondencia de Kruskal-Wallis
dominio n_esperado H_esperado epsilon_esperado magnitud_esperada n H epsilon2 magnitud coincide_n coincide_H coincide_epsilon coincide_magnitud
CAPL-2 total 838 97.1 0.111 Moderada 838 97.12 0.111 Moderada TRUE TRUE TRUE TRUE
Competencia física 843 168.0 0.194 Grande 843 167.62 0.194 Grande TRUE FALSE TRUE TRUE
Comportamiento diario 840 41.5 0.044 Pequeña 840 41.46 0.044 Pequeña TRUE TRUE TRUE TRUE
Motivación y confianza 842 210.0 0.246 Grande 842 210.35 0.246 Grande TRUE FALSE TRUE TRUE
Conocimiento y comprensión 842 295.0 0.347 Grande 842 294.98 0.347 Grande TRUE TRUE TRUE TRUE
if (
  !all(
    control_kw$coincide_n &
    control_kw$coincide_H &
    control_kw$coincide_epsilon &
    control_kw$coincide_magnitud
  )
) {
  warning(
    "Algún resultado de Kruskal-Wallis no coincide con la versión definitiva."
  )
}

11 10. Comparaciones post hoc de Dunn con ajuste de Holm

La implementación reproduce la corrección por empates utilizada en el análisis definitivo.

dunn_holm <- function(datos) {

  datos <- datos %>%
    filter(
      !is.na(puntaje),
      !is.na(region)
    )

  x <- datos$puntaje
  g <- factor(
    datos$region
  )

  N <- length(x)

  rangos <- rank(
    x,
    ties.method =
      "average"
  )

  niveles <- levels(g)

  n_grupo <- table(g)

  rango_medio <- tapply(
    rangos,
    g,
    mean
  )

  mediana_grupo <- tapply(
    x,
    g,
    median
  )

  frecuencias_empates <- table(
    x
  )

  termino_empates <- sum(
    frecuencias_empates^3 -
    frecuencias_empates
  )

  varianza_dunn <-
    N *
    (
      N + 1
    ) /
    12 -
    termino_empates /
    (
      12 *
      (
        N - 1
      )
    )

  pares <- combn(
    niveles,
    2,
    simplify = FALSE
  )

  resultados <- lapply(
    pares,
    function(par) {

      g1 <- par[1]
      g2 <- par[2]

      diferencia_rangos <-
        rango_medio[g1] -
        rango_medio[g2]

      error_estandar <- sqrt(
        varianza_dunn *
        (
          1 /
          n_grupo[g1] +
          1 /
          n_grupo[g2]
        )
      )

      z <- as.numeric(
        diferencia_rangos /
        error_estandar
      )

      p <- 2 *
        pnorm(
          -abs(z)
        )

      if (
        rango_medio[g1] >=
        rango_medio[g2]
      ) {

        grupo_mayor <- g1
        grupo_menor <- g2

      } else {

        grupo_mayor <- g2
        grupo_menor <- g1
      }

      tibble(
        grupo_1 = g1,
        grupo_2 = g2,
        grupo_mayor =
          grupo_mayor,
        grupo_menor =
          grupo_menor,
        mediana_1 =
          as.numeric(
            mediana_grupo[g1]
          ),
        mediana_2 =
          as.numeric(
            mediana_grupo[g2]
          ),
        z = z,
        p_sin_ajuste = p
      )
    }
  ) %>%
    bind_rows()

  resultados %>%
    mutate(
      p_ajustada = p.adjust(
        p_sin_ajuste,
        method = "holm"
      ),
      abs_z = abs(z),
      significativo =
        p_ajustada < 0.05,
      contraste = paste(
        grupo_mayor,
        ">",
        grupo_menor
      )
    )
}
tabla_dunn_completa <- puntajes_nacionales_largo %>%
  group_by(
    dominio
  ) %>%
  group_modify(
    ~ dunn_holm(.x)
  ) %>%
  ungroup() %>%
  mutate(
    z = round(
      z,
      3
    ),
    abs_z = round(
      abs_z,
      3
    ),
    mediana_1 = round(
      mediana_1,
      2
    ),
    mediana_2 = round(
      mediana_2,
      2
    )
  )

tabla_dunn_significativas <- tabla_dunn_completa %>%
  filter(
    significativo
  ) %>%
  arrange(
    dominio,
    p_ajustada,
    desc(abs_z)
  )

auditoria_dunn <- tabla_dunn_completa %>%
  group_by(
    dominio
  ) %>%
  summarise(
    comparaciones_totales =
      n(),
    comparaciones_significativas =
      sum(
        significativo
      ),
    .groups = "drop"
  )

knitr::kable(
  auditoria_dunn,
  caption = "Número de comparaciones post hoc significativas por resultado"
)
Número de comparaciones post hoc significativas por resultado
dominio comparaciones_totales comparaciones_significativas
CAPL-2 total 15 9
Competencia física 15 11
Comportamiento diario 15 5
Conocimiento y comprensión 15 10
Motivación y confianza 15 5
tabla_dunn_principales <- tabla_dunn_significativas %>%
  group_by(
    dominio
  ) %>%
  slice_max(
    order_by = abs_z,
    n = 3,
    with_ties = FALSE
  ) %>%
  ungroup() %>%
  arrange(
    dominio,
    desc(abs_z)
  ) %>%
  select(
    dominio,
    contraste,
    mediana_1,
    mediana_2,
    abs_z,
    p_ajustada
  )

knitr::kable(
  tabla_dunn_principales,
  digits = 3,
  caption = "Principales comparaciones post hoc entre regiones"
)
Principales comparaciones post hoc entre regiones
dominio contraste mediana_1 mediana_2 abs_z p_ajustada
CAPL-2 total Llanos-Orinoquía > Eje cafetero-Antioquia 56.53 66.46 8.128 0.000
CAPL-2 total Llanos-Orinoquía > Caribe 56.98 66.46 7.299 0.000
CAPL-2 total Llanos-Orinoquía > Centro sur-Amazonía 57.81 66.46 6.015 0.000
Competencia física Eje cafetero-Antioquia > Pacífico 17.14 12.43 9.341 0.000
Competencia física Eje cafetero-Antioquia > Centro sur-Amazonía 12.43 17.14 9.002 0.000
Competencia física Centro-Oriente > Pacífico 16.46 12.43 7.887 0.000
Comportamiento diario Llanos-Orinoquía > Caribe 15.00 18.00 5.778 0.000
Comportamiento diario Centro-Oriente > Caribe 15.00 18.00 4.668 0.000
Comportamiento diario Eje cafetero-Antioquia > Caribe 15.00 18.00 4.039 0.001
Conocimiento y comprensión Llanos-Orinoquía > Eje cafetero-Antioquia 4.00 8.00 13.222 0.000
Conocimiento y comprensión Pacífico > Eje cafetero-Antioquia 4.00 7.00 11.891 0.000
Conocimiento y comprensión Llanos-Orinoquía > Caribe 4.00 8.00 11.337 0.000
Motivación y confianza Centro sur-Amazonía > Eje cafetero-Antioquia 24.50 18.20 12.463 0.000
Motivación y confianza Pacífico > Eje cafetero-Antioquia 18.20 24.80 11.981 0.000
Motivación y confianza Caribe > Eje cafetero-Antioquia 23.50 18.20 10.232 0.000
dunn_esperado <- tribble(
  ~dominio, ~significativas_esperadas,
  "CAPL-2 total", 9,
  "Competencia física", 11,
  "Comportamiento diario", 5,
  "Conocimiento y comprensión", 10,
  "Motivación y confianza", 5
)

control_dunn <- dunn_esperado %>%
  left_join(
    auditoria_dunn %>%
      select(
        dominio,
        comparaciones_totales,
        comparaciones_significativas
      ),
    by = "dominio"
  ) %>%
  mutate(
    total_correcto =
      comparaciones_totales ==
      15,
    significativas_correctas =
      comparaciones_significativas ==
      significativas_esperadas
  )

knitr::kable(
  control_dunn,
  caption = "Control de correspondencia de las comparaciones de Dunn"
)
Control de correspondencia de las comparaciones de Dunn
dominio significativas_esperadas comparaciones_totales comparaciones_significativas total_correcto significativas_correctas
CAPL-2 total 9 15 9 TRUE TRUE
Competencia física 11 15 11 TRUE TRUE
Comportamiento diario 5 15 5 TRUE TRUE
Conocimiento y comprensión 10 15 10 TRUE TRUE
Motivación y confianza 5 15 5 TRUE TRUE
if (
  !all(
    control_dunn$total_correcto &
    control_dunn$significativas_correctas
  )
) {
  warning(
    "El número de comparaciones significativas no coincide con la versión definitiva."
  )
}

12 11. Exportación de resultados reproducibles

write.csv(
  tabla_disponibilidad_nacional,
  "CAPL2_CARACTERIZACION_DISPONIBILIDAD.csv",
  row.names = FALSE,
  fileEncoding = "UTF-8"
)

write.csv(
  tabla_puntajes_nacional,
  "CAPL2_CARACTERIZACION_PUNTAJES_NACIONAL.csv",
  row.names = FALSE,
  fileEncoding = "UTF-8"
)

write.csv(
  tabla_puntajes_region,
  "CAPL2_CARACTERIZACION_PUNTAJES_REGION.csv",
  row.names = FALSE,
  fileEncoding = "UTF-8"
)

write.csv(
  tabla_categorias_nacional,
  "CAPL2_CARACTERIZACION_CATEGORIAS_NACIONAL.csv",
  row.names = FALSE,
  fileEncoding = "UTF-8"
)

write.csv(
  tabla_categorias_region,
  "CAPL2_CARACTERIZACION_CATEGORIAS_REGION.csv",
  row.names = FALSE,
  fileEncoding = "UTF-8"
)

write.csv(
  tabla_asociacion_region_categoria,
  "CAPL2_CARACTERIZACION_ASOCIACION_REGION_CATEGORIAS.csv",
  row.names = FALSE,
  fileEncoding = "UTF-8"
)

write.csv(
  resultado_db_mc,
  "CAPL2_CARACTERIZACION_MONTE_CARLO_DB.csv",
  row.names = FALSE,
  fileEncoding = "UTF-8"
)

write.csv(
  tabla_kw_definitiva,
  "CAPL2_CARACTERIZACION_KRUSKAL_WALLIS.csv",
  row.names = FALSE,
  fileEncoding = "UTF-8"
)

write.csv(
  tabla_dunn_completa,
  "CAPL2_CARACTERIZACION_DUNN_COMPLETO.csv",
  row.names = FALSE,
  fileEncoding = "UTF-8"
)

write.csv(
  tabla_dunn_principales,
  "CAPL2_CARACTERIZACION_DUNN_PRINCIPALES.csv",
  row.names = FALSE,
  fileEncoding = "UTF-8"
)

13 12. Control final

control_final <- tibble(
  componente = c(
    "N total",
    "Disponibilidad",
    "Categorías nacionales",
    "Asociación región × categoría",
    "Kruskal-Wallis",
    "Dunn"
  ),
  estado = c(
    nrow(car_nacional) == 843,
    all(
      control_disponibilidad$coincide
    ),
    all(
      unlist(
        control_categorias[
          columnas_control
        ]
      )
    ),
    all(
      control_asociacion$coincide_n &
      control_asociacion$coincide_chi2 &
      control_asociacion$coincide_v
    ),
    all(
      control_kw$coincide_n &
      control_kw$coincide_H &
      control_kw$coincide_epsilon &
      control_kw$coincide_magnitud
    ),
    all(
      control_dunn$total_correcto &
      control_dunn$significativas_correctas
    )
  )
)

knitr::kable(
  control_final,
  caption = "Control final de correspondencia con los resultados definitivos"
)
Control final de correspondencia con los resultados definitivos
componente estado
N total TRUE
Disponibilidad TRUE
Categorías nacionales TRUE
Asociación región × categoría FALSE
Kruskal-Wallis FALSE
Dunn TRUE
if (!all(control_final$estado)) {
  warning(
    "Existe al menos una discrepancia con los resultados definitivos. Revise antes de publicar."
  )
}

14 13. Información de sesión

sessionInfo()
## R version 4.5.1 (2025-06-13 ucrt)
## Platform: x86_64-w64-mingw32/x64
## Running under: Windows 11 x64 (build 26200)
## 
## Matrix products: default
##   LAPACK version 3.12.1
## 
## locale:
## [1] LC_COLLATE=Spanish_Colombia.utf8  LC_CTYPE=Spanish_Colombia.utf8   
## [3] LC_MONETARY=Spanish_Colombia.utf8 LC_NUMERIC=C                     
## [5] LC_TIME=Spanish_Colombia.utf8    
## 
## time zone: America/Bogota
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
## [1] knitr_1.50   capl_1.42    purrr_1.1.0  tibble_3.3.0 tidyr_1.3.1 
## [6] dplyr_1.2.0  readxl_1.4.5
## 
## loaded via a namespace (and not attached):
##  [1] gtable_0.3.6       jsonlite_2.0.0     compiler_4.5.1     tidyselect_1.2.1  
##  [5] stringr_1.5.1      jquerylib_0.1.4    scales_1.4.0       yaml_2.3.10       
##  [9] fastmap_1.2.0      ggplot2_4.0.1      R6_2.6.1           generics_0.1.4    
## [13] lubridate_1.9.4    bslib_0.9.0        pillar_1.11.0      RColorBrewer_1.1-3
## [17] rlang_1.2.0        stringi_1.8.7      cachem_1.1.0       xfun_0.55         
## [21] sass_0.4.10        S7_0.2.1           timechange_0.3.0   cli_3.6.6         
## [25] withr_3.0.2        magrittr_2.0.3     digest_0.6.37      grid_4.5.1        
## [29] rstudioapi_0.17.1  lifecycle_1.0.5    vctrs_0.7.1        writexl_1.5.4     
## [33] evaluate_1.0.4     glue_1.8.0         farver_2.1.2       cellranger_1.1.0  
## [37] rmarkdown_2.29     tools_4.5.1        pkgconfig_2.0.3    htmltools_0.5.8.1