library(tidyverse)
## ── Attaching core tidyverse packages ──────────────────────── tidyverse 2.0.0 ──
## ✔ dplyr 1.1.4 ✔ readr 2.1.5
## ✔ forcats 1.0.1 ✔ stringr 1.5.1
## ✔ ggplot2 3.5.2 ✔ tibble 3.3.0
## ✔ lubridate 1.9.4 ✔ tidyr 1.3.1
## ✔ purrr 1.1.0
## ── Conflicts ────────────────────────────────────────── tidyverse_conflicts() ──
## ✖ dplyr::filter() masks stats::filter()
## ✖ dplyr::lag() masks stats::lag()
## ℹ Use the conflicted package (<http://conflicted.r-lib.org/>) to force all conflicts to become errors
library(caret)
## Loading required package: lattice
##
## Attaching package: 'caret'
##
## The following object is masked from 'package:purrr':
##
## lift
library(e1071)
library(rpart)
library(randomForest)
## randomForest 4.7-1.2
## Type rfNews() to see new features/changes/bug fixes.
##
## Attaching package: 'randomForest'
##
## The following object is masked from 'package:dplyr':
##
## combine
##
## The following object is masked from 'package:ggplot2':
##
## margin
library(splines)
set.seed(223)
data_train <- read.csv("C:/Users/HP/Documents/TUGAS/SMT 5/Statistika Lingkungan/UTS/kualitasair-Training.csv")
str(data_train)
## 'data.frame': 300 obs. of 7 variables:
## $ Lokasi: chr "S1" "S2" "S3" "S4" ...
## $ pH : num 7.69 6.72 7.18 7.32 7.2 ...
## $ DO : num NA 5.72 4.89 6.13 7.79 ...
## $ BOD : num 1.71 1.44 2.73 3.14 1.18 ...
## $ TSS : num 43.1 44.3 NA 41 48.1 ...
## $ Suhu : num 26.8 27.7 26 29.7 26.4 ...
## $ Status: chr "Tercemar ringan" "Tercemar ringan" "Tercemar ringan" "Tercemar ringan" ...
head(data_train)
## Lokasi pH DO BOD TSS Suhu Status
## 1 S1 7.6855 NA 1.7136 43.1415 26.7972 Tercemar ringan
## 2 S2 6.7177 5.7236 1.4402 44.2963 27.7284 Tercemar ringan
## 3 S3 7.1816 4.8906 2.7274 NA 26.0255 Tercemar ringan
## 4 S4 7.3164 6.1339 3.1398 41.0104 29.6639 Tercemar ringan
## 5 S5 7.2021 7.7853 1.1778 48.0967 26.4099 baik
## 6 S6 6.9469 8.4222 3.2324 48.5610 28.6809 Tercemar ringan
colSums(is.na(data_train))
## Lokasi pH DO BOD TSS Suhu Status
## 0 0 23 22 24 0 0
for (col in c("DO","BOD","TSS")) {
data_train[[col]][is.na(data_train[[col]])] <- median(data_train[[col]], na.rm = TRUE)
}
colSums(is.na(data_train))
## Lokasi pH DO BOD TSS Suhu Status
## 0 0 0 0 0 0 0
boxplot(data_train$pH, main = "Outlier pH")
boxplot(data_train$DO, main = "Outlier DO")
boxplot(data_train$BOD, main = "Outlier BOD")
boxplot(data_train$TSS, main = "Outlier TSS")
boxplot(data_train$Suhu,main = "Outlier Suhu")
for (col in c("pH","DO","BOD","TSS","Suhu")) {
Q1 <- quantile(data_train[[col]], 0.25, na.rm = TRUE)
Q3 <- quantile(data_train[[col]], 0.75, na.rm = TRUE)
IQR <- Q3 - Q1
lower <- Q1 - 1.5 * IQR
upper <- Q3 + 1.5 * IQR
}
Missing Value
Berdasarkan hasil colSums(is.na()), terdapat
missing value pada variabel DO, BOD,
dan TSS.
Nilai-nilai tersebut diimputasi menggunakan median, agar tidak mengubah distribusi data secara signifikan.
Outlier
Inkonsistensi Kategori
Variabel Status distandarkan jadi tiga kategori
tetap:
1 = Baik
2 = Tercemar Ringan
3 = Tercemar Berat
table(data_train$Status)
##
## baik Baik BAIK Tercemar berat tercemar ringan
## 1 70 1 7 1
## Tercemar ringan Tercemar Ringan
## 219 1
data_train$Status <- tolower(data_train$Status)
data_train$Status <- trimws(data_train$Status)
data_train$Status <- dplyr::recode(
data_train$Status,
"baik" = "1",
"tercemar ringan" = "2",
"tercemar berat" = "3",
.default = data_train$Status
)
data_train$Status <- as.factor(data_train$Status)
table(data_train$Status)
##
## 1 2 3
## 72 221 7
Pada tahap ini dilakukan standarisasi penulisan kategori variabel
Status agar konsisten dan tidak menimbulkan perbedaan
interpretasi.
Beberapa data memiliki variasi penulisan seperti “Baik”,
“BAIK”, dan “baik”, serta “Tercemar ringan”
dengan variasi kapitalisasi.
Langkah yang dilakukan:
Mengubah seluruh teks menjadi huruf kecil dengan fungsi
tolower().
Menghapus spasi berlebih menggunakan
trimws().
Melakukan pengkodean ulang dengan dplyr::recode()
menjadi:
- **1** = Baik
```{=html}
<!-- -->
```
- **2** = Tercemar ringan
```{=html}
<!-- -->
```
- **3** = Tercemar berat
factor.Hasil distribusi setelah standarisasi menunjukkan:
72 data berstatus Baik,
221 data berstatus Tercemar ringan, dan
7 data berstatus Tercemar berat.
Langkah ini memastikan kategori pada variabel Status seragam dan siap digunakan untuk proses klasifikasi pada tahap berikutnya.
summary(data_train[, c("pH","DO","BOD","TSS","Suhu")])
## pH DO BOD TSS
## Min. :5.503 Min. :2.982 Min. :0.3026 Min. :24.65
## 1st Qu.:6.670 1st Qu.:5.413 1st Qu.:2.4599 1st Qu.:44.28
## Median :6.988 Median :5.991 Median :3.0661 Median :49.52
## Mean :6.989 Mean :5.977 Mean :3.0053 Mean :49.68
## 3rd Qu.:7.318 3rd Qu.:6.611 3rd Qu.:3.5323 3rd Qu.:55.62
## Max. :8.351 Max. :9.229 Max. :5.7962 Max. :76.34
## Suhu
## Min. :22.77
## 1st Qu.:26.62
## Median :28.01
## Mean :28.31
## 3rd Qu.:29.46
## Max. :90.00
Tahap ini bertujuan untuk menampilkan gambaran umum dari data setelah
dilakukan pembersihan (data cleaning).
Ringkasan statistik deskriptif menunjukkan sebaran nilai untuk setiap
variabel numerik sebagai berikut:
Nilai pH berkisar antara 5.50 – 8.35 dengan rata-rata 6.99, menunjukkan kondisi air relatif netral.
Nilai DO (Dissolved Oxygen) berada pada rentang 2.98 – 9.23 mg/L, dengan rata-rata 5.98, menandakan variasi kadar oksigen terlarut antar lokasi.
Nilai BOD (Biochemical Oxygen Demand) memiliki kisaran 0.30 – 5.80 mg/L, dengan median sekitar 3.07, menandakan kualitas bahan organik terlarut tergolong sedang.
TSS (Total Suspended Solid) berkisar 24.65 – 76.34 mg/L, dengan rata-rata 49.68, menunjukkan perbedaan tingkat kekeruhan antar lokasi.
Suhu air berada di kisaran 22.77 – 90.00°C dengan rata-rata 28.31°C, di mana nilai ekstrem tinggi kemungkinan merupakan outlier pengukuran.
Secara keseluruhan, data telah bersih, terstandarisasi, dan siap digunakan untuk tahap analisis lanjutan yaitu klasifikasi status kualitas air.
num_vars <- c("pH", "DO", "BOD", "TSS", "Suhu")
data_class <- data_train[, c(num_vars, "Status")]
set.seed(123)
library(caret)
train_index <- createDataPartition(data_class$Status, p = 0.8, list = FALSE)
train_data <- data_class[train_index, ]
test_data <- data_class[-train_index, ]
# 1. SVM / SVR (pakai e1071)
library(e1071)
svm_model <- svm(Status ~ ., data = train_data)
svm_pred <- predict(svm_model, newdata = test_data)
# 2. Decision Tree
library(rpart)
library(rpart.plot)
tree_model <- rpart(Status ~ ., data = train_data, method = "class")
tree_pred <- predict(tree_model, newdata = test_data, type = "class")
# 3. Random Forest
library(randomForest)
rf_model <- randomForest(Status ~ ., data = train_data, ntree = 100)
rf_pred <- predict(rf_model, newdata = test_data)
library(caret)
cat("===== SVM =====\n")
## ===== SVM =====
cm_svm <- confusionMatrix(as.factor(svm_pred), as.factor(test_data$Status))
print(cm_svm)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 1 2 3
## 1 8 1 0
## 2 6 43 1
## 3 0 0 0
##
## Overall Statistics
##
## Accuracy : 0.8644
## 95% CI : (0.7502, 0.9396)
## No Information Rate : 0.7458
## P-Value [Acc > NIR] : 0.02096
##
## Kappa : 0.5913
##
## Mcnemar's Test P-Value : NA
##
## Statistics by Class:
##
## Class: 1 Class: 2 Class: 3
## Sensitivity 0.5714 0.9773 0.00000
## Specificity 0.9778 0.5333 1.00000
## Pos Pred Value 0.8889 0.8600 NaN
## Neg Pred Value 0.8800 0.8889 0.98305
## Prevalence 0.2373 0.7458 0.01695
## Detection Rate 0.1356 0.7288 0.00000
## Detection Prevalence 0.1525 0.8475 0.00000
## Balanced Accuracy 0.7746 0.7553 0.50000
cat("\n===== Decision Tree =====\n")
##
## ===== Decision Tree =====
cm_tree <- confusionMatrix(as.factor(tree_pred), as.factor(test_data$Status))
print(cm_tree)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 1 2 3
## 1 13 0 0
## 2 1 44 1
## 3 0 0 0
##
## Overall Statistics
##
## Accuracy : 0.9661
## 95% CI : (0.8829, 0.9959)
## No Information Rate : 0.7458
## P-Value [Acc > NIR] : 6.696e-06
##
## Kappa : 0.9075
##
## Mcnemar's Test P-Value : NA
##
## Statistics by Class:
##
## Class: 1 Class: 2 Class: 3
## Sensitivity 0.9286 1.0000 0.00000
## Specificity 1.0000 0.8667 1.00000
## Pos Pred Value 1.0000 0.9565 NaN
## Neg Pred Value 0.9783 1.0000 0.98305
## Prevalence 0.2373 0.7458 0.01695
## Detection Rate 0.2203 0.7458 0.00000
## Detection Prevalence 0.2203 0.7797 0.00000
## Balanced Accuracy 0.9643 0.9333 0.50000
cat("\n===== Random Forest =====\n")
##
## ===== Random Forest =====
cm_rf <- confusionMatrix(as.factor(rf_pred), as.factor(test_data$Status))
print(cm_rf)
## Confusion Matrix and Statistics
##
## Reference
## Prediction 1 2 3
## 1 13 1 0
## 2 1 43 1
## 3 0 0 0
##
## Overall Statistics
##
## Accuracy : 0.9492
## 95% CI : (0.8585, 0.9894)
## No Information Rate : 0.7458
## P-Value [Acc > NIR] : 4.59e-05
##
## Kappa : 0.8644
##
## Mcnemar's Test P-Value : NA
##
## Statistics by Class:
##
## Class: 1 Class: 2 Class: 3
## Sensitivity 0.9286 0.9773 0.00000
## Specificity 0.9778 0.8667 1.00000
## Pos Pred Value 0.9286 0.9556 NaN
## Neg Pred Value 0.9778 0.9286 0.98305
## Prevalence 0.2373 0.7458 0.01695
## Detection Rate 0.2203 0.7288 0.00000
## Detection Prevalence 0.2373 0.7627 0.00000
## Balanced Accuracy 0.9532 0.9220 0.50000
acc_svm <- cm_svm$overall["Accuracy"]
acc_tree <- cm_tree$overall["Accuracy"]
acc_rf <- cm_rf$overall["Accuracy"]
accuracy_df <- data.frame(
Model = c("SVM", "Decision Tree", "Random Forest"),
Akurasi = c(acc_svm, acc_tree, acc_rf)
)
print(accuracy_df)
## Model Akurasi
## 1 SVM 0.8644068
## 2 Decision Tree 0.9661017
## 3 Random Forest 0.9491525
Evaluasi dilakukan menggunakan Confusion Matrix dan
akurasi model.
Hasil evaluasi menunjukkan bahwa ketiga model memiliki performa cukup
baik dalam mengklasifikasikan status kualitas air berdasarkan
variabel numerik (pH, DO, BOD, TSS, dan Suhu).
Support Vector Machine (SVM) memperoleh akurasi sebesar 86,44%, menandakan performa yang baik namun cenderung sensitif terhadap data non-linear.
Decision Tree memiliki akurasi tertinggi sebesar 96,61%, menunjukkan model ini paling efektif dalam membedakan kategori Baik, Tercemar Ringan, dan Tercemar Berat.
Random Forest mencatat akurasi 94,92%, sedikit di bawah Decision Tree, namun tetap unggul dari sisi kestabilan karena sifatnya yang berbasis ensemble.
Dari Confusion Matrix terlihat bahwa model Decision Tree dan Random Forest mampu mengenali kelas “Baik” dan “Tercemar Ringan” dengan sensitivitas tinggi (di atas 0.92). Namun, kelas “Tercemar Berat” memiliki nilai sensitivitas 0, yang kemungkinan disebabkan oleh jumlah data kelas tersebut sangat sedikit (hanya 7 data) sehingga model sulit mengenalinya.
Secara keseluruhan, Decision Tree menjadi model terbaik dalam memprediksi status kualitas air, dengan keseimbangan antara akurasi dan interpretabilitas hasil.
library(ggplot2)
ggplot(accuracy_df, aes(x = Model, y = Akurasi, fill = Model)) +
geom_col(width = 0.6) +
geom_text(aes(label = round(Akurasi, 3)), vjust = -0.5) +
labs(
title = "Perbandingan Akurasi Model Klasifikasi Status Kualitas Air",
x = NULL, y = "Akurasi"
) +
theme_minimal()
Grafik di atas menunjukkan perbandingan tingkat akurasi dari ketiga
model klasifikasi yang digunakan, yaitu Decision Tree,
Random Forest, dan SVM.
Dari hasil visualisasi terlihat bahwa:
Decision Tree memiliki akurasi tertinggi sebesar 96,6%, menunjukkan bahwa model ini paling efektif dalam memisahkan kategori status kualitas air.
Random Forest menyusul dengan akurasi 94,9%, menunjukkan performa yang stabil dan hampir setara dengan Decision Tree.
SVM memiliki akurasi 86,4%, sedikit lebih rendah dibanding dua model lainnya karena lebih sensitif terhadap distribusi data yang tidak seimbang.
Secara keseluruhan, dapat disimpulkan bahwa Decision Tree menjadi model terbaik untuk mengklasifikasikan status kualitas air berdasarkan variabel pH, DO, BOD, TSS, dan Suhu, karena memberikan hasil akurasi tertinggi dengan kompleksitas yang relatif rendah.
num_vars <- c("pH", "BOD", "TSS", "Suhu", "DO")
data_reg <- data_train[, num_vars]
set.seed(123)
library(caret)
train_index <- createDataPartition(data_reg$DO, p = 0.8, list = FALSE)
train_reg <- data_reg[train_index, ]
test_reg <- data_reg[-train_index, ]
# Model 1: Regresi Linear
lm_model <- lm(DO ~ pH + BOD + TSS + Suhu, data = train_reg)
summary(lm_model)
##
## Call:
## lm(formula = DO ~ pH + BOD + TSS + Suhu, data = train_reg)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.00096 -0.52419 0.00635 0.61111 2.87205
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 5.8944142 1.0524885 5.600 5.93e-08 ***
## pH -0.0488955 0.1270069 -0.385 0.7006
## BOD 0.1687463 0.0793648 2.126 0.0345 *
## TSS 0.0002084 0.0066383 0.031 0.9750
## Suhu -0.0036443 0.0140490 -0.259 0.7956
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.9781 on 236 degrees of freedom
## Multiple R-squared: 0.01962, Adjusted R-squared: 0.003007
## F-statistic: 1.181 on 4 and 236 DF, p-value: 0.3198
# Model 2: Regresi Spline
library(splines)
spline_model <- lm(DO ~ bs(pH, df = 3) + bs(BOD, df = 3) + bs(TSS, df = 3) + bs(Suhu, df = 3),
data = train_reg)
summary(spline_model)
##
## Call:
## lm(formula = DO ~ bs(pH, df = 3) + bs(BOD, df = 3) + bs(TSS,
## df = 3) + bs(Suhu, df = 3), data = train_reg)
##
## Residuals:
## Min 1Q Median 3Q Max
## -3.03297 -0.51077 0.01215 0.56018 2.53244
##
## Coefficients:
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 5.22842 0.88370 5.917 1.2e-08 ***
## bs(pH, df = 3)1 0.80837 1.22752 0.659 0.510858
## bs(pH, df = 3)2 -0.54262 0.63479 -0.855 0.393555
## bs(pH, df = 3)3 0.59072 0.86551 0.683 0.495610
## bs(BOD, df = 3)1 3.08657 1.11518 2.768 0.006108 **
## bs(BOD, df = 3)2 -1.16757 0.64031 -1.823 0.069545 .
## bs(BOD, df = 3)3 3.48026 0.88861 3.917 0.000119 ***
## bs(TSS, df = 3)1 -0.82848 1.04768 -0.791 0.429893
## bs(TSS, df = 3)2 0.62061 0.60389 1.028 0.305186
## bs(TSS, df = 3)3 -0.89559 0.77871 -1.150 0.251306
## bs(Suhu, df = 3)1 -3.25497 2.66622 -1.221 0.223416
## bs(Suhu, df = 3)2 10.86728 14.29206 0.760 0.447818
## bs(Suhu, df = 3)3 -0.05014 1.00583 -0.050 0.960288
## ---
## Signif. codes: 0 '***' 0.001 '**' 0.01 '*' 0.05 '.' 0.1 ' ' 1
##
## Residual standard error: 0.9645 on 228 degrees of freedom
## Multiple R-squared: 0.07903, Adjusted R-squared: 0.03055
## F-statistic: 1.63 on 12 and 228 DF, p-value: 0.08435
Dibangun dua model regresi, yaitu Regresi Linear dan
Regresi Spline, dengan variabel prediktor pH, BOD, TSS,
dan Suhu.
Dari hasil regresi linear, variabel BOD memiliki
nilai p-value = 0.0345, yang berarti signifikan terhadap
perubahan DO, sedangkan variabel lain (pH, TSS, dan Suhu) tidak
signifikan.
Model spline menunjukkan adanya peningkatan fleksibilitas, namun belum memberikan peningkatan performa yang berarti terhadap nilai DO.
lm_pred <- predict(lm_model, newdata = test_reg)
spline_pred <- predict(spline_model, newdata = test_reg)
## Warning in bs(BOD, degree = 3L, knots = numeric(0), Boundary.knots = c(0.8993,
## : some 'x' values beyond boundary knots may cause ill-conditioned bases
## Warning in bs(Suhu, degree = 3L, knots = numeric(0), Boundary.knots =
## c(23.2896, : some 'x' values beyond boundary knots may cause ill-conditioned
## bases
R2_lm <- R2(lm_pred, test_reg$DO)
R2_spline <- R2(spline_pred, test_reg$DO)
MSE_lm <- mean((test_reg$DO - lm_pred)^2)
MSE_spline <- mean((test_reg$DO - spline_pred)^2)
RMSE_lm <- sqrt(MSE_lm)
RMSE_spline <- sqrt(MSE_spline)
eval_df <- data.frame(
Model = c("Regresi Linear", "Regresi Spline"),
R2 = c(R2_lm, R2_spline),
MSE = c(MSE_lm, MSE_spline),
RMSE = c(RMSE_lm, RMSE_spline)
)
print(eval_df)
## Model R2 MSE RMSE
## 1 Regresi Linear 0.02835838 0.8411642 0.9171500
## 2 Regresi Spline 0.00251645 0.9336114 0.9662357
Evaluasi dilakukan menggunakan metrik R², MSE, dan RMSE:
| Model | R² | MSE | RMSE |
|---|---|---|---|
| Regresi Linear | 0.028 | 0.841 | 0.917 |
| Regresi Spline | 0.002 | 0.934 | 0.966 |
Nilai R² yang rendah pada kedua model menunjukkan bahwa variabel pH,
BOD, TSS, dan Suhu belum cukup menjelaskan variasi nilai DO
secara signifikan.
Namun, model linear masih sedikit lebih baik dibanding spline karena
memiliki galat (RMSE) yang lebih kecil.
library(ggplot2)
pred_df <- data.frame(
DO_Aktual = test_reg$DO,
DO_LM = lm_pred,
DO_Spline = spline_pred
)
ggplot(pred_df, aes(x = DO_Aktual)) +
geom_point(aes(y = DO_LM, color = "Regresi Linear"), size = 2) +
geom_point(aes(y = DO_Spline, color = "Regresi Spline"), size = 2, shape = 17) +
geom_abline(slope = 1, intercept = 0, linetype = "dashed") +
labs(
title = "Perbandingan Nilai Aktual vs Prediksi DO",
x = "DO Aktual",
y = "DO Prediksi",
color = "Model"
) +
theme_minimal()
Plot perbandingan antara nilai aktual DO dan
hasil prediksi memperlihatkan bahwa sebaran titik
prediksi dari kedua model cenderung mendekati garis diagonal, meskipun
tidak terlalu rapat.
Artinya, prediksi DO cenderung mendekati nilai aktual, tetapi masih
terdapat penyimpangan yang cukup besar terutama di beberapa titik
ekstrem.
Model linear menghasilkan distribusi prediksi yang lebih stabil dibandingkan model spline.
summary(lm_model)$coefficients
## Estimate Std. Error t value Pr(>|t|)
## (Intercept) 5.8944142390 1.052488498 5.60045478 5.926828e-08
## pH -0.0488955018 0.127006888 -0.38498307 7.005967e-01
## BOD 0.1687462868 0.079364827 2.12620998 3.452423e-02
## TSS 0.0002083945 0.006638268 0.03139291 9.749827e-01
## Suhu -0.0036443269 0.014049037 -0.25940048 7.955524e-01
Berdasarkan hasil regresi linear:
BOD berpengaruh positif signifikan terhadap DO (p = 0.0345), artinya semakin tinggi nilai BOD, nilai DO cenderung meningkat sedikit. Namun secara teori, hubungan ini bisa berbeda tergantung kondisi perairan (karena peningkatan BOD biasanya menurunkan DO akibat konsumsi oksigen oleh mikroorganisme).
pH, TSS, dan Suhu tidak menunjukkan pengaruh signifikan terhadap DO dalam model ini.
Secara umum, model menunjukkan bahwa variabel-variabel ini belum cukup kuat untuk memprediksi DO secara akurat, dan dibutuhkan penambahan variabel lain seperti kadar nitrat, amonia, atau kecepatan arus air untuk meningkatkan performa model.