Este documento reproduce el análisis definitivo utilizado para construir los valores de referencia del CAPL-2 versión Colombia en la muestra multicéntrica de escolares de 8 a 12 años.
El flujo incluye:
Para Competencia física, Comportamiento diario, Motivación y confianza y el puntaje total, la localización se modeló mediante edad suavizada y región, mientras que la dispersión se modeló mediante edad suavizada. En Conocimiento y comprensión se utilizó una distribución beta inflada, con edad, región y grado en el parámetro de localización y los demás parámetros constantes.
paquetes <- c(
"readxl",
"dplyr",
"tidyr",
"tibble",
"purrr",
"gamlss",
"gamlss.dist",
"capl",
"knitr"
)
faltantes <- paquetes[
!vapply(paquetes, requireNamespace, logical(1), quietly = TRUE)
]
if (length(faltantes) > 0) {
stop(
paste0(
"Instale antes de continuar: ",
paste(faltantes, collapse = ", ")
)
)
}
library(readxl)
library(dplyr)
library(tidyr)
library(tibble)
library(purrr)
library(gamlss)
library(gamlss.dist)
library(capl)
library(knitr)
set.seed(20260907)
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 Rmd y el Excel en la misma carpeta ",
"o ajuste params$archivo_excel."
)
)
}
archivo <- localizar_archivo(
params$archivo_excel
)
cat("Archivo localizado en:\n", archivo, "\n")
## Archivo localizado en:
## C:\Users\Personal\Documents\análisis doctorado\Analisis\CAPL2_Colombia_BASE_FINAL_ANEXO_Y_AUDITORIA.xlsx
hojas <- readxl::excel_sheets(archivo)
if (!params$hoja_auditoria %in% hojas) {
stop("No se encontró la hoja Base_auditoria.")
}
if (!params$hoja_anexo %in% hojas) {
stop("No se encontró la hoja Base_anexo.")
}
base <- read_excel(
archivo,
sheet = params$hoja_auditoria
)
base_anexo <- read_excel(
archivo,
sheet = params$hoja_anexo
)
cat("Base_auditoria:", dim(base), "\n")
## Base_auditoria: 843 268
cat("Base_anexo:", dim(base_anexo), "\n")
## Base_anexo: 843 86
variables_necesarias <- c(
"age",
"gender",
"region",
"grade",
"pc_score_final",
"db_score_final",
"mc_score_final",
"ku_score_final",
"capl_total_final"
)
faltan <- setdiff(
variables_necesarias,
names(base)
)
if (length(faltan) > 0) {
stop(
paste(
"Faltan variables en Base_auditoria:",
paste(faltan, collapse = ", ")
)
)
}
ref <- base %>%
transmute(
age = as.numeric(age),
grade = as.numeric(grade),
gender = tolower(as.character(gender)),
region = as.character(region),
pc_score_final = as.numeric(pc_score_final),
db_score_final = as.numeric(db_score_final),
mc_score_final = as.numeric(mc_score_final),
ku_score_final = as.numeric(ku_score_final),
capl_total_final = as.numeric(capl_total_final)
) %>%
mutate(
gender = factor(
gender,
levels = c("boy", "girl")
),
region = factor(region)
)
if (nrow(ref) != 843) {
stop("La base de referencia no contiene los 843 escolares esperados.")
}
cat("Regiones:\n")
## Regiones:
print(levels(ref$region))
## [1] "Caribe" "Centro-Oriente" "Centro sur-Amazonía"
## [4] "Eje cafetero-Antioquia" "Llanos-Orinoquía" "Pacífico"
cat("\nSexo:\n")
##
## Sexo:
print(table(ref$gender, useNA = "ifany"))
##
## boy girl
## 418 425
cat("\nEdad:\n")
##
## Edad:
print(table(ref$age, useNA = "ifany"))
##
## 8 9 10 11 12
## 168 170 171 170 164
especificaciones <- tribble(
~variable, ~sexo, ~familia, ~usa_grado, ~n_esperado,
"pc_score_final", "boy", "BCCG", FALSE, 418,
"pc_score_final", "girl", "BCCG", FALSE, 425,
"db_score_final", "boy", "BCPE", FALSE, 415,
"db_score_final", "girl", "NO", FALSE, 425,
"mc_score_final", "boy", "BCPE", FALSE, 418,
"mc_score_final", "girl", "BCPE", FALSE, 424,
"ku_score_final", "boy", "BEINF", TRUE, 402,
"ku_score_final", "girl", "BEINF", TRUE, 413,
"capl_total_final", "boy", "BCCG", FALSE, 414,
"capl_total_final", "girl", "BCCG", FALSE, 424
)
crear_datos_modelo <- function(
variable_actual,
sexo_actual,
usa_grado
) {
datos <- ref %>%
mutate(
score_aux = as.numeric(.data[[variable_actual]])
)
if (usa_grado) {
datos %>%
filter(
gender == sexo_actual,
!is.na(age),
!is.na(region),
!is.na(grade),
!is.na(score_aux)
) %>%
transmute(
y = score_aux / 10,
score_original = score_aux,
age = as.numeric(age),
region = factor(
region,
levels = levels(ref$region)
),
grade = as.numeric(grade)
)
} else {
datos %>%
filter(
gender == sexo_actual,
!is.na(age),
!is.na(region),
!is.na(score_aux)
) %>%
transmute(
score = score_aux,
age = as.numeric(age),
region = factor(
region,
levels = levels(ref$region)
)
)
}
}
tabla_casos <- purrr::pmap_dfr(
especificaciones,
function(variable, sexo, familia, usa_grado, n_esperado) {
datos <- crear_datos_modelo(
variable,
sexo,
usa_grado
)
tibble(
variable = variable,
sexo = sexo,
familia = familia,
n_esperado = n_esperado,
n_observado = nrow(datos),
coincide = nrow(datos) == n_esperado
)
}
)
knitr::kable(
tabla_casos,
caption = "Casos incluidos en los modelos definitivos"
)
| variable | sexo | familia | n_esperado | n_observado | coincide |
|---|---|---|---|---|---|
| pc_score_final | boy | BCCG | 418 | 418 | TRUE |
| pc_score_final | girl | BCCG | 425 | 425 | TRUE |
| db_score_final | boy | BCPE | 415 | 415 | TRUE |
| db_score_final | girl | NO | 425 | 425 | TRUE |
| mc_score_final | boy | BCPE | 418 | 418 | TRUE |
| mc_score_final | girl | BCPE | 424 | 424 | TRUE |
| ku_score_final | boy | BEINF | 402 | 402 | TRUE |
| ku_score_final | girl | BEINF | 413 | 413 | TRUE |
| capl_total_final | boy | BCCG | 414 | 414 | TRUE |
| capl_total_final | girl | BCCG | 424 | 424 | TRUE |
if (!all(tabla_casos$coincide)) {
stop(
"Al menos uno de los tamaños analíticos no coincide con la versión definitiva."
)
}
Las familias retenidas fueron:
obtener_familia <- function(nombre) {
switch(
nombre,
"NO" = gamlss.dist::NO(),
"BCCG" = gamlss.dist::BCCG(),
"BCPE" = gamlss.dist::BCPE(),
"BEINF" = gamlss.dist::BEINF(),
stop("Familia no reconocida.")
)
}
ajustar_modelo_final <- function(
variable_actual,
sexo_actual,
familia_actual,
usa_grado
) {
datos <- crear_datos_modelo(
variable_actual,
sexo_actual,
usa_grado
)
if (usa_grado) {
modelo <- gamlss(
y ~ pb(age) + region + grade,
sigma.formula = ~ 1,
nu.formula = ~ 1,
tau.formula = ~ 1,
family = BEINF(),
data = datos,
control = gamlss.control(
n.cyc = 300,
trace = FALSE
)
)
} else {
modelo <- gamlss(
score ~ pb(age) + region,
sigma.formula = ~ pb(age),
family = obtener_familia(familia_actual),
data = datos,
control = gamlss.control(
n.cyc = 300,
trace = FALSE
)
)
}
list(
modelo = modelo,
datos = datos
)
}
ajustes <- list()
for (i in seq_len(nrow(especificaciones))) {
variable_actual <- especificaciones$variable[i]
sexo_actual <- especificaciones$sexo[i]
familia_actual <- especificaciones$familia[i]
usa_grado <- especificaciones$usa_grado[i]
clave <- paste(
variable_actual,
sexo_actual,
sep = "__"
)
cat(
"\nAjustando:",
clave,
"| familia:",
familia_actual,
"\n"
)
ajustes[[clave]] <- ajustar_modelo_final(
variable_actual,
sexo_actual,
familia_actual,
usa_grado
)
}
##
## Ajustando: pc_score_final__boy | familia: BCCG
##
## Ajustando: pc_score_final__girl | familia: BCCG
##
## Ajustando: db_score_final__boy | familia: BCPE
##
## Ajustando: db_score_final__girl | familia: NO
##
## Ajustando: mc_score_final__boy | familia: BCPE
##
## Ajustando: mc_score_final__girl | familia: BCPE
##
## Ajustando: ku_score_final__boy | familia: BEINF
##
## Ajustando: ku_score_final__girl | familia: BEINF
##
## Ajustando: capl_total_final__boy | familia: BCCG
##
## Ajustando: capl_total_final__girl | familia: BCCG
resumen_residuos <- function(modelo) {
z <- as.numeric(
residuals(
modelo,
what = "z-scores"
)
)
z <- z[is.finite(z)]
media <- mean(z)
de <- sd(z)
asimetria <- mean(
((z - media) / de)^3
)
curtosis <- mean(
((z - media) / de)^4
) - 3
tibble(
media_residuo = media,
DE_residuo = de,
asimetria = asimetria,
curtosis = curtosis
)
}
tabla_modelos <- purrr::pmap_dfr(
especificaciones,
function(variable, sexo, familia, usa_grado, n_esperado) {
clave <- paste(variable, sexo, sep = "__")
modelo <- ajustes[[clave]]$modelo
datos <- ajustes[[clave]]$datos
diag <- resumen_residuos(modelo)
tibble(
variable = variable,
sexo = sexo,
n = nrow(datos),
familia = familia,
convergio = isTRUE(modelo$converged),
GAIC = as.numeric(
GAIC(modelo, k = 2)
),
SBC = as.numeric(
GAIC(
modelo,
k = log(nrow(datos))
)
),
devianza_global = modelo$G.deviance,
df = modelo$df.fit,
media_residuo = diag$media_residuo,
DE_residuo = diag$DE_residuo,
asimetria = diag$asimetria,
curtosis = diag$curtosis
)
}
)
knitr::kable(
tabla_modelos,
digits = 3,
caption = "Ajuste y diagnóstico de los modelos GAMLSS definitivos"
)
| variable | sexo | n | familia | convergio | GAIC | SBC | devianza_global | df | media_residuo | DE_residuo | asimetria | curtosis |
|---|---|---|---|---|---|---|---|---|---|---|---|---|
| pc_score_final | boy | 418 | BCCG | TRUE | 2314.53 | 2354.887 | 2294.53 | 10.00 | 0.000 | 1.001 | -0.010 | -0.148 |
| pc_score_final | girl | 425 | BCCG | TRUE | 2307.19 | 2347.715 | 2287.19 | 10.00 | -0.001 | 1.001 | -0.035 | -0.417 |
| db_score_final | boy | 415 | BCPE | TRUE | 2310.42 | 2354.733 | 2288.42 | 11.00 | -0.013 | 1.000 | 0.015 | -0.049 |
| db_score_final | girl | 425 | NO | TRUE | 2403.90 | 2446.545 | 2382.85 | 10.53 | -0.001 | 1.001 | 0.079 | -0.376 |
| mc_score_final | boy | 418 | BCPE | TRUE | 2207.76 | 2252.202 | 2185.74 | 11.01 | -0.005 | 0.994 | 0.007 | -0.048 |
| mc_score_final | girl | 424 | BCPE | TRUE | 2284.17 | 2328.716 | 2262.17 | 11.00 | -0.023 | 1.000 | 0.028 | -0.015 |
| ku_score_final | boy | 402 | BEINF | TRUE | -45.55 | -1.594 | -67.56 | 11.00 | 0.000 | 0.987 | -0.117 | 0.001 |
| ku_score_final | girl | 413 | BEINF | TRUE | -86.70 | -42.447 | -108.70 | 11.00 | -0.004 | 0.986 | -0.026 | -0.266 |
| capl_total_final | boy | 414 | BCCG | TRUE | 2936.36 | 2980.105 | 2914.62 | 10.87 | 0.001 | 1.001 | -0.013 | -0.071 |
| capl_total_final | girl | 424 | BCCG | TRUE | 2997.29 | 3039.550 | 2976.42 | 10.44 | 0.000 | 1.001 | -0.004 | -0.035 |
Los worm plots se utilizan como diagnóstico gráfico complementario de los residuos cuantílicos normalizados.
for (clave in names(ajustes)) {
modelo <- ajustes[[clave]]$modelo
# Extraer directamente los residuos cuantílicos normalizados.
# Esto evita que wp() intente reconstruir la llamada original
# del modelo y buscar el objeto local "datos", que ya no existe
# fuera de la función de ajuste.
z <- as.numeric(
residuals(
modelo,
what = "z-scores"
)
)
z <- z[is.finite(z)]
wp(
resid = z,
main = clave,
ylim.all = 2.5
)
}
probabilidades <- seq(
0.05,
0.95,
by = 0.05
)
nombres_percentiles <- paste0(
"P",
seq(5, 95, by = 5)
)
limites_teoricos <- tribble(
~variable, ~minimo, ~maximo,
"pc_score_final", 0, 30,
"db_score_final", 0, 30,
"mc_score_final", 0, 30,
"ku_score_final", 0, 10,
"capl_total_final", 0, 100
)
predecir_parametros <- function(
modelo,
datos_entrenamiento,
datos_prediccion
) {
p <- gamlss::predictAll(
object = modelo,
newdata = datos_prediccion,
data = datos_entrenamiento,
type = "response",
output = "data.frame"
)
p <- as.data.frame(p)
if (!"mu" %in% names(p)) {
stop("predictAll no devolvió mu.")
}
if (!"sigma" %in% names(p)) {
p$sigma <- NA_real_
}
if (!"nu" %in% names(p)) {
p$nu <- NA_real_
}
if (!"tau" %in% names(p)) {
p$tau <- NA_real_
}
p
}
cuantil_modelo <- function(
familia,
p,
mu,
sigma,
nu,
tau
) {
if (familia == "NO") {
return(
gamlss.dist::qNO(
p,
mu = mu,
sigma = sigma
)
)
}
if (familia == "BCCG") {
return(
gamlss.dist::qBCCG(
p,
mu = mu,
sigma = sigma,
nu = nu
)
)
}
if (familia == "BCPE") {
return(
gamlss.dist::qBCPE(
p,
mu = mu,
sigma = sigma,
nu = nu,
tau = tau
)
)
}
if (familia == "BEINF") {
return(
10 *
gamlss.dist::qBEINF(
p,
mu = mu,
sigma = sigma,
nu = nu,
tau = tau
)
)
}
stop("Familia no reconocida.")
}
lista_percentiles_raw <- list()
lista_parametros <- list()
for (i in seq_len(nrow(especificaciones))) {
variable_actual <- especificaciones$variable[i]
sexo_actual <- especificaciones$sexo[i]
familia_actual <- especificaciones$familia[i]
usa_grado <- especificaciones$usa_grado[i]
clave <- paste(
variable_actual,
sexo_actual,
sep = "__"
)
modelo <- ajustes[[clave]]$modelo
datos <- ajustes[[clave]]$datos
if (usa_grado) {
grid <- datos %>%
count(
age,
region,
grade,
name = "n_perfil"
) %>%
arrange(
region,
age,
grade
)
nuevos <- grid %>%
select(
age,
region,
grade
)
} else {
soporte <- datos %>%
count(
age,
region,
name = "n_perfil"
)
grid <- tidyr::expand_grid(
age = 8:12,
region = levels(ref$region)
) %>%
mutate(
age = as.numeric(age),
region = factor(
region,
levels = levels(ref$region)
)
) %>%
left_join(
soporte,
by = c("age", "region")
) %>%
mutate(
n_perfil = tidyr::replace_na(
n_perfil,
0L
),
grade = NA_real_
)
nuevos <- grid %>%
select(
age,
region
)
}
parametros <- predecir_parametros(
modelo,
datos,
nuevos
)
tabla_actual <- grid %>%
mutate(
variable = variable_actual,
sexo = sexo_actual,
familia = familia_actual,
mu = parametros$mu,
sigma = parametros$sigma,
nu = parametros$nu,
tau = parametros$tau
)
for (j in seq_along(probabilidades)) {
nombre_p <- nombres_percentiles[j]
tabla_actual[[nombre_p]] <- cuantil_modelo(
familia = familia_actual,
p = probabilidades[j],
mu = parametros$mu,
sigma = parametros$sigma,
nu = parametros$nu,
tau = parametros$tau
)
}
lista_percentiles_raw[[clave]] <- tabla_actual
lista_parametros[[clave]] <- tabla_actual %>%
select(
variable,
sexo,
familia,
age,
region,
grade,
n_perfil,
mu,
sigma,
nu,
tau
)
}
tabla_percentiles_raw <- bind_rows(
lista_percentiles_raw
)
tabla_parametros <- bind_rows(
lista_parametros
)
tabla_percentiles_larga_raw <- tabla_percentiles_raw %>%
pivot_longer(
cols = all_of(nombres_percentiles),
names_to = "percentil",
values_to = "valor_raw"
) %>%
left_join(
limites_teoricos,
by = "variable"
) %>%
mutate(
fuera_limite =
valor_raw < minimo |
valor_raw > maximo
)
auditoria_limites <- tabla_percentiles_larga_raw %>%
group_by(
variable,
sexo,
familia
) %>%
summarise(
n_estimaciones = n(),
n_fuera_limite = sum(
fuera_limite,
na.rm = TRUE
),
minimo_estimado = min(
valor_raw,
na.rm = TRUE
),
maximo_estimado = max(
valor_raw,
na.rm = TRUE
),
.groups = "drop"
)
knitr::kable(
auditoria_limites,
digits = 3,
caption = "Auditoría de los límites teóricos antes de la restricción de presentación"
)
| variable | sexo | familia | n_estimaciones | n_fuera_limite | minimo_estimado | maximo_estimado |
|---|---|---|---|---|---|---|
| capl_total_final | boy | BCCG | 570 | 0 | 44.806 | 85.959 |
| capl_total_final | girl | BCCG | 570 | 0 | 39.690 | 79.927 |
| db_score_final | boy | BCPE | 570 | 0 | 11.868 | 27.720 |
| db_score_final | girl | NO | 570 | 0 | 5.148 | 24.596 |
| ku_score_final | boy | BEINF | 1254 | 0 | 0.229 | 9.997 |
| ku_score_final | girl | BEINF | 1197 | 0 | 0.439 | 9.959 |
| mc_score_final | boy | BCPE | 570 | 0 | 13.346 | 29.955 |
| mc_score_final | girl | BCPE | 570 | 1 | 12.076 | 30.098 |
| pc_score_final | boy | BCCG | 570 | 0 | 6.867 | 26.329 |
| pc_score_final | girl | BCCG | 570 | 0 | 5.842 | 24.045 |
tabla_percentiles <- tabla_percentiles_raw %>%
left_join(
limites_teoricos,
by = "variable"
) %>%
mutate(
across(
all_of(nombres_percentiles),
~ pmin(
maximo,
pmax(
minimo,
.x
)
)
)
) %>%
select(
-minimo,
-maximo
)
tabla_principales <- tabla_percentiles %>%
select(
variable,
sexo,
familia,
age,
region,
grade,
n_perfil,
P5,
P25,
P50,
P75,
P95
)
knitr::kable(
head(tabla_principales, 40),
digits = 2,
caption = "Ejemplo de los valores de referencia definitivos"
)
| variable | sexo | familia | age | region | grade | n_perfil | P5 | P25 | P50 | P75 | P95 |
|---|---|---|---|---|---|---|---|---|---|---|---|
| pc_score_final | boy | BCCG | 8 | Caribe | NA | 14 | 8.18 | 11.91 | 14.49 | 17.07 | 20.77 |
| pc_score_final | boy | BCCG | 8 | Centro-Oriente | NA | 14 | 8.88 | 12.93 | 15.74 | 18.54 | 22.56 |
| pc_score_final | boy | BCCG | 8 | Centro sur-Amazonía | NA | 14 | 7.06 | 10.28 | 12.51 | 14.73 | 17.92 |
| pc_score_final | boy | BCCG | 8 | Eje cafetero-Antioquia | NA | 14 | 9.25 | 13.47 | 16.39 | 19.31 | 23.49 |
| pc_score_final | boy | BCCG | 8 | Llanos-Orinoquía | NA | 14 | 10.15 | 14.79 | 18.00 | 21.20 | 25.80 |
| pc_score_final | boy | BCCG | 8 | Pacífico | NA | 14 | 6.87 | 10.00 | 12.17 | 14.34 | 17.44 |
| pc_score_final | boy | BCCG | 9 | Caribe | NA | 16 | 8.56 | 12.23 | 14.78 | 17.31 | 20.95 |
| pc_score_final | boy | BCCG | 9 | Centro-Oriente | NA | 14 | 9.28 | 13.26 | 16.02 | 18.77 | 22.72 |
| pc_score_final | boy | BCCG | 9 | Centro sur-Amazonía | NA | 14 | 7.41 | 10.59 | 12.79 | 14.99 | 18.14 |
| pc_score_final | boy | BCCG | 9 | Eje cafetero-Antioquia | NA | 14 | 9.66 | 13.80 | 16.67 | 19.54 | 23.64 |
| pc_score_final | boy | BCCG | 9 | Llanos-Orinoquía | NA | 14 | 10.59 | 15.13 | 18.28 | 21.42 | 25.93 |
| pc_score_final | boy | BCCG | 9 | Pacífico | NA | 14 | 7.21 | 10.31 | 12.45 | 14.59 | 17.66 |
| pc_score_final | boy | BCCG | 10 | Caribe | NA | 16 | 8.94 | 12.55 | 15.06 | 17.55 | 21.14 |
| pc_score_final | boy | BCCG | 10 | Centro-Oriente | NA | 15 | 9.68 | 13.59 | 16.30 | 19.01 | 22.89 |
| pc_score_final | boy | BCCG | 10 | Centro sur-Amazonía | NA | 14 | 7.76 | 10.90 | 13.07 | 15.24 | 18.35 |
| pc_score_final | boy | BCCG | 10 | Eje cafetero-Antioquia | NA | 14 | 10.06 | 14.13 | 16.95 | 19.77 | 23.80 |
| pc_score_final | boy | BCCG | 10 | Llanos-Orinoquía | NA | 14 | 11.02 | 15.48 | 18.56 | 21.64 | 26.06 |
| pc_score_final | boy | BCCG | 10 | Pacífico | NA | 14 | 7.56 | 10.62 | 12.74 | 14.85 | 17.88 |
| pc_score_final | boy | BCCG | 11 | Caribe | NA | 16 | 9.32 | 12.88 | 15.34 | 17.79 | 21.32 |
| pc_score_final | boy | BCCG | 11 | Centro-Oriente | NA | 9 | 10.08 | 13.92 | 16.58 | 19.24 | 23.05 |
| pc_score_final | boy | BCCG | 11 | Centro sur-Amazonía | NA | 16 | 8.11 | 11.21 | 13.35 | 15.49 | 18.56 |
| pc_score_final | boy | BCCG | 11 | Eje cafetero-Antioquia | NA | 14 | 10.47 | 14.47 | 17.24 | 20.00 | 23.96 |
| pc_score_final | boy | BCCG | 11 | Llanos-Orinoquía | NA | 14 | 11.45 | 15.82 | 18.85 | 21.86 | 26.19 |
| pc_score_final | boy | BCCG | 11 | Pacífico | NA | 16 | 7.91 | 10.93 | 13.02 | 15.10 | 18.09 |
| pc_score_final | boy | BCCG | 12 | Caribe | NA | 8 | 9.70 | 13.20 | 15.62 | 18.04 | 21.50 |
| pc_score_final | boy | BCCG | 12 | Centro-Oriente | NA | 14 | 10.48 | 14.25 | 16.87 | 19.47 | 23.22 |
| pc_score_final | boy | BCCG | 12 | Centro sur-Amazonía | NA | 14 | 8.47 | 11.52 | 13.63 | 15.74 | 18.77 |
| pc_score_final | boy | BCCG | 12 | Eje cafetero-Antioquia | NA | 14 | 10.88 | 14.80 | 17.52 | 20.23 | 24.11 |
| pc_score_final | boy | BCCG | 12 | Llanos-Orinoquía | NA | 14 | 11.88 | 16.16 | 19.13 | 22.08 | 26.33 |
| pc_score_final | boy | BCCG | 12 | Pacífico | NA | 12 | 8.26 | 11.24 | 13.30 | 15.35 | 18.31 |
| pc_score_final | girl | BCCG | 8 | Caribe | NA | 14 | 6.81 | 10.27 | 12.72 | 15.18 | 18.76 |
| pc_score_final | girl | BCCG | 8 | Centro-Oriente | NA | 14 | 7.71 | 11.63 | 14.39 | 17.18 | 21.23 |
| pc_score_final | girl | BCCG | 8 | Centro sur-Amazonía | NA | 14 | 6.07 | 9.16 | 11.34 | 13.54 | 16.73 |
| pc_score_final | girl | BCCG | 8 | Eje cafetero-Antioquia | NA | 14 | 8.27 | 12.48 | 15.44 | 18.44 | 22.78 |
| pc_score_final | girl | BCCG | 8 | Llanos-Orinoquía | NA | 14 | 8.32 | 12.55 | 15.53 | 18.54 | 22.92 |
| pc_score_final | girl | BCCG | 8 | Pacífico | NA | 14 | 5.84 | 8.81 | 10.91 | 13.03 | 16.10 |
| pc_score_final | girl | BCCG | 9 | Caribe | NA | 14 | 7.50 | 10.88 | 13.25 | 15.65 | 19.13 |
| pc_score_final | girl | BCCG | 9 | Centro-Oriente | NA | 14 | 8.45 | 12.25 | 14.93 | 17.63 | 21.55 |
| pc_score_final | girl | BCCG | 9 | Centro sur-Amazonía | NA | 14 | 6.72 | 9.75 | 11.88 | 14.03 | 17.15 |
| pc_score_final | girl | BCCG | 9 | Eje cafetero-Antioquia | NA | 14 | 9.04 | 13.11 | 15.98 | 18.87 | 23.07 |
write.csv(
tabla_percentiles,
"CAPL2_percentiles_GAMLSS_completos.csv",
row.names = FALSE,
fileEncoding = "UTF-8"
)
Este análisis no modifica los puntos de corte oficiales. Únicamente determina la posición percentílica que ocupa cada transición oficial dentro de las distribuciones colombianas estimadas.
protocolos_capl <- tribble(
~variable, ~protocolo, ~dominio, ~minimo, ~maximo,
"pc_score_final", "pc", "Competencia física", 0, 30,
"db_score_final", "db", "Comportamiento diario", 0, 30,
"mc_score_final", "mc", "Motivación y confianza", 0, 30,
"ku_score_final", "ku", "Conocimiento y comprensión", 0, 10,
"capl_total_final", "capl", "CAPL-2 total", 0, 100
)
niveles_categoria <- c(
beginning = 1,
progressing = 2,
achieving = 3,
excelling = 4
)
categorias_transicion <- tribble(
~categoria_oficial, ~nivel_objetivo,
"Progressing", 2,
"Achieving", 3,
"Excelling", 4
)
buscar_transicion_capl <- function(
edad,
sexo,
protocolo,
nivel_objetivo,
minimo,
maximo,
tolerancia = 1e-8
) {
obtener_nivel <- function(puntaje) {
interpretacion <- capl::get_capl_interpretation(
age = edad,
gender = sexo,
score = puntaje,
protocol = protocolo
)
if (
length(interpretacion) == 0 ||
is.na(interpretacion)
) {
return(NA_real_)
}
as.numeric(
niveles_categoria[
as.character(interpretacion)
]
)
}
inferior <- minimo
superior <- maximo
nivel_maximo <- obtener_nivel(superior)
if (
is.na(nivel_maximo) ||
nivel_maximo < nivel_objetivo
) {
return(NA_real_)
}
for (iteracion in seq_len(100)) {
medio <- (
inferior +
superior
) / 2
nivel_medio <- obtener_nivel(medio)
if (is.na(nivel_medio)) {
return(NA_real_)
}
if (nivel_medio >= nivel_objetivo) {
superior <- medio
} else {
inferior <- medio
}
if (
(superior - inferior) <
tolerancia
) {
break
}
}
superior
}
cortes_oficiales <- tidyr::crossing(
protocolos_capl,
age = 8:12,
sexo = c("boy", "girl"),
categorias_transicion
) %>%
rowwise() %>%
mutate(
punto_transicion_oficial =
buscar_transicion_capl(
edad = age,
sexo = sexo,
protocolo = protocolo,
nivel_objetivo = nivel_objetivo,
minimo = minimo,
maximo = maximo
)
) %>%
ungroup()
calcular_percentil_colombiano <- function(
familia,
corte,
mu,
sigma,
nu,
tau
) {
if (
is.na(corte) ||
is.na(mu) ||
is.na(sigma)
) {
return(NA_real_)
}
if (familia == "NO") {
prob <- gamlss.dist::pNO(
q = corte,
mu = mu,
sigma = sigma
)
} else if (familia == "BCCG") {
prob <- gamlss.dist::pBCCG(
q = corte,
mu = mu,
sigma = sigma,
nu = nu
)
} else if (familia == "BCPE") {
prob <- gamlss.dist::pBCPE(
q = corte,
mu = mu,
sigma = sigma,
nu = nu,
tau = tau
)
} else if (familia == "BEINF") {
prob <- gamlss.dist::pBEINF(
q = corte / 10,
mu = mu,
sigma = sigma,
nu = nu,
tau = tau
)
} else {
stop(
paste(
"Familia no reconocida:",
familia
)
)
}
pmin(
100,
pmax(
0,
as.numeric(prob) * 100
)
)
}
tabla_correspondencia_detallada <- tabla_parametros %>%
rename(
age = age,
sexo = sexo
) %>%
inner_join(
cortes_oficiales %>%
select(
variable,
sexo,
age,
dominio,
categoria_oficial,
punto_transicion_oficial
),
by = c(
"variable",
"sexo",
"age"
)
) %>%
rowwise() %>%
mutate(
percentil_colombiano =
calcular_percentil_colombiano(
familia = familia,
corte = punto_transicion_oficial,
mu = mu,
sigma = sigma,
nu = nu,
tau = tau
)
) %>%
ungroup()
tabla_correspondencia_resumen <- tabla_correspondencia_detallada %>%
group_by(
dominio,
categoria_oficial,
sexo
) %>%
summarise(
mediana = median(
percentil_colombiano,
na.rm = TRUE
),
P25 = quantile(
percentil_colombiano,
0.25,
na.rm = TRUE,
names = FALSE
),
P75 = quantile(
percentil_colombiano,
0.75,
na.rm = TRUE,
names = FALSE
),
.groups = "drop"
) %>%
arrange(
dominio,
categoria_oficial,
sexo
)
knitr::kable(
tabla_correspondencia_resumen,
digits = 1,
caption = "Correspondencia entre puntos de entrada oficiales y percentiles colombianos"
)
| dominio | categoria_oficial | sexo | mediana | P25 | P75 |
|---|---|---|---|---|---|
| CAPL-2 total | Achieving | boy | 75.2 | 61.0 | 83.9 |
| CAPL-2 total | Achieving | girl | 87.6 | 81.9 | 94.7 |
| CAPL-2 total | Excelling | boy | 95.3 | 88.1 | 97.8 |
| CAPL-2 total | Excelling | girl | 97.8 | 96.0 | 99.4 |
| CAPL-2 total | Progressing | boy | 6.2 | 3.2 | 8.2 |
| CAPL-2 total | Progressing | girl | 23.4 | 16.4 | 33.7 |
| Competencia física | Achieving | boy | 89.5 | 79.4 | 98.7 |
| Competencia física | Achieving | girl | 90.0 | 76.2 | 98.3 |
| Competencia física | Excelling | boy | 97.6 | 92.9 | 99.9 |
| Competencia física | Excelling | girl | 97.5 | 91.0 | 99.8 |
| Competencia física | Progressing | boy | 34.6 | 24.3 | 61.2 |
| Competencia física | Progressing | girl | 45.3 | 29.6 | 71.3 |
| Comportamiento diario | Achieving | boy | NA | NA | NA |
| Comportamiento diario | Achieving | girl | NA | NA | NA |
| Comportamiento diario | Excelling | boy | NA | NA | NA |
| Comportamiento diario | Excelling | girl | NA | NA | NA |
| Comportamiento diario | Progressing | boy | NA | NA | NA |
| Comportamiento diario | Progressing | girl | NA | NA | NA |
| Conocimiento y comprensión | Achieving | boy | 75.6 | 57.7 | 89.1 |
| Conocimiento y comprensión | Achieving | girl | 87.1 | 55.9 | 92.0 |
| Conocimiento y comprensión | Excelling | boy | 86.1 | 74.3 | 93.2 |
| Conocimiento y comprensión | Excelling | girl | 92.6 | 73.2 | 94.4 |
| Conocimiento y comprensión | Progressing | boy | 37.9 | 20.9 | 63.6 |
| Conocimiento y comprensión | Progressing | girl | 61.7 | 20.8 | 72.6 |
| Motivación y confianza | Achieving | boy | 41.0 | 38.0 | 43.6 |
| Motivación y confianza | Achieving | girl | 38.5 | 33.5 | 43.5 |
| Motivación y confianza | Excelling | boy | 63.0 | 59.1 | 65.7 |
| Motivación y confianza | Excelling | girl | 60.2 | 54.7 | 66.0 |
| Motivación y confianza | Progressing | boy | 2.8 | 2.4 | 3.2 |
| Motivación y confianza | Progressing | girl | 4.1 | 2.8 | 5.9 |
write.csv(
tabla_correspondencia_detallada,
"CAPL2_correspondencia_categorias_percentiles.csv",
row.names = FALSE,
fileEncoding = "UTF-8"
)
Los percentiles empíricos se estiman directamente de las
distribuciones observadas, utilizando
quantile(..., type = 7).
Para CAMSA se utiliza el menor tiempo registrado entre los dos ensayos disponibles.
variables_anexo <- c(
"edad",
"sexo",
"pacer_vueltas_20m",
"plancha_segundos",
"camsa_tiempo_ensayo1",
"camsa_tiempo_ensayo2",
"promedio_pasos"
)
faltan_anexo <- setdiff(
variables_anexo,
names(base_anexo)
)
if (length(faltan_anexo) > 0) {
stop(
paste(
"Faltan variables en Base_anexo:",
paste(faltan_anexo, collapse = ", ")
)
)
}
componentes <- base_anexo %>%
transmute(
edad = as.numeric(edad),
sexo = as.character(sexo),
pacer = as.numeric(pacer_vueltas_20m),
plancha = as.numeric(plancha_segundos),
camsa_1 = as.numeric(camsa_tiempo_ensayo1),
camsa_2 = as.numeric(camsa_tiempo_ensayo2),
pasos = as.numeric(promedio_pasos)
) %>%
mutate(
camsa_mejor = case_when(
is.na(camsa_1) &
is.na(camsa_2) ~ NA_real_,
is.na(camsa_1) ~ camsa_2,
is.na(camsa_2) ~ camsa_1,
TRUE ~ pmin(
camsa_1,
camsa_2
)
)
)
componentes_largo <- componentes %>%
select(
edad,
sexo,
pacer,
plancha,
camsa_mejor,
pasos
) %>%
pivot_longer(
cols = c(
pacer,
plancha,
camsa_mejor,
pasos
),
names_to = "variable",
values_to = "valor"
) %>%
mutate(
componente = case_when(
variable == "pacer" ~ "PACER",
variable == "plancha" ~ "Plancha",
variable == "camsa_mejor" ~ "CAMSA",
variable == "pasos" ~ "Pasos promedio diarios"
),
unidad = case_when(
variable == "pacer" ~ "Número de vueltas",
variable == "plancha" ~ "Segundos",
variable == "camsa_mejor" ~ "Segundos",
variable == "pasos" ~ "Pasos/día"
)
)
tabla_componentes_empiricos <- componentes_largo %>%
filter(
edad %in% 8:12
) %>%
group_by(
componente,
unidad,
sexo,
edad
) %>%
summarise(
n = sum(!is.na(valor)),
P5 = quantile(
valor,
0.05,
na.rm = TRUE,
type = 7,
names = FALSE
),
P25 = quantile(
valor,
0.25,
na.rm = TRUE,
type = 7,
names = FALSE
),
P50 = quantile(
valor,
0.50,
na.rm = TRUE,
type = 7,
names = FALSE
),
P75 = quantile(
valor,
0.75,
na.rm = TRUE,
type = 7,
names = FALSE
),
P95 = quantile(
valor,
0.95,
na.rm = TRUE,
type = 7,
names = FALSE
),
.groups = "drop"
) %>%
arrange(
componente,
sexo,
edad
)
knitr::kable(
tabla_componentes_empiricos,
digits = 2,
caption = "Percentiles empíricos de componentes específicos del CAPL-2"
)
| componente | unidad | sexo | edad | n | P5 | P25 | P50 | P75 | P95 |
|---|---|---|---|---|---|---|---|---|---|
| CAMSA | Segundos | Niña | 8 | 84 | 18.38 | 21.57 | 23.44 | 26.12 | 29.64 |
| CAMSA | Segundos | Niña | 9 | 84 | 18.94 | 21.69 | 23.37 | 24.84 | 27.98 |
| CAMSA | Segundos | Niña | 10 | 84 | 18.16 | 20.84 | 22.94 | 26.22 | 28.97 |
| CAMSA | Segundos | Niña | 11 | 85 | 18.41 | 20.36 | 23.45 | 26.54 | 28.10 |
| CAMSA | Segundos | Niña | 12 | 88 | 18.17 | 20.91 | 22.75 | 24.63 | 28.84 |
| CAMSA | Segundos | Niño | 8 | 84 | 17.76 | 19.94 | 22.55 | 25.21 | 27.48 |
| CAMSA | Segundos | Niño | 9 | 86 | 16.82 | 20.17 | 22.43 | 24.85 | 27.27 |
| CAMSA | Segundos | Niño | 10 | 87 | 18.28 | 20.91 | 23.38 | 25.68 | 28.71 |
| CAMSA | Segundos | Niño | 11 | 85 | 18.30 | 20.67 | 22.81 | 25.92 | 28.18 |
| CAMSA | Segundos | Niño | 12 | 76 | 17.77 | 20.18 | 22.20 | 24.90 | 28.10 |
| PACER | Número de vueltas | Niña | 8 | 84 | 8.00 | 13.00 | 18.00 | 25.00 | 34.70 |
| PACER | Número de vueltas | Niña | 9 | 84 | 8.15 | 13.00 | 20.00 | 26.00 | 32.85 |
| PACER | Número de vueltas | Niña | 10 | 84 | 8.00 | 15.75 | 23.00 | 30.25 | 40.70 |
| PACER | Número de vueltas | Niña | 11 | 85 | 12.00 | 17.00 | 24.00 | 30.00 | 40.60 |
| PACER | Número de vueltas | Niña | 12 | 88 | 13.00 | 18.75 | 25.50 | 31.00 | 44.30 |
| PACER | Número de vueltas | Niño | 8 | 84 | 10.15 | 14.00 | 21.00 | 27.25 | 38.55 |
| PACER | Número de vueltas | Niño | 9 | 86 | 10.25 | 14.25 | 22.00 | 28.00 | 40.75 |
| PACER | Número de vueltas | Niño | 10 | 87 | 9.90 | 18.00 | 22.00 | 29.00 | 41.70 |
| PACER | Número de vueltas | Niño | 11 | 85 | 11.20 | 18.00 | 23.00 | 30.00 | 42.80 |
| PACER | Número de vueltas | Niño | 12 | 76 | 12.00 | 18.00 | 26.00 | 34.00 | 47.25 |
| Pasos promedio diarios | Pasos/día | Niña | 8 | 84 | 6401.05 | 8058.00 | 9460.50 | 10336.00 | 12804.70 |
| Pasos promedio diarios | Pasos/día | Niña | 9 | 84 | 6637.05 | 8221.75 | 9395.50 | 10869.50 | 12952.95 |
| Pasos promedio diarios | Pasos/día | Niña | 10 | 84 | 6766.75 | 8777.50 | 9801.50 | 11044.00 | 13495.15 |
| Pasos promedio diarios | Pasos/día | Niña | 11 | 85 | 6923.60 | 8639.00 | 10085.00 | 11282.00 | 13253.20 |
| Pasos promedio diarios | Pasos/día | Niña | 12 | 88 | 7506.80 | 8871.75 | 10364.00 | 11342.75 | 13173.20 |
| Pasos promedio diarios | Pasos/día | Niño | 8 | 83 | 8597.00 | 10141.50 | 11141.00 | 12841.50 | 14366.30 |
| Pasos promedio diarios | Pasos/día | Niño | 9 | 85 | 8335.00 | 10499.00 | 11759.00 | 13038.00 | 15482.00 |
| Pasos promedio diarios | Pasos/día | Niño | 10 | 87 | 8121.00 | 10587.50 | 11830.00 | 12844.50 | 14428.90 |
| Pasos promedio diarios | Pasos/día | Niño | 11 | 85 | 8869.60 | 10371.00 | 11692.00 | 13066.00 | 15124.80 |
| Pasos promedio diarios | Pasos/día | Niño | 12 | 75 | 8726.80 | 10368.00 | 11613.00 | 13112.00 | 14659.10 |
| Plancha | Segundos | Niña | 8 | 84 | 24.25 | 40.65 | 54.10 | 63.75 | 81.18 |
| Plancha | Segundos | Niña | 9 | 84 | 30.41 | 44.50 | 54.05 | 66.30 | 78.85 |
| Plancha | Segundos | Niña | 10 | 84 | 33.46 | 50.15 | 59.10 | 71.97 | 86.16 |
| Plancha | Segundos | Niña | 11 | 85 | 38.52 | 48.10 | 61.40 | 68.70 | 82.24 |
| Plancha | Segundos | Niña | 12 | 88 | 38.64 | 50.67 | 62.70 | 70.55 | 89.68 |
| Plancha | Segundos | Niño | 8 | 84 | 34.93 | 47.13 | 60.70 | 70.23 | 82.49 |
| Plancha | Segundos | Niño | 9 | 86 | 38.02 | 45.20 | 58.35 | 72.10 | 91.70 |
| Plancha | Segundos | Niño | 10 | 87 | 33.60 | 52.35 | 62.20 | 71.20 | 84.69 |
| Plancha | Segundos | Niño | 11 | 85 | 34.90 | 52.90 | 60.30 | 71.80 | 95.54 |
| Plancha | Segundos | Niño | 12 | 76 | 37.95 | 55.25 | 64.55 | 76.30 | 90.35 |
write.csv(
tabla_componentes_empiricos,
"CAPL2_percentiles_empiricos_componentes.csv",
row.names = FALSE,
fileEncoding = "UTF-8"
)
control_final <- especificaciones %>%
select(
variable,
sexo,
familia,
n_esperado
) %>%
left_join(
tabla_modelos %>%
select(
variable,
sexo,
n,
familia_obtenida = familia,
convergio
),
by = c(
"variable",
"sexo"
)
) %>%
mutate(
n_correcto =
n == n_esperado,
familia_correcta =
familia ==
familia_obtenida
)
knitr::kable(
control_final,
caption = "Control de correspondencia de los modelos definitivos"
)
| variable | sexo | familia | n_esperado | n | familia_obtenida | convergio | n_correcto | familia_correcta |
|---|---|---|---|---|---|---|---|---|
| pc_score_final | boy | BCCG | 418 | 418 | BCCG | TRUE | TRUE | TRUE |
| pc_score_final | girl | BCCG | 425 | 425 | BCCG | TRUE | TRUE | TRUE |
| db_score_final | boy | BCPE | 415 | 415 | BCPE | TRUE | TRUE | TRUE |
| db_score_final | girl | NO | 425 | 425 | NO | TRUE | TRUE | TRUE |
| mc_score_final | boy | BCPE | 418 | 418 | BCPE | TRUE | TRUE | TRUE |
| mc_score_final | girl | BCPE | 424 | 424 | BCPE | TRUE | TRUE | TRUE |
| ku_score_final | boy | BEINF | 402 | 402 | BEINF | TRUE | TRUE | TRUE |
| ku_score_final | girl | BEINF | 413 | 413 | BEINF | TRUE | TRUE | TRUE |
| capl_total_final | boy | BCCG | 414 | 414 | BCCG | TRUE | TRUE | TRUE |
| capl_total_final | girl | BCCG | 424 | 424 | BCCG | TRUE | TRUE | TRUE |
if (
!all(control_final$n_correcto) ||
!all(control_final$familia_correcta) ||
!all(control_final$convergio)
) {
warning(
"Revise los modelos antes de publicar: existe alguna discrepancia en n, familia o convergencia."
)
}
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] parallel splines stats graphics grDevices utils datasets
## [8] methods base
##
## other attached packages:
## [1] knitr_1.50 capl_1.42 gamlss_5.5-0 nlme_3.1-168
## [5] gamlss.dist_6.1-1 gamlss.data_6.0-7 purrr_1.1.0 tibble_3.3.0
## [9] tidyr_1.3.1 dplyr_1.2.0 readxl_1.4.5
##
## loaded via a namespace (and not attached):
## [1] sass_0.4.10 generics_0.1.4 stringi_1.8.7 lattice_0.22-7
## [5] digest_0.6.37 magrittr_2.0.3 evaluate_1.0.4 grid_4.5.1
## [9] timechange_0.3.0 RColorBrewer_1.1-3 fastmap_1.2.0 cellranger_1.1.0
## [13] jsonlite_2.0.0 Matrix_1.7-3 writexl_1.5.4 survival_3.8-3
## [17] scales_1.4.0 jquerylib_0.1.4 cli_3.6.6 rlang_1.2.0
## [21] withr_3.0.2 cachem_1.1.0 yaml_2.3.10 tools_4.5.1
## [25] ggplot2_4.0.1 vctrs_0.7.1 R6_2.6.1 lifecycle_1.0.5
## [29] lubridate_1.9.4 stringr_1.5.1 MASS_7.3-65 pkgconfig_2.0.3
## [33] pillar_1.11.0 bslib_0.9.0 gtable_0.3.6 glue_1.8.0
## [37] xfun_0.55 tidyselect_1.2.1 rstudioapi_0.17.1 farver_2.1.2
## [41] htmltools_0.5.8.1 rmarkdown_2.29 compiler_4.5.1 S7_0.2.1