ML

[Feature Engineering] MI & Creating Features 연습하기

dontgive 2024. 3. 25. 13:19
728x90

새로운 Feature들을 발견해내기 위해서는 도메인 지식에 대한 이해와 더불어 이전 선행 연구들이 있는 지에 대해 살피는 것이 좋다. 또한 데이터 시각화를 통해 복잡한 관계를 단순화 할 방법이 있을 수 있기에 feature engineering 과정에서 시각화를 함께 진행하는 것이 좋다.

 

Mathematical Transforms

기본적인 사칙연산 등과 같은 연산의 경우 외에도 로그 변환 등을 할 때 활용하게 된다.

Counts

주로 Boolean 형태로 정의하게 되며 

 

df['feature1'] = df[['feature2', 'feature3', 'feature4']].sum(axis=1)

 

와 같은 형태로 변환할 수 있다.

Building-Up and Breaking-Down Features

시간에 대한 정보(SQL의 DATETIME)나 주소 정보와 같이 분리하거나 합쳐서 별도의 feature로 만들기도 한다.

Group Transforms

SQL 기준으로 비교해보자면 window function과 같은 변환을 의미한다.
SQL의 window function에서는 GROUP BY와 달리 하나의 집계를 나타내는 것이 아니라 Aggregate한 값을 열로 추가한다.
SUM(feature1)  OVER (PARTITION BY feature2 ORDER BY feature2 DESC) AS [name] 와 같은 기능을 한다.

 

Examples with dataset

https://www.kaggle.com/datasets/prevek18/ames-housing-dataset

 

kaggle에 있는 Ames Housing Dataset을 이용해 Mutual Information을 파악해보고 주요 변수들을 적절히 변형하여 모델 학습에 필요한 feature로 가공하는 과정이다.

 

Step1. Import required libraries and define functions

import matplotlib.pyplot as plt
import numpy as np
import pandas as pd
import seaborn as sns
from sklearn.feature_selection import mutual_info_regression
from sklearn.model_selection import cross_val_score
from xgboost import XGBRegressor

# Utility functions (MI)
def make_mi_scores(X, y):
    X = X.copy()
    for colname in X.select_dtypes(["object", "category"]):
        X[colname], _ = X[colname].factorize()
    # All discrete features should now have integer dtypes
    discrete_features = [pd.api.types.is_integer_dtype(t) for t in X.dtypes]
    mi_scores = mutual_info_regression(X, y, discrete_features=discrete_features, random_state=0)
    mi_scores = pd.Series(mi_scores, name="MI Scores", index=X.columns)
    mi_scores = mi_scores.sort_values(ascending=False)
    return mi_scores

def plot_mi_scores(scores):
    scores = scores.sort_values(ascending=True)
    width = np.arange(len(scores))
    ticks = list(scores.index)
    plt.barh(width, scores)
    plt.yticks(width, ticks)
    plt.title("Mutual Information Scores")

# Model Performance
def score_dataset(X, y, model = XGBRegressor()):
    
    # Label encoding for categoricals
    for colname in X.select_dtypes(['category', 'object']):
        X[colname], _ = X[colname].factorize()
    
    # Root Mean Squared Log Error
    score = cross_val_score(
        model, X, y, cv=5, scoring="neg_mean_squared_log_error",
    )
    score = -1 * score.mean()
    score = np.sqrt(score)
    return score

 

Step2. Mutual Information 영향 확인 및 독립변수들 간의 관계 파악

df = pd.read_csv("./ames.csv")

df.info()

 

X = df.copy()
y = X.pop("SalePrice")

# mi_scores 확인 결과
mi_scores = make_mi_scores(X, y)
plt.figure(dpi=100, figsize=(8, 5))
plot_mi_scores(mi_scores.head(20))

 

상위 20개로 선정된 변수들의 특징을 정리해보자면 다음과 같다.

  • Location과 관련된 변수 : Neighborhood
  • Size와 관련된 변수 : ~Area, ~SF, FullBath, GarageCars
  • Qualtity와 관련된 변수 : ~Qual 변수
  • Year와 관련된 변수 : YearBuilt, YearRemodAdd
  • Type과 관련된 변수 : Foundation or GarageType

~Area 관련된 변수들
~SF(Square Footage)와 관련된 변수들
~Qual(Quality)와 관련된 변수들

 

 

이 때 단순히 MI가 높다는 것은 독립변수들 사이의 영향은 반영되지 않기 때문에 MI가 낮은 변수들 중에서 유의미하게 MI가 높은 변수에 영향을 줄 수 있는 feature를 놓치지 않는 것도 중요하다. 

 

