3  결측치 처리

Keywords

결측치 (Missing Value), 데이터 전처리, Python, 결측치 처리, 결측치 제거, 결측치 대체

결측치(Missing Value)는 데이터 수집 과정에서 다양한 이유로 발생하는 누락된 값을 의미한다. 결측치가 존재하면 통계 분석이나 머신러닝 모델의 정확도가 저하될 수 있으므로 적절한 처리가 필수적이다. 이 장에서는 결측치를 탐지하고 제거하거나 대체하는 다양한 기법을 학습한다. 결측치 처리 방법은 데이터의 특성과 결측치의 패턴에 따라 달라지므로, 각 상황에 맞는 적절한 전략을 선택하는 것이 중요하다.

3.1 데이터 로드 및 결측치 현황 확인

결측치 처리를 시작하기 전에 먼저 데이터를 불러오고 결측치가 어느 컬럼에 얼마나 존재하는지 파악해야 한다.

예제: 데이터 로드 및 결측치 개수 확인

import pandas as pd
import seaborn as sns

# 데이터 로드
df = sns.load_dataset("penguins")

# 컬럼별 결측치 개수 확인
df.isna().sum()
species               0
island                0
bill_length_mm        2
bill_depth_mm         2
flipper_length_mm     2
body_mass_g           2
sex                  11
dtype: int64

3.2 결측치 비율 확인

결측치의 개수뿐만 아니라 전체 데이터 대비 비율을 확인하면 결측치 처리 전략을 수립하는 데 도움이 된다. 일반적으로 결측치 비율이 5% 미만이면 해당 행을 제거하고, 5~10%인 경우 대체 방법을 고려하며, 10% 이상인 경우 신중한 분석이 필요하다.

예제: 결측치 비율 확인

# 컬럼별 결측치 비율 (%)
(df.isna().mean() * 100).round(1)
species              0.0
island               0.0
bill_length_mm       0.6
bill_depth_mm        0.6
flipper_length_mm    0.6
body_mass_g          0.6
sex                  3.2
dtype: float64

위 결과를 통해 각 컬럼의 결측치 비율을 백분율로 확인할 수 있다. 비율이 낮은 컬럼은 행 제거를, 비율이 높은 컬럼은 대체 방법을 고려한다.

3.3 결측치 제거 (행 삭제)

결측치가 소수이거나 분석에 큰 영향을 미치지 않는 경우, 해당 행을 삭제하는 것이 가장 간단한 방법이다. 다만, 제거로 인한 데이터 손실이 크지 않은지 반드시 확인해야 한다.

3.3.1 결측치가 있는 모든 행 제거

하나라도 결측치가 있는 행을 모두 제거하는 방법이다. 가장 보수적인 접근이지만 데이터 손실이 클 수 있다.

예제: 전체 결측치 행 제거

# 결측치가 하나라도 있는 행 제거
df_drop_all = df.dropna()

# 제거 후 데이터 크기 확인
print(f"원본 데이터: {df.shape}")
print(f"결측치 제거 후: {df_drop_all.shape}")
원본 데이터: (344, 7)
결측치 제거 후: (333, 7)

3.3.2 결측치가 특정 개수 이상인 행 제거

전체 컬럼 중 일부만 결측치인 경우 해당 행을 유지하고, 결측치가 많은 행만 제거하는 방법이다.

예제: 결측치가 3개 이상인 행 제거

# 각 행의 결측치 개수 계산
df["na_count"] = df.isna().sum(axis=1)

# 결측치가 3개 미만인 행만 유지
df_row_filtered = df[df["na_count"] < 3].drop(columns="na_count")

# 제거 후 데이터 크기 확인
print(f"결측치 필터링 후: {df_row_filtered.shape}")
결측치 필터링 후: (342, 7)

예제: thresh 파라미터를 사용한 행 제거

# 최소 n개의 비결측값이 있는 행만 유지
# 전체 컬럼에서 2개까지 결측치 허용
df_thresh = df.dropna(thresh=len(df.columns) - 2)

