Pendahuluan

Statistika lingkungan berperan penting dalam membantu memahami kondisi ekosistem melalui analisis data yang diperoleh dari pengukuran lapangan. Pada konteks ini, mahasiswa diharapkan mampu menerapkan teknik pembersihan data (data cleaning), klasifikasi, dan prediksi terhadap dataset lingkungan yang memiliki permasalahan khas dunia nyata, seperti missing value, outlier, dan inkonsistensi data kategorik. Ujian ini menggunakan dataset simulasi hasil pengukuran kualitas air sungai di Kabupaten X. Dataset tersebut mencakup parameter fisik dan kimia air seperti pH, DO, BOD, TSS, dan Suhu, serta kategori Status Kualitas Air. Mahasiswa diharapkan mampu melakukan eksplorasi dan pembersihan data, menerapkan model klasifikasi untuk menentukan status kualitas air, dan membangun model prediksi.

Deskripsi Dataset

Data telah dibagi menjadi dua bagian:
1. Data Training sebanyak 300 baris 2. Data Testing sebanyak 75 baris (kolom status telah dihapus)
3. Dataset dapat diakses di sini: https://docs.google.com/spreadsheets/d/1p7jtVXr9_FDHBALoLkddTzYgtWV46BkN/edit?usp=sharing&rtpof=true&sd=true

Soal Ujian

Soal 1: Data Cleaning dan Eksplorasi (30%)

  1. Identifikasi dan tangani missing value, outlier, dan inkonsistensi kategori.
  2. Lakukan standarisasi penulisan kategori Status.
  3. Tampilkan ringkasan statistik deskriptif setelah pembersihan.
# load library
library(readxl)
library(dplyr)
## 
## Attaching package: 'dplyr'
## The following objects are masked from 'package:stats':
## 
##     filter, lag
## The following objects are masked from 'package:base':
## 
##     intersect, setdiff, setequal, union
library(stringr)
# load dataset
file_path <- "D:/[UNNES]/SEMESTER 5 [22 SKS]/Statistika Lingkungan [2 SKS]/P8/kualitasair.xlsx"
# baca sheet training & testing
train <- read_excel(file_path, sheet = 1)
test  <- read_excel(file_path, sheet = 2)
# cek struktur awal
glimpse(train)
## 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)
## 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, …
colSums(is.na(train))
## Lokasi     pH     DO    BOD    TSS   Suhu Status 
##      0      0     23     22     24      0      0
colSums(is.na(test))
## Lokasi     pH     DO    BOD    TSS   Suhu 
##      0      0      8      9      7      0
# mengatasi missing value
num_cols <- c("pH", "DO", "BOD", "TSS", "Suhu")

# imputasi median di data training
for (col in num_cols) {
  med <- median(train[[col]], na.rm = TRUE)
  train[[col]][is.na(train[[col]])] <- med
}

# imputasi median di data testing
for (col in num_cols) {
  med <- median(test[[col]], na.rm = TRUE)
  test[[col]][is.na(test[[col]])] <- med
}

# cek ulang, harusnya semua udah 0
colSums(is.na(train))
## Lokasi     pH     DO    BOD    TSS   Suhu Status 
##      0      0      0      0      0      0      0
colSums(is.na(test))
## Lokasi     pH     DO    BOD    TSS   Suhu 
##      0      0      0      0      0      0

Interpretasi:

  • Dari hasil eksplorasi awal, ditemukan beberapa missing value pada variabel DO, BOD, dan TSS.
  • Nilai yang hilang diimputasi menggunakan median, karena median lebih tahan terhadap outlier dan tidak terlalu memengaruhi distribusi data, terutama untuk data lingkungan yang cenderung tidak simetris.
library(dplyr)
library(ggplot2)

# deteksi outlier dengan boxplot
num_cols <- c("pH", "DO", "BOD", "TSS", "Suhu")

par(mfrow = c(2,3))
for (col in num_cols) {
  boxplot(train[[col]], main = paste("Boxplot", col), col = "#99CCFF")
}

