데이터 정제

Keywords

python, 파이썬, numpy, 넘파이, 넘피, pandas, 판다스, machine learning, 기계학습, 머신러닝, 회귀, 분류, 군집

결측치 처리

결측치는 누락된 값을 의미한다. 즉 데이터를 정상적으로 입력하지 못한 경우이다.

이런 결측치는 발생 원인과 특성에 따라 크게 3가지로 구분된다.

  • 완전 무작위 결측 (MCAR, Missing Completely At Random)
    • 결측 발생이 어떤 변수와도 관련이 없는 경우
    • 예시: 설문 데이터 입력 중 시스템 오류로 임의의 몇 행이 통째로 누락됨, 사람이 데이터 입력 시 실수로 데이터를 누락하는 경우
  • 무작위 결측 (MAR, Missing At Random)
    • 결측 여부가 다른 관측된 변수와는 관련 있지만, 자기 자신 값과는 직접적 관련이 없는 경우
    • 예시: 고연령층 응답자일수록 소득 항목을 응답하지 않는 경우 (연령은 존재, 소득만 결측), 여성질환 관련 검진 항목을 남성에 대해서는 미기입하는 경우
  • 비무작위 결측 (MNAR, Missing Not At Random)
    • 결측 여부가 해당 변수의 실제 값과 직접적으로 관련된 경우
    • 예시: 소득이 매우 높은 사람이 소득을 의도적으로 응답하지 않음, 우울감이 높을수록 정신건강 관련 설문을 건너뛰는 경우

이런 결측치는 상황에 따라 삭제, 대체, 모델링하여 처리한다.

삭제

palmerpenguins 데이터셋을 이용하여 결측치를 삭제하는 방법을 알아 본다

import pandas as pd
from palmerpenguins import load_penguins

df = load_penguins()
df.head()
species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 Adelie Torgersen 39.1 18.7 181.0 3750.0 male 2007
1 Adelie Torgersen 39.5 17.4 186.0 3800.0 female 2007
2 Adelie Torgersen 40.3 18.0 195.0 3250.0 female 2007
3 Adelie Torgersen NaN NaN NaN NaN NaN 2007
4 Adelie Torgersen 36.7 19.3 193.0 3450.0 female 2007

컬럼별 결측치 개수를 확인한다.

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
year                  0
dtype: int64
df.loc[df.isna().any(axis=1), :].reset_index()
index species island bill_length_mm bill_depth_mm flipper_length_mm body_mass_g sex year
0 3 Adelie Torgersen NaN NaN NaN NaN NaN 2007
1 8 Adelie Torgersen 34.1 18.1 193.0 3475.0 NaN 2007
2 9 Adelie Torgersen 42.0 20.2 190.0 4250.0 NaN 2007
3 10 Adelie Torgersen 37.8 17.1 186.0 3300.0 NaN 2007
4 11 Adelie Torgersen 37.8 17.3 180.0 3700.0 NaN 2007
5 47 Adelie Dream 37.5 18.9 179.0 2975.0 NaN 2007
6 178 Gentoo Biscoe 44.5 14.3 216.0 4100.0 NaN 2007
7 218 Gentoo Biscoe 46.2 14.4 214.0 4650.0 NaN 2008
8 256 Gentoo Biscoe 47.3 13.8 216.0 4725.0 NaN 2009
9 268 Gentoo Biscoe 44.5 15.7 217.0 4875.0 NaN 2009
10 271 Gentoo Biscoe NaN NaN NaN NaN NaN 2009

결측치가 하나라도 있는 행은 삭제한다.

df_dropna = df.dropna()
df_dropna.isna().sum()
species              0
island               0
bill_length_mm       0
bill_depth_mm        0
flipper_length_mm    0
body_mass_g          0
sex                  0
year                 0
dtype: int64

대체

수치형

컬럼이 수치형인 경우 평균값이나 중앙값 또는 특정값으로 대체한다.

df_mean = df.copy()

num_cols = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]

na_index = df.loc[df.loc[:, num_cols].isna().any(axis=1), num_cols].index 
print(df_mean.loc[na_index, num_cols])

list_means = []
for col in num_cols:
    list_means.append(df_mean[col].mean())
    df_mean[col] = df_mean[col].fillna(df_mean[col].mean())

print(df_mean.loc[na_index, num_cols])
print(pd.Series(list_means, index=num_cols).reset_index())
     bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
3               NaN            NaN                NaN          NaN
271             NaN            NaN                NaN          NaN
     bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
