1 Pendahuluan

Dokumen ini adalah bahan praktik workshop. Alurnya mengikuti kerangka from data to action: data mentah diolah menjadi informasi (z-score dan prevalensi), informasi diuji menjadi bukti (hubungan faktor risiko), lalu bukti dibaca untuk menyusun usulan tindakan.

Data yang dipakai adalah data simulasi 800 balita dari 16 kecamatan fiktif. Hasil di bawah tidak menggambarkan wilayah mana pun. Semua angka dalam narasi dihitung langsung dari data, sehingga bila berkas data diganti dengan data Anda sendiri dan dokumen dirender ulang, narasinya ikut berubah.

Variabel dalam data:

Variabel Keterangan
id nomor identitas balita
kode_kec, kecamatan wilayah tempat tinggal
jk jenis kelamin (1 = laki-laki, 2 = perempuan)
umur_bln umur dalam bulan (0–59)
tb_cm, bb_kg tinggi/panjang badan (cm) dan berat badan (kg)
cara_ukur L = telentang, H = berdiri
bblr riwayat berat badan lahir rendah (Ya/Tidak)
asi_eks mendapat ASI eksklusif (Ya/Tidak)
pend_ibu pendidikan terakhir ibu
sanitasi akses sanitasi rumah tangga (Layak/Tidak layak)

2 Impor dan pemeriksaan data

balita_mentah <- read_csv("data_stunting.csv", show_col_types = FALSE)
glimpse(balita_mentah)
## Rows: 802
## Columns: 12
## $ id        <chr> "B0001", "B0002", "B0003", "B0004", "B0005", "B0006", "B0007…
## $ kode_kec  <chr> "K16", "K03", "K12", "K03", "K03", "K10", "K13", "K04", "K08…
## $ kecamatan <chr> "Kecamatan P", "Kecamatan C", "Kecamatan L", "Kecamatan C", …
## $ jk        <dbl> 2, 1, 1, 2, 2, 1, 1, 2, 2, 2, 1, 2, 1, 1, 1, 1, 2, 2, 1, 1, …
## $ umur_bln  <dbl> 2, 37, 8, 14, 29, 53, 34, 48, 46, 15, 36, 56, 22, 29, 21, 32…
## $ tb_cm     <dbl> 48.0, 83.7, 68.6, 77.8, 82.8, 98.0, 94.1, 93.0, 100.0, 71.9,…
## $ bb_kg     <dbl> 3.9, 8.9, 9.2, 9.4, 12.6, 14.9, 14.2, 12.6, 14.7, 7.8, 12.1,…
## $ cara_ukur <chr> "L", "H", "L", "L", "H", "H", "H", "H", "H", "L", "H", "H", …
## $ bblr      <chr> "Ya", "Ya", "Tidak", "Tidak", "Tidak", "Tidak", "Tidak", "Ti…
## $ asi_eks   <chr> "Tidak", "Tidak", "Ya", "Tidak", "Ya", "Tidak", "Ya", "Tidak…
## $ pend_ibu  <chr> "SD ke bawah", "SMP", "SMA", "SMA", "SD ke bawah", "SMA", "S…
## $ sanitasi  <chr> "Tidak layak", "Tidak layak", "Tidak layak", "Layak", "Tidak…

Sebelum analisis, ada empat hal yang diperiksa: data hilang, data ganda, nilai yang tidak masuk akal, dan kesesuaian cara ukur dengan umur.

# data hilang
balita_mentah |>
  summarise(across(everything(), ~ sum(is.na(.x)))) |>
  pivot_longer(everything(), names_to = "variabel", values_to = "n_hilang") |>
  filter(n_hilang > 0)
variabel n_hilang
tb_cm 3
asi_eks 2
# ID ganda
balita_mentah |> count(id) |> filter(n > 1)
id n
B0015 2
B0230 2
# tinggi badan mustahil untuk balita
balita_mentah |> filter(tb_cm < 40 | tb_cm > 130) |>
  select(id, umur_bln, tb_cm)
