1 Persiapan Data

1.1 Load Library

# ── Manipulasi Data ───────────────────────────────────────────────
library(foreign)           # read.dbf
library(dplyr)
library(readxl)            # read_excel
library(tidyr)

# ── Visualisasi ───────────────────────────────────────────────────
library(ggplot2)
library(scales)
library(gridExtra)

# ── Pemodelan ─────────────────────────────────────────────────────
library(ResourceSelection) # Hosmer-Lemeshow
library(lmtest)            # lrtest
library(logistf)           # Firth logistic regression
library(smotefamily)       # SMOTE oversampling
library(pROC)              # ROC-AUC
library(car)               # VIF

# ── Output Tabel ──────────────────────────────────────────────────
library(knitr)
library(kableExtra)
library(htmltools)

1.2 Load Data Utama (SUSENAS 2023)

Data utama yang digunakan adalah data individu SUSENAS 2023 dalam format .dbf.

data_susenas <- read.dbf("46654/Susenas KOR maret 2023/bv2_2026_02_22_08_59_08_ssn202303kor_ind_1.dbf")

1.3 Filter Populasi Studi

Populasi studi adalah anak penyandang disabilitas usia 13–15 tahun yang berdomisili di wilayah Sulawesi dan Kalimantan (kode provinsi 60–79).

Variabel kunci yang dibentuk pada tahap ini:

  • JUMLAH_JENIS_DISABILITAS: jumlah jenis disabilitas yang dialami anak (dari R1002–R1009).
  • status (Variabel Y): partisipasi sekolah — 1 = sedang bersekolah, 0 = tidak bersekolah.
allowed_vals <- c(1, 2, 3, 5, 6, 7)

dataku_filtered <- data_susenas %>%
  mutate(
    across(R1002:R1009, ~ as.numeric(.x)),

    JUMLAH_JENIS_DISABILITAS = rowSums(
      across(R1002:R1009, ~ .x %in% allowed_vals),
      na.rm = TRUE
    ),

    prov = as.numeric(R101),
    kabu = as.numeric(R101) * 100 + as.numeric(R102),

    # Y: Partisipasi Sekolah (1 = Bersekolah, 0 = Tidak Bersekolah)
    status = ifelse(
      R407 >= 13 & R407 <= 15 & JUMLAH_JENIS_DISABILITAS > 0,
      ifelse(R610 == 2, 1, 0),
      NA_real_
    )
  ) %>%
  filter(
    R407 >= 13 & R407 <= 15,
    JUMLAH_JENIS_DISABILITAS > 0,
    as.numeric(R101) >= 10 & as.numeric(R101) < 100
  )

cat("Jumlah observasi setelah filter:", nrow(dataku_filtered), "\n")
Jumlah observasi setelah filter: 761 

1.4 Variabel Kepala Rumah Tangga (X4, X5, X6)

Variabel karakteristik kepala rumah tangga (KRT) diekstrak dari baris dengan R403 == 1 (penanda KRT dalam data).

Variabel Deskripsi Kategori
X4 Jenis kelamin KRT 1 = Laki-laki; 0 = Perempuan
X5 Pendidikan KRT 1 = ≥ SMA; 0 = ≤ SMP
X6 Pekerjaan KRT 1 = Non-pertanian; 0 = Pertanian
id_rt <- c("URUT")

data_krt <- data_susenas %>%
  filter(as.numeric(R403) == 1) %>%
  mutate(
    R405 = as.numeric(R405),
    R614 = as.numeric(R614),
    R706 = as.numeric(R706),

    # X4: Jenis Kelamin KRT
    X4 = case_when(
      R405 == 1 ~ 1L,
      R405 == 2 ~ 0L,
      TRUE      ~ NA_integer_
    ),

    # X5: Tingkat Pendidikan KRT
    X5 = case_when(
      R614 >= 11 & R614 <= 24   ~ 1L,
      R614 %in% c(0:10, 25)     ~ 0L,
      TRUE                      ~ NA_integer_
    ),

    # X6: Pekerjaan KRT
    X6 = case_when(
      R706 >= 1 & R706 <= 6     ~ 0L,
      R706 > 6  | R706 == 0     ~ 1L,
      TRUE                      ~ NA_integer_
    )
  ) %>%
  select(all_of(id_rt), X4, X5, X6)

1.5 Variabel Kemiskinan Rumah Tangga (X9)

Data pengeluaran per kapita (KAPITA) dari data KP Blok 4.3 digabung dengan Garis Kemiskinan (GK) provinsi/daerah untuk membentuk variabel status kemiskinan.

files_43 <- c(
  "46654/Susenas KP Maret 2023/bv2_2026_02_22_08_55_02_ssn202303kp_blok43_51_94.dbf",
  "46654/Susenas KP Maret 2023/bv2_2026_02_22_08_49_01_ssn202303kp_blok43_11_31.dbf",
  "46654/Susenas KP Maret 2023/bv2_2026_02_22_08_52_06_ssn202303kp_blok43_32_36.dbf"
)

data_kp_43 <- bind_rows(lapply(files_43, read.dbf, as.is = TRUE)) %>%
  mutate(
    R101 = as.numeric(R101),
    R102 = as.numeric(R102),
    URUT = as.numeric(URUT)
  )

GK <- read_excel(
  "Garis Kemiskinan (Rupiah_Kapita_Bulan) Menurut Provinsi dan Daerah , 2023.xlsx",
  sheet = "Sheet2"
) %>%
  mutate(R101 = as.numeric(R101))

data_kp_x9 <- data_kp_43 %>%
  left_join(GK, by = c("R101" = "R101", "R105" = "R105")) %>%
  mutate(
    # X9: Status Kemiskinan RT (1 = Miskin, 0 = Tidak Miskin)
    X9 = if_else(KAPITA < GK, 1L, 0L, missing = NA_integer_)
  ) %>%
  select(all_of(id_rt), X9)

1.6 Penggabungan & Pembuatan Variabel Final

Semua variabel level individu dibentuk dan digabung menjadi satu dataset analisis (dataku_final).

Variabel Deskripsi Kategori
Y Partisipasi sekolah anak 1 = Bersekolah; 0 = Tidak
X1 Jenis kelamin anak 1 = Laki-laki; 0 = Perempuan
X2 Jenis disabilitas 1 = Ganda (≥2); 0 = Tunggal
X3 Tingkat kesulitan disabilitas 0 = Sangat kesulitan; 1 = Kesulitan; 2 = Sedikit
X4 Jenis kelamin KRT 1 = Laki-laki; 0 = Perempuan
X5 Pendidikan KRT 1 = ≥ SMA; 0 = ≤ SMP
X6 Pekerjaan KRT 1 = Non-pertanian; 0 = Pertanian
X7 Daerah tempat tinggal 1 = Perkotaan; 0 = Perdesaan
X8 Kepemilikan KIP/PIP 1 = Memiliki; 0 = Tidak
X9 Status kemiskinan RT 1 = Miskin; 0 = Tidak Miskin
dataku_final <- dataku_filtered %>%
  mutate(
    across(R1002:R1009, ~ as.numeric(.x)),
    R105 = as.numeric(R105),
    R405 = as.numeric(R405),
    R615 = as.numeric(R615),
    R616 = as.numeric(R616),

    # Y: Partisipasi Sekolah
    Y  = as.integer(status),

    # X1: Jenis Kelamin Anak
    X1 = case_when(
      R405 == 1 ~ 1L,
      R405 == 2 ~ 0L,
      TRUE      ~ NA_integer_
    ),

    # X2: Jenis Disabilitas (Tunggal / Ganda)
    X2 = case_when(
      JUMLAH_JENIS_DISABILITAS > 1  ~ 1L,
      JUMLAH_JENIS_DISABILITAS == 1 ~ 0L,
      TRUE                          ~ NA_integer_
    ),

    # X3: Tingkat Kesulitan Disabilitas (prioritas terberat)
    X3 = case_when(
      rowSums(across(R1002:R1009, ~ .x %in% c(1, 5)), na.rm = TRUE) > 0 ~ 0L,
      rowSums(across(R1002:R1009, ~ .x %in% c(2, 6)), na.rm = TRUE) > 0 ~ 1L,
      rowSums(across(R1002:R1009, ~ .x %in% c(3, 7)), na.rm = TRUE) > 0 ~ 2L,
      TRUE ~ NA_integer_
    ),

    # X7: Daerah Tempat Tinggal
    X7 = case_when(
      R105 == 1 ~ 1L,
      R105 == 2 ~ 0L,
      TRUE      ~ NA_integer_
    ),

    # X8: Kepemilikan KIP/PIP
    X8 = if_else(
      R615 %in% c(1, 2) | R616 %in% c(1, 2),
      1L, 0L
    )
  ) %>%
  left_join(data_krt,   by = id_rt) %>%
  left_join(data_kp_x9, by = id_rt) %>%
  select(prov, kabu, Y, X1, X2, X3, X4, X5, X6, X7, X8, X9)

1.7 Encoding Variabel Kategorik (Factor)

dataku_final <- dataku_final %>%
  mutate(
    Y  = factor(Y,  levels = c(0, 1),    labels = c("Tidak bersekolah", "Bersekolah")),
    X1 = factor(X1, levels = c(0, 1),    labels = c("Perempuan", "Laki-laki")),
    X2 = factor(X2, levels = c(0, 1),    labels = c("Disabilitas tunggal", "Disabilitas ganda")),
    X3 = factor(X3, levels = c(0, 1, 2), labels = c("Sangat kesulitan", "Kesulitan", "Sedikit kesulitan")),
    X4 = factor(X4, levels = c(0, 1),    labels = c("Perempuan", "Laki-laki")),
    X5 = factor(X5, levels = c(0, 1),    labels = c("≤ SMP", "≥ SMA")),
    X6 = factor(X6, levels = c(0, 1),    labels = c("Pertanian", "Nonpertanian")),
    X7 = factor(X7, levels = c(0, 1),    labels = c("Perdesaan", "Perkotaan")),
    X8 = factor(X8, levels = c(0, 1),    labels = c("Tidak memiliki", "Memiliki")),
    X9 = factor(X9, levels = c(0, 1),    labels = c("Tidak miskin", "Miskin"))
  )

1.8 Referensi Variabel

data.frame(
  Kode     = c("Y", paste0("X", 1:9)),
  Nama     = c(
    "Partisipasi Sekolah",
    "Jenis Kelamin Anak",
    "Jenis Disabilitas",
    "Tingkat Kesulitan Disabilitas",
    "Jenis Kelamin KRT",
    "Pendidikan KRT",
    "Pekerjaan KRT",
    "Daerah Tempat Tinggal",
    "Kepemilikan KIP/PIP",
    "Status Kemiskinan RT"
  ),
  Kategori = c(
    "Dependen",
    rep("Karakteristik Anak", 3),
    rep("Karakteristik KRT",  3),
    rep("Sosial-Ekonomi RT",  3)
  ),
  Koding = c(
    "1 = Bersekolah; 0 = Tidak bersekolah",
    "1 = Laki-laki; 0 = Perempuan",
    "1 = Ganda (≥2 jenis); 0 = Tunggal",
    "0 = Sangat kesulitan; 1 = Kesulitan; 2 = Sedikit kesulitan",
    "1 = Laki-laki; 0 = Perempuan",
    "1 = ≥ SMA; 0 = ≤ SMP",
    "1 = Non-pertanian; 0 = Pertanian",
    "1 = Perkotaan; 0 = Perdesaan",
    "1 = Memiliki; 0 = Tidak memiliki",
    "1 = Miskin; 0 = Tidak miskin"
  )
) %>%
  kable(caption = "Referensi Variabel Penelitian (Y dan X1–X9)") %>%
  kable_styling(bootstrap_options = c("striped", "hover"), full_width = TRUE) %>%
  column_spec(1, bold = TRUE, width = "4em") %>%
  column_spec(3, italic = TRUE) %>%
  row_spec(1, background = "#fff3e0")
Referensi Variabel Penelitian (Y dan X1–X9)
Kode Nama Kategori Koding
Y Partisipasi Sekolah Dependen 1 = Bersekolah; 0 = Tidak bersekolah
X1 Jenis Kelamin Anak Karakteristik Anak 1 = Laki-laki; 0 = Perempuan
X2 Jenis Disabilitas Karakteristik Anak 1 = Ganda (≥2 jenis); 0 = Tunggal
X3 Tingkat Kesulitan Disabilitas Karakteristik Anak 0 = Sangat kesulitan; 1 = Kesulitan; 2 = Sedikit kesulitan
X4 Jenis Kelamin KRT Karakteristik KRT 1 = Laki-laki; 0 = Perempuan
X5 Pendidikan KRT Karakteristik KRT 1 = ≥ SMA; 0 = ≤ SMP
X6 Pekerjaan KRT Karakteristik KRT 1 = Non-pertanian; 0 = Pertanian
X7 Daerah Tempat Tinggal Sosial-Ekonomi RT 1 = Perkotaan; 0 = Perdesaan
X8 Kepemilikan KIP/PIP Sosial-Ekonomi RT 1 = Memiliki; 0 = Tidak memiliki
X9 Status Kemiskinan RT Sosial-Ekonomi RT 1 = Miskin; 0 = Tidak miskin

2 Eksplorasi Data

2.1 Ukuran & Struktur Dataset

