Dealing with NA-01

Page content

강의 홍보

공지

제 수업을 듣는 사람들이 계속적으로 실습할 수 있도록 강의 파일을 만들었습니다. 늘 도움이 되기를 바라며. 참고했던 교재 및 Reference는 꼭 확인하셔서 교재 구매 또는 관련 Reference를 확인하시기를 바랍니다.

I. Overview

실제 데이터를 다루다보면 여러가지 이유로 결측치와 마주하게 된다. 특별한 이유가 없다면, 현업에서는 당연히 NA를 처리해야 한다. 그렇지 않다면 데이터 분석(시각화, 통계, 머신러닝 모형)에 영향을 줄 수 밖에 없다.

원인은 크게 3가지로 구분될 수 있지만, 이러한 주제는 보통 논문을 통해서 다뤄지니, 여기에서는 일단 건너뛰자. (You like theory?)

질문, 어떻게 처리해야 할까? (결국 이런걸 원하는 것이니!)

참고로, 여기에서는 Module 설치 등은 다루지 않으며, 데이터는 Scikit-learnCalifornia Housing Dataset을 참고했다.

방법 1. 아무것도 하지 않는다!

굉장히 편한 방법이다. 그런데, 이 방법론을 쓰려면 알고리즘을 잘 선택해야 한다. XGBoost와 같은 알고리즘은 NA값에 대해 대체할 만한 가장 최적의 것으로 대체하며 학습하기도 하지만, 일반적으로 선형회귀모형은 결과가 도출되지 않는다.

방법 2. 평균 또는 중간값으로 대체

Numeric 데이터에만 적용이 가능하고, 각 Column마다 독립적으로 적용을 한다.

  • 장점: 쉽고 빠르고, small 데이터에 적용이 가능하다.
  • 단점 (오역 방지차 원문을 그대로 인용)
    • Doesn’t factor the correlations between features. It only works on the column level.
    • Will give poor results on encoded categorical features (do NOT use it on categorical features).
    • Not very accurate
    • Doesn’t account for the uncertainty in the imputations
from sklearn.datasets import fetch_california_housing
from sklearn.linear_model import LinearRegression
from sklearn.model_selection import StratifiedKFold
from sklearn.metrics import mean_squared_error
from math import sqrt
import random
import numpy as np
random.seed(20200502)

(1) Fetching 데이터

데이터를 가져와서 확인해보자.

import pandas as pd
from tabulate import tabulate

dataset = fetch_california_housing()
train, target = pd.DataFrame(dataset.data), pd.DataFrame(dataset.target)
train_columns = ['0', '1', '2', '3', '4', '5', '6' '7']
train.insert(loc=len(train_columns), column='target', value=target)

print(tabulate(train.head(5), tablefmt='pipe', headers='keys'))
|    |      0 |   1 |       2 |       3 |    4 |       5 |     6 |   target |       7 |
|---:|-------:|----:|--------:|--------:|-----:|--------:|------:|---------:|--------:|
|  0 | 8.3252 |  41 | 6.98413 | 1.02381 |  322 | 2.55556 | 37.88 |    4.526 | -122.23 |
|  1 | 8.3014 |  21 | 6.23814 | 0.97188 | 2401 | 2.10984 | 37.86 |    3.585 | -122.22 |
|  2 | 7.2574 |  52 | 8.28814 | 1.07345 |  496 | 2.80226 | 37.85 |    3.521 | -122.24 |
|  3 | 5.6431 |  52 | 5.81735 | 1.07306 |  558 | 2.54795 | 37.85 |    3.413 | -122.25 |
|  4 | 3.8462 |  52 | 6.28185 | 1.08108 |  565 | 2.18147 | 37.85 |    3.422 | -122.25 |

(2) NA값 임의 대체

실험을 위해 첫번째 Column에 약 40%에 해당하는 Column을 부여한다.

