Midterm Exam

Adinda Adelia Futri (52250055)

Kayla Aprilia (52250057)

Angelica Florentina M (52250063)

Syafif Azmi Lontoh (52250060)

2026-04-25

Midterm Exam

Mini Case Study: E-Commerce & Web Scraping

Eksplorasi lengkap proses membaca berbagai format file, pembersihan data, rekayasa fitur, hingga web scraping multi-sumber menggunakan R.

Adinda Adelia Futri
52250055

Adinda

Kayla Aprilia
52250057

Kayla

Angelica Florentina M
52250063

Angelica

Syafif Azmi Lontoh
52250060

Syafif

Adinda Adelia Futri52250055
Kayla Aprilia52250057
Angelica Florentina52250063
Syafif Azmi Lontoh52250060

Reading Various File Formats

Membaca dataset e-commerce dari 5 format berbeda: CSV, Excel, JSON, TXT, dan XML.

# ── Load library yang dibutuhkan ────────────────────────────────
library(readr)      # CSV, TXT
library(readxl)     # Excel (.xlsx)
library(jsonlite)   # JSON
library(xml2)       # XML
library(dplyr)

# ── Fungsi helper: menampilkan info dasar DataFrame ────────────
tampilkan_info <- function(nama_file, df) {
  cat(sprintf("\n%s\n", strrep("=", 55)))
  cat(sprintf("  FILE: %s\n", nama_file))
  cat(sprintf("%s\n", strrep("=", 55)))
  cat(sprintf("  Jumlah Baris : %d\n", nrow(df)))
  cat(sprintf("  Jumlah Kolom : %d\n", ncol(df)))
  cat("  Nama Kolom   :\n")
  for (i in seq_along(names(df))) {
    cat(sprintf("    %2d. %s\n", i, names(df)[i]))}
}

# ── 1. Membaca CSV ─────────────────────────────────────────────
df_csv <- read_csv("ecommerce.csv", show_col_types = FALSE)
tampilkan_info("ecommerce.csv", df_csv)
head(df_csv, 3)

# ── 2. Membaca Excel (.xlsx) ───────────────────────────────────
df_xlsx <- read_excel("ecommerce.xlsx")
tampilkan_info("ecommerce.xlsx", df_xlsx)
head(df_xlsx, 3)

# ── 3. Membaca JSON ────────────────────────────────────────────
df_json <- fromJSON("ecommerce.json")
if (!is.data.frame(df_json)) df_json <- as.data.frame(df_json)
tampilkan_info("ecommerce.json", df_json)
head(df_json, 3)

# ── 4. Membaca TXT (auto-detect delimiter) ─────────────────────
first_line <- readLines("ecommerce.txt", n = 1)
delim <- if (grepl("\t", first_line)) "\t" else
         if (grepl(",", first_line)) "," else ";"
df_txt <- read_delim("ecommerce.txt", delim = delim,
                     show_col_types = FALSE)
tampilkan_info("ecommerce.txt", df_txt)
head(df_txt, 3)

# ── 5. Membaca XML ─────────────────────────────────────────────
baca_xml <- function(filepath) {
  doc   <- read_xml(filepath)
  nodes <- xml_find_all(doc, ".//*[not(*)]/..")
  records <- lapply(nodes, function(node) {
    children <- xml_children(node)
    setNames(as.list(xml_text(children)), xml_name(children))
  })
  as.data.frame(do.call(rbind, lapply(records, as.data.frame)),
                stringsAsFactors = FALSE)
}
df_xml <- baca_xml("ecommerce.xml")
tampilkan_info("ecommerce.xml", df_xml)
head(df_xml, 3)
5
Format File
CSV
Format Utama
XML
Format Terkompleks
JSON
Format Web-Native
Format Fungsi R Package Catatan
CSV read_csv() readr Format paling umum, delimiter koma
XLSX read_excel() readxl Mendukung multiple sheet
JSON fromJSON() jsonlite Otomatis parse ke data.frame
TXT read_delim() readr Auto-detect delimiter
XML read_xml() xml2 Perlu parsing manual node

📘 Interpretasi Section A Dataset e-commerce berhasil dibaca dari 5 format berbeda. CSV dan Excel merupakan format paling sederhana karena bersifat tabular langsung. JSON bersifat hierarkis namun fromJSON() dari package jsonlite mampu mengkonversinya secara otomatis ke data frame. TXT menggunakan deteksi delimiter otomatis yang mengidentifikasi apakah file menggunakan tab, koma, atau titik koma. XML merupakan format paling kompleks karena memerlukan parsing node secara manual menggunakan xml2. Kelima file mengandung struktur kolom yang sama, sehingga dapat digabungkan pada tahap berikutnya.

Combining All Files

Menggabungkan seluruh dataset dari berbagai format menggunakan looping dan bind_rows().

# ── Penggabungan menggunakan looping ───────────────────────────
semua_df <- list(
  CSV   = df_csv,
  Excel = df_xlsx,
  JSON  = df_json,
  TXT   = df_txt,
  XML   = df_xml
)

# Tambahkan kolom sumber file dengan looping
for (sumber in names(semua_df)) {
  semua_df[[sumber]]$sumber_file <- sumber
}

# Cek struktur kolom – gunakan CSV sebagai referensi
kolom_referensi <- names(df_csv)

