Assignment DS Programming Week 11

Angelique Kiyoshi Lakeisha B.U

NIM: 52250001

Student Major Data Science at Institut Teknologi Sains Bandung

RPubs Statistics Data Science Programming Assignment Week 1 – Mr. Bakti Siregar, M.Sc., CDS
Assignment Week 11 — Descriptive Statistics & Data Preparation

Pada tugas ini dilakukan analisis statistik deskriptif menggunakan Employee Raw Dataset. Dataset ini digunakan untuk memahami pola utama data karyawan serta melihat apakah terdapat hubungan tertentu antara faktor-faktor yang berkaitan dengan performa dan kondisi kerja karyawan.

Analisis dilakukan mulai dari data understanding, data preraparion & cleaning, transformasi data, feature engineering, hingga analisis asosiasi antar variabel. Seluruh perhitungan statistik utama dikerjakan menggunakan logika dan function yang dibuat secara manual tanpa menggunakan library statistik instan, sehingga proses perhitungan dapat dipahami secara konseptual maupun implementatif.

Melalui analisis ini, akan dievaluasi bagaimana distribusi data karyawan, keberadaan missing value dan outlier, pola kategori dominan, serta kekuatan hubungan antar variabel numerik maupun kategorik dalam dataset.


1 Import Library & Load Data

Pada tahap awal, dilakukan import library yang diperlukan untuk proses analisis data dan visualisasi. Library seperti dplyr digunakan untuk membantu manipulasi data, sedangkan ggplot2 (plotly) digunakan untuk visualisasi distribusi dan pola data. Selain itu, DT digunakan untuk menampilkan dataset dalam bentuk tabel interaktif agar lebih mudah dibaca dan dieksplorasi.

Dataset yang digunakan adalah Employee Raw Dataset, yaitu dataset karyawan yang berisi informasi terkait karakteristik individu, performa kerja, kompensasi, pengalaman kerja, hingga aktivitas pelatihan karyawan. Dataset kemudian di-load ke dalam DataFrame agar dapat dianalisis lebih lanjut pada tahap data preparation dan descriptive statistics.

library(dplyr)
library(ggplot2)
library(plotly)
library(DT)
library(knitr)

2 Data Understanding

Pada tahap data understanding, dilakukan identifikasi struktur dasar dataset untuk memahami karakteristik data sebelum masuk ke proses cleaning dan analisis statistik. Pemeriksaan ini penting untuk memastikan jumlah observasi, jumlah variabel, tipe data, serta kondisi awal setiap kolom dapat dipahami dengan jelas.

Analisis awal dilakukan dengan melihat jumlah baris dan kolom dataset, kemudian dilanjutkan dengan identifikasi tipe data (data type) dan jumlah data valid (non-null count) pada setiap variabel. Informasi ini menjadi dasar untuk menentukan strategi data preparation, terutama dalam menangani missing value, inkonsistensi data, dan pemilihan metode analisis statistik yang sesuai pada tahap berikutnya.

# Ringkasan jumlah baris dan kolom
summary_df <- data.frame(
  Informasi = c("Jumlah Baris", "Jumlah Kolom"),
  Nilai = c(nrow(raw_df), ncol(raw_df))
)

datatable(
  summary_df
)
info_df <- data.frame(
  Column = names(raw_df),
  Non_Null_Count = sapply(raw_df, function(x) sum(!is.na(x))),
  Dtype = sapply(raw_df, class)
)

rownames(info_df) <- 1:nrow(info_df)

datatable(
  info_df,
  options = list(
    pageLength = 10
  )
)

3 Data Preparation

Setelah memahami struktur dasar dataset, tahap berikutnya adalah data preparation, yaitu proses membersihkan dan mempersiapkan data sebelum dilakukan analisis statistik lebih lanjut. Tahap ini penting karena kualitas hasil analisis sangat bergantung pada kualitas data yang digunakan.

Pada proses ini dilakukan beberapa tahapan utama, yaitu: 1. Handling Missing Value 2. Menghapus Data Duplikat 3. Standarisasi Data Kategorik 4. Validasi dan Cleaning Nilai Tidak Logis

Proses cleaning dilakukan menggunakan logika pemrograman manual untuk memastikan setiap tahapan data preparation dapat dipahami secara konseptual, bukan hanya menggunakan function instan dari library.

3.1 Handling Missing Value

Missing value adalah kondisi ketika suatu variabel tidak memiliki nilai pada beberapa observasi. Jika tidak ditangani, kondisi ini dapat menyebabkan bias analisis, error pada perhitungan statistik, serta menurunkan kualitas hasil interpretasi data.

Strategi penanganan dilakukan berdasarkan tipe data:

  • Variabel numerik menggunakan median imputation karena lebih robust terhadap outlier
  • Variabel kategorik menggunakan mode imputation karena kategori tidak memiliki rata-rata matematis
# Ringkasan missing value sebelum cleaning
missing_count <- sapply(raw_df, function(x) sum(is.na(x)))
missing_pct <- round((missing_count / nrow(raw_df)) * 100, 2)

missing_before <- data.frame(
  `Missing Count` = missing_count,
  `Missing pct` = paste0(missing_pct, "%")
)

datatable(
  missing_before,
  options = list(
    pageLength = 10
  )
)
Handling Missing Value Numerik
handle_missing_numeric <- function(df){

  df_out <- df
  num_cols <- names(df_out)[sapply(df_out, is.numeric)]
  summary_list <- list()

  for(col in num_cols){
    missing_count <- sum(is.na(df_out[[col]]))
    if(missing_count == 0){
      next
    }

    data <- sort(df_out[[col]][!is.na(df_out[[col]])])
    n <- length(data)
    mid <- n %/% 2

    # Median manual
    if(n %% 2 == 0){
      median_val <- (data[mid] + data[mid + 1]) / 2
    } else {
      median_val <- data[mid + 1]
    }

    # Replace missing
    df_out[[col]][is.na(df_out[[col]])] <- median_val
    summary_list[[length(summary_list) + 1]] <- data.frame(
      Column = col,
      `Missing Filled` = missing_count,
      Strategy = "Median",
      `Value Used` = median_val
    )
  }

  summary_df <- do.call(rbind, summary_list)
  return(list(df_out, summary_df))
}
Handling Missing Value Kategorik
handle_missing_categorical <- function(df){

  df_out <- df
  cat_cols <- names(df_out)[sapply(df_out, is.character)]
  summary_list <- list()

  for(col in cat_cols){
    missing_count <- sum(is.na(df_out[[col]]))
    if(missing_count == 0){
      next
    }
    values <- df_out[[col]][!is.na(df_out[[col]])]

    # Hitung frekuensi manual
    freq <- list()
    for(v in values){
      if(is.null(freq[[v]])){
        freq[[v]] <- 1
      } else {
        freq[[v]] <- freq[[v]] + 1
      }
    }

    # Cari mode manual
    freq_values <- unlist(freq)
    mode_val <- names(freq_values)[which.max(freq_values)]

    # Replace missing
    df_out[[col]][is.na(df_out[[col]])] <- mode_val
    summary_list[[length(summary_list) + 1]] <- data.frame(
      Column = col,
      `Missing Filled` = missing_count,
      Strategy = "Mode",
      `Value Used` = mode_val
    )
  }

  summary_df <- do.call(rbind, summary_list)
  return(list(df_out, summary_df))
}
Hasil Handling Missing Value

