```Program Studi S1 Sains Data Fakultas Matematika dan Ilmu Pengetahuan Alam Universitas Negeri Surabaya —
Remaja merupakan kelompok usia yang rentan terhadap ketidakseimbangan asupan gizi akibat pola makan yang kurang teratur dan preferensi terhadap makanan cepat saji. Penelitian ini bertujuan untuk mengelompokkan makanan berdasarkan jenis dan kandungan nutrisinya guna membentuk kategori diet seimbang yang sesuai untuk remaja. Tiga metode clustering digunakan dalam analisis ini adalah DBSCAN, Fuzzy C-Means, dan Hierarchical Clustering. Dataset yang digunakan mencakup informasi nutrisi dari berbagai jenis makanan yang umum dikonsumsi oleh remaja Indonesia, yang diolah menggunakan pendekatan analisis multivariat. Penelitian ini diharapkan dapat membantu dalam merancang rekomendasi diet yang lebih personal dan sesuai dengan kebutuhan gizi remaja, sekaligus memberikan dasar bagi program gizi yang lebih efektif.
Index Terms: clustering, DBSCAN, Fuzzy C-Means, Hierarchical, statistical analysis, nutrition, adolescent diet, food composition.
Masa remaja merupakan periode krusial yang ditandai oleh pertumbuhan fisik dan perkembangan kognitif yang pesat. Asupan nutrisi yang adekuat sangat penting untuk mendukung proses-proses ini, meningkatkan daya tahan tubuh, dan mencegah timbulnya masalah kesehatan di masa depan. Namun, kelompok usia ini seringkali rentan terhadap ketidakseimbangan gizi. Pola makan yang tidak teratur, ditambah dengan kecenderungan mengonsumsi makanan cepat saji, dapat menyebabkan defisiensi atau kelebihan nutrisi tertentu, yang pada gilirannya dapat menghambat pertumbuhan optimal dan memengaruhi kesehatan jangka panjang.
Profil gizi makanan memiliki karakteristik yang kompleks dan multidimensional. Setiap makanan mengandung berbagai komponen nutrien yang saling terkait, mulai dari makronutrien seperti energi, protein, lemak, dan karbohidrat, hingga mikronutrien seperti vitamin dan mineral. Keterkaitan dan banyaknya dimensi data nutrisi ini menciptakan tantangan dalam mengidentifikasi pola konsumsi gizi secara efektif. Mengurai kompleksitas ini memerlukan pendekatan analisis data yang canggih untuk mengungkap struktur tersembunyi dalam data nutrisi.
Untuk mengatasi kompleksitas data nutrisi, teknik analisis data multivariat menjadi sangat diperlukan. Metode clustering seperti DBSCAN, Fuzzy C-Means, dan Hierarchical Clustering dapat mengelompokkan makanan berdasarkan kemiripan kandungan nutrisinya. Pendekatan ini memudahkan pemahaman terhadap pola diet yang tersebar dalam populasi, karena makanan dengan profil gizi serupa akan dikelompokkan bersama. Selain itu, penggunaan analisis multivariat juga membantu mengurangi dimensi data nutrisi yang banyak dan memudahkan interpretasi hasil. Dengan mengkombinasikan teknik-teknik ini, peneliti dapat mengidentifikasi kelompok makanan yang memiliki profil gizi serupa dan mengaitkannya dengan rekomendasi diet yang tepat, khususnya bagi remaja yang membutuhkan asupan nutrisi optimal untuk mendukung pertumbuhan dan aktivitas sehari-hari.
Penelitian ini bertujuan untuk menerapkan metode clustering DBSCAN, Fuzzy C-Means, dan Hierarchical Clustering untuk menganalisis data profil gizi makanan Indonesia. Tujuan utamanya adalah mengelompokkan makanan berdasarkan jenis dan kandungan nutrisinya guna membentuk kategori diet seimbang yang sesuai untuk remaja. Pendekatan ini diharapkan dapat memfasilitasi pengembangan sistem rekomendasi diet yang lebih personal dan adaptif, berdasarkan pola konsumsi sebenarnya di masyarakat. Dengan demikian, program gizi dapat menjadi lebih efektif dan tepat sasaran, mendukung kebutuhan nutrisi remaja secara lebih mendalam dan komprehensif.
Dataset yang digunakan dalam analisis ini berasal dari Tabel Komposisi Pangan Indonesia (TKPI) tahun 2020, yang diterbitkan oleh Kementerian Kesehatan Republik Indonesia. Dataset ini memuat informasi komposisi nutrisi dari berbagai jenis makanan yang umum dikonsumsi di Indonesia, mencakup makronutrien (energi, protein, lemak, karbohidrat) serta mikronutrien (vitamin dan mineral).
Pada saat pemuatan awal, dataset memiliki dimensi 1147 baris (item makanan) dan 22 kolom (variabel nutrisi). Namun, ditemukan adanya missing value yang cukup signifikan. Oleh karena itu, variabel dengan lebih dari 25% missing value dikeluarkan dari analisis. Variabel yang dipertahankan adalah: NATRIUM, SERAT, THIAMIN, BESI, KALSIUM, FOSFOR, LEMAK, PROTEIN, ABU, AIR, ENERGI, dan KH. Imputasi dilakukan untuk mengisi missing value yang tersisa dan scaling dilakukan terhadap data numerik agar rentangnya seragam.
Table 1. Summary of Dataset Characteristics and Preprocessing Steps
| Characteristic / Step | Description / Value |
|---|---|
| Initial Data Dimensions | 1147 Rows x 22 Columns |
| Variables with >25% Missing Values (Excluded) | RETINOL, KAR_TOTAL, B_KAR, TEMBAGA, RIBOFLAVIN, SENG, NIASIN, VIT_C |
| Variables Retained for Analysis | NATRIUM, SERAT, THIAMIN, BESI, KALSIUM, FOSFOR, LEMAK, PROTEIN, ABU, AIR, ENERGI, KH |
| Missing Values After Imputation | 0 |
| Final Data Dimensions | 1147 Rows x 12 Columns |
Dalam penelitian ini, tiga algoritma clustering yang berbeda diterapkan untuk mengelompokkan data profil gizi makanan yang telah dipreprocessing menggunakan Hierarchical Clustering (HC), Fuzzy C-Means (FCM), dan DBSCAN. Pemilihan ketiga algoritma ini merefleksikan pendekatan komprehensif untuk mengeksplorasi struktur data nutrisi yang mendasari. Ini mengakui bahwa karakteristik data yang berbeda mungkin paling baik ditangkap oleh filosofi algoritmik yang berbeda, sehingga memungkinkan perbandingan kinerja yang mendalam.
eps
(epsilon), yang mendefinisikan radius maksimum lingkungan, dan
MinPts (minimum points), yang mendefinisikan jumlah minimum
titik yang diperlukan untuk membentuk daerah padat.Untuk mengevaluasi kualitas hasil clustering dan representasi data, dua metrik utama digunakan: 1. Silhouette Score: Metrik ini digunakan untuk mengukur seberapa mirip suatu objek dengan _cluster_nya sendiri (kohesi) dibandingkan dengan cluster lain (pemisahan). Skor berkisar dari -1 hingga +1, di mana nilai tinggi menunjukkan bahwa objek sangat cocok dengan _cluster_nya sendiri dan tidak cocok dengan cluster tetangga. Metrik ini digunakan untuk membandingkan kinerja Hierarchical Clustering dan Fuzzy C-Means. 2. Multidimensional Scaling (MDS): MDS adalah teknik visualisasi yang digunakan untuk merepresentasikan kemiripan kasus individu dari dataset. Tujuannya adalah untuk menggambarkan jarak antar titik data dalam ruang berdimensi tinggi seakurat mungkin dalam ruang berdimensi lebih rendah (biasanya 2D atau 3D). Nilai “stress” dalam MDS mengukur perbedaan antara jarak dalam ruang berdimensi tinggi asli dan jarak dalam representasi berdimensi rendah. Nilai stress yang lebih rendah menunjukkan kecocokan yang lebih baik.
Penggunaan kombinasi metrik kuantitatif kualitas clustering (Silhouette Score) dan teknik visualisasi dengan ukuran kecocokan (MDS Stress) memberikan evaluasi yang kuat dan multi-aspek terhadap hasil clustering. Ini memastikan bahwa cluster yang dipilih tidak hanya valid secara statistik tetapi juga dapat diinterpretasikan dalam ruang berdimensi rendah, yang penting untuk memahami dan mengomunikasikan pola nutrisi yang kompleks.
# ========================================
# R SCRIPT ANALISIS PROFIL GIZI MAKANAN REMAJA
# ========================================
# Script ini melakukan analisis multivariate pada data gizi makanan
# berdasarkan Rencana Analisis Ideal yang diberikan.
# ========================================
# 1. INSTALL & LOAD LIBRARIES
# ========================================
# Pastikan semua library yang dibutuhkan terinstall
packages <- c("cluster", "factoextra", "dbscan", "psych",
"MASS","tidyverse", "corrplot", "naniar", "vegan", "e1071", "reshape2",
"ggpubr", "clusterSim", "fpc", "RColorBrewer", "ggrepel")
# Cek dan install paket yang belum ada
installed_packages <- packages %in% rownames(installed.packages())
if (any(installed_packages == FALSE)) {
install.packages(packages[!installed_packages])
}
# Load semua library
lapply(packages, library, character.only = TRUE)
## [[1]]
## [1] "cluster" "stats" "graphics" "grDevices" "utils" "datasets"
## [7] "methods" "base"
##
## [[2]]
## [1] "factoextra" "ggplot2" "cluster" "stats" "graphics"
## [6] "grDevices" "utils" "datasets" "methods" "base"
##
## [[3]]
## [1] "dbscan" "factoextra" "ggplot2" "cluster" "stats"
## [6] "graphics" "grDevices" "utils" "datasets" "methods"
## [11] "base"
##
## [[4]]
## [1] "psych" "dbscan" "factoextra" "ggplot2" "cluster"
## [6] "stats" "graphics" "grDevices" "utils" "datasets"
## [11] "methods" "base"
##
## [[5]]
## [1] "MASS" "psych" "dbscan" "factoextra" "ggplot2"
## [6] "cluster" "stats" "graphics" "grDevices" "utils"
## [11] "datasets" "methods" "base"
##
## [[6]]
## [1] "lubridate" "forcats" "stringr" "dplyr" "purrr"
## [6] "readr" "tidyr" "tibble" "tidyverse" "MASS"
## [11] "psych" "dbscan" "factoextra" "ggplot2" "cluster"
## [16] "stats" "graphics" "grDevices" "utils" "datasets"
## [21] "methods" "base"
##
## [[7]]
## [1] "corrplot" "lubridate" "forcats" "stringr" "dplyr"
## [6] "purrr" "readr" "tidyr" "tibble" "tidyverse"
## [11] "MASS" "psych" "dbscan" "factoextra" "ggplot2"
## [16] "cluster" "stats" "graphics" "grDevices" "utils"
## [21] "datasets" "methods" "base"
##
## [[8]]
## [1] "naniar" "corrplot" "lubridate" "forcats" "stringr"
## [6] "dplyr" "purrr" "readr" "tidyr" "tibble"
## [11] "tidyverse" "MASS" "psych" "dbscan" "factoextra"
## [16] "ggplot2" "cluster" "stats" "graphics" "grDevices"
## [21] "utils" "datasets" "methods" "base"
##
## [[9]]
## [1] "vegan" "lattice" "permute" "naniar" "corrplot"
## [6] "lubridate" "forcats" "stringr" "dplyr" "purrr"
## [11] "readr" "tidyr" "tibble" "tidyverse" "MASS"
## [16] "psych" "dbscan" "factoextra" "ggplot2" "cluster"
## [21] "stats" "graphics" "grDevices" "utils" "datasets"
## [26] "methods" "base"
##
## [[10]]
## [1] "e1071" "vegan" "lattice" "permute" "naniar"
## [6] "corrplot" "lubridate" "forcats" "stringr" "dplyr"
## [11] "purrr" "readr" "tidyr" "tibble" "tidyverse"
## [16] "MASS" "psych" "dbscan" "factoextra" "ggplot2"
## [21] "cluster" "stats" "graphics" "grDevices" "utils"
## [26] "datasets" "methods" "base"
##
## [[11]]
## [1] "reshape2" "e1071" "vegan" "lattice" "permute"
## [6] "naniar" "corrplot" "lubridate" "forcats" "stringr"
## [11] "dplyr" "purrr" "readr" "tidyr" "tibble"
## [16] "tidyverse" "MASS" "psych" "dbscan" "factoextra"
## [21] "ggplot2" "cluster" "stats" "graphics" "grDevices"
## [26] "utils" "datasets" "methods" "base"
##
## [[12]]
## [1] "ggpubr" "reshape2" "e1071" "vegan" "lattice"
## [6] "permute" "naniar" "corrplot" "lubridate" "forcats"
## [11] "stringr" "dplyr" "purrr" "readr" "tidyr"
## [16] "tibble" "tidyverse" "MASS" "psych" "dbscan"
## [21] "factoextra" "ggplot2" "cluster" "stats" "graphics"
## [26] "grDevices" "utils" "datasets" "methods" "base"
##
## [[13]]
## [1] "clusterSim" "ggpubr" "reshape2" "e1071" "vegan"
## [6] "lattice" "permute" "naniar" "corrplot" "lubridate"
## [11] "forcats" "stringr" "dplyr" "purrr" "readr"
## [16] "tidyr" "tibble" "tidyverse" "MASS" "psych"
## [21] "dbscan" "factoextra" "ggplot2" "cluster" "stats"
## [26] "graphics" "grDevices" "utils" "datasets" "methods"
## [31] "base"
##
## [[14]]
## [1] "fpc" "clusterSim" "ggpubr" "reshape2" "e1071"
## [6] "vegan" "lattice" "permute" "naniar" "corrplot"
## [11] "lubridate" "forcats" "stringr" "dplyr" "purrr"
## [16] "readr" "tidyr" "tibble" "tidyverse" "MASS"
## [21] "psych" "dbscan" "factoextra" "ggplot2" "cluster"
## [26] "stats" "graphics" "grDevices" "utils" "datasets"
## [31] "methods" "base"
##
## [[15]]
## [1] "RColorBrewer" "fpc" "clusterSim" "ggpubr" "reshape2"
## [6] "e1071" "vegan" "lattice" "permute" "naniar"
## [11] "corrplot" "lubridate" "forcats" "stringr" "dplyr"
## [16] "purrr" "readr" "tidyr" "tibble" "tidyverse"
## [21] "MASS" "psych" "dbscan" "factoextra" "ggplot2"
## [26] "cluster" "stats" "graphics" "grDevices" "utils"
## [31] "datasets" "methods" "base"
##
## [[16]]
## [1] "ggrepel" "RColorBrewer" "fpc" "clusterSim" "ggpubr"
## [6] "reshape2" "e1071" "vegan" "lattice" "permute"
## [11] "naniar" "corrplot" "lubridate" "forcats" "stringr"
## [16] "dplyr" "purrr" "readr" "tidyr" "tibble"
## [21] "tidyverse" "MASS" "psych" "dbscan" "factoextra"
## [26] "ggplot2" "cluster" "stats" "graphics" "grDevices"
## [31] "utils" "datasets" "methods" "base"
# ========================================
# 2. LOAD DATA
# ========================================
# Pastikan file dataset berada di direktori kerja atau gunakan path lengkap
file_path <- "ANMUL - DATASET PANGAN.csv"
# Periksa apakah file ada
if (!file.exists(file_path)) {
stop("File dataset tidak ditemukan. Pastikan path file sudah benar.")
}
# Membaca data, melewati 2 baris pertama (jika ada header tambahan)
data_raw <- read.csv(file_path, skip = 2, stringsAsFactors = FALSE, na.strings = c("NA", "", "-"))
# Rename kolom sesuai dengan struktur data
colnames(data_raw) <- c("KODE", "NAMA_BAHAN", "SUMBER", "AIR", "ENERGI", "PROTEIN",
"LEMAK", "KH", "SERAT", "ABU", "KALSIUM", "FOSFOR", "BESI",
"NATRIUM", "KALIUM", "TEMBAGA", "ZINC", "RETINOL", "B_KAR",
"KAR_TOTAL", "THIAMIN", "RIBOFLAVIN", "NIASIN", "VIT_C", "BDD")
# Memproses kolom nutrien menjadi numerik
nutrien_cols_indices <- 4:24
nutrien_cols_names <- colnames(data_raw)[nutrien_cols_indices]
data_processed <- data_raw %>%
mutate(across(all_of(nutrien_cols_names), ~ suppressWarnings(as.numeric(as.character(.)))))
# Menyiapkan data numerik untuk analisis
data_numeric <- data_processed %>%
dplyr::select(NAMA_BAHAN, all_of(nutrien_cols_names))
# Tampilkan dimensi data sebagai validasi
cat("Dimensi data setelah load:", dim(data_numeric), "\n")
## Dimensi data setelah load: 1147 22
# ========================================
# 3. PREPROCESSING DATA
# ========================================
data_numeric_nutrients_only <- data_numeric %>% dplyr::select(-NAMA_BAHAN)
missing_summary <- miss_var_summary(data_numeric_nutrients_only)
print("Persentase Missing Value per Variabel (Hanya Nutrisi):")
## [1] "Persentase Missing Value per Variabel (Hanya Nutrisi):"
print(missing_summary)
## # A tibble: 21 × 3
## variable n_miss pct_miss
## <chr> <int> <num>
## 1 RETINOL 607 52.9
## 2 KAR_TOTAL 542 47.3
## 3 B_KAR 472 41.2
## 4 TEMBAGA 356 31.0
## 5 RIBOFLAVIN 353 30.8
## 6 ZINC 350 30.5
## 7 NIASIN 344 30.0
## 8 KALIUM 330 28.8
## 9 VIT_C 308 26.9
## 10 NATRIUM 286 24.9
## # ℹ 11 more rows
vars_to_keep_nutrients <- missing_summary %>%
filter(pct_miss < 25) %>%
pull(variable)
data_filtered <- data_numeric %>%
dplyr::select(NAMA_BAHAN, all_of(vars_to_keep_nutrients))
cat("\nVariabel yang digunakan setelah filtering missing value (>25%):")
##
## Variabel yang digunakan setelah filtering missing value (>25%):
kept_vars_after_missing <- colnames(data_filtered %>% dplyr::select(-NAMA_BAHAN))
print(kept_vars_after_missing)
## [1] "NATRIUM" "SERAT" "THIAMIN" "BESI" "KALSIUM" "FOSFOR" "LEMAK"
## [8] "PROTEIN" "ABU" "AIR" "ENERGI" "KH"
cat("Dimensi data setelah filtering variabel:", dim(data_filtered), "\n")
## Dimensi data setelah filtering variabel: 1147 13
# **Penting:** Pastikan nama_bahan selalu diambil dari data_filtered yang sudah bersih
data_for_analysis <- data_filtered %>% dplyr::select(-NAMA_BAHAN)
nama_bahan <- data_filtered$NAMA_BAHAN # Dipastikan diambil dari data_filtered
# Melihat distribusi data numerik setelah filtering variabel
cat("\nMembuat plot distribusi untuk variabel numerik...\n")
##
## Membuat plot distribusi untuk variabel numerik...
plot_distribusi <- data_for_analysis %>%
dplyr::select(where(is.numeric)) %>%
pivot_longer(cols = everything(), names_to = "Variabel", values_to = "Nilai") %>%
ggplot(aes(x = Nilai)) +
geom_histogram(bins = 30, fill = "skyblue", color = "black") +
facet_wrap(~Variabel, scales = "free") +
theme_minimal() +
labs(title = "Distribusi Data Numerik Setelah Filtering Variabel", x = "Nilai", y = "Frekuensi")
print(plot_distribusi)
## Warning: Removed 748 rows containing non-finite outside the scale range
## (`stat_bin()`).
# Menyimpan plot distribusi
ggsave("distribusi_data_numerik.png", plot_distribusi, width = 12, height = 8)
## Warning: Removed 748 rows containing non-finite outside the scale range
## (`stat_bin()`).
cat("Plot distribusi data numerik disimpan sebagai 'distribusi_data_numerik.png'.\n")
## Plot distribusi data numerik disimpan sebagai 'distribusi_data_numerik.png'.
# --- Perbaikan: Tangani nilai Inf, -Inf, dan NaN sebelum imputasi ---
# Mengubah nilai Inf, -Inf, dan NaN menjadi NA agar bisa diimputasi median
data_for_analysis <- data_for_analysis %>%
mutate(across(where(is.numeric), ~na_if(., Inf))) %>%
mutate(across(where(is.numeric), ~na_if(., -Inf))) %>%
mutate(across(where(is.numeric), ~ifelse(is.nan(.), NA, .)))
# --- Akhir Perbaikan ---
if (any(is.na(data_for_analysis))) {
cat("\nMelakukan imputasi median untuk missing values...\n")
data_imputed <- data_for_analysis %>%
mutate(across(everything(), ~ ifelse(is.na(.), median(., na.rm = TRUE), .)))
cat("Jumlah NA setelah imputasi:", sum(is.na(data_imputed)), "\n")
} else {
cat("\nTidak ada missing value yang perlu diimputasi setelah filtering variabel.\n")
data_imputed <- data_for_analysis
}
##
## Melakukan imputasi median untuk missing values...
## Jumlah NA setelah imputasi: 0
cat("\nMelakukan scaling data (Z-score)...\n")
##
## Melakukan scaling data (Z-score)...
data_scaled_original <- as.data.frame(scale(data_imputed))
# data_scaled_original ini yang akan langsung kita gunakan untuk clustering
cat("Dimensi data setelah scaling awal:", dim(data_scaled_original), "\n")
## Dimensi data setelah scaling awal: 1147 12
# ========================================
# 4. CLUSTERING (Menggunakan Data Asli yang Sudah Skala)
# ========================================
# Inisialisasi variabel hasil clustering
# Penting: Sesuaikan panjang rep() dengan jumlah baris data_scaled_original
clusters_hc <- rep(NA, nrow(data_scaled_original))
clusters_fcm <- rep(NA, nrow(data_scaled_original))
clusters_db <- rep(NA, nrow(data_scaled_original))
k_optimal_hc <- NA_integer_
k_fcm <- NA_integer_
eps_chosen <- NA_real_
min_pts_dbscan_val <- NA_integer_
dist_mat <- NULL # Mengganti nama dari dist_mat_pca agar lebih jelas
# Pastikan data_for_clustering menggunakan data yang sudah diskalakan
if (nrow(data_scaled_original) > 0 && ncol(data_scaled_original) > 0) {
cat("\nMemulai Analisis Clustering pada data asli yang sudah di-scaled...\n")
data_for_clustering <- data_scaled_original # Perubahan penting di sini!
# 4.1. Hierarchical Clustering (HC)
cat("\n--- Hierarchical Clustering (HC) ---\n")
dist_mat <- dist(data_for_clustering, method = "euclidean") # dihitung dari data_for_clustering yang baru
hc_ward <- hclust(dist_mat, method = "ward.D2")
print("Membuat Dendrogram HC...")
# Simpan Dendrogram HC
png("dendrogram_hc.png", width = 1000, height = 600, res = 100)
plot(hc_ward, main = "Dendrogram HC (Ward.D2)", xlab = "Observasi", ylab = "Height", sub = "", cex = 0.6, labels = FALSE)
dev.off()
cat("Dendrogram HC disimpan sebagai 'dendrogram_hc.png'.\n")
cat("\nMengevaluasi jumlah cluster optimal (k=2 to 5) untuk HC...\n")
sil_scores_hc <- numeric(4)
k_options_hc <- 2:5
valid_k_found_hc <- FALSE
if (nrow(data_for_clustering) >= max(k_options_hc) +1) { # +1 agar cukup data untuk cluster terakhir
for (i_hc in seq_along(k_options_hc)) {
k_hc <- k_options_hc[i_hc]
if (nrow(data_for_clustering) < k_hc) { # Cek jika data kurang dari k
cat("Data (",nrow(data_for_clustering),") < k (",k_hc,") untuk Silhouette. Lewati.\n")
sil_scores_hc[i_hc] <- NA
next
}
clusters_temp_hc <- cutree(hc_ward, k = k_hc)
if (length(unique(clusters_temp_hc)) > 1 && min(table(clusters_temp_hc)) > 1) {
sil_hc <- silhouette(clusters_temp_hc, dist_mat) # Menggunakan dist_mat yang baru
sil_scores_hc[i_hc] <- mean(sil_hc[, 3])
cat("Rata-rata Silhouette Score untuk k =", k_hc, ":", sil_scores_hc[i_hc], "\n")
# print(fviz_silhouette(sil_hc) + ggtitle(paste("Silhouette Plot HC, k=", k_hc)))
valid_k_found_hc <- TRUE
} else {
cat("Tidak dapat menghitung Silhouette untuk k =", k_hc, "(singleton cluster atau hanya 1 cluster).\n")
sil_scores_hc[i_hc] <- NA
}
}
if(valid_k_found_hc && any(!is.na(sil_scores_hc))){
k_optimal_hc <- k_options_hc[which.max(sil_scores_hc[!is.na(sil_scores_hc)])] # Ambil max dari yg tidak NA
} else {
k_optimal_hc <- 3 # Default jika tidak ada yang valid
cat("Tidak ada k optimal valid dari Silhouette, menggunakan default k=", k_optimal_hc, "\n")
}
} else {
k_optimal_hc <- min(c(2, if(nrow(data_for_clustering)>1) nrow(data_for_clustering) -1 else 2), na.rm=TRUE)
if(is.na(k_optimal_hc) || k_optimal_hc < 2) k_optimal_hc = 2
cat("Data terlalu sedikit untuk evaluasi k=2-5, menggunakan k=", k_optimal_hc, "\n")
}
cat("\nJumlah cluster optimal (k) yang dipilih untuk HC:", k_optimal_hc, "\n")
if (!is.na(k_optimal_hc) && k_optimal_hc >=2 && nrow(data_for_clustering) >= k_optimal_hc) {
clusters_hc <- cutree(hc_ward, k = k_optimal_hc)
} else {
clusters_hc <- rep(NA, nrow(data_for_clustering)) # Sesuaikan panjangnya
}
# 4.2. Fuzzy C-Means (FCM)
cat("\n--- Fuzzy C-Means (FCM) ---\n")
k_fcm <- k_optimal_hc # Menggunakan k optimal dari HC
cat("Menggunakan k =", k_fcm, "untuk FCM (berdasarkan hasil HC).\n")
if (!is.na(k_fcm) && k_fcm >= 2 && nrow(data_for_clustering) >= k_fcm) {
fcm_result <- cmeans(data_for_clustering, centers = k_fcm, iter.max = 100, m = 2, method = "cmeans", dist = "euclidean")
clusters_fcm <- fcm_result$cluster
membership_matrix <- fcm_result$membership
# (Evaluasi membership bisa ditambahkan kembali jika diperlukan)
} else {
cat("Tidak dapat melakukan FCM, k_fcm tidak valid atau data kurang.\n")
clusters_fcm <- rep(NA, nrow(data_for_clustering))
}
# 4.3. DBSCAN
cat("\n--- DBSCAN ---\n")
data_dbscan_input <- data_for_clustering # DBSCAN juga pakai data_for_clustering yang baru
potential_min_pts_db <- ncol(data_dbscan_input) + 1
min_pts_dbscan_val <- max(2, potential_min_pts_db)
cat("Menggunakan MinPts =", min_pts_dbscan_val, "\n")
k_for_knnplot_db <- min_pts_dbscan_val - 1
eps_chosen <- 1.0 # Default fallback
if (k_for_knnplot_db <= 0) {
cat("Nilai k untuk kNNdistplot (MinPts - 1) adalah <= 0. Tidak dapat membuat plot.\n")
} else if (nrow(data_dbscan_input) <= k_for_knnplot_db) {
cat("Jumlah observasi (", nrow(data_dbscan_input), ") tidak cukup untuk k =", k_for_knnplot_db, "pada kNNdistplot.\n")
} else {
# Simpan kNNdistplot
png("kNNdistplot_dbscan.png", width = 800, height = 600, res = 100)
kNNdistplot(data_dbscan_input, k = k_for_knnplot_db)
kth_nn_distances_db <- kNNdist(data_dbscan_input, k = k_for_knnplot_db)
if (length(kth_nn_distances_db) > 0 && !all(is.na(kth_nn_distances_db))) {
median_kth_dist_db <- median(kth_nn_distances_db, na.rm = TRUE)
abline(h = median_kth_dist_db, col = "red", lty = 2)
cat("Perhatikan 'elbow' pada plot kNNdistplot.\n")
eps_chosen <- median_kth_dist_db
cat("\nMenggunakan eps =", round(eps_chosen,2) ,", estimasi dari median k-NN distance. Sesuaikan berdasarkan plot.\n")
} else {
cat("Tidak dapat menghitung jarak kNN. Menggunakan eps default=", eps_chosen, "\n")
}
dev.off()
cat("kNNdistplot disimpan sebagai 'kNNdistplot_dbscan.png'.\n")
}
if (!is.na(eps_chosen) && !is.na(min_pts_dbscan_val) && eps_chosen > 0 && min_pts_dbscan_val > 0) {
if(min_pts_dbscan_val > nrow(data_dbscan_input)){
cat("MinPts (", min_pts_dbscan_val, ") > data (", nrow(data_dbscan_input), "). DBSCAN tidak dijalankan.\n")
clusters_db <- rep(NA, nrow(data_dbscan_input))
} else {
dbscan_res <- dbscan::dbscan(data_dbscan_input, eps = eps_chosen, minPts = min_pts_dbscan_val)
clusters_db <- dbscan_res$cluster
}
} else {
cat("Parameter eps atau MinPts tidak valid. DBSCAN tidak dijalankan.\n")
clusters_db <- rep(NA, nrow(data_dbscan_input))
}
} else {
cat("Data tidak tersedia atau kosong. Clustering tidak dilakukan.\n")
# Pastikan panjang clusters_xx cocok dengan nama_bahan yang tidak di PCA
clusters_hc <- rep(NA, length(nama_bahan))
clusters_fcm <- rep(NA, length(nama_bahan))
clusters_db <- rep(NA, length(nama_bahan))
}
##
## Memulai Analisis Clustering pada data asli yang sudah di-scaled...
##
## --- Hierarchical Clustering (HC) ---
## [1] "Membuat Dendrogram HC..."
## Dendrogram HC disimpan sebagai 'dendrogram_hc.png'.
##
## Mengevaluasi jumlah cluster optimal (k=2 to 5) untuk HC...
## Rata-rata Silhouette Score untuk k = 2 : 0.3530866
## Rata-rata Silhouette Score untuk k = 3 : 0.369881
## Rata-rata Silhouette Score untuk k = 4 : 0.3837861
## Rata-rata Silhouette Score untuk k = 5 : 0.3863501
##
## Jumlah cluster optimal (k) yang dipilih untuk HC: 5
##
## --- Fuzzy C-Means (FCM) ---
## Menggunakan k = 5 untuk FCM (berdasarkan hasil HC).
##
## --- DBSCAN ---
## Menggunakan MinPts = 13
## Perhatikan 'elbow' pada plot kNNdistplot.
##
## Menggunakan eps = 0.94 , estimasi dari median k-NN distance. Sesuaikan berdasarkan plot.
## kNNdistplot disimpan sebagai 'kNNdistplot_dbscan.png'.
# Cek jumlah cluster dan noise DBSCAN
cat("\nHasil DBSCAN Clustering (0 = Noise):")
##
## Hasil DBSCAN Clustering (0 = Noise):
print(table(clusters_db, useNA = "ifany"))
## clusters_db
## 0 1
## 425 722
num_noise_dbscan <- sum(clusters_db == 0, na.rm = TRUE)
num_clusters_dbscan <- length(unique(clusters_db[!is.na(clusters_db) & clusters_db != 0]))
cat("Jumlah Noise Points:", num_noise_dbscan, "\n")
## Jumlah Noise Points: 425
cat("Jumlah Cluster Ditemukan (tidak termasuk noise):", num_clusters_dbscan, "\n")
## Jumlah Cluster Ditemukan (tidak termasuk noise): 1
if(!is.na(eps_chosen) && !is.na(min_pts_dbscan_val)) {
cat("Saran DBSCAN: Coba eps sekitar", round(eps_chosen,2), "dan MinPts", min_pts_dbscan_val, ".\n")
}
## Saran DBSCAN: Coba eps sekitar 0.94 dan MinPts 13 .
# ========================================
# 5. EVALUASI CLUSTERING
# ========================================
cat("\nMemulai Evaluasi Hasil Clustering...\n")
##
## Memulai Evaluasi Hasil Clustering...
# 5.1. Silhouette Score
# HC
cat("\nMenghitung Silhouette Score untuk HC (k=", k_optimal_hc, ")...\n")
##
## Menghitung Silhouette Score untuk HC (k= 5 )...
mean_sil_hc <- NA
if (!is.na(k_optimal_hc) && length(clusters_hc) == nrow(data_for_clustering) && !all(is.na(clusters_hc)) && !is.null(dist_mat)) { # Menggunakan dist_mat
valid_hc_indices_sil <- !is.na(clusters_hc)
clusters_hc_valid_sil <- clusters_hc[valid_hc_indices_sil]
if (length(unique(clusters_hc_valid_sil)) > 1 && min(table(clusters_hc_valid_sil)) > 1 && length(clusters_hc_valid_sil) > 1) {
data_hc_sil_valid <- data_for_clustering[valid_hc_indices_sil, , drop = FALSE]
if(nrow(data_hc_sil_valid) > 1){
dist_mat_hc_sil_valid <- dist(data_hc_sil_valid)
sil_hc_final <- silhouette(clusters_hc_valid_sil, dist_mat_hc_sil_valid)
mean_sil_hc <- mean(sil_hc_final[, 3])
cat("Rata-rata Silhouette Score HC:", mean_sil_hc, "\n")
} else {cat("Data HC valid < 2 untuk Silhouette.\n")}
} else { cat("Tidak bisa hitung Silhouette HC (singleton/1 cluster setelah filter NA).\n")}
} else { cat("Tidak bisa memulai Silhouette HC (k, clusters_hc, data_for_clustering, atau dist_mat tidak valid).\n")}
## Rata-rata Silhouette Score HC: 0.3863501
# FCM
cat("\nMenghitung Silhouette Score untuk FCM (k=", k_fcm, ")...\n")
##
## Menghitung Silhouette Score untuk FCM (k= 5 )...
mean_sil_fcm <- NA
if (!is.na(k_fcm) && length(clusters_fcm) == nrow(data_for_clustering) && !all(is.na(clusters_fcm)) && !is.null(dist_mat)) { # Menggunakan dist_mat
valid_fcm_indices_sil <- !is.na(clusters_fcm)
clusters_fcm_valid_sil <- clusters_fcm[valid_fcm_indices_sil]
if (length(unique(clusters_fcm_valid_sil)) > 1 && min(table(clusters_fcm_valid_sil)) > 1 && length(clusters_fcm_valid_sil) > 1) {
data_fcm_sil_valid <- data_for_clustering[valid_fcm_indices_sil, , drop = FALSE]
if(nrow(data_fcm_sil_valid) > 1){
dist_mat_fcm_sil_valid <- dist(data_fcm_sil_valid)
sil_fcm <- silhouette(clusters_fcm_valid_sil, dist_mat_fcm_sil_valid)
mean_sil_fcm <- mean(sil_fcm[, 3])
cat("Rata-rata Silhouette Score FCM:", mean_sil_fcm, "\n")
} else {cat("Data FCM valid < 2 untuk Silhouette.\n")}
} else { cat("Tidak bisa hitung Silhouette FCM (singleton/1 cluster setelah filter NA).\n")}
} else { cat("Tidak bisa memulai Silhouette FCM (k, clusters_fcm, data_for_clustering, atau dist_mat tidak valid).\n")}
## Rata-rata Silhouette Score FCM: 0.1911777
# DBSCAN
cat("\nMenghitung Silhouette Score untuk DBSCAN (hanya non-noise points)...\n")
##
## Menghitung Silhouette Score untuk DBSCAN (hanya non-noise points)...
mean_sil_dbscan <- NA
# data_dbscan_input adalah data_for_clustering yang baru
if (exists("clusters_db") && length(clusters_db) == nrow(data_dbscan_input) && !all(is.na(clusters_db))) {
non_noise_indices_db_sil <- which(clusters_db != 0 & !is.na(clusters_db))
if (length(non_noise_indices_db_sil) > 1) {
clusters_db_non_noise_sil <- clusters_db[non_noise_indices_db_sil]
if (length(unique(clusters_db_non_noise_sil)) > 1 && min(table(clusters_db_non_noise_sil)) > 1) {
data_dbscan_non_noise_sil <- data_dbscan_input[non_noise_indices_db_sil, , drop = FALSE]
if (nrow(data_dbscan_non_noise_sil) > 1) {
dist_mat_dbscan_non_noise_sil <- dist(data_dbscan_non_noise_sil, method = "euclidean")
sil_dbscan <- silhouette(clusters_db_non_noise_sil, dist_mat_dbscan_non_noise_sil)
mean_sil_dbscan <- mean(sil_dbscan[, 3])
cat("Rata-rata Silhouette Score DBSCAN (non-noise):", mean_sil_dbscan, "\n")
} else {cat("Data DBSCAN non-noise valid < 2 untuk Silhouette.\n")}
} else { cat("Tidak bisa hitung Silhouette DBSCAN (kurang dari 2 cluster non-noise atau singleton).\n")}
} else { cat("Tidak cukup titik non-noise untuk Silhouette DBSCAN.\n")}
} else { cat("Tidak bisa memulai Silhouette DBSCAN (clusters_db atau data_dbscan_input tidak valid).\n")}
## Tidak bisa hitung Silhouette DBSCAN (kurang dari 2 cluster non-noise atau singleton).
# 5.2. Visualisasi Cluster pada Plot
# Karena tidak ada PCA, kita tidak bisa lagi menggunakan fviz_pca_ind.
# Kita bisa membuat plot MDS dengan cluster, atau plot 2D dari nutrisi pilihan.
cat("\nMembuat visualisasi cluster pada plot...\n")
##
## Membuat visualisasi cluster pada plot...
# ========================================
# 6. PERCEPTUAL MAPPING (MDS)
# ========================================
# Menggunakan data_scaled_original untuk MDS agar tidak terpengaruh KMO variable removal
cat("\nMelakukan Perceptual Mapping (Metric MDS) pada data_scaled_original...\n")
##
## Melakukan Perceptual Mapping (Metric MDS) pada data_scaled_original...
mds_stress <- NA
mds_coords <- data.frame(Dim1=numeric(0), Dim2=numeric(0))
mds_df <- data.frame()
if (nrow(data_scaled_original) > 1 && ncol(data_scaled_original) > 0) {
cat("Mengecek duplikat dalam data_scaled_original sebelum MDS...\n")
duplicated_rows_mds <- duplicated(data_scaled_original)
data_scaled_mds_unique <- data_scaled_original[!duplicated_rows_mds, ]
nama_bahan_mds_unique <- nama_bahan[!duplicated_rows_mds]
# Sesuaikan juga clusters_hc dan clusters_db untuk plot MDS
clusters_hc_mds_unique <- if(length(clusters_hc) == length(duplicated_rows_mds)) clusters_hc[!duplicated_rows_mds] else rep(NA, nrow(data_scaled_mds_unique))
clusters_db_mds_unique <- if(length(clusters_db) == length(duplicated_rows_mds)) clusters_db[!duplicated_rows_mds] else rep(NA, nrow(data_scaled_mds_unique))
if (nrow(data_scaled_mds_unique) < 2) {
cat("Setelah menghapus duplikat, data untuk MDS < 2. MDS tidak dilakukan.\n")
} else {
dist_mat_mds_unique <- dist(data_scaled_mds_unique, method = "euclidean")
if (any(dist_mat_mds_unique <= 1e-9)) {
cat("PERINGATAN MDS: Jarak sangat kecil ditemukan. Mencoba cmdscale...\n")
mds_result_cmd <- tryCatch({ cmdscale(dist_mat_mds_unique, k = 2, eig = TRUE) }, error = function(e) { cat("cmdscale juga gagal:", e$message, "\n"); return(NULL) })
if (!is.null(mds_result_cmd) && !is.null(mds_result_cmd$points)) {
mds_coords <- as.data.frame(mds_result_cmd$points); colnames(mds_coords) <- c("Dim1", "Dim2"); mds_stress <- NA; cat("cmdscale berhasil (Stress N/A).\n")
} else { cat("cmdscale gagal. MDS tidak dilanjutkan.\n") }
} else {
mds_result_iso <- tryCatch({ MASS::isoMDS(dist_mat_mds_unique, k = 2, trace = FALSE) },
error = function(e) { cat("isoMDS gagal:", e$message, ". Mencoba cmdscale...\n");
tryCatch({ cmdscale(dist_mat_mds_unique, k = 2, eig = TRUE) }, error = function(e_cmd) { cat("cmdscale (fallback) juga gagal:", e_cmd$message, "\n"); return(NULL) })
})
if (!is.null(mds_result_iso) && !is.null(mds_result_iso$points)) {
if("stress" %in% names(mds_result_iso)){ mds_stress <- mds_result_iso$stress; mds_coords <- as.data.frame(mds_result_iso$points); colnames(mds_coords) <- c("Dim1", "Dim2"); cat("isoMDS berhasil. Stress:", mds_stress, "\n")
} else if ("points" %in% names(mds_result_iso)) { mds_coords <- as.data.frame(mds_result_iso$points); colnames(mds_coords) <- c("Dim1", "Dim2"); mds_stress <- NA; cat("isoMDS gagal, cmdscale (fallback) berhasil (Stress N/A).\n")
} else { cat("Struktur hasil MDS tidak dikenali.\n") }
} else { cat("Baik isoMDS maupun cmdscale (fallback) gagal.\n") }
}
if (nrow(mds_coords) > 0) {
mds_df <- mds_coords %>%
mutate(NAMA_BAHAN = nama_bahan_mds_unique,
Cluster_HC = as.factor(clusters_hc_mds_unique), # Pastikan ini faktor
Cluster_DBSCAN = as.factor(clusters_db_mds_unique)) # Pastikan ini faktor
}
}
} else { cat("MDS tidak dilakukan (data_scaled_original tidak valid).\n")}
## Mengecek duplikat dalam data_scaled_original sebelum MDS...
## isoMDS berhasil. Stress: 9.99271
if (nrow(mds_df) > 0) {
cat("Membuat plot MDS...\n")
# Plot MDS HC
if (any(!is.na(mds_df$Cluster_HC))) { # Cek apakah ada cluster HC yang valid
plot_mds_hc <- ggplot(mds_df, aes(x = Dim1, y = Dim2, color = Cluster_HC)) +
geom_point(size = 2, alpha = 0.7) +
ggrepel::geom_text_repel(aes(label = NAMA_BAHAN), size = 2.5, max.overlaps = 15, min.segment.length = 0) +
scale_color_brewer(palette = "jco", na.value="grey80") + # Tambah na.value
theme_minimal() +
labs(title = paste("MDS with HC Clusters (k=", if(!is.na(k_optimal_hc)) k_optimal_hc else "NA", ")"),
subtitle = paste("Stress:", if(!is.na(mds_stress)) round(mds_stress, 3) else "N/A"),
x = "MDS Dim 1", y = "MDS Dim 2", color = "HC Cluster")
print(plot_mds_hc)
# Menyimpan MDS Plot HC
ggsave("mds_plot_hc.png", plot_mds_hc, width = 10, height = 8)
cat("MDS Plot HC disimpan sebagai 'mds_plot_hc.png'.\n")
} else {cat("Tidak dapat membuat plot MDS untuk HC (tidak ada cluster HC valid di mds_df).\n")}
# Plot MDS DBSCAN
if (any(!is.na(mds_df$Cluster_DBSCAN))) {
unique_db_mds_plot <- sort(unique(mds_df$Cluster_DBSCAN[!is.na(mds_df$Cluster_DBSCAN)]))
num_actual_db_mds_plot <- length(unique_db_mds_plot[unique_db_mds_plot != "0"])
palette_db_mds_values_plot <- RColorBrewer::brewer.pal(n = max(3, num_actual_db_mds_plot +1 ), name = "Dark2")
final_palette_db_mds_plot <- setNames(rep("grey80", length(unique_db_mds_plot)), as.character(unique_db_mds_plot)) # default grey
idx_color_mds_plot = 1
if ("0" %in% names(final_palette_db_mds_plot)) { final_palette_db_mds_plot["0"] <- "grey50" }
non_noise_mds_names_plot <- as.character(unique_db_mds_plot[unique_db_mds_plot != "0"])
if (length(non_noise_mds_names_plot) > 0) {
for(cl_name_mds_plot in non_noise_mds_names_plot){
final_palette_db_mds_plot[cl_name_mds_plot] <- palette_db_mds_values_plot[idx_color_mds_plot]
idx_color_mds_plot <- idx_color_mds_plot + 1
if(idx_color_mds_plot > length(palette_db_mds_values_plot)) idx_color_mds_plot = 1
}
}
plot_mds_dbscan <- ggplot(mds_df, aes(x = Dim1, y = Dim2, color = Cluster_DBSCAN)) +
geom_point(size = 2, alpha = 0.7) +
ggrepel::geom_text_repel(aes(label = NAMA_BAHAN), size = 2.5, max.overlaps = 15, min.segment.length = 0) +
scale_color_manual(values = final_palette_db_mds_plot, na.value="grey80") +
theme_minimal() +
labs(title = paste("MDS with DBSCAN (eps=", if(!is.na(eps_chosen)) round(eps_chosen,2) else "NA", ", MinPts=", if(!is.na(min_pts_dbscan_val)) min_pts_dbscan_val else "NA", ")"),
subtitle = paste("Stress:", if(!is.na(mds_stress)) round(mds_stress, 3) else "N/A"),
x = "MDS Dim 1", y = "MDS Dim 2", color = "DBSCAN Cluster (0=Noise)")
print(plot_mds_dbscan)
# Menyimpan MDS Plot DBSCAN
ggsave("mds_plot_dbscan.png", plot_mds_dbscan, width = 10, height = 8)
cat("MDS Plot DBSCAN disimpan sebagai 'mds_plot_dbscan.png'.\n")
} else {cat("Tidak dapat membuat plot MDS untuk DBSCAN (tidak ada cluster DBSCAN valid di mds_df).\n")}
} else { cat("Tidak ada data MDS yang valid untuk diplot.\n")}
## Membuat plot MDS...
## Warning: Unknown palette: "jco"
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_text_repel()`).
## Warning: ggrepel: 1137 unlabeled data points (too many overlaps). Consider
## increasing max.overlaps
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_text_repel()`).
## Warning: ggrepel: 1127 unlabeled data points (too many overlaps). Consider
## increasing max.overlaps
## MDS Plot HC disimpan sebagai 'mds_plot_hc.png'.
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_text_repel()`).
## Warning: ggrepel: 1138 unlabeled data points (too many overlaps). Consider
## increasing max.overlaps
## Warning: Removed 1 row containing missing values or values outside the scale range
## (`geom_text_repel()`).
## Warning: ggrepel: 1128 unlabeled data points (too many overlaps). Consider
## increasing max.overlaps
## MDS Plot DBSCAN disimpan sebagai 'mds_plot_dbscan.png'.
# ========================================
# 7. INTERPRETASI DAN INSIGHT
# ========================================
cat("\nMenyiapkan Tabel Karakteristik Cluster...\n")
##
## Menyiapkan Tabel Karakteristik Cluster...
# `data_imputed` sekarang sudah disesuaikan jika ada variabel yang hilang karena KMO.
# Pastikan panjang `nama_bahan` masih sesuai dengan `data_imputed` (jumlah barisnya).
# Jika KMO menghilangkan variabel (kolom), jumlah baris `data_imputed` seharusnya tidak berubah.
# `clusters_xx` panjangnya harus sama dengan `data_imputed` dan `nama_bahan`.
# Panjang `clusters_xx` akan sama dengan `data_for_clustering` yang baru.
# Jika PCA gagal, `clusters_xx` diinisialisasi dengan panjang `nama_bahan`.
# Cek panjang sebelum membuat data_final
if (nrow(data_imputed) == length(nama_bahan) &&
length(clusters_hc) == length(nama_bahan) &&
length(clusters_fcm) == length(nama_bahan) &&
length(clusters_db) == length(nama_bahan)
) {
data_final <- data_imputed %>%
mutate(NAMA_BAHAN = nama_bahan,
Cluster_HC = as.factor(clusters_hc),
Cluster_FCM = as.factor(clusters_fcm),
Cluster_DBSCAN = as.factor(clusters_db)) %>%
dplyr::select(NAMA_BAHAN, Cluster_HC, Cluster_FCM, Cluster_DBSCAN, everything())
# Menyimpan data_final ke CSV
write.csv(data_final, "hasil_clustering_makanan1.csv", row.names = FALSE)
cat("Hasil clustering makanan disimpan sebagai 'hasil_clustering_makanan1.csv'.\n")
} else {
cat("PERINGATAN: Ada ketidakcocokan panjang antara data_imputed, nama_bahan, atau hasil cluster.\n")
cat("Dim data_imputed:", dim(data_imputed), "\n")
cat("Length nama_bahan:", length(nama_bahan), "\n")
cat("Length clusters_hc:", length(clusters_hc), "\n")
cat("Membuat data_final dengan potensi data yang tidak sinkron atau NA.\n")
# Upaya membuat data_final dengan baris minimum yang cocok
min_rows_final <- min(nrow(data_imputed), length(nama_bahan), length(clusters_hc), length(clusters_fcm), length(clusters_db))
if (min_rows_final > 0) {
data_final <- data_imputed[1:min_rows_final, , drop=FALSE] %>%
mutate(NAMA_BAHAN = nama_bahan[1:min_rows_final],
Cluster_HC = as.factor(clusters_hc[1:min_rows_final]),
Cluster_FCM = as.factor(clusters_fcm[1:min_rows_final]),
Cluster_DBSCAN = as.factor(clusters_db[1:min_rows_final])) %>%
dplyr::select(NAMA_BAHAN, Cluster_HC, Cluster_FCM, Cluster_DBSCAN, everything())
# Menyimpan data_final yang disesuaikan ke CSV
write.csv(data_final, "hasil_clustering_makanan1.csv", row.names = FALSE)
cat("Hasil clustering makanan (disesuaikan) disimpan sebagai 'hasil_clustering_makanan.csv'.\n")
} else {
data_final <- data.frame() # Kosong jika tidak bisa
cat("Tidak ada data_final yang dapat dibuat atau disimpan.\n")
}
}
## Hasil clustering makanan disimpan sebagai 'hasil_clustering_makanan1.csv'.
# Fungsi create_summary_table (Anda yang sudah diperbaiki untuk syntax across)
create_summary_table <- function(df_final, cluster_col_name, df_imputed_source) {
imputed_nutrient_cols_summary <- colnames(df_imputed_source)
if (!cluster_col_name %in% names(df_final)) {
message(paste("Kolom cluster", cluster_col_name, "tidak ada di df_final."))
return(NULL)
}
if (all(is.na(df_final[[cluster_col_name]]))) {
message(paste("Semua nilai di kolom cluster", cluster_col_name, "adalah NA."))
return(NULL)
}
df_final[[cluster_col_name]] <- as.factor(df_final[[cluster_col_name]])
# --- Perbaikan: Gunakan kept_vars_after_missing untuk memastikan kolom numerik yang benar ---
# Pastikan kept_vars_after_missing tersedia di global environment atau diteruskan sebagai argumen
# Untuk kasus ini, kita asumsikan kept_vars_after_missing tersedia dari global scope
numeric_cols_summary <- intersect(kept_vars_after_missing, names(df_final))
# --- Akhir Perbaikan ---
if(length(numeric_cols_summary) == 0){
message("Tidak ada kolom numerik cocok untuk summary.")
return(NULL)
}
summary_t <- df_final %>%
filter(!is.na(.data[[cluster_col_name]])) %>%
group_by(!!sym(cluster_col_name)) %>%
summarise(
across(all_of(numeric_cols_summary), ~mean(., na.rm = TRUE)),
.groups = 'drop'
) %>%
left_join(
df_final %>%
filter(!is.na(.data[[cluster_col_name]])) %>%
count(!!sym(cluster_col_name), name = "Jumlah_Anggota"),
by = cluster_col_name
)
return(summary_t)
}
if(nrow(data_final) > 0) {
cat("\n--- Tabel Karakteristik Cluster HC (Rata-rata Variabel Asli) ---\n")
summary_hc <- create_summary_table(data_final, "Cluster_HC", data_imputed) # data_imputed sudah disesuaikan
if(!is.null(summary_hc)) {
print(as.data.frame(summary_hc))
write.csv(summary_hc, "summary_table_hc.csv", row.names = FALSE)
cat("Tabel ringkasan HC disimpan sebagai 'summary_table_hc.csv'.\n")
}
cat("\n--- Tabel Karakteristik Cluster FCM (Rata-rata Variabel Asli) ---\n")
summary_fcm <- create_summary_table(data_final, "Cluster_FCM", data_imputed)
if(!is.null(summary_fcm)) {
print(as.data.frame(summary_fcm))
write.csv(summary_fcm, "summary_table_fcm.csv", row.names = FALSE)
cat("Tabel ringkasan FCM disimpan sebagai 'summary_table_fcm.csv'.\n")
}
cat("\n--- Tabel Karakteristik Cluster DBSCAN (Rata-rata Variabel Asli, Termasuk Noise=0) ---\n")
summary_dbscan <- create_summary_table(data_final, "Cluster_DBSCAN", data_imputed)
if(!is.null(summary_dbscan)) {
print(as.data.frame(summary_dbscan))
write.csv(summary_dbscan, "summary_table_dbscan.csv", row.names = FALSE)
cat("Tabel ringkasan DBSCAN disimpan sebagai 'summary_table_dbscan.csv'.\n")
}
cat("\nContoh Anggota Cluster 1 HC:\n")
if("Cluster_HC" %in% names(data_final) && any(data_final$Cluster_HC == 1, na.rm = TRUE)){
cluster_1_hc_examples <- data_final %>% filter(Cluster_HC == 1) %>% dplyr::select(NAMA_BAHAN)
print(head(cluster_1_hc_examples))
write.csv(cluster_1_hc_examples, "cluster_1_hc_examples.csv", row.names = FALSE)
cat("Contoh anggota Cluster 1 HC disimpan sebagai 'cluster_1_hc_examples.csv'.\n")
} else {
cat("Tidak dapat menampilkan contoh anggota cluster HC 1.\n")
}
} else {
cat("data_final kosong, tabel summary tidak dapat dibuat.\n")
}
##
## --- Tabel Karakteristik Cluster HC (Rata-rata Variabel Asli) ---
## Cluster_HC NATRIUM SERAT THIAMIN BESI KALSIUM FOSFOR
## 1 1 86.04751 2.0407670 0.1616761 2.128125 98.17756 99.47727
## 2 2 65.86122 4.9436735 0.2220000 3.787347 117.51429 172.96327
## 3 3 404.83571 0.9878571 0.3230714 3.722143 111.04286 229.20000
## 4 4 290.78431 1.2431373 0.3713725 16.056863 1112.50980 818.62745
## 5 5 4972.14286 0.9000000 0.0700000 3.100000 124.71429 231.00000
## LEMAK PROTEIN ABU AIR ENERGI KH Jumlah_Anggota
## 1 2.247869 5.818324 1.359517 77.34020 95.08381 13.193750 704
## 2 9.202041 8.583265 2.275510 14.92122 374.11020 64.285306 245
## 3 28.222857 21.143571 3.866429 37.11643 370.87857 8.483571 140
## 4 11.772549 34.558824 10.566667 28.01961 312.96078 18.376471 51
## 5 2.457143 24.185714 33.742857 46.65714 172.71429 12.885714 7
## Tabel ringkasan HC disimpan sebagai 'summary_table_hc.csv'.
##
## --- Tabel Karakteristik Cluster FCM (Rata-rata Variabel Asli) ---
## Cluster_FCM NATRIUM SERAT THIAMIN BESI KALSIUM FOSFOR
## 1 1 678.17333 0.848000 0.3958667 5.204000 196.46667 270.93333
## 2 2 56.52767 2.524000 0.1220889 1.729778 77.54444 60.74444
## 3 3 177.37037 1.406667 0.2348889 2.789259 127.98519 164.79630
## 4 4 73.69091 4.204091 0.1826818 3.264545 93.27727 137.19545
## 5 5 322.90152 2.665152 0.3381061 8.695455 502.71212 496.43939
## LEMAK PROTEIN ABU AIR ENERGI KH Jumlah_Anggota
## 1 14.313333 22.930667 5.057333 47.68933 258.76000 7.630667 75
## 2 1.144667 2.457333 1.069333 82.46444 69.19333 12.734000 450
## 3 5.256296 12.237407 2.080370 69.01630 142.15926 11.392222 270
## 4 7.505455 5.929545 1.801818 16.95136 361.68182 67.345000 220
## 5 28.384848 27.702273 7.669697 15.19470 448.42424 23.174242 132
## Tabel ringkasan FCM disimpan sebagai 'summary_table_fcm.csv'.
##
## --- Tabel Karakteristik Cluster DBSCAN (Rata-rata Variabel Asli, Termasuk Noise=0) ---
## Cluster_DBSCAN NATRIUM SERAT THIAMIN BESI KALSIUM FOSFOR
## 1 0 315.18824 3.650588 0.3310824 5.940235 278.28471 294.82118
## 2 1 67.96461 1.806648 0.1276454 1.749584 73.12188 86.65374
## LEMAK PROTEIN ABU AIR ENERGI KH Jumlah_Anggota
## 1 14.720000 17.535529 4.744941 36.53224 307.8212 26.41129 425
## 2 2.977562 5.039197 1.127978 68.59958 134.1620 22.20028 722
## Tabel ringkasan DBSCAN disimpan sebagai 'summary_table_dbscan.csv'.
##
## Contoh Anggota Cluster 1 HC:
## NAMA_BAHAN
## 1 <NA>
## 2 Jagung muda, kuning, mentah
## 3 Nasi
## 4 Nasi tim
## 5 Beras, tapai
## 6 Beras merah, Nasi
## Contoh anggota Cluster 1 HC disimpan sebagai 'cluster_1_hc_examples.csv'.
# Menyimpan log hasil penting ke file teks
sink("analysis_log.txt")
cat("--- Ringkasan Analisis Profil Gizi Makanan Remaja ---\n\n")
cat("Dimensi data setelah load:", dim(data_numeric), "\n\n")
cat("Persentase Missing Value per Variabel (Hanya Nutrisi):\n")
print(missing_summary)
cat("\nVariabel yang digunakan setelah filtering missing value (>25%):", paste(kept_vars_after_missing, collapse=", "), "\n")
cat("Dimensi data setelah filtering variabel:", dim(data_filtered), "\n\n")
cat("Jumlah NA setelah imputasi:", sum(is.na(data_imputed)), "\n\n")
cat("Dimensi data setelah scaling awal:", dim(data_scaled_original), "\n\n")
cat("--- Hasil Clustering --- \n")
cat("Jumlah cluster optimal (k) yang dipilih untuk HC:", k_optimal_hc, "\n")
cat("Rata-rata Silhouette Score HC:", mean_sil_hc, "\n")
cat("Rata-rata Silhouette Score FCM:", mean_sil_fcm, "\n")
cat("Rata-rata Silhouette Score DBSCAN (non-noise):", mean_sil_dbscan, "\n")
cat("DBSCAN - Jumlah Noise Points:", num_noise_dbscan, "\n")
cat("DBSCAN - Jumlah Cluster Ditemukan (tidak termasuk noise):", num_clusters_dbscan, "\n\n")
# Bagian MDS tetap karena bisa dilakukan tanpa PCA
cat("--- Hasil Perceptual Mapping (MDS) ---\n")
if(exists("mds_stress") && !is.na(mds_stress)) {
cat("MDS Stress:", mds_stress, "\n\n")
} else {
cat("MDS Stress: N/A (MDS mungkin tidak dijalankan atau gagal)\n\n")
}
sink()
cat("Log analisis lengkap disimpan sebagai 'analysis_log.txt'.\n")
## Log analisis lengkap disimpan sebagai 'analysis_log.txt'.
# ========================================
# AKHIR SCRIPT
# ========================================
cat("\nAnalisis Selesai.\n")
##
## Analisis Selesai.