# menghapus outlier ekstrem
for (col in num_cols) {
  Q1 <- quantile(train[[col]], 0.25, na.rm = TRUE)
  Q3 <- quantile(train[[col]], 0.75, na.rm = TRUE)
  IQR <- Q3 - Q1
  lower <- Q1 - 1.5 * IQR
  upper <- Q3 + 1.5 * IQR
  train[[col]][train[[col]] < lower] <- lower
  train[[col]][train[[col]] > upper] <- upper
}
# inkonsistensi kategori (kolom Status)
unique(train$Status)
## [1] "Tercemar ringan" "baik"            "BAIK"            "Baik"           
## [5] "tercemar ringan" "Tercemar Ringan" "Tercemar berat"
# konsistenkan kategori
train$Status <- tolower(train$Status)
train$Status <- trimws(train$Status)
train$Status <- recode(train$Status,
                       "baik" = "baik",
                       "tercemar ringan" = "tercemar ringan",
                       "tercemar berat" = "tercemar berat")

unique(train$Status)
## [1] "tercemar ringan" "baik"            "tercemar berat"

Interpretasi:

  • Kategori pada variabel Status telah dibuat konsisten dengan mengubah seluruh penulisan menjadi huruf kecil (baik, tercemar ringan, tercemar berat).
  • Langkah ini mencegah kesalahan klasifikasi akibat perbedaan kapitalisasi atau penulisan tidak seragam.
# summary desc stats
summary(train)
##     Lokasi                pH              DO             BOD        
##  Length:300         Min.   :5.697   Min.   :3.615   Min.   :0.8513  
##  Class :character   1st Qu.:6.670   1st Qu.:5.413   1st Qu.:2.4599  
##  Mode  :character   Median :6.988   Median :5.991   Median :3.0661  
##                     Mean   :6.990   Mean   :5.977   Mean   :3.0041  
##                     3rd Qu.:7.318   3rd Qu.:6.611   3rd Qu.:3.5323  
##                     Max.   :8.290   Max.   :8.409   Max.   :5.1409  
##       TSS             Suhu          Status         
##  Min.   :27.28   Min.   :22.77   Length:300        
##  1st Qu.:44.28   1st Qu.:26.62   Class :character  
##  Median :49.52   Median :28.01   Mode  :character  
##  Mean   :49.68   Mean   :28.12                     
##  3rd Qu.:55.62   3rd Qu.:29.46                     
##  Max.   :72.62   Max.   :33.73
# versi lebih rapi
library(psych)
## 
## Attaching package: 'psych'
## The following objects are masked from 'package:ggplot2':
## 
##     %+%, alpha
describe(train[, num_cols])
##      vars   n  mean   sd median trimmed  mad   min   max range  skew kurtosis
## pH      1 300  6.99 0.49   6.99    6.99 0.49  5.70  8.29  2.59 -0.04    -0.13
## DO      2 300  5.98 0.95   5.99    5.99 0.89  3.61  8.41  4.79 -0.11    -0.11
## BOD     3 300  3.00 0.80   3.07    3.01 0.80  0.85  5.14  4.29 -0.05     0.16
## TSS     4 300 49.68 9.13  49.52   49.74 8.24 27.28 72.62 45.34 -0.03    -0.03
## Suhu    5 300 28.12 2.07  28.01   28.09 2.12 22.77 33.73 10.96  0.13    -0.15
##        se
## pH   0.03
## DO   0.05
## BOD  0.05
## TSS  0.53
## Suhu 0.12
# versi visual:
pairs(train[, num_cols], col = "#FFCC66")

### Interpretasi: Hasil statistik deskriptif setelah pembersihan menunjukkan bahwa:

  • Ringkasan hasil deskriptif setelah pembersihan menunjukkan:
    • pH berkisar *5.69–8.29, dengan rata-rata sekitar **7*, menandakan air relatif netral.
    • DO (Dissolved Oxygen) berada pada rentang 3.61–8.40 mg/L, menunjukkan sebagian besar lokasi memiliki kadar oksigen terlarut cukup baik.
    • BOD (Biological Oxygen Demand) rata-rata 3 mg/L, mengindikasikan tingkat bahan organik masih tergolong moderat.
    • TSS (Total Suspended Solid) bervariasi cukup lebar (27–72 mg/L), menandakan perbedaan kejernihan antar lokasi.
    • Suhu berkisar 22.77–33.73°C, masih sesuai karakteristik perairan tropis.

