1 BAB 1. Regresi Logistik Biner
1.1 Gambaran Umum Analisis
Dokumen ini menyajikan analisis regresi logistik biner untuk memodelkan peluang suatu asteroid tergolong berbahaya atau tidak berbahaya berdasarkan karakteristik orbit dan ukuran asteroid.
Respons yang digunakan adalah:
\[ Y = \begin{cases} 1, & \text{jika asteroid tergolong Hazardous} \\ 0, & \text{jika asteroid tidak tergolong Hazardous} \end{cases} \]
Prediktor yang digunakan adalah:
- Absolute Magnitude
- Est Dia in KM(max)
- Velocity km per sec
- Miss Dist.(kilometers)
- Eccentricity
1.2 Pendahuluan
Asteroid dekat bumi memiliki karakteristik fisik dan orbital yang dapat digunakan untuk menilai potensi bahayanya. Salah satu bentuk klasifikasi yang umum digunakan adalah apakah asteroid tersebut termasuk Hazardous atau Non-Hazardous.
Karena variabel respons hanya terdiri dari dua kategori, metode yang sesuai adalah regresi logistik biner. Model ini tidak langsung memodelkan nilai kategori, tetapi memodelkan peluang suatu asteroid masuk ke kategori berbahaya.
Tujuan analisis:
Mengetahui apakah variabel Absolute Magnitude,
Est Dia in KM(max), Velocity km per
sec, Miss Dist.(kilometers), dan
Eccentricity dapat digunakan untuk menjelaskan peluang
asteroid tergolong Hazardous.
1.3 Dasar Teori Regresi Logistik Biner
Regresi logistik biner digunakan ketika variabel respons memiliki dua kategori, misalnya:
\[ Y = 1 \quad \text{atau} \quad Y = 0 \]
Misalkan:
\[ \pi(x) = P(Y = 1 \mid X = x) \]
Model regresi logistik biner ditulis sebagai:
\[ \log \left(\frac{\pi(x)}{1-\pi(x)}\right) = \beta_0 + \beta_1X_1 + \beta_2X_2 + \cdots + \beta_pX_p \]
Bagian:
\[ \frac{\pi(x)}{1-\pi(x)} \]
disebut odds, yaitu perbandingan antara peluang kejadian sukses dengan peluang tidak sukses.
Jika koefisien \(\beta_j\) dieksponensialkan, maka diperoleh:
\[ OR_j = e^{\beta_j} \]
Nilai \(OR_j\) disebut odds ratio.
Cara membaca odds ratio:
Jika \(OR > 1\), maka kenaikan
prediktor meningkatkan odds asteroid tergolong Hazardous.
Jika \(OR < 1\), maka kenaikan
prediktor menurunkan odds asteroid tergolong Hazardous.
Jika \(OR = 1\), maka prediktor tidak
mengubah odds secara berarti.
1.4 Import Data
data_raw <- readxl::read_excel("~/Salwa/SMT 4/ADK 1/tugas regresi logostik/nasa_logistik_biner.xlsx")
dim(data_raw)
## [1] 4687 6
names(data_raw)
## [1] "Hazardous" "Absolute Magnitude"
## [3] "Est Dia in KM(max)" "Relative Velocity km per sec"
## [5] "Miss Dist.(kilometers)" "Eccentricity"
Dataset berhasil diimpor ke R. Pada tahap ini, nama kolom diperiksa terlebih dahulu agar variabel yang digunakan dalam model sesuai dengan nama kolom pada file Excel.
1.5 Pemeriksaan dan Penyesuaian Nama Variabel
Pada file Excel, variabel kecepatan dapat memiliki nama lengkap Relative Velocity km per sec. Dalam laporan ini, variabel tersebut tetap ditulis sebagai Velocity km per sec agar sesuai dengan tujuan analisis.
find_col <- function(data, candidates) {
nm <- names(data)
nm_lower <- tolower(trimws(nm))
cand_lower <- tolower(trimws(candidates))
exact <- match(cand_lower, nm_lower)
exact <- exact[!is.na(exact)]
if (length(exact) > 0) return(nm[exact[1]])
for (cand in cand_lower) {
idx <- grep(cand, nm_lower, fixed = TRUE)
if (length(idx) > 0) return(nm[idx[1]])
}
stop(paste("Kolom tidak ditemukan. Kandidat:", paste(candidates, collapse = ", ")))
}
col_hazard <- find_col(data_raw, c("Hazardous"))
col_abs_mag <- find_col(data_raw, c("Absolute Magnitude"))
col_dia <- find_col(data_raw, c("Est Dia in KM(max)", "Est Dia in KM"))
col_vel <- find_col(data_raw, c("Velocity km per sec", "Relative Velocity km per sec"))
col_miss <- find_col(data_raw, c("Miss Dist.(kilometers)", "Miss Dist"))
col_ecc <- find_col(data_raw, c("Eccentricity"))
kolom_digunakan <- tibble::tibble(
Variabel_Analisis = c(
"Hazardous",
"Absolute Magnitude",
"Est Dia in KM(max)",
"Velocity km per sec",
"Miss Dist.(kilometers)",
"Eccentricity"
),
Nama_Kolom_di_Excel = c(
col_hazard,
col_abs_mag,
col_dia,
col_vel,
col_miss,
col_ecc
)
)
kolom_digunakan %>%
knitr::kable(caption = "Penyesuaian nama variabel analisis dengan nama kolom pada Excel") %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
| Variabel_Analisis | Nama_Kolom_di_Excel |
|---|---|
| Hazardous | Hazardous |
| Absolute Magnitude | Absolute Magnitude |
| Est Dia in KM(max) | Est Dia in KM(max) |
| Velocity km per sec | Relative Velocity km per sec |
| Miss Dist.(kilometers) | Miss Dist.(kilometers) |
| Eccentricity | Eccentricity |
1.6 Cleaning Data
data_bin <- data_raw %>%
dplyr::transmute(
Hazardous_raw = .data[[col_hazard]],
Absolute_Magnitude = suppressWarnings(as.numeric(.data[[col_abs_mag]])),
Est_Dia_KM_max = suppressWarnings(as.numeric(.data[[col_dia]])),
Velocity_km_per_sec = suppressWarnings(as.numeric(.data[[col_vel]])),
Miss_Dist_kilometers = suppressWarnings(as.numeric(.data[[col_miss]])),
Eccentricity = suppressWarnings(as.numeric(.data[[col_ecc]]))
) %>%
dplyr::mutate(
Hazardous_num = dplyr::case_when(
Hazardous_raw %in% c(TRUE, "TRUE", "True", "true", "Yes", "YES", "1", 1) ~ 1,
Hazardous_raw %in% c(FALSE, "FALSE", "False", "false", "No", "NO", "0", 0) ~ 0,
TRUE ~ NA_real_
),
Hazardous = factor(
Hazardous_num,
levels = c(0, 1),
labels = c("Tidak Hazardous", "Hazardous")
)
) %>%
dplyr::filter(
!is.na(Hazardous_num),
!is.na(Absolute_Magnitude),
!is.na(Est_Dia_KM_max),
!is.na(Velocity_km_per_sec),
!is.na(Miss_Dist_kilometers),
!is.na(Eccentricity)
)
glimpse(data_bin)
## Rows: 4,687
## Columns: 8
## $ Hazardous_raw <chr> "TRUE", "FALSE", "TRUE", "FALSE", "TRUE", "FALSE"…
## $ Absolute_Magnitude <dbl> 21.6, 21.3, 20.3, 27.4, 21.6, 19.6, 19.6, 19.2, 1…
## $ Est_Dia_KM_max <dbl> 0.28447230, 0.32661790, 0.51765448, 0.01968067, 0…
## $ Velocity_km_per_sec <dbl> 6.115834, 18.113985, 7.590711, 11.173874, 9.84083…
## $ Miss_Dist_kilometers <dbl> 62753692, 57298148, 7622912, 42683616, 61010824, …
## $ Eccentricity <dbl> 0.4255491, 0.3516743, 0.3482483, 0.2165783, 0.210…
## $ Hazardous_num <dbl> 1, 0, 1, 0, 1, 0, 0, 0, 0, 1, 0, 0, 0, 0, 0, 0, 0…
## $ Hazardous <fct> Hazardous, Tidak Hazardous, Hazardous, Tidak Haza…
Data dibersihkan dengan cara memilih variabel yang diperlukan,
mengubah variabel numerik ke format angka, serta mengubah respons
Hazardous menjadi kode biner:
\(Y = 1\) untuk asteroid berbahaya dan
\(Y = 0\) untuk asteroid tidak
berbahaya.
1.7 Statistik Deskriptif
1.7.1 Distribusi Variabel Respons
tab_respon <- data_bin %>%
dplyr::count(Hazardous) %>%
dplyr::mutate(
Proporsi = n / sum(n),
Persentase = scales::percent(Proporsi, accuracy = 0.1)
)
tab_respon %>%
knitr::kable(caption = "Distribusi variabel respons Hazardous") %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
| Hazardous | n | Proporsi | Persentase |
|---|---|---|---|
| Tidak Hazardous | 3932 | 0.8389162 | 83.9% |
| Hazardous | 755 | 0.1610838 | 16.1% |
ggplot(tab_respon, aes(x = Hazardous, y = Proporsi, fill = Hazardous)) +
geom_col(width = 0.65, alpha = 0.92) +
geom_text(aes(label = Persentase), vjust = -0.4, fontface = "bold") +
scale_y_continuous(labels = scales::percent_format(), limits = c(0, max(tab_respon$Proporsi) + 0.12)) +
scale_fill_manual(values = c("#2563eb", "#dc2626")) +
labs(
title = "Distribusi Status Hazardous",
subtitle = "Proporsi asteroid berdasarkan kategori berbahaya dan tidak berbahaya",
x = NULL,
y = "Proporsi"
) +
theme_minimal(base_size = 13) +
theme(legend.position = "none")
Tabel dan grafik di atas menunjukkan komposisi asteroid berdasarkan status Hazardous. Pemeriksaan ini penting karena regresi logistik biner membutuhkan dua kategori respons yang jelas. Jika salah satu kategori terlalu sedikit, model dapat menjadi kurang stabil.
1.7.2 Ringkasan Prediktor Numerik
summary_prediktor <- data_bin %>%
dplyr::select(
Absolute_Magnitude,
Est_Dia_KM_max,
Velocity_km_per_sec,
Miss_Dist_kilometers,
Eccentricity
) %>%
dplyr::summarise(
dplyr::across(
dplyr::everything(),
list(
Min = ~min(.x, na.rm = TRUE),
Mean = ~mean(.x, na.rm = TRUE),
Median = ~median(.x, na.rm = TRUE),
Max = ~max(.x, na.rm = TRUE),
SD = ~sd(.x, na.rm = TRUE)
)
)
) %>%
tidyr::pivot_longer(
cols = dplyr::everything(),
names_to = c("Variabel", ".value"),
names_pattern = "(.+)_(Min|Mean|Median|Max|SD)"
)
summary_prediktor %>%
dplyr::mutate(
dplyr::across(dplyr::where(is.numeric), ~round(.x, 4))
) %>%
knitr::kable(caption = "Statistik deskriptif prediktor numerik") %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
)
| Variabel | Min | Mean | Median | Max | SD |
|---|---|---|---|---|---|
| Absolute_Magnitude | 11.1600 | 2.226790e+01 | 2.190000e+01 | 3.21000e+01 | 2.89100e+00 |
| Est_Dia_KM_max | 0.0023 | 4.575000e-01 | 2.478000e-01 | 3.48369e+01 | 8.26400e-01 |
| Velocity_km_per_sec | 0.3355 | 1.397080e+01 | 1.291790e+01 | 4.46337e+01 | 7.29320e+00 |
| Miss_Dist_kilometers | 26609.8867 | 3.841347e+07 | 3.964771e+07 | 7.47816e+07 | 2.18111e+07 |
| Eccentricity | 0.0075 | 3.826000e-01 | 3.725000e-01 | 9.60300e-01 | 1.80400e-01 |
Ringkasan statistik digunakan untuk melihat rentang nilai tiap prediktor. Variabel seperti Miss Dist.(kilometers) biasanya memiliki skala yang sangat besar, sehingga pada tahap pemodelan variabel numerik akan distandarisasi agar estimasi model lebih stabil dan mudah dibandingkan.
1.8 Visualisasi Eksploratif
data_long <- data_bin %>%
dplyr::select(
Hazardous,
Absolute_Magnitude,
Est_Dia_KM_max,
Velocity_km_per_sec,
Miss_Dist_kilometers,
Eccentricity
) %>%
dplyr::mutate(
Miss_Dist_kilometers = log10(Miss_Dist_kilometers + 1),
Est_Dia_KM_max = log10(Est_Dia_KM_max + 1)
) %>%
tidyr::pivot_longer(
cols = -Hazardous,
names_to = "Prediktor",
values_to = "Nilai"
)
ggplot(data_long, aes(x = Hazardous, y = Nilai, fill = Hazardous)) +
geom_boxplot(alpha = 0.82, outlier.alpha = 0.35) +
facet_wrap(~ Prediktor, scales = "free_y") +
scale_fill_manual(values = c("#2563eb", "#dc2626")) +
labs(
title = "Perbandingan Prediktor Berdasarkan Status Hazardous",
subtitle = "Beberapa variabel diskalakan log agar pola lebih mudah dibaca",
x = NULL,
y = "Nilai prediktor"
) +
theme_minimal(base_size = 12.5) +
theme(legend.position = "none")
Visualisasi eksploratif membantu melihat apakah distribusi prediktor berbeda antara asteroid Hazardous dan Tidak Hazardous. Jika pola distribusi antar-kategori terlihat berbeda, prediktor tersebut berpotensi membantu klasifikasi dalam model regresi logistik biner.
1.9 Pembagian Data Training dan Testing
Data dibagi menjadi data training dan testing. Data training digunakan untuk membangun model, sedangkan data testing digunakan untuk mengevaluasi kemampuan prediksi model pada data yang tidak digunakan saat estimasi.
set.seed(123)
idx_train <- sample(seq_len(nrow(data_bin)), size = floor(0.8 * nrow(data_bin)))
data_train <- data_bin[idx_train, ]
data_test <- data_bin[-idx_train, ]
dim(data_train)
## [1] 3749 8
dim(data_test)
## [1] 938 8
Pembagian data dilakukan dengan proporsi sekitar 80% untuk training dan 20% untuk testing. Tujuannya agar evaluasi model tidak hanya dilakukan pada data yang sama dengan data pembentukan model.
1.10 Standardisasi Prediktor
Karena prediktor memiliki skala yang sangat berbeda, dilakukan standardisasi pada variabel numerik.
\[ Z = \frac{X-\bar{X}}{s_X} \]
Standardisasi membuat setiap prediktor berada pada skala yang sebanding.
prediktor_num <- c(
"Absolute_Magnitude",
"Est_Dia_KM_max",
"Velocity_km_per_sec",
"Miss_Dist_kilometers",
"Eccentricity"
)
center_train <- sapply(data_train[prediktor_num], mean, na.rm = TRUE)
scale_train <- sapply(data_train[prediktor_num], sd, na.rm = TRUE)
scale_train[scale_train == 0 | is.na(scale_train)] <- 1
standardize_data <- function(data, center, scale) {
data_out <- data
for (v in names(center)) {
data_out[[paste0(v, "_z")]] <- (data_out[[v]] - center[[v]]) / scale[[v]]
}
data_out
}
data_train_z <- standardize_data(data_train, center_train, scale_train)
data_test_z <- standardize_data(data_test, center_train, scale_train)
head(data_train_z)
Standardisasi tidak mengubah hubungan dasar antarvariabel, tetapi membantu model lebih stabil karena seluruh prediktor berada pada skala yang relatif sama. Interpretasi koefisien setelah standardisasi dibaca sebagai perubahan odds akibat kenaikan 1 simpangan baku pada prediktor.
1.11 Estimasi Model Regresi Logistik Biner
Model yang digunakan adalah:
\[ \log \left(\frac{\pi(x)}{1-\pi(x)}\right) = \beta_0 + \beta_1Z_1 + \beta_2Z_2 + \beta_3Z_3 + \beta_4Z_4 + \beta_5Z_5 \]
dengan:
\[ \pi(x)=P(\text{Hazardous}=1 \mid X) \]
fit_logit <- tryCatch(
glm(
Hazardous_num ~ Absolute_Magnitude_z + Est_Dia_KM_max_z +
Velocity_km_per_sec_z + Miss_Dist_kilometers_z + Eccentricity_z,
data = data_train_z,
family = binomial(link = "logit")
),
error = function(e) {
message("Model gagal diestimasi: ", e$message)
NULL
}
)
if (!is.null(fit_logit)) {
summary(fit_logit)
} else {
cat("Model tidak dapat diestimasi. Periksa kembali data dan kategori respons.")
}
##
## Call:
## glm(formula = Hazardous_num ~ Absolute_Magnitude_z + Est_Dia_KM_max_z +
## Velocity_km_per_sec_z + Miss_Dist_kilometers_z + Eccentricity_z,
## family = binomial(link = "logit"), data = data_train_z)
##
## Coefficients:
## Estimate Std. Error z value Pr(>|z|)
## (Intercept) -2.62562 0.09877 -26.582 < 2e-16 ***
## Absolute_Magnitude_z -2.80502 0.18242 -15.376 < 2e-16 ***
## Est_Dia_KM_max_z -1.74710 0.17126 -10.201 < 2e-16 ***
## Velocity_km_per_sec_z 0.24210 0.05583 4.336 1.45e-05 ***
## Miss_Dist_kilometers_z -0.43915 0.05899 -7.445 9.73e-14 ***
## Eccentricity_z 0.02368 0.05763 0.411 0.681
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## (Dispersion parameter for binomial family taken to be 1)
##
## Null deviance: 3283.9 on 3748 degrees of freedom
## Residual deviance: 2586.7 on 3743 degrees of freedom
## AIC: 2598.7
##
## Number of Fisher Scoring iterations: 7
Model regresi logistik biner diestimasi menggunakan fungsi
glm() dengan family binomial(link = "logit").
Model ini digunakan untuk memprediksi peluang asteroid tergolong
Hazardous berdasarkan lima prediktor yang telah
distandarisasi.
1.12 Ringkasan Koefisien, Odds Ratio, dan Signifikansi
if (!is.null(fit_logit)) {
coef_tab <- as.data.frame(coef(summary(fit_logit))) %>%
tibble::rownames_to_column("Parameter") %>%
dplyr::rename(
Estimate = Estimate,
Std_Error = `Std. Error`,
z_value = `z value`,
p_value = `Pr(>|z|)`
) %>%
dplyr::mutate(
Odds_Ratio = exp(Estimate),
CI_low = exp(Estimate - 1.96 * Std_Error),
CI_high = exp(Estimate + 1.96 * Std_Error),
Perubahan_Odds_Persen = 100 * (Odds_Ratio - 1),
Keputusan = dplyr::case_when(
p_value < 0.001 ~ "Signifikan pada 0.1%",
p_value < 0.01 ~ "Signifikan pada 1%",
p_value < 0.05 ~ "Signifikan pada 5%",
p_value < 0.10 ~ "Signifikan pada 10%",
TRUE ~ "Tidak signifikan"
)
)
coef_tab %>%
dplyr::mutate(
dplyr::across(
c(Estimate, Std_Error, z_value, p_value, Odds_Ratio, CI_low, CI_high, Perubahan_Odds_Persen),
~round(.x, 4)
)
) %>%
knitr::kable(
caption = "Ringkasan koefisien regresi logistik biner",
col.names = c(
"Parameter", "Estimate", "SE", "z-value", "p-value",
"Odds Ratio", "CI 2.5%", "CI 97.5%", "Perubahan Odds (%)", "Keputusan"
)
) %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
)
}
| Parameter | Estimate | SE | z-value | p-value | Odds Ratio | CI 2.5% | CI 97.5% | Perubahan Odds (%) | Keputusan |
|---|---|---|---|---|---|---|---|---|---|
| (Intercept) | -2.6256 | 0.0988 | -26.5821 | 0.0000 | 0.0724 | 0.0597 | 0.0879 | -92.7605 | Signifikan pada 0.1% |
| Absolute_Magnitude_z | -2.8050 | 0.1824 | -15.3765 | 0.0000 | 0.0605 | 0.0423 | 0.0865 | -93.9494 | Signifikan pada 0.1% |
| Est_Dia_KM_max_z | -1.7471 | 0.1713 | -10.2015 | 0.0000 | 0.1743 | 0.1246 | 0.2438 | -82.5722 | Signifikan pada 0.1% |
| Velocity_km_per_sec_z | 0.2421 | 0.0558 | 4.3363 | 0.0000 | 1.2739 | 1.1419 | 1.4212 | 27.3927 | Signifikan pada 0.1% |
| Miss_Dist_kilometers_z | -0.4392 | 0.0590 | -7.4446 | 0.0000 | 0.6446 | 0.5742 | 0.7236 | -35.5418 | Signifikan pada 0.1% |
| Eccentricity_z | 0.0237 | 0.0576 | 0.4108 | 0.6812 | 1.0240 | 0.9146 | 1.1464 | 2.3959 | Tidak signifikan |
Tabel di atas menampilkan koefisien model, nilai signifikansi, dan odds ratio. Karena prediktor telah distandarisasi, maka odds ratio menunjukkan perubahan odds asteroid menjadi Hazardous untuk setiap kenaikan 1 simpangan baku pada prediktor, dengan prediktor lain dianggap konstan.
1.13 Interpretasi Variabel Secara Otomatis
if (!is.null(fit_logit)) {
coef_interpretasi <- coef_tab %>%
dplyr::filter(Parameter != "(Intercept)") %>%
dplyr::mutate(
Nama = dplyr::case_when(
Parameter == "Absolute_Magnitude_z" ~ "Absolute Magnitude",
Parameter == "Est_Dia_KM_max_z" ~ "Est Dia in KM(max)",
Parameter == "Velocity_km_per_sec_z" ~ "Velocity km per sec",
Parameter == "Miss_Dist_kilometers_z" ~ "Miss Dist.(kilometers)",
Parameter == "Eccentricity_z" ~ "Eccentricity",
TRUE ~ Parameter
),
Arah = ifelse(Odds_Ratio > 1, "meningkatkan", "menurunkan"),
Persen = abs(round(Perubahan_Odds_Persen, 2)),
Signif = ifelse(p_value < 0.05, "signifikan", "tidak signifikan")
)
cat("<div class='interpretasi'>")
cat("<strong>Interpretasi hasil model:</strong><br><br>")
for (i in seq_len(nrow(coef_interpretasi))) {
cat(
paste0(
"<p><strong>", coef_interpretasi$Nama[i], "</strong>: ",
"kenaikan 1 simpangan baku pada variabel ini diperkirakan ",
coef_interpretasi$Arah[i], " odds asteroid tergolong Hazardous sekitar ",
coef_interpretasi$Persen[i], "%, dengan variabel lain konstan. ",
"Secara statistik, pengaruh variabel ini tergolong <strong>",
coef_interpretasi$Signif[i], "</strong> pada taraf 5%.</p>"
)
)
}
cat("</div>")
}
Absolute Magnitude: kenaikan 1 simpangan baku pada variabel ini diperkirakan menurunkan odds asteroid tergolong Hazardous sekitar 93.95%, dengan variabel lain konstan. Secara statistik, pengaruh variabel ini tergolong signifikan pada taraf 5%.
Est Dia in KM(max): kenaikan 1 simpangan baku pada variabel ini diperkirakan menurunkan odds asteroid tergolong Hazardous sekitar 82.57%, dengan variabel lain konstan. Secara statistik, pengaruh variabel ini tergolong signifikan pada taraf 5%.
Velocity km per sec: kenaikan 1 simpangan baku pada variabel ini diperkirakan meningkatkan odds asteroid tergolong Hazardous sekitar 27.39%, dengan variabel lain konstan. Secara statistik, pengaruh variabel ini tergolong signifikan pada taraf 5%.
Miss Dist.(kilometers): kenaikan 1 simpangan baku pada variabel ini diperkirakan menurunkan odds asteroid tergolong Hazardous sekitar 35.54%, dengan variabel lain konstan. Secara statistik, pengaruh variabel ini tergolong signifikan pada taraf 5%.
Eccentricity: kenaikan 1 simpangan baku pada variabel ini diperkirakan meningkatkan odds asteroid tergolong Hazardous sekitar 2.4%, dengan variabel lain konstan. Secara statistik, pengaruh variabel ini tergolong tidak signifikan pada taraf 5%.
1.14 Uji Kesesuaian Model Secara Keseluruhan
Model penuh dibandingkan dengan model null. Model null hanya berisi intercept tanpa prediktor.
Hipotesis:
\[ H_0 : \beta_1 = \beta_2 = \cdots = \beta_p = 0 \]
\[ H_1 : \text{minimal ada satu } \beta_j \neq 0 \]
if (!is.null(fit_logit)) {
fit_null <- glm(
Hazardous_num ~ 1,
data = data_train_z,
family = binomial(link = "logit")
)
lr_test <- anova(fit_null, fit_logit, test = "Chisq")
lr_test %>%
knitr::kable(caption = "Likelihood Ratio Test antara model null dan model penuh") %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
}
| Resid. Df | Resid. Dev | Df | Deviance | Pr(>Chi) |
|---|---|---|---|---|
| 3748 | 3283.880 | NA | NA | NA |
| 3743 | 2586.706 | 5 | 697.1745 | 0 |
Likelihood Ratio Test digunakan untuk melihat apakah model dengan prediktor lebih baik dibandingkan model tanpa prediktor. Jika p-value kecil, maka secara keseluruhan prediktor yang digunakan mampu memberikan tambahan informasi dalam menjelaskan status Hazardous.
1.15 Pseudo R-Squared
Pada regresi logistik, ukuran \(R^2\) tidak sama seperti regresi linear. Salah satu ukuran yang umum digunakan adalah McFadden Pseudo \(R^2\):
\[ R^2_{McFadden} = 1-\frac{\log L_{\text{model}}}{\log L_{\text{null}}} \]
if (!is.null(fit_logit)) {
loglik_model <- as.numeric(logLik(fit_logit))
loglik_null <- as.numeric(logLik(fit_null))
pseudo_r2 <- 1 - (loglik_model / loglik_null)
tibble::tibble(
`Log Likelihood Model` = loglik_model,
`Log Likelihood Null` = loglik_null,
`McFadden Pseudo R2` = pseudo_r2
) %>%
dplyr::mutate(dplyr::across(dplyr::everything(), ~round(.x, 4))) %>%
knitr::kable(caption = "Nilai McFadden Pseudo R-Squared") %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
}
| Log Likelihood Model | Log Likelihood Null | McFadden Pseudo R2 |
|---|---|---|
| -1293.353 | -1641.94 | 0.2123 |
Pseudo \(R^2\) digunakan sebagai ukuran ringkas kemampuan model dalam memperbaiki model null. Nilainya tidak harus dibaca seperti \(R^2\) pada regresi linear, tetapi semakin besar nilainya menunjukkan model lebih informatif dibandingkan model tanpa prediktor.
1.16 Pemeriksaan Multikolinearitas Sederhana
Multikolinearitas terjadi ketika antar-prediktor memiliki hubungan linear yang kuat. Salah satu ukuran yang sering digunakan adalah Variance Inflation Factor (VIF).
\[ VIF_j = \frac{1}{1-R_j^2} \]
calc_vif <- function(data, vars) {
out <- lapply(vars, function(v) {
others <- setdiff(vars, v)
form <- as.formula(paste(v, "~", paste(others, collapse = " + ")))
fit <- lm(form, data = data)
r2 <- summary(fit)$r.squared
vif <- 1 / (1 - r2)
data.frame(Variabel = v, VIF = vif)
})
dplyr::bind_rows(out)
}
vif_tab <- calc_vif(
data_train_z,
c(
"Absolute_Magnitude_z",
"Est_Dia_KM_max_z",
"Velocity_km_per_sec_z",
"Miss_Dist_kilometers_z",
"Eccentricity_z"
)
) %>%
dplyr::mutate(
Variabel = dplyr::case_when(
Variabel == "Absolute_Magnitude_z" ~ "Absolute Magnitude",
Variabel == "Est_Dia_KM_max_z" ~ "Est Dia in KM(max)",
Variabel == "Velocity_km_per_sec_z" ~ "Velocity km per sec",
Variabel == "Miss_Dist_kilometers_z" ~ "Miss Dist.(kilometers)",
Variabel == "Eccentricity_z" ~ "Eccentricity",
TRUE ~ Variabel
),
VIF = round(VIF, 4),
Keterangan = dplyr::case_when(
VIF < 5 ~ "Aman",
VIF < 10 ~ "Perlu diperhatikan",
TRUE ~ "Indikasi multikolinearitas tinggi"
)
)
vif_tab %>%
knitr::kable(caption = "Pemeriksaan multikolinearitas menggunakan VIF") %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
| Variabel | VIF | Keterangan |
|---|---|---|
| Absolute Magnitude | 1.8961 | Aman |
| Est Dia in KM(max) | 1.5513 | Aman |
| Velocity km per sec | 1.5882 | Aman |
| Miss Dist.(kilometers) | 1.3063 | Aman |
| Eccentricity | 1.5028 | Aman |
VIF digunakan untuk melihat apakah ada prediktor yang terlalu kuat berkorelasi dengan prediktor lain. Secara praktis, VIF di bawah 5 biasanya masih dianggap aman, sedangkan nilai yang sangat tinggi menandakan potensi multikolinearitas.
1.17 Prediksi Peluang pada Data Testing
if (!is.null(fit_logit)) {
data_test_z$prob_hazardous <- predict(
fit_logit,
newdata = data_test_z,
type = "response"
)
data_test_z$pred_class <- ifelse(
data_test_z$prob_hazardous >= 0.5,
1,
0
)
data_test_z$pred_label <- factor(
data_test_z$pred_class,
levels = c(0, 1),
labels = c("Tidak Hazardous", "Hazardous")
)
data_test_z %>%
dplyr::select(Hazardous, prob_hazardous, pred_label) %>%
head(10) %>%
dplyr::mutate(prob_hazardous = round(prob_hazardous, 4)) %>%
knitr::kable(caption = "Contoh hasil prediksi peluang pada data testing") %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
}
| Hazardous | prob_hazardous | pred_label |
|---|---|---|
| Tidak Hazardous | 0.2623 | Tidak Hazardous |
| Hazardous | 0.3759 | Tidak Hazardous |
| Tidak Hazardous | 0.0543 | Tidak Hazardous |
| Tidak Hazardous | 0.1389 | Tidak Hazardous |
| Tidak Hazardous | 0.2352 | Tidak Hazardous |
| Tidak Hazardous | 0.0005 | Tidak Hazardous |
| Tidak Hazardous | 0.0077 | Tidak Hazardous |
| Hazardous | 0.2517 | Tidak Hazardous |
| Tidak Hazardous | 0.3843 | Tidak Hazardous |
| Tidak Hazardous | 0.2561 | Tidak Hazardous |
Nilai prob_hazardous menunjukkan peluang prediksi
asteroid tergolong Hazardous. Jika peluang prediksi
minimal 0.5, asteroid diklasifikasikan sebagai
Hazardous. Jika kurang dari 0.5, asteroid
diklasifikasikan sebagai Tidak Hazardous.
1.18 Confusion Matrix dan Ukuran Evaluasi
if (!is.null(fit_logit)) {
conf_mat <- table(
Aktual = data_test_z$Hazardous,
Prediksi = data_test_z$pred_label
)
conf_mat
TP <- sum(data_test_z$Hazardous_num == 1 & data_test_z$pred_class == 1)
TN <- sum(data_test_z$Hazardous_num == 0 & data_test_z$pred_class == 0)
FP <- sum(data_test_z$Hazardous_num == 0 & data_test_z$pred_class == 1)
FN <- sum(data_test_z$Hazardous_num == 1 & data_test_z$pred_class == 0)
accuracy <- (TP + TN) / (TP + TN + FP + FN)
sensitivity <- ifelse((TP + FN) == 0, NA, TP / (TP + FN))
specificity <- ifelse((TN + FP) == 0, NA, TN / (TN + FP))
precision <- ifelse((TP + FP) == 0, NA, TP / (TP + FP))
eval_tab <- tibble::tibble(
Ukuran = c("Accuracy", "Sensitivity", "Specificity", "Precision"),
Nilai = c(accuracy, sensitivity, specificity, precision)
)
eval_tab %>%
dplyr::mutate(Nilai = scales::percent(Nilai, accuracy = 0.1)) %>%
knitr::kable(caption = "Ukuran evaluasi klasifikasi pada data testing") %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
}
| Ukuran | Nilai |
|---|---|
| Accuracy | 83.5% |
| Sensitivity | 6.9% |
| Specificity | 99.1% |
| Precision | 61.1% |
Confusion matrix membandingkan kategori aktual dengan kategori hasil
prediksi.
Accuracy menunjukkan proporsi prediksi yang benar
secara keseluruhan.
Sensitivity menunjukkan kemampuan model mengenali
asteroid yang benar-benar Hazardous.
Specificity menunjukkan kemampuan model mengenali
asteroid yang tidak Hazardous.
1.19 Kurva ROC dan AUC
AUC digunakan untuk mengukur kemampuan model membedakan kategori \(Y=1\) dan \(Y=0\). Nilai AUC berada antara 0.5 sampai 1.
if (!is.null(fit_logit)) {
roc_data <- data.frame(
y = data_test_z$Hazardous_num,
prob = data_test_z$prob_hazardous
) %>%
dplyr::arrange(dplyr::desc(prob))
thresholds <- sort(unique(roc_data$prob), decreasing = TRUE)
roc_points <- lapply(thresholds, function(th) {
pred <- ifelse(roc_data$prob >= th, 1, 0)
TP <- sum(roc_data$y == 1 & pred == 1)
TN <- sum(roc_data$y == 0 & pred == 0)
FP <- sum(roc_data$y == 0 & pred == 1)
FN <- sum(roc_data$y == 1 & pred == 0)
data.frame(
threshold = th,
TPR = ifelse((TP + FN) == 0, 0, TP / (TP + FN)),
FPR = ifelse((FP + TN) == 0, 0, FP / (FP + TN))
)
}) %>%
dplyr::bind_rows()
roc_points <- dplyr::bind_rows(
data.frame(threshold = Inf, TPR = 0, FPR = 0),
roc_points,
data.frame(threshold = -Inf, TPR = 1, FPR = 1)
) %>%
dplyr::arrange(FPR, TPR)
auc_value <- sum(diff(roc_points$FPR) *
(head(roc_points$TPR, -1) + tail(roc_points$TPR, -1)) / 2)
ggplot(roc_points, aes(x = FPR, y = TPR)) +
geom_line(color = "#2563eb", linewidth = 1.25) +
geom_abline(linetype = "dashed", color = "gray45") +
coord_equal() +
labs(
title = "Kurva ROC",
subtitle = paste0("AUC = ", round(auc_value, 4)),
x = "False Positive Rate (1 - Specificity)",
y = "True Positive Rate (Sensitivity)"
) +
theme_minimal(base_size = 13)
}
Kurva ROC menunjukkan performa model pada berbagai kemungkinan threshold. Nilai AUC yang semakin mendekati 1 menunjukkan kemampuan diskriminasi model yang semakin baik dalam membedakan asteroid Hazardous dan Tidak Hazardous.
1.20 Visualisasi Odds Ratio
if (!is.null(fit_logit)) {
coef_tab %>%
dplyr::filter(Parameter != "(Intercept)") %>%
dplyr::mutate(
Variabel = dplyr::case_when(
Parameter == "Absolute_Magnitude_z" ~ "Absolute Magnitude",
Parameter == "Est_Dia_KM_max_z" ~ "Est Dia in KM(max)",
Parameter == "Velocity_km_per_sec_z" ~ "Velocity km per sec",
Parameter == "Miss_Dist_kilometers_z" ~ "Miss Dist.(kilometers)",
Parameter == "Eccentricity_z" ~ "Eccentricity",
TRUE ~ Parameter
)
) %>%
ggplot(aes(x = reorder(Variabel, Odds_Ratio), y = Odds_Ratio)) +
geom_hline(yintercept = 1, linetype = "dashed", color = "gray45") +
geom_pointrange(aes(ymin = CI_low, ymax = CI_high), color = "#2563eb", linewidth = 0.7) +
coord_flip() +
labs(
title = "Odds Ratio dan Interval Kepercayaan 95%",
subtitle = "Prediktor telah distandarisasi",
x = NULL,
y = "Odds Ratio"
) +
theme_minimal(base_size = 13)
}
Grafik odds ratio memudahkan pembaca melihat arah dan kekuatan pengaruh prediktor. Titik di atas 1 menunjukkan peningkatan odds asteroid menjadi Hazardous, sedangkan titik di bawah 1 menunjukkan penurunan odds. Garis interval yang melewati 1 menandakan efeknya belum kuat secara statistik pada taraf 5%.
1.21 Contoh Prediksi Individual
Misalkan ingin memprediksi peluang Hazardous untuk asteroid dengan karakteristik tertentu, kita dapat menggunakan data baru.
if (!is.null(fit_logit)) {
data_baru <- data.frame(
Absolute_Magnitude = median(data_bin$Absolute_Magnitude, na.rm = TRUE),
Est_Dia_KM_max = median(data_bin$Est_Dia_KM_max, na.rm = TRUE),
Velocity_km_per_sec = median(data_bin$Velocity_km_per_sec, na.rm = TRUE),
Miss_Dist_kilometers = median(data_bin$Miss_Dist_kilometers, na.rm = TRUE),
Eccentricity = median(data_bin$Eccentricity, na.rm = TRUE)
)
data_baru_z <- standardize_data(data_baru, center_train, scale_train)
peluang_baru <- predict(fit_logit, newdata = data_baru_z, type = "response")
data_baru %>%
dplyr::mutate(
Peluang_Hazardous = round(peluang_baru, 4),
Klasifikasi = ifelse(peluang_baru >= 0.5, "Hazardous", "Tidak Hazardous")
) %>%
knitr::kable(caption = "Contoh prediksi peluang Hazardous untuk data baru") %>%
kableExtra::kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
)
}
| Absolute_Magnitude | Est_Dia_KM_max | Velocity_km_per_sec | Miss_Dist_kilometers | Eccentricity | Peluang_Hazardous | Klasifikasi |
|---|---|---|---|---|---|---|
| 21.9 | 0.247765 | 12.91789 | 39647712 | 0.3724502 | 0.1318 | Tidak Hazardous |
Contoh prediksi individual menunjukkan cara menggunakan model untuk menghitung peluang asteroid baru tergolong Hazardous. Nilai peluang ini dapat digunakan sebagai dasar klasifikasi dengan threshold tertentu, misalnya 0.5.
1.22 Kesimpulan
Berdasarkan analisis regresi logistik biner, model digunakan untuk menjelaskan peluang asteroid tergolong Hazardous berdasarkan lima prediktor numerik. Prediktor yang memiliki odds ratio di atas 1 cenderung meningkatkan peluang asteroid masuk kategori berbahaya, sedangkan prediktor dengan odds ratio di bawah 1 cenderung menurunkan peluang tersebut.
Secara umum, regresi logistik biner sesuai digunakan pada kasus ini karena variabel respons hanya memiliki dua kategori, yaitu Hazardous dan Tidak Hazardous. Hasil model dapat dibaca melalui koefisien, p-value, odds ratio, confusion matrix, serta AUC. Dengan demikian, analisis ini tidak hanya memberikan prediksi, tetapi juga membantu menjelaskan faktor-faktor yang berkaitan dengan status bahaya asteroid.
2 BAB 2. Regresi Logistik Multinomial
2.1 Pendahuluan
Analisis ini bertujuan untuk memodelkan genre lagu Spotify menggunakan regresi logistik multinomial. Model ini digunakan karena variabel respon memiliki lebih dari dua kategori dan kategori tersebut bersifat nominal atau tidak memiliki urutan alami.
Variabel respon yang digunakan adalah track_genre, yang
terdiri dari lima kategori:
poprockhip-hopclassicalr-n-b
Variabel prediktor yang digunakan adalah:
danceabilityenergyacousticnesstempovalence
2.2 Membaca Data Excel
data <- read_excel("~/Salwa/SMT 4/ADK 1/tugas regresi logostik/spotify_filtered.xlsx")
glimpse(data)
## Rows: 5,000
## Columns: 21
## $ X <dbl> 16000, 16001, 16002, 16003, 16004, 16005, 16006, 1600…
## $ track_id <chr> "7wrYBASu0OoxoDErd4Edxd", "72HdutlIHBZJ7WT1xVAAZT", "…
## $ artists <chr> "Bombay Jayashri", "Shankar;Ehsaan;Loy;Alisha Chinai;…
## $ album_name <chr> "Rehnaa Hai Terre Dil Mein", "Bunty Aur Babli", "Hind…
## $ track_name <chr> "Zara Zara", "Kajra Re", "Zara Zara - Lofi", "Vaseega…
## $ popularity <dbl> 58, 59, 54, 68, 59, 62, 59, 0, 2, 1, 0, 1, 0, 2, 0, 1…
## $ duration_ms <dbl> 298266, 482586, 219437, 299146, 387716, 298266, 30662…
## $ explicit <chr> "False", "False", "False", "False", "False", "False",…
## $ danceability <dbl> 0.6430, 0.4840, 0.6080, 0.6950, 0.5830, 0.6430, 0.642…
## $ energy <dbl> 0.2680, 0.8980, 0.6380, 0.2930, 0.3080, 0.2680, 0.562…
## $ key <dbl> 11, 0, 11, 11, 7, 11, 9, 0, 9, 9, 2, 2, 7, 2, 2, 0, 4…
## $ loudness <dbl> -15.073, -4.132, -6.008, -16.278, -18.303, -15.073, -…
## $ mode <dbl> 0, 1, 0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 0, 1, 1,…
## $ speechiness <dbl> 0.0900, 0.1640, 0.0292, 0.0431, 0.0465, 0.0900, 0.060…
## $ acousticness <dbl> 0.593, 0.365, 0.581, 0.596, 0.581, 0.593, 0.584, 0.96…
## $ instrumentalness <dbl> 2.06e-06, 0.00e+00, 1.72e-02, 1.58e-02, 1.06e-02, 2.0…
## $ liveness <dbl> 0.3160, 0.0910, 0.4480, 0.1320, 0.2570, 0.3160, 0.104…
## $ valence <dbl> 0.6200, 0.6800, 0.4390, 0.6370, 0.2410, 0.6200, 0.671…
## $ tempo <dbl> 143.813, 91.975, 140.109, 143.804, 118.226, 143.813, …
## $ time_signature <dbl> 4, 4, 4, 4, 4, 4, 4, 3, 4, 4, 4, 4, 3, 4, 3, 3, 4, 4,…
## $ track_genre <chr> "classical", "classical", "classical", "classical", "…
Dataset yang digunakan merupakan data lagu Spotify. Variabel respon adalah genre lagu, sedangkan variabel prediktor merupakan karakteristik audio lagu seperti danceability, energy, acousticness, tempo, dan valence.
2.3 Memilih Variabel Penelitian
data_model <- data %>%
dplyr::select(
genre = track_genre,
danceability,
energy,
acousticness,
tempo,
valence
) %>%
dplyr::filter(
genre %in% c("pop", "rock", "hip-hop", "classical", "r-n-b")
) %>%
dplyr::mutate(
genre = factor(
genre,
levels = c("classical", "pop", "rock", "hip-hop", "r-n-b")
)
) %>%
tidyr::drop_na()
glimpse(data_model)
## Rows: 5,000
## Columns: 6
## $ genre <fct> classical, classical, classical, classical, classical, cl…
## $ danceability <dbl> 0.6430, 0.4840, 0.6080, 0.6950, 0.5830, 0.6430, 0.6420, 0…
## $ energy <dbl> 0.2680, 0.8980, 0.6380, 0.2930, 0.3080, 0.2680, 0.5620, 0…
## $ acousticness <dbl> 0.593, 0.365, 0.581, 0.596, 0.581, 0.593, 0.584, 0.965, 0…
## $ tempo <dbl> 143.813, 91.975, 140.109, 143.804, 118.226, 143.813, 149.…
## $ valence <dbl> 0.6200, 0.6800, 0.4390, 0.6370, 0.2410, 0.6200, 0.6710, 0…
Kategori acuan yang digunakan adalah classical. Artinya, model akan membandingkan peluang genre pop, rock, hip-hop, dan r-n-b terhadap genre classical.
2.4 Statistik Deskriptif
data_model %>%
dplyr::summarise(
Jumlah_Data = n(),
Rata_Rata_Danceability = mean(danceability),
Rata_Rata_Energy = mean(energy),
Rata_Rata_Acousticness = mean(acousticness),
Rata_Rata_Tempo = mean(tempo),
Rata_Rata_Valence = mean(valence)
) %>%
kable(
digits = 3,
caption = "Statistik Deskriptif Variabel Numerik"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
| Jumlah_Data | Rata_Rata_Danceability | Rata_Rata_Energy | Rata_Rata_Acousticness | Rata_Rata_Tempo | Rata_Rata_Valence |
|---|---|---|---|---|---|
| 5000 | 0.581 | 0.559 | 0.408 | 119.201 | 0.522 |
data_model %>%
dplyr::group_by(genre) %>%
dplyr::summarise(
Jumlah = n(),
Mean_Danceability = mean(danceability),
Mean_Energy = mean(energy),
Mean_Acousticness = mean(acousticness),
Mean_Tempo = mean(tempo),
Mean_Valence = mean(valence),
.groups = "drop"
) %>%
kable(
digits = 3,
caption = "Rata-rata Karakteristik Audio Berdasarkan Genre"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
)
| genre | Jumlah | Mean_Danceability | Mean_Energy | Mean_Acousticness | Mean_Tempo | Mean_Valence |
|---|---|---|---|---|---|---|
| classical | 1000 | 0.382 | 0.190 | 0.920 | 107.950 | 0.381 |
| pop | 1000 | 0.630 | 0.606 | 0.344 | 120.927 | 0.506 |
| rock | 1000 | 0.544 | 0.679 | 0.209 | 126.321 | 0.539 |
| hip-hop | 1000 | 0.736 | 0.683 | 0.194 | 116.770 | 0.551 |
| r-n-b | 1000 | 0.614 | 0.638 | 0.371 | 124.039 | 0.633 |
2.5 Distribusi Variabel Respon
data_model %>%
dplyr::count(genre) %>%
dplyr::mutate(Proporsi = n / sum(n)) %>%
kable(
digits = 3,
caption = "Distribusi Genre Lagu"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
| genre | n | Proporsi |
|---|---|---|
| classical | 1000 | 0.2 |
| pop | 1000 | 0.2 |
| rock | 1000 | 0.2 |
| hip-hop | 1000 | 0.2 |
| r-n-b | 1000 | 0.2 |
data_model %>%
dplyr::count(genre) %>%
dplyr::mutate(Proporsi = n / sum(n)) %>%
ggplot(aes(x = genre, y = Proporsi, fill = genre)) +
geom_col(width = 0.65, alpha = 0.95) +
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, 1)) +
scale_fill_manual(values = c(
"classical" = "#5568b8",
"pop" = "#d18b2f",
"rock" = "#b84f5a",
"hip-hop" = "#2f7f73",
"r-n-b" = "#7c3aed"
)) +
labs(
title = "Distribusi Genre Lagu Spotify",
subtitle = "Variabel respon terdiri dari lima kategori nominal.",
x = "Genre",
y = "Proporsi",
fill = "Genre"
) +
theme_profesional()
2.6 Visualisasi Awal Karakteristik Audio
data_long <- data_model %>%
tidyr::pivot_longer(
cols = c(danceability, energy, acousticness, tempo, valence),
names_to = "Variabel",
values_to = "Nilai"
)
ggplot(data_long, aes(x = genre, y = Nilai, fill = genre)) +
geom_boxplot(alpha = 0.85, outlier.alpha = 0.25) +
facet_wrap(~ Variabel, scales = "free_y") +
scale_fill_manual(values = c(
"classical" = "#5568b8",
"pop" = "#d18b2f",
"rock" = "#b84f5a",
"hip-hop" = "#2f7f73",
"r-n-b" = "#7c3aed"
)) +
labs(
title = "Sebaran Karakteristik Audio Berdasarkan Genre",
subtitle = "Boxplot digunakan untuk melihat perbedaan pola audio pada masing-masing genre.",
x = "Genre",
y = "Nilai",
fill = "Genre"
) +
theme_profesional() +
theme(axis.text.x = element_text(angle = 30, hjust = 1))
2.7 Rumusan Model Regresi Logistik Multinomial
Regresi logistik multinomial digunakan ketika variabel respon terdiri
dari lebih dari dua kategori nominal. Misalkan kategori acuan adalah
classical.
Untuk setiap kategori \(j\) selain kategori acuan, model dituliskan sebagai:
\[ \log\left(\frac{\pi_j}{\pi_{classical}}\right) = \beta_{0j} + \beta_{1j}danceability + \beta_{2j}energy + \beta_{3j}acousticness + \beta_{4j}tempo + \beta_{5j}valence \]
dengan:
\[ \pi_j = P(Y = j) \]
dan:
\[ j \in \{pop, rock, hip\text{-}hop, r\text{-}n\text{-}b\} \]
Secara lengkap, model membentuk empat persamaan logit:
\[ \log\left(\frac{\pi_{pop}}{\pi_{classical}}\right) = \beta_{0,pop} + \beta_{1,pop}danceability + \beta_{2,pop}energy + \beta_{3,pop}acousticness + \beta_{4,pop}tempo + \beta_{5,pop}valence \]
\[ \log\left(\frac{\pi_{rock}}{\pi_{classical}}\right) = \beta_{0,rock} + \beta_{1,rock}danceability + \beta_{2,rock}energy + \beta_{3,rock}acousticness + \beta_{4,rock}tempo + \beta_{5,rock}valence \]
\[ \log\left(\frac{\pi_{hip-hop}}{\pi_{classical}}\right) = \beta_{0,hip-hop} + \beta_{1,hip-hop}danceability + \beta_{2,hip-hop}energy + \beta_{3,hip-hop}acousticness + \beta_{4,hip-hop}tempo + \beta_{5,hip-hop}valence \]
\[ \log\left(\frac{\pi_{r-n-b}}{\pi_{classical}}\right) = \beta_{0,r-n-b} + \beta_{1,r-n-b}danceability + \beta_{2,r-n-b}energy + \beta_{3,r-n-b}acousticness + \beta_{4,r-n-b}tempo + \beta_{5,r-n-b}valence \]
Koefisien positif menunjukkan bahwa peningkatan variabel prediktor meningkatkan log-odds suatu genre dibandingkan genre classical. Koefisien negatif menunjukkan bahwa peningkatan variabel prediktor menurunkan log-odds genre tersebut dibandingkan genre classical.
2.8 Membagi Data Latih dan Data Uji
set.seed(123)
indeks_latih <- data_model %>%
dplyr::mutate(id = dplyr::row_number()) %>%
dplyr::group_by(genre) %>%
dplyr::slice_sample(prop = 0.8) %>%
dplyr::pull(id)
data_latih <- data_model[indeks_latih, ]
data_uji <- data_model[-indeks_latih, ]
dim(data_latih)
## [1] 4000 6
dim(data_uji)
## [1] 1000 6
Data dibagi menjadi 80% data latih dan 20% data uji. Pembagian dilakukan secara proporsional berdasarkan genre agar setiap kategori tetap terwakili pada data latih dan data uji.
2.9 Membentuk Model Regresi Logistik Multinomial
model_multinom <- nnet::multinom(
genre ~ danceability + energy + acousticness + tempo + valence,
data = data_latih,
trace = FALSE,
maxit = 1000
)
summary(model_multinom)
## Call:
## nnet::multinom(formula = genre ~ danceability + energy + acousticness +
## tempo + valence, data = data_latih, trace = FALSE, maxit = 1000)
##
## Coefficients:
## (Intercept) danceability energy acousticness tempo valence
## pop -1.208437 10.673277 4.633309 -6.252155 0.005873803 -4.739093
## rock 2.598124 4.285420 4.229088 -8.479957 0.002703960 -2.429884
## hip-hop -7.118033 18.295148 7.072579 -7.404278 0.004620625 -5.636935
## r-n-b -1.486820 7.557334 5.173339 -5.744653 0.005057915 -1.606806
##
## Std. Errors:
## (Intercept) danceability energy acousticness tempo valence
## pop 0.7688838 0.7874017 0.5879389 0.5754970 0.002990815 0.4720482
## rock 0.7605027 0.7800742 0.5996610 0.5862316 0.002995161 0.4774287
## hip-hop 0.8787138 0.8859196 0.6494834 0.6039369 0.003301820 0.5042145
## r-n-b 0.7561353 0.7685413 0.5721998 0.5666061 0.002938021 0.4586678
##
## Residual Deviance: 8528.958
## AIC: 8576.958
2.10 Koefisien Model
koefisien <- coef(model_multinom)
round(koefisien, 4)
## (Intercept) danceability energy acousticness tempo valence
## pop -1.2084 10.6733 4.6333 -6.2522 0.0059 -4.7391
## rock 2.5981 4.2854 4.2291 -8.4800 0.0027 -2.4299
## hip-hop -7.1180 18.2951 7.0726 -7.4043 0.0046 -5.6369
## r-n-b -1.4868 7.5573 5.1733 -5.7447 0.0051 -1.6068
Baris pada tabel koefisien menunjukkan genre yang dibandingkan terhadap kategori acuan classical. Kolom menunjukkan parameter model. Misalnya, koefisien danceability pada baris pop menunjukkan pengaruh danceability terhadap log-odds genre pop dibandingkan classical.
2.11 Uji Signifikansi Parameter
hasil_summary <- summary(model_multinom)
z_value <- hasil_summary$coefficients / hasil_summary$standard.errors
p_value <- 2 * (1 - pnorm(abs(z_value)))
tabel_uji <- data.frame(
Genre = rep(rownames(hasil_summary$coefficients),
each = ncol(hasil_summary$coefficients)),
Variabel = rep(colnames(hasil_summary$coefficients),
times = nrow(hasil_summary$coefficients)),
Koefisien = as.vector(t(hasil_summary$coefficients)),
Std_Error = as.vector(t(hasil_summary$standard.errors)),
Z_Value = as.vector(t(z_value)),
P_Value = as.vector(t(p_value))
) %>%
dplyr::mutate(
Kesimpulan = ifelse(P_Value < 0.05, "Signifikan", "Tidak signifikan")
)
tabel_uji %>%
dplyr::mutate(
dplyr::across(c(Koefisien, Std_Error, Z_Value, P_Value), ~ round(.x, 4))
) %>%
kable(
caption = "Uji Signifikansi Koefisien Model Multinomial"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
) %>%
row_spec(
which(tabel_uji$P_Value < 0.05),
bold = TRUE,
color = "white",
background = "#16a34a"
)
| Genre | Variabel | Koefisien | Std_Error | Z_Value | P_Value | Kesimpulan |
|---|---|---|---|---|---|---|
| pop | (Intercept) | -1.2084 | 0.7689 | -1.5717 | 0.1160 | Tidak signifikan |
| pop | danceability | 10.6733 | 0.7874 | 13.5551 | 0.0000 | Signifikan |
| pop | energy | 4.6333 | 0.5879 | 7.8806 | 0.0000 | Signifikan |
| pop | acousticness | -6.2522 | 0.5755 | -10.8639 | 0.0000 | Signifikan |
| pop | tempo | 0.0059 | 0.0030 | 1.9639 | 0.0495 | Signifikan |
| pop | valence | -4.7391 | 0.4720 | -10.0394 | 0.0000 | Signifikan |
| rock | (Intercept) | 2.5981 | 0.7605 | 3.4163 | 0.0006 | Signifikan |
| rock | danceability | 4.2854 | 0.7801 | 5.4936 | 0.0000 | Signifikan |
| rock | energy | 4.2291 | 0.5997 | 7.0525 | 0.0000 | Signifikan |
| rock | acousticness | -8.4800 | 0.5862 | -14.4652 | 0.0000 | Signifikan |
| rock | tempo | 0.0027 | 0.0030 | 0.9028 | 0.3666 | Tidak signifikan |
| rock | valence | -2.4299 | 0.4774 | -5.0895 | 0.0000 | Signifikan |
| hip-hop | (Intercept) | -7.1180 | 0.8787 | -8.1005 | 0.0000 | Signifikan |
| hip-hop | danceability | 18.2951 | 0.8859 | 20.6510 | 0.0000 | Signifikan |
| hip-hop | energy | 7.0726 | 0.6495 | 10.8895 | 0.0000 | Signifikan |
| hip-hop | acousticness | -7.4043 | 0.6039 | -12.2600 | 0.0000 | Signifikan |
| hip-hop | tempo | 0.0046 | 0.0033 | 1.3994 | 0.1617 | Tidak signifikan |
| hip-hop | valence | -5.6369 | 0.5042 | -11.1796 | 0.0000 | Signifikan |
| r-n-b | (Intercept) | -1.4868 | 0.7561 | -1.9663 | 0.0493 | Signifikan |
| r-n-b | danceability | 7.5573 | 0.7685 | 9.8333 | 0.0000 | Signifikan |
| r-n-b | energy | 5.1733 | 0.5722 | 9.0411 | 0.0000 | Signifikan |
| r-n-b | acousticness | -5.7447 | 0.5666 | -10.1387 | 0.0000 | Signifikan |
| r-n-b | tempo | 0.0051 | 0.0029 | 1.7215 | 0.0852 | Tidak signifikan |
| r-n-b | valence | -1.6068 | 0.4587 | -3.5032 | 0.0005 | Signifikan |
2.12 Odds Ratio
Odds ratio diperoleh dengan mengeksponensialkan koefisien:
\[ OR = e^{\beta} \]
odds_ratio <- exp(coef(model_multinom))
tabel_or <- data.frame(
Genre = rep(rownames(odds_ratio), each = ncol(odds_ratio)),
Variabel = rep(colnames(odds_ratio), times = nrow(odds_ratio)),
Odds_Ratio = as.vector(t(odds_ratio))
)
tabel_or %>%
dplyr::mutate(Odds_Ratio = round(Odds_Ratio, 4)) %>%
kable(
caption = "Odds Ratio Model Regresi Logistik Multinomial"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
)
| Genre | Variabel | Odds_Ratio |
|---|---|---|
| pop | (Intercept) | 2.987000e-01 |
| pop | danceability | 4.318623e+04 |
| pop | energy | 1.028539e+02 |
| pop | acousticness | 1.900000e-03 |
| pop | tempo | 1.005900e+00 |
| pop | valence | 8.700000e-03 |
| rock | (Intercept) | 1.343850e+01 |
| rock | danceability | 7.263310e+01 |
| rock | energy | 6.865460e+01 |
| rock | acousticness | 2.000000e-04 |
| rock | tempo | 1.002700e+00 |
| rock | valence | 8.800000e-02 |
| hip-hop | (Intercept) | 8.000000e-04 |
| hip-hop | danceability | 8.820272e+07 |
| hip-hop | energy | 1.179185e+03 |
| hip-hop | acousticness | 6.000000e-04 |
| hip-hop | tempo | 1.004600e+00 |
| hip-hop | valence | 3.600000e-03 |
| r-n-b | (Intercept) | 2.261000e-01 |
| r-n-b | danceability | 1.914733e+03 |
| r-n-b | energy | 1.765032e+02 |
| r-n-b | acousticness | 3.200000e-03 |
| r-n-b | tempo | 1.005100e+00 |
| r-n-b | valence | 2.005000e-01 |
Jika odds ratio lebih dari 1, maka peningkatan variabel prediktor meningkatkan odds suatu genre dibandingkan genre classical. Jika odds ratio kurang dari 1, maka peningkatan variabel prediktor menurunkan odds suatu genre dibandingkan genre classical.
2.13 Interpretasi Otomatis Odds Ratio
tabel_interpretasi <- tabel_or %>%
dplyr::filter(Variabel != "(Intercept)") %>%
dplyr::mutate(
Interpretasi = dplyr::case_when(
Odds_Ratio > 1 ~ paste0(
"Pada genre ", Genre, ", variabel ", Variabel,
" memiliki OR = ", round(Odds_Ratio, 4),
". Artinya, kenaikan variabel ini meningkatkan odds genre ",
Genre, " dibandingkan classical sebesar ",
round((Odds_Ratio - 1) * 100, 2),
"%, dengan asumsi variabel lain konstan."
),
Odds_Ratio < 1 ~ paste0(
"Pada genre ", Genre, ", variabel ", Variabel,
" memiliki OR = ", round(Odds_Ratio, 4),
". Artinya, kenaikan variabel ini menurunkan odds genre ",
Genre, " dibandingkan classical sebesar ",
round((1 - Odds_Ratio) * 100, 2),
"%, dengan asumsi variabel lain konstan."
),
TRUE ~ paste0(
"Pada genre ", Genre, ", variabel ", Variabel,
" memiliki OR = 1 sehingga tidak mengubah odds terhadap classical."
)
)
) %>%
dplyr::left_join(
tabel_uji %>% dplyr::select(Genre, Variabel, P_Value, Kesimpulan),
by = c("Genre", "Variabel")
)
tabel_interpretasi %>%
dplyr::mutate(
Odds_Ratio = round(Odds_Ratio, 4),
P_Value = round(P_Value, 4)
) %>%
dplyr::select(Genre, Variabel, Odds_Ratio, P_Value, Kesimpulan, Interpretasi) %>%
kable(
caption = "Interpretasi Odds Ratio Berdasarkan Genre"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
)
| Genre | Variabel | Odds_Ratio | P_Value | Kesimpulan | Interpretasi |
|---|---|---|---|---|---|
| pop | danceability | 4.318623e+04 | 0.0000 | Signifikan | Pada genre pop, variabel danceability memiliki OR = 43186.2305. Artinya, kenaikan variabel ini meningkatkan odds genre pop dibandingkan classical sebesar 4318523.05%, dengan asumsi variabel lain konstan. |
| pop | energy | 1.028539e+02 | 0.0000 | Signifikan | Pada genre pop, variabel energy memiliki OR = 102.8539. Artinya, kenaikan variabel ini meningkatkan odds genre pop dibandingkan classical sebesar 10185.39%, dengan asumsi variabel lain konstan. |
| pop | acousticness | 1.900000e-03 | 0.0000 | Signifikan | Pada genre pop, variabel acousticness memiliki OR = 0.0019. Artinya, kenaikan variabel ini menurunkan odds genre pop dibandingkan classical sebesar 99.81%, dengan asumsi variabel lain konstan. |
| pop | tempo | 1.005900e+00 | 0.0495 | Signifikan | Pada genre pop, variabel tempo memiliki OR = 1.0059. Artinya, kenaikan variabel ini meningkatkan odds genre pop dibandingkan classical sebesar 0.59%, dengan asumsi variabel lain konstan. |
| pop | valence | 8.700000e-03 | 0.0000 | Signifikan | Pada genre pop, variabel valence memiliki OR = 0.0087. Artinya, kenaikan variabel ini menurunkan odds genre pop dibandingkan classical sebesar 99.13%, dengan asumsi variabel lain konstan. |
| rock | danceability | 7.263310e+01 | 0.0000 | Signifikan | Pada genre rock, variabel danceability memiliki OR = 72.6331. Artinya, kenaikan variabel ini meningkatkan odds genre rock dibandingkan classical sebesar 7163.31%, dengan asumsi variabel lain konstan. |
| rock | energy | 6.865460e+01 | 0.0000 | Signifikan | Pada genre rock, variabel energy memiliki OR = 68.6546. Artinya, kenaikan variabel ini meningkatkan odds genre rock dibandingkan classical sebesar 6765.46%, dengan asumsi variabel lain konstan. |
| rock | acousticness | 2.000000e-04 | 0.0000 | Signifikan | Pada genre rock, variabel acousticness memiliki OR = 2e-04. Artinya, kenaikan variabel ini menurunkan odds genre rock dibandingkan classical sebesar 99.98%, dengan asumsi variabel lain konstan. |
| rock | tempo | 1.002700e+00 | 0.3666 | Tidak signifikan | Pada genre rock, variabel tempo memiliki OR = 1.0027. Artinya, kenaikan variabel ini meningkatkan odds genre rock dibandingkan classical sebesar 0.27%, dengan asumsi variabel lain konstan. |
| rock | valence | 8.800000e-02 | 0.0000 | Signifikan | Pada genre rock, variabel valence memiliki OR = 0.088. Artinya, kenaikan variabel ini menurunkan odds genre rock dibandingkan classical sebesar 91.2%, dengan asumsi variabel lain konstan. |
| hip-hop | danceability | 8.820272e+07 | 0.0000 | Signifikan | Pada genre hip-hop, variabel danceability memiliki OR = 88202716.6462. Artinya, kenaikan variabel ini meningkatkan odds genre hip-hop dibandingkan classical sebesar 8820271564.62%, dengan asumsi variabel lain konstan. |
| hip-hop | energy | 1.179185e+03 | 0.0000 | Signifikan | Pada genre hip-hop, variabel energy memiliki OR = 1179.1851. Artinya, kenaikan variabel ini meningkatkan odds genre hip-hop dibandingkan classical sebesar 117818.51%, dengan asumsi variabel lain konstan. |
| hip-hop | acousticness | 6.000000e-04 | 0.0000 | Signifikan | Pada genre hip-hop, variabel acousticness memiliki OR = 6e-04. Artinya, kenaikan variabel ini menurunkan odds genre hip-hop dibandingkan classical sebesar 99.94%, dengan asumsi variabel lain konstan. |
| hip-hop | tempo | 1.004600e+00 | 0.1617 | Tidak signifikan | Pada genre hip-hop, variabel tempo memiliki OR = 1.0046. Artinya, kenaikan variabel ini meningkatkan odds genre hip-hop dibandingkan classical sebesar 0.46%, dengan asumsi variabel lain konstan. |
| hip-hop | valence | 3.600000e-03 | 0.0000 | Signifikan | Pada genre hip-hop, variabel valence memiliki OR = 0.0036. Artinya, kenaikan variabel ini menurunkan odds genre hip-hop dibandingkan classical sebesar 99.64%, dengan asumsi variabel lain konstan. |
| r-n-b | danceability | 1.914733e+03 | 0.0000 | Signifikan | Pada genre r-n-b, variabel danceability memiliki OR = 1914.7334. Artinya, kenaikan variabel ini meningkatkan odds genre r-n-b dibandingkan classical sebesar 191373.34%, dengan asumsi variabel lain konstan. |
| r-n-b | energy | 1.765032e+02 | 0.0000 | Signifikan | Pada genre r-n-b, variabel energy memiliki OR = 176.5032. Artinya, kenaikan variabel ini meningkatkan odds genre r-n-b dibandingkan classical sebesar 17550.32%, dengan asumsi variabel lain konstan. |
| r-n-b | acousticness | 3.200000e-03 | 0.0000 | Signifikan | Pada genre r-n-b, variabel acousticness memiliki OR = 0.0032. Artinya, kenaikan variabel ini menurunkan odds genre r-n-b dibandingkan classical sebesar 99.68%, dengan asumsi variabel lain konstan. |
| r-n-b | tempo | 1.005100e+00 | 0.0852 | Tidak signifikan | Pada genre r-n-b, variabel tempo memiliki OR = 1.0051. Artinya, kenaikan variabel ini meningkatkan odds genre r-n-b dibandingkan classical sebesar 0.51%, dengan asumsi variabel lain konstan. |
| r-n-b | valence | 2.005000e-01 | 0.0005 | Signifikan | Pada genre r-n-b, variabel valence memiliki OR = 0.2005. Artinya, kenaikan variabel ini menurunkan odds genre r-n-b dibandingkan classical sebesar 79.95%, dengan asumsi variabel lain konstan. |
2.14 Uji Kelayakan Model dengan Likelihood Ratio Test
model_null <- nnet::multinom(
genre ~ 1,
data = data_latih,
trace = FALSE,
maxit = 1000
)
LR_stat <- 2 * (as.numeric(logLik(model_multinom)) - as.numeric(logLik(model_null)))
df_LR <- attr(logLik(model_multinom), "df") - attr(logLik(model_null), "df")
p_LR <- pchisq(LR_stat, df = df_LR, lower.tail = FALSE)
tibble(
Statistik_LR = LR_stat,
Derajat_Bebas = df_LR,
P_Value = p_LR
) %>%
kable(
digits = 4,
caption = "Likelihood Ratio Test Model Multinomial"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
| Statistik_LR | Derajat_Bebas | P_Value |
|---|---|---|
| 4346.545 | 20 | 0 |
Jika p-value pada Likelihood Ratio Test kurang dari 0,05, maka model dengan prediktor danceability, energy, acousticness, tempo, dan valence lebih baik dibandingkan model tanpa prediktor.
2.15 Prediksi pada Data Uji
prediksi_kelas <- predict(
model_multinom,
newdata = data_uji,
type = "class"
)
prediksi_prob <- predict(
model_multinom,
newdata = data_uji,
type = "probs"
)
head(prediksi_prob)
## classical pop rock hip-hop r-n-b
## 1 0.11289667 0.33752637 0.15873562 5.719927e-02 0.333642076
## 2 0.01392852 0.39580204 0.13714403 1.517983e-01 0.301327121
## 3 0.03091195 0.30725951 0.14541834 1.014093e-01 0.415000898
## 4 0.91336123 0.02594349 0.01500451 8.293724e-04 0.044861399
## 5 0.96560849 0.01001403 0.01662610 3.675918e-05 0.007714622
## 6 0.96493229 0.01268953 0.01264323 7.302781e-05 0.009661922
data_hasil_prediksi <- data_uji %>%
dplyr::bind_cols(as.data.frame(prediksi_prob)) %>%
dplyr::mutate(Prediksi = prediksi_kelas)
head(data_hasil_prediksi)
2.16 Confusion Matrix dan Akurasi
conf_matrix <- table(
Aktual = data_uji$genre,
Prediksi = prediksi_kelas
)
conf_matrix
## Prediksi
## Aktual classical pop rock hip-hop r-n-b
## classical 174 11 5 0 10
## pop 17 45 35 69 34
## rock 4 34 113 35 14
## hip-hop 0 22 22 134 22
## r-n-b 15 42 45 36 62
akurasi <- sum(diag(conf_matrix)) / sum(conf_matrix)
tibble(
Akurasi = akurasi
) %>%
dplyr::mutate(Akurasi = percent(Akurasi, accuracy = 0.01)) %>%
kable(
caption = "Akurasi Model Regresi Logistik Multinomial"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
| Akurasi |
|---|
| 52.80% |
Confusion matrix menunjukkan perbandingan antara genre aktual dan genre hasil prediksi. Akurasi menunjukkan proporsi lagu yang berhasil diklasifikasikan dengan benar oleh model.
2.17 Contoh Prediksi Lagu Baru
Misalkan terdapat lagu baru dengan karakteristik:
- danceability = 0.70
- energy = 0.65
- acousticness = 0.20
- tempo = 120
- valence = 0.55
lagu_baru <- tibble(
danceability = 0.70,
energy = 0.65,
acousticness = 0.20,
tempo = 120,
valence = 0.55
)
prob_lagu_baru <- predict(
model_multinom,
newdata = lagu_baru,
type = "probs"
)
prob_lagu_baru
## classical pop rock hip-hop r-n-b
## 0.0006361376 0.2900611302 0.1788474300 0.3325239966 0.1979313055
tibble(
Genre = names(prob_lagu_baru),
Probabilitas = as.numeric(prob_lagu_baru)
) %>%
dplyr::mutate(Probabilitas = percent(Probabilitas, accuracy = 0.01)) %>%
kable(
caption = "Probabilitas Prediksi Genre Lagu Baru"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
| Genre | Probabilitas |
|---|---|
| classical | 0.06% |
| pop | 29.01% |
| rock | 17.88% |
| hip-hop | 33.25% |
| r-n-b | 19.79% |
Genre dengan probabilitas terbesar merupakan genre yang paling mungkin untuk lagu baru tersebut berdasarkan karakteristik audio yang diberikan.
2.18 Visualisasi Pengaruh Danceability terhadap Probabilitas Genre
grid_dance <- tibble(
danceability = seq(
min(data_model$danceability, na.rm = TRUE),
max(data_model$danceability, na.rm = TRUE),
length.out = 200
),
energy = mean(data_model$energy, na.rm = TRUE),
acousticness = mean(data_model$acousticness, na.rm = TRUE),
tempo = mean(data_model$tempo, na.rm = TRUE),
valence = mean(data_model$valence, na.rm = TRUE)
)
prob_dance <- predict(
model_multinom,
newdata = grid_dance,
type = "probs"
)
plot_dance <- grid_dance %>%
dplyr::bind_cols(as.data.frame(prob_dance)) %>%
tidyr::pivot_longer(
cols = c("classical", "pop", "rock", "hip-hop", "r-n-b"),
names_to = "Genre",
values_to = "Probabilitas"
)
ggplot(plot_dance, aes(x = danceability, y = Probabilitas, color = Genre)) +
geom_line(linewidth = 1.25) +
scale_y_continuous(labels = percent_format(), limits = c(0, 1)) +
scale_color_manual(values = c(
"classical" = "#5568b8",
"pop" = "#d18b2f",
"rock" = "#b84f5a",
"hip-hop" = "#2f7f73",
"r-n-b" = "#7c3aed"
)) +
labs(
title = "Prediksi Probabilitas Genre Berdasarkan Danceability",
subtitle = "Variabel lain dibuat tetap pada nilai rata-rata.",
x = "Danceability",
y = "Probabilitas Prediksi",
color = "Genre"
) +
theme_profesional()
2.19 Visualisasi Pengaruh Energy terhadap Probabilitas Genre
grid_energy <- tibble(
danceability = mean(data_model$danceability, na.rm = TRUE),
energy = seq(
min(data_model$energy, na.rm = TRUE),
max(data_model$energy, na.rm = TRUE),
length.out = 200
),
acousticness = mean(data_model$acousticness, na.rm = TRUE),
tempo = mean(data_model$tempo, na.rm = TRUE),
valence = mean(data_model$valence, na.rm = TRUE)
)
prob_energy <- predict(
model_multinom,
newdata = grid_energy,
type = "probs"
)
plot_energy <- grid_energy %>%
dplyr::bind_cols(as.data.frame(prob_energy)) %>%
tidyr::pivot_longer(
cols = c("classical", "pop", "rock", "hip-hop", "r-n-b"),
names_to = "Genre",
values_to = "Probabilitas"
)
ggplot(plot_energy, aes(x = energy, y = Probabilitas, color = Genre)) +
geom_line(linewidth = 1.25) +
scale_y_continuous(labels = percent_format(), limits = c(0, 1)) +
scale_color_manual(values = c(
"classical" = "#5568b8",
"pop" = "#d18b2f",
"rock" = "#b84f5a",
"hip-hop" = "#2f7f73",
"r-n-b" = "#7c3aed"
)) +
labs(
title = "Prediksi Probabilitas Genre Berdasarkan Energy",
subtitle = "Variabel lain dibuat tetap pada nilai rata-rata.",
x = "Energy",
y = "Probabilitas Prediksi",
color = "Genre"
) +
theme_profesional()
2.20 Visualisasi Pengaruh Acousticness terhadap Probabilitas Genre
grid_acoustic <- tibble(
danceability = mean(data_model$danceability, na.rm = TRUE),
energy = mean(data_model$energy, na.rm = TRUE),
acousticness = seq(
min(data_model$acousticness, na.rm = TRUE),
max(data_model$acousticness, na.rm = TRUE),
length.out = 200
),
tempo = mean(data_model$tempo, na.rm = TRUE),
valence = mean(data_model$valence, na.rm = TRUE)
)
prob_acoustic <- predict(
model_multinom,
newdata = grid_acoustic,
type = "probs"
)
plot_acoustic <- grid_acoustic %>%
dplyr::bind_cols(as.data.frame(prob_acoustic)) %>%
tidyr::pivot_longer(
cols = c("classical", "pop", "rock", "hip-hop", "r-n-b"),
names_to = "Genre",
values_to = "Probabilitas"
)
ggplot(plot_acoustic, aes(x = acousticness, y = Probabilitas, color = Genre)) +
geom_line(linewidth = 1.25) +
scale_y_continuous(labels = percent_format(), limits = c(0, 1)) +
scale_color_manual(values = c(
"classical" = "#5568b8",
"pop" = "#d18b2f",
"rock" = "#b84f5a",
"hip-hop" = "#2f7f73",
"r-n-b" = "#7c3aed"
)) +
labs(
title = "Prediksi Probabilitas Genre Berdasarkan Acousticness",
subtitle = "Variabel lain dibuat tetap pada nilai rata-rata.",
x = "Acousticness",
y = "Probabilitas Prediksi",
color = "Genre"
) +
theme_profesional()
2.21 Kesimpulan
Berdasarkan analisis regresi logistik multinomial, genre lagu Spotify dapat dimodelkan menggunakan karakteristik audio berupa danceability, energy, acousticness, tempo, dan valence. Model ini sesuai karena variabel respon terdiri dari lima kategori nominal, yaitu pop, rock, hip-hop, classical, dan r-n-b.
Hasil model dapat digunakan untuk melihat karakteristik audio yang membedakan genre lagu serta memprediksi probabilitas sebuah lagu termasuk ke dalam masing-masing genre.
3 BAB 3. Regresi Logistik Ordinal
3.1 Pendahuluan
Analisis ini menggunakan regresi logistik ordinal
untuk memodelkan variabel respons kualitas_kopi. Variabel
respons ini bersifat bertingkat, sehingga kategori kualitas kopi tidak
diperlakukan sebagai kategori nominal biasa, tetapi sebagai kategori
yang memiliki urutan alami.
Tujuan analisis ini adalah mengetahui pengaruh Altitude_clean, Processing Method, Variety, Moisture Percentage, dan Country of Origin terhadap kecenderungan kopi berada pada kategori kualitas yang lebih tinggi.
3.2 Dasar Teori
Regresi logistik ordinal digunakan ketika variabel respons memiliki lebih dari dua kategori dan kategori tersebut memiliki urutan. Model yang digunakan adalah proportional odds model atau cumulative logit model.
Jika respons ordinal memiliki kategori:
\[ Y_1 < Y_2 < \cdots < Y_J \]
maka model cumulative logit dapat ditulis sebagai:
\[ \log\left(\frac{P(Y \leq j)}{P(Y > j)}\right) = \alpha_j - \beta_1X_1 - \beta_2X_2 - \cdots - \beta_pX_p \]
untuk:
\[ j = 1,2,\ldots,J-1 \]
Keterangan:
- \(Y\) adalah variabel respons ordinal.
- \(\alpha_j\) adalah cutpoint atau batas antar kategori.
- \(\beta\) adalah koefisien prediktor.
- \(X\) adalah variabel prediktor.
Pada fungsi MASS::polr(), koefisien positif menunjukkan
bahwa kenaikan nilai prediktor cenderung meningkatkan peluang respon
berada pada kategori ordinal yang lebih tinggi, dengan asumsi prediktor
lain konstan.
3.3 Import Data
data_raw <- readxl::read_excel("~/Salwa/SMT 4/ADK 1/tugas regresi logostik/data_kopi_ordinal.xlsx")
dim_data <- dim(data_raw)
kolom_data <- names(data_raw)
data_raw %>%
head(10) %>%
kable(caption = "Preview Data Awal") %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
)
| kualitas_kopi | Altitude_clean | Processing Method | Variety | Moisture Percentage | Country of Origin |
|---|---|---|---|---|---|
| Tinggi | 1700 | Double Anaerobic Washed | Castillo | 11.8 | Colombia |
| Tinggi | 1200 | Washed / Wet | Gesha | 10.5 | Taiwan |
| Tinggi | 1300 | Semi Washed | Java | 10.4 | Laos |
| Tinggi | 1900 | Washed / Wet | Gesha | 11.8 | Costa Rica |
| Tinggi | 1850 | Honey,Mossto | Red Bourbon | 11.6 | Colombia |
| Tinggi | 1668 | Washed / Wet | Gesha | 10.7 | Guatemala |
| Tinggi | 1250 | Washed / Wet | Gesha | 9.1 | Taiwan |
| Tinggi | 1200 | Natural / Dry | Sl34+Gesha | 10.0 | Taiwan |
| Tinggi | 1250 | Washed / Wet | SL34 | 10.8 | Taiwan |
| Tinggi | 1400 | Washed / Wet | Bourbon | 11.0 | Tanzania, United Republic Of |
cat("Jumlah baris data:", nrow(data_raw), "\n")
## Jumlah baris data: 197
cat("Jumlah kolom data:", ncol(data_raw), "\n")
## Jumlah kolom data: 6
cat("Nama kolom:\n")
## Nama kolom:
print(names(data_raw))
## [1] "kualitas_kopi" "Altitude_clean" "Processing Method"
## [4] "Variety" "Moisture Percentage" "Country of Origin"
3.4 Validasi Data
Sebelum model dijalankan, data perlu dicek terlebih dahulu. Regresi
logistik ordinal dengan MASS::polr() membutuhkan:
- data memiliki baris observasi,
- variabel respons tersedia,
- variabel prediktor tersedia,
- respons memiliki minimal 3 kategori setelah data kosong dibuang,
- prediktor numerik tidak bernilai
NA,NaN, atauInf.
required_cols <- c(
"kualitas_kopi",
"Altitude_clean",
"Processing Method",
"Variety",
"Moisture Percentage",
"Country of Origin"
)
missing_cols <- setdiff(required_cols, names(data_raw))
validasi <- tibble::tibble(
Pemeriksaan = c(
"Jumlah baris lebih dari 0",
"Semua kolom yang dibutuhkan tersedia",
"Respons kualitas_kopi tersedia",
"Respons memiliki minimal 3 kategori"
),
Status = c(
nrow(data_raw) > 0,
length(missing_cols) == 0,
"kualitas_kopi" %in% names(data_raw),
ifelse("kualitas_kopi" %in% names(data_raw),
dplyr::n_distinct(data_raw$kualitas_kopi, na.rm = TRUE) >= 3,
FALSE)
)
)
validasi %>%
dplyr::mutate(Status = ifelse(Status, "Memenuhi", "Belum memenuhi")) %>%
kable(caption = "Validasi Awal Data") %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
| Pemeriksaan | Status |
|---|---|
| Jumlah baris lebih dari 0 | Memenuhi |
| Semua kolom yang dibutuhkan tersedia | Memenuhi |
| Respons kualitas_kopi tersedia | Memenuhi |
| Respons memiliki minimal 3 kategori | Memenuhi |
if (length(missing_cols) > 0) {
cat("Kolom yang belum ada dalam file Excel:\n")
print(missing_cols)
}
kategori_respons <- if ("kualitas_kopi" %in% names(data_raw)) {
dplyr::n_distinct(data_raw$kualitas_kopi, na.rm = TRUE)
} else {
0
}
bisa_model <- nrow(data_raw) > 0 && length(missing_cols) == 0 && kategori_respons >= 3
if (!bisa_model) {
cat('<div class="danger-box">
')
cat('<b>Model belum bisa dijalankan.</b><br>')
cat('File Excel yang terbaca belum memenuhi syarat untuk regresi logistik ordinal. ')
cat('Pastikan file Excel berisi data observasi lengkap, bukan hanya header kolom. ')
cat('Minimal harus ada 3 kategori pada variabel <code>kualitas_kopi</code>.')
cat('</div>')
}
3.5 Persiapan Data
if (bisa_model) {
data_kopi <- data_raw %>%
dplyr::select(
kualitas_kopi,
Altitude_clean,
`Processing Method`,
Variety,
`Moisture Percentage`,
`Country of Origin`
) %>%
dplyr::mutate(
kualitas_kopi = as.character(kualitas_kopi),
kualitas_kopi = stringr::str_squish(kualitas_kopi),
kualitas_kopi = factor(kualitas_kopi, ordered = TRUE),
Altitude_clean = as.numeric(Altitude_clean),
`Moisture Percentage` = as.numeric(`Moisture Percentage`),
`Processing Method` = as.factor(`Processing Method`),
Variety = as.factor(Variety),
`Country of Origin` = as.factor(`Country of Origin`)
) %>%
dplyr::filter(
!is.na(kualitas_kopi),
!is.na(Altitude_clean),
!is.na(`Moisture Percentage`),
!is.na(`Processing Method`),
!is.na(Variety),
!is.na(`Country of Origin`)
)
data_kopi <- data_kopi %>%
dplyr::mutate(
`Processing Method` = forcats::fct_lump_min(`Processing Method`, min = 5, other_level = "Other"),
Variety = forcats::fct_lump_min(Variety, min = 5, other_level = "Other"),
`Country of Origin` = forcats::fct_lump_min(`Country of Origin`, min = 5, other_level = "Other"),
Altitude_z = as.numeric(scale(Altitude_clean)),
Moisture_z = as.numeric(scale(`Moisture Percentage`))
) %>%
dplyr::filter(is.finite(Altitude_z), is.finite(Moisture_z))
data_kopi$kualitas_kopi <- droplevels(data_kopi$kualitas_kopi)
data_kopi %>%
glimpse()
}
## Rows: 197
## Columns: 8
## $ kualitas_kopi <ord> Tinggi, Tinggi, Tinggi, Tinggi, Tinggi, Tinggi, …
## $ Altitude_clean <dbl> 1700, 1200, 1300, 1900, 1850, 1668, 1250, 1200, …
## $ `Processing Method` <fct> Other, Washed / Wet, Other, Washed / Wet, Other,…
## $ Variety <fct> Other, Gesha, Other, Gesha, Other, Gesha, Gesha,…
## $ `Moisture Percentage` <dbl> 11.8, 10.5, 10.4, 11.8, 11.6, 10.7, 9.1, 10.0, 1…
## $ `Country of Origin` <fct> "Colombia", "Taiwan", "Other", "Costa Rica", "Co…
## $ Altitude_z <dbl> 0.56444850, -0.12640089, 0.01176899, 0.84078826,…
## $ Moisture_z <dbl> 0.8752429267, -0.1581812943, -0.2376754651, 0.87…
Variabel numerik Altitude_clean dan Moisture
Percentage distandarisasi menjadi Altitude_z dan
Moisture_z. Tujuannya adalah membuat proses estimasi lebih
stabil, terutama jika skala antar variabel sangat berbeda.
3.6 Distribusi Respons Ordinal
data_kopi %>%
dplyr::count(kualitas_kopi) %>%
dplyr::mutate(proporsi = n / sum(n)) %>%
kable(
digits = 3,
caption = "Distribusi Kategori Kualitas Kopi"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = FALSE
)
| kualitas_kopi | n | proporsi |
|---|---|---|
| Rendah | 3 | 0.015 |
| Sedang | 149 | 0.756 |
| Tinggi | 45 | 0.228 |
data_kopi %>%
dplyr::count(kualitas_kopi) %>%
dplyr::mutate(proporsi = n / sum(n)) %>%
ggplot(aes(x = kualitas_kopi, y = proporsi, fill = kualitas_kopi)) +
geom_col(width = 0.65, alpha = 0.92) +
geom_text(aes(label = scales::percent(proporsi, accuracy = 0.1)),
vjust = -0.35, fontface = "bold") +
scale_y_continuous(labels = scales::percent_format(), limits = c(0, NA)) +
labs(
title = "Distribusi Kualitas Kopi",
subtitle = "Respons ordinal: kategori kualitas memiliki urutan alami.",
x = "Kualitas Kopi",
y = "Proporsi"
) +
theme_minimal(base_size = 13) +
theme(legend.position = "none")
Distribusi respons digunakan untuk melihat apakah kategori kualitas kopi cukup terwakili. Jika salah satu kategori memiliki jumlah data sangat sedikit, model ordinal dapat menjadi tidak stabil.
3.7 Estimasi Model Regresi Logistik Ordinal
fit_sukses <- FALSE
if (bisa_model) {
n_level_model <- nlevels(data_kopi$kualitas_kopi)
cat("Jumlah level respons setelah cleaning:", n_level_model, "\n")
bisa_fit <- nrow(data_kopi) > 0 && n_level_model >= 3
} else {
bisa_fit <- FALSE
}
## Jumlah level respons setelah cleaning: 3
## Model utama dengan semua prediktor sesuai permintaan.
## tryCatch dipakai supaya proses knit tidak berhenti jika polr gagal optimasi.
fit_formula_full <- kualitas_kopi ~ Altitude_z + `Processing Method` + Variety + Moisture_z + `Country of Origin`
fit_ord <- tryCatch(
MASS::polr(
formula = fit_formula_full,
data = data_kopi,
method = "logistic",
Hess = TRUE,
control = list(maxit = 1000)
),
error = function(e) e
)
fit_sukses <- inherits(fit_ord, "polr")
if (fit_sukses) {
summary(fit_ord)
} else {
cat("Model utama gagal dijalankan. Pesan error dari polr:\n")
cat(conditionMessage(fit_ord), "\n")
}
## Model utama gagal dijalankan. Pesan error dari polr:
## initial value in 'vmmin' is not finite
## Jika model penuh gagal, jalankan model cadangan yang lebih stabil
## dengan prediktor numerik saja. Ini menjaga dokumen tetap bisa di-knit.
if (!exists("fit_sukses")) fit_sukses <- FALSE
if (!fit_sukses) {
fit_formula_simple <- kualitas_kopi ~ Altitude_z + Moisture_z
fit_ord_simple <- tryCatch(
MASS::polr(
formula = fit_formula_simple,
data = data_kopi,
method = "logistic",
Hess = TRUE,
control = list(maxit = 1000)
),
error = function(e) e
)
fit_simple_sukses <- inherits(fit_ord_simple, "polr")
if (fit_simple_sukses) {
fit_ord <- fit_ord_simple
fit_sukses <- TRUE
model_yang_dipakai <- "Model cadangan: kualitas_kopi ~ Altitude_z + Moisture_z"
summary(fit_ord)
} else {
model_yang_dipakai <- "Tidak ada model yang berhasil diestimasi"
cat("Model cadangan juga gagal dijalankan. Pesan error:\n")
cat(conditionMessage(fit_ord_simple), "\n")
}
} else {
model_yang_dipakai <- "Model utama: semua prediktor"
}
## Call:
## MASS::polr(formula = fit_formula_simple, data = data_kopi, control = list(maxit = 1000),
## Hess = TRUE, method = "logistic")
##
## Coefficients:
## Value Std. Error t value
## Altitude_z 0.1829 0.1564 1.1697
## Moisture_z -0.1323 0.1587 -0.8335
##
## Intercepts:
## Value Std. Error t value
## Rendah|Sedang -4.1884 0.5823 -7.1924
## Sedang|Tinggi 1.2289 0.1712 7.1788
##
## Residual Deviance: 239.4656
## AIC: 247.4656
if (exists("bisa_fit") && !bisa_fit) {
cat('<div class="danger-box">
')
cat('<b>Model tidak dijalankan.</b><br>')
cat('Setelah proses cleaning, variabel <code>kualitas_kopi</code> memiliki kurang dari 3 kategori atau data observasi tidak tersedia. ')
cat('Regresi logistik ordinal dengan <code>MASS::polr()</code> membutuhkan minimal 3 level respons.')
cat('</div>')
}
if (exists("bisa_fit") && bisa_fit && exists("fit_sukses") && !fit_sukses) {
cat('<div class="danger-box">
')
cat('<b>Model gagal diestimasi walaupun data lolos validasi awal.</b><br>')
cat('Penyebab paling umum adalah kategori respons atau prediktor yang terlalu jarang, separation, atau kombinasi kategori yang terlalu banyak. ')
cat('Solusi analisis: gabungkan kategori yang sangat jarang, kurangi jumlah level pada prediktor kategorik, atau gunakan model ordinal alternatif.')
cat('</div>')
}
if (exists("fit_sukses") && fit_sukses) {
cat('<div class="interpretasi">
')
cat('<b>Model berhasil diestimasi.</b><br>')
cat('Model yang dipakai: ', model_yang_dipakai, '.')
cat('</div>')
}
Model yang dipakai: Model cadangan: kualitas_kopi ~ Altitude_z + Moisture_z .
3.8 Ringkasan Koefisien dan Odds Ratio
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`,
t_value = `t value`
) %>%
dplyr::mutate(
p_value = 2 * (1 - pnorm(abs(t_value))),
jenis = ifelse(grepl("\\|", parameter), "Cutpoint", "Koefisien"),
OR = ifelse(jenis == "Koefisien", exp(estimate), NA_real_),
CI_low = ifelse(jenis == "Koefisien", exp(estimate - 1.96 * std_error), NA_real_),
CI_high = ifelse(jenis == "Koefisien", exp(estimate + 1.96 * std_error), NA_real_)
)
result_ord %>%
dplyr::mutate(
dplyr::across(c(estimate, std_error, t_value, p_value, OR, CI_low, CI_high), ~ round(.x, 4))
) %>%
kable(
caption = "Ringkasan Hasil Regresi Logistik Ordinal",
col.names = c("Parameter", "Estimate", "SE", "z/t-value", "p-value", "Jenis", "Odds Ratio", "CI 2.5%", "CI 97.5%")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
)
| Parameter | Estimate | SE | z/t-value | p-value | Jenis | Odds Ratio | CI 2.5% | CI 97.5% |
|---|---|---|---|---|---|---|---|---|
| Altitude_z | 0.1829 | 0.1564 | 1.1697 | 0.2421 | Koefisien | 1.2007 | 0.8838 | 1.6313 |
| Moisture_z | -0.1323 | 0.1587 | -0.8335 | 0.4046 | Koefisien | 0.8761 | 0.6419 | 1.1958 |
| Rendah|Sedang | -4.1884 | 0.5823 | -7.1924 | 0.0000 | Cutpoint | NA | NA | NA |
| Sedang|Tinggi | 1.2289 | 0.1712 | 7.1788 | 0.0000 | Cutpoint | NA | NA | NA |
Interpretasi umum: jika koefisien bernilai positif, maka kenaikan prediktor tersebut cenderung meningkatkan peluang kopi berada pada kategori kualitas yang lebih tinggi. Jika koefisien bernilai negatif, maka kenaikan prediktor tersebut cenderung menurunkan peluang kopi berada pada kategori kualitas yang lebih tinggi.
3.9 Interpretasi Otomatis Variabel Signifikan
interpretasi <- result_ord %>%
dplyr::filter(jenis == "Koefisien") %>%
dplyr::mutate(
keputusan = ifelse(p_value < 0.05, "signifikan", "tidak signifikan"),
arah = ifelse(estimate > 0, "meningkatkan", "menurunkan"),
perubahan_odds = round((OR - 1) * 100, 2)
)
cat('<div class="interpretasi">
')
cat('<b>Interpretasi:</b><br>')
Interpretasi:
for (i in seq_len(nrow(interpretasi))) {
cat('- Variabel <b>', interpretasi$parameter[i], '</b> ', interpretasi$keputusan[i],
' pada taraf 5%. Nilai koefisiennya ', round(interpretasi$estimate[i], 4),
', sehingga variabel ini cenderung ', interpretasi$arah[i],
' peluang kopi berada pada kategori kualitas yang lebih tinggi. ', sep = '')
cat('Odds ratio = ', round(interpretasi$OR[i], 4), '.', '<br>', sep = '')
}
- Variabel Altitude_z tidak signifikan pada taraf 5%. Nilai
koefisiennya 0.1829, sehingga variabel ini cenderung meningkatkan
peluang kopi berada pada kategori kualitas yang lebih tinggi. Odds ratio
= 1.2007.
- Variabel Moisture_z tidak signifikan pada taraf 5%. Nilai koefisiennya -0.1323, sehingga variabel ini cenderung menurunkan peluang kopi berada pada kategori kualitas yang lebih tinggi. Odds ratio = 0.8761.
cat('</div>')
3.10 Prediksi Probabilitas
pred_prob_ord <- predict(fit_ord, type = "probs")
head(pred_prob_ord) %>%
as.data.frame() %>%
kable(digits = 4, caption = "Contoh Prediksi Probabilitas Tiap Kategori") %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
)
| Rendah | Sedang | Tinggi |
|---|---|---|
| 0.0151 | 0.7607 | 0.2242 |
| 0.0150 | 0.7590 | 0.2260 |
| 0.0145 | 0.7532 | 0.2323 |
| 0.0144 | 0.7525 | 0.2331 |
| 0.0143 | 0.7511 | 0.2346 |
| 0.0136 | 0.7429 | 0.2434 |
data_kopi_pred <- data_kopi %>%
dplyr::bind_cols(as.data.frame(pred_prob_ord)) %>%
dplyr::mutate(prediksi = predict(fit_ord, type = "class"))
head(data_kopi_pred, 10) %>%
kable(caption = "Data dengan Hasil Prediksi") %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE
)
| kualitas_kopi | Altitude_clean | Processing Method | Variety | Moisture Percentage | Country of Origin | Altitude_z | Moisture_z | Rendah | Sedang | Tinggi | prediksi |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Tinggi | 1700 | Other | Other | 11.8 | Colombia | 0.5644485 | 0.8752429 | 0.0151295 | 0.7606878 | 0.2241827 | Sedang |
| Tinggi | 1200 | Washed / Wet | Gesha | 10.5 | Taiwan | -0.1264009 | -0.1581813 | 0.0149762 | 0.7590376 | 0.2259862 | Sedang |
| Tinggi | 1300 | Other | Other | 10.4 | Other | 0.0117690 | -0.2376755 | 0.0144573 | 0.7532355 | 0.2323072 | Sedang |
| Tinggi | 1900 | Washed / Wet | Gesha | 11.8 | Costa Rica | 0.8407883 | 0.8752429 | 0.0143945 | 0.7525096 | 0.2330958 | Sedang |
| Tinggi | 1850 | Other | Other | 11.6 | Colombia | 0.7717033 | 0.7162546 | 0.0142759 | 0.7511243 | 0.2345998 | Sedang |
| Tinggi | 1668 | Washed / Wet | Gesha | 10.7 | Guatemala | 0.5202341 | 0.0008070 | 0.0136073 | 0.7429460 | 0.2434467 | Sedang |
| Tinggi | 1250 | Washed / Wet | Gesha | 9.1 | Taiwan | -0.0573160 | -1.2710997 | 0.0127921 | 0.7320457 | 0.2551622 | Sedang |
| Tinggi | 1200 | Natural / Dry | Other | 10.0 | Taiwan | -0.1264009 | -0.5556521 | 0.0142200 | 0.7504651 | 0.2353149 | Sedang |
| Tinggi | 1250 | Washed / Wet | SL34 | 10.8 | Taiwan | -0.0573160 | 0.0803012 | 0.0152577 | 0.7620466 | 0.2226957 | Sedang |
| Tinggi | 1400 | Washed / Wet | Bourbon | 11.0 | Tanzania, United Republic Of | 0.1499389 | 0.2392896 | 0.0150062 | 0.7593630 | 0.2256308 | Sedang |
3.11 Confusion Matrix dan Akurasi
conf_ord <- table(
Aktual = data_kopi_pred$kualitas_kopi,
Prediksi = data_kopi_pred$prediksi
)
conf_ord
## Prediksi
## Aktual Rendah Sedang Tinggi
## Rendah 0 3 0
## Sedang 0 148 1
## Tinggi 0 45 0
accuracy_ord <- sum(diag(conf_ord)) / sum(conf_ord)
accuracy_ord
## [1] 0.751269
Akurasi pada respons ordinal perlu dibaca hati-hati. Kesalahan prediksi antar kategori yang berdekatan lebih ringan dibanding kesalahan prediksi yang lompat jauh. Karena itu, confusion matrix hanya menjadi evaluasi awal, bukan satu-satunya ukuran kualitas model.
3.12 Visualisasi Prediksi Probabilitas
if ("Altitude_z" %in% names(data_kopi)) {
grid_ord <- data.frame(
Altitude_z = seq(min(data_kopi$Altitude_z), max(data_kopi$Altitude_z), length.out = 120),
Moisture_z = mean(data_kopi$Moisture_z),
`Processing Method` = names(sort(table(data_kopi$`Processing Method`), decreasing = TRUE))[1],
Variety = names(sort(table(data_kopi$Variety), decreasing = TRUE))[1],
`Country of Origin` = names(sort(table(data_kopi$`Country of Origin`), decreasing = TRUE))[1],
check.names = FALSE
)
grid_prob_ord <- predict(fit_ord, newdata = grid_ord, type = "probs")
grid_ord_plot <- grid_ord %>%
dplyr::bind_cols(as.data.frame(grid_prob_ord)) %>%
pivot_longer(
cols = dplyr::all_of(levels(data_kopi$kualitas_kopi)),
names_to = "kualitas_kopi",
values_to = "probabilitas"
)
ggplot(grid_ord_plot, aes(x = Altitude_z, y = probabilitas, color = kualitas_kopi)) +
geom_line(linewidth = 1.2) +
scale_y_continuous(labels = scales::percent_format()) +
labs(
title = "Prediksi Probabilitas Kualitas Kopi Berdasarkan Altitude",
subtitle = "Variabel lain ditahan pada nilai rata-rata atau kategori terbanyak.",
x = "Altitude_clean terstandarisasi",
y = "Probabilitas prediksi",
color = "Kualitas Kopi"
) +
theme_minimal(base_size = 13)
}
Grafik probabilitas menunjukkan perubahan peluang setiap kategori kualitas kopi ketika altitude berubah. Pada model ordinal, yang diasumsikan sejajar adalah cumulative logit, bukan kurva probabilitas kategori tunggal.
3.13 Pemeriksaan Asumsi Proportional Odds
Asumsi utama regresi logistik ordinal adalah proportional odds atau parallel lines assumption. Artinya, pengaruh prediktor dianggap sama pada setiap batas kategori kumulatif.
Secara matematis:
\[ \log\left(\frac{P(Y \leq j)}{P(Y > j)}\right) = \alpha_j - \mathbf{x}^T\boldsymbol{\beta} \]
Slope \(\beta\) sama untuk semua batas \(j\), sedangkan cutpoint \(\alpha_j\) berbeda antar batas kategori.
if (requireNamespace("ordinal", quietly = TRUE)) {
library(ordinal)
fit_clm <- ordinal::clm(
kualitas_kopi ~ Altitude_z + `Processing Method` + Variety + Moisture_z + `Country of Origin`,
data = data_kopi,
link = "logit"
)
ordinal::nominal_test(fit_clm)
} else {
cat("Paket 'ordinal' belum terpasang. Bagian nominal_test dilewati agar proses knit tetap berjalan.")
}
Jika hasil nominal_test() memiliki p-value kecil pada
suatu variabel, hal tersebut dapat menjadi indikasi bahwa asumsi
proportional odds untuk variabel tersebut kurang terpenuhi. Namun
keputusan akhir tetap perlu mempertimbangkan teori, ukuran sampel, dan
tujuan analisis.
3.14 Kesimpulan
if (exists("fit_sukses") && fit_sukses) {
cat('<div class="interpretasi">
')
cat('Berdasarkan model regresi logistik ordinal, variabel Altitude_clean, Processing Method, Variety, Moisture Percentage, dan Country of Origin dapat digunakan untuk menjelaskan kecenderungan kategori kualitas kopi. Variabel dengan p-value kurang dari 0,05 dapat dianggap memiliki pengaruh yang signifikan terhadap peluang kopi berada pada kategori kualitas yang lebih tinggi.')
cat('</div>')
} else {
cat('<div class="danger-box">
')
cat('Kesimpulan model belum dapat dibuat karena data pada file Excel belum memenuhi syarat analisis. File yang digunakan harus memuat observasi lengkap dan variabel kualitas_kopi harus memiliki minimal 3 kategori.')
cat('</div>')
}