cat("=== Pengecekan Struktur Kolom ===\n")
for (nama in names(semua_df)) {
  df_cek  <- semua_df[[nama]]
  kolom_i <- names(df_cek)[names(df_cek) != "sumber_file"]

  lebih  <- setdiff(kolom_i, kolom_referensi)
  kurang <- setdiff(kolom_referensi, kolom_i)

  status <- if (length(lebih) == 0 && length(kurang) == 0)
              "Ready to merge" else "Need adjustment"

  cat(sprintf("  [%s] %s – %s\n", nama, status,
              if (length(kurang) > 0) paste("Kolom hilang:", kurang) else ""))
}

# Pra-pemrosesan XML: konversi kolom numerik
kolom_numerik <- c("quantity","discount_pct","shipping_cost","customer_rating")
kolom_float   <- c("unit_price","gross_sales","discount_value","net_sales")

for (kol in c(kolom_numerik, kolom_float)) {
  if (kol %in% names(semua_df[["XML"]])) {
    semua_df[["XML"]][[kol]] <- suppressWarnings(
      as.numeric(semua_df[["XML"]][[kol]])
    )
  }
}

# Gabungkan semua DataFrame
df_gabungan <- bind_rows(semua_df)

cat(sprintf("\nTotal baris setelah penggabungan: %d\n", nrow(df_gabungan)))
cat(sprintf("Total kolom: %d\n", ncol(df_gabungan)))
head(df_gabungan, 5)
Sumber Baris Status Kolom
CSV ~2.000 Ready to merge
Excel ~2.000 Ready to merge
JSON ~2.000 Ready to merge
TXT ~2.000 Ready to merge
XML ~2.000 Perlu konversi tipe

⚠️ Catatan Penting – XML File XML membaca semua nilai sebagai character (string). Kolom numerik seperti quantity, unit_price, net_sales perlu dikonversi secara eksplisit ke tipe numerik menggunakan as.numeric() sebelum proses penggabungan agar tidak menyebabkan inkonsistensi tipe data pada dataset gabungan.

📘 Interpretasi Section A2 Proses penggabungan dilakukan menggunakan bind_rows() dari package dplyr. Sebelum penggabungan, kolom sumber_file ditambahkan pada setiap data frame melalui looping agar asal-usul setiap baris data tetap dapat ditelusuri (traceability). Pengecekan struktur kolom dilakukan dengan membandingkan setiap file terhadap referensi kolom CSV. Hasilnya menunjukkan bahwa semua file memiliki kolom yang sesuai, kecuali XML yang memerlukan konversi tipe data terlebih dahulu. Dataset gabungan akhir berisi kurang lebih 10.000 baris yang merepresentasikan transaksi dari kelima sumber file.

Data Quality Analysis

Identifikasi missing values, duplikat, tipe data, dan masalah kualitas data lainnya.

library(readr)
library(dplyr)

df <- read_csv("ecommerce.csv", show_col_types = FALSE)

# ── B.1 Informasi dasar dataset ────────────────────────────────
cat("=== DIMENSI DATASET ===\n")
cat(sprintf("  Jumlah Baris   : %d\n", nrow(df)))
cat(sprintf("  Jumlah Kolom   : %d\n", ncol(df)))

cat("\n=== TIPE DATA SETIAP KOLOM ===\n")
for (col in names(df)) {
  dtype  <- class(df[[col]])[1]
  n_null <- sum(is.na(df[[col]]))
  unique_n <- n_distinct(df[[col]])
  cat(sprintf("  %-22s : %-12s | unique: %5d | NA: %4d\n",
              col, dtype, unique_n, n_null))
}

# ── B.2 Missing values ─────────────────────────────────────────
cat("\n=== MISSING VALUES ANALYSIS ===\n")
missing_counts   <- colSums(is.na(df))
missing_pct      <- round(missing_counts / nrow(df) * 100, 2)
missing_df       <- data.frame(
  Kolom       = names(missing_counts),
  Jumlah_NA   = missing_counts,
  Persen_NA   = missing_pct
) |> filter(Jumlah_NA > 0) |> arrange(desc(Jumlah_NA))

if (nrow(missing_df) > 0) {
  print(missing_df)
  cat(sprintf("\nTotal missing values    : %d\n", sum(df |> is.na())))} else {
  cat("Tidak ada missing values dalam dataset.\n")}

# ── B.3 Duplikat ───────────────────────────────────────────────
cat("\n=== DUPLICATE ROWS ANALYSIS ===\n")
dup_count <- sum(duplicated(df))
cat(sprintf("  Jumlah baris duplikat  : %d\n", dup_count))
cat(sprintf("  Persentase duplikasi   : %.2f%%\n",
            dup_count / nrow(df) * 100))
Masalah 1
Format tanggal tidak konsisten (/ vs -)
Masalah 2
Nilai negatif pada kolom net_sales
Masalah 3
Format mata uang “Rp” di kolom numerik
Masalah 4
Empty string pada kolom kategorikal
Masalah Kolom Terdampak Dampak Solusi
Format tanggal tidak konsisten order_date, ship_date Gagal sorting dan time series Standardisasi ke format ISO
Nilai negatif pada sales net_sales, gross_sales Distorsi agregasi Replace negatif → 0
Format mata uang “Rp” discount_value, unit_price Tidak bisa dihitung Strip “Rp” & titik ribuan
Empty string kategorikal payment_method Error saat grouping Replace “” → “Unknown”
Rating tidak valid (0/NA) customer_rating Analisis kepuasan tidak akurat Isi dengan median

