본문 바로가기
개발/Python

Pandas 전처리 - 결측치 처리

by 피로물든딸기 2025. 8. 10.
반응형

전체 링크

 

결측치는 여러 방법으로 처리가 가능하다.

 

- 제거
- 평균, 중앙값, 최빈값, 상수값 대체

- Hot Deck : 비슷한 특성을 가진 다른 관측값에서 무작위로 대체

- Regression : 다른 변수로 회귀 예측하여 대체 (LinearRegression)

- K-NN : K 최근접 이웃 알고리즘으로 결측치 주변 데이터를 평균값으로 대체 (KNNImputer)

- Interpolation : 보간법 (interpolate())

- EM Algorithm : 확률 기반 반복추정으로 대체 (statsmodels, fancyimpute)

- Multiple imputation (MICE) : 결측치를 여러 번 대체 수행하여 결측치의 불확실성을 반영 (IterativeImputer)

 

다중대치(Multiple Imputation, MI)

- 여러 개의 plausible(가능한) 값으로 여러 번 대치하여 불확실성을 반영하는 방법

- 추정값의 분산을 적절히 증가

- 유의수준과 신뢰구간을 더 정확하게 반영

- 대치 단계 (Imputation) : 결측치를 예측 모델로 여러 개(M번) 대치하여 M개의 완성 데이터셋 생성

- 분석 단계 (Analysis) : 각 데이터셋을 동일한 통계 분석법으로 분석

- 결합 단계 (Pooling) : 각 분석 결과를 Rubin의 공식으로 결합하여 최종 결과 도출


결측치 유형

 

MCAR (Missing Completely At Random, 완전히 무작위 누락)

- 누락이 데이터의 어떤 특성과도 관련 없음

- 설문자가 동전을 던져 앞면이 나오면 특정 질문을 무조건 건너뜀

- 삭제해도 편향 없음, 데이터가 줄어 효율성(표본 수) 감소

 

MAR (Missing At Random, 조건부 무작위 누락)

- 누락 여부가 관측된 다른 변수들에는 의존하지만, 누락된 값 자체와는 무관

- 결혼 여부 목록에 미혼인 경우는 자녀 항목을 건너뛰는 경우

- 적절한 모델링 / 대체 가능. 삭제도 가능하지만 주의 필요 (관측된 변수로 보정 가능할 때)

 

MNAR (Missing Not At Random, 비무작위 누락)

- 누락 여부가 누락된 값 자체 또는 관측되지 않은 정보에 의존

- 자녀 항목이 누락되었는데, 결혼 여부 항목이 없어서 원인을 설명할 수 없음

- 삭제하면 심각한 편향 발생 (누락 자체가 정보)


예제

- 특정 원소 결측치 확인

- 결측치가 존재하는 행 삭제

- 결측치가 N개 이상인 행 삭제

- 결측치의 비중이 N% 이상인 행 삭제

- 그룹별 평균값 대체

- 이전, 이후 값으로 대체


예제 0

 

다음 데이터에서 특정 원소 하나만 결측치인지 확인해보자.

import pandas as pd
import numpy as np

# 예시 DataFrame
df = pd.DataFrame({
    'A': [1, 2, np.nan],
    'B': [4, np.nan, 6]
})

df


특정 원소 하나가 결측치인지 확인하는 방법 DataFrame의 isna() (= isnull())에 원소를 넣어서 확인하면 된다.

pd.isna(df.loc[2, 'A']) # True

예제 1

 

아래 데이터에서 결측치가 하나 이상 존재하는 행을 삭제하라.

import pandas as pd
import numpy as np

np.random.seed(1234)

rows = 10
cols = 2
data = np.random.randn(rows, cols)

# 20% 확률로 결측치 삽입
mask = np.random.rand(rows, cols) < 0.2
data[mask] = np.nan

# DataFrame 생성
df = pd.DataFrame(data, columns=[f"X_{i}" for i in range(cols)])
df


dropna()로 쉽게 해결할 수 있다.

df_na = df.dropna()
df_na


예제 2

 

결측값을 가지는 컬럼의 개수를 행 별로 집계하고, 결측 컬럼이 2개 이하인 행만 남겨라.

import pandas as pd
import numpy as np

np.random.seed(1234)

rows = 100
cols = 5
data = np.random.randn(rows, cols)

# 30% 확률로 결측치 삽입
mask = np.random.rand(rows, cols) < 0.3
data[mask] = np.nan

# DataFrame 생성
df = pd.DataFrame(data, columns=[f"X_{i}" for i in range(cols)])
df


isnull()을 실행하고 행 별(axis=1)sum()을 하면 각 행 별 결측치의 개수를 알 수 있다.

df.isnull().sum(axis=1)

 

개수가 2보다 작은 행만 남기면 17개의 행이 삭제된다.

