1 Setup

1.1 Instalasi Paket

Tahap awal adalah mempersiapkan environment R dengan memuat semua pustaka (paket) yang diperlukan. Kode di bawah ini secara otomatis akan memeriksa, menginstal (jika belum ada), dan memuat setiap paket yang dibutuhkan untuk analisis, mulai dari manipulasi data hingga pemodelan.

pkgs <- c("tidyverse", "readxl", "janitor", "skimr", "caret", "e1071",
          "rpart", "rpart.plot", "randomForest", "yardstick", "broom", "splines",
          "rsample")


for (pkg in pkgs) {
  if (!require(pkg, character.only = TRUE)) {
    install.packages(pkg)
    library(pkg, character.only = TRUE)
  }
} 

2 Soal 1 — Pembersihan Data

2.1 1.1 Muat dan Seleksi Kolom

Data bersumber dari sebuah file Excel (kualitasair.xlsx) yang berisi dua sheet terpisah. Sheet Training digunakan untuk melatih model, dan sheet Testing digunakan sebagai data baru yang akan diprediksi. Kedua sheet dibaca ke dalam R sebagai data frame terpisah.

library(readxl)

train_raw <- read_excel("kualitasair.xlsx", sheet = "Training") %>% janitor::clean_names()
test_raw  <- read_excel("kualitasair.xlsx", sheet = "Testing") %>% janitor::clean_names()

cat("Dimensi Data Latih:\n")
## Dimensi Data Latih:
dim(train_raw)
## [1] 300   7
cat("\nDimensi Data Uji:\n")
## 
## Dimensi Data Uji:
dim(test_raw)
## [1] 75  6
glimpse(test_raw)
## Rows: 75
## Columns: 6
## $ lokasi <chr> "S301", "S302", "S303", "S304", "S305", "S306", "S307", "S308",…
## $ p_h    <dbl> 6.9977, 7.3801, 7.0195, 7.3675, 6.9268, 6.9711, 7.2412, 7.4965,…
## $ do     <dbl> 5.0835, 4.7482, 6.5949, NA, 6.2444, 6.0028, 4.6718, 7.1797, 5.4…
## $ bod    <dbl> 3.1813, 3.3373, 3.0039, 3.4952, 3.3449, 3.4466, 3.3968, 4.3295,…
## $ tss    <dbl> 50.9339, 46.5210, NA, 39.0091, 47.1692, 39.1027, 45.9433, 55.26…
## $ suhu   <dbl> 25.5573, 27.0940, 26.5954, 26.6512, 23.3940, 27.7013, 29.8974, …

2.2 1.2 Tangani Missing Value & Outlier

Tahap pembersihan data adalah langkah krusial untuk memastikan kualitas dan konsistensi data sebelum masuk ke tahap pemodelan. Metodologi yang penting di sini adalah menghindari kebocoran data (data leakage), di mana informasi dari data uji tidak boleh “bocor” ke proses training. Oleh karena itu, semua parameter untuk preprocessing (seperti median, modus, dll.) dihitung hanya dari data latih, kemudian diterapkan secara konsisten ke kedua set data.

Proses ini diringkas dalam sebuah fungsi preprocess_data yang melakukan tiga hal utama:

Penanganan Nilai Hilang (Missing Values): Nilai numerik yang hilang diisi dengan median dari data latih (lebih robust terhadap outlier dibandingkan rata-rata). Nilai kategori yang hilang diisi dengan modus (nilai yang paling sering muncul).

Penanganan Outlier: Nilai ekstrem (outlier) ditangani dengan metode Winsorizing. Metode ini tidak menghapus outlier, melainkan membatasi nilainya ke ambang batas tertentu (di sini, 1.5 kali Interquartile Range), sehingga mengurangi pengaruhnya tanpa menghilangkan data.

Standardisasi Kategori: Teks pada kolom status dibersihkan dari spasi berlebih dan dikonversi ke format Title Case untuk memastikan konsistensi (misalnya, “baik” dan “Baik” dianggap sama).

numeric_vars <- c("p_h", "do", "bod", "tss", "suhu")
medians <- sapply(train_raw[numeric_vars], median, na.rm = TRUE)
modus_status <- names(sort(table(train_raw$status), decreasing = TRUE))[1]
winsor_params <- lapply(train_raw[numeric_vars], function(x) {
  q <- quantile(x, probs = c(0.25, 0.75), na.rm = TRUE)
  iqr <- q[2] - q[1]
  list(low = q[1] - 1.5 * iqr, up = q[2] + 1.5 * iqr)
})


preprocess_data <- function(df, medians, modus_status, winsor_params) {
  
  for (col in names(medians)) {
    df[[col]][is.na(df[[col]])] <- medians[col]
  }
  if ("status" %in% names(df)) {
    df$status[is.na(df$status)] <- modus_status
  }
  
  for (col in names(winsor_params)) {
    params <- winsor_params[[col]]
    df[[col]][df[[col]] < params$low] <- params$low
    
    df[[col]][df[[col]] > params$up] <- params$up
  }
  
  if ("status" %in% names(df)) {
    df <- df %>%
      mutate(status = str_to_title(str_trim(status)))
  }
  return(df)
}


train_clean <- preprocess_data(train_raw, medians, modus_status, winsor_params)
test_clean  <- preprocess_data(test_raw, medians, modus_status, winsor_params)


cat("\nFrekuensi kelas original di data latih:\n")
## 
## Frekuensi kelas original di data latih:
print(table(train_clean$status))
## 
##            Baik  Tercemar Berat Tercemar Ringan 
##              72               7             221

3 Soal 2 — Pemodelan Klasifikasi Status Kualitas Air