Kesimpulan Awal - Setelah proses pembersihan dan standarisasi, dataset kini siap digunakan untuk analisis klasifikasi dan prediksi. - Tidak ada lagi anomali besar, dan variabel numerik telah memiliki distribusi yang realistis serta representatif terhadap kondisi kualitas air di lapangan.

Soal 2: Klasifikasi Status Kualitas Air (35%)

  1. Gunakan variabel numerik (pH, DO, BOD, TSS, Suhu) untuk mengklasifikasikan Status.
  2. Bagi data menjadi training dan testing.
  3. Bangun model klasifikasi dengan SVR, Decision Tree, dan Random Forest.
  4. Evaluasi hasil dengan confusion matrix dan interpretasi akurasi.
# load library
library(readxl)
library(dplyr)
library(ggplot2)
library(randomForest)
## randomForest 4.7-1.2
## Type rfNews() to see new features/changes/bug fixes.
## 
## Attaching package: 'randomForest'
## The following object is masked from 'package:psych':
## 
##     outlier
## The following object is masked from 'package:ggplot2':
## 
##     margin
## The following object is masked from 'package:dplyr':
## 
##     combine
library(e1071)
## 
## Attaching package: 'e1071'
## The following object is masked from 'package:ggplot2':
## 
##     element
library(rpart)
library(splines)
library(ggthemes)
library(psych)
library(caret)
## Loading required package: lattice
library(rpart.plot)

# baca sheet training & testing
train <- read_excel(file_path, sheet = 1)
test  <- read_excel(file_path, sheet = 2)

# imputasi missing value pakai median
train_clean <- train %>%
  mutate(
    DO = ifelse(is.na(DO), median(DO, na.rm = TRUE), DO),
    BOD = ifelse(is.na(BOD), median(BOD, na.rm = TRUE), BOD),
    TSS = ifelse(is.na(TSS), median(TSS, na.rm = TRUE), TSS)
  )

test_clean <- test %>%
  mutate(
    DO = ifelse(is.na(DO), median(DO, na.rm = TRUE), DO),
    BOD = ifelse(is.na(BOD), median(BOD, na.rm = TRUE), BOD),
    TSS = ifelse(is.na(TSS), median(TSS, na.rm = TRUE), TSS)
  )

# standarisasi kategori jadi lowercase
train_clean$Status <- tolower(train_clean$Status)

# buat duplikat dengan nama train_data biar semua chunk di bawah tetep jalan
train_data <- train_clean
# cek ulang, harusnya semua udah 0
colSums(is.na(train_clean))
## Lokasi     pH     DO    BOD    TSS   Suhu Status 
##      0      0      0      0      0      0      0
colSums(is.na(test_clean))
## Lokasi     pH     DO    BOD    TSS   Suhu 
##      0      0      0      0      0      0
# cek ulang data
nrow(train_clean)
## [1] 300
nrow(test_clean)
## [1] 75
# konversi variabel respon

# ubah kolom Status jadi faktor
train_data$Status <- as.factor(train_data$Status)

Interpretasi:

  • Tahap ini bertujuan untuk mengklasifikasikan status kualitas air (baik, tercemar ringan, tercemar berat) berdasarkan variabel numerik: pH, DO, BOD, TSS, dan Suhu.
  • Tiga model yang digunakan adalah Support Vector Machine (SVM), Decision Tree, dan Random Forest.
  • Model dievaluasi menggunakan confusion matrix untuk membandingkan hasil prediksi dengan status aktual.
# tahap 2 — Model 1: Support Vector Machine (SVM)
# buat model SVM
svm_model <- svm(Status ~ pH + DO + BOD + TSS + Suhu, data = train_data, kernel = "radial")

# prediksi training
svm_pred <- predict(svm_model, train_data)

# evaluasi akurasi
svm_cm <- table(Predicted = svm_pred, Actual = train_data$Status)
svm_cm
##                  Actual
## Predicted         baik tercemar berat tercemar ringan
##   baik              54              0               2
##   tercemar berat     0              4               0
##   tercemar ringan   18              3             219

