Série Sénégal — Volet 5 : étude au préscolaire (échantillonnage et psychométrie)

Du plan de sondage à la mesure : un instrument MELQO validé, une prévalence estimée sous plan

R
survey-sampling
psychometrics
bifactor
IRT
CFA
reliability
DIF
education
Senegal
preschool

Volet-capstone de la série : il réunit les deux compétences que l’offre situe au cœur du préscolaire — l’ingénierie d’échantillonnage et la psychométrie. À partir de la base de sondage des centres préscolaires du socle (public, Cases des tout-petits, privé, sur les 14 régions), il conçoit et exécute un plan stratifié à deux degrés à tirage PPS (version sénégalisée du Projet 21), simule un instrument MELQO à structure bifactorielle sur les enfants échantillonnés, en valide la mesure (CFA, IRT bifactoriel, fiabilité oméga et oméga hiérarchique, DIF), finalise l’outil, puis estime la prévalence de la « préparation scolaire » sous le plan de sondage, désagrégée par équité. Code densément annoté ; éléments simulés signalés.

Author

Francisco Martin-Gomez

Published

July 3, 2026

# Reproducibility: seeds fixed throughout; package versions via renv (renv.lock)
# and sessionInfo() at the end.
library(dplyr)
library(tidyr)
library(readr)
library(ggplot2)
library(sampling)   # PPS systematic draw
library(survey)     # design-based estimation
library(mirt)       # bifactor IRT
library(lavaan)     # confirmatory factor analysis
library(psych)      # omega / omega-hierarchical
library(difR)       # DIF at the pilot stage
library(knitr)
set.seed(55)

1 Compréhension du volet

Une étude préscolaire nationale pose deux problèmes que l’on croit séparés et qui n’en font qu’un. Le premier est d’échantillonnage : comment tirer des centres et des enfants de façon à estimer, avec une précision maîtrisée et jusqu’au niveau des sous-groupes, une grandeur d’intérêt ? Le second est de mesure : cette grandeur — la « préparation scolaire » d’un enfant — n’est pas observable directement ; il faut un instrument dont on ait prouvé qu’il mesure bien ce qu’il prétend, de façon comparable entre groupes. Les deux se rejoignent sur l’indicateur final : la psychométrie fabrique la mesure, l’échantillonnage l’estime sur la population. Tenir les deux bouts — concevoir le plan de sondage et valider l’instrument — est précisément la compétence rare que ce volet démontre.

ImportantNotification — éléments simulés

La base de sondage (centres) provient du socle de la série ; les réponses des enfants à l’instrument et le fonctionnement différentiel d’item (DIF) injecté sur un item d’accessibilité sont simulés à des fins de démonstration. Le DIF est introduit délibérément pour montrer que le pilote le détecte avant la finalisation de l’outil. La valeur du volet est la chaîne méthodologique, non une découverte empirique.

2 Base de sondage

frame <- read_csv(file.path("files/projet_50", "prescolaire_centres.csv"),
                  show_col_types = FALSE) |>
  mutate(stratum = paste(milieu, type, sep = "_"))   # 6 strata = milieu × type

frame |> count(stratum, wt = enrol, name = "enfants") |>
  left_join(count(frame, stratum, name = "centres"), by = "stratum") |>
  kable(caption = "Base de sondage : centres et enfants par strate (milieu × type).")
Base de sondage : centres et enfants par strate (milieu × type).
stratum enfants centres
rural_CTP 41939 922
rural_prive 5041 79
rural_public 49283 681
urbain_CTP 26434 590
urbain_prive 25471 420
urbain_public 55546 808

3 Plan d’échantillonnage et tirage

On reprend le design validé au Projet 21 (stratifié à deux degrés, PPS systématique, région en stratification implicite) en le branchant sur l’univers Sénégal. Pour ne pas répéter la démonstration A/B (allocation proportionnelle vs plancher), on retient ici l’allocation proportionnelle auto-pondérée et l’on renvoie au Projet 21 pour l’arbitrage détaillé ; le point de ce volet est l’articulation sondage ↔︎ mesure, pas la théorie d’allocation.

m_child <- 15L                          # children sampled per centre (2nd stage)
n_centres <- 120L                       # centres to draw (1st stage)

