22.11.08. ~ 22.11.09.
피쳐 엔지니어링에 대해 배웠다.
상당히 다양한 피쳐 엔지니어링 방법이 존재한다.
잘 익혀두고 적재적소에 활용하면 좋겠다!
내용들은 멋쟁이사자처럼 AI SCHOOL 7기 박조은강사님(오늘코드)의 자료를 참고하였습니다.
데이터 분석에 정해진 과정과 순서는 없다. 그러나 대부분의 경우
데이터 수집 -> 데이터 전처리 -> 탐색적 데이터 분석(EDA) -> 피쳐 엔지니어링 -> 머신러닝 모델에 적용
의 과정을 거친다.
위 과정들은 서로 엄밀하게 구분된 영역도 아니며, 진행하다보면 얼마든지 순서가 뒤바뀌어 일어날 수 있는 과정이다.
그러나 내가 지금 하고 있는 일을 체계화하여 살펴보기 위해 해당 과정들을 나누어 진행해보면 좋다.
머신러닝의 전반적인 과정들을 살짝 체험했으니, 이 날부터는 피쳐 엔지니어링에 대해 공부해보았다.
피쳐엔지니어링을 위한 EDA를 진행할 때 파악해야할 사항 몇가지를 살펴보자면,
df.info()
df.select_dtypes(include = np.number)
df.select_dtypes(include = 'object')
위 방법들이 존재한다.
결측치 탐색 역시 중요하다. 결측치는 nan으로 들어가있을 수도 있으나, 공백, '-', 0 등 다양한 형태로 포함되어있을 수 있다. 따라서 자료의 도수분포를 살펴보거나 히스토그램을 살펴보는 등 다양한 방법으로 결측치를 탐색할 필요가 있다.
이상치를 탐색하고 판단하는 것 역시 중요하다. IQR등을 이용해 값의 범위에 따라 이상치를 찾을 수도 있고, 시각화를 통해 그래프를 이용하여 찾을 수도 있다. 이상치는 데이터의 해석과 모델의 학습을 방해하므로 제거하거나 대체해주는 것이 필요하다.
희소값에 대한 처리 역시 필요하다. 범주형 변수에 대해 희소값이 존재하는 경우, 이는 데이터의 경향을 파악하기 어렵게 만들어 해석을 어렵게하고 머신러닝의 성능을 낮출 수 있다. 인코딩을 진행할 때에도 오랜 계산시간과 많은 컴퓨팅 성능을 요구하기도 한다. 따라서 희소값들의 경우 '기타'와 같이 묶어주거나, 결측치 처리해주기도 한다.
그럼 피쳐 엔지니어링에 대해 하나씩 살펴보자.
# preprocessing - PolynomialFeatures
from sklearn.preprocessing import PolynomialFeatures
poly = PolynomialFeatures(degree = 2)
house_poly = poly.fit_transform(train[['MSSubClass', 'LotArea']])
pd.DataFrame(house_poly, columns = poly.get_feature_names_out())
>>>
1 MSSubClass LotArea MSSubClass^2 MSSubClass LotArea LotArea^2
0 1.00 60.00 8,450.00 3,600.00 507,000.00 71,402,500.00
1 1.00 20.00 9,600.00 400.00 192,000.00 92,160,000.00
2 1.00 60.00 11,250.00 3,600.00 675,000.00 126,562,500.00
3 1.00 70.00 9,550.00 4,900.00 668,500.00 91,202,500.00
4 1.00 60.00 14,260.00 3,600.00 855,600.00 203,347,600.00
... ... ... ... ... ... ..
(X - X.mean()) / X.std()
from sklearn.preprocessin import StandardScaler
ss = StandardScaler()
ss.fit(train[[feature]]).transform(train[[feature]])
# 또는
# ss.fit_transform(train[[feature]])
(X - X.min()) / (X.max() - X.min())
from sklearn.preprocessing import MinMaxScaler
ms = MinMaxScaler()
train[['SalePrice_mm']] = ms.fit(train[['SalePrice']]).transform(train[['SalePrice']])
(X - X.median()) / (IQR)
from sklearn.preprocessing import RobustScaler
rs = RobustScaler()
train[['SalePrice_rs']] = rs.fit_transform(train[['SalePrice']])
사이킷런으로 위 스케일링들을 진행한 경우, .inverse_transform을 통해 원래 값으로 복원할 수 있다.
확률 분포를 달라지게 하기 위해서는, log transform 을 진행해야 한다.
0602 실습에서!
pd.cut(train['SalePrice'], bins = 4, labels = [1,2,3,4])
pd.cut의 인자로는 구간을 나누어줄 개수인 bins가 들어간다.
pd.qcut(train['SalePrice'], q=4, labels = [1,2,3,4])
pd.qcut의 인자로는 구간을 나누어줄 개수인 q가 들어간다.
train['MSZoning'].astype('category').cat.codes
# preprocessing - ordinal encoder
from sklearn.preprocessing import OrdinalEncoder
oe = OrdinalEncoder()
train[['MSZoning_ohe']] = oe.fit_transform(train[['MSZoning']])
display(train[['MSZoning','MSZoning_ohe']].sample(6))
oe.categories_
ordinal encoder를 선언한 후, fit시키고 transform 시켜주면 된다.
역시 주의할 점은 인자로 DataFrame 형태를 주어야한다는 점.
그리고 fit은 train set에만 진행하고 train은 transform 만 진행해야한다는 점.
공식문서 예시는 이렇다.
from sklearn.preprocessing import OrdinalEncoder
enc = OrdinalEncoder()
X = [['male', 'from US', 'uses Safari'],
['female', 'from Europe', 'uses Firefox']]
enc.fit(X)
print(enc.transform([['female', 'from US', 'uses Safari']]))
print(enc.categories_)
>>>
[[0. 1. 1.]]
[array(['female', 'male'], dtype=object), array(['from Europe', 'from US'], dtype=object), array(['uses Firefox', 'uses Safari'], dtype=object)]
pd.get_dummies(train['MSZoning'])
# preprocessing - OneHotEncoder
from sklearn.preprocessing import OneHotEncoder
ohe = OneHotEncoder()
ohe_out = ohe.fit_transform(train[['MSZoning']]).toarray()
display(pd.DataFrame(ohe_out, columns = ohe.get_feature_names_out()).head())
ohe.categories_
OneHotEncoder는 좀 복잡하다. 차근차근히 살펴보면,
먼저 OneHotEncoder를 호출 및 선언 해주었고, fit_transform 해주었다.
이때 중요한 점은 ohe를 통해 transform 한 결과는 DataFrame이 아니라 scipy의 matrix 객체라는 것이다. 따라서 결과를 확인하기 위해서는 .toarray()를 해주어야 한다.
또한, 우리가 원하는 데이터프레임 형식으로 보기 위해서는 transform 결과 만들어진 피쳐들의 리스트를 컬럼으로 주어야 하는데, 이는 ohe.get_feature_names_out()으로 가져올 수 있다.
공식문서 예시는 이렇다.
from sklearn.preprocessing import OneHotEncoder
enc = OneHotEncoder()
X = [['male', 'from US', 'uses Safari'],
['female', 'from Europe', 'uses Firefox']]
enc.fit(X)
enc_out = enc.transform([['female', 'from US', 'uses Safari'],
['male', 'from Europe', 'uses Safari']]).toarray()
print(enc_out)
print(enc.get_feature_names_out())
pd.DataFrame(enc_out, columns=enc.get_feature_names_out())
>>>
[[1. 0. 0. 1. 0. 1.]
[0. 1. 1. 0. 0. 1.]]
['x0_female' 'x0_male' 'x1_from Europe' 'x1_from US' 'x2_uses Firefox'
'x2_uses Safari']
x0_female x0_male x1_from Europe x1_from US x2_uses Firefox x2_uses Safari
0 1.00 0.00 0.00 1.00 0.00 1.00
1 0.00 1.00 1.00 0.00 0.00 1.00
실수에 다양한 계산을 취할 때, 소수를 저장하는데에 있어 메모리의 한계가 존재해 소수점 아래 일부 자리들은 버려지게 되므로 정확한 값이 아니라 오차가 나올 수 있다. 따라서 정수로 변환하여 사용하거나, 반올림을 적절히 활용하여야 한다. 참고
neg_mean_root_square
label에 log를 씌운 후 모델 학습 및 예측을 진행한 실습에서, cross validation을 진행할때 scoring을 neg_mean_root_square로 진행했다. 평가 지표는 rmsle였다.
log가 이미 씌워져있으므로 rmse를 구하면 된다는 것은 이해가 되었지만, 앞에 -은 왜 붙었을까?
R2, accuracy 등 다양한 평가지표의 경우 클 수록 좋은 값이다. 그러나 rmse, mse, mae 와 같은 오차형 평가지표의 경우 작을 수록 좋은 값이다. 이 평가지표들의 정렬기준을 클 수록 좋은 값으로 통일시키기 위해 오차의 앞에는 -를 붙여 클수록(절대값이 작을 수록) 좋은 모델인 것으로 판단하고자 이렇게 했을 거라 추측할 수 있다. 라고 강사님이 말해주셨다!
house price competition 의 평가 지표
Submissions are evaluated on Root-Mean-Squared-Error (RMSE) between the logarithm of the predicted value and the logarithm of the observed sales price. (Taking logs means that errors in predicting expensive houses and cheap houses will affect the result equally.) 라고 나와있다.
결국 RMSLE이다. log를 취하는 이유는, log를 취하면 상대적 오차를 구하는 것이기 때문에, 비싼 집이든 저렴한 집이든 공평하게 적용하기 위해서!
pandas 옵션 코드
봤던 것들인데 음 찾아보며 쓰자~
pd.options.display.float_format = '{:,.2f}'.format
pd.set_option('display.max_columns', None)
뭔가 하긴 했는데... 실습 하면서 다시 공부해봐야 익숙해질 듯 하다. 다시 열심히 해보자!