그 중 대표적으로 "한 가구 내에 몇 명이 거주하는지"를 나타내는 변수인 'BldgType'의 경우 종속변수와의 직접적인 영향도는 낮게 도출되었지만 독립 변수들과 유의미한 영향을 가질 수 있다.

sns.catplot(x="BldgType", y="SalePrice", data=df, kind="boxen", palette="bright", hue='BldgType')

BldgType에 따른 유의미한 분포 차이는 시각적으로 확인되지 않는다.

 

거주하는 인원수가 실제로 영향을 끼칠 수 있을 법한 변수들에는 대표적으로 생활 공간과 기간 등이 있을 수 있다.

ground living area를 의미하는 'GrLivArea'와 month sold를 의미하는 'MoSold'와 타겟인 'SalePrice'의 관계를 나타내는 회귀직선을 포함한 scatter plot인 Seaborn의 lmplot을 그릴 때 hue의 값을 'BldgType'으로 설정하여 각 경우에 잔차를 최소화하도록 적합되는 직선의 기울기에 유의미한 차이가 나타나는지 확인해볼 수 있다.

 

'BldgType'의 unique value가 5개 이므로 col_wrap 인자의 값을 5로 설정하여  시각화하면 아래와 같다.

feature = "GrLivArea"

sns.lmplot(
    x=feature, y="SalePrice", hue="BldgType", col="BldgType",
    data=df, scatter_kws={"edgecolor": 'w'}, col_wrap=5, height=3,
)

feature = "MoSold"

sns.lmplot(
    x=feature, y="SalePrice", hue="BldgType", col="BldgType",
    data=df, scatter_kws={"edgecolor": 'w'}, col_wrap=5, height=3,
)

 

두 개의 feature에 대해 확인해보게 되면 'MoSold'에는 거주 인원수가 큰 영향이 없는 것으로 보이지만, 생활 공간 면적에 해당하는 'GrLivArea'의 경우에는 'BldgType'에 따른 종속변수와의 선형 관계를 나타내는 직선의 기울기에 유의미한 차이가 존재하는 것을 확인할 수 있다. 따라서 이러한 경우 기본적으로 MI값이 높게 나타난 변수들의 특징을 가지는 변수들과 'BldgType'처럼 간접적으로 모델의 학습을 용이하게 할 수 있는 변수를 사용해 Feature Engineering을 진행해볼 수 있다.

 

Step3. Creating Features - Mathematical Transformation for Numerical Features

앞서 Dataset을 통해 .info() 메서드를 통해 각 Column의 Datatype을 확인해보았을 때 수치형 변수의 경우 정수형 14개, 실수형 19개의 변수가 존재하였다. 

*Lot Area : (Land or Terrain의 의미로) 주택이나 건물이 위치한 토지 혹은 부지의 면적을 나타내는 변수

*Gr Liv Area : 생활 공간의 면적을 나타내는 변수

*FirsFlrSF, SecondFlrSF : 1층과 2층의 면적을 나타내는 변수

*TotRmsAbvGrd : "Total Rooms Above Ground"의 약자로, 지상층에 있는 방의 총 개수를 나타내는 변수

*WoodDeskSF, OpenPorchSF, EnclosedPorch, Threeseasonporch, ScreenPorch : 외부 공간 면적 변수

 

대표적으로 위와 같은 수치형 변수들이 존재하는데,

 

(Ground Living Area) / (Lot Area) : 건물 부지 대비 실질적인 생활공간의 비율 (공간의 효율성 & 밀도)

 

(Total Surface inside the building) / (Total Rooms Above Ground) : 평균적으로 방 당 할당된 공간의 넓이

 

(Total Outside Area/Surface) : 건물 외부에 존재하는 공간의 넓이

 

위와 같은 수치적인 변환 과정을 거치면 기본적으로 관측가능한 데이터 뿐만 아니라 "효율성", "밀도", "평균적인 할당 공간", "외부공간" 등을 표현할 수 있다.

X_1 = pd.DataFrame()

X_1["LivLotRatio"] = X['GrLivArea']/X['LotArea']
X_1["Spaciousness"] = (X['FirstFlrSF']+X['SecondFlrSF'])/X['TotRmsAbvGrd']
X_1["TotalOutsideSF"] = X[['WoodDeckSF', 'OpenPorchSF', 'EnclosedPorch', 'Threeseasonporch', 'ScreenPorch']].sum(axis=1)

X_1.head()

Step3. Creating Features - 범주형 변수와의 Interaction 고려하기