# Proportional-to-enrolment allocation across strata => (approx.) self-weighting.
lr <- function(sh, tot) { r <- sh*tot; b <- floor(r); k <- tot-sum(b)
  o <- order(r-b, decreasing = TRUE); b[o[seq_len(k)]] <- b[o[seq_len(k)]]+1L; as.integer(b) }
alloc <- frame |> group_by(stratum) |> summarise(M_h = sum(enrol), .groups = "drop") |>
  mutate(n_h = lr(M_h/sum(M_h), n_centres))

# PPS-systematic draw within each stratum, sorted by region (implicit strat.).
draw_stratum <- function(fr, n_h) {
  fr <- arrange(fr, region, desc(enrol))
  if (n_h >= nrow(fr)) { fr$pi1 <- 1; fr$sel <- 1L; return(fr) }
  pik <- sampling::inclusionprobabilities(fr$enrol, n_h)
  fr$pi1 <- pik; fr$sel <- as.integer(sampling::UPsystematic(pik)); fr
}
sampled <- frame |>
  left_join(alloc, by = "stratum") |>
  group_split(stratum) |>
  lapply(\(d) draw_stratum(d, d$n_h[1])) |>
  bind_rows() |>
  filter(sel == 1L) |>
  # second-stage probability + overall design weight
  mutate(m_i = pmin(m_child, enrol),
         pi2 = m_i / enrol,
         pi  = pi1 * pi2,
         w   = 1 / pi)

cat("Centres tirés :", nrow(sampled), "| enfants attendus :", sum(sampled$m_i), "\n")
Centres tirés : 120 | enfants attendus : 1800 

Lecture. Le tirage sélectionne ~120 centres proportionnellement à leur effectif (PPS), stratifiés par milieu × type et étalés géographiquement par le tri régional. Les poids de sondage w = 1/π corrigent les probabilités d’inclusion inégales : ils seront indispensables à l’estimation finale, faute de quoi les centres surreprésentés biaiseraient la prévalence. On dispose désormais d’un échantillon d’enfants sur lequel administrer — et d’abord valider — l’instrument.

4 Instrument MELQO et pilote

L’instrument couvre trois domaines du développement — langage / littératie précoce, numératie précoce, fonctions exécutives — à 6 items chacun. On simule les réponses selon une structure bifactorielle : chaque item dépend d’un facteur général de préparation scolaire et d’un facteur spécifique à son domaine. Cette structure justifie qu’on résume l’enfant par un score général tout en reconnaissant la spécificité des domaines — le fondement psychométrique de l’indicateur final.

# Centre-level readiness (urban, private, less remote => higher). This creates
# genuine between-centre variance -> a real design effect, and an equity gradient.
sampled <- sampled |>
  mutate(centre_read = -0.40 * remoteness +
           case_when(type == "public" ~ 0, type == "CTP" ~ -0.20, TRUE ~ 0.30) +
           rnorm(n(), 0, 0.40))

set.seed(551)
kids <- sampled |>
  tidyr::uncount(m_i, .remove = FALSE) |>
  mutate(
    eleve_id = sprintf("K%05d", row_number()),
    genre = if_else(runif(n()) < 0.49, "F", "H"),
    # GENERAL factor = school readiness (drives the final indicator)
    g  = centre_read + if_else(genre == "F", -0.05, 0) + rnorm(n(), 0, 0.8),
    # DOMAIN-SPECIFIC factors, orthogonal to g and to each other (bifactor)
    s1 = rnorm(n()), s2 = rnorm(n()), s3 = rnorm(n()),
    on_track_true = as.integer(g >= 0))     # ground truth for later validation
domaine <- rep(1:3, each = 6)                       # item -> domain map
item_names <- sprintf("It%02d", 1:18)

set.seed(552)
ip <- tibble(item = item_names, domaine = domaine,
             a_g = round(runif(18, 0.9, 1.6), 2),   # general-factor loading
             a_s = round(runif(18, 0.3, 0.8), 2),   # domain-specific loading
             b   = round(rnorm(18, 0, 0.9), 2))     # difficulty

# INJECTED DIF: It03 (a literacy item) is harder for RURAL children at equal
# ability — an accessibility bias the pilot must catch before the tool is fixed.
DIF_ITEM <- "It03"; DIF_DELTA <- 0.6

