Sektor transportasi merupakan salah satu sektor strategis yang menopang aktivitas perekonomian nasional. Perusahaan-perusahaan di sektor ini berperan penting dalam distribusi barang, jasa, dan mobilitas masyarakat. Namun, sektor ini juga rentan terhadap tekanan keuangan akibat tingginya kebutuhan modal, fluktuasi harga bahan bakar, dan volatilitas nilai tukar.
Financial distress β kondisi penurunan kinerja keuangan yang dapat berujung kebangkrutan β merupakan ancaman nyata yang perlu dideteksi secara dini. Platt & Platt (2002) mendefinisikannya sebagai proses akhir dari penurunan kinerja sebelum perusahaan mengalami kebangkrutan.
required_packages <- c("dplyr", "ggplot2", "broom", "knitr", "kableExtra",
"scales", "readr", "tidyr", "patchwork")
missing_packages <- required_packages[
!vapply(required_packages, requireNamespace, logical(1), quietly = TRUE)
]
if (length(missing_packages) > 0) {
stop(
"Paket berikut belum tersedia: ",
paste(missing_packages, collapse = ", "),
". Silakan install terlebih dahulu."
)
}
invisible(lapply(required_packages, library, character.only = TRUE))
cat("β
Semua paket berhasil dimuat:", paste(required_packages, collapse = ", "))β
Semua paket berhasil dimuat: dplyr, ggplot2, broom, knitr, kableExtra, scales, readr, tidyr, patchwork
Kode di atas memuat 9 paket yang diperlukan selama analisis.
Pemeriksaan awal (requireNamespace) memastikan tidak ada
paket yang hilang sebelum analisis dimulai β jika ada paket yang belum
terinstall, proses akan berhenti dengan pesan informatif.
raw_data <- readr::read_csv("data projek adk bismillah-3.csv",
show_col_types = FALSE)
# Periksa dimensi awal
ringkasan_data <- data.frame(
Keterangan = c("Jumlah observasi", "Jumlah variabel"),
Nilai = c(nrow(raw_data), ncol(raw_data)-5)
)
knitr::kable(
ringkasan_data,
caption = "Ukuran dataset Financial Distress"
)| Keterangan | Nilai |
|---|---|
| Jumlah observasi | 120 |
| Jumlah variabel | 5 |
Data berhasil dimuat dari file CSV. Dataset terdiri dari 120 observasi (panel data: 20 perusahaan Γ 6 tahun, 2019β2024) dengan sejumlah variabel yang mencakup rasio keuangan dan status financial distress perusahaan sektor transportasi di BEI.
kamus_variabel <- data.frame(
`Kode` = c("Xβ", "Xβ", "Xβ", "Xβ", "Xβ
", "Y"),
`Variabel` = c("Current Ratio", "Debt Ratio", "Return on Assets (ROA)",
"Sales Growth", "Firm Size", "Financial Distress"),
`Definisi` = c(
"Aset Lancar / Kewajiban Lancar β mengukur kemampuan memenuhi kewajiban jangka pendek",
"Total Utang / Total Aset β mengukur proporsi aset yang dibiayai utang",
"Laba Bersih / Total Aset β mengukur efisiensi menghasilkan laba dari aset",
"(Penjualan_t - Penjualan_{t-1}) / Penjualan_{t-1} β pertumbuhan penjualan tahunan",
"ln(Total Aset) β proksi ukuran perusahaan, mereduksi skewness",
"0 = Non-distress, 1 = Distress (berdasarkan nilai EPS negatif)"
),
`Tipe` = c("Numerik","Numerik","Numerik","Numerik","Numerik","Respon Biner"),
check.names = FALSE
)
knitr::kable(kamus_variabel,
caption = "Kamus variabel dataset Financial Distress") |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover")) |>
kableExtra::column_spec(1, bold = TRUE, width = "3em") |>
kableExtra::column_spec(3, width = "35em")| Kode | Variabel | Definisi | Tipe |
|---|---|---|---|
| Xβ | Current Ratio | Aset Lancar / Kewajiban Lancar β mengukur kemampuan memenuhi kewajiban jangka pendek | Numerik |
| Xβ | Debt Ratio | Total Utang / Total Aset β mengukur proporsi aset yang dibiayai utang | Numerik |
| Xβ | Return on Assets (ROA) | Laba Bersih / Total Aset β mengukur efisiensi menghasilkan laba dari aset | Numerik |
| Xβ | Sales Growth | (Penjualan_t - Penjualan_{t-1}) / Penjualan_{t-1} β pertumbuhan penjualan tahunan | Numerik |
| Xβ | Firm Size | ln(Total Aset) β proksi ukuran perusahaan, mereduksi skewness | Numerik |
| Y | Financial Distress | 0 = Non-distress, 1 = Distress (berdasarkan nilai EPS negatif) | Respon Biner |
Model menggunakan 5 variabel prediktor numerik (XββXβ ) dan 1 variabel respon biner (Y). Pemilihan variabel didasarkan pada kajian literatur terkait indikator keuangan yang relevan untuk mendeteksi financial distress, mencakup aspek likuiditas (CR), leverage (DR), profitabilitas (ROA), pertumbuhan (SG), dan ukuran perusahaan (FS).
fd <- raw_data %>%
transmute(
kode_saham = `Kode Saham`,
nama_perusahaan = `Nama Perusahaan`,
tahun = Tahun,
current_ratio = `Current Ratio\n(X1)`,
debt_ratio = `Debt Ratio\n(X2)`,
roa = `ROA\n(X3)`,
sales_growth = `Sales Growth\n(X4)`,
firm_size = `Firm Size\n(X5)\n=LN(Total Aset)`,
financial_distress = factor(
`Financial Distress\n(Y)\n0=Non | 1=Distress`,
levels = c(0, 1),
labels = c("Non-distress", "Distress")
),
distress_int = as.integer(`Financial Distress\n(Y)\n0=Non | 1=Distress`)
) %>%
na.omit()
cat("β
Data berhasil dibersihkan:", nrow(fd), "observasi dari",
n_distinct(fd$kode_saham), "perusahaan.\n")β
Data berhasil dibersihkan: 120 observasi dari 20 perusahaan.
Observasi dihapus (NA): 0
Tahap ini melakukan rename kolom dari nama panjang dengan
karakter khusus menjadi nama bersih yang R-friendly. Variabel respon
diubah menjadi factor berlabel (βNon-distressβ /
βDistressβ) untuk analisis, sekaligus tetap disimpan sebagai integer
(0/1) untuk keperluan pemodelan. Observasi dengan nilai hilang (NA)
dihapus menggunakan na.omit().
fd %>%
transmute(
`Kode Saham` = kode_saham,
Tahun = tahun,
`Current Ratio` = round(current_ratio, 3),
`Debt Ratio` = round(debt_ratio, 3),
ROA = round(roa, 3),
`Sales Growth` = round(sales_growth, 3),
`Firm Size` = round(firm_size, 2),
Status = financial_distress
) %>%
head(10) %>%
knitr::kable(caption = "Sepuluh baris pertama data setelah pembersihan") |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover")) |>
kableExtra::row_spec(which(fd$financial_distress[1:10] == "Distress"),
background = "#fff0ee")| Kode Saham | Tahun | Current Ratio | Debt Ratio | ROA | Sales Growth | Firm Size | Status |
|---|---|---|---|---|---|---|---|
| CMPP | 2019 | 0.471 | 0.923 | -0.060 | 0.585 | 7.87 | Distress |
| CMPP | 2020 | 0.035 | 1.479 | -0.453 | -0.760 | 8.71 | Distress |
| CMPP | 2021 | 0.025 | 2.011 | -0.454 | -0.611 | 8.55 | Distress |
| CMPP | 2022 | 0.038 | 2.272 | -0.307 | 5.040 | 8.59 | Distress |
| CMPP | 2023 | 0.039 | 1.445 | -0.177 | 0.752 | 8.72 | Distress |
| CMPP | 2024 | 0.047 | 1.641 | -0.267 | 0.199 | 8.65 | Distress |
| GIAA | 2019 | 0.348 | 0.838 | 0.001 | 0.038 | 11.03 | Non-distress |
| GIAA | 2020 | 0.125 | 1.180 | -0.237 | -0.664 | 11.93 | Non-distress |
| GIAA | 2021 | 0.053 | 1.849 | -0.582 | -0.121 | 11.54 | Distress |
| GIAA | 2022 | 0.477 | 1.246 | 0.572 | 0.633 | 11.48 | Non-distress |
Baris yang diarsir merah muda menunjukkan perusahaan berstatus Distress. Dari cuplikan ini terlihat variasi yang cukup lebar pada rasio keuangan antar perusahaan β perbedaan yang diharapkan mampu ditangkap oleh model regresi logistik.
vars_num <- c("current_ratio","debt_ratio","roa","sales_growth","firm_size")
label_vars <- c("Current Ratio","Debt Ratio","ROA","Sales Growth","Firm Size")
desc_list <- lapply(seq_along(vars_num), function(i) {
v <- vars_num[i]
nd <- fd[[v]][fd$financial_distress == "Non-distress"]
ds <- fd[[v]][fd$financial_distress == "Distress"]
data.frame(
Variabel = label_vars[i],
`Non-distress Mean` = round(mean(nd), 3),
`Non-distress Median` = round(median(nd),3),
`Non-distress SD` = round(sd(nd), 3),
`Distress Mean` = round(mean(ds), 3),
`Distress Median` = round(median(ds),3),
`Distress SD` = round(sd(ds), 3),
check.names = FALSE
)
})
desc_tidy <- do.call(rbind, desc_list)
knitr::kable(desc_tidy,
caption = "Statistik deskriptif berdasarkan status Financial Distress") |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover")) |>
kableExtra::add_header_above(c(" " = 1,
"Non-distress (n=91)" = 3,
"Distress (n=29)" = 3))| Variabel | Non-distress Mean | Non-distress Median | Non-distress SD | Distress Mean | Distress Median | Distress SD |
|---|---|---|---|---|---|---|
| Current Ratio | 1.730 | 1.256 | 1.875 | 2.697 | 0.580 | 10.146 |
| Debt Ratio | 0.557 | 0.525 | 0.307 | 0.860 | 0.577 | 0.794 |
| ROA | 0.072 | 0.021 | 0.242 | -0.153 | -0.060 | 0.170 |
| Sales Growth | 0.127 | 0.082 | 0.352 | 0.134 | -0.025 | 1.016 |
| Firm Size | 7.080 | 6.342 | 2.060 | 6.606 | 5.714 | 1.853 |
Perbandingan rata-rata kedua kelompok mengungkap pola yang bermakna:
| Variabel | Non-distress | Distress | Keterangan |
|---|---|---|---|
| ROA | +0,072 | β0,153 | β οΈ Perbedaan paling mencolok β ROA negatif = rugi |
| Debt Ratio | 0,557 | 0,860 | Distress lebih bergantung pada utang |
| Firm Size | 7,080 | 6,606 | Distress cenderung perusahaan lebih kecil |
| Sales Growth | 0,134 | 0,127 | Hampir sama β bukan pembeda utama |
| Current Ratio | Median 1,256 | Median 0,580 | Median jauh berbeda, namun rata-rata distors oleh outlier |
ROA menonjol sebagai pembeda terkuat antar kelompok, mengindikasikan bahwa profitabilitas adalah sinyal utama financial distress.
class_summary <- fd %>%
count(financial_distress, name = "Jumlah") %>%
mutate(Proporsi = scales::percent(Jumlah / sum(Jumlah), accuracy = 0.1)) %>%
rename(`Status Financial Distress` = financial_distress)
knitr::kable(class_summary,
caption = "Distribusi kelas respon Financial Distress",
align = c("l","c","c")) |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE)| Status Financial Distress | Jumlah | Proporsi |
|---|---|---|
| Non-distress | 91 | 75.8% |
| Distress | 29 | 24.2% |
Dari 120 observasi, 75,8% adalah Non-distress dan hanya 24,2% Distress. Ketidakseimbangan ini perlu diperhatikan dalam evaluasi model β akurasi saja tidak cukup sebagai metrik tunggal. Model yang selalu memprediksi βNon-distressβ pun bisa mencapai akurasi 75,8%, sehingga metrik seperti sensitivity, F1-score, dan balanced accuracy menjadi lebih relevan.
ggplot(fd, aes(x = financial_distress, fill = financial_distress)) +
geom_bar(width = 0.5, color = "white", linewidth = 0.8) +
geom_text(stat = "count",
aes(label = paste0(after_stat(count), "\n(",
scales::percent(after_stat(count)/nrow(fd), accuracy = 0.1),")")),
vjust = -0.3, fontface = "bold", size = 4.5) +
scale_fill_manual(values = c("Non-distress" = "#2a9d8f", "Distress" = "#e76f51")) +
scale_y_continuous(expand = expansion(mult = c(0, 0.18))) +
labs(title = "Distribusi Status Financial Distress",
subtitle = "20 perusahaan transportasi BEI, 2019β2024 (n = 120)",
x = NULL, y = "Jumlah observasi") +
theme_minimal(base_size = 13) +
theme(legend.position = "none",
plot.title = element_text(face = "bold", size = 14))p1 <- ggplot(fd, aes(x = roa, y = debt_ratio, color = financial_distress)) +
geom_point(alpha = 0.75, size = 2.5) +
scale_color_manual(values = c("Non-distress" = "#2a9d8f", "Distress" = "#e76f51")) +
labs(title = "ROA vs. Debt Ratio", x = "ROA (Xβ)", y = "Debt Ratio (Xβ)",
color = "Status") +
theme_minimal(base_size = 11) +
theme(legend.position = "none", plot.title = element_text(face = "bold", size = 11))
p2 <- ggplot(fd, aes(x = financial_distress, y = roa, fill = financial_distress)) +
geom_boxplot(alpha = 0.7, outlier.shape = 21, outlier.size = 2.5) +
scale_fill_manual(values = c("Non-distress" = "#2a9d8f", "Distress" = "#e76f51")) +
labs(title = "Distribusi ROA per Kelas", x = NULL, y = "ROA (Xβ)") +
theme_minimal(base_size = 11) +
theme(legend.position = "none", plot.title = element_text(face = "bold", size = 11))
p3 <- ggplot(fd, aes(x = financial_distress, y = debt_ratio, fill = financial_distress)) +
geom_boxplot(alpha = 0.7, outlier.shape = 21, outlier.size = 2.5) +
scale_fill_manual(values = c("Non-distress" = "#2a9d8f", "Distress" = "#e76f51")) +
labs(title = "Distribusi Debt Ratio per Kelas", x = NULL, y = "Debt Ratio (Xβ)") +
theme_minimal(base_size = 11) +
theme(legend.position = "none", plot.title = element_text(face = "bold", size = 11))
(p1 | p2 | p3) +
plot_annotation(
title = "Eksplorasi Prediktor vs. Status Financial Distress",
caption = "Warna hijau = Non-distress β Warna oranye = Distress",
theme = theme(plot.title = element_text(face = "bold", size = 13))
)set.seed(42)
# Fungsi stratified split: menjaga proporsi kelas di 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)
}
train_id <- stratified_split(fd$distress_int, prop = 0.8)
train_data <- fd[ train_id, ]
test_data <- fd[-train_id, ]
cat(sprintf("β
Training: %d observasi | Testing: %d observasi\n",
nrow(train_data), nrow(test_data)))β
Training: 95 observasi | Testing: 25 observasi
cat(sprintf(" Rasio split: %.0f%% : %.0f%%\n",
nrow(train_data)/nrow(fd)*100, nrow(test_data)/nrow(fd)*100)) Rasio split: 79% : 21%
split_summary <- bind_rows(
train_data %>% count(financial_distress) %>% mutate(data = "Training"),
test_data %>% count(financial_distress) %>% mutate(data = "Testing")
) %>%
group_by(data) %>%
mutate(proporsi = scales::percent(n / sum(n), accuracy = 0.1)) %>%
ungroup() %>%
rename(Data = data, Status = financial_distress, Jumlah = n, Proporsi = proporsi)
knitr::kable(split_summary,
caption = "Distribusi kelas pada data training dan testing (stratified split 80:20)") |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover")) |>
kableExtra::pack_rows("Training", 1, 2) |>
kableExtra::pack_rows("Testing", 3, 4)| Status | Jumlah | Data | Proporsi |
|---|---|---|---|
| Training | |||
| Non-distress | 72 | Training | 75.8% |
| Distress | 23 | Training | 24.2% |
| Testing | |||
| Non-distress | 19 | Testing | 76.0% |
| Distress | 6 | Testing | 24.0% |
Data dibagi dengan rasio 80:20 menggunakan stratified split β memastikan proporsi kelas Distress (Β±24%) terjaga sama di kedua subset. Ini krusial untuk mencegah bias estimasi parameter dan agar performa pada data testing representatif.
<div class="split-num">95</div>
<div class="split-lbl">Data Training</div>
<div class="split-detail">72 Non-distress + 23 Distress</div>
<div class="split-num">25</div>
<div class="split-lbl">Data Testing</div>
<div class="split-detail">19 Non-distress + 6 Distress</div>
Model regresi logistik biner memodelkan probabilitas kondisional:
\[\pi(\mathbf{x}) = P(Y = 1 \mid \mathbf{X} = \mathbf{x}) = \frac{\exp(\beta_0 + \beta_1 X_1 + \cdots + \beta_p X_p)}{1 + \exp(\beta_0 + \beta_1 X_1 + \cdots + \beta_p X_p)}\]
Transformasi logit mengubah rentang probabilitas \((0,1)\) menjadi \((-\infty, +\infty)\):
\[\text{logit}[\pi(\mathbf{x})] = \ln\!\left[\frac{\pi(\mathbf{x})}{1 - \pi(\mathbf{x})}\right] = \beta_0 + \beta_1 X_1 + \cdots + \beta_p X_p\]
Parameter diestimasi menggunakan Maximum Likelihood Estimation (MLE), yang memaksimalkan:
\[\ell(\boldsymbol{\beta}) = \sum_{i=1}^{n} \left\{ y_i \ln[\pi(\mathbf{x}_i)] + (1-y_i)\ln[1 - \pi(\mathbf{x}_i)] \right\}\]
# Membangun model regresi logistik biner dengan semua prediktor
fd_fit <- glm(
distress_int ~ current_ratio + debt_ratio + roa + sales_growth + firm_size,
data = train_data,
family = binomial(link = "logit") # link = "logit" adalah default
)
# Ringkasan kecocokan model
ringkasan_model <- data.frame(
Keterangan = c("Jumlah observasi training", "Null deviance",
"Residual deviance", "Penurunan deviance",
"Derajat bebas residual", "AIC", "McFadden RΒ²"),
Nilai = c(
nobs(fd_fit),
round(fd_fit$null.deviance, 3),
round(fd_fit$deviance, 3),
round(fd_fit$null.deviance - fd_fit$deviance, 3),
fd_fit$df.residual,
round(AIC(fd_fit), 3),
round(1 - (fd_fit$deviance / fd_fit$null.deviance), 4)
)
)
knitr::kable(ringkasan_model,
caption = "Ringkasan kecocokan model regresi logistik biner",
align = c("l","r")) |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) |>
kableExtra::row_spec(c(4, 7), bold = TRUE, background = "#f0f9f8")| Keterangan | Nilai |
|---|---|
| Jumlah observasi training | 95.0000 |
| Null deviance | 105.1640 |
| Residual deviance | 66.8470 |
| Penurunan deviance | 38.3170 |
| Derajat bebas residual | 89.0000 |
| AIC | 78.8470 |
| McFadden RΒ² | 0.3644 |
Hipotesis:
\[H_0: \beta_1 = \beta_2 = \beta_3 = \beta_4 = \beta_5 = 0 \quad \text{vs} \quad H_1: \text{minimal ada satu } \beta_j \neq 0\]
Statistik uji:
\[G = D_{\text{null}} - D_{\text{residual}} = 105{,}164 - 66{,}847 = 38{,}317 \sim \chi^2_{(5)}\]
# Menghitung statistik G (Likelihood Ratio Test)
G_stat <- fd_fit$null.deviance - fd_fit$deviance
df_lrt <- fd_fit$df.null - fd_fit$df.residual
p_lrt <- pchisq(G_stat, df = df_lrt, lower.tail = FALSE)
lrt_table <- data.frame(
`Statistik G` = round(G_stat, 3),
`Derajat Bebas` = df_lrt,
`Nilai kritis ΟΒ²(0.05, 5)` = round(qchisq(0.95, df = df_lrt), 3),
`p-value` = format.pval(p_lrt, digits = 4, eps = 1e-4),
`Keputusan` = ifelse(p_lrt < 0.05, "β
Tolak Hβ", "Gagal Tolak Hβ"),
check.names = FALSE
)
knitr::kable(lrt_table,
caption = "Hasil Uji Likelihood Ratio Test (uji simultan)",
align = c("r","c","r","r","c")) |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) |>
kableExtra::row_spec(1, bold = TRUE, background = "#f0f9f8")| Statistik G | Derajat Bebas | Nilai kritis ΟΒ²(0.05, 5) | p-value | Keputusan |
|---|---|---|---|---|
| 38.317 | 5 | 11.07 | < 1e-04 | β Tolak Hβ | |
G = 38.317 dengan df = 5 β p-value < 0,001 β Hβ Ditolak
Nilai statistik \(G = 38{,}317\) dengan 5 derajat bebas menghasilkan p-value < 0,001, jauh di bawah taraf signifikansi \(\alpha = 0{,}05\). Karena \(G = 38{,}317 > \chi^2_{0,05;5} = 11{,}07\), maka \(H_0\) ditolak.
Kesimpulan: Secara simultan, terdapat minimal satu variabel prediktor yang berpengaruh signifikan terhadap probabilitas financial distress. Model dengan kelima prediktor secara nyata lebih baik daripada model null (tanpa prediktor).
Uji Wald untuk masing-masing koefisien:
\[H_0: \beta_j = 0 \quad \text{vs} \quad H_1: \beta_j \neq 0, \qquad W_j = \left(\frac{\hat{\beta}_j}{SE(\hat{\beta}_j)}\right)^2 \sim \chi^2_{(1)}\]
# Mengekstrak koefisien, menghitung odds ratio dan confidence interval
coef_table <- broom::tidy(fd_fit) %>%
mutate(
odds_ratio = exp(estimate),
ci_low = exp(estimate - 1.96 * std.error),
ci_high = exp(estimate + 1.96 * std.error),
wald_stat = (estimate / std.error)^2
) %>%
arrange(p.value) %>%
transmute(
`Variabel` = term,
`Ξ²Μ (Koefisien)` = round(estimate, 4),
`SE(Ξ²Μ)` = round(std.error, 4),
`Statistik Wald (zΒ²)` = round(wald_stat, 3),
`Odds Ratio (eΞ²Μ)` = round(odds_ratio, 4),
`SK 95% OR` = paste0("[", round(ci_low, 3), "; ", round(ci_high, 3), "]"),
`p-value` = signif(p.value, 3),
`Signifikan (Ξ±=0,05)` = ifelse(p.value < 0.05, "β
Ya", "β Tidak")
)
knitr::kable(coef_table,
caption = "Estimasi koefisien, odds ratio, dan uji signifikansi parsial (Uji Wald)") |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover")) |>
kableExtra::row_spec(which(coef_table$`Signifikan (Ξ±=0,05)` == "β
Ya"),
bold = TRUE, background = "#f0f9f8") |>
kableExtra::column_spec(8, color = ifelse(coef_table$`Signifikan (Ξ±=0,05)` == "β
Ya",
"#2a9d8f", "#e76f51"), bold = TRUE)| Variabel | Ξ²Μ (Koefisien |
Β SE(Ξ²
| |||||
|---|---|---|---|---|---|---|---|
| roa | -16.0189 | 4.4192 | 13.139 | 0.0000 | [0; 0.001] | 0.000289 | β Ya | |
| sales_growth | 1.0455 | 0.9035 | 1.339 | 2.8449 | [0.484; 16.715] | 0.247000 | β Tidak | |
| current_ratio | 0.0462 | 0.0504 | 0.842 | 1.0473 | [0.949; 1.156] | 0.359000 | β Tidak | |
| firm_size | -0.1590 | 0.1739 | 0.836 | 0.8530 | [0.607; 1.199] | 0.361000 | β Tidak | |
| (Intercept) | -0.6364 | 1.1992 | 0.282 | 0.5292 | [0.05; 5.552] | 0.596000 | β Tidak | |
| debt_ratio | -0.2399 | 0.6681 | 0.129 | 0.7867 | [0.212; 2.914] | 0.720000 | β Tidak | |
Dari 5 prediktor, hanya ROA (Xβ) yang signifikan pada Ξ± = 0,05 Β· Ξ²Μ = β16,019 Β· OR β 0 Β· p-value = 0,000289
Berdasarkan hasil estimasi, persamaan model logit adalah:
\[ \ln\!\left(\frac{\hat{p}}{1-\hat{p}}\right) = -0{,}636 -16{,}019\,X_3 +1{,}046\,X_4 +0{,}046\,X_1 -0{,}159\,X_5 -0{,}240\,X_2 \]
Sehingga probabilitas prediksi financial distress adalah:
\[ \hat{p} = \frac{ \exp\!\left( -0{,}636 -16{,}019\,\text{ROA} +1{,}046\,\text{SG} +0{,}046\,\text{CR} -0{,}159\,\text{FS} -0{,}240\,\text{DR} \right) } { 1+ \exp\!\left( -0{,}636 -16{,}019\,\text{ROA} +1{,}046\,\text{SG} +0{,}046\,\text{CR} -0{,}159\,\text{FS} -0{,}240\,\text{DR} \right) } \]
Perusahaan dengan ROA = 0,05; Debt Ratio = 0,5; Current Ratio = 1,2; Sales Growth = 0,1; Firm Size = 7,0:
\[ \text{logit}(\hat{p}) = -0{,}636 -16{,}019(0{,}05) +1{,}046(0{,}1) +0{,}046(1{,}2) -0{,}159(7{,}0) -0{,}240(0{,}5) = -2{,}548 \]
\[ \hat{p} = \frac{e^{-2{,}548}} {1+e^{-2{,}548}} \approx \mathbf{7{,}3\%} \]
β Probabilitas perusahaan tersebut mengalami financial distress sekitar 7,3%, sehingga perusahaan diklasifikasikan sebagai Non-distress karena probabilitas yang dihasilkan berada di bawah threshold 0,5 (50%).
# Prediksi probabilitas pada data training dan testing
p_train <- predict(fd_fit, newdata = train_data, type = "response")
p_test <- predict(fd_fit, newdata = test_data, type = "response")
cat(sprintf("Range probabilitas prediksi (Testing): [%.4f, %.4f]\n",
min(p_test), max(p_test)))Range probabilitas prediksi (Testing): [0.0013, 0.9835]
cat(sprintf("Rata-rata prediksi kelas Distress : %.4f\n",
mean(p_test[test_data$distress_int == 1])))Rata-rata prediksi kelas Distress : 0.5208
cat(sprintf("Rata-rata prediksi kelas Non-distress: %.4f\n",
mean(p_test[test_data$distress_int == 0])))Rata-rata prediksi kelas Non-distress: 0.1057
prediction_preview <- data.frame(
`Kode Saham` = test_data$kode_saham,
`Tahun` = test_data$tahun,
`Status Aktual` = test_data$financial_distress,
`PΜ(Distress)` = round(p_test, 4),
`Klasifikasi (thr=0.5)` = ifelse(p_test >= 0.5, "Distress", "Non-distress"),
check.names = FALSE
) %>%
arrange(desc(`PΜ(Distress)`)) %>%
head(10)
knitr::kable(prediction_preview,
caption = "10 observasi testing dengan probabilitas prediksi tertinggi") |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover")) |>
kableExtra::row_spec(which(prediction_preview$`Status Aktual` == "Distress"),
background = "#fff0ee")| Kode Saham | Tahun | Status Aktual | PΜ(Distress | |Klasifikasi (thr=0.5) | |
|---|---|---|---|---|---|
| 1 | CMPP | 2020 | Distress | 0.9835 | Distress |
| 20 | SDMU | 2020 | Distress | 0.8859 | Distress |
| 9 | WEHA | 2020 | Non-distress | 0.5835 | Distress |
| 8 | LRNA | 2021 | Distress | 0.5781 | Distress |
| 15 | MIRA | 2023 | Distress | 0.2720 | Non-distress |
| 16 | MIRA | 2024 | Distress | 0.2498 | Non-distress |
| 25 | TNCA | 2023 | Non-distress | 0.2224 | Non-distress |
| 19 | SAPX | 2022 | Non-distress | 0.1782 | Non-distress |
| 24 | TNCA | 2022 | Non-distress | 0.1774 | Non-distress |
| 12 | BLTA | 2020 | Distress | 0.1556 | Non-distress |
Tabel diurutkan dari probabilitas prediksi tertinggi ke terendah. Baris yang diarsir merah muda adalah perusahaan yang sesungguhnya Distress. Situasi ideal adalah seluruh baris merah muda berada di posisi teratas dengan nilai PΜ mendekati 1. Perhatikan apakah ada perusahaan distress yang tersembunyi di bawah ambang batas β ini disebut false negative.
Output konsol menunjukkan bahwa rata-rata prediksi untuk kelas Distress jauh lebih tinggi dibandingkan Non-distress, menandakan model mampu memisahkan kedua kelompok dengan baik.
# Fungsi pembagi aman (menghindari pembagian dengan nol)
safe_div <- function(num, den) ifelse(den == 0, NA_real_, num / den)
# Fungsi utama: menghitung semua metrik klasifikasi dari actual & probabilitas
classification_metrics <- function(actual, prob, threshold = 0.5) {
pred <- as.integer(prob >= threshold)
tp <- sum(pred == 1 & actual == 1) # True Positive
tn <- sum(pred == 0 & actual == 0) # True Negative
fp <- sum(pred == 1 & actual == 0) # False Positive
fn <- sum(pred == 0 & actual == 1) # False Negative
sens <- safe_div(tp, tp + fn) # Sensitivity / Recall
spec <- safe_div(tn, tn + fp) # Specificity
prec <- safe_div(tp, tp + fp) # Precision
npv <- safe_div(tn, tn + fn) # Negative Predictive Value
acc <- safe_div(tp + tn, tp + tn + fp + fn) # Accuracy
data.frame(threshold = threshold, TP = tp, TN = tn, FP = fp, FN = fn,
accuracy = acc, error_rate = 1 - acc,
sensitivity = sens, specificity = spec,
precision = prec, npv = npv,
f1_score = safe_div(2*prec*sens, prec + sens),
balanced_accuracy = (sens + spec) / 2,
fpr = 1 - spec, fnr = 1 - sens)
}
# Fungsi pembantu: format confusion matrix
confusion_matrix_fmt <- function(actual, prob, threshold = 0.5) {
pred_label <- factor(ifelse(prob >= threshold, "Prediksi Distress", "Prediksi Non-distress"),
levels = c("Prediksi Distress","Prediksi Non-distress"))
actual_label <- factor(ifelse(actual == 1, "Aktual Distress", "Aktual Non-distress"),
levels = c("Aktual Distress","Aktual Non-distress"))
addmargins(table(actual_label, pred_label))
}
cat("β
Fungsi evaluasi (classification_metrics & confusion_matrix_fmt) berhasil didefinisikan.")β
Fungsi evaluasi (classification_metrics & confusion_matrix_fmt) berhasil didefinisikan.
classification_metrics(): menghitung 10+ metrik evaluasi
dari vektor aktual dan probabilitas prediksi, termasuk accuracy,
sensitivity, specificity, F1-score, dan balanced accuracy.
confusion_matrix_fmt(): memformat confusion matrix dengan
label yang jelas dan baris/kolom marginal menggunakan
addmargins().
safe_div(): menangani pembagian dengan nol (ketika suatu
kelas tidak muncul sama sekali di prediksi).
# Evaluasi pada threshold default 0,50
cm_05 <- confusion_matrix_fmt(test_data$distress_int, p_test, 0.5)
met_05 <- classification_metrics(test_data$distress_int, p_test, 0.5)
knitr::kable(cm_05,
caption = "Confusion matrix data testing β threshold = 0,50") |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) |>
kableExtra::add_header_above(c(" " = 1, "Prediksi" = 2, " " = 1)) |>
kableExtra::column_spec(2, background = "#f0f9f8") |>
kableExtra::column_spec(3, background = "#fff0ee")| Prediksi Distress | Prediksi Non-distress | Sum | |
|---|---|---|---|
| Aktual Distress | 3 | 3 | 6 |
| Aktual Non-distress | 1 | 18 | 19 |
| Sum | 4 | 21 | 25 |
metrik_display <- data.frame(
Metrik = c("Akurasi","Error Rate","Sensitivity (Recall)",
"Specificity","Presisi","NPV","F1-Score","Balanced Accuracy","FPR","FNR"),
Nilai = round(c(met_05$accuracy, met_05$error_rate, met_05$sensitivity,
met_05$specificity, met_05$precision, met_05$npv,
met_05$f1_score, met_05$balanced_accuracy,
met_05$fpr, met_05$fnr), 3),
Formula = c("(TP+TN)/N", "1βAkurasi", "TP/(TP+FN)",
"TN/(TN+FP)", "TP/(TP+FP)", "TN/(TN+FN)",
"2Β·PrecΒ·Sens/(Prec+Sens)", "(Sens+Spec)/2", "FP/(FP+TN)", "FN/(FN+TP)")
)
knitr::kable(metrik_display,
caption = "Metrik evaluasi pada threshold 0,50") |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover")) |>
kableExtra::row_spec(c(1,3,4), bold = TRUE, background = "#fffde7")| Metrik | Nilai | Formula |
|---|---|---|
| Akurasi | 0.840 | (TP+TN)/N |
| Error Rate | 0.160 | 1βAkurasi |
| Sensitivity (Recall) | 0.500 | TP/(TP+FN) |
| Specificity | 0.947 | TN/(TN+FP) |
| Presisi | 0.750 | TP/(TP+FP) |
| NPV | 0.857 | TN/(TN+FN) |
| F1-Score | 0.600 | 2Β·PrecΒ·Sens/(Prec+Sens) |
| Balanced Accuracy | 0.724 | (Sens+Spec)/2 |
| FPR | 0.053 | FP/(FP+TN) |
| FNR | 0.500 | FN/(FN+TP) |
Akurasi = 84,0% Β· Sensitivity = 50,0% (3 dari 6 Distress terdeteksi) Β· Specificity = 94,7% Β· F1-Score = 0,600
β οΈ Masalah Kritis: Dalam konteks sistem peringatan dini, false negative (perusahaan distress yang tidak terdeteksi) jauh lebih berbahaya daripada false positive. Threshold 0,50 terlalu konservatif untuk kelas minoritas ini β perlu disesuaikan ke nilai yang lebih rendah.
# Fungsi menghitung titik-titik kurva ROC untuk semua threshold
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)
data.frame(threshold = th,
sensitivity = safe_div(tp, tp + fn),
specificity = safe_div(tn, tn + fp),
fpr = 1 - safe_div(tn, tn + fp),
youden = safe_div(tp, tp+fn) + safe_div(tn, tn+fp) - 1)
})
bind_rows(out)
}
# Fungsi menghitung AUC menggunakan metode trapesium
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)
}
# Hitung ROC untuk training dan testing
roc_train <- roc_points(train_data$distress_int, p_train) %>% mutate(data = "Training")
roc_test <- roc_points(test_data$distress_int, p_test) %>% mutate(data = "Testing")
auc_train <- auc_value(roc_train)
auc_test <- auc_value(roc_test)
# Threshold optimal: memaksimalkan Indeks Youden J = Sensitivity + Specificity - 1
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_test %>%
filter(is.finite(threshold)) %>%
slice_min(abs(threshold - threshold_opt), n = 1, with_ties = FALSE) %>%
mutate(data = "Testing (threshold opt)")
cat(sprintf("AUC Training : %.4f\n", auc_train))AUC Training : 0.9124
AUC Testing : 0.9386
Threshold opt: 0.2657 (Indeks Youden J = 0.7723)
auc_table <- data.frame(
Data = c("Training","Testing"),
AUC = round(c(auc_train, auc_test), 3),
Interpretasi = c(
ifelse(auc_train > 0.9, "π Sangat baik (> 0,9)", "β
Baik (0,8β0,9)"),
ifelse(auc_test > 0.9, "π Sangat baik (> 0,9)", "β
Baik (0,8β0,9)")
)
)
threshold_table <- optimal_train %>%
transmute(
`Threshold optimal` = round(threshold, 3),
Sensitivity = round(sensitivity, 3),
Specificity = round(specificity, 3),
`Indeks Youden (J)` = round(youden, 3)
)
knitr::kable(auc_table,
caption = "Nilai AUC pada data training dan testing") |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) |>
kableExtra::row_spec(1:2, bold = TRUE, background = "#f0f9f8")| Data | AUC | Interpretasi |
|---|---|---|
| Training | 0.912 | π Sangat baik (> 0,9) | |
| Testing | 0.939 | π Sangat baik (> 0,9) | |
knitr::kable(threshold_table,
caption = "Threshold optimal berdasarkan Indeks Youden (dari kurva ROC training)") |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) |>
kableExtra::row_spec(1, bold = TRUE, background = "#fff8e1")| Threshold optimal | Sensitivity | Specificity | Indeks Youden (J) |
|---|---|---|---|
| 0.266 | 0.87 | 0.903 | 0.772 |
AUC Training = 0.912 Β· AUC Testing = 0.939 Β· Threshold Optimal = 0.266 (Youden J = 0.772)
roc_plot <- bind_rows(roc_train, roc_test)
ggplot(roc_plot, aes(x = fpr, y = sensitivity, color = data)) +
geom_path(linewidth = 1.3) +
geom_abline(intercept = 0, slope = 1, linetype = "dashed",
color = "#6c757d", linewidth = 0.8) +
# Titik optimal training
geom_point(data = optimal_train,
aes(x = fpr, y = sensitivity),
inherit.aes = FALSE,
color = "#ffb703", fill = "#fb8500",
shape = 21, size = 5.5, stroke = 1.5) +
# Titik testing pada threshold optimal
geom_point(data = test_at_opt,
aes(x = fpr, y = sensitivity),
inherit.aes = FALSE,
color = "#8338ec", fill = "#3a86ff",
shape = 24, size = 4.5, stroke = 1.5) +
annotate("text", x = optimal_train$fpr + 0.05, y = optimal_train$sensitivity - 0.05,
label = paste0("Opt. Training\n(thr=", round(threshold_opt,3),
", J=", round(optimal_train$youden,3),")"),
size = 3.4, color = "#fb8500", hjust = 0, fontface = "bold") +
annotate("text", x = test_at_opt$fpr + 0.05, y = test_at_opt$sensitivity + 0.04,
label = paste0("Testing\n(Sens=", round(test_at_opt$sensitivity,3),
", Spec=", round(test_at_opt$specificity,3),")"),
size = 3.4, color = "#8338ec", hjust = 0, fontface = "bold") +
coord_equal(xlim = c(0,1), ylim = c(0,1)) +
scale_color_manual(values = c("Training" = "#0077b6", "Testing" = "#e76f51")) +
labs(title = "Kurva ROC β Model Regresi Logistik Biner",
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 = "True Positive Rate (Sensitivity)",
color = "Data") +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom",
plot.title = element_text(face = "bold", size = 13),
plot.subtitle = element_text(color = "#555", size = 11))# Evaluasi pada threshold optimal
met_opt <- classification_metrics(test_data$distress_int, p_test, threshold_opt)
# Tabel perbandingan kedua threshold
compare_df <- bind_rows(
data.frame(Threshold = 0.50,
Akurasi = met_05$accuracy, `Error Rate` = met_05$error_rate,
Sensitivity = met_05$sensitivity, Specificity = met_05$specificity,
Presisi = met_05$precision, NPV = met_05$npv,
`F1-Score` = met_05$f1_score, `Balanced Acc.` = met_05$balanced_accuracy,
check.names = FALSE),
data.frame(Threshold = round(threshold_opt, 3),
Akurasi = met_opt$accuracy, `Error Rate` = met_opt$error_rate,
Sensitivity = met_opt$sensitivity, Specificity = met_opt$specificity,
Presisi = met_opt$precision, NPV = met_opt$npv,
`F1-Score` = met_opt$f1_score, `Balanced Acc.` = met_opt$balanced_accuracy,
check.names = FALSE)
) %>%
mutate(across(where(is.numeric), round, 3))
knitr::kable(compare_df,
caption = "Perbandingan metrik evaluasi pada dua threshold",
align = c("c", rep("r", ncol(compare_df)-1))) |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover")) |>
kableExtra::row_spec(2, bold = TRUE, background = "#f0f9f8") |>
kableExtra::add_header_above(c(" "=1, "Kinerja Klasifikasi"=8))| Threshold | Akurasi | Error Rate | Sensitivity | Specificity | Presisi | NPV | F1-Score | Balanced Acc. |
|---|---|---|---|---|---|---|---|---|
| 0.500 | 0.84 | 0.16 | 0.500 | 0.947 | 0.75 | 0.857 | 0.600 | 0.724 |
| 0.266 | 0.88 | 0.12 | 0.667 | 0.947 | 0.80 | 0.900 | 0.727 | 0.807 |
# Confusion matrix pada threshold optimal
cm_opt <- confusion_matrix_fmt(test_data$distress_int, p_test, threshold_opt)
knitr::kable(cm_opt,
caption = paste0("Confusion matrix data testing β threshold optimal (",
round(threshold_opt,3), ")")) |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover"),
full_width = FALSE) |>
kableExtra::add_header_above(c(" " = 1, "Prediksi" = 2, " " = 1))| Prediksi Distress | Prediksi Non-distress | Sum | |
|---|---|---|---|
| Aktual Distress | 4 | 2 | 6 |
| Aktual Non-distress | 1 | 18 | 19 |
| Sum | 5 | 20 | 25 |
Threshold 0,50 β Sensitivity 50% | Threshold 0.266 β Sensitivity 66,7% (deteksi 4/6 kasus Distress) Β· Specificity tetap 94,7% Β· Balanced Accuracy naik dari 72,4% β 80,7%
Penggunaan threshold optimal menghasilkan peningkatan performa konsisten:
| Metrik | Threshold 0,50 | Threshold 0.266 | Ξ |
|---|---|---|---|
| Akurasi | 84,0% | 88,0% | β +4,0% |
| Sensitivity | 50,0% | 66,7% | β +16,7% |
| Specificity | 94,7% | 94,7% | β sama |
| F1-Score | 0,600 | 0,727 | β +0,127 |
| Balanced Accuracy | 72,4% | 80,7% | β +8,3% |
Peningkatan terbesar pada Sensitivity (+16,7%): model kini mendeteksi 4 dari 6 perusahaan distress β tanpa mengorbankan Specificity sama sekali. Threshold 0.266 lebih direkomendasikan untuk sistem peringatan dini (early warning system).
test_prob_plot <- test_data %>% mutate(peluang_distress = p_test)
ggplot(test_prob_plot, aes(x = peluang_distress, fill = financial_distress)) +
geom_density(alpha = 0.55, color = "white", linewidth = 0.7) +
geom_rug(aes(color = financial_distress), alpha = 0.8, linewidth = 1) +
geom_vline(xintercept = threshold_opt, color = "#fb8500",
linewidth = 1.3, linetype = "dashed") +
geom_vline(xintercept = 0.5, color = "#adb5bd",
linewidth = 0.9, linetype = "dotted") +
annotate("label", x = threshold_opt, y = Inf,
label = paste0("Opt. = ", round(threshold_opt,3)),
vjust = 1.4, fill = "#fff3b0", color = "#5f370e",
label.size = 0, fontface = "bold") +
annotate("label", x = 0.5, y = Inf,
label = "Default = 0,50",
vjust = 3.0, fill = "#f8f9fa", color = "#6c757d",
label.size = 0) +
scale_fill_manual(values = c("Non-distress" = "#2a9d8f","Distress" = "#e76f51")) +
scale_color_manual(values = c("Non-distress" = "#2a9d8f","Distress" = "#e76f51"),
guide = "none") +
labs(title = "Distribusi Probabilitas Prediksi Financial Distress (Data Testing)",
subtitle = "Garis putus-putus oranye = threshold optimal Β· Titik di bawah = nilai individu",
x = "Probabilitas prediksi PΜ(Distress)",
y = "Kepadatan",
fill = "Status aktual") +
theme_minimal(base_size = 12) +
theme(legend.position = "bottom",
plot.title = element_text(face = "bold", size = 13))ringkasan_final <- data.frame(
Aspek = c(
"Metode", "Data",
"Variabel signifikan (Ξ±=0,05)",
"Uji simultan (LRT)", "McFadden RΒ²",
"AUC Training", "AUC Testing",
"Threshold optimal (Youden)",
"Akurasi (thr. optimal)",
"Sensitivity (thr. optimal)",
"Specificity (thr. optimal)",
"Balanced Accuracy (thr. optimal)"
),
Hasil = c(
"Regresi Logistik Biner (MLE)",
"120 obs., 20 perusahaan transportasi BEI 2019β2024 | Training 95 / Testing 25",
paste0("ROA (Xβ) β p-value = 0,000289; Ξ²Μ = β16,019; OR β 0"),
paste0("G = ", round(G_stat,3), ", df=5, p < 0,001 β Model signifikan"),
round(1 - (fd_fit$deviance / fd_fit$null.deviance), 4),
round(auc_train, 3),
round(auc_test, 3),
round(threshold_opt, 3),
paste0(round(met_opt$accuracy * 100, 1), "%"),
paste0(round(met_opt$sensitivity * 100, 1), "%"),
paste0(round(met_opt$specificity * 100, 1), "%"),
paste0(round(met_opt$balanced_accuracy * 100, 1), "%")
)
)
knitr::kable(ringkasan_final,
caption = "Ringkasan komprehensif hasil analisis",
col.names = c("Aspek", "Hasil")) |>
kableExtra::kable_styling(bootstrap_options = c("striped","hover")) |>
kableExtra::row_spec(c(3,7,8), bold = TRUE, background = "#f0f9f8")| Aspek | Hasil |
|---|---|
| Metode | Regresi Logistik Biner (MLE) |
| Data | 120 obs., 20 perusahaan transportasi BEI 2019β2024 | Training 95 / Testing 25 |
| Variabel signifikan (Ξ±=0,05) | ROA (Xβ) β p-value = 0,000289; Ξ²Μ = β16,019; OR β 0 |
| Uji simultan (LRT) | G = 38.317, df=5, p < 0,001 β Model signifikan |
| McFadden RΒ² | 0.3644 |
| AUC Training | 0.912 |
| AUC Testing | 0.939 |
| Threshold optimal (Youden) | 0.266 |
| Akurasi (thr. optimal) | 88% |
| Sensitivity (thr. optimal) | 66.7% |
| Specificity (thr. optimal) | 94.7% |
| Balanced Accuracy (thr. optimal) | 80.7% |
ROA adalah indikator utama yang perlu dipantau. Perusahaan dengan ROA negatif atau mendekati nol perlu mendapat perhatian lebih sebelum pemberian kredit atau keputusan investasi.
Peningkatan profitabilitas (diukur lewat ROA) adalah prioritas utama untuk menjauh dari risiko financial distress. Efisiensi penggunaan aset perlu terus ditingkatkan.
Model ini dapat diintegrasikan sebagai komponen early warning system untuk sektor transportasi, memungkinkan intervensi dini sebelum perusahaan jatuh ke kondisi bangkrut.