다양한 데이터 전처리

ROK·2022년 1월 9일
0

AIFFEL

목록 보기
6/17

전처리가 중요한 이유

모델에 데이터를 넣기 전까지 과정, 데이터 전처리를 알아보려고 한다.

"데이터 분석의 8할은 데이터 전처리이다"라는 말이 있다.
왜 데이터 분석에 있어서 전처리는 중요한 것일까??

전처리에 따라서 데이터 분석의 질이 달라지기 때문이다. 전처리가 충분히 되어있지 않거나 잘못된 데이터를 사용한 경우 분석 결과의 신뢰도가 떨어지고, 예측 모델의 정확도도 떨어질 것이기 때문이다. 전처리가 잘 되어 있는 겅유 데이터 분석의 질이 높아지고 예측 모델의 성능을 높일 수 있다.

데이터 준비

trade.csv
위 파일은 관세청 수출입 무역 통계에서 가공한 데이터이다.

특정 국가에 대한 월별 수출입 건수와 금액(단위:천 불(USD 1,000))이 있다.
무역수지는 수출금액 - 수입금액이 된다.

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import os

csv_file_path = "trade.csv"
trade = pd.read_csv(csv_file_path)
trade.head()

결측치(Missing Data)

결측치란 말 그대로 데이터에 값이 없는 것을 뜻한다.
현실에서 다룰 데이터는 결측치를 포함하고 있는 경우가 많다. 물론 데이터를 수집하는 과정에서 누락되지 않도록 하는 것이 더 좋은 방법이지만 이미 결측치가 존재한다면 이를 처리해 줘야 한다.

결측치를 처리하는 방법

  • 결측치가 있는 데이터를 제거한다
  • 결측치를 어떤 값으로 대체한다
    • 결측치를 대체하는 방법은 다양한데 데이터마다 특성을 반영하여 해결해야 한다.

우선 결측치 여부를 먼저 살펴본다

print('전체 데이터 건수 : ', len(trade))

전체 데이터 건수에서 각 컬럼별 값이 있는 데이터 수를 빼주면 컬럼별 결측치의 개수를 알 수 있다.

print('컬럼별 결측치 개수')
len(trade) - trade.count()

결과 값을 보면 기타사항이 있는데 이는 전부 결측치이다. 이는 아무런 정보도 없는 칼럼이므로 삭제한다.

trade = trade.drop('기타사항', axis=1)
trade.head()

기타사항 컬럼이 사라진 것을 알 수 있다.

이제 결측치가 있는 행을 살펴보자
DataFrame.isnull()은 데이터마다 결측치 여부를 True, False로 반환한다. DataFrame.any(axis=1)는 행마다 하나라도 True가 있으면 True, 아니면 False를 반환한다.

두 메서드를 조합하여 결측치가 하나라도 있는 행을 찾아본다

DataFrame에 isnull()을 적용하고, 여기에 any(axis=1) 메서드를 적용한다.
이 코드에 따라 각 행이 결측치가 하나라도 있는지의 여부를 불리언 값으로 가진 Series가 출력된다.

trade.isnull()
trade.isnull().any(axis=1)

trade.isnull().any(axis=1)을 다시 DataFrame에 넣어주면 값이 True인 데이터만 추출해준다.

trade[trade.isnull().any(axis=1)]

결과 값을 확인하면 index191 데이터는 수출금액과 무역수지 컬럼이 빠져있고, index196, 197, 198은 기간 국가명을 제외하고 모두 결측치임을 알 수 있다.
이러한 경우에 index191 데이터는 삭제하기보다 특정 값으로 대체하는 것이 좋다.
반면 index196, 197, 198은 제거하는 것이 바람직하다.

우선 삭제를 먼저 진행한다.
DataFrame의 dropna는 결측치를 삭제해 주는 메서드이다. subset옵션으로 특정 컬럼들을 선택했다. how 옵션으로 선택한 컬럼 전부가 결측치인 행을 삭제하겠다는 의미로 'all'을 선택한다.('any'는 하나라도 결측지인 경우) inplace옵션으로 해당 DataFrame 내부에 바로 적용시켰다.