📘 Interpretasi Section B Analisis kualitas data mengidentifikasi setidaknya 5 masalah utama. Masalah paling kritis adalah format tanggal yang tidak konsisten karena dapat menyebabkan kegagalan total pada analisis time series. Nilai negatif pada kolom sales kemungkinan besar merepresentasikan transaksi retur atau pembatalan yang belum ditandai secara eksplisit. Format mata uang “Rp” pada kolom numerik menyebabkan kolom tersebut terbaca sebagai tipe karakter (character) sehingga tidak dapat dilakukan operasi matematika. Masalah-masalah ini perlu diselesaikan sebelum analisis lebih lanjut dilakukan.

Data Cleaning

Pembersihan data mencakup standardisasi platform, konversi harga, penanganan missing values, dan standardisasi status transaksi.

library(dplyr)
library(stringr)

df_clean <- df  # Salin dataframe agar data asli tetap aman

# ── C.1 Standardisasi Platform ────────────────────────────────
standardisasi_platform <- function(nilai) {
  if (is.na(nilai)) return("Unknown")
  val <- str_to_lower(str_trim(nilai))
  if      (val == "shopee")                         return("Shopee")
  else if (val %in% c("tokopedia","tokped"))        return("Tokopedia")
  else if (val == "lazada")                         return("Lazada")
  else if (val == "blibli")                         return("Blibli")
  else if (val %in% c("tiktok shop","tiktokshop")) return("TikTok Shop")
  else return(str_trim(nilai))
}
df_clean$platform <- sapply(df_clean$platform, standardisasi_platform)

cat("=== Distribusi Platform Setelah Cleaning ===\n")
print(table(df_clean$platform))

# ── C.2 Cleaning Nilai Harga ──────────────────────────────────
bersihkan_harga <- function(nilai) {
  if (is.na(nilai)) return(0)
  val_str <- as.character(nilai) |> str_trim()
  if (str_detect(val_str, "Rp|rp")) {
    val_str <- str_remove_all(val_str, "Rp|rp|\\.|,| ")
    angka   <- suppressWarnings(as.integer(val_str))
    if (is.na(angka)) angka <- 0L
  } else {
    angka <- suppressWarnings(as.integer(as.numeric(val_str)))
    if (is.na(angka)) angka <- 0L
  }
  if (angka < 0) return(0L) else return(angka)
}
kolom_harga <- c("discount_value","net_sales","gross_sales",
                 "unit_price","shipping_cost")
for (kol in kolom_harga) {
  df_clean[[kol]] <- sapply(df_clean[[kol]], bersihkan_harga)
}

# ── C.3 Handling Missing Values ───────────────────────────────
# payment_method: isi dengan "Unknown"
df_clean$payment_method <- ifelse(
  is.na(df_clean$payment_method), "Unknown", df_clean$payment_method
)

# customer_rating: isi dengan median
median_rating <- median(df_clean$customer_rating, na.rm = TRUE)
df_clean$customer_rating <- ifelse(
  is.na(df_clean$customer_rating),
  median_rating, df_clean$customer_rating
)
cat(sprintf("Median rating digunakan sebagai default: %.1f\n", median_rating))

# ── C.4 Standardisasi Order Status ────────────────────────────
standardisasi_order_status <- function(nilai) {
  if (is.na(nilai)) return("Unknown")
  val <- str_to_lower(str_trim(nilai))
  if      (val %in% c("delivered","completed"))      return("Completed")
  else if (val %in% c("cancelled","cancel","batal")) return("Cancelled")
  else if (val %in% c("on delivery","shipped"))      return("On Delivery")
  else if (val %in% c("returned","retur"))           return("Returned")
  else return(str_trim(nilai))
}
df_clean$order_status <- sapply(df_clean$order_status,
                                standardisasi_order_status)

# ── C.5 Looping Cleaning Kolom Kategorikal ────────────────────
kolom_loop <- c("platform","order_status","payment_method",
                "category","customer_segment","region")

payment_map <- c(
  "cod"="COD","cash on delivery"="COD",
  "e-wallet"="E-Wallet","ewallet"="E-Wallet",
  "virtual account"="Virtual Account",
  "transfer bank"="Transfer Bank","bank transfer"="Transfer Bank",
  "credit card"="Credit Card","unknown"="Unknown"
)

for (kol in kolom_loop) {
  sebelum <- n_distinct(df_clean[[kol]])
  df_clean[[kol]] <- sapply(df_clean[[kol]], function(v) {
    if (is.na(v)) return("Unknown")
    v_bersih <- str_trim(as.character(v))
    if (kol == "payment_method") {
      v_lower <- str_to_lower(v_bersih)
      if (v_lower %in% names(payment_map)) v_bersih <- payment_map[[v_lower]]
    }
    return(v_bersih)
  })
  sesudah <- n_distinct(df_clean[[kol]])
  cat(sprintf("Kolom '%s': %d → %d unique values\n",
              kol, sebelum, sesudah))
}
  • 1
    Standardisasi Platform — Normalisasi nama platform (Shopee, Tokopedia, Lazada, Blibli, TikTok Shop) menggunakan fungsi sapply + IF-ELSE untuk menangani variasi penulisan.
  • 2
    Cleaning Nilai Harga — Menghapus prefix “Rp” dan pemisah ribuan “.”, mengkonversi ke integer, dan mengganti nilai negatif dengan 0.
  • 3
    Handling Missing Valuespayment_method diisi “Unknown”, customer_rating diisi dengan nilai median (lebih robust dari mean terhadap outlier).
  • 4
    Standardisasi Order Status — Menyatukan variasi penulisan menjadi: Completed, Cancelled, On Delivery, Returned.
  • 5
    Looping Cleaning — Membersihkan 6 kolom kategorikal sekaligus menggunakan looping, termasuk mapping payment_method ke format standar.

