“Garbage in, Garbage out (쓰레기를 넣으면, 쓰레기가 나온다)” - 데이터 분석의 제1원칙입니다. 이 문서는 성공적인 HR 분석을 위해 가장 중요하지만 가장 많은 시간이 소요되는 '데이터 전처리'의 모든 것을 다루는 실무 가이드입니다.
훌륭한 요리가 신선하고 깨끗하게 손질된 재료에서 시작되듯, 신뢰할 수 있는 분석 결과는 잘 정제된 데이터에서 나옵니다. 데이터 전처리는 단순히 데이터를 '깨끗하게' 만드는 것을 넘어, 분석의 목적에 맞게 데이터를 '요리하기 좋은 상태'로 만드는 과정입니다.
HR 데이터는 특히나 사람과 조직의 복잡한 특성이 얽혀 있어, 다른 어떤 데이터보다 세심한 '재료 손질(전처리)'이 필요합니다.
문제 예시
: '인사팀', '인사 팀'(띄어쓰기), 'HR' 등 담당자마다 다른 표기법 사용, 연봉이나 나이의 오타(0을 하나 더 붙이는 등)문제 예시
: 과거 '전략기획팀'이 현재 '미래전략실'로 변경된 경우, 두 부서를 동일한 부서로 인식하지 못하면 연도별 분석에 오류 발생문제 예시
: 직원 이름을 익명의 ID로 대체하거나, 상세 주소를 '수도권'/'비수도권' 등으로 그룹화하는 과정에서 데이터 구조가 변경됨문제 예시
: 과거에는 '850115' 형식으로 저장하던 생년월일을 현재는 '1985-01-15' 형식으로 저장하여, 두 데이터를 합쳤을 때 형식이 불일치함💡 실무자의 교훈:
“지저분한 데이터는 단순히 작업 시간을 늘리는 것을 넘어, 분석 결과 전체를 왜곡시킬 수 있습니다. 예를 들어, 부서명을 통일하지 않고 이직률을 계산하면 특정 부서의 이직률이 실제보다 낮게 계산되어 잘못된 의사결정으로 이어질 수 있습니다. 전처리에 투자하는 시간은 분석의 신뢰성을 담보하는 가장 확실한 투자입니다.”
① 범주형 데이터 불일치 (부서명/직급명 등)
# === 실무 예시: 부서명 표준화 === # 문제가 있는 원본 데이터 dept_names <- c("인사팀", "인사부", "HR팀", "HR부", "재무팀", "회계팀") # 해결 방법: dplyr 패키지의 case_when 함수로 규칙 기반 표준화 library(dplyr) dept_standardized <- case_when( dept_names %in% c("인사팀", "인사부", "HR팀", "HR부") ~ "인사팀", // 여러 표기를 '인사팀'으로 통일 dept_names %in% c("재무팀", "회계팀") ~ "재무회계팀", // 유사 부서를 '재무회계팀'으로 통합 TRUE ~ dept_names // 규칙에 해당하지 않는 나머지는 그대로 유지 ) # 코드 해설: # case_when은 여러 개의 if-else 구문을 체계적으로 처리해주는 매우 유용한 함수입니다. # 'A이면 B로 바꾸고, C이면 D로 바꾸고, 그 외에는 그대로 둬라' 와 같은 복잡한 규칙을 쉽게 구현할 수 있습니다.
② 날짜 형식 불일치
# === 실무 예시: 다양한 날짜 형식 통일 === # lubridate 패키지는 날짜 처리를 매우 쉽게 만들어주는 필수 패키지입니다. library(lubridate) # 문제가 있는 원본 데이터 dates_chr <- c("2024-01-15", "2024/02/20", "24.03.10", "2024년 4월 5일") # 해결 방법: lubridate 패키지의 파싱(Parsing) 함수 사용 clean_dates <- ymd(dates_chr) // '연-월-일' 순서로 된 대부분의 형식을 자동으로 인식하여 변환 # 코드 해설: # ymd() 함수는 'yyyy-mm-dd', 'yyyy/mm/dd', 'yy.mm.dd' 등 다양한 구분자를 가진 # '연-월-일' 순서의 문자열을 똑똑하게 인식하여 표준 날짜 객체로 바꿔줍니다. # 만약 '월-일-연' 순서라면 mdy()를, '일-월-연' 순서라면 dmy()를 사용하면 됩니다.
③ 결측치 (Missing Values, NA)
# === 실무 예시: 결측치 처리 === # 1. 결측치 확인: 어떤 변수에 얼마나 많은 결측치가 있는지 파악하는 것이 우선입니다. # summarise_all(~sum(is.na(.))) : 모든 컬럼에 대해 is.na() (결측치인가?)의 합계(sum)를 구함 hr_data %>% summarise_all(~sum(is.na(.))) # 2. 처리 방법 선택 # 방법 A: 삭제 (Listwise Deletion) - 결측치가 있는 행 전체를 제거. # 주의! 정보 손실이 크므로, 결측치 비율이 매우 낮고(보통 5% 미만) 데이터가 충분히 많을 때만 고려 clean_data_A <- hr_data %>% drop_na() # 방법 B: 평균/중앙값 대체 (Imputation) - 수치형 변수의 결측치를 해당 변수의 평균이나 중앙값으로 채움. # 주의! 데이터의 분포를 왜곡시킬 수 있음. 이상치가 많을 때는 평균보다 중앙값이 더 안정적. # hr_data$salary의 결측치(NA)를, 결측치를 제외한(na.rm=T) salary의 평균(mean)으로 대체 hr_data$salary[is.na(hr_data$salary)] <- mean(hr_data$salary, na.rm = TRUE) # 방법 C: 최빈값 대체 (Mode Imputation) - 범주형 변수의 결측치를 가장 자주 등장하는 값으로 채움. get_mode <- function(v) { names(which.max(table(v))) } hr_data$department[is.na(hr_data$department)] <- get_mode(hr_data$department)
④ 이상치 (Outliers)
# === 실무 예시: IQR 기법을 이용한 이상치 처리 === # 1. 시각적 탐지: 박스플롯(Boxplot)은 이상치를 한눈에 보여주는 훌륭한 도구입니다. # 박스(상자)의 위/아래 수염을 벗어나는 점들이 이상치 후보입니다. library(ggplot2) ggplot(hr_data, aes(y = salary)) + geom_boxplot() # 2. 통계적 처리: IQR(Interquartile Range) 규칙 # Q1(25% 지점)과 Q3(75% 지점)을 찾고, 그 사이의 거리(IQR)를 계산 Q1 <- quantile(hr_data$salary, 0.25, na.rm = TRUE) Q3 <- quantile(hr_data$salary, 0.75, na.rm = TRUE) IQR_value <- Q3 - Q1 # 정상 범위를 정의 (Q1 - 1.5*IQR ~ Q3 + 1.5*IQR) lower_bound <- Q1 - 1.5 * IQR_value upper_bound <- Q3 + 1.5 * IQR_value # 정상 범위 내의 값만 남기고 필터링 clean_data <- hr_data %>% filter(salary >= lower_bound & salary <= upper_bound) # 코드 해설: # 이 방법은 데이터 분포의 중앙 50%를 기준으로, 거기서 너무 멀리 떨어진 값들을 # 이상치로 간주하는 통계적으로 널리 쓰이는 방법입니다. # 하지만 CEO의 연봉처럼 '실제 이상치'일 수 있으므로, 제거 전 반드시 현업 담당자와 확인해야 합니다.
Step 1: 데이터 탐색 및 구조 파악 (EDA - Exploratory Data Analysis)
목표
: 데이터의 전체적인 모습과 건강 상태를 진단합니다.
# 데이터의 구조(데이터 타입 등) 확인 str(hr_data) # >> 'str'은 structure의 약자. 각 컬럼의 이름, 타입(예: num, chr, Date), 일부 값을 보여줌 # >> 여기서 숫자여야 할 컬럼이 chr(문자)로 되어 있지는 않은지 등을 1차로 확인 # 기술 통계량 요약 summary(hr_data) # >> 숫자형 컬럼에 대해서는 최소, 최대, 평균, 중앙값 등을 보여줌 # >> 여기서 나이가 200이라거나, 만족도 점수가 5점 만점에 6점으로 나오는 등의 오류를 발견할 수 있음 # 결측치 패턴 시각화 (VIM 패키지) library(VIM) aggr(hr_data, prop = FALSE, numbers = TRUE) # >> 어떤 컬럼에 결측치가 집중되어 있는지, 컬럼 간 결측치 발생이 연관성이 있는지 등을 시각적으로 파악
Step 2: 데이터 타입 변환
목표
: 각 변수를 분석 목적에 맞는 올바른 타입으로 바꿔줍니다. 컴퓨터가 '숫자'와 '문자', '날짜'를 구분하게 만들어야 합니다.
# 문자형 -> 숫자형 (계산을 위해) hr_data$tenure_str <- "3.5" # "3.5"는 문자 hr_data$tenure_num <- as.numeric(hr_data$tenure_str) # 3.5는 숫자 # 문자형 -> 날짜형 (기간 계산을 위해) hr_data$hire_date <- as.Date(hr_data$hire_date, format = "%Y-%m-%d") # 문자형 -> 범주형(Factor) (그룹 분석을 위해) # Factor로 변환하면 R은 이 변수를 그룹화 가능한 범주로 인식하고, 분석 시 편리하게 처리해 줍니다. hr_data$department <- as.factor(hr_data$department)
Step 3: 중복 데이터 처리
목표
: 동일한 정보가 중복으로 집계되는 것을 막아 분석의 정확성을 높입니다.
# 완전히 동일한 행(Row) 확인 및 제거 duplicated_rows <- hr_data[duplicated(hr_data), ] # 중복된 행을 찾아냄 hr_data_clean <- hr_data[!duplicated(hr_data), ] # 중복되지 않은 행만 남김 # 특정 ID 기준 중복 제거 (더 자주 사용됨) # 예를 들어, 한 직원의 정보가 실수로 두 번 입력된 경우, 직원 ID 기준으로 하나만 남겨야 합니다. hr_data_clean <- hr_data %>% distinct(employee_id, .keep_all = TRUE) # employee_id가 같은 행 중 첫 번째만 남기고 모두 제거
Step 4: 파생 변수 생성 (Feature Engineering)
목표
: 기존 변수를 조합하여 분석에 더 유용한 새로운 변수를 만듭니다. 이는 분석의 깊이를 더하는 창의적인 과정입니다.
# 입사일과 기준일로부터 '근속연수' 계산 hr_data <- hr_data %>% mutate( tenure_years = as.numeric(difftime(Sys.Date(), hire_date, units = "days")) / 365.25 ) # 생년월일로부터 '연령대' 생성 hr_data <- hr_data %>% mutate( age_group = case_when( age < 30 ~ "20대", age >= 30 & age < 40 ~ "30대", age >= 40 & age < 50 ~ "40대", TRUE ~ "50대 이상" ) )
데이터 검토 (Review) 단계
str()
확인)summary()
확인)데이터 정제 (Clean) 단계
데이터 변환 (Transform) 단계
데이터 검증 (Validate) 단계
❌ 자주 하는 실수들:
✅ 올바른 접근법:
'tidyverse' 생태계 하나면 대부분 해결됩니다. 'tidyverse'는 데이터 과학을 위해 설계된 R 패키지들의 모음이며, 아래 패키지들을 포함합니다. 일관된 문법으로 데이터 전처리를 매우 효율적으로 만들어줍니다.
filter()
(필터링), mutate()
(변수생성), group_by()
& summarise()
(그룹별요약) 등 데이터 전처리의 80%를 담당합니다.pivot_longer()
, pivot_wider()
(데이터 형태 변환), drop_na()
(결측치제거) 등의 함수를 제공합니다.str_replace()
(문자열 교체), str_detect()
(문자열 탐지) 등 텍스트 데이터 정제에 필수적입니다.전처리는 눈에 잘 띄지 않는 고된 작업이지만, 그 효과를 정량적으로 보여줌으로써 나의 기여를 증명할 수 있습니다.
전처리 전후 비교 지표:
# 전처리 효과를 보여주는 간단한 리포트 함수 preprocessing_report <- function(before_df, after_df) { cat("====== 데이터 전처리 효과 보고서 ======\n\n") cat("1. 데이터 볼륨 변화:\n") cat(" - 전처리 전 행 수:", nrow(before_df), "개\n") cat(" - 전처리 후 행 수:", nrow(after_df), "개\n") cat(" - 유효 데이터 변화율:", round(nrow(after_df) / nrow(before_df) * 100, 1), "%\n\n") cat("2. 데이터 완성도 개선:\n") before_na <- sum(is.na(before_df)) after_na <- sum(is.na(after_df)) cat(" - 전처리 전 총 결측치:", before_na, "개\n") cat(" - 전처리 후 총 결측치:", after_na, "개\n") cat(" - 결측치 감소율:", round((before_na - after_na) / before_na * 100, 1), "%\n\n") cat("3. 데이터 일관성 향상 (부서명 예시):\n") before_dept_count <- length(unique(before_df$department)) after_dept_count <- length(unique(after_df$department)) cat(" - 전처리 전 부서명 종류:", before_dept_count, "개\n") cat(" - 전처리 후 부서명 종류:", after_dept_count, "개\n") cat("=======================================\n") } # 사용 예시 # preprocessing_report(raw_hr_data, final_hr_data)
💡 최종 조언:
완벽한 전처리를 위해 너무 많은 시간을 쓰기보다, '이 분석의 목적을 달성하기 위해 최소한 어떤 정제가 필요한가?'를 먼저 생각하세요. 분석의 목적에 따라 전처리의 깊이와 범위는 달라질 수 있습니다. 작게 시작하고, 분석을 진행하며 필요한 전처리를 추가해 나가는 애자일(Agile)한 접근 방식이 더 효과적일 수 있습니다.