Analisis berikut melakukan tiga tugas utama sesuai instruksi UTS:
Soal 1 — Data cleaning & eksplorasi: identifikasi missing value, outlier, dan inkonsistensi kategori; Lakukan standarisasi penulisan kategori Status.; Tampilkan ringkasan statistik deskriptif setelah pembersihan.
Soal 2 — Klasifikasi Status Kualitas Air: Gunakan variabel numerik (pH, DO, BOD, TSS, Suhu) untuk mengklasifikasikan Status ; Bagi data menjadi training dan testing ; Bangun model klasifikasi dengan SVR, Decision Tree, dan Random Forest ; Evaluasi hasil dengan confusion matrix dan interpretasi akurasi.
Soal 3 — Prediksi DO: Gunakan Regresi Linear dan Regresi Spline untuk memprediksi nilai DO berdasarkan pH, BOD, TSS, dan Suhu ; Evaluasi performa model (R²/MSE/RMSE) ; Visualisasikan hasil prediksi vs aktual ; Jelaskan variabel yang paling memengaruhi DO.
library(readxl)
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## âś” dplyr 1.1.4 âś” readr 2.1.5
## âś” forcats 1.0.0 âś” stringr 1.5.2
## âś” ggplot2 3.5.2 âś” tibble 3.3.0
## âś” lubridate 1.9.4 âś” tidyr 1.3.1
## âś” purrr 1.1.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## âś– dplyr::filter() masks stats::filter()
## âś– dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(caret)
## Loading required package: lattice
##
## Attaching package: 'caret'
##
## The following object is masked from 'package:purrr':
##
## lift
library(e1071) # SVM backend
library(rpart) # Decision Tree
library(randomForest) # Random Forest
## randomForest 4.7-1.2
## Type rfNews() to see new features/changes/bug fixes.
##
## Attaching package: 'randomForest'
##
## The following object is masked from 'package:dplyr':
##
## combine
##
## The following object is masked from 'package:ggplot2':
##
## margin
library(splines) # Basis spline
library(mice) # Imputasi multivariat (jika diperlukan)
##
## Attaching package: 'mice'
##
## The following object is masked from 'package:stats':
##
## filter
##
## The following objects are masked from 'package:base':
##
## cbind, rbind
library(broom) # Merapikan output model
library(knitr)
set.seed(10251547)
options(stringsAsFactors = FALSE)
Tujuan: memastikan struktur data dan memahami masalah kualitas data dasar (missing / tipe data / range).
file_path <- "kualitasair.xlsx"
train_raw <- read_excel("C:/Users/ACER/Downloads/kualitasair.xlsx", sheet = "Training")
test_raw <- read_excel("C:/Users/ACER/Downloads/kualitasair.xlsx", sheet = "Testing")
# Ringkasan
cat("Training: ", dim(train_raw)[1], " baris x ", dim(train_raw)[2], " kolom\n")
## Training: 300 baris x 7 kolom
cat("Testing : ", dim(test_raw)[1], " baris x ", dim(test_raw)[2], " kolom\n\n")
## Testing : 75 baris x 6 kolom
glimpse(train_raw)
## Rows: 300
## Columns: 7
## $ Lokasi <chr> "S1", "S2", "S3", "S4", "S5", "S6", "S7", "S8", "S9", "S10", "S…
## $ pH <dbl> 7.6855, 6.7177, 7.1816, 7.3164, 7.2021, 6.9469, 7.7558, 6.9527,…
## $ DO <dbl> NA, 5.7236, 4.8906, 6.1339, 7.7853, 8.4222, 4.9232, 6.4859, 7.3…
## $ BOD <dbl> 1.7136, 1.4402, 2.7274, 3.1398, 1.1778, 3.2324, NA, 4.0358, 3.1…
## $ TSS <dbl> 43.1415, 44.2963, NA, 41.0104, 48.0967, 48.5610, 49.0343, 51.81…
## $ Suhu <dbl> 26.7972, 27.7284, 26.0255, 29.6639, 26.4099, 28.6809, 29.7409, …
## $ Status <chr> "Tercemar ringan", "Tercemar ringan", "Tercemar ringan", "Terce…
glimpse(test_raw)
## Rows: 75
## Columns: 6
## $ Lokasi <chr> "S301", "S302", "S303", "S304", "S305", "S306", "S307", "S308",…
## $ pH <dbl> 6.9977, 7.3801, 7.0195, 7.3675, 6.9268, 6.9711, 7.2412, 7.4965,…
## $ DO <dbl> 5.0835, 4.7482, 6.5949, NA, 6.2444, 6.0028, 4.6718, 7.1797, 5.4…
## $ BOD <dbl> 3.1813, 3.3373, 3.0039, 3.4952, 3.3449, 3.4466, 3.3968, 4.3295,…
## $ TSS <dbl> 50.9339, 46.5210, NA, 39.0091, 47.1692, 39.1027, 45.9433, 55.26…
## $ Suhu <dbl> 25.5573, 27.0940, 26.5954, 26.6512, 23.3940, 27.7013, 29.8974, …
# Missing per kolom (training)
train_raw %>% summarise(across(everything(), ~ sum(is.na(.))))
## # A tibble: 1 Ă— 7
## Lokasi pH DO BOD TSS Suhu Status
## <int> <int> <int> <int> <int> <int> <int>
## 1 0 0 23 22 24 0 0
dari inspeksi awal kita melihat berapa banyak missing per variabel dan tipe data. Informasi ini menentukan strategi imputasi dan apakah ada kolom yang perlu dikoreksi
Pendekatan umum yang dipakai dan alasan singkat:
Gunakan median untuk imputasi variabel numerik ketika proporsi missing kecil (robust terhadap outlier).
Jika ada variabel numerik dengan missing relatif besar, gunakan mice (pmm) untuk imputasi multivariat yang mempertahankan hubungan antar variabel.
Standarisasi penulisan kategori Status (normalisasi huruf, mapping angka ke label).
Identifikasi outlier melalui boxplot dan nilai ekstrem yang wajar diverifikasi (misal suhu > 40°C dianggap tidak wajar untuk sungai, perlu diperiksa).
1.1 Identifikasi dan tangani missing value, outlier, dan inkonsistensi kategori Tujuan: memastikan struktur data dan memahami masalah kualitas data dasar (missing / tipe data / inkonsistensi).
train <- train_raw
test <- test_raw
# Kolom numerik yang relevan
num_vars <- c("pH", "DO", "BOD", "TSS", "Suhu")
# Missing Value
prop_missing <- sapply(train[num_vars], function(x) mean(is.na(x)))
prop_missing <- tibble(var = names(prop_missing), prop_missing = prop_missing)
kable(prop_missing, caption = "Proporsi Missing Value per Variabel")
| var | prop_missing |
|---|---|
| pH | 0.0000000 |
| DO | 0.0766667 |
| BOD | 0.0733333 |
| TSS | 0.0800000 |
| Suhu | 0.0000000 |
# Imputasi: aturan praktis
# Jika prop < 0.10 -> median; else -> mice (pmm)
small_miss <- prop_missing %>% filter(prop_missing < 0.10) %>% pull(var)
large_miss <- prop_missing %>% filter(prop_missing >= 0.10) %>% pull(var)
# Median imputasi untuk small_miss
for (v in small_miss) {
medv <- median(train[[v]], na.rm = TRUE)
train[[v]][is.na(train[[v]])] <- medv
}
# MICE untuk variabel dengan missing >= 10%
if (length(large_miss) > 0) {
impute_df <- train %>% select(all_of(num_vars), Status)
impute_df$Status <- as.factor(impute_df$Status)
mice_out <- mice(impute_df, m = 1, method = "pmm", printFlag = FALSE, seed = 10251547)
completed <- complete(mice_out, 1)
train[num_vars] <- completed[num_vars]
}
# Verifikasi tidak ada missing numeric
train %>% summarise(across(all_of(num_vars), ~ sum(is.na(.)))) %>%
kable(caption = "Jumlah Missing Value Setelah Imputasi")
| pH | DO | BOD | TSS | Suhu |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
# Outlier Detection & Treatment
# Gunakan metode IQR (Interquartile Range)
for (v in num_vars) {
Q1 <- quantile(train[[v]], 0.25, na.rm = TRUE)
Q3 <- quantile(train[[v]], 0.75, na.rm = TRUE)
IQR_val <- Q3 - Q1
lower <- Q1 - 1.5 * IQR_val
upper <- Q3 + 1.5 * IQR_val
outlier_count <- sum(train[[v]] < lower | train[[v]] > upper)
cat(v, ": Jumlah outlier =", outlier_count, "\n")
# Winsorizing: ganti outlier ekstrem dengan batas bawah/atas
train[[v]][train[[v]] < lower] <- lower
train[[v]][train[[v]] > upper] <- upper
}
## pH : Jumlah outlier = 4
## DO : Jumlah outlier = 4
## BOD : Jumlah outlier = 5
## TSS : Jumlah outlier = 5
## Suhu : Jumlah outlier = 2
# Inkonsistensi Kategori
# Cek level unik di kolom Status
unique(train$Status)
## [1] "Tercemar ringan" "baik" "BAIK" "Baik"
## [5] "tercemar ringan" "Tercemar Ringan" "Tercemar berat"
sudah tidak ada missing values
outlier ada beberapa di variabel variabel seperti hasil diatas
inkonsistensi varriabel juga ada seperti “Baik”, “baik”, “Tercemar ringan”, “tercemar ringan”
1.2 Standarisasi standarisasi ini memastikan model belajar dari label konsisten dan memudahkan interpretasi hasil.
# Standarisasi penulisan kategori (huruf kecil dan tanpa typo)
train$Status <- tolower(train$Status)
train$Status <- trimws(train$Status)
# Perbaiki ejaan umum
train$Status <- recode(train$Status,
"baik" = "Baik",
"tercemar ringan" = "Tercemar ringan",
"tercemar berat" = "Tercemar berat",
"tercemar_berat" = "Tercemar berat",
"tercemar_ringan" = "Tercemar ringan"
)
# Konversi ke faktor dengan urutan logis
train$Status <- factor(train$Status, levels = c("Baik", "Tercemar ringan", "Tercemar berat"))
# Tabel frekuensi
train %>% count(Status) %>% mutate(prop = n / sum(n)) %>% kable()
| Status | n | prop |
|---|---|---|
| Baik | 72 | 0.2400000 |
| Tercemar ringan | 221 | 0.7366667 |
| Tercemar berat | 7 | 0.0233333 |
1.3 Ringkasan statistik deskriptif setelah pembersihan
# Ringkasan statistik
train %>% select(all_of(num_vars)) %>% summary() %>% print()
## pH DO BOD TSS
## Min. :5.697 Min. :3.615 Min. :0.8513 Min. :27.28
## 1st Qu.:6.670 1st Qu.:5.413 1st Qu.:2.4599 1st Qu.:44.28
## Median :6.988 Median :5.991 Median :3.0661 Median :49.52
## Mean :6.990 Mean :5.977 Mean :3.0041 Mean :49.68
## 3rd Qu.:7.318 3rd Qu.:6.611 3rd Qu.:3.5323 3rd Qu.:55.62
## Max. :8.290 Max. :8.409 Max. :5.1409 Max. :72.62
## Suhu
## Min. :22.77
## 1st Qu.:26.62
## Median :28.01
## Mean :28.12
## 3rd Qu.:29.46
## Max. :33.73
# Distribusi variabel (histogram)
train %>%
pivot_longer(cols = all_of(num_vars), names_to = "var", values_to = "val") %>%
ggplot(aes(x = val)) +
geom_histogram(bins = 30, alpha = 0.7) +
facet_wrap(~var, scales = "free") +
theme_minimal() +
labs(title = "Distribusi variabel numerik")
# Boxplots untuk deteksi outlier
train %>%
pivot_longer(cols = all_of(num_vars), names_to = "var", values_to = "val") %>%
ggplot(aes(x = var, y = val)) +
geom_boxplot(outlier.shape = 1) +
theme_minimal() +
labs(title = "Boxplot per variabel (cek outlier)")
Ringkasan Deskriptif Statistik
Kesimpulan awal: Mayoritas data menunjukkan kondisi perairan normal dan wajar untuk lingkungan tropis. Namun, beberapa nilai DO rendah, BOD tinggi, serta outlier suhu ekstrem perlu perhatian lebih lanjut sebelum pemodelan.
Distribusi Variabel (Histogram)
Distribusi yang relatif normal ini menunjukkan tidak diperlukan transformasi data besar-besaran, kecuali pada variabel dengan kemencengan tinggi.
Deteksi Outlier(Box Plot)
Nilai outlier terdeteksi pada beberapa variabel (pH, DO, BOD, TSS, dan Suhu). Karena outlier tersebut masih mungkin merepresentasikan variasi alami kualitas air di lapangan, data tidak dihapus maupun diubah agar analisis tetap mencerminkan kondisi sebenarnya.
Strategi modelisasi singkat:
Gunakan train/validation split internal (80/20) dari data training untuk memilih model terbaik.
Bandingkan SVM, Decision Tree, dan Random Forest.
Gunakan metrik Accuracy, Precision, Recall, dan F1 untuk evaluasi multiclass (melalui confusion matrix).
Pilih model terbaik berdasarkan balance antara akurasi dan stabilitas (jika dua model sama akurasi, pilih yang lebih stabil / explainable).
2.1 Gunakan variabel numerik (pH, DO, BOD, TSS, Suhu) untuk mengklasifikasikan Status dan bagi data training & testing
# Split internal
set.seed(20251015)
train_idx <- createDataPartition(train$Status, p = 0.8, list = FALSE)
train_int <- train[train_idx, ]
val_int <- train[-train_idx, ]
# Formula
formula_cls <- as.formula(paste("Status ~", paste(num_vars, collapse = " + ")))
# Helper predict & eval function
eval_cls <- function(true, pred) {
cm <- confusionMatrix(pred, true)
byClass <- as.data.frame(cm$byClass)
overall <- cm$overall
list(confusion = cm$table, overall = overall, byClass = byClass)
}
2.2 Bangun model klasifikasi dengan SVM, Decision Tree, dan Random Forest.
library(rpart.plot)
# Formula klasifikasi
formula_cls <- Status ~ pH + DO + BOD + TSS + Suhu
# Model SVM
svm_fit <- svm(formula_cls, data = train_int, probability = TRUE)
cat("Model SVM berhasil dibangun.\n")
## Model SVM berhasil dibangun.
print(svm_fit)
##
## Call:
## svm(formula = formula_cls, data = train_int, probability = TRUE)
##
##
## Parameters:
## SVM-Type: C-classification
## SVM-Kernel: radial
## cost: 1
##
## Number of Support Vectors: 134
ggplot(train_int, aes(x = DO, y = BOD, color = Status)) +
geom_point(size = 2, alpha = 0.8) +
stat_contour(data = train_int, aes(z = as.numeric(predict(svm_fit, train_int))),
bins = 2, color = "black", linetype = "dashed") +
theme_minimal() +
labs(title = "Visualisasi SVM Berdasarkan DO dan BOD", x = "DO", y = "BOD")
## Warning: Contour data has duplicated x, y coordinates.
## ℹ 1 duplicated row have been dropped.
## Warning: `stat_contour()`: Zero contours were generated
## Warning in min(x): no non-missing arguments to min; returning Inf
## Warning in max(x): no non-missing arguments to max; returning -Inf
# Model Decision Tree
tree_fit <- rpart(formula_cls, data = train_int, method = "class", control = rpart.control(cp = 0.01))
cat("\nModel Decision Tree berhasil dibangun.\n")
##
## Model Decision Tree berhasil dibangun.
rpart.plot(tree_fit, main = "Struktur Decision Tree")
# Model Random Forest
rf_fit <- randomForest(formula_cls, data = train_int, ntree = 500, importance = TRUE)
cat("\nModel Random Forest berhasil dibangun.\n")
##
## Model Random Forest berhasil dibangun.
print(rf_fit)
##
## Call:
## randomForest(formula = formula_cls, data = train_int, ntree = 500, importance = TRUE)
## Type of random forest: classification
## Number of trees: 500
## No. of variables tried at each split: 2
##
## OOB estimate of error rate: 7.05%
## Confusion matrix:
## Baik Tercemar ringan Tercemar berat class.error
## Baik 51 7 0 0.12068966
## Tercemar ringan 6 171 0 0.03389831
## Tercemar berat 0 4 2 0.66666667
# Perubahan OOB Error terhadap Jumlah Pohon (Random Forest)
plot(rf_fit, main = "Perubahan OOB Error terhadap Jumlah Pohon (Random Forest)")
Interpetasi : Semua model berhasil dibangun. Model SVM menggunakan kernel radial dengan 134 support vectors, menunjukkan model sudah terbentuk meski belum dievaluasi. Decision Tree menghasilkan struktur yang mudah diinterpretasikan, sementara Random Forest memberikan hasil OOB error 7.88%, menandakan model cukup akurat namun masih kurang optimal dalam mengenali kelas Tercemar berat.
Ketiga model klasifikasi berhasil dibangun dan divisualisasikan.
Plot SVM memperlihatkan bidang pemisah antar kelas menggunakan kombinasi variabel DO dan BOD, menunjukkan bahwa data masih tumpang tindih antar kelas.
Struktur Decision Tree menunjukkan urutan pembagian berdasarkan variabel dengan kontribusi tertinggi terhadap status kualitas air.
Plot Random Forest menunjukkan bagaimana tingkat kesalahan (Out-of-Bag Error) berubah seiring jumlah pohon yang ditambah.
2.3 Evaluasi hasil dengan confusion matrix dan interpretasi akurasi.
# Prediksi untuk masing-masing model
svm_pred <- predict(svm_fit, val_int)
tree_pred <- predict(tree_fit, val_int, type = "class")
rf_pred <- predict(rf_fit, val_int)
# Evaluasi dengan confusion matrix
res_svm <- confusionMatrix(svm_pred, val_int$Status)
res_tree <- confusionMatrix(tree_pred, val_int$Status)
res_rf <- confusionMatrix(rf_pred, val_int$Status)
# Tampilkan hasil
cat("Confusion Matrix SVM\n")
## Confusion Matrix SVM
print(res_svm$table)
## Reference
## Prediction Baik Tercemar ringan Tercemar berat
## Baik 9 0 0
## Tercemar ringan 5 44 1
## Tercemar berat 0 0 0
cat("\nAkurasi SVM:", round(res_svm$overall["Accuracy"], 4), "\n\n")
##
## Akurasi SVM: 0.8983
cat("Confusion Matrix Decision Tree\n")
## Confusion Matrix Decision Tree
print(res_tree$table)
## Reference
## Prediction Baik Tercemar ringan Tercemar berat
## Baik 14 1 0
## Tercemar ringan 0 43 1
## Tercemar berat 0 0 0
cat("\nAkurasi Decision Tree:", round(res_tree$overall["Accuracy"], 4), "\n\n")
##
## Akurasi Decision Tree: 0.9661
cat("Confusion Matrix Random Forest\n")
## Confusion Matrix Random Forest
print(res_rf$table)
## Reference
## Prediction Baik Tercemar ringan Tercemar berat
## Baik 14 0 0
## Tercemar ringan 0 44 1
## Tercemar berat 0 0 0
cat("\nAkurasi Random Forest:", round(res_rf$overall["Accuracy"], 4), "\n\n")
##
## Akurasi Random Forest: 0.9831
# Tabel perbandingan akurasi
acc_table <- tibble(
Model = c("SVM", "Decision Tree", "Random Forest"),
Accuracy = c(
round(res_svm$overall["Accuracy"], 4),
round(res_tree$overall["Accuracy"], 4),
round(res_rf$overall["Accuracy"], 4)
)
)
# Visualisasi perbandingan akurasi
ggplot(acc_table, aes(x = Model, y = Accuracy, fill = Model)) +
geom_col(width = 0.6) +
geom_text(aes(label = round(Accuracy, 3)), vjust = -0.5, size = 4) +
ylim(0, 1) +
theme_minimal() +
labs(
title = "Perbandingan Akurasi Model Klasifikasi",
x = "Model",
y = "Akurasi"
) +
theme(legend.position = "none")
Dari hasil confusion matrix:
Model Random Forest memiliki akurasi tertinggi sebesar 0.9831 (98.3%), menunjukkan performa sangat baik dalam mengklasifikasikan status kualitas air.
Decision Tree juga memiliki akurasi tinggi (0.9661 / 96.6%) dengan sedikit kesalahan klasifikasi pada kelas Tercemar ringan.
SVM menghasilkan akurasi lebih rendah (0.8983 / 89.8%), terutama karena beberapa data kelas Baik terklasifikasi sebagai Tercemar ringan.
Secara keseluruhan, model Random Forest paling unggul, menunjukkan kemampuannya menangkap pola kompleks pada data, sedangkan SVM kurang optimal untuk memisahkan kelas yang berdekatan.
#Prediksi 75 baris hasil ketiga model
# Imputasi missing value pada data test
test_work <- test_raw
for (v in num_vars) {
if (any(is.na(test_work[[v]]))) {
test_work[[v]][is.na(test_work[[v]])] <- median(train[[v]], na.rm = TRUE)
}
}
# Gunakan formula & model yang sudah dilatih
pred_svm <- predict(svm_fit, test_work)
pred_tree <- predict(tree_fit, test_work, type = "class")
pred_rf <- predict(rf_fit, test_work)
# Gabungkan hasil prediksi semua model
pred_output <- tibble(
Lokasi = test_work$Lokasi,
Pred_SVM = as.character(pred_svm),
Pred_Tree = as.character(pred_tree),
Pred_RF = as.character(pred_rf)
)
# Tampilkan semua hasil (75 baris)
kable(pred_output, caption = "Hasil Prediksi 75 Lokasi untuk Semua Model")
| Lokasi | Pred_SVM | Pred_Tree | Pred_RF |
|---|---|---|---|
| S301 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S302 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S303 | Baik | Baik | Baik |
| S304 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S305 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S306 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S307 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S308 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S309 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S310 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S311 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S312 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S313 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S314 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S315 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S316 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S317 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S318 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S319 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S320 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S321 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S322 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S323 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S324 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S325 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S326 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S327 | Baik | Baik | Baik |
| S328 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S329 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S330 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S331 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S332 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S333 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S334 | Tercemar ringan | Baik | Baik |
| S335 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S336 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S337 | Baik | Baik | Baik |
| S338 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S339 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S340 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S341 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S342 | Tercemar ringan | Baik | Baik |
| S343 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S344 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S345 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S346 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S347 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S348 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S349 | Baik | Baik | Baik |
| S350 | Tercemar ringan | Tercemar berat | Tercemar ringan |
| S351 | Baik | Baik | Baik |
| S352 | Baik | Baik | Baik |
| S353 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S354 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S355 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S356 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S357 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S358 | Tercemar ringan | Baik | Baik |
| S359 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S360 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S361 | Baik | Baik | Baik |
| S362 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S363 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S364 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S365 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S366 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S367 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S368 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S369 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S370 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S371 | Baik | Baik | Baik |
| S372 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S373 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S374 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
| S375 | Tercemar ringan | Tercemar ringan | Tercemar ringan |
Interpretasi :
Dari hasil prediksi ketiga model (SVM, Decision Tree, dan Random Forest), terlihat bahwa sebagian besar lokasi diklasifikasikan sebagai “Tercemar ringan”, hanya sebagian kecil yang masuk kategori “Baik”, dan hampir tidak ada yang “Tercemar berat”.
Ketiga model menghasilkan prediksi yang sangat konsisten, menunjukkan bahwa data kualitas air antar lokasi memiliki pola yang serupa. Random Forest dan Decision Tree umumnya memberikan hasil identik, sementara SVM memiliki sedikit perbedaan pada beberapa titik.
3.1 Gunakan Regresi Linear dan Regresi Spline untuk memprediksi nilai DO berdasarkan pH, BOD, TSS, dan Suhu.
# Model 1: Regresi Linear
lm_fit <- lm(DO ~ pH + BOD + TSS + Suhu, data = train)
summary(lm_fit)
##
## Call:
## lm(formula = DO ~ pH + BOD + TSS + Suhu, data = train)
##
## Residuals:
## Min 1Q Median 3Q Max
## -2.41303 -0.54131 0.01817 0.64138 2.42521
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 6.404415 1.159983 5.521 7.39e-08 ***
## pH -0.015239 0.112138 -0.136 0.892
## BOD 0.079449 0.069348 1.146 0.253
## TSS 0.000214 0.006015 0.036 0.972
## Suhu -0.020276 0.026703 -0.759 0.448
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.9491 on 295 degrees of freedom
## Multiple R-squared: 0.006004, Adjusted R-squared: -0.007474
## F-statistic: 0.4454 on 4 and 295 DF, p-value: 0.7757
# Evaluasi performa model linear
pred_lm <- predict(lm_fit, train)
R2_lm <- cor(train$DO, pred_lm)^2
MSE_lm <- mean((train$DO - pred_lm)^2)
RMSE_lm <- sqrt(MSE_lm)
Interpetasi : Hasil regresi menunjukkan bahwa model tidak signifikan secara keseluruhan (p-value = 0.7757), artinya variabel pH, BOD, TSS, dan Suhu tidak mampu menjelaskan variasi DO secara signifikan.
Nilai R² = 0.006 menandakan hanya sekitar 0,6% variasi DO yang bisa dijelaskan oleh model — sangat kecil. Tidak ada variabel dengan p-value < 0.05, sehingga semuanya tidak berpengaruh signifikan terhadap DO.
Kesimpulan singkat: model linear kurang cocok; hubungan DO dengan variabel lingkungan kemungkinan bersifat non-linear, sehingga perlu diuji dengan model spline.
#Regresi Spline
spline_fit <- lm(DO ~ bs(pH, df = 4) + bs(BOD, df = 4) + bs(TSS, df = 4) + bs(Suhu, df = 4), data = train)
summary(spline_fit)
##
## Call:
## lm(formula = DO ~ bs(pH, df = 4) + bs(BOD, df = 4) + bs(TSS,
## df = 4) + bs(Suhu, df = 4), data = train)
##
## Residuals:
## Min 1Q Median 3Q Max
## -2.39628 -0.51707 0.03633 0.59513 2.43208
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 6.21317 0.96478 6.440 5.11e-10 ***
## bs(pH, df = 4)1 -0.27182 0.70242 -0.387 0.69906
## bs(pH, df = 4)2 0.17304 0.53588 0.323 0.74700
## bs(pH, df = 4)3 -0.50827 0.66083 -0.769 0.44246
## bs(pH, df = 4)4 0.53918 0.66801 0.807 0.42026
## bs(BOD, df = 4)1 0.68815 0.71667 0.960 0.33777
## bs(BOD, df = 4)2 1.01856 0.51429 1.980 0.04862 *
## bs(BOD, df = 4)3 0.05936 0.64835 0.092 0.92711
## bs(BOD, df = 4)4 1.93658 0.60539 3.199 0.00154 **
## bs(TSS, df = 4)1 -0.50186 0.67594 -0.742 0.45843
## bs(TSS, df = 4)2 0.26001 0.46619 0.558 0.57747
## bs(TSS, df = 4)3 -0.43019 0.60998 -0.705 0.48123
## bs(TSS, df = 4)4 -0.19630 0.54912 -0.357 0.72100
## bs(Suhu, df = 4)1 -0.45345 0.85969 -0.527 0.59829
## bs(Suhu, df = 4)2 -0.91895 0.55450 -1.657 0.09858 .
## bs(Suhu, df = 4)3 -0.40864 0.78962 -0.518 0.60520
## bs(Suhu, df = 4)4 -0.72825 0.68524 -1.063 0.28880
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.9441 on 283 degrees of freedom
## Multiple R-squared: 0.05656, Adjusted R-squared: 0.003224
## F-statistic: 1.06 on 16 and 283 DF, p-value: 0.3933
# Evaluasi performa model spline
pred_spline <- predict(spline_fit, train)
R2_spl <- cor(train$DO, pred_spline)^2
MSE_spl <- mean((train$DO - pred_spline)^2)
RMSE_spl <- sqrt(MSE_spl)
interpretasi : Model regresi spline menunjukkan hasil yang lebih baik sedikit** dibanding regresi linear, dengan R² = 0.056 (sekitar 5,6% variasi DO dapat dijelaskan oleh model). Namun, model belum signifikan secara keseluruhan (p-value = 0.3933).
Dari hasil koefisien, hanya beberapa komponen dari BOD (khususnya
bs(BOD, df=4)2 dan bs(BOD, df=4)4) yang
berpengaruh signifikan terhadap DO (p < 0.05). Artinya, BOD merupakan
variabel yang paling memengaruhi DO, sedangkan pH, TSS, dan Suhu tidak
menunjukkan pengaruh yang signifikan.
Kesimpulan singkat: hubungan antara DO dan variabel lain bersifat non-linear, terutama dipengaruhi oleh BOD, namun model spline masih belum cukup kuat menjelaskan variasi DO secara keseluruhan.
3.2 Evaluasi Performa Model
# Ringkasan performa model
perf_table <- tibble(
Model = c("Regresi Linear", "Regresi Spline"),
R2 = c(R2_lm, R2_spl),
MSE = c(MSE_lm, MSE_spl),
RMSE = c(RMSE_lm, RMSE_spl)
)
kable(perf_table, caption = "Perbandingan Performa Model Regresi")
| Model | R2 | MSE | RMSE |
|---|---|---|---|
| Regresi Linear | 0.0060035 | 0.8858272 | 0.9411839 |
| Regresi Spline | 0.0565631 | 0.8407696 | 0.9169349 |
Interpretasi : Model Regresi Spline memiliki performa lebih baik dibanding Regresi Linear karena mampu menangkap hubungan yang lebih kompleks antara variabel (pH, BOD, TSS, Suhu) dengan DO, meskipun kedua model masih lemah secara prediktif (R² rendah)
3.3 Visualisasikan hasil prediksi vs aktual.
# Visualisasi hasil prediksi vs aktual
pred_df <- tibble(
Aktual = train$DO,
Pred_LM = pred_lm,
Pred_Spline = pred_spline
)
ggplot(pred_df, aes(x = Aktual)) +
geom_point(aes(y = Pred_LM), color = "#0052B1", alpha = 0.7) +
geom_point(aes(y = Pred_Spline), color = "#E45F00", alpha = 0.7) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed") +
theme_minimal() +
labs(
title = "Perbandingan Prediksi vs Aktual (DO)",
x = "Nilai Aktual DO",
y = "Nilai Prediksi DO",
caption = "Garis putus-putus = prediksi sempurna (y = x)"
)
Interpretasi :
Grafik di atas menunjukkan perbandingan antara nilai aktual DO (sumbu X) dan nilai prediksi DO (sumbu Y) untuk dua model (titik biru = regresi linear, titik oranye = regresi spline).
Terlihat bahwa sebagian besar titik berkumpul di sekitar garis horizontal, namun belum mengikuti garis putus-putus (y = x) secara ideal. Artinya, prediksi model masih belum akurat sepenuhnya — banyak nilai yang meleset dari prediksi sempurna.
Meskipun begitu, model Spline (oranye) tampak sedikit lebih menyebar mengikuti tren garis dibanding Linear (biru), menunjukkan bahwa Spline mampu menangkap variasi DO sedikit lebih baik dibanding model linear biasa.
3.4 Jelaskan variabel yang paling memengaruhi DO
# Ambil koefisien model tanpa intercept + interpretasi otomatis
coef_df <- summary(lm_fit)$coefficients[-1, , drop = FALSE] %>%
as.data.frame() %>%
rownames_to_column("Variabel") %>%
mutate(
Arah = ifelse(Estimate > 0, "Positif", "Negatif"),
Signifikan = ifelse(`Pr(>|t|)` < 0.05, "Signifikan", "Tidak signifikan"),
Pengaruh = abs(Estimate),
Interpretasi = case_when(
Arah == "Positif" & Signifikan == "Signifikan" ~ "Berpengaruh positif dan signifikan terhadap DO",
Arah == "Negatif" & Signifikan == "Signifikan" ~ "Berpengaruh negatif dan signifikan terhadap DO",
Arah == "Positif" & Signifikan == "Tidak signifikan" ~ "Cenderung meningkatkan DO, namun tidak signifikan",
Arah == "Negatif" & Signifikan == "Tidak signifikan" ~ "Cenderung menurunkan DO, namun tidak signifikan",
TRUE ~ "Tidak ada pengaruh yang berarti"
)
) %>%
arrange(desc(Pengaruh))
kable(coef_df, caption = "Urutan Pengaruh Variabel terhadap DO (berdasarkan nilai absolut koefisien dan interpretasi)")
| Variabel | Estimate | Std. Error | t value | Pr(>|t|) | Arah | Signifikan | Pengaruh | Interpretasi |
|---|---|---|---|---|---|---|---|---|
| BOD | 0.0794486 | 0.0693484 | 1.1456442 | 0.2528709 | Positif | Tidak signifikan | 0.0794486 | Cenderung meningkatkan DO, namun tidak signifikan |
| Suhu | -0.0202762 | 0.0267033 | -0.7593158 | 0.4482700 | Negatif | Tidak signifikan | 0.0202762 | Cenderung menurunkan DO, namun tidak signifikan |
| pH | -0.0152392 | 0.1121379 | -0.1358971 | 0.8919953 | Negatif | Tidak signifikan | 0.0152392 | Cenderung menurunkan DO, namun tidak signifikan |
| TSS | 0.0002140 | 0.0060148 | 0.0355791 | 0.9716420 | Positif | Tidak signifikan | 0.0002140 | Cenderung meningkatkan DO, namun tidak signifikan |