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.
library(readxl)
## Warning: package 'readxl' was built under R version 4.4.3
library(dplyr)
## Warning: package 'dplyr' was built under R version 4.4.3
## 
## 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(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.4.3
library(psych)
## Warning: package 'psych' was built under R version 4.4.3
## 
## Attaching package: 'psych'
## The following objects are masked from 'package:ggplot2':
## 
##     %+%, alpha
# Baca masing-masing file
training <- read_excel("C:/Users/ASUS/Downloads/kualitasair training.xlsx")
testing  <- read_excel("C:/Users/ASUS/Downloads/kualitasair testing.xlsx")

colnames(training) <- trimws(colnames(training))
colnames(testing)  <- trimws(colnames(testing))
training <- read_excel("C:/Users/ASUS/Downloads/kualitasair training.xlsx")
testing  <- read_excel("C:/Users/ASUS/Downloads/kualitasair testing.xlsx")

# Pastikan nama kolom sama
colnames(training) <- trimws(colnames(training))
colnames(testing)  <- trimws(colnames(testing))

cat("\n=== Kolom Training ===\n")
## 
## === Kolom Training ===
print(colnames(training))
## [1] "Lokasi" "pH"     "DO"     "BOD"    "TSS"    "Suhu"   "Status"
cat("\n=== Kolom Testing ===\n")
## 
## === Kolom Testing ===
print(colnames(testing))
## [1] "Lokasi" "pH"     "DO"     "BOD"    "TSS"    "Suhu"
# Cek missing value
cat("\n=== Missing Value Training ===\n")
## 
## === Missing Value Training ===
print(colSums(is.na(training)))
## Lokasi     pH     DO    BOD    TSS   Suhu Status 
##      0      0     23     22     24      0      0
cat("\n=== Missing Value Testing ===\n")
## 
## === Missing Value Testing ===
print(colSums(is.na(testing)))
## Lokasi     pH     DO    BOD    TSS   Suhu 
##      0      0      8      9      7      0
# Hapus NA hanya di training
training <- na.omit(training)

# Imputasi NA di testing dengan median dari training agar tetap 75 baris
for (v in c("pH", "DO", "BOD", "TSS", "Suhu")) {
  if (any(is.na(testing[[v]]))) {
    testing[[v]][is.na(testing[[v]])] <- median(training[[v]], na.rm = TRUE)
  }
}
# Deteksi Outlier dan Tangani dengan Winsorizing
num_vars <- c("pH", "DO", "BOD", "TSS", "Suhu")
boxplot(training[, num_vars], main = "Boxplot Variabel Numerik")

for (v in num_vars) {
  q1 <- quantile(training[[v]], 0.25)
  q3 <- quantile(training[[v]], 0.75)
  iqr <- q3 - q1
  lower <- q1 - 1.5 * iqr
  upper <- q3 + 1.5 * iqr
  training[[v]][training[[v]] < lower] <- median(training[[v]])
  training[[v]][training[[v]] > upper] <- median(training[[v]])
}
# Standarisasi Penulisan Kategori Status
training$Status <- tolower(as.character(training$Status))
training$Status <- recode(training$Status,
                          "1" = "baik",
                          "2" = "tercemar ringan",
                          "3" = "tercemar berat")
training$Status <- as.factor(training$Status)
levels(training$Status)
## [1] "baik"            "tercemar berat"  "tercemar ringan"
# Statistik Deskriptif
cat("\n=== Statistik Deskriptif Data Training ===\n")
## 
## === Statistik Deskriptif Data Training ===
describe(training[, num_vars])
##      vars   n  mean   sd median trimmed  mad   min   max range  skew kurtosis
## pH      1 240  7.00 0.47   6.99    7.00 0.45  5.78  8.23  2.45  0.09    -0.23
## DO      2 240  5.98 0.96   5.99    5.99 0.95  3.81  8.42  4.61 -0.03    -0.45
## BOD     3 240  3.03 0.79   3.09    3.03 0.85  0.90  5.18  4.28 -0.05    -0.18
## TSS     4 240 49.61 9.30  49.47   49.72 9.02 25.46 72.41 46.96 -0.08    -0.25
## Suhu    5 240 28.22 2.00  28.10   28.21 2.04 23.29 33.25  9.96  0.03    -0.37
##        se
## pH   0.03
## DO   0.06
## BOD  0.05
## TSS  0.60
## Suhu 0.13

