# TAHAP 1: DATA CLEANING & EKSPLORASI
train_file <- "C:/Users/User/Downloads/kualitasair.xlsx - Training.csv"
test_file  <- "C:/Users/User/Downloads/kualitasair.xlsx - Testing.csv"

library(readr)

# Baca data dari file CSV
train <- read_csv(train_file)
## Rows: 300 Columns: 7
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (2): Lokasi, Status
## dbl (5): pH, DO, BOD, TSS, Suhu
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
test  <- read_csv(test_file)
## Rows: 75 Columns: 6
## ── Column specification ────────────────────────────────────────────────────────
## Delimiter: ","
## chr (1): Lokasi
## dbl (5): pH, DO, BOD, TSS, Suhu
## 
## ℹ Use `spec()` to retrieve the full column specification for this data.
## ℹ Specify the column types or set `show_col_types = FALSE` to quiet this message.
# Cek struktur awal data
str(train)
## spc_tbl_ [300 × 7] (S3: spec_tbl_df/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" ...
##  - attr(*, "spec")=
##   .. cols(
##   ..   Lokasi = col_character(),
##   ..   pH = col_double(),
##   ..   DO = col_double(),
##   ..   BOD = col_double(),
##   ..   TSS = col_double(),
##   ..   Suhu = col_double(),
##   ..   Status = col_character()
##   .. )
##  - attr(*, "problems")=<externalptr>
head(train)
## # 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
# Tahap 2 Buat salinan data
df_clean <- train

# Tangani missing value numerik dengan median
num_vars <- c("pH", "DO", "BOD", "TSS", "Suhu")
for (v in num_vars) {
  med <- median(df_clean[[v]], na.rm = TRUE)
  df_clean[[v]][is.na(df_clean[[v]])] <- med
}

# Tangani missing pada variabel kategorik (Status)
modus_status <- names(sort(table(df_clean$Status), decreasing = TRUE))[1]
df_clean$Status[is.na(df_clean$Status)] <- modus_status
#Tahap 3mendeteksi outlier dengan metode IQR
detect_outliers <- function(x) {
  Q1 <- quantile(x, 0.24, na.rm = TRUE)
  Q3 <- quantile(x, 0.76, na.rm = TRUE)
  IQR_val <- Q3 - Q1
  lower <- Q1 - 1.5 * IQR_val
  upper <- Q3 + 1.5 * IQR_val
  return(which(x < lower | x > upper))
}

# Hitung jumlah outlier per variabel
sapply(df_clean[, num_vars], function(x) length(detect_outliers(x)))
##   pH   DO  BOD  TSS Suhu 
##    2    2    4    5    2
# Tahap 4 Menangani Outlier
for (v in num_vars) {
  Q1 <- quantile(df_clean[[v]], 0.25, na.rm = TRUE)
  Q3 <- quantile(df_clean[[v]], 0.75, na.rm = TRUE)
  IQR_val <- Q3 - Q1
  lower <- Q1 - 1.5 * IQR_val
  upper <- Q3 + 1.5 * IQR_val
  df_clean[[v]][df_clean[[v]] < lower] <- lower
  df_clean[[v]][df_clean[[v]] > upper] <- upper
}
# Tahap 5 Menangani Kekonsistenan kategori Pada status 
df_clean$Status <- trimws(tolower(df_clean$Status))

# Mapping kategori
df_clean$Status <- ifelse(df_clean$Status %in% c("1","baik","baik=1","1=baik"),
                          "Baik",
                   ifelse(df_clean$Status %in% c("2","tercemar ringan","2=tercemar ringan"),
                          "Tercemar Ringan",
                   ifelse(df_clean$Status %in% c("3","tercemar berat","3=tercemar berat"),
                          "Tercemar Berat",
                          NA)))

# Jadikan faktor
df_clean$Status <- factor(df_clean$Status, 
                          levels = c("Baik","Tercemar Ringan","Tercemar Berat"))
# Tahap 6 Mengecek Hasil Akhir Pembersihan
summary(df_clean)
##     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   Baik           : 72  
##  1st Qu.:44.28   1st Qu.:26.62   Tercemar Ringan:221  
##  Median :49.52   Median :28.01   Tercemar Berat :  7  
##  Mean   :49.68   Mean   :28.12                        
##  3rd Qu.:55.62   3rd Qu.:29.46                        
##  Max.   :72.62   Max.   :33.73
table(df_clean$Status)
## 
##            Baik Tercemar Ringan  Tercemar Berat 
##              72             221               7

Pada tahap data cleaning, dilakukan identifikasi terhadap missing value, outlier, dan inkonsistensi kategori. Missing value pada variabel numerik (pH, DO, BOD, TSS, Suhu) diimputasi menggunakan median untuk menjaga kestabilan nilai tengah terhadap pengaruh outlier. Untuk variabel kategorik Status, missing value diisi dengan modus atau kategori yang paling sering muncul. Deteksi outlier dilakukan menggunakan metode IQR (Interquartile Range), dan nilai ekstrem dikoreksi menggunakan winsorizing agar tetap berada dalam batas wajar. Selanjutnya, kategori Status distandarisasi menjadi tiga kelas utama, yaitu Baik, Tercemar Ringan, dan Tercemar Berat. Hasil pembersihan menunjukkan data bebas dari nilai kosong dan inkonsistensi kategori. Pada tahap pembersihan data, dilakukan pemeriksaan terhadap missing value, inkonsistensi kategori, dan keberadaan outlier. Berdasarkan hasil eksplorasi awal, tidak ditemukan nilai kosong (missing value) pada seluruh variabel numerik maupun kategorik. Kategori pada variabel Status telah terstandarisasi menjadi tiga kelas utama yaitu Baik, Tercemar Ringan, dan Tercemar Berat, dengan proporsi dominan pada kategori Tercemar Ringan (221 observasi). Berdasarkan rentang nilai setiap variabel, sebagian besar parameter berada pada kisaran normal untuk kualitas air sungai, namun nilai BOD maksimum (15.14 mg/L) terindikasi sebagai outlier ringan. Nilai tersebut akan ditangani melalui metode winsorizing agar tidak memengaruhi analisis model selanjutnya.

