Members

Lulu Najla Salsabila

🌸 DATA SCIENCE STUDENT

🎫 NIM: 52250069
🏛️ Institut Teknologi Sains Bandung
👨‍🏫 Bakti Siregar, M.Sc., CDS.

Aurora Sekarningrum

✨ DATA SCIENCE STUDENT

🎫 NIM: 52250072
🏛️ Institut Teknologi Sains Bandung
👨‍🏫 Bakti Siregar, M.Sc., CDS.

Raihania Syah Putri

⚡ DATA SCIENCE STUDENT

🎫 NIM: 52250054
🏛️ Institut Teknologi Sains Bandung
👨‍🏫 Bakti Siregar, M.Sc., CDS.

Soal 1 — E-Commerce

## Column

Section A – Data Collection

📁 Sumber Data: Google Drive Folder — 5 file (CSV, Excel, JSON, TXT, XML)  |  Total baris gabungan: 10000 baris  |  22 kolom

Membaca 5 File Berbeda (LOOPING)

Program menggunakan looping (for (cfg in file_configs)) untuk membaca 5 file sekaligus dari Google Drive: ecommerce.csv, ecommerce.xlsx, ecommerce.json, ecommerce.txt (sep=|), dan ecommerce.xml. Setiap file dibaca dengan fungsi yang sesuai formatnya — read.csv(), read_excel(), fromJSON(), read.table(), xmlToDataFrame(). Tanpa looping, kita harus menulis kode baca file 5 kali secara manual.

Informasi Setiap File yang Ditampilkan

Untuk setiap file, program menampilkan tiga informasi wajib: (1) Jumlah baris — seberapa banyak data transaksi, (2) Jumlah kolom — seberapa banyak atribut, (3) Nama kolom — antara lain order_id, order_date, product_name, category, platform, unit_price, quantity, gross_sales, net_sales, order_status, payment_method, customer_rating, customer_segment, region, is_returned, discount.

Cek Struktur Kolom (IF / IF-ELSE)

Setelah setiap file dibaca, program mengecek dengan IF-ELSE: apakah kolom-kolomnya identik dengan file pertama (referensi)? Jika identical(sort(names(df_tmp)), ref_cols) → cetak “✅ Ready to merge”. Jika berbeda → cetak “⚠️ Need adjustment”. Ini memastikan hanya file yang kompatibel yang digabung ke dataset utama.

Gabungkan Jadi 1 Dataset Utama

Semua file yang berstatus “Ready to merge” digabungkan menggunakan bind_rows() (R) menjadi satu DataFrame utama bernama df_combined_raw. Hasilnya: 10000 baris dari 5 file format berbeda — siap dianalisis dan dibersihkan di section berikutnya.

Autentikasi Google Drive

Untuk membaca file dari Google Drive, digunakan googledrive::drive_deauth() agar bisa mengakses folder publik tanpa login interaktif. File diunduh satu per satu ke direktori sementara (tempdir()) menggunakan drive_download(), lalu dibaca sesuai formatnya. Jika koneksi Drive gagal, data sintetis berstruktur sama digunakan sebagai fallback agar kode tetap berjalan.

DT::datatable(
  tbl_sectionA,
  caption  = "📁 Ringkasan Pembacaan 5 File dari Google Drive",
  options  = list(dom = 't', pageLength = 10, scrollX = TRUE),
  rownames = FALSE,
  class    = "display compact"
) |>
  DT::formatStyle("Status",
    backgroundColor = DT::styleEqual(
      c("✅ Ready to merge", "⚠️ Need adjustment"),
      c("#D1FAE5", "#FEF3C7")
    ))
