1 Pendahuluan

1.1 Latar Belakang

Kemajuan teknologi digital membuat banyak orang dapat bekerja secara lepas (freelance) tanpa terikat pada satu tempat kerja. Para freelancer di pasar kerja ini sangat beragam, baik dari segi usia, keahlian, lama pengalaman, maupun tarif yang ditawarkan. Lama pengalaman kerja (years of experience) menarik untuk ditelaah karena diduga berkaitan dengan besarnya tarif per jam (hourly rate).

Analisis ini memakai Global Freelancers (Raw) Dataset dari Kaggle (Ahir, 2025) yang memuat 1.000 observasi freelancer dan 12 variabel. Variabel tersebut mencakup karakteristik individu, pengalaman kerja, tarif jasa, rating, kepuasan klien, dan status keaktifan.

Sebelum dianalisis, data harus diperiksa lebih dulu untuk menemukan nilai yang hilang (missing value), kategori yang tidak seragam, kesalahan format, baris ganda, serta nilai yang tampak tidak wajar. Pembersihan dilakukan agar data konsisten. Setelah itu, analisis difokuskan pada gambaran pengalaman kerja freelancer dan hubungannya dengan tarif per jam. Pendekatan yang dipakai mengikuti prinsip analisis data eksploratif (exploratory data analysis), yaitu memeriksa struktur, sebaran, dan kejanggalan data sebelum menarik kesimpulan (Tukey, 1993; Velleman & Hoaglin, 2004).

1.2 Rumusan Masalah

  1. Masalah kualitas data apa saja yang ada pada Global Freelancers Dataset?
  2. Bagaimana tahapan preprocessing menangani nilai hilang, format yang tidak seragam, dan tipe data yang tidak sesuai?
  3. Seperti apa karakteristik pengalaman kerja freelancer setelah data dibersihkan, dilihat dari statistik deskriptif dan uji normalitas?
  4. Bagaimana hubungan antara pengalaman kerja dan tarif per jam freelancer?

1.3 Tujuan Analisis

  1. Menemukan masalah kualitas data pada Global Freelancers Dataset.
  2. Menjalankan preprocessing untuk menangani nilai hilang, menyeragamkan format, dan memperbaiki tipe data.
  3. Menggambarkan karakteristik pengalaman kerja melalui statistik deskriptif dan pemeriksaan normalitas setelah pembersihan.
  4. Menelaah hubungan antara pengalaman kerja dan tarif per jam freelancer.

2 Import dan Gambaran Dataset

Data bersumber dari Global Freelancers (Raw) Dataset di Kaggle (Ahir, 2025). File Excel dibaca dari folder yang sama dengan file .Rmd ini. Jika nama file sedikit berbeda (misalnya ada tambahan (1) dari hasil unduhan), kode akan mencari file .xlsx yang namanya memuat kata freelancers.

file_excel <- "data_ade_global_freelancers_raw.xlsx"

if (!file.exists(file_excel)) {
  kandidat <- list.files(pattern = "freelancers.*\\.xlsx$", ignore.case = TRUE)
  if (length(kandidat) == 0) {
    stop("File Excel tidak ditemukan di folder: ", getwd(),
         ". Letakkan file .xlsx satu folder dengan file .Rmd ini.")
  }
  file_excel <- kandidat[1]
}

ADE <- readxl::read_excel(file_excel)
ADE <- as.data.frame(ADE)

knitr::kable(
  data.frame(
    Keterangan = c("Jumlah observasi", "Jumlah variabel"),
    Nilai = c(nrow(ADE), ncol(ADE))
  ),
  caption = "Ringkasan ukuran dataset"
)
Ringkasan ukuran dataset
Keterangan Nilai
Jumlah observasi 1000
Jumlah variabel 12
knitr::kable(
  data.frame(Nama_Variabel = names(ADE)),
  caption = "Daftar variabel dalam dataset"
)
Daftar variabel dalam dataset
Nama_Variabel
freelancer_ID
name
gender
age
country
language
primary_skill
years_of_experience
hourly_rate (USD)
rating
is_active
client_satisfaction
head(ADE, 10) |>
  knitr::kable(caption = "Sepuluh observasi pertama")
Sepuluh observasi pertama
freelancer_ID name gender age country language primary_skill years_of_experience hourly_rate (USD) rating is_active client_satisfaction
FL250001 Ms. Nicole Kidd f 52.0 Italy Italian Blockchain Development 11.0 100 NA 0 NA
FL250002 Vanessa Garcia FEMALE 52.0 Australia English Mobile Apps 34.0 USD 100 3.3 1 0.84
FL250003 Juan Nelson male 53.0 Germany German Graphic Design 31.0 50 0.0 N 0.71
FL250004 Amanda Spencer F 38.0 Australia English Web Development 4.0 $40 1.5 N 0.90
FL250005 Lynn Curtis DDS female 53.0 Germany German Web Development 27.0 30 4.8 0 0.83
FL250006 Lisa Johnson female 59.0 Netherlands Dutch AI 14.0 $30 2.4 FALSE NA
FL250007 Eric Myers m 52.0 Indonesia Indonesian Data Analysis 10.0 USD 75 3.1 0 0.94
FL250008 Ricky Graham male 43.0 Italy Italian Blockchain Development 14.0 USD 40 4.6 FALSE 0.94
FL250009 Sean Martin male 26.0 United States English Blockchain Development 4.0 NA 4.0 TRUE 0.76
FL250010 Matthew Lloyd MALE 52.0 Turkey Turkish AI 22.0 $50 NA 1 NA
str(ADE)
## 'data.frame':    1000 obs. of  12 variables:
##  $ freelancer_ID      : chr  "FL250001" "FL250002" "FL250003" "FL250004" ...
##  $ name               : chr  "Ms. Nicole Kidd" "Vanessa Garcia" "Juan Nelson" "Amanda Spencer" ...
##  $ gender             : chr  "f" "FEMALE" "male" "F" ...
##  $ age                : chr  "52.0" "52.0" "53.0" "38.0" ...
##  $ country            : chr  "Italy" "Australia" "Germany" "Australia" ...
##  $ language           : chr  "Italian" "English" "German" "English" ...
##  $ primary_skill      : chr  "Blockchain Development" "Mobile Apps" "Graphic Design" "Web Development" ...
##  $ years_of_experience: chr  "11.0" "34.0" "31.0" "4.0" ...
##  $ hourly_rate (USD)  : chr  "100" "USD 100" "50" "$40" ...
##  $ rating             : chr  NA "3.3" "0.0" "1.5" ...
##  $ is_active          : chr  "0" "1" "N" "N" ...
##  $ client_satisfaction: num  NA 0.84 0.71 0.9 0.83 NA 0.94 0.94 0.76 NA ...
summary(ADE)
##  freelancer_ID          name              gender              age           
##  Length:1000        Length:1000        Length:1000        Length:1000       
##  Class :character   Class :character   Class :character   Class :character  
##  Mode  :character   Mode  :character   Mode  :character   Mode  :character  
##                                                                             
##                                                                             
##                                                                             
##                                                                             
##    country            language         primary_skill      years_of_experience
##  Length:1000        Length:1000        Length:1000        Length:1000        
##  Class :character   Class :character   Class :character   Class :character   
##  Mode  :character   Mode  :character   Mode  :character   Mode  :character   
##                                                                              
##                                                                              
##                                                                              
##                                                                              
##  hourly_rate (USD)     rating           is_active         client_satisfaction
##  Length:1000        Length:1000        Length:1000        Min.   :  0.60     
##  Class :character   Class :character   Class :character   1st Qu.:  0.71     
##  Mode  :character   Mode  :character   Mode  :character   Median :  0.82     
##                                                           Mean   : 11.73     
##                                                           3rd Qu.:  0.94     
##                                                           Max.   :100.00     
##                                                           NA's   :176