print(f"thresh 적용 후: {df_thresh.shape}")
thresh 적용 후: (342, 8)

thresh 파라미터는 행을 유지하기 위해 필요한 최소 비결측값 개수를 지정한다. 예를 들어, 전체 컬럼이 8개인 경우 thresh=6이면 최소 6개의 값이 있어야 행이 유지된다.

3.3.3 특정 컬럼 기준 제거

특정 컬럼의 결측치만 제거하고 싶을 때 사용하는 방법이다. 분석에 필수적인 컬럼의 결측치를 제거할 때 유용하다.

예제: 특정 컬럼 결측치 제거

# sex 컬럼에 결측치가 있는 행 제거
df_drop_sex = df.dropna(subset=["sex"])

print(f"sex 컬럼 결측치 제거 후: {df_drop_sex.shape}")
sex 컬럼 결측치 제거 후: (333, 8)

여러 컬럼을 동시에 지정할 수도 있다.

# 여러 컬럼 중 하나라도 결측치가 있으면 제거
df_drop_multi = df.dropna(subset=["sex", "bill_length_mm"])

df_drop_multi.shape
(333, 8)

3.4 수치형 변수 결측치 대체

결측치를 제거하면 데이터가 손실되므로, 특정 값으로 대체하는 방법을 고려할 수 있다. 수치형 변수는 주로 평균, 중앙값, 최빈값 등의 대푯값으로 대체한다.

3.4.1 평균(mean)으로 대체

평균은 데이터의 중심 경향을 나타내는 대표적인 값이다. 다만, 이상치에 민감하므로 이상치가 많은 경우 중앙값 사용을 권장한다.

예제: 단일 컬럼 평균 대체

# 데이터 복사 (원본 보존)
df_mean = df.copy()

# bill_length_mm 컬럼의 결측치를 평균으로 대체
df_mean["bill_length_mm"] = df_mean["bill_length_mm"].fillna(
    df_mean["bill_length_mm"].mean()
)

# 결측치 확인
print(f'결측치: {df_mean["bill_length_mm"].isna().sum()}')
결측치: 0

예제: 다중 컬럼 평균 대체 (방법 1)

# 수치형 컬럼 목록
num_cols = [
    "bill_length_mm",
    "bill_depth_mm",
    "flipper_length_mm",
    "body_mass_g"
]

df_cols = df.copy()

# 여러 컬럼의 결측치를 각각의 평균으로 대체
df_cols[num_cols] = df_cols[num_cols].fillna(df_cols[num_cols].mean())

# 결측치 확인
print(df_cols[num_cols].isna().sum())
bill_length_mm       0
bill_depth_mm        0
flipper_length_mm    0
body_mass_g          0
dtype: int64

예제: 다중 컬럼 평균 대체 (방법 2 - 반복문 활용)

df_loop = df.copy()

# 반복문을 사용한 평균 대체
for col in num_cols:
    mean_value = df_loop[col].mean()
    df_loop[col] = df_loop[col].fillna(mean_value)

# 결측치 확인
print(df_loop[num_cols].isna().sum())
bill_length_mm       0
bill_depth_mm        0
flipper_length_mm    0
body_mass_g          0
dtype: int64

예제: 그룹별 평균으로 대체

범주형 변수로 그룹을 나눈 후, 각 그룹의 평균으로 결측치를 대체하는 방법이다. 예를 들어, 펭귄 종별로 평균 부리 길이가 다르므로 종별 평균으로 대체하는 것이 더 정확하다.

df_group = df.copy()

# 종(species)별 평균으로 결측치 대체
df_group[num_cols] = df_group.groupby("species")[num_cols].transform(
    lambda x: x.fillna(x.mean())
)

# 결측치 확인
print(df_group[num_cols].isna().sum())
bill_length_mm       0
bill_depth_mm        0
flipper_length_mm    0
body_mass_g          0
dtype: int64