id umur_bln tb_cm
B0120 20 8.2
B0509 48 315.0
# cara ukur menurut umur
balita_mentah |> count(cara_ukur, dibawah_24_bln = umur_bln < 24)
cara_ukur dibawah_24_bln n
H FALSE 490
L TRUE 312
balita_bersih <- balita_mentah |>
  distinct() |>
  mutate(tb_cm = if_else(tb_cm < 40 | tb_cm > 130, NA_real_, tb_cm)) |>
  filter(!is.na(tb_cm), between(umur_bln, 0, 59))

Dari 802 baris data awal, tersisa 795 baris setelah data ganda, tinggi badan yang tidak masuk akal, dan tinggi badan yang kosong dikeluarkan. Dalam pekerjaan nyata, nilai yang mencurigakan sebaiknya dikonfirmasi ke formulir atau petugas pengukur sebelum dibuang.

3 Z-score TB/U dan status stunting

Tinggi badan mentah tidak bisa dibandingkan antaranak karena umur dan jenis kelaminnya berbeda. Karena itu tinggi badan diubah menjadi z-score tinggi badan menurut umur (TB/U) dengan acuan standar pertumbuhan WHO 2006 melalui paket anthro.

z <- with(balita_bersih, anthro_zscores(
  sex = jk, age = umur_bln, is_age_in_month = TRUE,
  weight = bb_kg, lenhei = tb_cm, measure = cara_ukur))

balita_z <- balita_bersih |>
  mutate(haz = z$zlen, flag_haz = z$flen)

balita_z |> count(flag_haz)
flag_haz n
0 795

Nilai dengan flag_haz = 1 (|z| > 6) secara biologis hampir mustahil dan dikeluarkan dari analisis.

balita <- balita_z |>
  filter(flag_haz == 0) |>
  mutate(
    status_tbu = cut(haz, breaks = c(-Inf, -3, -2, Inf), right = FALSE,
                     labels = c("Sangat pendek", "Pendek", "Normal")),
    stunting   = factor(if_else(haz < -2, "Ya", "Tidak"), levels = c("Tidak", "Ya")),
    jk         = factor(jk, levels = 1:2, labels = c("Laki-laki", "Perempuan")),
    kel_umur   = cut(umur_bln, breaks = c(0, 6, 12, 24, 36, 48, 60), right = FALSE,
                     labels = c("0-5", "6-11", "12-23", "24-35", "36-47", "48-59")),
    bblr       = factor(bblr,     levels = c("Tidak", "Ya")),
    asi_eks    = factor(asi_eks,  levels = c("Ya", "Tidak")),
    sanitasi   = factor(sanitasi, levels = c("Layak", "Tidak layak")),
    pend_ibu   = factor(pend_ibu, levels = c("Perguruan tinggi", "SMA", "SMP", "SD ke bawah"))
  )

balita |> count(status_tbu) |> mutate(persen = round(100 * n / sum(n), 1))
status_tbu n persen
Sangat pendek 60 7.5
Pendek 149 18.7
Normal 586 73.7
ggplot(balita, aes(haz)) +
  geom_histogram(binwidth = 0.25, fill = warna_normal, colour = "white") +
  geom_vline(xintercept = c(-3, -2), linetype = "dashed", colour = warna_stunting) +
  labs(title = "Sebaran z-score TB/U",
       subtitle = "Garis putus-putus: batas -3 SD dan -2 SD",
       x = "z-score TB/U", y = "Jumlah balita")

4 Besar masalah dan distribusinya

uji_prev <- binom.test(sum(balita$stunting == "Ya"), nrow(balita))
prev_total <- 100 * unname(uji_prev$estimate)
prev_ik    <- 100 * uji_prev$conf.int

prev_per <- function(data, ...) {
  data |>
    group_by(...) |>
    summarise(n = n(), kasus = sum(stunting == "Ya"), .groups = "drop") |>
    mutate(prevalensi = 100 * kasus / n,
           ik = map2(kasus, n, \(k, m) 100 * binom.test(k, m)$conf.int),
           ik_bawah = map_dbl(ik, 1),
           ik_atas  = map_dbl(ik, 2)) |>
    select(-ik)
}