Interpretasi. Dataset terdiri dari 1.000 freelancer dan 12 variabel: identitas (freelancer_ID, name), demografi (gender, age, country, language), profesi (primary_skill, years_of_experience, hourly_rate (USD)), dan kinerja (rating, is_active, client_satisfaction). Kolom hourly_rate (USD) dan is_active masih berupa teks karena formatnya campur aduk (mis. “$40”, “USD 75”, “yes”, “N”), sehingga belum bisa dihitung. Pada ringkasan numerik, client_satisfaction bernilai maksimum 100 dengan rata-rata sekitar 11,7 padahal mediannya hanya 0,82. Ini tanda awal bahwa kolom tersebut memakai dua skala berbeda.

3 Pemeriksaan Permasalahan Data

3.1 Missing Value

Missing value adalah nilai yang tidak tersedia pada suatu observasi. Persentasenya dihitung terhadap jumlah seluruh baris.

missing_awal <- data.frame(
  Variabel = names(ADE),
  Jumlah_NA = colSums(is.na(ADE)),
  Persentase_NA = round(colMeans(is.na(ADE)) * 100, 2)
)

knitr::kable(
  missing_awal,
  caption = "Jumlah dan persentase missing value sebelum cleaning",
  digits = 2,
  row.names = FALSE
)
Jumlah dan persentase missing value sebelum cleaning
Variabel Jumlah_NA Persentase_NA
freelancer_ID 0 0.0
name 0 0.0
gender 0 0.0
age 30 3.0
country 0 0.0
language 0 0.0
primary_skill 0 0.0
years_of_experience 51 5.1
hourly_rate (USD) 94 9.4
rating 101 10.1
is_active 89 8.9
client_satisfaction 176 17.6
cat("Total missing value:", sum(is.na(ADE)), "\n")
## Total missing value: 541

Interpretasi. Terdapat 541 sel kosong. Kolom client_satisfaction paling banyak kosong (176 baris, 17,6%), disusul rating (101; 10,1%), hourly_rate (USD) (94; 9,4%), is_active (89; 8,9%), years_of_experience (51; 5,1%), dan age (30; 3,0%). Kolom identitas, gender, country, language, dan primary_skill terisi lengkap. Karena semua persentase di bawah 20%, imputasi masih layak dilakukan, walaupun client_satisfaction perlu diwaspadai.

3.2 Duplikasi Awal

jumlah_duplikat_awal <- sum(duplicated(ADE))
cat("Jumlah baris duplikat sebelum dihapus:", jumlah_duplikat_awal, "\n")
## Jumlah baris duplikat sebelum dihapus: 0
if (jumlah_duplikat_awal > 0) {
  knitr::kable(
    head(ADE[duplicated(ADE), , drop = FALSE], 10),
    caption = "Contoh baris duplikat"
  )
}

Interpretasi. Tidak ada baris yang terduplikasi (0), dan setiap freelancer_ID unik. Namun pemeriksaan ini diulang setelah pembersihan format, karena dua baris yang sama bisa tampak berbeda hanya akibat penulisan yang tidak seragam.

4 Data Cleaning

4.1 Membersihkan Gender

Variasi penulisan seperti f, female, m, dan male disatukan agar tidak dihitung sebagai kategori yang berbeda.

if ("gender" %in% names(ADE)) {
  print(table(ADE$gender, useNA = "ifany"))

  gender_raw <- tolower(trimws(as.character(ADE$gender)))
  ADE$gender <- ifelse(
    gender_raw %in% c("f", "female"), "Female",
    ifelse(
      gender_raw %in% c("m", "male"), "Male",
      ifelse(is.na(ADE$gender) | gender_raw == "", NA_character_,
             trimws(as.character(ADE$gender)))
    )
  )
  cat("\nSesudah cleaning:\n")
  print(table(ADE$gender, useNA = "ifany"))
}
## 
##      f      F female Female FEMALE      m      M   male   Male   MALE 
##    103     90     86     96    115     99    106    100    103    102 
## 
## Sesudah cleaning:
## 
## Female   Male 
##    490    510

Interpretasi. Sebelum dibersihkan, gender tertulis dalam 10 bentuk (F, f, female, FEMALE, Female, M, m, male, MALE, Male). Setelah diseragamkan tersisa dua kategori: Male 510 (51%) dan Female 490 (49%), tanpa NA. Komposisinya hampir seimbang, jadi perbandingan antar gender tidak terganggu oleh perbedaan jumlah.

4.2 Membersihkan Hourly Rate

Simbol dolar, teks USD, koma ribuan, dan spasi dibuang sebelum kolom diubah menjadi angka.

if ("hourly_rate (USD)" %in% names(ADE)) {
  ADE[["hourly_rate (USD)"]] <- bersihkan_numerik(
    ADE[["hourly_rate (USD)"]],
    mata_uang = TRUE
  )
  print(summary(ADE[["hourly_rate (USD)"]]))
  str(ADE[["hourly_rate (USD)"]])
}
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
##   20.00   30.00   40.00   52.46   75.00  100.00      94 
##  num [1:1000] 100 100 50 40 30 30 75 40 NA 50 ...

Interpretasi. Nilai berformat “$40” atau “USD 75” berhasil menjadi angka. Dari 1.000 baris, 906 terisi dan 94 tetap NA (kosong sejak awal, bukan gagal konversi). Tarif berkisar USD 20 sampai USD 100 dengan rata-rata sekitar USD 52,5 dan median USD 40. Rata-rata yang lebih besar dari median menandakan sebaran condong ke kanan.

4.3 Membersihkan Client Satisfaction

if ("client_satisfaction" %in% names(ADE)) {
  ADE$client_satisfaction <- bersihkan_numerik(
    ADE$client_satisfaction,
    persen = TRUE
  )
  print(summary(ADE$client_satisfaction))
}
##    Min. 1st Qu.  Median    Mean 3rd Qu.    Max.    NA's 
##    0.60    0.71    0.82   11.73    0.94  100.00     176

Interpretasi. Kolom ini sudah berupa angka, dan pada data ini tidak ada tanda “%” yang perlu dibuang. Masalah sebenarnya adalah skala: 710 nilai berada pada rentang 0,60 sampai 1,00 (proporsi), sedangkan 114 nilai berada pada rentang 60 sampai 100 (persen). Membuang simbol saja tidak menyatukan kedua skala ini.

4.4 Membersihkan Age, Years of Experience, dan Rating

if ("age" %in% names(ADE)) {
  ADE$age <- bersihkan_numerik(ADE$age, persen = TRUE)
}
if ("years_of_experience" %in% names(ADE)) {
  ADE$years_of_experience <- bersihkan_numerik(
    ADE$years_of_experience,
    persen = TRUE
  )
}
if ("rating" %in% names(ADE)) {
  ADE$rating <- bersihkan_numerik(ADE$rating, persen = TRUE)
}