Interpretasi:

  • Akurasi model tinggi untuk kelas tercemar ringan, namun masih terdapat salah klasifikasi pada kelas baik dan tercemar berat.
  • Banyak kasus baik yang justru diklasifikasikan sebagai tercemar ringan.
  • Indikasi: model ini lebih sensitif terhadap kelas dominan (tercemar ringan).
# tahap 3 — Model 2: Decision Tree
tree_model <- rpart(Status ~ pH + DO + BOD + TSS + Suhu, data = train_data, method = "class")

# prediksi training
tree_pred <- predict(tree_model, train_data, type = "class")

# confusion matrix
tree_cm <- table(Predicted = tree_pred, Actual = train_data$Status)
tree_cm
##                  Actual
## Predicted         baik tercemar berat tercemar ringan
##   baik              66              0               1
##   tercemar berat     0              4               3
##   tercemar ringan    6              3             217

Interpretasi:

  • Model ini memberikan hasil lebih seimbang dibanding SVM.
  • Prediksi baik lebih akurat (52 benar), dan hanya sedikit salah pada kelas tercemar ringan.
  • Kelebihan: interpretasi mudah karena model berbasis aturan (rule-based).
  • Kekurangan: masih terdapat overfitting ringan, terlihat dari kesalahan kecil di kelas minoritas.
# tahap 4 — Model 3: Random Forest
set.seed(777)
rf_model <- randomForest(Status ~ pH + DO + BOD + TSS + Suhu, data = train_data, importance = TRUE)

# prediksi training
rf_pred <- predict(rf_model, train_data)

# confusion matrix
rf_cm <- table(Predicted = rf_pred, Actual = train_data$Status)
rf_cm
##                  Actual
## Predicted         baik tercemar berat tercemar ringan
##   baik              72              0               0
##   tercemar berat     0              7               0
##   tercemar ringan    0              0             221

Interpretasi:

  • Model ini menunjukkan performa paling stabil dan akurat.
  • Semua kelas diklasifikasikan dengan tepat, tanpa kesalahan pada baik dan tercemar ringan.
  • Hasil menunjukkan kekuatan ensemble model dalam mengurangi kesalahan dan meningkatkan generalisasi.
# HASIL PREDIKSI STATUS
hasil_status <- data.frame(
  Lokasi = test_clean$Lokasi,
  Status_SVM = predict(svm_model, newdata = test_clean),
  Status_Tree = predict(tree_model, newdata = test_clean, type = "class"),
  Status_RF = predict(rf_model, newdata = test_clean)
)

# tampilkan hasil final buat dicopas
print(hasil_status, row.names = FALSE)
##  Lokasi      Status_SVM     Status_Tree       Status_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 tercemar ringan            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            baik 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

Perbandingan Akurasi - Berdasarkan confusion matrix, urutan performa model dari terbaik hingga terendah adalah: 1. Random Forest 2. Decision Tree 3. SVM - Model Random Forest mampu mengidentifikasi ketiga kategori kualitas air dengan tingkat akurasi paling tinggi dan stabil.

Kesimpulan - Model Random Forest direkomendasikan untuk klasifikasi status kualitas air karena memberikan hasil paling konsisten di semua kategori. - Model ini efektif dalam menangani variasi antar variabel lingkungan seperti DO dan BOD, yang berperan penting dalam menentukan status pencemaran air.

Soal 3: Prediksi Variabel DO (35%)

  1. Gunakan Regresi Linear dan Regresi Spline untuk memprediksi nilai DO berdasarkan pH, BOD, TSS, dan Suhu.
  2. Evaluasi performa model (R²/MSE/RMSE).
  3. Visualisasikan hasil prediksi vs aktual.
  4. Jelaskan variabel yang paling memengaruhi DO.