# Salin data hasil cleaning sebelumnya
df_winsor <- train

# Tentukan variabel numerik
num_vars <- c("pH", "DO", "BOD", "TSS", "Suhu")

# Fungsi untuk winsorizing
winsorize <- function(x){
  Q1 <- quantile(x, 0.25, na.rm = TRUE)
  Q3 <- quantile(x, 0.75, na.rm = TRUE)
  IQR_val <- Q3 - Q1
  lower <- Q1 - 1.5 * IQR_val
  upper <- Q3 + 1.5 * IQR_val
  
  # Ganti nilai di luar batas dengan batasnya
  x[x < lower] <- lower
  x[x > upper] <- upper
  return(x)
}

# Terapkan ke semua variabel numerik
df_winsor[num_vars] <- lapply(df_winsor[num_vars], winsorize)

# Cek kembali ringkasan setelah winsorizing
summary(df_winsor[num_vars])
##        pH              DO             BOD              TSS       
##  Min.   :5.697   Min.   :3.407   Min.   :0.5261   Min.   :24.65  
##  1st Qu.:6.670   1st Qu.:5.375   1st Qu.:2.3573   1st Qu.:43.73  
##  Median :6.988   Median :5.991   Median :3.0661   Median :49.52  
##  Mean   :6.990   Mean   :5.976   Mean   :2.9994   Mean   :49.70  
##  3rd Qu.:7.318   3rd Qu.:6.688   3rd Qu.:3.5781   3rd Qu.:56.44  
##  Max.   :8.290   Max.   :8.656   Max.   :5.4093   Max.   :75.52  
##                  NA's   :23      NA's   :22       NA's   :24     
##       Suhu      
##  Min.   :22.77  
##  1st Qu.:26.62  
##  Median :28.01  
##  Mean   :28.12  
##  3rd Qu.:29.46  
##  Max.   :33.73  
## 

Penanganan dilakukan dengan teknik winsorizing, yaitu mengganti nilai di luar batas tersebut dengan nilai batas bawah atau batas atasnya. Hasil analisis menunjukkan bahwa variabel BOD mengalami sedikit penyesuaian karena memiliki nilai ekstrem, sementara variabel lain tetap dalam rentang wajar. Langkah ini bertujuan agar distribusi data lebih stabil dan tidak dipengaruhi nilai ekstrim pada tahap pemodelan berikutnya.

library(ggplot2)

# Contoh: visualisasi BOD sebelum dan sesudah winsor
par(mfrow = c(1,2))
boxplot(train$BOD, main = "BOD Sebelum Winsorizing")
boxplot(df_winsor$BOD, main = "BOD Sesudah Winsorizing")

par(mfrow = c(2,3))
for(v in num_vars){
  boxplot(df_winsor[[v]], main = paste("Sesudah Winsor:", v))
}

#Soal 1 Standarisasi Penilisan Kategori Status
df_final <- df_winsor

#a. Ubah semua huruf jadi lowercase dan hilangkan spasi tambahan
df_final$Status <- tolower(trimws(df_final$Status))

#b.  Ganti berbagai bentuk ejaan agar jadi tiga kategori utama
df_final$Status <- ifelse(df_final$Status %in% c("1", "baik", "baik=1", "1=baik"),
                          "Baik",
                   ifelse(df_final$Status %in% c("2", "tercemar ringan", "2=tercemar ringan", "ringan"),
                          "Tercemar Ringan",
                   ifelse(df_final$Status %in% c("3", "tercemar berat", "3=tercemar berat", "berat"),
                          "Tercemar Berat",
                          NA))) 

#c. Jadikan faktor dengan urutan level yang jelas
df_final$Status <- factor(df_final$Status,
                          levels = c("Baik", "Tercemar Ringan", "Tercemar Berat"))

# d.  Cek hasil standarisasi
table(df_final$Status)
## 
##            Baik Tercemar Ringan  Tercemar Berat 
##              72             221               7
summary(df_final$Status)
##            Baik Tercemar Ringan  Tercemar Berat 
##              72             221               7

Penulisan kategori pada variabel Status distandarisasi agar konsisten dan mudah diolah secara statistik. Proses standarisasi dilakukan dengan mengubah seluruh teks menjadi huruf kecil, menghapus spasi berlebih, dan memetakan berbagai bentuk penulisan yang serupa menjadi tiga kategori utama, yaitu Baik, Tercemar Ringan, dan Tercemar Berat. Setelah standarisasi, variabel Status dikonversi menjadi faktor dengan urutan level yang tetap. Hasil akhir menunjukkan bahwa seluruh observasi telah terklasifikasi ke dalam tiga kategori utama tanpa ada nilai yang tidak terdefinisi. Setelah proses pembersihan data, dilakukan standarisasi penulisan kategori pada variabel Status untuk memastikan konsistensi antarobservasi. Seluruh variasi ejaan dan simbol (misalnya “1”, “baik=1”, atau “tercemar ringan”) diseragamkan menjadi tiga kategori utama, yaitu Baik, Tercemar Ringan, dan Tercemar Berat. Hasil akhir menunjukkan distribusi kategori yang seimbang secara format, dengan jumlah masing-masing: Baik sebanyak 72 observasi, Tercemar Ringan sebanyak 221 observasi, dan Tercemar Berat sebanyak 7 observasi. Tidak ditemukan nilai kategori yang tidak dikenali (NA).