Tujuan bagian ini adalah melatih beberapa model klasifikasi untuk memprediksi kolom status pada data uji. Tiga model populer dipilih untuk perbandingan.

# --- Persiapan Data ---
train_cls <- train_clean %>% mutate(status = as.factor(status))
test_cls <- test_clean


# Model 1: SVM dengan Penskalaan menggunakan `caret`
train_control <- trainControl(method = "cv", number = 10)
svm_model <- caret::train(status ~ p_h + do + bod + tss + suhu, data = train_cls,
                   method = "svmRadial", trControl = train_control,
                   preProcess = c("center", "scale"))

# Model 2: Decision Tree
tree_model <- rpart::rpart(status ~ p_h + do + bod + tss + suhu, data = train_cls, method = "class")

# Model 3: Random Forest
set.seed(123)
rf_model <- randomForest::randomForest(status ~ p_h + do + bod + tss + suhu, data = train_cls, ntree = 300)

# --- Prediksi pada Data Uji  ---
pred_svm  <- predict(svm_model, newdata = test_cls)
pred_tree <- predict(tree_model, newdata = test_cls, type = "class")
pred_rf   <- predict(rf_model, newdata = test_cls)

map_status <- c("Baik" = 1, "Tercemar Ringan" = 2, "Tercemar Berat" = 3)

pred_svm_num  <- unname(map_status[as.character(pred_svm)])
pred_tree_num <- unname(map_status[as.character(pred_tree)])
pred_rf_num   <- unname(map_status[as.character(pred_rf)])

# --- Gabungkan dan Simpan Hasil Prediksi ---
hasil_prediksi_status <- tibble(
  Lokasi        = test_cls$lokasi,
  Pred_SVM      = pred_svm,
  Pred_SVM_num  = pred_svm_num,
  Pred_Tree     = pred_tree,
  Pred_Tree_num = pred_tree_num,
  Pred_RF       = pred_rf,
  Pred_RF_num   = pred_rf_num
)

head(hasil_prediksi_status, 10)
## # A tibble: 10 × 7
##    Lokasi Pred_SVM      Pred_SVM_num Pred_Tree Pred_Tree_num Pred_RF Pred_RF_num
##    <chr>  <fct>                <dbl> <fct>             <dbl> <fct>         <dbl>
##  1 S301   Tercemar Rin…            2 Tercemar…             2 Tercem…           2
##  2 S302   Tercemar Rin…            2 Tercemar…             2 Tercem…           2
##  3 S303   Baik                     1 Baik                  1 Baik              1
##  4 S304   Tercemar Rin…            2 Tercemar…             2 Tercem…           2
##  5 S305   Tercemar Rin…            2 Tercemar…             2 Tercem…           2
##  6 S306   Tercemar Rin…            2 Tercemar…             2 Tercem…           2
##  7 S307   Tercemar Rin…            2 Tercemar…             2 Tercem…           2
##  8 S308   Tercemar Rin…            2 Tercemar…             2 Tercem…           2
##  9 S309   Tercemar Rin…            2 Tercemar…             2 Tercem…           2
## 10 S310   Tercemar Rin…            2 Tercemar…             2 Tercem…           2
readr::write_csv(hasil_prediksi_status, "prediksi_status_kualitas_air.csv")

4 Soal 3 — Pemodelan Regresi untuk Prediksi DO

Pada bagian ini, fokusnya adalah memprediksi nilai numerik kontinu, yaitu Dissolved Oxygen (DO). Dua model regresi digunakan untuk tugas ini.

train_reg <- train_clean
test_reg  <- test_clean

# Model 1: Linear Regression
lm_model <- lm(do ~ p_h + bod + tss + suhu, data = train_reg)

# Model 2: Spline Regression
spline_model <- lm(do ~ splines::ns(p_h, 3) + splines::ns(bod, 3) +
                        splines::ns(tss, 3) + splines::ns(suhu, 3), data = train_reg)

lm_pred     <- predict(lm_model, newdata = test_reg)
spline_pred <- predict(spline_model, newdata = test_reg)

# Fungsi evaluasi
eval_metrics <- function(truth, pred){
  tibble(R2 = cor(truth, pred)^2, MSE = mean((truth - pred)^2), RMSE = sqrt(MSE))
}

# Tampilkan hasil evaluasi
bind_rows(
  eval_metrics(test_reg$do, lm_pred)     %>% mutate(Model = "Linear"),
  eval_metrics(test_reg$do, spline_pred) %>% mutate(Model = "Spline")
) %>% select(Model, everything())
## # A tibble: 2 × 4
##   Model      R2   MSE  RMSE
##   <chr>   <dbl> <dbl> <dbl>
## 1 Linear 0.0179 0.616 0.785
## 2 Spline 0.0103 0.629 0.793
hasil_prediksi_do <- tibble(
  Lokasi         = test_reg$lokasi,
  DO_Aktual      = test_reg$do,
  Pred_Linear_DO = lm_pred,
  Pred_Spline_DO = spline_pred
)

head(hasil_prediksi_do, 10)
## # A tibble: 10 × 4
##    Lokasi DO_Aktual Pred_Linear_DO Pred_Spline_DO
##    <chr>      <dbl>          <dbl>          <dbl>
##  1 S301        5.08           6.04           6.01
##  2 S302        4.75           6.02           5.78
##  3 S303        6.59           6.01           5.94
##  4 S304        5.99           6.05           5.78
##  5 S305        6.24           6.11           6.32
##  6 S306        6.00           6.03           5.77
##  7 S307        4.67           5.97           5.77
##  8 S308        7.18           6.04           6.11
##  9 S309        5.41           5.83           6.15
## 10 S310        7.2            6.07           6.01
readr::write_csv(hasil_prediksi_do, "prediksi_do_kualitas_air_data_uji.csv")