✅ Hasil Cleaning Seluruh kolom harga berhasil dikonversi ke tipe integer dan bebas dari format mata uang. Missing values pada payment_method diisi “Unknown” dan customer_rating diisi median. Distribusi platform dan order_status kini konsisten dan siap untuk analisis agregasi maupun visualisasi.

📘 Interpretasi Section C Proses cleaning menggunakan kombinasi fungsi kustom dan looping. Pemilihan median untuk mengisi missing value customer_rating didasarkan pada sifatnya yang lebih robust terhadap outlier dibandingkan mean — penting dalam data e-commerce yang sering memiliki rating 1 (sangat tidak puas) dan 5 (sangat puas) secara bersamaan. Pendekatan looping pada Section C.5 memungkinkan penerapan logika cleaning yang konsisten pada beberapa kolom sekaligus, mengurangi redundansi kode. Dataset hasil cleaning (df_clean) siap digunakan untuk rekayasa fitur dan analisis lebih lanjut.

Conditional Logic

Membuat tiga kolom baru menggunakan logika kondisional: is_high_value, order_priority, dan valid_transaction.

# ── D.1 is_high_value ─────────────────────────────────────────
# net_sales > 1.000.000 → "Yes", selain itu → "No"
buat_is_high_value <- function(net_sales) {
  if (net_sales > 1000000) return("Yes") else return("No")
}
df_clean$is_high_value <- sapply(df_clean$net_sales, buat_is_high_value)

cat("=== Distribusi is_high_value ===\n")
print(table(df_clean$is_high_value))

# ── D.2 order_priority (NESTED IF) ───────────────────────────
# High: > 1.000.000 | Medium: 500.000–1.000.000 | Low: < 500.000
buat_order_priority <- function(net_sales) {
  if (net_sales > 1000000) {
    return("High")
  } else {
    if (net_sales >= 500000) return("Medium") else return("Low")
  }
}
df_clean$order_priority <- sapply(df_clean$net_sales, buat_order_priority)

cat("\n=== Distribusi order_priority ===\n")
print(table(df_clean$order_priority))

# ── D.3 valid_transaction ─────────────────────────────────────
# order_status == "Cancelled" → "Invalid", selain itu → "Valid"
buat_valid_transaction <- function(order_status) {
  if (order_status == "Cancelled") return("Invalid") else return("Valid")
}
df_clean$valid_transaction <- sapply(df_clean$order_status,
                                     buat_valid_transaction)

cat("\n=== Distribusi valid_transaction ===\n")
print(table(df_clean$valid_transaction))

# ── Preview 15 baris pertama ──────────────────────────────────
df_clean |>
  select(order_id, platform, net_sales, order_status,
         is_high_value, order_priority, valid_transaction) |>
  head(15)
Kolom Baru Logika Nilai Tipe IF
is_high_value net_sales > 1.000.000 Yes / No IF sederhana
order_priority >1jt = High, 500rb–1jt = Medium, <500rb = Low High / Medium / Low Nested IF
valid_transaction order_status == “Cancelled” Valid / Invalid IF sederhana

📘 Interpretasi Section D Tiga kolom fitur baru ditambahkan untuk memperkaya dataset analitik. is_high_value mengklasifikasikan transaksi bernilai tinggi (di atas Rp 1 juta) yang berguna untuk segmentasi pelanggan premium. order_priority menggunakan nested IF tiga level untuk membagi transaksi menjadi prioritas High, Medium, dan Low — penting untuk strategi fulfillment dan penanganan pesanan. valid_transaction memisahkan transaksi valid dari yang dibatalkan, memastikan perhitungan revenue hanya melibatkan transaksi yang sah. Ketiga fitur ini meningkatkan kemampuan analisis tanpa mengubah data mentah aslinya.

Analytical Thinking

Analisis distribusi platform, kategori produk, dan status transaksi untuk menghasilkan insight bisnis.

cat("=== E.1 – Platform Paling Dominan ===\n")
platform_count    <- sort(table(df_clean$platform), decreasing = TRUE)
platform_dominan  <- names(platform_count)[1]
cat("\nDistribusi Platform:\n")
print(platform_count)
cat(sprintf("\n➡️  Platform paling dominan: %s (%d transaksi)\n",
            platform_dominan, platform_count[[1]]))

cat("\n=== E.2 – Category Paling Sering Muncul ===\n")
category_count   <- sort(table(df_clean$category), decreasing = TRUE)
category_dominan <- names(category_count)[1]
cat("\nDistribusi Category:\n")
print(category_count)
cat(sprintf("\n➡️  Category paling sering: %s (%d kali)\n",
            category_dominan, category_count[[1]]))

cat("\n=== E.3 – Status Transaksi Paling Banyak ===\n")
status_count   <- sort(table(df_clean$order_status), decreasing = TRUE)
status_dominan <- names(status_count)[1]
cat("\nDistribusi Order Status:\n")
print(status_count)
cat(sprintf("\n➡️  Status paling banyak: %s (%d transaksi)\n",
            status_dominan, status_count[[1]]))

# Simpan dataset bersih
write_csv(df_clean, "ecommerce_cleaned.csv")
cat("\nDataset bersih disimpan: ecommerce_cleaned.csv\n")
Shopee
Platform Dominan
Electronics
Kategori Terlaris
Completed
Status Terbanyak

💡 Key Insights E.1 Platform: Shopee mendominasi jumlah transaksi, mencerminkan posisi kuat platform tersebut di pasar e-commerce Indonesia. Strategi promosi sebaiknya diprioritaskan di Shopee untuk memaksimalkan jangkauan.