str(ADE)
## 'data.frame':    1000 obs. of  12 variables:
##  $ freelancer_ID      : chr  "FL250001" "FL250002" "FL250003" "FL250004" ...
##  $ name               : chr  "Ms. Nicole Kidd" "Vanessa Garcia" "Juan Nelson" "Amanda Spencer" ...
##  $ gender             : chr  "Female" "Female" "Male" "Female" ...
##  $ age                : num  52 52 53 38 53 59 52 43 26 52 ...
##  $ country            : chr  "Italy" "Australia" "Germany" "Australia" ...
##  $ language           : chr  "Italian" "English" "German" "English" ...
##  $ primary_skill      : chr  "Blockchain Development" "Mobile Apps" "Graphic Design" "Web Development" ...
##  $ years_of_experience: num  11 34 31 4 27 14 10 14 4 22 ...
##  $ hourly_rate (USD)  : num  100 100 50 40 30 30 75 40 NA 50 ...
##  $ rating             : num  NA 3.3 0 1.5 4.8 2.4 3.1 4.6 4 NA ...
##  $ is_active          : chr  "0" "1" "N" "N" ...
##  $ client_satisfaction: num  NA 0.84 0.71 0.9 0.83 NA 0.94 0.94 0.76 NA ...
summary(ADE)
##  freelancer_ID          name              gender               age       
##  Length:1000        Length:1000        Length:1000        Min.   :20.00  
##  Class :character   Class :character   Class :character   1st Qu.:31.00  
##  Mode  :character   Mode  :character   Mode  :character   Median :41.00  
##                                                           Mean   :40.51  
##                                                           3rd Qu.:51.00  
##                                                           Max.   :60.00  
##                                                           NA's   :30     
##    country            language         primary_skill      years_of_experience
##  Length:1000        Length:1000        Length:1000        Min.   : 0.00      
##  Class :character   Class :character   Class :character   1st Qu.: 3.00      
##  Mode  :character   Mode  :character   Mode  :character   Median : 9.00      
##                                                           Mean   :11.34      
##                                                           3rd Qu.:17.00      
##                                                           Max.   :41.00      
##                                                           NA's   :51         
##  hourly_rate (USD)     rating       is_active         client_satisfaction
##  Min.   : 20.00    Min.   :0.000   Length:1000        Min.   :  0.60     
##  1st Qu.: 30.00    1st Qu.:1.400   Class :character   1st Qu.:  0.71     
##  Median : 40.00    Median :2.600   Mode  :character   Median :  0.82     
##  Mean   : 52.46    Mean   :2.513                      Mean   : 11.73     
##  3rd Qu.: 75.00    3rd Qu.:3.800                      3rd Qu.:  0.94     
##  Max.   :100.00    Max.   :5.000                      Max.   :100.00     
##  NA's   :94        NA's   :101                        NA's   :176

Interpretasi. Ketiga kolom ini sudah berupa angka sejak awal, sehingga langkah ini lebih berfungsi sebagai pengaman tipe data; jumlah NA tidak berubah (usia 30, pengalaman 51, rating 101). Ada satu temuan pada rating: 145 baris bernilai tepat 0, sementara nilai terkecil berikutnya adalah 1,0. Karena tidak ada rating di antara 0 dan 1, angka 0 kemungkinan berarti “belum dinilai”, bukan penilaian terburuk.

4.5 Membersihkan Status Is Active

Status yang dikenali diubah menjadi Active atau Inactive. Nilai kosong atau tidak dikenali dicatat sebagai Unknown agar tidak dianggap sebagai status yang pasti.

if ("is_active" %in% names(ADE)) {
  active_raw <- tolower(trimws(as.character(ADE$is_active)))
  ADE$is_active <- ifelse(
    active_raw %in% c("1", "y", "yes", "true", "active"), "Active",
    ifelse(
      active_raw %in% c("0", "n", "no", "false", "inactive"), "Inactive",
      "Unknown"
    )
  )
  print(table(ADE$is_active, useNA = "ifany"))
}
## 
##   Active Inactive  Unknown 
##      446      465       89

Interpretasi. Delapan bentuk penulisan (0, 1, y, n, yes, no, TRUE, FALSE) kini menjadi tiga kategori: Inactive 465 (46,5%), Active 446 (44,6%), dan Unknown 89 (8,9%). Freelancer aktif dan tidak aktif jumlahnya hampir sama. Kategori Unknown dipertahankan supaya 89 baris tidak terbuang.

4.6 Menghapus Duplikasi

Duplikasi dihapus setelah format dibersihkan, agar baris yang sebenarnya sama tidak lolos hanya karena penulisannya berbeda.

duplikat_sebelum <- sum(duplicated(ADE))
ADE <- ADE[!duplicated(ADE), , drop = FALSE]
duplikat_sesudah <- sum(duplicated(ADE))

knitr::kable(
  data.frame(
    Tahap = c("Sebelum penghapusan", "Sesudah penghapusan"),
    Jumlah_Duplikat = c(duplikat_sebelum, duplikat_sesudah),
    Jumlah_Baris = c(NA_integer_, nrow(ADE))
  ),
  caption = "Pemeriksaan duplikasi"
)
Pemeriksaan duplikasi
Tahap Jumlah_Duplikat Jumlah_Baris
Sebelum penghapusan 0 NA
Sesudah penghapusan 0 1000

Interpretasi. Tetap tidak ada duplikat setelah format diseragamkan, sehingga jumlah data tidak berubah: 1.000 baris.

5 Pemeriksaan Nilai Tidak Masuk Akal (Data Noisy)

Pemeriksaan ini hanya menandai nilai yang patut dicurigai. Nilai tidak langsung dihapus karena perlu dicocokkan dengan sumber data atau definisi variabel, sejalan dengan prinsip analisis eksploratif untuk memeriksa data secara teliti sebelum memodelkannya (Tukey, 1993).

cek_rentang <- function(data, variabel, batas_bawah = -Inf, batas_atas = Inf) {
  if (!variabel %in% names(data)) return(NULL)
  x <- data[[variabel]]
  if (!is.numeric(x)) return(NULL)

  data.frame(
    Variabel = variabel,
    Jumlah_NA = sum(is.na(x)),
    Di_Bawah_Batas = sum(x < batas_bawah, na.rm = TRUE),
    Di_Atas_Batas = sum(x > batas_atas, na.rm = TRUE),
    Minimum = if (all(is.na(x))) NA_real_ else min(x, na.rm = TRUE),
    Maksimum = if (all(is.na(x))) NA_real_ else max(x, na.rm = TRUE)
  )
}

hasil_rentang <- do.call(rbind, Filter(Negate(is.null), list(
  cek_rentang(ADE, "age", 0, 100),
  cek_rentang(ADE, "years_of_experience", 0, Inf),
  cek_rentang(ADE, "rating", 0, 5),
  cek_rentang(ADE, "hourly_rate (USD)", 0, Inf),
  cek_rentang(ADE, "client_satisfaction", 0, 100)
)))

if (!is.null(hasil_rentang) && nrow(hasil_rentang) > 0) {
  knitr::kable(hasil_rentang, digits = 2, row.names = FALSE,
               caption = "Ringkasan pemeriksaan nilai di luar rentang")
} else {
  cat("Tidak ada variabel numerik yang sesuai untuk pemeriksaan rentang.\n")
}
Ringkasan pemeriksaan nilai di luar rentang
Variabel Jumlah_NA Di_Bawah_Batas Di_Atas_Batas Minimum Maksimum
age 30 0 0 20.0 60
years_of_experience 51 0 0 0.0 41
rating 101 0 0 0.0 5
hourly_rate (USD) 94 0 0 20.0 100
client_satisfaction 176 0 0 0.6 100

Interpretasi. Semua variabel lolos pemeriksaan batas: tidak ada usia di luar 0-100, pengalaman negatif, rating di luar 0-5, tarif negatif, maupun kepuasan di luar 0-100. Meski begitu, lolos batas belum tentu berarti data bersih. Ada dua catatan penting:

  1. client_satisfaction memakai dua skala (0,60-1,00 dan 60-100). Batas 0-100 tidak menangkap masalah ini karena kedua skala sama-sama masuk di dalamnya.
  2. rating bernilai 0 sebanyak 145 baris (14,5%), kemungkinan penanda “belum dinilai”.

Kedua hal ini dicatat sebagai keterbatasan di bagian kesimpulan.

6 Membuat Data Clean dan Mengatasi Missing Value

Missing value pada variabel numerik terpilih diisi dengan median. Kolom asli dipertahankan, dan hasil imputasi disimpan pada kolom berakhiran _clean agar perubahannya bisa dibandingkan.

data_clean <- ADE