# Statistik Deskriptif Setelah Pembersihan
num_vars <- c("pH", "DO", "BOD", "TSS", "Suhu")

# a.  Statistik deskriptif umum (seluruh data)
summary(df_final[num_vars])
##        pH              DO             BOD              TSS       
##  Min.   :5.697   Min.   :3.407   Min.   :0.5261   Min.   :24.65  
##  1st Qu.:6.670   1st Qu.:5.375   1st Qu.:2.3573   1st Qu.:43.73  
##  Median :6.988   Median :5.991   Median :3.0661   Median :49.52  
##  Mean   :6.990   Mean   :5.976   Mean   :2.9994   Mean   :49.70  
##  3rd Qu.:7.318   3rd Qu.:6.688   3rd Qu.:3.5781   3rd Qu.:56.44  
##  Max.   :8.290   Max.   :8.656   Max.   :5.4093   Max.   :75.52  
##                  NA's   :23      NA's   :22       NA's   :24     
##       Suhu      
##  Min.   :22.77  
##  1st Qu.:26.62  
##  Median :28.01  
##  Mean   :28.12  
##  3rd Qu.:29.46  
##  Max.   :33.73  
## 
# b. Statistik deskriptif per kategori Status
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
desc_by_status <- df_final %>%
  group_by(Status) %>%
  summarise(
    n = n(),
    mean_pH   = mean(pH, na.rm = TRUE),
    sd_pH     = sd(pH, na.rm = TRUE),
    mean_DO   = mean(DO, na.rm = TRUE),
    sd_DO     = sd(DO, na.rm = TRUE),
    mean_BOD  = mean(BOD, na.rm = TRUE),
    sd_BOD    = sd(BOD, na.rm = TRUE),
    mean_TSS  = mean(TSS, na.rm = TRUE),
    sd_TSS    = sd(TSS, na.rm = TRUE),
    mean_Suhu = mean(Suhu, na.rm = TRUE),
    sd_Suhu   = sd(Suhu, na.rm = TRUE)
  )

desc_by_status
## # A tibble: 3 × 12
##   Status           n mean_pH sd_pH mean_DO sd_DO mean_BOD sd_BOD mean_TSS sd_TSS
##   <fct>        <int>   <dbl> <dbl>   <dbl> <dbl>    <dbl>  <dbl>    <dbl>  <dbl>
## 1 Baik            72    6.95 0.487    6.66 0.449     2.37  0.404     48.8   8.94
## 2 Tercemar Ri…   221    7.00 0.494    5.75 0.957     3.15  0.797     49.9   9.70
## 3 Tercemar Be…     7    7.01 0.448    6.03 2.22      4.27  1.59      51.7  14.5 
## # ℹ 2 more variables: mean_Suhu <dbl>, sd_Suhu <dbl>

Setelah proses pembersihan, dilakukan analisis deskriptif untuk menggambarkan karakteristik data kualitas air. Secara umum, nilai pH air berada pada kisaran 5.7–8.3 dengan rata-rata sekitar 7, menunjukkan kondisi netral. Nilai DO (Dissolved Oxygen) rata-rata sekitar 6 mg/L yang menandakan kadar oksigen terlarut cukup baik. Rata-rata BOD (Biological Oxygen Demand) sebesar 3 mg/L menunjukkan tingkat pencemaran biologis masih tergolong ringan. TSS (Total Suspended Solid) berkisar 27–73 mg/L dan Suhu air antara 22–34°C, masih dalam rentang alami untuk sungai tropis. Ketika dilihat berdasarkan kategori Status, terlihat bahwa air dengan status Baik memiliki nilai DO lebih tinggi dan BOD lebih rendah dibandingkan kategori Tercemar Berat, sesuai dengan karakteristik umum kualitas air sungai. Setelah proses pembersihan dan standarisasi data, dilakukan analisis deskriptif terhadap variabel numerik yang merepresentasikan parameter kualitas air. Hasil ringkasan menunjukkan bahwa nilai pH berkisar antara 5.7–8.3 dengan rata-rata 6.99, menandakan air sungai berada pada kondisi netral. Nilai DO rata-rata 5.98 mg/L menunjukkan kadar oksigen terlarut yang relatif baik, sedangkan BOD rata-rata 3.00 mg/L menandakan tingkat pencemaran organik rendah hingga sedang. Nilai TSS berkisar antara 25–75 mg/L, yang masih dalam batas alami untuk sungai tropis, dan suhu air berkisar 22–34°C yang konsisten dengan iklim tropis. Berdasarkan kategori Status, ketiga kelompok (Baik, Tercemar Ringan, dan Tercemar Berat) memiliki nilai pH yang relatif sama, sehingga perbedaan status kualitas air kemungkinan lebih disebabkan oleh variabel lain seperti DO dan BOD.

# Soal 2 Gunakan variabel numerik (pH, DO, BOD, TSS, Suhu) untuk mengklasifikasikan Status.
data_class <- df_final %>%
  select(pH, DO, BOD, TSS, Suhu, Status)

# Cek struktur data
str(data_class)
## tibble [300 × 6] (S3: tbl_df/tbl/data.frame)
##  $ 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: Factor w/ 3 levels "Baik","Tercemar Ringan",..: 2 2 2 2 1 2 2 2 2 1 ...
# Pastikan kolom target (Status) bertipe faktor
data_class$Status <- factor(data_class$Status,
                            levels = c("Baik", "Tercemar Ringan", "Tercemar Berat"))

