"데이터 분석의 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()
학습목표
배울 내용
1. 결측치(Missing Data)
2. 중복된 데이터
3. 이상치(Outlier)
4. 정규화(Normalization)
5. 원-핫 인코딩(One-Hot Encoding)
6. 구간화(Binning)
다뤄야 하는 데이터는 보통 결측치를 포함하고 있는 경우가 많습니다. 따라서 이를 처리해주어야 합니다. 처리하는 방법은 크게 두 가지가 있습니다. 이때 데이터마다 특성을 반영하여 해결해야 합니다.
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은 제거하는 것이 좋습니다.
dropna는 결측치를 삭제해주는 메서드입니다.subset 옵션으로 특정 컬럼들을 선택했습니다.how 옵션으로 선택한 컬럼 전부가 결측치인 행을 삭제하겠다는 의미로 'all'을 선택합니다. ('any': 하나라도 결측치인 경우)inplace 옵션으로 해당 DataFrame 내부에 바로 적용시켰습니다.trade.dropna(how='all', subset=['수출건수', '수출금액', '수입건수', '수입금액',
'무역수지'], inplace=True)
trade[trade.isnull().any(axis=1)]
특정 값을 지정해줄 수 있습니다. 그러나 결측치가 많은 경우, 모두 같은 값으로 대체한다면 데이터의 분산이 실제보다 작아지는 문제가 생길 수 있습니다.
평균, 중앙값 등으로 대체할 수 있습니다. 1번에서 특정 값으로 대체했을 때와 마찬가지로 결측치가 많은 경우 데이터의 분산이 실제보다 작아지는 문제가 발생할 수 있습니다.
다른 데이터를 이용해 예측값으로 대체할 수 있습니다. 예를 들어 머신러닝 모델로 2020년 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]]
다른 방법들도 연습해봅니다.
특정 값을 지정해줄 수 있습니다. 예를 들어 ‘기타’, ‘결측’과 같이 새로운 범주를 만들어 결측치를 채울 수 있습니다.
최빈값 등으로 대체합니다. 결측치가 많은 경우 최빈값이 지나치게 많아질 수 있으므로 결측치가 많을 때는 다른 방법을 사용합니다.
다른 데이터를 이용해 예측값으로 대체할 수 있습니다.
시계열 특성을 가진 데이터의 경우 앞뒤 데이터를 통해 결측치를 대체할 수 있습니다. 예를 들어 특정인의 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_duplicates의 subset, keep 옵션을 통해 손쉽게 중복을 제거할 수 있습니다.
df.drop_duplicates(subset=['id'], keep='last')
이상치란 대부분 값의 범위에서 벗어나 극단적으로 크거나 작은 값을 의미합니다.
현실에서 이상치를 찾는 것(anomaly detection) 자체가 큰 분야입니다.
가장 먼저 생각해 볼 수 있는 간단하고 자주 사용되는 방법은 평균과 표준편차를 이용하는 z score 방법입니다.
평균을 빼주고 표준편차로 나눠 z score를 계산합니다. 그리고 z score가 특정 기준을 넘어서는 데이터에 대해 이상치라고 판단합니다.
이상치를 판단한 뒤 아래와 같은 방법이 있습니다.
이상치인 데이터의 인덱스를 리턴하는 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)]
하지만 이상치를 찾는 방법에는 위에 설명한 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은 제 3사분위수에서 제 1사분위 값을 뺀 값으로 데이터의 중간 50%의 범위라고 생각하면 됩니다.
보다 왼쪽에 있거나, 보다 오른쪽에 있는 경우 이상치라고 판단합니다.
아래 그림을 참고하세요.
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, '무역수지')
trade 데이터를 보면 수입건수, 수출건수와 수입금액, 수출금액, 무역수지는 단위가 다르다는 것을 알 수 있습니다. 이처럼 스케일이 크게 차이가 나는 데이터를 입력하면 머신러닝 모델 학습에 문제가 발생할 수 있습니다.
그래서 일반적으로 컬럼간에 범위가 크게 다를 경우 전처리 과정에서 데이터를 정규화합니다.
정규화를 하는 방법은 다양하지만, 가장 잘 알려진 표준화(Standardization)와 Min-Max Scaling을 알아봅시다.
Standardization 데이터의 평균은 0, 분산은 1로 변환
Min-Max Scaling 데이터의 최솟값은 0, 최댓값은 1로 변환
# 정규분포를 따라 랜덤하게 데이터 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)
++ 추가로 로그 변환 등의 기법도 정규화와 함께 사용하면 도움이 될 수 있습니다.
참고링크
머신러닝이나 딥러닝 프레임워크에서 범주형을 지원하지 않는 경우 원-핫 인코딩을 해야 합니다.
원-핫 인코딩이란?
카테고리별 이진 특성을 만들어 해당하는 특성만 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()
아래와 같이 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