cat("Jumlah observasi :", nrow(dataku_final), "\n")
Jumlah observasi : 761 
cat("Jumlah variabel  :", ncol(dataku_final), "\n\n")
Jumlah variabel  : 12 
glimpse(dataku_final)
Rows: 761
Columns: 12
$ prov <dbl> 33, 74, 31, 31, 65, 31, 53, 74, 71, 31, 62, 64, 17, 15, 64, 33, 5…
$ kabu <dbl> 3375, 7411, 3172, 3172, 6571, 3171, 5302, 7414, 7106, 3171, 6201,…
$ Y    <fct> Bersekolah, Bersekolah, Bersekolah, Tidak bersekolah, Bersekolah,…
$ X1   <fct> Laki-laki, Perempuan, Perempuan, Perempuan, Laki-laki, Laki-laki,…
$ X2   <fct> Disabilitas ganda, Disabilitas ganda, Disabilitas tunggal, Disabi…
$ X3   <fct> Sangat kesulitan, Sedikit kesulitan, Sedikit kesulitan, Sedikit k…
$ X4   <fct> Laki-laki, Perempuan, Perempuan, Laki-laki, Laki-laki, Laki-laki,…
$ X5   <fct> ≥ SMA, ≤ SMP, ≤ SMP, ≥ SMA, ≤ SMP, ≥ SMA, ≤ SMP, ≤ SMP, ≥ SMA, ≥ …
$ X6   <fct> Nonpertanian, Pertanian, Nonpertanian, Nonpertanian, Pertanian, N…
$ X7   <fct> Perkotaan, Perdesaan, Perkotaan, Perkotaan, Perkotaan, Perkotaan,…
$ X8   <fct> Memiliki, Tidak memiliki, Tidak memiliki, Tidak memiliki, Tidak m…
$ X9   <fct> Tidak miskin, Tidak miskin, Tidak miskin, Tidak miskin, Tidak mis…

2.2 Statistik Deskriptif Ringkas

summary(dataku_final)
      prov            kabu                     Y               X1     
 Min.   :11.00   Min.   :1102   Tidak bersekolah:164   Perempuan:340  
 1st Qu.:16.00   1st Qu.:1674   Bersekolah      :597   Laki-laki:421  
 Median :35.00   Median :3501                                         
 Mean   :41.69   Mean   :4192                                         
 3rd Qu.:63.00   3rd Qu.:6310                                         
 Max.   :94.00   Max.   :9436                                         
                   X2                      X3              X4          X5     
 Disabilitas tunggal:400   Sangat kesulitan :209   Perempuan:101   ≤ SMP:493  
 Disabilitas ganda  :361   Kesulitan        :168   Laki-laki:660   ≥ SMA:268  
                           Sedikit kesulitan:384                              
                                                                              
                                                                              
                                                                              
            X6              X7                   X8                 X9     
 Pertanian   :320   Perdesaan:418   Tidak memiliki:622   Tidak miskin:654  
 Nonpertanian:441   Perkotaan:343   Memiliki      :139   Miskin      :107  
                                                                           
                                                                           
                                                                           
                                                                           

2.3 Pemeriksaan Missing Values

missing_tbl <- dataku_final %>%
  summarise(across(everything(), ~ sum(is.na(.)))) %>%
  pivot_longer(everything(),
               names_to  = "Variabel",
               values_to = "Jumlah_NA") %>%
  mutate(Persen_NA = round(Jumlah_NA / nrow(dataku_final) * 100, 2)) %>%
  arrange(desc(Jumlah_NA))

kable(missing_tbl,
      caption = "Jumlah dan Persentase Missing Value per Variabel",
      col.names = c("Variabel", "Jumlah NA", "% NA")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE)
Jumlah dan Persentase Missing Value per Variabel
Variabel Jumlah NA % NA
prov 0 0
kabu 0 0
Y 0 0
X1 0 0
X2 0 0
X3 0 0
X4 0 0
X5 0 0
X6 0 0
X7 0 0
X8 0 0
X9 0 0

2.4 Distribusi Variabel Dependen (Y)

tbl_y <- dataku_final %>%
  count(Y) %>%
  mutate(Persen = round(n / sum(n) * 100, 2))

kable(tbl_y,
      caption = "Distribusi Partisipasi Sekolah Anak Disabilitas Usia 13–15 Tahun",
      col.names = c("Status Sekolah", "Frekuensi", "%")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE)
Distribusi Partisipasi Sekolah Anak Disabilitas Usia 13–15 Tahun
Status Sekolah Frekuensi %
Tidak bersekolah 164 21.55
Bersekolah 597 78.45
ggplot(tbl_y, aes(x = Y, y = n, fill = Y)) +
  geom_bar(stat = "identity", width = 0.5, show.legend = FALSE) +
  geom_text(aes(label = paste0(n, "\n(", Persen, "%)")),
            vjust = -0.4, size = 4) +
  scale_fill_manual(values = c("#D73027", "#4575B4")) +
  labs(title    = "Distribusi Partisipasi Sekolah",
       subtitle = "Anak Penyandang Disabilitas Usia 13–15 Tahun\nSulawesi & Kalimantan (SUSENAS 2023)",
       x = NULL, y = "Frekuensi") +
  theme_minimal(base_size = 13)


2.5 Distribusi Variabel Independen

vars_cat   <- c("X1", "X2", "X3", "X4", "X5", "X6", "X7", "X8", "X9")
labels_cat <- c(
  "X1: Jenis Kelamin Anak",
  "X2: Jenis Disabilitas",
  "X3: Tingkat Kesulitan",
  "X4: Jenis Kelamin KRT",
  "X5: Pendidikan KRT",
  "X6: Pekerjaan KRT",
  "X7: Daerah Tinggal",
  "X8: Kepemilikan KIP/PIP",
  "X9: Status Kemiskinan RT"
)

plot_list <- lapply(seq_along(vars_cat), function(i) {
  var    <- vars_cat[i]
  df_tmp <- dataku_final %>%
    count(.data[[var]]) %>%
    filter(!is.na(.data[[var]])) %>%
    mutate(pct = round(n / sum(n) * 100, 1))

  ggplot(df_tmp, aes(x = .data[[var]], y = n, fill = .data[[var]])) +
    geom_bar(stat = "identity", show.legend = FALSE) +
    geom_text(aes(label = paste0(pct, "%")), vjust = -0.4, size = 3) +
    labs(title = labels_cat[i], x = NULL, y = "n") +
    theme_minimal(base_size = 10) +
    theme(plot.title = element_text(size = 10, face = "bold"))
})

do.call(grid.arrange, c(plot_list, ncol = 3))


2.6 Distribusi Jumlah Jenis Disabilitas

tbl_dis <- dataku_filtered %>%
  count(JUMLAH_JENIS_DISABILITAS) %>%
  mutate(Persen = round(n / sum(n) * 100, 2))

kable(tbl_dis,
      caption  = "Distribusi Jumlah Jenis Disabilitas",
      col.names = c("Jumlah Jenis Disabilitas", "Frekuensi", "%")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE)
Distribusi Jumlah Jenis Disabilitas
Jumlah Jenis Disabilitas Frekuensi %
1 400 52.56
2 88 11.56
3 88 11.56
4 82 10.78
5 40 5.26
6 31 4.07
7 24 3.15
8 8 1.05
ggplot(tbl_dis, aes(x = factor(JUMLAH_JENIS_DISABILITAS), y = n)) +
  geom_bar(stat = "identity", fill = "#4575B4", width = 0.6) +
  geom_text(aes(label = paste0(n, "\n(", Persen, "%)")),
            vjust = -0.3, size = 3.5) +
  labs(title = "Distribusi Jumlah Jenis Disabilitas",
       x = "Jumlah Jenis Disabilitas", y = "Frekuensi") +
  theme_minimal(base_size = 12)


2.7 Distribusi Observasi per Provinsi

tbl_prov <- dataku_final %>%
  count(prov) %>%
  mutate(Persen = round(n / sum(n) * 100, 2)) %>%
  arrange(desc(n))

kable(tbl_prov,
      caption   = "Distribusi Observasi per Provinsi",
      col.names = c("Kode Provinsi", "Frekuensi", "%")) %>%
  kable_styling(bootstrap_options = c("striped", "hover"),
                full_width = FALSE)
Distribusi Observasi per Provinsi
Kode Provinsi Frekuensi %
12 61 8.02
32 50 6.57
33 50 6.57
53 49 6.44
35 48 6.31
11 45 5.91
73 42 5.52
13 36 4.73
16 24 3.15
18 22 2.89
31 22 2.89
94 22 2.89
63 21 2.76
82 20 2.63
52 19 2.50
14 18 2.37
17 17 2.23
62 17 2.23
71 17 2.23
74 17 2.23
81 16 2.10
61 15 1.97
51 14 1.84
36 13 1.71
21 11 1.45
64 11 1.45
91 11 1.45
15 10 1.31
19 10 1.31
72 9 1.18
75 8 1.05
65 6 0.79
76 6 0.79
34 4 0.53
ggplot(tbl_prov, aes(x = reorder(factor(prov), n), y = n)) +
  geom_bar(stat = "identity", fill = "#74ADD1") +
  geom_text(aes(label = n), hjust = -0.2, size = 3.5) +
  coord_flip() +
  labs(title = "Jumlah Observasi per Provinsi",
       x = "Kode Provinsi", y = "Frekuensi") +
  theme_minimal(base_size = 12)


2.8 Tabulasi Silang: Partisipasi Sekolah (Y) vs Setiap Prediktor

Tabel berikut menyajikan distribusi status sekolah anak berdasarkan masing-masing variabel prediktor, beserta hasil uji Chi-Square.

vars_for_tab <- c("X1", "X2", "X3", "X4", "X5", "X6", "X7", "X8", "X9")
label_vars   <- c(
  "Jenis Kelamin Anak (X1)",
  "Jenis Disabilitas (X2)",
  "Tingkat Kesulitan (X3)",
  "Jenis Kelamin KRT (X4)",
  "Pendidikan KRT (X5)",
  "Pekerjaan KRT (X6)",
  "Daerah Tinggal (X7)",
  "Kepemilikan KIP/PIP (X8)",
  "Status Kemiskinan RT (X9)"
)

for (i in seq_along(vars_for_tab)) {
  var <- vars_for_tab[i]
  cat("\n###", label_vars[i], "\n\n")

  df_tmp <- dataku_final %>%
    filter(!is.na(.data[[var]]), !is.na(Y))

  tab     <- table(df_tmp[[var]], df_tmp$Y)
  tab_pct <- prop.table(tab, margin = 1) * 100

  tbl_out <- as.data.frame.matrix(tab) %>%
    mutate(
      Total          = rowSums(.),
      `% Bersekolah` = round(tab_pct[, "Bersekolah"], 1)
    )

  chi_res <- tryCatch(chisq.test(tab), error = function(e) NULL)
  p_val   <- if (!is.null(chi_res)) round(chi_res$p.value, 4) else NA

  print(
    kable(tbl_out,
          caption = paste0(label_vars[i], " — Chi-Square p-value: ", p_val)) %>%
      kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                    full_width = FALSE)
  )
}

2.8.1 Jenis Kelamin Anak (X1)

Jenis Kelamin Anak (X1) — Chi-Square p-value: 0.7533
Tidak bersekolah Bersekolah Total % Bersekolah
Perempuan 71 269 340 79.1
Laki-laki 93 328 421 77.9

2.8.2 Jenis Disabilitas (X2)

Jenis Disabilitas (X2) — Chi-Square p-value: 0
Tidak bersekolah Bersekolah Total % Bersekolah
Disabilitas tunggal 38 362 400 90.5
Disabilitas ganda 126 235 361 65.1

2.8.3 Tingkat Kesulitan (X3)

Tingkat Kesulitan (X3) — Chi-Square p-value: 0
Tidak bersekolah Bersekolah Total % Bersekolah
Sangat kesulitan 81 128 209 61.2
Kesulitan 36 132 168 78.6
Sedikit kesulitan 47 337 384 87.8

2.8.4 Jenis Kelamin KRT (X4)

Jenis Kelamin KRT (X4) — Chi-Square p-value: 0.2676
Tidak bersekolah Bersekolah Total % Bersekolah
Perempuan 17 84 101 83.2
Laki-laki 147 513 660 77.7

2.8.5 Pendidikan KRT (X5)

Pendidikan KRT (X5) — Chi-Square p-value: 0.0049
Tidak bersekolah Bersekolah Total % Bersekolah
≤ SMP 122 371 493 75.3
≥ SMA 42 226 268 84.3

2.8.6 Pekerjaan KRT (X6)

Pekerjaan KRT (X6) — Chi-Square p-value: 0.1273
Tidak bersekolah Bersekolah Total % Bersekolah
Pertanian 78 242 320 75.6
Nonpertanian 86 355 441 80.5

2.8.7 Daerah Tinggal (X7)

Daerah Tinggal (X7) — Chi-Square p-value: 0.002
Tidak bersekolah Bersekolah Total % Bersekolah
Perdesaan 108 310 418 74.2
Perkotaan 56 287 343 83.7

2.8.8 Kepemilikan KIP/PIP (X8)

Kepemilikan KIP/PIP (X8) — Chi-Square p-value: 0
Tidak bersekolah Bersekolah Total % Bersekolah
Tidak memiliki 159 463 622 74.4
Memiliki 5 134 139 96.4

2.8.9 Status Kemiskinan RT (X9)

Status Kemiskinan RT (X9) — Chi-Square p-value: 0.911
Tidak bersekolah Bersekolah Total % Bersekolah
Tidak miskin 140 514 654 78.6
Miskin 24 83 107 77.6

2.9 Visualisasi: Partisipasi Sekolah per Prediktor (Stacked Bar)

plot_stacked <- function(var, label) {
  df_tmp <- dataku_final %>%
    filter(!is.na(.data[[var]]), !is.na(Y)) %>%
    count(.data[[var]], Y) %>%
    group_by(.data[[var]]) %>%
    mutate(pct = round(n / sum(n) * 100, 1)) %>%
    ungroup()

  ggplot(df_tmp, aes(x = .data[[var]], y = pct, fill = Y)) +
    geom_bar(stat = "identity", position = "stack") +
    geom_text(aes(label = paste0(pct, "%")),
              position = position_stack(vjust = 0.5),
              size = 3, color = "white", fontface = "bold") +
    scale_fill_manual(values = c("Tidak bersekolah" = "#D73027",
                                 "Bersekolah"        = "#4575B4")) +
    labs(title = label, x = NULL, y = "%", fill = NULL) +
    theme_minimal(base_size = 10) +
    theme(legend.position = "bottom",
          plot.title      = element_text(size = 10, face = "bold"))
}

p_list <- lapply(seq_along(vars_for_tab), function(i) {
  plot_stacked(vars_for_tab[i], label_vars[i])
})

do.call(grid.arrange, c(p_list, ncol = 3))


2.10 Partisipasi Sekolah per Provinsi