# Cek apakah ada missing value yang tersisa pada prediktor
colSums(is.na(data_class))
##     pH     DO    BOD    TSS   Suhu Status 
##      0     23     22     24      0      0
# melakukan imputasi sederhana (median)
for (v in c("pH", "DO", "BOD", "TSS", "Suhu")) {
  data_class[[v]][is.na(data_class[[v]])] <- median(data_class[[v]], na.rm = TRUE)
}

# Skala data numerik
data_scaled <- data_class
data_scaled[, c("pH","DO","BOD","TSS","Suhu")] <-
  scale(data_class[, c("pH","DO","BOD","TSS","Suhu")])

#Cek ringkasan hasil scaling
summary(data_scaled)
##        pH                  DO                BOD                TSS          
##  Min.   :-2.634307   Min.   :-2.70398   Min.   :-3.07297   Min.   :-2.71070  
##  1st Qu.:-0.652516   1st Qu.:-0.59357   1st Qu.:-0.67501   1st Qu.:-0.58464  
##  Median :-0.003563   Median : 0.01471   Median : 0.07669   Median :-0.01724  
##  Mean   : 0.000000   Mean   : 0.00000   Mean   : 0.00000   Mean   : 0.00000  
##  3rd Qu.: 0.668677   3rd Qu.: 0.66766   3rd Qu.: 0.65479   3rd Qu.: 0.64309  
##  Max.   : 2.650468   Max.   : 2.81884   Max.   : 2.98231   Max.   : 2.79822  
##       Suhu                      Status   
##  Min.   :-2.59006   Baik           : 72  
##  1st Qu.:-0.72906   Tercemar Ringan:221  
##  Median :-0.05156   Tercemar Berat :  7  
##  Mean   : 0.00000                        
##  3rd Qu.: 0.64870                        
##  Max.   : 2.71534

Pada tahap ini dilakukan pemilihan variabel prediktor yang akan digunakan untuk mengklasifikasikan status kualitas air, yaitu pH, DO, BOD, TSS, dan Suhu. Semua variabel prediktor bersifat numerik, sedangkan variabel target Status bertipe kategorik dengan tiga kelas: Baik, Tercemar Ringan, dan Tercemar Berat. Data kemudian diperiksa untuk memastikan tidak ada nilai kosong, dan dilakukan imputasi median untuk nilai yang hilang. Selanjutnya, dilakukan proses standarisasi (scaling) terhadap variabel numerik agar memiliki rata-rata 0 dan simpangan baku 1. Tahapan ini bertujuan agar seluruh prediktor berada pada skala yang sebanding sebelum dilakukan pemodelan klasifikasi.

# Soal 2 point 2 Membagi data training dan testing 
if(!require(caret)) install.packages("caret", dependencies = TRUE)
## Loading required package: caret
## Loading required package: lattice
library(caret)

#Set seed agar hasil pembagian data bisa direproduksi
set.seed(123)

# Buat indeks untuk data training 
train_index <- createDataPartition(data_scaled$Status, p = 0.74, list = FALSE)

#Bagi data jadi training & testing
train_data <- data_scaled[train_index, ]
test_data  <- data_scaled[-train_index, ]

#Cek jumlah baris dan proporsi kategori di masing-masing
nrow(train_data); nrow(test_data)
## [1] 224
## [1] 76
prop.table(table(train_data$Status))
## 
##            Baik Tercemar Ringan  Tercemar Berat 
##      0.24107143      0.73214286      0.02678571
prop.table(table(test_data$Status))
## 
##            Baik Tercemar Ringan  Tercemar Berat 
##      0.23684211      0.75000000      0.01315789

Dataset dibagi menjadi dua bagian, yaitu data training (75%) dan data testing (25%) menggunakan fungsi createDataPartition dari paket caret. Pembagian dilakukan secara stratified sampling agar proporsi setiap kategori Status tetap seimbang antara data training dan data testing. Data training akan digunakan untuk membangun model klasifikasi, sedangkan data testing digunakan untuk mengevaluasi performa prediksi. Dengan demikian, hasil evaluasi yang diperoleh dapat menggambarkan kemampuan model secara objektif terhadap data baru.

# Membangun Model SVM
if(!require(e1071)) install.packages("e1071", dependencies = TRUE)
## Loading required package: e1071
library(e1071)
set.seed(123)
svm_model <- svm(Status ~ pH + DO + BOD + TSS + Suhu,
                 data = train_data,
                 kernel = "radial",    # kernel RBF (paling umum)
                 cost = 1,             # parameter regulasi
                 gamma = 0.5)          # parameter kernel


#Lihat ringkasan model
summary(svm_model)
## 
## Call:
## svm(formula = Status ~ pH + DO + BOD + TSS + Suhu, data = train_data, 
##     kernel = "radial", cost = 1, gamma = 0.5)
## 
## 
## Parameters:
##    SVM-Type:  C-classification 
##  SVM-Kernel:  radial 
##        cost:  1 
## 
## Number of Support Vectors:  147
## 
##  ( 96 45 6 )
## 
## 
## Number of Classes:  3 
## 
## Levels: 
##  Baik Tercemar Ringan Tercemar Berat
# Lakukan prediksi pada data testing
svm_pred <- predict(svm_model, newdata = test_data)

#Evaluasi hasil dengan confusion matrix
if(!require(caret)) install.packages("caret", dependencies = TRUE)
library(caret)