transform() 메서드는 그룹별 계산 결과를 원래 DataFrame의 인덱스에 맞춰 반환하므로 대체 작업에 적합하다.

3.4.2 중앙값(median)으로 대체

중앙값은 이상치의 영향을 받지 않는 강건한(robust) 통계량이다. 데이터 분포가 왜곡되어 있거나 이상치가 존재하는 경우 평균보다 중앙값 대체가 더 적절하다.

예제: 중앙값으로 대체

df_median = df.copy()

# bill_depth_mm 컬럼의 결측치를 중앙값으로 대체
df_median["bill_depth_mm"] = df_median["bill_depth_mm"].fillna(
    df_median["bill_depth_mm"].median()
)

# 결측치 확인
print(f'결측치: {df_median["bill_depth_mm"].isna().sum()}')
결측치: 0

중앙값도 평균과 마찬가지로 그룹별 중앙값으로 대체할 수 있다.

# 종별 중앙값으로 대체
df_group_median = df.copy()
df_group_median[num_cols] = df_group_median.groupby("species")[num_cols].transform(
    lambda x: x.fillna(x.median())
)

3.5 범주형 변수 결측치 대체

범주형 변수는 수치형과 달리 평균이나 중앙값을 사용할 수 없다. 대신 최빈값(mode)으로 대체하거나, 결측치를 별도의 범주로 처리하는 방법을 사용한다.

3.5.1 최빈값(mode)으로 대체

최빈값은 가장 자주 나타나는 값으로, 범주형 변수의 결측치를 대체할 때 일반적으로 사용된다.

예제: 단일 컬럼 최빈값 대체

df_mode = df.copy()

# sex 컬럼의 최빈값 찾기
mode_sex = df_mode["sex"].mode()[0]

# 최빈값으로 결측치 대체
df_mode["sex"] = df_mode["sex"].fillna(mode_sex)

# 결측치 확인
print(f'결측치: {df_mode["sex"].isna().sum()}')
결측치: 0

mode() 메서드는 Series를 반환하므로 [0]을 사용하여 첫 번째 최빈값을 추출한다.

예제: 다중 범주형 컬럼 최빈값 대체

df_mode_multi = df.copy()

# 범주형 컬럼 목록
cat_cols = ["sex", "island"]

# 반복문을 사용한 최빈값 대체
for col in cat_cols:
    mode_value = df_mode_multi[col].mode()[0]
    df_mode_multi[col] = df_mode_multi[col].fillna(mode_value)

# 결측치 확인
print(df_mode_multi[cat_cols].isna().sum())
sex       0
island    0
dtype: int64

예제: 그룹별 최빈값으로 대체

특정 그룹 내에서 가장 빈번한 값으로 대체하는 방법이다.

df_mode_group = df.copy()

# 종별 성별 최빈값으로 대체
df_mode_group["sex"] = df_mode_group.groupby("species")["sex"].transform(
    lambda x: x.fillna(x.mode()[0] if not x.mode().empty else "Unknown")
)

# 결측치 확인
print(f'결측치: {df_mode_group["sex"].isna().sum()}')
결측치: 0

예제: 다중 컬럼 그룹별 최빈값 대체

df_mode_group_multi = df.copy()

cat_cols = ["sex", "island"]

# 여러 범주형 컬럼을 그룹별 최빈값으로 대체
for col in cat_cols:
    df_mode_group_multi[col] = df_mode_group_multi.groupby("species")[col].transform(
        lambda x: x.fillna(x.mode()[0] if not x.mode().empty else "Unknown")
    )

# 결측치 확인
print(df_mode_group_multi[cat_cols].isna().sum())
sex       0
island    0
dtype: int64

3.5.2 명시적 범주 추가

결측치를 “Unknown”, “Missing” 등의 별도 범주로 명시적으로 표시하는 방법이다. 결측치 자체가 의미 있는 정보일 수 있는 경우 유용하다.