df_prov_y <- dataku_final %>%
  filter(!is.na(Y)) %>%
  count(prov, Y) %>%
  group_by(prov) %>%
  mutate(pct = round(n / sum(n) * 100, 1)) %>%
  ungroup()

ggplot(df_prov_y, aes(x = factor(prov), y = pct, fill = Y)) +
  geom_bar(stat = "identity", position = "stack") +
  geom_text(aes(label = paste0(pct, "%")),
            position = position_stack(vjust = 0.5),
            size = 2.8, color = "white", fontface = "bold") +
  scale_fill_manual(values = c("Tidak bersekolah" = "#D73027",
                               "Bersekolah"        = "#4575B4")) +
  labs(title    = "Partisipasi Sekolah Anak Disabilitas Usia 13–15 Tahun\nper Provinsi (Sulawesi & Kalimantan)",
       x = "Kode Provinsi", y = "%", fill = NULL) +
  theme_minimal(base_size = 12) +
  theme(legend.position = "bottom")

df_pulau_y <- dataku_final %>%
  filter(!is.na(Y)) %>%
  mutate(
    pulau = case_when(
      floor(prov / 10) == 6 ~ "Kalimantan",
      floor(prov / 10) == 7 ~ "Sulawesi",
      TRUE                   ~ NA_character_
    ),
    pulau = factor(pulau, levels = c("Kalimantan", "Sulawesi"))
  ) %>%
  filter(!is.na(pulau)) %>%
  count(pulau, Y) %>%
  group_by(pulau) %>%
  mutate(
    total = sum(n),
    pct   = round(n / total * 100, 1)
  ) %>%
  ungroup()

ggplot(df_pulau_y, aes(x = pulau, y = pct, fill = Y)) +
  geom_bar(stat = "identity", position = "stack", width = 0.65) +
  geom_text(aes(label = paste0(pct, "%")),
            position = position_stack(vjust = 0.5),
            size = 3.5, color = "white", fontface = "bold") +
  geom_text(
    data = df_pulau_y %>% distinct(pulau, total),
    aes(x = pulau, y = 102, label = paste0("n=", total)),
    inherit.aes = FALSE, size = 3.2, color = "grey40"
  ) +
  scale_fill_manual(values = c("Tidak bersekolah" = "#D73027",
                               "Bersekolah"        = "#4575B4")) +
  scale_y_continuous(limits = c(0, 110), labels = function(x) paste0(x, "%")) +
  labs(
    title    = "Partisipasi Sekolah Anak Penyandang Disabilitas Usia 13–15 Tahun",
    subtitle = "per Pulau — SUSENAS 2023 (Sulawesi & Kalimantan)",
    x        = NULL,
    y        = "Persentase (%)",
    fill     = NULL
  ) +
  theme_minimal(base_size = 13) +
  theme(
    legend.position = "bottom",
    plot.title      = element_text(face = "bold"),
    axis.text.x     = element_text(size = 11)
  )


2.11 Ringkasan Persentase Bersekolah per Variabel

Tabel ringkasan tingkat partisipasi sekolah (% bersekolah) untuk setiap kategori prediktor.

ringkasan <- lapply(seq_along(vars_for_tab), function(i) {
  var <- vars_for_tab[i]
  dataku_final %>%
    filter(!is.na(.data[[var]]), !is.na(Y)) %>%
    group_by(Variabel = label_vars[i], Kategori = .data[[var]]) %>%
    summarise(
      n              = n(),
      n_bersekolah   = sum(Y == "Bersekolah"),
      pct_bersekolah = round(n_bersekolah / n * 100, 1),
      .groups        = "drop"
    )
}) %>% bind_rows()

kable(ringkasan,
      caption   = "Ringkasan % Bersekolah per Kategori Prediktor",
      col.names = c("Variabel", "Kategori", "n", "n Bersekolah", "% Bersekolah")) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = FALSE) %>%
  collapse_rows(columns = 1, valign = "top")
Ringkasan % Bersekolah per Kategori Prediktor
Variabel Kategori n n Bersekolah % Bersekolah
Jenis Kelamin Anak (X1) Perempuan 340 269 79.1
Laki-laki 421 328 77.9
Jenis Disabilitas (X2) Disabilitas tunggal 400 362 90.5
Disabilitas ganda 361 235 65.1
Tingkat Kesulitan (X3) Sangat kesulitan 209 128 61.2
Kesulitan 168 132 78.6
Sedikit kesulitan 384 337 87.8
Jenis Kelamin KRT (X4) Perempuan 101 84 83.2
Laki-laki 660 513 77.7
Pendidikan KRT (X5) ≤ SMP 493 371 75.3
≥ SMA 268 226 84.3
Pekerjaan KRT (X6) Pertanian 320 242 75.6
Nonpertanian 441 355 80.5
Daerah Tinggal (X7) Perdesaan 418 310 74.2
Perkotaan 343 287 83.7
Kepemilikan KIP/PIP (X8) Tidak memiliki 622 463 74.4
Memiliki 139 134 96.4
Status Kemiskinan RT (X9) Tidak miskin 654 514 78.6
Miskin 107 83 77.6

3 Estimasi Langsung Indikator

Indikator: Persentase penyandang disabilitas usia 13–15 tahun yang sedang bersekolah, diestimasi di tingkat provinsi menggunakan metode estimasi langsung dengan desain survei SUSENAS 2023 (Sulawesi & Kalimantan).

3.1 Pembentukan Data Indikator & Desain Survei

library(survey)

allowed_vals_ind <- c(1, 2, 3, 5, 6, 7)

# ── Bentuk data indikator dari data_susenas mentah ────────────────────────
indicator_disab <- data_susenas %>%
  mutate(
    across(R1002:R1009, ~ as.numeric(.x)),
    R101 = as.numeric(R101),
    R102 = as.numeric(R102),
    R105 = as.numeric(R105),
    R407 = as.numeric(R407),
    R610 = as.numeric(R610),
    FWT  = as.numeric(FWT),

    # Jumlah jenis disabilitas (nilai 1,2,3,5,6,7 = ada kesulitan)
    JUMLAH_DISAB = rowSums(
      across(R1002:R1009, ~ .x %in% allowed_vals_ind),
      na.rm = TRUE
    ),

    # Strata: kombinasi kode provinsi × daerah (perkotaan/perdesaan)
    STRATA_IND = paste0(sprintf("%02d", R101), R105),

    jml_pddk = 100,

    # Variabel respons: 100 = bersekolah (R610==2), 0 = tidak
    pa = ifelse(R610 == 2, 100, 0)
  ) %>%
  filter(
    R407 >= 13 & R407 <= 15,          # Usia 13–15 tahun
    JUMLAH_DISAB > 0               # Penyandang disabilitas
    
  )

cat("Jumlah observasi domain indikator:", nrow(indicator_disab), "\n")
Jumlah observasi domain indikator: 761 
# ── Desain survei ─────────────────────────────────────────────────────────
options(survey.adjust.domain.lonely = TRUE)
options(survey.lonely.psu           = "adjust")

susenas_ind_design <- svydesign(
  id      = ~PSU + SSU,
  strata  = ~STRATA_IND,
  data    = indicator_disab,
  weights = ~FWT,
  nest    = TRUE
)

summary(susenas_ind_design$prob)
     Min.   1st Qu.    Median      Mean   3rd Qu.      Max. 
0.0004872 0.0039392 0.0095784 0.0265952 0.0230631 0.3169584 

3.2 Estimasi Tingkat Provinsi

# ── Estimasi langsung per provinsi ────────────────────────────────────────
resultP_ind <- svyby(
  formula = ~pa,
  denom   = ~jml_pddk,
  by      = ~R101,
  design  = susenas_ind_design,
  deff    = TRUE,
  svyratio,
  vartype = c("se", "ci", "cv", "cvpct", "var")
)
resultP_ind[is.na(resultP_ind)] <- 0

# ── Hitung turunan statistik ──────────────────────────────────────────────
resultP_ind <- resultP_ind %>%
  mutate(
    theta    = round(`pa/jml_pddk` * 100, 2),
    SE       = round(`se.pa/jml_pddk` * 100, 2),
    VAR      = round(SE^2, 2),
    CI_LOWER = round(ci_l * 100, 2),
    CI_UPPER = round(ci_u * 100, 2),
    RSE      = round(`cv%`, 2),
    DEFF     = round(DEff, 2)
  )
resultP_ind$CI_LOWER[resultP_ind$CI_LOWER < 0] <- 0

# ── Label Nama Provinsi Lengkap (Kode BPS) ───────────────────────────────────
prov_label_ind <- data.frame(
  R101 = c(
    11, 12, 13, 14, 15, 16, 17, 18, 19, 21, 
    31, 32, 33, 34, 35, 36, 
    51, 52, 53, 
    61, 62, 63, 64, 65, 
    71, 72, 73, 74, 75, 76, 
    81, 82, 
    91, 94
  ),
  Nama_Prov = c(
    "Aceh", "Sumatera Utara", "Sumatera Barat", "Riau", "Jambi", 
    "Sumatera Selatan", "Bengkulu", "Lampung", "Kepulauan Bangka Belitung", "Kepulauan Riau",
    "DKI Jakarta", "Jawa Barat", "Jawa Tengah", "DI Yogyakarta", "Jawa Timur", 
    "Banten",
    "Bali", "Nusa Tenggara Barat", "Nusa Tenggara Timur",
    "Kalimantan Barat", "Kalimantan Tengah", "Kalimantan Selatan", "Kalimantan Timur", "Kalimantan Utara",
    "Sulawesi Utara", "Sulawesi Tengah", "Sulawesi Selatan", "Sulawesi Tenggara", "Gorontalo", 
    "Sulawesi Barat",
    "Maluku", "Maluku Utara",
    "Papua Barat", "Papua"
  ),
  Wilayah = c(
    rep("Sumatera", 10),
    rep("Jawa", 6),
    rep("Nusa Tenggara & Bali", 3),
    rep("Kalimantan", 5),
    rep("Sulawesi", 6),
    rep("Maluku", 2),
    rep("Papua", 2)
  )
)

# ── Tabel output bersih ───────────────────────────────────────────────────
outputP_ind <- resultP_ind %>%
  select(R101, theta, SE, VAR, CI_LOWER, CI_UPPER, RSE, DEFF) %>%
  rename(
    Prov      = R101,
    Estimasi  = theta,
    CI_LOWER  = CI_LOWER,
    CI_UPPER  = CI_UPPER
  ) %>%
  left_join(prov_label_ind, by = c("Prov" = "R101")) %>%
  arrange(Prov)

kable(
  outputP_ind[, c("Nama_Prov", "Wilayah", "Estimasi", "SE",
                  "CI_LOWER", "CI_UPPER", "RSE", "DEFF")],
  caption   = "Estimasi Langsung: % Penyandang Disabilitas 13–15 Tahun yang Bersekolah per Provinsi",
  col.names = c("Provinsi", "Wilayah", "Estimasi (%)", "SE",
                "CI Lower", "CI Upper", "RSE (%)", "DEFF"),
  digits    = 2
) %>%
  kable_styling(bootstrap_options = c("striped", "hover", "condensed"),
                full_width = TRUE) %>%
  column_spec(3, bold = TRUE, color = "#1565c0") %>%
  column_spec(7, color = ifelse(outputP_ind$RSE > 25, "red", "darkgreen"),
              bold = ifelse(outputP_ind$RSE > 25, TRUE, FALSE)) %>%
  row_spec(which(outputP_ind$RSE > 25), background = "#fff3e0")
Estimasi Langsung: % Penyandang Disabilitas 13–15 Tahun yang Bersekolah per Provinsi
Provinsi Wilayah Estimasi (%) SE CI Lower CI Upper RSE (%) DEFF
Aceh Sumatera 93.03 5.39 82.47 103.58 5.79 1.99
Sumatera Utara Sumatera 70.71 11.22 48.72 92.71 15.87 3.67
Sumatera Barat Sumatera 89.86 5.59 78.90 100.82 6.22 1.21
Riau Sumatera 71.42 14.18 43.62 99.22 19.86 1.68
Jambi Sumatera 82.29 14.30 54.26 110.31 17.38 1.27
Sumatera Selatan Sumatera 60.03 15.86 28.95 91.12 26.42 2.43
Bengkulu Sumatera 96.01 4.17 87.84 104.18 4.34 0.73
Lampung Sumatera 63.22 12.73 38.27 88.18 20.14 1.47
Kepulauan Bangka Belitung Sumatera 52.73 19.23 15.04 90.42 36.47 1.35
Kepulauan Riau Sumatera 99.62 0.31 99.01 100.23 0.31 0.03
DKI Jakarta Jawa 87.99 11.51 65.43 110.54 13.08 2.64
Jawa Barat Jawa 79.23 8.76 62.07 96.40 11.05 2.29
Jawa Tengah Jawa 78.26 9.28 60.07 96.45 11.86 2.49
DI Yogyakarta Jawa 100.00 0.00 100.00 100.00 0.00 0.00
Jawa Timur Jawa 82.50 8.37 66.09 98.91 10.15 2.29
Banten Jawa 84.36 11.76 61.30 107.41 13.95 1.26
Bali Nusa Tenggara & Bali 78.10 15.59 47.55 108.65 19.96 1.86
Nusa Tenggara Barat Nusa Tenggara & Bali 83.39 11.17 61.49 105.29 13.40 1.63
Nusa Tenggara Timur Nusa Tenggara & Bali 76.48 8.26 60.30 92.66 10.79 1.84
Kalimantan Barat Kalimantan 68.91 12.11 45.17 92.66 17.58 0.97
Kalimantan Tengah Kalimantan 88.74 6.02 76.94 100.54 6.79 0.59
Kalimantan Selatan Kalimantan 49.54 14.75 20.62 78.45 29.78 1.75
Kalimantan Timur Kalimantan 93.18 5.85 81.71 104.65 6.28 0.54
Kalimantan Utara Kalimantan 99.18 0.93 97.36 101.01 0.94 0.05
Sulawesi Utara Sulawesi 82.46 10.52 61.84 103.09 12.76 1.24
Sulawesi Tengah Sulawesi 86.23 12.31 62.11 110.35 14.27 1.03
Sulawesi Selatan Sulawesi 67.28 10.36 46.97 87.59 15.40 2.01
Sulawesi Tenggara Sulawesi 29.57 12.54 4.99 54.16 42.42 1.23
Gorontalo Sulawesi 61.81 24.79 13.22 110.39 40.11 1.85
Sulawesi Barat Sulawesi 9.15 9.97 0.00 28.70 109.01 0.60
Maluku Maluku 89.16 8.66 72.20 106.13 9.71 1.17
Maluku Utara Maluku 84.10 8.34 67.75 100.45 9.92 1.01
Papua Barat Papua 84.91 10.55 64.23 105.59 12.42 0.90
Papua Papua 68.44 15.75 37.58 99.30 23.01 2.47

