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(ggplot2)
library(tidyr)

Data

Data Training

training <- read_excel("kualitasair.xlsx", sheet = "Training")
head(training)
## # A tibble: 6 × 7
##   Lokasi    pH    DO   BOD   TSS  Suhu Status         
##   <chr>  <dbl> <dbl> <dbl> <dbl> <dbl> <chr>          
## 1 S1      7.69 NA     1.71  43.1  26.8 Tercemar ringan
## 2 S2      6.72  5.72  1.44  44.3  27.7 Tercemar ringan
## 3 S3      7.18  4.89  2.73  NA    26.0 Tercemar ringan
## 4 S4      7.32  6.13  3.14  41.0  29.7 Tercemar ringan
## 5 S5      7.20  7.79  1.18  48.1  26.4 baik           
## 6 S6      6.95  8.42  3.23  48.6  28.7 Tercemar ringan

Data Testing

datatesting <- read_excel("kualitasair.xlsx", sheet = "Testing")
head(datatesting)
## # A tibble: 6 × 6
##   Lokasi    pH    DO   BOD   TSS  Suhu
##   <chr>  <dbl> <dbl> <dbl> <dbl> <dbl>
## 1 S301    7.00  5.08  3.18  50.9  25.6
## 2 S302    7.38  4.75  3.34  46.5  27.1
## 3 S303    7.02  6.59  3.00  NA    26.6
## 4 S304    7.37 NA     3.50  39.0  26.7
## 5 S305    6.93  6.24  3.34  47.2  23.4
## 6 S306    6.97  6.00  3.45  39.1  27.7

Soal 1 Data Cleaning dan Eksplorasi

Identifikasi dan tangani missing value, outlier

Data Training

# struktur data
str(training)
## tibble [300 × 7] (S3: tbl_df/tbl/data.frame)
##  $ Lokasi: chr [1:300] "S1" "S2" "S3" "S4" ...
##  $ pH    : num [1:300] 7.69 6.72 7.18 7.32 7.2 ...
##  $ DO    : num [1:300] NA 5.72 4.89 6.13 7.79 ...
##  $ BOD   : num [1:300] 1.71 1.44 2.73 3.14 1.18 ...
##  $ TSS   : num [1:300] 43.1 44.3 NA 41 48.1 ...
##  $ Suhu  : num [1:300] 26.8 27.7 26 29.7 26.4 ...
##  $ Status: chr [1:300] "Tercemar ringan" "Tercemar ringan" "Tercemar ringan" "Tercemar ringan" ...
summary(training)
##     Lokasi                pH              DO             BOD        
##  Length:300         Min.   :5.503   Min.   :2.982   Min.   :0.3026  
##  Class :character   1st Qu.:6.670   1st Qu.:5.375   1st Qu.:2.3573  
##  Mode  :character   Median :6.988   Median :5.991   Median :3.0661  
##                     Mean   :6.989   Mean   :5.976   Mean   :3.0005  
##                     3rd Qu.:7.318   3rd Qu.:6.688   3rd Qu.:3.5781  
##                     Max.   :8.351   Max.   :9.229   Max.   :5.7962  
##                                     NA's   :23      NA's   :22      
##       TSS             Suhu          Status         
##  Min.   :24.65   Min.   :22.77   Length:300        
##  1st Qu.:43.73   1st Qu.:26.62   Class :character  
##  Median :49.52   Median :28.01   Mode  :character  
##  Mean   :49.70   Mean   :28.31                     
##  3rd Qu.:56.44   3rd Qu.:29.46                     
##  Max.   :76.34   Max.   :90.00                     
##  NA's   :24

Mengatasi missing value

# Identifikasi missing value
colSums(is.na(training))
## Lokasi     pH     DO    BOD    TSS   Suhu Status 
##      0      0     23     22     24      0      0
# menangani missing value
for (col in names(training)) {
  if (is.numeric(training[[col]])) {
    training[[col]][is.na(training[[col]])] <- median(training[[col]], na.rm = TRUE)
  } else {
    mode_val <- names(sort(table(training[[col]]), decreasing = TRUE))[1]
    training[[col]][is.na(training[[col]])] <- mode_val
  }
}
# Identifikasi missing value 
colSums(is.na(training))
## Lokasi     pH     DO    BOD    TSS   Suhu Status 
##      0      0      0      0      0      0      0

Mengatasi outlier