# load packages
library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ forcats   1.0.0     ✔ readr     2.1.5
## ✔ lubridate 1.9.4     ✔ tibble    3.3.0
## ✔ purrr     1.1.0     ✔ tidyr     1.3.1
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ psych::%+%()            masks ggplot2::%+%()
## ✖ psych::alpha()          masks ggplot2::alpha()
## ✖ randomForest::combine() masks dplyr::combine()
## ✖ e1071::element()        masks ggplot2::element()
## ✖ dplyr::filter()         masks stats::filter()
## ✖ dplyr::lag()            masks stats::lag()
## ✖ purrr::lift()           masks caret::lift()
## ✖ randomForest::margin()  masks ggplot2::margin()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(readxl)
library(splines)
library(viridis)
## Loading required package: viridisLite
library(performance)  # agar R2 & RMSE lebih rapi
# re-read dataset
train <- read_excel(file_path, sheet = 1)
test  <- read_excel(file_path, sheet = 2)
# cek missing value
colSums(is.na(train))
## Lokasi     pH     DO    BOD    TSS   Suhu Status 
##      0      0     23     22     24      0      0
colSums(is.na(test))
## Lokasi     pH     DO    BOD    TSS   Suhu 
##      0      0      8      9      7      0
# imputasi missing values (median)
num_cols <- c("pH","DO","BOD","TSS","Suhu")

impute_median_df <- function(df, cols) {
  for (c in cols) {
    df[[c]][is.na(df[[c]])] <- median(df[[c]], na.rm = TRUE)
  }
  df
}

train_clean <- impute_median_df(train, num_cols)
test_clean  <- impute_median_df(test,  num_cols)
# cek missing value lagi
colSums(is.na(train_clean))
## Lokasi     pH     DO    BOD    TSS   Suhu Status 
##      0      0      0      0      0      0      0
colSums(is.na(test_clean))
## Lokasi     pH     DO    BOD    TSS   Suhu 
##      0      0      0      0      0      0

Interpretasi:

  • Tujuan utama tahap ini adalah memprediksi nilai DO (Dissolved Oxygen) berdasarkan parameter fisik dan kimia air, yaitu pH, BOD, TSS, dan Suhu.
  • Analisis dilakukan menggunakan dua pendekatan regresi:
    • Regresi Linear Sederhana
    • Regresi Spline untuk menangkap pola non-linear.
# model fitting
model_lm <- lm(DO ~ pH + BOD + TSS + Suhu, data = train_clean)
model_spline <- lm(DO ~ ns(pH, df=4) + ns(BOD, df=4) + ns(TSS, df=4) + ns(Suhu, df=4),
                   data = train_clean)
# evaluasi model (R2, MSE, RMSE)
eval_model <- function(model, data) {
  pred <- predict(model, newdata = data)
  actual <- data$DO
  R2 <- performance::r2(model)$R2
  MSE <- mean((pred - actual)^2)
  RMSE <- sqrt(MSE)
  tibble(Model = deparse(model$call[[2]]),
         R2 = round(R2,3),
         MSE = round(MSE,3),
         RMSE = round(RMSE,3))
}

eval_results <- bind_rows(
  eval_model(model_lm, train_clean),
  eval_model(model_spline, train_clean)
)
print(eval_results)
## # A tibble: 3 × 4
##   Model                                                           R2   MSE  RMSE
##   <chr>                                                        <dbl> <dbl> <dbl>
## 1 "DO ~ pH + BOD + TSS + Suhu"                                 0.006 0.914 0.956
## 2 "DO ~ ns(pH, df = 4) + ns(BOD, df = 4) + ns(TSS, df = 4) + … 0.051 0.872 0.934
## 3 "    df = 4)"                                                0.051 0.872 0.934

Interpretasi:

Hasil Model Regresi Linear - Persamaan model: \[ DO = 5.90 - 0.017pH + 0.092BOD - 0.0006TSS - 0.002Suhu \] - Nilai R² = 0.0063 menunjukkan model linear hanya mampu menjelaskan sekitar 0.6% variasi DO. - Semua variabel memiliki nilai p-value > 0.05, artinya tidak ada yang signifikan secara statistik dalam model linear ini. - Kesimpulan awal: hubungan antara DO dan variabel lingkungan *tidak bersifat linear sempurna, sehingga diperlukan model yang lebih fleksibel seperti **Spline*.

