[F-09] 다양한 데이터 전처리 기법

Seungil Ko·2021년 10월 17일

Fundamental

목록 보기
7/11

"데이터 분석의 8할은 데이터 전처리이다."라는 말이 있습니다.
전처리에 따라서 데이터 분석의 질이 달라지고 이는 모델의 정확도에도 영향을 미칩니다. 전처리가 잘 되어 있는 경우 예측 모델의 성능을 높일 수 있습니다.

trade.csv
위에서 받은 데이터를 확인합니다. trade.csv는 관세청 수출입 무역 통계에서 가공한 데이터입니다.
특정 국가에 대한 월별 수출입 건수와 금액(단위:천불(USD 1,000))이 있습니다. 무역수지는 수출금액-수입금액이 됩니다.

csv_file_path = os.getenv("HOME") + '/gseung/data/trade.csv'
trade = pd.read_csv(csv_file_path)
trade.head()

학습목표

  • 중복된 데이터를 찾아 제거할 수 있고, 결측치(missing data)를 제거하거나 채워 넣을 수 있다.
  • 데이터를 정규화시킬 수 있다.
  • 이상치(outlier)를 찾고, 이를 처리할 수 있다.
  • 범주형 데이터를 원-핫 인코딩할 수 있다.
  • 연속적인 데이터를 구간으로 나눠 범주형 데이터로 변환할 수 있다.

배울 내용
1. 결측치(Missing Data)
2. 중복된 데이터
3. 이상치(Outlier)
4. 정규화(Normalization)
5. 원-핫 인코딩(One-Hot Encoding)
6. 구간화(Binning)

결측치(Missing Data)

다뤄야 하는 데이터는 보통 결측치를 포함하고 있는 경우가 많습니다. 따라서 이를 처리해주어야 합니다. 처리하는 방법은 크게 두 가지가 있습니다. 이때 데이터마다 특성을 반영하여 해결해야 합니다.
1. 결측치가 있는 데이터를 제거한다.
2. 결측치를 어떤 값으로 대체한다.

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를 반환합니다.

trade.isnull()
# '각 행이 결측치가 하나라도 있는지' 여부를 boolean값으로 가진 Series가 출력됩니다.
trade.isnull().any(axis=1)

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

index 191 데이터는 수출금액과 무역수지 컬럼이 빠져있고, index 196, 197, 198은 기간, 국가명을 제외하고 모두 결측치입니다.
이 경우 index 191 데이터는 삭제보다 특정 값으로 대체하는 것이 좋습니다.
반면 index 196, 197, 198은 제거하는 것이 좋습니다.

  • DataFrame의 dropna는 결측치를 삭제해주는 메서드입니다.
    subset 옵션으로 특정 컬럼들을 선택했습니다.
    how 옵션으로 선택한 컬럼 전부가 결측치인 행을 삭제하겠다는 의미로 'all'을 선택합니다. ('any': 하나라도 결측치인 경우)
    inplace 옵션으로 해당 DataFrame 내부에 바로 적용시켰습니다.
trade.dropna(how='all', subset=['수출건수', '수출금액', '수입건수', '수입금액',
'무역수지'], inplace=True)
trade[trade.isnull().any(axis=1)]

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

  1. 특정 값을 지정해줄 수 있습니다. 그러나 결측치가 많은 경우, 모두 같은 값으로 대체한다면 데이터의 분산이 실제보다 작아지는 문제가 생길 수 있습니다.

  2. 평균, 중앙값 등으로 대체할 수 있습니다. 1번에서 특정 값으로 대체했을 때와 마찬가지로 결측치가 많은 경우 데이터의 분산이 실제보다 작아지는 문제가 발생할 수 있습니다.

  3. 다른 데이터를 이용해 예측값으로 대체할 수 있습니다. 예를 들어 머신러닝 모델로 2020년 4월 미국의 예측값을 만들고, 이 값으로 결측치를 보완할 수 있습니다.

  4. 시계열 특성을 가진 데이터의 경우 앞뒤 데이터를 통해 결측치를 대체할 수 있습니다. 예를 들어 기온을 측정하는 센서 데이터에서 결측치가 발생할 경우, 전후 데이터의 평균으로 보완할 수 있습니다.