numeric_cols <- sapply(training, is.numeric)
for (col in names(training)[numeric_cols]) {
  Q1 <- quantile(training[[col]], 0.25, na.rm = TRUE)
  Q3 <- quantile(training[[col]], 0.75, na.rm = TRUE)
  IQR_val <- Q3 - Q1
  batas_bawah <- Q1 - 1.5 * IQR_val
  batas_atas <- Q3 + 1.5 * IQR_val
  outlier_count <- sum(training[[col]] < batas_bawah | training[[col]] > batas_atas, na.rm = TRUE)
  cat(col, ": jumlah outlier =", outlier_count, "\n")
}
## pH : jumlah outlier = 4 
## DO : jumlah outlier = 4 
## BOD : jumlah outlier = 5 
## TSS : jumlah outlier = 5 
## Suhu : jumlah outlier = 2
# Visualisasi outlier dengan box Plot
training %>% 
  pivot_longer(cols = names(training)[numeric_cols], names_to = "variabel", values_to = "nilai") %>%
  ggplot(aes(x = variabel, y = nilai)) +
  geom_boxplot(fill = "skyblue") +
  theme_minimal() +
  coord_flip()

# Menangani outlier
for (col in names(training)[numeric_cols]) {
  Q1 <- quantile(training[[col]], 0.25, na.rm = TRUE)
  Q3 <- quantile(training[[col]], 0.75, na.rm = TRUE)
  IQR_val <- Q3 - Q1
  batas_bawah <- Q1 - 1.5 * IQR_val
  batas_atas <- Q3 + 1.5 * IQR_val
  training[[col]] <- ifelse(training[[col]] < batas_bawah, batas_bawah,
                        ifelse(training[[col]] > batas_atas, batas_atas, training[[col]]))
}

Data Testing

# struktur data
str(datatesting)
## tibble [75 × 6] (S3: tbl_df/tbl/data.frame)
##  $ Lokasi: chr [1:75] "S301" "S302" "S303" "S304" ...
##  $ pH    : num [1:75] 7 7.38 7.02 7.37 6.93 ...
##  $ DO    : num [1:75] 5.08 4.75 6.59 NA 6.24 ...
##  $ BOD   : num [1:75] 3.18 3.34 3 3.5 3.34 ...
##  $ TSS   : num [1:75] 50.9 46.5 NA 39 47.2 ...
##  $ Suhu  : num [1:75] 25.6 27.1 26.6 26.7 23.4 ...
summary(datatesting)
##     Lokasi                pH               DO             BOD       
##  Length:75          Min.   : 1.500   Min.   :3.807   Min.   :0.957  
##  Class :character   1st Qu.: 6.615   1st Qu.:5.279   1st Qu.:2.528  
##  Mode  :character   Median : 6.965   Median :5.644   Median :3.062  
##                     Mean   : 6.946   Mean   :5.820   Mean   :3.031  
##                     3rd Qu.: 7.349   3rd Qu.:6.331   3rd Qu.:3.440  
##                     Max.   :10.500   Max.   :7.730   Max.   :5.082  
##                                      NA's   :8       NA's   :9      
##       TSS             Suhu      
##  Min.   :24.06   Min.   :23.39  
##  1st Qu.:41.93   1st Qu.:26.79  
##  Median :49.57   Median :28.28  
##  Mean   :49.11   Mean   :29.04  
##  3rd Qu.:56.34   3rd Qu.:29.88  
##  Max.   :74.09   Max.   :85.00  
##  NA's   :7

Mengatasi missing value

# Identifikasi missing value
colSums(is.na(datatesting))
## Lokasi     pH     DO    BOD    TSS   Suhu 
##      0      0      8      9      7      0
# menangani missing value
for (col in names(datatesting)) {
  if (is.numeric(datatesting[[col]])) {
    datatesting[[col]][is.na(datatesting[[col]])] <- median(datatesting[[col]], na.rm = TRUE)
  } else {
    mode_val <- names(sort(table(datatesting[[col]]), decreasing = TRUE))[1]
    datatesting[[col]][is.na(datatesting[[col]])] <- mode_val
  }
}
# Identifikasi missing value 
colSums(is.na(datatesting))
## Lokasi     pH     DO    BOD    TSS   Suhu 
##      0      0      0      0      0      0

Mengatasi outlier

numeric_cols <- sapply(datatesting, is.numeric)
for (col in names(datatesting)[numeric_cols]) {
  Q1 <- quantile(datatesting[[col]], 0.25, na.rm = TRUE)
  Q3 <- quantile(datatesting[[col]], 0.75, na.rm = TRUE)
  IQR_val <- Q3 - Q1
  batas_bawah <- Q1 - 1.5 * IQR_val
  batas_atas <- Q3 + 1.5 * IQR_val
  outlier_count <- sum(datatesting[[col]] < batas_bawah | datatesting[[col]] > batas_atas, na.rm = TRUE)
  cat(col, ": jumlah outlier =", outlier_count, "\n")
}
## pH : jumlah outlier = 2 
## DO : jumlah outlier = 3 
## BOD : jumlah outlier = 2 
## TSS : jumlah outlier = 2 
## Suhu : jumlah outlier = 1
# Visualisasi outlier dengan box Plot
datatesting %>% 
  pivot_longer(cols = names(training)[numeric_cols], names_to = "variabel", values_to = "nilai") %>%
  ggplot(aes(x = variabel, y = nilai)) +
  geom_boxplot(fill = "skyblue") +
  theme_minimal() +
  coord_flip()