conf_svm <- confusionMatrix(svm_pred, test_data$Status)
conf_svm
## Confusion Matrix and Statistics
## 
##                  Reference
## Prediction        Baik Tercemar Ringan Tercemar Berat
##   Baik              10               1              0
##   Tercemar Ringan    8              56              1
##   Tercemar Berat     0               0              0
## 
## Overall Statistics
##                                           
##                Accuracy : 0.8684          
##                  95% CI : (0.7713, 0.9351)
##     No Information Rate : 0.75            
##     P-Value [Acc > NIR] : 0.008845        
##                                           
##                   Kappa : 0.5942          
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: Baik Class: Tercemar Ringan Class: Tercemar Berat
## Sensitivity               0.5556                 0.9825               0.00000
## Specificity               0.9828                 0.5263               1.00000
## Pos Pred Value            0.9091                 0.8615                   NaN
## Neg Pred Value            0.8769                 0.9091               0.98684
## Prevalence                0.2368                 0.7500               0.01316
## Detection Rate            0.1316                 0.7368               0.00000
## Detection Prevalence      0.1447                 0.8553               0.00000
## Balanced Accuracy         0.7692                 0.7544               0.50000

Secara keseluruhan, model SVM dengan kernel radial menunjukkan performa yang baik dan stabil, terutama dalam mengklasifikasikan kategori Tercemar Ringan, yang merupakan kelas mayoritas pada data. Nilai akurasi sebesar 86.8% menunjukkan bahwa model mampu menangkap pola umum hubungan antara variabel fisik-kimia (pH, DO, BOD, TSS, dan Suhu) dengan status kualitas air. Namun, performa pada kategori Tercemar Berat masih sangat rendah (sensitivity = 0), disebabkan oleh jumlah data yang terlalu sedikit untuk membentuk support vector yang representatif.

# Buat salinan test_data tanpa NA
test_data_clean <- na.omit(test_data)

#Jalankan prediksi SVM pada data yang bersih
svm_pred <- predict(svm_model, newdata = test_data_clean)

# Gabungkan hasil prediksi dengan data aktual yang bersih
hasil_pred_svm <- data.frame(
  No = 1:nrow(test_data_clean),
  Status_Aktual = test_data_clean$Status,
  Prediksi_SVM = svm_pred
)

# Simpan ke CSV
write.csv(hasil_pred_svm, "Hasil_Prediksi_SVMm.csv", row.names = FALSE)

#  Cek hasil
head(hasil_pred_svm)
##   No   Status_Aktual    Prediksi_SVM
## 1  1 Tercemar Ringan Tercemar Ringan
## 2  2 Tercemar Ringan Tercemar Ringan
## 3  3            Baik Tercemar Ringan
## 4  4 Tercemar Ringan Tercemar Ringan
## 5  5            Baik Tercemar Ringan
## 6  6 Tercemar Ringan Tercemar Ringan
# Membangun Model Decicion Tree
if(!require(rpart)) install.packages("rpart", dependencies = TRUE)
## Loading required package: rpart
if(!require(rpart.plot)) install.packages("rpart.plot", dependencies = TRUE)
## Loading required package: rpart.plot
library(rpart)
library(rpart.plot)
set.seed(123)
tree_model <- rpart(Status ~ pH + DO + BOD + TSS + Suhu,
                    data = train_data,
                    method = "class",   # klasifikasi
                    parms = list(split = "gini")) 

summary(tree_model)
## Call:
## rpart(formula = Status ~ pH + DO + BOD + TSS + Suhu, data = train_data, 
##     method = "class", parms = list(split = "gini"))
##   n= 224 
## 
##           CP nsplit rel error    xerror       xstd
## 1 0.41666667      0 1.0000000 1.0000000 0.11046439
## 2 0.01666667      2 0.1666667 0.2333333 0.06038074
## 3 0.01000000      3 0.1500000 0.2833333 0.06605936
## 
## Variable importance
##  BOD   DO Suhu  TSS   pH 
##   46   34    7    7    5 
## 
## Node number 1: 224 observations,    complexity param=0.4166667
##   predicted class=Tercemar Ringan  expected loss=0.2678571  P(node) =1
##     class counts:    54   164     6
##    probabilities: 0.241 0.732 0.027 
##   left son=2 (102 obs) right son=3 (122 obs)
##   Primary splits:
##       DO   < 0.02444312 to the right, improve=26.3752000, (0 missing)
##       BOD  < 0.07755806 to the left,  improve=22.8097500, (0 missing)
##       TSS  < 0.5630409  to the left,  improve= 1.9212760, (0 missing)
##       Suhu < -0.2035628 to the left,  improve= 1.0052080, (0 missing)
##       pH   < -1.314209  to the left,  improve= 0.6077858, (0 missing)
##   Surrogate splits:
##       BOD  < 1.901136   to the right, agree=0.567, adj=0.049, (0 split)
##       pH   < 1.463931   to the right, agree=0.562, adj=0.039, (0 split)
##       TSS  < -1.927517  to the left,  agree=0.558, adj=0.029, (0 split)
##       Suhu < 2.184051   to the right, agree=0.558, adj=0.029, (0 split)
## 
## Node number 2: 102 observations,    complexity param=0.4166667
##   predicted class=Baik             expected loss=0.5  P(node) =0.4553571
##     class counts:    51    47     4
##    probabilities: 0.500 0.461 0.039 
##   left son=4 (52 obs) right son=5 (50 obs)
##   Primary splits:
##       BOD  < 0.07755806 to the left,  improve=45.364740, (0 missing)
##       DO   < 1.45534    to the left,  improve= 3.754567, (0 missing)
##       Suhu < -0.2031754 to the left,  improve= 1.546776, (0 missing)
##       TSS  < 0.5630409  to the left,  improve= 1.477184, (0 missing)
##       pH   < 1.49043    to the left,  improve= 0.839658, (0 missing)
##   Surrogate splits:
##       DO   < 1.005731   to the left,  agree=0.627, adj=0.24, (0 split)
##       TSS  < 0.5630409  to the left,  agree=0.588, adj=0.16, (0 split)
##       Suhu < -0.2031754 to the left,  agree=0.588, adj=0.16, (0 split)
##       pH   < 0.03985306 to the right, agree=0.559, adj=0.10, (0 split)
## 
## Node number 3: 122 observations
##   predicted class=Tercemar Ringan  expected loss=0.04098361  P(node) =0.5446429
##     class counts:     3   117     2
##    probabilities: 0.025 0.959 0.016 
## 
## Node number 4: 52 observations
##   predicted class=Baik             expected loss=0.01923077  P(node) =0.2321429
##     class counts:    51     1     0
##    probabilities: 0.981 0.019 0.000 
## 
## Node number 5: 50 observations,    complexity param=0.01666667
##   predicted class=Tercemar Ringan  expected loss=0.08  P(node) =0.2232143
##     class counts:     0    46     4
##    probabilities: 0.000 0.920 0.080 
##   left son=10 (43 obs) right son=11 (7 obs)
##   Primary splits:
##       BOD  < 1.644513   to the left,  improve=3.9314290, (0 missing)
##       DO   < 0.9789528  to the left,  improve=0.6933333, (0 missing)
##       TSS  < 1.061861   to the left,  improve=0.6889037, (0 missing)
##       Suhu < 0.6059562  to the left,  improve=0.5438235, (0 missing)
##       pH   < -0.8938034 to the right, improve=0.2924009, (0 missing)
## 
## Node number 10: 43 observations
##   predicted class=Tercemar Ringan  expected loss=0  P(node) =0.1919643
##     class counts:     0    43     0
##    probabilities: 0.000 1.000 0.000 
## 
## Node number 11: 7 observations
##   predicted class=Tercemar Berat   expected loss=0.4285714  P(node) =0.03125
##     class counts:     0     3     4
##    probabilities: 0.000 0.429 0.571
# Visualisasikan pohon
rpart.plot(tree_model, main = "Decision Tree - Status Kualitas Air", type = 3, extra = 101)