trade.dropna(how='all', subset=['수출건수', '수출금액', '수입건수', '수입금액', '무역수지'], inplace=True)
print("삭제 완료")
trade[trade.isnull().any(axis=1)]

삭제완료가 출력된 후 다시 DataFrame을 확인하면 삭제가 진행된 것을 확인할 수 있다.

수치형 데이터를 보완할 방법

  • 특정값을 지정해준다. 단, 결측치가 많은 경우, 모두 같은 값으로 대체한다면 데이터의 분산이 실제보다 작아지는 문제가 생길 수 있다.
  • 평균, 중앙값 등으로 대체할 수 있다. 앞에서 같이 결측치가 많은 경우 데이터 분산이 실제보다 작아지는 문제가 발생할 수 있다.
  • 다른 데이터를 이용해 예측값으로 대체할 수 있다. 예를 들어 머신러닝 모델로 2020년 4월 미국의 예측값을 만들고, 이 예측값으로 결측치를 보완한다.
  • 시계열 특성을 가진 데이터의 경우 앞뒤 데이터를 통해 결측치를 대체할 수 있다. 예를 들어 기온을 측정하는 센서 데이터에서 결측치가 발생할 경우, 전후 데이터의 평균으로 보완할 수 있다.

네 번째 방법을 통해 보완하도록 한다.
DataFrame.loc[행 라벨, 열 라벨]을 입력하면 해당 라벨을 가진 데이터를 출력해 준다.

trade.loc[[188,191,194]]

미국의 2020년 3월, 4월, 5월 데이터가 출력

index 191의 수출금액 컬럼값을 이전 달과 다음 달의 평균으로 채우고, 무역수지 컬럼은 수출금액과 수입금액의 차이를 이용하여 채운다

trade.loc[191, '수출금액'] = (trade.loc[188, '수출금액'] + trade.loc[194, '수출금액']) / 2
trade.loc[[191]]

trade.loc[191, '무역수지'] = trade.loc[188, '수출금액'] - trade.loc[194, '수입금액']
trade.loc[[191]]

중복된 데이터

데이터를 수집하는 과정에서 중복된 데이터가 생길 수 있다. 같은 값을 가진 데이터 없이 행별로 값이 유일해야 한다면 중복된 데이터를 제거해야 한다.

중복된 데이터 확인하기, DataFrame.duplicated()는 중복된 데이터 여부를 불리언 값으로 반환해준다.

trade.duplicated()

trade[trade.duplicated()]

trade[(trade['기간']=='2020년 03월')&(trade['국가명']=='중국')]

index 186, 187이 중복되어 있다.

pandas에서 DataFrame.drop_duplicates를 통해 중복된 데이터를 손쉽게 삭제할 수 있다.

trade.drop_duplicates(inplace=True)
print("삭제 완료")

trade[trade.duplicated()]

DataFrame.drop_duplicates를 간단한 예시를 통해 조금 더 자세히 알아보자

다음과 같이 id와 name을 컬럼으로 갖는 df가 있다고 하면

df = pd.DataFrame({'id':['001', '002', '003', '004', '002'], 'name':['Park Yun', 'Kim Sung', 'Park Jin', 'Lee Han', 'Kim Min']})

df

id가 002인 데이터가 2개가 있다. id는 사람마다 고유하다고 할 때, 둘 중 하나는 삭제해야 한다.
index가 클수록 나중에 들어온 데이터이고, 사용자가 이름을 수정했을 때 업데이트 되지않고 삽입이 되어 생긴 문제라고 가정하면, 나중에 들어온 값을 남겨야 한다.

DataFrame.drop_duplicatessubset, keep 옵션을 통해 손쉽게 중복을 제거할 수 있다. (DataFrame.drop_duplicates 참조 pandas)

df.drop_duplicates(subset=['id'], keep='last')

위 코드를 실행하면 앞의 002 데이터는 삭제되고 뒤에 수정된 002 데이터만 남은 것을 볼 수 있다.

이상치(Outlier)