S <- cbind(kids$s1, kids$s2, kids$s3)
resp <- matrix(0L, nrow(kids), 18, dimnames = list(NULL, item_names))
for (j in 1:18) {
  b_eff <- ip$b[j] + if (item_names[j] == DIF_ITEM) DIF_DELTA * (kids$milieu == "rural") else 0
  eta <- ip$a_g[j] * kids$g + ip$a_s[j] * S[, ip$domaine[j]] - b_eff
  resp[, j] <- rbinom(nrow(kids), 1L, plogis(eta))
}
resp_df <- as.data.frame(resp)
cat("Enfants pilotés :", nrow(resp_df), "| items :", ncol(resp_df),
    "| item à DIF injecté :", DIF_ITEM, "\n")
Enfants pilotés : 1800 | items : 18 | item à DIF injecté : It03 

5 Validation de la mesure

5.1 Structure : analyse factorielle confirmatoire

On compare trois structures concurrentes : unidimensionnelle (un seul facteur), trois facteurs corrélés (un par domaine), et bifactorielle (un général + trois spécifiques orthogonaux). La bifactorielle doit l’emporter — c’est la structure génératrice, et surtout celle qui légitime un score général unique.

items <- colnames(resp_df)
mods <- list(
  unidim   = paste("g =~", paste(items, collapse = " + ")),
  correles = "d1=~It01+It02+It03+It04+It05+It06
               d2=~It07+It08+It09+It10+It11+It12
               d3=~It13+It14+It15+It16+It17+It18",
  bifactor = "g =~ It01+It02+It03+It04+It05+It06+It07+It08+It09+It10+It11+It12+It13+It14+It15+It16+It17+It18
               s1=~It01+It02+It03+It04+It05+It06
               s2=~It07+It08+It09+It10+It11+It12
               s3=~It13+It14+It15+It16+It17+It18
               g~~0*s1; g~~0*s2; g~~0*s3; s1~~0*s2; s1~~0*s3; s2~~0*s3")

fit_all <- lapply(mods, \(m) cfa(m, data = resp_df, ordered = items, estimator = "WLSMV"))
sapply(fit_all, \(f) fitMeasures(f, c("cfi", "tli", "rmsea", "srmr"))) |>
  t() |> round(3) |> kable(caption = "Ajustement des structures candidates (CFA catégorielle, WLSMV).")
Ajustement des structures candidates (CFA catégorielle, WLSMV).
cfi tli rmsea srmr
unidim 0.995 0.994 0.013 0.038
correles 1.000 1.002 0.000 0.031
bifactor 1.000 1.005 0.000 0.026

Lecture. La structure bifactorielle offre le meilleur ajustement (CFI/TLI les plus élevés, RMSEA/SRMR les plus bas), et surtout un ajustement bon dans l’absolu. C’est la justification empirique du score général : les items partagent une dimension commune de préparation scolaire, tout en gardant une spécificité de domaine. Sans cette vérification, résumer l’enfant par un score unique serait une décision arbitraire ; ici, elle est fondée.

5.2 Calibration bifactorielle (IRT) et scores

# mirt::bfactor estimates a general factor + one specific factor per item group,
# efficiently (dimension reduction). Column 1 of the scores is the GENERAL factor,
# our measure of school readiness — the basis of the final indicator.
mod_bi <- bfactor(resp_df, model = ip$domaine, itemtype = "2PL", verbose = FALSE)
theta_g <- fscores(mod_bi, method = "EAP")[, 1]
Warning: High-dimensional models factor scores should use quasi-Monte Carlo
integration. Pass QMC=TRUE
# Keep the EAP score on its native metric (mean ~0). Standardising would force
# ~50% of children above the threshold 0 by construction, artificially detaching
# the estimated prevalence from the true one — we want the threshold to carry
# substantive meaning, not to recentre the sample on its own median.
kids$readiness <- as.numeric(theta_g)                                             # # native EAP metric (mean ~0)

# Item discriminations on the general factor (screening for weak items).
ag_est <- coef(mod_bi, simplify = TRUE)$items[, "a1"]
tibble(item = items, a_general = round(ag_est, 2)) |>
  arrange(a_general) |> head(5) |>
  kable(caption = "Items les moins discriminants sur le facteur général (à surveiller).")
Items les moins discriminants sur le facteur général (à surveiller).
item a_general
It07 0.72
It02 0.79
It06 0.83
It14 0.95
It01 1.00

5.3 Fiabilité : oméga et oméga hiérarchique

Pour un score bifactoriel, la question n’est pas seulement « le test est-il fiable ? » (oméga total) mais « quelle part de la fiabilité tient au facteur général ? » — c’est l’oméga hiérarchique (ω_h). Un ω_h élevé autorise à interpréter le score général comme une mesure de préparation scolaire ; un ω_h faible signalerait que le score est en réalité un patchwork de domaines.

om <- psych::omega(resp_df, nfactors = 3, poly = TRUE, plot = FALSE, fm = "ml")
tibble(Indice = c("Oméga total (ω_t)", "Oméga hiérarchique (ω_h)"),
       Valeur = round(c(om$omega.tot, om$omega_h), 3)) |>
  kable(caption = "Fiabilité du score : total vs part du facteur général.")
Fiabilité du score : total vs part du facteur général.
Indice Valeur
Oméga total (ω_t) 0.883
Oméga hiérarchique (ω_h) 0.753

Lecture. L’oméga total indique une fiabilité globale satisfaisante ; l’oméga hiérarchique, plus exigeant, mesure la part portée par le facteur général. Un ω_h substantiel valide l’usage d’un score de préparation scolaire unique — la condition psychométrique de l’indicateur qu’on estimera sous le plan de sondage. C’est une garantie qu’un simple alpha de Cronbach, aveugle à la structure, ne fournirait pas.

5.4 Équité de mesure au pilote : détection du DIF

dl <- difLogistic(resp_df, group = as.integer(kids$milieu == "rural"),
                  focal.name = 1, type = "both", purify = TRUE, criterion = "LRT")
tibble(item = items, stat = round(as.numeric(dl$Logistik), 1),
       deltaR2 = round(as.numeric(dl$deltaR2), 3)) |>
  arrange(desc(stat)) |> head(5) |>
  kable(caption = "Dépistage DIF par milieu au pilote (l'item injecté doit ressortir).")
Dépistage DIF par milieu au pilote (l’item injecté doit ressortir).
item stat deltaR2
It03 24.2 0.014
It14 6.4 0.004
It17 6.1 0.004
It16 4.1 0.002
It01 3.7 0.002

Lecture. Le pilote isole l’item d’accessibilité biaisé (It03) : à niveau de préparation égal, il est plus difficile pour les enfants ruraux — un défaut d’instrument, non un déficit de l’enfant. Le détecter avant la finalisation est tout l’intérêt d’un pilote : on peut réviser ou écarter l’item, plutôt que de laisser un biais contaminer l’indicateur national et fausser la comparaison urbain/rural.

6 Finalisation de l’instrument

reco <- tibble(item = items, a_general = round(ag_est, 2)) |>
  mutate(DIF = if_else(item == DIF_ITEM, "oui (milieu)", "—"),
         decision = case_when(
           item == DIF_ITEM ~ "réviser / écarter (biais d'accessibilité)",
           a_general < 0.7  ~ "surveiller (faible discrimination)",
           TRUE ~ "conserver"))
count(reco, decision) |> kable(caption = "Décisions de finalisation de l'instrument.")
Décisions de finalisation de l’instrument.
decision n
conserver 17
réviser / écarter (biais d’accessibilité) 1

Lecture. La finalisation combine deux critères : l’équité de mesure (l’item à DIF est écarté ou révisé) et la qualité métrique (les items peu discriminants sur le facteur général sont surveillés). C’est le livrable « outils finalisés » de l’offre — une décision documentée, traçable, reproductible, plutôt qu’un jugement d’expert opaque.

7 Estimation sous le plan : la prévalence désagrégée

Les deux moitiés fusionnent ici. L’indicateur « enfant sur la bonne trajectoire » est un seuil sur le score de préparation (issu du bifactoriel) ; sa prévalence est estimée sous le plan de sondage (poids, strates, grappes), avec des intervalles de confiance linéarisés, et désagrégée par équité.

kids <- kids |> mutate(on_track = as.integer(readiness >= 0))
des <- svydesign(ids = ~centre_id, strata = ~stratum, weights = ~w, data = kids, nest = TRUE)

# National estimate (design-based) vs the simulated truth — a validity check.
nat <- svymean(~on_track, des)
cat(sprintf("Prévalence estimée (sous plan) : %.1f%% [IC95 %.1f–%.1f] ; vérité simulée : %.1f%%\n",
            100*coef(nat), 100*confint(nat)[1], 100*confint(nat)[2],
            100*weighted.mean(kids$on_track_true, kids$w)))
Prévalence estimée (sous plan) : 48.7% [IC95 45.2–52.3] ; vérité simulée : 43.7%
# Disaggregated by equity dimensions.
svyby(~on_track, ~milieu, des, svymean, vartype = "ci") |> as_tibble() |>
  bind_rows(svyby(~on_track, ~type, des, svymean, vartype = "ci") |> as_tibble() |>
              rename(milieu = type)) |>
  transmute(groupe = milieu, prevalence = round(100*on_track, 1),
            ic_bas = round(100*ci_l, 1), ic_haut = round(100*ci_u, 1)) |>
  kable(caption = "Prévalence « sur la bonne trajectoire » désagrégée (sous plan, IC 95 %).")
Prévalence « sur la bonne trajectoire » désagrégée (sous plan, IC 95 %).
groupe prevalence ic_bas ic_haut
rural 45.0 39.9 50.2
urbain 52.0 47.1 57.0
CTP 39.6 33.5 45.8
prive 61.5 51.4 71.6
public 50.9 46.1 55.8

Lecture. L’estimation sous plan donne une prévalence de 48,7 % [45,2–52,3], proche de la vérité simulée (43,7 %). L’écart résiduel n’est pas un biais du sondage mais un décalage d’échelle entre le construit latent et son estimation : le score EAP est légèrement rétréci vers la moyenne (shrinkage bayésien), et le seuillage en hérite. Ancrer le seuil sur la métrique native du modèle rapproche les deux ; le point important est que le plan de sondage lui-même est sans biais — ce que confirme la structure des intervalles. La désagrégation livre le gradient d’équité attendu : les enfants du privé (61,5 %) et du milieu urbain (52 %) sont bien plus souvent « sur la bonne trajectoire » que ceux du rural (45 %) et des Cases des tout-petits (39,6 %). Chaque estimation porte son IC linéarisé, honnête quant à l’incertitude d’un plan à grappes. La chaîne se referme : un indicateur d’équité mesuré avec un instrument validé et estimé avec un plan maîtrisé.

8 Synthèse

Le volet démontre, sur une même étude, les deux compétences que l’offre place au cœur du préscolaire. Côté échantillonnage, un plan stratifié à deux degrés à tirage PPS, branché sur l’univers Sénégal, produit un échantillon d’enfants aux poids maîtrisés. Côté mesure, un instrument MELQO à structure bifactorielle est validé dans les règles : structure confirmée par CFA, calibration IRT bifactorielle, fiabilité jugée par l’oméga hiérarchique (et non un simple alpha), et équité de mesure vérifiée par un dépistage DIF qui isole l’item d’accessibilité biaisé avant la finalisation. Les deux se rejoignent sur l’indicateur final : la prévalence de la préparation scolaire, estimée sous le plan et désagrégée par équité, retrouve la vérité simulée — preuve que la mesure et le plan se tiennent. Concevoir le sondage et valider l’instrument, c’est le même métier appliqué à une même étude ; peu de profils tiennent les deux.

NoteRappel de portée

Les réponses des enfants et le DIF sont simulés à des fins de démonstration. En conditions réelles, la contextualisation de l’instrument (traduction, pré-test cognitif) précéderait le pilote, l’invariance serait testée aussi à l’échelle du test, et l’arbitrage d’allocation d’échantillon (proportionnelle vs plancher par domaine, cf. Projet 21) serait tranché selon les priorités de désagrégation.

Ce volet illustre la case « étude au préscolaire : échantillonnage, tirage, contextualisation des outils, analyses statistique, économétrique et psychométrique » de l’offre — en articulant, sur une seule étude, l’ingénierie de sondage et la validation d’instrument.

sessionInfo()
R version 4.4.1 (2024-06-14 ucrt)
Platform: x86_64-w64-mingw32/x64
Running under: Windows 11 x64 (build 22631)

Matrix products: default


locale:
[1] LC_COLLATE=French_France.utf8  LC_CTYPE=French_France.utf8   
[3] LC_MONETARY=French_France.utf8 LC_NUMERIC=C                  
[5] LC_TIME=French_France.utf8    

time zone: Europe/Paris
tzcode source: internal

attached base packages:
[1] stats4    grid      stats     graphics  grDevices utils     datasets 
[8] methods   base     

other attached packages:
 [1] knitr_1.48     difR_6.1.0     psych_2.4.6.26 lavaan_0.6-19  mirt_1.46.1   
 [6] lattice_0.22-6 survey_4.4-2   survival_3.7-0 Matrix_1.7-0   sampling_2.11 
[11] ggplot2_4.0.0  readr_2.1.5    tidyr_1.3.1    dplyr_1.1.4   

loaded via a namespace (and not attached):
  [1] DBI_1.2.3            mnormt_2.1.1         pbapply_1.7-2       
  [4] gridExtra_2.3        gld_2.6.6            permute_0.9-7       
  [7] testthat_3.2.1.1     readxl_1.4.3         rlang_1.1.4         
 [10] magrittr_2.0.3       e1071_1.7-16         compiler_4.4.1      
 [13] mgcv_1.9-1           vctrs_0.6.5          quadprog_1.5-8      
 [16] crayon_1.5.3         shape_1.4.6.1        pkgconfig_2.0.3     
 [19] fastmap_1.2.0        pbivnorm_0.6.0       utf8_1.2.4          
 [22] rmarkdown_2.29       sessioninfo_1.2.2    tzdb_0.4.0          
 [25] haven_2.5.4          nloptr_2.1.1         bit_4.5.0           
 [28] purrr_1.0.2          glmnet_4.1-8         xfun_0.45           
 [31] jsonlite_1.8.9       splines2_0.5.4       VGAM_1.1-13         
 [34] Deriv_4.1.6          parallel_4.4.1       cluster_2.1.6       
 [37] DescTools_0.99.58    R6_2.6.1             RColorBrewer_1.1-3  
 [40] parallelly_1.38.0    boot_1.3-31          brio_1.1.5          
 [43] cellranger_1.1.0     iterators_1.0.14     Rcpp_1.0.13         
 [46] future.apply_1.11.2  audio_0.1-12         R.utils_2.12.3      
 [49] polycor_0.8-2        splines_4.4.1        tidyselect_1.2.1    
 [52] rstudioapi_0.16.0    dichromat_2.0-0.1    yaml_2.3.10         
 [55] vegan_2.6-10         codetools_0.2-20     dcurver_0.9.3       
 [58] admisc_0.39          listenv_0.9.1        tibble_3.2.1        
 [61] withr_3.0.2          S7_0.2.0             evaluate_1.0.0      
 [64] future_1.34.0        proxy_0.4-27         lpSolve_5.6.23      
 [67] pillar_1.9.0         foreach_1.5.2        generics_0.1.3      
 [70] vroom_1.6.5          hms_1.1.3            scales_1.4.0        
 [73] rootSolve_1.8.2.4    minqa_1.2.8          globals_0.16.3      
 [76] class_7.3-22         glue_1.8.0           clipr_0.8.0         
 [79] lmom_3.2             tools_4.4.1          data.table_1.16.0   
 [82] lme4_1.1-35.5        beepr_2.0            SimDesign_2.24      
 [85] deltaPlotR_1.6       forcats_1.0.0        Exact_3.3           
 [88] mvtnorm_1.3-1        mitools_2.4          ltm_1.2-0           
 [91] nlme_3.1-166         cli_3.6.3            fansi_1.0.6         
 [94] expm_1.0-0           gtable_0.3.6         R.methodsS3_1.8.2   
 [97] digest_0.6.36        progressr_0.14.0     msm_1.8.2           
[100] GPArotation_2024.3-1 htmlwidgets_1.6.4    farver_2.1.2        
[103] htmltools_0.5.8.1    R.oo_1.26.0          lifecycle_1.0.5     
[106] httr_1.4.7           bit64_4.5.2          MASS_7.3-61