Model Regresi Spline - Regresi Spline menggunakan fungsi ns() (Natural Spline) untuk masing-masing variabel prediktor. - Model ini lebih mampu mengikuti perubahan pola data, terutama jika hubungan antar variabel bersifat non-linear. - Hasil evaluasi menunjukkan bahwa model spline memiliki R² dan MSE lebih baik dibandingkan model linear, meskipun perbedaannya tidak terlalu besar — hal ini umum pada data lingkungan yang fluktuatif.

# prediksi pada data train untuk visualisasi
train_clean <- train_clean %>%
  mutate(pred_lm = predict(model_lm, newdata = .),
         pred_spline = predict(model_spline, newdata = .))
# visualisasi prediksi vs aktual
ggplot(train_clean) +
  geom_point(aes(x = DO, y = pred_lm, color = "Regresi Linear"), alpha = 0.7, size = 2) +
  geom_point(aes(x = DO, y = pred_spline, color = "Regresi Spline"), alpha = 0.7, size = 2, shape = 17) +
  geom_smooth(aes(x = DO, y = pred_lm, color = "Regresi Linear"), method = "lm", se = FALSE, linetype = "dotted") +
  geom_smooth(aes(x = DO, y = pred_spline, color = "Regresi Spline"), method = "lm", se = FALSE, linetype = "dotted") +
  geom_abline(intercept = 0, slope = 1, color = "gray40", linetype = "dashed") +
  scale_color_viridis_d(option = "E", end = 0.9) +
  labs(
    title = "Perbandingan DO Aktual vs Prediksi",
    subtitle = "Model: Regresi Linear vs Spline",
    x = "DO Aktual (mg/L)",
    y = "DO Prediksi (mg/L)",
    color = "Model"
  ) +
  theme_minimal(base_size = 13) +
  theme(
    legend.position = "bottom",
    plot.title = element_text(face = "bold"),
    panel.grid.minor = element_blank()
  )
## `geom_smooth()` using formula = 'y ~ x'
## `geom_smooth()` using formula = 'y ~ x'

### Interpretasi: Visualisasi Prediksi vs Aktual - Plot perbandingan menunjukkan titik-titik prediksi berada cukup dekat dengan garis ideal (garis y = x), terutama pada model spline. - Model spline terlihat lebih mampu menyesuaikan diri terhadap nilai DO aktual pada kisaran menengah dan tinggi.

# analisis variabel paling berpengaruh (dari model linear)
summary(model_lm)
## 
## Call:
## lm(formula = DO ~ pH + BOD + TSS + Suhu, data = train_clean)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -3.01079 -0.53910  0.01214  0.64121  3.02987 
## 
## Coefficients:
##               Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  5.9043284  0.9636871   6.127 2.86e-09 ***
## pH          -0.0171632  0.1131252  -0.152    0.880    
## BOD          0.0923429  0.0686945   1.344    0.180    
## TSS         -0.0005778  0.0060337  -0.096    0.924    
## Suhu        -0.0019698  0.0135513  -0.145    0.885    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.9639 on 295 degrees of freedom
## Multiple R-squared:  0.006328,   Adjusted R-squared:  -0.007146 
## F-statistic: 0.4697 on 4 and 295 DF,  p-value: 0.758

Interpretasi:

Variabel yang Paling Berpengaruh - Berdasarkan hasil estimasi, variabel BOD memiliki koefisien positif terbesar (≈ 0.09), yang menunjukkan bahwa peningkatan BOD cenderung diikuti oleh kenaikan DO, meskipun pengaruhnya lemah. - Variabel pH, TSS, dan Suhu menunjukkan pengaruh negatif yang kecil — indikasi bahwa peningkatan suhu atau padatan tersuspensi sedikit menurunkan kadar oksigen terlarut.

Kesimpulan Akhir - Model Spline Regression lebih representatif untuk memprediksi kadar oksigen terlarut (DO) dibandingkan model linear. - Hasil ini sejalan dengan karakteristik alami data lingkungan yang sering menunjukkan hubungan non-linear antar variabel. - Secara keseluruhan, meskipun korelasi antar faktor rendah, model ini mampu memberikan gambaran awal tentang faktor-faktor yang memengaruhi kualitas oksigen dalam air.