데이터 전처리
데이터 과학의 분석 과정에서 가장 중요한 단계 중 하나가 바로 데이터 전처리(Data Preprocessing)이다. 아무리 정교한 알고리즘을 사용하더라도 입력 데이터가 불완전하거나 왜곡되어 있다면, 모델의 성능은 크게 저하될 수밖에 없다. 실제로 데이터 분석의 품질은 데이터 전처리 과정에서 얼마나 꼼꼼하게 준비했는지에 따라 결정된다고 해도 과언이 아니다.
데이터 전처리는 원시(raw) 데이터를 분석 및 학습에 적합한 형태로 변환하는 일련의 과정을 의미한다. 수집된 데이터에는 종종 결측치(missing value)가 존재하거나, 이상치(outlier)가 포함되며, 변수 간 스케일(scale)이 제각각인 경우도 많다. 또한 범주형(categorical) 변수와 같은 비수치 데이터는 기계학습 알고리즘이 직접 처리할 수 없으므로 적절한 변환이 필요하다.
본 장에서는 데이터 전처리의 핵심 요소를 다룬다. 먼저 결측치 처리 방법을 통해 누락된 데이터를 보완하는 방법을 살펴본다. 이어서 이상치 탐지를 통해 데이터 분포에서 벗어난 값을 식별하고 처리하는 방법을 배운다. 다음으로 정규화(normalization)와 표준화(standardization)를 통해 변수 간 크기 차이를 조정하여 알고리즘의 효율성을 높이는 방법을 학습한다. 마지막으로 범주형 데이터 처리 과정을 통해 문자열 기반 데이터를 수치형으로 변환하는 방법을 다룬다.
이러한 전처리 과정은 단순히 데이터를 "깨끗하게(cleaning)" 만드는 것을 넘어, 분석 결과의 신뢰성과 해석 가능성을 보장한다. 따라서 데이터 전처리는 데이터 과학 전 과정에서 반드시 거쳐야 하는 필수 단계이며, 분석가가 가장 많은 시간을 투자하는 영역 중 하나이다.
학습목표
- 결측치 처리 (Missing Value Handling)
- 이상치 탐지 (Outlier Detection)
- 데이터 정규화 (Data Normalization/Scaling)
- 범주형 데이터 처리 (Categorical Data Handling)
결측치 처리
결측치의 정의와 필요성
결측치(Missing Value)란 무엇인가?
결측치(Missing Value)란 데이터셋에서 특정 변수의 값이 비어 있거나 기록되지 않은 상태를 의미한다. 이는 데이터 수집 과정에서 흔히 발생하는 현상으로, 설문조사에서 응답자가 특정 문항을 건너뛰거나, 센서 장치가 일시적으로 고장을 일으켜 값을 기록하지 못했을 때 나타난다. 데이터베이스 통합 과정에서 키 값 불일치로 인해 정보가 빠지는 경우도 있다.
결측치는 단순한 빈칸이 아니라, 분석 전반에 중요한 영향을 미치는 요소다. 데이터 과학에서는 “Garbage in, garbage out”이라는 말처럼, 불완전한 데이터를 그대로 사용하면 잘못된 분석 결과를 얻게 된다. 따라서 결측치를 어떻게 다루는지는 데이터 전처리의 핵심 과제이다.
데이터 분석에서 결측치를 무시하면 어떤 문제가 생기는가?
통계적 왜곡
결측치가 존재하는 데이터를 그대로 분석하면 평균, 분산, 상관계수 등 기초 통계량이 왜곡된다. 예를 들어, 소득 데이터에서 고소득층의 응답이 빠져 있다면 평균 소득은 실제보다 낮게 계산된다. 이는 표본이 모집단을 제대로 대표하지 못하는 문제를 낳는다.
모델 정확도 저하
머신러닝 모델은 입력값이 비어 있는 경우 정상적으로 학습하지 못한다. 많은 알고리즘이 결측치를 직접 처리하지 못하기 때문에, 결측치를 포함한 데이터셋을 그대로 학습에 사용하면 예측력이 크게 떨어진다. 특히, 결측치가 특정 패턴을 가진 집단에서 집중적으로 발생한다면 모델은 편향된 학습 결과를 얻게 된다.
학습 불안정성
결측치가 많은 데이터셋은 모델 학습 과정에서 불안정성을 야기한다. 예를 들어, 결측치를 가진 샘플이 무작위로 제거되면 학습 데이터의 크기가 줄어들어 통계적 검정력이 약화된다. 또한 결측치를 잘못된 값으로 대체하면 모델이 실제 패턴을 반영하지 못하고 불안정한 결과를 내놓을 수 있다.
결측치 발생 원인
결측치(Missing Value)는 다양한 이유로 데이터셋에 발생하며, 그 원인을 이해하는 것은 적절한 처리 방법을 선택하는 데 중요한 출발점이 된다. 결측치가 왜 생겼는지를 파악하지 못한 채 단순히 제거하거나 대체하면 분석 결과가 왜곡되거나, 모델이 편향된 학습을 하게 될 위험이 크다. 대표적인 결측치 발생 원인은 다음과 같다.
첫째, 데이터 수집 과정의 오류이다. 데이터는 보통 설문조사, 센서 측정, 웹 로그 기록 등 다양한 방식으로 수집된다. 이 과정에서 입력 실수, 네트워크 지연, 저장 매체의 손상 등이 발생하면 특정 값이 제대로 기록되지 못한다. 예를 들어, 데이터 입력자가 나이를 입력하지 않거나 잘못된 형식으로 기록하는 경우 결측치가 생긴다.
둘째, 설문조사 응답 거부가 있다. 사회과학이나 마케팅 조사에서는 응답자가 일부 문항에 답하지 않는 일이 흔하다. 특히 소득, 재산, 건강 상태처럼 민감한 질문일수록 결측치 발생 가능성이 높다. 이러한 경우 단순히 누락으로 처리하면 데이터의 편향을 초래할 수 있다.
셋째, 센서 또는 시스템 고장도 결측치의 주요 원인이다. 예컨대 IoT 센서가 일시적으로 전원이 꺼지거나 네트워크 연결이 끊어지면, 해당 시간대의 데이터가 수집되지 않는다. 산업 현장에서는 장비의 오작동으로 인해 특정 기간 동안 값이 전혀 기록되지 않는 사례가 빈번하다.
마지막으로, 데이터 통합 과정에서의 불일치가 있다. 여러 데이터베이스나 파일을 통합하는 과정에서 키 값이 일치하지 않으면 특정 항목이 비어 있게 된다. 예를 들어, 한 데이터베이스에는 고객의 이메일 주소가 저장되어 있으나 다른 데이터베이스에는 해당 정보가 없는 경우, 통합 후 이메일 항목에는 결측치가 발생한다.
이처럼 결측치는 단순한 빈칸이 아니라, 데이터가 만들어지고 관리되는 과정에서 발생한 다양한 맥락의 결과물이다. 따라서 결측치를 올바르게 처리하려면 그 원인을 먼저 이해하는 것이 중요하며, 이후의 분석 단계에서 발생할 수 있는 왜곡을 줄이는 데 도움이 된다.
결측치 유형
결측치는 단순히 값이 비어 있는 현상으로만 볼 수 없다. 결측치가 어떤 메커니즘에 의해 발생했는가를 이해해야 올바른 처리 방법을 선택할 수 있다. 통계학에서는 결측치를 발생 메커니즘에 따라 크게 세 가지 유형으로 구분한다.
MCAR (Missing Completely at Random): 완전히 무작위로 누락
완전히 무작위로 결측이 발생하는 경우를 말한다. 특정 값이 빠진 이유가 다른 변수나 해당 값과 아무런 관련이 없다. 예를 들어, 설문지를 인쇄하는 과정에서 우연히 한 장이 손상되어 일부 응답 항목이 지워진 경우가 이에 해당한다. MCAR 상황에서는 결측치가 분석에 체계적인 편향을 주지 않으므로, 단순히 결측 데이터를 제거해도 통계적 신뢰성이 크게 훼손되지 않는다. 하지만 실제 데이터에서는 MCAR가 발생하는 경우가 드물다.
MAR (Missing at Random): 특정 변수와 관련하여 누락
어떤 변수와 관련하여 결측이 발생하지만, 결측 그 자체의 값과는 직접적인 연관이 없는 경우를 말한다. 예를 들어, 고소득자일수록 소득 관련 질문에 응답하지 않는 경향이 있다면, 결측은 응답자의 ‘소득 수준’이라는 변수와 관련이 있다. 그러나 같은 소득 수준의 응답자들 사이에서는 결측 여부가 무작위적이다. 이 경우에는 단순 삭제보다, 다른 변수를 활용하여 결측치를 추정하거나 보완하는 방법이 필요하다.
MNAR (Missing Not at Random): 데이터 자체의 값과 관련해 누락
결측이 해당 값 자체와 직접적으로 연관된 경우를 말한다. 예컨대, 건강 조사에서 비만도가 높은 사람들이 체중을 의도적으로 기록하지 않는다면, 결측은 체중 값과 밀접하게 연결된다. 이 경우 단순히 평균값이나 다른 변수로 대체하면 분석이 크게 왜곡된다. MNAR은 가장 다루기 어려운 유형으로, 도메인 지식이나 심층적인 통계적 기법을 활용해야 한다.
결측치 처리 방법
제거법 (Deletion)
결측치가 발견되면 가장 먼저 고려할 수 있는 방법은 제거법(Deletion)이다. 제거법은 결측치가 포함된 데이터를 분석에서 제외하여, 남아 있는 완전한 데이터만을 활용하는 방식이다. 가장 단순하면서도 직관적인 접근이지만, 데이터 손실과 편향의 위험 때문에 신중히 적용해야 한다.
-
행 제거 (listwise deletion)
행 제거는 결측치가 포함된 전체 행(row)을 삭제하는 방법이다. 예를 들어, 한 응답자가 설문 문항 중 나이(age)를 기입하지 않았다면, 그 응답자의 다른 정보(소득, 직업 등)까지 모두 분석에서 제외된다. 이 방법은 구현이 간단하고, 남은 데이터가 완전하므로 추가적인 결측치 처리가 필요 없다는 장점이 있다. 그러나 결측치가 많을 경우 전체 데이터셋의 크기가 급격히 줄어들어, 표본 수 부족과 통계적 검정력 저하를 초래할 수 있다. 또한 결측이 무작위로 발생하지 않고 특정 집단에 집중되어 있다면, 분석 결과에 편향(bias)이 발생한다.
-
열 제거 (variable deletion)
열 제거는 특정 변수(column)에 결측치가 많이 포함된 경우, 해당 변수를 아예 분석에서 제외하는 방법이다. 예를 들어, 설문조사에서 '취미 활동' 항목에 응답하지 않은 비율이 80% 이상이라면, 이 변수를 삭제하고 다른 변수들만 분석에 활용할 수 있다. 이 방법 역시 단순하고 효과적이지만, 중요한 변수일 경우 데이터의 정보 손실이 크고, 분석 모델의 설명력이 떨어질 수 있다는 단점이 있다.
-
장점/단점 설명
-
장점
- 구현이 매우 간단하다.
- 남은 데이터가 완전하므로, 추가적인 결측치 보완 과정이 필요 없다.
- 결측치가 극히 일부일 경우, 분석 결과에 큰 영향을 주지 않는다.
-
단점
- 데이터 손실이 크며, 결측치 비율이 높을수록 분석 가능 표본이 줄어든다.
- 결측치가 특정 집단에 편중되어 있을 경우, 분석 결과가 왜곡될 위험이 크다.
- 중요한 변수가 삭제될 경우, 모델의 성능과 해석력이 저하된다.
-
제거법 실습
# File: preprocessing_deletion_demo.py
# 목적: 결측치 제거법 실습 (행 제거: listwise deletion, 열 제거: variable deletion)
# 의존성: pandas>=1.5
from __future__ import annotations
import pandas as pd
from typing import Tuple, Optional
def make_sample_dataframe() -> pd.DataFrame:
"""실습용 샘플 데이터프레임 생성"""
data = {
"id": [1, 2, 3, 4, 5, 6, 7],
"age": [23, None, 31, 29, None, 41, 36],
"income": [52000, 61000, None, 58000, 60000, None, 72000],
"city": ["Seoul", "Busan", None, "Daejeon", "Seoul", "Seoul", None],
"hobby": [None, None, "Run", None, "Music", None, None],
}
return pd.DataFrame(data)
def missing_report(df: pd.DataFrame) -> pd.DataFrame:
"""컬럼별 결측 개수와 결측률 리포트"""
n = len(df)
report = pd.DataFrame({
"missing_count": df.isna().sum(),
"missing_rate": (df.isna().mean() * 100).round(2)
}).sort_values("missing_rate", ascending=False)
return report
def listwise_delete(
df: pd.DataFrame,
subset: Optional[list[str]] = None,
keep_threshold: Optional[int] = None
) -> pd.DataFrame:
"""
행 제거(listwise deletion)
- subset: 지정된 컬럼들 중 하나라도 NaN이면 해당 행 삭제
- keep_threshold(thresh): NaN이 아닌 값의 최소 개수 미만인 행을 삭제
(예: keep_threshold=3 -> 최소 3개 이상의 유효 값이 있는 행만 유지)
"""
if keep_threshold is not None:
# thresh는 NaN이 아닌 값의 최소 개수 기준
return df.dropna(thresh=keep_threshold)
if subset is not None:
return df.dropna(subset=subset)
# 기본: 하나라도 NaN이 있으면 행 삭제 (모든 컬럼 기준)
return df.dropna()
def variable_delete_by_missing_ratio(
df: pd.DataFrame,
threshold: float = 0.4
) -> Tuple[pd.DataFrame, pd.Series]:
"""
열 제거(variable deletion)
- threshold: 결측률이 threshold 이상인 컬럼을 제거 (0~1 사이)
예: 0.4 -> 결측률 40% 이상 컬럼 삭제
반환: (제거 후 DF, 제거된 컬럼 목록 시리즈)
"""
miss_rate = df.isna().mean()
drop_cols = miss_rate[miss_rate >= threshold].index
return df.drop(columns=drop_cols), miss_rate[drop_cols]
def main():
# 0) 샘플 데이터 준비
df = make_sample_dataframe()
print("원본 데이터\n", df, "\n")
print("결측 리포트(원본)\n", missing_report(df), "\n")
# 1) 행 제거: 모든 컬럼 기준으로 결측 포함 행 제거 (가장 보수적)
df_listwise_all = listwise_delete(df)
print(f"[행 제거 - 전체 컬럼 기준] shape: {df.shape} -> {df_listwise_all.shape}")
print(df_listwise_all, "\n")
# 2) 행 제거: subset 기준 (예: age, income 중 하나라도 NaN이면 삭제)
df_listwise_subset = listwise_delete(df, subset=["age", "income"])
print(f"[행 제거 - subset=['age','income']] shape: {df.shape} -> {df_listwise_subset.shape}")
print(df_listwise_subset, "\n")
# 3) 행 제거: keep_threshold 사용 (유효값이 4개 미만이면 삭제)
df_listwise_thresh = listwise_delete(df, keep_threshold=4)
print(f"[행 제거 - keep_threshold=4] shape: {df.shape} -> {df_listwise_thresh.shape}")
print(df_listwise_thresh, "\n")
# 4) 열 제거: 결측률 40% 이상인 컬럼 제거
df_col_drop_40, dropped_40 = variable_delete_by_missing_ratio(df, threshold=0.40)
print("[열 제거 - 결측률 40% 이상 삭제] 제거된 컬럼:")
print(dropped_40.apply(lambda r: f"{round(r*100,2)}%"), "\n")
print(f"shape: {df.shape} -> {df_col_drop_40.shape}")
print(df_col_drop_40, "\n")
print("결측 리포트(열 제거 후)\n", missing_report(df_col_drop_40), "\n")
# 5) 열 제거: 결측률 60% 이상인 컬럼만 더 강하게 제거
df_col_drop_60, dropped_60 = variable_delete_by_missing_ratio(df, threshold=0.60)
print("[열 제거 - 결측률 60% 이상 삭제] 제거된 컬럼:")
print(dropped_60.apply(lambda r: f"{round(r*100,2)}%"), "\n")
print(f"shape: {df.shape} -> {df_col_drop_60.shape}")
print(df_col_drop_60, "\n")
# 6) 파이프라인 예시: 먼저 결측률 높은 열 제거 -> 그 다음 subset 기준 행 제거
df_pipe, _ = variable_delete_by_missing_ratio(df, threshold=0.5)
df_pipe = listwise_delete(df_pipe, subset=["age", "income"])
print("[파이프라인] (열 제거 50%) -> (subset 행 제거)")
print(f"최종 shape: {df.shape} -> {df_pipe.shape}")
print(df_pipe, "\n")
# 팁: 실제 분석에서는 제거 전후로 분포가 어떻게 달라지는지 반드시 확인
# 예) df['age'].describe(), df_listwise_subset['age'].describe() 등 비교
if __name__ == "__main__":
main()
실행 결과
$ python .\test.py
원본 데이터
id age income city hobby
0 1 23.0 52000.0 Seoul None
1 2 NaN 61000.0 Busan None
2 3 31.0 NaN None Run
3 4 29.0 58000.0 Daejeon None
4 5 NaN 60000.0 Seoul Music
5 6 41.0 NaN Seoul None
6 7 36.0 72000.0 None None
결측 리포트(원본)
missing_count missing_rate
hobby 5 71.43
income 2 28.57
age 2 28.57
city 2 28.57
id 0 0.00
[행 제거 - 전체 컬럼 기준] shape: (7, 5) -> (0, 5)
Empty DataFrame
Columns: [id, age, income, city, hobby]
Index: []
[행 제거 - subset=['age','income']] shape: (7, 5) -> (3, 5)
id age income city hobby
0 1 23.0 52000.0 Seoul None
3 4 29.0 58000.0 Daejeon None
6 7 36.0 72000.0 None None
[행 제거 - keep_threshold=4] shape: (7, 5) -> (3, 5)
id age income city hobby
0 1 23.0 52000.0 Seoul None
3 4 29.0 58000.0 Daejeon None
4 5 NaN 60000.0 Seoul Music
[열 제거 - 결측률 40% 이상 삭제] 제거된 컬럼:
hobby 71.43%
dtype: object
shape: (7, 5) -> (7, 4)
id age income city
0 1 23.0 52000.0 Seoul
1 2 NaN 61000.0 Busan
2 3 31.0 NaN None
3 4 29.0 58000.0 Daejeon
4 5 NaN 60000.0 Seoul
5 6 41.0 NaN Seoul
6 7 36.0 72000.0 None
결측 리포트(열 제거 후)
missing_count missing_rate
age 2 28.57
income 2 28.57
city 2 28.57
id 0 0.00
[열 제거 - 결측률 60% 이상 삭제] 제거된 컬럼:
hobby 71.43%
dtype: object
shape: (7, 5) -> (7, 4)
id age income city
0 1 23.0 52000.0 Seoul
1 2 NaN 61000.0 Busan
2 3 31.0 NaN None
3 4 29.0 58000.0 Daejeon
4 5 NaN 60000.0 Seoul
5 6 41.0 NaN Seoul
6 7 36.0 72000.0 None
[파이프라인] (열 제거 50%) -> (subset 행 제거)
최종 shape: (7, 5) -> (3, 4)
id age income city
0 1 23.0 52000.0 Seoul
3 4 29.0 58000.0 Daejeon
6 7 36.0 72000.0 None
실행 결과 분석
원본 데이터는 총 7행 5열로 구성되어 있으며, hobby 변수는 71% 이상이 결측으로 나타나 분석에 활용하기 어려운 수준이었다.
또한 age, income, city 변수는 각각 약 28% 정도의 결측치를 가지고 있었으며, id 변수는 결측이 없었다.
먼저 모든 컬럼을 기준으로 결측치가 포함된 행을 삭제하는 경우, 남는 데이터가 하나도 없어 분석 자체가 불가능해졌다. 이는 데이터셋에 결측치가 널리 분포할 때 전체 행 제거 방법이 적합하지 않음을 보여준다.
이에 비해 age와 income 두 변수를 기준으로 결측치가 있는 행만 제거한 경우에는 세 개의 행이 남아, 제한적이지만 분석이 가능한 결과를 얻을 수 있었다. 이는 분석 목적에 따라 핵심 변수를 지정해 선택적으로 행을 제거하는 전략이 현실적일 수 있음을 시사한다.
또한 하나의 행에서 최소한 네 개 이상의 값이 존재해야 유지되도록 설정한 경우, 세 개의 행이 남았다. 이처럼 임계값을 설정하는 방법은 전체 데이터의 손실을 줄이면서도 일정 수준 이상의 정보가 확보된 데이터를 선별할 수 있는 장점이 있다.
열 제거 방법에서는 결측률이 40% 이상인 변수(hobby)가 삭제되었으며, 그 결과 남은 네 개의 변수는 상대적으로 결측률이 낮아졌다. 결측률을 60% 이상으로 설정했을 때도 hobby만 제거되는 결과가 나타나, 이 변수가 결측치 비율이 과도하게 높음을 다시 확인할 수 있었다.
마지막으로, 파이프라인 방식으로 결측률이 높은 변수를 먼저 제거한 뒤, 핵심 변수(age, income)를 기준으로 행을 삭제한 결과 세 개의 행과 네 개의 변수가 남았다.
이는 실제 분석에서 흔히 사용하는 접근으로, 결측률이 높은 변수를 과감히 제거하고 이후 남은 데이터에서 필요한 기준에 따라 행을 선별함으로써 데이터 손실을 최소화하면서도 분석의 신뢰성을 확보하는 방법이다.
단순한 전체 행 제거는 실무에서 적절하지 않으며, 변수별 결측률과 분석 목적을 고려하여 부분적인 행 제거와 열 제거를 병행하는 것이 더 효과적이다. 특히 파이프라인 접근법은 데이터 품질을 유지하면서 분석 가능한 자료를 확보하는 데 있어 가장 실용적인 전략임을 확인할 수 있었다.
대체법 (Imputation)
제거법은 간단하지만 데이터 손실이 크다는 한계가 있다. 따라서 결측치를 채워 넣어 데이터셋을 보완하는 방식인 대체법(Imputation)이 자주 사용된다. 대체법은 결측치를 특정 값이나 통계적 예측으로 메워, 데이터의 크기를 유지하면서 분석을 진행할 수 있도록 한다. 그러나 잘못된 대체는 새로운 왜곡을 만들 수 있기 때문에 방법의 특성과 한계를 이해하고 적용해야 한다.
첫째, 단순 대체 방법이 있다. 이는 결측치를 평균(mean), 중앙값(median), 최빈값(mode) 등으로 채우는 방식이다. 예를 들어, 나이 변수의 결측치를 전체 평균 나이로 채우거나, 성별 변수의 결측치를 가장 많이 등장한 값으로 대체할 수 있다. 이 방법은 구현이 쉽고 직관적이라는 장점이 있지만, 데이터의 분산을 인위적으로 축소시키거나 분포의 왜곡을 초래할 수 있다는 단점이 있다.
둘째, 통계적 대체 방법이 있다. 회귀분석을 이용해 결측치를 가진 변수를 다른 변수들로 설명하고 예측하여 채우는 방식이 대표적이다. 예를 들어, 소득 변수가 결측일 경우, 나이와 학력 정보를 기반으로 회귀모형을 세워 소득을 추정해 넣을 수 있다. 또 다른 방법으로 K-최근접 이웃(K-Nearest Neighbors, KNN) 기반 대체가 있다. 이는 결측치를 가진 샘플과 가장 가까운 이웃들의 값을 참고해 평균이나 다수결로 채우는 방식이다. 이러한 통계적 대체법은 단순 대체보다 더 정교하게 결측치를 보완할 수 있으나, 계산량이 많고 데이터 특성에 따라 성능이 달라진다는 제약이 있다. 회귀 기반 대체는 통계적 대체 방법의 대표적인 사례이며, Iterative Imputer와 같은 확장된 방식은 고급 기법으로 발전한 형태로 볼 수 있다.
여기서 잠깐! Iterative Imputer란 무엇인가?
회귀 기반 대체의 한 방법으로, Scikit-learn에서 제공하는 Iterative Imputer는
다중 대체(MICE: Multiple Imputation by Chained Equations) 방법을 구현한 도구이다.
이 방식은 결측치가 있는 각 변수를 다른 변수들의 함수로 가정하고,
반복적으로 회귀 모형을 세워 결측치를 추정한다.
구체적인 절차는 다음과 같다.
1. 먼저 결측치가 있는 변수를 하나 선택한다.
2. 선택된 변수를 제외한 나머지 변수를
설명 변수로 사용하여 회귀모형을 학습한다.
3. 학습된 회귀모형으로 결측값을 예측하여 채운다.
4. 이 과정을 모든 변수에 대해 순차적으로 반복한다.
5. 여러 번 반복(iteration)하면서 추정값을 점점 안정화시킨다.
Iterative Imputer의 장점은 단순히 평균이나 최빈값으로
채우는 것보다 변수 간 상관관계와 데이터의 구조적 특성을
더 잘 반영할 수 있다는 점이다.
예를 들어, 소득 데이터가 결측이라면, 나이·학력·지역 등의
다른 정보를 활용해 보다 그럴듯한 값을 예측할 수 있다.
하지만 이 방법에도 한계가 있다.
- 데이터가 적거나 변수 간 상관성이 약한 경우에는 부정확한 값이 생성될 수 있다.
- 계산 비용이 크고, 반복 횟수와 초기값 설정에 따라 결과가 달라질 수 있다.
- 예측된 값이 실제 범위와 크게 벗어나는 경우도 발생할 수 있다.
따라서 Iterative Imputer는 고급 기법으로 분류되며,
단순 대체나 KNN 대체보다 정교하지만,
반드시 데이터의 특성과 목적을 고려해 적용해야 한다.
셋째, 고급 기법으로는 다중 대체(Multiple Imputation)와 머신러닝 기반 예측 활용이 있다. 다중 대체는 결측치를 하나의 값이 아닌 여러 개의 가능한 값으로 반복적으로 채워 넣고, 그 결과를 종합해 불확실성을 반영하는 방법이다. 이는 통계적으로 가장 엄밀한 접근법 중 하나이다.
머신러닝 기법을 활용하는 방법은 의사결정나무, 랜덤포레스트, 심지어 신경망 등을 이용해 결측치를 예측하여 채우는 것이다. 이러한 방법들은 결측 메커니즘이 복잡하거나 데이터의 상호작용이 강한 경우에 효과적일 수 있다.
대체법은 데이터의 손실을 최소화하면서 결측 문제를 해결할 수 있는 유용한 방법이다. 그러나 단순 대체에서 고급 기법까지 방법마다 장단점이 다르므로, 데이터의 특성과 분석 목적을 고려하여 적절한 방식을 선택하는 것이 중요하다.
구분 | 기법 | 설명 | 장점 | 단점 |
---|---|---|---|---|
단순 대체 | 평균/최빈값 대체 | 수치형은 평균(또는 중앙값), 범주형은 최빈값으로 결측치를 채움 | 구현이 매우 간단, 계산이 빠름 | 데이터 분산이 줄고, 분포 왜곡 발생 가능 |
통계적 대체 | KNN 대체 | 결측치가 있는 샘플과 유사한 K개의 이웃 데이터를 찾아 평균/다수결로 대체 | 데이터 패턴을 잘 반영, 단순 대체보다 현실성 높음 | 계산 비용이 크고, K 값 선택에 따라 결과 달라짐 |
고급 대체 | Iterative Imputer | 각 변수를 다른 변수로 회귀 예측하여 반복적으로 대체 | 변수 간 상관관계 반영, 정교한 대체 가능, 불확실성 고려 가능 | 계산량 많음, 잘못 적용 시 비현실적 값 생성, 초기값·설정에 민감 |
회귀 기반 대체법이란?
결측치(누락된 값)가 있는 변수를 다른 변수들의 관계를 활용해 예측하고 채워 넣는 방법
쉽게 말해, 누락된 값을 "예측"해서 채우는 방식을 의미함.
-
예시 1: 학생 성적 데이터
-
학생들의 성적 데이터가 있다고 가정
-
변수: 수학 점수, 영어 점수, 국어 점수
-
문제: 일부 학생의 영어 점수가 결측치로 빠져 있음
-
이때 영어 점수를 그냥 평균으로 채우는 대신, 수학 점수와 국어 점수를 이용해 회귀모형을 만든 뒤 예측값으로 채움.
-
“수학과 국어 성적이 비슷하면 영어 점수도 비슷하겠지?”라는 가정을 활용하면, 결과적으로 단순 평균보다 더 개별 학생의 특성을 반영한 대체가 가능
-
-
예시 2: 키와 몸무게 데이터
-
병원 환자 데이터에서 일부 환자의 몸무게(Weight)가 결측이라고 가정
-
다른 변수: 키(Height), 나이(Age)
-
접근: 키와 나이를 독립 변수로, 몸무게를 종속 변수로 해서 회귀모델을 만든다.
-
결측치가 있는 몸무게 값은 회귀모델이 예측한 값으로 채워 넣는다.
-
단순 평균으로 채우는 것보다 실제 개인의 신체적 특성을 반영한 합리적인 보정이 가능하다.
-
대체법 실습
# File: preprocessing_imputation_demo_clean_min.py
# 목적: 결측치 대체(Imputation) 기본 실습
# 기능: 단순 대체(평균·최빈값), KNN 대체, 회귀 기반 대체
# 설명 수준: 데이터사이언스 입문 교재용 (친절한 주석 포함)
import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer, KNNImputer
from sklearn.experimental import enable_iterative_imputer # IterativeImputer 사용 가능하게 함
from sklearn.impute import IterativeImputer
from sklearn.linear_model import BayesianRidge
# =========================
# 데이터 준비
# =========================
def make_sample_dataframe() -> pd.DataFrame:
"""실습용 샘플 데이터프레임 생성"""
return pd.DataFrame({
"id": [1, 2, 3, 4, 5, 6, 7],
"age": [23, None, 31, 29, None, 41, 36],
"income": [52000, 61000, None, 58000, 60000, None, 72000],
"city": ["Seoul", "Busan", None, "Daejeon", "Seoul", "Seoul", None],
"hobby": [None, None, "Run", None, "Music", None, None],
})
# =========================
# 유틸리티 함수
# =========================
def normalize_missing(df: pd.DataFrame) -> pd.DataFrame:
"""None → np.nan 변환"""
return df.replace({None: np.nan})
def detect_dtypes(df: pd.DataFrame, exclude: list[str] | None = None) -> tuple[list[str], list[str]]:
"""수치형/범주형 컬럼 구분"""
exclude = exclude or []
num_cols = [c for c in df.select_dtypes(include=[np.number]).columns if c not in exclude]
cat_cols = [c for c in df.select_dtypes(exclude=[np.number]).columns if c not in exclude]
return num_cols, cat_cols
def missing_report(df: pd.DataFrame) -> pd.DataFrame:
"""결측 개수와 결측률 리포트"""
return pd.DataFrame({
"missing_count": df.isna().sum(),
"missing_rate": (df.isna().mean() * 100).round(2)
}).sort_values("missing_rate", ascending=False)
# =========================
# 결측치 대체 방법
# =========================
def simple_impute_mean_mode(df: pd.DataFrame) -> pd.DataFrame:
"""단순 대체: 수치형=평균, 범주형=최빈값"""
df = normalize_missing(df.copy())
num_cols, cat_cols = detect_dtypes(df, exclude=["id"])
if num_cols:
df[num_cols] = SimpleImputer(strategy="mean").fit_transform(df[num_cols])
if cat_cols:
df[cat_cols] = SimpleImputer(strategy="most_frequent").fit_transform(df[cat_cols])
return df
def knn_impute(df: pd.DataFrame, n_neighbors: int = 3) -> pd.DataFrame:
"""KNN 기반 대체"""
df = normalize_missing(df.copy())
num_cols, cat_cols = detect_dtypes(df, exclude=["id"])
out = df.copy()
if num_cols:
out[num_cols] = KNNImputer(n_neighbors=n_neighbors).fit_transform(df[num_cols])
if cat_cols:
out[cat_cols] = SimpleImputer(strategy="most_frequent").fit_transform(df[cat_cols])
return out
def iterative_impute(df: pd.DataFrame) -> pd.DataFrame:
"""회귀 기반 대체 (Iterative Imputer)"""
df = normalize_missing(df.copy())
num_cols, cat_cols = detect_dtypes(df, exclude=["id"])
out = df.copy()
if num_cols:
it = IterativeImputer(
estimator=BayesianRidge(),
max_iter=10,
random_state=42,
sample_posterior=True
)
out[num_cols] = it.fit_transform(df[num_cols])
if cat_cols:
out[cat_cols] = SimpleImputer(strategy="most_frequent").fit_transform(out[cat_cols])
return out
# =========================
# 실행 예제
# =========================
def main() -> None:
df = make_sample_dataframe()
print("========== 원본 데이터 ==========\n")
print(f"{df}\n")
print("결측 리포트(원본)\n")
print(f"{missing_report(df)}\n")
print("---------- 단순 대체 (평균/최빈값) ---------\n")
df1 = simple_impute_mean_mode(df)
print(f"{df1}\n")
print("결측 리포트(단순 대체)\n")
print(f"{missing_report(df1)}\n")
print("---------- KNN 기반 대체 ---------\n")
df2 = knn_impute(df, n_neighbors=3)
print(f"{df2}\n")
print("결측 리포트(KNN)\n")
print(f"{missing_report(df2)}\n")
print("---------- 회귀 기반 대체 (Iterative) ---------\n")
df3 = iterative_impute(df)
print(f"{df3.round(2)}\n") # 보기 좋게 소수점 반올림
print("결측 리포트(Iterative)\n")
print(f"{missing_report(df3)}\n")
if __name__ == "__main__":
main()
실행 결과
$ python test.py
========== 원본 데이터 ==========
id age income city hobby
0 1 23.0 52000.0 Seoul None
1 2 NaN 61000.0 Busan None
2 3 31.0 NaN None Run
3 4 29.0 58000.0 Daejeon None
4 5 NaN 60000.0 Seoul Music
5 6 41.0 NaN Seoul None
6 7 36.0 72000.0 None None
결측 리포트(원본)
missing_count missing_rate
hobby 5 71.43
income 2 28.57
age 2 28.57
city 2 28.57
id 0 0.00
---------- 단순 대체 (평균/최빈값) ---------
id age income city hobby
0 1 23.0 52000.0 Seoul Music
1 2 32.0 61000.0 Busan Music
2 3 31.0 60600.0 Seoul Run
3 4 29.0 58000.0 Daejeon Music
4 5 32.0 60000.0 Seoul Music
5 6 41.0 60600.0 Seoul Music
6 7 36.0 72000.0 Seoul Music
결측 리포트(단순 대체)
missing_count missing_rate
id 0 0.0
age 0 0.0
income 0 0.0
city 0 0.0
hobby 0 0.0
---------- KNN 기반 대체 ---------
id age income city hobby
0 1 23.000000 52000.000000 Seoul Music
1 2 29.333333 61000.000000 Busan Music
2 3 31.000000 60666.666667 Seoul Run
3 4 29.000000 58000.000000 Daejeon Music
4 5 29.333333 60000.000000 Seoul Music
5 6 41.000000 60666.666667 Seoul Music
6 7 36.000000 72000.000000 Seoul Music
결측 리포트(KNN)
missing_count missing_rate
id 0 0.0
age 0 0.0
income 0 0.0
city 0 0.0
hobby 0 0.0
---------- 회귀 기반 대체 (Iterative) ---------
id age income city hobby
0 1 23.00 52000.00 Seoul Music
1 2 21.57 61000.00 Busan Music
2 3 31.00 63716.89 Seoul Run
3 4 29.00 58000.00 Daejeon Music
4 5 7.87 60000.00 Seoul Music
5 6 41.00 59621.37 Seoul Music
6 7 36.00 72000.00 Seoul Music
결측 리포트(Iterative)
missing_count missing_rate
id 0 0.0
age 0 0.0
income 0 0.0
city 0 0.0
hobby 0 0.0
실행 결과 해석
원본 데이터는 7개의 행과 5개의 열로 구성되어 있으며, hobby 변수는 약 71%가 결측치로 확인되었다.
또한 age, income, city 역시 각각 28.6%의 결측치를 포함하고 있어 데이터 품질이 낮은 상태임을 알 수 있다.
단순 대체(평균/최빈값) 방법을 적용하면 수치형 변수는 평균값으로, 범주형 변수는 최빈값으로 채워졌다.
예를 들어 age의 결측치는 평균 32로, income의 결측치는 평균 60,600으로 대체되었다. city의 결측치는 모두 Seoul로 채워졌으며, hobby는 Music으로 채워졌다.
이 방식은 간단하게 결측 문제를 해결하지만, 데이터의 다양성이 줄어들고 분포가 왜곡될 수 있다.
KNN 기반 대체에서는 각 샘플과 유사한 이웃들의 값을 참고하여 결측치를 채웠다. 그 결과 age와 income의 결측치는 주변 값들의 평균으로 보완되었고, city와 hobby 역시 최빈값으로 채워졌다.
이 방법은 단순 대체보다 데이터의 실제 패턴을 더 잘 반영하지만, 계산 비용이 크고 K 값 선택에 따라 결과가 달라질 수 있다는 한계가 있다.
회귀 기반 대체(Iterative Imputer)는 각 변수를 다른 변수들로 회귀 예측하면서 반복적으로 결측치를 채우는 방식이다.
예를 들어 id=5번의 age는 단순 평균이나 KNN과는 다른 값인 약 7.87로 추정되었다. 이는 Iterative Imputer가 다른 변수와의 관계를 적극적으로 활용했음을 보여준다.
이 방식은 가장 정교하게 결측치를 보완할 수 있지만, 계산량이 많고 경우에 따라 비현실적인 값이 생성될 수 있다는 점을 주의해야 한다.
세 가지 방법 모두 결측치를 제거하는 데 성공했지만 결과는 서로 달랐다. 단순 대체는 구현이 가장 간단하나 데이터의 변동성을 축소시키고, KNN 대체는 데이터 패턴을 잘 반영하며, Iterative Imputer는 변수 간 상관관계를 활용해 가장 정교한 대체를 제공한다. 따라서 실제 분석에서는 데이터의 특성과 분석 목적에 맞는 방법을 선택하는 것이 중요하다.
도메인 지식 활용
결측치 처리를 할 때는 단순히 통계적 방법이나 알고리즘만으로는 부족한 경우가 많다. 실제 분석 대상이 되는 데이터의 맥락과 도메인 지식을 함께 고려해야 한다. 도메인 지식이란 해당 분야의 전문가적 배경지식을 의미하며, 이를 활용하면 결측치를 단순히 '비어 있음'으로 처리하지 않고 더 정확하게 해석할 수 있다.
예를 들어 환자 데이터에서 검사 결과 값이 0
으로 기록된 경우와 아예 값이 기록되지 않은 경우는 의미가 다르다. 0
은 실제로 해당 증상이 없음을 나타낼 수 있지만, “미기록”은 검사가 이루어지지 않았거나 기록이 누락된 것일 수 있다. 만약 두 경우를 동일하게 결측치로 처리한다면 환자의 건강 상태에 대한 잘못된 결론을 내릴 위험이 있다.
또한 금융 데이터에서 0원
과 데이터 없음
을 구분하는 것도 중요하다. 고객의 거래 금액이 0원이라는 것은 실제로 거래가 없었다는 의미이지만, 값이 비어 있는 경우는 데이터 입력 과정에서 오류가 있었을 가능성이 있다. 이처럼 도메인 지식은 데이터가 갖는 맥락을 해석하고 결측치를 어떻게 처리해야 하는지 방향을 제시해준다.
따라서 결측치 처리 과정에서는 항상 데이터의 본질적 의미와 수집 배경을 고려해야 하며, 필요하다면 도메인 전문가와 협업하여 “없음”과 “미기록”을 명확히 구분하는 작업이 필요하다. 이는 단순히 결측을 채우는 수준을 넘어, 분석의 신뢰도를 높이고 실제 현장에 적용 가능한 결과를 도출하는 데 필수적인 과정이다.
간단한 예를 들면 다음과 같다.
- 환자 데이터
- 검사 결과가
0
→ 실제로 해당 증상이 없음 - 값이 기록되지 않음(
미기록
) → 검사 자체가 이루어지지 않았거나 누락
- 검사 결과가
- 금융 데이터
- 거래 금액이 “0원” → 실제 거래가 없음
- 값이 없음(미기록) → 입력 오류나 시스템 문제로 인한 누락 가능성
도메인 지식 활용 실습
결측값을 기계적으로 채우는 데서 그치지 않고, 도메인 지식을 적용해 값의 의미를 구분하는 것이다. 실습 코드는 생리학적으로 0이 될 수 없는 연속형 지표(수축기·이완기 혈압, 혈당)를 포함한 환자 데이터를 생성하고, 0과 미기록(NaN)을 다르게 다루는 두 가지 접근을 비교하도록 구성되어 있다.
문제 정의
여러분이 다룰 데이터에는 다음 변수가 포함된다:
컬럼명 | 데이터 유형 | 의미 | 일반적 범위/값 |
---|---|---|---|
sbp | 연속형 | 수축기 혈압 (Systolic BP) | 정상: 약 90 \~ 120 mmHg |
dbp | 연속형 | 이완기 혈압 (Diastolic BP) | 정상: 약 60 \~ 80 mmHg |
glucose | 연속형 | 혈당 수치 (Blood Glucose) | 정상 공복 혈당: 약 70 \~ 100 mg/dL |
symptom_present | 이진 | 증상 존재 여부 | 0 = 증상 없음, 1 = 증상 있음 |
diagnosis | 범주형 | 진단 결과 | Hypertension, Diabetes, None 등 |
이때 sbp
/dbp
/glucose
의 0
은 실제값이 아니라 기록 누락을 숫자 0
으로 입력한 것으로 가정한다.
반면 symptom_present
의 0
은 증상 없음
이라는 실제값이며, diagnosis
의 문자열 None
도 실제 진단 없음으로 해석한다. 이처럼 같은 0
이라도 변수에 따라 의미가 다르므로, 일괄 처리하면 분석을 왜곡할 수 있다.
실습 시나리오
- 실습은 클래스 DomainImputationPractice의 run 메서드 흐름을 따른다.
- 원본 데이터 확인: 결측 리포트(각 변수의 결측 개수·율), 수치형 요약 통계, 범주형 분포를 출력한다.
- 접근 A(naive)
0
은 그대로 유효값으로 두고, NaN만 대체- 연속형은 중앙값, 이진·범주형은 최빈값으로 채움
- 접근 B(domain)
sbp
,dbp
,glucose
에서0
을 도메인 규칙으로 결측(NaN
)으로 재분류- 접근 A와 동일한 방식으로 대체한다.
- 재분류 건수를 출력해
0
→NaN
변환 규모를 확인 - 비교 요약 해보기
- 두 접근의 결과를 원본과 함께 평균·중앙값(연속형)과 분포(이진·범주형)로 비교
- 해석 가이드 문구로 차이를 정리
실습 포인트
- 같은
0
이라도 변수마다 의미가 다르며, 일괄 대체는 위험하다는 점 - 도메인 지식으로
0
을 결측으로 재분류하면 요약 통계가 달라지고, 임상적으로 더 타당한 분포가 나올 수 있다는 점 - 결측 처리의 성패가 후속 분석과 의사결정의 신뢰도에 직결된다는 점
코드 구현 요약
- 결측 현황 파악
missing_report
로 결측 개수·비율을 확인numeric_summary·categorical_counts
로 현재 분포를 진단
- 접근 A(단순 방식)
normalize_missing
으로None
→NaN
통일SimpleImputer
로NaN
만 대체0
은 그대로 유지
- 접근 B(도메인 방식)
zero_is_missing
목록(sbp, dbp, glucose)에 대해0
을NaN
으로 치환(domain_mark_missing)- 이후 같은 규칙으로 대체(domain_impute)
- 결과 비교와 해석
- 평균·중앙값이 어떻게 달라졌는지 표로 확인
symptom_present
/diagnosis
분포가 임상적 상식과 더 부합하는지 점검- 변동이 크다면
0
의 재분류가 분석 품질에 큰 영향을 미쳤다는 의미임을 파악하기
정리 이 실습은 결측치 처리를 단순 기술이 아닌 해석의 문제로 다룬다. 여러분은 접근 A와 B를 직접 비교하며, 도메인 규칙(0을 결측으로 재분류)이 혈압·혈당 통계와 진단 분포에 어떤 변화를 가져오는지 확인하게 된다.
최종적으로, 데이터의 의미를 이해하고 규칙을 명시하는 과정이 결측 처리의 핵심임을 확인할 수 있다.
# File: domain_imputation_practice_class.py
# 목적: 도메인 지식 활용 실습 (클래스 기반)
# 내용: 0과 미기록(NaN)을 구분해 결측을 처리하는 방법 비교
# 환경: Python 3.10+, pandas, numpy, scikit-learn
import numpy as np
import pandas as pd
from sklearn.impute import SimpleImputer
class DomainImputationPractice:
"""
도메인 지식 활용 결측치 처리 실습을 클래스 형태로 구성
- make_dataset: 샘플 환자 데이터 생성
- normalize_missing: None → np.nan 통일
- missing_report / numeric_summary / categorical_counts: 리포트·요약
- naive_impute: 접근 A (0을 값으로 간주, NaN만 대체)
- domain_mark_missing / domain_impute: 접근 B (0을 결측으로 재분류 후 대체)
- run: 전체 시나리오 실행 및 비교 출력
"""
def __init__(self):
# 분석 대상 컬럼 정의
# 연속형 생체지표: 0은 생리학적으로 불가능 → 도메인 판단 시 결측으로 재분류
self.numeric_cols: list[str] = ["sbp", "dbp", "glucose"]
# 이진 변수: 0은 '증상 없음'이라는 실제 값 → 유지
self.binary_cols: list[str] = ["symptom_present"]
# 범주형 변수
self.cat_cols: list[str] = ["diagnosis"]
# 0을 결측으로 재분류할 컬럼 목록(도메인 규칙)
self.zero_is_missing: list[str] = ["sbp", "dbp", "glucose"]
# =========================
# 데이터 생성 및 유틸
# =========================
def make_dataset(self) -> pd.DataFrame:
"""
환자 데이터 샘플 생성
- sbp, dbp, glucose는 연속형 생체지표 (0과 NaN 섞어 둠)
- symptom_present는 0/1 이진 변수 (0은 의미 있는 실제 값)
- diagnosis는 범주형 (None=미기록, "None" 문자열은 실제 '진단 없음')
"""
df = pd.DataFrame(
{
"patient_id": [101, 102, 103, 104, 105, 106, 107, 108],
"sbp": [120, 0, 138, 145, np.nan, 132, 0, 118],
"dbp": [80, 0, 92, 95, 88, np.nan, 0, 76],
"glucose": [98, 0, np.nan, 115, 102, 0, 140, np.nan],
"symptom_present": [0, 1, 0, 1, 0, 0, 1, np.nan],
"diagnosis": ["Hypertension", None, "Diabetes", "Hypertension",
"None", None, "Diabetes", "None"],
}
)
return df
@staticmethod
def normalize_missing(df: pd.DataFrame) -> pd.DataFrame:
"""
None → np.nan 통일
- pandas/Scikit-Learn에서 결측을 일관되게 다루기 위함
"""
return df.replace({None: np.nan})
@staticmethod
def missing_report(df: pd.DataFrame) -> pd.DataFrame:
"""
각 컬럼별 결측 개수와 결측률 출력용 표 생성
"""
return pd.DataFrame(
{
"missing_count": df.isna().sum(),
"missing_rate": (df.isna().mean() * 100).round(2),
}
).sort_values("missing_rate", ascending=False)
@staticmethod
def numeric_summary(df: pd.DataFrame, cols: list[str]) -> pd.DataFrame:
"""
수치형 요약 통계 반환
"""
if not cols:
return pd.DataFrame()
return df[cols].describe().T
@staticmethod
def categorical_counts(df: pd.DataFrame, cols: list[str]) -> dict[str, pd.Series]:
"""
범주형 분포 반환
- dropna=False로 결측까지 카운트
"""
out: dict[str, pd.Series] = {}
for c in cols:
out[c] = df[c].value_counts(dropna=False)
return out
@staticmethod
def print_cat_counts(title: str, counts: dict[str, pd.Series]) -> None:
print(title)
if not counts:
print("(범주형 컬럼 없음)\n")
return
for k, s in counts.items():
print(f"[{k}]")
print(f"{s}\n")
# =========================
# 접근 A: 단순 방식
# =========================
def naive_impute(self, df: pd.DataFrame) -> pd.DataFrame:
"""
단순 방식
- 0을 그대로 '유효한 값'으로 간주
- NaN만 대체
- 연속형(sbp, dbp, glucose): 중앙값
- 이진/범주형(symptom_present, diagnosis): 최빈값
"""
work = self.normalize_missing(df.copy())
if self.numeric_cols:
work[self.numeric_cols] = SimpleImputer(strategy="median").fit_transform(work[self.numeric_cols])
if self.binary_cols:
work[self.binary_cols] = SimpleImputer(strategy="most_frequent").fit_transform(work[self.binary_cols])
if self.cat_cols:
work[self.cat_cols] = SimpleImputer(strategy="most_frequent").fit_transform(work[self.cat_cols])
return work
# =========================
# 접근 B: 도메인 방식
# =========================
def domain_mark_missing(self, df: pd.DataFrame) -> pd.DataFrame:
"""
도메인 규칙으로 '0을 미기록으로 간주해야 하는 컬럼'을 NaN으로 치환
- sbp, dbp, glucose: 0은 생리학적으로 불가능 → 결측으로 재분류
- symptom_present: 0은 '증상 없음'이라는 실제 값 → 유지
- diagnosis: "None" 문자열은 실제 값 → 유지, NaN은 미기록
"""
work = self.normalize_missing(df.copy())
for col in self.zero_is_missing:
if col in work.columns:
work.loc[work[col] == 0, col] = np.nan
return work
def domain_impute(self, df: pd.DataFrame) -> pd.DataFrame:
"""
도메인 방식
- 0을 결측으로 재분류한 뒤 impute
- 연속형: 중앙값
- 이진/범주형: 최빈값
"""
work = self.domain_mark_missing(df)
if self.numeric_cols:
work[self.numeric_cols] = SimpleImputer(strategy="median").fit_transform(work[self.numeric_cols])
if self.binary_cols:
work[self.binary_cols] = SimpleImputer(strategy="most_frequent").fit_transform(work[self.binary_cols])
if self.cat_cols:
work[self.cat_cols] = SimpleImputer(strategy="most_frequent").fit_transform(work[self.cat_cols])
return work
# =========================
# 실행 흐름
# =========================
def run(self) -> None:
# 데이터 로드
df = self.make_dataset()
print("========== 원본 데이터 ==========\n")
print(f"{df}\n")
print("결측 리포트(원본)\n")
print(f"{self.missing_report(df)}\n")
print("수치형 요약 통계(원본)\n")
print(f"{self.numeric_summary(df, self.numeric_cols)}\n")
self.print_cat_counts("범주형 분포(원본)\n", self.categorical_counts(df, self.binary_cols + self.cat_cols))
# 접근 A: 단순 방식
print("---------- 접근 A: 단순 방식 (0을 값으로 간주, NaN만 대체) ---------\n")
naive = self.naive_impute(df)
print(f"{naive}\n")
print("결측 리포트(접근 A)\n")
print(f"{self.missing_report(naive)}\n")
print("수치형 요약 통계(접근 A)\n")
print(f"{self.numeric_summary(naive, self.numeric_cols)}\n")
self.print_cat_counts("범주형 분포(접근 A)\n", self.categorical_counts(naive, self.binary_cols + self.cat_cols))
# 접근 B: 도메인 방식
print("---------- 접근 B: 도메인 방식 (0을 결측으로 재분류 후 대체) ---------\n")
domain_pre = self.domain_mark_missing(df)
# 0 → NaN 재분류 건수 확인
zero_to_nan_counts = {
c: int(((df[c] == 0) if c in df.columns else pd.Series([], dtype=bool)).sum())
for c in self.zero_is_missing
}
print(f"0을 결측으로 재분류한 건수: {zero_to_nan_counts}\n")
print("0 재분류 적용 후 데이터 스냅샷(미대체)\n")
print(f"{domain_pre}\n")
print("결측 리포트(도메인 재분류 후, 미대체)\n")
print(f"{self.missing_report(domain_pre)}\n")
domain = self.domain_impute(df)
print("도메인 방식 대체 완료 데이터\n")
print(f"{domain}\n")
print("결측 리포트(접근 B)\n")
print(f"{self.missing_report(domain)}\n")
print("수치형 요약 통계(접근 B)\n")
print(f"{self.numeric_summary(domain, self.numeric_cols)}\n")
self.print_cat_counts("범주형 분포(접근 B)\n", self.categorical_counts(domain, self.binary_cols + self.cat_cols))
# 간단 비교 요약
print("========== 간단 비교 요약 ==========\n")
mean_comp = pd.DataFrame(
{
"원본": df[self.numeric_cols].mean(numeric_only=True),
"접근A(단순)": naive[self.numeric_cols].mean(numeric_only=True),
"접근B(도메인)": domain[self.numeric_cols].mean(numeric_only=True),
}
).round(2)
print("수치형 평균 비교\n")
print(f"{mean_comp}\n")
median_comp = pd.DataFrame(
{
"원본": df[self.numeric_cols].median(numeric_only=True),
"접근A(단순)": naive[self.numeric_cols].median(numeric_only=True),
"접근B(도메인)": domain[self.numeric_cols].median(numeric_only=True),
}
).round(2)
print("수치형 중앙값 비교\n")
print(f"{median_comp}\n")
print("이진/범주형 분포 비교\n")
for col in self.binary_cols + self.cat_cols:
orig_counts = df[col].value_counts(dropna=False)
a_counts = naive[col].value_counts(dropna=False)
b_counts = domain[col].value_counts(dropna=False)
print(f"[{col}]")
print(f" 원본 분포:\n{orig_counts}\n")
print(f" 접근A 분포:\n{a_counts}\n")
print(f" 접근B 분포:\n{b_counts}\n")
def main():
app = DomainImputationPractice()
app.run()
if __name__ == "__main__":
main()
실행 결과
$ python test.py
========== 원본 데이터 ==========
patient_id sbp dbp glucose symptom_present diagnosis
0 101 120.0 80.0 98.0 0.0 Hypertension
1 102 0.0 0.0 0.0 1.0 None
2 103 138.0 92.0 NaN 0.0 Diabetes
3 104 145.0 95.0 115.0 1.0 Hypertension
4 105 NaN 88.0 102.0 0.0 None
5 106 132.0 NaN 0.0 0.0 None
6 107 0.0 0.0 140.0 1.0 Diabetes
7 108 118.0 76.0 NaN NaN None
결측 리포트(원본)
missing_count missing_rate
diagnosis 2 25.0
glucose 2 25.0
dbp 1 12.5
sbp 1 12.5
symptom_present 1 12.5
patient_id 0 0.0
수치형 요약 통계(원본)
count mean std min 25% 50% 75% max
sbp 7.0 93.285714 64.422342 0.0 59.0 120.0 135.00 145.0
dbp 7.0 61.571429 42.567034 0.0 38.0 80.0 90.00 95.0
glucose 6.0 75.833333 60.545575 0.0 24.5 100.0 111.75 140.0
범주형 분포(원본)
[symptom_present]
symptom_present
0.0 4
1.0 3
NaN 1
Name: count, dtype: int64
[diagnosis]
diagnosis
Hypertension 2
None 2
Diabetes 2
None 2
Name: count, dtype: int64
---------- 접근 A: 단순 방식 (0을 값으로 간주, NaN만 대체) ---------
patient_id sbp dbp glucose symptom_present diagnosis
0 101 120.0 80.0 98.0 0.0 Hypertension
1 102 0.0 0.0 0.0 1.0 Diabetes
2 103 138.0 92.0 100.0 0.0 Diabetes
3 104 145.0 95.0 115.0 1.0 Hypertension
4 105 120.0 88.0 102.0 0.0 None
5 106 132.0 80.0 0.0 0.0 Diabetes
6 107 0.0 0.0 140.0 1.0 Diabetes
7 108 118.0 76.0 100.0 0.0 None
결측 리포트(접근 A)
missing_count missing_rate
patient_id 0 0.0
sbp 0 0.0
dbp 0 0.0
glucose 0 0.0
symptom_present 0 0.0
diagnosis 0 0.0
수치형 요약 통계(접근 A)
count mean std min 25% 50% 75% max
sbp 8.0 96.625 60.386700 0.0 88.5 120.0 133.50 145.0
dbp 8.0 63.875 39.944381 0.0 57.0 80.0 89.00 95.0
glucose 8.0 81.875 52.378942 0.0 73.5 100.0 105.25 140.0
범주형 분포(접근 A)
[symptom_present]
symptom_present
0.0 5
1.0 3
Name: count, dtype: int64
[diagnosis]
diagnosis
Diabetes 4
Hypertension 2
None 2
Name: count, dtype: int64
---------- 접근 B: 도메인 방식 (0을 결측으로 재분류 후 대체) ---------
0을 결측으로 재분류한 건수: {'sbp': 2, 'dbp': 2, 'glucose': 2}
0 재분류 적용 후 데이터 스냅샷(미대체)
patient_id sbp dbp glucose symptom_present diagnosis
0 101 120.0 80.0 98.0 0.0 Hypertension
1 102 NaN NaN NaN 1.0 NaN
2 103 138.0 92.0 NaN 0.0 Diabetes
3 104 145.0 95.0 115.0 1.0 Hypertension
4 105 NaN 88.0 102.0 0.0 None
5 106 132.0 NaN NaN 0.0 NaN
6 107 NaN NaN 140.0 1.0 Diabetes
7 108 118.0 76.0 NaN NaN None
결측 리포트(도메인 재분류 후, 미대체)
missing_count missing_rate
glucose 4 50.0
sbp 3 37.5
dbp 3 37.5
diagnosis 2 25.0
symptom_present 1 12.5
patient_id 0 0.0
도메인 방식 대체 완료 데이터
patient_id sbp dbp glucose symptom_present diagnosis
0 101 120.0 80.0 98.0 0.0 Hypertension
1 102 132.0 88.0 108.5 1.0 Diabetes
2 103 138.0 92.0 108.5 0.0 Diabetes
3 104 145.0 95.0 115.0 1.0 Hypertension
4 105 132.0 88.0 102.0 0.0 None
5 106 132.0 88.0 108.5 0.0 Diabetes
6 107 132.0 88.0 140.0 1.0 Diabetes
7 108 118.0 76.0 108.5 0.0 None
결측 리포트(접근 B)
missing_count missing_rate
patient_id 0 0.0
sbp 0 0.0
dbp 0 0.0
glucose 0 0.0
symptom_present 0 0.0
diagnosis 0 0.0
수치형 요약 통계(접근 B)
count mean std min 25% 50% 75% max
sbp 8.0 131.125 8.773947 118.0 129.000 132.0 133.500 145.0
dbp 8.0 86.875 6.128097 76.0 86.000 88.0 89.000 95.0
glucose 8.0 111.125 12.715991 98.0 106.875 108.5 110.125 140.0
범주형 분포(접근 B)
[symptom_present]
symptom_present
0.0 5
1.0 3
Name: count, dtype: int64
[diagnosis]
diagnosis
Diabetes 4
Hypertension 2
None 2
Name: count, dtype: int64
========== 간단 비교 요약 ==========
수치형 평균 비교
원본 접근A(단순) 접근B(도메인)
sbp 93.29 96.62 131.12
dbp 61.57 63.88 86.88
glucose 75.83 81.88 111.12
수치형 중앙값 비교
원본 접근A(단순) 접근B(도메인)
sbp 120.0 120.0 132.0
dbp 80.0 80.0 88.0
glucose 100.0 100.0 108.5
이진/범주형 분포 비교
[symptom_present]
원본 분포:
symptom_present
0.0 4
1.0 3
NaN 1
Name: count, dtype: int64
접근A 분포:
symptom_present
0.0 5
1.0 3
Name: count, dtype: int64
접근B 분포:
symptom_present
0.0 5
1.0 3
Name: count, dtype: int64
[diagnosis]
원본 분포:
diagnosis
Hypertension 2
None 2
Diabetes 2
None 2
Name: count, dtype: int64
접근A 분포:
diagnosis
Diabetes 4
Hypertension 2
None 2
Name: count, dtype: int64
접근B 분포:
diagnosis
Diabetes 4
Hypertension 2
None 2
Name: count, dtype: int64
실험 결과 해석
실험 결과를 살펴보면, 접근 A(단순 방식)과 접근 B(도메인 방식)가 서로 다른 양상을 보이는 것을 확인할 수 있다. 접근 A는 0을 실제 값으로 간주하고 NaN만 대체하였기 때문에, 수축기·이완기 혈압(sbp, dbp)과 혈당(glucose)에서 0이 그대로 반영되었다. 이로 인해 평균과 표준편차가 낮아지고 분산이 크게 증가하는 결과가 나타났다. 특히, sbp 평균이 약 96.6, glucose 평균이 약 81.9로 계산되어, 실제 임상에서 관찰되는 정상 범위보다 지나치게 낮게 나타나는 왜곡이 발생하였다. 이는 0이 결측이 아니라 실제 값이라고 가정했을 때 생기는 통계적 왜곡의 전형적인 예라 할 수 있다.
반면 접근 B는 sbp, dbp, glucose에서 0을 생리학적으로 불가능한 값으로 판단하여 결측으로 재분류한 뒤 중앙값으로 대체하였다. 그 결과 평균과 중앙값이 모두 현실적으로 더 타당한 수준으로 상승하였다. 예를 들어, sbp 평균은 131.1, glucose 평균은 111.1로 계산되어, 임상적으로도 신뢰할 수 있는 범위에 근접한 결과를 보였다. 또한 표준편차 역시 접근 A에 비해 크게 줄어들어, 데이터 분포가 보다 안정적으로 나타났다.
범주형 변수(symptom_present, diagnosis)의 경우 두 접근법 모두 동일한 방식(최빈값 대체)을 사용했기 때문에 큰 차이가 나타나지 않았다. 이는 도메인 지식의 적용 여부가 범주형 변수에는 직접적인 영향을 주지 않았음을 보여준다. 따라서 이번 실험의 차이는 주로 연속형 변수에서 도메인 규칙을 적용했는지 여부에 따라 발생한 것이다.
결론적으로, 접근 A는 단순하면서도 빠르게 결측을 채울 수 있지만, 도메인 특성을 고려하지 못해 실제 데이터 해석에 왜곡을 초래할 수 있다. 반면 접근 B는 도메인 지식을 적용하여 데이터의 의미를 올바르게 반영하고, 보다 타당하고 안정적인 통계 요약 결과를 제공하였다. 이번 실험을 통해 여러분은 결측치 처리 과정에서 단순한 기술적 접근보다 도메인 지식을 반영한 해석과 판단이 왜 중요한지를 확인할 수 있다.
학습 포인트 - 연속형 변수의 0값은 단순히 실제 값으로 취급하면 통계적 왜곡을 일으킬 수 있다. - 도메인 지식을 활용해 0을 결측으로 재분류하면, 더 타당하고 안정적인 분석 결과를 얻을 수 있다. - 결측치 처리에서 가장 중요한 것은 단순한 기술적 채움이 아니라, 데이터의 의미와 맥락을 올바르게 이해하는 것이다.
이상치 탐지
학습목표
- 이상치(outlier)의 정의와 필요성 이해
- 이상치가 데이터 분석에 미치는 영향 파악
- 이상치 탐지 (Outlier Detextion) 방법(통계적 방법, 거리 기반 방법, 머신러닝 방법) 이해 및 실습
이상치란 무엇인가?
데이터 분석에서 가장 중요한 출발점 중 하나는 데이터를 올바르게 이해하는 것이다. 데이터를 자세히 들여다보면, 대부분의 값들이 일정한 범위 안에 고르게 분포하는 것을 확인할 수 있다. 그러나 때로는 다른 값들과 비교했을 때 지나치게 크거나 작아 보이는 관측치가 나타난다. 이러한 관측치를 우리는 이상치(Outlier)라고 부른다.
이상치는 단순히 평균에서 벗어난 값이 아니라, 데이터의 일반적인 패턴을 크게 벗어난 값이다. 예를 들어, 사람들의 키를 조사했을 때 대부분이 150~190cm 범위에 분포하지만, 어떤 데이터에 300cm라는 값이 들어 있다면 이는 명백히 이상치다. 또 다른 예로, 하루 소비 금액을 기록했는데 대부분이 1만 원에서 50만 원 사이인데 특정 날만 1억 원이 기록되었다면 이것도 이상치라고 할 수 있다. 이렇게 이상치는 데이터의 일반적인 분포에서 벗어나기 때문에 분석자의 주의를 끌고, 올바른 처리 없이는 분석 결과에 왜곡을 일으킬 수 있다.
이상치는 여러 가지 원인으로 발생한다. 첫째, 데이터 수집 과정에서 발생하는 단순한 오류이다. 센서의 오작동으로 잘못된 수치가 기록되거나, 사람이 데이터를 입력하면서 실수를 하는 경우가 대표적이다. 둘째, 서로 다른 시스템에서 데이터를 통합하는 과정에서 단위 변환을 잘못 처리할 때도 이상치가 발생할 수 있다. 예를 들어, 어떤 시스템은 금액을 원 단위로 기록하는데 다른 시스템은 천 원 단위로 기록하는 경우, 단위 변환이 잘못되면 값이 비정상적으로 커지거나 작아진다. 셋째, 드물게는 실제로 발생한 극단적인 사건이 이상치로 나타나기도 한다. 예컨대, 대부분의 신용카드 사용 금액은 정상 범위에 있지만, 특정 거래에서 갑자기 수억 원이 결제되었다면 이는 단순 오류가 아니라 실제 발생한 ‘이상한 사건’일 수 있다. 따라서 이상치는 반드시 잘못된 값이라고 단정할 수는 없으며, 상황에 따라 중요한 분석 대상이 되기도 한다.
이상치를 무조건 제거하거나 무시하는 것은 위험하다. 왜냐하면 이상치가 분석 대상에 따라 서로 다른 의미를 가지기 때문이다. 만약 키나 체중 같은 신체 데이터에서 측정 오류로 인한 이상치가 포함된다면, 이는 분명히 제거해야 한다. 반대로 금융 데이터에서는 이상치가 곧 사기 거래나 비정상적인 행위일 가능성이 있기 때문에 오히려 중요한 분석 신호가 된다. 제조업에서 센서 데이터의 이상치는 기계 고장의 조짐일 수도 있다. 이처럼 이상치는 때로는 분석의 ‘잡음’이 되고, 때로는 분석의 ‘핵심 정보’가 된다. 따라서 이상치를 올바르게 탐지하고 해석하는 과정은 데이터 과학의 필수 단계라 할 수 있다.
또한 이상치는 통계적 지표에도 큰 영향을 준다. 평균이나 표준편차 같은 값들은 데이터 전체의 분포를 요약하는데, 이상치가 포함되면 그 값들이 크게 흔들린다. 예를 들어, 한 반의 학생들 시험 점수가 대부분 70~90점인데, 한 학생이 0점을 기록했다면 평균은 크게 낮아지고 표준편차는 불필요하게 커진다. 이 경우 평균 점수만 보면 반 전체의 성취도가 실제보다 낮다고 해석될 수 있다. 반대로 극단적으로 높은 값이 포함되면 전체가 지나치게 잘한 것처럼 보일 수도 있다. 즉, 이상치는 통계적 요약값을 왜곡시켜 잘못된 해석으로 이어질 수 있다.
이상치는 단순히 통계적 계산 문제를 넘어서, 데이터 분석 모델에도 직접적인 영향을 미친다. 예를 들어, 선형 회귀 분석에서는 이상치가 회귀선을 크게 왜곡시켜 모델의 예측력이 떨어질 수 있다. 분류 문제에서도 이상치는 결정 경계를 잘못 학습하게 만들어 성능 저하를 초래한다. 특히 머신러닝 모델은 데이터의 패턴을 학습하는 과정에서 극단적인 관측치에 민감하게 반응하는 경우가 많다. 따라서 이상치를 처리하지 않고 그대로 두면 모델의 안정성과 일반화 능력이 떨어지는 결과를 가져올 수 있다.
이상치 탐지의 중요성은 바로 여기에 있다. 데이터 분석과 머신러닝 모델링에서 이상치를 정확히 탐지하고, 그 의미를 올바르게 해석하며, 적절히 처리하는 것은 결과의 신뢰도를 크게 좌우한다. 이상치가 단순한 잡음인지, 혹은 중요한 신호인지 파악하는 것은 데이터 과학자가 반드시 수행해야 하는 중요한 단계이다. 앞으로 이어질 내용에서는 이상치가 구체적으로 어떤 영향을 미치는지, 그리고 이를 탐지하고 처리하는 다양한 방법에 대해 살펴보게 될 것이다.
이상치를 무시했을 때 발생하는 문제점
이상치를 탐지하지 않고 그대로 둔다면 데이터 분석 과정 전반에 걸쳐 다양한 문제가 발생한다. 그 문제는 크게 세 가지로 정리할 수 있다.
첫째, 통계적 왜곡이다. 평균, 표준편차, 분산과 같은 기본 통계량은 데이터의 전반적인 분포를 요약하는 데 사용된다. 그런데 데이터에 극단적으로 크거나 작은 값이 포함되면, 이 지표들이 본래 데이터의 특징을 제대로 반영하지 못한다. 예를 들어, 한 반의 시험 점수가 대부분 70~90점 사이인데 한 학생이 0점을 기록했다고 가정해 보자. 이 경우 반 평균은 실제보다 크게 낮아지고, 표준편차는 불필요하게 커진다. 이는 반 전체가 성적이 낮거나 편차가 큰 집단으로 잘못 해석될 수 있는 위험을 낳는다. 따라서 이상치를 무시하면 분석의 출발점인 기본 통계량부터 왜곡되며, 이후 모든 분석 과정에 부정적인 영향을 미친다.
둘째, 모델 성능 저하이다. 머신러닝이나 통계 모델은 데이터를 기반으로 규칙성을 학습한다. 그런데 이상치가 포함되어 있으면 모델은 잘못된 패턴을 학습하거나, 극단적인 값에 지나치게 끌려가게 된다. 예를 들어, 회귀 분석에서는 단 하나의 이상치가 전체 회귀선을 크게 왜곡할 수 있다. 분류 문제에서는 이상치가 데이터 경계 근처에 놓이면서 결정 경계를 비정상적으로 구부리게 만들 수 있다. 이런 경우 모델은 훈련 데이터에 과적합(overfitting)되고, 새로운 데이터를 잘 예측하지 못하게 된다. 결국 이상치를 무시하면 모델의 정확도와 일반화 성능이 모두 떨어진다.
셋째, 학습 불안정성이다. 이상치는 단순히 모델의 성능을 떨어뜨리는 것을 넘어, 학습 과정 자체를 불안정하게 만들 수 있다. 특히 신경망 기반의 딥러닝 모델은 이상치에 매우 민감하게 반응한다. 극단적인 값이 손실 함수(loss function)를 비정상적으로 크게 만들어, 역전파 과정에서 기울기 폭발(gradient explosion) 같은 현상을 유발할 수 있다. 이는 학습이 정상적으로 진행되지 못하고, 최적화 과정이 불안정하게 수렴하거나 아예 발산하는 문제로 이어질 수 있다. 그 결과 훈련 시간이 늘어나고, 모델이 안정적으로 수렴하지 않아 분석의 신뢰도를 보장하기 어려워진다.
이상치를 무시하는 것은 단순한 데이터 오류를 넘어 분석의 전 과정에 악영향을 끼친다. 통계적 왜곡으로 인해 데이터에 대한 잘못된 이해가 발생하고, 모델 성능 저하로 인해 예측력이 떨어지며, 학습 불안정성으로 인해 분석 자체가 불안정해진다. 따라서 이상치는 반드시 탐지하고 적절히 처리해야 하며, 이것이 데이터 전처리 과정에서 중요한 이유이다.
이상치 탐지 방법
구분 | 주요 기법 | 설명 | 장점 | 단점 |
---|---|---|---|---|
통계적 방법 | Z-Score, IQR(사분위 범위) | 데이터 분포(평균, 표준편차, 사분위수)를 기준으로 임계값 이상 벗어난 관측치를 이상치로 간주 | 계산이 간단하고 직관적임 | 분포가 정규분포가 아닐 경우 정확도 저하 |
거리 기반 방법 | k-NN Distance, DBSCAN | 데이터 간의 거리(distance)를 계산해 밀도가 낮거나 주변과 동떨어진 점을 이상치로 판단 | 다양한 데이터 분포에 적용 가능 | 고차원 데이터에서는 거리 계산이 비효율적 |
머신러닝 기반 방법 | Isolation Forest, One-Class SVM | 학습 알고리즘을 이용해 정상 데이터 패턴을 학습하고, 그와 크게 다른 점을 이상치로 탐지 | 복잡한 패턴의 이상치 탐지가 가능 | 계산 비용이 크고 모델 선택·튜닝이 필요함 |
통계적 방법
이상치를 탐지하는 가장 기본적인 방법은 통계적 기준을 활용하는 것이다. 데이터의 분포를 요약하는 통계량(평균, 표준편차, 사분위수 등)을 이용해 정상 범위를 정의하고, 그 범위를 벗어나는 값을 이상치로 간주하는 방식이다. 이러한 방법은 계산이 간단하고 직관적이어서, 데이터 분석의 초기에 빠르게 이상치를 확인할 때 자주 사용된다.
첫 번째로 많이 쓰이는 기법은 Z-점수(Z-score)를 이용하는 방법이다. Z-점수는 각 데이터 값이 평균으로부터 몇 배의 표준편차만큼 떨어져 있는지를 나타내는 지표이다. 일반적으로 Z-점수가 ±3을 넘는 값은 이상치로 판단한다. 예를 들어, 시험 점수의 평균이 70점이고 표준편차가 10점일 때, 100점 이상의 값은 평균보다 3표준편차 이상 큰 값이므로 이상치로 볼 수 있다.
두 번째 방법은 사분위 범위(IQR, Interquartile Range)를 활용하는 것이다. IQR은 데이터 분포의 1사분위(Q1)와 3사분위(Q3) 사이의 범위를 말한다. 이때 IQR을 벗어난 값, 특히 Q1 − 1.5×IQR보다 작은 값이나 Q3 + 1.5×IQR보다 큰 값은 이상치로 판정한다. 예를 들어, 어떤 데이터의 Q1이 20, Q3가 40이라면 IQR은 20이 된다. 이 경우 하한은 20 − 30 = −10, 상한은 40 + 30 = 70이 되며, −10보다 작거나 70보다 큰 값은 이상치로 간주된다.
이 두 가지 기법은 데이터가 단일 변수(1차원)일 때 특히 유용하다. 단순히 평균과 분산, 또는 사분위수를 계산하는 것만으로도 이상치를 확인할 수 있기 때문이다. 또한 데이터의 크기가 작더라도 쉽게 적용할 수 있으며, 계산 비용이 낮아 탐색적 데이터 분석 단계에서 널리 활용된다.
하지만 통계적 방법은 한계도 존재한다. 데이터가 정규분포에 가깝다는 가정을 바탕으로 하기 때문에, 분포가 비대칭적이거나 꼬리가 두터운 경우에는 이상치를 제대로 탐지하지 못한다. 또한 다차원 데이터(예: 여러 개의 특성을 동시에 고려해야 하는 경우)에는 적용이 어렵다. 따라서 통계적 방법은 가장 기본적이고 직관적인 도구이지만, 복잡한 데이터셋을 다룰 때는 다른 방법과 함께 사용하는 것이 바람직하다.
거리 기반 방법
이상치를 탐지하는 또 다른 대표적인 접근은 거리 기반 방법(distance-based methods) 이다. 이 방식은 각 데이터가 다른 데이터와 얼마나 가까운지 혹은 떨어져 있는지를 측정하여, 주변과 지나치게 멀리 떨어진 점을 이상치로 간주한다.
가장 직관적인 기법은 k-최근접 이웃(k-Nearest Neighbors, k-NN) 거리 기반 탐지이다. 특정 데이터 포인트를 기준으로 가장 가까운 k개의 이웃까지의 평균 거리나 최대 거리를 계산하고, 이 값이 임계값(threshold)을 초과하면 이상치로 판단한다. 예를 들어, 대부분의 데이터가 밀집해 있는 영역에 속하지 않고 멀리 떨어져 있다면 해당 점은 정상적인 데이터 분포에 속하지 않는다고 볼 수 있다.
또 다른 방법은 DBSCAN(Density-Based Spatial Clustering of Applications with Noise) 같은 밀도 기반 클러스터링 알고리즘이다. DBSCAN은 데이터가 충분히 밀집된 영역을 클러스터로 정의하고, 밀집 영역에 속하지 못하는 점들을 “잡음(noise)” 또는 이상치로 분류한다. 이 방법은 데이터의 분포 형태를 미리 가정하지 않고도 자연스럽게 군집과 이상치를 동시에 찾을 수 있다는 장점이 있다.
거리 기반 방법의 가장 큰 강점은 단순히 통계적 분포를 전제하지 않고, 데이터 간 상대적 거리에 기반해 이상치를 판단할 수 있다는 점이다. 따라서 데이터가 정규분포처럼 이상적인 형태를 따르지 않더라도 적용할 수 있으며, 다차원 데이터에도 활용 가능하다. 실제로 이미지, 텍스트 임베딩과 같은 고차원 벡터 데이터에서 이상치를 찾는 데도 응용된다.
그러나 단점도 존재한다. 데이터 차원이 높아질수록 거리가 비슷해지는 차원의 저주(curse of dimensionality) 문제가 발생하여, 정상치와 이상치를 구분하기 어려워진다. 또한 데이터의 개수가 많아질수록 거리 계산에 드는 비용이 급격히 증가하기 때문에, 대규모 데이터셋에서는 효율성이 떨어질 수 있다. 따라서 거리 기반 방법은 데이터 크기와 차원을 고려해 적절히 선택해야 하며, 필요할 경우 차원 축소(PCA 등)와 결합하여 사용하는 것이 효과적이다.
머신러닝 기반 방법
이상치 탐지에서 가장 최근에 주목받고 있는 방식은 머신러닝 기반 방법이다. 이 접근은 정상 데이터의 패턴을 학습하고, 해당 패턴으로 설명되지 않는 데이터를 이상치로 분류하는 원리로 작동한다. 단순히 평균이나 거리 기준을 적용하는 대신, 데이터의 복잡한 구조와 비선형 관계까지 고려할 수 있다는 점에서 강력한 장점을 가진다.
대표적인 기법 중 하나는 Isolation Forest이다. 이 방법은 ‘이상치는 정상 데이터보다 쉽게 고립될 수 있다’는 아이디어에 기반한다. 무작위로 데이터의 특성과 분할값을 선택해 트리를 구성할 때, 정상 데이터는 여러 번 분할해야 고립되지만 이상치는 몇 번의 분할만으로도 고립된다. 따라서 평균적으로 짧은 경로 길이를 가지는 데이터 포인트가 이상치로 판정된다. Isolation Forest는 계산 효율이 높고 대규모 데이터에도 적용 가능하다는 장점이 있다.
또 다른 중요한 방법은 One-Class SVM(Support Vector Machine) 이다. 이 기법은 정상 데이터가 주어진다고 가정하고, 가능한 한 많은 데이터를 둘러싸는 경계를 학습한다. 이후 이 경계 밖에 위치하는 데이터 포인트를 이상치로 분류한다. One-Class SVM은 비선형 데이터에도 적용 가능하며, 커널 함수를 활용해 복잡한 데이터 구조까지 모델링할 수 있다. 다만 데이터 크기가 크거나 파라미터 선택이 적절하지 않을 경우 성능이 크게 달라질 수 있다는 한계가 있다.
이 외에도 오토인코더(Autoencoder)를 활용한 이상치 탐지 방법이 널리 사용된다. 오토인코더는 입력 데이터를 압축한 후 다시 복원하는 신경망 구조인데, 정상 데이터는 잘 복원되지만 이상치는 복원 오차가 크게 나타난다. 따라서 복원 오차를 기준으로 이상치를 탐지할 수 있다. 이러한 방법은 이미지나 시계열 데이터 등 복잡한 형태의 데이터에서도 효과적으로 동작한다.
머신러닝 기반 방법의 강점은 데이터의 분포 형태를 사전에 가정하지 않고도 비정형적이고 복잡한 패턴을 학습할 수 있다는 것이다. 특히 비선형 관계나 다차원 구조를 가진 데이터에서 이상치를 탐지하는 데 매우 효과적이다. 그러나 계산 비용이 높고, 모델의 성능이 데이터 양과 품질에 크게 의존한다는 단점도 존재한다. 또한 파라미터 선택이나 하이퍼파라미터 튜닝이 까다로워, 초급자에게는 다소 진입 장벽이 될 수 있다.
머신러닝 기반 방법은 이상치 탐지의 최신 트렌드로서 복잡한 데이터 환경에서 강력한 도구를 제공하지만, 동시에 높은 계산 자원과 정교한 모델링이 요구된다는 점에서 전문적인 활용 능력이 필요하다.
이상치 처리 방법
이상치를 탐지한 뒤에는 그것을 어떻게 다룰 것인지 결정해야 한다. 이상치는 단순 오류일 수도 있고, 분석에 중요한 단서일 수도 있기 때문에 무조건 제거하거나 그대로 두는 것은 바람직하지 않다. 따라서 데이터의 특성과 분석 목적에 따라 적절한 처리 방법을 선택하는 것이 중요하다. 일반적으로 이상치 처리 방법은 제거(Removal), 대체(Imputation/Replacement), 변환(Transformation) 세 가지로 구분할 수 있다.
1) 제거(Removal)
가장 직관적인 방법은 이상치를 데이터셋에서 직접 제거하는 것이다. 예를 들어, 입력 오류나 센서 고장으로 인해 발생한 값은 분석에 도움이 되지 않으므로 삭제하는 것이 합리적이다. 이 방법은 간단하고 데이터 분포를 깔끔하게 정리할 수 있다는 장점이 있다. 그러나 지나치게 많은 데이터를 제거하면 표본 수가 줄어들어 분석의 신뢰성이 낮아질 수 있다. 특히 이상치가 드문 사건이나 중요한 신호일 가능성이 있는 경우, 단순 제거는 귀중한 정보를 잃게 되는 결과를 초래할 수 있다.
2) 대체(Imputation/Replacement)
이상치를 통째로 삭제하는 대신, 적절한 값으로 대체하는 방법도 있다. 보통 평균, 중앙값, 최빈값으로 교체하거나, 이상치가 위치한 구간의 인접 값으로 보정하는 방식이 사용된다. 예를 들어, 나이 데이터에서 200살로 기록된 값은 중앙값인 35세로 대체할 수 있다. 또 다른 방법으로는 회귀 모델이나 KNN과 같은 알고리즘을 활용하여, 주변 데이터의 패턴을 참고해 이상치를 추정 값으로 바꿀 수 있다. 대체 방법은 데이터 손실을 줄일 수 있다는 장점이 있지만, 인위적으로 값을 채우는 과정에서 원래의 변동성을 왜곡할 수 있다는 한계도 있다.
3) 변환(Transformation)
이상치 자체를 제거하거나 대체하지 않고, 데이터 전체를 변환하여 이상치의 영향을 줄이는 방법도 있다. 대표적으로 로그 변환, 제곱근 변환, 표준화와 같은 기법이 사용된다. 예를 들어, 소득 데이터는 극단적으로 큰 값이 포함되는 경우가 많다. 이때 로그 변환을 적용하면 분포가 보다 정규분포에 가깝게 변하며 이상치의 영향력이 완화된다. 변환은 데이터의 구조를 유지하면서 이상치의 왜곡 효과를 줄일 수 있다는 장점이 있다. 다만 모든 상황에서 효과적인 것은 아니며, 변수의 의미가 변형될 수 있다는 점을 주의해야 한다.
이상치 처리에는 제거, 대체, 변환이라는 세 가지 기본 접근법이 있으며, 각각 장단점이 뚜렷하다. 분석 목적과 데이터의 특성을 고려하여 어떤 방법을 적용할지 신중하게 선택하는 것이 무엇보다 중요하다. 예컨대 단순 오류는 제거하는 것이 옳지만, 중요한 신호일 수 있는 이상치는 오히려 보존하고 적절히 변환하거나 대체해야 한다.
이상치 변환 실습
앞서 결측치 처리 단원에서는 결측값을 제거하거나 대체하는 방법을 다루었다. 이상치 처리에서도 유사하게 제거와 대체가 가능하지만, 여기서는 같은 방식의 중복 학습을 피하고 변환(Transformation) 을 통한 이상치 완화 방법에 집중하여 실습을 진행한다.
실습의 목표는 데이터에 존재하는 극단적인 값이 분석 결과를 왜곡하지 않도록, 변환 기법을 적용해 이상치의 영향을 줄이는 것이다. 특히 로그 변환(log transformation), 제곱근 변환(square root transformation), 표준화(standardization)와 같은 기법을 활용하여 데이터 분포를 더 안정적으로 만드는 과정을 경험하게 된다.
예를 들어, 소득 데이터에는 극단적으로 높은 값이 포함되는 경우가 많다. 원본 데이터 그대로 평균을 계산하면 상위 몇 개의 큰 값이 전체 평균을 끌어올려 대표성을 잃게 된다. 그러나 로그 변환을 적용하면 큰 값들의 스케일이 압축되어, 전체 분포가 보다 정규분포에 가까워지고 이상치의 영향력이 줄어든다. 제곱근 변환 역시 유사한 효과를 가지며, 데이터의 분산을 완화하는 데 유용하다. 또한 표준화를 적용하면 데이터가 평균 0, 분산 1의 스케일로 변환되어, 이상치가 상대적으로 덜 두드러지게 된다.
이번 실습에서는 이상치가 포함된 가상의 소득 데이터를 준비한 뒤, 원본 데이터의 분포를 확인하고, 각각의 변환을 적용했을 때 분포가 어떻게 변화하는지를 비교한다. 이를 통해 이상치가 그대로 남아 있을 때와 변환을 거친 후의 차이를 직접 체험할 수 있다. 여러분은 변환 기법이 이상치를 반드시 제거하거나 수정하지 않더라도, 분석에 미치는 영향을 효과적으로 완화할 수 있음을 확인하게 될 것이다.
# File: outlier_transformation_practice_class.py
# 목적: 이상치 처리 - 변환(로그, 제곱근, 표준화)을 통한 영향 완화 실습
# 환경: Python 3.10+, pandas, numpy
import numpy as np
import pandas as pd
class OutlierTransformationPractice:
"""
이상치 처리를 위한 변환 실습 클래스
- 로그 변환
- 제곱근 변환
- 표준화
"""
def __init__(self, n: int = 500, n_outliers: int = 5, seed: int = 42):
"""
초기화 시 데이터 생성
:param n: 기본 표본 개수
:param n_outliers: 극단적 이상치 개수
:param seed: 난수 시드
"""
self.df = self._make_income_data(n, n_outliers, seed)
self.income = self.df["income"]
@staticmethod
def _make_income_data(n: int, n_outliers: int, seed: int) -> pd.DataFrame:
"""
소득 데이터 생성 함수
- base: 정상 소득 데이터 (로그정규 분포)
- outliers: 비정상적으로 큰 소득 값 (극단값)
- income: 정상 + 이상치를 합친 최종 데이터셋
"""
rng = np.random.default_rng(seed)
base = rng.lognormal(mean=10.5, sigma=0.6, size=n).astype(float)
outliers = rng.lognormal(mean=13.0, sigma=0.4, size=n_outliers)
income = np.concatenate([base, outliers])
# ------------------------------
# 정상값과 이상치 분포 비교 출력 (표 형태)
# ------------------------------
def summary(arr: np.ndarray) -> dict:
return {
"count": len(arr),
"mean": np.mean(arr),
"median": np.median(arr),
"min": np.min(arr),
"max": np.max(arr)
}
base_summary = summary(base)
outlier_summary = summary(outliers)
# DataFrame으로 보기 좋게 출력
summary_df = pd.DataFrame({
"정상값": base_summary,
"이상치": outlier_summary
})
print("========== 데이터 생성 요약 ==========\n")
# 숫자 포맷: 세자리마다 콤마, 소수점 둘째 자리까지
formatted_df = summary_df.map(lambda x: f"{x:,.2f}")
print(formatted_df.round(2).to_string())
print(f"\n정상값 최대치 대비 이상치 최소치 배율: {outliers.min() / base.max():.2f}배\n")
return pd.DataFrame({"income": income})
# ------------------------------
# 변환 메서드들
# ------------------------------
@staticmethod
def log_transform(s: pd.Series) -> pd.Series:
"""
로그 변환: log(x + c)
- 로그는 0 이하 값에서 정의되지 않으므로 양의 시프트 c를 더함
- c = 1 - min(s) + 1e-6 (min <= 0인 경우)
"""
min_val = s.min()
c = 0.0
if pd.notna(min_val) and min_val <= 0:
c = (1 - min_val) + 1e-6
return np.log(s + c)
@staticmethod
def sqrt_transform(s: pd.Series) -> pd.Series:
"""
제곱근 변환: sqrt(x + c)
- 0 이하 값 방지를 위해 필요 시 양의 시프트 c 적용
"""
min_val = s.min()
c = 0.0
if pd.notna(min_val) and min_val < 0:
c = (-min_val) + 1e-6
return np.sqrt(s + c)
@staticmethod
def standardize(s: pd.Series) -> pd.Series:
"""
표준화: (x - μ) / σ
- 평균을 0, 표준편차를 1로 변환
- 이상치의 절대적 영향력이 줄어듦
"""
mu = s.mean()
sd = s.std(ddof=1)
if sd == 0 or pd.isna(sd):
return s * 0
return (s - mu) / sd
# ------------------------------
# 요약 리포트
# ------------------------------
@staticmethod
def _metrics(s: pd.Series) -> dict:
"""데이터 분포 요약 지표 계산"""
q1 = s.quantile(0.25)
q3 = s.quantile(0.75)
return {
"mean": s.mean(),
"std": s.std(ddof=1),
"skew": s.skew(),
"median": s.median(),
"q1": q1,
"q3": q3,
"IQR": q3 - q1,
"max": s.max()
}
def summary_table(self) -> pd.DataFrame:
"""
원본 vs 변환별 분포 비교 테이블 생성
"""
income_log = self.log_transform(self.income)
income_sqrt = self.sqrt_transform(self.income)
income_std = self.standardize(self.income)
comp = pd.DataFrame({
"원본": self._metrics(self.income),
"로그 변환": self._metrics(income_log),
"제곱근 변환": self._metrics(income_sqrt),
"표준화": self._metrics(income_std)
})
return comp.round(4)
# ------------------------------
# 실행
# ------------------------------
def run(self) -> None:
"""실습 실행: 표 출력 및 해석 안내"""
print("========== 이상치 변환 비교 요약 ==========\n")
comp = self.summary_table()
print(comp.to_string())
print("\n해석 가이드")
print("- 로그/제곱근 변환은 큰 값(상위 아웃라이어)의 영향력을 줄여 왜도(skew)를 완화한다.")
print("- 표준화는 데이터의 스케일을 평균≈0, 표준편차≈1로 맞춰 이상치의 절대 크기 영향력을 줄인다.")
print("- IQR 비교를 통해 이상치가 분포에 미치는 영향이 얼마나 줄어들었는지를 정량적으로 확인할 수 있다.")
# ------------------------------
# 메인 실행부
# ------------------------------
def main():
practice = OutlierTransformationPractice()
practice.run()
if __name__ == "__main__":
main()
실행 결과
$ python test.py
========== 데이터 생성 요약 ==========
정상값 이상치
count 500.00 5.00
mean 42,635.02 421,159.66
median 36,385.62 331,773.54
min 7,785.40 135,156.48
max 208,629.97 763,405.70
정상값 최대치 대비 이상치 최소치 배율: 0.65배
========== 이상치 변환 비교 요약 ==========
원본 로그 변환 제곱근 변환 표준화
mean 46382.7857 10.5146 202.0982 0.0000
std 52085.4926 0.6190 74.4991 1.0000
skew 8.7337 0.5714 3.2452 8.7337
median 36588.7638 10.5075 191.2819 -0.1880
q1 24412.3501 10.1028 156.2445 -0.4218
q3 52654.2729 10.8715 229.4652 0.1204
IQR 28241.9227 0.7687 73.2207 0.5422
max 763405.7046 13.5455 873.7309 13.7663
실행 결과 해석
데이터 생성 요약 결과를 보면, 정상값의 평균 소득은 약 42,635원, 중앙값은 약 36,386원인 반면, 이상치의 평균은 약 421,160원, 중앙값은 약 331,774원으로 정상값보다 10배 이상 큰 수준임을 확인할 수 있다. 최소값을 비교해도 정상값의 최댓값(208,630원)보다 이상치의 최솟값(135,156원)이 0.65배 정도로 크거나 비슷한 수준임을 알 수 있다. 이러한 차이는 극단적으로 큰 소득값이 데이터에 포함되었음을 보여주며, 이상치가 전체 분포를 크게 왜곡할 수 있다는 점을 직관적으로 이해할 수 있다.
이상치 변환 비교 요약 결과를 살펴보면, 원본 데이터의 평균은 약 46,383원, 표준편차는 약 52,085원으로 상당히 큰 편차를 보인다. 왜도(skew) 값도 8.73으로 매우 높은데, 이는 데이터 분포가 오른쪽으로 긴 꼬리를 가진다는 것을 의미한다. 즉, 소수의 큰 값이 분포를 심각하게 왜곡하고 있음을 보여준다.
로그 변환을 적용하면 평균은 10.51, 표준편차는 0.62로 안정화되며, 왜도 역시 0.57로 크게 감소한다. 이는 로그 변환이 큰 값들의 스케일을 압축하여 분포가 정규분포에 가까워졌음을 의미한다. 제곱근 변환 역시 평균 202.10, 표준편차 74.50으로 원본보다 분산이 줄었고, 왜도도 3.25로 다소 완화되었다. 그러나 로그 변환만큼 효과적이지는 않다.
표준화의 경우 평균은 0, 표준편차는 1로 스케일이 정규화되었지만, 왜도는 여전히 8.73으로 원본과 동일하다. 이는 표준화가 데이터의 분포 모양을 바꾸지 않고 단순히 크기만 재조정했음을 의미한다. 따라서 이상치가 분포에 미치는 영향은 그대로 남아 있다.
로그 변환은 이상치의 영향을 가장 효과적으로 완화하여 데이터 분포를 안정화시켰다. 제곱근 변환은 부분적으로 효과가 있으나 로그 변환보다는 덜 강력하며, 표준화는 스케일을 맞추는 데는 유용하지만 이상치 자체를 줄여주지는 못한다. 따라서 데이터 분석에서 이상치가 분포를 심하게 왜곡하는 경우, 로그 변환과 같은 적절한 변환 기법을 적용하는 것이 매우 중요하다.
데이터 정규화
학습목표
- 데이터 정규화(Normalization)와 표준화(Standardization)의 차이를 이해한다.
- 스케일 차이가 머신러닝 모델 성능에 미치는 영향을 설명할 수 있다.
- 대표적인 정규화 기법(최소-최대 정규화, Z-점수 표준화, 로버스트 스케일링 등)을 학습한다.
- 데이터 특성에 따라 적절한 스케일링 방법을 선택할 수 있다.
정규화란 무엇인가?
데이터 분석과 머신러닝에서 중요한 전처리 과정 중 하나가 정규화(Normalization)이다. 정규화란 데이터의 크기나 범위를 일정한 기준에 맞추어 조정하는 과정을 의미한다. 일반적으로 데이터셋은 서로 다른 단위를 가진 다양한 변수들로 구성된다. 예를 들어, 환자의 키는 센티미터(cm), 몸무게는 킬로그램(kg), 혈압은 mmHg 단위로 측정된다. 이러한 변수들을 그대로 하나의 데이터셋에 넣으면, 값의 범위가 크게 달라져 모델 학습 과정에서 불균형이 발생한다. 정규화는 이러한 차이를 줄여, 모든 변수가 동등하게 모델에 기여할 수 있도록 돕는다.
정규화의 핵심 목적은 데이터의 “스케일(Scale)”을 맞추는 데 있다. 어떤 변수는 값의 범위가 0에서 1,000까지인데, 다른 변수는 0에서 10 사이에 분포한다면, 단순히 수치의 크기만 보아도 첫 번째 변수가 더 중요한 것처럼 모델이 학습할 수 있다. 하지만 실제로는 두 변수 모두 예측에 중요한 역할을 할 수 있으며, 단지 단위와 범위가 다를 뿐이다. 이때 정규화를 통해 두 변수를 동일한 기준으로 조정하면, 모델이 공정하게 두 변수를 비교하고 해석할 수 있다.
또한 정규화는 단순히 값의 범위를 맞추는 것 이상의 의미를 가진다. 많은 머신러닝 알고리즘들은 수학적 최적화 과정을 기반으로 학습한다. 특히 경사 하강법(Gradient Descent)을 사용하는 알고리즘은 데이터의 스케일에 따라 학습 속도가 크게 달라진다. 만약 어떤 변수의 값이 다른 변수보다 지나치게 크다면, 경사 하강법은 기울기를 계산할 때 큰 변수에 더 민감하게 반응하게 된다. 이로 인해 최적의 해에 도달하는 과정이 불안정해지거나, 학습 속도가 지나치게 느려질 수 있다. 따라서 정규화는 학습을 효율적이고 안정적으로 수행하기 위한 필수적인 과정이 된다.
정규화의 방식은 다양하다. 가장 단순한 방법은 모든 값을 0과 1 사이로 맞추는 최소-최대 정규화(Min-Max Scaling)이다. 이 방법은 직관적이고 계산이 간단해, 데이터의 상대적인 분포를 유지하면서 범위만 제한할 수 있다. 또 다른 대표적인 방법은 평균과 표준편차를 기준으로 데이터를 표준화하는 Z-점수 정규화(Z-score Standardization)이다. 이 방식은 데이터의 분포를 평균 0, 표준편차 1로 변환하여, 값이 평균으로부터 얼마나 떨어져 있는지를 상대적 단위로 표현한다. 만약 데이터에 이상치가 많다면, 중앙값과 사분위수를 활용하는 로버스트 스케일링(Robust Scaling)을 적용해 이상치의 영향을 줄일 수 있다.
정규화는 데이터의 해석에도 중요한 영향을 미친다. 예를 들어, 정규화되지 않은 데이터를 시각화할 경우 값의 범위가 큰 변수가 차트를 지배해, 다른 변수들의 변동이 잘 드러나지 않을 수 있다. 하지만 정규화를 거치면 변수들이 동일한 범위에서 비교 가능해지므로, 데이터의 패턴과 상관관계를 더욱 명확히 파악할 수 있다. 이는 탐색적 데이터 분석(EDA) 단계에서 특히 유용하다.
정규화란 데이터의 스케일을 일정한 기준으로 맞추는 작업이며, 그 목적은 (1) 서로 다른 단위와 범위를 가진 변수를 공정하게 비교하고, (2) 머신러닝 모델의 학습 효율성과 안정성을 높이며, (3) 데이터 해석과 시각화를 용이하게 하는 데 있다. 정규화는 결측치 처리나 이상치 탐지와 함께, 데이터 전처리의 필수 요소로 자리 잡고 있다. 따라서 여러분은 단순히 정규화를 수학적 변환으로만 이해하기보다는, 모델링 과정 전체에서 성능과 해석력을 향상시키는 중요한 도구로 받아들여야 한다.
정규화가 필요한 이유
정규화는 데이터 전처리 단계에서 단순한 선택이 아니라, 많은 경우 필수적으로 수행해야 하는 과정이다. 그 이유는 크게 세 가지로 나누어 설명할 수 있다: 변수 단위 차이로 인한 왜곡, 학습 속도와 안정성 문제, 그리고 이상치와 분포 왜곡에 대한 민감성이다.
첫째, 변수 단위 차이 때문이다. 실제 데이터셋은 서로 다른 단위를 가진 변수를 포함하는 경우가 많다. 예를 들어, 키는 센티미터 단위, 몸무게는 킬로그램 단위, 연령은 세 단위, 소득은 원화 단위로 기록될 수 있다. 이 값들을 그대로 하나의 모델에 입력하면, 수치 크기가 큰 소득 변수가 모델에서 더 큰 비중을 차지하는 것처럼 보이게 된다. 결과적으로 알고리즘은 본래보다 특정 변수에 과도하게 민감하게 반응할 수 있으며, 이는 예측의 정확성을 저하시킨다. 정규화를 통해 이러한 스케일 차이를 조정하면, 변수들이 동등한 조건에서 학습에 기여할 수 있다.
둘째, 학습 속도와 안정성을 위해 필요하다. 많은 머신러닝 알고리즘은 경사 하강법(Gradient Descent)을 사용하여 최적화 과정을 수행한다. 경사 하강법은 손실 함수를 최소화하는 방향으로 파라미터를 반복적으로 업데이트하는 방식인데, 입력 변수들의 크기가 서로 크게 다르면 기울기 계산이 불균형해진다. 이 경우 어떤 방향으로는 빠르게 수렴하지만, 다른 방향으로는 매우 느리게 움직여 “지그재그” 형태의 비효율적인 학습 경로를 그리게 된다. 결과적으로 최적점에 도달하는 속도가 늦어지고, 경우에 따라서는 수렴하지 못하는 문제도 발생할 수 있다. 정규화를 적용하면 각 변수의 스케일이 비슷해져, 경사 하강법이 더 안정적이고 효율적으로 작동한다.
셋째, 이상치와 분포 왜곡의 영향을 줄이기 위해서다. 예를 들어, 특정 변수에 극단적으로 큰 값이 포함되어 있으면, 모델은 그 값의 스케일에 지나치게 영향을 받을 수 있다. 이 경우 변수 전체의 범위가 불필요하게 커져, 다른 정상적인 데이터 포인트들이 상대적으로 중요성을 잃게 된다. 정규화를 통해 이러한 값들을 일정한 범위 안으로 조정하면, 이상치의 영향력이 완화되고 모델이 전체적인 데이터 분포를 더 잘 반영할 수 있다.
정규화는 단순히 “데이터를 보기 좋게 만드는 과정”이 아니라, (1) 변수 간 공정한 비교를 보장하고, (2) 학습 속도와 안정성을 높이며, (3) 이상치로 인한 왜곡을 줄이는 중요한 절차다. 따라서 정규화를 적절히 수행하지 않으면, 통계적 분석뿐 아니라 머신러닝 모델의 성능과 해석 가능성 모두에 부정적인 영향을 미칠 수 있다.
주요 정규화 기법
구분 | 기법 | 설명 | 장점 | 단점 |
---|---|---|---|---|
최소-최대 정규화 | Min-Max Scaling | 데이터 값을 0\~1 범위로 선형 변환 | 직관적이고 계산이 간단 값의 상대적 비율 유지 |
이상치에 민감하여 범위가 왜곡될 수 있음 |
Z-점수 표준화 | Z-score Standardization | 평균을 0, 표준편차를 1로 변환하여 표준 정규분포 기반으로 스케일 조정 | 분포가 다른 변수들을 비교 가능 경사 하강법 기반 모델에 적합 |
이상치에 민감 정규분포 가정이 잘 맞지 않으면 효과 제한 |
로버스트 스케일링 | Robust Scaling | 중앙값을 0, IQR(사분위 범위)을 1로 변환 | 이상치에 강건함 비대칭적 분포에도 안정적 |
데이터의 세밀한 변화를 과도하게 단순화할 수 있음 |
로그 변환 | Log Transformation | 로그 함수를 적용하여 큰 값을 압축 | 긴 꼬리 분포 완화 이상치 영향 완화 |
0 이하 값에는 적용 불가 해석이 직관적이지 않을 수 있음 |
제곱근 변환 | Square Root Transformation | 제곱근 함수를 적용하여 분산을 줄임 | 분산이 큰 데이터 완화 로그 변환보다 완만한 압축 효과 |
음수 데이터에는 적용 불가 효과가 제한적일 수 있음 |
최소-최대 정규화 (Min-Max Scaling)
최소-최대 정규화는 가장 직관적이고 널리 사용되는 정규화 방법 중 하나이다. 이 기법은 데이터의 최소값을 0, 최대값을 1로 맞추어 모든 값을 0과 1 사이로 선형 변환하는 방식이다. 변환 공식은 다음과 같다.
여기서 x는 원본 값, min(x)는 해당 변수의 최소값, max(x)는 최대값, 그리고 x'는 정규화된 값이다. 이 수식을 통해 원본 값이 최소값일 경우 0, 최대값일 경우 1이 되며, 그 사이의 값들은 0과 1 사이의 실수로 변환된다.
최소-최대 정규화의 가장 큰 장점은 결과가 항상 일정한 범위(보통 0과 1)에 놓이게 된다는 점이다. 따라서 서로 다른 단위와 범위를 가진 변수들도 같은 스케일에서 비교할 수 있으며, 거리 기반 알고리즘(K-최근접 이웃, K-평균 클러스터링 등)에서 특히 효과적이다. 예를 들어, 키(150cm)와 몸무게(120kg)를 동시에 고려해야 하는 경우, 원본 값을 그대로 사용하면 단위 차이 때문에 몸무게가 더 큰 영향을 줄 수 있다. 그러나 최소-최대 정규화를 적용하면 두 변수 모두 0~1 범위로 변환되어 공정한 비교가 가능하다.
그러나 이 기법은 이상치(outlier)에 매우 민감하다는 단점이 있다. 만약 데이터에 극단적으로 큰 값이나 작은 값이 포함되어 있으면, 그 값이 새로운 최소값이나 최대값으로 반영되어 나머지 데이터들의 스케일이 지나치게 압축될 수 있다. 예를 들어, 대부분의 소득 데이터가 2천만 원에서 1억 원 사이에 분포하는데, 한 명이 100억 원의 소득을 기록한다면, 최소-최대 정규화를 적용했을 때 나머지 값들은 0에 가까운 작은 수로 몰리게 된다. 이 경우 데이터의 변별력이 약해져 모델 성능이 떨어질 수 있다.
따라서 최소-최대 정규화는 이상치가 거의 없거나, 데이터 범위가 이미 일정하게 제한되어 있는 경우에 적합하다. 반대로 이상치가 많을 것으로 예상되는 데이터셋에서는 다른 정규화 방법(예: Z-점수 표준화, 로버스트 스케일링 등)을 고려하는 것이 더 바람직하다.
정리하면, 최소-최대 정규화는 간단하면서도 강력한 기법으로, 특히 거리 기반 모델에서 유용하다. 다만 이상치에 대한 민감성을 충분히 이해하고, 데이터의 특성을 고려하여 적절히 사용할 필요가 있다.
Z-점수 표준화 (Z-score Standardization)
Z-점수 표준화는 데이터의 평균과 표준편차를 활용하여 각 값을 변환하는 방법이다. 이 기법은 데이터 분포를 평균 0, 표준편차 1을 갖는 새로운 분포로 변환한다. 변환된 값은 “해당 데이터가 평균으로부터 몇 개의 표준편차만큼 떨어져 있는지”를 의미하며, 이를 Z-점수(Z-score)라고 부른다. 변환 공식은 다음과 같다.
Z-점수 표준화의 가장 큰 장점은 데이터의 상대적인 위치를 해석하기 쉽다는 점이다. 예를 들어, 어떤 학생의 수학 점수가 Z-점수 1.5라면, 이는 평균보다 1.5 표준편차 위에 있음을 의미한다. 따라서 서로 다른 시험 과목의 점수를 동일한 기준으로 비교할 때 매우 유용하다.
머신러닝 관점에서 Z-점수 표준화는 특히 경사 하강법을 사용하는 모델에서 안정적인 학습을 보장한다. 모든 변수가 평균 0을 중심으로 하고 분산이 1로 맞추어져 있기 때문에, 학습 과정에서 특정 변수가 다른 변수보다 지나치게 큰 영향을 주는 문제가 줄어든다. 또한 분포의 형태가 다르더라도 표준화 과정을 거치면 모델이 각 변수를 공정하게 반영할 수 있다.
그러나 이 방식은 이상치(outlier)에 민감하다는 단점이 있다. 평균과 표준편차 자체가 이상치의 영향을 크게 받기 때문에, 데이터에 극단값이 포함되어 있다면 표준화된 값이 왜곡될 수 있다. 예를 들어, 대부분의 값이 평균 근처에 몰려 있는데 한두 개의 값이 지나치게 크거나 작다면, 표준편차가 크게 계산되어 나머지 값들의 변환 결과가 상대적으로 작아지는 문제가 발생한다. 따라서 이상치가 많은 데이터셋에서는 로버스트 스케일링과 같은 다른 방법이 더 적합할 수 있다.
정리하면, Z-점수 표준화는 데이터의 분포를 평균 0, 표준편차 1로 변환하여, 각 값이 평균으로부터 얼마나 떨어져 있는지를 직관적으로 해석할 수 있게 한다. 이 기법은 다양한 머신러닝 알고리즘에서 효과적으로 사용되며, 특히 변수 단위가 다른 경우나 데이터 범위가 넓게 퍼져 있는 경우에 유용하다. 다만 이상치가 많을 경우 주의 깊게 적용해야 한다.
로버스트 스케일링 (Robust Scaling)
로버스트 스케일링은 데이터의 중앙값(Median)과 사분위 범위(IQR, Interquartile Range)를 기준으로 값을 변환하는 기법이다. 변환 공식은 다음과 같다.
여기서 x
는 원본 값, Median
은 중앙값, IQR은 제3사분위수(Q3)와 제1사분위수(Q1)의 차이(Q3 - Q1), 그리고 x'
는 변환된 값이다.
이 방식의 특징은 평균과 표준편차 대신 중앙값과 사분위 범위를 사용한다는 점이다. 평균과 표준편차는 데이터에 이상치가 포함될 경우 크게 왜곡될 수 있지만, 중앙값과 사분위수는 극단값의 영향을 거의 받지 않는다. 따라서 로버스트 스케일링은 이상치에 강건(Robust)한 정규화 기법으로 평가된다.
예를 들어, 대부분의 소득이 3천만 원에서 8천만 원 사이에 분포하는 데이터가 있다고 하자. 이때 극소수의 값(수십억 원)이 포함되어 있다면, 최소-최대 정규화나 Z-점수 표준화에서는 이상치가 전체 스케일을 왜곡시켜 다른 값들이 0이나 1 근처에 몰리게 된다. 그러나 로버스트 스케일링을 적용하면 중앙값을 중심으로 데이터가 재조정되고, IQR을 기준으로 스케일을 맞추기 때문에 극단적인 값의 영향이 최소화된다. 결과적으로 데이터의 일반적인 패턴을 더 잘 반영할 수 있다.
하지만 로버스트 스케일링에도 한계가 있다. 이상치의 영향을 줄여주는 대신, 데이터가 대칭적이고 정규분포에 가까운 경우에는 오히려 다른 스케일링 방법보다 효율이 떨어질 수 있다. 또한, 모든 데이터를 동일한 구간(예: 0~1)으로 제한해 주지는 않기 때문에, 변수 간 비교가 직관적이지 않을 수 있다.
정리하면, 로버스트 스케일링은 중앙값과 사분위 범위를 활용하여 이상치에 강건한 스케일링 방법을 제공한다. 특히 극단값이 존재하는 실제 데이터셋에서 안정적으로 활용될 수 있으며, Z-점수 표준화나 최소-최대 정규화와 비교해 이상치에 더 유연한 대안을 제시한다. 따라서 데이터에 이상치가 많을 것으로 예상된다면, 로버스트 스케일링을 우선적으로 고려하는 것이 바람직하다.
로그/제곱근 변환 (Log/Square Root Transformation)
로그 변환과 제곱근 변환은 데이터의 분포를 안정화(stabilization)하고 이상치의 영향을 완화하기 위해 자주 사용되는 기법이다. 두 방법 모두 원본 데이터의 크기를 압축하는 효과가 있어, 극단적으로 큰 값이 존재하는 경우에도 전체 분포를 더 균형 있게 표현할 수 있다.
- 로그 변환 (Log Transformation) 공식
여기서 c
는 0 이하 값에 로그를 적용할 수 없으므로, 필요할 경우 데이터를 양수로 만들기 위해 추가하는 작은 상수이다.
로그 변환의 가장 큰 효과는 긴 꼬리 분포(long-tailed distribution)를 완화하는 데 있다. 예를 들어, 소득, 주택 가격, 거래 금액과 같은 데이터는 소수의 큰 값이 전체 평균을 끌어올리는 경우가 많다. 로그 변환을 적용하면 큰 값의 스케일이 압축되어 분포가 정규분포에 가까워지고, 분석 및 시각화가 훨씬 용이해진다. 또한 회귀분석이나 신경망 학습에서 안정적인 결과를 얻는 데도 도움이 된다.
다만 로그 변환은 0 이하 값에는 직접 적용할 수 없다는 한계가 있다. 이 때문에 데이터에 음수 값이나 0이 포함되어 있다면, 변환 전에 적절한 상수 c
를 더해주어야 한다.
-
제곱근 변환 (Square Root Transformation) 공식
\[x' = \sqrt{x + c}\]마찬가지로, 음수 값이 포함될 수 있는 경우에는
c
라는 보정 상수를 더하여 양수로 만든 뒤 변환한다.제곱근 변환은 로그 변환보다는 완만하지만, 여전히 큰 값을 압축하는 효과가 있다. 특히 카운트 기반 데이터(사건 발생 횟수, 교통사고 건수, 방문자 수 등)에 자주 활용된다. 이러한 데이터는 종종 포아송 분포(Poisson distribution)를 따른다고 가정하는데, 이 분포는 단위 시간(또는 공간) 안에서 어떤 사건이 몇 번 발생하는지를 모델링하는 확률 분포이다. 예를 들어, 한 시간 동안 들어오는 콜센터 전화 수, 하루 동안 발생하는 교통사고 수, 특정 웹사이트의 분당 방문자 수 등이 포아송 분포의 전형적인 예이다. 포아송 분포의 중요한 특징은 평균과 분산이 동일하다는 것인데, 제곱근 변환을 적용하면 이러한 데이터의 분산을 줄이고 분포를 안정화하는 데 도움이 된다.
로그 변환과 제곱근 변환은 모두 이상치와 비대칭 분포를 다루는 데 효과적이다. 다만 로그 변환은 큰 값에 대해 더 강력한 압축 효과를 보이며, 제곱근 변환은 상대적으로 완만한 압축을 제공한다. 따라서 데이터의 분포 형태와 분석 목적에 따라 두 방법을 적절히 선택해야 한다. 예를 들어, 소득과 같이 극단값이 많은 경우에는 로그 변환이 더 적합하고, 사건 발생 횟수와 같이 양의 정수 데이터에는 제곱근 변환이 자주 사용된다.
로그/제곱근 변환은 데이터의 스케일을 줄이고 분포를 안정화하여, 분석과 모델링의 정확성을 높이는 유용한 방법이다. 특히 이상치나 긴 꼬리 분포가 존재할 때 효과가 크며, 단순한 선형 변환으로는 해결하기 어려운 문제를 보완해준다.
실습: 정규화 적용
이번 실습에서는 서로 다른 단위와 범위를 가진 변수를 대상으로 정규화를 적용해보고, 그 결과가 데이터의 분포와 분석에 어떤 영향을 미치는지 확인한다.
실습의 문제 정의는 다음과 같다. 우리가 사용하는 데이터에는 키(단위: cm), 몸무게(단위: kg), 소득(단위: 만원)과 같은 서로 다른 범위의 값들이 포함되어 있다. 원본 데이터를 그대로 사용하면 소득처럼 값의 크기가 큰 변수가 거리 기반 알고리즘이나 최적화 과정에서 더 큰 영향을 주어 모델이 편향될 수 있다. 따라서 정규화를 통해 모든 변수를 일정한 스케일로 조정하여, 공정하고 안정적인 분석이 가능하도록 해야 한다.
실습 시나리오는 다음과 같이 설정한다. 먼저 원본 데이터를 확인하여 각 변수의 값의 범위와 분포를 살펴본다. 이후 대표적인 정규화 기법인 최소-최대 정규화(Min-Max Scaling), Z-점수 표준화(Z-score Standardization), 로버스트 스케일링(Robust Scaling)을 차례대로 적용한다. 각 방법이 적용된 데이터를 원본과 비교하여, 평균, 분산, 최소값과 최대값, 사분위수(IQR) 등의 변화가 어떻게 나타나는지를 정리한다.
실습의 목표는 세 가지다. 첫째, 정규화가 실제 데이터 값의 크기를 어떻게 바꾸는지 직접 확인한다. 둘째, 서로 다른 정규화 기법이 데이터 분포에 미치는 영향을 비교함으로써, 각 방법의 특징과 장단점을 이해한다. 셋째, 데이터의 특성(예: 이상치 존재 여부, 변수 단위의 차이)에 따라 어떤 정규화 방법을 선택하는 것이 적절한지 스스로 판단할 수 있는 능력을 기른다.
접근 방법은 단계적으로 진행한다. 먼저 원본 데이터에 대한 기초 통계량과 분포를 확인하고, 세 가지 정규화 방법을 적용한 결과를 표 형식으로 비교한다. 이어서 시각화를 통해 각 방법이 데이터 분포를 어떻게 변형했는지 직관적으로 살펴본다. 이 과정을 통해 여러분은 정규화가 단순한 수학적 변환이 아니라, 모델 성능과 데이터 해석력에 직접적으로 연결되는 중요한 과정임을 체험하게 될 것이다.
실습 예제 코드
# File: normalization_practice_class.py
# 목적: 정규화/스케일링 실습 (Min-Max, Z-score, Robust)
# 환경: Python 3.10+, pandas, numpy, scikit-learn
import numpy as np
import pandas as pd
from sklearn.preprocessing import MinMaxScaler, StandardScaler, RobustScaler
class NormalizationPractice:
"""
정규화/스케일링 실습 클래스
- 데이터: height_cm(키), weight_kg(몸무게), income_m(소득/만원)
- 스케일러: Min-Max, Z-score(Standard), Robust(IQR 기반)
- 출력: 변수별로 원본 vs 변환 후 요약 지표 비교 표
"""
def __init__(self, n: int = 300, n_outliers: int = 5, seed: int = 42):
self.df = self._make_dataset(n, n_outliers, seed)
self.num_cols = ["height_cm", "weight_kg", "income_m"]
@staticmethod
def _make_dataset(n: int, n_outliers: int, seed: int) -> pd.DataFrame:
"""
서로 다른 스케일의 변수를 가지는 데이터 생성
- height_cm: N(170, 7^2)
- weight_kg: N(70, 12^2)
- income_m (만원): 로그정규 분포(긴 꼬리) + 상위 아웃라이어 몇 개
"""
rng = np.random.default_rng(seed)
# 키(cm): 평균 170, 표준편차 7
height = rng.normal(loc=170, scale=7, size=n)
# 몸무게(kg): 평균 70, 표준편차 12
weight = rng.normal(loc=70, scale=12, size=n)
# 소득(만원): 로그정규로 긴 꼬리 분포, 스케일 차이를 크게 만들기 위해 100배 스케일링
income_base = rng.lognormal(mean=2.8, sigma=0.5, size=n) * 100
# 상위 아웃라이어 추가(소득만): 현실적인 긴 꼬리를 더 강조
income_out = rng.lognormal(mean=4.2, sigma=0.25, size=n_outliers) * 100
income = np.concatenate([income_base, income_out])
# 길이를 맞추기 위해 height/weight에도 동일 개수만큼 샘플 추가
height_out = rng.normal(loc=170, scale=7, size=n_outliers)
weight_out = rng.normal(loc=70, scale=12, size=n_outliers)
height = np.concatenate([height, height_out])
weight = np.concatenate([weight, weight_out])
df = pd.DataFrame(
{
"height_cm": height.astype(float),
"weight_kg": weight.astype(float),
"income_m": income.astype(float), # 만원 단위
}
)
return df
# ------------------------------
# 스케일러 적용
# ------------------------------
def _fit_transform(self, scaler) -> pd.DataFrame:
"""주어진 스케일러로 수치 컬럼 변환"""
arr = scaler.fit_transform(self.df[self.num_cols])
out = pd.DataFrame(arr, columns=self.num_cols, index=self.df.index)
return out
def minmax(self) -> pd.DataFrame:
return self._fit_transform(MinMaxScaler())
def zscore(self) -> pd.DataFrame:
return self._fit_transform(StandardScaler())
def robust(self) -> pd.DataFrame:
return self._fit_transform(RobustScaler(with_centering=True, with_scaling=True))
# ------------------------------
# 요약 지표
# ------------------------------
@staticmethod
def _metrics(s: pd.Series) -> dict:
"""분포 요약 지표: 평균, 표준편차, 최솟값, Q1, 중앙값, Q3, IQR, 최댓값"""
q1 = s.quantile(0.25)
q3 = s.quantile(0.75)
return {
"mean": s.mean(),
"std": s.std(ddof=1),
"min": s.min(),
"q1": q1,
"median": s.median(),
"q3": q3,
"IQR": q3 - q1,
"max": s.max(),
}
def _summary_by_column(self, col: str, df_minmax: pd.DataFrame,
df_z: pd.DataFrame, df_robust: pd.DataFrame) -> pd.DataFrame:
"""
단일 변수에 대해 원본/세 스케일러 결과를 한 표로 정리
행: 지표, 열: 원본 | Min-Max | Z-score | Robust
"""
original = self._metrics(self.df[col])
mmin = self._metrics(df_minmax[col])
zsc = self._metrics(df_z[col])
rbs = self._metrics(df_robust[col])
tbl = pd.DataFrame(
{
"원본": original,
"Min-Max": mmin,
"Z-score": zsc,
"Robust": rbs,
}
).round(4)
return tbl
# ------------------------------
# 실행
# ------------------------------
def run(self) -> None:
# 스케일러 적용
df_minmax = self.minmax()
df_z = self.zscore()
df_rb = self.robust()
# 원본 범위 미리보기(단위 차이 확인)
print("========== 원본 데이터 범위 미리보기 ==========\n")
preview = pd.DataFrame(
{
"min": self.df[self.num_cols].min(),
"median": self.df[self.num_cols].median(),
"max": self.df[self.num_cols].max(),
}
)
# 소수점 2자리 및 세자리 콤마 포맷
print(preview.map(lambda x: f"{x:,.2f}").to_string())
print()
# 변수별 요약 표 출력
for col in self.num_cols:
print(f"---------- 변수: {col} ----------\n")
tbl = self._summary_by_column(col, df_minmax, df_z, df_rb)
# 보기 좋게 포맷: std/IQR 제외한 값은 콤마, 소수점 2자리
def fmt(x):
try:
return f"{x:,.2f}"
except Exception:
return x
formatted = tbl.copy()
for c in formatted.columns:
formatted[c] = formatted[c].apply(fmt)
print(formatted.to_string())
print()
# 해석 가이드
print("해석 가이드")
print("- Min-Max: 각 변수의 min→0, max→1. 범위가 0~1로 통일되어 거리 기반 모델에 유리.")
print("- Z-score: mean≈0, std≈1로 맞춰 경사 하강법 기반 모델 학습 안정화에 유리.")
print("- Robust: 중앙값 기준, IQR 스케일(이상치에 강건). 이상치 많은 데이터에 유리.")
# ------------------------------
# 메인 실행부
# ------------------------------
def main():
app = NormalizationPractice(n=300, n_outliers=5, seed=42)
app.run()
if __name__ == "__main__":
main()
실행 결과
$ python test.py
========== 원본 데이터 범위 미리보기 ==========
min median max
height_cm 152.03 169.34 190.40
weight_kg 34.43 70.80 101.17
income_m 432.14 1,633.28 9,919.32
---------- 변수: height_cm ----------
원본 Min-Max Z-score Robust
mean 169.71 0.46 0.00 0.05
std 6.55 0.17 1.00 0.81
min 152.03 0.00 -2.70 -2.15
q1 165.23 0.34 -0.68 -0.51
median 169.34 0.45 -0.06 0.00
q3 173.30 0.55 0.55 0.49
IQR 8.07 0.21 1.23 1.00
max 190.40 1.00 3.17 2.61
---------- 변수: weight_kg ----------
원본 Min-Max Z-score Robust
mean 69.89 0.53 -0.00 -0.06
std 12.15 0.18 1.00 0.74
min 34.43 0.00 -2.92 -2.21
q1 61.54 0.41 -0.69 -0.56
median 70.80 0.54 0.07 0.00
q3 78.02 0.65 0.67 0.44
IQR 16.48 0.25 1.36 1.00
max 101.17 1.00 2.58 1.84
---------- 변수: income_m ----------
원본 Min-Max Z-score Robust
mean 1,885.63 0.15 0.00 0.23
std 1,202.81 0.13 1.00 1.07
min 432.14 0.00 -1.21 -1.07
q1 1,128.59 0.07 -0.63 -0.45
median 1,633.28 0.13 -0.21 0.00
q3 2,247.81 0.19 0.30 0.55
IQR 1,119.22 0.12 0.93 1.00
max 9,919.32 1.00 6.69 7.40
실행 결과 해석
원본 데이터를 살펴보면, 키(height_cm)는 약 152cm에서 190cm 사이, 몸무게(weight_kg)는 약 34kg에서 101kg 사이에 분포한다. 반면 소득(income_m)은 최소 432만원에서 최대 9,919만원까지 나타나며, 다른 변수들보다 훨씬 큰 범위와 긴 꼬리를 가지고 있다. 이처럼 변수마다 단위와 값의 크기가 다르기 때문에 정규화를 통해 스케일을 조정하지 않으면 모델 학습 과정에서 불균형이 발생할 수 있다.
Min-Max 정규화 결과를 보면, 세 변수 모두 0에서 1 사이 값으로 변환되었다. 키와 몸무게는 고르게 매핑되어 분포가 잘 유지되었으나, 소득의 경우 이상치가 최대값으로 설정되면서 나머지 값들이 0 근처로 몰려버렸다. 이는 Min-Max 방식이 이상치의 영향을 크게 받는다는 점을 잘 보여준다.
Z-점수 표준화를 적용하면, 세 변수 모두 평균이 0, 표준편차가 1로 맞추어졌다. 키와 몸무게는 -2에서 +3 사이의 값으로 변환되어 상대적인 분포를 안정적으로 표현한다. 그러나 소득은 여전히 최대 6.69 표준편차까지 뻗어 있으며, 이상치가 존재할 경우 극단적으로 큰 값이 그대로 드러난다. 즉, 단위 차이는 해결되지만 이상치의 문제는 남아 있는 셈이다.
로버스트 스케일링은 중앙값을 0, 사분위 범위(IQR)를 1로 변환하여 이상치의 영향을 줄인다. 그 결과 키와 몸무게는 안정적으로 -2~+2 범위에 분포하며, 소득도 중앙값과 사분위 범위를 기준으로 균형 있게 표현되었다. Z-점수 표준화에서는 소득 이상치가 매우 큰 값으로 튀어나왔지만, 로버스트 스케일링에서는 비교적 완화된 형태로 나타났다. 이는 로버스트 스케일링이 이상치가 많은 데이터셋에서 효과적임을 보여준다.
종합적으로 정리하면, Min-Max 정규화는 범위를 일정하게 맞추는 데 유리하지만 이상치에 취약하다. Z-점수 표준화는 변수 간 단위 차이를 해소하고 경사 하강법 기반 모델에서 유용하지만, 이상치의 영향을 크게 받는다. 로버스트 스케일링은 이상치가 존재하는 실제 데이터셋에서 상대적으로 안정적이며, 데이터의 일반적인 패턴을 더 잘 반영할 수 있다. 따라서 정규화 기법은 데이터의 특성과 이상치 존재 여부를 고려해 선택해야 한다.
범주형 데이터 처리
범주형 데이터란 무엇인가?
데이터는 크게 수치형 데이터(numerical data) 와 범주형 데이터(categorical data) 로 나눌 수 있다. 수치형 데이터가 연속적인 값이나 정량적인 크기를 표현한다면, 범주형 데이터는 수치적 크기보다는 특정한 집단이나 범주를 구분하기 위해 사용된다.
범주형 데이터는 값이 숫자 대신 문자열, 라벨(label), 기호 등으로 기록되는 경우가 많으며, 각 값은 고유한 범주(category)를 나타낸다. 예를 들어 성별(남, 여), 혈액형(A, B, O, AB), 도시(서울, 부산, 대전), 학년(1학년, 2학년, 3학년) 등이 대표적인 범주형 데이터다. 이 데이터는 단순히 "구분"의 의미를 가지며, 값 자체로는 크고 작음을 비교할 수 없다.
범주형 데이터는 다시 두 가지로 나눌 수 있다.
-
명목형 데이터(Nominal Data)
: 순서나 크기의 개념이 없는 단순한 범주. 예를 들어 성별(남/여), 혈액형(A/B/O/AB), 국적(한국/미국/일본)은 모두 서로 다른 집단을 구분할 뿐, 어느 값이 더 크거나 작다고 할 수는 없다. -
순서형 데이터(Ordinal Data)
: 순서의 개념이 포함된 범주. 예를 들어 학년(1학년 < 2학년 < 3학년), 고객 만족도(매우 불만족 < 보통 < 매우 만족)는 크기의 차이가 정량적이지는 않지만, 순서의 의미가 존재한다.
범주형 데이터는 분석과 모델링에서 매우 중요한 역할을 한다. 예를 들어 “도시별 매출 비교”에서는 도시라는 범주형 변수가 집단을 구분하는 기준이 되고, “고객 만족도에 따른 구매율 예측”에서는 순서형 범주가 중요한 설명 변수가 된다. 하지만 컴퓨터와 머신러닝 알고리즘은 일반적으로 숫자 데이터만 처리할 수 있으므로, 범주형 데이터를 그대로 입력할 수는 없다. 따라서 범주형 데이터를 적절한 수치 데이터로 변환하는 과정, 즉 범주형 데이터 처리(Categorical Data Handling) 가 필요하다.
범주형 데이터는 데이터의 의미를 구분짓는 중요한 속성이지만, 수학적 연산에는 바로 사용할 수 없다는 특징이 있다. 따라서 모델링 과정에서 범주형 변수를 올바르게 변환하고 처리하는 것은 데이터 전처리에서 반드시 고려해야 할 핵심 요소다.
범주형 데이터의 종류 (명목형 vs 순서형)
범주형 데이터는 그 자체로는 수학적 연산이 불가능하지만, 데이터의 성격에 따라 다시 두 가지로 나눌 수 있다. 바로 명목형 데이터(Nominal Data) 와 순서형 데이터(Ordinal Data)이다. 이 두 유형을 구분하는 핵심 기준은 순서의 존재 여부이다.
1) 명목형 데이터 (Nominal Data)
명목형 데이터는 순서나 크기의 개념이 전혀 없는 범주형 데이터이다. 각 값은 단순히 다른 집단을 구분하기 위한 이름표(label)의 역할만 한다. 예를 들어, 성별(남/여), 혈액형(A/B/O/AB), 국적(한국/미국/일본), 도시명(서울/부산/대전)은 모두 명목형 데이터에 해당한다.
특징: 값들 간의 크기 비교 불가, 단순한 구분
분석 예시: “어떤 혈액형이 가장 많이 분포했는가?”, “서울과 부산 중 매출이 더 큰 지역은 어디인가?”
2) 순서형 데이터 (Ordinal Data)
순서형 데이터는 순서의 개념이 존재하는 범주형 데이터이다. 값의 크기를 정확히 수치화할 수는 없지만, 상대적인 순서나 등급을 표현할 수 있다. 예를 들어, 학년(1학년 < 2학년 < 3학년), 고객 만족도(매우 불만족 < 불만족 < 보통 < 만족 < 매우 만족), 교육 수준(초등학교 < 중학교 < 고등학교 < 대학교)은 모두 순서형 데이터이다.
특징: 값들 간의 순서는 비교 가능, 하지만 간격의 크기는 일정하지 않음
분석 예시: “3학년이 1학년보다 시험 성적이 높은가?”, “매우 만족 그룹의 구매율은 다른 그룹과 어떻게 다른가?”
3) 두 유형의 비교
명목형과 순서형 데이터는 모두 범주형 데이터에 속하지만, 처리 방식에서 차이가 있다.
-
명목형 데이터는 원-핫 인코딩(One-Hot Encoding)처럼 단순히 구분을 위한 더미 변수(dummy variable)로 변환하는 경우가 많다.
-
순서형 데이터는 값의 순서를 반영하기 위해 라벨 인코딩(Label Encoding)이나 순위 변환 방식을 활용하는 경우가 있다. 다만 순서형 데이터를 단순히 숫자로 치환하면 간격이 동일하다고 잘못 해석될 수 있으므로 주의해야 한다.
명목형 데이터는 “이름표”로서 단순히 구분하는 역할을 하고, 순서형 데이터는 “서열”의 의미를 가진다. 데이터 분석에서 두 유형을 명확히 구분하는 것은 이후 인코딩 및 모델링 과정에서 적절한 방법을 선택하는 데 있어 매우 중요하다.
범주형 데이터 처리 방법
범주형 데이터는 문자열이나 라벨 형태로 되어 있기 때문에 대부분의 머신러닝 알고리즘에서 직접 사용할 수 없다. 따라서 이를 수치 데이터로 변환하는 과정이 필요하다. 대표적인 방법으로는 라벨 인코딩(Label Encoding), 원-핫 인코딩(One-Hot Encoding), 더미 변수(Dummy Variables), 그리고 고급 인코딩 기법이 있다.
1) 라벨 인코딩 (Label Encoding)
라벨 인코딩은 범주형 데이터를 정수형 숫자로 변환하는 방식이다. 예를 들어, 도시에 대한 데이터 [서울, 부산, 대전]을 각각 [0, 1, 2
]로 바꿔 표현한다.
-
장점: 구현이 간단하고 메모리 효율이 높다.
-
단점: 범주 간의 순서가 없는 경우에도 숫자가 크기와 순서를 의미하는 것처럼 잘못 해석될 수 있다. 예를 들어, “서울=0, 부산=1, 대전=2”라고 하면 모델이 대전을 부산보다 큰 값으로 오인할 수 있다.
2) 원-핫 인코딩 (One-Hot Encoding)
원-핫 인코딩은 각 범주마다 새로운 이진 변수(0
또는 1
)를 생성하는 방식이다. 예를 들어, [서울, 부산, 대전]이라는 변수가 있으면, ‘Seoul’, ‘Busan’, ‘Daejeon’이라는 세 개의 열을 만들고, 해당 값이 있으면 1
, 없으면 0
으로 표시한다.
-
장점: 범주 간 크기 관계가 없으므로 잘못된 순서 해석 문제가 없다.
-
단점: 범주의 개수가 많으면 차원이 크게 늘어나(차원의 저주 문제), 메모리와 계산 효율이 떨어진다.
3) 더미 변수 (Dummy Variables)
더미 변수는 원-핫 인코딩과 유사하지만, 불필요한 중복을 제거하기 위해 하나의 범주를 기준으로 제외하는 방식이다. 예를 들어, [서울, 부산, 대전]을 원-핫 인코딩하면 3개의 열이 생기지만, 더미 변수 방식에서는 2개의 열만 생성된다(서울=0,0
/ 부산=1,0
/ 대전=0,1
). 기준 범주는 모든 값이 0일 때 자동으로 식별되므로, 다중공선성(multicollinearity)을 줄일 수 있다.
-
장점: 차원 수를 줄이고 회귀분석 등 통계적 모델에서 안정성을 높인다.
-
단점: 기준 범주의 선택에 따라 해석이 달라질 수 있다.
4) 고급 기법
최근에는 단순한 변환을 넘어 머신러닝 모델 성능을 높이기 위한 고급 인코딩 기법들이 사용된다.
-
타깃 인코딩(Target Encoding)
범주를 해당 범주의 평균 목표값(target mean)으로 치환하는 방식. 예를 들어, 각 도시별 평균 매출을 새로운 값으로 대체할 수 있다. 하지만 데이터 누수를 방지하기 위한 교차검증이 필요하다.
-
임베딩(Embedding)
범주형 변수를 벡터 공간에 매핑하는 방식으로, 특히 딥러닝에서 자주 사용된다. 예를 들어, 자연어 처리에서 단어를 임베딩 벡터로 변환하는 것과 같은 원리다. 임베딩은 고차원 범주형 데이터를 저차원 벡터로 변환하면서도 의미를 보존하는 장점이 있다.
범주형 데이터는 단순히 문자열을 숫자로 치환하는 것이 아니라, 데이터의 성격(명목형/순서형)과 모델 특성을 고려하여 적절한 인코딩 방식을 선택해야 한다. 간단한 문제에는 라벨 인코딩이나 원-핫 인코딩이 충분하지만, 범주의 수가 많거나 복잡한 관계를 학습해야 하는 경우에는 타깃 인코딩이나 임베딩 같은 고급 기법이 필요하다.
실습실습: 범주형 변수 인코딩 비교
이번 실습에서는 범주형 데이터를 다양한 방식으로 인코딩했을 때, 데이터 형태와 분석에 어떤 차이가 발생하는지를 직접 확인한다.
실습의 문제 정의는 다음과 같다. 범주형 데이터는 문자열이나 라벨로 기록되어 있어 머신러닝 모델이 직접 처리할 수 없다. 따라서 이를 수치 데이터로 변환해야 하는데, 인코딩 방식에 따라 데이터의 구조가 달라지고, 이는 모델 성능과 해석에 직접적인 영향을 미칠 수 있다.
실습 시나리오는 먼저 범주형 변수가 포함된 작은 데이터셋을 준비하는 것으로 시작한다. 예를 들어, "도시(city: 서울, 부산, 대전)"와 "만족도(satisfaction: 불만족, 보통, 만족)" 같은 변수를 포함한 데이터셋을 생성한다. 이후 같은 데이터를 라벨 인코딩(Label Encoding), 원-핫 인코딩(One-Hot Encoding), 더미 변수(Dummy Variables) 세 가지 방식으로 변환해본다. 변환된 데이터프레임을 서로 비교하여, 각 방법이 어떤 구조적 차이를 만들어내는지 확인한다.
실습의 목표는 세 가지다. 첫째, 라벨 인코딩이 단순히 범주를 숫자로 치환하는 방식임을 확인하고, 그 과정에서 순서가 없는 범주가 잘못된 의미를 가질 수 있음을 이해한다. 둘째, 원-핫 인코딩이 범주 간 순서 문제를 해결하지만, 차원의 수가 증가하는 한계를 가짐을 체험한다. 셋째, 더미 변수를 통해 다중공선성 문제를 줄이면서도 동일한 정보를 보존할 수 있음을 확인한다.
접근 방법은 다음과 같다. (1) 원본 데이터셋을 출력하여 범주형 변수의 형태를 살펴본다. (2) 세 가지 인코딩 방법을 적용한 결과를 각각 출력한다. (3) 결과를 비교 표 형식으로 정리하여, 각 방식의 장단점을 다시 확인한다. 이 과정을 통해 여러분은 범주형 데이터를 단순히 숫자로 바꾸는 것 이상의 의미를 파악하고, 실제 분석 상황에서 적절한 방법을 선택할 수 있는 안목을 기르게 될 것이다.
실습 예제 코드
# File: categorical_encoding_practice.py
# 목적: 범주형 변수 인코딩 비교 실습 (Label / One-Hot / Dummy)
# 환경: Python 3.10+, pandas, scikit-learn
import pandas as pd
from sklearn.preprocessing import LabelEncoder
# =========================================================
# 1) 예제 데이터 생성
# =========================================================
def make_sample() -> pd.DataFrame:
"""
범주형 예제 데이터 생성
- city: 명목형(순서 없음) -> ['Seoul', 'Busan', 'Daejeon']
- satisfaction: 순서형(서열 있음) -> ['불만족', '보통', '만족']
- purchase: 이진 범주형 -> ['Yes', 'No']
"""
data = {
"city": ["Seoul", "Busan", "Seoul", "Daejeon", "Busan", "Seoul", "Daejeon", "Seoul"],
"satisfaction": ["만족", "보통", "만족", "불만족", "보통", "만족", "불만족", "보통"],
"purchase": ["Yes", "No", "Yes", "No", "No", "Yes", "No", "Yes"],
"amount": [35, 12, 55, 8, 15, 48, 5, 30], # 수치형 변수(참고용)
}
return pd.DataFrame(data)
# =========================================================
# 2) 헬퍼: 보기 좋게 출력
# =========================================================
def print_df(title: str, df: pd.DataFrame) -> None:
print(f"\n========== {title} ==========\n")
print(df.to_string(index=False))
print() # 줄바꿈
def print_cols(title: str, df: pd.DataFrame) -> None:
print(f"{title}: {list(df.columns)}")
# =========================================================
# 3) 인코딩 함수들
# =========================================================
def label_encode(df: pd.DataFrame) -> pd.DataFrame:
"""
라벨 인코딩
- 문자열 범주를 정수(0,1,2,...)로 치환
- 순서가 없는 명목형 변수에 사용하면 "숫자가 크다/작다"라는 잘못된 의미가 생길 수 있음(주의)
"""
out = df.copy()
# city (명목형) 라벨 인코딩: 교육용으로 보여주되, 모델에 바로 쓰기엔 부적절할 수 있음을 안내
le_city = LabelEncoder()
out["city_le"] = le_city.fit_transform(out["city"])
# satisfaction (순서형) 라벨 인코딩: 순서를 반영하려면 직접 맵핑(불만족 < 보통 < 만족)
ord_map = {"불만족": 0, "보통": 1, "만족": 2}
out["satisfaction_le"] = out["satisfaction"].map(ord_map)
# purchase (이진) 라벨 인코딩
le_purchase = LabelEncoder()
out["purchase_le"] = le_purchase.fit_transform(out["purchase"]) # Yes/No -> 1/0 (알파벳 순서)
# 원본 범주와 매핑 정보 안내
print("라벨 인코딩 매핑 정보")
print(f"- city: {dict(zip(le_city.classes_, le_city.transform(le_city.classes_)))}")
print(f"- satisfaction(순서 반영 수동 매핑): {ord_map}")
print(f"- purchase: {dict(zip(le_purchase.classes_, le_purchase.transform(le_purchase.classes_)))}\n")
# 라벨 인코딩 결과만 추려서 반환
return out[["city", "city_le", "satisfaction", "satisfaction_le", "purchase", "purchase_le", "amount"]]
def one_hot_encode(df: pd.DataFrame) -> pd.DataFrame:
"""
원-핫 인코딩
- 각 범주마다 0/1 열 생성 (순서 왜곡 없음)
- 범주 수가 많으면 열이 급격히 늘어날 수 있음(차원 증가)
"""
# city, satisfaction, purchase에 대해 one-hot
oh = pd.get_dummies(df, columns=["city", "satisfaction", "purchase"], dtype=int)
return oh
def dummy_encode(df: pd.DataFrame) -> pd.DataFrame:
"""
더미 변수 (drop_first=True)
- 원-핫과 유사하지만 기준 범주 1개를 제거하여 다중공선성 완화
- 회귀모형 등에서 안정성/해석성 향상
"""
dummy = pd.get_dummies(df, columns=["city", "satisfaction", "purchase"], drop_first=True, dtype=int)
return dummy
# =========================================================
# 4) 실행
# =========================================================
def main() -> None:
df = make_sample()
print_df("원본 데이터", df)
# 라벨 인코딩
df_label = label_encode(df)
print_df("라벨 인코딩 결과 (LE)", df_label)
# 원-핫 인코딩
df_onehot = one_hot_encode(df)
print_df("원-핫 인코딩 결과 (One-Hot)", df_onehot)
print_cols("원-핫 인코딩 컬럼", df_onehot); print()
# 더미 변수
df_dummy = dummy_encode(df)
print_df("더미 변수 결과 (Dummy, drop_first=True)", df_dummy)
print_cols("더미 인코딩 컬럼", df_dummy); print()
# 간단 비교 요약
print("\n---------- 비교 요약 ---------\n")
print(f"원본 컬럼 수: {df.shape[1]}")
print(f"라벨 인코딩 컬럼 수: {df_label.shape[1]}")
print(f"원-핫 인코딩 컬럼 수: {df_onehot.shape[1]} (차원 증가 주의)")
print(f"더미 인코딩 컬럼 수: {df_dummy.shape[1]} (기준 범주 제거로 차원 축소)\n")
print("\n해석 가이드")
print("- 라벨 인코딩: 명목형에 그대로 쓰면 숫자 순서가 생겨 해석/학습 왜곡 가능.")
print("- 원-핫 인코딩: 순서 왜곡 없음, 대신 범주 수만큼 열 증가(차원의 저주 유의).")
print("- 더미 변수: 원-핫에서 기준 범주 1개 제거 → 다중공선성 완화, 회귀계수 해석 용이.")
if __name__ == "__main__":
main()
출력 결과
$ python test.py
========== 원본 데이터 ==========
city satisfaction purchase amount
Seoul 만족 Yes 35
Busan 보통 No 12
Seoul 만족 Yes 55
Daejeon 불만족 No 8
Busan 보통 No 15
Seoul 만족 Yes 48
Daejeon 불만족 No 5
Seoul 보통 Yes 30
라벨 인코딩 매핑 정보
- city: {'Busan': np.int64(0), 'Daejeon': np.int64(1), 'Seoul': np.int64(2)}
- satisfaction(순서 반영 수동 매핑): {'불만족': 0, '보통': 1, '만족': 2}
- purchase: {'No': np.int64(0), 'Yes': np.int64(1)}
========== 라벨 인코딩 결과 (LE) ==========
city city_le satisfaction satisfaction_le purchase purchase_le amount
Seoul 2 만족 2 Yes 1 35
Busan 0 보통 1 No 0 12
Seoul 2 만족 2 Yes 1 55
Daejeon 1 불만족 0 No 0 8
Busan 0 보통 1 No 0 15
Seoul 2 만족 2 Yes 1 48
Daejeon 1 불만족 0 No 0 5
Seoul 2 보통 1 Yes 1 30
========== 원-핫 인코딩 결과 (One-Hot) ==========
amount city_Busan city_Daejeon city_Seoul satisfaction_만족 satisfaction_보통 satisfaction_불만족 purchase_No purchase_Yes
35 0 0 1 1 0 0 0 1
12 1 0 0 0 1 0 1 0
55 0 0 1 1 0 0 0 1
8 0 1 0 0 0 1 1 0
15 1 0 0 0 1 0 1 0
48 0 0 1 1 0 0 0 1
5 0 1 0 0 0 1 1 0
30 0 0 1 0 1 0 0 1
원-핫 인코딩 컬럼:
[
'amount',
'city_Busan',
'city_Daejeon',
'city_Seoul',
'satisfaction_만족',
'satisfaction_보통',
'satisfaction_불만족',
'purchase_No',
'purchase_Yes'
]
========== 더미 변수 결과 (Dummy, drop_first=True) ==========
amount city_Daejeon city_Seoul satisfaction_보통 satisfaction_불만족 purchase_Yes
35 0 1 0 0 1
12 0 0 1 0 0
55 0 1 0 0 1
8 1 0 0 1 0
15 0 0 1 0 0
48 0 1 0 0 1
5 1 0 0 1 0
30 0 1 1 0 1
더미 인코딩 컬럼:
[
'amount',
'city_Daejeon',
'city_Seoul',
'satisfaction_보통',
'satisfaction_불만족',
'purchase_Yes'
]
출력 결과 해석
구분 | 설명 | 장점 | 단점 |
---|---|---|---|
라벨 인코딩 | 범주를 정수(0,1,2,...)로 치환 | 구현이 간단, 메모리 효율적 | 순서가 없는 범주에 적용 시 크기·순서가 잘못 해석될 수 있음 |
원-핫 인코딩 | 각 범주마다 새로운 열 생성, 해당 범주이면 1, 아니면 0 | 순서 왜곡 없음, 모든 범주 정보를 온전히 반영 | 범주 수가 많으면 열이 급격히 늘어나 차원의 저주 발생 가능 |
더미 변수 | 원-핫 인코딩에서 기준 범주를 제외하여 다중공선성 방지 | 불필요한 중복 제거, 회귀분석 등에서 안정적이고 해석 용이 | 기준 범주 선택에 따라 해석 결과가 달라질 수 있음 |
원본 데이터를 보면, city
, satisfaction
, purchase
같은 범주형 변수가 문자열 형태로 존재한다. 이러한 상태에서는 대부분의 머신러닝 알고리즘이 직접적으로 연산을 수행할 수 없기 때문에, 적절한 인코딩 과정을 거쳐야 한다.
먼저 라벨 인코딩(Label Encoding) 결과를 살펴보면, city
변수는 Busan=0, Daejeon=1, Seoul=2와 같이 정수 값으로 변환되었다. satisfaction
변수는 불만족=0, 보통=1, 만족=2로 수동 매핑되어 순서를 반영했으며, purchase
변수는 No=0, Yes=1로 변환되었다. 이처럼 라벨 인코딩은 간단하게 범주를 숫자로 치환할 수 있지만, 순서가 없는 city
같은 명목형 변수에 그대로 적용하면 모델이 숫자의 크기를 잘못 해석할 수 있다는 한계가 있다.
다음으로 원-핫 인코딩(One-Hot Encoding)을 적용한 결과, 각 범주마다 새로운 이진 컬럼이 생성되었다. 예를 들어 city
변수는 city_Busan
, city_Daejeon
, city_Seoul
세 개의 열로 확장되었고, 각 행에서 해당 도시에만 1이 할당되었다. satisfaction 역시 세 개의 상태(만족, 보통, 불만족)에 대해 각각의 열이 생성되었으며, purchase 변수도 Yes와 No에 대해 두 개의 열로 표현되었다. 이 방식은 범주 간 순서를 잘못 해석하는 문제가 없지만, 범주가 많아질수록 열의 수가 급격히 늘어나 차원의 저주 문제가 발생할 수 있다.
마지막으로 더미 변수(Dummy Variables) 방식은 원-핫 인코딩에서 하나의 기준 범주를 제거하여 다중공선성 문제를 방지한 결과다. city
변수는 Busan
기준으로 제거하여 city_Daejeon
과 city_Seoul
두 열만 남았고, 모든 값이 0인 경우 자동으로 Busan
임을 알 수 있다. satisfaction
변수도 기준을 만족으로 두고 보통과 불만족 두 열만 생성되었으며, purchase 역시 Yes만 남기고 No는 제외되었다. 이렇게 더미 변수 방식은 원-핫 인코딩보다 열의 수를 줄이면서도 동일한 정보를 유지할 수 있어, 회귀분석과 같은 통계 모델에서 해석이 용이하다.
라벨 인코딩은 간단하지만 순서 해석의 오류가 있을 수 있고, 원-핫 인코딩은 안전하지만 차원이 늘어난다는 단점이 있다. 더미 변수 방식은 불필요한 중복을 제거해 다중공선성을 완화할 수 있다. 따라서 실제 분석에서는 데이터의 성격과 모델의 특성에 따라 적절한 인코딩 방식을 선택하는 것이 중요하다.