3          43.92193       17.15117         200.915205  4201.754386
271        43.92193       17.15117         200.915205  4201.754386
               index            0
0     bill_length_mm    43.921930
1      bill_depth_mm    17.151170
2  flipper_length_mm   200.915205
3        body_mass_g  4201.754386

각 컬럼이 범주로 분류된다면 다음과 같이 처리할 수 있다.

df_mean = df.copy()

num_cols = ["bill_length_mm", "bill_depth_mm", "flipper_length_mm", "body_mass_g"]

na_index = df.loc[df.loc[:, num_cols].isna().any(axis=1), num_cols].index 
print(df_mean.loc[na_index, ['species'] + num_cols])

for col in num_cols:
   df_mean[col] = df_mean.groupby(['species'])[col].transform(lambda x: x.mean())

print(df_mean.loc[na_index, ['species'] + num_cols])
    species  bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
3    Adelie             NaN            NaN                NaN          NaN
271  Gentoo             NaN            NaN                NaN          NaN
    species  bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
3    Adelie       38.791391      18.346358         189.953642  3700.662252
271  Gentoo       47.504878      14.982114         217.186992  5076.016260

범주형

컬럼이 범주형인 경우 최빈값이나 특정 범주를 만들어 대체한다.

import numpy as np

df_mode = df.copy()
num_cols = ["sex"]
df_mode.loc[277, 'sex'] = np.nan 

na_index = df_mode.loc[df_mode.loc[:, num_cols].isna().any(axis=1), num_cols].index 
print(df_mode.loc[na_index, ['species']+num_cols])
       species  sex
3       Adelie  NaN
8       Adelie  NaN
9       Adelie  NaN
10      Adelie  NaN
11      Adelie  NaN
47      Adelie  NaN
178     Gentoo  NaN
218     Gentoo  NaN
256     Gentoo  NaN
268     Gentoo  NaN
271     Gentoo  NaN
277  Chinstrap  NaN
df_mode[df_mode['species']=='Chinstrap']['sex'].value_counts()
sex
female    34
male      33
Name: count, dtype: int64
df_mode[df_mode['species']=='Chinstrap']['sex'].mode()
0    female
Name: sex, dtype: object
list_modes = []
for col in num_cols:
    list_modes.append(df_mode[col].mode()[0])
    df_mode[col] = df_mode[col].fillna(df_mode[col].mode()[0])
    
print(df_mode.loc[na_index, ['species']+num_cols])
print(pd.Series(list_modes, index=num_cols).reset_index())
       species   sex
3       Adelie  male
8       Adelie  male
9       Adelie  male
10      Adelie  male
11      Adelie  male
47      Adelie  male
178     Gentoo  male
218     Gentoo  male
256     Gentoo  male
268     Gentoo  male
271     Gentoo  male
277  Chinstrap  male
  index     0
0   sex  male

위 예제는 전체 컬럼 기준으로 최빈값을 계산 후 결측치를 대체한다. 아래 예제는 범주별 최빈값을 결측치에 대체하는 코드이다.

list_modes = []
for col in num_cols:
    list_modes.append(df_mode[col].mode()[0])
    df_mode[col] = df_mode.groupby(['species'])[col].transform(lambda x: x.mode()[0])
    
print(df_mode.loc[na_index, ['species']+num_cols])
       species     sex
3       Adelie    male
8       Adelie    male
9       Adelie    male
10      Adelie    male
11      Adelie    male
47      Adelie    male
178     Gentoo    male
218     Gentoo    male
256     Gentoo    male
268     Gentoo    male
271     Gentoo    male
277  Chinstrap  female

모델링

import pandas as pd
from palmerpenguins import load_penguins
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor

# 1. 데이터셋 로드
penguins = load_penguins()

# 2. 수치형 데이터 선택
numeric_cols = ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']
df_numeric = penguins[numeric_cols]

# 대체 전 결측치 확인
print("대체 전 결측치 상황:")
print(df_numeric.isnull().sum())
na_index = df_numeric.loc[df_numeric.isna().any(axis=1), :].index
print(df_numeric.loc[na_index, :])


# 3. IterativeImputer 설정
imputer = IterativeImputer(
    estimator=RandomForestRegressor(n_estimators=10, random_state=42),
    max_iter=10,
    random_state=42
)

# 4. 결측치 대체 수행
df_imputed = imputer.fit_transform(df_numeric)

# 5. 결과를 다시 데이터프레임으로 변환
df_final = pd.DataFrame(df_imputed, columns=numeric_cols)