DT::datatable(
  head(df_combined_raw, 20),
  caption  = "📋 Preview df_combined — 20 Baris Pertama (Sebelum Cleaning)",
  options  = list(pageLength = 10, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)

Section B – Data Handling

📊 Dimensi Dataset: 10000 baris × 22 kolom  |  Missing values: 2685  |  Duplikat: 5313 baris (53.13%)

Dimensi Dataset Gabungan

Dataset gabungan df_combined berisi 10000 baris dan 22 kolom. Setiap baris mewakili satu transaksi e-commerce. Dimensi ini didapat dari penggabungan 5 file format berbeda menggunakan bind_rows(). Informasi ini penting sebagai baseline sebelum proses cleaning dimulai.

Tipe Data Setiap Kolom

Kolom order_id, product_name, platform, order_status, payment_method, category, customer_segment, region bertipe character. Kolom unit_price, gross_sales, net_sales seharusnya numerik tetapi tersimpan sebagai string karena ada format "Rp 1.800.000". Kolom order_date harus bertipe Date, quantity bertipe integer, is_returned bertipe logical, customer_rating & discount bertipe numeric.

Missing Values

Nilai kosong (NA) ditemukan pada kolom payment_method dan customer_rating. Kolom payment_method bisa kosong karena pelanggan tidak mengisi metode pembayaran. Kolom customer_rating kosong karena transaksi yang belum diberi ulasan. Total missing values: 2685 dari seluruh dataset.

Duplicate Rows (Baris Ganda)

Ditemukan 5313 baris duplikat (53.13% dari total). Duplikat bisa terjadi karena file yang berbeda format memiliki data yang tumpang tindih. Baris yang persis sama dua kali akan menyebabkan double counting dalam analisis revenue dan frekuensi transaksi — harus dihapus.

Masalah #1 — Format Mata Uang di Kolom Numerik

Kolom unit_price, gross_sales, net_sales berisi nilai seperti "Rp 1.800.000" — bertipe string, bukan angka. Akibatnya operasi matematika (sum, mean) tidak bisa dilakukan langsung. Ini adalah masalah paling umum saat data dikumpulkan dari berbagai sumber yang berbeda format pelaporannya.

Masalah #2 & #3 — Inkonsistensi Teks & Nilai Negatif

Masalah #2: Nama platform tidak konsisten — "shopee", "SHOPEE", " SHOPEE " semuanya merujuk hal yang sama. Ini menyebabkan group_by(platform) menghasilkan grup terpisah padahal seharusnya satu. Masalah #3: Ada nilai negatif pada kolom numerik dan quantity ≤ 0 — tidak masuk akal secara bisnis dan harus diganti dengan 0 atau median.

DT::datatable(
  col_info,
  caption  = "📋 Tipe Data, Missing Values & Unique Values per Kolom",
  options  = list(dom = 't', pageLength = 20, scrollX = TRUE),
  rownames = FALSE, class = "display compact"
) |>
  DT::formatStyle("Missing",
    backgroundColor = DT::styleInterval(0, c("white", "#FEF3C7")))
if (nrow(missing_df) > 0) {
  DT::datatable(
    missing_df,
    caption  = "⚠️ Kolom dengan Missing Values",
    options  = list(dom = 't', pageLength = 10),
    rownames = FALSE, class = "display compact"
  )
}

Section C – Data Cleaning

🧹 Hasil Cleaning: 4687 baris bersih  |  Platform distandardisasi: 5 platform  |  Median rating default: 5

Standardisasi Platform (WAJIB IF)

Menggunakan fungsi standardisasi_platform() dengan kondisi IF berjenjang: "shopee", " SHOPEE ""Shopee" | "tokped""Tokopedia" | "lazada""Lazada" | "tiktok shop""TikTok Shop" | "blibli""Blibli". Fungsi ini diterapkan ke seluruh kolom platform menggunakan sapply().

Cleaning Nilai Harga (LOOPING WAJIB)

Format "Rp 1.800.000" dikonversi ke angka murni 1800000 menggunakan bersihkan_harga(): hapus "Rp" → hapus titik ribuan → as.integer(). Looping diterapkan sekaligus pada kolom net_sales, gross_sales, dan unit_price. Nilai negatif diubah ke 0. Ini memungkinkan operasi matematika dilakukan pada ketiga kolom sekaligus.

Handling Missing Values (WAJIB IF)

payment_method kosong/NA → diisi "Unknown" menggunakan ifelse(). customer_rating kosong → diisi dengan median (5). Logika median dipilih karena: data rating e-commerce sering miring ke kiri (banyak rating rendah 1), sehingga median lebih mewakili “nilai tengah” yang sebenarnya dibanding mean yang bisa terdistorsi oleh outlier.

Standardisasi Order Status

Variasi penulisan diseragamkan: "delivered""Completed" | "cancelled", "cancel", "batal""Cancelled" | "on delivery", "shipped""On Delivery" | "returned", "retur""Returned" | "processing""Processing". Hasilnya: distribusi status jadi konsisten dan bisa diagregasi dengan benar.

WAJIB LOOPING — Bersihkan Banyak Kolom Sekaligus

Looping pertama: 7 kolom teks (product_name, category, platform, order_status, payment_method, customer_segment, region) di-trim dan diubah lowercase sekaligus. Looping kedua: 3 kolom numerik (net_sales, gross_sales, unit_price) dibersihkan format Rp. Looping ketiga: 4 kolom diubah ke proper case. Total: 14 kolom dibersihkan via looping — jauh lebih efisien.

Konversi Tipe Data & Sortir

order_dateas.Date() | quantityas.integer() (nilai ≤ 0 diisi 1) | is_returnedas.logical() | discountas.numeric(). Dataset diurutkan berdasarkan order_date agar analisis time-series lebih mudah. Setelah semua tahap, df_clean siap untuk Section D dan E.

DT::datatable(
  df_clean |> select(order_id, order_date, product_name, category, platform,
                     unit_price, quantity, net_sales, order_status,
                     payment_method, customer_rating, region),
  caption  = "🧹 Dataset E-Commerce — Setelah Cleaning",
  options  = list(pageLength = 10, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)

Section D – Conditional Logic

🔖 3 Kolom Baru: is_high_value  |  order_priority (nested IF)  |  valid_transaction  |  High Value: 1757 transaksi  |  Invalid: 473 transaksi

Kolom is_high_value

Logika: ifelse(net_sales > 1000000, "Yes", "No"). Transaksi dengan penjualan bersih di atas Rp 1.000.000 diberi label "Yes". Kolom ini memudahkan tim marketing untuk mengidentifikasi pelanggan premium yang berpotensi masuk program loyalitas atau mendapat penanganan khusus. Dari 4687 transaksi, 1757 berstatus High Value.

Kolom order_priority (WAJIB Nested IF)

Menggunakan fungsi order_priority_fn() dengan nested IF: net_sales > 1.000.000"High" | net_sales ≥ 500.000"Medium" | net_sales < 500.000"Low". Nested IF artinya kondisi berada di dalam kondisi lain. Distribusi: High = 1757 | Medium = 947 | Low = 1983 transaksi.

Kolom valid_transaction

Logika: ifelse(order_status == "Cancelled", "Invalid", "Valid"). Transaksi yang dibatalkan tidak menghasilkan pendapatan nyata dan tidak boleh masuk dalam perhitungan revenue. Label "Invalid" pada transaksi Cancelled memudahkan filter saat analisis pendapatan — tinggal filter valid_transaction == "Valid". Jumlah "Invalid": 473 transaksi.

Mengapa Logika Bisnis Penting?

Tiga kolom baru ini adalah penerapan business logic ke dalam data. Tanpanya, tim harus menghitung manual setiap kali: mana yang bernilai tinggi? Mana yang prioritas? Mana yang valid? Dengan kolom-kolom ini, analisis bisa dilakukan dengan satu baris filter() atau group_by() — menghemat waktu dan mengurangi human error.

DT::datatable(
  df_clean |> select(order_id, platform, net_sales, order_status,
                     is_high_value, order_priority, valid_transaction),
  caption  = "🔖 Dataset dengan 3 Kolom Conditional Logic",
  options  = list(pageLength = 10, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
) |>
  DT::formatStyle("is_high_value",
    backgroundColor = DT::styleEqual(c("Yes","No"), c("#D1FAE5","#FEE2E2"))) |>
  DT::formatStyle("order_priority",
    backgroundColor = DT::styleEqual(
      c("High","Medium","Low"), c("#DBEAFE","#FEF9C3","#F3F4F6"))) |>
  DT::formatStyle("valid_transaction",
    backgroundColor = DT::styleEqual(c("Valid","Invalid"), c("#D1FAE5","#FEE2E2")))

Section E – Analytical Thinking

📈 Platform Dominan: Shopee (980 transaksi)  |  Kategori Terbanyak: Sports (986 transaksi)  |  Status Terbanyak: Completed (76.6%)

1. Platform Paling Dominan?

Platform dengan transaksi terbanyak adalah Shopee dengan 980 transaksi. Diukur menggunakan count(platform, sort=TRUE). Dominansi ini dapat mencerminkan kepercayaan konsumen yang lebih tinggi atau strategi harga yang lebih kompetitif di platform tersebut. Rekomendasi: alokasikan anggaran promosi lebih besar di platform ini untuk ROI yang lebih tinggi.

2. Category Paling Sering Muncul?

Kategori produk yang paling sering ditransaksikan adalah Sports dengan 986 transaksi. Diukur menggunakan count(category, sort=TRUE). Kategori ini menjadi tulang punggung penjualan — perlu dijaga ketersediaan stoknya dan menjadi fokus kampanye promosi utama. Kategori lain bisa ditingkatkan dengan bundling bersama kategori ini.

3. Status Transaksi Terbanyak?

Status transaksi yang paling banyak adalah Completed dengan 3588 transaksi (76.6% dari total). Diukur menggunakan count(order_status, sort=TRUE). Persentase ini memberikan gambaran kesehatan operasional bisnis — rasio Completed yang tinggi berarti layanan berjalan baik, sedangkan Cancelled yang tinggi bisa menandakan masalah stok atau pengiriman.

p1 <- plot_ly(platform_count, x = ~platform, y = ~Jumlah,
  type = 'bar', color = ~platform,
  colors = c('#1A2744','#3D7EF0','#0D9488','#F59E0B','#EF4444','#8B5CF6'),
  text = ~Jumlah, textposition = 'outside') |>
  layout(
    title  = list(text = "📊 Jumlah Transaksi per Platform", font = list(size = 13)),
    xaxis  = list(title = "Platform"),
    yaxis  = list(title = "Jumlah Transaksi"),
    showlegend = FALSE,
    paper_bgcolor = 'rgba(0,0,0,0)',
    plot_bgcolor  = 'rgba(0,0,0,0)',
    height = 300, margin = list(t=40, b=40)
  )
p1

Insight: Platform Shopee mendominasi dengan 980 transaksi. Ini menunjukkan basis pengguna terbesar ada di platform tersebut — alokasi iklan dan promosi sebaiknya diprioritaskan di sini untuk ROI tertinggi.

p2 <- plot_ly(category_count, x = ~category, y = ~Jumlah,
  type = 'bar', color = ~category,
  colors = c('#1A2744','#3D7EF0','#0D9488','#F59E0B','#EF4444','#8B5CF6','#EC4899','#14B8A6'),
  text = ~Jumlah, textposition = 'outside') |>
  layout(
    title  = list(text = "🛍️ Frekuensi Transaksi per Kategori Produk", font = list(size = 13)),
    xaxis  = list(title = "Kategori", tickangle = -30),
    yaxis  = list(title = "Jumlah Transaksi"),
    showlegend = FALSE,
    paper_bgcolor = 'rgba(0,0,0,0)',
    plot_bgcolor  = 'rgba(0,0,0,0)',
    height = 300, margin = list(t=40, b=60)
  )
p2

Insight: Kategori Sports adalah yang paling banyak ditransaksikan (986 transaksi). Fashion menjadi kategori utama karena tingginya frekuensi pembelian ulang dan impulse buying di platform e-commerce. Strategi: pastikan stok Fashion selalu lengkap dan beri diskon bundling dengan kategori Beauty.

p3 <- plot_ly(status_count, labels = ~order_status, values = ~Jumlah,
  type = 'pie', hole = 0.45,
  textinfo = 'label+percent',
  marker = list(colors = c('#3D7EF0','#EF4444','#F59E0B','#0D9488','#8B5CF6','#EC4899'))) |>
  layout(
    title      = list(text = "📦 Distribusi Status Transaksi", font = list(size = 13)),
    showlegend = TRUE,
    paper_bgcolor = 'rgba(0,0,0,0)',
    height = 300
  )
p3

Insight: Status Completed mendominasi (76.6% dari total transaksi). Persentase ini adalah indikator kesehatan operasional bisnis — semakin tinggi Completed, semakin baik. Perlu perhatian khusus pada transaksi Cancelled untuk mengidentifikasi penyebab pembatalan.

DT::datatable(
  platform_rev |>
    mutate(Total_Revenue = paste0("Rp ", formatC(Total_Revenue, format="d", big.mark=","))),
  caption  = "💰 Revenue & Rating per Platform",
  options  = list(dom = 't', pageLength = 10),
  rownames = FALSE, class = "display compact"
)

Soal 2 — Web Scraping

## Column

Section A – Data Collection

🌐 Web Scraping dengan R: Website 3 (Oscar AJAX) — 87 film  |  Website 4 (Turtles iFrame) — 14 famili

Dua Bahasa Wajib: Python & R

Soal mewajibkan penggunaan Python (untuk Website 1 Countries & Website 2 Hockey Teams) dan R (untuk Website 3 Oscar Films & Website 4 Turtles). File .Rmd ini mengerjakan Website 3 & 4 menggunakan R dengan library rvest, httr, dan jsonlite. Website 1 & 2 dikerjakan di file .py/.ipynb Python terpisah.

WAJIB LOOPING — Scraping Banyak Halaman

Looping digunakan untuk: (1) iterasi setiap tahun Oscar (dari daftar years yang diambil dari halaman utama), kirim request AJAX satu per satu, dan simpan hasilnya — ini adalah scraping multi-halaman via AJAX parameter. (2) Iterasi setiap baris <tr class="family"> di dalam iframe Turtles. Tanpa looping, scraping ratusan tahun Oscar tidak akan efisien.

Simpan ke DataFrame dan CSV

Setelah berhasil dikumpulkan, data Oscar disimpan ke oscar_films.csv dan data Turtles ke turtles_iframe_data.csv menggunakan write.csv(..., row.names=FALSE). Dua file CSV ini adalah bagian dari 4 file CSV output wajib (masing-masing 1 per website). File CSV bisa dibuka langsung di Excel untuk verifikasi.

🎬 Website 3 — Oscar Winning Films (AJAX / Javascript)

Cara Kerja AJAX

Website Oscar tidak menampilkan data film langsung saat halaman pertama dibuka. Data dimuat belakangan oleh JavaScript melalui AJAX request di background ke endpoint tersembunyi. Scraping HTML biasa menghasilkan halaman kosong karena film belum ada saat request dikirim — ini tantangan utama website modern yang dinamis.

Solusi: Temukan Endpoint API

Dengan DevTools (Inspect → Network → Filter XHR), ditemukan endpoint: ?ajax=true&year=XXXX. Menggunakan GET(url_oscar, query=list(ajax="true", year=yr)), kita dapat response JSON berisi seluruh film untuk tahun tersebut — jauh lebih cepat dan bersih dibanding simulasi browser penuh dengan Selenium.

Data yang Diambil

Kolom yang diambil per tahun: year (tahun Oscar), title (judul film), nominations (jumlah nominasi), awards (jumlah penghargaan menang), best_picture (apakah menang Best Picture), category (Best Picture / Award Winner / Nominated — dibuat dengan IF), description (keterangan otomatis dibuat dari field lain).

LOOPING Antar Tahun Oscar

for (yr in years) {
  r <- GET(url_oscar, query=list(
            ajax="true", year=yr))
  films <- fromJSON(content(r,"text"))
  oscar_data <- append(oscar_data, 
                       list(films))
  Sys.sleep(0.2)  # jeda server
}

Loop berjalan pada vektor years yang diambil dari tag <a class="year-link"> halaman Oscar. Sys.sleep(0.2) mencegah rate-limit dari server.

🐢 Website 4 — Turtles All the Way Down (Frames & iFrames)

Cara Kerja iFrame

Halaman Turtles menyimpan seluruh kontennya di dalam sebuah iframe — seperti jendela di dalam jendela. Jika scraping halaman utama saja, tidak ada data kura-kura yang terambil. Kita harus masuk ke dalam iframe terlebih dahulu untuk bisa membaca isinya.

Solusi: Ambil src dari iframe

Langkah: (1) scrape halaman utama → temukan <iframe id="iframe">, (2) ambil atribut src menggunakan html_attr(fr, "src"), (3) gabungkan dengan base URL, (4) scrape URL iframe tersebut secara terpisah sebagai halaman mandiri. Ini adalah teknik standar menangani konten berbasis frame.

Data yang Diambil

Dari dalam iframe: name (nama ilmiah famili, contoh "Cheloniidae"), description (nama umum, contoh "Sea turtles"), additional_info (kalimat ringkasan yang dibuat dari kombinasi kedua kolom sebelumnya). Minimal 3 field data sesuai ketentuan soal.

Fallback Data & IF Default

Jika koneksi iframe gagal (timeout/blocked), data fallback 14 famili kura-kura digunakan. Ini memastikan laporan tetap bisa dibuat. Di dalam looping baris, IF digunakan: jika elemen <td class="family-name"> tidak ditemukan → nilai default "Unknown". Pendekatan defensif ini penting untuk scraping yang robust.

DT::datatable(
  df_oscar_raw |> select(any_of(c("year","title","nominations","awards","category","description"))) |>
    head(30),
  caption  = paste0("🎬 Preview Oscar Films — ", nrow(df_oscar_raw),
                    " film dari ", length(unique(df_oscar_raw$year)), " tahun"),
  options  = list(pageLength = 8, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)
DT::datatable(
  df_turtles_raw,
  caption  = paste0("🐢 Data Turtles All the Way Down — ", nrow(df_turtles_raw), " famili"),
  options  = list(pageLength = 14, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)

Section B – Data Handling

🎬 Oscar Films — Data Handling

Dimensi & Tipe Data Oscar

Dataset Oscar memiliki 87 baris dan 7 kolom. Kolom year, nominations, awards seharusnya bertipe integer — namun saat pertama diambil dari HTML, year bertipe string (karena diambil dari teks tag <a>). Perlu dikonversi dengan as.integer(). Kolom best_picture bertipe logical, kolom teks lainnya bertipe character.

Data Issue #1 — year Bertipe String

Kolom year dari scraping awal bertipe character (misal "2015"). Ini membuat pengurutan tidak bisa numerik — "9" secara alfabet lebih besar dari "2", sehingga urutan tahun bisa keliru. Perlu konversi as.integer(year) agar filter dan sort tahun berjalan benar.

Data Issue #2 — Kolom category Bersifat Derivatif

Kolom category dan description tidak tersedia langsung dari JSON response Oscar — harus dibuat manual dengan logika IF: best_picture==TRUE"Best Picture", awards>0"Award Winner", else → "Nominated". Nilai turunan seperti ini perlu didokumentasikan agar tidak dikira data asli dari sumber.

Missing Values & Duplikat Oscar

Missing values minimal karena data JSON dari Oscar sudah terstruktur rapi. Nilai kosong yang muncul (category kosong) langsung ditangani dengan default "Unknown" saat pengambilan. Duplikat bisa terjadi jika AJAX response mengembalikan film yang sama di dua tahun berbeda — dihapus dengan distinct().

🐢 Turtles — Data Handling

Dimensi & Tipe Data Turtles

Dataset Turtles memiliki 14 baris dan 2 kolom. Semua kolom bertipe character — tidak ada data kuantitatif karena ini adalah data klasifikasi taksonomi. Tidak ada kolom numerik yang perlu dikonversi, namun cleaning teks tetap diperlukan untuk konsistensi.

Data Issue #1 — Scraping Rentan Gagal

Koneksi ke URL iframe bisa gagal karena mekanisme anti-scraping, timeout, atau perubahan struktur HTML. Jika looping baris <tr class="family"> tidak menghasilkan data, seluruh dataset Turtles akan kosong. Ini adalah risiko scraping live yang harus diantisipasi dengan data fallback statis yang selalu tersedia.

Data Issue #2 — additional_info Redundan

Kolom additional_info dibuat dengan template: “The [name] family — commonly known as [description]”. Informasi ini sebenarnya tidak menambah data baru — hanya menyusun ulang dua kolom yang sudah ada. Namun kolom ini memenuhi syarat soal untuk menyertakan informasi tambahan dari setiap famili kura-kura.

Missing Values & Duplikat Turtles

Dataset Turtles tidak memiliki missing values karena data fallback selalu tersedia sebagai pengaman. Nilai "Unknown" dimasukkan eksplisit jika elemen HTML tidak ditemukan (via IF). Duplikat mungkin terjadi jika nama famili muncul dari scraping live sekaligus dari fallback — dihapus dengan distinct().

miss_o <- data.frame(
  Kolom   = names(df_oscar_raw),
  Tipe    = sapply(df_oscar_raw, function(x) class(x)[1]),
  Missing = colSums(is.na(df_oscar_raw)),
  Unique  = sapply(df_oscar_raw, function(x) length(unique(x))),
  row.names = NULL
)
DT::datatable(miss_o,
  caption = "📊 Tipe Data & Info Kolom — Oscar Films",
  options = list(dom='t', pageLength=10), rownames=FALSE, class="display compact")
miss_t <- data.frame(
  Kolom   = names(df_turtles_raw),
  Tipe    = sapply(df_turtles_raw, function(x) class(x)[1]),
  Missing = colSums(is.na(df_turtles_raw)),
  Unique  = sapply(df_turtles_raw, function(x) length(unique(x))),
  row.names = NULL
)
DT::datatable(miss_t,
  caption = "📊 Tipe Data & Info Kolom — Turtles",
  options = list(dom='t', pageLength=6), rownames=FALSE, class="display compact")

Section C – Data Cleaning

🧹 Cleaning Oscar: 87 film bersih  |  Cleaning Turtles: 14 famili bersih  |  Output: oscar_films.csv + turtles_iframe_data.csv
🎬 Oscar Films — Cleaning

LOOPING — Cleaning 3 Kolom Teks Sekaligus

oscar_text_cols <- c("title","category",
                     "description")
for (col in oscar_text_cols) {
  df_oscar[[col]] <- str_squish(
    str_trim(df_oscar[[col]]))
  if (col == "title")
    df_oscar[[col]] <- str_to_title(...)
  else
    df_oscar[[col]] <- str_to_sentence(...)
}

Tiga kolom dibersihkan dalam satu loop — efisien dan konsisten.

IF untuk Missing Value & Tipe Data

category atau description kosong → diisi "Unknown [kolom]" menggunakan if_else(). Kolom year, nominations, awards dikonversi ke integer agar operasi matematika valid. title diubah ke title case (nama film), category dan description ke sentence case agar konsisten.

Hapus Duplikat & Simpan CSV

Duplikat dihapus dengan distinct(). Data bersih disimpan: write.csv(df_oscar, "oscar_films.csv", row.names=FALSE). Ini adalah output CSV wajib #3 dari 4 yang ditentukan soal. File ini bisa langsung dibuka di Excel untuk verifikasi manual hasil scraping.

🐢 Turtles — Cleaning

LOOPING Semua Kolom (WAJIB)

for (col in names(df_turtles)) {
  if (is.character(df_turtles[[col]])) {
    df_turtles[[col]] <- str_squish(
      str_trim(df_turtles[[col]]))
    # IF untuk tiap kolom berbeda
    if (col == "name") {
      df_turtles[[col]] <- if_else(
        df_turtles[[col]] == "",
        "Unknown Turtle", ...)
    }
  }
}

Semua kolom character diproses dalam satu loop dengan IF di dalamnya.

IF-ELSE Default Berbeda per Kolom

name kosong → "Unknown Turtle" | description kosong → "No description available" | additional_info kosong → "No additional information". Setiap kolom punya nilai default yang berbeda sesuai konteks kolomnya — lebih informatif dibanding mengisi semua dengan NA atau string kosong.

Proper Case & Simpan CSV

name (nama ilmiah) dipertahankan dalam title case sesuai konvensi taksonomi. description diubah ke sentence case agar konsisten. Data bersih disimpan: write.csv(df_turtles, "turtles_iframe_data.csv", row.names=FALSE). Ini adalah output CSV wajib #4 dari 4 yang ditentukan soal.

DT::datatable(
  df_oscar |> select(any_of(c("year","title","nominations","awards","category","description","data_status"))),
  caption  = "🧹 Oscar Films — Setelah Cleaning",
  options  = list(pageLength = 8, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)
DT::datatable(
  df_turtles |> select(name, description, additional_info, data_status),
  caption  = "🧹 Turtles — Setelah Cleaning",
  options  = list(pageLength = 14, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)

Section D – Conditional Logic

data_status Oscar: Complete = 87  |  Incomplete = 0    |    data_status Turtles: Complete = 14  |  Incomplete = 0

Kondisi 1 — Elemen Tidak Ditemukan → Default

Jika elemen HTML tidak berhasil ditemukan saat scraping (misal <td class="family-name"> tidak ada di iframe, atau title film berisi "Unknown Title") → tandai data_status = "Incomplete". Ini berarti data tidak berhasil diambil dengan benar. Implementasi: if (!is.null(nm) && !is.na(nm)) html_text(nm) else "Unknown".

Kondisi 2 — Data Tidak Lengkap → “Incomplete”

Jika data kurang informatif — Oscar: nominations == 0 & awards == 0 (tidak ada data statistik sama sekali) | Turtles: description %in% c("Unknown","") (tidak ada nama umum) → tandai "Incomplete". Logika ini memastikan hanya data yang benar-benar bermakna yang lolos ke analisis final.

Kondisi 3 — Data Valid → “Complete”

Jika semua field terisi dengan benar (bukan placeholder, bukan kosong, bukan "Unknown") → tandai "Complete". Implementasi menggunakan case_when() dari dplyr: kondisi 1 dan 2 dicek lebih dulu, sisanya otomatis "Complete". Ini adalah pola if/else if/else yang diterapkan secara vektorial.

Kolom data_status sebagai Quality Gate

Kolom data_status berfungsi sebagai quality gate — filter otomatis kualitas data. Sebelum data digunakan untuk analisis lanjut, cukup filter data_status == "Complete". Ini jauh lebih andal dibanding memeriksa manual setiap kolom. Pola ini umum digunakan dalam pipeline data production di dunia industri.

DT::datatable(
  df_oscar |> select(any_of(c("year","title","nominations","awards","data_status"))) |>
    head(30),
  caption  = "✅ Oscar Films — Kolom data_status",
  options  = list(pageLength = 8, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
) |>
  DT::formatStyle("data_status",
    backgroundColor = DT::styleEqual(c("Complete","Incomplete"), c("#D1FAE5","#FEE2E2")))
DT::datatable(
  df_turtles |> select(name, description, additional_info, data_status),
  caption  = "✅ Turtles — Kolom data_status",
  options  = list(pageLength = 14, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
) |>
  DT::formatStyle("data_status",
    backgroundColor = DT::styleEqual(c("Complete","Incomplete"), c("#D1FAE5","#FEE2E2")))

Section E – Analytical Thinking

1. Website Paling Mudah di-Scrape?

Countries of the World (Static Page) adalah yang paling mudah. Semua data sudah ada di HTML saat halaman pertama dibuka — tidak ada JavaScript, tidak ada form, tidak ada halaman berikutnya. Cukup satu GET request dan semua data langsung bisa dibaca dengan html_table(). Seperti membaca buku yang sudah terbuka di halaman yang tepat — tidak ada kejutan.

2. Website Paling Sulit di-Scrape?

Oscar Winning Films (AJAX/JavaScript) adalah yang paling sulit. Data tidak ada di HTML awal — baru muncul setelah JavaScript berjalan di browser. Scraping langsung dengan requests menghasilkan halaman kosong. Perlu teknik khusus: temukan endpoint API tersembunyi via DevTools Inspect Network, atau gunakan Selenium untuk simulasi browser penuh yang lebih lambat dan berat.

3a. Perbedaan: Static vs Pagination

Static: data ada di HTML langsung saat halaman dibuka — satu GET request cukup, parsing langsung dengan html_table() atau html_nodes(). Tidak ada state yang perlu dikelola. Pagination: data tersebar di banyak halaman (page 1, 2, 3…). Perlu looping dengan parameter ?page=N sambil memantau kapan halaman terakhir — harus tahu kapan menghentikan loop agar tidak error 404.

3b. Perbedaan: AJAX vs iFrame

AJAX: data dimuat JavaScript setelah halaman terbuka melalui request tersembunyi ke endpoint API. Solusi terbaik: temukan URL endpoint via DevTools → request langsung ke URL tersebut untuk mendapat JSON murni. iFrame: konten ada di URL terpisah dalam tag <iframe>. Solusi: ambil atribut src dari iframe → scrape URL tersebut sebagai halaman mandiri. Keduanya butuh pemahaman cara browser bekerja di balik layar.

3 Insights dari Proses Scraping

(1) Kompleksitas ≠ volume data — website static dengan ribuan baris lebih mudah di-scrape dari website AJAX dengan puluhan baris. (2) Data hasil scraping hampir selalu perlu cleaning — spasi ekstra, kapitalisasi tidak konsisten, tipe data salah, dan missing values ditemukan di semua website yang di-scrape. (3) Looping adalah pondasi — tanpa looping, scraping multi-halaman dan multi-elemen tidak bisa dilakukan secara efisien.

2 Rekomendasi

(1) Cek endpoint AJAX sebelum pakai Selenium — jika URL API ditemukan lewat Inspect Network, datanya bisa diambil lebih cepat dan bersih dalam format JSON tanpa perlu simulasi browser. Selenium hanya digunakan jika endpoint tidak bisa ditemukan. (2) Selalu gunakan tryCatch() + Sys.sleep()tryCatch mencegah program berhenti total saat satu elemen tidak ditemukan, Sys.sleep memberi jeda agar server tidak memblokir request karena terlalu cepat.

if (nrow(df_oscar) > 0 && "year" %in% names(df_oscar) && "awards" %in% names(df_oscar)) {
  oscar_by_year <- df_oscar |>
    group_by(year) |>
    summarise(Total_Film = n(), Total_Awards = sum(awards, na.rm=TRUE), .groups="drop")
  
  p_o <- plot_ly(oscar_by_year, x = ~year, y = ~Total_Film,
    type = 'bar', name = 'Jumlah Film',
    marker = list(color = '#3D7EF0')) |>
    layout(
      title  = list(text = "🎬 Jumlah Film Oscar per Tahun", font=list(size=13)),
      xaxis  = list(title = "Tahun"),
      yaxis  = list(title = "Jumlah Film"),
      paper_bgcolor = 'rgba(0,0,0,0)',
      plot_bgcolor  = 'rgba(0,0,0,0)'
    )
  p_o
}
if (nrow(df_oscar) > 0 && "category" %in% names(df_oscar)) {
  cat_count <- df_oscar |> count(category, sort=TRUE)
  p_c <- plot_ly(cat_count, labels=~category, values=~n,
    type='pie', hole=0.4, textinfo='label+percent',
    marker=list(colors=c('#3D7EF0','#F59E0B','#0D9488'))) |>
    layout(title=list(text="🏆 Distribusi Kategori Film Oscar", font=list(size=13)),
           paper_bgcolor='rgba(0,0,0,0)')
  p_c
}

Referensi

📚 Daftar Referensi

[1] Wickham, H. (2022). rvest: Easily Harvest (Scrape) Web Pages. R package version 1.0.3. https://rvest.tidyverse.org/

[2] Bryan, J. & Posit team. (2023). googledrive: An Interface to Google Drive. https://googledrive.tidyverse.org/

[3] Ooms, J. (2023). jsonlite: A Simple and Robust JSON Parser for R. https://cran.r-project.org/package=jsonlite

[4] Wickham, H., François, R., Henry, L., & Müller, K. (2023). dplyr: A Grammar of Data Manipulation. https://dplyr.tidyverse.org/

[5] Wickham, H. et al. (2023). httr: Tools for Working with URLs and HTTP. https://httr.r-lib.org/

[6] Sievert, C. (2020). Interactive Web-Based Data Visualization with R, plotly, and shiny. Chapman and Hall/CRC. https://plotly-r.com/

[7] Xie, Y., Cheng, J., & Tan, X. (2023). DT: A Wrapper of the JavaScript Library DataTables. https://rstudio.github.io/DT/

[8] ScrapeThisSite.com. (2024). Oscar Winning Films — AJAX and Javascript [Dataset]. https://www.scrapethissite.com/pages/ajax-javascript/

---
title: "UAS — Pemrograman Sains Data I"
output:
  flexdashboard::flex_dashboard:
    css: newstyle1.css
    vertical_layout: scroll
    theme: yeti
    source_code: embed
---

```{r setup, include=FALSE}
# ── Auto-install packages ─────────────────────────────────────────
packages <- c(
  "flexdashboard", "tidyverse", "DT", "plotly",
  "httr", "rvest", "jsonlite", "stringr", "dplyr",
  "readxl", "knitr", "htmltools", "googledrive", "XML"
)
installed <- packages %in% rownames(installed.packages())
if (any(!installed)) install.packages(packages[!installed], repos="https://cloud.r-project.org")

suppressPackageStartupMessages({
  library(flexdashboard); library(tidyverse); library(DT); library(plotly)
  library(httr); library(rvest); library(jsonlite); library(stringr)
  library(dplyr); library(readxl); library(htmltools); library(googledrive)
  library(XML)
})
knitr::opts_chunk$set(echo = TRUE, warning = FALSE, message = FALSE)
```

```{r soal1-section-a, include=FALSE}
# ════════════════════════════════════════════════════════
#  SOAL 1 — SECTION A: DATA COLLECTION
#  Membaca 5 file dari Google Drive menggunakan looping
# ════════════════════════════════════════════════════════

# Autentikasi Google Drive tanpa browser (token cache)
# Di lingkungan non-interaktif gunakan drive_deauth()
drive_deauth()

# Link folder Google Drive yang diberikan
folder_url <- "https://drive.google.com/drive/folders/1m4xC40ovhcgLKgyIhrLywZaIhiow04vo"

# Daftar file yang akan dibaca (dengan format dan separator)
file_configs <- list(
  list(name = "ecommerce.csv",  format = "CSV",   sep = ","),
  list(name = "ecommerce.xlsx", format = "Excel",  sep = NULL),
  list(name = "ecommerce.json", format = "JSON",   sep = NULL),
  list(name = "ecommerce.txt",  format = "TXT",    sep = "|"),
  list(name = "ecommerce.xml",  format = "XML",    sep = NULL)
)

# ── Coba ambil dari Google Drive ─────────────────────────────────
file_results  <- list()          # menyimpan setiap df per file
merge_status  <- character(0)    # status per file
ref_cols      <- NULL            # kolom referensi dari file pertama
drive_success <- FALSE

tryCatch({
  # Ambil daftar file dari folder
  folder_id <- sub(".*folders/", "", folder_url)
  folder_files <- drive_ls(as_id(folder_id))
  drive_success <- nrow(folder_files) > 0
}, error = function(e) { drive_success <<- FALSE })

if (drive_success) {
  # ── LOOPING membaca file dari Drive ─────────────────────────────
  for (cfg in file_configs) {
    tryCatch({
      # Temukan file di Drive berdasarkan nama
      fmatch <- folder_files[folder_files$name == cfg$name, ]
      if (nrow(fmatch) == 0) stop(paste("File tidak ditemukan:", cfg$name))
      
      # Download ke tmp
      tmp_path <- file.path(tempdir(), cfg$name)
      drive_download(as_id(fmatch$id[1]), path = tmp_path, overwrite = TRUE)
      
      # Baca sesuai format — LOOPING dengan kondisi IF/IF-ELSE
      df_tmp <- if (cfg$format == "CSV") {
        read.csv(tmp_path, stringsAsFactors = FALSE)
      } else if (cfg$format == "Excel") {
        as.data.frame(read_excel(tmp_path))
      } else if (cfg$format == "JSON") {
        fromJSON(tmp_path, flatten = TRUE)
      } else if (cfg$format == "TXT") {
        read.table(tmp_path, header = TRUE, sep = cfg$sep,
                   stringsAsFactors = FALSE, fill = TRUE, quote = "")
      } else if (cfg$format == "XML") {
        # Robust XML parsing: coba berbagai node name
        xml_data <- tryCatch({
          xml_doc <- xmlParse(tmp_path)
          # Coba node record / Record / row / item / data
          node_names <- c("record","Record","row","Row","item","Item","data","Data")
          parsed_df  <- NULL
          for (nd_nm in node_names) {
            nodes <- getNodeSet(xml_doc, paste0("//", nd_nm))
            if (length(nodes) > 0) {
              parsed_df <- xmlToDataFrame(nodes = nodes)
              break
            }
          }
          # Jika semua gagal, ambil semua child root
          if (is.null(parsed_df)) {
            root <- xmlRoot(xml_doc)
            child_list <- xmlChildren(root)
            parsed_df <- do.call(rbind, lapply(child_list, function(nd) {
              ch <- xmlChildren(nd)
              vals <- sapply(ch, xmlValue)
              as.data.frame(as.list(vals), stringsAsFactors = FALSE)
            }))
          }
          parsed_df
        }, error = function(e) {
          message("XML error: ", e$message, " -> menggunakan data sintetis XML")
          make_synthetic(seed_offset = 4)
        })
        xml_data
      }
      df_tmp <- as.data.frame(df_tmp, stringsAsFactors = FALSE)
      
      # Simpan info file
      if (is.null(ref_cols)) ref_cols <- sort(names(df_tmp))
      
      # ── IF-ELSE cek struktur kolom ──────────────────────────────
      status_msg <- if (identical(sort(names(df_tmp)), ref_cols)) {
        "✅ Ready to merge"
      } else {
        "⚠️ Need adjustment"
      }
      
      file_results[[cfg$name]] <- list(
        df     = df_tmp,
        format = cfg$format,
        rows   = nrow(df_tmp),
        cols   = ncol(df_tmp),
        colnames = names(df_tmp),
        status = status_msg
      )
      merge_status <- c(merge_status, status_msg)
      
    }, error = function(e) {
      merge_status <<- c(merge_status, paste("❌ Error:", e$message))
    })
  }
}

# ── Fallback: buat data sintetis jika Drive tidak bisa diakses ───
# (digunakan saat publish RPubs tanpa autentikasi Drive)
make_synthetic <- function(seed_offset = 0, n = 20) {
  set.seed(42 + seed_offset)
  platforms <- c("shopee", "SHOPEE", " SHOPEE ", "tokped", "Tokopedia",
                 "lazada", "Lazada", "tiktok shop", "TikTok Shop", "blibli")
  statuses  <- c("delivered", "cancelled", "on delivery", "processing", "returned", "Completed")
  payments  <- c("Transfer Bank", "COD", "E-Wallet", "Kartu Kredit", NA)
  cats      <- c("Fashion","Fashion","Fashion","Electronics","Home Living","Beauty","Sports","Books","Toys")
  
  unit_p <- round(runif(n, 20000, 800000), -3)
  qty    <- sample(1:10, n, replace = TRUE)
  disc   <- round(runif(n, 0, 0.3), 2)
  gross  <- unit_p * qty
  net    <- round(gross * (1 - disc))
  
  data.frame(
    order_id        = paste0("ORD", sprintf("%04d", (seed_offset*20+1):(seed_offset*20+n))),
    order_date      = sample(seq.Date(as.Date("2024-01-01"), as.Date("2024-12-31"), by="day"), n),
    product_name    = paste0("Produk-", (seed_offset*20+1):(seed_offset*20+n)),
    category        = sample(cats, n, replace = TRUE),
    platform        = sample(platforms, n, replace = TRUE),
    unit_price      = ifelse(runif(n) < 0.6,
                             paste0("Rp ", formatC(unit_p, format="d", big.mark=".")),
                             as.character(unit_p)),
    quantity        = qty,
    gross_sales     = ifelse(runif(n) < 0.6,
                             paste0("Rp ", formatC(gross, format="d", big.mark=".")),
                             as.character(gross)),
    net_sales       = ifelse(runif(n) < 0.6,
                             paste0("Rp ", formatC(net, format="d", big.mark=".")),
                             as.character(net)),
    order_status    = sample(statuses, n, replace = TRUE),
    payment_method  = sample(payments, n, replace = TRUE, prob = c(.25,.2,.25,.2,.1)),
    customer_rating = sample(c(1:5, NA), n, replace = TRUE,
                             prob = c(.08,.12,.2,.3,.2,.1)),
    customer_segment= sample(c("Regular","Premium","VIP","New Customer"), n, replace=TRUE),
    region          = sample(c("Jawa","Sumatera","Kalimantan","Sulawesi","Bali"), n, replace=TRUE),
    is_returned     = sample(c(FALSE,TRUE), n, replace=TRUE, prob=c(.88,.12)),
    discount        = disc,
    stringsAsFactors = FALSE
  )
}

# ── Jika Drive gagal, gunakan data sintetis ──────────────────────
fmt_names <- c("ecommerce.csv","ecommerce.xlsx","ecommerce.json",
               "ecommerce.txt","ecommerce.xml")
fmt_labels <- c("CSV","Excel","JSON","TXT","XML")

if (length(file_results) == 0) {
  ref_cols <- NULL
  for (i in seq_along(fmt_names)) {
    df_tmp   <- make_synthetic(seed_offset = i-1)
    if (is.null(ref_cols)) ref_cols <- sort(names(df_tmp))
    status_msg <- if (identical(sort(names(df_tmp)), ref_cols)) "✅ Ready to merge" else "⚠️ Need adjustment"
    file_results[[fmt_names[i]]] <- list(
      df = df_tmp, format = fmt_labels[i],
      rows = nrow(df_tmp), cols = ncol(df_tmp),
      colnames = names(df_tmp), status = status_msg
    )
    merge_status <- c(merge_status, status_msg)
  }
}

# ── Gabungkan file yang Ready to merge ───────────────────────────
ready_dfs <- lapply(file_results, function(x) {
  if (grepl("Ready", x$status)) x$df else NULL
})
ready_dfs <- Filter(Negate(is.null), ready_dfs)

df_combined_raw <- if (length(ready_dfs) > 0) {
  tryCatch(bind_rows(ready_dfs), error = function(e) do.call(rbind, ready_dfs))
} else {
  do.call(rbind, lapply(file_results, function(x) x$df))
}
df_combined_raw <- as.data.frame(df_combined_raw, stringsAsFactors = FALSE)

# ── Tabel ringkasan Section A ─────────────────────────────────────
tbl_sectionA <- data.frame(
  File   = fmt_names,
  Format = fmt_labels,
  Baris  = sapply(file_results, function(x) x$rows),
  Kolom  = sapply(file_results, function(x) x$cols),
  Status = merge_status,
  stringsAsFactors = FALSE,
  row.names = NULL
)
```

```{r soal1-section-b, include=FALSE}
# ════════════════════════════════════════════════════════
#  SOAL 1 — SECTION B: DATA HANDLING
# ════════════════════════════════════════════════════════

# Dimensi dataset
total_rows <- nrow(df_combined_raw)
total_cols <- ncol(df_combined_raw)

# Tipe data setiap kolom
col_types <- data.frame(
  Kolom   = names(df_combined_raw),
  Tipe    = sapply(df_combined_raw, function(x) class(x)[1]),
  row.names = NULL
)

# Missing values (diperbaiki: gunakan is.na() saja, tidak bandingkan dengan "")
# Karena df_combined_raw tidak punya string kosong, missing hanya dari NA
missing_counts <- colSums(is.na(df_combined_raw))

missing_df <- data.frame(
  Kolom      = names(missing_counts),
  Jumlah_NA  = as.integer(missing_counts),
  Persen_NA  = round(missing_counts / nrow(df_combined_raw) * 100, 2),
  row.names  = NULL
) |> filter(Jumlah_NA > 0) |> arrange(desc(Jumlah_NA))

# Duplikat
dup_count <- sum(duplicated(df_combined_raw))
dup_pct   <- round(dup_count / nrow(df_combined_raw) * 100, 2)

# Info tipe per kolom lengkap
col_info <- data.frame(
  Kolom   = names(df_combined_raw),
  Tipe    = sapply(df_combined_raw, function(x) class(x)[1]),
  Missing = missing_counts,
  Unique  = sapply(df_combined_raw, function(x) length(unique(x))),
  row.names = NULL
)
```

```{r soal1-section-c, include=FALSE}
# ════════════════════════════════════════════════════════
#  SOAL 1 — SECTION C: DATA CLEANING
# ════════════════════════════════════════════════════════

df_clean <- df_combined_raw

# ── C.1: Hapus duplikat ──────────────────────────────────────────
df_clean <- df_clean[!duplicated(df_clean), ]

# ── C.2: LOOPING — trim + lowercase kolom teks ──────────────────
# (Wajib Looping — membersihkan minimal 3 kolom sekaligus)
text_cols <- c("product_name", "category", "platform",
               "order_status", "payment_method", "customer_segment", "region")
# Hanya ambil kolom yang benar-benar ada di df_clean
text_cols <- text_cols[text_cols %in% names(df_clean)]

for (col in text_cols) {                                   # LOOPING WAJIB
  df_clean[[col]] <- str_trim(as.character(df_clean[[col]]))
  df_clean[[col]] <- tolower(df_clean[[col]])
}

# ── C.3: Standardisasi Platform (WAJIB IF) ───────────────────────
if ("platform" %in% names(df_clean)) {
  standardisasi_platform <- function(nilai) {
    if (is.na(nilai)) return("Unknown")
    val <- str_trim(tolower(nilai))
    if      (val == "shopee")                            return("Shopee")
    else if (val %in% c("tokopedia", "tokped"))          return("Tokopedia")
    else if (val == "lazada")                            return("Lazada")
    else if (val %in% c("tiktok shop", "tiktokshop"))   return("TikTok Shop")
    else if (val %in% c("blibli"))                       return("Blibli")
    else return(str_trim(nilai))
  }
  df_clean$platform <- sapply(df_clean$platform, standardisasi_platform)
  
  cat("\n=== Distribusi Platform Setelah Cleaning ===\n")
  print(table(df_clean$platform))
}

# ── C.4: Cleaning Nilai Harga ────────────────────────────────────
bersihkan_harga <- function(nilai) {
  if (is.na(nilai)) return(0L)
  val_str <- as.character(nilai)
  val_str <- str_remove_all(val_str, "Rp\\s*|\\.|,")
  angka   <- suppressWarnings(as.integer(as.numeric(val_str)))
  if (is.na(angka)) angka <- 0L
  if (angka < 0L) angka <- 0L
  return(angka)
}

# Hanya proses kolom yang ada di dataframe
kolom_harga <- c("discount_value", "net_sales", "gross_sales", "unit_price", "shipping_cost")
kolom_harga_ada <- intersect(kolom_harga, names(df_clean))

for (kol in kolom_harga_ada) {                             # LOOPING WAJIB
  df_clean[[kol]] <- sapply(df_clean[[kol]], bersihkan_harga)
}

# Pastikan kolom numerik ada (buat jika tidak ada)
if (!"net_sales"   %in% names(df_clean)) df_clean$net_sales   <- 0L
if (!"gross_sales" %in% names(df_clean)) df_clean$gross_sales <- 0L
if (!"unit_price"  %in% names(df_clean)) df_clean$unit_price  <- 0L

# ── C.5: Handling Missing Values (WAJIB IF) ─────────────────────
# payment_method: isi dengan "Unknown"
if ("payment_method" %in% names(df_clean)) {
  df_clean$payment_method <- ifelse(
    is.na(df_clean$payment_method) | df_clean$payment_method == "",
    "Unknown",
    df_clean$payment_method
  )
}

# customer_rating: isi dengan median
if ("customer_rating" %in% names(df_clean)) {
  df_clean$customer_rating <- as.numeric(df_clean$customer_rating)
  df_clean$customer_rating[df_clean$customer_rating < 1 | df_clean$customer_rating > 5] <- NA
  median_rating <- median(df_clean$customer_rating, na.rm = TRUE)
  if (is.na(median_rating)) median_rating <- 3.0
  df_clean$customer_rating <- ifelse(
    is.na(df_clean$customer_rating),
    median_rating,
    df_clean$customer_rating
  )
  cat(sprintf("\nMedian rating digunakan sebagai default: %.1f\n", median_rating))
}

# ── C.6: Standardisasi Order Status ─────────────────────────────
if ("order_status" %in% names(df_clean)) {
  standardisasi_order_status <- function(nilai) {
    if (is.na(nilai)) return("Unknown")
    val <- str_trim(tolower(as.character(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 if (val == "processing")                            return("Processing")
    else return(str_trim(nilai))
  }
  df_clean$order_status <- sapply(df_clean$order_status, standardisasi_order_status)
}

# ── C.7: LOOPING — Proper case kolom teks akhir ─────────────────
proper_cols <- c("product_name", "category", "customer_segment", "region")
proper_cols <- proper_cols[proper_cols %in% names(df_clean)]

for (col in proper_cols) {
  df_clean[[col]] <- str_to_title(str_trim(df_clean[[col]]))
}

# ── C.8: Konversi tipe data ──────────────────────────────────────
# order_date
if ("order_date" %in% names(df_clean)) {
  df_clean$order_date <- suppressWarnings(as.Date(df_clean$order_date))
}

# quantity
if ("quantity" %in% names(df_clean)) {
  df_clean$quantity <- suppressWarnings(as.integer(df_clean$quantity))
  df_clean$quantity[is.na(df_clean$quantity) | df_clean$quantity <= 0] <- 1L
}

# is_returned (cek dulu apakah kolomnya ada)
if ("is_returned" %in% names(df_clean)) {
  df_clean$is_returned <- as.logical(df_clean$is_returned)
}

# discount
if ("discount" %in% names(df_clean)) {
  df_clean$discount <- suppressWarnings(as.numeric(df_clean$discount))
  df_clean$discount[is.na(df_clean$discount)] <- 0
}

# Urutkan berdasarkan tanggal
if ("order_date" %in% names(df_clean)) {
  df_clean <- df_clean[order(df_clean$order_date, na.last = TRUE), ]
}
rownames(df_clean) <- NULL

# Rebuild kolom numerik (hanya jika kolomnya ada)
for (kol in c("net_sales", "gross_sales", "unit_price")) {
  if (kol %in% names(df_clean)) {
    df_clean[[kol]] <- sapply(df_clean[[kol]], bersihkan_harga)
  }
}

cat("\n✅ Section C — Data Cleaning selesai\n")
cat("📊 Dimensi df_clean:", nrow(df_clean), "baris x", ncol(df_clean), "kolom\n")
```

```{r soal1-section-d, include=FALSE}
# ════════════════════════════════════════════════════════
#  SOAL 1 — SECTION D: CONDITIONAL LOGIC
# ════════════════════════════════════════════════════════

# 1. is_high_value: net_sales > 1.000.000 → "Yes", selain itu → "No"
df_clean$is_high_value <- ifelse(df_clean$net_sales > 1000000, "Yes", "No")

# 2. order_priority (WAJIB nested IF)
order_priority_fn <- function(x) {
  if      (is.na(x))     return("Unknown")
  else if (x > 1000000)  return("High")
  else if (x >= 500000)  return("Medium")
  else                   return("Low")
}
df_clean$order_priority <- sapply(df_clean$net_sales, order_priority_fn)

# 3. valid_transaction: Cancelled → "Invalid", selain itu → "Valid"
df_clean$valid_transaction <- ifelse(df_clean$order_status == "Cancelled", "Invalid", "Valid")
```

```{r soal1-section-e, include=FALSE}
# ════════════════════════════════════════════════════════
#  SOAL 1 — SECTION E: ANALYTICAL THINKING
# ════════════════════════════════════════════════════════

platform_count <- df_clean |> count(platform, sort = TRUE) |> rename(Jumlah = n)
category_count <- df_clean |> count(category, sort = TRUE) |> rename(Jumlah = n)
status_count   <- df_clean |> count(order_status, sort = TRUE) |> rename(Jumlah = n)

platform_dominant <- platform_count$platform[1]
platform_dom_n    <- platform_count$Jumlah[1]
category_top      <- category_count$category[1]
category_top_n    <- category_count$Jumlah[1]
status_top        <- status_count$order_status[1]
status_top_n      <- status_count$Jumlah[1]
status_top_pct    <- round(status_top_n / sum(status_count$Jumlah) * 100, 1)

platform_rev <- df_clean |>
  group_by(platform) |>
  summarise(
    Total_Transaksi  = n(),
    Total_Revenue    = sum(net_sales, na.rm = TRUE),
    Avg_Rating       = round(mean(customer_rating, na.rm = TRUE), 2),
    .groups = "drop"
  ) |>
  arrange(desc(Total_Revenue))
```

```{r soal2-scraping, include=FALSE}
# ════════════════════════════════════════════════════════
#  SOAL 2 — SECTION A: WEB SCRAPING
#  Website 3 (Oscar AJAX) & Website 4 (Turtles iFrame)
#  menggunakan R — rvest + httr + jsonlite
# ════════════════════════════════════════════════════════

# ── Website 3: Oscar Winning Films (AJAX/Javascript) ─────────────
url_oscar <- "https://www.scrapethissite.com/pages/ajax-javascript/"
oscar_data <- list()

tryCatch({
  resp_main <- GET(url_oscar, timeout(15))
  pg        <- read_html(content(resp_main, "text"))
  years     <- html_nodes(pg, "a.year-link") |> html_text() |> str_trim()
  
  # WAJIB LOOPING — iterasi setiap tahun Oscar
  for (yr in years) {
    tryCatch({
      r <- GET(url_oscar, query = list(ajax = "true", year = yr), timeout(10))
      if (status_code(r) == 200) {
        films <- fromJSON(content(r, "text", encoding = "UTF-8"))
        if (is.data.frame(films) && nrow(films) > 0) {
          films$year        <- as.integer(yr)
          # IF untuk menentukan category (WAJIB IF)
          films$category <- sapply(seq_len(nrow(films)), function(i) {
            if      (isTRUE(films$best_picture[i]))     "Best Picture"
            else if (!is.na(films$awards[i]) && films$awards[i] > 0) "Award Winner"
            else                                        "Nominated"
          })
          films$description <- paste0(
            "Film tahun ", yr, " dengan ",
            films$nominations, " nominasi dan ",
            films$awards, " penghargaan Oscar."
          )
          oscar_data <- append(oscar_data, list(films))
        }
      }
      Sys.sleep(0.2)
    }, error = function(e) NULL)
  }
}, error = function(e) NULL)

if (length(oscar_data) > 0) {
  df_oscar_raw <- bind_rows(oscar_data) |>
    select(any_of(c("year","title","nominations","awards","best_picture","category","description"))) |>
    mutate(
      year        = as.integer(year),
      nominations = as.integer(nominations),
      awards      = as.integer(awards),
      title       = str_to_title(str_trim(title)),
      # IF untuk missing value (WAJIB IF)
      category    = if_else(is.na(category) | category == "", "Unknown", category),
      description = if_else(is.na(description), "No description", description)
    ) |>
    distinct()
} else {
  # Fallback Oscar (data statis representatif)
  df_oscar_raw <- data.frame(
    year        = rep(2010:2023, each = 5),
    title       = paste0("Film-", 1:70),
    nominations = sample(1:13, 70, replace = TRUE),
    awards      = sample(0:11, 70, replace = TRUE),
    best_picture= c(TRUE, rep(FALSE,4), TRUE, rep(FALSE,4), TRUE, rep(FALSE,4),
                    TRUE, rep(FALSE,4), TRUE, rep(FALSE,4), TRUE, rep(FALSE,4),
                    TRUE, rep(FALSE,4), TRUE, rep(FALSE,4), TRUE, rep(FALSE,4),
                    TRUE, rep(FALSE,4), TRUE, rep(FALSE,4), TRUE, rep(FALSE,4),
                    TRUE, rep(FALSE,4), TRUE, rep(FALSE,4)),
    category    = c("Best Picture","Award Winner","Nominated","Nominated","Award Winner"),
    description = paste0("Film nominasi Oscar tahun ", rep(2010:2023, each=5)),
    stringsAsFactors = FALSE
  )
}

# ── Website 4: Turtles All the Way Down (Frames & iFrames) ───────
url_turtles <- "https://www.scrapethissite.com/pages/frames/"
base_url_t  <- "https://www.scrapethissite.com"
turtle_rows <- list()

tryCatch({
  rp <- GET(url_turtles, timeout(15))
  pg <- read_html(content(rp, "text"))
  fr <- html_node(pg, "iframe#iframe")
  
  # WAJIB — masuk ke dalam iframe
  iframe_src <- if (!is.na(fr)) html_attr(fr, "src") else
    "/pages/frames/iframe/?family=Chelydridae"
  iframe_url <- paste0(base_url_t, iframe_src)
  
  ip    <- read_html(content(GET(iframe_url, timeout(12)), "text"))
  frows <- html_nodes(ip, "tr.family")
  
  # WAJIB LOOPING — iterasi baris di dalam iframe
  for (row in frows) {
    nm  <- html_node(row, "td.family-name")
    dsc <- html_node(row, "td.family-common-name")
    
    # IF untuk nilai default jika elemen tidak ditemukan (WAJIB IF)
    nama_val <- if (!is.null(nm) && !is.na(nm))  str_trim(html_text(nm))  else "Unknown"
    desc_val <- if (!is.null(dsc) && !is.na(dsc)) str_trim(html_text(dsc)) else "Unknown"
    
    turtle_rows <- append(turtle_rows, list(data.frame(
      name        = nama_val,
      description = desc_val,
      stringsAsFactors = FALSE
    )))
  }
}, error = function(e) NULL)

# Data fallback 14 famili kura-kura
fallback_turtles <- data.frame(
  name = c("Cheloniidae","Carettochelyidae","Dermochelyidae","Chelydridae","Emydidae",
           "Geoemydidae","Kinosternidae","Pelomedusidae","Podocnemididae","Staurotypidae",
           "Testudinidae","Trionychidae","Platysternidae","Chelidae"),
  description = c("Sea turtles","Pig-nosed turtle","Leatherback turtle","Snapping turtle",
                  "Pond turtle","Asian river turtle","Mud turtle","Afro-American sideneck",
                  "Madagascan big-headed turtle","Giant musk turtle","Tortoise",
                  "Softshell turtle","Big-headed turtle","Austro-American sideneck"),
  stringsAsFactors = FALSE
)

df_turtles_raw <- if (length(turtle_rows) > 0) {
  bind_rows(turtle_rows) |> distinct()
} else {
  fallback_turtles
}

# Tambah data fallback jika ada yang kurang
df_turtles_raw <- bind_rows(
  df_turtles_raw,
  fallback_turtles[!fallback_turtles$name %in% df_turtles_raw$name, ]
) |> distinct()
```

```{r soal2-cleaning, include=FALSE}
# ════════════════════════════════════════════════════════
#  SOAL 2 — SECTION C: DATA CLEANING
# ════════════════════════════════════════════════════════

# ── Cleaning Oscar ────────────────────────────────────────────────
df_oscar <- df_oscar_raw

# LOOPING — cleaning kolom teks Oscar sekaligus
oscar_text_cols <- c("title", "category", "description")
for (col in oscar_text_cols) {                             # LOOPING WAJIB
  df_oscar[[col]] <- str_squish(str_trim(df_oscar[[col]]))
  # IF-ELSE untuk kapitalisasi berbeda per kolom
  if (col == "title") {
    df_oscar[[col]] <- str_to_title(df_oscar[[col]])
  } else {
    df_oscar[[col]] <- str_to_sentence(df_oscar[[col]])
  }
  # IF untuk missing value (WAJIB IF)
  df_oscar[[col]] <- if_else(
    is.na(df_oscar[[col]]) | df_oscar[[col]] == "",
    paste0("Unknown ", col),
    df_oscar[[col]]
  )
}
df_oscar <- distinct(df_oscar)

# Tambah kolom data_status (Section D)
df_oscar <- df_oscar |> mutate(
  data_status = case_when(
    title == "Unknown Title" | is.na(title)       ~ "Incomplete",  # Kondisi 1
    nominations == 0L & awards == 0L              ~ "Incomplete",  # Kondisi 2
    TRUE                                           ~ "Complete"    # Kondisi 3
  )
)

# ── Cleaning Turtles ──────────────────────────────────────────────
df_turtles <- df_turtles_raw

# LOOPING — cleaning semua kolom teks Turtles
for (col in names(df_turtles)) {                           # LOOPING WAJIB
  if (is.character(df_turtles[[col]])) {
    df_turtles[[col]] <- str_squish(str_trim(df_turtles[[col]]))
    # IF untuk default value jika kosong (WAJIB IF)
    if (col == "name") {
      df_turtles[[col]] <- if_else(
        df_turtles[[col]] == "" | is.na(df_turtles[[col]]),
        "Unknown Turtle", df_turtles[[col]])
    } else if (col == "description") {
      df_turtles[[col]] <- if_else(
        df_turtles[[col]] == "" | is.na(df_turtles[[col]]),
        "No description available", df_turtles[[col]])
    }
  }
}

df_turtles <- df_turtles |> distinct() |>
  mutate(
    additional_info = paste0(
      "The ", name, " family — commonly known as \"", description, "\"."
    ),
    # Section D: data_status
    data_status = case_when(
      name        %in% c("Unknown","Unknown Turtle","")      ~ "Incomplete",  # Kondisi 1
      description %in% c("Unknown","No description available","") ~ "Incomplete", # Kondisi 2
      TRUE                                                    ~ "Complete"   # Kondisi 3
    )
  )

# Simpan ke CSV (output wajib)
tryCatch(write.csv(df_oscar,   "oscar_films.csv",        row.names = FALSE), error = function(e) NULL)
tryCatch(write.csv(df_turtles, "turtles_iframe_data.csv", row.names = FALSE), error = function(e) NULL)
```


Members {data-orientation=rows}
=======================================================================

```{r profile-cards, echo=FALSE}
HTML('
<div style="display:flex;flex-wrap:wrap;justify-content:center;gap:28px;padding:40px 20px;
background:linear-gradient(135deg,#0F172A 0%,#1E293B 50%,#0F172A 100%);
border-radius:24px;margin:20px 0;">

  <div style="flex:1;min-width:260px;max-width:300px;background:linear-gradient(135deg,#F0F4FF,#E8EEFF);
  border-radius:24px;overflow:hidden;box-shadow:0 16px 32px rgba(26,39,68,0.15);border:1px solid rgba(61,126,240,0.2);">
    <div style="background:linear-gradient(135deg,#1A2744,#2E4480);padding:22px 18px 14px;text-align:center;">
      <div style="width:110px;height:110px;margin:0 auto;border-radius:50%;background:#fff;padding:4px;">
        <img src="https://raw.githubusercontent.com/raihaniasyahputri/Foto_lulu/09392fb4c08cd80d3e2004a69ee313d703cb1664/Foto_lulu.jpg"
          style="width:102px;height:102px;border-radius:50%;object-fit:cover;border:3px solid #3D7EF0;" onerror="this.style.display=\'none\'">
      </div>
      <h3 style="color:#fff;margin:10px 0 4px;font-size:17px;font-weight:700;">Lulu Najla Salsabila</h3>
      <p style="color:#8AAEEF;margin:0;font-size:11px;letter-spacing:0.5px;">🌸 DATA SCIENCE STUDENT</p>
    </div>
    <div style="padding:16px;">
      <div style="margin-bottom:8px;background:rgba(29,78,216,0.08);padding:8px 12px;border-radius:12px;font-size:13px;">
        🎫 NIM: <strong style="color:#1E40AF;">52250069</strong>
      </div>
      <div style="margin-bottom:8px;background:rgba(29,78,216,0.08);padding:8px 12px;border-radius:12px;font-size:13px;">
        🏛️ Institut Teknologi Sains Bandung
      </div>
      <div style="background:rgba(29,78,216,0.08);padding:8px 12px;border-radius:12px;font-size:13px;">
        👨‍🏫 Bakti Siregar, M.Sc., CDS.
      </div>
    </div>
  </div>

  <div style="flex:1;min-width:260px;max-width:300px;background:linear-gradient(135deg,#F0F4FF,#E8EEFF);
  border-radius:24px;overflow:hidden;box-shadow:0 16px 32px rgba(26,39,68,0.15);border:1px solid rgba(61,126,240,0.2);">
    <div style="background:linear-gradient(135deg,#1A2744,#2E4480);padding:22px 18px 14px;text-align:center;">
      <div style="width:110px;height:110px;margin:0 auto;border-radius:50%;background:#fff;padding:4px;">
        <img src="https://raw.githubusercontent.com/raihaniasyahputri/Foto_lulu/09392fb4c08cd80d3e2004a69ee313d703cb1664/Foto_aurora.png"
          style="width:102px;height:102px;border-radius:50%;object-fit:cover;border:3px solid #3D7EF0;" onerror="this.style.display=\'none\'">
      </div>
      <h3 style="color:#fff;margin:10px 0 4px;font-size:17px;font-weight:700;">Aurora Sekarningrum</h3>
      <p style="color:#8AAEEF;margin:0;font-size:11px;letter-spacing:0.5px;">✨ DATA SCIENCE STUDENT</p>
    </div>
    <div style="padding:16px;">
      <div style="margin-bottom:8px;background:rgba(29,78,216,0.08);padding:8px 12px;border-radius:12px;font-size:13px;">
        🎫 NIM: <strong style="color:#1E40AF;">52250072</strong>
      </div>
      <div style="margin-bottom:8px;background:rgba(29,78,216,0.08);padding:8px 12px;border-radius:12px;font-size:13px;">
        🏛️ Institut Teknologi Sains Bandung
      </div>
      <div style="background:rgba(29,78,216,0.08);padding:8px 12px;border-radius:12px;font-size:13px;">
        👨‍🏫 Bakti Siregar, M.Sc., CDS.
      </div>
    </div>
  </div>

  <div style="flex:1;min-width:260px;max-width:300px;background:linear-gradient(135deg,#F0F4FF,#E8EEFF);
  border-radius:24px;overflow:hidden;box-shadow:0 16px 32px rgba(26,39,68,0.15);border:1px solid rgba(61,126,240,0.2);">
    <div style="background:linear-gradient(135deg,#1A2744,#2E4480);padding:22px 18px 14px;text-align:center;">
      <div style="width:110px;height:110px;margin:0 auto;border-radius:50%;background:#fff;padding:4px;">
        <img src="https://raw.githubusercontent.com/Raihaniaputri/Foto_formal/c5d0ed71cd963019cfb7eb620c52806210cf3ecb/Foto_formal.jpg"
          style="width:102px;height:102px;border-radius:50%;object-fit:cover;border:3px solid #3D7EF0;" onerror="this.style.display=\'none\'">
      </div>
      <h3 style="color:#fff;margin:10px 0 4px;font-size:17px;font-weight:700;">Raihania Syah Putri</h3>
      <p style="color:#8AAEEF;margin:0;font-size:11px;letter-spacing:0.5px;">⚡ DATA SCIENCE STUDENT</p>
    </div>
    <div style="padding:16px;">
      <div style="margin-bottom:8px;background:rgba(29,78,216,0.08);padding:8px 12px;border-radius:12px;font-size:13px;">
        🎫 NIM: <strong style="color:#1E40AF;">52250054</strong>
      </div>
      <div style="margin-bottom:8px;background:rgba(29,78,216,0.08);padding:8px 12px;border-radius:12px;font-size:13px;">
        🏛️ Institut Teknologi Sains Bandung
      </div>
      <div style="background:rgba(29,78,216,0.08);padding:8px 12px;border-radius:12px;font-size:13px;">
        👨‍🏫 Bakti Siregar, M.Sc., CDS.
      </div>
    </div>
  </div>

</div>
')
```


Soal 1 — E-Commerce {data-orientation=rows}
=======================================================================

## Column {.tabset .tabset-fade data-height=700}
-----------------------------------------------------------------------

### Section A – Data Collection

```{r badge-drive, echo=FALSE}
HTML(paste0('
<div class="section-badge">
  📁 <strong>Sumber Data:</strong> Google Drive Folder — 5 file (CSV, Excel, JSON, TXT, XML)
  &nbsp;|&nbsp; Total baris gabungan: <strong>', nrow(df_combined_raw), ' baris</strong>
  &nbsp;|&nbsp; <strong>', ncol(df_combined_raw), ' kolom</strong>
</div>'))
```

<div class="flex-container">
<div class="small-box" data-index="1">

**Membaca 5 File Berbeda (LOOPING)**

Program menggunakan **looping** (`for (cfg in file_configs)`) untuk membaca 5 file sekaligus dari Google Drive: `ecommerce.csv`, `ecommerce.xlsx`, `ecommerce.json`, `ecommerce.txt` (sep=`|`), dan `ecommerce.xml`. Setiap file dibaca dengan fungsi yang sesuai formatnya — `read.csv()`, `read_excel()`, `fromJSON()`, `read.table()`, `xmlToDataFrame()`. Tanpa looping, kita harus menulis kode baca file 5 kali secara manual.

</div>
<div class="small-box" data-index="2">

**Informasi Setiap File yang Ditampilkan**

Untuk **setiap file**, program menampilkan tiga informasi wajib: (1) **Jumlah baris** — seberapa banyak data transaksi, (2) **Jumlah kolom** — seberapa banyak atribut, (3) **Nama kolom** — antara lain `order_id`, `order_date`, `product_name`, `category`, `platform`, `unit_price`, `quantity`, `gross_sales`, `net_sales`, `order_status`, `payment_method`, `customer_rating`, `customer_segment`, `region`, `is_returned`, `discount`.

</div>
<div class="small-box" data-index="3">

**Cek Struktur Kolom (IF / IF-ELSE)**

Setelah setiap file dibaca, program mengecek dengan **IF-ELSE**: apakah kolom-kolomnya identik dengan file pertama (referensi)? Jika `identical(sort(names(df_tmp)), ref_cols)` → cetak **"✅ Ready to merge"**. Jika berbeda → cetak **"⚠️ Need adjustment"**. Ini memastikan hanya file yang kompatibel yang digabung ke dataset utama.

</div>
<div class="small-box" data-index="4">

**Gabungkan Jadi 1 Dataset Utama**

Semua file yang berstatus **"Ready to merge"** digabungkan menggunakan `bind_rows()` (R) menjadi satu DataFrame utama bernama `df_combined_raw`. Hasilnya: **`r nrow(df_combined_raw)` baris** dari **`r length(file_results)` file** format berbeda — siap dianalisis dan dibersihkan di section berikutnya.

</div>
<div class="small-box" data-index="5">

**Autentikasi Google Drive**

Untuk membaca file dari Google Drive, digunakan `googledrive::drive_deauth()` agar bisa mengakses folder publik tanpa login interaktif. File diunduh satu per satu ke direktori sementara (`tempdir()`) menggunakan `drive_download()`, lalu dibaca sesuai formatnya. Jika koneksi Drive gagal, data sintetis berstruktur sama digunakan sebagai fallback agar kode tetap berjalan.

</div>
</div>

```{r tbl-section-a}
DT::datatable(
  tbl_sectionA,
  caption  = "📁 Ringkasan Pembacaan 5 File dari Google Drive",
  options  = list(dom = 't', pageLength = 10, scrollX = TRUE),
  rownames = FALSE,
  class    = "display compact"
) |>
  DT::formatStyle("Status",
    backgroundColor = DT::styleEqual(
      c("✅ Ready to merge", "⚠️ Need adjustment"),
      c("#D1FAE5", "#FEF3C7")
    ))
```

```{r tbl-combined-preview}
DT::datatable(
  head(df_combined_raw, 20),
  caption  = "📋 Preview df_combined — 20 Baris Pertama (Sebelum Cleaning)",
  options  = list(pageLength = 10, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)
```

### Section B – Data Handling

```{r badge-b, echo=FALSE}
HTML(paste0('
<div class="section-badge">
  📊 <strong>Dimensi Dataset:</strong> ',
  nrow(df_combined_raw), ' baris × ', ncol(df_combined_raw), ' kolom
  &nbsp;|&nbsp; Missing values: <strong>', sum(is.na(df_combined_raw)), '</strong>
  &nbsp;|&nbsp; Duplikat: <strong>', dup_count, ' baris (', dup_pct, '%)</strong>
</div>'))
```

<div class="flex-container">
<div class="small-box" data-index="1">

**Dimensi Dataset Gabungan**

Dataset gabungan `df_combined` berisi **`r nrow(df_combined_raw)` baris** dan **`r ncol(df_combined_raw)` kolom**. Setiap baris mewakili satu transaksi e-commerce. Dimensi ini didapat dari penggabungan 5 file format berbeda menggunakan `bind_rows()`. Informasi ini penting sebagai baseline sebelum proses cleaning dimulai.

</div>
<div class="small-box" data-index="2">

**Tipe Data Setiap Kolom**

Kolom `order_id`, `product_name`, `platform`, `order_status`, `payment_method`, `category`, `customer_segment`, `region` bertipe **character**. Kolom `unit_price`, `gross_sales`, `net_sales` seharusnya numerik tetapi tersimpan sebagai **string** karena ada format `"Rp 1.800.000"`. Kolom `order_date` harus bertipe **Date**, `quantity` bertipe **integer**, `is_returned` bertipe **logical**, `customer_rating` & `discount` bertipe **numeric**.

</div>
<div class="small-box" data-index="3">

**Missing Values**

Nilai kosong (NA) ditemukan pada kolom `payment_method` dan `customer_rating`. Kolom `payment_method` bisa kosong karena pelanggan tidak mengisi metode pembayaran. Kolom `customer_rating` kosong karena transaksi yang belum diberi ulasan. Total missing values: **`r sum(is.na(df_combined_raw))`** dari seluruh dataset.

</div>
<div class="small-box" data-index="4">

**Duplicate Rows (Baris Ganda)**

Ditemukan **`r dup_count` baris duplikat** (`r dup_pct`% dari total). Duplikat bisa terjadi karena file yang berbeda format memiliki data yang tumpang tindih. Baris yang persis sama dua kali akan menyebabkan double counting dalam analisis revenue dan frekuensi transaksi — harus dihapus.

</div>
<div class="small-box" data-index="5">

**Masalah #1 — Format Mata Uang di Kolom Numerik**

Kolom `unit_price`, `gross_sales`, `net_sales` berisi nilai seperti `"Rp 1.800.000"` — bertipe **string**, bukan angka. Akibatnya operasi matematika (sum, mean) **tidak bisa dilakukan** langsung. Ini adalah masalah paling umum saat data dikumpulkan dari berbagai sumber yang berbeda format pelaporannya.

</div>
<div class="small-box" data-index="6">

**Masalah #2 & #3 — Inkonsistensi Teks & Nilai Negatif**

**Masalah #2:** Nama platform tidak konsisten — `"shopee"`, `"SHOPEE"`, `" SHOPEE "` semuanya merujuk hal yang sama. Ini menyebabkan `group_by(platform)` menghasilkan grup terpisah padahal seharusnya satu. **Masalah #3:** Ada nilai negatif pada kolom numerik dan `quantity ≤ 0` — tidak masuk akal secara bisnis dan harus diganti dengan 0 atau median.

</div>
</div>

```{r tbl-col-info}
DT::datatable(
  col_info,
  caption  = "📋 Tipe Data, Missing Values & Unique Values per Kolom",
  options  = list(dom = 't', pageLength = 20, scrollX = TRUE),
  rownames = FALSE, class = "display compact"
) |>
  DT::formatStyle("Missing",
    backgroundColor = DT::styleInterval(0, c("white", "#FEF3C7")))
```

```{r tbl-missing}
if (nrow(missing_df) > 0) {
  DT::datatable(
    missing_df,
    caption  = "⚠️ Kolom dengan Missing Values",
    options  = list(dom = 't', pageLength = 10),
    rownames = FALSE, class = "display compact"
  )
}
```

### Section C – Data Cleaning

```{r badge-c, echo=FALSE}
HTML(paste0('
<div class="section-badge success">
  🧹 <strong>Hasil Cleaning:</strong> ',
  nrow(df_clean), ' baris bersih
  &nbsp;|&nbsp; Platform distandardisasi: <strong>', length(unique(df_clean$platform)), ' platform</strong>
  &nbsp;|&nbsp; Median rating default: <strong>', median_rating, '</strong>
</div>'))
```

<div class="flex-container">
<div class="small-box" data-index="1">

**Standardisasi Platform (WAJIB IF)**

Menggunakan fungsi `standardisasi_platform()` dengan kondisi **IF berjenjang**: `"shopee"`, `" SHOPEE "` → `"Shopee"` | `"tokped"` → `"Tokopedia"` | `"lazada"` → `"Lazada"` | `"tiktok shop"` → `"TikTok Shop"` | `"blibli"` → `"Blibli"`. Fungsi ini diterapkan ke seluruh kolom `platform` menggunakan `sapply()`.

</div>
<div class="small-box" data-index="2">

**Cleaning Nilai Harga (LOOPING WAJIB)**

Format `"Rp 1.800.000"` dikonversi ke angka murni `1800000` menggunakan `bersihkan_harga()`: hapus `"Rp"` → hapus titik ribuan → `as.integer()`. **Looping** diterapkan sekaligus pada kolom `net_sales`, `gross_sales`, dan `unit_price`. Nilai negatif diubah ke `0`. Ini memungkinkan operasi matematika dilakukan pada ketiga kolom sekaligus.

</div>
<div class="small-box" data-index="3">

**Handling Missing Values (WAJIB IF)**

`payment_method` kosong/NA → diisi `"Unknown"` menggunakan `ifelse()`. `customer_rating` kosong → diisi dengan **median** (`r median_rating`). Logika median dipilih karena: data rating e-commerce sering miring ke kiri (banyak rating rendah 1), sehingga median lebih mewakili "nilai tengah" yang sebenarnya dibanding mean yang bisa terdistorsi oleh outlier.

</div>
<div class="small-box" data-index="4">

**Standardisasi Order Status**

Variasi penulisan diseragamkan: `"delivered"` → `"Completed"` | `"cancelled"`, `"cancel"`, `"batal"` → `"Cancelled"` | `"on delivery"`, `"shipped"` → `"On Delivery"` | `"returned"`, `"retur"` → `"Returned"` | `"processing"` → `"Processing"`. Hasilnya: distribusi status jadi konsisten dan bisa diagregasi dengan benar.

</div>
<div class="small-box" data-index="5">

**WAJIB LOOPING — Bersihkan Banyak Kolom Sekaligus**

**Looping pertama**: 7 kolom teks (`product_name`, `category`, `platform`, `order_status`, `payment_method`, `customer_segment`, `region`) di-trim dan diubah lowercase sekaligus. **Looping kedua**: 3 kolom numerik (`net_sales`, `gross_sales`, `unit_price`) dibersihkan format Rp. **Looping ketiga**: 4 kolom diubah ke proper case. Total: **14 kolom** dibersihkan via looping — jauh lebih efisien.

</div>
<div class="small-box" data-index="6">

**Konversi Tipe Data & Sortir**

`order_date` → `as.Date()` | `quantity` → `as.integer()` (nilai ≤ 0 diisi 1) | `is_returned` → `as.logical()` | `discount` → `as.numeric()`. Dataset diurutkan berdasarkan `order_date` agar analisis time-series lebih mudah. Setelah semua tahap, `df_clean` siap untuk Section D dan E.

</div>
</div>

```{r tbl-clean-ecommerce}
DT::datatable(
  df_clean |> select(order_id, order_date, product_name, category, platform,
                     unit_price, quantity, net_sales, order_status,
                     payment_method, customer_rating, region),
  caption  = "🧹 Dataset E-Commerce — Setelah Cleaning",
  options  = list(pageLength = 10, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)
```

### Section D – Conditional Logic

```{r badge-d, echo=FALSE}
HTML(paste0('
<div class="section-badge">
  🔖 <strong>3 Kolom Baru:</strong>
  is_high_value &nbsp;|&nbsp; order_priority (nested IF) &nbsp;|&nbsp; valid_transaction
  &nbsp;|&nbsp; High Value: <strong>',
  sum(df_clean$is_high_value == "Yes"), ' transaksi</strong>
  &nbsp;|&nbsp; Invalid: <strong>',
  sum(df_clean$valid_transaction == "Invalid"), ' transaksi</strong>
</div>'))
```

<div class="flex-container">
<div class="small-box" data-index="1">

**Kolom `is_high_value`**

**Logika:** `ifelse(net_sales > 1000000, "Yes", "No")`. Transaksi dengan penjualan bersih di atas **Rp 1.000.000** diberi label `"Yes"`. Kolom ini memudahkan tim marketing untuk mengidentifikasi pelanggan premium yang berpotensi masuk program loyalitas atau mendapat penanganan khusus. Dari **`r nrow(df_clean)`** transaksi, **`r sum(df_clean$is_high_value=="Yes")`** berstatus High Value.

</div>
<div class="small-box" data-index="2">

**Kolom `order_priority` (WAJIB Nested IF)**

Menggunakan fungsi `order_priority_fn()` dengan **nested IF**: `net_sales > 1.000.000` → `"High"` | `net_sales ≥ 500.000` → `"Medium"` | `net_sales < 500.000` → `"Low"`. Nested IF artinya kondisi berada di dalam kondisi lain. Distribusi: **High = `r sum(df_clean$order_priority=="High")`** | **Medium = `r sum(df_clean$order_priority=="Medium")`** | **Low = `r sum(df_clean$order_priority=="Low")`** transaksi.

</div>
<div class="small-box" data-index="3">

**Kolom `valid_transaction`**

**Logika:** `ifelse(order_status == "Cancelled", "Invalid", "Valid")`. Transaksi yang dibatalkan tidak menghasilkan pendapatan nyata dan tidak boleh masuk dalam perhitungan revenue. Label `"Invalid"` pada transaksi Cancelled memudahkan filter saat analisis pendapatan — tinggal filter `valid_transaction == "Valid"`. Jumlah `"Invalid"`: **`r sum(df_clean$valid_transaction=="Invalid")`** transaksi.

</div>
<div class="small-box" data-index="4">

**Mengapa Logika Bisnis Penting?**

Tiga kolom baru ini adalah penerapan **business logic** ke dalam data. Tanpanya, tim harus menghitung manual setiap kali: mana yang bernilai tinggi? Mana yang prioritas? Mana yang valid? Dengan kolom-kolom ini, analisis bisa dilakukan dengan satu baris `filter()` atau `group_by()` — menghemat waktu dan mengurangi human error.

</div>
</div>

```{r tbl-conditional}
DT::datatable(
  df_clean |> select(order_id, platform, net_sales, order_status,
                     is_high_value, order_priority, valid_transaction),
  caption  = "🔖 Dataset dengan 3 Kolom Conditional Logic",
  options  = list(pageLength = 10, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
) |>
  DT::formatStyle("is_high_value",
    backgroundColor = DT::styleEqual(c("Yes","No"), c("#D1FAE5","#FEE2E2"))) |>
  DT::formatStyle("order_priority",
    backgroundColor = DT::styleEqual(
      c("High","Medium","Low"), c("#DBEAFE","#FEF9C3","#F3F4F6"))) |>
  DT::formatStyle("valid_transaction",
    backgroundColor = DT::styleEqual(c("Valid","Invalid"), c("#D1FAE5","#FEE2E2")))
```

### Section E – Analytical Thinking

```{r badge-e, echo=FALSE}
HTML(paste0('
<div class="section-badge success">
  📈 <strong>Platform Dominan:</strong> ', platform_dominant, ' (', platform_dom_n, ' transaksi)
  &nbsp;|&nbsp; <strong>Kategori Terbanyak:</strong> ', category_top, ' (', category_top_n, ' transaksi)
  &nbsp;|&nbsp; <strong>Status Terbanyak:</strong> ', status_top, ' (', status_top_pct, '%)
</div>'))
```

<div class="flex-container">
<div class="small-box" data-index="1">

**1. Platform Paling Dominan?**

Platform dengan transaksi terbanyak adalah **`r platform_dominant`** dengan **`r platform_dom_n` transaksi**. Diukur menggunakan `count(platform, sort=TRUE)`. Dominansi ini dapat mencerminkan kepercayaan konsumen yang lebih tinggi atau strategi harga yang lebih kompetitif di platform tersebut. Rekomendasi: alokasikan anggaran promosi lebih besar di platform ini untuk ROI yang lebih tinggi.

</div>
<div class="small-box" data-index="2">

**2. Category Paling Sering Muncul?**

Kategori produk yang paling sering ditransaksikan adalah **`r category_top`** dengan **`r category_top_n` transaksi**. Diukur menggunakan `count(category, sort=TRUE)`. Kategori ini menjadi tulang punggung penjualan — perlu dijaga ketersediaan stoknya dan menjadi fokus kampanye promosi utama. Kategori lain bisa ditingkatkan dengan bundling bersama kategori ini.

</div>
<div class="small-box" data-index="3">

**3. Status Transaksi Terbanyak?**

Status transaksi yang paling banyak adalah **`r status_top`** dengan `r status_top_n` transaksi (**`r status_top_pct`%** dari total). Diukur menggunakan `count(order_status, sort=TRUE)`. Persentase ini memberikan gambaran kesehatan operasional bisnis — rasio Completed yang tinggi berarti layanan berjalan baik, sedangkan Cancelled yang tinggi bisa menandakan masalah stok atau pengiriman.

</div>
</div>

```{r viz-platform, fig.height=3.5}
p1 <- plot_ly(platform_count, x = ~platform, y = ~Jumlah,
  type = 'bar', color = ~platform,
  colors = c('#1A2744','#3D7EF0','#0D9488','#F59E0B','#EF4444','#8B5CF6'),
  text = ~Jumlah, textposition = 'outside') |>
  layout(
    title  = list(text = "📊 Jumlah Transaksi per Platform", font = list(size = 13)),
    xaxis  = list(title = "Platform"),
    yaxis  = list(title = "Jumlah Transaksi"),
    showlegend = FALSE,
    paper_bgcolor = 'rgba(0,0,0,0)',
    plot_bgcolor  = 'rgba(0,0,0,0)',
    height = 300, margin = list(t=40, b=40)
  )
p1
```

<p class="viz-caption">**Insight:** Platform **`r platform_dominant`** mendominasi dengan `r platform_dom_n` transaksi. Ini menunjukkan basis pengguna terbesar ada di platform tersebut — alokasi iklan dan promosi sebaiknya diprioritaskan di sini untuk ROI tertinggi.</p>

```{r viz-category, fig.height=3.5}
p2 <- plot_ly(category_count, x = ~category, y = ~Jumlah,
  type = 'bar', color = ~category,
  colors = c('#1A2744','#3D7EF0','#0D9488','#F59E0B','#EF4444','#8B5CF6','#EC4899','#14B8A6'),
  text = ~Jumlah, textposition = 'outside') |>
  layout(
    title  = list(text = "🛍️ Frekuensi Transaksi per Kategori Produk", font = list(size = 13)),
    xaxis  = list(title = "Kategori", tickangle = -30),
    yaxis  = list(title = "Jumlah Transaksi"),
    showlegend = FALSE,
    paper_bgcolor = 'rgba(0,0,0,0)',
    plot_bgcolor  = 'rgba(0,0,0,0)',
    height = 300, margin = list(t=40, b=60)
  )
p2
```

<p class="viz-caption">**Insight:** Kategori **`r category_top`** adalah yang paling banyak ditransaksikan (`r category_top_n` transaksi). Fashion menjadi kategori utama karena tingginya frekuensi pembelian ulang dan impulse buying di platform e-commerce. Strategi: pastikan stok Fashion selalu lengkap dan beri diskon bundling dengan kategori Beauty.</p>

```{r viz-status, fig.height=3.5}
p3 <- plot_ly(status_count, labels = ~order_status, values = ~Jumlah,
  type = 'pie', hole = 0.45,
  textinfo = 'label+percent',
  marker = list(colors = c('#3D7EF0','#EF4444','#F59E0B','#0D9488','#8B5CF6','#EC4899'))) |>
  layout(
    title      = list(text = "📦 Distribusi Status Transaksi", font = list(size = 13)),
    showlegend = TRUE,
    paper_bgcolor = 'rgba(0,0,0,0)',
    height = 300
  )
p3
```

<p class="viz-caption">**Insight:** Status **`r status_top`** mendominasi (`r status_top_pct`% dari total transaksi). Persentase ini adalah indikator kesehatan operasional bisnis — semakin tinggi Completed, semakin baik. Perlu perhatian khusus pada transaksi Cancelled untuk mengidentifikasi penyebab pembatalan.</p>

```{r tbl-revenue-platform}
DT::datatable(
  platform_rev |>
    mutate(Total_Revenue = paste0("Rp ", formatC(Total_Revenue, format="d", big.mark=","))),
  caption  = "💰 Revenue & Rating per Platform",
  options  = list(dom = 't', pageLength = 10),
  rownames = FALSE, class = "display compact"
)
```


Soal 2 — Web Scraping {data-orientation=rows}
=======================================================================

## Column {.tabset .tabset-fade data-height=750}
-----------------------------------------------------------------------

### Section A – Data Collection

```{r badge-s2a, echo=FALSE}
HTML(paste0('
<div class="section-badge">
  🌐 <strong>Web Scraping dengan R:</strong>
  Website 3 (Oscar AJAX) — <strong>', nrow(df_oscar_raw), ' film</strong>
  &nbsp;|&nbsp;
  Website 4 (Turtles iFrame) — <strong>', nrow(df_turtles_raw), ' famili</strong>
</div>'))
```

<div class="flex-container">
<div class="small-box" data-index="1">

**Dua Bahasa Wajib: Python & R**

Soal mewajibkan penggunaan **Python** (untuk Website 1 Countries & Website 2 Hockey Teams) dan **R** (untuk Website 3 Oscar Films & Website 4 Turtles). File `.Rmd` ini mengerjakan Website 3 & 4 menggunakan R dengan library `rvest`, `httr`, dan `jsonlite`. Website 1 & 2 dikerjakan di file `.py`/`.ipynb` Python terpisah.

</div>
<div class="small-box" data-index="2">

**WAJIB LOOPING — Scraping Banyak Halaman**

Looping digunakan untuk: (1) iterasi setiap **tahun** Oscar (dari daftar `years` yang diambil dari halaman utama), kirim request AJAX satu per satu, dan simpan hasilnya — ini adalah scraping **multi-halaman** via AJAX parameter. (2) Iterasi setiap **baris** `<tr class="family">` di dalam iframe Turtles. Tanpa looping, scraping ratusan tahun Oscar tidak akan efisien.

</div>
<div class="small-box" data-index="3">

**Simpan ke DataFrame dan CSV**

Setelah berhasil dikumpulkan, data Oscar disimpan ke `oscar_films.csv` dan data Turtles ke `turtles_iframe_data.csv` menggunakan `write.csv(..., row.names=FALSE)`. Dua file CSV ini adalah bagian dari **4 file CSV output wajib** (masing-masing 1 per website). File CSV bisa dibuka langsung di Excel untuk verifikasi.

</div>
</div>

<div class="two-col-split">
<div class="col-left">
<div class="col-header">🎬 Website 3 — Oscar Winning Films (AJAX / Javascript)</div>
<div class="flex-container">

<div class="small-box" data-index="1">

**Cara Kerja AJAX**

Website Oscar tidak menampilkan data film langsung saat halaman pertama dibuka. Data dimuat *belakangan* oleh JavaScript melalui **AJAX request** di background ke endpoint tersembunyi. Scraping HTML biasa menghasilkan halaman kosong karena film belum ada saat request dikirim — ini tantangan utama website modern yang dinamis.

</div>
<div class="small-box" data-index="2">

**Solusi: Temukan Endpoint API**

Dengan DevTools (Inspect → Network → Filter XHR), ditemukan endpoint: `?ajax=true&year=XXXX`. Menggunakan `GET(url_oscar, query=list(ajax="true", year=yr))`, kita dapat response **JSON** berisi seluruh film untuk tahun tersebut — jauh lebih cepat dan bersih dibanding simulasi browser penuh dengan Selenium.

</div>
<div class="small-box" data-index="3">

**Data yang Diambil**

Kolom yang diambil per tahun: `year` (tahun Oscar), `title` (judul film), `nominations` (jumlah nominasi), `awards` (jumlah penghargaan menang), `best_picture` (apakah menang Best Picture), `category` (Best Picture / Award Winner / Nominated — dibuat dengan IF), `description` (keterangan otomatis dibuat dari field lain).

</div>
<div class="small-box" data-index="4">

**LOOPING Antar Tahun Oscar**

```
for (yr in years) {
  r <- GET(url_oscar, query=list(
            ajax="true", year=yr))
  films <- fromJSON(content(r,"text"))
  oscar_data <- append(oscar_data, 
                       list(films))
  Sys.sleep(0.2)  # jeda server
}
```
Loop berjalan pada vektor `years` yang diambil dari tag `<a class="year-link">` halaman Oscar. `Sys.sleep(0.2)` mencegah rate-limit dari server.

</div>
</div>
</div>

<div class="col-right">
<div class="col-header">🐢 Website 4 — Turtles All the Way Down (Frames & iFrames)</div>
<div class="flex-container">

<div class="small-box" data-index="1">

**Cara Kerja iFrame**

Halaman Turtles menyimpan seluruh kontennya di dalam sebuah **iframe** — seperti jendela di dalam jendela. Jika scraping halaman utama saja, tidak ada data kura-kura yang terambil. Kita harus **masuk ke dalam iframe** terlebih dahulu untuk bisa membaca isinya.

</div>
<div class="small-box" data-index="2">

**Solusi: Ambil src dari iframe**

Langkah: (1) scrape halaman utama → temukan `<iframe id="iframe">`, (2) ambil atribut `src` menggunakan `html_attr(fr, "src")`, (3) gabungkan dengan base URL, (4) scrape URL iframe tersebut secara terpisah sebagai halaman mandiri. Ini adalah teknik standar menangani konten berbasis frame.

</div>
<div class="small-box" data-index="3">

**Data yang Diambil**

Dari dalam iframe: `name` (nama ilmiah famili, contoh `"Cheloniidae"`), `description` (nama umum, contoh `"Sea turtles"`), `additional_info` (kalimat ringkasan yang dibuat dari kombinasi kedua kolom sebelumnya). Minimal 3 field data sesuai ketentuan soal.

</div>
<div class="small-box" data-index="4">

**Fallback Data & IF Default**

Jika koneksi iframe gagal (timeout/blocked), **data fallback 14 famili** kura-kura digunakan. Ini memastikan laporan tetap bisa dibuat. Di dalam looping baris, **IF** digunakan: jika elemen `<td class="family-name">` tidak ditemukan → nilai default `"Unknown"`. Pendekatan defensif ini penting untuk scraping yang robust.

</div>
</div>
</div>
</div>

```{r tbl-oscar-s2a}
DT::datatable(
  df_oscar_raw |> select(any_of(c("year","title","nominations","awards","category","description"))) |>
    head(30),
  caption  = paste0("🎬 Preview Oscar Films — ", nrow(df_oscar_raw),
                    " film dari ", length(unique(df_oscar_raw$year)), " tahun"),
  options  = list(pageLength = 8, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)
```

```{r tbl-turtles-s2a}
DT::datatable(
  df_turtles_raw,
  caption  = paste0("🐢 Data Turtles All the Way Down — ", nrow(df_turtles_raw), " famili"),
  options  = list(pageLength = 14, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)
```

### Section B – Data Handling

<div class="two-col-split">
<div class="col-left">
<div class="col-header">🎬 Oscar Films — Data Handling</div>
<div class="flex-container">

<div class="small-box" data-index="1">

**Dimensi & Tipe Data Oscar**

Dataset Oscar memiliki **`r nrow(df_oscar_raw)` baris** dan **`r ncol(df_oscar_raw)` kolom**. Kolom `year`, `nominations`, `awards` seharusnya bertipe **integer** — namun saat pertama diambil dari HTML, `year` bertipe **string** (karena diambil dari teks tag `<a>`). Perlu dikonversi dengan `as.integer()`. Kolom `best_picture` bertipe **logical**, kolom teks lainnya bertipe **character**.

</div>
<div class="small-box" data-index="2">

**Data Issue #1 — year Bertipe String**

Kolom `year` dari scraping awal bertipe `character` (misal `"2015"`). Ini membuat pengurutan tidak bisa numerik — `"9"` secara alfabet lebih besar dari `"2"`, sehingga urutan tahun bisa keliru. Perlu konversi `as.integer(year)` agar filter dan sort tahun berjalan benar.

</div>
<div class="small-box" data-index="3">

**Data Issue #2 — Kolom category Bersifat Derivatif**

Kolom `category` dan `description` **tidak tersedia langsung** dari JSON response Oscar — harus dibuat manual dengan logika IF: `best_picture==TRUE` → `"Best Picture"`, `awards>0` → `"Award Winner"`, else → `"Nominated"`. Nilai turunan seperti ini perlu didokumentasikan agar tidak dikira data asli dari sumber.

</div>
<div class="small-box" data-index="4">

**Missing Values & Duplikat Oscar**

Missing values minimal karena data JSON dari Oscar sudah terstruktur rapi. Nilai kosong yang muncul (category kosong) langsung ditangani dengan default `"Unknown"` saat pengambilan. Duplikat bisa terjadi jika AJAX response mengembalikan film yang sama di dua tahun berbeda — dihapus dengan `distinct()`.

</div>
</div>
</div>

<div class="col-right">
<div class="col-header">🐢 Turtles — Data Handling</div>
<div class="flex-container">

<div class="small-box" data-index="1">

**Dimensi & Tipe Data Turtles**

Dataset Turtles memiliki **`r nrow(df_turtles_raw)` baris** dan **`r ncol(df_turtles_raw)` kolom**. Semua kolom bertipe **character** — tidak ada data kuantitatif karena ini adalah data klasifikasi taksonomi. Tidak ada kolom numerik yang perlu dikonversi, namun cleaning teks tetap diperlukan untuk konsistensi.

</div>
<div class="small-box" data-index="2">

**Data Issue #1 — Scraping Rentan Gagal**

Koneksi ke URL iframe bisa **gagal** karena mekanisme anti-scraping, timeout, atau perubahan struktur HTML. Jika looping baris `<tr class="family">` tidak menghasilkan data, seluruh dataset Turtles akan kosong. Ini adalah risiko scraping live yang harus diantisipasi dengan data fallback statis yang selalu tersedia.

</div>
<div class="small-box" data-index="3">

**Data Issue #2 — additional_info Redundan**

Kolom `additional_info` dibuat dengan template: *"The [name] family — commonly known as [description]"*. Informasi ini sebenarnya tidak menambah data baru — hanya menyusun ulang dua kolom yang sudah ada. Namun kolom ini memenuhi syarat soal untuk menyertakan informasi tambahan dari setiap famili kura-kura.

</div>
<div class="small-box" data-index="4">

**Missing Values & Duplikat Turtles**

Dataset Turtles tidak memiliki missing values karena data fallback selalu tersedia sebagai pengaman. Nilai `"Unknown"` dimasukkan eksplisit jika elemen HTML tidak ditemukan (via IF). Duplikat mungkin terjadi jika nama famili muncul dari scraping live sekaligus dari fallback — dihapus dengan `distinct()`.

</div>
</div>
</div>
</div>

```{r tbl-handling-oscar}
miss_o <- data.frame(
  Kolom   = names(df_oscar_raw),
  Tipe    = sapply(df_oscar_raw, function(x) class(x)[1]),
  Missing = colSums(is.na(df_oscar_raw)),
  Unique  = sapply(df_oscar_raw, function(x) length(unique(x))),
  row.names = NULL
)
DT::datatable(miss_o,
  caption = "📊 Tipe Data & Info Kolom — Oscar Films",
  options = list(dom='t', pageLength=10), rownames=FALSE, class="display compact")
```

```{r tbl-handling-turtles}
miss_t <- data.frame(
  Kolom   = names(df_turtles_raw),
  Tipe    = sapply(df_turtles_raw, function(x) class(x)[1]),
  Missing = colSums(is.na(df_turtles_raw)),
  Unique  = sapply(df_turtles_raw, function(x) length(unique(x))),
  row.names = NULL
)
DT::datatable(miss_t,
  caption = "📊 Tipe Data & Info Kolom — Turtles",
  options = list(dom='t', pageLength=6), rownames=FALSE, class="display compact")
```

### Section C – Data Cleaning

```{r badge-s2c, echo=FALSE}
HTML(paste0('
<div class="section-badge success">
  🧹 <strong>Cleaning Oscar:</strong> ', nrow(df_oscar), ' film bersih
  &nbsp;|&nbsp; <strong>Cleaning Turtles:</strong> ', nrow(df_turtles), ' famili bersih
  &nbsp;|&nbsp; Output: oscar_films.csv + turtles_iframe_data.csv
</div>'))
```

<div class="two-col-split">
<div class="col-left">
<div class="col-header">🎬 Oscar Films — Cleaning</div>
<div class="flex-container">

<div class="small-box" data-index="1">

**LOOPING — Cleaning 3 Kolom Teks Sekaligus**

```
oscar_text_cols <- c("title","category",
                     "description")
for (col in oscar_text_cols) {
  df_oscar[[col]] <- str_squish(
    str_trim(df_oscar[[col]]))
  if (col == "title")
    df_oscar[[col]] <- str_to_title(...)
  else
    df_oscar[[col]] <- str_to_sentence(...)
}
```
Tiga kolom dibersihkan dalam satu loop — efisien dan konsisten.

</div>
<div class="small-box" data-index="2">

**IF untuk Missing Value & Tipe Data**

`category` atau `description` kosong → diisi `"Unknown [kolom]"` menggunakan `if_else()`. Kolom `year`, `nominations`, `awards` dikonversi ke `integer` agar operasi matematika valid. `title` diubah ke *title case* (nama film), `category` dan `description` ke *sentence case* agar konsisten.

</div>
<div class="small-box" data-index="3">

**Hapus Duplikat & Simpan CSV**

Duplikat dihapus dengan `distinct()`. Data bersih disimpan: `write.csv(df_oscar, "oscar_films.csv", row.names=FALSE)`. Ini adalah output CSV wajib #3 dari 4 yang ditentukan soal. File ini bisa langsung dibuka di Excel untuk verifikasi manual hasil scraping.

</div>
</div>
</div>

<div class="col-right">
<div class="col-header">🐢 Turtles — Cleaning</div>
<div class="flex-container">

<div class="small-box" data-index="1">

**LOOPING Semua Kolom (WAJIB)**

```
for (col in names(df_turtles)) {
  if (is.character(df_turtles[[col]])) {
    df_turtles[[col]] <- str_squish(
      str_trim(df_turtles[[col]]))
    # IF untuk tiap kolom berbeda
    if (col == "name") {
      df_turtles[[col]] <- if_else(
        df_turtles[[col]] == "",
        "Unknown Turtle", ...)
    }
  }
}
```
Semua kolom character diproses dalam satu loop dengan IF di dalamnya.

</div>
<div class="small-box" data-index="2">

**IF-ELSE Default Berbeda per Kolom**

`name` kosong → `"Unknown Turtle"` | `description` kosong → `"No description available"` | `additional_info` kosong → `"No additional information"`. Setiap kolom punya nilai default yang berbeda sesuai konteks kolomnya — lebih informatif dibanding mengisi semua dengan `NA` atau string kosong.

</div>
<div class="small-box" data-index="3">

**Proper Case & Simpan CSV**

`name` (nama ilmiah) dipertahankan dalam *title case* sesuai konvensi taksonomi. `description` diubah ke *sentence case* agar konsisten. Data bersih disimpan: `write.csv(df_turtles, "turtles_iframe_data.csv", row.names=FALSE)`. Ini adalah output CSV wajib #4 dari 4 yang ditentukan soal.

</div>
</div>
</div>
</div>

```{r tbl-clean-oscar}
DT::datatable(
  df_oscar |> select(any_of(c("year","title","nominations","awards","category","description","data_status"))),
  caption  = "🧹 Oscar Films — Setelah Cleaning",
  options  = list(pageLength = 8, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)
```

```{r tbl-clean-turtles}
DT::datatable(
  df_turtles |> select(name, description, additional_info, data_status),
  caption  = "🧹 Turtles — Setelah Cleaning",
  options  = list(pageLength = 14, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
)
```

### Section D – Conditional Logic

```{r badge-s2d, echo=FALSE}
HTML(paste0('
<div class="section-badge">
  ✅ <strong>data_status Oscar:</strong>
  Complete = ', sum(df_oscar$data_status=="Complete"),
  ' &nbsp;|&nbsp; Incomplete = ', sum(df_oscar$data_status=="Incomplete"),
  ' &nbsp;&nbsp; | &nbsp;&nbsp; <strong>data_status Turtles:</strong>
  Complete = ', sum(df_turtles$data_status=="Complete"),
  ' &nbsp;|&nbsp; Incomplete = ', sum(df_turtles$data_status=="Incomplete"),
'</div>'))
```

<div class="flex-container">
<div class="small-box" data-index="1">

**Kondisi 1 — Elemen Tidak Ditemukan → Default**

**Jika** elemen HTML tidak berhasil ditemukan saat scraping (misal `<td class="family-name">` tidak ada di iframe, atau title film berisi `"Unknown Title"`) → tandai `data_status = "Incomplete"`. Ini berarti data tidak berhasil diambil dengan benar. Implementasi: `if (!is.null(nm) && !is.na(nm)) html_text(nm) else "Unknown"`.

</div>
<div class="small-box" data-index="2">

**Kondisi 2 — Data Tidak Lengkap → "Incomplete"**

**Jika** data kurang informatif — Oscar: `nominations == 0 & awards == 0` (tidak ada data statistik sama sekali) | Turtles: `description %in% c("Unknown","")` (tidak ada nama umum) → tandai `"Incomplete"`. Logika ini memastikan hanya data yang benar-benar bermakna yang lolos ke analisis final.

</div>
<div class="small-box" data-index="3">

**Kondisi 3 — Data Valid → "Complete"**

**Jika** semua field terisi dengan benar (bukan placeholder, bukan kosong, bukan `"Unknown"`) → tandai `"Complete"`. Implementasi menggunakan `case_when()` dari `dplyr`: kondisi 1 dan 2 dicek lebih dulu, sisanya otomatis `"Complete"`. Ini adalah pola `if/else if/else` yang diterapkan secara vektorial.

</div>
<div class="small-box" data-index="4">

**Kolom `data_status` sebagai Quality Gate**

Kolom `data_status` berfungsi sebagai **quality gate** — filter otomatis kualitas data. Sebelum data digunakan untuk analisis lanjut, cukup filter `data_status == "Complete"`. Ini jauh lebih andal dibanding memeriksa manual setiap kolom. Pola ini umum digunakan dalam pipeline data production di dunia industri.

</div>
</div>

```{r tbl-status-oscar}
DT::datatable(
  df_oscar |> select(any_of(c("year","title","nominations","awards","data_status"))) |>
    head(30),
  caption  = "✅ Oscar Films — Kolom data_status",
  options  = list(pageLength = 8, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
) |>
  DT::formatStyle("data_status",
    backgroundColor = DT::styleEqual(c("Complete","Incomplete"), c("#D1FAE5","#FEE2E2")))
```

```{r tbl-status-turtles}
DT::datatable(
  df_turtles |> select(name, description, additional_info, data_status),
  caption  = "✅ Turtles — Kolom data_status",
  options  = list(pageLength = 14, scrollX = TRUE, dom = 'frtip'),
  rownames = FALSE, class = "display compact"
) |>
  DT::formatStyle("data_status",
    backgroundColor = DT::styleEqual(c("Complete","Incomplete"), c("#D1FAE5","#FEE2E2")))
```

### Section E – Analytical Thinking

<div class="flex-container">
<div class="small-box" data-index="1">

**1. Website Paling Mudah di-Scrape?**

**Countries of the World (Static Page)** adalah yang paling mudah. Semua data sudah ada di HTML saat halaman pertama dibuka — tidak ada JavaScript, tidak ada form, tidak ada halaman berikutnya. Cukup satu `GET` request dan semua data langsung bisa dibaca dengan `html_table()`. Seperti membaca buku yang sudah terbuka di halaman yang tepat — tidak ada kejutan.

</div>
<div class="small-box" data-index="2">

**2. Website Paling Sulit di-Scrape?**

**Oscar Winning Films (AJAX/JavaScript)** adalah yang paling sulit. Data tidak ada di HTML awal — baru muncul setelah JavaScript berjalan di browser. Scraping langsung dengan `requests` menghasilkan halaman kosong. Perlu teknik khusus: temukan endpoint API tersembunyi via DevTools Inspect Network, atau gunakan Selenium untuk simulasi browser penuh yang lebih lambat dan berat.

</div>
<div class="small-box" data-index="3">

**3a. Perbedaan: Static vs Pagination**

**Static**: data ada di HTML langsung saat halaman dibuka — satu GET request cukup, parsing langsung dengan `html_table()` atau `html_nodes()`. Tidak ada state yang perlu dikelola. **Pagination**: data tersebar di banyak halaman (page 1, 2, 3...). Perlu looping dengan parameter `?page=N` sambil memantau kapan halaman terakhir — harus tahu kapan menghentikan loop agar tidak error 404.

</div>
<div class="small-box" data-index="4">

**3b. Perbedaan: AJAX vs iFrame**

**AJAX**: data dimuat JavaScript setelah halaman terbuka melalui request tersembunyi ke endpoint API. Solusi terbaik: temukan URL endpoint via DevTools → request langsung ke URL tersebut untuk mendapat JSON murni. **iFrame**: konten ada di URL terpisah dalam tag `<iframe>`. Solusi: ambil atribut `src` dari iframe → scrape URL tersebut sebagai halaman mandiri. Keduanya butuh pemahaman cara browser bekerja di balik layar.

</div>
<div class="small-box" data-index="5">

**3 Insights dari Proses Scraping**

*(1)* **Kompleksitas ≠ volume data** — website static dengan ribuan baris lebih mudah di-scrape dari website AJAX dengan puluhan baris. *(2)* **Data hasil scraping hampir selalu perlu cleaning** — spasi ekstra, kapitalisasi tidak konsisten, tipe data salah, dan missing values ditemukan di semua website yang di-scrape. *(3)* **Looping adalah pondasi** — tanpa looping, scraping multi-halaman dan multi-elemen tidak bisa dilakukan secara efisien.

</div>
<div class="small-box" data-index="6">

**2 Rekomendasi**

*(1)* **Cek endpoint AJAX sebelum pakai Selenium** — jika URL API ditemukan lewat Inspect Network, datanya bisa diambil lebih cepat dan bersih dalam format JSON tanpa perlu simulasi browser. Selenium hanya digunakan jika endpoint tidak bisa ditemukan. *(2)* **Selalu gunakan `tryCatch()` + `Sys.sleep()`** — `tryCatch` mencegah program berhenti total saat satu elemen tidak ditemukan, `Sys.sleep` memberi jeda agar server tidak memblokir request karena terlalu cepat.

</div>
</div>

```{r viz-oscar}
if (nrow(df_oscar) > 0 && "year" %in% names(df_oscar) && "awards" %in% names(df_oscar)) {
  oscar_by_year <- df_oscar |>
    group_by(year) |>
    summarise(Total_Film = n(), Total_Awards = sum(awards, na.rm=TRUE), .groups="drop")
  
  p_o <- plot_ly(oscar_by_year, x = ~year, y = ~Total_Film,
    type = 'bar', name = 'Jumlah Film',
    marker = list(color = '#3D7EF0')) |>
    layout(
      title  = list(text = "🎬 Jumlah Film Oscar per Tahun", font=list(size=13)),
      xaxis  = list(title = "Tahun"),
      yaxis  = list(title = "Jumlah Film"),
      paper_bgcolor = 'rgba(0,0,0,0)',
      plot_bgcolor  = 'rgba(0,0,0,0)'
    )
  p_o
}
```

```{r viz-oscar-cat}
if (nrow(df_oscar) > 0 && "category" %in% names(df_oscar)) {
  cat_count <- df_oscar |> count(category, sort=TRUE)
  p_c <- plot_ly(cat_count, labels=~category, values=~n,
    type='pie', hole=0.4, textinfo='label+percent',
    marker=list(colors=c('#3D7EF0','#F59E0B','#0D9488'))) |>
    layout(title=list(text="🏆 Distribusi Kategori Film Oscar", font=list(size=13)),
           paper_bgcolor='rgba(0,0,0,0)')
  p_c
}
```


Referensi {data-orientation=rows}
=======================================================================

```{r referensi, echo=FALSE}
HTML('
<div style="max-width:900px;margin:24px auto;padding:28px 32px;
background:#fff;border-radius:14px;border:1px solid #E5E7EB;
box-shadow:0 4px 20px rgba(26,39,68,.08);font-family:Segoe UI,sans-serif;">
  <h3 style="color:#1A2744;font-size:17px;font-weight:800;margin:0 0 20px;
  padding-bottom:10px;border-bottom:2px solid #3D7EF0;">📚 Daftar Referensi</h3>

  <p style="font-size:13px;color:#374151;line-height:1.8;margin-bottom:10px;">
    [1] Wickham, H. (2022). <em>rvest: Easily Harvest (Scrape) Web Pages.</em>
    R package version 1.0.3. <a href="https://rvest.tidyverse.org/" target="_blank"
    style="color:#3D7EF0;">https://rvest.tidyverse.org/</a>
  </p>
  <p style="font-size:13px;color:#374151;line-height:1.8;margin-bottom:10px;">
    [2] Bryan, J. & Posit team. (2023). <em>googledrive: An Interface to Google Drive.</em>
    <a href="https://googledrive.tidyverse.org/" target="_blank"
    style="color:#3D7EF0;">https://googledrive.tidyverse.org/</a>
  </p>
  <p style="font-size:13px;color:#374151;line-height:1.8;margin-bottom:10px;">
    [3] Ooms, J. (2023). <em>jsonlite: A Simple and Robust JSON Parser for R.</em>
    <a href="https://cran.r-project.org/package=jsonlite" target="_blank"
    style="color:#3D7EF0;">https://cran.r-project.org/package=jsonlite</a>
  </p>
  <p style="font-size:13px;color:#374151;line-height:1.8;margin-bottom:10px;">
    [4] Wickham, H., François, R., Henry, L., &amp; Müller, K. (2023).
    <em>dplyr: A Grammar of Data Manipulation.</em>
    <a href="https://dplyr.tidyverse.org/" target="_blank"
    style="color:#3D7EF0;">https://dplyr.tidyverse.org/</a>
  </p>
  <p style="font-size:13px;color:#374151;line-height:1.8;margin-bottom:10px;">
    [5] Wickham, H. et al. (2023). <em>httr: Tools for Working with URLs and HTTP.</em>
    <a href="https://httr.r-lib.org/" target="_blank"
    style="color:#3D7EF0;">https://httr.r-lib.org/</a>
  </p>
  <p style="font-size:13px;color:#374151;line-height:1.8;margin-bottom:10px;">
    [6] Sievert, C. (2020). <em>Interactive Web-Based Data Visualization with R, plotly, and shiny.</em>
    Chapman and Hall/CRC. <a href="https://plotly-r.com/" target="_blank"
    style="color:#3D7EF0;">https://plotly-r.com/</a>
  </p>
  <p style="font-size:13px;color:#374151;line-height:1.8;margin-bottom:10px;">
    [7] Xie, Y., Cheng, J., &amp; Tan, X. (2023).
    <em>DT: A Wrapper of the JavaScript Library DataTables.</em>
    <a href="https://rstudio.github.io/DT/" target="_blank"
    style="color:#3D7EF0;">https://rstudio.github.io/DT/</a>
  </p>
  <p style="font-size:13px;color:#374151;line-height:1.8;margin-bottom:0;">
    [8] ScrapeThisSite.com. (2024). <em>Oscar Winning Films — AJAX and Javascript</em> [Dataset].
    <a href="https://www.scrapethissite.com/pages/ajax-javascript/" target="_blank"
    style="color:#3D7EF0;">https://www.scrapethissite.com/pages/ajax-javascript/</a>
  </p>
</div>
')
```