Imputasi median dan mode membantu menjaga stabilitas distribusi data tanpa menggeser pusat data secara signifikan. Dengan tidak adanya missing value, seluruh observasi dapat digunakan secara optimal pada tahap analisis berikutnya.

3.2 Menghapus Data Duplikat

Data duplikat merupakan observasi yang tercatat lebih dari satu kali pada dataset. Jika tidak dihapus, data duplikat dapat menyebabkan distorsi pada statistik agregat seperti mean, distribusi kategori, dan frekuensi observasi.

n_before <- nrow(df)                 # Jumlah data sebelum cleaning
n_duplicates <- sum(duplicated(df))  # Hitung data duplikat
df <- unique(df)                     # Hapus duplikat
n_after <- nrow(df)                  # Jumlah data sesudah cleaning

# Summary duplikat
duplicate_summary <- data.frame(
  Keterangan = c(
    "Jumlah Total Data",
    "Jumlah Data Duplikat"
  ),
  Jumlah = c(
    n_before,
    n_duplicates
  )
)

datatable(duplicate_summary)
validation_df <- data.frame(
  `Jumlah Data Awal` = n_before,
  `Jumlah Data Setelah Cleaning` = n_after,
  `Jumlah Duplikat Dihapus` = n_duplicates
)

datatable(validation_df)

Penghapusan data duplikat dilakukan untuk menjaga representasi distribusi data tetap objektif dan tidak bias akibat penghitungan observasi yang berulang.

3.3 Standarisasi Data Kategorik

Pada dataset nyata, data kategorik sering memiliki format yang tidak konsisten, seperti perbedaan huruf besar-kecil, singkatan, maupun penulisan yang berbeda namun memiliki makna yang sama.

Jika tidak distandarisasi, kondisi ini dapat menyebabkan kategori identik dianggap berbeda sehingga distribusi data menjadi tidak akurat.

standardize_col <- function(df, col, mapping){
  df_out <- df
  standardized <- c()
  for(val in df_out[[col]]){
    if(is.na(val)){
      standardized <- c(standardized, "Unknown")
      next
    }

    cleaned <- trimws(tolower(as.character(val)))

    if(cleaned %in% names(mapping)){
      standardized <- c(standardized, mapping[[cleaned]])
    } else {
      standardized <- c(standardized, "Unknown")
    }
  }

  df_out[[col]] <- standardized
  return(df_out)
}

# Standardisasi Gender
df <- standardize_col(df, "Gender", list(
  "female"="Female",
  "f"="Female",
  "male"="Male",
  "m"="Male",
  "unknown"="Unknown",
  "-"="Unknown"
))

# Standardisasi Department
df <- standardize_col(df, "Department", list(
  "hr"="HR",
  "operations"="Operations",
  "risk"="Risk",
  "it"="IT",
  "finance"="Finance",
  "compliance"="Compliance",
  "marketing"="Marketing"
))

# Standardisasi Education
df <- standardize_col(df, "Education_Level", list(
  "phd"="PhD",
  "master"="Master",
  "magister"="Master",
  "bachelor"="Bachelor",
  "s1"="Bachelor",
  "bachelor degree"="Bachelor",
  "diploma"="Diploma"
))

# Standardisasi Job Level
df <- standardize_col(df, "Job_Level", list(
  "junior"="Junior",
  "officer"="Officer",
  "supervisor"="Supervisor",
  "manager"="Manager",
  "senior manager"="Senior Manager"
))

Standarisasi berhasil membuat kategori menjadi lebih konsisten sehingga distribusi data kategorik dapat dianalisis dengan lebih akurat pada tahap berikutnya.

3.4 Validasi dan Cleaning Nilai Tidak Logis

Selain missing value dan inkonsistensi kategori, dataset juga perlu divalidasi terhadap nilai yang secara domain tidak masuk akal, seperti salary negatif atau attendance di atas 100%.

Nilai seperti ini dapat merusak distribusi statistik dan menghasilkan interpretasi yang bias.

manual_median <- function(data){
  data_sorted <- sort(data)
  n <- length(data_sorted)
  if(n == 0){
    return(NA)
  }
  mid <- n %/% 2
  if(n %% 2 == 0){
    return((data_sorted[mid] + data_sorted[mid + 1]) / 2)
  } else {
    return(data_sorted[mid + 1])
  }
}
clean_invalid_values <- function(df, rules){
  df_clean <- df
  summary_list <- list()
  for(col in names(rules)){
    if(!(col %in% names(df_clean))){
      next
    }

    min_val <- rules[[col]]$min
    max_val <- rules[[col]]$max

    valid_data <- c()

    for(x in df_clean[[col]]){
      if(is.na(x)){
        next
      }

      valid_condition <- TRUE

      if(!is.null(min_val) && x < min_val){
        valid_condition <- FALSE
      }
      if(!is.null(max_val) && x > max_val){
        valid_condition <- FALSE
      }
      if(valid_condition){
        valid_data <- c(valid_data, x)
      }
    }

    median_val <- manual_median(valid_data)
    replaced_count <- 0
    new_values <- c()
    for(x in df_clean[[col]]){
      if(is.na(x)){
        new_values <- c(new_values, x)
      } else if(
        (!is.null(min_val) && x < min_val) ||
        (!is.null(max_val) && x > max_val)
      ){
        new_values <- c(new_values, median_val)
        replaced_count <- replaced_count + 1
      } else {
        new_values <- c(new_values, x)
      }
    }

    df_clean[[col]] <- new_values
    summary_list[[length(summary_list)+1]] <- data.frame(
      Column = col,
      `Min Rule` = ifelse(is.null(min_val), NA, min_val),
      `Max Rule` = ifelse(is.null(max_val), NA, max_val),
      Replaced = replaced_count,
      `Replacement Value (Median)` = median_val
    )
  }

  summary_df <- do.call(rbind, summary_list)
  return(list(df_clean, summary_df))
}

Nilai tidak logis diperbaiki menggunakan median dari data valid agar distribusi tetap stabil dan tidak terlalu dipengaruhi oleh nilai ekstrem.

3.5 Cek Data Setelah Cleaning

Secara keseluruhan, dataset telah dibersihkan dari missing value, data duplikat, inkonsistensi kategori, dan nilai tidak logis sehingga data menjadi lebih stabil dan siap digunakan untuk analisis statistik deskriptif serta transformasi data pada tahap berikutnya.


Task Data Science Programming

4 Descriptive Statistic

Setelah data melalui proses data preparation dan dipastikan bersih dari missing value, duplikasi, inkonsistensi kategori, serta nilai tidak logis, tahap selanjutnya adalah melakukan analisis statistik deskriptif.

Tahap ini bertujuan untuk memahami karakteristik utama data, mulai dari pusat distribusi, penyebaran, bentuk distribusi, hingga potensi outlier pada setiap variabel. Analisis dilakukan menggunakan perhitungan statistik secara manual tanpa fungsi instan statistik bawaan, sehingga proses perhitungan dapat dipahami secara konseptual sesuai tujuan pembelajaran pada praktikum ini.

