미가공 데이터의 문제점

미가공 데이터는 다음과 같은 문제점들을 가지고 있다.

  • 데이터의 스케일이 다름.
  • 카테고리, 순서형 데이터가 있음.
  • 잘못 기입되거나 누락된 값(결측치)이 있음.
  • 극단적으로 크거나 작은 값이 있음.

이번 시간에는 다음과 같은 방법들을 배울 것이다.

  • 결측치 처리
  • 이산형(카테고리형) 데이터 처리
  • 스케일이 많이 차이나는 데이터 처리

위 방법들로 데이터를 전처리한다면 앞서 살펴본 문제들을 해결할 수 있을 것이다.


결측치 처리

결측치를 처리하는 아이디어를 다음과 같이 생각해볼 수 있다.

  • 특성을 가지지 않는 샘플을 삭제(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에서도 원-핫 인코딩을 지원한다. 사이킷런에서 레이블링과 인코딩을 하는 과정은 두 가지로 나뉜다.

  1. Encoder 생성
  2. fit()으로 규칙 생성
  3. 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']])

판다스에 비해 좀 더 간단하다.

스케일링을 해도 데이터의 분포 자체가 변하는 것은 아니라는 점은 꼭 기억하자.



별도의 출처 표시가 있는 이미지를 제외한 모든 이미지는 강의자료에서 발췌하였음을 밝힙니다.

댓글남기기