이상치란 말그대로 이상(문제)이 있는 데이터를 의미한다. 일반적인 데이터 패턴과 다르게 매우 이상한 패턴을 가지고 있는 등의 데이터를 뜻한다.

MinMaxScaling : 각 feature들에서 최소값을 빼고, 전체 범위로 나눈다(전체 범위는 데이터 최대값에서 최소값을 뺀 값) 계산된 값은 0과 1사이의 범위로 축소된다.
Y=XXminXmaxXminY = \frac{X - X_{min}}{X_{max} - X_{min}}

머신러닝과 딥러닝에서는 이러한 이상치 데이터 때문에 모델의 성능이 크게 좌우된다.
모델은 데이터 패턴을 분석하는데 이상치 데이터로 인해 이상한 패턴을 학습하게 되기 때문에 이상치를 탐색하고 제거하는 것은 중요하다.

이상치를 찾는 방법 중 간단하고 자주 사용되는 방법은 평균과 표준편차를 이용하는 z score 방법이다.

평균을 빼주고 표준편차로 나눠 z score(Xμσ{\frac {X-\mu }{\sigma }})를 계산한다. 그리고 z score가 특정 기준을 넘어서는 데이터에 대해 이상치라고 판단한다. 기준을 작게하면 이상치라고 판단하는 데이터가 많아지고, 기준을 크게하면 이상치라고 판단하는 데이터가 적어진다.

이상치를 판단한 후 처리 방법

  • 가장 간단한 방법으로 이상치 삭제, 원래 데이터에서 삭제하고 이상치끼리 따로 분석하는 방법
  • 이상치를 다른 값으로 대체, 데이터가 적으면 이상치를 삭제하기보다 다른 값으로 대체하는 것이 나을 수 있다. 예를 들어 최댓값, 최솟값을 설정해 데이터의 범위를 제한할 수 있다.
  • 결측치와 마찬가지로 다른 데이터를 활용하여 예측 모델을 만들어 예측값을 활용할 수도 있다.
  • binning을 통해 수치형 데이터를 범주형으로 바꾼다.

z-score method

데이터 처리, 이상치인 데이터의 인덱스를 리턴하는 outlier라는 함수를 만들고, 데이터프레임 df, 컬럼 col, 기준z를 인풋으로 받는다

def outlier(df, col, z):
	return df[abs(df[col] - np.mean(df[col]))/np.std(df[col]) > z].index
  • abs(df[col] - np.mean(df[col])) : 데이터에서 평균을 빼준 것에 절대값을 취한다.
  • abs(df[col] - np.mean(df[col]))/np.std(df[col]) : 위 작업을 표준편차로 나눠준다
  • df[abs(df[col] - np.mean(df[col]))/np.std(df[col]) > z].index : 값이 Z보다 큰 데이터의 인덱스를 추출한다.
trade.loc[outlier(trade, '무역수지', 1.5)]

trade.loc[outlier(trade, '무역수지', 2)]

trade.loc[outlier(trade, '무역수지', 3)]

무역수지의 이상치를 확인하는데 기준 되는 값이 클 수록 이상치가 적어지는 것을 볼 수 있다.

not_outlier라는 함수를 통해 무역수지가 이상치 값이 아닌 데이터만 추출한다.

def not_outlier(df, col, z):
	return df[abs(df[col] - np.mean(df[col]))/np.std(df[col]) <= z].index

trade.loc[not_outlier(trade, '무역수지', 1.5)]

IQR method

이상치를 찾는 방법에는 IQR 메서드를 사용하는 방법도 있다. IQR(InterQuartile Range)은 사분 범위라고 하고 표준편차에서 중앙 50%의 데이터들이 흩어진 정도를 의미한다.

아웃라이어(표본 중에서 다른 대상과 확연히 구분되는 관측치)가 포함된 임의의 데이터를 만들어 본다

np.random.seed(2020)
data = np.random.randn(100)
# 평균 0, 표준편차 1의 분포에서 100개의 숫자를 샘플리앟ㄴ 데이터 생성
data = np.concatenate((data, np.array([8, 10, -3, -5])))
# [8, 10, -3, -5]를 데이터 뒤에 추가함
data