4.1 TASK 1

Extended Describe: Variabel Numerik
Analisis statistik deskriptif pada variabel numerik dilakukan untuk mengevaluasi pola distribusi data setelah proses cleaning selesai dilakukan. Perhitungan dilakukan secara manual untuk memperoleh ukuran statistik utama dengan konsep berikut:

  • Mean adalah rata-rata aritmatik:
\[ \bar{x} = \frac{\sum x_i}{n} \]
  • Median adalah nilai tengah data setelah diurutkan. Kalau \(n\) genap, diambil rata-rata dua nilai tengahnya.
  • Mode adalah nilai yang paling sering muncul. Fungsi ini mendukung multimode — kalau ada lebih dari satu nilai dengan frekuensi tertinggi, semuanya ditampilkan.
  • Variance dan Standar Deviasi mengukur seberapa jauh data menyebar dari mean:
\[ s^2 = \frac{\sum (x_i - \bar{x})^2}{n-1}, \quad s = \sqrt{s^2} \]

Dibagi \(n-1\) karena ini data sampel, bukan populasi penuh.

  • Q1 dan Q3 adalah persentil ke-25 dan ke-75, dihitung dengan interpolasi linear.
  • Skewness mengukur kemiringan distribusi:
\[ \text{skewness} = \frac{n}{(n-1)(n-2)} \sum \left( \frac{x_i - \bar{x}}{s} \right)^3 \] Positif → ekor condong ke kanan (ada nilai ekstrem besar)
Negatif → ekor condong ke kiri
Mendekati 0 → distribusi cukup simetris
  • Kurtosis (excess kurtosis) mengukur keruncingan distribusi:
\[ \text{kurtosis} = \frac{ n(n+1) }{ (n-1)(n-2)(n-3) } \sum \left( \frac{x_i - \bar{x}}{s} \right)^4 - \frac{ 3(n-1)^2 }{ (n-2)(n-3) } \] Nilai \(> 0\) → lebih runcing dari distribusi normal
Nilai \(< 0\) → lebih datar dari distribusi normal
  • Normality check sederhana: distribusi dianggap mendekati normal jika \(|\text{skewness}| \leq 0.5\).
  • Outlier count dihitung pakai metode IQR (penjelasan lengkap di Task 3).
# FUNCTION PERCENTILE MANUAL
calc_percentile <- function(data, p){
  data <- sort(data)
  n <- length(data)
  if(n == 0){
    return(NA)
  }
  idx <- (p / 100) * (n - 1)
  lo <- floor(idx) + 1
  hi <- lo + 1
  frac <- idx - floor(idx)
  if(hi > n){
    return(data[lo])
  }
  data[lo] + frac * (data[hi] - data[lo])
}

# EXTENDED DESCRIBE MANUAL
extended_describe_numeric <- function(df, exclude_cols = NULL){
  num_cols <- names(df)[sapply(df, is.numeric)]
  num_cols <- num_cols[!(num_cols %in% exclude_cols)]
  results <- list()
  for(col in num_cols){
    series <- df[[col]]
    data <- series[!is.na(series)]
    n_total <- length(series)
    n <- length(data)
    if(n == 0){
      next
    }

    # Mean
    mean_val <- sum(data) / n

    # Median
    sorted_data <- sort(data)
    mid <- floor(n / 2)
    if(n %% 2 == 0){
      median_val <- (sorted_data[mid] + sorted_data[mid + 1]) / 2
    } else {
      median_val <- sorted_data[mid + 1]
    }

    # Mode (multimode)
    freq <- table(data)
    max_freq <- max(freq)
    mode_vals <- names(freq[freq == max_freq])

    # Variance
    variance_val <- sum((data - mean_val)^2) / (n - 1)

    # Standard deviation
    std_val <- sqrt(variance_val)

    # Min Max Range
    min_val <- min(data)
    max_val <- max(data)
    range_val <- max_val - min_val

    # Quartile
    q1 <- calc_percentile(data, 25)
    q3 <- calc_percentile(data, 75)

    # Skewness
    if(n >= 3 && std_val > 0){
      skew_sum <- sum(((data - mean_val) / std_val)^3)
      skewness_val <- (n / ((n - 1) * (n - 2))) * skew_sum
    } else {
      skewness_val <- NA
    }

    # Kurtosis
    if(n >= 4 && std_val > 0){
      kurt_sum <- sum(((data - mean_val) / std_val)^4)
      c1 <- (n * (n + 1)) /
        ((n - 1) * (n - 2) * (n - 3))
      c2 <- (3 * (n - 1)^2) /
        ((n - 2) * (n - 3))
      kurtosis_val <- c1 * kurt_sum - c2
    } else {
      kurtosis_val <- NA
    }

    # Missing
    missing_val <- n_total - n
    missing_pct <- round((missing_val / n_total) * 100, 2)

    # Outlier IQR
    iqr <- q3 - q1
    lb <- q1 - (1.5 * iqr)
    ub <- q3 + (1.5 * iqr)
    n_outlier <- sum(data < lb | data > ub)

    # Normality heuristic
    if(abs(skewness_val) <= 0.5){
      normality <- "Relative Normal"
    } else if(skewness_val > 0.5){
      normality <- "Right Skew"
    } else {
      normality <- "Left Skew"
    }
    results[[col]] <- data.frame(
      Count = n,
      Mean = round(mean_val, 2),
      Median = round(median_val, 2),
      Mode = paste(mode_vals, collapse = ", "),
      Std = round(std_val, 2),
      Variance = round(variance_val, 2),
      Min = round(min_val, 2),
      Max = round(max_val, 2),
      Range = round(range_val, 2),
      Q1 = round(q1, 2),
      Q3 = round(q3, 2),
      Skewness = round(skewness_val, 4),
      Kurtosis = round(kurtosis_val, 4),
      Missing = missing_val,
      Missing_Percent = paste0(missing_pct, "%"),
      Outlier_Count = n_outlier,
      Normality = normality
    )

  }

  final_df <- bind_rows(results, .id = "Variable")
  return(final_df)
}

Hasil analisis menunjukkan bahwa distribusi variabel numerik tidak sepenuhnya homogen. Variabel seperti Age dan Years_Experience cenderung relatif simetris dengan mean dan median yang berdekatan, sehingga distribusinya lebih stabil.

Sebaliknya, Monthly_Salary, Training_Hours, dan Project_Completed menunjukkan pola right skew, yang mengindikasikan adanya sebagian kecil observasi bernilai tinggi dibanding mayoritas data. Sementara Attendance_Rate cenderung left skew karena mayoritas nilai berada pada tingkat kehadiran tinggi.

Deteksi outlier juga menunjukkan bahwa beberapa variabel masih memiliki nilai ekstrem meskipun proses cleaning telah dilakukan. Kondisi ini menegaskan bahwa median dan quartile lebih representatif dibanding mean dalam menggambarkan pusat distribusi pada data yang tidak sepenuhnya normal.

4.2 TASK 2