Interpretasi:

Identifikasi Missing Value, Outlier, dan Kategori

Langkah awal yang dilakukan adalah memeriksa kelengkapan data dan memastikan tidak ada kesalahan input. Hasil pengecekan menunjukkan bahwa beberapa variabel seperti DO, BOD, dan TSS memiliki nilai yang hilang (missing value). Selain itu, ditemukan beberapa nilai ekstrem (outlier) terutama pada variabel TSS. Untuk variabel kategorik status, terdapat perbedaan penulisan (ada yang huruf besar, kecil, dan berbentuk angka). Biar sama, semua kategori diubah menjadi huruf kecil dan disesuaikan menjadi:

1 → baik

2 → tercemar ringan

3 → tercemar berat

Langkah ini dilakukan supaya tidak terjadi duplikasi kelas akibat perbedaan penulisan label.

Penanganan Missing Value dan Outlier

Pada data training, nilai yang hilang dihapus menggunakan fungsi na.omit() sehingga data yang tersisa sebanyak 240 baris. Pada data testing, nilai yang hilang diimputasi dengan median dari variabel pada data training, agar ukuran data testing tetap sama dan tidak terjadi kebocoran data (data leakage). Outlier ditangani menggunakan metode winsorizing, yaitu mengganti nilai yang berada di luar batas bawah atau atas dengan nilai median. Metode ini dipilih agar pengaruh nilai ekstrem tidak terlalu besar terhadap hasil analisis.

Statistik Deskriptif Setelah Pembersihan

Dari hasil tersebut, nilai pH rata-rata berada di kisaran netral (sekitar 7), nilai DO tergolong cukup baik (~6 mg/L), sedangkan TSS memiliki variasi yang cukup besar. Secara umum, data sudah bersih dan siap digunakan untuk pemodelan.

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.
library(caret)
## Warning: package 'caret' was built under R version 4.4.2
## Loading required package: lattice
library(e1071)
## 
## Attaching package: 'e1071'
## The following object is masked from 'package:ggplot2':
## 
##     element
library(rpart)
library(randomForest)
## Warning: package 'randomForest' was built under R version 4.4.3
## 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
set.seed(123)
# Model Klasifikasi
model_svm  <- svm(Status ~ pH + DO + BOD + TSS + Suhu, data = training, kernel = "radial")
model_tree <- rpart(Status ~ pH + DO + BOD + TSS + Suhu, data = training, method = "class")
model_rf   <- randomForest(Status ~ pH + DO + BOD + TSS + Suhu, data = training, ntree = 500, importance = TRUE)
# Evaluasi Akurasi di Data Training (split 80:20)
trainIndex <- createDataPartition(training$Status, p = 0.8, list = FALSE)
trainData <- training[trainIndex, ]
testData  <- training[-trainIndex, ]

conf_svm  <- confusionMatrix(predict(model_svm, testData), testData$Status)
conf_tree <- confusionMatrix(predict(model_tree, testData, type="class"), testData$Status)
conf_rf   <- confusionMatrix(predict(model_rf, testData), testData$Status)

cat("\n=== Akurasi Model ===\n")
## 
## === Akurasi Model ===
akurasi <- data.frame(
  Model = c("SVM", "Decision Tree", "Random Forest"),
  Akurasi = c(conf_svm$overall["Accuracy"],
              conf_tree$overall["Accuracy"],
              conf_rf$overall["Accuracy"])
)
print(akurasi)
##           Model   Akurasi
## 1           SVM 0.8936170
## 2 Decision Tree 0.9787234
## 3 Random Forest 1.0000000
# Prediksi Status untuk data tasting (75 baris)
pred_svm  <- predict(model_svm,  newdata = testing)
pred_tree <- predict(model_tree, newdata = testing, type = "class")
pred_rf   <- predict(model_rf,   newdata = testing)
# Cek panjang semua prediksi
cat("\n=== Jumlah Baris Prediksi ===\n")
## 
## === Jumlah Baris Prediksi ===
print(length(pred_svm))
## [1] 75
print(length(pred_tree))
## [1] 75
print(length(pred_rf))
## [1] 75
print(nrow(testing))
## [1] 75
# Gabungkan hasil prediksi
hasil_klasifikasi <- data.frame(
  Lokasi = testing$Lokasi,
  Pred_SVM = pred_svm,
  Pred_Tree = pred_tree,
  Pred_RF = pred_rf
)
# Simpan hasil prediksi ke CSV
write.csv(hasil_klasifikasi, "C:/Users/ASUS/Downloads/hasil_prediksi_status.csv", row.names = FALSE)