# Menangani outlier
for (col in names(datatesting)[numeric_cols]) {
  Q1 <- quantile(datatesting[[col]], 0.25, na.rm = TRUE)
  Q3 <- quantile(datatesting[[col]], 0.75, na.rm = TRUE)
  IQR_val <- Q3 - Q1
  batas_bawah <- Q1 - 1.5 * IQR_val
  batas_atas <- Q3 + 1.5 * IQR_val
  datatesting[[col]] <- ifelse(datatesting[[col]] < batas_bawah, batas_bawah,
                        ifelse(datatesting[[col]] > batas_atas, batas_atas, datatesting[[col]]))
}

Lakukan standarisasi penulisan kategori Status

for (col in "Status") {
  cat("\nKolom:", col, "\n")
  print(table(training[[col]], useNA = "ifany"))
}
## 
## Kolom: Status 
## 
##            baik            Baik            BAIK  Tercemar berat tercemar ringan 
##               1              70               1               7               1 
## Tercemar ringan Tercemar Ringan 
##             219               1
# normalisasi huruf & trimming spasi
training <- training %>%
  mutate(across(all_of(col), ~trimws(tolower(.))))

Tampilkan ringkasan statistik deskriptif setelah pembersihan.

# Ringkasan umum semua kolom
summary(training)
##     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
# Ringkasan numerik saja
training %>%
  select(where(is.numeric)) %>%
  summary()
##        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
# Ringkasan kategorik
training %>%
  select(where(is.character)) %>%
  summarise(across(everything(), ~paste(unique(.), collapse = ", ")))
## # A tibble: 1 × 2
##   Lokasi                                                                  Status
##   <chr>                                                                   <chr> 
## 1 S1, S2, S3, S4, S5, S6, S7, S8, S9, S10, S11, S12, S13, S14, S15, S16,… terce…
# Distribusi frekuensi Status
cat("\nDistribusi Kategori Status:\n")
## 
## Distribusi Kategori Status:
print(table(training$Status, useNA = "ifany"))
## 
##            baik  tercemar berat tercemar ringan 
##              72               7             221
training %>%
  summarise(across(where(is.numeric),
                   list(mean = ~mean(., na.rm = TRUE),
                        sd = ~sd(., na.rm = TRUE),
                        min = ~min(., na.rm = TRUE),
                        max = ~max(., na.rm = TRUE))))
## # A tibble: 1 × 20
##   pH_mean pH_sd pH_min pH_max DO_mean DO_sd DO_min DO_max BOD_mean BOD_sd
##     <dbl> <dbl>  <dbl>  <dbl>   <dbl> <dbl>  <dbl>  <dbl>    <dbl>  <dbl>
## 1    6.99 0.491   5.70   8.29    5.98 0.946   3.61   8.41     3.00  0.796
## # ℹ 10 more variables: BOD_min <dbl>, BOD_max <dbl>, TSS_mean <dbl>,
## #   TSS_sd <dbl>, TSS_min <dbl>, TSS_max <dbl>, Suhu_mean <dbl>, Suhu_sd <dbl>,
## #   Suhu_min <dbl>, Suhu_max <dbl>
library(ggplot2)

ggplot(training, aes(x = Status)) +
  geom_bar(fill = "skyblue") +
  theme_minimal() +
  labs(title = "Distribusi Kategori Status", x = "Status", y = "Jumlah")

Klasifikasi Status Kualitas Air

library(caret)
## Loading required package: lattice
library(e1071)
## 
## Attaching package: 'e1071'
## The following object is masked from 'package:ggplot2':
## 
##     element
library(rpart)
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:ggplot2':
## 
##     margin
## The following object is masked from 'package:dplyr':
## 
##     combine
library(dplyr)
library(rpart.plot)

Split data

# Ambil variabel numerik dan target
data_model <- training %>%
  select(pH, DO, BOD, TSS, Suhu, Status)

# Pastikan Status bertipe faktor
data_model$Status <- as.factor(data_model$Status)