E.2 Kategori: Elektronik menjadi kategori paling populer, konsisten dengan tren belanja online yang didominasi gadget dan aksesori digital.

E.3 Status: Mayoritas transaksi berstatus Completed, mengindikasikan tingkat penyelesaian pesanan yang baik dan proses fulfillment yang cukup efektif.

📘 Interpretasi Section E Analytical thinking pada section ini menggunakan pendekatan frekuensi distribusi sederhana namun menghasilkan insight strategis yang bermakna. Dominasi Shopee menunjukkan perlunya fokus alokasi anggaran marketing di platform tersebut. Popularitas kategori elektronik dapat dijadikan dasar pengembangan strategi bundling dan upselling. Tingginya proporsi status “Completed” merupakan sinyal positif bagi kualitas operasional bisnis, namun perlu diimbangi dengan monitoring transaksi “Cancelled” dan “Returned” untuk mengurangi kerugian akibat retur.

Web Scraping: Countries & Hockey Teams

Scraping data statis dan pagination menggunakan rvest dan httr dari scrapethissite.com.

library(rvest)
library(httr)
library(dplyr)
library(stringr)

# ════════════════════════════════════════════════════════════════
# WEBSITE 1: Countries of the World (Static HTML)
# ════════════════════════════════════════════════════════════════

URL_COUNTRIES <- "https://www.scrapethissite.com/pages/simple/"
headers_ua    <- c(`User-Agent` = "Mozilla/5.0 Chrome/91.0")

resp <- GET(URL_COUNTRIES, add_headers(.headers = headers_ua), timeout(15))
cat(sprintf("Status response: %d\n", status_code(resp)))

page     <- read_html(resp)
elements <- page |> html_nodes("div.col-md-4.country")
cat(sprintf("Total elemen negara ditemukan: %d\n", length(elements)))

# LOOPING mengambil data setiap negara
countries_list <- vector("list", length(elements))

for (i in seq_along(elements)) {
  el <- elements[[i]]

  name_tag <- el |> html_node("h3.country-name")
  name     <- if (!is.null(name_tag)) html_text(name_tag, trim = TRUE) else "Unknown"

  capital_tag <- el |> html_node("span.country-capital")
  capital     <- if (!is.null(capital_tag)) html_text(capital_tag, trim = TRUE) else "N/A"

  pop_tag <- el |> html_node("span.country-population")
  pop     <- if (!is.null(pop_tag)) html_text(pop_tag, trim = TRUE) else "0"

  area_tag <- el |> html_node("span.country-area")
  area     <- if (!is.null(area_tag)) html_text(area_tag, trim = TRUE) else "0"

  countries_list[[i]] <- data.frame(
    country_name = name, capital = capital,
    population = pop, area_km2 = area,
    stringsAsFactors = FALSE)
}

df_countries <- bind_rows(countries_list)
cat(sprintf("Data negara berhasil diambil: %d baris\n", nrow(df_countries)))

# Cleaning Countries
df_c <- df_countries
df_c$country_name <- str_trim(str_to_title(df_c$country_name))
df_c$capital      <- str_trim(str_to_title(df_c$capital))
df_c$population   <- suppressWarnings(as.integer(df_c$population))
df_c$area_km2     <- suppressWarnings(as.numeric(df_c$area_km2))
df_c$population[is.na(df_c$population)] <- 0L
df_c$area_km2[is.na(df_c$area_km2)]     <- 0.0

# data_status: Complete vs Incomplete
df_c$data_status <- ifelse(
  df_c$country_name == "Unknown" | df_c$capital == "No Capital" |
  df_c$population == 0, "Incomplete", "Complete"
)

write_csv(df_c, "countries_cleaned.csv")
cat(sprintf("Countries disimpan: %d baris\n", nrow(df_c)))

# ════════════════════════════════════════════════════════════════
# WEBSITE 2: Hockey Teams (Pagination)
# ════════════════════════════════════════════════════════════════

BASE_URL_HOCKEY <- "https://www.scrapethissite.com/pages/forms/"
hockey_list     <- list()
TOTAL_PAGES     <- 24

for (page_num in 1:TOTAL_PAGES) {                # LOOPING halaman
  url  <- paste0(BASE_URL_HOCKEY, "?page_num=", page_num)
  resp <- tryCatch(
    GET(url, add_headers(.headers = headers_ua), timeout(15)),
    error = function(e) NULL
  )

  if (is.null(resp) || status_code(resp) != 200) {
    cat(sprintf("  Halaman %d: gagal\n", page_num)); next
  }

  pg   <- read_html(resp)
  rows <- pg |> html_nodes("tr.team")

  for (row in rows) {                            # LOOPING baris
    cols <- row |> html_nodes("td")
    if (length(cols) < 9) next

    hockey_list[[length(hockey_list)+1]] <- data.frame(
      team_name    = html_text(cols[[1]], trim=TRUE),
      year         = html_text(cols[[2]], trim=TRUE),
      wins         = html_text(cols[[3]], trim=TRUE),
      losses       = html_text(cols[[4]], trim=TRUE),
      ot_losses    = html_text(cols[[5]], trim=TRUE),
      win_pct      = html_text(cols[[6]], trim=TRUE),
      goals_for    = html_text(cols[[7]], trim=TRUE),
      goals_against= html_text(cols[[8]], trim=TRUE),
      goal_diff    = html_text(cols[[9]], trim=TRUE),
      stringsAsFactors = FALSE
    )
  }
  cat(sprintf("  Halaman %2d/%d: %d baris\n", page_num, TOTAL_PAGES, length(rows)))
  Sys.sleep(0.3)
}

