Pendahuluan

Latar Belakang

Perkembangan teknologi digital memberikan kesempatan bagi masyarakat untuk bekerja secara lepas (freelance) tanpa harus terikat pada satu lokasi kerja. Dalam pasar kerja tersebut, freelancer memiliki karakteristik yang beragam, mulai dari usia, keahlian, pengalaman, hingga tarif jasa yang ditawarkan. Lama pengalaman kerja (years of experience) menjadi salah satu faktor yang menarik untuk diteliti karena diduga berkaitan dengan tarif per jam (hourly rate).

Analisis ini menggunakan Global Freelancers Dataset yang bersumber dari Kaggle. Dataset tersebut terdiri atas 1.000 observasi freelancer dan 12 variabel yang memuat informasi mengenai karakteristik individu, pengalaman kerja, tarif jasa, rating, kepuasan klien, serta status keaktifan.

Sebelum dianalisis, data perlu diperiksa untuk mengetahui kemungkinan adanya nilai yang hilang (missing value), ketidakkonsistenan kategori, kesalahan format, duplikasi, dan nilai yang berpotensi tidak wajar. Proses pembersihan dan pengolahan data dilakukan agar informasi yang digunakan lebih konsisten. Selanjutnya, analisis diarahkan untuk menggambarkan karakteristik pengalaman kerja dan mengkaji hubungannya dengan tarif per jam freelancer.

Rumusan Masalah

  1. Apa saja permasalahan kualitas data yang terdapat pada Global Freelancers Dataset?
  2. Bagaimana proses preprocessing dilakukan untuk menangani nilai yang hilang, inkonsistensi format, dan ketidaksesuaian tipe data?
  3. Bagaimana karakteristik pengalaman kerja freelancer setelah pembersihan data berdasarkan statistik deskriptif dan pengujian normalitas?
  4. Bagaimana hubungan antara pengalaman kerja dan tarif per jam freelancer?

Tujuan Analisis

  1. Mengidentifikasi permasalahan kualitas data yang terdapat pada Global Freelancers Dataset.
  2. Melakukan preprocessing untuk menangani nilai yang hilang, menyeragamkan format, dan memperbaiki tipe data.
  3. Mendeskripsikan karakteristik pengalaman kerja melalui statistik deskriptif dan pemeriksaan normalitas setelah pembersihan data.
  4. Menganalisis hubungan antara pengalaman kerja dan tarif per jam freelancer.

Import dan Gambaran Dataset

Pilih file Excel melalui jendela pemilihan file. Cara ini membuat dokumen tidak bergantung pada alamat folder khusus di satu komputer.

file_excel <- file.choose()
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

Pemeriksaan Permasalahan Data

Missing Value

Missing value adalah nilai yang tidak tersedia pada suatu observasi. Persentase dihitung terhadap jumlah baris dataset.

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
)
Jumlah dan persentase missing value sebelum cleaning
Variabel Jumlah_NA Persentase_NA
freelancer_ID freelancer_ID 0 0.0
name name 0 0.0
gender gender 0 0.0
age age 30 3.0
country country 0 0.0
language language 0 0.0
primary_skill primary_skill 0 0.0
years_of_experience years_of_experience 51 5.1
hourly_rate (USD) hourly_rate (USD) 94 9.4
rating rating 101 10.1
is_active is_active 89 8.9
client_satisfaction client_satisfaction 176 17.6
cat("Total missing value:", sum(is.na(ADE)), "\n")
## Total missing value: 541

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

Data Cleaning

Membersihkan Gender

Nilai kategori diseragamkan agar variasi penulisan seperti f, female, m, dan male tidak dianggap sebagai kategori berbeda.

if ("gender" %in% names(ADE)) {
  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)))
    )
  )
  print(table(ADE$gender, useNA = "ifany"))
}
## 
## Female   Male 
##    490    510

Membersihkan Hourly Rate

Simbol dolar, teks USD, koma pemisah ribuan, dan spasi dihapus sebelum kolom diubah menjadi numerik.

if ("hourly_rate (USD)" %in% names(ADE)) {
  ADE[["hourly_rate (USD)"]] <- bersihkan_numerik(
    ADE[["hourly_rate (USD)"]],
    mata_uang = TRUE
  )
  summary(ADE[["hourly_rate (USD)"]])
  str(ADE[["hourly_rate (USD)"]])
}
##  num [1:1000] 100 100 50 40 30 30 75 40 NA 50 ...