set.seed(123)
split <- createDataPartition(data_model$Status, p = 0.75, list = FALSE)
training <- data_model[split, ]
testing  <- data_model[-split, ]

Model SVM

preproc <- preProcess(training[, 1:5], method = c("center", "scale"))
training_scaled <- training
training_scaled[, 1:5] <- predict(preproc, training[, 1:5])

testing_scaled <- testing
testing_scaled[, 1:5] <- predict(preproc, testing[, 1:5])

## SVM
set.seed(123)
model_svm <- train(
  Status ~ pH + DO + BOD + TSS + Suhu,
  data = training_scaled,
  method = "svmLinear",
  trControl = trainControl(method = "cv", number = 5)
)

## Prediksi di data testing
pred_svm <- predict(model_svm, newdata = testing_scaled)

## Evaluasi model
cat("\n=== Evaluasi SVM ===\n")
## 
## === Evaluasi SVM ===
conf_svm <- confusionMatrix(pred_svm, testing_scaled$Status)
print(conf_svm)
## Confusion Matrix and Statistics
## 
##                  Reference
## Prediction        baik tercemar berat tercemar ringan
##   baik               6              0               6
##   tercemar berat     0              0               0
##   tercemar ringan   12              1              49
## 
## Overall Statistics
##                                           
##                Accuracy : 0.7432          
##                  95% CI : (0.6284, 0.8378)
##     No Information Rate : 0.7432          
##     P-Value [Acc > NIR] : 0.5613          
##                                           
##                   Kappa : 0.24            
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: baik Class: tercemar berat Class: tercemar ringan
## Sensitivity              0.33333               0.00000                 0.8909
## Specificity              0.89286               1.00000                 0.3158
## Pos Pred Value           0.50000                   NaN                 0.7903
## Neg Pred Value           0.80645               0.98649                 0.5000
## Prevalence               0.24324               0.01351                 0.7432
## Detection Rate           0.08108               0.00000                 0.6622
## Detection Prevalence     0.16216               0.00000                 0.8378
## Balanced Accuracy        0.61310               0.50000                 0.6033

Hasil evaluasi model SVM menunjukkan bahwa model memiliki tingkat akurasi sebesar 74,32%, yang berarti sekitar tiga perempat dari seluruh data uji berhasil diklasifikasikan dengan benar. Namun, nilai Kappa sebesar 0,24 menandakan bahwa kesesuaian antara hasil prediksi dan kondisi sebenarnya masih tergolong rendah, atau hanya sedikit lebih baik dibandingkan tebakan acak. Selain itu, nilai p-value sebesar 0,5613 menunjukkan bahwa akurasi model tidak jauh berbeda dari akurasi yang bisa dicapai hanya dengan menebak kelas dominan, sehingga secara statistik model belum signifikan lebih baik.

Jika dilihat berdasarkan masing-masing kelas, model cukup baik dalam mengenali kategori “tercemar ringan” dengan sensitivitas 0,89 dan precision 0,79, meskipun nilai specificity-nya hanya 0,32, yang menandakan adanya kecenderungan model untuk mengklasifikasikan banyak data ke dalam kelas ini. Hal ini sejalan dengan distribusi data, di mana kelas “tercemar ringan” merupakan kelas yang paling dominan. Untuk kelas “baik”, model hanya mampu mendeteksi sekitar 33% dari data yang seharusnya termasuk ke dalam kategori tersebut, meskipun cukup baik dalam menghindari kesalahan klasifikasi ke kelas lain (specificity 0,89). Sementara itu, pada kelas “tercemar berat”, model tidak mampu mengenali satupun data dengan benar (sensitivitas 0,00), menandakan bahwa SVM gagal mempelajari pola dari kelas ini kemungkinan besar karena jumlah datanya yang sangat sedikit.

Model Decision Tree

# Bangun model Decision Tree
set.seed(123)
model_tree <- rpart(
  Status ~ pH + DO + BOD + TSS + Suhu,
  data = training,
  method = "class",
  control = rpart.control(cp = 0.05)  # cp = complexity parameter
)

# Visualisasi pohon keputusan
rpart.plot(model_tree, type = 3, extra = 101, fallen.leaves = TRUE, main = "Decision Tree Kualitas Air")

# Prediksi di data testing
pred_tree <- predict(model_tree, newdata = testing, type = "class")