#  Prediksi pada data testing
tree_pred <- predict(tree_model, newdata = test_data, type = "class")

#  Evaluasi model
if(!require(caret)) install.packages("caret", dependencies = TRUE)
library(caret)

conf_tree <- confusionMatrix(tree_pred, test_data$Status)
conf_tree
## Confusion Matrix and Statistics
## 
##                  Reference
## Prediction        Baik Tercemar Ringan Tercemar Berat
##   Baik              15               0              0
##   Tercemar Ringan    3              56              1
##   Tercemar Berat     0               1              0
## 
## Overall Statistics
##                                           
##                Accuracy : 0.9342          
##                  95% CI : (0.8531, 0.9783)
##     No Information Rate : 0.75            
##     P-Value [Acc > NIR] : 3.031e-05       
##                                           
##                   Kappa : 0.8177          
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: Baik Class: Tercemar Ringan Class: Tercemar Berat
## Sensitivity               0.8333                 0.9825               0.00000
## Specificity               1.0000                 0.7895               0.98667
## Pos Pred Value            1.0000                 0.9333               0.00000
## Neg Pred Value            0.9508                 0.9375               0.98667
## Prevalence                0.2368                 0.7500               0.01316
## Detection Rate            0.1974                 0.7368               0.00000
## Detection Prevalence      0.1974                 0.7895               0.01316
## Balanced Accuracy         0.9167                 0.8860               0.49333

Model Decision Tree menunjukkan performa yang sangat baik dan mudah diinterpretasikan, dengan akurasi mencapai 93.4% pada data testing. Walaupun lebih sederhana dari Random Forest, hasilnya mendekati sempurna untuk dua kategori utama (Baik dan Tercemar Ringan).

# Buat data frame hasil prediksi
hasil_pred_tree <- data.frame(
  No = 1:nrow(test_data),
  Status_Aktual = test_data$Status,
  Prediksi_DecisionTree = tree_pred
)

# Simpan ke file CSV
write.csv(hasil_pred_tree, 
          file = "Hasil_Prediksi_DecisionTtrre.csv", 
          row.names = FALSE)

# Opsional: tampilkan 5 baris pertama
head(hasil_pred_tree)
##   No   Status_Aktual Prediksi_DecisionTree
## 1  1 Tercemar Ringan       Tercemar Ringan
## 2  2 Tercemar Ringan       Tercemar Ringan
## 3  3            Baik                  Baik
## 4  4 Tercemar Ringan       Tercemar Ringan
## 5  5            Baik       Tercemar Ringan
## 6  6 Tercemar Ringan       Tercemar Ringan
# Membangun Model Random Forest
if(!require(randomForest)) install.packages("randomForest", dependencies = TRUE)
## Loading required package: 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:dplyr':
## 
##     combine
## The following object is masked from 'package:ggplot2':
## 
##     margin
library(randomForest)
set.seed(123)
rf_model <- randomForest(Status ~ pH + DO + BOD + TSS + Suhu,
                         data = train_data,
                         ntree = 500,       # jumlah pohon
                         mtry = 3,          # jumlah variabel acak per pohon
                         importance = TRUE) # aktifkan fitur importance