예제: 결측치를 별도 범주로 대체

df_category = df.copy()

# sex 컬럼의 결측치를 "Unknown"으로 대체
df_category["sex"] = df_category["sex"].fillna("Unknown")

# 범주 분포 확인
print(df_category["sex"].value_counts())
sex
Male       168
Female     165
Unknown     11
Name: count, dtype: int64

이 방법은 결측치의 패턴이 종속 변수와 관련이 있을 때 특히 유용하다.

3.6 결측치 처리 후 검증

결측치 처리를 완료한 후에는 반드시 결측치가 제대로 처리되었는지 검증해야 한다.

예제: 결측치 처리 후 확인

df_sub_na = df.copy()

# 수치형 결측치 처리
df_sub_na[num_cols] = df_sub_na.groupby('species')[num_cols].transform(lambda x: x.fillna(x.mean()))

# 범주형 결측치 처리
df_sub_na[cat_cols] = df_sub_na.groupby('species')[cat_cols].transform(lambda x: x.fillna(x.mode()[0] if not x.mode().empty else "Unknown"))

# 그룹별 대체를 적용한 데이터 검증
print("결측치 개수:")
print(df_sub_na.isna().sum())
print("\n데이터 정보:")
df_sub_na.info()
결측치 개수:
species              0
island               0
bill_length_mm       0
bill_depth_mm        0
flipper_length_mm    0
body_mass_g          0
sex                  0
na_count             0
dtype: int64

데이터 정보:
<class 'pandas.DataFrame'>
RangeIndex: 344 entries, 0 to 343
Data columns (total 8 columns):
 #   Column             Non-Null Count  Dtype  
---  ------             --------------  -----  
 0   species            344 non-null    str    
 1   island             344 non-null    str    
 2   bill_length_mm     344 non-null    float64
 3   bill_depth_mm      344 non-null    float64
 4   flipper_length_mm  344 non-null    float64
 5   body_mass_g        344 non-null    float64
 6   sex                344 non-null    str    
 7   na_count           344 non-null    int64  
dtypes: float64(4), int64(1), str(3)
memory usage: 21.6 KB

모든 컬럼의 결측치가 0인지 확인하고, 각 컬럼의 데이터 타입과 비결측값 개수가 예상과 일치하는지 점검한다.

3.7 요약

이 장에서는 결측치를 탐지하고 처리하는 다양한 방법을 학습했다. 주요 내용은 다음과 같다.

결측치 처리 방법 비교

처리 방법 적용 대상 장점 단점 사용 상황
행 제거 전체 간단하고 명확함 데이터 손실 결측치 비율 < 5%
평균 대체 수치형 구현이 쉬움 이상치에 민감, 분산 감소 정규분포 데이터
중앙값 대체 수치형 이상치에 강건함 분산 감소 왜곡된 분포, 이상치 존재
최빈값 대체 범주형 자연스러운 대체 불균형 심화 가능 명확한 최빈값 존재
그룹별 대체 전체 그룹 특성 반영 복잡한 구현 그룹 간 차이가 큰 경우
범주 추가 범주형 정보 손실 없음 범주 증가 결측치에 의미가 있는 경우

결측치 처리 권장 순서

  1. 결측치 현황 파악: isna().sum(), isna().mean() 사용
  2. 제거 가능한 행/열 판단: 결측치 비율과 데이터 중요도 고려
  3. 수치형 변수 대체: 그룹별 평균 또는 중앙값 우선 고려
  4. 범주형 변수 대체: 그룹별 최빈값 또는 “Unknown” 범주 추가
  5. 최종 검증: isna().sum(), info()로 처리 결과 확인

결측치 처리는 데이터 분석의 품질을 좌우하는 중요한 과정이다. 데이터의 특성과 분석 목적에 맞는 적절한 방법을 선택해야 하며, 처리 후에는 반드시 결과를 검증해야 한다. 다음 장에서는 이상치 탐지 및 처리 방법을 학습할 것이다.