Step2에서 시각화를 통해 MI 값은 낮지만 다른 독립변수인 'GrLivArea'에 영향을 주는 변수인 'BldgType'의 존재를 확인할 수 있었다.

 

즉 'BldgType' x 'GrLivArea' 의 형태로 각 'BldgType'를 기준으로 'GrLivArea'를 통해 가중치를 부여하는 방식으로 Feature를 만들어볼 수 있다.

 

pandas의 get_dummies를 사용할 때, prefix 옵션을 사용하여 Feature가 추가될 때 'BldgType'에서 파생된 feature라는 것을 확인할 수 있도록 해준다.

X_2 = pd.get_dummies(X.BldgType, prefix='Bldg')

X_2 = X_2.mul(df.GrLivArea, axis=0)

X_2

 

Step3. Creating Features - Count를 통해 변수 추가하기

*Porch : 집이나 건물의 입구 부분에 있는 지붕이 있는 공간 (ex. front porch)

 

Porch에 해당하는 공간의 경우 실질적인 생활 영역이라기 보다는 특색을 나타내게 하는 변수라고도 해석할 수 있기 때문에 데이터 상에 Porch가 존재하는 경우(면적이 0 초과), 집 내에 총 Porch에 해당하는 공간이 몇 개 존재하는 지도 집의 가격에 영향을 끼치는 요인이 될 수 있다. 따라서 이를 파악할 수 있는 새로운 PorchCnts 라는 변수를 추가해볼 수 있다.

X_3 = pd.DataFrame()
X_3["PorchCnts"] = X[[
    'WoodDeckSF',
    'OpenPorchSF',
    'EnclosedPorch',
    'Threeseasonporch',
    'ScreenPorch'
]].gt(0).sum(axis=1)

X_3

 

Step3. Creating Features - 범주형 변수 분해하기

*MSSubClass : type of dwelling을 나타내는 변수

list(df.MSSubClass.unique())

 

이렇게 텍스트로 된 변수의 경우 ML모델의 feature로 사용하기 위해 단순화 한 후에 인코딩 해줄 필요가 있기 때문에 underline(_)으로 구분된 가장 첫 단어만을 독립변수로 사용할 수도 있다. ("_"가 최소 한 번 등장하므로)

 

split의 경우 string을 handling 하기 위해 자주 사용하는 함수이지만 Pandas.DataFrame의 한 Column(Feature)에 대해 적용하게 될 경우 가장 앞 부터 Greedy 하게 동작하여 구분 기준 (여기서는 _)에 해당하는 캐릭터를 n개 찾아서 split하게 된다.

 

마지막 부분의 expand = False로 설정하게 될 경우 DataFrame 형태가 아닌 split을 실행한 list type의 리턴 값을 가지게 되기 때문에 expand = True로 설정해주어 split한 결과가 두 개의 컬럼을 가진 DataFrame으로 리턴되도록 한다. expand=True 옵션 사용 시 만들어지는 DataFrame의 경우 별도로 컬럼의 이름을 지정하지 않았기 때문에 0과 1이 부여되게 되고, 그 중 첫 번째 값에 해당하는 0 컬럼을 선택하여 기존의 긴 텍스트 값 대신 활용할 'MSClass'라는 변수로 저장해준다.

X_4 = pd.DataFrame()
X_4['MSClass'] = X.MSSubClass.str.split('_', n=1, expand=True)[0]
X_4

 

Step3. Creating Features - 동네(Neighborhood)에 따른 부지 면적(GrLivArea)

우리나라의 부동산을 보게 되었을 때 서울 내에서도 어느 구에 건물이 있는 지에 따라 그 가격이 크게 다르다. 특히 Mutual Information을 계산한 결과에서 두 번째로 높은 값을 나타낸 'Neighborhood'가 부동산의 가격에 영향을 미치는 경우를 생각해보면

 

'Neighborhood'에 따라, 'GrLivArea'가 더 작은 지역임에도 가격이 더 비싼 경우가 존재할 수 있다. 따라서 지역에 따른 중위 수준의 부지 면적을 통해 일반적으로 해당 지역의 면적이 가지는 영향력을 반영해볼 수 있다.

X_5 = pd.DataFrame()
X_5['MedNhbdArea'] = X.groupby(by='Neighborhood')['GrLivArea'].transform("median") # np.median
X_5

 

Step4. 학습에 활용할 Feature 합치고 Train Loss 계산하기

X_new = X.join([X_1, X_2, X_3, X_4, X_5])
# negative mean squared log error
score_dataset(X_new, y)

 

 

728x90

'ML' 카테고리의 다른 글

[Feature Engineering] Mutual Information이란?  (1) 2024.03.24