# Lihat ringkasan model
print(rf_model)
## 
## Call:
##  randomForest(formula = Status ~ pH + DO + BOD + TSS + Suhu, data = train_data,      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.02%
## Confusion matrix:
##                 Baik Tercemar Ringan Tercemar Berat class.error
## Baik              50               4              0  0.07407407
## Tercemar Ringan    2             162              0  0.01219512
## Tercemar Berat     0               3              3  0.50000000
# Evaluasi model pada data testing
rf_pred <- predict(rf_model, newdata = test_data)
if(!require(caret)) install.packages("caret", dependencies = TRUE)
library(caret)
conf_rf <- confusionMatrix(rf_pred, test_data$Status)
conf_rf
## Confusion Matrix and Statistics
## 
##                  Reference
## Prediction        Baik Tercemar Ringan Tercemar Berat
##   Baik              15               0              0
##   Tercemar Ringan    3              57              1
##   Tercemar Berat     0               0              0
## 
## Overall Statistics
##                                           
##                Accuracy : 0.9474          
##                  95% CI : (0.8707, 0.9855)
##     No Information Rate : 0.75            
##     P-Value [Acc > NIR] : 6.005e-06       
##                                           
##                   Kappa : 0.8502          
##                                           
##  Mcnemar's Test P-Value : NA              
## 
## Statistics by Class:
## 
##                      Class: Baik Class: Tercemar Ringan Class: Tercemar Berat
## Sensitivity               0.8333                 1.0000               0.00000
## Specificity               1.0000                 0.7895               1.00000
## Pos Pred Value            1.0000                 0.9344                   NaN
## Neg Pred Value            0.9508                 1.0000               0.98684
## Prevalence                0.2368                 0.7500               0.01316
## Detection Rate            0.1974                 0.7500               0.00000
## Detection Prevalence      0.1974                 0.8026               0.00000
## Balanced Accuracy         0.9167                 0.8947               0.50000
# Lihat pentingnya variabel
importance(rf_model)
##           Baik Tercemar Ringan Tercemar Berat MeanDecreaseAccuracy
## pH   -1.845396       0.7546019     -0.3625717           -0.1770283
## DO   73.812160      66.1799338     11.4375397           84.8234952
## BOD  76.591648      65.9540918     21.2687152           89.1669435
## TSS   2.780943      -1.8401951     -2.5767096           -0.2682037
## Suhu -1.081135      -0.3579684      1.3919255           -0.7775447
##      MeanDecreaseGini
## pH           2.648443
## DO          40.159186
## BOD         40.562850
## TSS          3.436495
## Suhu         3.522865
varImpPlot(rf_model, main = "Variable Importance - Random Forest")

Random Forest menggabungkan banyak Decision Tree sehingga memberikan prediksi yang stabil dan presisi tinggi, bahkan lebih baik dari Decision Tree tunggal.

Kombinasi DO tinggi dan BOD rendah mengarah ke status Baik, sedangkan DO rendah dan BOD tinggi menunjukkan Tercemar Ringan.

Model sangat andal membedakan dua kategori utama, namun tidak cukup kuat untuk kelas minoritas.

if(!require(randomForest)) install.packages("randomForest", dependencies = TRUE)
library(randomForest)

# Bersihkan data testing (hapus baris dengan NA)
test_data_clean <- na.omit(test_data)

# Lakukan prediksi dengan model Random Forest
rf_pred <- predict(rf_model, newdata = test_data_clean)

# Buat data frame hasil prediksi
hasil_pred_rf <- data.frame(
  No = 1:nrow(test_data_clean),
  Status_Aktual = test_data_clean$Status,
  Prediksi_RandomForest = rf_pred
)

# Simpan ke file CSV
write.csv(hasil_pred_rf,
          file = "Hasil_Prediksi_RrandomForest.csv",
          row.names = FALSE)


if(!require(openxlsx)) install.packages("openxlsx", dependencies = TRUE)
## Loading required package: openxlsx
library(openxlsx)
write.xlsx(hasil_pred_rf,
           file = "Hasil_Prediksi_RandomForest.xlsx",
           rowNames = FALSE)

# Cek hasil 5 baris pertama
head(hasil_pred_rf)
##   No   Status_Aktual Prediksi_RandomForest
## 1  1 Tercemar Ringan       Tercemar Ringan
## 2  2 Tercemar Ringan       Tercemar Ringan
## 3  3            Baik                  Baik
## 4  4 Tercemar Ringan       Tercemar Ringan
## 5  5            Baik       Tercemar Ringan
## 6  6 Tercemar Ringan       Tercemar Ringan
#Soal 3 point 1 memBangun model regresi linear
lm_DO <- lm(DO ~ pH + BOD + TSS + Suhu, data = train_data)

# Lihat ringkasan model
summary(lm_DO)
## 
## Call:
## lm(formula = DO ~ pH + BOD + TSS + Suhu, data = train_data)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.53845 -0.60469  0.02063  0.73564  2.51265 
## 
## Coefficients:
##              Estimate Std. Error t value Pr(>|t|)
## (Intercept)  0.033539   0.068894   0.487    0.627
## pH           0.011928   0.069694   0.171    0.864
## BOD          0.067050   0.066435   1.009    0.314
## TSS         -0.061500   0.068769  -0.894    0.372
## Suhu         0.005029   0.070805   0.071    0.943
## 
## Residual standard error: 1.028 on 219 degrees of freedom
## Multiple R-squared:  0.009165,   Adjusted R-squared:  -0.008932 
## F-statistic: 0.5064 on 4 and 219 DF,  p-value: 0.731
# Prediksi pada data testing
pred_lm <- predict(lm_DO, newdata = test_data)

#  Evaluasi performa
library(Metrics)  # untuk hitung MSE
## 
## Attaching package: 'Metrics'
## The following objects are masked from 'package:caret':
## 
##     precision, recall
mse_lm <- mse(test_data$DO, pred_lm)
r2_lm <- 1 - sum((test_data$DO - pred_lm)^2) / sum((test_data$DO - mean(test_data$DO))^2)

mse_lm; r2_lm
## [1] 0.8891079
## [1] -0.0492366

