본문 바로가기

개발 공부/데이터 분석

[데이터 분석] 데이터 클렌징 시작하기 | 결측치 확인·삭제·대체·검증

1. 결측치부터 개수와 비율로 확인하기

missing_count = df.isna().sum().sort_values(ascending=False)
missing_count

isna()는 각 값이 결측치인지 True/False로 표시하고, sum()은 True를 1처럼 세어 컬럼별 결측치 개수를 만든다. sort_values(ascending=False)는 결측치가 많은 컬럼부터 위로 정렬한다.

missing_ratio = (df.isna().mean() * 100).sort_values(ascending=False)
missing_ratio

데이터 크기가 달라지면 개수만으로 심각도를 비교하기 어렵다. isna().mean()은 True의 평균, 즉 결측치 비율을 계산한다. Titanic에서는 deck의 결측 비율이 매우 높고, age는 일부 결측, embarked는 극히 적은 결측을 가진다.

중요 결측치 비율 10%·20%·50% 같은 숫자는 절대 기준이 아니다. 컬럼의 중요성, 결측 원인, 데이터 양, 모델 성능을 함께 고려한다.

 

2. 처리할 컬럼 이름만 골라내기

condition = missing_count > 0
missing_cols = missing_count[condition].index
missing_cols

missing_count는 인덱스가 컬럼명이고 값이 결측치 개수인 Series다.
값이 0보다 큰 항목만 고른 뒤 .index를 사용하면 결측치가 있는 컬럼 이름만 추출할 수 있다.

 

3. 삭제: 행과 열은 다르게 판단한다

df = df.drop(columns=['deck'])

deck처럼 결측 비율이 매우 높고 대체할 근거도 약하면 컬럼 삭제를 검토할 수 있다. columns=는 열을 삭제한다는 의도가 명확하다.

df = df.dropna(subset=['embarked'])

embarked처럼 결측 행이 아주 적고 해당 행을 삭제해도 전체 데이터 손실이 미미하다면 행 삭제도 선택지가 될 수 있다. 다만 행 삭제는 학습 데이터 수를 줄이므로, 특히 결측이 많은 경우 무조건 사용하면 안 된다.

 

4. 수치형 결측치: 평균과 중앙값 대체

age_mean = df['age'].mean()
age_median = df['age'].median()

df['age_mean'] = df['age'].fillna(age_mean)
df['age_median'] = df['age'].fillna(age_median)

방법 언제 검토할까? 장점·주의점
평균 대체 분포가 크게 치우치지 않았을 때 계산이 간단하지만 큰 값의 영향을 받는다.
중앙값 대체 왜도·이상치가 있는 수치형 데이터 극단값에 비교적 강하지만 한 값에 데이터가 몰릴 수 있다.

age처럼 결측이 있는 수치형 데이터는 평균·중앙값으로 대체할 수 있다. 다만 이 방법은 실제 나이를 새로 관측한 것이 아니라 대표값을 넣은 것이므로, 처리 후 분포가 달라지는지 확인해야 한다.

 

5. 범주형 결측치: 최빈값 대체

embarked_mode = df['embarked'].mode()[0]
df['embarked'] = df['embarked'].fillna(embarked_mode)

mode()는 최빈값이 여러 개일 수 있어 Series를 반환한다. [0]은 그 최빈값 목록의 첫 번째 값 하나를 꺼내는 것이다. 원본 컬럼의 첫 번째 범주를 가져오는 것이 아니라, 이미 mode()가 골라낸 최빈값 결과의 첫 번째다.

 

6. 평균 하나로 채우기보다 분포를 유지하는 방법

missing_mask = df['age'].isna()
n_missing = missing_mask.sum()

random_age = df['age'].dropna().sample(
n=n_missing, replace=True, random_state=42
).to_numpy()

df['age_random'] = df['age'].copy()
df.loc[missing_mask, 'age_random'] = random_age

랜덤 샘플링 대체는 결측이 아닌 age 값에서 결측 개수만큼 무작위로 뽑아 결측 위치에 넣는 방법이다. 평균 하나로 채울 때보다 원본의 넓은 분포를 유지하기 쉽다. random_state=42를 지정하면 실행할 때마다 같은 결과가 나와 재현할 수 있다.

주의 랜덤 샘플링도 결측이 무작위로 발생했다는 가정이 필요하다. 결측이 성별·객실 등급·호칭 같은 특정 집단과 관련되어 있다면, 관련 그룹별 중앙값이나 모델 기반 대체가 더 적절할 수 있다.

 

7. 처리 후 반드시 검증하기

df[['age_mean', 'age_median', 'age_random', 'embarked']].isna().sum()

결과가 모두 0이면 선택한 컬럼의 결측치는 사라진 것이다. 하지만 이것만으로 끝나면 부족하다. 처리 전후의 shape, 평균·중앙값·왜도와 분포를 비교해야 한다.

sns.kdeplot(df['age'].dropna(), label='original')
sns.kdeplot(df['age_mean'], label='mean')
sns.kdeplot(df['age_median'], label='median')
sns.kdeplot(df['age_random'], label='random')
plt.legend()

대표값 대체 후 평균 또는 중앙값 부근에 인공적인 봉우리가 생기면, 결측치가 사라졌어도 분포는 왜곡된 것이다. 마지막에는 여러 대체 방법으로 모델을 학습해 교차검증 성능까지 비교하는 것이 가장 확실하다.

 

8. EDA와 데이터 클렌징 체크리스트

  • shape(), head(), tail(), info()로 데이터 구조와 결측치를 확인한다.
  • describe(), value_counts(), unique()로 값의 분포와 의미를 확인한다.
  • 결측치 개수와 비율을 함께 보고, 컬럼·행 삭제 여부를 판단한다.
  • 수치형은 평균·중앙값·그룹별 대표값·랜덤 샘플링을 비교한다.
  • 범주형은 최빈값 또는 별도 “Unknown” 범주를 검토한다.
  • 처리 후 결측치 개수, 분포, 통계값, 모델 성능을 다시 검증한다.
최종 정리 데이터 클렌징의 목표는 빈칸을 0개로 만드는 것이 아니라, 정보 손실과 분포 왜곡을 최소화하면서 모델이 사용할 수 있는 데이터로 만드는 것이다.

 

반응형