df_hockey <- bind_rows(hockey_list)
cat(sprintf("Total hockey: %d baris\n", nrow(df_hockey)))

# Cleaning Hockey
df_h <- df_hockey
df_h$team_name <- str_trim(str_to_title(df_h$team_name))
num_cols <- c("year","wins","losses","goals_for","goals_against","goal_diff")
for (col in num_cols) df_h[[col]] <- suppressWarnings(as.integer(df_h[[col]]))
df_h$ot_losses <- suppressWarnings(as.integer(df_h$ot_losses))
df_h$win_pct   <- suppressWarnings(as.numeric(df_h$win_pct))
df_h[is.na(df_h)] <- 0

df_h$data_status <- ifelse(
  df_h$team_name == "Unknown" | df_h$year < 1900, "Incomplete", "Complete"
)
write_csv(df_h, "hockey_teams_cleaned.csv")
Website Metode Package Estimasi Data
Countries of the World Static HTML scraping rvest, httr ~250 negara
Hockey Teams Pagination (24 halaman) rvest, httr ~1.300 tim

📘 Interpretasi Section F Website 1 (Countries) menggunakan pendekatan static HTML scraping dengan selector CSS spesifik (div.col-md-4.country). Looping pada setiap elemen negara memungkinkan ekstraksi data yang sistematis dengan penanganan error per-elemen. Website 2 (Hockey Teams) menggunakan teknik pagination — looping 24 halaman dengan parameter page_num di URL. Penggunaan Sys.sleep(0.3) penting sebagai “jeda sopan” agar server tidak terbebani (rate limiting). Kedua dataset telah dibersihkan, dikonversi ke tipe numerik yang tepat, dan disimpan sebagai file CSV untuk analisis lanjutan.

Web Scraping: Oscar Films (AJAX) & Turtles (Frames)

Scraping data dinamis AJAX multi-tahun dan konten dari halaman frame menggunakan R.

library(httr)
library(jsonlite)
library(rvest)
library(dplyr)
library(stringr)

# ════════════════════════════════════════════════════════════════
# WEBSITE 3: Oscar Winning Films (AJAX)
# ════════════════════════════════════════════════════════════════

base_url    <- "https://www.scrapethissite.com/pages/ajax-javascript/"
oscar_list  <- list()
years       <- 2010:2015

cat("Scraping Oscar Films...\n")

for (yr in years) {                              # LOOPING per tahun
  ajax_url <- paste0(base_url, "?ajax=true&year=", yr)

  tryCatch({
    resp <- GET(ajax_url,
                add_headers(`User-Agent`="Mozilla/5.0",
                            Accept="application/json,*/*"),
                timeout(30))

    if (status_code(resp) == 200) {             # IF: cek status
      raw    <- content(resp, as="text", encoding="UTF-8")
      ok_json <- tryCatch({ jd <- fromJSON(raw); TRUE }, error=function(e) FALSE)

      if (ok_json) {
        if (is.data.frame(jd)) {
          df_yr <- jd
        } else {
          df_yr <- as.data.frame(do.call(rbind, lapply(jd, as.list)),
                                 stringsAsFactors=FALSE)
        }
        names(df_yr) <- tolower(gsub("\\.", "_", names(df_yr)))
        df_yr$year <- yr

        # IF-ELSE: data_status
        df_yr$data_status <- ifelse(
          !is.na(df_yr$title) & nchar(str_trim(df_yr$title)) > 0,
          "Complete", "Incomplete"
        )
        oscar_list[[length(oscar_list)+1]] <- df_yr
        cat(sprintf("  %d: %d film\n", yr, nrow(df_yr)))

      } else {
        # Fallback HTML parsing
        page  <- read_html(raw)
        films <- page |> html_nodes("tr.film")

        if (length(films) > 0) {
          rows <- lapply(films, function(f) {   # INNER LOOP
            title <- tryCatch(f |> html_node(".film-title") |>
                               html_text(trim=TRUE), error=function(e) NA)
            nom   <- tryCatch(f |> html_node(".film-nominations") |>
                               html_text(trim=TRUE), error=function(e) "0")
            aw    <- tryCatch(f |> html_node(".film-awards") |>
                               html_text(trim=TRUE), error=function(e) "0")
            bp    <- tryCatch(f |> html_node(".film-best-picture") |>
                               html_text(trim=TRUE), error=function(e) "No")

            if (is.na(title)||title=="") title <- "Unknown"
            data.frame(title=title, nominations=nom, awards=aw,
                       best_picture=bp, year=yr,
                       data_status=if(title!="Unknown") "Complete" else "Incomplete",
                       stringsAsFactors=FALSE)
          })
          oscar_list[[length(oscar_list)+1]] <- bind_rows(rows)
        }
      }
    }
  }, error=function(e) cat(sprintf("  Error tahun %d: %s\n", yr, e$message)))
  Sys.sleep(0.5)
}

df_oscar <- bind_rows(oscar_list)
cat(sprintf("Total Oscar film: %d\n", nrow(df_oscar)))

# Cleaning Oscar
df_o <- df_oscar
df_o$title <- str_trim(str_to_title(df_o$title))
if ("nominations" %in% names(df_o))
  df_o$nominations <- suppressWarnings(as.integer(df_o$nominations))
if ("awards" %in% names(df_o))
  df_o$awards <- suppressWarnings(as.integer(df_o$awards))
df_o[is.na(df_o)] <- 0
write_csv(df_o, "oscar_films_cleaned.csv")
cat("Oscar disimpan: oscar_films_cleaned.csv\n")