3.3 Visualisasi Estimasi & RSE per Provinsi

library(dplyr)
library(ggplot2)
library(tidyr)

# ── 1. Restrukturisasi Data untuk Faceting Vertikal ───────────────────────
plot_data_ind <- outputP_ind %>%
  select(Nama_Prov, Wilayah, Estimasi, RSE, CI_LOWER, CI_UPPER) %>%
  mutate(
    # Urutkan provinsi berdasarkan nilai Estimasi dari terbesar ke terkecil
    Nama_Prov = factor(Nama_Prov, levels = Nama_Prov[order(-Estimasi)]),
    RSE_flag  = factor(
      ifelse(RSE > 25, "RSE > 25% (Tidak Reliabel)", "RSE ≤ 25% (Reliabel)"),
      levels = c("RSE ≤ 25% (Reliabel)", "RSE > 25% (Tidak Reliabel)")
    )
  ) %>%
  # Mengubah data menjadi format long agar bisa di-facet secara vertikal
  pivot_longer(cols = c(Estimasi, RSE), names_to = "Indikator", values_to = "Nilai")

# ── 2. Pembuatan Single Plot dengan Facet ─────────────────────────────────
ggplot(plot_data_ind, aes(x = Nama_Prov, y = Nilai, fill = Wilayah)) +
  # Batang utama grafik
  geom_bar(stat = "identity", width = 0.65, alpha = 0.88, color = "white", linewidth = 0.2) +
  
  # Error Bar Khusus untuk panel Estimasi saja
  geom_errorbar(
    data = filter(plot_data_ind, Indikator == "Estimasi"),
    aes(ymin = CI_LOWER, ymax = CI_UPPER),
    width = 0.2, color = "gray20", linewidth = 0.6
  ) +
  
  # Garis Batas RSE 25% Khusus untuk panel RSE saja
  geom_hline(
    data = filter(plot_data_ind, Indikator == "RSE"),
    aes(yintercept = 25), linetype = "dashed", color = "#C62828", linewidth = 0.7
  ) +
  
  # Label angka di atas bar (fleksibel mengikuti posisi CI_UPPER atau Nilai RSE)
  geom_text(
    aes(
      label = paste0(round(Nilai, 1), "%"),
      y = ifelse(Indikator == "Estimasi", CI_UPPER, Nilai)
    ),
    vjust = -0.5, size = 3.3, fontface = "bold", color = "gray10"
  ) +
  
  # Pewarnaan khusus untuk wilayah Kalimantan & Sulawesi
  scale_fill_manual(values = c("Kalimantan" = "#1976D2", "Sulawesi" = "#E64A19")) +
  
  # Pengaturan skala Y otomatis melonggar di atas agar text tidak terpotong
  scale_y_continuous(expand = expansion(mult = c(0, 0.15))) +
  
  # Pembagian Panel Vertikal (Atas: Estimasi, Bawah: RSE) dengan skala Y bebas
  facet_wrap(~ Indikator, ncol = 1, scales = "free_y", 
             labeller = as_labeller(c("Estimasi" = "Estimasi Angka Sekolah (95% CI)", 
                                     "RSE" = "Relative Standard Error / RSE (Batas 25%)"))) +
  
  # Labeling Grafis
  labs(
    title    = "Analisis Indikator Disabilitas Usia 13–15 Tahun yang Bersekolah",
    subtitle = "Sulawesi & Kalimantan — SUSENAS 2023 | Estimasi Langsung",
    x        = NULL,
    y        = "Persentase (%)",
    fill     = "Wilayah:",
    caption  = "Garis putus-putus merah menyatakan batas maksimal reliabilitas RSE BPS (25%)"
  ) +
  
  # Tema visual minimalis & profesional
  theme_minimal(base_size = 12) +
  theme(
    plot.title         = element_text(face = "bold", size = 14, color = "gray10"),
    plot.subtitle      = element_text(color = "gray40", size = 10, margin = margin(b = 12)),
    
    # Merapikan text nama provinsi di sumbu X agar vertikal/miring dan tidak bertumpuk
    axis.text.x        = element_text(angle = 45, hjust = 1, vjust = 1, face = "bold", color = "gray20"),
    axis.title.y       = element_text(size = 10, color = "gray30"),
    
    # Desain strip panel facet yang clean
    strip.background   = element_rect(fill = "gray95", color = NA),
    strip.text         = element_text(face = "bold", size = 11, color = "gray20", margin = margin(t = 6, b = 6)),
    
    # Menghapus grid line yang tidak perlu
    panel.grid.major.x = element_blank(),
    panel.grid.minor   = element_blank(),
    
    legend.position    = "bottom",
    legend.margin      = margin(t = -5)
  )

## 1. AGREGASI DATA PER PULAU (Menggabungkan Nilai Varians & SE)
# Menggabungkan data provinsi ke tingkat pulau dengan pendekatan survey
pulau_data <- aggregate(cbind(Estimasi, VAR) ~ Wilayah, data = outputP_ind, FUN = mean)

# Menghitung ulang SE, CI, dan RSE tingkat Pulau agar akurat secara statistik
pulau_data$SE       <- sqrt(pulau_data$VAR)
pulau_data$CI_LOWER <- pulau_data$Estimasi - (1.96 * pulau_data$SE)
pulau_data$CI_UPPER <- pulau_data$Estimasi + (1.96 * pulau_data$SE)
pulau_data$RSE      <- (pulau_data$SE / pulau_data$Estimasi) * 100

# Urutkan pulau berdasarkan nilai Estimasi terbesar ke terkecil agar plot rapi
pulau_data <- pulau_data[order(-pulau_data$Estimasi), ]


## 2. SETTING LAYOUT GRAFIK (Base R)
# Membagi layar menjadi 2 kolom (Kiri: Estimasi, Kanan: RSE)
old_par <- par(mfrow = c(1, 2), mar = c(6, 5, 4, 2))

# Palette warna estetik modern untuk masing-masing pulau
warna_pulau <- c("#1976D2", "#E64A19", "#388E3C", "#0097A7", "#7B1FA2", "#FBC02D", "#E91E63")[1:nrow(pulau_data)]


## ── PLOT 1: ESTIMASI LENGKAP DENGAN CI PER PULAU ──────────────────────────
y_max_est <- max(pulau_data$CI_UPPER, na.rm = TRUE) * 1.15

bp1 <- barplot(pulau_data$Estimasi,
               names.arg = pulau_data$Wilayah,
               las = 2,                  # Nama pulau tegak lurus/vertikal
               col = warna_pulau,
               border = NA,
               ylim = c(0, y_max_est),
               ylab = "Estimasi (%)",
               main = "Estimasi Angka Sekolah Per Pulau",
               font.main = 2, cex.main = 1.1, cex.names = 0.9)

# Tambahkan Grid & Gambar Ulang Bar agar bersih
grid(nx = NA, ny = NULL, col = "gray93", lty = "solid")
barplot(pulau_data$Estimasi, col = warna_pulau, border = NA, add = TRUE, las = 2, names.arg = NULL)

# Tambahkan Error Bar (Confidence Interval)
arrows(x0 = bp1, y0 = pulau_data$CI_LOWER, 
       x1 = bp1, y1 = pulau_data$CI_UPPER, 
       code = 3, angle = 90, length = 0.05, col = "gray20", lwd = 1.5)

# Tampilkan angka persentase di atas bar
text(x = bp1, y = pulau_data$Estimasi + (y_max_est * 0.02), 
     labels = paste0(round(pulau_data$Estimasi, 1), "%"), 
     cex = 0.85, font = 2)


## ── PLOT 2: RELATIVE STANDARD ERROR (RSE) PER PULAU ──────────────────────
y_max_rse <- max(c(pulau_data$RSE, 30), na.rm = TRUE) * 1.15

# Warna RSE otomatis: Hijau jika <= 25% (Reliabel), Merah jika > 25%
warna_rse <- ifelse(pulau_data$RSE > 25, "#EF5350", "#66BB6A")

bp2 <- barplot(pulau_data$RSE,
               names.arg = pulau_data$Wilayah,
               las = 2,
               col = warna_rse,
               border = NA,
               ylim = c(0, y_max_rse),
               ylab = "RSE (%)",
               main = "Nilai RSE (Reliabilitas Data) Per Pulau",
               font.main = 2, cex.main = 1.1, cex.names = 0.9)

# Tambahkan Grid & Gambar Ulang Bar RSE
grid(nx = NA, ny = NULL, col = "gray93", lty = "solid")
barplot(pulau_data$RSE, col = warna_rse, border = NA, add = TRUE, las = 2, names.arg = NULL)

# Tambahkan Garis Batas RSE 25% (Standard BPS untuk data reliabel)
abline(h = 25, col = "#C62828", linetype = "dashed", lwd = 1.5)
text(x = max(bp2)*0.3, y = 26.5, labels = "Batas RSE 25%", col = "#C62828", cex = 0.8, font = 3)

# Tampilkan nilai RSE di atas bar
text(x = bp2, y = pulau_data$RSE + (y_max_rse * 0.02), 
     labels = paste0(round(pulau_data$RSE, 1), "%"), 
     cex = 0.85, font = 2)

# Reset Layout Parameter R
par(old_par)

4 Analisis Regresi Logistik

Catatan: Semua 4 metode (Reglog Biasa, Firth, SMOTE, Class Weight) dijalankan terhadap satu dataset analisis (dataku_final). Gunakan panel filter di bawah untuk memilih metode yang ingin ditampilkan.

4.1 Fungsi Pembantu

xvars_global <- paste0("X", 1:9)

# ── Konversi kable ke string HTML ─────────────────────────────────
kbl_h <- function(df, caption = NULL) {
  knitr::kable(df, format = "html", caption = caption, escape = FALSE) %>%
    kable_styling(
      bootstrap_options = c("striped", "hover", "condensed"),
      full_width = FALSE, font_size = 13, position = "left"
    ) %>%
    as.character()
}

# ── Pra-pemrosesan data standar ───────────────────────────────────
# Y (factor "Tidak bersekolah"/"Bersekolah") dikonversi ke status_miskin 0/1
# agar semua fungsi helper berjalan langsung tanpa modifikasi tambahan
prep_data <- function(data) {
  df <- data %>%
    select(all_of(c("Y", xvars_global))) %>%
    mutate(
      status_miskin = as.integer(Y) - 1L,   # 0 = Tidak bersekolah, 1 = Bersekolah
      across(all_of(xvars_global), as.numeric)
    ) %>%
    select(-Y) %>%
    na.omit()

  konstan     <- xvars_global[sapply(xvars_global, function(v) length(unique(df[[v]])) < 2)]
  xvars_model <- setdiff(xvars_global, konstan)
  formula_mod <- as.formula(paste("status_miskin ~", paste(xvars_model, collapse = " + ")))
  list(df = df, xvars_model = xvars_model, formula_mod = formula_mod, konstan = konstan)
}

# ── Wrapper HTML untuk tiap sub-bagian ───────────────────────────
sec <- function(icon, judul, isi) {
  paste0(
    '<div style="margin-bottom:18px">',
    '<h5 style="color:#1565c0;border-bottom:2px solid #bbdefb;',
    'padding-bottom:5px;margin-top:18px;font-size:13.5px;">',
    icon, ' ', judul, '</h5>', isi, '</div>'
  )
}

# ── EDA: distribusi Y ────────────────────────────────────────────
eda_html <- function(df) {
  tbl <- table(df$status_miskin)
  pct <- round(prop.table(tbl) * 100, 2)
  kbl_h(data.frame(
    Kategori = c("Tidak Bersekolah (0)", "Bersekolah (1)"),
    N        = as.integer(c(tbl["0"], tbl["1"])),
    Persen   = paste0(c(pct["0"], pct["1"]), " %")
  ), "Distribusi Y — Partisipasi Sekolah")
}

# ── Uji Simultan (GLM-based) ─────────────────────────────────────
simultan_html <- function(model, model_null, xvars_model) {
  G2    <- model_null$deviance - model$deviance
  df_G2 <- length(xvars_model)
  chi_k <- qchisq(0.95, df_G2)
  pval  <- pchisq(G2, df_G2, lower.tail = FALSE)
  kbl_h(data.frame(
    Statistik = c("-2LL Null", "-2LL Model", "G² Hitung", "df",
                  "χ² Tabel (α=5%)", "p-value", "Keputusan"),
    Nilai = c(
      round(model_null$deviance, 3), round(model$deviance, 3),
      round(G2, 3), df_G2, round(chi_k, 3), round(pval, 4),
      ifelse(G2 > chi_k, "✅ Tolak H0 — Model Signifikan", "❌ Gagal Tolak H0")
    )
  ), "Uji Simultan (Likelihood Ratio Test / G²)")
}

# ── Uji Parsial Wald (GLM-based) ────────────────────────────────
wald_html <- function(model) {
  koef <- summary(model)$coefficients
  kbl_h(data.frame(
    Variabel   = rownames(koef),
    B          = round(koef[, 1], 4),
    SE         = round(koef[, 2], 4),
    Wald       = round((koef[, 1] / koef[, 2])^2, 4),
    `p-value`  = round(koef[, 4], 4),
    Kesimpulan = ifelse(koef[, 4] < 0.05, "✅ Signifikan", "❌ Tdk Signifikan"),
    check.names = FALSE
  ), "Uji Parsial (Wald Test)")
}