if ("years_of_experience" %in% names(data_clean)) {
  data_clean$years_of_experience_clean <-
    imputasi_median(data_clean$years_of_experience)
}
if ("hourly_rate (USD)" %in% names(data_clean)) {
  data_clean$hourly_rate_clean <-
    imputasi_median(data_clean[["hourly_rate (USD)"]])
}
if ("rating" %in% names(data_clean)) {
  data_clean$rating_clean <- imputasi_median(data_clean$rating)
}
if ("client_satisfaction" %in% names(data_clean)) {
  data_clean$client_satisfaction_clean <-
    imputasi_median(data_clean$client_satisfaction)
}
if ("age" %in% names(data_clean)) {
  data_clean$age_clean <- imputasi_median(data_clean$age)
}

missing_sesudah <- data.frame(
  Variabel = names(data_clean),
  Jumlah_NA = colSums(is.na(data_clean)),
  Persentase_NA = round(colMeans(is.na(data_clean)) * 100, 2)
)

knitr::kable(
  missing_sesudah,
  caption = "Missing value setelah membuat kolom imputasi",
  digits = 2,
  row.names = FALSE
)
Missing value setelah membuat kolom imputasi
Variabel Jumlah_NA Persentase_NA
freelancer_ID 0 0.0
name 0 0.0
gender 0 0.0
age 30 3.0
country 0 0.0
language 0 0.0
primary_skill 0 0.0
years_of_experience 51 5.1
hourly_rate (USD) 94 9.4
rating 101 10.1
is_active 0 0.0
client_satisfaction 176 17.6
years_of_experience_clean 0 0.0
hourly_rate_clean 0 0.0
rating_clean 0 0.0
client_satisfaction_clean 0 0.0
age_clean 0 0.0

Interpretasi. Median dipilih karena lebih tahan terhadap nilai ekstrem dan sebaran yang condong. Nilai pengisinya: usia 41, pengalaman 9 tahun, tarif USD 40, rating 2,6, dan kepuasan 0,82. Seluruh kolom _clean kini tanpa NA dengan jumlah baris tetap 1.000. Konsekuensinya, imputasi median mempersempit variasi data (simpangan baku tarif turun dari sekitar 27,3 menjadi sekitar 26,3) dan menarik nilai ke tengah. Efek ini paling terasa pada client_satisfaction (17,6% diisi) dan rating (10,1% diisi). Median kepuasan 0,82 juga berasal dari campuran dua skala, sehingga hasilnya perlu dibaca hati-hati.

Catatan: jika suatu kolom seluruhnya kosong, median tidak bisa dihitung dan kolom itu tetap berisi NA. Pada data ini hal tersebut tidak terjadi.

7 Validasi Setelah Cleaning

validasi <- data.frame(
  Pemeriksaan = c(
    "Duplikasi tersisa",
    "Age di bawah 0",
    "Age di atas 100",
    "Pengalaman kerja negatif",
    "Rating di bawah 0",
    "Rating di atas 5",
    "Hourly rate negatif",
    "Satisfaction di bawah 0",
    "Satisfaction di atas 100"
  ),
  Jumlah = c(
    sum(duplicated(data_clean)),
    if ("age" %in% names(data_clean)) sum(data_clean$age < 0, na.rm = TRUE) else NA,
    if ("age" %in% names(data_clean)) sum(data_clean$age > 100, na.rm = TRUE) else NA,
    if ("years_of_experience_clean" %in% names(data_clean)) sum(data_clean$years_of_experience_clean < 0, na.rm = TRUE) else NA,
    if ("rating_clean" %in% names(data_clean)) sum(data_clean$rating_clean < 0, na.rm = TRUE) else NA,
    if ("rating_clean" %in% names(data_clean)) sum(data_clean$rating_clean > 5, na.rm = TRUE) else NA,
    if ("hourly_rate_clean" %in% names(data_clean)) sum(data_clean$hourly_rate_clean < 0, na.rm = TRUE) else NA,
    if ("client_satisfaction_clean" %in% names(data_clean)) sum(data_clean$client_satisfaction_clean < 0, na.rm = TRUE) else NA,
    if ("client_satisfaction_clean" %in% names(data_clean)) sum(data_clean$client_satisfaction_clean > 100, na.rm = TRUE) else NA
  )
)

knitr::kable(validasi, caption = "Hasil validasi data")
Hasil validasi data
Pemeriksaan Jumlah
Duplikasi tersisa 0
Age di bawah 0 0
Age di atas 100 0
Pengalaman kerja negatif 0
Rating di bawah 0 0
Rating di atas 5 0
Hourly rate negatif 0
Satisfaction di bawah 0 0
Satisfaction di atas 100 0

Interpretasi. Semua hitungan bernilai 0, artinya tidak ada duplikat dan imputasi tidak menghasilkan nilai yang melanggar batas logis. Validasi ini hanya menguji batas nilai, bukan konsistensi skala client_satisfaction.

8 Encoding Pengalaman Kerja

Pengalaman kerja dikelompokkan menjadi empat kategori. Pengkodeannya bersifat ordinal: angka kode menunjukkan urutan tingkat pengalaman, bukan jarak numerik yang pasti sama.

Rentang pengalaman Kategori Kode
0 sampai 10 tahun Pemula 1
Lebih dari 10 sampai 20 tahun Menengah 2
Lebih dari 20 sampai 30 tahun Jago 3
Lebih dari 30 tahun Master 4
if ("years_of_experience_clean" %in% names(data_clean)) {
  exp <- data_clean$years_of_experience_clean

  data_clean$kategori_pengalaman <- ifelse(
    is.na(exp), NA_character_,
    ifelse(exp <= 10, "Pemula",
           ifelse(exp <= 20, "Menengah",
                  ifelse(exp <= 30, "Jago", "Master")))
  )

  data_clean$kategori_pengalaman <- factor(
    data_clean$kategori_pengalaman,
    levels = c("Pemula", "Menengah", "Jago", "Master"),
    ordered = TRUE
  )

  data_clean$kategori_encode <- as.integer(data_clean$kategori_pengalaman)

  tabel_encoding <- as.data.frame(
    table(
      Kategori = data_clean$kategori_pengalaman,
      Kode = data_clean$kategori_encode,
      useNA = "no"
    )
  )
  tabel_encoding <- tabel_encoding[tabel_encoding$Freq > 0, ]

  knitr::kable(
    tabel_encoding,
    row.names = FALSE,
    caption = "Frekuensi kategori dan kode pengalaman kerja"
  )
  print(table(data_clean$kategori_pengalaman, useNA = "ifany"))
}
## 
##   Pemula Menengah     Jago   Master 
##      571      252      122       55

Interpretasi. Sebaran kategori: Pemula 571 (57,1%), Menengah 252 (25,2%), Jago 122 (12,2%), dan Master 55 (5,5%). Lebih dari separuh freelancer berpengalaman 10 tahun atau kurang, dan jumlahnya menyusut tajam pada tingkat yang lebih tinggi. Tabel frekuensi menunjukkan tiap kategori hanya berpasangan dengan satu kode (1-4), jadi pengkodean konsisten. Kategori Master hanya berisi 55 data, sehingga perbandingan yang melibatkannya kurang stabil.

9 Visualisasi Data

9.1 Histogram Pengalaman Kerja

if ("years_of_experience_clean" %in% names(data_clean)) {
  ggplot(data_clean, aes(x = years_of_experience_clean)) +
    geom_histogram(
      bins = 20,
      fill = biru_utama,
      color = "white",
      linewidth = 0.5,
      alpha = 0.95
    ) +
    labs(
      title = "Distribusi Pengalaman Kerja Freelancer",
      subtitle = "Frekuensi jumlah tahun pengalaman",
      x = "Pengalaman kerja (tahun)",
      y = "Frekuensi"
    ) +
    tema_laporan
}