# Evaluasi performa model
conf_tree <- confusionMatrix(pred_tree, testing$Status)
print(conf_tree)
## Confusion Matrix and Statistics
## 
##                  Reference
## Prediction        baik tercemar berat tercemar ringan
##   baik              15              0               2
##   tercemar berat     0              0               0
##   tercemar ringan    3              1              53
## 
## Overall Statistics
##                                           
##                Accuracy : 0.9189          
##                  95% CI : (0.8318, 0.9697)
##     No Information Rate : 0.7432          
##     P-Value [Acc > NIR] : 0.0001203       
##                                           
##                   Kappa : 0.7818          
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: baik Class: tercemar berat Class: tercemar ringan
## Sensitivity               0.8333               0.00000                 0.9636
## Specificity               0.9643               1.00000                 0.7895
## Pos Pred Value            0.8824                   NaN                 0.9298
## Neg Pred Value            0.9474               0.98649                 0.8824
## Prevalence                0.2432               0.01351                 0.7432
## Detection Rate            0.2027               0.00000                 0.7162
## Detection Prevalence      0.2297               0.00000                 0.7703
## Balanced Accuracy         0.8988               0.50000                 0.8766
# Lihat akurasi
cat("\nAkurasi Decision Tree:", round(conf_tree$overall["Accuracy"], 3), "\n")
## 
## Akurasi Decision Tree: 0.919

Hasil evaluasi model Decision Tree menunjukkan bahwa model memiliki akurasi tinggi sebesar 91,89%, dengan interval kepercayaan 95% antara 83,18% hingga 96,97%, dan p-value 0,00012 menunjukkan bahwa akurasi model secara signifikan lebih baik daripada tebakan berdasarkan kelas dominan (No Information Rate 74,32%). Nilai Kappa 0,7818 mengindikasikan kesesuaian prediksi model dengan kondisi sebenarnya tergolong kuat, sehingga model dapat dikatakan memiliki performa yang andal secara keseluruhan.

Dilihat dari analisis per kelas, model mampu mengenali kelas “baik” dengan baik (sensitivitas 0,83, precision 0,88) dan juga cukup baik dalam menghindari kesalahan klasifikasi ke kelas lain (specificity 0,96). Kelas “tercemar ringan” bahkan lebih berhasil dikenali (sensitivitas 0,96, precision 0,93), meskipun specificity-nya lebih rendah (0,79), yang menunjukkan beberapa data dari kelas lain masih terprediksi sebagai “tercemar ringan”. Namun, kelas “tercemar berat” tetap menjadi tantangan, karena model tidak berhasil mendeteksi satupun data dari kelas ini (sensitivitas 0,00), meskipun specificity-nya sempurna (1,00), menandakan tidak ada kesalahan klasifikasi data lain ke kelas ini.

Secara keseluruhan, Decision Tree menunjukkan performa yang sangat baik secara umum, terutama pada kelas “baik” dan “tercemar ringan”, namun masih perlu perbaikan untuk mendeteksi kelas minoritas “tercemar berat”, misalnya melalui penyeimbangan data, penyesuaian parameter tree, atau teknik boosting agar semua kelas dapat dikenali secara lebih merata.

Model Random Forest

# Bangun model Random Forest
set.seed(123)
model_rf <- randomForest(
  Status ~ pH + DO + BOD + TSS + Suhu,
  data = training,
  ntree = 500,        # jumlah pohon
  mtry = 3,           # jumlah variabel acak per pohon
  importance = TRUE   # biar bisa lihat variabel paling berpengaruh
)

# menampilkan ringkasan model
print(model_rf)
## 
## Call:
##  randomForest(formula = Status ~ pH + DO + BOD + TSS + Suhu, data = training,      ntree = 500, mtry = 3, importance = TRUE) 
##                Type of random forest: classification
##                      Number of trees: 500
## No. of variables tried at each split: 3
## 
##         OOB estimate of  error rate: 4.42%
## Confusion matrix:
##                 baik tercemar berat tercemar ringan class.error
## baik              50              0               4 0.074074074
## tercemar berat     0              1               5 0.833333333
## tercemar ringan    0              1             165 0.006024096
# Evaluasi performa pada data testing
pred_rf <- predict(model_rf, newdata = testing)