# ── Hosmer-Lemeshow ──────────────────────────────────────────────
hl_html <- function(model, y) {
  hl <- tryCatch(hoslem.test(y, fitted(model), g = 10), error = function(e) NULL)
  if (is.null(hl)) return("<p><em>⚠️ Hosmer-Lemeshow tidak dapat dihitung.</em></p>")
  kbl_h(data.frame(
    Statistik = c("χ² HL", "df", "p-value", "Keputusan"),
    Nilai = c(
      round(hl$statistic, 3), hl$parameter, round(hl$p.value, 4),
      ifelse(hl$p.value > 0.05,
             "✅ Gagal Tolak H0 — Model FIT",
             "❌ Tolak H0 — TIDAK FIT")
    )
  ), "Goodness of Fit (Hosmer-Lemeshow)")
}

# ── Odds Ratio (GLM-based) ───────────────────────────────────────
or_html <- function(model) {
  or <- exp(coef(model))
  ci <- exp(confint.default(model))
  kbl_h(data.frame(
    Variabel     = names(or),
    `exp(B)`     = round(or, 4),
    `CI 2.5%`    = round(ci[, 1], 4),
    `CI 97.5%`   = round(ci[, 2], 4),
    Interpretasi = ifelse(names(or) == "(Intercept)", "Baseline odds",
                   ifelse(or > 1,
                          "↑ Lebih berisiko tidak bersekolah",
                          "↓ Lebih kecil risiko tidak bersekolah")),
    check.names = FALSE
  ), "Interpretasi Parameter (Odds Ratio)")
}

# ── VIF (Multikolinearitas) ──────────────────────────────────────
vif_html <- function(model, df, xm, method_label = "GLM Biasa") {
  vif_val    <- tryCatch(car::vif(model), error = function(e) NULL)
  note_extra <- ""
  if (is.null(vif_val)) {
    fm_vif  <- as.formula(paste("status_miskin ~", paste(xm, collapse = " + ")))
    mdl_vif <- tryCatch(glm(fm_vif, data = df, family = binomial), error = function(e) NULL)
    vif_val <- if (!is.null(mdl_vif)) tryCatch(car::vif(mdl_vif), error = function(e) NULL) else NULL
    note_extra <- paste0(
      '<p><em><small>&#9432; VIF dihitung ulang via GLM biasa (binomial) karena model utama (',
      method_label, ') tidak kompatibel langsung dengan car::vif(). ',
      'VIF adalah properti matriks desain X — hasil tetap valid.</small></em></p>')
  }
  if (is.null(vif_val))
    return('<p><em>⚠️ VIF tidak dapat dihitung.</em></p>')

  if (is.matrix(vif_val)) {
    vif_vec  <- setNames(vif_val[, "GVIF^(1/(2*Df))"]^2, rownames(vif_val))
    col_note <- " (GVIF disesuaikan untuk faktor multi-level)"
  } else {
    vif_vec  <- vif_val
    col_note <- ""
  }

  vif_df <- data.frame(
    Variabel = names(vif_vec),
    VIF      = round(vif_vec, 4),
    Kategori = ifelse(vif_vec > 10, "❌ Parah (>10)",
               ifelse(vif_vec > 5,  "⚠️ Sedang (5–10)",
                                    "✅ Tidak Ada (<5)")),
    check.names = FALSE
  )

  status <- if (any(vif_vec > 10, na.rm = TRUE)) {
    '<p style="color:#c62828;font-weight:600;">⚠️ Terdeteksi multikolinearitas parah.</p>'
  } else if (any(vif_vec > 5, na.rm = TRUE)) {
    '<p style="color:#e65100;font-weight:600;">⚠️ Terdeteksi multikolinearitas sedang. Perlu diwaspadai.</p>'
  } else {
    '<p style="color:#2e7d32;font-weight:600;">✅ Tidak terdeteksi masalah multikolinearitas (semua VIF < 5).</p>'
  }

  paste0(status, note_extra, kbl_h(vif_df, paste0("Variance Inflation Factor (VIF)", col_note)))
}

# ── Kriteria Informasi: AIC / AICc / BIC ─────────────────────────
ic_html <- function(model, n, k, method = "glm") {
  if (method == "glm") {
    aic_val <- tryCatch(AIC(model),                error = function(e) NA)
    bic_val <- tryCatch(BIC(model),                error = function(e) NA)
    ll_val  <- tryCatch(as.numeric(logLik(model)), error = function(e) NA)
  } else if (method == "firth") {
    ll_val  <- max(model$loglik)
    aic_val <- -2 * ll_val + 2 * k
    bic_val <- -2 * ll_val + log(n) * k
  } else {
    aic_val <- NA; bic_val <- NA; ll_val <- NA
  }

  aicc_val <- if (!is.na(aic_val) && (n - k - 1) > 0) {
    aic_val + (2 * k^2 + 2 * k) / (n - k - 1)
  } else NA

  if (method == "quasi") {
    phi_est  <- tryCatch(summary(model)$dispersion, error = function(e) 1)
    dev_val  <- model$deviance
    df_r     <- model$df.residual
    qaic_val <- round(dev_val / phi_est + 2 * k, 3)
    rows <- paste0(
      '<tr><td><strong>QAIC</strong></td><td>', qaic_val,
      '</td><td>Quasi-AIC (φ̂ = ', round(phi_est, 3), ')</td></tr>',
      '<tr><td>Deviance Residual</td><td>', round(dev_val, 3),
      '</td><td>df = ', df_r, '</td></tr>',
      '<tr><td>Dispersi (φ̂)</td><td>', round(phi_est, 3),
      '</td><td>Estimasi dispersi quasibinomial</td></tr>',
      '<tr><td style="color:#888;font-style:italic">AIC / BIC</td>',
      '<td style="color:#888">N/A</td>',
      '<td style="color:#888;font-style:italic">Tidak terdefinisi untuk quasibinomial</td></tr>'
    )
  } else {
    rows <- paste0(
      '<tr><td><strong>AIC</strong></td><td>',  round(aic_val,  3),
      '</td><td>Semakin kecil semakin baik</td></tr>',
      '<tr><td><strong>AICc</strong></td><td>', round(aicc_val, 3),
      '</td><td>Koreksi utk sampel kecil</td></tr>',
      '<tr><td><strong>BIC</strong></td><td>',  round(bic_val,  3),
      '</td><td>Penalti lebih besar pada parameter</td></tr>',
      '<tr><td>Log-Likelihood</td><td>',         round(ll_val,  3),
      '</td><td></td></tr>'
    )
  }

  paste0(
    '<table class="table table-striped table-condensed" style="font-size:13px;width:auto;margin-bottom:0">',
    '<thead><tr>',
    '<th style="min-width:130px">Kriteria</th>',
    '<th style="min-width:90px">Nilai</th>',
    '<th>Keterangan</th></tr></thead><tbody>',
    rows,
    '<tr><td>k (parameter)</td><td>', k, '</td><td>Termasuk intercept</td></tr>',
    '<tr><td>n (observasi)</td><td>', n, '</td><td></td></tr>',
    '</tbody></table>'
  )
}

4.2 Fungsi Evaluasi Model

# ── A. Perfect Separation Check ──────────────────────────────────
separation_html <- function(coefs, ses, is_firth = FALSE) {
  if (is_firth) {
    return(paste0(
      '<div class="eval-sub">',
      '<h6>Perfect Separation Check</h6>',
      '<p class="sep-ok">&#10003; Firth (Penalized Likelihood) dirancang khusus ',
      'menangani complete/quasi-complete separation — hasil tetap valid tanpa bias.</p>',
      '</div>'
    ))
  }
  flag_b  <- abs(coefs) > 10
  flag_se <- !is.na(ses) & ses > 5
  flag    <- flag_b | flag_se
  status  <- if (any(flag, na.rm = TRUE)) {
    '<p class="sep-flag">&#9888; Terdeteksi potensi separation pada variabel berikut:</p>'
  } else {
    '<p class="sep-ok">&#10003; Tidak terdeteksi perfect/quasi-complete separation.</p>'
  }
  tbl <- kbl_h(data.frame(
    Variabel   = names(coefs),
    `|B|>10`   = ifelse(flag_b,  "&#9888; Ya", "&#10003; Tidak"),
    `SE>5`     = ifelse(flag_se, "&#9888; Ya", "&#10003; Tidak"),
    Status     = ifelse(flag,
                        '<span class="sep-flag">&#9888; Potensi Separation</span>',
                        '<span class="sep-ok">&#10003; Aman</span>'),
    check.names = FALSE
  ), "Perfect Separation Check")
  paste0('<div class="eval-sub"><h6>Perfect Separation Check</h6>', status, tbl, '</div>')
}

# ── B. Confusion Matrix + Metrics ────────────────────────────────
conf_metrics <- function(y_true, y_prob, cutoff) {
  y_pred <- as.integer(y_prob >= cutoff)
  tp <- sum(y_true == 1 & y_pred == 1)
  tn <- sum(y_true == 0 & y_pred == 0)
  fp <- sum(y_true == 0 & y_pred == 1)
  fn <- sum(y_true == 1 & y_pred == 0)
  acc  <- ifelse((tp + tn + fp + fn) > 0, (tp + tn) / (tp + tn + fp + fn), NA)
  sens <- ifelse((tp + fn) > 0, tp / (tp + fn), NA)
  spec <- ifelse((tn + fp) > 0, tn / (tn + fp), NA)
  prec <- ifelse((tp + fp) > 0, tp / (tp + fp), NA)
  f1   <- ifelse(!is.na(prec) & !is.na(sens) & (prec + sens) > 0,
                 2 * prec * sens / (prec + sens), NA)
  list(tp = tp, tn = tn, fp = fp, fn = fn,
       acc = acc, sens = sens, spec = spec, prec = prec, f1 = f1)
}

conf_block_html <- function(y_true, y_prob, cutoff, label_class, label_text) {
  m   <- conf_metrics(y_true, y_prob, cutoff)
  pct <- function(x) paste0(round(x * 100, 2), " %")
  cm_tbl <- kbl_h(data.frame(
    ` `          = c("Pred: Tidak Bersekolah (0)", "Pred: Bersekolah (1)"),
    `Aktual: 0`  = c(m$tn, m$fp),
    `Aktual: 1`  = c(m$fn, m$tp),
    check.names  = FALSE
  ), paste0("Confusion Matrix — ", label_text))
  chips <- paste0(
    '<div class="metric-row">',
    '<span class="metric-chip">Accuracy: ',    pct(m$acc),  '</span>',
    '<span class="metric-chip">Sensitivity: ', pct(m$sens), '</span>',
    '<span class="metric-chip">Specificity: ', pct(m$spec), '</span>',
    '<span class="metric-chip">Precision: ',   pct(m$prec), '</span>',
    '<span class="metric-chip">F1-Score: ',    pct(m$f1),   '</span>',
    '</div>'
  )
  paste0(
    '<div class="cm-box">',
    '<span class="cm-label ', label_class, '">', label_text, '</span>',
    cm_tbl, chips,
    '</div>'
  )
}

cm_html <- function(y_true, y_prob) {
  roc_obj <- tryCatch(pROC::roc(y_true, y_prob, quiet = TRUE), error = function(e) NULL)
  cut_you <- if (!is.null(roc_obj)) {
    co <- pROC::coords(roc_obj, "best", best.method = "youden", ret = "threshold")
    as.numeric(co[1, 1])
  } else 0.5

  b50 <- conf_block_html(y_true, y_prob, 0.5,     "cut50",  "Cutoff 0.5")
  byo <- conf_block_html(y_true, y_prob, cut_you, "cutyou",
                         paste0("Youden Optimal (", round(cut_you, 3), ")"))
  paste0('<div class="eval-sub"><h6>Confusion Matrix &amp; Metrik Klasifikasi</h6>',
         '<div class="cm-wrap">', b50, byo, '</div></div>')
}

# ── C. ROC-AUC + Toggle Plot ─────────────────────────────────────
roc_html <- function(y_true, y_prob, plot_id) {
  roc_obj <- tryCatch(pROC::roc(y_true, y_prob, quiet = TRUE), error = function(e) NULL)
  if (is.null(roc_obj))
    return('<div class="eval-sub"><h6>ROC-AUC</h6><p><em>ROC tidak dapat dihitung.</em></p></div>')

  auc_val <- as.numeric(pROC::auc(roc_obj))
  auc_cat <- if (auc_val >= 0.9) list(cls = "auc-great", txt = "Sangat Baik")
        else if (auc_val >= 0.8) list(cls = "auc-good",  txt = "Baik")
        else if (auc_val >= 0.7) list(cls = "auc-fair",  txt = "Cukup")
        else                     list(cls = "auc-poor",  txt = "Kurang")

  tmp <- tempfile(fileext = ".png")
  png(tmp, width = 400, height = 400, res = 90)
  plot(roc_obj, col = "#1565c0", lwd = 2.5,
       main = paste0("ROC Curve\nAUC = ", round(auc_val, 4)),
       cex.main = 0.9, cex.axis = 0.8, cex.lab = 0.85)
  abline(a = 0, b = 1, lty = 2, col = "gray60")
  dev.off()
  b64 <- knitr::image_uri(tmp)
  unlink(tmp)

  pid <- gsub("[^a-zA-Z0-9]", "_", plot_id)
  paste0(
    '<div class="eval-sub"><h6>ROC-AUC</h6>',
    '<span class="auc-badge ', auc_cat$cls, '">AUC = ', round(auc_val, 4), '</span>',
    '<strong>', auc_cat$txt, '</strong>',
    ' &nbsp;|&nbsp; Skala: 0.9–1.0 Sangat Baik | 0.8–0.9 Baik | 0.7–0.8 Cukup | &lt;0.7 Kurang<br>',
    '<button class="btn-roc" onclick="toggleRoc(\'', pid, '\')">',
    '&#128200; Tampilkan / Sembunyikan Plot ROC</button>',
    '<div class="roc-plot-wrap" id="roc_', pid, '">',
    '<img src="', b64, '" alt="ROC Curve">',
    '</div></div>'
  )
}