Interpretasi. Batang tertinggi berada di sisi kiri dan ekornya memanjang ke kanan (kemencengan sekitar +0,95). Pola ini sejalan dengan dominasi freelancer kategori Pemula. Lonjakan di nilai pengalaman 9 sebagian berasal dari imputasi median.

9.2 Jumlah Freelancer per Kategori Pengalaman

if ("kategori_pengalaman" %in% names(data_clean)) {
  ggplot(data_clean, aes(x = kategori_pengalaman, fill = kategori_pengalaman)) +
    geom_bar(color = "white", linewidth = 0.8, show.legend = FALSE) +
    geom_text(stat = "count", aes(label = after_stat(count)),
              vjust = -0.5, color = biru_tua, fontface = "bold") +
    scale_fill_manual(values = c(
      "Pemula" = biru_utama,
      "Menengah" = biru_muda,
      "Jago" = pink_muda,
      "Master" = pink_tua
    ), na.value = abu_biru) +
    labs(
      title = "Komposisi Freelancer Menurut Pengalaman",
      x = "Kategori pengalaman",
      y = "Jumlah freelancer"
    ) +
    tema_laporan
}

Interpretasi. Jumlah freelancer menurun dari Pemula (571) ke Master (55). Struktur ini berbentuk piramida: semakin tinggi tingkat pengalaman, semakin sedikit orangnya.

9.3 Distribusi Hourly Rate

if ("hourly_rate_clean" %in% names(data_clean)) {
  ggplot(data_clean, aes(x = hourly_rate_clean)) +
    geom_histogram(
      bins = 25,
      fill = pink_tua,
      color = "white",
      linewidth = 0.4,
      alpha = 0.9
    ) +
    labs(
      title = "Distribusi Tarif per Jam",
      subtitle = "Hourly rate dalam USD",
      x = "Hourly rate (USD)",
      y = "Frekuensi"
    ) +
    tema_laporan
}

Interpretasi. Grafik tampak berupa batang-batang terpisah karena tarif hanya memiliki enam nilai berbeda: 20, 30, 40, 50, 75, dan 100 USD. Batang tertinggi ada di USD 40 (nilai median yang dipakai untuk mengisi data kosong), dan sebaran condong ke kanan.

9.4 QQ Plot

QQ plot membandingkan kuantil data dengan kuantil distribusi normal. Titik yang mengikuti garis menunjukkan data lebih sesuai dengan normal, tetapi keputusan sebaiknya tidak hanya bersandar pada grafik (Velleman & Hoaglin, 2004).

if ("hourly_rate_clean" %in% names(data_clean)) {
  x <- data_clean$hourly_rate_clean
  x <- x[is.finite(x)]

  if (length(x) >= 2) {
    qqnorm(
      x,
      main = "QQ Plot Hourly Rate",
      pch = 19,
      col = biru_utama,
      cex = 0.8
    )
    qqline(x, col = pink_tua, lwd = 2)
  } else {
    cat("QQ plot hourly rate membutuhkan setidaknya dua observasi valid.")
  }
}

if ("years_of_experience_clean" %in% names(data_clean)) {
  x <- data_clean$years_of_experience_clean
  x <- x[is.finite(x)]

  if (length(x) >= 2) {
    qqnorm(
      x,
      main = "QQ Plot Pengalaman Kerja",
      pch = 19,
      col = pink_tua,
      cex = 0.8
    )
    qqline(x, col = biru_utama, lwd = 2)
  } else {
    cat("QQ plot pengalaman kerja membutuhkan setidaknya dua observasi valid.")
  }
}

Interpretasi. Pada kedua plot, titik menyimpang dari garis, terutama di kedua ujung, sehingga data tidak tampak berdistribusi normal. Pada tarif, titik membentuk anak tangga karena nilainya diskret (hanya enam nilai berbeda).

9.5 Hubungan Pengalaman Kerja dan Hourly Rate

Karena tarif hanya punya enam nilai, titik-titik digeser sedikit (jitter) supaya tidak menumpuk. Pergeseran ini hanya untuk tampilan, bukan mengubah data.

if (all(c("years_of_experience_clean", "hourly_rate_clean") %in% names(data_clean))) {
  ggplot(
    data_clean,
    aes(x = years_of_experience_clean, y = hourly_rate_clean)
  ) +
    geom_jitter(color = biru_utama, alpha = 0.55, size = 2.2,
                width = 0.25, height = 1.2) +
    geom_smooth(
      method = "lm",
      formula = y ~ x,
      se = TRUE,
      color = pink_tua,
      fill = pink_muda,
      linewidth = 1.1
    ) +
    labs(
      title = "Pengalaman Kerja dan Tarif per Jam",
      subtitle = "Garis menunjukkan kecenderungan linear, bukan bukti sebab-akibat",
      x = "Pengalaman kerja (tahun)",
      y = "Hourly rate (USD)"
    ) +
    tema_laporan
}

9.6 Ukuran Hubungan dan Tarif per Kategori

Karena data tidak normal (lihat uji normalitas di bawah), selain korelasi Pearson ditampilkan juga korelasi Spearman yang berbasis peringkat.

if (all(c("years_of_experience_clean", "hourly_rate_clean") %in% names(data_clean))) {
  pearson <- cor.test(data_clean$years_of_experience_clean,
                      data_clean$hourly_rate_clean, method = "pearson")
  spearman <- suppressWarnings(
    cor.test(data_clean$years_of_experience_clean,
             data_clean$hourly_rate_clean,
             method = "spearman", exact = FALSE)
  )

  knitr::kable(
    data.frame(
      Metode = c("Pearson", "Spearman"),
      Koefisien = c(unname(pearson$estimate), unname(spearman$estimate)),
      Nilai_p = c(pearson$p.value, spearman$p.value)
    ),
    digits = 4,
    caption = "Korelasi pengalaman kerja dan hourly rate"
  )
}
Korelasi pengalaman kerja dan hourly rate
Metode Koefisien Nilai_p
Pearson 0.0786 0.0129
Spearman 0.0782 0.0134
if (all(c("kategori_pengalaman", "hourly_rate_clean") %in% names(data_clean))) {
  data_clean %>%
    group_by(Kategori = kategori_pengalaman) %>%
    summarise(
      Jumlah = n(),
      Rata_rata_Tarif = mean(hourly_rate_clean),
      Median_Tarif = median(hourly_rate_clean),
      .groups = "drop"
    ) %>%
    knitr::kable(digits = 2, caption = "Tarif per jam menurut kategori pengalaman")
}
Tarif per jam menurut kategori pengalaman
Kategori Jumlah Rata_rata_Tarif Median_Tarif
Pemula 571 49.81 40
Menengah 252 50.81 40
Jago 122 56.64 50
Master 55 57.00 50
if (all(c("kategori_pengalaman", "hourly_rate_clean") %in% names(data_clean))) {
  ggplot(data_clean, aes(x = kategori_pengalaman, y = hourly_rate_clean,
                         fill = kategori_pengalaman)) +
    geom_boxplot(color = biru_tua, alpha = 0.9, show.legend = FALSE) +
    scale_fill_manual(values = c(
      "Pemula" = biru_utama,
      "Menengah" = biru_muda,
      "Jago" = pink_muda,
      "Master" = pink_tua
    )) +
    labs(
      title = "Tarif per Jam Menurut Kategori Pengalaman",
      x = "Kategori pengalaman",
      y = "Hourly rate (USD)"
    ) +
    tema_laporan
}

Interpretasi. Hubungan pengalaman dan tarif sangat lemah tetapi positif: koefisien korelasi sekitar 0,08 (Pearson maupun Spearman), dengan nilai p Spearman sekitar 0,013. Artinya hubungan ini signifikan secara statistik pada taraf 5%, namun ukurannya sangat kecil, sehingga pengalaman hanya menjelaskan sebagian kecil sekali dari variasi tarif (kurang dari 1% menurut r kuadrat). Rata-rata tarif naik tipis menurut kategori: Pemula sekitar USD 49,8, Menengah 50,8, Jago 56,6, dan Master 57,0. Kenaikan ini kecil dibanding sebaran tarif yang lebar pada tiap kategori. Hubungan ini juga hanya menunjukkan keterkaitan, bukan sebab-akibat.