prev_kec  <- prev_per(balita, kode_kec, kecamatan) |> arrange(desc(prevalensi))
prev_pend <- prev_per(balita, pend_ibu)

Prevalensi stunting pada 795 balita adalah 26,3% (IK 95%: 23,3–29,5%). Menurut pendidikan ibu, prevalensi terendah ada pada kelompok Perguruan tinggi (14,4%) dan tertinggi pada kelompok SD ke bawah (38,8%).

4.1 Karakteristik balita menurut status stunting

balita |>
  select(stunting, jk, kel_umur, pend_ibu, asi_eks, sanitasi, bblr) |>
  tbl_summary(
    by = stunting, percent = "row",
    label = list(jk ~ "Jenis kelamin", kel_umur ~ "Kelompok umur (bulan)",
                 pend_ibu ~ "Pendidikan ibu", asi_eks ~ "ASI eksklusif",
                 sanitasi ~ "Sanitasi", bblr ~ "BBLR"),
    missing_text = "Tidak tercatat"
  ) |>
  add_overall() |>
  add_p() |>
  modify_spanning_header(c("stat_1", "stat_2") ~ "**Stunting**")
Characteristic Overall
N = 795
1
Stunting
p-value2
Tidak
N = 586
1
Ya
N = 209
1
Jenis kelamin


0.093
    Laki-laki 394 (100%) 280 (71%) 114 (29%)
    Perempuan 401 (100%) 306 (76%) 95 (24%)
Kelompok umur (bulan)


<0.001
    0-5 79 (100%) 40 (51%) 39 (49%)
    6-11 76 (100%) 54 (71%) 22 (29%)
    12-23 154 (100%) 111 (72%) 43 (28%)
    24-35 169 (100%) 128 (76%) 41 (24%)
    36-47 162 (100%) 128 (79%) 34 (21%)
    48-59 155 (100%) 125 (81%) 30 (19%)
Pendidikan ibu


<0.001
    Perguruan tinggi 97 (100%) 83 (86%) 14 (14%)
    SMA 315 (100%) 246 (78%) 69 (22%)
    SMP 223 (100%) 159 (71%) 64 (29%)
    SD ke bawah 160 (100%) 98 (61%) 62 (39%)
ASI eksklusif


<0.001
    Ya 400 (100%) 329 (82%) 71 (18%)
    Tidak 393 (100%) 255 (65%) 138 (35%)
    Tidak tercatat 2 2 0
Sanitasi


<0.001
    Layak 581 (100%) 454 (78%) 127 (22%)
    Tidak layak 214 (100%) 132 (62%) 82 (38%)
BBLR


<0.001
    Tidak 720 (100%) 552 (77%) 168 (23%)
    Ya 75 (100%) 34 (45%) 41 (55%)
1 n (%)
2 Pearson’s Chi-squared test

Persentase dalam tabel adalah persentase baris, sehingga kolom “Ya” dapat dibaca langsung sebagai prevalensi stunting pada tiap kelompok.

5 Faktor yang berhubungan dengan stunting

5.1 Analisis bivariat

faktor <- c("bblr", "asi_eks", "sanitasi", "pend_ibu")

map_dfr(faktor, \(v) {
  u <- chisq.test(table(balita[[v]], balita$stunting))
  tibble(variabel = v, chisq = round(unname(u$statistic), 2),
         df = unname(u$parameter), p_value = signif(u$p.value, 3))
})
variabel chisq df p_value
bblr 32.82 1 0.00e+00
asi_eks 29.91 1 0.00e+00
sanitasi 21.02 1 4.50e-06
pend_ibu 23.65 3 2.96e-05

5.2 Regresi logistik biner

model <- glm(stunting ~ bblr + asi_eks + sanitasi + pend_ibu,
             data = balita, family = binomial)

tabel_or <- tbl_regression(
  model, exponentiate = TRUE,
  label = list(bblr ~ "BBLR", asi_eks ~ "ASI eksklusif",
               sanitasi ~ "Sanitasi", pend_ibu ~ "Pendidikan ibu"))

