Fityanandra
Fityanandra Athar Adyaksa
52250059
Cahaya Medina
Cahaya Medina Semidang
52250053
Cloise Shafira
Cloise Shafira
52250044


Data Collecting

SECTION A — Data Collection Using Programming

Tujuan

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.


Mendefinisikan Daftar File

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")
)

Membaca File dengan Looping

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)
Ringkasan Setiap File Dataset
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

Pengecekan Kesamaan Struktur Kolom (If/If-Else)

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")
  )
Status Kesamaan Struktur Kolom Antar File
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 |

Menggabungkan Dataset (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)
Ringkasan Dataset Gabungan
Metrik Nilai
Total Baris 10000
Total Kolom 22
Jumlah Sumber File 5

Dataset gabungan berhasil terbentuk dengan 10000 baris dari 5 file sumber.


SECTION B — Data Handling

Tujuan

Memahami kondisi dataset hasil penggabungan — mencakup profil tipe data, missing values, duplikasi, dan masalah kualitas data.


Profil Dataset Gabungan

# ── 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)
Profil Tipe Data Setiap Kolom
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.


Identifikasi Missing Values

# ── 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")
Jumlah dan Persentase Missing Values per Kolom
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

Identifikasi Duplicate Rows

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)
Ringkasan Duplikasi Data
Metrik Nilai
Total Baris 10000
Baris Duplikat 5581
Persentase Duplikat 55.81%

Masalah Kualitas Data

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.


SECTION C — Data Cleaning

Tujuan

Membersihkan dataset menggunakan kombinasi looping dan if/if-else secara terstruktur.


Tahap 1: Konversi 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))

Tahap 2–4: Cleaning Multi-Kolom dengan LOOPING

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]])
  }
}

Tahap 5: Hapus Duplikat

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.


Validasi Hasil Cleaning

# ── 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")
Distribusi Nilai Setelah Cleaning
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

SECTION D — Conditional Logic

Tujuan

Menerapkan logika bisnis menggunakan if / if-else bertingkat (nested IF) untuk membuat kolom-kolom baru yang bermakna.


Membuat Kolom Baru

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")
  )

Ringkasan Kolom Baru

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)
Distribusi Nilai Kolom Baru
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

Visualisasi Kolom Baru

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


Tabel Data Final (Interaktif)

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"
)

SECTION E — Analytical Thinking

Pertanyaan 1: Platform Mana yang Paling Dominan?

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")
Distribusi Transaksi per Platform
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.


Pertanyaan 2: Kategori Mana yang Paling Sering Muncul?

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")
Distribusi Transaksi per Kategori Produk
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.


Pertanyaan 3: Status Transaksi Apa yang Paling Banyak?

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")
Distribusi Status Transaksi
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.).


Ringkasan Insight Keseluruhan

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.





Web Scraping & Data Programming Process

2.3 Oscar Winning Films: AJAX and Javascript

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.



Section A — Data Collection

Metodologi Scraping

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.

Fungsi Scraping

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)
}

Looping Scraping

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.




Section B — Data Handling

Profil Dataset

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
  )
Profil Kolom Dataset Mentah — Oscar Winning Films
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”

Audit Missing Values & Duplikat

# --- 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
  )
Audit Missing Values & Status Kelengkapan | Duplikat Ditemukan: 0 baris
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.




Section C — Data Cleaning

Logika Pembersihan

Strategi Pembersihan yang Diterapkan:

Proses cleaning dirancang dalam tiga lapisan berurutan:

  1. 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.

  2. 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.

  3. Lapisan Nilai — Missing values diisi menggunakan logika if/else: kolom teks diisi "Unknown", kolom numerik diisi 0, duplikat dihapus dengan dplyr::distinct().

Kode Cleaning

# ==============================================================
# 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

Hasil Cleaning

# --- 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
  )
Ringkasan Perbandingan: Sebelum vs Sesudah Data Cleaning
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



Section D — Conditional Logic

Pembuatan Kolom data_status

Logika 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), "%")

Tabel Final

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



Ringkasan Teknis Laporan:

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




2.4 Turtles All the Way Down: Frames & iFrames

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 penyu
  • description — Deskripsi singkat spesies
  • additional_info — Informasi tambahan (berat, panjang, status konservasi)



Section A — Data Collection

Metodologi Scraping

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:

  1. Inisialisasi browser otomatis → navigasi ke URL utama.
  2. Identifikasi elemen <iframe> di halaman induk.
  3. Jalankan switchToFrame() → konteks berpindah ke dalam iframe.
  4. Ambil elemen HTML yang dibutuhkan menggunakan CSS selector.
  5. Iterasi dengan loop untuk mengambil seluruh entri data.
  6. Jalankan switchToFrame(NULL) → kembali ke konteks parent.
  7. Tutup browser dan hentikan Selenium server.

Kode Scraping (RSelenium)

# ==============================================================
# 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=FALSE karena memerlukan Selenium server aktif di lingkungan lokal. Dataset hasil scraping disimulasikan pada chunk berikutnya untuk keperluan demonstrasi laporan, mencerminkan data aktual dari website tersebut.

Data Hasil Scraping

# ==============================================================
# 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.




Section B — Data Handling

Profil Dataset

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
  )
Tabel B.1 — Profil Lengkap Dataset Mentah: Turtles All the Way Down
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.

Audit Missing Values & Duplikat

# --- 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
  )
Tabel B.2 — Audit Missing Values & Duplikat | Duplikat Ditemukan: 0 baris
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.




Section C — Data Cleaning

Logika Pembersihan

Strategi Pembersihan yang Diterapkan:

Proses cleaning dirancang dalam tiga lapisan berurutan:

  1. Lapisan Baris — Hapus baris yang kolom name-nya NA karena tanpa identitas nama, rekod tidak dapat digunakan untuk analisis apapun.

  2. 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.

  3. 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.

Kode Cleaning

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

Hasil Cleaning

# --- 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)
Tabel C.1 — Perbandingan Kondisi Dataset: Sebelum vs Sesudah Cleaning
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



Section D — Conditional Logic

Pembuatan Kolom data_status

Logika 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
  )
Tabel D.1 — Distribusi Kolom data_status
Status Jumlah Rekod Persentase
Complete 13 92.9%
Incomplete 1 7.1%

Tabel Final

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



Section E — Analytical Thinking

1. Website Paling Mudah di-Scrape

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.


2. Website Paling Sulit

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.


3. Perbedaan Pendekatan Scraping

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
  )
Tabel E.1 — Perbandingan Pendekatan Scraping
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


4. Insights & Rekomendasi

Insights

  1. 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.

  2. 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.

  3. 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.

Rekomendasi

  1. 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.

  2. 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.




Referensi

  1. 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

  2. Wickham, H. (2021). Mastering Shiny. O’Reilly.
    Bab tentang interaktivitas dan DT::datatable().

  3. **RSelenium Team (2022). RSelenium: R Bindings for Selenium WebDriver.
    Dokumentasi resmi: https://cran.r-project.org/package=RSelenium

  4. **Scrape This Site (n.d.). Tutorial Series: Countries, Hockey, Oscar, Turtles.
    Sumber data latihan: https://www.scrapethissite.com

  5. Xie, Y. (2021). R Markdown Cookbook. Chapman & Hall/CRC.
    Panduan pembuatan laporan interaktif.

  6. **Grolemund, G. (2018). Web Scraping Reference Guide (RStudio Webinar).
    Video dan materi: https://rstudio.com/resources/webinars/