10 Transformasi Numerik

Transformasi membantu menyamakan skala variabel. Pilihlah transformasi sesuai kebutuhan analisis, karena tidak ada satu transformasi yang selalu terbaik untuk semua data.

10.1 Rentang Awal

variabel_transformasi <- intersect(
  c("years_of_experience_clean", "hourly_rate_clean"),
  names(data_clean)
)

ringkasan_rentang <- lapply(variabel_transformasi, function(nama) {
  x <- data_clean[[nama]]
  data.frame(
    Variabel = nama,
    Minimum = if (all(is.na(x))) NA_real_ else min(x, na.rm = TRUE),
    Maksimum = if (all(is.na(x))) NA_real_ else max(x, na.rm = TRUE),
    Mean = if (all(is.na(x))) NA_real_ else mean(x, na.rm = TRUE),
    SD = if (sum(!is.na(x)) < 2) NA_real_ else sd(x, na.rm = TRUE)
  )
})

if (length(ringkasan_rentang) > 0) {
  knitr::kable(do.call(rbind, ringkasan_rentang), digits = 3, row.names = FALSE,
               caption = "Rentang dan ringkasan variabel sebelum transformasi")
}
Rentang dan ringkasan variabel sebelum transformasi
Variabel Minimum Maksimum Mean SD
years_of_experience_clean 0 41 11.221 9.444
hourly_rate_clean 20 100 51.290 26.260

Interpretasi. Pengalaman berkisar 0-41 tahun dan tarif USD 20-100. Skala keduanya sangat berbeda, sehingga perlu disamakan bila dipakai bersama pada analisis yang peka terhadap skala (misalnya perhitungan jarak, regresi berpenalti, atau clustering).

10.2 Normalisasi Min-Max dan Standardisasi Z-Score

Min-Max mengubah skala menjadi 0 sampai 1 (jika nilainya bervariasi). Z-Score memusatkan data pada rata-rata 0 dengan simpangan baku 1 (jika simpangan baku tidak nol).

data_final <- data_clean

if ("years_of_experience_clean" %in% names(data_final)) {
  data_final$exp_minmax <- min_max(data_final$years_of_experience_clean)
  data_final$exp_z <- z_score(data_final$years_of_experience_clean)
}
if ("hourly_rate_clean" %in% names(data_final)) {
  data_final$rate_minmax <- min_max(data_final$hourly_rate_clean)
  data_final$rate_z <- z_score(data_final$hourly_rate_clean)
}

ringkasan_z <- data.frame(
  Variabel = character(),
  Mean_ZScore = numeric(),
  SD_ZScore = numeric()
)

if ("exp_z" %in% names(data_final)) {
  ringkasan_z <- rbind(
    ringkasan_z,
    data.frame(
      Variabel = "Pengalaman kerja",
      Mean_ZScore = mean(data_final$exp_z, na.rm = TRUE),
      SD_ZScore = sd(data_final$exp_z, na.rm = TRUE)
    )
  )
}
if ("rate_z" %in% names(data_final)) {
  ringkasan_z <- rbind(
    ringkasan_z,
    data.frame(
      Variabel = "Hourly rate",
      Mean_ZScore = mean(data_final$rate_z, na.rm = TRUE),
      SD_ZScore = sd(data_final$rate_z, na.rm = TRUE)
    )
  )
}

knitr::kable(ringkasan_z, digits = 4, row.names = FALSE,
             caption = "Verifikasi hasil standardisasi Z-Score")
Verifikasi hasil standardisasi Z-Score
Variabel Mean_ZScore SD_ZScore
Pengalaman kerja 0 1
Hourly rate 0 1

Interpretasi. Setelah Min-Max, kedua variabel berada pada rentang 0 sampai 1 (pengalaman 0 tahun menjadi 0 dan 41 tahun menjadi 1; tarif USD 20 menjadi 0 dan USD 100 menjadi 1). Bentuk sebaran tidak berubah, hanya skalanya. Pada Z-Score, rata-rata praktis 0 dan simpangan baku tepat 1, sehingga standardisasi berhasil. Sebagai gambaran, pengalaman 41 tahun berada sekitar 3,2 simpangan baku di atas rata-rata, tanda adanya ekor kanan yang panjang.

10.3 Transformasi Inverse Square

Transformasi yang dipakai adalah \(1/(x+1)^2\). Penambahan 1 mencegah pembagian dengan nol saat pengalaman bernilai nol. Transformasi ini mengubah makna nilai, sehingga kolom asli tetap disimpan.

if ("years_of_experience_clean" %in% names(data_final)) {
  data_final$exp_inv_sq <-
    1 / ((data_final$years_of_experience_clean + 1)^2)

  cat("Nilai minimum pengalaman:", min(data_final$years_of_experience_clean), "\n")
  cat("Jumlah pengalaman bernilai 0:",
      sum(data_final$years_of_experience_clean == 0), "\n")

  print(
    ggplot(data_final, aes(x = years_of_experience_clean)) +
      geom_histogram(bins = 20, fill = biru_utama,
                     color = "white", linewidth = 0.4) +
      labs(
        title = "Pengalaman Kerja Sebelum Transformasi",
        x = "Pengalaman kerja (tahun)",
        y = "Frekuensi"
      ) +
      tema_laporan
  )

  print(
    ggplot(data_final, aes(x = exp_inv_sq)) +
      geom_histogram(bins = 20, fill = pink_tua,
                     color = "white", linewidth = 0.4) +
      labs(
        title = "Distribusi Setelah Transformasi Inverse Square",
        x = "Nilai inverse square",
        y = "Frekuensi"
      ) +
      tema_laporan
  )
}
## Nilai minimum pengalaman: 0 
## Jumlah pengalaman bernilai 0: 72

Interpretasi. Terdapat 72 baris dengan pengalaman 0 tahun. Tanpa penambahan +1, rumus 1/x² akan menghasilkan tak hingga untuk baris-baris ini. Dengan +1, hasil transformasi berkisar dari sekitar 0,0006 (41 tahun) sampai 1 (0 tahun), dengan rata-rata sekitar 0,106. Transformasi ini membalik urutan: pengalaman besar menjadi nilai kecil. Karena sangat kuat, sebaran hasilnya bisa menjadi terlalu menumpuk di dekat nol, jadi bentuk sebarannya perlu dicek sebelum dipakai.

10.4 Evaluasi Perbandingan Transformasi

if ("years_of_experience_clean" %in% names(data_final)) {
  evaluasi_transformasi <- data.frame(
    Statistik = c("Mean asli", "SD asli", "Minimum Min-Max",
                  "Maksimum Min-Max", "Mean Z-Score", "SD Z-Score"),
    Nilai = c(
      mean(data_final$years_of_experience_clean, na.rm = TRUE),
      sd(data_final$years_of_experience_clean, na.rm = TRUE),
      if (all(is.na(data_final$exp_minmax))) NA_real_
      else min(data_final$exp_minmax, na.rm = TRUE),
      if (all(is.na(data_final$exp_minmax))) NA_real_
      else max(data_final$exp_minmax, na.rm = TRUE),
      mean(data_final$exp_z, na.rm = TRUE),
      sd(data_final$exp_z, na.rm = TRUE)
    )
  )
  knitr::kable(evaluasi_transformasi, digits = 4, row.names = FALSE,
               caption = "Evaluasi transformasi pengalaman kerja")
}
Evaluasi transformasi pengalaman kerja
Statistik Nilai
Mean asli 11.2210
SD asli 9.4443
Minimum Min-Max 0.0000
Maksimum Min-Max 1.0000
Mean Z-Score 0.0000
SD Z-Score 1.0000