박스 플롯 출력

fig, ax = plt.subplots()
ax.boxplot(data)
plt.show()

박스 플롯에서 박스를 벗어난 점들을 볼 수 있다.
위 결과값과 같이 사분 범위 IQR을 이용해 이상치를 찾아낼 수 있다. IQR=Q3Q1IQR = Q_3 - Q_1

IQR은 제 3사분위수에서 제 1사분위 값을 뺀 값으로 데이터의 중간 50% 범위에 해당하는 부분이라고 생각하면된다.
Q11.5IQRQ_1 - 1.5 * IQR보다 왼쪽에 있거나 Q3+1.5IQRQ_3 + 1.5 * IQR보다 오른쪽에 있는 경우 이상치라고 판단한다.

그림을 보면 쉽게 이해할 수 있다.

IQR을 구하기 위해 우선 제 1사분위수와 제 3사분위수를 구한다.

Q3, Q1 = np.percentile(data, [75, 25])
IQR = Q3 - Q1
IQR

IQR과 제 1사분위수, 제 3사분위수를 이용하여 이상치를 확인할 수 있다.

data[(Q1-1.5*IQR > data) | (Q3+1.5*IQR < data)]

이상치에 대한 자세한 내용을 좀 더 알아보고 싶다면 Three ways to detect outliers를 참고하자

정규화(Nomalization)

trade 데이터를 확인하면 각 항목 수입건수, 수출건수, 수입금액, 수출금액, 무역수지는 단위가 다 다른 것을 알 수 있다.
이와 같이 컬럼마다 스케일이 크게 차이나는 데이터를 입력하면 머신러닝 모델 학습에 문제가 발생할 수 있다.
예를 들어 데이터의 범위가 0~1인 컬럼A와 1000~10000 사이인 컬럼B가 있다고 했을 때, 이 데이터들을 클러스터링한다면, 데이터 간의 거리를 잴 때, 범위가 큰 컬럼 B의 값에만 영향을 크게 받을 것이다.
다른 예로 간단한 linear regression을 한다고 가정했을 때, 모델의 파라미터를 업데이트 하는 과정에서 범위가 큰 컬럼 B의 파라미터만 집중적으로 업데이트하는 문제가 생길 수 있다.
이러한 이유로 인해 컬럼 간의 범위가 크게 다를 경우 전처리 과정에서 데이터를 정규화한다.

정규화를 하는 방법은 다양하지만, 가장 잘 알려진 표준화(Standardiztion)와 MinMaxScaling을 알아보자

Standardization 데이터의 평균은 0, 분산은 1로 변환한다 Xμσ\frac{X-\mu}{\sigma}

MinMax Scaling 데이터의 최솟값은 0, 최댓값은 1로 변환 XXminXmaxXmin\frac{X - X_{min}}{X_{max} - X_{min}}

정규화 기법이 데이터 분포를 어떻게 바꾸는지 살펴보자
임의의 데이터를 생성하고, 각각의 기법으로 데이터를 정규화시켜준다.

# 정규분포를 따라 랜덤하게 데이터 x를 생성한다.
x = pd.DataFrame({'A' : np.random.randn(100)*4+4, 'B' : np.random.randn(100)-1})

x

# 데이터 x를 Standardization 기법으로 정규화
x_standardization = (x - x.mean())/x.std()
x_standardization

# 데이터 x를 MinMaxScaling 기법으로 정규화
x_minmax = (x-x.min())/(x.max()-x.min())
x_minmax

아래 코드는 데이터를 Standardization 기법으로 정규화를 했을 때, 분포가 어떻게 바뀌는지 보여준다. 컬럼의 평균은 0, 분산은 1로 데이터를 바꿔준다.

fig, axs = plt.subplots(1,2, figsize=(12,4), gridspec_kw={'width_ratios': [2,1]})