index 191번은 4번 방법으로 보완합니다.

# DataFrame.loc[행 라벨, 열 라벨]: 해당 라벨을 가진 데이터를 출력
trade.loc[[188, 191, 194]]

# index 191의 수출금액 컬럼값을 이전 달과 다음 달의 평균으로 채웁니다.
trade.loc[191, '수출금액'] = (trade.loc[188, '수출금액'] + trade.loc[194, '수출금액']) / 2
trade.loc[[191]]

# index 191의 무역수지 컬럼은 수출금액과 수입금액의 차이를 이용해 채웁니다.
trade.loc[191, '무역수지'] = trade.loc[191, '수출금액'] - trade.loc[191, '수입금액']
trade.loc[[191]]

다른 방법들도 연습해봅니다.

데이터가 범주형인 경우

  1. 특정 값을 지정해줄 수 있습니다. 예를 들어 ‘기타’, ‘결측’과 같이 새로운 범주를 만들어 결측치를 채울 수 있습니다.

  2. 최빈값 등으로 대체합니다. 결측치가 많은 경우 최빈값이 지나치게 많아질 수 있으므로 결측치가 많을 때는 다른 방법을 사용합니다.

  3. 다른 데이터를 이용해 예측값으로 대체할 수 있습니다.

  4. 시계열 특성을 가진 데이터의 경우 앞뒤 데이터를 통해 결측치를 대체할 수 있습니다. 예를 들어 특정인의 2019년 직업이 결측치이고, 2018년과 2020년 직업이 일치한다면 그 값으로 보완할 수 있습니다. 만약 다르다면 둘 중 하나로 보완하도록 합니다.

중복된 데이터

같은 값을 가진 데이터 없이 행(row)별로 값이 유일해야 한다면 중복된 데이터를 제거해야 합니다.

DataFrame.duplicated()는 중복된 데이터 여부를 boolean값으로 반환합니다.

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

index 186, 187이 중복된 것을 확인할 수 있습니다.
trade.drop_duplicates(inplace=True)
: pandas에서는 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가 사람마다 unique 하다고 할 때, 둘 중 하나는 삭제해야 합니다. index가 클수록 나중에 들어온 데이터이고, 사용자가 이름을 수정했을 때 업데이트가 되지 않고 삽입이 되어 생긴 문제라고 가정합니다.
즉, id가 중복된 경우 맨 나중에 들어온 값만 남겨 야 합니다.
DataFrame.drop_duplicatessubset, keep 옵션을 통해 손쉽게 중복을 제거할 수 있습니다.

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

이상치(Outlier)

이상치란 대부분 값의 범위에서 벗어나 극단적으로 크거나 작은 값을 의미합니다.

현실에서 이상치를 찾는 것(anomaly detection) 자체가 큰 분야입니다.
가장 먼저 생각해 볼 수 있는 간단하고 자주 사용되는 방법은 평균과 표준편차를 이용하는 z score 방법입니다.
평균을 빼주고 표준편차로 나눠 z score(Xμσ)(\frac{X-\mu}{\sigma})를 계산합니다. 그리고 z score가 특정 기준을 넘어서는 데이터에 대해 이상치라고 판단합니다.

이상치를 판단한 뒤 아래와 같은 방법이 있습니다.

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

z-score method

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

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보다 큰 데이터의 인덱스를 추출합니다.

def outlier(df, col, z):
	return df[abs(df[col] - np.mean(df[col]))/np.std(df[col])>z].index

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