Extended Describe: Variabel Kategorikal
Tahap berikutnya adalah mengevaluasi karakteristik variabel kategorik untuk memahami pola distribusi, dominasi kategori, serta konsistensi data non-numerik pada dataset. Karena variabel kategorik berbentuk label atau kelompok, maka ukuran statistik seperti mean dan standar deviasi tidak relevan digunakan. Oleh sebab itu, analisis difokuskan pada frekuensi, jumlah kategori unik, kategori dominan, serta proporsi missing value untuk melihat struktur distribusi data secara lebih representatif.

Statistik yang digunakan pada analisis ini meliputi:

  • Count → jumlah data valid (non-missing)
  • Unique → jumlah kategori berbeda dalam suatu variabel
  • Mode → kategori yang paling sering muncul (multimode supported)
  • Frequency → jumlah kemunculan kategori mode
  • Mode % → persentase kategori dominan terhadap total data
  • Missing → jumlah data kosong
  • Missing % → persentase missing value

Persentase missing value dihitung menggunakan rumus:

\[ \text{Missing \%} = \frac{ \text{Jumlah Missing Value} }{ \text{Jumlah Data} } \times 100\% \]

Persentase kategori dominan dihitung dengan:

\[ \text{Mode \%} = \frac{ \text{Frekuensi Mode} }{ \text{Jumlah Data} } \times 100\% \]
extended_describe_categorical <- function(df){
  cat_cols <- names(df)[
    sapply(df, function(x)
      is.character(x) || is.factor(x))
  ]
  results <- list()
  for(col in cat_cols){
    series <- df[[col]]
    n_total <- length(series)

    # Data non-missing
    non_null <- series[!is.na(series)]
    n_count <- length(non_null)

    # Unique manual
    unique_vals <- c()
    for(x in non_null){
      if(!(x %in% unique_vals)){
        unique_vals <- c(unique_vals, x)
      }
    }
    n_unique <- length(unique_vals)

    # Frekuensi manual
    freq_dict <- list()
    for(x in non_null){
      if(is.null(freq_dict[[x]])){
        freq_dict[[x]] <- 1
      } else {
        freq_dict[[x]] <- freq_dict[[x]] + 1
      }
    }

    # Cari mode manual
    if(length(freq_dict) > 0){
      freq_values <- unlist(freq_dict)
      max_freq <- max(freq_values)
      mode_vals <- names(freq_values[freq_values == max_freq])
      mode_pct <- (max_freq / n_total) * 100
    } else {
      mode_vals <- c()
      max_freq <- 0
      mode_pct <- 0
    }

    # Missing info
    missing_val <- n_total - n_count
    missing_pct <- (missing_val / n_total) * 100

    # Simpan hasil
    results[[col]] <- data.frame(
      Variable = col,
      Count = n_count,
      Unique = n_unique,
      Mode = paste(mode_vals, collapse = ", "),
      Frequency = max_freq,
      Mode_Percent = paste0(
        round(mode_pct, 2),
        "%"
      ),
      Missing = missing_val,
      Missing_Percent = paste0(
        round(missing_pct, 2),
        "%"
      )
    )
  }

  final_df <- bind_rows(results)
  return(final_df)
}

cat_stats <- extended_describe_categorical(df)

Distribusi variabel kategorik menunjukkan dominasi yang cukup kuat pada beberapa kategori utama. Variabel Gender didominasi oleh Female, sedangkan Department paling banyak berada pada divisi Finance. Pada sisi pendidikan dan jabatan, mayoritas karyawan berada pada tingkat Bachelor dan Officer.

Pola ini menunjukkan bahwa struktur dataset lebih merepresentasikan kelompok operasional dengan pendidikan sarjana. Dominasi kategori tertentu perlu diperhatikan karena dapat memengaruhi interpretasi hasil analisis lanjutan, terutama pada perbandingan antar kelompok yang distribusinya tidak seimbang.

4.3 TASK 3

Task 3 — Outlier Detection dan Replacement
Setelah distribusi data dianalisis pada tahap sebelumnya, ditemukan bahwa beberapa variabel numerik memiliki pola skew dan nilai ekstrem yang cukup tinggi. Oleh karena itu, tahap berikutnya difokuskan pada deteksi serta penanganan outlier untuk menjaga kestabilan distribusi dan mengurangi distorsi pada statistik deskriptif.

Metode yang digunakan adalah Interquartile Range (IQR) karena lebih robust terhadap distribusi tidak normal dibanding metode berbasis mean dan standar deviasi.

IQR dihitung menggunakan:

\[ IQR = Q3 - Q1 \]

Batas bawah dan batas atas outlier ditentukan dengan:

\[ \text{Lower Bound} = Q1 - 1.5 \times IQR \] \[ \text{Upper Bound} = Q3 + 1.5 \times IQR \]

Nilai yang berada di luar batas tersebut dikategorikan sebagai outlier.

Karena beberapa variabel seperti Monthly_Salary dan Training_Hours memiliki distribusi right skew, maka replacement dilakukan menggunakan median agar pusat distribusi tidak bergeser akibat nilai ekstrem.

# DETECT OUTLIER IQR
detect_outliers_iqr <- function(df, exclude_cols = NULL){
  num_cols <- names(df)[sapply(df, is.numeric)]
  num_cols <- num_cols[
    !(num_cols %in% exclude_cols)
  ]
  results <- list()
  for(col in num_cols){
    data <- df[[col]]
    data <- data[!is.na(data)]
    if(length(data) == 0){
      next
    }

    # Quartile Manual
    q1 <- calc_percentile(data, 25)
    q3 <- calc_percentile(data, 75)
    iqr_val <- q3 - q1

    # Bound
    lb <- q1 - (1.5 * iqr_val)
    ub <- q3 + (1.5 * iqr_val)
    
    # Hitung Outlier Manual
    outlier_vals <- c()
    for(x in data){
      if(x < lb || x > ub){
        outlier_vals <- c(outlier_vals, x)
      }
    }
    n_outlier <- length(outlier_vals)
    outlier_pct <- (n_outlier / length(data)) * 100

    # Simpan Result
    results[[col]] <- data.frame(
      Variable = col,
      Q1 = round(q1, 2),
      Q3 = round(q3, 2),
      IQR = round(iqr_val, 2),
      Lower_Bound = round(lb, 2),
      Upper_Bound = round(ub, 2),
      Outlier_Count = n_outlier,
      Outlier_Percent = paste0(
        round(outlier_pct, 2),
        "%"
      )
    )
  }
  bind_rows(results)
}

# Exclude Variable
exclude_outlier_cols <- c(
  "Employee_ID",
  "Promotion_Last_3Y"
)

