미가공 데이터의 문제점
미가공 데이터는 다음과 같은 문제점들을 가지고 있다.
- 데이터의 스케일이 다름.
- 카테고리, 순서형 데이터가 있음.
- 잘못 기입되거나 누락된 값(결측치)이 있음.
- 극단적으로 크거나 작은 값이 있음.
이번 시간에는 다음과 같은 방법들을 배울 것이다.
- 결측치 처리
- 이산형(카테고리형) 데이터 처리
- 스케일이 많이 차이나는 데이터 처리
위 방법들로 데이터를 전처리한다면 앞서 살펴본 문제들을 해결할 수 있을 것이다.
결측치 처리
결측치를 처리하는 아이디어를 다음과 같이 생각해볼 수 있다.
- 특성을 가지지 않는 샘플을 삭제(drop)
- 특성의 최소 개수를 정해서 이를 넘지 못하는 데이터 삭제
- 데이터가 거의 없는 특성은 특성 자체를 삭제
- 어떠한 값으로 결측치 채우기
결측치가 얼마나 존재하는지는 isnull().sum()
을 이용해 다음과 같이 알아볼 수 있다. 가끔은 절대적인 개수보다 비율이 중요할 수도 있으니, 전체 데이터의 개수로 나누어 주는 방법도 알아두자.
>>> df
first_name last_name age sex preTestScore postTestScore
0 Jason Miller 42.0 m 4.0 25.0
1 NaN NaN NaN NaN NaN NaN
2 Tina Ali 36.0 f NaN NaN
3 Jake Milner 24.0 m 2.0 62.0
4 Amy Cooze 73.0 f 3.0 70.0
>>> df.isnull().sum()
first_name 1
last_name 1
age 1
sex 1
preTestScore 2
postTestScore 2
dtype: int64
>>> df.isnull().sum() / len(df)
first_name 0.2
last_name 0.2
age 0.2
sex 0.2
preTestScore 0.4
postTestScore 0.4
dtype: float64
dropna()
를 사용하면 결측치가 존재하는 샘플을 제거할 수 있다. 결측치가 하나라도 있으면 샘플을 제거한다.
>>> df_no_missing = df.dropna()
>>> df_no_missing
first_name last_name age sex preTestScore postTestScore
0 Jason Miller 42.0 m 4.0 25.0
3 Jake Milner 24.0 m 2.0 62.0
4 Amy Cooze 73.0 f 3.0 70.0
모든 특성에 대해 데이터가 없을 때만 샘플을 제거하고 싶다면 아래와 같이 하면 된다.
>>> df_cleaned = df.dropna(how='all')
>>> df_cleaned
first_name last_name age sex preTestScore postTestScore
0 Jason Miller 42.0 m 4.0 25.0
2 Tina Ali 36.0 f NaN NaN
3 Jake Milner 24.0 m 2.0 62.0
4 Amy Cooze 73.0 f 3.0 70.0
샘플(혹은 특성)을 삭제할 최소 결측치 개수를 설정할 때는 thresh
를 이용하면 된다.
>>> df['location'] = np.nan # 결측치가 있는 column 생성
>>> df
first_name last_name age sex preTestScore postTestScore location
0 Jason Miller 42.0 m 4.0 25.0 NaN
1 NaN NaN NaN NaN NaN NaN NaN
2 Tina Ali 36.0 f NaN NaN NaN
3 Jake Milner 24.0 m 2.0 62.0 NaN
4 Amy Cooze 73.0 f 3.0 70.0 NaN
>>> df.dropna(axis=1, how='all') # 모든 데이터가 결측치인 column 제거
first_name last_name age sex preTestScore postTestScore
0 Jason Miller 42.0 m 4.0 25.0
1 NaN NaN NaN NaN NaN NaN
2 Tina Ali 36.0 f NaN NaN
3 Jake Milner 24.0 m 2.0 62.0
4 Amy Cooze 73.0 f 3.0 70.0
>>> df.dropna(axis=0, thresh=3) # 3개 이상의 특성에 대해 결측치인 샘플 제거
first_name last_name age sex preTestScore postTestScore location
0 Jason Miller 42.0 m 4.0 25.0 NaN
2 Tina Ali 36.0 f NaN NaN NaN
3 Jake Milner 24.0 m 2.0 62.0 NaN
4 Amy Cooze 73.0 f 3.0 70.0 NaN
축을 이용해 특성을 제거할 것인지, 샘플을 제거할 것인지를 결정할 수 있다.
데이터의 값을 채울 때는 평균 값, 중간 값, 최빈 값을 주로 사용할 수 있다. 각 값에 대한 설명은 생략한다 0으로 채울 수도 있지만 거의 쓰지 않는다
평균 값을 집어넣을 때는 다음과 같이 fillna()
와 mean()
을 사용하면 된다.
>>> df["preTestScore"].fillna(df["preTestScore"].mean(), inplace=True)
>>> df
first_name last_name age sex preTestScore postTestScore location
0 Jason Miller 42.0 m 4.0 25.0 NaN
1 NaN NaN NaN NaN 3.0 NaN NaN
2 Tina Ali 36.0 f 3.0 NaN NaN
3 Jake Milner 24.0 m 2.0 62.0 NaN
4 Amy Cooze 73.0 f 3.0 70.0 NaN
중간 값은 mean()
대신 median()
, 최빈 값은 mode()
를 사용한다.
어떨 때는 특정한 특성에 대해 서로 다른 값을 집어넣고 싶을 때가 있을 것이다. 예를 들어, 시험 성적의 분포가 남녀 다르다고 생각할 때는 결측치에 서로 다른 값을 집어 넣어야 더 제대로 된 전처리가 될 것이다. 그럴 때는 groupby()
와 transform()
을 사용해 다음과 같이 처리한다.
>>> df['postTestScore'].fillna(df.groupby('sex')['postTestScore'].transform('mean'), inplace=True)
>>> df
first_name last_name age sex preTestScore postTestScore location
0 Jason Miller 42.0 m 4.0 25.0 NaN
1 NaN NaN NaN NaN 3.0 NaN NaN
2 Tina Ali 36.0 f 3.0 70.0 NaN
3 Jake Milner 24.0 m 2.0 62.0 NaN
4 Amy Cooze 73.0 f 3.0 70.0 NaN
이산형 데이터 처리
다음과 같이 카테고리형의 데이터를 이산형 데이터라고 한다.
{Green, Blue, Yellow}
이산형 데이터를 처리하는 가장 일반적인 방법은 원-핫 인코딩이다. 다음과 같이 벡터의 각 인덱스와 범주를 연결하고 그 범주이면 1 아니면 0을 할당한다.
\[\{Green\} \rightarrow [1, 0, 0] \\ \{Blue\} \rightarrow [0, 1, 0] \\ \{Yellow\} \rightarrow [0, 0, 1]\]판다스에서 이산형 데이터를 원-핫 인코딩으로 변환할 때는 get_dummies
를 사용한다.
>>> edges
source target weight color
0 0 2 3 red
1 1 2 4 blue
2 2 3 5 blue
>>> pd.get_dummies(edges)
source target weight color_blue color_red
0 0 2 3 0 1
1 1 2 4 1 0
2 2 3 5 1 0
>>> pd.get_dummies(edges["color"])
blue red
0 0 1
1 1 0
2 1 0
>>> pd.get_dummies(edges[["color"]])
color_blue color_red
0 0 1
1 1 0
2 1 0
다양한 방법이 있으니 원하는 것을 사용하면 된다.
옷의 사이즈와 같은 데이터는 순서형(ordinary) 데이터이다. 이와 같은 순서형 데이터가 숫자로 되어있다면, 이 숫자들을 범주로 바꾸어주고, 그 다음에 다시 원-핫 인코딩으로 변환해주어야 비로소 분석에 사용할 수 있다. 그 과정은 다음과 같이 하면 된다.
>>> weight_dict = {3:"M", 4:"L", 5:"XL"}
>>> edges["weight_sign"] = edges["weight"].map(weight_dict)
>>> edges
source target weight color weight_sign
0 0 2 3 red M
1 1 2 4 blue L
2 2 3 5 blue XL
>>> pd.get_dummies(edges).values
array([[0, 2, 3, 0, 1, 0, 1, 0],
[1, 2, 4, 1, 0, 1, 0, 0],
[2, 3, 5, 1, 0, 0, 0, 1]])
데이터의 구간을 나눌 수(binning)도 있다. 구간과 각 구간의 이름을 지정해서 연속적인 데이터를 이산형 데이터로 변환할 수 있다.
>>> df
regiment company name preTestScore postTestScore
0 Nighthawks 1st Miller 4 25
1 Nighthawks 1st Jacobson 24 94
2 Nighthawks 2nd Ali 31 57
3 Nighthawks 2nd Milner 2 62
4 Dragoons 1st Cooze 3 70
5 Dragoons 1st Jacon 4 25
6 Dragoons 2nd Ryaner 24 94
7 Dragoons 2nd Sone 31 57
8 Scouts 1st Sloan 2 62
9 Scouts 1st Piger 3 70
10 Scouts 2nd Riani 2 62
11 Scouts 2nd Ali 3 70
>>> bins = [0, 25, 50, 75, 100] # 구간 설정
>>> group_names = ['Low', 'Okay', 'Good', 'Great'] # 구간의 이름 설정
>>> categories = pd.cut(df['postTestScore'], bins, labels=group_names)
>>> categories
0 Low
1 Great
2 Good
3 Good
4 Good
5 Low
6 Great
7 Good
8 Good
9 Good
10 Good
11 Good
Name: postTestScore, dtype: category
Categories (4, object): ['Low' < 'Okay' < 'Good' < 'Great']
사이킷런의 preprocessing에서도 원-핫 인코딩을 지원한다. 사이킷런에서 레이블링과 인코딩을 하는 과정은 두 가지로 나뉜다.
- Encoder 생성
fit()
으로 규칙 생성transform()
으로 규칙 적용
규칙 생성과 적용을 분리한 이유는 새로운 데이터가 들어 오더라도 기존 규칙을 적용하기 편하게 하기 위함이다.
>>> from sklearn import preprocessing
>>> le = preprocessing.LabelEncoder() # Encoder 생성
>>> le.fit(raw_example[:,0]) # 규칙 생성
LabelEncoder()
>>> le.transform(raw_example[:,0]) # 규칙 적용
array([1, 1, 1, 1, 0, 0, 0, 0, 2, 2, 2, 2])
새로운 데이터에 기존 규칙을 적용하는 과정은 다음과 같이 for문을 사용하면 된다.
>>> label_column = [0,1,2,5]
>>> label_enconder_list = []
>>> for column_index in label_column:
... le = preprocessing.LabelEncoder()
... le.fit(raw_example[:,column_index])
... data[:,column_index] = le.transform(raw_example[:,column_index])
... label_enconder_list.append(le)
... del le
...
LabelEncoder()
LabelEncoder()
LabelEncoder()
LabelEncoder()
>>> data[:3]
array([[1, 0, 4, 4, 25, 2],
[1, 0, 2, 24, 94, 1],
[1, 1, 0, 31, 57, 0]], dtype=object)
>>> label_enconder_list[0].transform(raw_example[:10,0])
array([1, 1, 1, 1, 0, 0, 0, 0, 2, 2])
여기까지 하면, 이산형 데이터의 각 범주에 숫자를 대응하고(원-핫 인코딩이 아니다! 순서형 데이터에 가깝다), 특성을 숫자로 표현하는 것까지 완료된 상황이다. 이제 이를 원-핫 인코더로 바꿔보자.
>>> one_hot_enc = preprocessing.OneHotEncoder()
>>> one_hot_enc.fit(data[:,0].reshape(-1,1))
OneHotEncoder()
>>> onehotlabels = one_hot_enc.transform(data[:,0].reshape(-1,1)).toarray()
>>> onehotlabels
array([[0., 1., 0.],
[0., 1., 0.],
[0., 1., 0.],
[0., 1., 0.],
[1., 0., 0.],
[1., 0., 0.],
[1., 0., 0.],
[1., 0., 0.],
[0., 0., 1.],
[0., 0., 1.],
[0., 0., 1.],
[0., 0., 1.]])
데이터 스케일 처리
특성 간의 스케일이 너무 다를 때는 이를 어느 정도 맞추어야 한다. 그렇지 않으면 어느 한 특성이 결과 값에 과하게 영향을 줄 수 있다. 대표적인 데이터 스케일링 전략에는 두 가지가 있다.
먼저, 최소-최대 정규화(Min-Max Normalization)는 기존 변수의 범위를 본인이 정한 최대-최소의 범위로 변경하는 것이다. 일반적으로 0과 1사이의 값으로 많이 바꾼다. 다음과 같은 공식으로 표현할 수 있다(new max와 new low는 본인이 직접 정하면 된다).
\[x_{norm}^{(i)} = \frac{x^{(i)} - x_{min}}{x_{max} - x_{min}}(new \, max - new \, low) + new \, low\]다음으로 Z-score 정규화(Z-score Normalization, Standardization)가 있다. 이 방법은 본인이 정해야 할 것은 없고, 기존 데이터에 따라 스케일 된 값이 결정된다.
\[x_{std \, norm}^{(i)} = \frac{x^{(i)} - \mu}{s_i}\]번역 상의 이유로 두 방법이 혼동될 때가 있으니 영어 표현까지 알아두자.
중요한 것은 위 방법들에서 등장하는 정규화 매개변수(최대/최소, 평균/표준편차) 들은 꼭 저장해두었다가 새로운 데이터가 들어올 때도 적용해주어야 한다는 것이다.
이제 개념을 알았으니 구현하는 방법도 살펴보자. 먼저 판다스에서 구현하는 방법이다.
>>> df
A B C
0 14.00 103.02 big
1 90.20 107.26 small
2 90.95 110.35 big
3 96.27 114.23 small
4 91.21 114.68 small
# Min-Max Normalization
>>> df["A"] = ( df["A"] - df["A"].min() ) / (df["A"].max() - df["A"].min()) * (5 - 1) + 1
>>> df
A B C
0 1.000000 103.02 big
1 4.704874 107.26 small
2 4.741339 110.35 big
3 5.000000 114.23 small
4 4.753981 114.68 small
# Z-score Normalization
>>> df["B"] = ( df["B"] - df["B"].mean() ) / (df["B"].std() )
>>> df
A B C
0 1.000000 -1.405250 big
1 4.704874 -0.540230 small
2 4.741339 0.090174 big
3 5.000000 0.881749 small
4 4.753981 0.973556 small
지속적으로 새로운 데이터에 적용해야 한다면 함수를 만들어서 반복 이용할 수도 있다.
사이킷런의 preprocessing에서도 스케일링을 지원한다. 역시나 fit과 transform의 과정을 거친다.
>>> df
Class label Alcohol Malic acid
0 1 14.23 1.71
1 1 13.20 1.78
2 1 13.16 2.36
3 1 14.37 1.95
4 1 13.24 2.59
.. ... ... ...
173 3 13.71 5.65
174 3 13.40 3.91
175 3 13.27 4.28
176 3 13.17 2.59
177 3 14.13 4.10
[178 rows x 3 columns]
>>> from sklearn import preprocessing
# Z-score Normalization
>>> std_scaler = preprocessing.StandardScaler().fit(df[['Alcohol', 'Malic acid']])
>>> df_std = std_scaler.transform(df[['Alcohol', 'Malic acid']])
# Min-Max Normalization
>>> minmax_scaler = preprocessing.MinMaxScaler().fit(df[['Alcohol', 'Malic acid']])
>>> minmax_scaler.transform(df[['Alcohol', 'Malic acid']])
판다스에 비해 좀 더 간단하다.
스케일링을 해도 데이터의 분포 자체가 변하는 것은 아니라는 점은 꼭 기억하자.
별도의 출처 표시가 있는 이미지를 제외한 모든 이미지는 강의자료에서 발췌하였음을 밝힙니다.
댓글남기기