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)
}
}
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, …
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
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")
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")