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) |
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.
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")
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%).
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 |
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.
# 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.
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.
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:
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