tabel_or_kasar <- balita |>
  select(stunting, all_of(faktor)) |>
  tbl_uvregression(method = glm, y = stunting,
                   method.args = list(family = binomial),
                   exponentiate = TRUE,
                   label = list(bblr ~ "BBLR", asi_eks ~ "ASI eksklusif",
                                sanitasi ~ "Sanitasi", pend_ibu ~ "Pendidikan ibu"))

tbl_merge(list(tabel_or_kasar, tabel_or),
          tab_spanner = c("**OR kasar**", "**OR terkoreksi**"))
Characteristic
OR kasar
OR terkoreksi
N OR 95% CI p-value OR 95% CI p-value
BBLR 795





    Tidak
— —
— —
    Ya
3.96 2.44, 6.47 <0.001 4.07 2.44, 6.84 <0.001
ASI eksklusif 793





    Ya
— —
— —
    Tidak
2.51 1.81, 3.50 <0.001 2.32 1.64, 3.31 <0.001
Sanitasi 795





    Layak
— —
— —
    Tidak layak
2.22 1.58, 3.11 <0.001 2.29 1.59, 3.28 <0.001
Pendidikan ibu 795





    Perguruan tinggi
— —
— —
    SMA
1.66 0.91, 3.22 0.11 1.56 0.82, 3.14 0.2
    SMP
2.39 1.30, 4.66 0.007 2.18 1.13, 4.46 0.025
    SD ke bawah
3.75 2.01, 7.42 <0.001 3.46 1.78, 7.12 <0.001
Abbreviations: CI = Confidence Interval, OR = Odds Ratio
or_adj <- tidy(model, exponentiate = TRUE, conf.int = TRUE) |>
  filter(term != "(Intercept)")

label_term <- c(bblrYa = "BBLR",
                asi_eksTidak = "Tidak ASI eksklusif",
                `sanitasiTidak layak` = "Sanitasi tidak layak",
                pend_ibuSMA = "Ibu SMA",
                pend_ibuSMP = "Ibu SMP",
                `pend_ibuSD ke bawah` = "Ibu SD ke bawah")

or_adj |>
  mutate(label = factor(label_term[term], levels = rev(label_term))) |>
  ggplot(aes(estimate, label)) +
  geom_vline(xintercept = 1, linetype = "dashed", colour = "grey50") +
  geom_errorbar(aes(xmin = conf.low, xmax = conf.high), width = 0.2,
                orientation = "y", colour = "grey25") +
  geom_point(colour = warna_stunting, size = 3.2) +
  geom_text(aes(label = sprintf("%.2f (%.2f-%.2f)", estimate, conf.low, conf.high)),
            vjust = -1.1, size = 3.2, colour = "grey25") +
  scale_x_log10(breaks = c(0.5, 1, 2, 4, 8)) +
  labs(title = "Faktor yang berhubungan dengan stunting",
       subtitle = "Odds ratio terkoreksi dan IK 95% (skala log)",
       x = "Odds ratio", y = NULL)

or_bblr <- or_adj |> filter(term == "bblrYa")

Setelah dikontrol oleh variabel lain dalam model, odds stunting pada balita dengan riwayat BBLR sekitar 4,07 kali odds pada balita dengan berat lahir normal (IK 95%: 2,44–6,84). Faktor lain dibaca dengan cara yang sama, selalu terhadap kategori pembandingnya: ASI eksklusif “Ya”, sanitasi “Layak”, dan ibu berpendidikan perguruan tinggi.

5.3 Pemeriksaan model

# Multikolinearitas
nilai_vif <- vif(model)
nilai_vif
##              GVIF Df GVIF^(1/(2*Df))
## bblr     1.013411  1        1.006683
## asi_eks  1.027036  1        1.013428
## sanitasi 1.013969  1        1.006960
## pend_ibu 1.041314  3        1.006770
gvif_maks <- max(nilai_vif[, "GVIF^(1/(2*Df))"])