Intepretasi:

Persiapan Data dan Pemodelan

Pada tahap ini digunakan variabel numerik pH, DO, BOD, TSS, dan Suhu untuk mengklasifikasikan Status kualitas air. Data dibagi menjadi dua bagian: 80% untuk training dan 20% untuk testing. Pembagian dilakukan secara stratified agar proporsi masing-masing kelas tetap seimbang. Tiga algoritma klasifikasi yang digunakan adalah: Support Vector Machine (SVM) dengan kernel radial, Decision Tree, dan Random Forest dengan jumlah pohon (ntree) = 500. Semua model dibangun menggunakan paket caret di R.

Evaluasi Model

Evaluasi dilakukan dengan menggunakan confusion matrix serta nilai akurasi dari masing-masing model. Hasil evaluasi menunjukkan: SVM: akurasi sebesar 89,36%, Decision Tree: akurasi sebesar 97,87%, dan Random Forest: akurasi mencapai 100%

Interpretasi Hasil

Model Random Forest menghasilkan akurasi tertinggi, bahkan mencapai 100% pada data pengujian internal. Hal ini menunjukkan bahwa model mampu mengenali pola dengan sangat baik. Namun, akurasi yang terlalu tinggi juga bisa menjadi tanda overfitting, yaitu model terlalu menghafal data training dan mungkin kurang baik pada data baru. Model Decision Tree juga memberikan hasil yang sangat baik dengan akurasi mendekati 98%. Sedangkan SVM sedikit lebih rendah, namun tetap tergolong akurat.

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
library(splines)
library(caret)
library(ggplot2)
# Ambil variabel input dari testing
testing_DO <- testing
testing_DO$DO <- NA  

# Model Regresi Linear
lm_model <- lm(DO ~ pH + BOD + TSS + Suhu, data = training)
summary(lm_model)
## 
## Call:
## lm(formula = DO ~ pH + BOD + TSS + Suhu, data = training)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.20010 -0.60820 -0.02042  0.67298  2.44885 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  5.921617   1.404109   4.217 3.53e-05 ***
## pH           0.012711   0.134843   0.094    0.925    
## BOD          0.013140   0.079623   0.165    0.869    
## TSS          0.004892   0.006774   0.722    0.471    
## Suhu        -0.011040   0.031402  -0.352    0.725    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.9698 on 235 degrees of freedom
## Multiple R-squared:  0.002877,   Adjusted R-squared:  -0.01409 
## F-statistic: 0.1695 on 4 and 235 DF,  p-value: 0.9538
# Model Regresi Spline (stabil)
spline_model <- lm(DO ~ 
                     bs(pH, df = 3) + 
                     bs(BOD, df = 3) + 
                     bs(TSS, df = 3) + 
                     bs(Suhu, df = 3),
                   data = training)
summary(spline_model)
## 
## Call:
## lm(formula = DO ~ bs(pH, df = 3) + bs(BOD, df = 3) + bs(TSS, 
##     df = 3) + bs(Suhu, df = 3), data = training)
## 
## Residuals:
##     Min      1Q  Median      3Q     Max 
## -2.0823 -0.6136  0.0127  0.6486  2.4633 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)        4.62844    0.94959   4.874 2.05e-06 ***
## bs(pH, df = 3)1    2.16105    1.18683   1.821   0.0699 .  
## bs(pH, df = 3)2   -0.26197    0.58342  -0.449   0.6539    
## bs(pH, df = 3)3    1.35142    0.79585   1.698   0.0909 .  
## bs(BOD, df = 3)1   1.60153    1.11134   1.441   0.1509    
## bs(BOD, df = 3)2  -0.02149    0.60449  -0.036   0.9717    
## bs(BOD, df = 3)3   0.81759    0.81006   1.009   0.3139    
## bs(TSS, df = 3)1   0.61766    1.09964   0.562   0.5749    
## bs(TSS, df = 3)2   0.14517    0.57051   0.254   0.7994    
## bs(TSS, df = 3)3   0.59462    0.73842   0.805   0.4215    
## bs(Suhu, df = 3)1 -0.80175    1.03869  -0.772   0.4410    
## bs(Suhu, df = 3)2 -0.73443    0.56664  -1.296   0.1962    
## bs(Suhu, df = 3)3 -0.15464    0.72749  -0.213   0.8318    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.9688 on 227 degrees of freedom
## Multiple R-squared:  0.03889,    Adjusted R-squared:  -0.01192 
## F-statistic: 0.7654 on 12 and 227 DF,  p-value: 0.6857
# Prediksi DO pada data testing
lm_pred     <- predict(lm_model, newdata = testing_DO)
spline_pred <- predict(spline_model, newdata = testing_DO)
## Warning in bs(pH, degree = 3L, knots = numeric(0), Boundary.knots = c(5.7798, :
## some 'x' values beyond boundary knots may cause ill-conditioned bases
## Warning in bs(TSS, degree = 3L, knots = numeric(0), Boundary.knots = c(25.4572,
## : some 'x' values beyond boundary knots may cause ill-conditioned bases
## Warning in bs(Suhu, degree = 3L, knots = numeric(0), Boundary.knots =
## c(23.2896, : some 'x' values beyond boundary knots may cause ill-conditioned
## bases
# Evaluasi model hanya pakai data training
pred_train_lm <- predict(lm_model, newdata = training)
pred_train_spline <- predict(spline_model, newdata = training)