outlier_before <- detect_outliers_iqr(
  df,
  exclude_cols = exclude_outlier_cols
)
# REPLACE OUTLIER IQR
replace_outliers_iqr <- function(df, exclude_cols = NULL){
  df_clean <- df
  num_cols <- names(df_clean)[
    sapply(df_clean, is.numeric)
  ]
  num_cols <- num_cols[
    !(num_cols %in% exclude_cols)
  ]

  summary <- list()

  for(col in num_cols){
    data <- df_clean[[col]]
    data <- data[!is.na(data)]
    if(length(data) == 0){
      next
    }

    # Quartile Manual
    q1 <- calc_percentile(data, 25)
    q3 <- calc_percentile(data, 75)
    iqr_val <- q3 - q1
    lb <- q1 - (1.5 * iqr_val)
    ub <- q3 + (1.5 * iqr_val)

    # Median Manual
    median_val <- manual_median(data)
    replaced_count <- 0
    new_values <- c()

    # Replacement Manual
    for(x in df_clean[[col]]){
      if(is.na(x)){
        new_values <- c(new_values, x)
      } else if(x < lb || x > ub){
        new_values <- c(new_values, median_val)
        replaced_count <- replaced_count + 1
      } else {
        new_values <- c(new_values, x)
      }
    }
    df_clean[[col]] <- new_values

    # Summary
    summary[[col]] <- data.frame(
      Variable = col,
      Lower_Bound = round(lb, 2),
      Upper_Bound = round(ub, 2),
      Median_Used = round(median_val, 2),
      Outlier_Replaced = replaced_count
    )
  }
  summary_df <- bind_rows(summary)
  return(list(
    clean_df = df_clean,
    summary = summary_df
  ))
}

outlier_result <- replace_outliers_iqr(
  df,
  exclude_cols = exclude_outlier_cols
)

df_outlier_clean <- outlier_result$clean_df
outlier_replace_summary <- outlier_result$summary

Outlier paling dominan ditemukan pada Monthly_Salary, Training_Hours, dan Project_Completed, yang menunjukkan adanya sebagian kecil karyawan dengan nilai jauh di atas distribusi mayoritas. Pola ini konsisten dengan hasil skewness pada tahap sebelumnya yang menunjukkan distribusi cenderung right skew.

Replacement menggunakan median membuat distribusi menjadi lebih stabil tanpa menghilangkan pola utama data. Setelah cleaning, persebaran data terlihat lebih terkonsentrasi dan tidak lagi didominasi oleh nilai ekstrem.

Pemisahan visualisasi Monthly_Salary dilakukan karena perbedaan skala yang sangat besar dibanding variabel lain. Jika digabung dalam satu boxplot, distribusi variabel lain akan terkompres sehingga pola outlier sulit dianalisis secara visual.

Secara keseluruhan, hasil ini memperkuat bahwa median lebih representatif dibanding mean untuk variabel dengan distribusi tidak simetris dan mengandung outlier tinggi.


5 Data Transformation & Feature Engineering

Setelah data melalui proses data preparation dan analisis statistik deskriptif, tahap selanjutnya adalah melakukan transformasi data dan feature engineering.

Tahap ini bertujuan untuk menyesuaikan representasi data agar lebih siap digunakan pada proses analisis lanjutan. Transformasi dilakukan untuk menyamakan skala data, membentuk kategori baru, mengubah data kategorik menjadi numerik, serta melihat hubungan antar variabel.

Pada bagian ini dilakukan beberapa proses utama, yaitu standardization & normalization, binning, encoding, dan association matrix. Seluruh proses tetap dilakukan menggunakan perhitungan manual tanpa function instan machine learning agar mekanisme transformasi data dapat dipahami secara konseptual sesuai tujuan pembelajaran pada praktikum ini.

5.1 Standarization & Normalization

Standarization & Normalization (Z-Score & Min-Max Scalling)
Tahap awal transformasi dilakukan dengan menyamakan skala antar variabel numerik menggunakan metode Z-Score Standardization dan Min-Max Normalization.

Transformasi ini penting karena beberapa variabel, seperti Monthly_Salary, memiliki rentang nilai jauh lebih besar dibanding variabel lainnya. Jika tidak distandarisasi, variabel berskala besar dapat mendominasi analisis dan membuat interpretasi menjadi kurang proporsional.

1. Z-Score Standardization

Z-score digunakan untuk mengubah distribusi data agar memiliki mean ≈ 0 dan standar deviasi ≈ 1. Rumus:

\[ z = \frac{x - \bar{x}}{s} \]

Keterangan:

  • \(x\) = nilai asli
  • \(\bar{x}\) = mean
  • \(s\) = standar deviasi

2. Min-Max Normalization

Min-Max Scaling digunakan untuk mengubah rentang data menjadi skala 0 sampai 1. Rumus:

\[ x' = \frac{x - x_{\min}}{x_{\max} - x_{\min}} \]
manual_mean <- function(data){
  sum(data) / length(data)
}
manual_std <- function(data){
  mean_val <- manual_mean(data)
  var_val <- sum((data - mean_val)^2) /
    (length(data) - 1)
  sqrt(var_val)
}
manual_min <- function(data){
  min(data)
}
manual_max <- function(data){
  max(data)
}

# Z SCORE MANUAL
z_score_manual <- function(series){
  data <- series[!is.na(series)]
  mean_val <- manual_mean(data)
  std_val <- manual_std(data)
  if(std_val == 0){
    return(
      ifelse(!is.na(series), 0, NA)
    )
  }
  sapply(series, function(x){
    if(is.na(x)){
      return(NA)
    }
    (x - mean_val) / std_val
  })
}

# MIN MAX MANUAL
minmax_manual <- function(series){
  data <- series[!is.na(series)]
  min_val <- manual_min(data)
  max_val <- manual_max(data)
  if((max_val - min_val) == 0){
    return(
      ifelse(!is.na(series), 0, NA)
    )
  }

  sapply(series, function(x){
    if(is.na(x)){
      return(NA)
    }
    (x - min_val) /
      (max_val - min_val)
  })
}

# KOLOM SCALING
scaling_cols <- c(
  "Monthly_Salary",
  "Training_Hours",
  "Years_Experience",
  "Performance_Score"
)

# PROSES SCALING
for(col in scaling_cols){
  df[[paste0(col, "_ZScore")]] <-
    z_score_manual(df[[col]])
  df[[paste0(col, "_MinMax")]] <-
    minmax_manual(df[[col]])
}
manual_mean_list <- function(data){
  sum(data) / length(data)
}
manual_std_list <- function(data){
  mean_val <- manual_mean_list(data)
  var_val <- sum((data - mean_val)^2) /
    (length(data) - 1)
  sqrt(var_val)
}
manual_min_list <- function(data){
  m <- data[1]
  for(x in data){
    if(x < m){
      m <- x
    }
  }
  m
}
manual_max_list <- function(data){
  m <- data[1]
  for(x in data){
    if(x > m){
      m <- x
    }
  }
  m
}

# SUMMARY VALIDASI
scaling_summary <- list()
for(col in scaling_cols){
  z_col <- paste0(col, "_ZScore")
  m_col <- paste0(col, "_MinMax")
  z_data <- df[[z_col]][!is.na(df[[z_col]])]
  m_data <- df[[m_col]][!is.na(df[[m_col]])]
  scaling_summary[[col]] <- data.frame(
    Variable = col,
    ZScore_Mean = round(
      manual_mean_list(z_data), 4
    ),
    ZScore_Std = round(
      manual_std_list(z_data), 4
    ),
    MinMax_Min = round(
      manual_min_list(m_data), 4
    ),
    MinMax_Max = round(
      manual_max_list(m_data), 4
    )
  )
}

scaling_summary_df <- bind_rows(scaling_summary)

Hasil standardization menunjukkan bahwa seluruh variabel hasil Z-score memiliki mean mendekati 0 dan standar deviasi mendekati 1. Sementara itu, hasil Min-Max normalization berhasil mengubah rentang data ke skala 0–1 secara konsisten.

Transformasi ini membuat distribusi antar variabel menjadi lebih seimbang dan siap digunakan pada proses feature engineering berikutnya. Dengan skala yang sudah seragam, pembentukan kategori atau segmentasi data melalui proses binning dapat dilakukan dengan lebih stabil dan representatif tanpa dipengaruhi perbedaan rentang nilai antar variabel.

5.2 TASK 4

Numerical Grouping (Binning)
Tahap berikutnya adalah melakukan feature engineering melalui teknik binning. Proses ini bertujuan mengubah data numerik kontinu menjadi beberapa kategori interval agar distribusi data lebih mudah diinterpretasikan secara statistik maupun bisnis.

Binning juga membantu menyederhanakan pola distribusi yang sebelumnya terlihat kompleks pada analisis skewness dan outlier. Dengan pengelompokan ini, interpretasi menjadi lebih jelas, misalnya membandingkan kelompok usia produktif, kategori performa, maupun band gaji antar karyawan.

Konsep Statistik — Numerical Grouping (Binning)

Binning adalah proses membagi data numerik ke dalam beberapa interval tertentu.

Jika diberikan interval: \[ [b_0, b_1), [b_1, b_2), \ldots, [b_{k-1}, b_k] \] Maka setiap nilai \(x\) akan dimasukkan ke kategori sesuai interval tempat nilai tersebut berada.

Tujuan utama binning:

  • Menyederhanakan interpretasi data numerik
  • Membantu analisis distribusi kelompok
  • Mempermudah analisis kategorik lanjutan
  • Mengurangi sensitivitas terhadap variasi ekstrem
# FUNCTION NUMERICAL GROUPING MANUAL
numerical_grouping <- function(df, col, bins, labels){
  if(length(labels) != (length(bins) - 1)){
    stop("Jumlah labels harus sama dengan jumlah bins - 1")
  }
  grouped <- c()
  for(val in df[[col]]){
    # missing value
    if(is.na(val)){
      grouped <- c(grouped, "Unknown")
      next
    }
    category <- "Unknown"
    for(i in 1:(length(bins) - 1)){
      left  <- bins[i]
      right <- bins[i + 1]
      # interval terakhir inclusive
      if(i == (length(bins) - 1)){
        if(val >= left && val <= right){
          category <- labels[i]
          break
        }
      } else {
        if(val >= left && val < right){
          category <- labels[i]
          break
        }
      }
    }

    grouped <- c(grouped, category)
  }
  return(grouped)
}


# BUAT KATEGORI BININH
# AGE GROUP
df$Age_Group <- numerical_grouping(
  df,
  "Age",
  bins = c(0, 30, 40, 50, 100),
  labels = c(
    "Young (<30)",
    "Mid Career (30-40)",
    "Senior (40-50)",
    "Late Career (50+)"
  )
)

# SALARY BAND
df$Salary_Band <- numerical_grouping(
  df,
  "Monthly_Salary",
  bins = c(
    0,
    8000000,
    15000000,
    25000000,
    999999999
  ),
  labels = c(
    "Low",
    "Middle",
    "High",
    "Very High"
  )
)

# PERFORMANCE CATEGORY
df$Performance_Category <- numerical_grouping(
  df,
  "Performance_Score",
  bins = c(0, 70, 85, 101),
  labels = c(
    "Low Performer",
    "Moderate Performer",
    "High Performer"
  )
)

# ATTENDANCE CATEGORY
df$Attendance_Category <- numerical_grouping(
  df,
  "Attendance_Rate",
  bins = c(0, 80, 90, 95, 101),
  labels = c(
    "Poor",
    "Warning",
    "Good",
    "Excellent"
  )
)

# DISTRIBUTION FUNCTION MANUAL
category_distribution <- function(df, col){
  freq_dict <- list()
  # hitung frekuensi manual
  for(val in df[[col]]){
    if(is.na(val)){
      next
    }
    if(is.null(freq_dict[[val]])){
      freq_dict[[val]] <- 1
    } else {
      freq_dict[[val]] <- freq_dict[[val]] + 1
    }
  }
  total <- nrow(df)
  results <- data.frame()
  for(name in names(freq_dict)){
    count_val <- freq_dict[[name]]
    temp <- data.frame(
      Category = name,
      Count = count_val,
      Percentage = paste0(
        round((count_val / total) * 100, 2), "%"
        )
    )
    results <- rbind(results, temp)
  }

  # sorting manual descending
  results <- results[
    order(-results$Count),
  ]

  rownames(results) <- NULL
  return(results)
}

Hasil binning menunjukkan bahwa mayoritas karyawan berada pada kelompok Mid Career (30–40 tahun), yang mengindikasikan dominasi tenaga kerja usia produktif menengah. Dari sisi kompensasi, sebagian besar karyawan berada pada kategori Middle Salary, sehingga distribusi gaji cenderung terkonsentrasi pada level menengah.

Pada aspek performa, mayoritas observasi berada pada kategori Moderate Performer, sedangkan kategori High Performer memiliki proporsi lebih kecil. Temuan ini konsisten dengan analisis sebelumnya, di mana distribusi performance score relatif stabil tanpa dominasi nilai ekstrem.

Sementara itu, tingkat kehadiran didominasi kategori Good dan Excellent, yang menunjukkan pola disiplin kerja yang cukup baik secara keseluruhan. Hasil grouping ini memperjelas pola distribusi numerik yang sebelumnya terlihat pada analisis statistik deskriptif dan visualisasi distribusi data.

5.3 TASK 5

Task 5 — Encoding Variabel Kategorik
Setelah variabel numerik dikelompokkan melalui proses binning, tahap berikutnya adalah mengubah variabel kategorik menjadi bentuk numerik melalui teknik encoding. Proses ini diperlukan agar data kategorik dapat digunakan dalam analisis statistik lanjutan maupun pemodelan komputasi tanpa menghilangkan informasi utama dari setiap kategori.

Encoding dilakukan menggunakan dua pendekatan berbeda sesuai karakteristik datanya, yaitu Label Encoding untuk kategori ordinal dan One-Hot Encoding untuk kategori nominal.

Konsep Statistik — Encoding Variabel Kategorik

1. Label Encoding (Ordinal Encoding)

Label Encoding digunakan pada data kategorik yang memiliki urutan logis.

Contoh:

\[ \text{Junior} < \text{Officer} < \text{Supervisor} < \text{Manager} \]

Setiap kategori dipetakan menjadi nilai integer:

\[ \text{Junior}=1,\; \text{Officer}=2,\; \text{Supervisor}=3,\; \text{Manager}=4 \]

Teknik ini mempertahankan informasi urutan antar kategori.


2. One-Hot Encoding

One-Hot Encoding digunakan pada data nominal yang tidak memiliki urutan.

Setiap kategori akan dibuat menjadi variabel biner:

\[ x = \begin{cases} 1, & \text{jika observasi termasuk kategori} \\ 0, & \text{jika tidak} \end{cases} \]

Metode ini mencegah model menganggap terdapat hubungan urutan antar kategori nominal.

Label Encoding (Ordinal Encoding)
# LABEL ENCODING MANUAL
label_encoding <- function(df, col, order){
  mapping_df <- data.frame(
    Category = order,
    Encoded_Value = 1:length(order)
  )
  encoded <- c()
  for(val in df[[col]]){
    idx <- which(order == val)
    if(length(idx) == 0){
      encoded <- c(encoded, NA)
    } else {
      encoded <- c(encoded, idx)
    }
  }
  return(list(
    encoded_series = encoded,
    mapping_table = mapping_df
  ))
}

# URUTAN ORDINAL
job_order <- c(
  "Junior",
  "Officer",
  "Supervisor",
  "Manager",
  "Senior Manager"
)

edu_order <- c(
  "Diploma",
  "Bachelor",
  "Master",
  "PhD"
)

# JOB LEVEL ENCODING
job_result <- label_encoding(
  df,
  "Job_Level",
  job_order
)
df$Job_Level_Code <- job_result$encoded_series
job_mapping <- job_result$mapping_table

# EDUCATION ENCODING
edu_result <- label_encoding(
  df,
  "Education_Level",
  edu_order
)
df$Education_Code <- edu_result$encoded_series
edu_mapping <- edu_result$mapping_table
One Hot Encoding
# ONE HOT ENCODING MANUAL
onehot_encoding <- function(df, col, drop_first = FALSE){
  categories <- sort(unique(df[[col]]))
  categories <- categories[!is.na(categories)]
  if(drop_first == TRUE){
    categories <- categories[-1]
  }
  ohe_df <- data.frame(row_id = 1:nrow(df))
  for(cat in categories){
    new_col <- c()
    for(val in df[[col]]){
      if(val == cat){
        new_col <- c(new_col, 1)
      } else {
        new_col <- c(new_col, 0)
      }
    }
    ohe_df[[paste0(col, "_", cat)]] <- new_col
  }
  ohe_df$row_id <- NULL
  return(ohe_df)
}

gender_ohe <- onehot_encoding(df,"Gender")
dept_ohe <- onehot_encoding(df,"Department",drop_first = TRUE)

# gabungkan ke dataframe utama
df <- cbind(df, gender_ohe, dept_ohe)

Encoding berhasil mengubah variabel kategorik menjadi representasi numerik tanpa menghilangkan struktur informasi utama pada data. Label encoding mempertahankan hubungan ordinal pada variabel seperti job level dan education level, sedangkan one-hot encoding memastikan kategori nominal seperti gender dan department tidak dianggap memiliki urutan tertentu.

Hasil transformasi ini membuat dataset lebih siap digunakan untuk analisis kuantitatif lanjutan, termasuk perhitungan asosiasi, korelasi, maupun pemodelan statistik berbasis numerik.

5.4 TASK 6

Task 6 — Matriks Asosiasi
Setelah variabel numerik distandardisasi, dilakukan analisis asosiasi untuk memahami hubungan antar variabel dalam dataset. Analisis ini penting untuk melihat apakah terdapat pola keterkaitan yang cukup kuat antar fitur, baik pada data numerik maupun kategorik.

Karena tipe data berbeda, metode asosiasi yang digunakan juga disesuaikan:

  • Kovariansi dan Pearson Correlation digunakan untuk variabel numerik
  • Cramér’s V digunakan untuk variabel kategorik

Seluruh perhitungan dilakukan secara manual tanpa menggunakan fungsi statistik instan agar proses matematis dapat dipahami secara konseptual sesuai tujuan praktikum.

1. Kovariansi (Numerik vs Numerik)

Kovariansi mengukur arah hubungan linear antara dua variabel numerik.

\[ \operatorname{Cov}(X,Y) = \frac{ \sum_{i=1}^{n} (x_i-\bar{x}) (y_i-\bar{y}) }{ n-1 } \]

Interpretasi:

\(\operatorname{Cov} > 0\) → hubungan searah
\(\operatorname{Cov} < 0\) → hubungan berlawanan arah
\(\operatorname{Cov} \approx 0\) → hubungan linear lemah

Namun, nilai kovariansi dipengaruhi skala data sehingga sulit dibandingkan secara langsung antar variabel.

2. Korelasi Pearson (Numerik vs Numerik)

Pearson Correlation merupakan bentuk normalisasi dari kovariansi sehingga nilainya berada pada rentang \(-1\) sampai \(1\).

\[ r = \frac{ \operatorname{Cov}(X,Y) }{ s_X \cdot s_Y } \]

Interpretasi:

\(r \rightarrow 1\) → hubungan positif kuat
\(r \rightarrow -1\) → hubungan negatif kuat
\(r \rightarrow 0\) → hubungan linear lemah

3. Cramér’s V (Kategorik vs Kategorik)

Cramér’s V digunakan untuk mengukur kekuatan asosiasi antar variabel kategorik.

\[ V = \sqrt{ \frac{ \chi^2 }{ n \cdot \min(r-1, k-1) } } \]

Interpretasi:

\(V = 0\) → tidak ada asosiasi
\(V \rightarrow 1\) → asosiasi sangat kuat

# COVARIANCE MANUAL
covariance_manual <- function(x, y){
  pairs <- data.frame(x, y)
  pairs <- pairs %>%
    filter(!is.na(x) & !is.na(y))
  if(nrow(pairs) < 2){
    return(NA)
  }
  xs <- pairs$x
  ys <- pairs$y
  n <- length(xs)
  mean_x <- sum(xs) / n
  mean_y <- sum(ys) / n
  cov_val <- sum(
    (xs - mean_x) * (ys - mean_y)
  ) / (n - 1)
  return(cov_val)
}
covariance_matrix_manual <- function(df, numeric_cols){
  mat <- matrix(
    nrow = length(numeric_cols),
    ncol = length(numeric_cols)
  )
  rownames(mat) <- numeric_cols
  colnames(mat) <- numeric_cols
  for(i in seq_along(numeric_cols)){
    for(j in seq_along(numeric_cols)){
      col1 <- numeric_cols[i]
      col2 <- numeric_cols[j]
      mat[i, j] <- round(
        covariance_manual(df[[col1]], df[[col2]]),
        4
      )
    }
  }
  as.data.frame(mat)
}

# PEARSON CORRELATION MANUAL
std_manual <- function(data){
  clean_data <- data[!is.na(data)]
  mean_val <- sum(clean_data) / length(clean_data)
  var_val <- sum(
    (clean_data - mean_val)^2
  ) / (length(clean_data) - 1)
  sqrt(var_val)
}

pearson_corr_manual <- function(x, y){
  pairs <- data.frame(x, y)
  pairs <- pairs %>%
    filter(!is.na(x) & !is.na(y))
  if(nrow(pairs) < 2){
    return(NA)
  }
  xs <- pairs$x
  ys <- pairs$y
  std_x <- std_manual(xs)
  std_y <- std_manual(ys)
  if(std_x == 0 || std_y == 0){
    return(NA)
  }
  cov_val <- covariance_manual(xs, ys)
  r <- cov_val / (std_x * std_y)
  return(r)
}
pearson_matrix_manual <- function(df, numeric_cols){
  mat <- matrix(
    nrow = length(numeric_cols),
    ncol = length(numeric_cols)
  )
  rownames(mat) <- numeric_cols
  colnames(mat) <- numeric_cols
  for(i in seq_along(numeric_cols)){
    for(j in seq_along(numeric_cols)){
      col1 <- numeric_cols[i]
      col2 <- numeric_cols[j]
      mat[i, j] <- round(
        pearson_corr_manual(
          df[[col1]],
          df[[col2]]
        ),
        4
      )
    }
  }
  as.data.frame(mat)
}

# NUMERIC COLUMNS
numeric_cols <- c(
  "Age",
  "Years_Experience",
  "Training_Hours",
  "Monthly_Salary",
  "Performance_Score",
  "Attendance_Rate",
  "Project_Completed"
)

# GENERATE MATRICES
cov_matrix <- covariance_matrix_manual(df, numeric_cols)
pearson_matrix <- pearson_matrix_manual(df,numeric_cols)
# CONTINGENCY TABLE MANUAL
contingency_table_manual <- function(df, row_col, col_col){
  row_categories <- sort(
    unique(df[[row_col]])
  )
  col_categories <- sort(
    unique(df[[col_col]])
  )
  row_categories <- row_categories[!is.na(row_categories)]
  col_categories <- col_categories[!is.na(col_categories)]
  ct <- matrix(
    0,
    nrow = length(row_categories),
    ncol = length(col_categories)
  )
  rownames(ct) <- row_categories
  colnames(ct) <- col_categories
  for(i in 1:nrow(df)){
    r <- df[[row_col]][i]
    c <- df[[col_col]][i]
    if(is.na(r) || is.na(c)){
      next
    }
    row_idx <- which(row_categories == r)
    col_idx <- which(col_categories == c)
    ct[row_idx, col_idx] <-
      ct[row_idx, col_idx] + 1
  }
  as.data.frame(ct)
}

# COMBINED TABLE
contingency_table_combined <- function(df, row_col, col_col){
  ct <- contingency_table_manual(
    df,
    row_col,
    col_col
  )
  percent_df <- ct
  for(i in 1:nrow(percent_df)){
    row_sum <- sum(percent_df[i, ])
    percent_df[i, ] <-
      round(
        (percent_df[i, ] / row_sum) * 100,
        2
      )
  }
  combined <- data.frame(
    Category = rownames(ct)
  )
  for(col in names(ct)){
    combined[[paste0(col, "_Count")]] <- ct[[col]]
    combined[[paste0(col, "_Percent")]] <-
      paste0(percent_df[[col]], "%")
  }
  combined
}

# GENERATE TABLE
ct_combined <- contingency_table_combined(df, "Department", "Performance_Category")
# CHI-SQUARE MANUAL
chi_square_manual <- function(ct){
  total <- sum(as.matrix(ct))
  row_totals <- rowSums(ct)
  col_totals <- colSums(ct)
  chi2 <- 0
  for(i in 1:nrow(ct)){
    for(j in 1:ncol(ct)){
      observed <- ct[i, j]
      expected <- (
        row_totals[i] *
        col_totals[j]
      ) / total
      if(expected == 0){
        next
      }
      chi2 <- chi2 +
        ((observed - expected)^2 / expected)
    }
  }
  chi2
}

# CRAMER'S V MANUAL
cramers_v_manual <- function(df, col1, col2){
  ct <- contingency_table_manual(
    df,
    col1,
    col2
  )
  chi2 <- chi_square_manual(ct)
  n <- sum(as.matrix(ct))
  r <- nrow(ct)
  k <- ncol(ct)
  denom <- min(r - 1, k - 1)
  if(denom == 0){
    return(NA)
  }
  v <- sqrt(
    chi2 / (n * denom)
  )
  return(v)
}
cramers_v_matrix_manual <- function(df, categorical_cols){
  mat <- matrix(
    nrow = length(categorical_cols),
    ncol = length(categorical_cols)
  )
  rownames(mat) <- categorical_cols
  colnames(mat) <- categorical_cols
  for(i in seq_along(categorical_cols)){
    for(j in seq_along(categorical_cols)){
      col1 <- categorical_cols[i]
      col2 <- categorical_cols[j]
      if(col1 == col2){
        mat[i, j] <- 1
      } else {
        mat[i, j] <- round(
          cramers_v_manual(
            df,
            col1,
            col2
          ),
          4
        )
      }
    }
  }
  as.data.frame(mat)
}
categorical_cols <- c(
  "Gender",
  "Department",
  "Education_Level",
  "Job_Level",
  "Age_Group",
  "Salary_Band",
  "Performance_Category",
  "Attendance_Category"
)

cramers_matrix <- cramers_v_matrix_manual(df, categorical_cols)

Hasil analisis menunjukkan bahwa hubungan antar variabel dalam dataset cenderung lemah. Nilai Pearson Correlation pada heatmap sebagian besar berada di sekitar nol, sehingga tidak terlihat hubungan linear yang kuat antar variabel numerik.

Scatter plot juga memperkuat hasil tersebut. Hubungan antara Years_Experience terhadap Performance_Score maupun Monthly_Salary terhadap Performance_Score menunjukkan pola sebaran yang acak tanpa tren linear yang jelas. Hal ini mengindikasikan bahwa peningkatan pengalaman kerja atau salary tidak selalu diikuti peningkatan performa secara konsisten.

Pada visualisasi stacked bar chart, distribusi kategori performance antar department terlihat relatif merata tanpa dominasi ekstrem pada kategori tertentu. Hasil ini selaras dengan nilai Cramér’s V yang cenderung rendah, sehingga asosiasi antar variabel kategorik dapat dikatakan lemah.

Secara keseluruhan, baik analisis numerik maupun kategorik menunjukkan bahwa performa karyawan kemungkinan dipengaruhi oleh faktor lain di luar variabel yang tersedia pada dataset.


KESIMPULAN DATASET

Secara keseluruhan, dataset menunjukkan kondisi organisasi yang relatif stabil, dengan mayoritas karyawan berada pada kategori performa moderat, tingkat kehadiran yang baik, serta struktur gaji yang didominasi kategori menengah.

Variasi ekstrem hanya muncul pada sebagian kecil observasi, terutama pada variabel kompensasi dan aktivitas kerja, yang terlihat melalui pola skewness dan keberadaan outlier pada beberapa variabel numerik.

Hasil transformasi data menunjukkan bahwa proses standardisasi, normalisasi, binning, dan encoding berhasil membuat data lebih terstruktur dan siap digunakan untuk analisis lanjutan tanpa menghilangkan karakteristik utama dataset.

Namun, analisis asosiasi menunjukkan bahwa hubungan antar variabel cenderung lemah, baik pada hubungan numerik maupun kategorik. Variabel seperti usia, pengalaman kerja, salary, maupun departemen tidak menunjukkan pengaruh linear yang kuat terhadap performance score.

Hal ini mengindikasikan bahwa performa karyawan kemungkinan dipengaruhi oleh faktor lain di luar dataset yang tersedia. Oleh karena itu, diperlukan pendekatan analisis lanjutan, seperti segmentasi, feature engineering tambahan, atau pemodelan prediktif, untuk mengidentifikasi faktor utama yang lebih berpengaruh terhadap kinerja karyawan.