column = train[0] # 20640
missing_pct = int(column.size * 0.4) # 8256
i = [random.choice(range(column.shape[0])) for _ in range(missing_pct)]
print(i[0:5])
[17455, 4645, 7718, 11453, 7913]
column[i]=np.NaN
column.head(10)
0    8.3252
1    8.3014
2       NaN
3       NaN
4       NaN
5       NaN
6       NaN
7       NaN
8    2.0804
9       NaN
Name: 0, dtype: float64

(3) 평균값 대체 (Scikit-learn) 활용

Scikit-learn 모듈을 활용해서 결측값을 대체해보자.

from sklearn.impute import SimpleImputer
imp_mean = SimpleImputer(strategy='mean')
imputed_DF = pd.DataFrame(imp_mean.fit_transform(train))

print(tabulate(imputed_DF.head(10), tablefmt='pipe', headers='keys'))
|    |       0 |   1 |       2 |        3 |    4 |       5 |     6 |     7 |       8 |
|---:|--------:|----:|--------:|---------:|-----:|--------:|------:|------:|--------:|
|  0 | 8.3252  |  41 | 6.98413 | 1.02381  |  322 | 2.55556 | 37.88 | 4.526 | -122.23 |
|  1 | 8.3014  |  21 | 6.23814 | 0.97188  | 2401 | 2.10984 | 37.86 | 3.585 | -122.22 |
|  2 | 3.86937 |  52 | 8.28814 | 1.07345  |  496 | 2.80226 | 37.85 | 3.521 | -122.24 |
|  3 | 3.86937 |  52 | 5.81735 | 1.07306  |  558 | 2.54795 | 37.85 | 3.413 | -122.25 |
|  4 | 3.86937 |  52 | 6.28185 | 1.08108  |  565 | 2.18147 | 37.85 | 3.422 | -122.25 |
|  5 | 3.86937 |  52 | 4.76166 | 1.10363  |  413 | 2.1399  | 37.85 | 2.697 | -122.25 |
|  6 | 3.86937 |  52 | 4.93191 | 0.951362 | 1094 | 2.1284  | 37.84 | 2.992 | -122.25 |
|  7 | 3.86937 |  52 | 4.79753 | 1.06182  | 1157 | 1.78825 | 37.84 | 2.414 | -122.25 |
|  8 | 2.0804  |  42 | 4.29412 | 1.11765  | 1206 | 2.02689 | 37.84 | 2.267 | -122.26 |
|  9 | 3.86937 |  52 | 4.97059 | 0.990196 | 1551 | 2.17227 | 37.84 | 2.611 | -122.25 |

첫번째 행의 값을 보면, NaN3.869374 형태로 바뀐 것을 볼 수가 있다.

만약, 바로 머신러닝 모형에 적용한다면, 2D array 형태로 출력해야 되면, 다음과 같은 코드를 유지하면 된다.

imp_mean = SimpleImputer(strategy='mean')
imp_mean.fit(train)
imputed_train_df = imp_mean.transform(train)

for i in range(len(imputed_train_df)):
  if i < 10:
    print(i, "-", imputed_train_df[i])
0 - [   8.3252       41.            6.98412698    1.02380952  322.
    2.55555556   37.88          4.526      -122.23      ]
1 - [ 8.30140000e+00  2.10000000e+01  6.23813708e+00  9.71880492e-01
  2.40100000e+03  2.10984183e+00  3.78600000e+01  3.58500000e+00
 -1.22220000e+02]
2 - [   3.86937366   52.            8.28813559    1.07344633  496.
    2.80225989   37.85          3.521      -122.24      ]
3 - [   3.86937366   52.            5.8173516     1.07305936  558.
    2.54794521   37.85          3.413      -122.25      ]
4 - [   3.86937366   52.            6.28185328    1.08108108  565.
    2.18146718   37.85          3.422      -122.25      ]
5 - [   3.86937366   52.            4.76165803    1.10362694  413.
    2.13989637   37.85          2.697      -122.25      ]
6 - [ 3.86937366e+00  5.20000000e+01  4.93190661e+00  9.51361868e-01
  1.09400000e+03  2.12840467e+00  3.78400000e+01  2.99200000e+00
 -1.22250000e+02]