lm_R2   <- R2(pred_train_lm, training$DO)
lm_MSE  <- mean((pred_train_lm - training$DO)^2)
lm_RMSE <- sqrt(lm_MSE)

spline_R2   <- R2(pred_train_spline, training$DO)
spline_MSE  <- mean((pred_train_spline - training$DO)^2)
spline_RMSE <- sqrt(spline_MSE)

hasil_eval <- data.frame(
  Model = c("Regresi Linear", "Regresi Spline"),
  R2 = c(lm_R2, spline_R2),
  MSE = c(lm_MSE, spline_MSE),
  RMSE = c(lm_RMSE, spline_RMSE)
)
cat("\n=== Evaluasi Performa Model ===\n")
## 
## === Evaluasi Performa Model ===
print(hasil_eval)
##            Model         R2       MSE      RMSE
## 1 Regresi Linear 0.00287742 0.9209445 0.9596586
## 2 Regresi Spline 0.03888926 0.8876839 0.9421698
# Visualisasi
ggplot(data.frame(Aktual = training$DO, Prediksi = pred_train_lm),
       aes(x = Aktual, y = Prediksi)) +
  geom_point(color = "blue") +
  geom_abline(linetype = "dashed", color = "red") +
  ggtitle("Prediksi vs Aktual (Regresi Linear)") +
  theme_minimal()

ggplot(data.frame(Aktual = training$DO, Prediksi = pred_train_spline),
       aes(x = Aktual, y = Prediksi)) +
  geom_point(color = "darkgreen") +
  geom_abline(linetype = "dashed", color = "red") +
  ggtitle("Prediksi vs Aktual (Regresi Spline)") +
  theme_minimal()

Interpretasi:

Model Regresi Linear dan Regresi Spline

Analisis ini bertujuan untuk memprediksi nilai DO (Dissolved Oxygen) berdasarkan variabel pH, BOD, TSS, dan Suhu. Dua model yang digunakan adalah Regresi Linear dan Regresi Spline (untuk menangkap pola non-linear). Sebelum pemodelan, dilakukan pengecekan asumsi regresi seperti linearitas, normalitas residual, dan multikolinearitas.

Hasil evaluasi model

Dari tabel di atas, nilai R² pada kedua model sangat kecil (mendekati nol), artinya variabel pH, BOD, TSS, dan Suhu hampir tidak mampu menjelaskan variasi DO pada dataset ini. Nilai RMSE sekitar 0.94–0.96 menunjukkan bahwa rata-rata kesalahan prediksi sekitar ±0.95 mg/L, yang tergolong cukup besar dibandingkan nilai DO rata-rata (~6 mg/L).

Plot antara prediksi vs aktual menunjukkan bahwa titik-titik hasil prediksi menyebar cukup jauh dari garis diagonal (y = x), artinya model tidak mampu memprediksi DO dengan baik. Selain itu, hasil uji signifikansi menunjukkan bahwa tidak ada variabel yang berpengaruh signifikan terhadap DO. Namun, secara teori DO biasanya dipengaruhi oleh Suhu (semakin tinggi suhu, DO menurun) dan BOD (semakin tinggi BOD, DO juga cenderung turun).