conf_rf <- confusionMatrix(pred_rf, testing$Status)
print(conf_rf)
## Confusion Matrix and Statistics
## 
##                  Reference
## Prediction        baik tercemar berat tercemar ringan
##   baik              15              0               2
##   tercemar berat     0              1               0
##   tercemar ringan    3              0              53
## 
## Overall Statistics
##                                           
##                Accuracy : 0.9324          
##                  95% CI : (0.8493, 0.9777)
##     No Information Rate : 0.7432          
##     P-Value [Acc > NIR] : 2.87e-05        
##                                           
##                   Kappa : 0.8229          
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: baik Class: tercemar berat Class: tercemar ringan
## Sensitivity               0.8333               1.00000                 0.9636
## Specificity               0.9643               1.00000                 0.8421
## Pos Pred Value            0.8824               1.00000                 0.9464
## Neg Pred Value            0.9474               1.00000                 0.8889
## Prevalence                0.2432               0.01351                 0.7432
## Detection Rate            0.2027               0.01351                 0.7162
## Detection Prevalence      0.2297               0.01351                 0.7568
## Balanced Accuracy         0.8988               1.00000                 0.9029
# Akurasi
cat("\nAkurasi Random Forest:", round(conf_rf$overall["Accuracy"], 3), "\n")
## 
## Akurasi Random Forest: 0.932
importance(model_rf)
##           baik tercemar berat tercemar ringan MeanDecreaseAccuracy
## pH   -1.741323      0.3514679      -1.1585749            -1.730003
## DO   83.255958     13.2402293      74.7870724            98.122494
## BOD  69.565794     15.5795929      74.4966015            87.405875
## TSS   3.009945     -1.6746405       0.3214843             1.520602
## Suhu -3.242164      1.3019204      -1.1474031            -2.259675
##      MeanDecreaseGini
## pH           2.527638
## DO          40.762923
## BOD         39.904209
## TSS          3.831115
## Suhu         3.397850
varImpPlot(model_rf, main = "Pentingnya Variabel dalam Model Random Forest")

Hasil evaluasi model Random Forest menunjukkan performa yang sangat baik dengan akurasi keseluruhan sebesar 93,24% dan interval kepercayaan 95% antara 84,93% hingga 97,77%, sementara p-value 2,87e-05 menunjukkan bahwa akurasi ini secara signifikan lebih baik dibandingkan tebakan berdasarkan kelas dominan (No Information Rate 74,32%). Nilai Kappa 0,8229 mengindikasikan bahwa prediksi model sangat konsisten dengan kondisi sebenarnya, sehingga Random Forest dapat dikatakan sangat andal untuk klasifikasi ini.

Jika dilihat berdasarkan kelas, model mampu mendeteksi kelas “baik” dengan baik (sensitivitas 0,83, precision 0,88) dan mampu membedakan dengan kelas lain (specificity 0,96). Untuk kelas “tercemar berat”, model berhasil sempurna dalam mendeteksi seluruh data yang termasuk kelas ini (sensitivitas 1,00, precision 1,00, specificity 1,00), meskipun jumlah datanya sedikit, menunjukkan bahwa Random Forest sangat efektif menangani kelas minoritas. Kelas “tercemar ringan” juga dikenali dengan baik (sensitivitas 0,96, precision 0,95, specificity 0,84), meskipun beberapa data dari kelas lain masih terprediksi sebagai kelas ini.

Analisis pentingnya variabel menunjukkan bahwa DO (dissolved oxygen) dan BOD memiliki kontribusi terbesar dalam klasifikasi, baik dilihat dari Mean Decrease Accuracy maupun Mean Decrease Gini, menandakan bahwa kedua parameter ini paling menentukan kualitas air dalam model. Variabel TSS dan Suhu memiliki pengaruh yang lebih kecil, sedangkan pH tampak kurang berpengaruh atau bahkan memberikan kontribusi negatif pada beberapa kelas.

Prediksi Data Testing Dengan SVM

library(writexl)
prediksi_baru <- predict(model_svm, newdata = datatesting)

print(prediksi_baru)
##  [1] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
##  [5] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
##  [9] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [13] tercemar ringan tercemar ringan baik            tercemar ringan
## [17] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [21] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [25] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [29] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [33] tercemar ringan baik            tercemar ringan tercemar ringan
## [37] baik            tercemar ringan tercemar ringan tercemar ringan
## [41] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [45] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [49] tercemar ringan tercemar ringan tercemar ringan baik           
## [53] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [57] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [61] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [65] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [69] tercemar ringan tercemar ringan tercemar ringan tercemar ringan
## [73] tercemar ringan tercemar ringan tercemar ringan
## Levels: baik tercemar berat tercemar ringan
# Gabungkan hasil dengan data aslinya 
hasil_prediksi <- cbind(datatesting, Prediksi = prediksi_baru)

# Simpan ke Excel 
write_xlsx(hasil_prediksi, "hasil_prediksi_SVM.xlsx")

Prediksi Data Testing Decision Tree

# Prediksi pakai model Decision Tree
prediksi_tree <- predict(model_tree, newdata = datatesting, type = "class")

# Gabungkan hasil dengan data asli
hasil_prediksi_tree <- cbind(datatesting, Prediksi_Status = prediksi_tree)

# Simpan ke Excel
write_xlsx(hasil_prediksi_tree, "hasil_prediksi_decision_tree.xlsx")

Prediksi Data Testing Random Forest

