# 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()