axs[0].scatter(x['A'], x['B'])
axs[0].set_xlim(-5, 15)
axs[0].set_ylim(-5, 5)
axs[0].axvline(c='grey', lw=1)
axs[0].axhline(c='grey', lw=1)
axs[0].set_title('Original Data')

axs[1].scatter(x_standardization['A'], x_standardization['B'])
axs[1].set_xlim(-5, 5)
axs[1].set_ylim(-5, 5)
axs[1].axvline(c='grey', lw=1)
axs[1].axhline(c='grey', lw=1)
axs[1].set_title('Data after standardization')

plt.show()

다음 코드는 동일한 데이터를 MinMax Scaling 기법으로 정규화 했을 때 분포가 어떻게 봐뀌는지 보여준다. 컬럼의 최솟값은 0, 최댓값은 1로 바꿔준다.

fig,axs = plt.subplots(1,2, figsize=(12,4), gridspec_kw={'width_ratios' : [2,1]})

axs[0].scatter(x['A'], x['B'])
axs[0].set_xlim(-5, 15)
axs[0].set_ylim(-5, 5)
axs[0].axvline(c='grey', lw=1)
axs[0].axhline(c='grey', lw=1)
axs[0].set_title('Original Data')

axs[1].scatter(x_minmax['A'], x_minmax['B'])
axs[1].set_xlim(-5,5)
axs[1].set_ylim(-5,5)
axs[1].axvline(c='grey', lw=1)
axs[1].axhline(c='grey', lw=1)
axs[1].set_title('Data after minmax Scaling')

plt.show()

이제 우리가 연습하는 trade 데이터를 활용해서 살펴본다.

Standardization

정규화를 시켜야 할 수치형 컬럼들을 cols 변수에 담은 후, 데이터에서 평균을 빼고, 표준편차로 나눠준다.

cols = ['수출건수', '수출금액', '수입건수', '수입금액', '무역수지']
trade_Standardization = (trade[cols]-trade[cols].mean())/trade[cols].std()
trade_Standardization.head()

정규화 시킨 trade_Standardization을 확인해 보면, 각 컬럼의 평균들은 거의 0에 가깝고, 표준편차는 1에 가까운 것을 확인할 수 있다.

trade_Standardization.describe()

MinMax Scaling

데이터에서 최솟값을 빼주고, 최댓값-최솟값으로 나눠준다.

trade[cols] = (trade[cols]-trade[cols].min())/(trade[cols].max()-trade[cols].min())
trade.head()

정규화 시킨후 각 컬럼을 보면 최솟값(min)은 0이고, 최댓값(max)은 1임을 확인할 수 있다.

주의할 점

train 데이터와 test 데이터가 나눠져 있는 경우 train 데이터를 정규화시켰던 기준 그대로 test 데이터도 정규화 시켜줘야 한다.

train = pd.DataFrame([[10, -10], [30, 10,], [50, 0]])
test = pd.DataFrame([[0,1], [10,10]])
train_min = train.min()
train_max = train.max()

train_minmax = (train - train_min)/(train_max - train.min)
test_minmax = (test - traim_min)/(train_max - train_min)
# test를 minmax Scaling 할 때도 train 정규화 기준으로 수행

train_minmax
test_minmax

scikit-learn의 StandardScaler, MinMaxScaler를 사용하는 방법도 있다.

from sklearn.preprocessing import MinMaxScaler
train = [[10,-10], [30,10], [50,0]]
test = [[0,1]]
scaler = MinMaxScaler()

scaler.fit_transform(train)

scaler.transform(test)

로그 변환 등 다양한 기법을 정규화와 함께 사용하면 효과적이다.

원-핫 인코딩(One-Hot Encoding)

범주형 데이터인 국가명 컬럼을 다뤄본다
머신러닝이나 딥러닝 프레임워크에서 범주형을 지원하지 않는 경우 원-핫 인코딩을 해야한다
원-핫 인코딩이란 카테고리별 이진 특성을 만들어 해당하는 특성만 1, 나머지는 0으로 만드는 방법이다. pandas를 사용해 국가명 컬럼을 원-핫 인코딩 해보자