# Prediksi Status menggunakan model Random Forest
prediksi_rf <- predict(model_rf, newdata = datatesting)

# Gabungkan hasil dengan data asli
hasil_prediksi_rf <- cbind(datatesting, Prediksi_Status = prediksi_rf)

# Simpan hasil ke file Excel
write_xlsx(hasil_prediksi_rf, "hasil_prediksi_random_forest.xlsx")

Prediksi Variabel DO

Model SVR

# Pastikan library aktif
library(caret)
library(splines)
library(ggplot2)

# Gunakan data training yang sudah ada
set.seed(666)

# Model Regresi Linear
model_lm <- lm(DO ~ pH + BOD + TSS + Suhu, data = training)
summary(model_lm)
## 
## Call:
## lm(formula = DO ~ pH + BOD + TSS + Suhu, data = training)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.39717 -0.50827 -0.00661  0.67022  2.40421 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)    
## (Intercept)  5.819259   1.382750   4.208 3.74e-05 ***
## pH           0.017234   0.136645   0.126    0.900    
## BOD          0.102284   0.079421   1.288    0.199    
## TSS          0.001482   0.007114   0.208    0.835    
## Suhu        -0.011727   0.030713  -0.382    0.703    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.9704 on 221 degrees of freedom
## Multiple R-squared:  0.00797,    Adjusted R-squared:  -0.009985 
## F-statistic: 0.4439 on 4 and 221 DF,  p-value: 0.7768

Hasil analisis regresi linier untuk memodelkan DO (dissolved oxygen) berdasarkan variabel pH, BOD, TSS, dan Suhu menunjukkan bahwa model tidak mampu menjelaskan variasi DO secara signifikan. Hal ini tercermin dari nilai R-squared sebesar 0,00797 dan adjusted R-squared negatif (-0,009985), yang menunjukkan bahwa kurang dari 1% variasi DO dapat dijelaskan oleh keempat prediktor tersebut. Nilai F-statistic 0,4439 dengan p-value 0,7768 juga menegaskan bahwa secara keseluruhan model tidak signifikan secara statistik.

Secara individual, koefisien variabel pH (0,0172), BOD (0,1023), TSS (0,0015), dan Suhu (-0,0117) semuanya memiliki p-value lebih besar dari 0,05, sehingga tidak ada satupun variabel yang memiliki pengaruh signifikan terhadap DO dalam dataset ini. Residual model menunjukkan distribusi yang relatif simetris dengan median mendekati nol (-0,0066) dan residual standard error 0,9704, namun karena variabel prediktor tidak signifikan, model ini tidak dapat digunakan untuk prediksi atau interpretasi hubungan yang bermakna antara DO dan faktor-faktor lingkungan yang diteliti.

Secara keseluruhan, model regresi linier sederhana ini tidak cocok untuk memprediksi DO, dan diperlukan pendekatan lain, seperti model non-linear, machine learning, atau penambahan variabel prediktor lain yang lebih relevan dengan kualitas air.

Model Regresi Spline

# Model Regresi Spline 
model_spline <- lm(
  DO ~ bs(pH, df = 3) + bs(BOD, df = 3) + bs(TSS, df = 3) + bs(Suhu, df = 3),
  data = training
)
summary(model_spline)
## 
## 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.46230 -0.55490  0.06861  0.61789  2.58778 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)    
## (Intercept)         5.7776     0.9548   6.051  6.4e-09 ***
## bs(pH, df = 3)1     0.5110     1.2455   0.410  0.68202    
## bs(pH, df = 3)2    -0.2893     0.5997  -0.482  0.62997    
## bs(pH, df = 3)3     0.5855     0.8380   0.699  0.48553    
## bs(BOD, df = 3)1    2.6487     1.0375   2.553  0.01138 *  
## bs(BOD, df = 3)2   -0.4990     0.5720  -0.872  0.38399    
## bs(BOD, df = 3)3    2.0147     0.7033   2.865  0.00459 ** 
## bs(TSS, df = 3)1   -0.8436     0.9704  -0.869  0.38562    
## bs(TSS, df = 3)2   -0.2036     0.5675  -0.359  0.72007    
## bs(TSS, df = 3)3   -0.2172     0.6376  -0.341  0.73368    
## bs(Suhu, df = 3)1  -1.1733     1.0834  -1.083  0.28006    
## bs(Suhu, df = 3)2  -0.4977     0.5573  -0.893  0.37287    
## bs(Suhu, df = 3)3  -0.6150     0.7124  -0.863  0.38893    
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 0.9636 on 213 degrees of freedom
## Multiple R-squared:  0.05717,    Adjusted R-squared:  0.004048 
## F-statistic: 1.076 on 12 and 213 DF,  p-value: 0.3816