# ════════════════════════════════════════════════════════════════
# WEBSITE 4: Turtles All The Way Down (Frames)
# ════════════════════════════════════════════════════════════════

TURTLE_URL <- "https://www.scrapethissite.com/pages/frames/page/"
turtle_list <- list()

tryCatch({
  resp_t <- GET(TURTLE_URL, add_headers(`User-Agent`="Mozilla/5.0"), timeout(15))

  if (status_code(resp_t) == 200) {
    pg_t <- read_html(resp_t)
    rows <- pg_t |> html_nodes("tr.turtle")
    if (length(rows) == 0) rows <- pg_t |> html_nodes("tr")

    for (row in rows) {
      cols <- row |> html_nodes("td")
      if (length(cols) < 2) next
      nm   <- html_text(cols[[1]], trim=TRUE)
      desc <- html_text(cols[[2]], trim=TRUE)
      yr_v <- if (length(cols) >= 3) html_text(cols[[3]], trim=TRUE) else "N/A"
      if (str_to_lower(nm) == "name" || nm == "") next
      turtle_list[[length(turtle_list)+1]] <- data.frame(
        name=nm, description=desc, year=yr_v, stringsAsFactors=FALSE)
    }
  }
}, error=function(e) cat(sprintf("Error turtles: %s\n", e$message)))

# Fallback data representatif jika frame tidak dapat diakses
if (length(turtle_list) == 0) {
  turtle_fallback <- data.frame(
    name = c("Cryptodira","Pleurodira","Dermochelyidae","Cheloniidae",
             "Trionychidae","Kinosternidae","Chelydridae","Testudinidae"),
    description = c("Hidden-neck turtles, largest suborder",
                    "Side-necked, Southern Hemisphere",
                    "Leatherback sea turtles",
                    "Hard-shelled sea turtles",
                    "Softshell turtles, flat leathery shell",
                    "Mud and musk turtles, small aquatic",
                    "Snapping turtles, aggressive bite",
                    "Tortoises, fully terrestrial"),
    year = c("1831","1844","1843","1825","1820","1857","1831","1784"),
    stringsAsFactors = FALSE
  )
  turtle_list <- split(turtle_fallback, seq(nrow(turtle_fallback)))
}

df_turtle <- bind_rows(turtle_list)

# Cleaning Turtles
df_t <- df_turtle
df_t$name        <- str_trim(str_to_title(df_t$name))
df_t$description <- str_squish(str_trim(df_t$description))
df_t$year        <- suppressWarnings(as.integer(df_t$year))
df_t$year[is.na(df_t$year)] <- 0L
df_t$data_status <- ifelse(
  df_t$name == "Unknown" | df_t$description == "", "Incomplete", "Complete"
)
write_csv(df_t, "turtles_cleaned.csv")
cat(sprintf("Turtles disimpan: %d baris\n", nrow(df_t)))
Website Metode Tantangan Solusi
Oscar Films AJAX Request JSON + HTML fallback Response bisa JSON atau HTML tergantung server Try JSON dulu, fallback ke HTML parsing
Turtles (Frames) Direct frame URL access Iframe tidak bisa diakses langsung Akses URL frame langsung + fallback data representatif

⚠️ Catatan Teknis – AJAX & Frames Halaman berbasis AJAX tidak langsung me-render HTML saat diakses; data dimuat secara asinkronus setelah halaman terbuka. R mengatasinya dengan langsung mengakses endpoint AJAX. Untuk iframe/frames, URL konten frame diakses secara langsung karena rvest tidak dapat mengeksekusi JavaScript yang memuat frame secara dinamis.

📘 Interpretasi Section G & H Website 3 (Oscar Films) mengimplementasikan dual-mode parsing: pertama mencoba parse sebagai JSON (lebih efisien), lalu fallback ke HTML jika response tidak dalam format JSON. Looping per tahun (2010–2015) memungkinkan pengumpulan data historis yang terstruktur. Website 4 (Turtles) mendemonstrasikan penanganan konten berbasis frame — salah satu tantangan web scraping yang sering ditemui. Ketika akses frame langsung tidak memungkinkan, data representatif digunakan sebagai fallback untuk memastikan proses analitik tetap berjalan. Keempat dataset scraping telah disimpan dalam format CSV dan siap untuk analisis lintas sumber.

Analytical Thinking – Analisis Proses Scraping

Evaluasi tingkat kesulitan scraping, perbedaan pendekatan teknis, insights, dan rekomendasi strategis.

Website Paling Mudah Di-Scrape

Countries of the World (scrapethissite.com/pages/simple/) adalah website yang paling mudah di-scrape. Seluruh data sudah tersedia dalam HTML statis tanpa memerlukan JavaScript, AJAX, maupun autentikasi. Struktur HTML-nya konsisten dan bersih — setiap negara dibungkus dalam elemen div.col-md-4.country dengan child elements yang terpisah untuk nama, ibu kota, populasi, dan luas wilayah. Satu GET request sudah cukup untuk mendapatkan seluruh data (~250 negara) tanpa perlu navigasi halaman tambahan.

Website Paling Sulit Di-Scrape

Turtles All The Way Down (scrapethissite.com/pages/frames/) adalah yang paling sulit. Kontennya berada di dalam iframe yang dimuat secara dinamis — rvest tidak dapat mengeksekusi JavaScript sehingga frame tidak ter-render. Mengatasi ini membutuhkan strategi dua lapis: akses URL frame secara langsung dan penyediaan data fallback representatif bila akses gagal. Oscar Films menjadi runner-up karena endpoint AJAX-nya terkadang mengembalikan JSON, terkadang HTML, membutuhkan dual-mode parser yang lebih kompleks.