# ── D. Full Evaluasi Section (GLM-based) ─────────────────────────
evaluasi_html <- function(model, y_true, y_prob, plot_id, is_firth = FALSE) {
  coefs <- coef(model)
  ses   <- tryCatch(sqrt(diag(vcov(model))), error = function(e) rep(NA, length(coefs)))
  paste0(
    '<div class="eval-wrap">',
    separation_html(coefs, ses, is_firth),
    cm_html(y_true, y_prob),
    roc_html(y_true, y_prob, plot_id),
    '</div>'
  )
}

# ── D2. Evaluasi SMOTE (2 data: asli + SMOTE) ────────────────────
evaluasi_smote_html <- function(model,
                                 y_true_ori, y_prob_ori,
                                 y_true_sm,  y_prob_sm,
                                 plot_id) {
  coefs <- coef(model)
  ses   <- tryCatch(sqrt(diag(vcov(model))), error = function(e) rep(NA, length(coefs)))
  pid   <- gsub("[^a-zA-Z0-9]", "_", plot_id)

  panel_ori <- paste0(
    '<div class="smote-panel active" id="panel_ori_', pid, '">',
    cm_html(y_true_ori, y_prob_ori),
    roc_html(y_true_ori, y_prob_ori, paste0(plot_id, "_ori")),
    '</div>'
  )
  panel_sm <- paste0(
    '<div class="smote-panel" id="panel_sm_', pid, '">',
    cm_html(y_true_sm, y_prob_sm),
    roc_html(y_true_sm, y_prob_sm, paste0(plot_id, "_smote")),
    '</div>'
  )
  tabs <- paste0(
    '<div class="smote-tabs">',
    '<button class="smote-tab-btn active" ',
    paste0('onclick="switchSmote(\'', pid, '\',\'ori\')">&#128202; Data Asli (Fair)</button>'),
    '<button class="smote-tab-btn" ',
    paste0('onclick="switchSmote(\'', pid, '\',\'sm\')">&#129512; Data SMOTE (In-Sample)</button>'),
    '</div>'
  )
  paste0(
    '<div class="eval-wrap">',
    separation_html(coefs, ses, FALSE),
    '<div class="eval-sub"><h6>Confusion Matrix, Metrik &amp; ROC — Pilih Data</h6>',
    tabs, panel_ori, panel_sm,
    '</div></div>'
  )
}

4.3 Fungsi Analisis — Reglog Biasa

run_biasa <- function(data, nama_dataset) {
  p   <- prep_data(data)
  df  <- p$df; xm <- p$xvars_model; fm <- p$formula_mod

  if (length(xm) == 0) return("<p>⚠️ Tidak ada variabel valid.</p>")

  mdl  <- tryCatch(glm(fm, data = df, family = binomial), error = function(e) NULL)
  mdl0 <- tryCatch(glm(status_miskin ~ 1, data = df, family = binomial), error = function(e) NULL)
  if (is.null(mdl)) return("<p>⚠️ Model gagal di-fit.</p>")

  info <- paste0('<p><strong>n valid:</strong> ', nrow(df), ' obs.',
    if (length(p$konstan) > 0) paste0(' | <em>Konstan (dikeluarkan): ',
                                       paste(p$konstan, collapse = ", "), '</em>') else '',
    '</p>')

  k_biasa <- length(coef(mdl))
  n_biasa <- nrow(df)
  pid     <- paste0("biasa-", gsub("[^a-zA-Z0-9]", "_", nama_dataset))

  paste0(info,
    sec("📊", "EDA — Distribusi Y",               eda_html(df)),
    sec("📐", "Uji Simultan (G²)",                simultan_html(mdl, mdl0, xm)),
    sec("📋", "Uji Parsial (Wald Test)",           wald_html(mdl)),
    sec("🔗", "Multikolinearitas (VIF)",           vif_html(mdl, df, xm, "Reglog Biasa")),
    sec("✅", "Goodness of Fit (H-L Test)",        hl_html(mdl, df$status_miskin)),
    sec("📊", "Kriteria Informasi (AIC/AICc/BIC)", ic_html(mdl, n_biasa, k_biasa, "glm")),
    sec("📈", "Interpretasi Parameter (OR)",       or_html(mdl)),
    sec("🔍", "Evaluasi Model",
        evaluasi_html(mdl, df$status_miskin, fitted(mdl), pid))
  )
}

4.4 Fungsi Analisis — Firth (Penalized Likelihood)

run_firth <- function(data, nama_dataset) {
  p  <- prep_data(data)
  df <- p$df; xm <- p$xvars_model; fm <- p$formula_mod

  if (length(xm) == 0) return("<p>⚠️ Tidak ada variabel valid.</p>")

  mdl <- tryCatch(logistf(fm, data = df), error = function(e) NULL)
  if (is.null(mdl)) return("<p>⚠️ Model Firth gagal di-fit.</p>")

  # Uji Simultan — penalized LRT
  G2     <- 2 * abs(diff(mdl$loglik))
  df_G2  <- length(xm)
  chi_k  <- qchisq(0.95, df_G2)
  pval   <- pchisq(G2, df_G2, lower.tail = FALSE)
  sim_tbl <- kbl_h(data.frame(
    Statistik = c("LogLik Null", "LogLik Model", "G² Hitung", "df",
                  "χ² Tabel (α=5%)", "p-value", "Keputusan"),
    Nilai = c(
      round(min(mdl$loglik), 3), round(max(mdl$loglik), 3),
      round(G2, 3), df_G2, round(chi_k, 3), round(pval, 4),
      ifelse(G2 > chi_k, "✅ Tolak H0 — Signifikan", "❌ Gagal Tolak H0")
    )
  ), "Uji Simultan (Penalized LRT)")

  # Koefisien & uji parsial
  coefs   <- mdl$coefficients
  se_raw  <- sqrt(diag(mdl$var))
  se      <- setNames(se_raw[seq_along(coefs)], names(coefs))
  pvals   <- mdl$prob[names(coefs)]
  chi_sq  <- (coefs / se)^2

  wald_tbl <- kbl_h(data.frame(
    Variabel   = names(coefs),
    B          = round(coefs, 4),
    SE         = round(se, 4),
    `Chi-sq`   = round(chi_sq, 4),
    `p-value`  = round(pvals, 4),
    Kesimpulan = ifelse(pvals < 0.05, "✅ Signifikan", "❌ Tdk Signifikan"),
    check.names = FALSE
  ), "Uji Per Variabel (Penalized LRT)")

  # Odds Ratio dengan Firth CI
  or_tbl <- kbl_h(data.frame(
    Variabel     = names(coefs),
    `exp(B)`     = round(exp(coefs), 4),
    `CI 2.5%`    = round(exp(mdl$ci.lower), 4),
    `CI 97.5%`   = round(exp(mdl$ci.upper), 4),
    Interpretasi = ifelse(names(coefs) == "(Intercept)", "Baseline odds",
                   ifelse(exp(coefs) > 1,
                          "↑ Lebih berisiko tidak bersekolah",
                          "↓ Lebih kecil risiko tidak bersekolah")),
    check.names = FALSE
  ), "Interpretasi Parameter (Odds Ratio — Firth CI)")

  info <- paste0('<p><strong>n valid:</strong> ', nrow(df),
    ' obs. | <em>Firth menangani quasi-complete separation & sampel kecil</em></p>')

  # GoF Hosmer-Lemeshow
  hl_firth <- tryCatch(
    hoslem.test(df$status_miskin, mdl$predict, g = 10),
    error = function(e) NULL
  )
  hl_firth_tbl <- if (is.null(hl_firth)) {
    "<p><em>⚠️ Hosmer-Lemeshow tidak dapat dihitung.</em></p>"
  } else {
    kbl_h(data.frame(
      Statistik = c("χ² HL", "df", "p-value", "Keputusan"),
      Nilai = c(
        round(hl_firth$statistic, 3),
        hl_firth$parameter,
        round(hl_firth$p.value, 4),
        ifelse(hl_firth$p.value > 0.05,
               "✅ Gagal Tolak H0 — Model FIT",
               "❌ Tolak H0 — TIDAK FIT")
      )
    ), "Goodness of Fit (Hosmer-Lemeshow)")
  }

  k_firth <- length(coefs)
  n_firth <- nrow(df)
  pid     <- paste0("firth-", gsub("[^a-zA-Z0-9]", "_", nama_dataset))

  paste0(info,
    sec("📊", "EDA — Distribusi Y",                     eda_html(df)),
    sec("📐", "Uji Simultan (Penalized LRT)",            sim_tbl),
    sec("📋", "Uji Per Variabel",                        wald_tbl),
    sec("🔗", "Multikolinearitas (VIF)",                 vif_html(mdl, df, xm, "Firth")),
    sec("✅", "Goodness of Fit (H-L Test)",              hl_firth_tbl),
    sec("📊", "Kriteria Informasi (AIC/AICc/BIC)",       ic_html(mdl, n_firth, k_firth, "firth")),
    sec("📈", "Interpretasi Parameter (OR — Firth CI)",  or_tbl),
    sec("🔍", "Evaluasi Model",
        evaluasi_html(mdl, df$status_miskin, mdl$predict, pid, is_firth = TRUE))
  )
}

4.5 Fungsi Analisis — SMOTE

run_smote <- function(data, nama_dataset) {
  p  <- prep_data(data)
  df <- p$df; xm <- p$xvars_model; fm <- p$formula_mod

  if (length(xm) == 0) return("<p>⚠️ Tidak ada variabel valid.</p>")

  df_sm <- tryCatch({
    res <- smotefamily::SMOTE(
      X        = df[, xm, drop = FALSE],
      target   = as.character(df$status_miskin),
      K        = 5,
      dup_size = 0
    )$data
    res$status_miskin <- as.numeric(as.character(res$class))
    res$class         <- NULL
    res[, c("status_miskin", xm)]
  }, error = function(e) NULL)

  if (is.null(df_sm))
    return("<p>⚠️ SMOTE gagal — kemungkinan n terlalu kecil atau kelas sangat tidak seimbang.</p>")

  mdl  <- tryCatch(glm(fm, data = df_sm, family = binomial), error = function(e) NULL)
  mdl0 <- tryCatch(glm(status_miskin ~ 1, data = df_sm, family = binomial), error = function(e) NULL)
  if (is.null(mdl)) return("<p>⚠️ Model GLM setelah SMOTE gagal.</p>")

  # EDA before & after
  t_ori   <- table(df$status_miskin)
  t_sm    <- table(df_sm$status_miskin)
  eda_tbl <- kbl_h(data.frame(
    Kategori  = c("Tidak Bersekolah (0)", "Bersekolah (1)"),
    `N Asal`  = as.integer(c(t_ori["0"], t_ori["1"])),
    `N SMOTE` = as.integer(c(t_sm["0"], t_sm["1"])),
    check.names = FALSE
  ), "Distribusi Y Sebelum & Sesudah SMOTE")

  info <- paste0('<p><strong>n asli:</strong> ', nrow(df),
    ' | <strong>n setelah SMOTE:</strong> ', nrow(df_sm), '</p>')

  k_smote <- length(coef(mdl))
  n_smote <- nrow(df_sm)
  pid     <- paste0("smote-", gsub("[^a-zA-Z0-9]", "_", nama_dataset))

  paste0(info,
    sec("📊", "EDA — Distribusi Y (Sebelum & Sesudah SMOTE)", eda_tbl),
    sec("📐", "Uji Simultan (G²)",       simultan_html(mdl, mdl0, xm)),
    sec("📋", "Uji Parsial (Wald Test)",  wald_html(mdl)),
    sec("🔗", "Multikolinearitas (VIF)",
        paste0(vif_html(mdl, df_sm, xm, "SMOTE"),
               '<p><em><small>&#9432; VIF dihitung dari data SMOTE (in-sample). ',
               'VIF pada data asli mungkin sedikit berbeda.</small></em></p>')),
    sec("✅", "Goodness of Fit (H-L Test)", hl_html(mdl, df_sm$status_miskin)),
    sec("📊", "Kriteria Informasi (AIC/AICc/BIC)",
        paste0(ic_html(mdl, n_smote, k_smote, "glm"),
               '<p><em><small>&#9432; AIC/AICc/BIC dihitung dari data SMOTE (n = ',
               n_smote, '). Gunakan sebagai perbandingan; evaluasi fair tetap di data asli.</small></em></p>')),
    sec("📈", "Interpretasi Parameter (OR)", or_html(mdl)),
    sec("🔍", "Evaluasi Model",
        evaluasi_smote_html(
          mdl,
          df$status_miskin,    predict(mdl, newdata = df,    type = "response"),
          df_sm$status_miskin, predict(mdl, newdata = df_sm, type = "response"),
          pid
        ))
  )
}

4.6 Fungsi Analisis — Class Weight