Interpretasi. Data asli (setelah imputasi) memiliki rata-rata sekitar 11,22 tahun dan simpangan baku sekitar 9,44. Min-Max menghasilkan rentang 0 sampai 1, sedangkan Z-Score menghasilkan rata-rata 0 dan simpangan baku 1. Semuanya sesuai harapan, jadi fungsi transformasi dinilai benar. Pilihan metode bergantung tujuan: Min-Max untuk rentang tetap, Z-Score bila metode mengandaikan data berpusat di nol, dan inverse square bila sebaran perlu diredam.

11 Uji Asumsi Normalitas

Uji Shapiro-Wilk memeriksa hipotesis nol bahwa sampel berasal dari distribusi normal. Nilai p yang kecil menjadi bukti melawan normalitas. R mensyaratkan 3 sampai 5000 observasi dan data yang tidak semuanya sama.

  • H0: data berdistribusi normal
  • H1: data tidak berdistribusi normal
  • Taraf signifikansi: \(\alpha = 0{,}05\)
uji_shapiro_aman <- function(x, nama) {
  x <- x[is.finite(x)]
  if (length(x) >= 3 && length(x) <= 5000 && length(unique(x)) > 1) {
    hasil <- shapiro.test(x)
    cat("\nVariabel:", nama, "\n")
    print(hasil)
  } else {
    cat("\nUji Shapiro-Wilk dilewati untuk", nama,
        ": dibutuhkan 3-5000 observasi valid dengan variasi nilai.\n")
  }
}

if ("hourly_rate_clean" %in% names(data_final)) {
  uji_shapiro_aman(data_final$hourly_rate_clean, "Hourly rate")
}
## 
## Variabel: Hourly rate 
## 
##  Shapiro-Wilk normality test
## 
## data:  x
## W = 0.85064, p-value < 2.2e-16
if ("years_of_experience_clean" %in% names(data_final)) {
  uji_shapiro_aman(data_final$years_of_experience_clean, "Years of experience")
}
## 
## Variabel: Years of experience 
## 
##  Shapiro-Wilk normality test
## 
## data:  x
## W = 0.9101, p-value < 2.2e-16

Interpretasi. Untuk hourly rate, statistik W sekitar 0,851 dengan nilai p sangat kecil (sekitar 1e-29). Untuk pengalaman kerja, W sekitar 0,910 dengan nilai p sekitar 8e-24. Kedua nilai p jauh di bawah 0,05, maka H0 ditolak: tidak ada bukti bahwa tarif maupun pengalaman berdistribusi normal. Hasil ini konsisten dengan QQ plot. Perlu dicatat bahwa dengan 1.000 data, Shapiro-Wilk sangat sensitif terhadap penyimpangan kecil, dan tarif hanya memiliki enam nilai berbeda. Karena itu, uji yang mensyaratkan normalitas perlu dipertimbangkan ulang; uji nonparametrik atau metode yang tahan terhadap pelanggaran asumsi bisa menjadi alternatif.

12 Uji Homogenitas Varians (Bartlett)

Uji Bartlett memeriksa apakah varians beberapa kelompok sama. Di sini tarif per jam dibandingkan antar gender. Uji ini peka terhadap ketidaknormalan, jadi hasilnya perlu dibaca bersama hasil uji normalitas.

  • H0: varians tarif sama antar gender
  • H1: minimal satu kelompok memiliki varians berbeda
  • Taraf signifikansi: \(\alpha = 0{,}05\)
if (all(c("hourly_rate_clean", "gender") %in% names(data_final))) {
  data_bartlett <- data_final[
    !is.na(data_final$hourly_rate_clean) &
      !is.na(data_final$gender) &
      data_final$gender != "",
    , drop = FALSE
  ]

  data_bartlett$gender <- droplevels(factor(data_bartlett$gender))

  if (nlevels(data_bartlett$gender) >= 2 &&
      all(table(data_bartlett$gender) >= 2)) {
    print(bartlett.test(hourly_rate_clean ~ gender, data = data_bartlett))

    data_bartlett %>%
      group_by(Gender = gender) %>%
      summarise(Jumlah = n(),
                Rata_rata = mean(hourly_rate_clean),
                SD = sd(hourly_rate_clean),
                .groups = "drop") %>%
      knitr::kable(digits = 2, caption = "Ringkasan tarif per jam menurut gender")
  } else {
    cat("Uji Bartlett dilewati: diperlukan setidaknya dua kelompok, masing-masing minimal dua observasi.\n")
  }
}
## 
##  Bartlett test of homogeneity of variances
## 
## data:  hourly_rate_clean by gender
## Bartlett's K-squared = 1.4921, df = 1, p-value = 0.2219
Ringkasan tarif per jam menurut gender
Gender Jumlah Rata_rata SD
Female 490 51.91 26.99
Male 510 50.70 25.55

Interpretasi. Statistik Bartlett sekitar 1,49 dengan derajat bebas 1 dan nilai p sekitar 0,222. Karena p lebih besar dari 0,05, H0 gagal ditolak: tidak ada bukti perbedaan varians tarif antara Female (SD sekitar 27,0) dan Male (SD sekitar 25,6). Namun karena Bartlett peka terhadap ketidaknormalan dan data ini tidak normal, uji Levene atau Fligner-Killeen dapat dipakai sebagai pembanding yang lebih tahan.

13 Dataset Final dan Integrasi Variabel

Bagian ini menyusun satu dataset analisis dari data yang sudah dibersihkan. Karena hanya ada satu file sumber, prosesnya berupa konsolidasi dan pemilihan kolom, bukan penggabungan dua dataset yang berbeda.

# Isi kolom analisis utama dengan hasil imputasi jika tersedia
if ("years_of_experience_clean" %in% names(data_final)) {
  data_final$years_of_experience <- data_final$years_of_experience_clean
}
if ("hourly_rate_clean" %in% names(data_final)) {
  data_final[["hourly_rate (USD)"]] <- data_final$hourly_rate_clean
}
if ("rating_clean" %in% names(data_final)) {
  data_final$rating <- data_final$rating_clean
}
if ("client_satisfaction_clean" %in% names(data_final)) {
  data_final$client_satisfaction <- data_final$client_satisfaction_clean
}
if ("age_clean" %in% names(data_final)) {
  data_final$age <- data_final$age_clean
}

kolom_analisis <- c(
  "age", "gender", "country", "years_of_experience",
  "kategori_pengalaman", "kategori_encode",
  "hourly_rate (USD)", "client_satisfaction", "rating", "is_active",
  "exp_minmax", "rate_minmax", "exp_z", "rate_z", "exp_inv_sq"
)

kolom_tersedia <- intersect(kolom_analisis, names(data_final))
data_analisis <- data_final[, kolom_tersedia, drop = FALSE]

