Assignment DS Programming Week 11
Angelique Kiyoshi Lakeisha B.U
NIM: 52250001
Student Major Data Science at Institut Teknologi Sains Bandung
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.
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
)
)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))
}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))
}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.
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
- Mean adalah rata-rata aritmatik:
- 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:
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:
Negatif → ekor condong ke kiri
Mendekati 0 → distribusi cukup simetris
- Kurtosis (excess kurtosis) mengukur keruncingan distribusi:
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
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
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$summaryOutlier 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
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
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
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 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 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
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.
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.