run_weight <- function(data, nama_dataset) {
  p  <- prep_data(data)
  df <- p$df; xm <- p$xvars_model; fm <- p$formula_mod

  if (length(xm) == 0) return("<p>⚠️ Tidak ada variabel valid.</p>")

  n  <- nrow(df)
  n1 <- sum(df$status_miskin == 1)
  n0 <- sum(df$status_miskin == 0)

  # Simpan bobot sebagai kolom di df agar glm selalu menemukannya
  df$.w <- ifelse(df$status_miskin == 1, n / (2 * n1), n / (2 * n0))

  ctrl <- glm.control(maxit = 200, epsilon = 1e-8)

  mdl <- suppressWarnings(tryCatch(
    glm(fm, data = df, family = quasibinomial(link = "logit"),
        weights = .w, control = ctrl),
    error = function(e) NULL
  ))

  mdl0 <- suppressWarnings(tryCatch(
    glm(status_miskin ~ 1, data = df,
        family  = quasibinomial(link = "logit"),
        weights = .w, control = ctrl),
    error = function(e) NULL
  ))

  if (is.null(mdl)) return("<p>⚠️ Model Class Weight gagal di-fit.</p>")

  # EDA + weight info
  t   <- table(df$status_miskin)
  pct <- round(prop.table(t) * 100, 2)
  eda_tbl <- kbl_h(data.frame(
    Kategori       = c("Tidak Bersekolah (0)", "Bersekolah (1)"),
    N              = as.integer(c(t["0"], t["1"])),
    Persen         = paste0(c(pct["0"], pct["1"]), " %"),
    `Class Weight` = c(round(n / (2 * n0), 4), round(n / (2 * n1), 4)),
    check.names    = FALSE
  ), "Distribusi Y + Class Weight")

  info <- paste0(
    '<p><strong>n valid:</strong> ', nrow(df), ' obs.',
    ' | <strong>w bersekolah:</strong> ', round(n / (2 * n1), 3),
    ' | <strong>w tidak bersekolah:</strong> ', round(n / (2 * n0), 3),
    ' | <em>family: quasibinomial</em></p>'
  )

  k_weight <- length(coef(mdl))
  n_weight <- nrow(df)
  pid      <- paste0("weight-", gsub("[^a-zA-Z0-9]", "_", nama_dataset))

  paste0(info,
    sec("📊", "EDA — Distribusi Y + Class Weight",    eda_tbl),
    sec("📐", "Uji Simultan (G²)",                    simultan_html(mdl, mdl0, xm)),
    sec("📋", "Uji Parsial (Wald Test)",               wald_html(mdl)),
    sec("🔗", "Multikolinearitas (VIF)",               vif_html(mdl, df, xm, "Class Weight")),
    sec("✅", "Goodness of Fit (H-L Test)",            hl_html(mdl, df$status_miskin)),
    sec("📊", "Kriteria Informasi (QAIC/Deviance)",    ic_html(mdl, n_weight, k_weight, "quasi")),
    sec("📈", "Interpretasi Parameter (OR)",           or_html(mdl)),
    sec("🔍", "Evaluasi Model",
        evaluasi_html(mdl, df$status_miskin, fitted(mdl), pid))
  )
}

5 Hasil Analisis Interaktif

🔧 Filter Hasil Analisis

Metode Analisis

Menghitung...
⚠ Tidak ada hasil yang sesuai pilihan.
Centang minimal satu Metode.

Disabilitas 13–15 Thn | Sulawesi & Kalimantan | SUSENAS 2023

Reglog Biasa

Reglog Biasa

n valid: 761 obs.

📊 EDA — Distribusi Y
Distribusi Y — Partisipasi Sekolah
Kategori N Persen
Tidak Bersekolah (0) 164 21.55 %
Bersekolah (1) 597 78.45 %
📐 Uji Simultan (G²)
Uji Simultan (Likelihood Ratio Test / G²)
Statistik Nilai
-2LL Null 793.207
-2LL Model 666.638
G² Hitung 126.569
df 9
χ² Tabel (α=5%) 16.919
p-value 0
Keputusan ✅ Tolak H0 — Model Signifikan
📋 Uji Parsial (Wald Test)
Uji Parsial (Wald Test)
Variabel B SE Wald p-value Kesimpulan
(Intercept) (Intercept) -0.3493 1.1236 0.0966 0.7559 ❌ Tdk Signifikan
X1 X1 0.0400 0.1945 0.0422 0.8372 ❌ Tdk Signifikan
X2 X2 -1.1185 0.2313 23.3817 0.0000 ✅ Signifikan
X3 X3 0.4020 0.1250 10.3486 0.0013 ✅ Signifikan
X4 X4 -0.5098 0.3041 2.8108 0.0936 ❌ Tdk Signifikan
X5 X5 0.5269 0.2264 5.4158 0.0200 ✅ Signifikan
X6 X6 0.0194 0.2137 0.0083 0.9275 ❌ Tdk Signifikan
X7 X7 0.4479 0.2199 4.1501 0.0416 ✅ Signifikan
X8 X8 1.7780 0.4765 13.9202 0.0002 ✅ Signifikan
X9 X9 0.1522 0.2801 0.2952 0.5869 ❌ Tdk Signifikan
🔗 Multikolinearitas (VIF)

✅ Tidak terdeteksi masalah multikolinearitas (semua VIF < 5).

Variance Inflation Factor (VIF)
Variabel VIF Kategori
X1 X1 1.0057 ✅ Tidak Ada (<5)
X2 X2 1.2219 ✅ Tidak Ada (<5)
X3 X3 1.2166 ✅ Tidak Ada (<5)
X4 X4 1.0194 ✅ Tidak Ada (<5)
X5 X5 1.1483 ✅ Tidak Ada (<5)
X6 X6 1.2236 ✅ Tidak Ada (<5)
X7 X7 1.2310 ✅ Tidak Ada (<5)
X8 X8 1.0213 ✅ Tidak Ada (<5)
X9 X9 1.0399 ✅ Tidak Ada (<5)
✅ Goodness of Fit (H-L Test)
Goodness of Fit (Hosmer-Lemeshow)
Statistik Nilai
χ² HL 3.414
df 8
p-value 0.9058
Keputusan ✅ Gagal Tolak H0 — Model FIT
📊 Kriteria Informasi (AIC/AICc/BIC)
KriteriaNilaiKeterangan
AIC686.638Semakin kecil semakin baik
AICc686.931Koreksi utk sampel kecil
BIC732.984Penalti lebih besar pada parameter
Log-Likelihood-333.319
k (parameter)10Termasuk intercept
n (observasi)761
📈 Interpretasi Parameter (OR)
Interpretasi Parameter (Odds Ratio)
Variabel exp(B) CI 2.5% CI 97.5% Interpretasi
(Intercept) (Intercept) 0.7052 0.0780 6.3789 Baseline odds
X1 X1 1.0408 0.7108 1.5238 ↑ Lebih berisiko tidak bersekolah
X2 X2 0.3268 0.2077 0.5142 ↓ Lebih kecil risiko tidak bersekolah
X3 X3 1.4949 1.1701 1.9098 ↑ Lebih berisiko tidak bersekolah
X4 X4 0.6006 0.3310 1.0900 ↓ Lebih kecil risiko tidak bersekolah
X5 X5 1.6937 1.0867 2.6396 ↑ Lebih berisiko tidak bersekolah
X6 X6 1.0196 0.6707 1.5501 ↑ Lebih berisiko tidak bersekolah
X7 X7 1.5651 1.0171 2.4082 ↑ Lebih berisiko tidak bersekolah
X8 X8 5.9180 2.3256 15.0594 ↑ Lebih berisiko tidak bersekolah
X9 X9 1.1643 0.6725 2.0159 ↑ Lebih berisiko tidak bersekolah
🔍 Evaluasi Model
Perfect Separation Check

✓ Tidak terdeteksi perfect/quasi-complete separation.

Perfect Separation Check
Variabel |B|>10 SE>5 Status
(Intercept) (Intercept) ✓ Tidak ✓ Tidak ✓ Aman
X1 X1 ✓ Tidak ✓ Tidak ✓ Aman
X2 X2 ✓ Tidak ✓ Tidak ✓ Aman
X3 X3 ✓ Tidak ✓ Tidak ✓ Aman
X4 X4 ✓ Tidak ✓ Tidak ✓ Aman
X5 X5 ✓ Tidak ✓ Tidak ✓ Aman
X6 X6 ✓ Tidak ✓ Tidak ✓ Aman
X7 X7 ✓ Tidak ✓ Tidak ✓ Aman
X8 X8 ✓ Tidak ✓ Tidak ✓ Aman
X9 X9 ✓ Tidak ✓ Tidak ✓ Aman
Confusion Matrix & Metrik Klasifikasi
Cutoff 0.5
Confusion Matrix — Cutoff 0.5
Aktual: 0 Aktual: 1
Pred: Tidak Bersekolah (0) 38 27
Pred: Bersekolah (1) 126 570
Accuracy: 79.89 %Sensitivity: 95.48 %Specificity: 23.17 %Precision: 81.9 %F1-Score: 88.17 %
Youden Optimal (0.779)
Confusion Matrix — Youden Optimal (0.779)
Aktual: 0 Aktual: 1
Pred: Tidak Bersekolah (0) 122 191
Pred: Bersekolah (1) 42 406
Accuracy: 69.38 %Sensitivity: 68.01 %Specificity: 74.39 %Precision: 90.62 %F1-Score: 77.7 %
ROC-AUC
AUC = 0.7733Cukup  |  Skala: 0.9–1.0 Sangat Baik | 0.8–0.9 Baik | 0.7–0.8 Cukup | <0.7 Kurang
ROC Curve

Disabilitas 13–15 Thn | Sulawesi & Kalimantan | SUSENAS 2023

Firth

Firth

n valid: 761 obs. | Firth menangani quasi-complete separation & sampel kecil

📊 EDA — Distribusi Y
Distribusi Y — Partisipasi Sekolah
Kategori N Persen
Tidak Bersekolah (0) 164 21.55 %
Bersekolah (1) 597 78.45 %
📐 Uji Simultan (Penalized LRT)
Uji Simultan (Penalized LRT)
Statistik Nilai
LogLik Null -379.436
LogLik Model -317.742
G² Hitung 123.387
df 9
χ² Tabel (α=5%) 16.919
p-value 0
Keputusan ✅ Tolak H0 — Signifikan
📋 Uji Per Variabel
Uji Per Variabel (Penalized LRT)
Variabel B SE Chi-sq p-value Kesimpulan
(Intercept) (Intercept) -0.3003 1.0949 0.0752 0.7857 ❌ Tdk Signifikan
X1 X1 0.0397 0.1916 0.0430 0.8368 ❌ Tdk Signifikan
X2 X2 -1.0982 0.2270 23.3960 0.0000 ✅ Signifikan
X3 X3 0.3962 0.1229 10.3822 0.0014 ✅ Signifikan
X4 X4 -0.4856 0.2974 2.6662 0.0949 ❌ Tdk Signifikan
X5 X5 0.5162 0.2228 5.3668 0.0197 ✅ Signifikan
X6 X6 0.0204 0.2104 0.0094 0.9231 ❌ Tdk Signifikan
X7 X7 0.4397 0.2164 4.1264 0.0425 ✅ Signifikan
X8 X8 1.6802 0.4499 13.9453 0.0000 ✅ Signifikan
X9 X9 0.1408 0.2748 0.2623 0.6087 ❌ Tdk Signifikan
🔗 Multikolinearitas (VIF)

✅ Tidak terdeteksi masalah multikolinearitas (semua VIF < 5).

ⓘ VIF dihitung ulang via GLM biasa (binomial) karena model utama (Firth) tidak kompatibel langsung dengan car::vif(). VIF adalah properti matriks desain X — hasil tetap valid.

Variance Inflation Factor (VIF)
Variabel VIF Kategori
X1 X1 1.0057 ✅ Tidak Ada (<5)
X2 X2 1.2219 ✅ Tidak Ada (<5)
X3 X3 1.2166 ✅ Tidak Ada (<5)
X4 X4 1.0194 ✅ Tidak Ada (<5)
X5 X5 1.1483 ✅ Tidak Ada (<5)
X6 X6 1.2236 ✅ Tidak Ada (<5)
X7 X7 1.2310 ✅ Tidak Ada (<5)
X8 X8 1.0213 ✅ Tidak Ada (<5)
X9 X9 1.0399 ✅ Tidak Ada (<5)
✅ Goodness of Fit (H-L Test)
Goodness of Fit (Hosmer-Lemeshow)
Statistik Nilai
χ² HL 3.222
df 8
p-value 0.9197
Keputusan ✅ Gagal Tolak H0 — Model FIT
📊 Kriteria Informasi (AIC/AICc/BIC)
KriteriaNilaiKeterangan
AIC655.485Semakin kecil semakin baik
AICc655.778Koreksi utk sampel kecil
BIC701.831Penalti lebih besar pada parameter
Log-Likelihood-317.742
k (parameter)10Termasuk intercept
n (observasi)761
📈 Interpretasi Parameter (OR — Firth CI)
Interpretasi Parameter (Odds Ratio — Firth CI)
Variabel exp(B) CI 2.5% CI 97.5% Interpretasi
(Intercept) (Intercept) 0.7406 0.0846 6.5021 Baseline odds
X1 X1 1.0405 0.7123 1.5179 ↑ Lebih berisiko tidak bersekolah
X2 X2 0.3335 0.2112 0.5192 ↓ Lebih kecil risiko tidak bersekolah
X3 X3 1.4861 1.1662 1.8960 ↑ Lebih berisiko tidak bersekolah
X4 X4 0.6153 0.3336 1.0852 ↓ Lebih kecil risiko tidak bersekolah
X5 X5 1.6757 1.0850 2.6184 ↑ Lebih berisiko tidak bersekolah
X6 X6 1.0206 0.6729 1.5451 ↑ Lebih berisiko tidak bersekolah
X7 X7 1.5522 1.0150 2.3875 ↑ Lebih berisiko tidak bersekolah
X8 X8 5.3666 2.3929 14.7890 ↑ Lebih berisiko tidak bersekolah
X9 X9 1.1511 0.6770 2.0090 ↑ Lebih berisiko tidak bersekolah
🔍 Evaluasi Model
Perfect Separation Check

✓ Firth (Penalized Likelihood) dirancang khusus menangani complete/quasi-complete separation — hasil tetap valid tanpa bias.

Confusion Matrix & Metrik Klasifikasi
Cutoff 0.5
Confusion Matrix — Cutoff 0.5
Aktual: 0 Aktual: 1
Pred: Tidak Bersekolah (0) 38 27
Pred: Bersekolah (1) 126 570
Accuracy: 79.89 %Sensitivity: 95.48 %Specificity: 23.17 %Precision: 81.9 %F1-Score: 88.17 %
Youden Optimal (0.773)
Confusion Matrix — Youden Optimal (0.773)
Aktual: 0 Aktual: 1
Pred: Tidak Bersekolah (0) 122 191
Pred: Bersekolah (1) 42 406
Accuracy: 69.38 %Sensitivity: 68.01 %Specificity: 74.39 %Precision: 90.62 %F1-Score: 77.7 %
ROC-AUC
AUC = 0.7733Cukup  |  Skala: 0.9–1.0 Sangat Baik | 0.8–0.9 Baik | 0.7–0.8 Cukup | <0.7 Kurang
ROC Curve

Disabilitas 13–15 Thn | Sulawesi & Kalimantan | SUSENAS 2023