knitr::kable(
  data.frame(
    Keterangan = c("Jumlah baris", "Jumlah kolom", "Jumlah duplikat"),
    Nilai = c(nrow(data_analisis), ncol(data_analisis),
              sum(duplicated(data_analisis)))
  ),
  caption = "Ringkasan dataset analisis akhir"
)
Ringkasan dataset analisis akhir
Keterangan Nilai
Jumlah baris 1000
Jumlah kolom 15
Jumlah duplikat 0
knitr::kable(
  head(data_analisis, 10),
  caption = "Pratinjau 10 baris pertama"
)
Pratinjau 10 baris pertama
age gender country years_of_experience kategori_pengalaman kategori_encode hourly_rate (USD) client_satisfaction rating is_active exp_minmax rate_minmax exp_z rate_z exp_inv_sq
52 Female Italy 11 Menengah 2 100 0.82 2.6 Inactive 0.2682927 1.0000 -0.0234003 1.8549287 0.0069444
52 Female Australia 34 Master 4 100 0.84 3.3 Active 0.8292683 1.0000 2.4119235 1.8549287 0.0008163
53 Male Germany 31 Master 4 50 0.71 0.0 Inactive 0.7560976 0.3750 2.0942726 -0.0491246 0.0009766
38 Female Australia 4 Pemula 1 40 0.90 1.5 Inactive 0.0975610 0.2500 -0.7645858 -0.4299352 0.0400000
53 Female Germany 27 Jago 3 30 0.83 4.8 Inactive 0.6585366 0.1250 1.6707380 -0.8107459 0.0012755
59 Female Netherlands 14 Menengah 2 30 0.82 2.4 Inactive 0.3414634 0.1250 0.2942506 -0.8107459 0.0044444
52 Male Indonesia 10 Pemula 1 75 0.94 3.1 Inactive 0.2439024 0.6875 -0.1292839 0.9029021 0.0082645
43 Male Italy 14 Menengah 2 40 0.94 4.6 Inactive 0.3414634 0.2500 0.2942506 -0.4299352 0.0044444
26 Male United States 4 Pemula 1 40 0.76 4.0 Active 0.0975610 0.2500 -0.7645858 -0.4299352 0.0400000
52 Male Turkey 22 Jago 3 50 0.82 2.6 Active 0.5365854 0.3750 1.1413198 -0.0491246 0.0018904

Interpretasi. Dataset analisis terdiri dari 1.000 baris dan 15 kolom tanpa duplikat. Kolom freelancer_ID, name, language, dan primary_skill sengaja tidak dimasukkan; bila analisis lanjutan membutuhkan keahlian utama, kolom primary_skill perlu ditambahkan.

14 Validasi Akhir

validasi_akhir <- data.frame(
  Variabel = names(data_analisis),
  Jumlah_NA = colSums(is.na(data_analisis)),
  Persentase_NA = round(colMeans(is.na(data_analisis)) * 100, 2)
)

knitr::kable(
  validasi_akhir,
  digits = 2,
  row.names = FALSE,
  caption = "Missing value pada dataset analisis akhir"
)
Missing value pada dataset analisis akhir
Variabel Jumlah_NA Persentase_NA
age 0 0
gender 0 0
country 0 0
years_of_experience 0 0
kategori_pengalaman 0 0
kategori_encode 0 0
hourly_rate (USD) 0 0
client_satisfaction 0 0
rating 0 0
is_active 0 0
exp_minmax 0 0
rate_minmax 0 0
exp_z 0 0
rate_z 0 0
exp_inv_sq 0 0
cat("Jumlah baris:", nrow(data_analisis), "\n")
## Jumlah baris: 1000
cat("Jumlah kolom:", ncol(data_analisis), "\n")
## Jumlah kolom: 15
cat("Jumlah duplikat:", sum(duplicated(data_analisis)), "\n")
## Jumlah duplikat: 0
if ("is_active" %in% names(data_analisis)) {
  print(table(data_analisis$is_active, useNA = "ifany"))
}
## 
##   Active Inactive  Unknown 
##      446      465       89
if ("kategori_pengalaman" %in% names(data_analisis)) {
  print(table(data_analisis$kategori_pengalaman, useNA = "ifany"))
}
## 
##   Pemula Menengah     Jago   Master 
##      571      252      122       55

Interpretasi. Seluruh kolom memiliki 0 NA (0%) dan tidak ada baris duplikat. Status keaktifan: Inactive 465, Active 446, Unknown 89. Kategori pengalaman: Pemula 571, Menengah 252, Jago 122, Master 55. Data siap dianalisis, dengan catatan dua keterbatasan dari pemeriksaan data noisy (skala client_satisfaction dan rating bernilai 0).

15 Ekspor Dataset

File CSV disimpan di folder kerja R. Gunakan getwd() untuk melihat lokasinya.

write.csv(
  data_analisis,
  "data_analisis_final.csv",
  row.names = FALSE,
  na = ""
)

cat("Dataset analisis berhasil disimpan sebagai data_analisis_final.csv\n")
## Dataset analisis berhasil disimpan sebagai data_analisis_final.csv
cat("Lokasi penyimpanan:", getwd(), "\n")
## Lokasi penyimpanan: C:/Users/ASUS/OneDrive/TUGAS KULIAH PUTRI/SEMESTER 3/ADE

16 Kesimpulan

Pemeriksaan terhadap Global Freelancers Dataset menemukan beberapa masalah kualitas data, yaitu 541 sel kosong (terbanyak pada client_satisfaction, 17,6%), penulisan gender dan status keaktifan yang tidak seragam, serta tarif per jam yang tersimpan sebagai teks dengan simbol mata uang. Tidak ditemukan baris duplikat maupun nilai di luar batas logis, tetapi client_satisfaction ternyata memakai dua skala (0,60-1,00 pada 710 baris dan 60-100 pada 114 baris) dan 145 baris rating bernilai 0 yang kemungkinan berarti “belum dinilai”. Melalui preprocessing, kategori diseragamkan, tarif diubah menjadi angka, dan nilai hilang pada variabel numerik diisi dengan median (usia 41, pengalaman 9 tahun, tarif USD 40, rating 2,6, kepuasan 0,82), sedangkan status yang tidak diketahui dicatat sebagai kategori Unknown. Hasilnya adalah dataset analisis berisi 1.000 baris dan 15 kolom tanpa NA dan tanpa duplikat.

Setelah data dibersihkan, pengalaman kerja freelancer berkisar 0-41 tahun dengan rata-rata sekitar 11,2 tahun dan simpangan baku sekitar 9,4. Sebarannya condong ke kanan: 57,1% freelancer berkategori Pemula, 25,2% Menengah, 12,2% Jago, dan hanya 5,5% Master. Uji Shapiro-Wilk menolak normalitas pada pengalaman kerja (W ≈ 0,910; p < 0,001) maupun tarif per jam (W ≈ 0,851; p < 0,001), sedangkan uji Bartlett tidak menemukan perbedaan varians tarif antar gender (p ≈ 0,222). Pengalaman kerja berhubungan positif tetapi sangat lemah dengan tarif per jam (korelasi sekitar 0,08; p Spearman sekitar 0,013). Hubungan ini signifikan secara statistik namun kecil secara praktis, karena rata-rata tarif hanya naik dari sekitar USD 49,8 pada kategori Pemula menjadi sekitar USD 57,0 pada kategori Master. Dengan demikian, pengalaman kerja bukan penentu utama tarif pada data ini, dan hasil tersebut tidak menunjukkan hubungan sebab-akibat.

16.1 Keterbatasan dan Saran

  • Imputasi median menyempitkan variasi data dan menarik nilai ke tengah, terutama pada client_satisfaction dan rating yang banyak kosong.
  • Skala client_satisfaction yang bercampur sebaiknya disatukan (misalnya nilai 60-100 dibagi 100) sebelum imputasi dan analisis lanjutan.
  • Rating bernilai 0 sebaiknya diperlakukan sebagai NA bila memang berarti “belum dinilai”.
  • Karena data tidak normal, analisis lanjutan sebaiknya memakai metode nonparametrik atau metode yang tahan terhadap pelanggaran asumsi.
  • Tarif hanya memiliki enam nilai berbeda, sehingga ia lebih tepat diperlakukan sebagai data berkategori berurutan daripada data kontinu.

Referensi

Ahir, U. (2025). Global freelancers (raw) dataset [Data set]. Kaggle. https://www.kaggle.com/datasets/urvishahir/global-freelancers-raw-dataset

Tukey, J. W. (1993). Exploratory data analysis: Past, present and future. Princeton University.

Velleman, P. F., & Hoaglin, D. C. (2004). Applications, basics, and computing of exploratory data analysis. Duxbury Press.