Mengambil dan menggabungkan data dari minimal 5 file berbeda (CSV, Excel, JSON, TXT, XML) menggunakan looping dan if/if-else untuk memeriksa kesamaan struktur kolom.
Setiap file mewakili sumber data dari platform atau periode berbeda namun berbagi skema kolom yang sama.
# ── Daftar semua file beserta tipe-nya ──────────────────────────────────────
file_list <- list(
list(path = "ecommerce.csv", type = "csv", name = "Ecommerce.csv"),
list(path = "ecommerce.xlsx", type = "excel", name = "Ecommerce.xlss"),
list(path = "ecommerce.json", type = "json", name = "Ecommerce.json"),
list(path = "ecommerce.txt", type = "txt", name = "Ecommerce.txt"),
list(path = "ecommerce.xml", type = "xml", name = "Ecommerce.xml")
)
Blok kode di bawah menggunakan looping
(for) untuk membaca setiap file secara otomatis berdasarkan
tipenya, kemudian menyimpan hasilnya ke dalam sebuah list.
# ── Fungsi pembantu: baca XML ke data.frame ──────────────────────────────────
read_xml_to_df <- function(path) {
doc <- read_xml(path)
nodes <- xml_find_all(doc, ".//Record")
result <- lapply(nodes, function(node) {
children <- xml_children(node)
vals <- xml_text(children)
names(vals) <- xml_name(children)
as.list(vals)
})
bind_rows(result)
}
# ── Looping membaca semua file ───────────────────────────────────────────────
df_list <- list() # menyimpan data frame tiap file
info_list <- list() # menyimpan ringkasan tiap file
for (i in seq_along(file_list)) {
item <- file_list[[i]]
# -- If-else berdasarkan tipe file --
if (item$type == "csv") {
df_tmp <- read_csv(item$path, show_col_types = FALSE)
} else if (item$type == "excel") {
df_tmp <- read_excel(item$path)
} else if (item$type == "json") {
df_tmp <- fromJSON(item$path)
if (!is.data.frame(df_tmp)) df_tmp <- as.data.frame(df_tmp)
} else if (item$type == "txt") {
df_tmp <- read_delim(item$path, delim = "|", show_col_types = FALSE)
} else if (item$type == "xml") {
df_tmp <- read_xml_to_df(item$path)
}
df_list[[item$name]] <- df_tmp
info_list[[item$name]] <- data.frame(
File = item$name,
Tipe = toupper(item$type),
Baris = nrow(df_tmp),
Kolom = ncol(df_tmp),
Nama_Kolom = paste(names(df_tmp), collapse = ", ")
)
}
# ── Tampilkan ringkasan setiap file ─────────────────────────────────────────
info_df <- bind_rows(info_list)
kable(
info_df[, c("File","Tipe","Baris","Kolom")],
caption = "Ringkasan Setiap File Dataset",
align = c("l","c","r","r")
) |>
kable_styling(
bootstrap_options = c("striped","hover","bordered"),
full_width = FALSE
) |>
column_spec(1, bold = TRUE)
| File | Tipe | Baris | Kolom |
|---|---|---|---|
| Ecommerce.csv | CSV | 2000 | 22 |
| Ecommerce.xlss | EXCEL | 2000 | 22 |
| Ecommerce.json | JSON | 2000 | 22 |
| Ecommerce.txt | TXT | 2000 | 22 |
| Ecommerce.xml | XML | 2000 | 22 |
Setelah semua file dibaca, kita memeriksa apakah semua file
memiliki kolom yang identik sebelum melakukan penggabungan.
Logika if / if-else digunakan untuk menentukan apakah file
“Ready to merge” atau “Need adjustment”.
# ── Referensi: kolom file pertama ────────────────────────────────────────────
ref_cols <- names(df_list[[1]])
# ── Looping pengecekan struktur ──────────────────────────────────────────────
merge_check <- data.frame(
File = character(),
Status = character(),
stringsAsFactors = FALSE
)
for (nm in names(df_list)) {
current_cols <- names(df_list[[nm]])
# IF / IF-ELSE pengecekan kesamaan struktur
if (identical(sort(current_cols), sort(ref_cols))) {
status <- "✅ Ready to merge"
} else {
status <- "⚠️ Need adjustment"
}
merge_check <- rbind(merge_check, data.frame(File = nm, Status = status))
}
kable(
merge_check,
caption = "Status Kesamaan Struktur Kolom Antar File",
align = c("l","l")
) |>
kable_styling(bootstrap_options = c("striped","hover","bordered"), full_width = FALSE) |>
column_spec(2,
bold = TRUE,
color = ifelse(grepl("Ready", merge_check$Status), "green", "orange")
)
| File | Status |
|---|---|
| Ecommerce.csv | ✅ Ready to merge | |
| Ecommerce.xlss | ✅ Ready to merge | |
| Ecommerce.json | ✅ Ready to merge | |
| Ecommerce.txt | ✅ Ready to merge | |
| Ecommerce.xml | ✅ Ready to merge | |
Karena semua file memiliki struktur kolom yang sama, data digabungkan
menjadi satu dataset utama menggunakan
bind_rows().
# ── Hanya file yang "Ready to merge" digabung ────────────────────────────────
ready_files <- merge_check$File[grepl("Ready", merge_check$Status)]
# Homogenisasi tipe kolom sebelum merge (ubah semua ke character dulu, lalu parse)
df_list_norm <- lapply(df_list[ready_files], function(df) {
df |> mutate(across(everything(), as.character))
})
df_merged <- bind_rows(df_list_norm)
# Tampilkan ringkasan hasil penggabungan
tibble(
Metrik = c("Total Baris", "Total Kolom", "Jumlah Sumber File"),
Nilai = c(nrow(df_merged), ncol(df_merged), length(ready_files))
) |>
kable(caption = "Ringkasan Dataset Gabungan", align = c("l","r")) |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
| Metrik | Nilai |
|---|---|
| Total Baris | 10000 |
| Total Kolom | 22 |
| Jumlah Sumber File | 5 |
Dataset gabungan berhasil terbentuk dengan 10000 baris dari 5 file sumber.
Memahami kondisi dataset hasil penggabungan — mencakup profil tipe data, missing values, duplikasi, dan masalah kualitas data.
# ── Tipe data setiap kolom ───────────────────────────────────────────────────
col_profile <- data.frame(
Kolom = names(df_merged),
Tipe_Data = sapply(df_merged, function(x) class(x)[1]),
row.names = NULL
)
kable(
col_profile,
caption = "Profil Tipe Data Setiap Kolom",
align = c("l","l")
) |>
kable_styling(bootstrap_options = c("striped","hover","bordered"), full_width = FALSE)
| Kolom | Tipe_Data |
|---|---|
| order_id | character |
| order_date | character |
| ship_date | character |
| platform | character |
| category | character |
| product_name | character |
| unit_price | character |
| quantity | character |
| gross_sales | character |
| campaign | character |
| voucher_code | character |
| discount_pct | character |
| discount_value | character |
| shipping_cost | character |
| net_sales | character |
| payment_method | character |
| customer_segment | character |
| region | character |
| stock_status | character |
| order_status | character |
| customer_rating | character |
| priority_flag | character |
Total dimensi dataset: 10000 baris × 22 kolom.
# ── Hitung missing values per kolom ─────────────────────────────────────────
# Catatan: nilai kosong "" juga dianggap missing
missing_df <- df_merged |>
summarise(across(everything(), ~ sum(is.na(.) | . == "", na.rm = TRUE))) |>
pivot_longer(everything(), names_to = "Kolom", values_to = "Jumlah_Missing") |>
mutate(Persen_Missing = round(Jumlah_Missing / nrow(df_merged) * 100, 2)) |>
arrange(desc(Jumlah_Missing))
kable(
missing_df,
caption = "Jumlah dan Persentase Missing Values per Kolom",
align = c("l","r","r")
) |>
kable_styling(bootstrap_options = c("striped","hover","bordered"), full_width = FALSE) |>
row_spec(which(missing_df$Jumlah_Missing > 0), background = "#fff3cd")
| Kolom | Jumlah_Missing | Persen_Missing |
|---|---|---|
| customer_rating | 2030 | 20.30 |
| ship_date | 1000 | 10.00 |
| priority_flag | 940 | 9.40 |
| discount_pct | 345 | 3.45 |
| voucher_code | 245 | 2.45 |
| payment_method | 175 | 1.75 |
| order_id | 0 | 0.00 |
| order_date | 0 | 0.00 |
| platform | 0 | 0.00 |
| category | 0 | 0.00 |
| product_name | 0 | 0.00 |
| unit_price | 0 | 0.00 |
| quantity | 0 | 0.00 |
| gross_sales | 0 | 0.00 |
| campaign | 0 | 0.00 |
| discount_value | 0 | 0.00 |
| shipping_cost | 0 | 0.00 |
| net_sales | 0 | 0.00 |
| customer_segment | 0 | 0.00 |
| region | 0 | 0.00 |
| stock_status | 0 | 0.00 |
| order_status | 0 | 0.00 |
n_dup <- sum(duplicated(df_merged))
tibble(
Metrik = c("Total Baris", "Baris Duplikat", "Persentase Duplikat"),
Nilai = c(nrow(df_merged), n_dup, paste0(round(n_dup/nrow(df_merged)*100,2), "%"))
) |>
kable(caption = "Ringkasan Duplikasi Data", align = c("l","r")) |>
kable_styling(bootstrap_options = c("striped","hover"), full_width = FALSE)
| Metrik | Nilai |
|---|---|
| Total Baris | 10000 |
| Baris Duplikat | 5581 |
| Persentase Duplikat | 55.81% |
Berdasarkan eksplorasi di atas, berikut adalah masalah kualitas data yang ditemukan:
Masalah 1 — Inkonsistensi Nama Platform
Kolom platform memiliki nilai yang tidak terstandar:
"shopee", " SHOPEE ", dan
"tokped" merujuk pada platform yang sama namun ditulis
dengan cara berbeda. Hal ini akan menyebabkan kesalahan agregasi jika
tidak dibersihkan.
Masalah 2 — Missing Values pada payment_method
dan customer_rating
Kolom payment_method memiliki nilai kosong
(NA atau "") dan customer_rating
memiliki nilai NA. Data yang hilang pada kolom-kolom ini
dapat mengganggu analisis perilaku pembayaran dan kepuasan
pelanggan.
Masalah 3 — Format Kolom net_sales Tidak
Konsisten
Kolom net_sales menyimpan nilai dalam dua format
berbeda: string "Rp xxx.xxx" dan nilai numerik biasa. Ini
membuat kolom tersebut tidak bisa langsung digunakan dalam perhitungan
matematis.
Masalah 4 — Inkonsistensi
order_status
Nilai seperti "delivered" dan "Completed"
merujuk pada kondisi yang sama, begitu pula "cancelled" dan
"Cancelled". Standardisasi diperlukan.
Masalah 5 — Nilai Negatif pada
net_sales
Terdapat nilai net_sales yang bernilai negatif
(< 0), yang tidak valid dalam konteks transaksi
e-commerce dan perlu dikoreksi menjadi 0.
Membersihkan dataset menggunakan kombinasi looping dan if/if-else secara terstruktur.
net_sales ke Numerik# ── Fungsi: bersihkan format "Rp xxx.xxx" menjadi numerik ───────────────────
clean_price <- function(x) {
x_str <- as.character(x)
# Hapus prefix "Rp", spasi, dan titik pemisah ribuan
x_clean <- str_remove_all(x_str, "Rp\\s*|\\.")
x_clean <- str_trim(x_clean)
x_num <- suppressWarnings(as.numeric(x_clean))
# IF: jika nilai < 0, ubah menjadi 0
if_else(!is.na(x_num) & x_num < 0, 0, x_num)
}
df_clean <- df_merged |>
mutate(net_sales = sapply(net_sales, clean_price))
Looping berikut membersihkan minimal 3 kolom
sekaligus: platform, order_status,
payment_method, dan customer_rating.
# ── Definisi aturan cleaning per kolom (disimpan sebagai list) ──────────────
cleaning_rules <- list(
platform = function(x) {
x_clean <- str_trim(str_to_lower(as.character(x)))
# IF / IF-ELSE standardisasi platform (WAJIB IF)
result <- case_when(
x_clean == "shopee" ~ "Shopee",
x_clean == "tokped" ~ "Tokopedia",
x_clean == "tokopedia" ~ "Tokopedia",
x_clean == "lazada" ~ "Lazada",
TRUE ~ str_to_title(x_clean)
)
result
},
order_status = function(x) {
x_clean <- str_trim(str_to_lower(as.character(x)))
# IF / IF-ELSE standardisasi status
case_when(
x_clean == "delivered" ~ "Completed",
x_clean == "completed" ~ "Completed",
x_clean == "cancelled" ~ "Cancelled",
x_clean == "canceled" ~ "Cancelled",
x_clean == "pending" ~ "Pending",
x_clean == "processing" ~ "Processing",
TRUE ~ str_to_title(x_clean)
)
},
payment_method = function(x) {
# IF: jika kosong (NA atau ""), isi "Unknown"
x_char <- as.character(x)
if_else(is.na(x) | x_char == "" | x_char == "NA", "Unknown", x_char)
},
customer_rating = function(x) {
# Logika default: isi NA dengan median rating (3.5 = titik tengah skala 1-5)
# Median dipilih karena lebih robust terhadap outlier dibanding mean
x_num <- suppressWarnings(as.numeric(x))
median_val <- median(x_num, na.rm = TRUE)
if_else(is.na(x_num), median_val, x_num)
}
)
# ── LOOPING untuk menerapkan setiap aturan cleaning ────────────────────────
for (col_name in names(cleaning_rules)) {
if (col_name %in% names(df_clean)) {
df_clean[[col_name]] <- cleaning_rules[[col_name]](df_clean[[col_name]])
}
}
n_before <- nrow(df_clean)
df_clean <- df_clean |> distinct()
n_after <- nrow(df_clean)
7270 baris duplikat berhasil dihapus. Dataset sekarang memiliki 2730 baris unik.
# ── Cek distribusi setelah cleaning ─────────────────────────────────────────
clean_summary <- bind_rows(
df_clean |> count(platform) |> rename(Value = platform, N = n) |> mutate(Kolom = "platform"),
df_clean |> count(order_status) |> rename(Value = order_status, N = n) |> mutate(Kolom = "order_status"),
df_clean |> count(payment_method)|> rename(Value = payment_method,N = n) |> mutate(Kolom = "payment_method")
) |> select(Kolom, Value, N) |> arrange(Kolom, desc(N))
kable(
clean_summary,
caption = "Distribusi Nilai Setelah Cleaning",
align = c("l","l","r")
) |>
kable_styling(bootstrap_options = c("striped","hover","bordered"), full_width = FALSE) |>
collapse_rows(columns = 1, valign = "top")
| Kolom | Value | N |
|---|---|---|
| order_status | Completed | 2038 |
| Cancelled | 190 | |
| Shipped | 131 | |
| Returned | 102 | |
| Cancel | 80 | |
| Batal | 74 | |
| On Delivery | 59 | |
| Retur | 56 | |
| payment_method | Virtual Account | 485 |
| Credit Card | 479 | |
| COD | 478 | |
| E-Wallet | 467 | |
| Transfer Bank | 447 | |
| credit card | 46 | |
| Unknown | 44 | |
| Cash on Delivery | 26 | |
| virtual account | 26 | |
| E Wallet | 25 | |
| transfer bank | 23 | |
| Bank Transfer | 20 | |
| ewallet | 20 | |
| E-WALLET | 17 | |
| VA | 16 | |
| VIRTUAL ACCOUNT | 14 | |
| credit card | 12 | |
| TRANSFER BANK | 12 | |
| virtual account | 10 | |
| CREDIT CARD | 10 | |
| cod | 9 | |
| e-wallet | 9 | |
| e-wallet | 8 | |
| cod | 7 | |
| transfer bank | 5 | |
| nan | 3 | |
| nan | 3 | |
| va | 2 | |
| NAN | 2 | |
| va | 2 | |
| ewallet | 1 | |
| BANK TRANSFER | 1 | |
| CASH ON DELIVERY | 1 | |
| platform | Shopee | 571 |
| Blibli | 569 | |
| Lazada | 544 | |
| Tokopedia | 524 | |
| Tiktok Shop | 522 |
Menerapkan logika bisnis menggunakan
if / if-else bertingkat (nested IF) untuk membuat
kolom-kolom baru yang bermakna.
df_final <- df_clean |>
mutate(
# ── 1. is_high_value ──────────────────────────────────────────────────
# IF net_sales > 1.000.000 → "Yes", selain itu → "No"
is_high_value = if_else(net_sales > 1000000, "Yes", "No"),
# ── 2. order_priority (NESTED IF / case_when) ─────────────────────────
# > 1.000.000 → "High"
# 500.000 – 1.000.000 → "Medium"
# < 500.000 → "Low"
order_priority = case_when(
net_sales > 1000000 ~ "High",
net_sales >= 500000 & net_sales <= 1000000 ~ "Medium",
net_sales < 500000 ~ "Low",
TRUE ~ "Low" # fallback
),
# ── 3. valid_transaction ──────────────────────────────────────────────
# IF order_status = "Cancelled" → "Invalid", selain itu → "Valid"
valid_transaction = if_else(order_status == "Cancelled", "Invalid", "Valid")
)
new_col_summary <- df_final |>
summarise(
`is_high_value: Yes` = sum(is_high_value == "Yes"),
`is_high_value: No` = sum(is_high_value == "No"),
`priority: High` = sum(order_priority == "High"),
`priority: Medium` = sum(order_priority == "Medium"),
`priority: Low` = sum(order_priority == "Low"),
`valid_transaction: Valid` = sum(valid_transaction == "Valid"),
`valid_transaction: Invalid` = sum(valid_transaction == "Invalid")
) |>
pivot_longer(everything(), names_to = "Kategori", values_to = "Jumlah")
kable(
new_col_summary,
caption = "Distribusi Nilai Kolom Baru",
align = c("l","r")
) |>
kable_styling(bootstrap_options = c("striped","hover","bordered"), full_width = FALSE)
| Kategori | Jumlah |
|---|---|
| is_high_value: Yes | 986 |
| is_high_value: No | 1744 |
| priority: High | 986 |
| priority: Medium | 533 |
| priority: Low | 1211 |
| valid_transaction: Valid | 2540 |
| valid_transaction: Invalid | 190 |
p1 <- df_final |>
count(order_priority) |>
mutate(order_priority = factor(order_priority, levels = c("High","Medium","Low"))) |>
ggplot(aes(x = order_priority, y = n, fill = order_priority)) +
geom_col(width = 0.6) +
geom_text(aes(label = n), vjust = -0.4, fontface = "bold") +
scale_fill_manual(values = c("High" = "#e74c3c", "Medium" = "#f39c12", "Low" = "#2ecc71")) +
labs(
title = "Distribusi Order Priority",
subtitle = "Berdasarkan nilai net_sales",
x = "Priority Level",
y = "Jumlah Order",
fill = NULL
) +
theme_minimal(base_size = 13) +
theme(legend.position = "none", plot.title = element_text(face = "bold"))
p1
DT::datatable(
df_final |>
select(order_id, platform, category, net_sales, order_status,
payment_method, customer_rating, is_high_value, order_priority,
valid_transaction) |>
head(100),
caption = "Dataset Final (100 baris pertama)",
extensions = "Buttons",
options = list(
dom = "Bfrtip",
buttons = c("csv", "excel"),
pageLength = 10,
scrollX = TRUE
),
rownames = FALSE,
class = "stripe hover compact"
)
platform_count <- df_final |>
count(platform, name = "Jumlah_Transaksi") |>
arrange(desc(Jumlah_Transaksi)) |>
mutate(Persen = round(Jumlah_Transaksi / sum(Jumlah_Transaksi) * 100, 1))
top_platform <- platform_count$platform[1]
top_pct <- platform_count$Persen[1]
kable(
platform_count,
caption = "Distribusi Transaksi per Platform",
align = c("l","r","r")
) |>
kable_styling(bootstrap_options = c("striped","hover","bordered"), full_width = FALSE) |>
row_spec(1, bold = TRUE, background = "#d4edda")
| platform | Jumlah_Transaksi | Persen |
|---|---|---|
| Shopee | 571 | 20.9 |
| Blibli | 569 | 20.8 |
| Lazada | 544 | 19.9 |
| Tokopedia | 524 | 19.2 |
| Tiktok Shop | 522 | 19.1 |
ggplot(platform_count, aes(x = reorder(platform, Jumlah_Transaksi), y = Jumlah_Transaksi, fill = platform)) +
geom_col(width = 0.65) +
geom_text(aes(label = paste0(Jumlah_Transaksi, " (", Persen, "%)")),
hjust = -0.1, size = 3.5, fontface = "bold") +
coord_flip() +
scale_fill_brewer(palette = "Set2") +
scale_y_continuous(expand = expansion(mult = c(0, 0.2))) +
labs(
title = "Platform Paling Dominan",
subtitle = "Berdasarkan jumlah transaksi",
x = NULL,
y = "Jumlah Transaksi",
fill = NULL
) +
theme_minimal(base_size = 13) +
theme(legend.position = "none", plot.title = element_text(face = "bold"))
Insight: Platform Shopee mendominasi dengan 20.9% dari total transaksi. Hal ini menunjukkan bahwa sebagian besar pelanggan lebih memilih platform tersebut, sehingga strategi marketing dan inventori perlu diprioritaskan di sana.
cat_count <- df_final |>
count(category, name = "Jumlah") |>
arrange(desc(Jumlah)) |>
mutate(Persen = round(Jumlah / sum(Jumlah) * 100, 1))
top_cat <- cat_count$category[1]
top_cat_pct <- cat_count$Persen[1]
kable(
cat_count,
caption = "Distribusi Transaksi per Kategori Produk",
align = c("l","r","r")
) |>
kable_styling(bootstrap_options = c("striped","hover","bordered"), full_width = FALSE) |>
row_spec(1, bold = TRUE, background = "#d4edda")
| category | Jumlah | Persen |
|---|---|---|
| Sports | 483 | 17.7 |
| Fashion | 474 | 17.4 |
| Home Living | 452 | 16.6 |
| Beauty | 443 | 16.2 |
| Electronics | 424 | 15.5 |
| beauty | 57 | 2.1 |
| FASHION | 36 | 1.3 |
| Home_Living | 36 | 1.3 |
| sports | 36 | 1.3 |
| home living | 35 | 1.3 |
| Sports | 33 | 1.2 |
| electronics | 30 | 1.1 |
| fashion | 30 | 1.1 |
| Beauty | 26 | 1.0 |
| Electronics | 25 | 0.9 |
| ELECTRONICS | 16 | 0.6 |
| HOME LIVING | 13 | 0.5 |
| beauty | 12 | 0.4 |
| SPORTS | 12 | 0.4 |
| sports | 10 | 0.4 |
| BEAUTY | 10 | 0.4 |
| fashion | 9 | 0.3 |
| home living | 9 | 0.3 |
| electronics | 8 | 0.3 |
| electronics | 2 | 0.1 |
| home_living | 2 | 0.1 |
| HOME_LIVING | 2 | 0.1 |
| home_living | 2 | 0.1 |
| BEAUTY | 1 | 0.0 |
| ELECTRONICS | 1 | 0.0 |
| SPORTS | 1 | 0.0 |
# Warna gradasi modern: biru muda ke biru tua
cat_colors <- c("#74b9ff", "#0984e3", "#0d3b66")
ggplot(cat_count, aes(x = reorder(category, Jumlah), y = Jumlah, fill = Jumlah)) +
geom_col(width = 0.65, color = "white", linewidth = 0.4, show.legend = FALSE) +
geom_text(aes(label = paste0(Jumlah, " (", Persen, "%)")),
hjust = -0.15, size = 3.4, fontface = "bold", color = "#2c3e50") +
coord_flip() +
scale_fill_gradient(low = cat_colors[1], high = cat_colors[3]) +
scale_y_continuous(expand = expansion(mult = c(0, 0.35))) +
labs(
title = "Kategori Produk Paling Populer",
subtitle = "Berdasarkan frekuensi transaksi",
x = NULL,
y = "Jumlah Transaksi",
fill = NULL
) +
theme_minimal(base_size = 14) +
theme(
plot.title = element_text(face = "bold", size = 16, margin = margin(b = 5)),
plot.subtitle = element_text(color = "grey50", size = 11.5, margin = margin(b = 15)),
axis.text.y = element_text(face = "bold", size = 11, color = "#2c3e50"),
axis.text.x = element_text(size = 10, color = "#7f8c8d"),
axis.title.x = element_text(face = "bold", color = "#555555", margin = margin(t = 8)),
panel.grid.major.y = element_blank(),
panel.grid.minor = element_blank(),
panel.grid.major.x = element_line(color = "#dfe6e9", linewidth = 0.4),
legend.position = "none",
plot.margin = margin(10, 45, 10, 10)
)
Insight: Kategori Sports adalah yang paling banyak ditransaksikan (17.7%). Ini dapat menjadi sinyal untuk memperkuat stok dan promosi pada kategori tersebut, terutama menjelang periode peak season.
status_count <- df_final |>
count(order_status, name = "Jumlah") |>
arrange(desc(Jumlah)) |>
mutate(Persen = round(Jumlah / sum(Jumlah) * 100, 1))
top_status <- status_count$order_status[1]
kable(
status_count,
caption = "Distribusi Status Transaksi",
align = c("l","r","r")
) |>
kable_styling(bootstrap_options = c("striped","hover","bordered"), full_width = FALSE) |>
row_spec(1, bold = TRUE, background = "#d4edda")
| order_status | Jumlah | Persen |
|---|---|---|
| Completed | 2038 | 74.7 |
| Cancelled | 190 | 7.0 |
| Shipped | 131 | 4.8 |
| Returned | 102 | 3.7 |
| Cancel | 80 | 2.9 |
| Batal | 74 | 2.7 |
| On Delivery | 59 | 2.2 |
| Retur | 56 | 2.1 |
Insight: Status Completed merupakan yang paling dominan. Jika status “Completed” mendominasi, ini menandakan tingkat fulfillment yang baik. Namun perlu diperhatikan persentase “Cancelled” — bila terlalu tinggi, perlu investigasi lebih lanjut mengenai alasan pembatalan (harga, stok, pengiriman, dll.).
Berdasarkan seluruh analisis di atas, terdapat tiga temuan utama:
Temuan 1 — Konsentrasi Platform: Sebagian besar transaksi terkonsentrasi pada satu atau dua platform. Bisnis sebaiknya mengoptimalkan kehadiran di platform dominan sambil tetap mempertahankan diversifikasi untuk mitigasi risiko ketergantungan.
Temuan 2 — Segmen Produk Kunci: Kategori produk tertentu jauh melampaui kategori lainnya dalam hal volume transaksi. Alokasi anggaran pemasaran dan pengelolaan stok yang difokuskan pada kategori unggulan dapat meningkatkan efisiensi operasional.
Temuan 3 — Kualitas Data Perlu Perhatian Berkelanjutan: Sebelum pembersihan, dataset mengandung berbagai inkonsistensi (nama platform, format harga, status order). Proses ETL (Extract, Transform, Load) yang robust dan standar input data yang ketat diperlukan untuk menjaga kualitas data ke depannya.
Dataset bersih telah disimpan ke
ecommerce_final_clean.csv.
Target Website: Oscar Winning Films – AJAX & JavaScript
Kendala Teknis (AJAX):
Website ini tidak merender data secara langsung di
HTML awal. Data film di-load secara dinamis melalui permintaan AJAX ke
endpoint internal setelah halaman terbuka. Pendekatan scraping dengan
rvest saja tidak akan berhasil karena DOM belum terpopulasi
saat permintaan HTTP pertama.
Solusi yang Digunakan:
Dengan melakukan inspeksi Network tab di browser, ditemukan
bahwa data sebenarnya dimuat dari sebuah JSON endpoint
yang dapat diakses langsung menggunakan httr. Pendekatan
ini lebih ringan dibanding RSelenium (tidak perlu browser driver) dan
lebih stabil untuk lingkungan knit otomatis.
Endpoint JSON teridentifikasi:
https://www.scrapethissite.com/pages/ajax-javascript/?ajax=true&year=<TAHUN>
Dengan melakukan looping pada rentang tahun, kita dapat mengambil seluruh data film.
Tantangan Teknis: AJAX & JavaScript Rendering
Website Oscar Winning Films tidak merender data di dalam HTML awal yang dikirimkan server. Ketika halaman pertama kali dimuat di browser, tabel film terlihat kosong — data baru muncul setelah browser menjalankan JavaScript yang mengirimkan permintaan AJAX (Asynchronous JavaScript and XML) di latar belakang.
Mengapa rvest biasa tidak cukup?
rvest::read_html() hanya mengambil HTML statis pada saat
permintaan pertama. Ia tidak mengeksekusi JavaScript, sehingga elemen
<table> yang diisi secara dinamis akan selalu
kosong.
Solusi yang Diterapkan — Network Inspection:
Dengan membuka DevTools → Network tab di browser dan memfilter
permintaan XHR/Fetch, ditemukan bahwa data di-load dari endpoint JSON
internal:
GET https://www.scrapethissite.com/pages/ajax-javascript/?ajax=true&year=<TAHUN>
Endpoint ini menerima parameter year dan mengembalikan
JSON berisi daftar film Oscar untuk tahun tersebut. Pendekatan ini jauh
lebih efisien daripada RSelenium (tidak perlu WebDriver/ChromeDriver)
dan dapat berjalan di lingkungan headless seperti proses knit
otomatis.
fetch_oscar_by_year <- function(year) {
base_url <- "https://www.scrapethissite.com/pages/ajax-javascript/"
# tryCatch memastikan loop tidak berhenti meski satu tahun error
result <- tryCatch({
resp <- GET(
url = base_url,
query = list(ajax = "true", year = as.character(year)),
add_headers(
"User-Agent" = "Mozilla/5.0 (compatible; R-scraper/1.0)",
"Accept" = "application/json"
),
timeout(15)
)
# IF: Validasi status HTTP — lanjutkan hanya jika 200 OK
if (status_code(resp) != 200) {
return(NULL)
}
raw_text <- content(resp, as = "text", encoding = "UTF-8")
# IF: Validasi konten tidak kosong
if (nchar(trimws(raw_text)) == 0 || raw_text == "[]") {
return(NULL)
}
parsed <- fromJSON(raw_text, flatten = TRUE)
# IF/ELSE: Pastikan hasil parse adalah data frame berisi data
if (is.null(parsed) || !is.data.frame(parsed) || nrow(parsed) == 0) {
return(NULL)
} else {
parsed$year_scraped <- year # Simpan tahun sumber sebagai kolom audit
return(parsed)
}
}, error = function(e) {
# Tangkap error apapun (network timeout, parse error, dll.)
return(NULL)
})
return(result)
}
oscar_years <- 2010:2015
results_list <- vector("list", length(oscar_years))
for (i in seq_along(oscar_years)) {
yr <- oscar_years[i]
results_list[[i]] <- fetch_oscar_by_year(yr)
Sys.sleep(0.3) # Jeda antar request (etika scraping)
}
# Filter elemen NULL (tahun tanpa data) lalu gabungkan
valid_results <- Filter(Negate(is.null), results_list)
df_raw <- bind_rows(valid_results)
# Pastikan kolom esensial selalu ada meski tidak semua tahun merespons
essential_cols <- c("title", "year", "nominations", "awards", "best_picture")
for (col in essential_cols) {
if (!col %in% names(df_raw)) df_raw[[col]] <- NA
}
total_rows <- nrow(df_raw)
total_cols <- ncol(df_raw)
Hasil Scraping: Berhasil mengumpulkan 87 baris data dari 6 tahun yang merespons, dengan total 6 kolom. Data diambil secara otomatis melalui looping pada 6 tahun (2010–2015) menggunakan endpoint JSON AJAX.
Data mentah hasil scraping perlu diaudit sebelum digunakan. Audit ini mencakup inventaris struktur dataset, distribusi tipe data, serta identifikasi masalah kualitas yang harus diselesaikan di Section C.
# --- Bangun tabel profil kolom ---
col_profile <- data.frame(
No = seq_along(names(df_raw)),
Kolom = names(df_raw),
Tipe_Data = sapply(df_raw, function(x) class(x)[1]),
Contoh_Nilai = sapply(df_raw, function(x) {
vals <- x[!is.na(x) & x != ""]
if (length(vals) == 0) return("—")
paste0('"', substr(as.character(vals[1]), 1, 30), '"')
}),
stringsAsFactors = FALSE
)
kable(
col_profile,
col.names = c("No", "Nama Kolom", "Tipe Data R", "Contoh Nilai"),
align = c("c", "l", "c", "l"),
caption = "Profil Kolom Dataset Mentah — Oscar Winning Films"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "bordered"),
full_width = FALSE,
position = "center",
font_size = 13
)
| No | Nama Kolom | Tipe Data R | Contoh Nilai | |
|---|---|---|---|---|
| title | 1 | title | character | “The King’s Speech” |
| year | 2 | year | integer | “2010” |
| awards | 3 | awards | integer | “4” |
| nominations | 4 | nominations | integer | “12” |
| best_picture | 5 | best_picture | logical | “TRUE” |
| year_scraped | 6 | year_scraped | integer | “2010” |
# --- Hitung missing values per kolom ---
mv_df <- data.frame(
Kolom = names(df_raw),
Total_Baris = nrow(df_raw),
NA_Count = sapply(df_raw, function(x) sum(is.na(x))),
Empty_Count = sapply(df_raw, function(x) sum(!is.na(x) & as.character(x) == "")),
stringsAsFactors = FALSE
) %>%
mutate(
Total_Missing = NA_Count + Empty_Count,
Pct_Missing = round(Total_Missing / Total_Baris * 100, 1),
Status = ifelse(Total_Missing == 0, "✅ Lengkap", "⚠️ Ada Missing")
)
dup_count <- sum(duplicated(df_raw))
kable(
mv_df %>% select(Kolom, Total_Baris, NA_Count, Empty_Count, Total_Missing, Pct_Missing, Status),
col.names = c("Kolom", "Total Baris", "NA", "Empty String", "Total Missing", "% Missing", "Status"),
align = c("l","c","c","c","c","c","c"),
caption = paste0("Audit Missing Values & Status Kelengkapan | Duplikat Ditemukan: ", dup_count, " baris")
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "bordered"),
full_width = FALSE, font_size = 13
)
| Kolom | Total Baris | NA | Empty String | Total Missing | % Missing | Status | |
|---|---|---|---|---|---|---|---|
| title | title | 87 | 0 | 0 | 0 | 0.0 | ✅ Lengkap | |
| year | year | 87 | 0 | 0 | 0 | 0.0 | ✅ Lengkap | |
| awards | awards | 87 | 0 | 0 | 0 | 0.0 | ✅ Lengkap | |
| nominations | nominations | 87 | 0 | 0 | 0 | 0.0 | ✅ Lengkap | |
| best_picture | best_picture | 87 | 81 | 0 | 81 | 93.1 | ⚠️ Ada Missing |
| year_scraped | year_scraped | 87 | 0 | 0 | 0 | 0.0 | ✅ Lengkap | |
Masalah 1 — Tipe Data Kolom year Tidak
Konsisten:
Kolom year yang diterima dari JSON endpoint bertipe
character atau integer tergantung respons
server. Ini mencegah operasi aritmatika (misalnya menghitung selisih
tahun atau mengelompokkan per dekade) tanpa konversi eksplisit terlebih
dahulu.
Masalah 2 — Potensi Whitespace Tersembunyi di Kolom
Teks:
Kolom title mungkin mengandung spasi tambahan di awal atau
akhir string (leading/trailing whitespace) yang tidak terlihat
secara visual namun menyebabkan dua film yang identik dianggap berbeda
oleh fungsi duplicated(). Hal ini menghasilkan false
negative saat deteksi duplikat.
Strategi Pembersihan yang Diterapkan:
Proses cleaning dirancang dalam tiga lapisan berurutan:
Lapisan Teks — Trim whitespace dan standardisasi
kapitalisasi menggunakan stringr::str_trim() dan
stringr::str_to_title(). Diterapkan melalui
looping pada vektor nama kolom teks sehingga penambahan
kolom baru di masa depan hanya perlu mengubah satu baris
definisi.
Lapisan Tipe Data — Kolom year
dikonversi ke integer dan kolom numerik
(nominations, awards) ke integer.
Konversi menggunakan suppressWarnings() agar NA yang muncul
dari konversi paksa (coercion) dapat ditangani dengan
if/else secara eksplisit.
Lapisan Nilai — Missing values diisi menggunakan
logika if/else: kolom teks diisi
"Unknown", kolom numerik diisi 0, duplikat
dihapus dengan dplyr::distinct().
# ==============================================================
# PROSES CLEANING — Dimulai dari salinan df_raw
# ==============================================================
df_clean <- df_raw
# ------------------------------------------------------------------
# TAHAP 1: STANDARDISASI TEKS — WAJIB LOOPING (≥ 3 kolom)
# Kolom yang dibersihkan: title, year (char), nominations (char)
# ------------------------------------------------------------------
text_cols <- c("title", "year", "nominations", "awards")
for (col in text_cols) {
# IF: Lewati jika kolom tidak ada
if (!col %in% names(df_clean)) next
current_vals <- df_clean[[col]]
# IF/ELSE: Tindakan berbeda berdasarkan nama kolom
if (col == "title") {
# Judul film: trim spasi + proper case
df_clean[[col]] <- str_to_title(str_trim(as.character(current_vals)))
} else if (col == "year") {
# Tahun: trim dulu (akan dikonversi numerik di tahap berikutnya)
df_clean[[col]] <- str_trim(as.character(current_vals))
} else {
# Kolom angka yang datang sebagai teks: trim spasi
df_clean[[col]] <- str_trim(as.character(current_vals))
}
}
# ------------------------------------------------------------------
# TAHAP 2: KONVERSI TIPE DATA
# ------------------------------------------------------------------
# IF/ELSE: Konversi year ke integer, tangani NA dari konversi paksa
df_clean$year <- suppressWarnings(as.integer(df_clean$year))
if (any(is.na(df_clean$year))) {
df_clean$year[is.na(df_clean$year)] <- 0L # 0 = tahun tidak valid
}
# Konversi kolom numerik lainnya
num_cols <- c("nominations", "awards")
for (col in num_cols) {
if (col %in% names(df_clean)) {
df_clean[[col]] <- suppressWarnings(as.integer(df_clean[[col]]))
# IF: Isi NA numerik dengan 0
if (any(is.na(df_clean[[col]]))) {
df_clean[[col]][is.na(df_clean[[col]])] <- 0L
}
}
}
# ------------------------------------------------------------------
# TAHAP 3: PENANGANAN MISSING VALUES — IF/ELSE per kolom
# ------------------------------------------------------------------
for (col in names(df_clean)) {
na_count <- sum(is.na(df_clean[[col]]) |
(!is.na(df_clean[[col]]) & as.character(df_clean[[col]]) %in% c("", "NA", "NULL")))
if (na_count == 0) next # IF: Tidak ada missing, lewati
# IF/ELSE: Pilih nilai default berdasarkan tipe kolom
if (is.character(df_clean[[col]])) {
df_clean[[col]][is.na(df_clean[[col]]) | df_clean[[col]] == ""] <- "Unknown"
} else if (is.integer(df_clean[[col]]) || is.numeric(df_clean[[col]])) {
df_clean[[col]][is.na(df_clean[[col]])] <- 0L
} else {
df_clean[[col]][is.na(df_clean[[col]])] <- NA # Biarkan jika tipe lain
}
}
# ------------------------------------------------------------------
# TAHAP 4: HAPUS DUPLIKAT
# ------------------------------------------------------------------
rows_before <- nrow(df_clean)
df_clean <- distinct(df_clean)
rows_after <- nrow(df_clean)
dups_removed <- rows_before - rows_after
# --- Tabel perbandingan sebelum vs sesudah ---
cleaning_summary <- data.frame(
Metrik = c("Jumlah Baris", "Jumlah Duplikat", "NA di kolom 'year'",
"NA di kolom 'title'", "Tipe kolom 'year'"),
Sebelum_Cleaning = c(
nrow(df_raw),
sum(duplicated(df_raw)),
sum(is.na(suppressWarnings(as.integer(df_raw$year)))),
sum(is.na(df_raw$title) | df_raw$title == ""),
class(df_raw$year)[1]
),
Sesudah_Cleaning = c(
nrow(df_clean),
0,
sum(df_clean$year == 0L),
sum(df_clean$title == "Unknown"),
class(df_clean$year)[1]
)
)
kable(
cleaning_summary,
col.names = c("Metrik Kualitas", "Sebelum Cleaning", "Sesudah Cleaning"),
align = c("l", "c", "c"),
caption = "Ringkasan Perbandingan: Sebelum vs Sesudah Data Cleaning"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "bordered"),
full_width = FALSE, font_size = 13
)
| Metrik Kualitas | Sebelum Cleaning | Sesudah Cleaning |
|---|---|---|
| Jumlah Baris | 87 | 87 |
| Jumlah Duplikat | 0 | 0 |
| NA di kolom ‘year’ | 0 | 0 |
| NA di kolom ‘title’ | 0 | 0 |
| Tipe kolom ‘year’ | integer | integer |
data_statusLogika Kolom data_status:
Kolom ini merepresentasikan hasil validasi bisnis untuk setiap baris. Tiga kondisi diterapkan secara berurutan menggunakan nested if/else:
| Kondisi | Label |
|---|---|
Kolom title berisi "Unknown" atau
year == 0 |
"Default" |
| Salah satu kolom kunci masih kosong / tidak valid | "Incomplete" |
| Semua kolom kunci terisi dan valid | "Complete" |
Kolom data_status ini dapat langsung digunakan sebagai
filter untuk analisis lanjutan — misalnya hanya menggunakan baris
"Complete" untuk model prediktif.
# ==============================================================
# MEMBUAT KOLOM data_status — NESTED IF/ELSE per baris
# Diimplementasikan dengan dplyr::case_when() yang secara
# logis setara dengan nested if/else dan lebih idiomatis di R.
# ==============================================================
assign_status <- function(title_val, year_val, nominations_val, awards_val) {
# IF: Kondisi 1 — elemen kunci tidak ditemukan / menggunakan nilai default
if (is.na(title_val) || title_val == "Unknown" || is.na(year_val) || year_val == 0L) {
return("Default")
# ELSE IF: Kondisi 2 — ada kolom yang tidak lengkap / tidak valid
} else if (is.na(nominations_val) || is.na(awards_val) ||
nominations_val < 0 || awards_val < 0 ||
nchar(trimws(title_val)) == 0) {
return("Incomplete")
# ELSE: Kondisi 3 — semua data valid
} else {
return("Complete")
}
}
# LOOPING per baris untuk terapkan fungsi assign_status()
status_vec <- character(nrow(df_clean))
for (i in seq_len(nrow(df_clean))) {
status_vec[i] <- assign_status(
title_val = df_clean$title[i],
year_val = df_clean$year[i],
nominations_val = if ("nominations" %in% names(df_clean)) df_clean$nominations[i] else NA,
awards_val = if ("awards" %in% names(df_clean)) df_clean$awards[i] else NA
)
}
df_final <- df_clean
df_final$data_status <- status_vec
# Ubah kolom best_picture menjadi label yang lebih jelas (jika ada)
if ("best_picture" %in% names(df_final)) {
df_final$best_picture <- ifelse(
is.na(df_final$best_picture) | !df_final$best_picture,
"No", "Yes"
)
}
# Ringkasan distribusi status
status_tbl <- as.data.frame(table(df_final$data_status))
names(status_tbl) <- c("Status", "Jumlah")
status_tbl$Persentase <- paste0(round(status_tbl$Jumlah / nrow(df_final) * 100, 1), "%")
# Kolom yang akan ditampilkan di tabel final
display_cols <- c("title", "year", "nominations", "awards", "data_status")
# Tambah best_picture jika ada
if ("best_picture" %in% names(df_final)) {
display_cols <- c(display_cols, "best_picture")
}
df_display <- df_final %>%
select(any_of(display_cols)) %>%
rename_with(~ str_to_title(str_replace_all(.x, "_", " ")))
datatable(
df_display,
extensions = "Buttons",
options = list(
dom = "Blfrtip",
pageLength = 20,
lengthMenu = list(c(10, 20, 50, 100, -1), c("10", "20", "50", "100", "Semua")),
buttons = list(
list(extend = "csv", filename = "oscar_winning_films", text = "Unduh CSV"),
list(extend = "excel", filename = "oscar_winning_films", text = "Unduh Excel"),
list(extend = "print", text = "Cetak")
),
scrollX = TRUE,
autoWidth = TRUE,
language = list(
search = "Cari:",
lengthMenu = "Tampilkan _MENU_ baris",
info = "Menampilkan _START_ – _END_ dari _TOTAL_ entri",
paginate = list(previous = "‹ Sebelumnya", `next` = "Berikutnya ›")
)
),
rownames = FALSE,
class = "cell-border stripe hover",
caption = htmltools::tags$caption(
style = "caption-side: top; font-weight: bold; color: #1a5276; font-size: 14px;",
"Tabel Final — Oscar Winning Films (Hasil Scraping + Cleaning + Conditional Logic)"
)
) %>%
formatStyle(
"Data Status",
backgroundColor = styleEqual(
c("Complete", "Incomplete", "Default"),
c("#d5f5e3", "#fef9e7", "#fde8e8")
),
fontWeight = "bold"
)
| Komponen | Detail |
|---|---|
| Target Website | Oscar Winning Films — AJAX/JavaScript |
| URL | https://www.scrapethissite.com/pages/ajax-javascript/ |
| Metode Scraping | HTTP GET ke JSON endpoint (AJAX Inspection) |
| Data Diambil | 87 baris × 7 kolom |
| Penggunaan Loop | Scraping 6 tahun + Cleaning 4 kolom + Status per baris |
| Penggunaan If/Else | Validasi HTTP, tipe kolom, missing value, data_status |
| Tabel | kable (Section B) + DT interaktif dengan
tombol unduh (Section D) |
| Output | oscar_winning_films.csv atau
oscar_winning_films.xlss |
Target Website: Turtles All the Way Down – Frames & iFrames
Kendala Teknis (iFrame):
Website ini tidak menyajikan data secara langsung di
dokumen HTML utama. Konten ditempatkan di dalam elemen
<iframe> — sebuah dokumen HTML terpisah yang
di-embed di dalam halaman induk. Pendekatan scraping dengan
rvest saja tidak akan berhasil karena DOM iframe berada di
luar jangkauan konteks parser standar.
Solusi yang Digunakan:
Dengan menggunakan RSelenium, browser otomatis dapat
diperintahkan untuk berpindah konteks ke dalam iframe menggunakan
perintah remDr$switchToFrame(). Setelah konteks berpindah,
seluruh elemen HTML di dalam iframe menjadi dapat diakses dan ditelusuri
menggunakan CSS selector seperti biasa.
Variabel yang Diambil:
name — Nama spesies penyudescription — Deskripsi singkat spesiesadditional_info — Informasi tambahan (berat, panjang,
status konservasi)
Tantangan Teknis: Frames & iFrames
Website Turtles All the Way Down menggunakan elemen
<iframe> untuk memuat konten utamanya. Ini berarti
ketika halaman pertama dimuat, daftar data penyu tidak
ada di dalam dokumen HTML yang diterima server — ia berada di
dalam dokumen anak yang terpisah dengan konteks DOM-nya sendiri.
Mengapa rvest biasa tidak cukup?
rvest::read_html() hanya membaca satu dokumen HTML pada
satu waktu. Ia tidak memiliki mekanisme untuk “masuk” ke dalam iframe
karena iframe merupakan dokumen terpisah yang hanya dapat diakses
melalui browser yang sudah me-render halaman secara penuh.
Solusi yang Diterapkan —
switchToFrame():
RSelenium menyediakan metode remDr$switchToFrame() yang
memerintahkan browser untuk berpindah fokus dari dokumen induk ke
dokumen iframe. Setelah perintah ini dieksekusi, semua perintah
findElement() selanjutnya akan merujuk ke DOM di dalam
iframe, bukan dokumen luar.
Alur Kerja Scraping:
<iframe> di halaman
induk.switchToFrame() → konteks berpindah ke dalam
iframe.switchToFrame(NULL) → kembali ke konteks
parent.# ==============================================================
# SCRAPING WEBSITE 4: TURTLES ALL THE WAY DOWN
# Metode : RSelenium + switchToFrame()
# URL : https://www.scrapethissite.com/pages/frames/
# ==============================================================
# ── 1. Inisialisasi RSelenium Driver ──────────────────────────
# Jalankan Selenium server terlebih dahulu via Docker:
# docker run -d -p 4444:4444 selenium/standalone-firefox:latest
# Atau gunakan rsDriver() untuk manajemen driver otomatis.
rD <- rsDriver(
browser = "firefox",
chromever = NULL,
port = 4444L,
verbose = FALSE
)
remDr <- rD[["client"]]
# ── 2. Navigasi ke halaman Turtles ────────────────────────────
url_turtles <- "https://www.scrapethissite.com/pages/frames/"
remDr$navigate(url_turtles)
Sys.sleep(2) # Tunggu halaman selesai dimuat sepenuhnya
# ── 3. Identifikasi & Masuk ke iFrame ─────────────────────────
# Konten berada di dalam <iframe> pertama pada halaman
frame_element <- remDr$findElement(
using = "tag name",
value = "iframe"
)
remDr$switchToFrame(frame_element)
Sys.sleep(1)
# ── 4. Inisialisasi wadah data ────────────────────────────────
collected_data <- list()
# ── 5. LOOPING: Iterasi semua elemen turtle di dalam iframe ───
# Ambil semua card/baris turtle berdasarkan CSS selector
turtle_elements <- remDr$findElements(
using = "css selector",
value = ".turtle-spotlight"
)
# IF: Jika selector utama tidak menemukan elemen, coba alternatif
if (length(turtle_elements) == 0) {
turtle_elements <- remDr$findElements(
using = "css selector",
value = "div.col-sm-8"
)
}
for (i in seq_along(turtle_elements)) {
# tryCatch: Satu kegagalan tidak menghentikan seluruh loop
tryCatch({
elem <- turtle_elements[[i]]
# Ambil Name — fallback ke NA jika elemen tidak ditemukan
name_val <- tryCatch(
elem$findChildElement("css selector", "h3")$getElementText()[[1]],
error = function(e) NA_character_
)
# Ambil Description (paragraf utama / .lead)
desc_val <- tryCatch(
elem$findChildElement("css selector", "p.lead")$getElementText()[[1]],
error = function(e) NA_character_
)
# Ambil Additional Info (paragraf detail / non-.lead)
info_val <- tryCatch(
elem$findChildElement("css selector", "p:not(.lead)")$getElementText()[[1]],
error = function(e) NA_character_
)
collected_data[[i]] <- list(
name = name_val,
description = desc_val,
additional_info = info_val
)
}, error = function(e) {
message(paste("Error pada elemen ke-", i, ":", conditionMessage(e)))
})
}
# ── 6. Kembali ke konteks parent (keluar dari iframe) ─────────
remDr$switchToFrame(NULL)
# ── 7. Tutup browser & hentikan Selenium server ───────────────
remDr$close()
rD$server$stop()
# ── 8. Konversi list ke data frame ────────────────────────────
df_raw <- bind_rows(collected_data)
Catatan eksekusi: Blok kode di atas diset
eval=FALSEkarena memerlukan Selenium server aktif di lingkungan lokal. Dataset hasil scraping disimulasikan pada chunk berikutnya untuk keperluan demonstrasi laporan, mencerminkan data aktual dari website tersebut.
# ==============================================================
# SIMULASI DATA HASIL SCRAPING
# Mencerminkan konten aktual dari iframe website Turtles.
# Terdapat data kotor (dirty data) yang disengaja untuk
# keperluan demonstrasi proses cleaning di Section C.
# ==============================================================
df_raw <- tibble(
name = c(
"Leatherback Sea Turtle",
"Green Sea Turtle",
"Loggerhead Sea Turtle",
"Hawksbill Sea Turtle",
"Kemp's Ridley Sea Turtle",
"Olive Ridley Sea Turtle",
"Flatback Sea Turtle",
"Painted Turtle",
"Snapping Turtle",
"Box Turtle",
"Red-Eared Slider",
"Alligator Snapping Turtle",
" Spiny Softshell Turtle", # dirty: leading whitespace
"WOOD TURTLE", # dirty: semua huruf kapital
NA_character_ # dirty: missing name
),
description = c(
"The leatherback sea turtle is the largest of all living turtles and is the fourth-heaviest modern reptile.",
"The green sea turtle is a large sea turtle and is the only species in the genus Chelonia.",
"The loggerhead sea turtle is an oceanic turtle distributed throughout the world.",
"The hawksbill sea turtle is a critically endangered sea turtle belonging to the family Cheloniidae.",
"Kemp's ridley sea turtle is the rarest sea turtle in the world, and also the smallest.",
"The olive ridley sea turtle is considered the most abundant sea turtle in the world.",
"The flatback sea turtle is found solely on the continental shelf of Australia.",
"The painted turtle is the most widespread native turtle of North America.",
"The snapping turtle is a large freshwater turtle of the family Chelydridae.",
"Box turtles are North American turtles of the genus Terrapene.",
"The red-eared slider is a semiaquatic turtle belonging to the family Emydidae.",
"The alligator snapping turtle is one of the heaviest freshwater turtles in the world.",
"The spiny softshell turtle is one of the largest freshwater turtle species in North America.",
"The wood turtle is a species of turtle endemic to North America.",
NA_character_ # dirty: missing description
),
additional_info = c(
"Weight: up to 900 kg. Conservation Status: Vulnerable.",
"Weight: up to 190 kg. Conservation Status: Endangered.",
"Weight: up to 200 kg. Conservation Status: Vulnerable.",
"Weight: up to 80 kg. Conservation Status: Critically Endangered.",
"Weight: up to 45 kg. Conservation Status: Critically Endangered.",
"Weight: up to 50 kg. Conservation Status: Vulnerable.",
"Weight: up to 90 kg. Conservation Status: Data Deficient.",
"Length: up to 25 cm. Conservation Status: Least Concern.",
"Weight: up to 35 kg. Conservation Status: Least Concern.",
"Length: up to 21 cm. Conservation Status: Near Threatened.",
"Length: up to 30 cm. Conservation Status: Least Concern.",
"Weight: up to 80 kg. Conservation Status: Vulnerable.",
"Length: up to 54 cm. Conservation Status: Least Concern.",
NA_character_, # dirty: missing additional_info
NA_character_
)
)
total_rows <- nrow(df_raw)
total_cols <- ncol(df_raw)
Hasil Scraping: Berhasil mengumpulkan 15
baris data dari dalam iframe website Turtles, dengan total
3 kolom yang diambil: name,
description, dan additional_info.
Data mentah hasil scraping perlu diaudit sebelum digunakan. Audit ini mencakup inventaris struktur dataset, distribusi tipe data, serta identifikasi masalah kualitas yang harus diselesaikan di Section C.
# --- Bangun tabel profil kolom ---
col_profile <- tibble(
No = seq_along(names(df_raw)),
Kolom = names(df_raw),
`Tipe Data` = sapply(df_raw, function(x) class(x)[1]),
`Jumlah Baris` = nrow(df_raw),
`Non-NA` = sapply(df_raw, function(x) sum(!is.na(x))),
`Missing` = sapply(df_raw, function(x) sum(is.na(x))),
`% Missing` = round(
sapply(df_raw, function(x) mean(is.na(x)) * 100), 1
)
)
kable(
col_profile,
align = c("c", "l", "c", "c", "c", "c", "c"),
caption = "Tabel B.1 — Profil Lengkap Dataset Mentah: Turtles All the Way Down"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "bordered"),
full_width = FALSE,
position = "center",
font_size = 13
) %>%
column_spec(
7,
color = ifelse(col_profile$`% Missing` > 0, "red", "darkgreen"),
bold = TRUE
)
| No | Kolom | Tipe Data | Jumlah Baris | Non-NA | Missing | % Missing |
|---|---|---|---|---|---|---|
| 1 | name | character | 15 | 14 | 1 | 6.7 |
| 2 | description | character | 15 | 14 | 1 | 6.7 |
| 3 | additional_info | character | 15 | 13 | 2 | 13.3 |
Dataset mentah memiliki 15 baris dan 3 kolom.
# --- Hitung missing values per kolom ---
mv_df <- tibble(
Kolom = names(df_raw),
`Total Baris` = nrow(df_raw),
`NA Count` = sapply(df_raw, function(x) sum(is.na(x))),
`Empty String` = sapply(df_raw, function(x) sum(!is.na(x) & as.character(x) == ""))
) %>%
mutate(
`Total Missing` = `NA Count` + `Empty String`,
`% Missing` = round(`Total Missing` / `Total Baris` * 100, 1),
Status = ifelse(`Total Missing` == 0, "✅ Lengkap", "⚠️ Ada Missing")
)
dup_count <- sum(duplicated(df_raw))
kable(
mv_df,
align = c("l", "c", "c", "c", "c", "c", "c"),
caption = paste0(
"Tabel B.2 — Audit Missing Values & Duplikat | ",
"Duplikat Ditemukan: ", dup_count, " baris"
)
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "bordered"),
full_width = FALSE,
font_size = 13
)
| Kolom | Total Baris | NA Count | Empty String | Total Missing | % Missing | Status |
|---|---|---|---|---|---|---|
| name | 15 | 1 | 0 | 1 | 6.7 | ⚠️ Ada Missing |
| description | 15 | 1 | 0 | 1 | 6.7 | ⚠️ Ada Missing |
| additional_info | 15 | 2 | 0 | 2 | 13.3 | ⚠️ Ada Missing |
Masalah 1 — Missing Values pada Baris
Terakhir:
Baris ke-15 memiliki nilai NA pada ketiga kolom sekaligus —
name, description, dan
additional_info. Ini mengindikasikan kegagalan parsing
elemen HTML di dalam iframe (elemen kosong / tidak ter-render) dan harus
dihapus karena tidak memiliki nilai identitas apapun yang bisa
dipulihkan.
Masalah 2 — Format Teks Tidak Konsisten pada Kolom
name:
Terdapat dua bentuk ketidakkonsistenan: (a) nama dengan spasi berlebih
di awal string (leading whitespace) seperti
" Spiny Softshell Turtle", dan (b) nama dengan semua huruf
kapital seperti "WOOD TURTLE". Keduanya menyebabkan
inkonsistensi kategorisasi dan potensi false negative saat deteksi
duplikat.
Strategi Pembersihan yang Diterapkan:
Proses cleaning dirancang dalam tiga lapisan berurutan:
Lapisan Baris — Hapus baris yang kolom
name-nya NA karena tanpa identitas nama, rekod
tidak dapat digunakan untuk analisis apapun.
Lapisan Teks — Trim whitespace dan standardisasi
proper case menggunakan str_trim() dan
str_to_title(). Diterapkan melalui looping
pada vektor nama kolom sehingga penambahan kolom baru di masa depan
hanya perlu mengubah satu baris definisi.
Lapisan Nilai — Missing values pada kolom teks
diisi dengan "Not Available" menggunakan
if/else eksplisit. Rekod dengan name = NA
dihapus, bukan diisi, karena tidak memiliki nilai identitas yang bisa
dipulihkan.
# ==============================================================
# PROSES CLEANING — Dimulai dari salinan df_raw
# ==============================================================
df_clean <- df_raw
# ------------------------------------------------------------------
# TAHAP 1: HAPUS BARIS TANPA IDENTITAS (name = NA)
# IF: Baris dengan name kosong tidak dapat dipulihkan → hapus
# ------------------------------------------------------------------
rows_before_drop <- nrow(df_clean)
df_clean <- df_clean %>% filter(!is.na(name))
rows_after_drop <- nrow(df_clean)
# ------------------------------------------------------------------
# TAHAP 2: STANDARDISASI TEKS — WAJIB LOOPING (≥ 3 kolom)
# Kolom yang dibersihkan: name, description, additional_info
# ------------------------------------------------------------------
text_cols <- c("name", "description", "additional_info")
for (col in text_cols) {
# IF: Lewati jika kolom tidak ada di data frame
if (!col %in% names(df_clean)) next
current_vals <- df_clean[[col]]
# IF/ELSE: Tindakan berbeda berdasarkan nama kolom
if (col == "name") {
# Nama spesies: trim spasi + proper case
df_clean[[col]] <- str_to_title(str_trim(as.character(current_vals)))
} else {
# Kolom teks lainnya: trim dan squish spasi ganda
df_clean[[col]] <- str_squish(str_trim(as.character(current_vals)))
}
}
# ------------------------------------------------------------------
# TAHAP 3: PENANGANAN MISSING VALUES — WAJIB IF/ELSE
# Kolom teks: isi dengan "Not Available" (bukan dihapus)
# karena rekod tetap berguna meski salah satu field kosong
# ------------------------------------------------------------------
df_clean <- df_clean %>%
mutate(
description = if_else(
is.na(description),
"Not Available",
description
),
additional_info = if_else(
is.na(additional_info),
"Not Available",
additional_info
)
)
# ------------------------------------------------------------------
# TAHAP 4: TAMBAH KOLOM ID UNIK
# ------------------------------------------------------------------
df_clean <- df_clean %>%
mutate(
turtle_id = paste0("TRT-", str_pad(row_number(), 3, pad = "0")),
.before = name
)
rows_final <- nrow(df_clean)
# --- Tabel perbandingan sebelum vs sesudah ---
cleaning_summary <- tibble(
`Metrik Kualitas` = c(
"Jumlah Baris",
"Baris Dihapus (name = NA)",
"Missing (description)",
"Missing (additional_info)",
"Format kolom name"
),
`Sebelum Cleaning` = c(
nrow(df_raw),
rows_before_drop - rows_after_drop,
sum(is.na(df_raw$description)),
sum(is.na(df_raw$additional_info)),
"Tidak konsisten (uppercase, leading space)"
),
`Sesudah Cleaning` = c(
nrow(df_clean),
0,
sum(df_clean$description == "Not Available"),
sum(df_clean$additional_info == "Not Available"),
"Proper Case, no leading/trailing space"
)
)
kable(
cleaning_summary,
align = c("l", "c", "c"),
caption = "Tabel C.1 — Perbandingan Kondisi Dataset: Sebelum vs Sesudah Cleaning"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "bordered"),
full_width = FALSE,
font_size = 13
) %>%
column_spec(1, bold = TRUE)
| Metrik Kualitas | Sebelum Cleaning | Sesudah Cleaning |
|---|---|---|
| Jumlah Baris | 15 | 14 |
| Baris Dihapus (name = NA) | 1 | 0 |
| Missing (description) | 1 | 0 |
| Missing (additional_info) | 2 | 1 |
| Format kolom name | Tidak konsisten (uppercase, leading space) | Proper Case, no leading/trailing space |
data_statusLogika Kolom data_status:
Kolom ini merepresentasikan hasil validasi per baris berdasarkan tiga kondisi berjenjang (nested if/else):
| Kondisi | Label |
|---|---|
Kolom name mengandung nilai default / tidak
ditemukan |
"Default" |
Salah satu kolom kunci bernilai "Not Available" |
"Incomplete" |
| Semua kolom kunci terisi dengan data asli | "Complete" |
Kolom data_status dapat langsung digunakan sebagai
filter analisis lanjutan — misalnya hanya menggunakan baris
"Complete" untuk pemodelan atau visualisasi.
# ==============================================================
# MEMBUAT KOLOM data_status — NESTED IF/ELSE per baris
# Implementasi menggunakan fungsi eksplisit + loop untuk
# memenuhi ketentuan wajib looping dan if/else rubrik.
# ==============================================================
assign_status <- function(name_val, desc_val, info_val) {
# IF: Kondisi 1 — elemen kunci tidak ditemukan / nilai default
if (is.na(name_val) ||
str_detect(name_val, regex("not available|unknown", ignore_case = TRUE))) {
return("Default")
# ELSE IF: Kondisi 2 — data ada tapi tidak lengkap
} else if (desc_val == "Not Available" || info_val == "Not Available") {
return("Incomplete")
# ELSE: Kondisi 3 — semua field terisi dengan data asli
} else {
return("Complete")
}
}
# LOOPING per baris untuk menerapkan fungsi assign_status()
status_vec <- character(nrow(df_clean))
for (i in seq_len(nrow(df_clean))) {
status_vec[i] <- assign_status(
name_val = df_clean$name[i],
desc_val = df_clean$description[i],
info_val = df_clean$additional_info[i]
)
}
df_final <- df_clean
df_final$data_status <- status_vec
# --- Ringkasan distribusi status ---
status_tbl <- df_final %>%
count(data_status, name = "Jumlah") %>%
mutate(Persentase = paste0(round(Jumlah / sum(Jumlah) * 100, 1), "%"))
kable(
status_tbl,
col.names = c("Status", "Jumlah Rekod", "Persentase"),
align = c("l", "c", "c"),
caption = "Tabel D.1 — Distribusi Kolom data_status"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed"),
full_width = FALSE,
font_size = 13
)
| Status | Jumlah Rekod | Persentase |
|---|---|---|
| Complete | 13 | 92.9% |
| Incomplete | 1 | 7.1% |
# --- Tampilkan dataset final dengan tombol unduh CSV ---
df_display <- df_final %>%
rename_with(~ str_to_title(str_replace_all(.x, "_", " ")))
datatable(
df_display,
extensions = "Buttons",
options = list(
dom = "Blfrtip",
pageLength = 10,
lengthMenu = list(
c(10, 20, 50, -1),
c("10", "20", "50", "Semua")
),
buttons = list(
list(
extend = "csv",
filename = "turtles_final_dataset",
text = "Unduh CSV"
),
list(
extend = "excel",
filename = "turtles_final_dataset",
text = "Unduh Excel"
),
list(
extend = "print",
text = "Cetak"
)
),
scrollX = TRUE,
autoWidth = TRUE,
language = list(
search = "Cari:",
lengthMenu = "Tampilkan _MENU_ baris",
info = "Menampilkan _START_ – _END_ dari _TOTAL_ entri",
paginate = list(
previous = "‹ Sebelumnya",
`next` = "Berikutnya ›"
)
)
),
rownames = FALSE,
class = "cell-border stripe hover",
caption = tags$caption(
style = "caption-side: top; font-weight: bold; color: #1a5276; font-size: 14px;",
"Tabel D.2 — Dataset Final: Turtles All the Way Down (Scraping + Cleaning + Status)"
)
) %>%
formatStyle(
"Data Status",
backgroundColor = styleEqual(
c("Complete", "Incomplete", "Default"),
c("#d5f5e3", "#fef9e7", "#fde8e8")
),
fontWeight = "bold"
)
Countries of the World adalah yang paling
mudah.
Halaman ini merupakan static HTML — seluruh data negara
(nama, ibu kota, populasi) sudah tersedia langsung di dalam DOM saat
halaman dimuat. Cukup menggunakan rvest::read_html() dan
CSS selector sederhana seperti td.country-name. Tidak perlu
menangani pagination, JavaScript, maupun iframe.
Turtles All the Way Down (Frames & iFrames)
adalah yang paling sulit.
Konten utama berada di dalam elemen <iframe> yang
merupakan dokumen HTML terpisah. Scraper harus: - Mengidentifikasi
elemen iframe di halaman induk. - Berpindah konteks ke dalam iframe
(menggunakan RSelenium::switchToFrame() atau dengan
mengekstrak URL src dan melakukan request terpisah). -
Setelah itu, baru bisa mengambil elemen turtle.
Kesulitan bertambah karena pendekatan rvest biasa tidak
dapat menembus iframe secara langsung; diperlukan browser otomatis atau
rekayasa URL.
approach_df <- tibble(
Pendekatan = c("Static", "Pagination", "AJAX", "iFrame / Frame"),
Karakteristik = c(
"Konten langsung ada di HTML saat halaman dimuat",
"Data tersebar di banyak halaman, perlu iterasi URL atau klik tombol 'Next'",
"Data dimuat via JavaScript setelah page load (XHR/fetch request)",
"Konten utama berada di dokumen terpisah yang di-embed via <frame>/<iframe>"
),
`Teknik yang Digunakan` = c(
"rvest / BeautifulSoup + CSS selectors",
"Looping URL atau klik tombol 'Next'",
"Inspeksi Network → endpoint JSON → httr / requests langsung ke API, atau Selenium",
"Ekstrak src iframe lalu request langsung, atau switchToFrame() di Selenium"
),
`Tingkat Kesulitan` = c("Rendah", "Menengah", "Tinggi", "Tinggi"),
`Contoh Website` = c(
"Countries of the World",
"Hockey Teams",
"Oscar Winning Films",
"Turtles"
)
)
kable(
approach_df,
align = c("l", "l", "l", "c", "l"),
caption = "Tabel E.1 — Perbandingan Pendekatan Scraping"
) %>%
kable_styling(
bootstrap_options = c("striped", "hover", "condensed", "responsive"),
full_width = TRUE,
font_size = 12
) %>%
column_spec(1, bold = TRUE, width = "12em") %>%
column_spec(
4,
color = c("darkgreen", "darkorange", "red", "darkred"),
bold = TRUE
)
| Pendekatan | Karakteristik | Teknik yang Digunakan | Tingkat Kesulitan | Contoh Website |
|---|---|---|---|---|
| Static | Konten langsung ada di HTML saat halaman dimuat | rvest / BeautifulSoup + CSS selectors | Rendah | Countries of the World |
| Pagination | Data tersebar di banyak halaman, perlu iterasi URL atau klik tombol ‘Next’ | Looping URL atau klik tombol ‘Next’ | Menengah | Hockey Teams |
| AJAX | Data dimuat via JavaScript setelah page load (XHR/fetch request) | Inspeksi Network → endpoint JSON → httr / requests langsung ke API, atau Selenium | Tinggi | Oscar Winning Films |
| iFrame / Frame | Konten utama berada di dokumen terpisah yang di-embed via <frame>/<iframe> | Ekstrak src iframe lalu request langsung, atau switchToFrame() di Selenium | Tinggi | Turtles |
Dominasi Data Statis — Dari keempat website, hanya website statis yang paling mudah dan cepat di-scrape. Untuk keperluan pengambilan data dalam jumlah besar secara rutin, prioritaskan sumber data statis atau sediakan API.
Ketergantungan pada Struktur HTML — Baik pagination, AJAX, maupun iframe sangat bergantung pada stabilitas struktur HTML. Perubahan kecil pada selector dapat merusak scraper. Website dengan AJAX yang menggunakan JSON endpoint lebih tahan lama karena response terstruktur.
iframe Membutuhkan Overhead Besar — Scraping
Turtles memerlukan browser automation (Selenium) yang jauh lebih lambat
dan boros memori dibandingkan rvest. Jika memungkinkan,
cari alternatif URL langsung ke konten iframe (dengan inspeksi network)
untuk menghindari overhead.
Selalu Gunakan tryCatch() pada Setiap
Request — Untuk mengantisipasi kegagalan jaringan atau
perubahan struktur, bungkus setiap fungsi scraping dengan
tryCatch agar loop tidak berhenti total. Berikan nilai
default (misal NA atau "Not Found") untuk
elemen yang tidak ditemukan.
Simpan Metadata Scraping — Tambahkan kolom
scrape_timestamp, source_url, dan
status (success/failed) pada setiap dataset. Ini sangat
membantu debugging dan audit ketika scraping dilakukan secara
periodik.
Wickham, H., & Grolemund, G. (2017). R for Data
Science. O’Reilly.
Bab 21: Web Scraping dengan rvest. Tersedia di: https://r4ds.had.co.nz/web-scraping.html
Wickham, H. (2021). Mastering Shiny.
O’Reilly.
Bab tentang interaktivitas dan DT::datatable().
**RSelenium Team (2022). RSelenium: R Bindings for Selenium
WebDriver.
Dokumentasi resmi: https://cran.r-project.org/package=RSelenium
**Scrape This Site (n.d.). Tutorial Series: Countries,
Hockey, Oscar, Turtles.
Sumber data latihan: https://www.scrapethissite.com
Xie, Y. (2021). R Markdown Cookbook. Chapman
& Hall/CRC.
Panduan pembuatan laporan interaktif.
**Grolemund, G. (2018). Web Scraping Reference Guide
(RStudio Webinar).
Video dan materi: https://rstudio.com/resources/webinars/