Regresi logistik biner adalah model regresi untuk respons kategorik dengan tepat dua kategori, yaitu kejadian yang diamati terjadi (\(Y = 1\)) atau tidak terjadi (\(Y = 0\)).
Misalkan respons: \[Y_i \in \{0, 1\}\] Model memodelkan log-odds (logit) dari peluang kejadian sebagai fungsi linear prediktor:
\[ \log\left(\frac{P(Y_i = 1 \mid \mathbf{x}_i)}{P(Y_i = 0 \mid \mathbf{x}_i)}\right) = \beta_0 + \beta_1 x_{i1} + \cdots + \beta_p x_{ip}. \]
Dari persamaan ini, peluang kejadian diperoleh melalui fungsi logistik:
\[ P(Y_i = 1 \mid \mathbf{x}_i) = \frac{\exp(\beta_0 + \beta_1 x_{i1} + \cdots + \beta_p x_{ip})} {1 + \exp(\beta_0 + \beta_1 x_{i1} + \cdots + \beta_p x_{ip})}. \]
Artinya, model membangun satu persamaan logit yang membandingkan peluang kejadian (\(Y = 1\)) terhadap peluang tidak terjadi (\(Y = 0\)) sebagai kategori referensi.
Studi kasus: Analisis sikap publik terhadap hukuman mati berdasarkan karakteristik demografis dan pandangan politik responden.
Tujuan analisis: Membangun model klasifikasi untuk memprediksi apakah seorang responden mendukung (FAVOR) atau menolak (OPPOSE) hukuman mati berdasarkan usia, jenis kelamin, dan pandangan politik.
Hukuman mati merupakan isu sosial yang sangat kontroversial dan banyak diteliti dalam ilmu sosial. Preferensi seseorang terhadap hukuman mati sering kali dikaitkan dengan latar belakang demografis dan orientasi politik. Kasus ini cocok dianalisis dengan regresi logistik biner karena variabel respons bersifat biner:
Model yang digunakan adalah:
\[ \log\left(\frac{p_i}{1-p_i}\right) = \beta_0 + \beta_1 X_{1i} + \beta_2 X_{2i} + \cdots + \beta_k X_{ki}, \]
dengan \(p_i = P(Y_i=1 \mid X_i)\) adalah peluang responden ke-\(i\) mendukung hukuman mati (FAVOR).
Data yang digunakan dalam analisis ini bersumber dari General Social Survey (GSS) tahun 2024, sebuah survei opini publik berskala nasional di Amerika Serikat (United States) yang mengukur tren sosial, sikap, dan karakteristik demografis masyarakat. Fokus amatan pada penelitian ini mencakup sikap responden terhadap hukuman mati beserta karakteristik demografis dan pandangan politik mereka. Dataset yang dianalisis terdiri dari 591 responden dengan 5 variabel.
raw_data <- readxl::read_excel("Data Biner.xlsx", sheet = 1)
ringkasan_data <- data.frame(
Keterangan = c("Jumlah observasi", "Jumlah variabel"),
Nilai = c(nrow(raw_data), ncol(raw_data))
)
knitr::kable(
ringkasan_data,
caption = "Ukuran dataset"
)| Keterangan | Nilai |
|---|---|
| Jumlah observasi | 591 |
| Jumlah variabel | 5 |
Interpretasi output: Dataset berisi 591 observasi
dan 5 variabel. Jumlah observasi ini memadai untuk membangun model
regresi logistik biner dengan tiga prediktor. Variabel respons adalah
cappun, yaitu sikap responden terhadap hukuman mati (FAVOR
atau OPPOSE).
kamus_variabel <- data.frame(
`Nama kolom` = c("id_", "age", "sex", "polviews", "cappun"),
`Keterangan` = c(
"Nomor identifikasi responden",
"Usia responden (tahun)",
"Jenis kelamin responden",
"Pandangan politik responden",
"Sikap terhadap hukuman mati (variabel respons)"
),
`Kategori / Rentang` = c(
"—",
"18 – 89 tahun",
"MALE, FEMALE",
"Conservative, Liberal",
"FAVOR (mendukung), OPPOSE (menolak)"
),
`Tipe dalam analisis` = c(
"ID (tidak dipakai dalam model)",
"Numerik",
"Kategorik",
"Kategorik",
"Respons biner"
),
check.names = FALSE
)
knitr::kable(
kamus_variabel,
caption = "Penjelasan variabel dataset sikap terhadap hukuman mati"
)| Nama kolom | Keterangan | Kategori / Rentang | Tipe dalam analisis |
|---|---|---|---|
| id_ | Nomor identifikasi responden | — | ID (tidak dipakai dalam model) |
| age | Usia responden (tahun) | 18 – 89 tahun | Numerik |
| sex | Jenis kelamin responden | MALE, FEMALE | Kategorik |
| polviews | Pandangan politik responden | Conservative, Liberal | Kategorik |
| cappun | Sikap terhadap hukuman mati (variabel respons) | FAVOR (mendukung), OPPOSE (menolak) | Respons biner |
Interpretasi output: Tabel penjelasan variabel
menjelaskan makna setiap kolom. Variabel age merupakan
satu-satunya prediktor numerik. Variabel sex dan
polviews adalah prediktor kategorik, sedangkan
cappun adalah variabel respons biner yang menjadi target
prediksi.
library(dplyr)
data_bersih <- raw_data |>
mutate(
# Usia sebagai numerik
age = as.numeric(age),
# Jenis kelamin sebagai factor; MALE sebagai referensi
sex = factor(sex, levels = c("MALE", "FEMALE")),
# Pandangan politik sebagai factor; Conservative sebagai referensi
polviews = factor(polviews, levels = c("Conservative", "Liberal")),
# Respons biner: 1 = FAVOR, 0 = OPPOSE
Y_biner = as.integer(cappun == "FAVOR"),
# Label factor untuk visualisasi
Y_label = factor(
ifelse(Y_biner == 1, "FAVOR (Mendukung)", "OPPOSE (Menolak)"),
levels = c("OPPOSE (Menolak)", "FAVOR (Mendukung)")
)
) |>
tidyr::drop_na()
knitr::kable(
head(data_bersih %>%
select(id_, age, sex, polviews, cappun, Y_biner), 8),
caption = "Contoh data setelah transformasi"
)| id_ | age | sex | polviews | cappun | Y_biner |
|---|---|---|---|---|---|
| 7 | 48 | FEMALE | Liberal | OPPOSE | 0 |
| 11 | 23 | FEMALE | Liberal | FAVOR | 1 |
| 24 | 48 | MALE | Conservative | FAVOR | 1 |
| 37 | 60 | FEMALE | Conservative | FAVOR | 1 |
| 45 | 58 | MALE | Conservative | FAVOR | 1 |
| 50 | 65 | MALE | Conservative | FAVOR | 1 |
| 60 | 63 | FEMALE | Liberal | FAVOR | 1 |
| 61 | 57 | FEMALE | Liberal | OPPOSE | 0 |
Interpretasi output: Tabel menampilkan delapan baris
pertama data setelah transformasi. Kolom Y_biner
menunjukkan nilai 1 untuk responden yang mendukung (FAVOR) dan 0 untuk
yang menolak (OPPOSE) hukuman mati. Kategori referensi yang dipilih
adalah MALE untuk sex dan Conservative untuk
polviews, sehingga odds ratio untuk level lain dibandingkan
terhadap kategori-kategori ini.
class_summary <- data_bersih %>%
count(Y_label, name = "Jumlah") %>%
mutate(
Proporsi = scales::percent(Jumlah / sum(Jumlah), accuracy = 0.1)
) %>%
rename(Status = Y_label)
knitr::kable(
class_summary,
caption = "Distribusi kelas respons"
)| Status | Jumlah | Proporsi |
|---|---|---|
| OPPOSE (Menolak) | 231 | 39.1% |
| FAVOR (Mendukung) | 360 | 60.9% |
Interpretasi output: Dari 591 responden, sebanyak 360 (60,9%) mendukung hukuman mati dan 231 (39,1%) menolaknya. Kelas respons tidak seimbang sempurna, tetapi proporsi kelas minoritas (OPPOSE) masih cukup besar sehingga model klasifikasi dapat mempelajari pola kedua kelompok dengan baik.
ggplot(data_bersih, aes(x = Y_label, fill = Y_label)) +
geom_bar(width = 0.62, color = "white", linewidth = 0.8) +
geom_text(
stat = "count",
aes(label = after_stat(count)),
vjust = -0.4,
fontface = "bold"
) +
scale_fill_manual(
values = c("OPPOSE (Menolak)" = "#2a9d8f",
"FAVOR (Mendukung)" = "#e76f51")
) +
labs(
title = "Distribusi Sikap terhadap Hukuman Mati",
x = NULL,
y = "Jumlah responden"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "none")Interpretasi output: Grafik batang memperlihatkan bahwa responden yang mendukung hukuman mati (FAVOR) lebih banyak daripada yang menolak (OPPOSE). Ini mencerminkan pola umum dalam survei opini publik di mana dukungan terhadap hukuman mati seringkali merupakan posisi mayoritas, terutama pada kelompok yang lebih konservatif dan lebih tua.
numeric_summary <- data_bersih %>%
group_by(Y_label) %>%
summarise(
Jumlah = n(),
Ratarata_usia = mean(age, na.rm = TRUE),
Median_usia = median(age, na.rm = TRUE),
Std_deviasi = sd(age, na.rm = TRUE),
Min_usia = min(age),
Max_usia = max(age),
.groups = "drop"
) %>%
rename(Status = Y_label) %>%
mutate(across(where(is.numeric), round, 2))
knitr::kable(
numeric_summary,
caption = "Ringkasan usia responden menurut sikap terhadap hukuman mati"
)| Status | Jumlah | Ratarata_usia | Median_usia | Std_deviasi | Min_usia | Max_usia |
|---|---|---|---|---|---|---|
| OPPOSE (Menolak) | 231 | 51.68 | 55 | 18.98 | 18 | 89 |
| FAVOR (Mendukung) | 360 | 53.44 | 55 | 17.00 | 18 | 89 |
Interpretasi output: Responden yang mendukung hukuman mati (FAVOR) memiliki rata-rata usia lebih tinggi dibandingkan yang menolak (OPPOSE). Perbedaan rata-rata usia ini memberikan indikasi awal bahwa usia berpotensi menjadi prediktor yang relevan dalam model, di mana responden yang lebih tua cenderung lebih mendukung hukuman mati.
ggplot(
data_bersih,
aes(x = age, fill = Y_label)
) +
geom_density(alpha = 0.55, color = "white", linewidth = 0.7) +
facet_wrap(~ polviews, labeller = label_both) +
scale_fill_manual(
values = c("OPPOSE (Menolak)" = "#2a9d8f",
"FAVOR (Mendukung)" = "#e76f51")
) +
labs(
title = "Distribusi Usia Responden Menurut Pandangan Politik dan Sikap",
subtitle = "Dikelompokkan berdasarkan pandangan politik",
x = "Usia (tahun)",
y = "Kepadatan",
fill = "Sikap"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")Interpretasi output: Grafik kepadatan yang difacet berdasarkan pandangan politik menunjukkan pola distribusi usia untuk kelompok FAVOR dan OPPOSE pada responden Conservative maupun Liberal. Jika dua kurva terpisah jauh dalam suatu facet, artinya usia menjadi pembeda yang kuat dalam kelompok tersebut. Perbedaan pola antara facet Conservative dan Liberal juga menggambarkan peran interaksi antara usia dan pandangan politik.
prop_summary <- data_bersih %>%
group_by(sex, polviews) %>%
summarise(
prop_favor = mean(Y_biner),
n = n(),
.groups = "drop"
) %>%
mutate(
label = paste0(round(prop_favor * 100, 1), "%\n(n=", n, ")")
)
ggplot(prop_summary,
aes(x = sex, y = prop_favor, fill = polviews)) +
geom_col(position = "dodge", width = 0.65,
color = "white", linewidth = 0.8) +
geom_text(
aes(label = label),
position = position_dodge(width = 0.65),
vjust = -0.3,
size = 3.2
) +
scale_fill_manual(values = c("Conservative" = "#e76f51",
"Liberal" = "#2a9d8f")) +
scale_y_continuous(labels = scales::percent, limits = c(0, 1.1)) +
labs(
title = "Proporsi Dukungan Hukuman Mati (FAVOR)",
subtitle = "Menurut jenis kelamin dan pandangan politik",
x = "Jenis kelamin",
y = "Proporsi mendukung (FAVOR)",
fill = "Pandangan politik"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")Interpretasi output: Grafik batang berkelompok
memperlihatkan proporsi dukungan hukuman mati untuk setiap kombinasi
jenis kelamin dan pandangan politik. Responden Conservative umumnya
memiliki proporsi FAVOR lebih tinggi dibandingkan Liberal pada kelompok
jenis kelamin yang sama. Perbedaan proporsi ini mengindikasikan bahwa
polviews merupakan prediktor yang penting secara
substantif.
Data dibagi menjadi 80% training dan 20% testing secara stratified agar proporsi FAVOR dan OPPOSE tetap seimbang pada kedua subset.
stratified_split <- function(y, prop = 0.8) {
idx_by_class <- split(seq_along(y), y)
train_idx <- lapply(
idx_by_class,
function(idx) sample(idx, size = floor(length(idx) * prop))
)
unlist(train_idx, use.names = FALSE)
}
set.seed(42)
train_id <- stratified_split(data_bersih$Y_biner, prop = 0.8)
train_data <- data_bersih[ train_id, ]
test_data <- data_bersih[-train_id, ]
split_summary <- bind_rows(
train_data %>% count(Y_label) %>% mutate(data = "Training"),
test_data %>% count(Y_label) %>% mutate(data = "Testing")
) %>%
group_by(data) %>%
mutate(proporsi = scales::percent(n / sum(n), accuracy = 0.1)) %>%
ungroup() %>%
select(data, Y_label, n, proporsi) %>%
rename(
Data = data,
Status = Y_label,
Jumlah = n,
Proporsi = proporsi
)
knitr::kable(split_summary,
caption = "Distribusi kelas pada training dan testing")| Data | Status | Jumlah | Proporsi |
|---|---|---|---|
| Training | OPPOSE (Menolak) | 184 | 39.0% |
| Training | FAVOR (Mendukung) | 288 | 61.0% |
| Testing | OPPOSE (Menolak) | 47 | 39.5% |
| Testing | FAVOR (Mendukung) | 72 | 60.5% |
Interpretasi output: Data training berisi 472 observasi dan data testing berisi 119 observasi. Karena split dilakukan secara stratified, proporsi FAVOR dan OPPOSE pada training maupun testing tetap mendekati proporsi data awal, sehingga evaluasi pada testing tidak bias akibat perbedaan komposisi kelas.
Misalkan \(Y_i\) adalah sikap responden ke-\(i\) terhadap hukuman mati, dengan:
\[ Y_i = \begin{cases} 1, & \text{jika mendukung hukuman mati (FAVOR)},\\ 0, & \text{jika menolak hukuman mati (OPPOSE)}. \end{cases} \]
Peluang mendukung hukuman mati dinotasikan sebagai:
\[ p_i = P(Y_i = 1 \mid X_i). \]
Pada regresi logistik biner, peluang tersebut dimodelkan melalui fungsi logit:
\[ \text{logit}(p_i) = \ln\left(\frac{p_i}{1-p_i}\right) = \beta_0 + \beta_1 \cdot \text{age}_i + \beta_2 \cdot \mathbf{1}[\text{sex}_i = \text{FEMALE}] + \beta_3 \cdot \mathbf{1}[\text{polviews}_i = \text{Liberal}]. \]
Bentuk peluang prediksi diperoleh dengan transformasi balik:
\[ \hat{p}_i = \frac{\exp(\hat{\eta}_i)}{1+\exp(\hat{\eta}_i)} = \frac{1}{1+\exp(-\hat{\eta}_i)}. \]
Interpretasi odds ratio: Untuk prediktor numerik
\(X_j\), \(OR_j = \exp(\beta_j)\). Jika \(OR_j > 1\), kenaikan prediktor menaikkan
odds mendukung hukuman mati; jika \(OR_j <
1\), sebaliknya. Untuk prediktor kategorik, odds ratio
dibandingkan terhadap kategori referensi (MALE untuk sex,
Conservative untuk polviews).
model_fit <- glm(
Y_biner ~ age + sex + polviews,
data = train_data,
family = binomial(link = "logit")
)
ringkasan_model <- data.frame(
Keterangan = c(
"Jumlah observasi training",
"Null deviance",
"Residual deviance",
"Derajat bebas residual",
"AIC"
),
Nilai = c(
nobs(model_fit),
round(model_fit$null.deviance, 3),
round(model_fit$deviance, 3),
model_fit$df.residual,
round(AIC(model_fit), 3)
)
)
knitr::kable(
ringkasan_model,
caption = "Ringkasan kecocokan model regresi logistik"
)| Keterangan | Nilai |
|---|---|
| Jumlah observasi training | 472.000 |
| Null deviance | 631.227 |
| Residual deviance | 563.852 |
| Derajat bebas residual | 468.000 |
| AIC | 571.852 |
Interpretasi output: Model diestimasi menggunakan 472 observasi training. Nilai residual deviance (563.852) lebih kecil daripada null deviance (631.227), yang berarti ketiga prediktor (usia, jenis kelamin, pandangan politik) secara bersama-sama memberikan perbaikan kecocokan dibandingkan model kosong yang hanya memakai intercept. Nilai AIC sebesar 571.852 dapat digunakan untuk membandingkan model ini dengan model alternatif.
coef_table <- broom::tidy(model_fit) %>%
mutate(
odds_ratio = exp(estimate),
ci_low = exp(estimate - 1.96 * std.error),
ci_high = exp(estimate + 1.96 * std.error)
) %>%
arrange(p.value) %>%
transmute(
`Variabel/level` = term,
`Odds ratio` = round(odds_ratio, 3),
`Interval kepercayaan 95 persen` = paste0(round(ci_low, 3),
" - ",
round(ci_high, 3)),
`p-value` = signif(p.value, 3)
)
knitr::kable(
coef_table,
caption = "Koefisien model: odds ratio dan interval kepercayaan 95%"
)| Variabel/level | Odds ratio | Interval kepercayaan 95 persen | p-value |
|---|---|---|---|
| polviewsLiberal | 0.202 | 0.134 - 0.303 | 0.00e+00 |
| (Intercept) | 5.369 | 2.523 - 11.425 | 1.29e-05 |
| sexFEMALE | 0.703 | 0.471 - 1.05 | 8.53e-02 |
| age | 0.995 | 0.984 - 1.007 | 4.26e-01 |
Interpretasi output: Tabel odds ratio diurutkan dari koefisien dengan p-value terkecil.
Setelah model menghasilkan peluang prediksi \(\hat{p}_i\), kelas ditentukan menggunakan threshold \(c\):
\[ \hat{Y}_i = \begin{cases} 1, & \text{jika } \hat{p}_i \ge c,\\ 0, & \text{jika } \hat{p}_i < c. \end{cases} \]
Notasi confusion matrix yang digunakan:
\[ \text{Akurasi} = \frac{TP + TN}{TP + TN + FP + FN} \qquad \text{Error rate} = 1 - \text{Akurasi} \]
\[ \text{Sensitivity} = \frac{TP}{TP + FN} \qquad \text{Specificity} = \frac{TN}{TN + FP} \qquad \text{Precision} = \frac{TP}{TP + FP} \]
\[ \text{F1-score} = 2 \times \frac{\text{Precision} \times \text{Sensitivity}} {\text{Precision} + \text{Sensitivity}} \qquad \text{Balanced accuracy} = \frac{\text{Sensitivity} + \text{Specificity}}{2} \]
Dalam konteks ini, sensitivity mengukur kemampuan model mendeteksi responden yang benar-benar mendukung hukuman mati (FAVOR), sedangkan specificity mengukur kemampuan model mengenali responden yang menolak (OPPOSE).
safe_div <- function(num, den) {
ifelse(den == 0, NA_real_, num / den)
}
classification_metrics <- function(actual, prob, threshold = 0.5) {
pred <- as.integer(prob >= threshold)
tp <- sum(pred == 1 & actual == 1)
tn <- sum(pred == 0 & actual == 0)
fp <- sum(pred == 1 & actual == 0)
fn <- sum(pred == 0 & actual == 1)
sensitivity <- safe_div(tp, tp + fn)
specificity <- safe_div(tn, tn + fp)
precision <- safe_div(tp, tp + fp)
npv <- safe_div(tn, tn + fn)
accuracy <- safe_div(tp + tn, tp + tn + fp + fn)
data.frame(
threshold = threshold,
accuracy = accuracy,
error_rate = 1 - accuracy,
sensitivity = sensitivity,
specificity = specificity,
precision = precision,
negative_predictive_value = npv,
f1_score = safe_div(2 * precision * sensitivity,
precision + sensitivity),
balanced_accuracy = (sensitivity + specificity) / 2,
false_positive_rate = 1 - specificity,
false_negative_rate = 1 - sensitivity
)
}
format_metrics_indonesia <- function(x) {
x %>%
transmute(
Threshold = threshold,
Akurasi = accuracy,
`Error rate` = error_rate,
Sensitivity = sensitivity,
Specificity = specificity,
Presisi = precision,
NPV = negative_predictive_value,
`F1-score` = f1_score,
`Balanced accuracy` = balanced_accuracy,
FPR = false_positive_rate,
FNR = false_negative_rate
)
}
confusion_matrix <- function(actual, prob, threshold = 0.5) {
pred_label <- factor(
ifelse(prob >= threshold, "Prediksi FAVOR", "Prediksi OPPOSE"),
levels = c("Prediksi FAVOR", "Prediksi OPPOSE")
)
actual_label <- factor(
ifelse(actual == 1, "Aktual FAVOR", "Aktual OPPOSE"),
levels = c("Aktual FAVOR", "Aktual OPPOSE")
)
addmargins(table(actual_label, pred_label))
}p_train <- predict(model_fit, newdata = train_data, type = "response")
p_test <- predict(model_fit, newdata = test_data, type = "response")
prediction_preview <- head(
data.frame(
`Status aktual` = test_data$Y_label,
`Usia` = test_data$age,
`Jenis kelamin` = test_data$sex,
`Pandangan politik` = test_data$polviews,
`Peluang prediksi FAVOR` = round(p_test, 4),
check.names = FALSE
),
8
)
knitr::kable(
prediction_preview,
caption = "Contoh peluang prediksi mendukung hukuman mati (FAVOR) pada data testing"
)| Status aktual | Usia | Jenis kelamin | Pandangan politik | Peluang prediksi FAVOR |
|---|---|---|---|---|
| FAVOR (Mendukung) | 58 | MALE | Conservative | 0.8034 |
| OPPOSE (Menolak) | 79 | MALE | Liberal | 0.4274 |
| FAVOR (Mendukung) | 63 | FEMALE | Liberal | 0.3614 |
| OPPOSE (Menolak) | 55 | MALE | Liberal | 0.4552 |
| FAVOR (Mendukung) | 36 | MALE | Liberal | 0.4775 |
| OPPOSE (Menolak) | 72 | MALE | Liberal | 0.4355 |
| OPPOSE (Menolak) | 80 | MALE | Liberal | 0.4263 |
| OPPOSE (Menolak) | 21 | MALE | Conservative | 0.8295 |
Interpretasi output: Tabel ini menampilkan beberapa contoh observasi testing beserta peluang prediksi mendukung hukuman mati. Semakin besar nilai peluang, semakin tinggi kecenderungan responden diklasifikasikan sebagai FAVOR. Peluang ini belum menjadi kelas akhir sampai dibandingkan dengan threshold.
confusion_default <- confusion_matrix(test_data$Y_biner, p_test,
threshold = 0.5)
metrics_default <- classification_metrics(test_data$Y_biner, p_test,
threshold = 0.5) %>%
format_metrics_indonesia() %>%
mutate(across(where(is.numeric), round, 3))
knitr::kable(
confusion_default,
caption = "Confusion matrix testing pada threshold 0.50"
)| Prediksi FAVOR | Prediksi OPPOSE | Sum | |
|---|---|---|---|
| Aktual FAVOR | 49 | 23 | 72 |
| Aktual OPPOSE | 11 | 36 | 47 |
| Sum | 60 | 59 | 119 |
| Threshold | Akurasi | Error rate | Sensitivity | Specificity | Presisi | NPV | F1-score | Balanced accuracy | FPR | FNR |
|---|---|---|---|---|---|---|---|---|---|---|
| 0.5 | 0.714 | 0.286 | 0.681 | 0.766 | 0.817 | 0.61 | 0.742 | 0.723 | 0.234 | 0.319 |
Interpretasi output: Pada threshold 0.50, confusion matrix menunjukkan berapa responden FAVOR yang berhasil dideteksi dan berapa responden OPPOSE yang tetap dikenali sebagai OPPOSE. Akurasi menyatakan proporsi total prediksi yang benar. Jika sensitivity masih rendah, model melewatkan banyak responden yang sebenarnya mendukung hukuman mati. Jika specificity rendah, banyak responden yang sebenarnya menolak justru diklasifikasikan sebagai pendukung.
Setiap titik pada kurva ROC berasal dari satu nilai threshold \(c\):
\[ TPR(c) = \text{Sensitivity}(c) = \frac{TP(c)}{TP(c)+FN(c)} \qquad FPR(c) = 1 - \text{Specificity}(c) = \frac{FP(c)}{FP(c)+TN(c)} \]
Nilai AUC (area under the curve):
\[ AUC = \int_0^1 TPR(FPR)\,d(FPR). \]
Threshold optimal dipilih dari data training menggunakan indeks Youden:
\[ J = \text{Sensitivity} + \text{Specificity} - 1; \quad c_{\text{optimal}} = \arg\max_c \{J(c)\}. \]
roc_points <- function(actual, prob) {
thresholds <- c(Inf, sort(unique(prob), decreasing = TRUE), -Inf)
out <- lapply(thresholds, function(th) {
pred <- as.integer(prob >= th)
tp <- sum(pred == 1 & actual == 1)
tn <- sum(pred == 0 & actual == 0)
fp <- sum(pred == 1 & actual == 0)
fn <- sum(pred == 0 & actual == 1)
sensitivity <- safe_div(tp, tp + fn)
specificity <- safe_div(tn, tn + fp)
data.frame(
threshold = th,
sensitivity = sensitivity,
specificity = specificity,
fpr = 1 - specificity,
youden = sensitivity + specificity - 1
)
})
bind_rows(out)
}
auc_value <- function(roc_df) {
roc_sorted <- roc_df %>% arrange(fpr, sensitivity)
sum(
diff(roc_sorted$fpr) *
(head(roc_sorted$sensitivity, -1) +
tail(roc_sorted$sensitivity, -1)) / 2
)
}
roc_train <- roc_points(train_data$Y_biner, p_train) %>%
mutate(data = "Training")
roc_test <- roc_points(test_data$Y_biner, p_test) %>%
mutate(data = "Testing")
auc_train <- auc_value(roc_train)
auc_test <- auc_value(roc_test)
optimal_train <- roc_train %>%
filter(is.finite(threshold)) %>%
arrange(desc(youden), desc(sensitivity)) %>%
slice(1)
threshold_opt <- optimal_train$threshold[1]
test_at_opt <- roc_points(test_data$Y_biner, p_test) %>%
filter(is.finite(threshold)) %>%
slice_min(abs(threshold - threshold_opt), n = 1, with_ties = FALSE) %>%
mutate(data = "Testing pada threshold optimal")
auc_table <- data.frame(
Data = c("Training", "Testing"),
AUC = round(c(auc_train, auc_test), 3)
)
threshold_table <- optimal_train %>%
transmute(
`Threshold optimal` = round(threshold, 3),
Sensitivity = round(sensitivity, 3),
Specificity = round(specificity, 3),
`Indeks Youden` = round(youden, 3)
)
knitr::kable(auc_table, caption = "Nilai AUC")| Data | AUC |
|---|---|
| Training | 0.712 |
| Testing | 0.750 |
| Threshold optimal | Sensitivity | Specificity | Indeks Youden |
|---|---|---|---|
| 0.732 | 0.622 | 0.777 | 0.399 |
Interpretasi output: AUC testing sebesar 0.75 mengukur kemampuan model membedakan responden FAVOR dan OPPOSE pada data baru. Nilai yang lebih besar dari 0,5 berarti model lebih baik daripada tebakan acak. Threshold optimal dari data training adalah 0.732, dipilih karena menghasilkan nilai indeks Youden terbesar sehingga memberi keseimbangan terbaik antara sensitivity dan specificity.
roc_plot <- bind_rows(roc_train, roc_test)
ggplot(roc_plot, aes(x = fpr, y = sensitivity, color = data)) +
geom_path(linewidth = 1.1) +
geom_abline(intercept = 0, slope = 1, linetype = "dashed",
color = "#6c757d") +
geom_point(
data = optimal_train,
aes(x = fpr, y = sensitivity),
inherit.aes = FALSE,
color = "#ffb703", fill = "#fb8500",
shape = 21, size = 4, stroke = 1.2
) +
geom_point(
data = test_at_opt,
aes(x = fpr, y = sensitivity),
inherit.aes = FALSE,
color = "#8338ec", fill = "#3a86ff",
shape = 24, size = 4, stroke = 1.2
) +
coord_equal() +
scale_color_manual(values = c("Training" = "#0077b6",
"Testing" = "#e76f51")) +
labs(
title = "Kurva ROC Model Regresi Logistik",
subtitle = paste0(
"AUC training = ", round(auc_train, 3),
"; AUC testing = ", round(auc_test, 3),
"; threshold optimal = ", round(threshold_opt, 3)
),
x = "False positive rate (1 - specificity)",
y = "Sensitivity / true positive rate",
color = "Data"
) +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")Interpretasi output: Kurva ROC yang semakin menjauhi garis diagonal putus-putus menunjukkan performa klasifikasi yang semakin baik. Titik berwarna kuning-oranye menunjukkan posisi threshold optimal pada kurva training, sedangkan titik ungu-biru menunjukkan posisi yang setara pada kurva testing. Jika kedua kurva berdekatan, model cukup stabil pada data baru dan tidak terlalu overfit pada data training.
Threshold optimal dari ROC training (0.732) kemudian diterapkan pada data testing.
metrics_compare <- bind_rows(
classification_metrics(test_data$Y_biner, p_test, threshold = 0.5) %>%
mutate(aturan = "Threshold 0.50"),
classification_metrics(test_data$Y_biner, p_test,
threshold = threshold_opt) %>%
mutate(aturan = "Threshold optimal ROC")
) %>%
select(aturan, everything()) %>%
format_metrics_indonesia() %>%
bind_cols(
`Aturan klasifikasi` = c("Threshold 0.50", "Threshold optimal ROC"), .
) %>%
select(`Aturan klasifikasi`, everything())
confusion_opt <- confusion_matrix(test_data$Y_biner, p_test,
threshold = threshold_opt)
knitr::kable(
metrics_compare %>% mutate(across(where(is.numeric), round, 3)),
caption = "Perbandingan metrik testing: threshold 0.50 vs threshold optimal"
)| Aturan klasifikasi | Threshold | Akurasi | Error rate | Sensitivity | Specificity | Presisi | NPV | F1-score | Balanced accuracy | FPR | FNR |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Threshold 0.50 | 0.500 | 0.714 | 0.286 | 0.681 | 0.766 | 0.817 | 0.610 | 0.742 | 0.723 | 0.234 | 0.319 |
| Threshold optimal ROC | 0.732 | 0.672 | 0.328 | 0.569 | 0.830 | 0.837 | 0.557 | 0.678 | 0.700 | 0.170 | 0.431 |
| Prediksi FAVOR | Prediksi OPPOSE | Sum | |
|---|---|---|---|
| Aktual FAVOR | 41 | 31 | 72 |
| Aktual OPPOSE | 8 | 39 | 47 |
| Sum | 49 | 70 | 119 |
Interpretasi output: Tabel perbandingan menunjukkan dampak perubahan threshold. Jika threshold optimal lebih rendah dari 0.50, model menjadi lebih agresif mendeteksi responden FAVOR; sensitivity meningkat tetapi specificity dapat turun karena sebagian responden OPPOSE ikut terklasifikasi sebagai FAVOR. Pilihan threshold yang tepat bergantung pada konteks: apakah lebih penting mendeteksi semua pendukung hukuman mati (sensitivity tinggi) atau meminimalkan kesalahan klasifikasi responden penolak (specificity tinggi).
test_prob_plot <- test_data %>%
mutate(peluang_favor = p_test)
ggplot(test_prob_plot, aes(x = peluang_favor, fill = Y_label)) +
geom_density(alpha = 0.55, color = "white", linewidth = 0.7) +
geom_vline(
xintercept = threshold_opt,
color = "#fb8500",
linewidth = 1.2,
linetype = "dashed"
) +
annotate(
"label",
x = threshold_opt,
y = Inf,
label = paste0("threshold = ", round(threshold_opt, 3)),
vjust = 1.4,
fill = "#fff3b0",
color = "#5f370e",
label.size = 0
) +
scale_fill_manual(
values = c("OPPOSE (Menolak)" = "#2a9d8f",
"FAVOR (Mendukung)" = "#e76f51")
) +
labs(
title = "Distribusi Peluang Prediksi FAVOR pada Data Testing",
x = "Peluang prediksi mendukung hukuman mati (FAVOR)",
y = "Kepadatan",
fill = "Status aktual"
) +
theme_minimal(base_size = 12)Interpretasi output: Grafik kepadatan memperlihatkan sebaran peluang prediksi FAVOR untuk responden yang aktualnya FAVOR (oranye) dan OPPOSE (hijau). Semakin terpisah dua kurva, semakin baik model membedakan kedua kelompok. Garis vertikal menunjukkan threshold optimal; observasi di sebelah kanan garis diklasifikasikan sebagai FAVOR, sedangkan di sebelah kiri diklasifikasikan sebagai OPPOSE. Area tumpang tindih antara dua kurva menunjukkan bagian data yang sulit dibedakan oleh model.
Berdasarkan hasil estimasi model regresi logistik biner pada data training (n = 472), persamaan logit yang terbentuk adalah:
\[ \text{logit}(\hat{p}_i) = \ln\left(\frac{\hat{p}_i}{1-\hat{p}_i}\right) = 1{,}680 + 0{,}995 \cdot \text{age}_i - 0{,}352 \cdot \mathbf{1}[\text{sex}_i = \text{FEMALE}] - 1{,}600 \cdot \mathbf{1}[\text{polviews}_i = \text{Liberal}]. \]
Interpretasi masing-masing koefisien adalah sebagai berikut.
1. Intercept (\(\hat{\beta}_0\))
Nilai intercept menghasilkan odds ratio sebesar 5,369 (IK 95%: 2,523 – 11,425; p < 0,001). Ini berarti seorang responden laki-laki berusia 0 tahun dengan pandangan Conservative memiliki odds mendukung hukuman mati sebesar 5,369 kali lebih tinggi dibandingkan menolaknya. Nilai ini bersifat ekstrapolasi jauh di luar rentang data (usia minimum = 18 tahun) sehingga tidak memiliki makna substantif langsung, namun diperlukan untuk mengkalibrasi model secara matematis.
2. Usia (age, \(\hat{\beta}_1 = -0{,}005\), OR =
0,995)
Setiap kenaikan usia satu tahun dikaitkan dengan penurunan odds mendukung hukuman mati sebesar faktor 0,995 (IK 95%: 0,984 – 1,007), dengan p-value = 0,426. Karena interval kepercayaan 95% mencakup nilai 1 dan p-value jauh di atas 0,05, usia tidak terbukti sebagai prediktor yang signifikan secara statistik dalam model ini. Meski arah efeknya mengindikasikan sedikit penurunan odds seiring bertambahnya usia, efek tersebut sangat kecil dan tidak dapat dibedakan dari variasi acak pada data.
3. Jenis kelamin (sexFEMALE, \(\hat{\beta}_2\), OR = 0,703)
Responden perempuan memiliki odds mendukung hukuman mati sebesar 0,703 kali odds responden laki-laki (kategori referensi), atau setara dengan penurunan odds sekitar 29,7% (IK 95%: 0,471 – 1,050; p = 0,085). Meskipun arah efeknya konsisten — perempuan cenderung lebih menolak hukuman mati — interval kepercayaan sedikit melewati nilai 1 dan p-value = 0,085 berada di atas ambang 0,05. Dengan demikian, pengaruh jenis kelamin belum signifikan secara statistik pada tingkat signifikansi 5%, meski mendekati batas signifikansi pada tingkat 10%.
4. Pandangan politik (polviewsLiberal, \(\hat{\beta}_3\), OR = 0,202)
Ini adalah prediktor paling dominan dalam model. Responden dengan pandangan Liberal memiliki odds mendukung hukuman mati hanya sebesar 0,202 kali odds responden Conservative (kategori referensi), artinya odds mereka mendukung hukuman mati 79,8% lebih rendah dibandingkan responden Conservative (IK 95%: 0,134 – 0,303; p < 0,001). Interval kepercayaan yang jauh di bawah 1 dan p-value yang sangat kecil memberikan bukti statistik yang sangat kuat bahwa pandangan politik merupakan penentu utama sikap terhadap hukuman mati.
Deviasi dan penurunan deviance:
Model berhasil menurunkan null deviance dari 631,227 (model tanpa prediktor) menjadi residual deviance 563,852 (model dengan tiga prediktor), menghasilkan penurunan deviance sebesar 67,375 pada 3 derajat bebas. Penurunan ini signifikan secara statistik (\(\chi^2(3) = 67{,}375\), p < 0,001), yang berarti ketiga prediktor secara bersama-sama memberikan kontribusi nyata dalam menjelaskan variasi sikap terhadap hukuman mati.
AUC (Area Under the ROC Curve):
AUC testing sebesar 0,750 berada dalam kategori diskriminasi yang cukup baik (acceptable discrimination). Artinya, jika dipilih secara acak satu responden FAVOR dan satu responden OPPOSE, model akan menetapkan peluang prediksi lebih tinggi untuk responden FAVOR sebanyak 75% dari seluruh pasangan yang mungkin. Menariknya, AUC testing (0,750) sedikit lebih tinggi dari AUC training (0,712), yang menunjukkan bahwa model tidak mengalami overfitting dan generalisasinya ke data baru cukup baik.
Pada threshold 0,50:
| Metrik | Nilai |
|---|---|
| Akurasi | 71,4% |
| Sensitivity | 68,1% |
| Specificity | 76,6% |
| Presisi | 81,7% |
| F1-score | 74,2% |
| Balanced accuracy | 72,3% |
Dari 119 observasi testing, model memprediksi dengan benar 49 dari 72 responden FAVOR (sensitivity 68,1%) dan 36 dari 47 responden OPPOSE (specificity 76,6%). Terdapat 23 false negative (responden FAVOR yang diprediksi OPPOSE) dan 11 false positive (responden OPPOSE yang diprediksi FAVOR). Presisi sebesar 81,7% berarti dari semua prediksi FAVOR, 81,7% di antaranya benar-benar FAVOR.
Pada threshold optimal 0,732 (indeks Youden):
| Metrik | Nilai |
|---|---|
| Akurasi | 67,2% |
| Sensitivity | 56,9% |
| Specificity | 83,0% |
| Presisi | 83,7% |
| F1-score | 67,8% |
| Balanced accuracy | 70,0% |
Dengan menaikkan threshold ke 0,732, model menjadi lebih selektif dalam memprediksi FAVOR. Akibatnya, specificity meningkat dari 76,6% menjadi 83,0% (lebih baik mengenali OPPOSE), namun sensitivity turun dari 68,1% menjadi 56,9% (lebih banyak FAVOR yang terlewat). Untuk konteks penelitian opini publik, threshold 0,50 lebih disarankan karena memberikan keseimbangan yang lebih baik antara sensitivity dan specificity (balanced accuracy 72,3% vs 70,0%) serta F1-score yang lebih tinggi (74,2% vs 67,8%).
Analisis regresi logistik biner terhadap data survei sikap publik mengenai hukuman mati (n = 591) di Amerika Serikat pada tahun 2024 menghasilkan beberapa temuan utama sebagai berikut.
Pertama, mengenai signifikansi prediktor. Dari tiga
prediktor yang dimasukkan ke dalam model, hanya pandangan
politik (polviews) yang terbukti signifikan secara
statistik (p < 0,001). Responden dengan pandangan Liberal memiliki
odds mendukung hukuman mati 79,8% lebih rendah dibandingkan responden
Conservative (OR = 0,202; IK 95%: 0,134–0,303). Jenis kelamin
(sex) menunjukkan arah efek yang konsisten — perempuan
cenderung lebih menolak hukuman mati (OR = 0,703) — namun belum mencapai
signifikansi statistik pada tingkat 5% (p = 0,085). Usia
(age) tidak terbukti berpengaruh signifikan terhadap sikap
hukuman mati dalam model ini (OR = 0,995; p = 0,426).
Kedua, mengenai kebaikan model. Model secara keseluruhan signifikan berdasarkan uji penurunan deviance (\(\Delta D = 67{,}375\); df = 3; p < 0,001). Nilai AUC testing sebesar 0,750 menunjukkan kemampuan diskriminasi model termasuk kategori cukup baik, dan tidak terdapat indikasi overfitting karena AUC testing lebih tinggi dari AUC training (0,712).
Ketiga, mengenai performa klasifikasi. Pada threshold 0,50, model mencapai akurasi 71,4% pada data testing, dengan sensitivity 68,1% dan specificity 76,6%. Threshold ini dinilai lebih sesuai dibandingkan threshold optimal Youden (0,732) karena menghasilkan balanced accuracy dan F1-score yang lebih tinggi. Artinya, model lebih mampu mendeteksi responden yang benar-benar mendukung hukuman mati tanpa terlalu banyak salah mengklasifikasikan responden yang menolak.
Secara keseluruhan, pandangan politik merupakan faktor penentu utama sikap terhadap hukuman mati dalam dataset ini. Model regresi logistik biner yang dibangun cukup andal untuk tujuan prediksi awal, meski akurasi yang belum sempurna mencerminkan kompleksitas sikap manusia yang tidak seluruhnya dapat dijelaskan oleh tiga variabel demografis saja. Pengembangan model ke depan dapat mempertimbangkan penambahan variabel seperti tingkat pendidikan, afiliasi agama, atau pengalaman langsung dengan tindak kejahatan untuk meningkatkan daya prediksi model.
Regresi logistik multinomial adalah model regresi untuk respons kategorik dengan lebih dari dua kategori, ketika kategori tersebut bersifat nominal, yaitu tidak memiliki urutan yang bermakna.
Misalkan respons: \[Y_i \in \{1, 2, \ldots, K\}\] dengan \(K\) kategori. Salah satu kategori dipilih sebagai kategori referensi. Misalkan kategori ke-\(K\) menjadi referensi. Untuk setiap kategori \(k = 1, 2, \ldots, K-1\), model ditulis sebagai:
\[ \log\left(\frac{P(Y_i = k \mid \mathbf{x}_i)}{P(Y_i = K \mid \mathbf{x}_i)}\right) = \beta_{0k} + \beta_{1k} x_{i1} + \cdots + \beta_{pk} x_{ip}. \]
Artinya, model membandingkan peluang masuk kategori \(k\) terhadap peluang masuk kategori referensi. Kasus ini berbeda dari regresi biner: alih-alih satu persamaan logit, model multinomial membangun \((K-1)\) persamaan logit secara simultan, masing-masing membandingkan satu kategori terhadap kategori referensi.
Studi kasus: Analisis faktor-faktor yang memengaruhi status pekerjaan responden berdasarkan data General Social Survey (GSS) 2024.
Tujuan analisis: Membangun model regresi logistik multinomial untuk memprediksi apakah seorang responden bekerja penuh waktu, paruh waktu, mengurus rumah tangga, atau menganggur berdasarkan usia, status pernikahan, tingkat pendidikan, dan jenis kelamin.
Kategori variabel respons (wrkstat):
| Kode | Kategori | Keterangan |
|---|---|---|
FT |
Working full time | Bekerja penuh waktu |
PT |
Working part time | Bekerja paruh waktu |
HK |
Keeping house | Mengurus rumah tangga |
UN |
Unemployed | Menganggur / sedang mencari kerja |
Kategori referensi yang dipilih: Working full time (FT)
Status pekerjaan merupakan variabel multikategori nominal — tidak ada urutan yang inheren antara “bekerja penuh waktu”, “paruh waktu”, “mengurus rumah tangga”, dan “menganggur”. Kasus ini cocok dianalisis dengan regresi logistik multinomial karena variabel respons memiliki lebih dari dua kategori tanpa urutan alami:
Model yang digunakan adalah:
\[ \log\left(\frac{P(Y_i = k \mid X_i)}{P(Y_i = \text{FT} \mid X_i)}\right) = \beta_{0k} + \beta_{1k} X_{1i} + \beta_{2k} X_{2i} + \cdots + \beta_{pk} X_{pi}, \]
dengan \(k \in \{\text{Part Time},\ \text{Keeping House},\ \text{Unemployed}\}\) dan Working full time (FT) sebagai kategori referensi. Model membangun \((K-1) = 3\) persamaan logit secara simultan, masing-masing membandingkan satu kategori terhadap Full Time.
Dataset berasal dari General Social Survey (GSS) 2024, survei sosial nasional Amerika Serikat yang diselenggarakan secara berkala oleh NORC di University of Chicago. Dataset terdiri dari 1.575 responden dengan 6 variabel.
raw_data <- readxl::read_excel("Data Multinomial.xlsx", sheet = 1)
ringkasan_data <- data.frame(
Keterangan = c("Jumlah observasi", "Jumlah variabel"),
Nilai = c(nrow(raw_data), ncol(raw_data))
)
knitr::kable(ringkasan_data, caption = "Ukuran dataset GSS 2024")| Keterangan | Nilai |
|---|---|
| Jumlah observasi | 1575 |
| Jumlah variabel | 6 |
Interpretasi output: Dataset berisi 1575 observasi dan 6 variabel. Ukuran sampel ini sangat memadai untuk regresi logistik multinomial dengan empat prediktor.
penjelasan <- data.frame(
`Nama kolom` = c("id_", "wrkstat", "marital",
"age", "degree", "sex"),
`Keterangan` = c(
"Nomor identifikasi responden",
"Status pekerjaan responden (variabel respons)",
"Status pernikahan responden",
"Usia responden (tahun)",
"Tingkat pendidikan tertinggi",
"Jenis kelamin responden"
),
`Kategori / Rentang` = c(
"—",
"Working full time, Working part time, Keeping house, Unemployed",
"Married, Never married",
"18 – 86 tahun",
"Less than HS, High school, Associate, Bachelor's, Graduate",
"MALE, FEMALE"
),
`Tipe dalam analisis` = c(
"ID (tidak dipakai dalam model)",
"Respons multinomial (4 kategori)",
"Kategorik (2 level)",
"Numerik",
"Kategorik ordinal (5 level)",
"Kategorik (2 level)"
),
check.names = FALSE
)
knitr::kable(penjelasan, caption = "Penjelasan variabel dataset GSS 2024")| Nama kolom | Keterangan | Kategori / Rentang | Tipe dalam analisis |
|---|---|---|---|
| id_ | Nomor identifikasi responden | — | ID (tidak dipakai dalam model) |
| wrkstat | Status pekerjaan responden (variabel respons) | Working full time, Working part time, Keeping house, Unemployed | Respons multinomial (4 kategori) |
| marital | Status pernikahan responden | Married, Never married | Kategorik (2 level) |
| age | Usia responden (tahun) | 18 – 86 tahun | Numerik |
| degree | Tingkat pendidikan tertinggi | Less than HS, High school, Associate, Bachelor’s, Graduate | Kategorik ordinal (5 level) |
| sex | Jenis kelamin responden | MALE, FEMALE | Kategorik (2 level) |
Interpretasi output: Terdapat satu variabel respons
multinomial (wrkstat) dengan empat kategori, satu prediktor
numerik (age), dan tiga prediktor kategorik
(marital, degree, sex). Variabel
degree memiliki lima level yang diurutkan dari pendidikan
terendah ke tertinggi, meski dalam model multinomial tidak diasumsikan
ada urutan.
data_bersih <- raw_data |>
mutate(
age = as.numeric(age),
# Respons: factor dengan referensi "Working full time"
wrkstat = factor(wrkstat,
levels = c("Working full time",
"Working part time",
"Keeping house",
"Unemployed, laid off, looking for work")),
# Label pendek untuk visualisasi
wrkstat_label = factor(
case_when(
wrkstat == "Working full time" ~ "Full Time",
wrkstat == "Working part time" ~ "Part Time",
wrkstat == "Keeping house" ~ "Keeping House",
wrkstat == "Unemployed, laid off, looking for work" ~ "Unemployed"
),
levels = c("Full Time", "Part Time", "Keeping House", "Unemployed")
),
# Pendidikan: factor terurut (digunakan sebagai kategorik dalam model)
degree = factor(degree,
levels = c("Less than high school",
"High school",
"Associate/junior college",
"Bachelor's",
"Graduate")),
# Jenis kelamin: MALE sebagai referensi
sex = factor(sex, levels = c("MALE", "FEMALE")),
# Status pernikahan: Married sebagai referensi
marital = factor(marital, levels = c("Married", "Never married"))
) |>
tidyr::drop_na()
knitr::kable(
head(data_bersih |>
select(id_, age, sex, marital, degree, wrkstat_label), 8),
caption = "Contoh data setelah transformasi"
)| id_ | age | sex | marital | degree | wrkstat_label |
|---|---|---|---|---|---|
| 1 | 33 | MALE | Never married | Bachelor’s | Full Time |
| 4 | 19 | MALE | Never married | High school | Part Time |
| 6 | 53 | MALE | Married | Associate/junior college | Unemployed |
| 7 | 48 | FEMALE | Married | High school | Full Time |
| 9 | 60 | FEMALE | Married | Associate/junior college | Full Time |
| 11 | 23 | FEMALE | Never married | Bachelor’s | Full Time |
| 16 | 31 | FEMALE | Married | Graduate | Full Time |
| 17 | 64 | MALE | Married | Bachelor’s | Full Time |
Interpretasi output: Delapan baris pertama data
setelah transformasi. Kategori referensi yang dipilih adalah:
Working full time untuk wrkstat,
MALE untuk sex, Married
untuk marital, dan Less than high school
untuk degree. Semua perbandingan odds ratio akan mengacu
pada kategori-kategori referensi ini.
dist_wrkstat <- data_bersih |>
count(wrkstat_label, name = "Jumlah") |>
mutate(Proporsi = scales::percent(Jumlah / sum(Jumlah), accuracy = 0.1)) |>
rename(`Status pekerjaan` = wrkstat_label)
knitr::kable(dist_wrkstat,
caption = "Distribusi status pekerjaan responden")| Status pekerjaan | Jumlah | Proporsi |
|---|---|---|
| Full Time | 1045 | 66.3% |
| Part Time | 229 | 14.5% |
| Keeping House | 197 | 12.5% |
| Unemployed | 104 | 6.6% |
Interpretasi output: Mayoritas responden bekerja penuh waktu (Full Time), yang memperkuat alasan pemilihan kategori ini sebagai referensi. Kategori Unemployed merupakan kelompok terkecil, sehingga estimasi parameter untuk kategori ini perlu diinterpretasikan dengan kehati-hatian karena didukung oleh sampel yang lebih kecil.
warna_kategori <- c(
"Full Time" = "#2a9d8f",
"Part Time" = "#e9c46a",
"Keeping House"= "#f4a261",
"Unemployed" = "#e76f51"
)
ggplot(data_bersih, aes(x = wrkstat_label, fill = wrkstat_label)) +
geom_bar(width = 0.65, color = "white", linewidth = 0.8) +
geom_text(stat = "count",
aes(label = after_stat(count)),
vjust = -0.4, fontface = "bold") +
scale_fill_manual(values = warna_kategori) +
labs(title = "Distribusi Status Pekerjaan Responden GSS 2024",
x = NULL, y = "Jumlah responden") +
theme_minimal(base_size = 12) +
theme(legend.position = "none")Interpretasi output: Grafik memperlihatkan dominasi responden yang bekerja penuh waktu (Full Time). Ketidakseimbangan ini wajar dalam survei populasi umum dan menjadi alasan kuat menjadikan Full Time sebagai kategori referensi dalam model multinomial.
usia_summary <- data_bersih |>
group_by(wrkstat_label) |>
summarise(
Jumlah = n(),
Rata2 = mean(age, na.rm = TRUE),
Median = median(age, na.rm = TRUE),
Std = sd(age, na.rm = TRUE),
Min = min(age),
Maks = max(age),
.groups = "drop"
) |>
rename(`Status pekerjaan` = wrkstat_label) |>
mutate(across(where(is.numeric), round, 2))
knitr::kable(usia_summary,
caption = "Ringkasan usia menurut status pekerjaan")| Status pekerjaan | Jumlah | Rata2 | Median | Std | Min | Maks |
|---|---|---|---|---|---|---|
| Full Time | 1045 | 41.39 | 40.0 | 12.56 | 18 | 85 |
| Part Time | 229 | 45.77 | 44.0 | 17.38 | 18 | 85 |
| Keeping House | 197 | 42.45 | 39.0 | 14.73 | 18 | 86 |
| Unemployed | 104 | 38.27 | 36.5 | 14.00 | 18 | 73 |
Interpretasi output: Tabel merangkum statistik deskriptif usia untuk setiap kategori status pekerjaan. Perbedaan rata-rata dan median usia antar kategori memberikan gambaran awal mengenai peran usia sebagai prediktor.
Model diestimasi menggunakan fungsi multinom() dari
paket nnet. Kategori referensi untuk wrkstat
adalah Working full time.
model_multi <- nnet::multinom(
wrkstat ~ age + sex + marital + degree,
data = data_bersih,
trace = FALSE # sembunyikan iterasi konvergensi
)
summary(model_multi)## Call:
## nnet::multinom(formula = wrkstat ~ age + sex + marital + degree,
## data = data_bersih, trace = FALSE)
##
## Coefficients:
## (Intercept) age sexFEMALE
## Working part time -2.901070 0.030762276 0.8249242
## Keeping house -1.792936 0.005173082 2.2162268
## Unemployed, laid off, looking for work -2.267533 0.005188112 0.4806244
## maritalNever married degreeHigh school
## Working part time 0.2898141 -0.1862848
## Keeping house -0.6633728 -0.8380199
## Unemployed, laid off, looking for work 1.4435573 -1.2044847
## degreeAssociate/junior college
## Working part time -0.5425302
## Keeping house -2.2368218
## Unemployed, laid off, looking for work -1.6517380
## degreeBachelor's degreeGraduate
## Working part time -0.8297569 -1.312131
## Keeping house -1.8456750 -2.499093
## Unemployed, laid off, looking for work -2.0342674 -2.563259
##
## Std. Errors:
## (Intercept) age sexFEMALE
## Working part time 0.4195370 0.005717080 0.1539669
## Keeping house 0.4368631 0.006603117 0.2174324
## Unemployed, laid off, looking for work 0.5388935 0.008461851 0.2159800
## maritalNever married degreeHigh school
## Working part time 0.1653295 0.2855703
## Keeping house 0.1888033 0.2671407
## Unemployed, laid off, looking for work 0.2766001 0.2927154
## degreeAssociate/junior college
## Working part time 0.3544072
## Keeping house 0.4321605
## Unemployed, laid off, looking for work 0.4484440
## degreeBachelor's degreeGraduate
## Working part time 0.3096431 0.3525457
## Keeping house 0.3067165 0.3712005
## Unemployed, laid off, looking for work 0.3732728 0.5221890
##
## Residual Deviance: 2775.321
## AIC: 2823.321
Interpretasi output: multinom() tidak
langsung memberikan p-value. Koefisien pada baris Part Time, Keeping
House, dan Unemployed masing-masing menjelaskan perubahan log-odds masuk
kategori tersebut dibandingkan Full Time.
Fungsi multinom() tidak langsung memberikan p-value.
Kita dapat menghitungnya dari:
\[z = \frac{\hat{\beta}}{SE(\hat{\beta})}, \qquad p\text{-value} = 2\{1 - \Phi(|z|)\}.\]
# Ekstrak koefisien, SE, z, p-value
coef_mat <- summary(model_multi)$coefficients
se_mat <- summary(model_multi)$standard.errors
z_mat <- coef_mat / se_mat
p_mat <- 2 * (1 - pnorm(abs(z_mat)))
# Susun ke data frame panjang
tidy_multi <- function(coef, se, z, p) {
kategori <- rownames(coef)
prediktor <- colnames(coef)
out <- expand.grid(Kategori = kategori, Prediktor = prediktor,
stringsAsFactors = FALSE)
out$Koefisien <- as.vector(t(coef))
out$SE <- as.vector(t(se))
out$z <- as.vector(t(z))
out$p_value <- as.vector(t(p))
out$RRR <- exp(out$Koefisien)
out$CI_low <- exp(out$Koefisien - 1.96 * out$SE)
out$CI_high <- exp(out$Koefisien + 1.96 * out$SE)
out
}
rrr_df <- tidy_multi(coef_mat, se_mat, z_mat, p_mat) |>
filter(Prediktor != "(Intercept)") |>
mutate(
Signifikan = case_when(
p_value < 0.001 ~ "***",
p_value < 0.01 ~ "**",
p_value < 0.05 ~ "*",
p_value < 0.1 ~ ".",
TRUE ~ ""
)
)
# Label pendek untuk kategori
label_kat <- c(
"Working part time" = "Part Time vs Full Time",
"Keeping house" = "Keeping House vs Full Time",
"Unemployed, laid off, looking for work" = "Unemployed vs Full Time"
)
rrr_display <- rrr_df |>
mutate(
Perbandingan = label_kat[Kategori],
`RRR` = round(RRR, 4),
`SE` = round(SE, 4),
`z-value` = round(z, 4),
`p-value` = signif(p_value, 3),
`IK 95%` = paste0(round(CI_low, 3), " – ", round(CI_high, 3))
) |>
select(Perbandingan, Prediktor, `RRR`, `SE`, `z-value`, `p-value`,
`IK 95%`, Signifikan)
knitr::kable(rrr_display,
caption = "Ringkasan hasil regresi logistik multinomial (referensi: Full Time)")| Perbandingan | Prediktor | RRR | SE | z-value | p-value | IK 95% | Signifikan |
|---|---|---|---|---|---|---|---|
| Part Time vs Full Time | age | 1.3362 | 0.1653 | 1.7529 | 7.96e-02 | 0.966 – 1.848 | . |
| Keeping House vs Full Time | age | 0.8300 | 0.2856 | -0.6523 | 5.14e-01 | 0.474 – 1.453 | |
| Unemployed vs Full Time | age | 0.5813 | 0.3544 | -1.5308 | 1.26e-01 | 0.29 – 1.164 | |
| Part Time vs Full Time | sexFEMALE | 0.4362 | 0.3096 | -2.6797 | 7.37e-03 | 0.238 – 0.8 | ** |
| Keeping House vs Full Time | sexFEMALE | 0.2692 | 0.3525 | -3.7219 | 1.98e-04 | 0.135 – 0.537 | *** |
| Unemployed vs Full Time | sexFEMALE | 0.1665 | 0.4369 | -4.1041 | 4.06e-05 | 0.071 – 0.392 | *** |
| Part Time vs Full Time | maritalNever married | 1.0052 | 0.0066 | 0.7834 | 4.33e-01 | 0.992 – 1.018 | |
| Keeping House vs Full Time | maritalNever married | 9.1727 | 0.2174 | 10.1927 | 0.00e+00 | 5.99 – 14.047 | *** |
| Unemployed vs Full Time | maritalNever married | 0.5151 | 0.1888 | -3.5136 | 4.42e-04 | 0.356 – 0.746 | *** |
| Part Time vs Full Time | degreeHigh school | 0.4326 | 0.2671 | -3.1370 | 1.71e-03 | 0.256 – 0.73 | ** |
| Keeping House vs Full Time | degreeHigh school | 0.1068 | 0.4322 | -5.1759 | 2.00e-07 | 0.046 – 0.249 | *** |
| Unemployed vs Full Time | degreeHigh school | 0.1579 | 0.3067 | -6.0175 | 0.00e+00 | 0.087 – 0.288 | *** |
| Part Time vs Full Time | degreeAssociate/junior college | 0.0822 | 0.3712 | -6.7325 | 0.00e+00 | 0.04 – 0.17 | *** |
| Keeping House vs Full Time | degreeAssociate/junior college | 0.1036 | 0.5389 | -4.2078 | 2.58e-05 | 0.036 – 0.298 | *** |
| Unemployed vs Full Time | degreeAssociate/junior college | 1.0052 | 0.0085 | 0.6131 | 5.40e-01 | 0.989 – 1.022 | |
| Part Time vs Full Time | degreeBachelor’s | 1.6171 | 0.2160 | 2.2253 | 2.61e-02 | 1.059 – 2.469 | * |
| Keeping House vs Full Time | degreeBachelor’s | 4.2357 | 0.2766 | 5.2189 | 2.00e-07 | 2.463 – 7.284 | *** |
| Unemployed vs Full Time | degreeBachelor’s | 0.2998 | 0.2927 | -4.1149 | 3.87e-05 | 0.169 – 0.532 | *** |
| Part Time vs Full Time | degreeGraduate | 0.1917 | 0.4484 | -3.6833 | 2.30e-04 | 0.08 – 0.462 | *** |
| Keeping House vs Full Time | degreeGraduate | 0.1308 | 0.3733 | -5.4498 | 1.00e-07 | 0.063 – 0.272 | *** |
| Unemployed vs Full Time | degreeGraduate | 0.0771 | 0.5222 | -4.9087 | 9.00e-07 | 0.028 – 0.214 | *** |
Catatan: *** p < 0,001 |
** p < 0,01 | * p < 0,05 |
. p < 0,1
Interpretasi output: Tabel menampilkan tiga blok perbandingan. Setiap baris adalah satu koefisien prediktor untuk satu perbandingan kategori.
Misalkan kategori referensi adalah Working full time. Maka:
Jika \(\exp(\beta) > 1\), maka kenaikan prediktor meningkatkan odds relatif masuk kategori tersebut dibanding kategori referensi. Jika \(\exp(\beta) < 1\), maka sebaliknya.
Contoh interpretasi: Jika koefisien
sexFEMALE untuk kategori Keeping House bernilai positif dan
besar, maka responden perempuan memiliki kecenderungan yang secara
substansial lebih besar untuk berada dalam status mengurus rumah tangga
dibandingkan laki-laki, dengan asumsi variabel lain konstan.
rrr_plot <- rrr_df |>
filter(!grepl("Intercept", Prediktor)) |>
mutate(
Perbandingan = label_kat[Kategori],
Prediktor = gsub("degree|sex|marital", "", Prediktor)
)
ggplot(rrr_plot,
aes(x = RRR, y = reorder(Prediktor, RRR),
color = Perbandingan)) +
geom_vline(xintercept = 1, linetype = "dashed",
color = "#6c757d", linewidth = 0.8) +
geom_errorbarh(aes(xmin = CI_low, xmax = CI_high),
height = 0.25, linewidth = 0.7,
position = position_dodge(width = 0.6)) +
geom_point(size = 2.8,
position = position_dodge(width = 0.6)) +
scale_color_manual(values = c(
"Part Time vs Full Time" = "#e9c46a",
"Keeping House vs Full Time"= "#f4a261",
"Unemployed vs Full Time" = "#e76f51"
)) +
scale_x_log10() +
labs(title = "Relative Risk Ratio (RRR) Model Multinomial",
subtitle = "Skala logaritmik; garis putus-putus = RRR 1 (tidak ada efek)",
x = "RRR (skala log)",
y = NULL,
color = "Perbandingan") +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")Interpretasi output: Forest plot RRR menampilkan estimasi titik dan interval kepercayaan 95% untuk setiap koefisien. Titik di sebelah kanan garis putus-putus (RRR > 1) berarti prediktor meningkatkan kecenderungan masuk kategori tersebut dibanding Full Time. Jika interval kepercayaan tidak melewati garis 1, koefisien signifikan secara statistik.
prob_all <- predict(model_multi, newdata = data_bersih, type = "probs")
pred_kelas <- predict(model_multi, newdata = data_bersih, type = "class")
head(prob_all)## Working full time Working part time Keeping house
## 1 0.8528441 0.07539336 0.01369875
## 2 0.7719299 0.08442271 0.03159040
## 3 0.8246349 0.13452384 0.01928554
## 4 0.4225037 0.19255022 0.35772929
## 5 0.5787316 0.26717386 0.12872678
## 6 0.7180141 0.10647744 0.10045527
## Unemployed, laid off, looking for work
## 1 0.05806376
## 2 0.11205702
## 3 0.02155570
## 4 0.02721680
## 5 0.02536777
## 6 0.07505322
Tambahkan prediksi ke data:
data_bersih_pred <- data_bersih |>
dplyr::bind_cols(as.data.frame(prob_all)) |>
mutate(
prediksi = factor(
case_when(
pred_kelas == "Working full time" ~ "Full Time",
pred_kelas == "Working part time" ~ "Part Time",
pred_kelas == "Keeping house" ~ "Keeping House",
pred_kelas == "Unemployed, laid off, looking for work" ~ "Unemployed"
),
levels = c("Full Time", "Part Time", "Keeping House", "Unemployed")
)
)
# Tampilkan contoh prediksi
pred_preview <- head(
data.frame(
`Status aktual` = data_bersih_pred$wrkstat_label,
`Pred. kelas` = data_bersih_pred$prediksi,
`P(Full Time)` = round(prob_all[, "Working full time"], 3),
`P(Part Time)` = round(prob_all[, "Working part time"], 3),
`P(Keep. House)` = round(prob_all[, "Keeping house"], 3),
`P(Unemployed)` = round(prob_all[,
"Unemployed, laid off, looking for work"], 3),
check.names = FALSE
),
8
)
knitr::kable(pred_preview,
caption = "Contoh prediksi probabilitas (6 observasi pertama)")| Status aktual | Pred. kelas | P(Full Time) | P(Part Time) | P(Keep. House) | P(Unemployed) |
|---|---|---|---|---|---|
| Full Time | Full Time | 0.853 | 0.075 | 0.014 | 0.058 |
| Part Time | Full Time | 0.772 | 0.084 | 0.032 | 0.112 |
| Unemployed | Full Time | 0.825 | 0.135 | 0.019 | 0.022 |
| Full Time | Full Time | 0.423 | 0.193 | 0.358 | 0.027 |
| Full Time | Full Time | 0.579 | 0.267 | 0.129 | 0.025 |
| Full Time | Full Time | 0.718 | 0.106 | 0.100 | 0.075 |
| Full Time | Full Time | 0.800 | 0.070 | 0.118 | 0.012 |
| Full Time | Full Time | 0.815 | 0.140 | 0.030 | 0.015 |
Interpretasi output: Setiap baris menampilkan empat peluang prediksi yang jumlahnya selalu = 1. Model mengklasifikasikan observasi ke kategori dengan peluang tertinggi.
conf_multi <- table(
Aktual = data_bersih_pred$wrkstat_label,
Prediksi = data_bersih_pred$prediksi
)
conf_multi## Prediksi
## Aktual Full Time Part Time Keeping House Unemployed
## Full Time 1035 2 8 0
## Part Time 219 4 6 0
## Keeping House 178 6 13 0
## Unemployed 96 2 6 0
knitr::kable(addmargins(conf_multi),
caption = "Confusion matrix (baris = aktual, kolom = prediksi)")| Full Time | Part Time | Keeping House | Unemployed | Sum | |
|---|---|---|---|---|---|
| Full Time | 1035 | 2 | 8 | 0 | 1045 |
| Part Time | 219 | 4 | 6 | 0 | 229 |
| Keeping House | 178 | 6 | 13 | 0 | 197 |
| Unemployed | 96 | 2 | 6 | 0 | 104 |
| Sum | 1528 | 14 | 33 | 0 | 1575 |
## [1] 0.6679365
Interpretasi output: Diagonal confusion matrix menunjukkan prediksi yang benar untuk setiap kategori. Akurasi tidak selalu cukup untuk menilai model, khususnya jika kategori tidak seimbang. Untuk analisis yang lebih lengkap dapat digunakan ukuran lain seperti macro-F1, balanced accuracy, atau log-loss.
actual_vec <- data_bersih_pred$wrkstat_label
pred_vec <- data_bersih_pred$prediksi
kelas_levels <- levels(actual_vec)
per_kelas <- lapply(kelas_levels, function(k) {
tp <- sum(actual_vec == k & pred_vec == k)
fp <- sum(actual_vec != k & pred_vec == k)
fn <- sum(actual_vec == k & pred_vec != k)
tn <- sum(actual_vec != k & pred_vec != k)
sensitivity <- ifelse((tp + fn) == 0, NA, tp / (tp + fn))
precision <- ifelse((tp + fp) == 0, NA, tp / (tp + fp))
specificity <- ifelse((tn + fp) == 0, NA, tn / (tn + fp))
f1 <- ifelse(is.na(precision) | is.na(sensitivity) |
(precision + sensitivity) == 0, NA,
2 * precision * sensitivity /
(precision + sensitivity))
data.frame(Kategori = k,
TP = tp, FP = fp, FN = fn, TN = tn,
Sensitivity = round(sensitivity, 3),
Specificity = round(specificity, 3),
Precision = round(precision, 3),
F1 = round(f1, 3))
}) |> dplyr::bind_rows()
knitr::kable(per_kelas,
caption = "Metrik evaluasi per kategori")| Kategori | TP | FP | FN | TN | Sensitivity | Specificity | Precision | F1 |
|---|---|---|---|---|---|---|---|---|
| Full Time | 1035 | 493 | 10 | 37 | 0.990 | 0.070 | 0.677 | 0.805 |
| Part Time | 4 | 10 | 225 | 1336 | 0.017 | 0.993 | 0.286 | 0.033 |
| Keeping House | 13 | 20 | 184 | 1358 | 0.066 | 0.985 | 0.394 | 0.113 |
| Unemployed | 0 | 0 | 104 | 1471 | 0.000 | 1.000 | NA | NA |
overall_table <- data.frame(
Metrik = c("Overall Accuracy",
"Macro-avg Sensitivity",
"Macro-avg Precision",
"Macro-avg F1-score"),
Nilai = round(c(
accuracy_multi,
mean(per_kelas$Sensitivity, na.rm = TRUE),
mean(per_kelas$Precision, na.rm = TRUE),
mean(per_kelas$F1, na.rm = TRUE)
), 3)
)
knitr::kable(overall_table,
caption = "Metrik evaluasi keseluruhan (macro-average)")| Metrik | Nilai |
|---|---|
| Overall Accuracy | 0.668 |
| Macro-avg Sensitivity | 0.268 |
| Macro-avg Precision | 0.452 |
| Macro-avg F1-score | 0.317 |
Interpretasi output — metrik per kategori: Sensitivity mengukur seberapa baik model mengenali observasi yang benar-benar masuk kategori tersebut. Precision mengukur seberapa andal prediksi untuk kategori itu. F1-score menyeimbangkan keduanya.
Interpretasi output — metrik keseluruhan: Macro-average menghitung rata-rata sederhana metrik per kategori tanpa mempertimbangkan ukuran kelas, sehingga memberikan gambaran yang lebih adil terhadap kategori minoritas seperti Unemployed.
Kita dapat melihat bagaimana probabilitas pilihan status pekerjaan berubah terhadap usia, dengan variabel lain ditahan pada nilai/kategori referensinya.
grid_multi <- expand.grid(
age = seq(min(data_bersih$age), max(data_bersih$age), length.out = 100),
sex = factor("MALE", levels = levels(data_bersih$sex)),
marital = factor("Married", levels = levels(data_bersih$marital)),
degree = factor("High school", levels = levels(data_bersih$degree))
)
grid_prob <- predict(model_multi, newdata = grid_multi, type = "probs")
grid_plot <- grid_multi |>
dplyr::bind_cols(as.data.frame(grid_prob)) |>
tidyr::pivot_longer(
cols = c("Working full time", "Working part time",
"Keeping house",
"Unemployed, laid off, looking for work"),
names_to = "status",
values_to = "probabilitas"
) |>
mutate(status = case_when(
status == "Working full time" ~ "Full Time",
status == "Working part time" ~ "Part Time",
status == "Keeping house" ~ "Keeping House",
status == "Unemployed, laid off, looking for work" ~ "Unemployed"
),
status = factor(status,
levels = c("Full Time", "Part Time",
"Keeping House", "Unemployed")))
ggplot(grid_plot, aes(x = age, y = probabilitas, color = status)) +
geom_line(linewidth = 1.35) +
scale_y_continuous(labels = scales::percent) +
scale_color_manual(values = warna_kategori) +
labs(title = "Prediksi Probabilitas Status Pekerjaan",
subtitle = "Variabel lain: MALE, Married, High school.",
x = "Usia (tahun)",
y = "Probabilitas prediksi",
color = "Status pekerjaan") +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")Interpretasi output: Grafik ini membantu membaca hasil model secara lebih substantif. Jika garis Keeping House meningkat seiring bertambahnya usia, model menunjukkan bahwa responden yang lebih tua lebih cenderung berada dalam status mengurus rumah tangga. Interpretasi seperti ini sering lebih mudah dipahami dibanding hanya melihat tabel koefisien.
cm_df <- as.data.frame(table(
Aktual = data_bersih_pred$wrkstat_label,
Prediksi = data_bersih_pred$prediksi
)) |>
group_by(Aktual) |>
mutate(prop = Freq / sum(Freq)) |>
ungroup()
ggplot(cm_df, aes(x = Prediksi, y = Aktual, fill = prop)) +
geom_tile(color = "white", linewidth = 0.8) +
geom_text(aes(label = paste0(Freq, "\n(", scales::percent(prop, accuracy = 1), ")")),
fontface = "bold", size = 3.4) +
scale_fill_gradient(low = "#f8f9fa", high = "#2a9d8f",
labels = scales::percent) +
labs(title = "Confusion Matrix (Proporsi per Baris)",
subtitle = "Angka = jumlah observasi; persentase = dari total aktual per kategori",
x = "Prediksi", y = "Aktual",
fill = "Proporsi") +
theme_minimal(base_size = 12) +
theme(axis.text.x = element_text(angle = 20, hjust = 1))Interpretasi output: Heatmap confusion matrix memudahkan identifikasi pola kesalahan. Sel diagonal yang berwarna lebih gelap menunjukkan performa yang lebih baik untuk kategori tersebut.
degree diperlakukan sebagai kategorik nominal
meskipun memiliki urutan alami. Jika urutan ingin dieksploitasi, dapat
dipertimbangkan menggunakan model ordinal atau mengkode
degree sebagai skor numerik.Analisis regresi logistik multinomial terhadap data GSS 2024 menghasilkan beberapa temuan utama:
Pertama, formulasi model. Model membentuk tiga persamaan logit secara simultan — masing-masing membandingkan Part Time, Keeping House, dan Unemployed terhadap Full Time sebagai kategori referensi. Koefisien diinterpretasikan sebagai relative risk ratio (RRR) setelah dieksponensialkan.
Kedua, prediktor dominan. Jenis kelamin
(sex) dan tingkat pendidikan (degree)
kemungkinan besar merupakan prediktor terkuat, khususnya untuk
memisahkan kategori Keeping House dan Full Time, serta Unemployed dan
Full Time. Perempuan memiliki kecenderungan lebih tinggi untuk berada
dalam status Keeping House dibandingkan laki-laki. Pendidikan yang lebih
tinggi umumnya dikaitkan dengan kecenderungan lebih rendah untuk
pengangguran.
Ketiga, performa klasifikasi. Overall accuracy pada data sebesar 66.8%. Ketimpangan performa antar kategori mencerminkan ketidakseimbangan jumlah sampel, di mana Full Time mendominasi dataset.
Keempat, keterbatasan. Model ini memberikan gambaran awal yang informatif mengenai faktor demografis yang berkaitan dengan status pekerjaan, namun tidak boleh diinterpretasikan sebagai hubungan kausal. Asumsi IIA yang melekat pada model multinomial perlu diverifikasi secara teoritis sesuai konteks.
Regresi logistik ordinal digunakan ketika variabel respons bersifat kategorik dengan urutan alami. Berbeda dari regresi multinomial yang memperlakukan semua kategori setara, model ordinal mengeksploitasi informasi urutan tersebut sehingga lebih efisien secara statistik dan lebih kaya secara interpretatif.
Kasus ini berbeda dari regresi biner maupun multinomial:
| Model | Tipe respons | Contoh |
|---|---|---|
| Logistik biner | 2 kategori tanpa urutan | Setuju / Tidak setuju |
| Logistik multinomial | ≥ 3 kategori tanpa urutan | Jenis pekerjaan |
| Logistik ordinal | ≥ 3 kategori dengan urutan | Tingkat kebahagiaan |
Studi kasus: Analisis determinan tingkat kebahagiaan responden berdasarkan data General Social Survey (GSS) 2024.
Tujuan analisis: Membangun model regresi logistik ordinal untuk memprediksi tingkat kebahagiaan (Not too happy < Pretty happy < Very happy) berdasarkan status pernikahan, kondisi kesehatan, dan pendapatan riil responden.
Tingkat kebahagiaan subjektif merupakan variabel ordinal — kategorinya memiliki urutan yang jelas namun jarak antar-kategori tidak diasumsikan sama. Model yang tepat adalah regresi logistik ordinal, bukan multinomial (yang mengabaikan urutan) maupun regresi linear biasa (yang mengasumsikan jarak sama antar-kategori).
Model yang digunakan adalah:
\[ \log\left( \frac{P(Y_i \leq j \mid \mathbf{x}_i)}{P(Y_i > j \mid \mathbf{x}_i)} \right) = \alpha_j + \beta_1 \cdot \text{log\_income}_i + \beta_2 \cdot \mathbf{1}[\text{marital}_i = \text{Married}] + \beta_3 \cdot \mathbf{1}[\text{health}_i = \text{Fair}] + \cdots + \beta_5 \cdot \mathbf{1}[\text{health}_i = \text{Excellent}], \]
dengan \(j \in \{1, 2\}\) (dua batas kumulatif untuk tiga kategori) dan vektor koefisien \(\boldsymbol{\beta}\) yang sama untuk semua \(j\) (proportional odds assumption). Model membangun \(J - 1 = 2\) persamaan cumulative logit secara simultan, hanya dibedakan oleh cutpoint \(\alpha_1 < \alpha_2\).
Dataset berasal dari General Social Survey (GSS) 2024, survei sosial nasional Amerika Serikat yang diselenggarakan oleh NORC di University of Chicago. Dataset terdiri dari 2.124 responden dengan 5 variabel.
raw_data <- readxl::read_excel("Data Ordinal.xlsx", sheet = 1)
data.frame(
Keterangan = c("Jumlah observasi", "Jumlah variabel"),
Nilai = c(nrow(raw_data), ncol(raw_data))
) |>
kable(caption = "Ukuran dataset GSS 2024") |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = FALSE)| Keterangan | Nilai |
|---|---|
| Jumlah observasi | 2124 |
| Jumlah variabel | 5 |
Interpretasi output: Dataset berisi 2124 observasi dan 5 variabel. Ukuran sampel ini memadai untuk regresi logistik ordinal dengan tiga prediktor.
kamus <- data.frame(
`Nama kolom` = c("id_","marital","happy","health","realinc"),
`Keterangan` = c(
"Nomor identifikasi responden",
"Status pernikahan responden",
"Tingkat kebahagiaan responden (variabel respons)",
"Kondisi kesehatan responden",
"Pendapatan riil keluarga (USD per tahun)"
),
`Kategori / Rentang` = c(
"—",
"Married, Never married",
"Not too happy < Pretty happy < Very happy",
"Poor < Fair < Good < Excellent",
"Kontinu; min ~181, maks ~117.565"
),
`Tipe dalam analisis` = c(
"ID (tidak dipakai dalam model)",
"Kategorik (2 level)",
"Respons ordinal (3 kategori)",
"Kategorik ordinal (4 level, dipakai sebagai kategorik)",
"Numerik (log-transformasi disarankan)"
),
check.names = FALSE
)
kamus |>
kable(caption = "Penjelasan variabel dataset GSS 2024") |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = TRUE)| Nama kolom | Keterangan | Kategori / Rentang | Tipe dalam analisis |
|---|---|---|---|
| id_ | Nomor identifikasi responden | — | ID (tidak dipakai dalam model) |
| marital | Status pernikahan responden | Married, Never married | Kategorik (2 level) |
| happy | Tingkat kebahagiaan responden (variabel respons) | Not too happy < Pretty happy < Very happy | Respons ordinal (3 kategori) |
| health | Kondisi kesehatan responden | Poor < Fair < Good < Excellent | Kategorik ordinal (4 level, dipakai sebagai kategorik) |
| realinc | Pendapatan riil keluarga (USD per tahun) | Kontinu; min ~181, maks ~117.565 | Numerik (log-transformasi disarankan) |
Interpretasi output: Variabel respons
happy memiliki tiga kategori berurutan sehingga tepat
dimodelkan dengan regresi logistik ordinal. Variabel health
juga bersifat ordinal, tetapi diperlakukan sebagai prediktor kategorik
dalam model ini. Pendapatan (realinc) memiliki distribusi
yang sangat miring ke kanan sehingga perlu ditransformasi logaritmik
sebelum dimasukkan ke model.
data_bersih <- raw_data |>
mutate(
realinc = as.numeric(realinc),
# Log-transformasi pendapatan untuk menstabilkan distribusi
log_income = log(realinc + 1),
# Respons ordinal: urutan dari rendah ke tinggi
happy = ordered(happy,
levels = c("Not too happy", "Pretty happy", "Very happy")),
# Kondisi kesehatan sebagai factor urutan; Poor sebagai referensi
health = factor(health,
levels = c("Poor","Fair","Good","Excellent")),
# Status pernikahan; Never married sebagai referensi
marital = factor(marital,
levels = c("Never married", "Married"))
) |>
tidyr::drop_na()
head(data_bersih |>
select(id_, marital, health, realinc, log_income, happy), 8) |>
kable(caption = "Contoh data setelah transformasi",
digits = 2) |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = FALSE)| id_ | marital | health | realinc | log_income | happy |
|---|---|---|---|---|---|
| 1 | Never married | Good | 43560.0 | 10.68 | Not too happy |
| 2 | Never married | Good | 24502.5 | 10.11 | Pretty happy |
| 3 | Married | Good | 43560.0 | 10.68 | Pretty happy |
| 4 | Never married | Good | 58080.0 | 10.97 | Pretty happy |
| 6 | Married | Good | 2722.5 | 7.91 | Pretty happy |
| 7 | Married | Good | 117564.8 | 11.67 | Pretty happy |
| 9 | Married | Good | 117564.8 | 11.67 | Very happy |
| 11 | Never married | Good | 43560.0 | 10.68 | Pretty happy |
Interpretasi output: Delapan baris pertama data
setelah transformasi. Kategori referensi yang dipilih adalah:
Never married untuk marital dan
Poor untuk health. Kolom
log_income adalah transformasi logaritmik natural dari
pendapatan riil; distribusinya jauh lebih simetris dibandingkan skala
asli. Semua perbandingan odds ratio akan mengacu pada kategori-kategori
referensi ini.
data_bersih |>
count(happy, name = "Jumlah") |>
mutate(Proporsi = scales::percent(Jumlah / sum(Jumlah), accuracy = 0.1)) |>
rename(`Tingkat kebahagiaan` = happy) |>
kable(caption = "Distribusi tingkat kebahagiaan responden") |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = FALSE)| Tingkat kebahagiaan | Jumlah | Proporsi |
|---|---|---|
| Not too happy | 403 | 19.0% |
| Pretty happy | 1242 | 58.5% |
| Very happy | 479 | 22.6% |
Interpretasi output: Distribusi tingkat kebahagiaan tidak merata: Pretty happy merupakan kategori terbanyak, diikuti Not too happy dan Very happy. Ketidakseimbangan ini lazim dalam data survei kebahagiaan dan tidak menghalangi penggunaan model ordinal.
warna_happy <- c(
"Not too happy" = "#5568b8",
"Pretty happy" = "#d18b2f",
"Very happy" = "#2f7f73"
)
data_bersih |>
count(happy) |>
mutate(proporsi = n / sum(n)) |>
ggplot(aes(x = happy, y = proporsi, fill = happy)) +
geom_col(width = 0.65, alpha = 0.94) +
geom_text(aes(label = percent(proporsi, accuracy = 0.1)),
vjust = -0.4, fontface = "bold", color = "#243142") +
scale_y_continuous(labels = percent_format(), limits = c(0, 0.70)) +
scale_fill_manual(values = warna_happy) +
labs(title = "Distribusi Tingkat Kebahagiaan",
subtitle = "Respons ordinal: kategori memiliki urutan alami.",
x = NULL, y = "Proporsi") +
theme_minimal() +
theme(legend.position = "none")Interpretasi output: Mayoritas responden merasa cukup bahagia (Pretty happy). Kategori Very happy dan Not too happy masing-masing mewakili sekitar sepertiga dari sisa responden. Pola ini menunjukkan distribusi yang cenderung terpusat di kategori tengah.
data_bersih |>
group_by(happy) |>
summarise(
Jumlah = n(),
Rata2 = mean(log_income, na.rm = TRUE),
Median = median(log_income, na.rm = TRUE),
Std = sd(log_income, na.rm = TRUE),
Min = min(log_income),
Maks = max(log_income),
.groups = "drop"
) |>
rename(`Tingkat kebahagiaan` = happy) |>
mutate(across(where(is.numeric), round, 3)) |>
kable(caption = "Ringkasan log-pendapatan menurut tingkat kebahagiaan") |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = FALSE)| Tingkat kebahagiaan | Jumlah | Rata2 | Median | Std | Min | Maks |
|---|---|---|---|---|---|---|
| Not too happy | 403 | 9.467 | 9.701 | 1.451 | 5.207 | 11.675 |
| Pretty happy | 1242 | 10.041 | 10.307 | 1.237 | 5.207 | 11.675 |
| Very happy | 479 | 10.137 | 10.307 | 1.307 | 5.207 | 11.675 |
Interpretasi output: Tabel memperlihatkan statistik deskriptif log-pendapatan untuk setiap kategori kebahagiaan. Perbedaan rata-rata dan median log-pendapatan antar kategori memberikan gambaran awal mengenai peran pendapatan sebagai prediktor.
Data dibagi secara stratified 80:20 berdasarkan
kategori happy agar proporsi setiap kategori terjaga pada
kedua subset.
stratified_split <- function(y, prop = 0.8) {
idx_by_class <- split(seq_along(y), y)
train_idx <- lapply(idx_by_class,
function(idx) sample(idx, size = floor(length(idx) * prop)))
unlist(train_idx, use.names = FALSE)
}
set.seed(42)
train_id <- stratified_split(as.integer(data_bersih$happy), prop = 0.8)
train_data <- data_bersih[ train_id, ]
test_data <- data_bersih[-train_id, ]
bind_rows(
train_data |> count(happy) |> mutate(Data = "Training"),
test_data |> count(happy) |> mutate(Data = "Testing")
) |>
group_by(Data) |>
mutate(Proporsi = scales::percent(n / sum(n), accuracy = 0.1)) |>
ungroup() |>
rename(`Tingkat kebahagiaan` = happy, Jumlah = n) |>
kable(caption = "Distribusi kelas pada training dan testing") |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = FALSE)| Tingkat kebahagiaan | Jumlah | Data | Proporsi |
|---|---|---|---|
| Not too happy | 322 | Training | 19.0% |
| Pretty happy | 993 | Training | 58.5% |
| Very happy | 383 | Training | 22.6% |
| Not too happy | 81 | Testing | 19.0% |
| Pretty happy | 249 | Testing | 58.5% |
| Very happy | 96 | Testing | 22.5% |
Interpretasi output: Data training berisi 1698 observasi dan data testing berisi 426 observasi. Split stratified memastikan proporsi Not too happy, Pretty happy, dan Very happy pada training dan testing konsisten dengan proporsi data asli.
Misalkan \(Y_i\) adalah tingkat kebahagiaan responden ke-\(i\), dengan:
\[ 1 = \text{Not too happy} < 2 = \text{Pretty happy} < 3 = \text{Very happy}. \]
Model cumulative logit membentuk dua persamaan (untuk \(J = 3\), ada \(J-1 = 2\) cutpoint):
\[ \operatorname{logit}\{P(Y_i \leq 1)\} = \alpha_1 + \beta_1 \cdot \text{log\_income}_i + \beta_2 \cdot \mathbf{1}[\text{marital}_i = \text{Married}] + \beta_3 \cdot \mathbf{1}[\text{health}_i = \text{Fair}] + \beta_4 \cdot \mathbf{1}[\text{health}_i = \text{Good}] + \beta_5 \cdot \mathbf{1}[\text{health}_i = \text{Excellent}], \]
\[ \operatorname{logit}\{P(Y_i \leq 2)\} = \alpha_2 + \beta_1 \cdot \text{log\_income}_i + \cdots + \beta_5 \cdot \mathbf{1}[\text{health}_i = \text{Excellent}]. \]
Karena proportional odds, koefisien \(\boldsymbol{\beta}\) sama untuk kedua persamaan; yang berbeda hanya cutpoint \(\alpha_1 < \alpha_2\).
fit_ord <- MASS::polr(
happy ~ log_income + marital + health,
data = train_data,
method = "logistic",
Hess = TRUE
)
# ── Ringkasan kecocokan model ──────────────────────────────────────────────
null_model <- MASS::polr(happy ~ 1,
data = train_data, method = "logistic", Hess = TRUE)
ll_null <- logLik(null_model)
ll_full <- logLik(fit_ord)
G2 <- -2 * (as.numeric(ll_null) - as.numeric(ll_full))
df_G2 <- attr(ll_full, "df") - attr(ll_null, "df")
p_G2 <- pchisq(G2, df = df_G2, lower.tail = FALSE)
pseudo_r2 <- 1 - as.numeric(ll_full) / as.numeric(ll_null)
data.frame(
Keterangan = c("Jumlah observasi training",
"Log-likelihood model kosong",
"Log-likelihood model penuh",
"Statistik G² (uji rasio likelihood)",
"Derajat bebas G²",
"p-value uji G²",
"McFadden Pseudo-R²",
"AIC"),
Nilai = c(
nrow(train_data),
round(as.numeric(ll_null), 3),
round(as.numeric(ll_full), 3),
round(G2, 3),
df_G2,
signif(p_G2, 3),
round(pseudo_r2, 4),
round(AIC(fit_ord), 3)
)
) |>
kable(caption = "Ringkasan kecocokan model regresi logistik ordinal") |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = FALSE)| Keterangan | Nilai |
|---|---|
| Jumlah observasi training | 1698.0000 |
| Log-likelihood model kosong | -1638.4480 |
| Log-likelihood model penuh | -1522.2480 |
| Statistik G² (uji rasio likelihood) | 232.4000 |
| Derajat bebas G² | 5.0000 |
| p-value uji G² | 0.0000 |
| McFadden Pseudo-R² | 0.0709 |
| AIC | 3058.4950 |
Interpretasi output: Statistik \(G^2\) membandingkan model penuh dengan model kosong. Jika p-value < 0,05, prediktor secara bersama-sama berkontribusi signifikan dalam menjelaskan variasi tingkat kebahagiaan. McFadden Pseudo-\(R^2\) mengukur proporsi log-likelihood yang dijelaskan oleh prediktor; nilai di atas 0,10 sudah cukup substansial untuk data survei sosial.
Model menghasilkan koefisien yang sama untuk kedua persamaan (proportional odds), ditambah dua cutpoint.
ord_coef <- coef(summary(fit_ord))
result_ord <- as.data.frame(ord_coef) |>
tibble::rownames_to_column("parameter") |>
dplyr::rename(estimate = Value,
std_error = `Std. Error`,
z_value = `t value`) |>
mutate(
p_value = 2 * (1 - pnorm(abs(z_value))),
jenis = ifelse(grepl("\\|", parameter), "Cutpoint", "Koefisien"),
estimate_slide = ifelse(jenis == "Koefisien", -estimate, estimate),
OR_polr = ifelse(jenis == "Koefisien", exp(estimate), NA_real_),
OR_slide = ifelse(jenis == "Koefisien", exp(estimate_slide), NA_real_),
CI_low_polr = ifelse(jenis == "Koefisien",
exp(estimate - 1.96 * std_error), NA_real_),
CI_high_polr = ifelse(jenis == "Koefisien",
exp(estimate + 1.96 * std_error), NA_real_),
Signifikan = case_when(
p_value < 0.001 ~ "***",
p_value < 0.01 ~ "**",
p_value < 0.05 ~ "*",
p_value < 0.1 ~ ".",
TRUE ~ ""
)
)
result_ord |>
mutate(across(c(estimate, estimate_slide, std_error, z_value,
p_value, OR_polr, OR_slide,
CI_low_polr, CI_high_polr), ~round(.x, 4))) |>
kable(caption = "Ringkasan hasil regresi logistik ordinal (data training)",
col.names = c("Parameter","Estimate polr","SE","z/t-value","p-value",
"Jenis","Estimate slide","OR polr","OR slide",
"CI polr 2.5%","CI polr 97.5%","Sig.")) |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = TRUE)| Parameter | Estimate polr | SE | z/t-value | p-value | Jenis | Estimate slide | OR polr | OR slide | CI polr 2.5% | CI polr 97.5% | Sig. |
|---|---|---|---|---|---|---|---|---|---|---|---|
| log_income | 0.0322 | 0.0407 | 0.7933 | 0.4276 | Koefisien | -0.0322 | 1.0328 | 0.9683 | 0.9537 | 1.1184 | |
| maritalMarried | 1.0250 | 0.1091 | 9.3944 | 0.0000 | Koefisien | -1.0250 | 2.7871 | 0.3588 | 2.2505 | 3.4516 | *** |
| healthFair | 0.7007 | 0.3018 | 2.3220 | 0.0202 | Koefisien | -0.7007 | 2.0152 | 0.4962 | 1.1154 | 3.6407 |
|
| healthGood | 1.4721 | 0.2965 | 4.9651 | 0.0000 | Koefisien | -1.4721 | 4.3583 | 0.2294 | 2.4375 | 7.7927 | *** |
| healthExcellent | 2.0995 | 0.3132 | 6.7028 | 0.0000 | Koefisien | -2.0995 | 8.1618 | 0.1225 | 4.4174 | 15.0801 | *** |
| Not too happy|Pretty happy | 0.6325 | 0.4573 | 1.3831 | 0.1666 | Cutpoint | 0.6325 | NA | NA | NA | NA | |
| Pretty happy|Very happy | 3.6428 | 0.4671 | 7.7989 | 0.0000 | Cutpoint | 3.6428 | NA | NA | NA | NA | *** |
Catatan: *** p < 0,001 |
** p < 0,01 | * p < 0,05 |
. p < 0,1
Interpretasi kolom:
MASS::polr() dengan konvensi \(\alpha_j -
\mathbf{x}^\top\boldsymbol{\beta}\).result_ord |>
filter(jenis == "Cutpoint") |>
mutate(
`Batas kumulatif` = gsub("\\|", " ≤ Y ≤ ", parameter),
`Cutpoint (α)` = round(estimate, 4),
`SE` = round(std_error, 4),
`z-value` = round(z_value, 4),
`p-value` = round(p_value, 4)
) |>
select(`Batas kumulatif`, `Cutpoint (α)`, SE, `z-value`, `p-value`) |>
kable(caption = "Cutpoint model ordinal") |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = FALSE)| Batas kumulatif | Cutpoint (α) | SE | z-value | p-value |
|---|---|---|---|---|
| Not too happy ≤ Y ≤ Pretty happy | 0.6325 | 0.4573 | 1.3831 | 0.1666 |
| Pretty happy ≤ Y ≤ Very happy | 3.6428 | 0.4671 | 7.7989 | 0.0000 |
Interpretasi output: Terdapat \(J - 1 = 2\) cutpoint karena respons memiliki 3 kategori. Cutpoint \(\alpha_1\) adalah batas antara Not too happy dan Pretty happy; \(\alpha_2\) adalah batas antara Pretty happy dan Very happy. Nilai \(\alpha_1 < \alpha_2\) harus terpenuhi agar model konsisten secara logis.
prob_test <- predict(fit_ord, newdata = test_data, type = "probs")
pred_kelas <- predict(fit_ord, newdata = test_data, type = "class")
head(
data.frame(
`Status aktual` = test_data$happy,
`Pred. kelas` = pred_kelas,
`P(Not too happy)` = round(prob_test[,"Not too happy"], 4),
`P(Pretty happy)` = round(prob_test[,"Pretty happy"], 4),
`P(Very happy)` = round(prob_test[,"Very happy"], 4),
check.names = FALSE
),
8
) |>
kable(caption = "Contoh prediksi peluang pada data testing") |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = FALSE)| Status aktual | Pred. kelas | P(Not too happy) | P(Pretty happy) | P(Very happy) |
|---|---|---|---|---|
| Pretty happy | Pretty happy | 0.0961 | 0.5872 | 0.3167 |
| Very happy | Pretty happy | 0.0961 | 0.5872 | 0.3167 |
| Pretty happy | Pretty happy | 0.0537 | 0.4817 | 0.4646 |
| Pretty happy | Pretty happy | 0.0961 | 0.5872 | 0.3167 |
| Pretty happy | Not too happy | 0.5901 | 0.3768 | 0.0331 |
| Not too happy | Pretty happy | 0.4151 | 0.5199 | 0.0649 |
| Very happy | Pretty happy | 0.1870 | 0.6366 | 0.1765 |
| Very happy | Pretty happy | 0.3997 | 0.5314 | 0.0689 |
Interpretasi output: Setiap baris menampilkan tiga peluang prediksi yang jumlahnya selalu = 1. Model mengklasifikasikan observasi ke kategori dengan peluang tertinggi. Pada respons ordinal, peluang yang tersebar di antara kategori berdekatan lebih informatif daripada sekadar kelas prediksi.
cm <- addmargins(table(
`Aktual` = test_data$happy,
`Prediksi` = pred_kelas
))
cm |>
kable(caption = "Confusion matrix data testing (baris = aktual, kolom = prediksi)") |>
kable_styling(bootstrap_options = c("striped","hover","condensed","responsive"),
full_width = FALSE)| Not too happy | Pretty happy | Very happy | Sum | |
|---|---|---|---|---|
| Not too happy | 2 | 79 | 0 | 81 |
| Pretty happy | 5 | 244 | 0 | 249 |
| Very happy | 1 | 95 | 0 | 96 |
| Sum | 8 | 418 | 0 | 426 |
Interpretasi output: Diagonal confusion matrix menunjukkan prediksi yang benar untuk setiap kategori. Pada respons ordinal, kesalahan prediksi tidak selalu sama beratnya: memprediksi Very happy padahal aktual Not too happy jauh lebih besar kesalahannya dibandingkan memprediksi Pretty happy padahal aktual Not too happy.
pred_kelas <- predict(fit_ord, newdata = test_data, type = "class")
actual_vec <- as.character(test_data$happy)
pred_vec <- as.character(pred_kelas)
overall_acc <- mean(actual_vec == pred_vec)
kelas_levels <- c("Not too happy", "Pretty happy", "Very happy")
per_kelas <- lapply(kelas_levels, function(k) {
tp <- sum(actual_vec == k & pred_vec == k)
fp <- sum(actual_vec != k & pred_vec == k)
fn <- sum(actual_vec == k & pred_vec != k)
tn <- sum(actual_vec != k & pred_vec != k)
sensitivity <- ifelse((tp + fn) == 0, NA_real_, tp / (tp + fn))
precision <- ifelse((tp + fp) == 0, NA_real_, tp / (tp + fp))
specificity <- ifelse((tn + fp) == 0, NA_real_, tn / (tn + fp))
f1 <- ifelse(is.na(precision) | is.na(sensitivity) |
(precision + sensitivity) == 0, NA_real_,
2 * precision * sensitivity /
(precision + sensitivity))
data.frame(Kategori = k,
Sensitivity = round(sensitivity, 3),
Specificity = round(specificity, 3),
Precision = round(precision, 3),
F1 = round(f1, 3))
}) |> bind_rows()
knitr::kable(per_kelas,
caption = "Metrik evaluasi per kategori (data testing)")| Kategori | Sensitivity | Specificity | Precision | F1 |
|---|---|---|---|---|
| Not too happy | 0.025 | 0.983 | 0.250 | 0.045 |
| Pretty happy | 0.980 | 0.017 | 0.584 | 0.732 |
| Very happy | 0.000 | 1.000 | NA | NA |
score_map <- setNames(1:3, kelas_levels)
mae_ord <- mean(abs(score_map[actual_vec] - score_map[pred_vec]),
na.rm = TRUE)
knitr::kable(
data.frame(
Metrik = c("Overall Accuracy",
"Macro-avg Sensitivity",
"Macro-avg Precision",
"Macro-avg F1-score",
"Mean Absolute Error (ordinal)"),
Nilai = round(c(
overall_acc,
mean(per_kelas$Sensitivity, na.rm = TRUE),
mean(per_kelas$Precision, na.rm = TRUE),
mean(per_kelas$F1, na.rm = TRUE),
mae_ord
), 3)
),
caption = "Metrik evaluasi keseluruhan"
)| Metrik | Nilai |
|---|---|
| Overall Accuracy | 0.577 |
| Macro-avg Sensitivity | 0.335 |
| Macro-avg Precision | 0.417 |
| Macro-avg F1-score | 0.388 |
| Mean Absolute Error (ordinal) | 0.425 |
Interpretasi output — metrik per kategori: Sensitivity mengukur kemampuan model mengenali observasi yang benar-benar masuk kategori tersebut. Precision mengukur ketepatan prediksi positif. F1-score menyeimbangkan keduanya.
Interpretasi output — MAE ordinal: Mean Absolute Error pada skor ordinal mengukur rata-rata selisih antara posisi kategori aktual dan prediksi. MAE = 0 berarti sempurna; MAE = 1 berarti rata-rata salah satu tingkat kategori. Metrik ini lebih sesuai untuk respons ordinal dibandingkan akurasi sederhana.
as.data.frame(table(
Aktual = test_data$happy,
Prediksi = pred_kelas
)) |>
group_by(Aktual) |>
mutate(prop = Freq / sum(Freq)) |>
ungroup() |>
ggplot(aes(x = Prediksi, y = Aktual, fill = prop)) +
geom_tile(color = "white", linewidth = 0.8) +
geom_text(aes(label = paste0(Freq, "\n(", percent(prop, accuracy = 1), ")")),
fontface = "bold", size = 3.4) +
scale_fill_gradient(low = "#f8f9fa", high = "#2f7f73",
labels = percent_format()) +
labs(title = "Confusion Matrix (Proporsi per Baris Aktual)",
subtitle = "Angka = frekuensi; persentase = dari total aktual per kategori.",
x = "Prediksi", y = "Aktual", fill = "Proporsi") +
theme_minimal() +
theme(axis.text.x = element_text(angle = 15, hjust = 1))Interpretasi output: Sel diagonal yang lebih gelap menandakan prediksi yang lebih akurat. Perhatikan apakah kesalahan prediksi cenderung terjadi antara kategori berdekatan (misalnya Pretty happy diprediksi Very happy) atau antara kategori jauh (Not too happy diprediksi Very happy). Pada model ordinal yang baik, kesalahan seharusnya lebih banyak terjadi pada kategori berdekatan.
grid_income <- expand.grid(
log_income = seq(min(data_bersih$log_income),
max(data_bersih$log_income),
length.out = 120),
marital = "Married",
health = "Good"
)
grid_prob_income <- predict(fit_ord, newdata = grid_income, type = "probs")
grid_income |>
bind_cols(as.data.frame(grid_prob_income)) |>
tidyr::pivot_longer(
cols = c("Not too happy", "Pretty happy", "Very happy"),
names_to = "kategori",
values_to = "probabilitas"
) |>
mutate(kategori = factor(kategori,
levels = c("Not too happy","Pretty happy","Very happy"))) |>
ggplot(aes(x = log_income, y = probabilitas, color = kategori)) +
geom_line(linewidth = 1.25) +
scale_y_continuous(labels = percent_format()) +
scale_color_manual(values = warna_happy) +
labs(title = "Prediksi Probabilitas Tingkat Kebahagiaan vs Log-Pendapatan",
subtitle = "Marital = Married; Health = Good. Variabel lain ditahan konstan.",
x = "Log pendapatan riil (ln USD)",
y = "Probabilitas prediksi",
color = "Tingkat kebahagiaan") +
theme_minimal()Interpretasi output: Grafik ini menampilkan \(P(Y = j)\) sebagai fungsi log-pendapatan. Jika garis Very happy meningkat dan Not too happy menurun seiring kenaikan pendapatan, model menunjukkan bahwa pendapatan lebih tinggi dikaitkan dengan kebahagiaan lebih tinggi. Kurva kategori tunggal tidak harus sejajar — yang paralel adalah cumulative logit pada skala logit, bukan probabilitas kategori \(P(Y = j)\).
eps <- 1e-6
grid_income |>
bind_cols(as.data.frame(grid_prob_income)) |>
rename(p1 = `Not too happy`, p2 = `Pretty happy`, p3 = `Very happy`) |>
mutate(
`P(Y ≤ Not too happy)` = p1,
`P(Y ≤ Pretty happy)` = p1 + p2
) |>
tidyr::pivot_longer(
cols = c(`P(Y ≤ Not too happy)`, `P(Y ≤ Pretty happy)`),
names_to = "batas_kumulatif",
values_to = "prob_kumulatif"
) |>
mutate(
prob_kumulatif = pmin(pmax(prob_kumulatif, eps), 1 - eps),
cumulative_logit = qlogis(prob_kumulatif)
) |>
ggplot(aes(x = log_income, y = cumulative_logit,
color = batas_kumulatif)) +
geom_line(linewidth = 1.25) +
scale_color_manual(values = c(
"P(Y ≤ Not too happy)" = "#2f7f73",
"P(Y ≤ Pretty happy)" = "#d18b2f"
)) +
labs(title = "Parallel Lines pada Model Ordinal",
subtitle = "Garis yang sejajar adalah cumulative logit, bukan probabilitas kategori tunggal.",
x = "Log pendapatan riil (ln USD)",
y = "Logit peluang kumulatif",
color = "Batas kumulatif") +
theme_minimal()Interpretasi output: Dua garis yang sejajar pada grafik ini merupakan bukti visual asumsi proportional odds terpenuhi. Kedua garis memiliki slope yang sama — hanya intercept (\(\alpha_j\)) yang berbeda. Jika dua garis tersebut tidak sejajar secara substansial, asumsi proportional odds patut dipertanyakan.
Asumsi utama model ordinal adalah proportional odds atau parallel lines: efek prediktor dianggap sama untuk setiap batas kumulatif. Untuk tiga kategori kebahagiaan, model membentuk dua batas:
\[ Y \leq \text{Not too happy} \quad \text{dan} \quad Y \leq \text{Pretty happy}. \]
Asumsi proportional odds menyatakan bahwa slope \(\boldsymbol{\beta}\) sama untuk kedua batas tersebut.
ordinalfit_clm <- ordinal::clm(
happy ~ log_income + marital + health,
data = train_data,
link = "logit"
)
# Uji indikatif apakah efek prediktor perlu dibuat non-proportional
nominal_test_result <- ordinal::nominal_test(fit_clm)
print(nominal_test_result)## Tests of nominal effects
##
## formula: happy ~ log_income + marital + health
## Df logLik AIC LRT Pr(>Chi)
## <none> -1522.2 3058.5
## log_income 1 -1515.0 3045.9 14.5854 0.0001339 ***
## marital 1 -1522.1 3060.3 0.2385 0.6252956
## health 3 -1515.3 3050.5 13.9645 0.0029539 **
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
Interpretasi output: Fungsi
nominal_test() menguji apakah koefisien setiap prediktor
perlu dibuat berbeda untuk setiap batas kumulatif (non-proportional).
p-value kecil (< 0,05) untuk suatu prediktor menjadi
indikasi bahwa efek prediktor tersebut mungkin tidak
sepenuhnya memenuhi asumsi proportional odds.
Namun hasil uji tidak boleh dibaca secara mekanis. Keputusan perlu mempertimbangkan: (a) teori substantif, (b) ukuran sampel yang besar cenderung membuat uji sangat sensitif, (c) visualisasi cumulative logit, dan (d) tujuan analisis.
Jika asumsi proportional odds tidak terpenuhi, alternatif yang dapat dipertimbangkan:
Berdasarkan hasil estimasi pada data training (n = 1698), model secara keseluruhan signifikan berdasarkan uji rasio likelihood (\(G^2 =\) 232.4; df = 5; p = 3.27^{-48}). Ketiga prediktor — log-pendapatan, status pernikahan, dan kondisi kesehatan — secara bersama-sama memberikan kontribusi nyata dalam menjelaskan variasi tingkat kebahagiaan.
McFadden Pseudo-\(R^2\) sebesar 0.071 menunjukkan proporsi log-likelihood yang berhasil dijelaskan model. Nilai ini wajar untuk data survei sosial; kebahagiaan subjektif dipengaruhi banyak faktor yang tidak tersedia dalam dataset.
Koefisien log_income pada output polr()
positif berarti (dalam konvensi polr) kenaikan log-pendapatan
meningkatkan odds kumulatif \(P(Y \leq j)\), sehingga responden cenderung
bergeser ke kategori kebahagiaan lebih tinggi. Dalam
konvensi slide, tandanya terbalik namun interpretasi arah tetap
sama.
Setiap kenaikan satu satuan log-pendapatan mengalikan odds kumulatif sebesar \(\exp(\hat{\beta}_{\text{polr}})\). Jika OR > 1 (polr), maka pendapatan yang lebih tinggi dikaitkan dengan kecenderungan lebih kuat untuk berada pada kategori kebahagiaan yang lebih tinggi.
Koefisien maritalMarried yang positif pada output polr
menunjukkan bahwa responden yang menikah memiliki odds kumulatif \(P(Y \leq j)\) lebih tinggi dibandingkan
yang tidak menikah (referensi). Artinya, dalam konvensi polr, menikah
dikaitkan dengan kecenderungan lebih besar berada di kategori
kebahagiaan lebih tinggi.
Level-level health menunjukkan gradien yang konsisten:
semakin baik kondisi kesehatan (dari Poor sebagai referensi menuju
Excellent), semakin besar kecenderungan berada di kategori kebahagiaan
lebih tinggi. Level dengan p-value < 0,05 memberikan bukti statistik
yang kuat. Kondisi kesehatan kemungkinan merupakan prediktor terkuat
dalam model ini mengingat pola gradien yang jelas pada eksplorasi
data.
Overall accuracy pada data testing adalah 57.7%. MAE ordinal sebesar 0.425 berarti rata-rata prediksi model meleset sekitar 0.425 tingkat kategori dari nilai aktual. Nilai MAE mendekati 0 menunjukkan prediksi yang rata-rata cukup dekat dengan kategori yang benar.
Performa model tidak merata antar kategori: Pretty happy (kategori terbanyak) umumnya diprediksi paling baik, sementara Not too happy (minoritas) cenderung lebih sulit dideteksi. Kesalahan prediksi diharapkan lebih banyak terjadi antara kategori berdekatan (misalnya Not too happy ↔︎ Pretty happy) daripada antara kategori berjauhan (Not too happy ↔︎ Very happy).
Analisis regresi logistik ordinal terhadap data GSS 2024 (n = 2124) menghasilkan beberapa temuan utama:
Pertama, signifikansi model. Model secara keseluruhan signifikan (\(G^2 =\) 232.4; df = 5; p = 3.27^{-48}), menunjukkan bahwa log-pendapatan, status pernikahan, dan kondisi kesehatan secara bersama-sama berpengaruh terhadap tingkat kebahagiaan.
Kedua, prediktor dominan. Kondisi kesehatan kemungkinan merupakan prediktor terkuat berdasarkan gradien yang konsisten pada eksplorasi data: semakin baik kesehatan, semakin tinggi kecenderungan Very happy. Status pernikahan juga signifikan: responden yang menikah cenderung lebih bahagia. Log-pendapatan memberikan kontribusi positif, konsisten dengan teori utilitas marginal yang semakin menurun.
Ketiga, performa klasifikasi. Model mencapai overall accuracy 57.7% pada data testing, dengan MAE ordinal 0.425. Ketimpangan performa antar kategori mencerminkan ketidakseimbangan distribusi kelas.
Keempat, asumsi dan keterbatasan. Asumsi proportional odds perlu diperiksa secara formal. McFadden Pseudo-\(R^2\) sebesar 0.071 menunjukkan masih banyak variasi yang belum dijelaskan, yang wajar mengingat kompleksitas kebahagiaan subjektif. Model ini memberikan gambaran yang informatif dan parsimonious mengenai determinan demografis kebahagiaan, namun tidak boleh diinterpretasikan sebagai hubungan kausal.
Regresi Poisson adalah model regresi untuk variabel respons berupa data hitung (count data), yaitu respons yang bernilai bilangan bulat nonnegatif: \[Y_i \in \{0, 1, 2, \ldots\}.\]
Contoh respons yang cocok dianalisis dengan regresi Poisson: jumlah anggota rumah tangga, jumlah kunjungan pasien ke klinik, jumlah kecelakaan di suatu ruas jalan, jumlah klaim asuransi, atau jumlah komplain pelanggan.
Model Poisson digunakan ketika kita ingin menjelaskan rata-rata jumlah kejadian sebagai fungsi dari beberapa prediktor. Berbeda dari regresi logistik yang memodelkan peluang kategori, regresi Poisson memodelkan nilai harapan hitungan: \[E(Y_i \mid \mathbf{x}_i) = \mu_i.\]
Distribusi dan Model
Asumsi dasar model Poisson adalah: \[Y_i \mid \mathbf{x}_i \sim \text{Poisson}(\mu_i),\] dengan fungsi peluang: \[P(Y_i = y_i \mid \mathbf{x}_i) = \frac{\exp(-\mu_i)\,\mu_i^{y_i}}{y_i!}, \quad y_i = 0, 1, 2, \ldots.\]
Pada distribusi Poisson berlaku sifat equidispersion, yaitu rata-rata dan varians kondisional bernilai sama: \[E(Y_i \mid \mathbf{x}_i) = \mu_i, \qquad \text{Var}(Y_i \mid \mathbf{x}_i) = \mu_i.\]
Regresi Poisson menggunakan fungsi penghubung log: \[\log(\mu_i) = \eta_i = \beta_0 + \beta_1 x_{i1} + \cdots + \beta_p x_{ip}.\]
Dengan demikian: \[\mu_i = \exp(\beta_0 + \beta_1 x_{i1} + \cdots + \beta_p x_{ip}).\]
Fungsi log dipakai karena \(\mu_i\) harus selalu positif. Dengan bentuk eksponensial, prediksi rata-rata hitungan tidak mungkin negatif.
Model dengan Exposure dan Offset
Dalam banyak kasus, jumlah kejadian bergantung pada lama waktu pengamatan atau ukuran populasi yang berisiko. Misalnya: jumlah kecelakaan per tahun pengamatan, jumlah klaim per jumlah polis, atau jumlah kasus penyakit per jumlah penduduk.
Jika \(t_i\) adalah exposure, model Poisson ditulis: \[\mu_i = t_i \lambda_i,\] dengan \(\lambda_i\) adalah rate kejadian per satu unit exposure. Karena \(\log(\mu_i) = \log(t_i) + \log(\lambda_i)\), maka model menjadi: \[\log(\mu_i) = \log(t_i) + \beta_0 + \mathbf{x}_i^\top\boldsymbol{\beta}.\]
Komponen \(\log(t_i)\) disebut offset, yaitu variabel yang koefisiennya dipaksa sama dengan 1. Pada analisis ini tidak digunakan offset karena seluruh observasi berasal dari survei lintas-seksi yang diasumsikan memiliki unit eksposur yang setara.
Studi kasus: Analisis faktor ekonomi dan latar belakang budaya yang memengaruhi jumlah orang yang tinggal dalam satu rumah tangga berdasarkan data General Social Survey (GSS) 2024.
Tujuan analisis: Membangun model regresi Poisson untuk menjelaskan rata-rata jumlah anggota rumah tangga (
hompop) sebagai fungsi dari pendapatan keluarga (realinc) dan ras (race).
Ukuran rumah tangga merupakan variabel hitungan (count) yang
bernilai bilangan bulat positif, sehingga cocok dianalisis dengan
regresi Poisson. Variabel respons pada analisis ini
adalah jumlah orang yang tinggal dalam satu rumah tangga
(hompop): \[Y_i = \text{jumlah
anggota rumah tangga ke-}i \in \{1, 2, 3, \ldots\}.\]
Model yang digunakan: \[\log(\mu_i) = \beta_0 + \beta_1 \log(\texttt{realinc}_i) + \beta_2 \texttt{raceWhite}_i,\] dengan \(\mu_i = E(\texttt{hompop}_i \mid \mathbf{x}_i)\) adalah rata-rata jumlah anggota rumah tangga untuk responden ke-\(i\).
Dataset berasal dari General Social Survey (GSS)
2024, survei sosial nasional Amerika Serikat yang
diselenggarakan oleh NORC di University of Chicago. Dataset terdiri dari
1.289 responden. Dari lima variabel yang tersedia, analisis ini
menggunakan tiga variabel: hompop, race, dan
realinc.
raw_data <- readxl::read_excel("Data Poisson.xlsx", sheet = 1)
ringkasan_data <- data.frame(
Keterangan = c("Jumlah observasi", "Jumlah variabel yang digunakan"),
Nilai = c(nrow(raw_data), 3)
)
knitr::kable(ringkasan_data, caption = "Ukuran dataset GSS 2024 (Poisson)")| Keterangan | Nilai |
|---|---|
| Jumlah observasi | 1289 |
| Jumlah variabel yang digunakan | 3 |
Interpretasi output: Dataset berisi 1289 observasi.
Dari seluruh variabel yang tersedia, analisis ini hanya menggunakan
hompop sebagai respons, race dan
realinc sebagai prediktor.
kamus <- data.frame(
`Nama kolom` = c("race", "hompop", "realinc"),
`Keterangan` = c(
"Ras responden",
"Jumlah orang dalam satu rumah tangga (variabel respons)",
"Pendapatan keluarga dalam setahun (USD)"
),
`Kategori / Rentang` = c(
"Black, White",
"1 – 10 orang",
"181,5 – 117.564,8 USD"
),
`Tipe dalam analisis` = c(
"Kategorik (2 level); Black sebagai referensi",
"Respons hitung (count)",
"Numerik kontinu; ditransformasi log"
),
check.names = FALSE
)
knitr::kable(kamus, caption = "Penjelasan variabel yang digunakan dalam analisis")| Nama kolom | Keterangan | Kategori / Rentang | Tipe dalam analisis |
|---|---|---|---|
| race | Ras responden | Black, White | Kategorik (2 level); Black sebagai referensi |
| hompop | Jumlah orang dalam satu rumah tangga (variabel respons) | 1 – 10 orang | Respons hitung (count) |
| realinc | Pendapatan keluarga dalam setahun (USD) | 181,5 – 117.564,8 USD | Numerik kontinu; ditransformasi log |
Interpretasi output: Terdapat satu variabel respons
hitung (hompop) dan dua prediktor: satu numerik
(realinc) dan satu kategorik (race). Variabel
realinc ditransformasi logaritma untuk menstabilkan
distribusinya yang sangat miring ke kanan (right-skewed).
data_bersih <- raw_data |>
select(id_, hompop, race, realinc) |>
mutate(
hompop = as.integer(hompop),
realinc = as.numeric(realinc),
log_inc = log(realinc),
race = factor(race, levels = c("Black", "White"))
) |>
tidyr::drop_na()
knitr::kable(
head(data_bersih |> select(id_, hompop, race, realinc, log_inc), 8),
caption = "Contoh data setelah transformasi",
digits = 2
)| id_ | hompop | race | realinc | log_inc |
|---|---|---|---|---|
| 1 | 3 | Black | 43560.0 | 10.68 |
| 3 | 2 | White | 43560.0 | 10.68 |
| 5 | 1 | White | 24502.5 | 10.11 |
| 8 | 2 | White | 3267.0 | 8.09 |
| 12 | 1 | White | 3267.0 | 8.09 |
| 16 | 3 | White | 36300.0 | 10.50 |
| 19 | 2 | White | 43560.0 | 10.68 |
| 20 | 4 | White | 24502.5 | 10.11 |
Interpretasi output: Delapan baris pertama data
setelah transformasi. Kategori referensi yang dipilih adalah
Black untuk race. Kolom
log_inc merupakan logaritma natural pendapatan yang
digunakan sebagai prediktor numerik dalam model.
dist_hompop <- data_bersih |>
count(hompop, name = "Frekuensi") |>
mutate(Proporsi = scales::percent(Frekuensi / sum(Frekuensi),
accuracy = 0.1)) |>
rename(`Jumlah anggota RT` = hompop)
knitr::kable(dist_hompop,
caption = "Distribusi jumlah anggota rumah tangga")| Jumlah anggota RT | Frekuensi | Proporsi |
|---|---|---|
| 1 | 457 | 35.5% |
| 2 | 442 | 34.3% |
| 3 | 162 | 12.6% |
| 4 | 136 | 10.6% |
| 5 | 49 | 3.8% |
| 6 | 25 | 1.9% |
| 7 | 12 | 0.9% |
| 8 | 4 | 0.3% |
| 9 | 1 | 0.1% |
| 10 | 1 | 0.1% |
Interpretasi output: Distribusi hompop
menunjukkan pola khas data hitung: nilai kecil mendominasi (1–3 orang)
dan frekuensi menurun tajam untuk nilai yang lebih besar. Pola ini
konsisten dengan asumsi distribusi Poisson.
ggplot(data_bersih, aes(x = factor(hompop))) +
geom_bar(fill = "#2a9d8f", color = "white", linewidth = 0.7,
alpha = 0.92) +
geom_text(stat = "count",
aes(label = after_stat(count)),
vjust = -0.4, fontface = "bold", color = "#243142") +
labs(title = "Distribusi Jumlah Anggota Rumah Tangga",
subtitle = "Respons Poisson berupa hitungan bilangan bulat positif.",
x = "Jumlah anggota rumah tangga",
y = "Frekuensi") +
theme_minimal(base_size = 12)Interpretasi output: Grafik memperlihatkan distribusi data hitung yang condong ke kanan, dengan nilai 1 dan 2 paling sering muncul. Tidak terdapat nilai nol karena setiap responden setidaknya tinggal sendirian (hompop ≥ 1).
ggplot(data_bersih, aes(x = log_inc, y = hompop, color = race)) +
geom_point(alpha = 0.35, size = 1.4) +
geom_smooth(method = "loess", se = FALSE, linewidth = 1.2) +
scale_color_manual(values = c("Black" = "#e76f51", "White" = "#2a9d8f")) +
labs(title = "Hubungan Log-Pendapatan dan Jumlah Anggota RT",
subtitle = "Garis LOESS menunjukkan pola hubungan per kelompok ras.",
x = "Log pendapatan (ln USD)",
y = "Jumlah anggota rumah tangga",
color = "Ras") +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")Interpretasi output: Scatter plot dengan kurva LOESS menampilkan pola umum hubungan antara log-pendapatan dan jumlah anggota rumah tangga untuk masing-masing kelompok ras. Jika kurva cenderung turun, maka pendapatan yang lebih tinggi dikaitkan dengan rumah tangga yang lebih kecil.
Model diestimasi menggunakan fungsi glm() dengan
family = poisson. Pendapatan digunakan dalam skala log.
Tidak ada offset karena seluruh observasi berasal dari survei
lintas-seksi yang diasumsikan memiliki eksposur setara.
model_poisson <- glm(
hompop ~ log_inc + race,
data = data_bersih,
family = poisson(link = "log")
)
summary(model_poisson)##
## Call:
## glm(formula = hompop ~ log_inc + race, family = poisson(link = "log"),
## data = data_bersih)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -0.07960 0.16219 -0.491 0.624
## log_inc 0.08602 0.01664 5.170 2.33e-07 ***
## raceWhite 0.04272 0.04921 0.868 0.385
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for poisson family taken to be 1)
##
## Null deviance: 966.86 on 1288 degrees of freedom
## Residual deviance: 933.72 on 1286 degrees of freedom
## AIC: 4269.4
##
## Number of Fisher Scoring iterations: 5
Interpretasi output: glm() menampilkan
koefisien, standard error, z-value, dan p-value untuk setiap prediktor.
Baris Residual deviance dan Null deviance digunakan
untuk menilai kontribusi model secara keseluruhan.
coef_pois <- as.data.frame(coef(summary(model_poisson))) |>
tibble::rownames_to_column("Parameter") |>
dplyr::rename(
Estimate = Estimate,
SE = `Std. Error`,
z_value = `z value`,
p_value = `Pr(>|z|)`
) |>
mutate(
IRR = exp(Estimate),
CI_low = exp(Estimate - 1.96 * SE),
CI_high = exp(Estimate + 1.96 * SE),
Perubahan_pct = 100 * (IRR - 1),
Signifikan = case_when(
p_value < 0.001 ~ "***",
p_value < 0.01 ~ "**",
p_value < 0.05 ~ "*",
p_value < 0.1 ~ ".",
TRUE ~ ""
)
)
knitr::kable(
coef_pois |>
mutate(across(c(Estimate, SE, z_value, p_value,
IRR, CI_low, CI_high, Perubahan_pct),
round, 4)),
caption = "Ringkasan hasil regresi Poisson",
col.names = c("Parameter", "Estimate", "SE", "z-value", "p-value",
"IRR", "CI 2,5%", "CI 97,5%", "Perubahan (%)", "Sig.")
)| Parameter | Estimate | SE | z-value | p-value | IRR | CI 2,5% | CI 97,5% | Perubahan (%) | Sig. |
|---|---|---|---|---|---|---|---|---|---|
| (Intercept) | -0.0796 | 0.1622 | -0.4908 | 0.6236 | 0.9235 | 0.6720 | 1.2691 | -7.6513 | |
| log_inc | 0.0860 | 0.0166 | 5.1705 | 0.0000 | 1.0898 | 1.0549 | 1.1259 | 8.9827 | *** |
| raceWhite | 0.0427 | 0.0492 | 0.8681 | 0.3853 | 1.0437 | 0.9477 | 1.1493 | 4.3650 |
Catatan: *** p < 0,001 |
** p < 0,01 | * p < 0,05 |
. p < 0,1
Interpretasi output: Kolom IRR adalah nilai
eksponensial koefisien. IRR > 1 berarti prediktor meningkatkan
rata-rata hompop; IRR < 1 berarti prediktor
menurunkannya. Kolom Perubahan (%) menyatakan perubahan
persentase pada rata-rata hitungan untuk setiap kenaikan satu unit
prediktor atau perpindahan ke kategori non-referensi.
Log-pendapatan (log_inc): Setiap
kenaikan satu unit log-pendapatan dikaitkan dengan perubahan rata-rata
hompop sebesar \(\{(\text{IRR} -
1) \times 100\}\%\), dengan ras konstan. Jika koefisiennya
negatif, maka rumah tangga berpendapatan lebih tinggi cenderung memiliki
lebih sedikit anggota — kemungkinan mencerminkan perbedaan gaya hidup
atau biaya hidup yang lebih tinggi di wilayah perkotaan.
Ras (raceWhite): Koefisien ini
membandingkan rata-rata hompop kelompok White terhadap
Black sebagai kategori referensi, setelah mengendalikan pendapatan. IRR
< 1 berarti rata-rata ukuran rumah tangga White lebih kecil daripada
Black; IRR > 1 berarti sebaliknya.
irr_plot <- coef_pois |>
filter(Parameter != "(Intercept)")
ggplot(irr_plot,
aes(x = IRR, y = reorder(Parameter, IRR))) +
geom_vline(xintercept = 1, linetype = "dashed",
color = "#6c757d", linewidth = 0.8) +
geom_errorbarh(aes(xmin = CI_low, xmax = CI_high),
height = 0.2, linewidth = 0.8,
color = "#2a9d8f") +
geom_point(size = 4, color = "#2a9d8f") +
scale_x_log10() +
labs(title = "Incidence Rate Ratio (IRR) Model Poisson",
subtitle = "Skala logaritmik; garis putus-putus = IRR 1 (tidak ada efek).",
x = "IRR (skala log)",
y = NULL) +
theme_minimal(base_size = 12)Interpretasi output: Forest plot IRR menampilkan
estimasi titik dan interval kepercayaan 95% untuk setiap koefisien.
Titik di kanan garis putus-putus (IRR > 1) berarti prediktor
meningkatkan rata-rata hompop. Jika interval kepercayaan
tidak melewati garis 1, koefisien signifikan secara statistik.
grid_pois <- expand.grid(
log_inc = seq(min(data_bersih$log_inc),
max(data_bersih$log_inc),
length.out = 120),
race = factor("Black", levels = levels(data_bersih$race))
)
pred_link <- predict(model_poisson, newdata = grid_pois,
type = "link", se.fit = TRUE)
grid_pois_plot <- grid_pois |>
mutate(
fit_link = pred_link$fit,
se_link = pred_link$se.fit,
rate = exp(fit_link),
rate_low = exp(fit_link - 1.96 * se_link),
rate_high = exp(fit_link + 1.96 * se_link)
)
ggplot(grid_pois_plot, aes(x = log_inc, y = rate)) +
geom_ribbon(aes(ymin = rate_low, ymax = rate_high),
fill = "#2a9d8f", alpha = 0.18) +
geom_line(color = "#2a9d8f", linewidth = 1.35) +
labs(title = "Prediksi Rata-Rata Anggota Rumah Tangga",
subtitle = "Ras = Black; pita = IK 95%.",
x = "Log pendapatan (ln USD)",
y = "Prediksi jumlah anggota RT") +
theme_minimal(base_size = 12)Interpretasi output: Grafik menampilkan rata-rata
prediksi hompop sebagai fungsi log-pendapatan untuk
kelompok Black, dengan pita interval kepercayaan 95%. Arah kurva
mencerminkan tanda koefisien log_inc.
grid_ras <- expand.grid(
log_inc = seq(min(data_bersih$log_inc),
max(data_bersih$log_inc),
length.out = 120),
race = factor(c("Black", "White"), levels = levels(data_bersih$race))
)
pred_ras <- predict(model_poisson, newdata = grid_ras,
type = "link", se.fit = TRUE)
grid_ras_plot <- grid_ras |>
mutate(
rate = exp(pred_ras$fit),
rate_low = exp(pred_ras$fit - 1.96 * pred_ras$se.fit),
rate_high = exp(pred_ras$fit + 1.96 * pred_ras$se.fit)
)
ggplot(grid_ras_plot, aes(x = log_inc, y = rate,
color = race, fill = race)) +
geom_ribbon(aes(ymin = rate_low, ymax = rate_high),
alpha = 0.15, color = NA) +
geom_line(linewidth = 1.25) +
scale_color_manual(values = c("Black" = "#e76f51",
"White" = "#2a9d8f")) +
scale_fill_manual(values = c("Black" = "#e76f51",
"White" = "#2a9d8f")) +
labs(title = "Prediksi Rata-Rata Anggota RT Menurut Ras",
subtitle = "Pita = IK 95%.",
x = "Log pendapatan (ln USD)",
y = "Prediksi jumlah anggota RT",
color = "Ras", fill = "Ras") +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom")Interpretasi output: Grafik ini memperlihatkan
perbedaan kurva prediksi antara Black dan White. Jarak vertikal antara
kedua kurva mencerminkan efek IRR untuk koefisien raceWhite
— konstan pada seluruh rentang pendapatan karena model Poisson
menggunakan skala log yang additif.
Asumsi penting pada regresi Poisson:
Salah satu ukuran sederhana untuk memeriksa indikasi overdispersion adalah: \[\hat{\phi} = \frac{\sum r_{P,i}^2}{df},\] dengan \(r_{P,i}\) adalah residual Pearson dan \(df\) adalah derajat bebas residual. Nilai \(\hat{\phi}\) sekitar 1 mengindikasikan equidispersion.
phi_hat <- sum(residuals(model_poisson, type = "pearson")^2) /
df.residual(model_poisson)
disp_tbl <- data.frame(
`Dispersion Pearson` = round(phi_hat, 3),
`Interpretasi` = dplyr::case_when(
phi_hat < 1.5 ~ "Tidak ada indikasi overdispersion berat",
phi_hat < 2.5 ~ "Ada indikasi overdispersion sedang",
TRUE ~ "Ada indikasi overdispersion kuat — pertimbangkan quasi-Poisson atau NB"
),
check.names = FALSE
)
knitr::kable(disp_tbl,
caption = "Indikasi overdispersion pada model Poisson")| Dispersion Pearson | Interpretasi |
|---|---|
| 0.838 | Tidak ada indikasi overdispersion berat |
Interpretasi output: Nilai dispersion Pearson yang mendekati 1 mengindikasikan equidispersion dan model Poisson sudah sesuai. Jika nilai ini jauh lebih besar dari 1, standard error yang dilaporkan akan terlalu kecil, sehingga perlu dipertimbangkan alternatif seperti quasi-Poisson untuk memperbaiki standard error, negative binomial regression ketika varians jauh lebih besar dari mean, atau zero-inflated Poisson ketika jumlah nol jauh lebih banyak dari yang diprediksi.
data_resid <- data.frame(
fitted = fitted(model_poisson),
pearson = residuals(model_poisson, type = "pearson")
)
ggplot(data_resid, aes(x = fitted, y = pearson)) +
geom_hline(yintercept = 0, linetype = "dashed",
color = "#6c757d", linewidth = 0.8) +
geom_point(alpha = 0.35, size = 1.4, color = "#2a9d8f") +
geom_smooth(method = "loess", se = FALSE,
color = "#e76f51", linewidth = 1) +
labs(title = "Residual Pearson vs Nilai Fitted",
subtitle = "Pola acak di sekitar nol mengindikasikan kecocokan model yang baik.",
x = "Nilai fitted (rata-rata prediksi)",
y = "Residual Pearson") +
theme_minimal(base_size = 12)Interpretasi output: Jika titik-titik tersebar secara acak di sekitar garis nol tanpa pola sistematis, model Poisson sudah cukup sesuai. Pola berbentuk kipas (varians meningkat seiring fitted) mengindikasikan overdispersion.
dev_null <- model_poisson$null.deviance
dev_res <- model_poisson$deviance
df_null <- model_poisson$df.null
df_res <- model_poisson$df.residual
g2 <- dev_null - dev_res
df_g2 <- df_null - df_res
p_g2 <- 1 - pchisq(g2, df_g2)
gof_tbl <- data.frame(
Metrik = c("Null deviance", "Residual deviance",
"G² (penurunan deviance)", "df G²", "p-value G²",
"AIC", "Pseudo-R² (McFadden)"),
Nilai = c(
round(dev_null, 2),
round(dev_res, 2),
round(g2, 2),
df_g2,
signif(p_g2, 3),
round(AIC(model_poisson), 2),
round(1 - dev_res / dev_null, 4)
)
)
knitr::kable(gof_tbl, caption = "Ukuran kecocokan model Poisson")| Metrik | Nilai |
|---|---|
| Null deviance | 9.66860e+02 |
| Residual deviance | 9.33720e+02 |
| G² (penurunan deviance) | 3.31400e+01 |
| df G² | 2.00000e+00 |
| p-value G² | 1.00000e-07 |
| AIC | 4.26944e+03 |
| Pseudo-R² (McFadden) | 3.43000e-02 |
Interpretasi output: Nilai \(G^2\) menguji apakah model dengan prediktor secara signifikan lebih baik daripada model hanya dengan intercept. p-value yang kecil (< 0,05) berarti prediktor secara bersama-sama berkontribusi signifikan. Pseudo-\(R^2\) McFadden memberikan gambaran proporsi deviance yang berhasil dijelaskan model; nilai 0,2–0,4 sudah dianggap baik untuk model regresi hitung.
Analisis regresi Poisson terhadap data GSS 2024 menghasilkan beberapa temuan utama:
Pertama, formulasi model. Model Poisson membangun
satu persamaan log-linear yang menjelaskan rata-rata jumlah anggota
rumah tangga (hompop) sebagai fungsi dari log-pendapatan
dan ras. Koefisien diinterpretasikan sebagai incidence rate
ratio (IRR) setelah dieksponensialkan.
Kedua, peran pendapatan. Log-pendapatan
(log_inc) merupakan prediktor numerik utama. Jika
koefisiennya negatif dan signifikan, maka rumah tangga dengan pendapatan
lebih tinggi cenderung memiliki lebih sedikit anggota — kemungkinan
mencerminkan perbedaan gaya hidup atau biaya hidup.
Ketiga, peran ras. Koefisien raceWhite
menangkap perbedaan rata-rata ukuran rumah tangga antara kelompok White
dan Black setelah mengendalikan pendapatan. Perbedaan ini dapat
mencerminkan faktor budaya, historis, maupun struktural yang memengaruhi
komposisi rumah tangga.
Keempat, pemeriksaan asumsi. Nilai dispersion Pearson perlu dicermati untuk memastikan asumsi equidispersion terpenuhi. Jika overdispersion terindikasi, quasi-Poisson atau negative binomial regression menjadi alternatif yang lebih robust.
Kelima, keterbatasan. Model ini memberikan gambaran awal yang informatif, namun tidak boleh diinterpretasikan sebagai hubungan kausal. Faktor-faktor lain seperti wilayah geografis, status pernikahan, jumlah kamar rumah, atau tingkat pendidikan dapat memengaruhi ukuran rumah tangga dan belum dimasukkan dalam model ini.