Membersihkan Client Satisfaction

if ("client_satisfaction" %in% names(ADE)) {
  ADE$client_satisfaction <- bersihkan_numerik(
    ADE$client_satisfaction,
    persen = TRUE
  )
  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

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

Membersihkan Status Is Active

Status yang dikenali diubah menjadi Active atau Inactive. Nilai kosong atau tidak dikenali dicatat sebagai Unknown, bukan 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

Menghapus Duplikasi

Duplikasi dihapus setelah pembersihan format agar baris yang sama tidak tetap terhitung berbeda hanya karena variasi format.

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

Pemeriksaan Nilai Tidak Masuk Akal (Data Noisy)

Pemeriksaan ini menandai nilai yang patut diperiksa. Nilai tidak otomatis dihapus karena perlu dibandingkan dengan sumber data atau definisi variabel.

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,
               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

Membuat Data Clean dan Mengatasi Missing Value

Imputasi median dilakukan untuk variabel numerik terpilih. Kolom asli tetap dipertahankan, sedangkan hasil imputasi disimpan pada kolom dengan akhiran _clean agar perubahan dapat 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
)
Missing value setelah membuat kolom imputasi
Variabel Jumlah_NA Persentase_NA
freelancer_ID freelancer_ID 0 0.0
name name 0 0.0
gender gender 0 0.0
age age 30 3.0
country country 0 0.0
language language 0 0.0
primary_skill primary_skill 0 0.0
years_of_experience years_of_experience 51 5.1
hourly_rate (USD) hourly_rate (USD) 94 9.4
rating rating 101 10.1
is_active is_active 0 0.0
client_satisfaction client_satisfaction 176 17.6
years_of_experience_clean years_of_experience_clean 0 0.0
hourly_rate_clean hourly_rate_clean 0 0.0
rating_clean rating_clean 0 0.0
client_satisfaction_clean client_satisfaction_clean 0 0.0
age_clean age_clean 0 0.0

Catatan interpretasi: jika sebuah kolom seluruh nilainya kosong, median tidak dapat dihitung. Kolom tersebut akan tetap berisi NA dan perlu ditinjau secara khusus.

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

Encoding Pengalaman Kerja

Variabel pengalaman kerja dikelompokkan ke empat kategori. Pengkodean kategori bersifat ordinal, karena 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,
    caption = "Frekuensi kategori dan kode pengalaman kerja"
  )
  print(table(data_clean$kategori_pengalaman, useNA = "ifany"))
}
## 
##   Pemula Menengah     Jago   Master 
##      571      252      122       55

Visualisasi Data

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 = warna_utama,
      color = "white",
      linewidth = 0.5,
      alpha = 0.9
    ) +
    labs(
      title = "Distribusi Pengalaman Kerja Freelancer",
      subtitle = "Frekuensi jumlah tahun pengalaman",
      x = "Pengalaman kerja (tahun)",
      y = "Frekuensi"
    ) +
    tema_laporan
}

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.6, show.legend = FALSE) +
    scale_fill_manual(values = c(
      "Pemula" = "#58A6FF",
      "Menengah" = "#00A6A6",
      "Jago" = "#F28E2B",
      "Master" = "#8064A2"
    ), na.value = "#B8C1CC") +
    labs(
      title = "Komposisi Freelancer Menurut Pengalaman",
      x = "Kategori pengalaman",
      y = "Jumlah freelancer"
    ) +
    tema_laporan
}

Distribusi Hourly Rate

if ("hourly_rate_clean" %in% names(data_clean)) {
  ggplot(data_clean, aes(x = hourly_rate_clean)) +
    geom_histogram(
      bins = 25,
      fill = warna_kedua,
      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
}

QQ Plot

QQ plot membandingkan kuantil data dengan kuantil teoretis distribusi normal. Titik yang mengikuti garis secara umum lebih konsisten dengan distribusi normal, tetapi keputusan sebaiknya tidak hanya didasarkan pada grafik.

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 = warna_utama,
      cex = 0.8,
      family = "serif"
    )
    qqline(x, col = warna_merah, 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 = warna_kedua,
      cex = 0.8,
      family = "serif"
    )
    qqline(x, col = warna_merah, lwd = 2)
  } else {
    cat("QQ plot pengalaman kerja membutuhkan setidaknya dua observasi valid.")
  }
}