pandas에서 get_dummies함수를 통해 쉽게 원-핫 인코딩을 할 수 있다.

# trade 데이터의 국가명 컬럼 원본
print(trade['국가명'].head())

# get_dummies를 통해 국가명 원-핫 인코딩
country = pd.get_dummies(trade['국가명'])
country.head()

pd.concat함수를 사용해 데이터프레임 trade와 country를 합쳐준다.

trade = pd.concat([trade, country], axis=1)
trade.head()

필요없어진 국가명 컬럼을 삭제하고 나면 trade는 우리가 원하는 데이터프레임이 된다.

trade.drop(['국가명'], axis=1, inplace=True)
trade.head()

구간화(Binning)

salary에 소득 데이터가 있다고 가정

salary = pd.Series([4300, 8370, 1750, 3830, 1840, 4220, 3020, 2290, 4740, 4600, 
                    2860, 3400, 4800, 4470, 2440, 4530, 4850, 4850, 4760, 4500, 
                    4640, 3000, 1880, 4880, 2240, 4750, 2750, 2810, 3100, 4290, 
                    1540, 2870, 1780, 4670, 4150, 2010, 3580, 1610, 2930, 4300, 
                    2740, 1680, 3490, 4350, 1680, 6420, 8740, 8980, 9080, 3990, 
                    4960, 3700, 9600, 9330, 5600, 4100, 1770, 8280, 3120, 1950, 
                    4210, 2020, 3820, 3170, 6330, 2570, 6940, 8610, 5060, 6370,
                    9080, 3760, 8060, 2500, 4660, 1770, 9220, 3380, 2490, 3450, 
                    1960, 7210, 5810, 9450, 8910, 3470, 7350, 8410, 7520, 9610, 
                    5150, 2630, 5610, 2750, 7050, 3350, 9450, 7140, 4170, 3090])

위 데이터를 구간별로 나눌려고 한다. 이를 구간화(Data binning or bucketing)이라고 하고, 연속적인 데이터를 구간을 나눠 분석할 때 사용한다

salary.hist()

pandas의 cutqcut을 이용해 수치형 데이터를 범주형 데이터로 변형시켜보자.

cut을 사용하기 위해서는 구간을 정해주어야 한다. 구간 변수를 설정하고 cut함수에 데이터와 구간을 입력한다.

bins = [0, 2000, 4000, 6000, 8000, 10000]

ctg = pd.cut(salary, bins=bins)
ctg

print('salary[0] : ', salary[0])
print('salary[0]가 속한 카테고리 : ', ctg[0])

salary[0]은 4300으로 4000에서 6000사이에 포함된 것을 확인할 수 있다.

이제 구간별로 값이 몇 개가 속해 있는지 value_counts()로 확인해보자

ctg.value_counts().sort_index()

위와 같이 특정 구간을 지정해 줄 수도 있고, 구간의 개수를 지정해 줄 수도 있다.
bins에 정수를 입력하면 데이터의 최솟값에서 최댓값을 균등하게 bins 개수만큼 나눠준다.

ctg = pd.cut(salary, bins=6)
ctg

ctg.value_counts().sort_index()

qcut함수는 구간을 일정하게 나누는 것이 아니라 데이터의 분포를 비슷한 크기의 그룹으로 나눠준다

ctg = pd.qcut(salary, q=5)
ctg

print(ctg.value_counts().sort_index())

마무리하며

지금까지 데이터 전처리에 대해서 배웠다
결측치, 중복된 데이터, 이상치, 정규화, 원-핫 인코딩, 구간화

위의 과정을 모든 데이터에 일괄적으로 적용해야 하는 것은 아니다. 데이터 전처리는 데이터의 특성을 파악해서 해야한다.

추가로 공부하기 위해서 아래 데이터셋의 전처리를 해보며 복습해보자.
vgsales
이 데이터는 캐글의 Video Game Sales 데이터 셋이다.
이외에도 다양한 데이터를 가져와 전처리 연습을 해볼 수 있다.
공공데이터포털, 캐글

profile
하루에 집중하자

0개의 댓글