print("\n대체 후 결측치 상황:")
print(df_final.isnull().sum())
print(df_final.loc[na_index, :])

# 원본 데이터와 합치기 (필요한 경우)
penguins[numeric_cols] = df_final
대체 전 결측치 상황:
bill_length_mm       2
bill_depth_mm        2
flipper_length_mm    2
body_mass_g          2
dtype: int64
     bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
3               NaN            NaN                NaN          NaN
271             NaN            NaN                NaN          NaN

대체 후 결측치 상황:
bill_length_mm       0
bill_depth_mm        0
flipper_length_mm    0
body_mass_g          0
dtype: int64
     bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
3             51.89          19.23              205.9       4010.0
271           51.89          19.23              205.9       4010.0

다음은 범주별 모델링을 적용한 예제이다.

import pandas as pd
from sklearn.experimental import enable_iterative_imputer
from sklearn.impute import IterativeImputer
from sklearn.ensemble import RandomForestRegressor

# 1. 데이터셋 로드
penguins = load_penguins()

# 대체에 사용할 수치형 컬럼 정의
numeric_cols = ['bill_length_mm', 'bill_depth_mm', 'flipper_length_mm', 'body_mass_g']

# 2. 그룹별 대체를 수행할 함수 정의
def impute_by_group(group):
    # 각 그룹 내에서 수치형 데이터만 추출
    group_numeric = group[numeric_cols]
    
    # 모델 기반 대치기 설정 (RandomForest 사용)
    imputer = IterativeImputer(
        estimator=RandomForestRegressor(n_estimators=10, random_state=42),
        max_iter=10,
        random_state=42
    )
    
    # 데이터가 모두 결측치인 경우 등을 대비해 예외 처리 후 대치
    if group_numeric.isnull().all().all():
        return group
    
    imputed_values = imputer.fit_transform(group_numeric)
    group[numeric_cols] = imputed_values
    return group

# 3. species 컬럼을 기준으로 그룹화하여 함수 적용
# 성별(sex)에 결측치가 있는 경우도 있으므로 전체 데이터프레임 유지
penguins_imputed = penguins.groupby('species', group_keys=False).apply(impute_by_group)

# 4. 결과 확인
print("종별 대체 후 결측치 현황:")
print(penguins_imputed[numeric_cols].isnull().sum())
na_index = df_numeric.loc[df_numeric.isna().any(axis=1), :].index
print(penguins_imputed.loc[na_index, :])

# 특정 종의 결과 예시 출력 (Adelie)
print("\nAdelie 종의 요약 통계량 (대체 후):")
print(penguins_imputed[penguins_imputed['species'] == 'Adelie'][numeric_cols].describe())
종별 대체 후 결측치 현황:
bill_length_mm       0
bill_depth_mm        0
flipper_length_mm    0
body_mass_g          0
dtype: int64
    species     island  bill_length_mm  bill_depth_mm  flipper_length_mm  \
3    Adelie  Torgersen           37.25          18.06              184.5   
271  Gentoo     Biscoe           50.91          15.84              225.0   

     body_mass_g  sex  year  
3         3715.0  NaN  2007  
271       5330.0  NaN  2009  

Adelie 종의 요약 통계량 (대체 후):
       bill_length_mm  bill_depth_mm  flipper_length_mm  body_mass_g
count      152.000000     152.000000         152.000000   152.000000
mean        38.781250      18.344474         189.917763  3700.756579
std          2.657513       1.212837           6.532761   457.046652
min         32.100000      15.500000         172.000000  2850.000000
25%         36.775000      17.500000         185.750000  3350.000000
50%         38.800000      18.400000         190.000000  3700.000000
75%         40.725000      19.000000         195.000000  4000.000000
max         46.000000      21.500000         210.000000  4775.000000

이상치 처리

이상치는 데이터 패턴이나 범위를 크게 벗어난 값을 의미한다. 이러한 이상치는 다음과 같은 방법으로 탐지가 가능하다. 탐지된 이상치는 상황 및 모델 특성에 따라 삭제, 대체, 변환 등으로 처리한다.

  • Tukey Fence: 통계 기반 IQR을 이용하여 탐지
  • Local Outlier Factor: 주변 이웃 밀도 기반 이상 탐지
  • Isolation Forest: 결정 트리 기반 이상 탐지
  • 시계열 데이터: rolling 함수, decomposition

Tukey Fence

Local Outlider Factor

Isolation Forest