Model regresi linear menunjukkan bahwa hubungan antara variabel pH, BOD, TSS, dan Suhu terhadap DO bersifat lemah dan tidak signifikan secara statistik. Nilai R² yang sangat kecil (<1%) menunjukkan bahwa hubungan antarvariabel bersifat non-linear, sehingga model linier tidak cukup untuk menangkap pola kompleks dalam data kualitas air.

# model regresi Spline
library(splines)

# Bangun model spline (gunakan basis cubic spline)
spline_DO <- lm(DO ~ ns(pH, df = 3) + ns(BOD, df = 3) +
                     ns(TSS, df = 3) + ns(Suhu, df = 3),
                data = train_data)

# Lihat ringkasan model spline
summary(spline_DO)
## 
## Call:
## lm(formula = DO ~ ns(pH, df = 3) + ns(BOD, df = 3) + ns(TSS, 
##     df = 3) + ns(Suhu, df = 3), data = train_data)
## 
## Residuals:
##      Min       1Q   Median       3Q      Max 
## -2.49150 -0.60321  0.05631  0.64484  2.62728 
## 
## Coefficients:
##                   Estimate Std. Error t value Pr(>|t|)   
## (Intercept)       -0.74890    0.93209  -0.803  0.42261   
## ns(pH, df = 3)1   -0.04875    0.30846  -0.158  0.87458   
## ns(pH, df = 3)2    0.30560    0.97703   0.313  0.75475   
## ns(pH, df = 3)3    0.33848    0.47752   0.709  0.47922   
## ns(BOD, df = 3)1   0.03465    0.32499   0.107  0.91519   
## ns(BOD, df = 3)2   3.05025    1.15232   2.647  0.00873 **
## ns(BOD, df = 3)3   1.29765    0.50730   2.558  0.01123 * 
## ns(TSS, df = 3)1  -0.20781    0.32052  -0.648  0.51746   
## ns(TSS, df = 3)2  -0.48701    0.98587  -0.494  0.62183   
## ns(TSS, df = 3)3  -0.32423    0.46075  -0.704  0.48239   
## ns(Suhu, df = 3)1 -0.06990    0.32277  -0.217  0.82877   
## ns(Suhu, df = 3)2 -0.54204    1.02488  -0.529  0.59744   
## ns(Suhu, df = 3)3 -0.07403    0.46952  -0.158  0.87486   
## ---
## Signif. codes:  0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
## 
## Residual standard error: 1.027 on 211 degrees of freedom
## Multiple R-squared:  0.04755,    Adjusted R-squared:  -0.006616 
## F-statistic: 0.8779 on 12 and 211 DF,  p-value: 0.5704
# Prediksi pada data testing
pred_spline <- predict(spline_DO, newdata = test_data)

# Evaluasi performa
mse_spline <- mse(test_data$DO, pred_spline)
r2_spline <- 1 - sum((test_data$DO - pred_spline)^2) / sum((test_data$DO - mean(test_data$DO))^2)

mse_spline; r2_spline
## [1] 0.9280375
## [1] -0.09517743

Model Regresi Spline menunjukkan peningkatan kecil dibanding regresi linear dalam memprediksi DO, terutama karena kemampuannya menangkap efek non-linear dari variabel BOD.

# point 2 Bandingkan performa model
compare_stats <- data.frame(
  Model = c("Regresi Linear", "Regresi Spline"),
  MSE   = c(mse_lm, mse_spline),
  R2    = c(r2_lm, r2_spline)
)
compare_stats
##            Model       MSE          R2
## 1 Regresi Linear 0.8891079 -0.04923660
## 2 Regresi Spline 0.9280375 -0.09517743

edua model (Linear dan Spline) memiliki performa rendah (R² negatif, MSE tinggi).

Namun, model spline berhasil menunjukkan efek non-linear signifikan dari BOD terhadap DO.

Secara ekologis, BOD tetap menjadi indikator terpenting untuk menilai kondisi oksigen terlarut pada perairan.

# Pastikan library ggplot2 terinstal
if(!require(ggplot2)) install.packages("ggplot2", dependencies = TRUE)
library(ggplot2)

# Gabungkan hasil prediksi dalam satu data frame
compare_plot <- data.frame(
  Actual = test_data$DO,
  Pred_Linear = pred_lm,
  Pred_Spline = pred_spline
)

# Plot 1️⃣: Regresi Linear
ggplot(compare_plot, aes(x = Actual, y = Pred_Linear)) +
  geom_point(color = "steelblue", size = 2, alpha = 0.7) +
  geom_abline(intercept = 0, slope = 1, color = "red", linetype = "dashed") +
  labs(title = "Prediksi DO - Model Regresi Linear",
       x = "Nilai Aktual DO",
       y = "Nilai Prediksi DO (Linear)") +
  theme_minimal()

# Plot 2️⃣: Regresi Spline
ggplot(compare_plot, aes(x = Actual, y = Pred_Spline)) +
  geom_point(color = "forestgreen", size = 2, alpha = 0.7) +
  geom_abline(intercept = 0, slope = 1, color = "red", linetype = "dashed") +
  labs(title = "Prediksi DO - Model Regresi Spline",
       x = "Nilai Aktual DO",
       y = "Nilai Prediksi DO (Spline)") +
  theme_minimal()

# Plot 3️⃣: Perbandingan kedua model dalam satu grafik
library(reshape2)
compare_melt <- melt(compare_plot, id.vars = "Actual")

ggplot(compare_melt, aes(x = Actual, y = value, color = variable)) +
  geom_point(alpha = 0.7, size = 2) +
  geom_abline(intercept = 0, slope = 1, linetype = "dashed", color = "black") +
  labs(title = "Perbandingan Prediksi DO antara Model Linear dan Spline",
       x = "Nilai Aktual DO",
       y = "Nilai Prediksi DO",
       color = "Model") +
  theme_minimal()