# not_outlier 함수를 통해 무역수지가 이상치 값이 아닌 데이터만 추출
def not_outlief(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

하지만 이상치를 찾는 방법에는 위에 설명한 z-scor 방법만 있는 것이 아닙니다. 그리고 z-score 방법은 몇 가지 뚜렷한 한계를 가지고 있습니다.

z-score 방법의 대안으로 사분위범위수 IQR(Interquartile range)로 이상치를 알아내는 방법을 살펴봅니다.

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=Q3Q1IQR = Q_3 - Q_1

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

아래 그림을 참고하세요.
IQR

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

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

아래는 도움이 되는 아티클입니다.

z-score의 단점
1) Robust하지 못하다 - 왜냐하면 평균과 표준편차 자체가 이상치의 존재에 크게 영향을 받기 때문이다.
2) 작은 데이터셋의 경우 z-score의 방법으로 이상치를 알아내기 어렵다. 특히 item이 12개 이하인 데이터셋에서는 불가능하다.

# 이상치 찾기
def outlier2(df, col):
	q1 = df[col].quantile(0.25)
    q3 = df[col].quantile(0.75)
    iqr = q3 - q1
    return df[(df[col] < q1 - 1.5 * iqr)|(df[col] > q3 + 1.5 * iqr)]
    
outlier2(trade, '무역수지')

정규화(Normalization)

trade 데이터를 보면 수입건수, 수출건수와 수입금액, 수출금액, 무역수지는 단위가 다르다는 것을 알 수 있습니다. 이처럼 스케일이 크게 차이가 나는 데이터를 입력하면 머신러닝 모델 학습에 문제가 발생할 수 있습니다.

그래서 일반적으로 컬럼간에 범위가 크게 다를 경우 전처리 과정에서 데이터를 정규화합니다.

정규화를 하는 방법은 다양하지만, 가장 잘 알려진 표준화(Standardization)와 Min-Max Scaling을 알아봅시다.

Standardization 데이터의 평균은 0, 분산은 1로 변환

Xμσ\frac{X-\mu}{\sigma}

Min-Max Scaling 데이터의 최솟값은 0, 최댓값은 1로 변환

XXminXmaxXmin\frac{X-X_{min}}{X_{max} - X_{min}}
# 정규분포를 따라 랜덤하게 데이터 x를 생성합니다. 
np.random.seed(2020)
x = pd.DataFrame({'A': np.random.randn(100)*4+4,
                 'B': np.random.randn(100)-1})
                 
# 데이터 x를 Standardization 기법으로 정규화
x_standardization = (x - x.mean())/x.std()

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

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

# Standardization 정규화 후 분포 확인
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'])
# ax[1].scatter(x_min_max['A'], x_min_max['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()

trade 데이터 활용

# trade 데이터를 Standardization 기법으로 정규화합니다. 
cols = ['수출건수', '수출금액', '수입건수', '수입금액', '무역수지']
trade_Standardization= (trade[cols]-trade[cols].mean())/trade[cols].std()
trade_Standardization.head()

# 각 컬럼 평균(0), 표준편차(1) 확인
trade_Standardization.describe()

# trade 데이터를 min-max scaling 기법으로 정규화합니다. 
trade[cols] = (trade[cols]-trade[cols].min())/(trade[cols].max()-trade[cols].min())
trade.head()

# 각 컬럼 평균(0), 표준편차(1) 확인
trade.describe()

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

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.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 혹은 bucketing)이라고 부릅니다.
아래 히스토그램과 같이 연속적인 데이터를 구간을 나눠 분석할 때 사용하는 방법입니다.

# 히스토그램을 출력합니다.
salary.hist()

# pandas의 cut과 qcut을 이용해 수치형 데이터를 범주형 데이터로 변형시킵니다.
bins = [0, 2000, 4000, 6000, 8000, 10000]
ctg = pd.cut(salary, bins=bins)

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

# 구간별로 값이 몇 개가 속해 있는지 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)

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

추가 실습 데이터
16,500개 이상의 비디오 게임에 대한 매출 정보.
vasales.csv
출처: 캐글 Video Game Sales

profile
임베디드와 AI를 공부하고 있습니다 :)

0개의 댓글