Hasil analisis regresi spline (menggunakan basis splines dengan df = 3 untuk masing-masing variabel pH, BOD, TSS, dan Suhu) menunjukkan bahwa model hanya sedikit mampu menjelaskan variasi DO. Multiple R-squared sebesar 0,05717 dan adjusted R-squared 0,004048 mengindikasikan bahwa hanya sekitar 5,7% variasi DO yang dijelaskan oleh model, dan setelah disesuaikan untuk jumlah prediktor, kontribusi efektifnya hampir nihil. F-statistic 1,076 dengan p-value 0,3816 menunjukkan bahwa secara keseluruhan, model tidak signifikan secara statistik, sehingga prediktor spline secara kolektif tidak memberikan pengaruh yang jelas terhadap DO.

Secara individu, hanya beberapa koefisien spline dari BOD yang signifikan (bs(BOD, df=3)1 dengan p=0,01138 dan bs(BOD, df=3)3 dengan p=0,00459), sedangkan semua komponen spline dari pH, TSS, dan Suhu tidak signifikan (p-value > 0,05). Ini menunjukkan bahwa BOD mungkin memiliki hubungan non-linear tertentu dengan DO, tetapi variabel lainnya tidak berkontribusi signifikan dalam model ini. Residual model menunjukkan residual standard error 0,9636 dan distribusi residual yang masih relatif simetris, tetapi karena sebagian besar prediktor tidak signifikan, model spline ini tidak cukup baik untuk prediksi DO.

Kesimpulannya, pendekatan regresi spline menunjukkan sedikit peningkatan fleksibilitas dibanding regresi linier sederhana, tetapi tetap kurang efektif, sehingga diperlukan data tambahan, variabel lain, atau metode non-linear/machine learning yang lebih kompleks untuk memodelkan DO dengan lebih akurat.

Evaluasi Performa Model

# Prediksi di data testing
pred_lm <- predict(model_lm, newdata = testing)
pred_spline <- predict(model_spline, newdata = testing)

# Fungsi untuk menghitung metrik
mse <- function(actual, pred) mean((actual - pred)^2)
rmse <- function(actual, pred) sqrt(mse(actual, pred))
r2 <- function(actual, pred) cor(actual, pred)^2

# Hitung performa
perf <- data.frame(
  Model = c("Regresi Linear", "Regresi Spline"),
  R2 = c(r2(testing$DO, pred_lm), r2(testing$DO, pred_spline)),
  MSE = c(mse(testing$DO, pred_lm), mse(testing$DO, pred_spline)),
  RMSE = c(rmse(testing$DO, pred_lm), rmse(testing$DO, pred_spline))
)
print(perf)
##            Model           R2       MSE      RMSE
## 1 Regresi Linear 2.963834e-05 0.7839207 0.8853929
## 2 Regresi Spline 2.751446e-05 0.8179485 0.9044051

Hasil perbandingan kinerja kedua model menunjukkan bahwa regresi linier dan regresi spline memiliki performa yang sangat rendah dalam memprediksi DO. Nilai R² untuk regresi linier (2,96×10⁻⁵) dan regresi spline (2,75×10⁻⁵) hampir nol, menunjukkan bahwa hampir tidak ada variasi DO yang dijelaskan oleh variabel prediktor pada kedua model.

Untuk metrik kesalahan, regresi linier memiliki MSE 0,784 dan RMSE 0,885, sedangkan regresi spline sedikit lebih tinggi dengan MSE 0,818 dan RMSE 0,904, menandakan bahwa prediksi spline justru sedikit lebih meleset dibanding regresi linier. Secara keseluruhan, kedua model ini tidak cocok untuk prediksi DO, dan perbedaan kinerjanya sangat kecil, sehingga perlu pendekatan lain, seperti model non-linear lebih kompleks atau metode machine learning, untuk meningkatkan akurasi prediksi.

Visualisasi Prediksi dan Aktual

# Gabungkan hasil prediksi
df_plot <- data.frame(
  Actual = testing$DO,
  Linear = pred_lm,
  Spline = pred_spline
)

# Visualisasi
ggplot(df_plot, aes(x = Actual)) +
  geom_point(aes(y = Linear, color = "Linear"), alpha = 0.6) +
  geom_point(aes(y = Spline, color = "Spline"), alpha = 0.6) +
  geom_abline(slope = 1, intercept = 0, linetype = "dashed") +
  labs(title = "Prediksi vs Aktual DO",
       x = "Nilai DO Aktual",
       y = "Nilai DO Prediksi",
       color = "Model") +
  theme_minimal()