SMOTE

SMOTE

n asli: 761 | n setelah SMOTE: 1089

📊 EDA — Distribusi Y (Sebelum & Sesudah SMOTE)
Distribusi Y Sebelum & Sesudah SMOTE
Kategori N Asal N SMOTE
Tidak Bersekolah (0) 164 492
Bersekolah (1) 597 597
📐 Uji Simultan (G²)
Uji Simultan (Likelihood Ratio Test / G²)
Statistik Nilai
-2LL Null 1499.535
-2LL Model 1209.695
G² Hitung 289.84
df 9
χ² Tabel (α=5%) 16.919
p-value 0
Keputusan ✅ Tolak H0 — Model Signifikan
📋 Uji Parsial (Wald Test)
Uji Parsial (Wald Test)
Variabel B SE Wald p-value Kesimpulan
(Intercept) (Intercept) -1.9550 0.8197 5.6882 0.0171 ✅ Signifikan
X1 X1 0.1072 0.1418 0.5719 0.4495 ❌ Tdk Signifikan
X2 X2 -1.1470 0.1651 48.2734 0.0000 ✅ Signifikan
X3 X3 0.3663 0.0915 16.0427 0.0001 ✅ Signifikan
X4 X4 -0.5543 0.2245 6.0984 0.0135 ✅ Signifikan
X5 X5 0.6976 0.1665 17.5430 0.0000 ✅ Signifikan
X6 X6 -0.0955 0.1622 0.3465 0.5561 ❌ Tdk Signifikan
X7 X7 0.4813 0.1670 8.3035 0.0040 ✅ Signifikan
X8 X8 2.1746 0.3391 41.1343 0.0000 ✅ Signifikan
X9 X9 0.2508 0.2165 1.3420 0.2467 ❌ Tdk Signifikan
🔗 Multikolinearitas (VIF)

✅ Tidak terdeteksi masalah multikolinearitas (semua VIF < 5).

Variance Inflation Factor (VIF)
Variabel VIF Kategori
X1 X1 1.0156 ✅ Tidak Ada (<5)
X2 X2 1.2902 ✅ Tidak Ada (<5)
X3 X3 1.2878 ✅ Tidak Ada (<5)
X4 X4 1.0260 ✅ Tidak Ada (<5)
X5 X5 1.2132 ✅ Tidak Ada (<5)
X6 X6 1.3036 ✅ Tidak Ada (<5)
X7 X7 1.2994 ✅ Tidak Ada (<5)
X8 X8 1.0166 ✅ Tidak Ada (<5)
X9 X9 1.0551 ✅ Tidak Ada (<5)

ⓘ VIF dihitung dari data SMOTE (in-sample). VIF pada data asli mungkin sedikit berbeda.

✅ Goodness of Fit (H-L Test)
Goodness of Fit (Hosmer-Lemeshow)
Statistik Nilai
χ² HL 4.915
df 8
p-value 0.7666
Keputusan ✅ Gagal Tolak H0 — Model FIT
📊 Kriteria Informasi (AIC/AICc/BIC)
KriteriaNilaiKeterangan
AIC1229.695Semakin kecil semakin baik
AICc1229.899Koreksi utk sampel kecil
BIC1279.625Penalti lebih besar pada parameter
Log-Likelihood-604.848
k (parameter)10Termasuk intercept
n (observasi)1089

ⓘ AIC/AICc/BIC dihitung dari data SMOTE (n = 1089). Gunakan sebagai perbandingan; evaluasi fair tetap di data asli.

📈 Interpretasi Parameter (OR)
Interpretasi Parameter (Odds Ratio)
Variabel exp(B) CI 2.5% CI 97.5% Interpretasi
(Intercept) (Intercept) 0.1416 0.0284 0.7058 Baseline odds
X1 X1 1.1132 0.8431 1.4698 ↑ Lebih berisiko tidak bersekolah
X2 X2 0.3176 0.2298 0.4389 ↓ Lebih kecil risiko tidak bersekolah
X3 X3 1.4424 1.2057 1.7256 ↑ Lebih berisiko tidak bersekolah
X4 X4 0.5745 0.3700 0.8919 ↓ Lebih kecil risiko tidak bersekolah
X5 X5 2.0089 1.4494 2.7843 ↑ Lebih berisiko tidak bersekolah
X6 X6 0.9090 0.6615 1.2490 ↓ Lebih kecil risiko tidak bersekolah
X7 X7 1.6182 1.1664 2.2449 ↑ Lebih berisiko tidak bersekolah
X8 X8 8.7986 4.5270 17.1010 ↑ Lebih berisiko tidak bersekolah
X9 X9 1.2851 0.8407 1.9645 ↑ Lebih berisiko tidak bersekolah
🔍 Evaluasi Model
Perfect Separation Check

✓ Tidak terdeteksi perfect/quasi-complete separation.

Perfect Separation Check
Variabel |B|>10 SE>5 Status
(Intercept) (Intercept) ✓ Tidak ✓ Tidak ✓ Aman
X1 X1 ✓ Tidak ✓ Tidak ✓ Aman
X2 X2 ✓ Tidak ✓ Tidak ✓ Aman
X3 X3 ✓ Tidak ✓ Tidak ✓ Aman
X4 X4 ✓ Tidak ✓ Tidak ✓ Aman
X5 X5 ✓ Tidak ✓ Tidak ✓ Aman
X6 X6 ✓ Tidak ✓ Tidak ✓ Aman
X7 X7 ✓ Tidak ✓ Tidak ✓ Aman
X8 X8 ✓ Tidak ✓ Tidak ✓ Aman
X9 X9 ✓ Tidak ✓ Tidak ✓ Aman
Confusion Matrix, Metrik & ROC — Pilih Data
Confusion Matrix & Metrik Klasifikasi
Cutoff 0.5
Confusion Matrix — Cutoff 0.5
Aktual: 0 Aktual: 1
Pred: Tidak Bersekolah (0) 111 164
Pred: Bersekolah (1) 53 433
Accuracy: 71.48 %Sensitivity: 72.53 %Specificity: 67.68 %Precision: 89.09 %F1-Score: 79.96 %
Youden Optimal (0.564)
Confusion Matrix — Youden Optimal (0.564)
Aktual: 0 Aktual: 1
Pred: Tidak Bersekolah (0) 124 202
Pred: Bersekolah (1) 40 395
Accuracy: 68.2 %Sensitivity: 66.16 %Specificity: 75.61 %Precision: 90.8 %F1-Score: 76.55 %
ROC-AUC
AUC = 0.7716Cukup  |  Skala: 0.9–1.0 Sangat Baik | 0.8–0.9 Baik | 0.7–0.8 Cukup | <0.7 Kurang
ROC Curve
Confusion Matrix & Metrik Klasifikasi
Cutoff 0.5
Confusion Matrix — Cutoff 0.5
Aktual: 0 Aktual: 1
Pred: Tidak Bersekolah (0) 338 164
Pred: Bersekolah (1) 154 433
Accuracy: 70.8 %Sensitivity: 72.53 %Specificity: 68.7 %Precision: 73.76 %F1-Score: 73.14 %
Youden Optimal (0.454)
Confusion Matrix — Youden Optimal (0.454)
Aktual: 0 Aktual: 1
Pred: Tidak Bersekolah (0) 326 143
Pred: Bersekolah (1) 166 454
Accuracy: 71.63 %Sensitivity: 76.05 %Specificity: 66.26 %Precision: 73.23 %F1-Score: 74.61 %
ROC-AUC
AUC = 0.7809Cukup  |  Skala: 0.9–1.0 Sangat Baik | 0.8–0.9 Baik | 0.7–0.8 Cukup | <0.7 Kurang
ROC Curve

Disabilitas 13–15 Thn | Sulawesi & Kalimantan | SUSENAS 2023

Class Weight

Class Weight

n valid: 761 obs. | w bersekolah: 0.637 | w tidak bersekolah: 2.32 | family: quasibinomial

📊 EDA — Distribusi Y + Class Weight
Distribusi Y + Class Weight
Kategori N Persen Class Weight
Tidak Bersekolah (0) 164 21.55 % 2.3201
Bersekolah (1) 597 78.45 % 0.6374
📐 Uji Simultan (G²)
Uji Simultan (Likelihood Ratio Test / G²)
Statistik Nilai
-2LL Null 1054.97
-2LL Model 866.093
G² Hitung 188.877
df 9
χ² Tabel (α=5%) 16.919
p-value 0
Keputusan ✅ Tolak H0 — Model Signifikan
📋 Uji Parsial (Wald Test)
Uji Parsial (Wald Test)
Variabel B SE Wald p-value Kesimpulan
(Intercept) (Intercept) -1.6916 0.9259 3.3379 0.0681 ❌ Tdk Signifikan
X1 X1 0.0877 0.1667 0.2767 0.5990 ❌ Tdk Signifikan
X2 X2 -1.1282 0.1898 35.3213 0.0000 ✅ Signifikan
X3 X3 0.3783 0.1066 12.5894 0.0004 ✅ Signifikan
X4 X4 -0.4049 0.2539 2.5429 0.1112 ❌ Tdk Signifikan
X5 X5 0.5898 0.1915 9.4826 0.0021 ✅ Signifikan
X6 X6 -0.0321 0.1873 0.0293 0.8641 ❌ Tdk Signifikan
X7 X7 0.3792 0.1868 4.1179 0.0428 ✅ Signifikan
X8 X8 1.8221 0.3366 29.3036 0.0000 ✅ Signifikan
X9 X9 0.0543 0.2462 0.0486 0.8256 ❌ Tdk Signifikan
🔗 Multikolinearitas (VIF)

✅ Tidak terdeteksi masalah multikolinearitas (semua VIF < 5).

Variance Inflation Factor (VIF)
Variabel VIF Kategori
X1 X1 1.0159 ✅ Tidak Ada (<5)
X2 X2 1.2711 ✅ Tidak Ada (<5)
X3 X3 1.2664 ✅ Tidak Ada (<5)
X4 X4 1.0287 ✅ Tidak Ada (<5)
X5 X5 1.1908 ✅ Tidak Ada (<5)
X6 X6 1.2787 ✅ Tidak Ada (<5)
X7 X7 1.2520 ✅ Tidak Ada (<5)
X8 X8 1.0219 ✅ Tidak Ada (<5)
X9 X9 1.0528 ✅ Tidak Ada (<5)
✅ Goodness of Fit (H-L Test)
Goodness of Fit (Hosmer-Lemeshow)
Statistik Nilai
χ² HL 207.302
df 8
p-value 0
Keputusan ❌ Tolak H0 — TIDAK FIT
📊 Kriteria Informasi (QAIC/Deviance)
KriteriaNilaiKeterangan
QAIC892.783Quasi-AIC (φ̂ = 0.992)
Deviance Residual866.093df = 751
Dispersi (φ̂)0.992Estimasi dispersi quasibinomial
AIC / BICN/ATidak terdefinisi untuk quasibinomial
k (parameter)10Termasuk intercept
n (observasi)761
📈 Interpretasi Parameter (OR)
Interpretasi Parameter (Odds Ratio)
Variabel exp(B) CI 2.5% CI 97.5% Interpretasi
(Intercept) (Intercept) 0.1842 0.0300 1.1310 Baseline odds
X1 X1 1.0917 0.7874 1.5135 ↑ Lebih berisiko tidak bersekolah
X2 X2 0.3236 0.2231 0.4695 ↓ Lebih kecil risiko tidak bersekolah
X3 X3 1.4598 1.1845 1.7991 ↑ Lebih berisiko tidak bersekolah
X4 X4 0.6671 0.4055 1.0972 ↓ Lebih kecil risiko tidak bersekolah
X5 X5 1.8037 1.2391 2.6254 ↑ Lebih berisiko tidak bersekolah
X6 X6 0.9685 0.6709 1.3979 ↓ Lebih kecil risiko tidak bersekolah
X7 X7 1.4611 1.0130 2.1072 ↑ Lebih berisiko tidak bersekolah
X8 X8 6.1847 3.1975 11.9628 ↑ Lebih berisiko tidak bersekolah
X9 X9 1.0558 0.6517 1.7105 ↑ Lebih berisiko tidak bersekolah
🔍 Evaluasi Model
Perfect Separation Check

✓ Tidak terdeteksi perfect/quasi-complete separation.

Perfect Separation Check
Variabel |B|>10 SE>5 Status
(Intercept) (Intercept) ✓ Tidak ✓ Tidak ✓ Aman
X1 X1 ✓ Tidak ✓ Tidak ✓ Aman
X2 X2 ✓ Tidak ✓ Tidak ✓ Aman
X3 X3 ✓ Tidak ✓ Tidak ✓ Aman
X4 X4 ✓ Tidak ✓ Tidak ✓ Aman
X5 X5 ✓ Tidak ✓ Tidak ✓ Aman
X6 X6 ✓ Tidak ✓ Tidak ✓ Aman
X7 X7 ✓ Tidak ✓ Tidak ✓ Aman
X8 X8 ✓ Tidak ✓ Tidak ✓ Aman
X9 X9 ✓ Tidak ✓ Tidak ✓ Aman
Confusion Matrix & Metrik Klasifikasi
Cutoff 0.5
Confusion Matrix — Cutoff 0.5
Aktual: 0 Aktual: 1
Pred: Tidak Bersekolah (0) 123 201
Pred: Bersekolah (1) 41 396
Accuracy: 68.2 %Sensitivity: 66.33 %Specificity: 75 %Precision: 90.62 %F1-Score: 76.6 %
Youden Optimal (0.488)
Confusion Matrix — Youden Optimal (0.488)
Aktual: 0 Aktual: 1
Pred: Tidak Bersekolah (0) 122 193
Pred: Bersekolah (1) 42 404
Accuracy: 69.12 %Sensitivity: 67.67 %Specificity: 74.39 %Precision: 90.58 %F1-Score: 77.47 %
ROC-AUC
AUC = 0.7705Cukup  |  Skala: 0.9–1.0 Sangat Baik | 0.8–0.9 Baik | 0.7–0.8 Cukup | <0.7 Kurang
ROC Curve