library(ggplot2)
## Warning: package 'ggplot2' was built under R version 4.5.3
set.seed(123)
eps <- 0.78
minPts <- 4
#pusat cluster
centers <- data.frame(
cluster = c("A", "C", "B", "D"),
x = c(2.6, 5.9, 2.7, 5.3),
y = c(5.2, 5.2, 2.0, 2.0)
)
#core point
generate_cluster <- function(cx, cy, n = 28, radius = 0.48) {
angle <- runif(n, 0, 2 * pi)
distance <- sqrt(runif(n)) * radius
data.frame(
x = cx + distance * cos(angle),
y = cy + distance * sin(angle)
)
}
core_points <- do.call(
rbind,
lapply(1:nrow(centers), function(i) {
d <- generate_cluster(centers$x[i], centers$y[i])
d$cluster <- centers$cluster[i]
d
})
)
#border point
sudut_tepi <- c(20, 110, 200, 290) # derajat
cari_titik_tepi <- function(cx, cy, derajat) {
a <- derajat * pi / 180
for (r in seq(0.30, 3.0, by = 0.002)) {
px <- cx + r * cos(a)
py <- cy + r * sin(a)
n_core <- sum(sqrt((core_points$x - px)^2 + (core_points$y - py)^2) <= eps)
if (n_core <= minPts - 2) return(c(px, py))
}
stop("Titik tepi tidak ditemukan, ubah sudut atau parameter.")
}
kandidat_tepi <- do.call(
rbind,
lapply(1:nrow(centers), function(i) {
do.call(rbind, lapply(sudut_tepi, function(s) {
p <- cari_titik_tepi(centers$x[i], centers$y[i], s)
data.frame(x = p[1], y = p[2], cluster = centers$cluster[i])
}))
})
)
#noise
kandidat_noise <- data.frame(
x = c(0.8, 7.0, 6.5),
y = c(6.0, 3.2, 1.0),
cluster = NA_character_
)
#klasifikasi semua titik
semua <- rbind(
data.frame(core_points, asal = "core"),
data.frame(kandidat_tepi, asal = "kandidat_border"),
data.frame(kandidat_noise, asal = "kandidat_noise")
)
D <- as.matrix(dist(semua[, c("x", "y")]))
semua$tetangga <- rowSums(D <= eps) # termasuk titik itu sendiri
is_core <- semua$tetangga >= minPts
idx_core <- which(is_core)
semua$jarak_core <- sapply(seq_len(nrow(semua)), function(i)
if (is_core[i]) 0 else min(D[i, idx_core]))
semua$jenis <- ifelse(is_core, "Core",
ifelse(semua$jarak_core <= eps, "Border", "Noise"))
# Pemeriksaan: hasil klasifikasi harus sesuai maksud tiap kelompok titik
stopifnot(
"Semua titik asal 'core' harus berjenis Core" =
all(semua$jenis[semua$asal == "core"] == "Core"),
"Semua kandidat border harus berjenis Border" =
all(semua$jenis[semua$asal == "kandidat_border"] == "Border"),
"Semua kandidat noise harus berjenis Noise" =
all(semua$jenis[semua$asal == "kandidat_noise"] == "Noise")
)
cat("Jumlah titik per jenis:\n")
## Jumlah titik per jenis:
print(table(semua$jenis))
##
## Border Core Noise
## 16 112 3
cat("\nDaftar titik non-core (border dan noise):\n")
##
## Daftar titik non-core (border dan noise):
tabel_nc <- semua[semua$jenis != "Core",
c("x", "y", "cluster", "jenis", "tetangga", "jarak_core")]
tabel_nc$x <- round(tabel_nc$x, 3)
tabel_nc$y <- round(tabel_nc$y, 3)
tabel_nc$jarak_core <- round(tabel_nc$jarak_core, 3)
print(tabel_nc, row.names = FALSE)
## x y cluster jenis tetangga jarak_core
## 3.604 5.565 A Border 3 0.641
## 2.255 6.149 A Border 3 0.696
## 1.578 4.828 A Border 3 0.670
## 2.958 4.217 A Border 3 0.677
## 6.815 5.533 C Border 3 0.635
## 5.504 6.288 C Border 3 0.730
## 4.893 4.833 C Border 3 0.667
## 6.271 4.179 C Border 3 0.663
## 3.705 2.366 B Border 2 0.728
## 2.340 2.989 B Border 3 0.610
## 1.698 1.635 B Border 3 0.626
## 3.053 1.030 B Border 3 0.685
## 6.228 2.338 D Border 3 0.740
## 4.906 3.083 D Border 3 0.696
## 4.189 1.596 D Border 3 0.741
## 5.652 1.032 D Border 3 0.615
## 0.800 6.000 <NA> Noise 1 1.506
## 7.000 3.200 <NA> Noise 1 1.779
## 6.500 1.000 <NA> Noise 1 1.177
cat("\nJarak border terjauh ke core terdekat:",
round(max(semua$jarak_core[semua$jenis == "Border"]), 3), "(eps =", eps, ")\n")
##
## Jarak border terjauh ke core terdekat: 0.741 (eps = 0.78 )
cat("Jarak noise terdekat ke core terdekat :",
round(min(semua$jarak_core[semua$jenis == "Noise"]), 3), "(eps =", eps, ")\n")
## Jarak noise terdekat ke core terdekat : 1.177 (eps = 0.78 )
core_df <- semua[which(semua$jenis == "Core"), ]
border_df <- semua[which(semua$jenis == "Border"), ]
noise_df <- semua[which(semua$jenis == "Noise"), ]
#Lingkaran epsilon di sekitar core point yang menjangkau tiap border
create_circle <- function(cx, cy, radius = eps, id = "A") {
angle <- seq(0, 2 * pi, length.out = 300)
data.frame(x = cx + radius * cos(angle),
y = cy + radius * sin(angle),
group = id)
}
idx_border <- which(semua$jenis == "Border")
anchor <- sapply(idx_border, function(i) idx_core[which.min(D[i, idx_core])])
anchor_unik <- unique(anchor)
# Core yang paling dekat dengan pusat tiap cluster: dipakai untuk panah epsilon
pusat_core <- sapply(seq_len(nrow(centers)), function(i) {
kandidat <- idx_core[which(semua$cluster[idx_core] == centers$cluster[i])]
kandidat[which.min(sqrt((semua$x[kandidat] - centers$x[i])^2 +
(semua$y[kandidat] - centers$y[i])^2))]
})
lingkaran_idx <- unique(c(anchor_unik, pusat_core))
circles <- do.call(rbind, lapply(seq_along(lingkaran_idx), function(k)
create_circle(semua$x[lingkaran_idx[k]], semua$y[lingkaran_idx[k]],
id = paste0("c", k))))
# Panah epsilon: dari core pusat tiap cluster ke kanan, panjang = eps
panah <- data.frame(x = semua$x[pusat_core], y = semua$y[pusat_core])
panah$xend <- panah$x + eps
# Titik acuan untuk anotasi (diambil dari data, bukan koordinat manual)
b_A <- border_df[which(border_df$cluster == "A"), ][1, ]
b_C2 <- border_df[which(border_df$cluster == "C"), ][2, ]
b_Cr <- border_df[which(border_df$cluster == "C"), ]
b_Cr <- b_Cr[which.max(b_Cr$x), ]
b_B2 <- border_df[which(border_df$cluster == "B"), ][2, ]
b_B3 <- border_df[which(border_df$cluster == "B"), ][3, ]
core_C <- core_df[which(core_df$cluster == "C"), ]
core_Cr <- core_C[which.max(core_C$x), ]
#visualisasi
panah_tipe <- grid::arrow(length = grid::unit(0.13, "inches"))
p <- ggplot() +
# Lingkaran epsilon
geom_path(
data = circles,
aes(x = x, y = y, group = group),
color = "grey65", linetype = "dashed", linewidth = 0.45
) +
# Core points
geom_point(data = core_df, aes(x, y), shape = 21, size = 2.8,
fill = "#3986C6", color = "#245F99", stroke = 0.3) +
# Border points
geom_point(data = border_df, aes(x, y), shape = 21, size = 2.8,
fill = "#B5D98B", color = "#7EAD5B", stroke = 0.4) +
# Noise points
geom_point(data = noise_df, aes(x, y), shape = 21, size = 3.3,
fill = "#F28C36", color = "#B96C24", stroke = 0.4) +
# Label cluster (nama unik)
annotate("text", x = 2.6, y = 3.7, label = "Cluster A", size = 4.2) +
annotate("text", x = 5.9, y = 3.7, label = "Cluster C", size = 4.2) +
annotate("text", x = 2.7, y = 0.65, label = "Cluster B", size = 4.2) +
annotate("text", x = 5.3, y = 0.65, label = "Cluster D", size = 4.2) +
# CORE POINTS
annotate("text", x = 3.0, y = 6.75, label = "CORE POINTS", size = 5.2) +
annotate("segment", x = 3.55, y = 6.5, xend = 2.85, yend = 5.55,
arrow = panah_tipe, linewidth = 0.7) +
annotate("segment", x = 3.7, y = 6.5, xend = 5.62, yend = 5.6,
arrow = panah_tipe, linewidth = 0.7) +
# BORDER POINTS (atas): panah ke border sungguhan cluster C
annotate("text", x = 4.8, y = 6.95, label = "BORDER POINTS", size = 5) +
annotate("segment", x = 5.15, y = 6.75,
xend = b_C2$x - 0.05, yend = b_C2$y + 0.06,
arrow = panah_tipe, linewidth = 0.7) +
# BORDER POINTS (kiri bawah): panah ke border sungguhan cluster B
annotate("text", x = 0.85, y = 3.55, label = "BORDER\nPOINTS", size = 4.5) +
annotate("segment", x = 1.5, y = 3.4,
xend = b_B2$x - 0.06, yend = b_B2$y + 0.06,
arrow = panah_tipe, linewidth = 0.7) +
# Penjelasan border point (atas)
annotate("text", x = 7.25, y = 5.95,
label = "Border Point (Green) within\nCore Point's radius",
hjust = 0, size = 4.1) +
annotate("segment", x = 7.15, y = 5.8,
xend = b_Cr$x + 0.08, yend = b_Cr$y + 0.02,
arrow = panah_tipe, linewidth = 0.7) +
# Penjelasan core point (bawah)
annotate("text", x = 7.25, y = 4.55,
label = "Core Point (Blue) density radius\ncontains MinPts points",
hjust = 0, size = 4.1) +
annotate("segment", x = 7.15, y = 4.7,
xend = core_Cr$x + 0.07, yend = core_Cr$y - 0.02,
arrow = panah_tipe, linewidth = 0.7) +
# Radius epsilon (panjang panah = eps)
geom_segment(data = panah,
aes(x = x, y = y, xend = xend, yend = y),
arrow = grid::arrow(length = grid::unit(0.15, "inches"),
type = "closed"),
linewidth = 0.7) +
geom_text(data = panah, aes(x = x + eps / 2, y = y + 0.22),
label = "\u03b5", size = 5) +
# Contoh titik border
annotate("text", x = b_A$x + 0.12, y = b_A$y + 0.2,
label = "e.g., border point\nin cluster A", hjust = 0, size = 3.5) +
annotate("text", x = b_B3$x, y = b_B3$y - 0.33,
label = "e.g., border point\nin cluster B", size = 3.5) +
# Label noise
annotate("text", x = 0.9, y = 6.2, label = "Point N1", hjust = 0, size = 3.5) +
annotate("text", x = 0.9, y = 5.8,
label = "outside every core\npoint's radius = noise",
hjust = 0, size = 3.1, color = "grey30") +
annotate("text", x = 7.1, y = 3.0, label = "Point N2", hjust = 0, size = 3.5) +
annotate("text", x = 6.5, y = 1.4, label = "NOISE (OUTLIERS)",
size = 4) +
# Informasi parameter
annotate("text", x = 8.1, y = 2.65,
label = paste0("MinPts = ", minPts, "\nRadius = \u03b5"),
hjust = 0, size = 4.5) +
# Legenda
annotate("rect", xmin = 7.35, xmax = 9.9, ymin = 0.55, ymax = 2.15,
fill = "white", color = NA) +
annotate("point", x = 7.55, y = 1.85, shape = 21, size = 4,
fill = "#3986C6", color = "#245F99") +
annotate("text", x = 7.75, y = 1.85, label = "Core Point (Blue)",
hjust = 0, size = 4) +
annotate("point", x = 7.55, y = 1.35, shape = 21, size = 4,
fill = "#B5D98B", color = "#7EAD5B") +
annotate("text", x = 7.75, y = 1.35, label = "Border Point (Light Green)",
hjust = 0, size = 4) +
annotate("point", x = 7.55, y = 0.85, shape = 21, size = 4,
fill = "#F28C36", color = "#B96C24") +
annotate("text", x = 7.75, y = 0.85, label = "Noise/Outlier (Orange)",
hjust = 0, size = 4) +
# Sumbu koordinat
annotate("segment", x = 0.35, y = 0.35, xend = 9.5, yend = 0.35,
linewidth = 0.8,
arrow = grid::arrow(length = grid::unit(0.16, "inches"), type = "closed")) +
annotate("segment", x = 0.35, y = 0.35, xend = 0.35, yend = 7.1,
linewidth = 0.8,
arrow = grid::arrow(length = grid::unit(0.16, "inches"), type = "closed")) +
annotate("text", x = 9.35, y = 0.12, label = "X", size = 5) +
annotate("text", x = 0.15, y = 7.05, label = "Y", size = 5) +
labs(title = "DBSCAN CLUSTERING: KEY CONCEPTS") +
# ratio = 1 agar lingkaran epsilon benar-benar bulat
coord_fixed(ratio = 1, xlim = c(0, 10), ylim = c(0, 7.3),
expand = FALSE, clip = "off") +
theme_void(base_size = 14) +
theme(
plot.title = element_text(size = 21, face = "bold", hjust = 0.5,
color = "black", margin = margin(b = 18)),
plot.background = element_rect(fill = "white", color = NA),
plot.margin = margin(15, 15, 15, 15)
)
print(p)

ggsave("DBSCAN_Clustering_Key_Concepts.png", plot = p,
width = 13, height = 8.5, dpi = 300, bg = "white")
ggsave("DBSCAN_Clustering_Key_Concepts.pdf", plot = p,
width = 13, height = 8.5, bg = "white", device = cairo_pdf)