# Hosmer-Lemeshow
hosmer_lemeshow <- function(y, p, g = 10) {
  kelompok <- cut(p, breaks = unique(quantile(p, probs = seq(0, 1, length.out = g + 1))),
                  include.lowest = TRUE)
  amatan  <- tapply(y, kelompok, sum)
  harapan <- tapply(p, kelompok, sum)
  n_kel   <- tapply(y, kelompok, length)
  stat <- sum((amatan - harapan)^2 / (harapan * (1 - harapan / n_kel)))
  df   <- length(n_kel) - 2
  tibble(chisq = stat, df = df, p_value = pchisq(stat, df, lower.tail = FALSE))
}
hl <- hosmer_lemeshow(as.integer(model$model$stunting == "Ya"), fitted(model))
hl
chisq df p_value
7.055766 7 0.4230967
# AUC
kurva_roc <- roc(model$model$stunting, fitted(model),
                 levels = c("Tidak", "Ya"), direction = "<", quiet = TRUE)
ik_auc <- ci.auc(kurva_roc)

Nilai GVIF^(1/(2·Df)) terbesar adalah 1,01; angka ini jauh dari batas 2,2 sehingga multikolinearitas tidak menjadi masalah. Uji Hosmer-Lemeshow menghasilkan p = 0,423, sehingga tidak ada bukti bahwa model kurang cocok dengan data. AUC sebesar 0,723 (IK 95%: 0,684–0,762) menunjukkan kemampuan model membedakan balita stunting dan tidak stunting. Model ini dibuat untuk mencari faktor yang berhubungan, bukan untuk menapis anak satu per satu, sehingga AUC tidak perlu sangat tinggi.

6 Rasio prevalensi

Stunting bukan kejadian langka. Pada prevalensi di atas 10%, odds ratio akan tampak lebih besar daripada rasio prevalensi (RP). Regresi Poisson dengan galat baku robust memberi RP yang lebih mudah dikomunikasikan kepada pengelola program.

model_rp <- glm(I(stunting == "Ya") ~ bblr + asi_eks + sanitasi + pend_ibu,
                data = balita, family = poisson(link = "log"))
vc_robust <- vcovHC(model_rp, type = "HC0")
uji_rp <- coeftest(model_rp, vcov. = vc_robust)
ik_rp  <- coefci(model_rp, vcov. = vc_robust)

rasio_prevalensi <- tibble(
  term     = rownames(uji_rp),
  rp       = exp(uji_rp[, "Estimate"]),
  ik_bawah = exp(ik_rp[, 1]),
  ik_atas  = exp(ik_rp[, 2]),
  p_value  = uji_rp[, "Pr(>|z|)"]
) |>
  filter(term != "(Intercept)")

perbandingan <- or_adj |>
  select(term, or = estimate) |>
  left_join(rasio_prevalensi |> select(term, rp, ik_bawah, ik_atas), by = "term") |>
  mutate(faktor = label_term[term], .before = 1) |>
  select(-term) |>
  mutate(across(where(is.numeric), \(x) round(x, 2)))
perbandingan
faktor or rp ik_bawah ik_atas
BBLR 4.07 2.20 1.73 2.80
Tidak ASI eksklusif 2.32 1.79 1.40 2.28
Sanitasi tidak layak 2.29 1.69 1.35 2.11
Ibu SMA 1.56 1.43 0.86 2.38
Ibu SMP 2.18 1.79 1.07 2.98
Ibu SD ke bawah 3.46 2.37 1.43 3.93
rp_bblr <- rasio_prevalensi |> filter(term == "bblrYa")

Untuk BBLR, OR = 4,07 sedangkan RP = 2,20. Kalimat untuk laporan program: prevalensi stunting pada balita dengan riwayat BBLR sekitar 2,2 kali prevalensi pada balita yang lahir dengan berat normal.

7 Dari bukti ke tindakan

Faktor yang tetap berhubungan bermakna setelah dikontrol satu sama lain, diurutkan dari RP terbesar: Ibu SD ke bawah, BBLR, Tidak ASI eksklusif, Ibu SMP, Sanitasi tidak layak. Usulan tindakan dapat disusun dengan menelusuri setiap faktor tersebut:

Temuan Contoh tindakan Jenis intervensi
BBLR ANC berkualitas, tablet tambah darah, pemantauan gizi ibu hamil Spesifik
Tidak ASI eksklusif Konseling menyusui di posyandu, dukungan laktasi di tempat kerja Spesifik
Sanitasi tidak layak Koordinasi program air minum dan sanitasi di desa prioritas Sensitif
Pendidikan ibu rendah Edukasi gizi dengan media yang mudah dipahami Sensitif
Kecamatan dengan prevalensi tertinggi Prioritas pendampingan dan alokasi sumber daya Penargetan

Beberapa batasan yang perlu disampaikan bersama hasil ini:

  1. Desain potong lintang hanya menunjukkan hubungan, bukan sebab-akibat.
  2. Variabel yang tidak diukur (misalnya penyakit infeksi, tinggi badan orang tua, pendapatan) bisa ikut memengaruhi hasil.
  3. Prevalensi tingkat kecamatan dihitung dari sampel kecil sehingga kurang stabil.

8 Informasi sesi

sessionInfo()
## R version 4.6.1 (2026-06-24 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=English_Indonesia.utf8  LC_CTYPE=English_Indonesia.utf8   
## [3] LC_MONETARY=English_Indonesia.utf8 LC_NUMERIC=C                      
## [5] LC_TIME=English_Indonesia.utf8    
## 
## time zone: Asia/Makassar
## tzcode source: internal
## 
## attached base packages:
## [1] stats     graphics  grDevices utils     datasets  methods   base     
## 
## other attached packages:
##  [1] car_3.1-5       carData_3.0-6   pROC_1.19.0.1   lmtest_0.9-40  
##  [5] zoo_1.9-0       sandwich_3.1-2  sf_1.1-2        broom_1.0.13   
##  [9] gtsummary_2.6.1 anthro_1.1.0    lubridate_1.9.5 forcats_1.0.1  
## [13] stringr_1.6.0   dplyr_1.2.1     purrr_1.2.2     readr_2.2.0    
## [17] tidyr_1.3.2     tibble_3.3.1    ggplot2_4.0.3   tidyverse_2.0.0
## 
## loaded via a namespace (and not attached):
##  [1] tidyselect_1.2.1     farver_2.1.2         S7_0.2.2            
##  [4] fastmap_1.2.0        broom.helpers_1.23.0 labelled_2.16.1     
##  [7] digest_0.6.39        timechange_0.4.0     lifecycle_1.0.5     
## [10] survival_3.8-6       magrittr_2.0.5       compiler_4.6.1      
## [13] rlang_1.3.0          sass_0.4.10          tools_4.6.1         
## [16] yaml_2.3.12          gt_1.3.0             knitr_1.51          
## [19] labeling_0.4.3       bit_4.6.0            classInt_0.4-11     
## [22] xml2_1.6.0           RColorBrewer_1.1-3   abind_1.4-8         
## [25] KernSmooth_2.23-26   withr_3.0.3          grid_4.6.1          
## [28] e1071_1.7-17         scales_1.4.0         cli_3.6.6           
## [31] survey_4.5           rmarkdown_2.32       crayon_1.5.3        
## [34] generics_0.1.4       otel_0.2.0           rstudioapi_0.19.0   
## [37] tzdb_0.5.0           commonmark_2.0.0     DBI_1.3.0           
## [40] cachem_1.1.0         proxy_0.4-29         splines_4.6.1       
## [43] parallel_4.6.1       mitools_2.4          vctrs_0.7.3         
## [46] Matrix_1.7-5         jsonlite_2.0.0       litedown_0.11       
## [49] hms_1.1.4            bit64_4.8.4          Formula_1.2-5       
## [52] jquerylib_0.1.4      units_1.0-1          glue_1.8.1          
## [55] stringi_1.8.7        gtable_0.3.6         pillar_1.11.1       
## [58] htmltools_0.5.9      R6_2.6.1             vroom_1.7.1         
## [61] evaluate_1.0.5       lattice_0.22-9       haven_2.5.5         
## [64] markdown_2.0         backports_1.5.1      cards_0.9.0         
## [67] bslib_0.11.0         class_7.3-23         Rcpp_1.1.2          
## [70] cardx_0.3.4          xfun_0.60            fs_2.1.0            
## [73] pkgconfig_2.0.3