cnt = df.isnull().sum(axis=1)
df_del = df[cnt <= 2].copy()
df_del


예제 3

 

각 컬럼별로 결측치의 비중을 구하고, 결측치의 비중이 30%를 초과하는 컬럼을 제거하라.

5개의 column이 있는 100개의 데이터에 아래와 같은 결측치가 있다고 가정한다.

import pandas as pd
import numpy as np

np.random.seed(1234)

rows = 100
cols = 5
data = np.random.randn(rows, cols)

# 30% 확률로 결측치 삽입
mask = np.random.rand(rows, cols) < 0.3
data[mask] = np.nan

# DataFrame 생성
df = pd.DataFrame(data, columns=[f"X_{i}" for i in range(cols)])
df


결측치의 비중은 isnull()과 mean()을 이용하여서 구할 수 있다.

결측치의 비중이 30%를 초과하는 컬럼X_0, X_3인 것을 알 수 있다.

df.isnull().mean().reset_index(name='val')

 

위의 데이터 프레임을 tmp라고 하고, 비중이 30% 이하인 컬럼(=index)만 list 형태로 가져온다.

tmp = df.isnull().mean().reset_index(name='val')
cols = tmp[tmp['val'] <= 0.3]['index'].tolist()
cols

 

결측치가 30% 이하인 컬럼만 원본 데이터 프레임에서 가져오면, 30%를 초과하는 컬럼은 사라지게 된다.

출력 결과 X_0, X_3 컬럼이 제거되었다.

df_del = df[cols]
df_del


예제 4

 

아래 데이터에 대해 결측치를 제외한 각 그룹별 평균값으로 결측치를 대체하라.

import pandas as pd
import numpy as np

# 예시 데이터 생성
df = pd.DataFrame({
    'Group': ['A', 'B', 'A', 'B', 'A', 'B', 'C', 'C', 'A', 'B', 'C'],
    'Value': [1.0, np.nan, 3.0, 4.0, np.nan, np.nan, 7.0, 8.0, 1.0, 4.0, 7.0]
})

df


groupby를 이용해서 각 그룹별 Value의 평균을 구하면 다음과 같다.

df_g = df.groupby('Group')['Value'].mean()
df_g

 

하지만 이 결과를 결측치로 대체하긴 까다롭다.

대신 transform을 이용하면 각 행별로 A, B, C의 평균값을 얻을 수 있다.

df_g = df.groupby('Group')['Value'].transform('mean')
df_g

 

이제 fillna()를 이용해 결측치를 처리하면 된다. 

비교를 위해 'Value_filled' 컬럼을 만들어서 비교하였다.

결측치가 아닌 행원본 값으로 나오고, 결측치였던 값그룹별 평균값으로 대체되었다.

df['Value_filled'] = df['Value'].fillna(df_g)
df


예제 5

 

1. 다음 데이터에 대해 이전의 행 중 마지막으로 결측이 아닌 데이터로 결측치를 대체하라.

2. 다음 데이터에 대해 이후의 행 중 처음으로 결측이 아닌 데이터로 결측치를 대체하라.

import pandas as pd
import numpy as np

data = {
    'A': [np.nan, 2, np.nan, 4],
    'B': [1, np.nan, 3, np.nan],
    'C': [np.nan, 1, 2, np.nan]
}

df = pd.DataFrame(data)

df


1. fillna에서 methodffill로 선택하면 된다.

# forward fill : 이전 값을 가져와서 결측값 채움
df = df.fillna(method='ffill')
df

 

2. fillna에서 method를 bfill로 선택하면 된다.

# backward fill : 다음 값을 가져와서 결측값 채움
df = df.fillna(method='bfill')
df


다음 데이터에 대해 ID 별로 선형 보간법을 이용하여 결측치를 대체하라

import pandas as pd
import numpy as np

np.random.seed(1234)

df = pd.DataFrame({
    'ID': np.random.randint(0, 10, size=100),
    'Value': np.random.randn(100)  # 임의의 값
})

n_missing = int(0.2 * df.shape[0])
missing_indices = np.random.choice(df.index, n_missing, replace=False)
df.loc[missing_indices, 'Value'] = np.nan

df


ID별로 데이터를 필터링하고, interpolate()을 이용해서 해결할 수 있다.

df_new = pd.DataFrame()

for _id in df['ID'].unique():
    tmp = df[df['ID'] == _id].copy() # id별 데이터
    tmp['interpolate'] = tmp['Value'].interpolate(limit_direction='both') # 선형 보간법 적용
    df_new = pd.concat([df_new, tmp], axis=0)

df_new

 

ID가 4인 경우는 아래와 같이 데이터가 변경되었다.

df_new[df_new['ID'] == 4]

반응형

댓글