Hubungan Pengalaman Kerja dan Hourly Rate

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_point(color = warna_utama, alpha = 0.65, size = 2.2) +
    geom_smooth(
      method = "lm",
      formula = y ~ x,
      se = TRUE,
      color = warna_aksen,
      fill = "#F8D9B4",
      linewidth = 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
}

Transformasi Numerik

Transformasi dapat membantu membandingkan skala variabel. Pilih transformasi sesuai kebutuhan analisis, dan jangan menganggap satu transformasi selalu paling baik untuk semua data.

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,
               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

Normalisasi Min-Max dan Standardisasi Z-Score

Min-Max mengubah skala menjadi 0 sampai 1 (jika terdapat variasi nilai). 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,
             caption = "Verifikasi hasil standardisasi Z-Score")
Verifikasi hasil standardisasi Z-Score
Variabel Mean_ZScore SD_ZScore
Pengalaman kerja 0 1
Hourly rate 0 1

Transformasi Inverse Square

Transformasi yang digunakan adalah \(1/(x+1)^2\). Penambahan 1 mencegah pembagian dengan nol saat pengalaman bernilai nol. Transformasi ini mengubah interpretasi 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)

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

  ggplot(data_final, aes(x = exp_inv_sq)) +
    geom_histogram(bins = 20, fill = warna_aksen,
                   color = "white", linewidth = 0.4) +
    labs(
      title = "Distribusi Setelah Transformasi Inverse Square",
      x = "Nilai inverse square",
      y = "Frekuensi"
    ) +
    tema_laporan
}

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,
               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

Uji Asumsi Normalitas

Uji Shapiro-Wilk digunakan untuk memeriksa hipotesis nol bahwa sampel berasal dari distribusi normal. Nilai p yang kecil memberikan bukti terhadap normalitas. Untuk uji ini, R mensyaratkan 3 sampai 5000 observasi dan data tidak semuanya sama.

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

Uji Homogenitas Varians (Bartlett)

Uji Bartlett memeriksa apakah varians beberapa kelompok sama. Di sini hourly rate dibandingkan berdasarkan gender. Uji ini sensitif terhadap penyimpangan normalitas, sehingga interpretasikan bersama hasil pemeriksaan distribusi.

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

Dataset Final dan Integrasi Variabel

Bagian ini menyusun satu dataset analisis dari dataset yang telah dibersihkan. Karena hanya ada satu file sumber pada alur ini, prosesnya merupakan konsolidasi/pemilihan kolom, bukan penggabungan dua dataset berbeda.

# Isi kolom analisis utama dari 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
DT::datatable(
  head(data_analisis, 100),
  options = list(pageLength = 10, scrollX = TRUE),
  rownames = FALSE,
  caption = "Pratinjau maksimal 100 baris pertama"
)

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,
  caption = "Missing value pada dataset analisis akhir"
)
Missing value pada dataset analisis akhir
Variabel Jumlah_NA Persentase_NA
age age 0 0
gender gender 0 0
country country 0 0
years_of_experience years_of_experience 0 0
kategori_pengalaman kategori_pengalaman 0 0
kategori_encode kategori_encode 0 0
hourly_rate (USD) hourly_rate (USD) 0 0
client_satisfaction client_satisfaction 0 0
rating rating 0 0
is_active is_active 0 0
exp_minmax exp_minmax 0 0
rate_minmax rate_minmax 0 0
exp_z exp_z 0 0
rate_z rate_z 0 0
exp_inv_sq 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

Ekspor Dataset

CSV akan disimpan pada working directory R. Gunakan getwd() untuk melihat lokasi folder penyimpanan.

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/ADVAN/Downloads

Kesimpulan

Tahapan yang dilakukan meliputi pemeriksaan struktur dan missing value, penyeragaman format, penghapusan duplikasi, pemeriksaan nilai yang tidak masuk akal, imputasi median untuk variabel numerik terpilih, encoding pengalaman kerja, visualisasi, transformasi numerik, uji asumsi, validasi, dan ekspor data.

Kesimpulan substantif mengenai karakteristik freelancer harus ditulis setelah melihat hasil aktual dari tabel, grafik, dan uji statistik. Jangan menyimpulkan normalitas, hubungan, atau perbedaan kelompok sebelum memeriksa hasil yang muncul saat dokumen dijalankan.