7 - [ 3.86937366e+00  5.20000000e+01  4.79752705e+00  1.06182380e+00
  1.15700000e+03  1.78825348e+00  3.78400000e+01  2.41400000e+00
 -1.22250000e+02]
8 - [ 2.08040000e+00  4.20000000e+01  4.29411765e+00  1.11764706e+00
  1.20600000e+03  2.02689076e+00  3.78400000e+01  2.26700000e+00
 -1.22260000e+02]
9 - [ 3.86937366e+00  5.20000000e+01  4.97058824e+00  9.90196078e-01
  1.55100000e+03  2.17226891e+00  3.78400000e+01  2.61100000e+00
 -1.22250000e+02]

방법 3. 최빈값 대체

최빈값은, 특정 Column에서 가장 많이 나타나는 값으로 대체하는 것이다. 특히 이 방법은 categorical features를 다룰 때 사용한다. 그러나, 데이터에 자칫 편향성을 가져다 줄 수 있다.

column = train[1] # 20640
missing_pct = int(column.size * 0.4) # 8256
i = [random.choice(range(column.shape[0])) for _ in range(missing_pct)]
column[i]=np.NaN
column.head(10)
0    41.0
1    21.0
2    52.0
3     NaN
4     NaN
5     NaN
6     NaN
7    52.0
8    42.0
9    52.0
Name: 1, dtype: float64
from sklearn.impute import SimpleImputer
imp_mean = SimpleImputer(strategy='most_frequent')
imputed_DF = pd.DataFrame(imp_mean.fit_transform(train))

print(tabulate(imputed_DF.head(10), tablefmt='pipe', headers='keys'))
|    |      0 |   1 |       2 |        3 |    4 |       5 |     6 |     7 |       8 |
|---:|-------:|----:|--------:|---------:|-----:|--------:|------:|------:|--------:|
|  0 | 8.3252 |  41 | 6.98413 | 1.02381  |  322 | 2.55556 | 37.88 | 4.526 | -122.23 |
|  1 | 8.3014 |  21 | 5       | 0.97188  | 2401 | 2.10984 | 37.86 | 3.585 | -122.22 |
|  2 | 3.125  |  52 | 8.28814 | 1.07345  |  496 | 2.80226 | 37.85 | 3.521 | -122.24 |
|  3 | 3.125  |  52 | 5.81735 | 1.07306  |  558 | 2.54795 | 37.85 | 3.413 | -122.25 |
|  4 | 3.125  |  52 | 5       | 1.08108  |  565 | 2.18147 | 37.85 | 3.422 | -122.25 |
|  5 | 3.125  |  52 | 5       | 1.10363  |  413 | 2.1399  | 37.85 | 2.697 | -122.25 |
|  6 | 3.125  |  52 | 4.93191 | 0.951362 | 1094 | 2.1284  | 37.84 | 2.992 | -122.25 |
|  7 | 3.125  |  52 | 5       | 1.06182  | 1157 | 1.78825 | 37.84 | 2.414 | -122.25 |
|  8 | 2.0804 |  42 | 5       | 1.11765  | 1206 | 2.02689 | 37.84 | 2.267 | -122.26 |
|  9 | 3.125  |  52 | 5       | 0.990196 | 1551 | 2.17227 | 37.84 | 2.611 | -122.25 |

1 Column을 보면 알겠지만, 모두 가장 빈번하게 나온 52로 대체가 된 것을 확인할 수 있다.

결론

결측치 처리에 대해 잠깐 다뤘다. 각각의 방법론에는 모두 장단점이 있기 때문에 신중을 기해야 하며, 특히, numeric feature에 적용해야 할 방법과 categorical feature에 적용해야 할 방법에 대해 구분 지어서 생각을 해야 한다.

다음 시간에는 통계적 기법을 활용한 결측치 대체에 대해 포스팅을 하도록 하겠다. 작은 도움이 되기를 바란다.