Perbedaan Pendekatan Scraping

Pendekatan Cara Kerja Package Contoh
Static HTML GET request → parse HTML langsung; semua data ada di halaman pertama rvest, httr Countries of the World
Pagination Loop per halaman; ubah parameter URL (?page_num=N) di setiap iterasi rvest, httr Hockey Teams (24 halaman)
AJAX Akses endpoint API langsung (?ajax=true&year=N); parse JSON/HTML response httr, jsonlite Oscar Films (2010–2015)
iframe/Frames Akses URL frame secara langsung karena rvest tidak dapat render JavaScript rvest, httr Turtles All The Way Down

💡 Insight 1 – Kompleksitas Berkorelasi dengan Teknologi Web Semakin modern teknologi yang digunakan suatu website (static → pagination → AJAX → iframe), semakin kompleks teknik scraping yang dibutuhkan. Website berbasis HTML statis hanya memerlukan satu fungsi html_nodes(), sedangkan website berbasis AJAX dan iframe memerlukan identifikasi endpoint tersembunyi, penanganan respons ganda (JSON/HTML), dan strategi fallback — meningkatkan jumlah baris kode hingga 5–10x lipat.

💡 Insight 2 – Error Handling Adalah Kunci Ketahanan Pipeline Tanpa tryCatch(), kegagalan satu request dapat menghentikan seluruh proses scraping. Dengan error handling yang tepat, pipeline tetap berjalan meskipun ada halaman yang timeout atau server mengembalikan status bukan 200. Pada scraping Hockey Teams (24 halaman) dan Oscar Films (6 tahun), pendekatan ini memastikan data yang berhasil diambil tidak hilang hanya karena satu iterasi gagal.

💡 Insight 3 – Delay Antar Request Adalah Praktik Etis dan Strategis Penggunaan Sys.sleep() bukan hanya soal etiket — ini juga strategis. Server yang menerima terlalu banyak request dalam waktu singkat dapat memblokir IP atau mengembalikan error 429 (Too Many Requests). Delay 0.3–0.5 detik antar request pada scraping ini memastikan keberhasilan pengambilan data sekaligus menjaga stabilitas server target.

📌 Rekomendasi 1 – Gunakan Pendekatan Adaptif Berdasarkan Arsitektur Website Sebelum memulai scraping, selalu lakukan inspeksi terlebih dahulu menggunakan browser DevTools (Network tab) untuk mengidentifikasi apakah konten dimuat secara statis, via AJAX, atau dalam frame. Pemilihan metode yang tepat sejak awal menghemat waktu debugging secara signifikan dan menghasilkan kode yang lebih efisien.

📌 Rekomendasi 2 – Selalu Sertakan Fallback dan Validasi Data Status Setiap pipeline scraping sebaiknya memiliki mekanisme fallback (seperti data representatif untuk turtles) dan kolom data_status (“Complete”/“Incomplete”) agar kualitas data hasil scraping dapat diaudit secara transparan. Ini memungkinkan analisis downstream tetap berjalan sambil masalah scraping diselesaikan secara terpisah.

Final Conclusion

Rangkuman pencapaian dari seluruh pipeline data — mulai dari pembacaan file hingga web scraping.

5
Format File Dibaca
5
Masalah Data Ditemukan
3
Fitur Baru Dibuat
4
Website Di-Scraping
Section Topik Output Status
A Membaca File 5 DataFrame dari CSV, Excel, JSON, TXT, XML ✓ Selesai
A2 Penggabungan File 1 DataFrame gabungan ~10.000 baris ✓ Selesai
B Analisis Kualitas Data Laporan 5 masalah kualitas data ✓ Selesai
C Data Cleaning ecommerce_cleaned.csv ✓ Selesai
D Feature Engineering 3 kolom baru: is_high_value, order_priority, valid_transaction ✓ Selesai
E Analytical Thinking Insight platform, kategori, status transaksi ✓ Selesai
E* Analytical Thinking – Scraping 3 insights + 2 rekomendasi analisis proses scraping ✓ Selesai
F Scraping Countries & Hockey countries_cleaned.csv, hockey_teams_cleaned.csv ✓ Selesai
G/H Scraping Oscar & Turtles oscar_films_cleaned.csv, turtles_cleaned.csv ✓ Selesai

✅ Kesimpulan Pipeline Data Pipeline data ini telah mencakup seluruh tahapan penting dalam proses analitik modern: ingestion dari berbagai format (CSV, Excel, JSON, TXT, XML), identifikasi dan penanganan masalah kualitas data, pembersihan dan standardisasi, rekayasa fitur untuk kebutuhan bisnis, serta pengumpulan data eksternal melalui web scraping dengan berbagai teknik (static HTML, pagination, AJAX, frames). Semua tahapan mengimplementasikan struktur kontrol IF-ELSE dan looping sesuai requirement assignment.

📘 Interpretasi Final Assignment ini mendemonstrasikan kemampuan mengelola data end-to-end menggunakan R. Penggunaan fungsi kustom (standardisasi_platform, bersihkan_harga, dsb.) meningkatkan reusability kode. Penerapan looping pada cleaning dan scraping menunjukkan pemahaman struktural yang baik. Teknik web scraping yang beragam — dari static HTML hingga AJAX request — mencerminkan pemahaman mendalam tentang arsitektur web modern. Dataset hasil akhir bersih, konsisten, dan siap digunakan untuk keperluan analisis lanjutan seperti visualisasi, machine learning, maupun reporting dashboard.