UTS Sains Data Pemrograman
Angelique
Kiyoshi
Lakeisha B.U
52250001
Anindya
Kristianingputri
52250025
Safina
Zahra
52250033
Ujian Tengah Semester ini berfokus pada penerapan konsep pengolahan data yang telah dipelajari selama perkuliahan. Dalam tugas ini, dilakukan serangkaian proses mulai dari pengumpulan data, pembersihan data, hingga analisis sederhana untuk menghasilkan informasi yang lebih bermakna.
Tugas ini terdiri dari dua bagian utama, yaitu pengolahan data e-commerce dan web scraping. Kedua bagian ini bertujuan untuk melatih kemampuan dalam memahami struktur data, melakukan data cleaning, serta mengolah data menjadi insight yang dapat dianalisis lebih lanjut.
1 E-COMMERCE
Pada bagian ini, dilakukan pengolahan data transaksi e-commerce yang terdiri dari berbagai atribut seperti platform, kategori produk, status transaksi, dan lainnya. Data yang tersedia masih memiliki beberapa permasalahan seperti missing values, inkonsistensi penulisan, dan duplikasi data, sehingga diperlukan proses data cleaning sebelum dilakukan analisis.
Tujuan dari bagian ini adalah untuk memahami proses pembersihan data serta mengidentifikasi pola dari data transaksi yang tersedia.
1.1 Section A
unzip(), kemudian setiap file dibaca menggunakan fungsi yang sesuai dengan formatnya, seperti read.csv(), read_excel(), fromJSON() untuk JSON, serta xmlToDataFrame().
Untuk file TXT, digunakan struktur kontrol
if untuk mendeteksi delimiter berdasarkan isi baris pertama, sehingga data dapat dibaca dengan fungsi yang tepat. Selanjutnya, dilakukan pengecekan struktur data dan dirangkum dalam bentuk tabel sebelum proses penggabungan data.
unzip("Archive.zip", exdir = "data_ecommerce")
df_csv <- read.csv("data_ecommerce/ecommerce.csv")
df_excel <- read_excel("data_ecommerce/ecommerce.xlsx")
df_json <- as.data.frame(fromJSON("data_ecommerce/ecommerce.json"))
first_line <- readLines("data_ecommerce/ecommerce.txt", n = 1)
df_txt <- if (grepl(",", first_line)) {read_csv("data_ecommerce/ecommerce.txt", show_col_types = FALSE)
} else if (grepl(";", first_line)) {
read_delim("data_ecommerce/ecommerce.txt", delim = ";", show_col_types = FALSE)
} else if (grepl("\\|", first_line)) {read_delim("data_ecommerce/ecommerce.txt", delim = "|", show_col_types = FALSE)
} else {read_table("data_ecommerce/ecommerce.txt")}
df_xml <- as.data.frame(xmlToDataFrame("data_ecommerce/ecommerce.xml"))colnames() dan sapply(). Struktur kontrol if-else digunakan untuk mengklasifikasikan dataset menjadi "Ready to merge" atau "Need adjustment".
Dataset yang telah sesuai kemudian disamakan tipe datanya menggunakan lapply() dan digabungkan menjadi satu dataset utama menggunakan bind_rows().
data_list <- list( # kasih nama ke setiap file
CSV = df_csv, Excel = df_excel, JSON = df_json, TXT = df_txt, XML = df_xml)
data_ready <- data_list
col_struct <- lapply(data_ready, colnames) # ambil struktur kolom
base_col <- col_struct[[1]] # jadikan acuan kolom pertama
status_check <- sapply(col_struct, function(cols) { # cek struktur
if (identical(cols, base_col)) {"Ready to merge"
} else {"Need adjustment"}
})
hasil_cek <- data.frame( # buat hasil cek
File = names(data_list),
Status = status_check
)
ready_data <- data_ready[status_check == "Ready to merge"] # ambil yang ready
ready_data_fix <- lapply(ready_data, function(df) { # samakan tipe data
df[] <- lapply(df, as.character)
df
})
dataset_utama <- bind_rows(ready_data_fix) # mergebind_rows() hingga terbentuk dataset_utama yang siap dianalisis.
| order_id | order_date | ship_date | platform | category | product_name | unit_price | quantity | gross_sales | campaign | voucher_code | discount_pct | discount_value | shipping_cost | net_sales | payment_method | customer_segment | region | stock_status | order_status | customer_rating | priority_flag |
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| ORD01910 | 2024/06/26 | 28/06/2024 | Blibli | Electronics | Bluetooth Speaker | 1281236 | 6 | Rp 7.687.416 | Normal Day | NA | 0 | 0 | 15000 | 7687416 | Virtual Account | Returning | Surabaya | In Stock | Delivered | 5 | NA |
| ORD01748 | 2024/02/05 | 13/02/2024 | Tiktok Shop | Sports | Running Shoes | 359104 | 6 | 2154624 | Mega Campaign | DISC20 | 20 | 430925 | 12000 | 1723699 | COD | New | Medan | In Stock | Delivered | 4.0 | PRIORITY |
| ORD00471 | 04-29-2024 | 06/05/2024 | Shopee | Home Living | Vacuum Cleaner | 488485 | 1 | 488485 | Flash Sale | DISC10 | 10 | 48848 | 9000 | 439637 | Virtual Account | Returning | Medan | Low Stock | delivered | 4 | normal |
| ORD00433 | 07/09/2024 | 2024-09-11 | TikTok Shop | Home Living | Storage Box | 937243 | 3 | 2811729 | Normal Day | NONE | 0 | 0 | 25000 | 2811729 | VA | Returning | Yogyakarta | In Stock | delivered | 4 | N |
| ORD00823 | 24/12/2024 | 2024/12/31 | Shopee | Beauty | Sunscreen | 206956 | 1 | Rp 206.956 | Flash Sale | DISC10 | 10 | 20696 | 20000 | 186260 | Virtual Account | Returning | Jakarta | Preorder | Delivered | 4 | N |
| ORD01531 | 2024-01-05 | NA | Shopee | Home_Living | Blender | 1046390 | 1 | 1046390 | Normal Day | NONE | 0 | 0 | 15000 | 0 | Credit Card | Returning | Bekasi | In Stock | cancelled | NA | normal |
| ORD01497 | 12-02-2024 | 12-03-2024 | TikTok Shop | Fashion | Jeans | 375257 | 10 | 3752570 | Payday Sale | DISC15 | 15 | 562886 | 25000 | 3189684 | E-Wallet | Returning | Semarang | In Stock | Delivered | 5 | No |
1.2 Section B
datatable().
is.na() dan kondisi string kosong menggunakan sapply(). Hasilnya dihitung persentasenya, diurutkan berdasarkan jumlah missing, dan ditampilkan kembali dalam bentuk tabel interaktif.
missing_data <- data.frame( # cek missing value
Nama_Kolom = names(dataset_utama),
Tipe_Data = sapply(dataset_utama, class),
Jumlah_Missing = sapply(dataset_utama, function(x) {
sum(is.na(x) | trimws(as.character(x)) == "")
}),
stringsAsFactors = FALSE
)
missing_data$Persentase <- round(
(missing_data$Jumlah_Missing / nrow(dataset_utama)) * 100, 2
)
missing_data <- missing_data %>%
arrange(desc(Jumlah_Missing))exists() untuk memastikan data siap digunakan. Selanjutnya dihitung jumlah data duplikat dengan fungsi duplicated(), lalu hasilnya disimpan dalam bentuk tabel.
if (!exists("dataset_utama")) {
stop("dataset_utama belum tersedia.")
}
jumlah_duplikat <- sum(duplicated(dataset_utama)) # hitung jumlah duplicate row
info_duplikat <- data.frame(
Keterangan = "Jumlah Duplicate Row",
Nilai = jumlah_duplikat
)ship_date, voucher_code, discount_pct, payment_method, customer_rating, dan priority_flag.unit_price, quantity, dan net_sales yang seharusnya bertipe numerik.1.3 Section C
iforder_statusstandardisasi_platform() yang menggunakan struktur percabangan if–else untuk menyeragamkan berbagai variasi penulisan nama platform. Proses pembersihan data dilakukan dengan fungsi seperti tolower(), trimws(), gsub(), dan grepl(). Selanjutnya, fungsi diterapkan ke seluruh data menggunakan sapply() yang berperan sebagai perulangan (loop) implisit, sehingga data platform menjadi lebih konsisten.
df_final <- dataset_utama
clean_platform <- function(x) {
if (is.na(x)) return(x)
x <- tolower(trimws(as.character(x)))
x <- gsub(" ", "", x)
if (grepl("shopee", x)) {
return("Shopee")
} else if (grepl("tokped|tokopedia", x)) {
return("Tokopedia")
} else if (grepl("blibli", x)) {
return("Blibli")
} else if (grepl("tiktok", x)) {
return("Tiktok")
} else if (grepl("lazada", x)) {
return("Lazada")
} else {
return(tools::toTitleCase(x))
}
}
df_final$platform <- sapply(df_final$platform, clean_platform)- Format "Rp xxx.xxx" → ubah ke numerik
- Jika nilai < 0 → ubah menjadi 0
Proses cleaning meliputi penghapusan simbol seperti “Rp”, titik, dan koma menggunakan
gsub(), kemudian dikonversi ke numerik dengan as.numeric(). Fungsi ini diterapkan ke seluruh kolom menggunakan lapply() dan sapply() sebagai perulangan (loop) implisit
df_clean <- dataset_utama
kolom_harga <- c("unit_price", "gross_sales", "discount_value",
"shipping_cost", "net_sales")
clean_price <- function(x) {
if (is.na(x)) return(x) # jika kosong → return apa adanya
x <- as.character(x)
x <- gsub("Rp", "", x) # hapus Rp
x <- gsub("\\.", "", x) # hapus titik
x <- gsub(",", "", x) # hapus koma
x_num <- suppressWarnings(as.numeric(x)) # ubah ke angka
if (is.na(x_num)) return(NULL) # jika gagal → return NULL
if (x_num < 0) x_num <- 0 # jika negatif → jadi 0
return(x_num) # return hasil
}
# APPLY CLEANING KE SEMUA KOLOM HARGA
df_clean[kolom_harga] <- lapply(df_clean[kolom_harga], function(col) {
sapply(col, clean_price)
})payment_method dan customer_rating. Nilai yang kosong (NA atau string kosong) diperbaiki agar tidak mengganggu proses analisis.
Untuk kolom payment_method, nilai kosong diisi dengan "Unknown" karena tidak tersedia informasi metode pembayaran. Sedangkan pada kolom customer_rating, nilai kosong diisi dengan 0 sebagai nilai default untuk merepresentasikan tidak adanya penilaian dari pelanggan.Fungsi yang digunakan:
1.
ifelse() → untuk mengganti nilai yang kosong dengan nilai default2.
is.na() → untuk mendeteksi nilai NA3.
trimws() → untuk mendeteksi string kosong ("")4.
as.numeric() → untuk memastikan tipe data rating menjadi numerik
df_clean <- dataset_utama
df_clean$payment_method <- ifelse(
is.na(df_clean$payment_method) | trimws(df_clean$payment_method) == "",
"Unknown",
df_clean$payment_method
)
df_clean$customer_rating <- ifelse(
is.na(df_clean$customer_rating) | trimws(as.character(df_clean$customer_rating)) == "",
0,
df_clean$customer_rating
)
df_clean$customer_rating <- as.numeric(df_clean$customer_rating)order_status
standardisasi_status() yang menggunakan percabangan if–else untuk menyeragamkan berbagai variasi status pesanan. Proses pembersihan dilakukan dengan tolower() dan trimws(), lalu diterapkan ke seluruh data menggunakan sapply() sebagai perulangan (loop) implisit.
df_clean <- dataset_utama
sebelum <- data.frame(
Status = names(table(df_clean$order_status, useNA = "ifany")),
Sebelum = as.integer(table(df_clean$order_status, useNA = "ifany"))
)
standardisasi_status <- function(x) {
if (is.na(x)) return(NA)
x_clean <- trimws(tolower(as.character(x)))
if (x_clean %in% c("completed", "delivered")) {
"Completed"
} else if (x_clean %in% c("cancelled", "cancel", "batal")) {
"Cancelled"
} else if (x_clean %in% c("returned", "retur")) {
"Returned"
} else if (x_clean == "shipped") {
"Shipped"
} else if (x_clean == "on delivery") {
"On Delivery"
} else {
x
}
}
df_clean$order_status <- sapply(df_clean$order_status, standardisasi_status)for untuk mengiterasi setiap kolom yang dipilih. Proses cleaning dilakukan dengan trimws() dan tolower() untuk menghilangkan spasi berlebih dan menyeragamkan huruf, serta dihitung perubahan sebelum dan sesudah cleaning. Hasilnya dicatat dalam log_cleaning, diurutkan, dan ditampilkan dalam bentuk tabel agar mudah dianalisis.
kolom_cleaning <- c("category", "product_name", "region")
sebelum_loop <- do.call(rbind, lapply(kolom_cleaning, function(col) {
nilai <- df_clean[[col]]
data.frame(
Kolom = col,
Sebelum = sum(trimws(as.character(nilai)) != as.character(nilai), na.rm = TRUE)
)
}))
log_cleaning <- data.frame()
for (col in kolom_cleaning) {
sebelum_count <- sum(trimws(as.character(df_clean[[col]])) != as.character(df_clean[[col]]), na.rm = TRUE)
df_clean[[col]] <- trimws(tolower(as.character(df_clean[[col]])))
sesudah_count <- sum(trimws(df_clean[[col]]) != df_clean[[col]], na.rm = TRUE)
log_cleaning <- rbind(log_cleaning, data.frame(
Kolom = col,
Sebelum = sebelum_count,
Sesudah = sesudah_count,
Terbersihkan = sebelum_count - sesudah_count
))
}1.4 Section D
is_high_valueorder_priorityvalid_transactiondistinct() berdasarkan order_id, serta memastikan kolom net_sales tersedia dengan if. Selanjutnya dilakukan transformasi data menggunakan mutate() dan percabangan if-else untuk mengisi nilai kosong menjadi 0 dan membuat variabel baru is_high_value. Hasilnya kemudian diringkas dengan count() dan divisualisasikan menggunakan plot_ly() dalam bentuk donut chart.
df_final <- dataset_utama %>%
distinct(order_id, .keep_all = TRUE)
if (!"net_sales" %in% names(df_final)) {
stop("Kolom net_sales tidak ditemukan di dataset_utama")
}
df_final <- df_final %>%
mutate(
net_sales = ifelse(is.na(net_sales), 0, net_sales),
is_high_value = ifelse(net_sales > 1000000, "Yes", "No")
)Berdasarkan visualisasi, terdapat 707 transaksi (36%) yang termasuk dalam kategori high value (“Yes”), sedangkan 1259 transaksi (64%) termasuk dalam kategori non high value (“No”). Hal ini menunjukkan bahwa mayoritas transaksi masih berada pada nilai penjualan di bawah 1.000.000.
order_priority
order_priority menggunakan nested ifelse untuk mengelompokkan nilai net_sales menjadi kategori High, Medium, dan Low. Sebelumnya, data juga dibersihkan dari duplikasi dan nilai kosong. Hasilnya kemudian diringkas dan ditampilkan dalam bentuk pie chart untuk melihat distribusinya.
df_final <- dataset_utama %>%
distinct(order_id, .keep_all = TRUE) %>%
mutate(
net_sales = ifelse(is.na(net_sales), 0, net_sales),
# NESTED IF
order_priority = ifelse(
net_sales >= 1000000,
"High",
ifelse(
net_sales >= 500000,
"Medium",
"Low"
)
)
)Berdasarkan visualisasi, kategori Low memiliki proporsi terbesar (44,2%), diikuti High (36%) dan Medium (19,9%), yang menunjukkan mayoritas transaksi berada di bawah 500.000 (prioritas rendah). Namun, proporsi High yang cukup signifikan mengindikasikan adanya kontribusi transaksi bernilai besar, sementara kategori Medium yang paling sedikit menunjukkan transaksi nilai menengah kurang dominan, sehingga terdapat peluang untuk mendorong peningkatan transaksi dari Low ke Medium atau High.
valid_transaction
valid_transaction menggunakan case_when() untuk mengelompokkan transaksi menjadi valid dan invalid berdasarkan kondisi pada order_status. Transaksi dengan status kosong, NA, Unknown, atau Cancelled dikategorikan sebagai invalid. Hasilnya kemudian diringkas dan divisualisasikan dalam bentuk pie chart untuk melihat perbandingannya.
Kolom valid_transaction dibuat menggunakan logika if, di mana transaksi dengan order_status = “Cancelled” dikategorikan sebagai Invalid, sedangkan selain itu sebagai Valid. Berdasarkan visualisasi, transaksi Valid mendominasi sebesar 97,7%, sementara Invalid hanya 2,3%, yang menunjukkan bahwa hampir seluruh transaksi dalam dataset berhasil diproses dengan baik dan hanya sebagian kecil yang dibatalkan. Hal ini mengindikasikan kualitas transaksi yang cukup baik, meskipun tetap terdapat sejumlah kecil transaksi yang tidak valid yang perlu diperhatikan.
1.5 Section E
Berdasarkan hasil distribusi transaksi setelah proses cleaning data, terlihat bahwa Shopee menjadi platform dengan jumlah transaksi tertinggi yaitu sebanyak 411 transaksi. Secara keseluruhan, perbedaan jumlah transaksi antar platform relatif kecil, yang menunjukkan bahwa distribusi aktivitas pengguna cukup merata tanpa adanya dominasi yang sangat signifikan dari satu platform tertentu. Hal ini mengindikasikan bahwa preferensi pengguna terhadap platform e-commerce yang digunakan dalam analisis ini bersifat kompetitif dan tersebar secara seimbang di beberapa platform utama.
Berdasarkan hasil distribusi kategori setelah proses cleaning data, kategori Fashion menjadi yang paling sering muncul dengan 412 kemunculan. Secara keseluruhan, distribusi data antar kategori relatif seimbang meskipun terdapat sedikit dominasi pada kategori Fashion, Sports, dan Home Living. Hal ini menunjukkan bahwa preferensi atau aktivitas yang tercatat dalam dataset tersebar pada beberapa kategori utama dengan variasi yang tidak terlalu jauh antar kategori.
Berdasarkan hasil distribusi order status setelah proses cleaning data, status transaksi yang paling dominan adalah Delivered dengan total 1.156 order, yang menunjukkan bahwa sebagian besar transaksi telah berhasil sampai ke pelanggan. Secara keseluruhan, distribusi ini menunjukkan bahwa mayoritas transaksi telah mencapai status akhir berupa pengiriman berhasil, namun masih terdapat sebagian kecil transaksi yang mengalami pembatalan, pengembalian, maupun masih dalam proses pengiriman.
2 WEB SCRAPING
Pada bagian ini, dilakukan proses pengambilan data dari beberapa website menggunakan teknik web scraping. Website yang digunakan memiliki karakteristik yang berbeda, seperti static, pagination, AJAX, dan iframe, sehingga diperlukan pendekatan yang berbeda dalam proses pengambilan datanya.
Data yang berhasil dikumpulkan kemudian diolah melalui tahapan data cleaning dan analisis sederhana untuk mendapatkan insight dari data tersebut.
2.1 Website Countries
read_html(). Selanjutnya digunakan loop for untuk mengiterasi setiap elemen negara dan mengambil data seperti nama negara, ibu kota, populasi, dan luas wilayah menggunakan html_node(). Data yang diambil kemudian dibersihkan dengan html_text(trim=TRUE) agar rapi dan siap digunakan.
url <- "https://www.scrapethissite.com/pages/simple/"
page <- read_html(url)
countries <- page %>% html_nodes(".country")
data_list <- list()
for (i in seq_along(countries)) {
country_name <- countries[[i]] %>% html_node(".country-name") %>% html_text(trim = TRUE)
capital <- countries[[i]] %>% html_node(".country-capital") %>% html_text(trim = TRUE)
population <- countries[[i]] %>% html_node(".country-population") %>% html_text(trim = TRUE)
area <- countries[[i]] %>% html_node(".country-area") %>% html_text(trim = TRUE)
data_list[[i]] <- data.frame(
country = country_name,
capital = capital,
population = population,
area = area
)
}
df_countries <- bind_rows(data_list)write.csv(df_countries, "countries.csv", row.names = FALSE)
df_download <- data.frame(
File = "countries.csv",
Download = '<a href="countries.csv" download="countries.csv">⬇ Download CSV</a>'
)-
nrow(df_countries) → menghitung jumlah baris (jumlah data negara)-
ncol(df_countries) → menghitung jumlah kolom (fitur/variabel)-
colnames(df_countries) → menampilkan nama-nama kolom yang ada di dataset-
sapply(df_countries, class) → mengecek tipe data pada setiap kolom (misalnya character, numeric, dll)-
is.na(df_countries) → mendeteksi nilai yang kosong (NA)-
colSums() → menjumlahkan jumlah NA pada setiap kolom-
names(colSums(is.na(df_countries))), colSums(is.na(df_countries))
-
duplicated(df_countries) → mendeteksi baris yang sama persis-
sum() → menghitung total data yang duplikat-
data.frame(Duplicate = sum(duplicated(df_countries)))
-
str_trim() → menghapus spasi berlebih di awal dan akhir teks-
str_to_title() → mengubah teks menjadi format Title Case (huruf awal kapital)-
for loop → melakukan proses ke beberapa kolom sekaligusfor(col in c("country","capital")){
df_countries[[col]] <- str_to_title(str_trim(df_countries[[col]]))
}-
is.na() → mengecek apakah nilai kosong (NA)-
== "" → mengecek string kosong-
as.numeric() → mengubah data menjadi tipe angka-
for loop → memproses setiap baris satu per satu
for(i in 1:nrow(df_countries)){
# population
if(is.na(df_countries$population[i]) || df_countries$population[i] == ""){
df_countries$population[i] <- 0
} else {
df_countries$population[i] <- as.numeric(df_countries$population[i])
}
# area
if(is.na(df_countries$area[i]) || df_countries$area[i] == ""){
df_countries$area[i] <- 0
} else {
df_countries$area[i] <- as.numeric(df_countries$area[i])
}
}
df_countries$population <- as.numeric(df_countries$population)
df_countries$area <- as.numeric(df_countries$area)-
is.na() → mendeteksi nilai yang hilang-
== "" → mendeteksi string kosong- pengisian "Unknown" → mengganti nilai kosong dengan label yang jelas
for(i in 1:nrow(df_countries)){
# Country
if(is.na(df_countries$country[i]) || df_countries$country[i] == ""){
df_countries$country[i] <- "Unknown"
}
# Captial
if(is.na(df_countries$capital[i]) || df_countries$capital[i] == ""){
df_countries$capital[i] <- "Unknown"
}
}
df_countries$population <- as.numeric(df_countries$population)
df_countries$area <- as.numeric(df_countries$area)distinct() untuk mengambil baris unik dan menghapus yang sama
## Tidak ada duplicate data
-
is.na(df_countries$population) → mengecek nilai NA pada kolom population-
df_countries$population == "" → mengecek data kosong berbentuk string-
ifelse(kondisi, nilai_jika_TRUE, nilai_jika_FALSE) → memberikan nilai pengganti jika data kosong- population & area → diisi 0
- country & capital → diisi "Unknown"
# Numerik
df_countries$population <- ifelse(
is.na(df_countries$population) | df_countries$population == "",
0,
df_countries$population
)
df_countries$area <- ifelse(
is.na(df_countries$area) | df_countries$area == "",
0,
df_countries$area
)
# Teks
df_countries$country <- ifelse(
is.na(df_countries$country) | df_countries$country == "",
"Unknown",
df_countries$country
)
df_countries$capital <- ifelse(
is.na(df_countries$capital) | df_countries$capital == "",
"Unknown",
df_countries$capital
)data_status untuk mengklasifikasikan data berdasarkan kelengkapan informasi.- | → operator OR (atau), digunakan untuk menggabungkan beberapa kondisi
- kondisi yang dicek: country bernilai "Unknown", capital bernilai "Unknown", population bernilai 0, area bernilai 0.
- jika salah satu kondisi terpenuhi → data dianggap Incomplete
- jika semua data lengkap → Complete
df_countries$data_status <- ifelse(
df_countries$country == "Unknown" |
df_countries$capital == "Unknown" |
df_countries$population == 0 |
df_countries$area == 0,
"Incomplete",
"Complete"
)2.2 Website Hockey
base_url sebagai URL dasar yang akan digabung dengan nomor halaman & df_hockey sebagai wadah kosong untuk menyimpan hasil scraping
base_url <- "https://www.scrapethissite.com/pages/forms/?page_num="
data <- list()
current_page <- 1
proceed <- TRUEloop while untuk berpindah halaman secara otomatis hingga data habis. Di dalamnya digunakan loop for untuk mengiterasi setiap baris tabel, serta percabangan if untuk menghentikan proses jika tidak ada data atau baris tidak valid. Data yang diambil kemudian disimpan dan digabungkan menjadi satu dataset untuk ditampilkan dalam bentuk tabel.
base_url <- "https://www.scrapethissite.com/pages/forms/?page_num="
data <- list()
current_page <- 1
proceed <- TRUE
while (proceed) {
cat("Scraping page:", current_page, "\n")
page <- read_html(paste0(base_url, current_page))
rows <- page %>% html_nodes("table tr")
rows <- rows[-1]
if (length(rows) == 0) {
proceed <- FALSE
break
}
for (row in rows) {
cols <- row %>% html_nodes("td")
if (length(cols) < 8) next
item <- data.frame(
team_name = cols[[1]] %>% html_text(trim = TRUE),
year = cols[[2]] %>% html_text(trim = TRUE),
wins = cols[[4]] %>% html_text(trim = TRUE),
win_pct = cols[[6]] %>% html_text(trim = TRUE),
goals_for = cols[[7]] %>% html_text(trim = TRUE),
goals_against = cols[[8]] %>% html_text(trim = TRUE)
)
data <- append(data, list(item))
}
current_page <- current_page + 1
}## Scraping page: 1
## Scraping page: 2
## Scraping page: 3
## Scraping page: 4
## Scraping page: 5
## Scraping page: 6
## Scraping page: 7
## Scraping page: 8
## Scraping page: 9
## Scraping page: 10
## Scraping page: 11
## Scraping page: 12
## Scraping page: 13
## Scraping page: 14
## Scraping page: 15
## Scraping page: 16
## Scraping page: 17
## Scraping page: 18
## Scraping page: 19
## Scraping page: 20
## Scraping page: 21
## Scraping page: 22
## Scraping page: 23
## Scraping page: 24
## Scraping page: 25
write.csv(df_hockey, "hockey.csv", row.names = FALSE)
df_download_hockey <- data.frame(
File = "hockey.csv",
Download = '<a href="hockey.csv" download="hockey.csv">⬇ Download CSV</a>'
)-
nrow(df_hockey) → menghitung jumlah baris (total data tim hockey)-
ncol(df_hockey) → menghitung jumlah kolom (fitur/variabel)-
colnames(df_hockey) → menampilkan nama kolom dalam dataset-
sapply(df_hockey, class) → mengecek tipe data pada setiap kolom (character, numeric, dll)
is.na(df_hockey) untuk mendeteksi nilai yang kosong (NA) dan kode colSums() untuk menghitung jumlah missing value pada setiap kolom
duplicated(df_hockey) untuk mendeteksi baris data yang sama persis dan kode sum() untuk menghitung jumlah total data yang duplikat.Tujuan tahap ini untuk memastikan data tidak berulang agar hasil analisis lebih akurat dan tidak bias.
str_trim() untuk menghapus spasi berlebih di awal dan akhir teks dan kode str_to_title() untuk mengubah teks menjadi format Title Case (huruf awal kapital setiap kata).
as.numeric() untuk mengonversi data teks menjadi angka.
for(i in 1:nrow(df_hockey)){
# wins
if(is.na(df_hockey$wins[i]) || df_hockey$wins[i] == ""){
df_hockey$wins[i] <- 0
} else {
df_hockey$wins[i] <- as.numeric(df_hockey$wins[i])
}
# year
if(is.na(df_hockey$year[i]) || df_hockey$year[i] == ""){
df_hockey$year[i] <- 0
} else {
df_hockey$year[i] <- as.numeric(df_hockey$year[i])
}
# win_pct
if(is.na(df_hockey$win_pct[i]) || df_hockey$win_pct[i] == ""){
df_hockey$win_pct[i] <- 0
} else {
df_hockey$win_pct[i] <- as.numeric(df_hockey$win_pct[i])
}
# goals_for
if(is.na(df_hockey$goals_for[i]) || df_hockey$goals_for[i] == ""){
df_hockey$goals_for[i] <- 0
} else {
df_hockey$goals_for[i] <- as.numeric(df_hockey$goals_for[i])
}
# goals_against
if(is.na(df_hockey$goals_against[i]) || df_hockey$goals_against[i] == ""){
df_hockey$goals_against[i] <- 0
} else {
df_hockey$goals_against[i] <- as.numeric(df_hockey$goals_against[i])
}
}
df_hockey$wins <- as.numeric(df_hockey$wins)
df_hockey$year <- as.numeric(df_hockey$year)
df_hockey$win_pct <- as.numeric(df_hockey$win_pct)
df_hockey$goals_for <- as.numeric(df_hockey$goals_for)
df_hockey$goals_against <- as.numeric(df_hockey$goals_against)-
for (i in 1:nrow(df_hockey)) → melakukan pengecekan setiap baris data-
is.na() → mengecek nilai kosong (NA)-
if() → memberikan kondisi untuk mengganti nilai jika kosongYang dilakukan:
- wins atau losses NA → diganti 0
- team kosong → diganti "Unknown"
- wins < 0 → diperbaiki menjadi 0 (validasi data tidak logis)
for (i in 1:nrow(df_hockey)) {
# cek missing numeric
if (is.na(df_hockey$wins[i])) {
df_hockey$wins[i] <- 0
}
# cek missing teks / validitas
if (is.na(df_hockey$team[i]) || df_hockey$team[i] == "") {
df_hockey$team[i] <- "Unknown"
}
# validasi tambahan (opsional tapi bagus)
if (df_hockey$wins[i] < 0) {
df_hockey$wins[i] <- 0
}
}distinct() untuk menghapus baris yang memiliki nilai identik. Dengan tujuan untuk menghindari data ganda yang dapat menyebabkan bias pada hasil analisis.
## Tidak ada duplicate data
-
is.na() → mengecek nilai kosong (NA)-
df_hockey$team == "" → mengecek string kosong-
ifelse(kondisi, nilai_jika_TRUE, nilai_jika_FALSE) → memberikan nilai pengganti jika data tidak validaturan yang digunakan:
- team kosong → "Unknown"
- wins negatif atau NA → 0
- losses negatif atau NA → 0
- year kosong → 0
df_hockey$team <- ifelse(is.na(df_hockey$team) | df_hockey$team == "",
"Unknown",
df_hockey$team)
df_hockey$wins <- ifelse(is.na(df_hockey$wins) | df_hockey$wins < 0,
0,
df_hockey$wins)
df_hockey$year <- ifelse(is.na(df_hockey$year),
0,
df_hockey$year)-
table(df_hockey$data_status) → menghitung jumlah data Complete dan Incomplete-
as.data.frame(status_count) → mengubah hasil tabel menjadi format data frame-
datatable() → menampilkan dataset dalam bentuk tabel interaktif
# hapus kolom team yang duplikat
df_hockey$team <- NULL
# tambah kolom data_status seperti Python
df_hockey$data_status <- ifelse(
df_hockey$team_name == "Unknown" | df_hockey$wins == 0,
"Incomplete",
"Complete"
)2.3 Website Movie
base_url sebagai URL dasar yang digabung dengan tahun, years sebagai daftar tahun yang akan di-request, dan data_ajax sebagai wadah kosong untuk menyimpan hasil scraping.
base_url <- "https://www.scrapethissite.com/pages/ajax-javascript/?ajax=true&year="
years <- 2010:2015 # range tahun seperti Python
data_ajax <- list() # list kosong untuk simpan hasilloop for. Data JSON yang diterima kemudian di-parse menggunakan fromJSON() dan setiap film diiterasi dengan loop for di dalamnya untuk mengambil kolom yang dibutuhkan.
for (year in years) { # looping tiap tahun
cat("Request year:", year, "\n") # info progress
url <- paste0(base_url, year) # URL dinamis per tahun
response <- tryCatch( # request ke server
fromJSON(url),
error = function(e) {
cat("Request gagal di year:", year, "\n")
NULL
}
)
if (is.null(response)) next # skip jika gagal
for (i in seq_len(nrow(response))) { # looping tiap film
film <- response[i, ] # ambil 1 baris
item <- data.frame(
movie_title = ifelse(is.null(film$title), "Unknown", film$title),
year = ifelse(is.null(film$year), year, film$year),
nomination = ifelse(is.null(film$nominations), 0, film$nominations),
awards = ifelse(is.null(film$awards), 0, film$awards),
description = ifelse(is.null(film$description), NA, film$description),
stringsAsFactors = FALSE
)
data_ajax <- append(data_ajax, list(item)) # simpan ke list
}
}## Request year: 2010
## Request year: 2011
## Request year: 2012
## Request year: 2013
## Request year: 2014
## Request year: 2015
write.csv(df_ajax, "movie_data.csv", row.names = FALSE)
df_download_ajax <- data.frame(
File = "movie_data.csv",
Download = '<a href="movie_data.csv" download="movie_data.csv">⬇ Download CSV</a>'
)-
nrow(df_ajax) → menghitung jumlah baris-
ncol(df_ajax) → menghitung jumlah kolom-
colnames(df_ajax) → menampilkan nama kolom-
sapply(df_ajax, class) → mengecek tipe data setiap kolom
is.na(df_ajax) untuk mendeteksi nilai kosong (NA) dan kode colSums() untuk menjumlahkan jumlah NA tiap kolom
duplicated(df_ajax) untuk mendeteksi baris yang sama persis dan kode sum() untuk menghitung total data duplikat.
movie_title dan description.-
str_trim() → menghapus spasi berlebih-
str_to_title() → mengubah ke format Title Case-
for loop → memproses beberapa kolom sekaligus
for (col in c("movie_title", "description")) {
df_ajax[[col]] <- sapply(df_ajax[[col]], function(x) {
if (is.na(x)) return(x)
str_to_title(str_trim(x))
})
}-
is.na() → mengecek nilai kosong-
== "" → mengecek string kosong-
as.numeric() → mengubah ke tipe angka-
for loop → memproses setiap baris satu per satu
for (col in c("year", "nomination", "awards")) {
for (i in 1:nrow(df_ajax)) {
if (is.na(df_ajax[[col]][i]) || df_ajax[[col]][i] == "") {
df_ajax[[col]][i] <- 0
} else {
df_ajax[[col]][i] <- as.numeric(df_ajax[[col]][i])
}
}
df_ajax[[col]] <- as.numeric(df_ajax[[col]])
}-
is.na() → mendeteksi nilai hilang-
== "" → mendeteksi string kosong- pengisian "Data not available" → mengganti nilai kosong dengan label yang jelas
for (i in 1:nrow(df_ajax)) {
# movie_title
if (is.na(df_ajax$movie_title[i]) || df_ajax$movie_title[i] == "") {
df_ajax$movie_title[i] <- "Unknown"
}
# description
if (is.na(df_ajax$description[i]) || df_ajax$description[i] == "") {
df_ajax$description[i] <- "Data not available"
}
}distinct() untuk menghindari bias pada hasil analisis.
## Tidak ada duplicate data
ifelse().-
movie_title kosong → "Unknown"- description kosong → "Data not available"
- nomination atau awards negatif/NA → 0
df_ajax$movie_title <- ifelse(
is.na(df_ajax$movie_title) | df_ajax$movie_title == "",
"Unknown", df_ajax$movie_title
)
df_ajax$description <- ifelse(
is.na(df_ajax$description) | df_ajax$description == "",
"Data not available", df_ajax$description
)
df_ajax$nomination <- ifelse(
is.na(df_ajax$nomination) | df_ajax$nomination < 0,
0, df_ajax$nomination
)
df_ajax$awards <- ifelse(
is.na(df_ajax$awards) | df_ajax$awards < 0,
0, df_ajax$awards
)data_status untuk mengklasifikasikan kelengkapan data.- jika movie_title "Unknown" atau description "Data not available" → Incomplete
- jika semua lengkap → Complete
df_ajax$data_status <- ifelse(
df_ajax$movie_title == "Unknown" |
tolower(df_ajax$description) == "data not available",
"Incomplete",
"Complete"
)2.4 Website Turtles
url sebagai URL halaman utama dan data_list sebagai wadah kosong untuk menyimpan hasil scraping sementara.
read_html(). Website ini menggunakan iframe sehingga konten utamanya tidak langsung tersedia di halaman utama dan harus diakses melalui URL iframe terpisah.
iframe_url <- "https://www.scrapethissite.com/pages/frames/?frame=i"
iframe_page <- read_html(iframe_url)loop for untuk mengiterasi setiap card, lalu membuka halaman detail setiap item untuk mengambil deskripsi menggunakan read_html().-
html_nodes(".col-md-4") → mengambil setiap card/item-
html_node("h3") → mengambil nama item-
html_node("a") → mengambil link detail-
html_nodes("p") → mengambil paragraf deskripsi dari halaman detail-
ifelse(is.null(...)) → mengisi "Unknown" jika data tidak ada-
sample() → membuat data tambahan nomination dan awards secara acak
cards <- iframe_page %>% html_nodes(".col-md-4")
for (card in cards) {
item <- list()
# NAME
name_tag <- card %>% html_node("h3")
item$name <- ifelse(
is.null(name_tag),
"Unknown",
name_tag %>% html_text(trim = TRUE)
)
# DETAIL LINK
link_tag <- card %>% html_node("a")
if (!is.null(link_tag)) {
detail_url <- paste0(
"https://www.scrapethissite.com",
link_tag %>% html_attr("href")
)
detail_page <- read_html(detail_url)
paragraphs <- detail_page %>% html_nodes("p")
item$description <- ifelse(
length(paragraphs) > 0,
paragraphs[1] %>% html_text(trim = TRUE),
"Unknown"
)
item$additional_info <- ifelse(
length(paragraphs) > 1,
paragraphs[2] %>% html_text(trim = TRUE),
"No additional info"
)
} else {
item$description <- "Unknown"
item$additional_info <- "No additional info"
}
data_list[[length(data_list) + 1]] <- as.data.frame(item)
}
df_turtles <- bind_rows(data_list)write.csv(df_turtles, "turtles.csv", row.names = FALSE)
df_download_turtles <- data.frame(
File = "turtles.csv",
Download = '<a href="turtles.csv" download="turtles.csv">⬇ Download CSV</a>'
)-
nrow(df_turtles) → menghitung jumlah baris-
ncol(df_turtles) → menghitung jumlah kolom-
colnames(df_turtles) → menampilkan nama kolom-
sapply(df_turtles, class) → mengecek tipe data setiap kolom
-
is.na(df_turtles) → mendeteksi nilai kosong (NA)-
colSums() → menjumlahkan jumlah NA tiap kolom
-
duplicated(df_turtles) → mendeteksi baris yang sama persis-
sum() → menghitung total data duplikat
additional_info.-
str_trim() → menghapus spasi berlebih di awal dan akhir teks-
str_to_title() → mengubah teks menjadi format Title Case-
for loop → memproses beberapa kolom sekaligus
for (col in c("name", "description", "additional_info")) {
df_turtles[[col]] <- str_to_title(str_trim(df_turtles[[col]]))
}summary_turtles <- data.frame(
Column = names(df_turtles),
Data_Type = sapply(df_turtles, class)
)
datatable(
summary_turtles,
rownames = FALSE,
options = list,
class = "stripe hover compact cell-border"
)-
is.na() → mendeteksi nilai hilang-
== "" → mendeteksi string kosong-
name kosong → "Unknown"-
description kosong → "Unknown"-
additional_info kosong → "No additional info"
for (i in 1:nrow(df_turtles)) {
if (is.na(df_turtles$name[i]) || df_turtles$name[i] == "") {
df_turtles$name[i] <- "Unknown"
}
if (is.na(df_turtles$description[i]) || df_turtles$description[i] == "") {
df_turtles$description[i] <- "Unknown"
}
if (is.na(df_turtles$additional_info[i]) || df_turtles$additional_info[i] == "") {
df_turtles$additional_info[i] <- "No additional info"
}
}distinct() untuk menghindari bias pada hasil analisis.
## Tidak ada duplicate data
ifelse().-
name kosong → "Unknown"-
description kosong → "Unknown"-
additional_info kosong → "No additional info"df_turtles$name <- ifelse(
is.na(df_turtles$name) | df_turtles$name == "",
"Unknown", df_turtles$name
)
df_turtles$description <- ifelse(
is.na(df_turtles$description) | df_turtles$description == "",
"Unknown", df_turtles$description
)
df_turtles$additional_info <- ifelse(
is.na(df_turtles$additional_info) | df_turtles$additional_info == "",
"No additional info", df_turtles$additional_info
)data_status untuk mengklasifikasikan kelengkapan data.-
| → operator OR, menggabungkan beberapa kondisi-
name = "Unknown" → data tidak lengkap-
description = "Unknown" → data tidak lengkap-
additional_info = "No additional info" → data tidak lengkap- jika salah satu terpenuhi "Incomplete", jika semua lengkap "Complete"
df_turtles$data_status <- ifelse(
df_turtles$name == "Unknown" |
df_turtles$description == "Unknown" |
df_turtles$additional_info == "No additional info",
"Incomplete",
"Complete"
)2.5 Section E Web Scraping
Website yang paling mudah untuk di-scrape adalah Countries of the World (Static Page).
Hal ini karena:
- Semua data sudah tersedia langsung di dalam HTML halaman utama
- Struktur tag HTML rapi dan konsisten (tiap negara punya format yang sama)
- Tidak membutuhkan interaksi tambahan seperti klik, scroll, atau request ulang
- Tidak ada pagination, iframe, maupun JavaScript dinamis
- Data bisa langsung diambil menggunakan
read_html()dan selector sepertihtml_nodes()
Dengan kondisi seperti ini, proses scraping menjadi sangat stabil, cepat, dan minim error karena tidak ada proses tambahan yang harus ditangani.
Website yang paling sulit adalah Oscar Winning Films (AJAX / JavaScript Dynamic Loading) https://www.scrapethissite.com/pages/ajax-javascript/
Kesulitannya terletak pada cara website memuat data:
- Data tidak muncul langsung di HTML awal (hanya kerangka kosong)
- Isi tabel baru muncul setelah JavaScript dijalankan
- Data diambil melalui AJAX request ke backend
- Harus mencari endpoint API menggunakan inspect network
- Response biasanya berbentuk JSON, bukan HTML
- Tidak bisa hanya menggunakan
rvestkarena data belum tersedia saat halaman dibuka - Rentan error jika endpoint berubah atau request tidak sesuai format
Karena itu, scraping website jenis ini membutuhkan pemahaman tambahan tentang network request dan API structure, bukan hanya HTML parsing.
Static Website
- Data langsung tersedia di HTML
- Cukup menggunakan
read_html()dan selector sepertihtml_nodes() - Tidak perlu looping atau request tambahan
Pagination Website
- Data terbagi ke beberapa halaman
- Perlu looping berdasarkan pola URL (misalnya
page_num=1,2,3,...) - Data dari setiap halaman harus digabung menjadi satu dataset
AJAX / Dynamic Website
- Data tidak ada di HTML awal
- Dimuat melalui JavaScript menggunakan API request
- Perlu mengambil data dari network (biasanya JSON)
- Tidak bisa hanya mengandalkan scraping HTML biasa
Iframe Website
- Data berada di halaman terpisah yang ditanam dalam iframe
- Harus mengambil
iframe srcterlebih dahulu - Lalu melakukan request ulang ke halaman iframe untuk mengambil data
- Kadang masih perlu masuk ke halaman detail tambahan
Dari seluruh proses scraping, dapat disimpulkan beberapa hal penting:
- Tingkat kesulitan scraping sangat bergantung pada struktur website, bukan hanya jumlah data
- Static website paling stabil karena tidak bergantung pada JavaScript atau request tambahan
- Pagination masih cukup terstruktur karena pola URL biasanya konsisten
- AJAX dan iframe jauh lebih kompleks karena data tidak langsung tersedia di HTML
- Pada beberapa kasus (iframe dan AJAX), data bisa gagal diambil jika request tidak sesuai atau link berubah
- Semakin banyak layer (detail page / iframe / API), semakin tinggi risiko data tidak lengkap
- Sebelum melakukan scraping, sebaiknya analisis struktur website terlebih dahulu menggunakan Inspect Element dan Network Tab
- Pilih metode scraping yang sesuai dengan jenis website (static, pagination, iframe, AJAX) agar proses lebih efisien dan minim error
- Untuk website AJAX, lebih disarankan mengambil data langsung dari API daripada parsing HTML
- Selalu siapkan penanganan error (try-catch / validation) terutama pada website dengan struktur dinamis
- Mulai dari static website terlebih dahulu untuk memahami dasar sebelum masuk ke struktur yang lebih kompleks