특징(feature) 생성(1)

우수민·2021년 8월 7일
0

ML 관련 정리

목록 보기
3/10
post-thumbnail
post-custom-banner

모델과 특징

모델과 특징의 관계

  • 그레이디언트 부스팅 의사 결정 나무(GBDT) 특징

    • 수치의 크기(범위) 자체에는 의미가 없고, 크고 작은 관계에만 영향이 있다.
    • 결측값이 있어도 그대로 처리할 수 있다.
    • 결정 트리의 내부 반복 작업에 따라 변수 간 상호 작용을 반영한다.
  • 신경망 특징

    • 값의 범위에 영향을 받는다.
    • 결측값을 채워야 한다.
    • 앞 층의 출력을 결합하여 계산하는 연산으로 변수 간 상호작용을 반영한다.

결정 트리의 사고방식으로 생각하기

  • GBDT의 머신러닝 모델은 충분한 데이터가 있을 때 적절한 정보를 포함하는 데이터를 입력하면 변수간 상호작용이나 비선형 관계성도 정확히 반영하여 예측한다.
  • 그러나 기본적으로 존재하지 않는 정보를 입력 정보로 반영할 수 없다.
  • 여기서 모델에 주어진 정보를 활용하여 추가 정보를 제공하는 것이 바로 특징을 생성하는 작업이라고 상상해볼 수 있다.

결측값 처리

  • GBDT(xgboost, lightgmb) 라이브러리에서는 결측값을 그대로 사용할 수 있다. 따라서 GBDT를 사용할 때는 결측값을 그대로 사용하는 게 기본 옵션이다.
  1. 결측값인 채 처리하기
  2. 대푯값으로 결측값 채우기
  3. 다른 변붓로부터 결측값 예측하기
  4. 결측값으로 새로운 특징 만들기
  5. 데이터의 결측값 인식하기

수치형 변수 변환

  • 수치형 변수는 기본적으로 모델 입력에 그대로 사용할 수 있지만, 적절하게 변환하거나 가공하면 더 효과적인 특징을 만들어 낼 수 있다.
  • GBDT 등 트리 모델에 기반을 둔 모델에서 대소 관계가 저장되는 변환은 학습에 거의 영향을 주지 않는다. 다만, 통계량을 구해서 특징을 만들 때는 그 전단계 처리로서 변환 작업에 의미가 있다.
  1. 표준화
    • 예를 들어 선형 회귀나 로지스틱 회귀 등의 선형 모델에서는 값의 범위가 큰 변수일수록 회귀 계수가 작아지므로, 표준화하지 않으면 그러한 변수의 정규화가 여러워진다.
    • 신경망에서도 변수들 간의 값의 범위가 크게 차이나느 상태로는 학습이 잘 진행되지 않을 때가 많다. 또한 평균은 0에서 크게 벗어나지 않는게 좋다.
# 표준화
# -----------------------------------
# 데이터 읽어오기
train_x, test_x = load_data()
# -----------------------------------
from sklearn.preprocessing import StandardScaler

# 학습 데이터를 기반으로 복수 열의 표준화를 정의(평균 0, 표준편차 1)
scaler = StandardScaler()
scaler.fit(train_x[num_cols])

# 표준화를 수행한 후 각 열을 치환
train_x[num_cols] = scaler.transform(train_x[num_cols])
test_x[num_cols] = scaler.transform(test_x[num_cols])
  1. 최대-최소 스케일링
# Min-Max 스케일링
# -----------------------------------
# 데이터 읽어오기
train_x, test_x = load_data()
# -----------------------------------
from sklearn.preprocessing import MinMaxScaler

# 학습 데이터를 기반으로 여러 열의 최소-최대 스케일링 정의
scaler = MinMaxScaler()
scaler.fit(train_x[num_cols])

# 정규화(0~1) 변환 후의 데이터로 각 열을 치환
train_x[num_cols] = scaler.transform(train_x[num_cols])
test_x[num_cols] = scaler.transform(test_x[num_cols])
  1. 비선형변환
  • 표준화와 최소-최대 스케일링은 선형변환이므로 변수의 분포가 유동적일 뿐 형태 그 자체는 변하지 않는다. 한편으로는 비선형변환을 통해 변수의 분포형태를 바꾸는 편이 바람직한 경우도 있다.

  • 변수의 분포는 보통 어느 한 쪽으로 지나치게 치우치지 않는 게 좋다.

  • 로그 변환, log(x+1)변환, 절댓값 로그 변환

# 로그 변환
# -----------------------------------
x = np.array([1.0, 10.0, 100.0, 1000.0, 10000.0])

# 단순히 값에 로그를 취함
x1 = np.log(x)

# 1을 더한 뒤에 로그를 취함
x2 = np.log1p(x)

# 절댓값의 로그를 취한 후, 원래의 부호를 추가
x3 = np.sign(x) * np.log(np.abs(x))
  • 박스-칵스 변환, 여-존슨 변환
    • 로그 변환을 일반화한 박스-칵스 변환(즉, 박스-칵스 변환의 매개변수 λ=0일때 로그 변환)과 더불어, 음의 값을 갖는 변수에도 적용할 수 있는 여-존슨 변환도 존재한다.
    • 변환에 사용하는 매개변수는 되도록 정규분포에 근접하도록 라이브러리에서 최적의 값을 추정해줄 때가 많은 만큼 반드시 명시적으로 지정할 필요는 없다.
# Box-Cox 변환
# -----------------------------------
# 데이터 읽어오기
train_x, test_x = load_data()
# -----------------------------------

# 양의 정숫값만을 취하는 변수를 변환 대상으로 목록에 저장
# 또한, 결측값을 포함하는 경우는 (~(train_x[c] <= 0.0)).all() 등으로 해야 하므로 주의
pos_cols = [c for c in num_cols if (train_x[c] > 0.0).all() and (test_x[c] > 0.0).all()]

from sklearn.preprocessing import PowerTransformer

# 학습 데이터를 기반으로 복수 열의 박스-칵스 변환 정의
pt = PowerTransformer(method='box-cox')
pt.fit(train_x[pos_cols])

# 변환 후의 데이터로 각 열을 치환
train_x[pos_cols] = pt.transform(train_x[pos_cols])
test_x[pos_cols] = pt.transform(test_x[pos_cols])

# Yeo-Johnson변환
# -----------------------------------
# 데이터 읽어오기
train_x, test_x = load_data()
# -----------------------------------

from sklearn.preprocessing import PowerTransformer

# 학습 데이터를 기반으로 복수 열의 여-존슨 변환 정의
pt = PowerTransformer(method='yeo-johnson')
pt.fit(train_x[num_cols])

# 변환 후의 데이터로 각 열을 치환
train_x[num_cols] = pt.transform(train_x[num_cols])
test_x[num_cols] = pt.transform(test_x[num_cols])
  • 일반화 로그 변환
  • 절댓값 구하기
  • 제곱근 구하기
  • 제곱하기, 거듭제곱(n제곱)하기
  • 두 값(0 또는 1)의 이진변수화하기
  • 수치의 끝 수 얻기(소숫점 이하 등)
  • 반올림, 올림, 버림하기
  1. 클리핑
  • 수치형 변수에는 이상치가 포함되기도 하지만, 상한과 하한을 설정한 뒤 해당 범위를 벗어나는 값은 상한값과 하한값으로 치환함으로써 일정 범위를 벗어난 이상치를 제외할 수 있다.
  1. 구간 분할
  • 수치형 변수를 구간별로 나누어 범주형 변수로 변환하는 방법
  • 같은 간격으로 분할하는 방법, 분위점을 이용하여 분할하는 방법, 구간 구분을 짖어하여 분할하는 방법 등
# binning
# -----------------------------------
x = [1, 7, 5, 4, 6, 3]

# 팬더스 라이브러리의 cut 함수로 구간분할 수행

# bin의 수를 지정할 경우
binned = pd.cut(x, 3, labels=False)
print(binned)
# [0 2 1 1 2 0] - 변환된 값은 세 구간(0, 1, 2)를 만들고 원본 x의 값이 어디에 해당되는지 나타냄

# bin의 범위를 지정할 경우(3.0 이하, 3.0보다 크고 5.0보다 이하, 5.0보다 큼)
bin_edges = [-float('inf'), 3.0, 5.0, float('inf')]
binned = pd.cut(x, bin_edges, labels=False)
print(binned)
# [0 2 1 1 2 0] - 변환된 값은 세 구간을 만들고 원본 x의 값이 어디에 해당되는지 나타냄
  1. 순위로 분할
  • 수치형 변수를 대소 관계에 따른 순위로 변화하는 방법이다.
  • 단순위 순위로 바꾸는 것 외에, 순위를 행 데이터 수로 나누면 0부터 1의 범위에 들어가고, 값의 범위가 행 데이터의 수에 의존하지 않으므로 다루기 쉽다. 수치의 크기나 간격 정보를 버리고 대소 관계만을 얻어내는 방법이다.
# 순위로 변환
# -----------------------------------
x = [10, 20, 30, 0, 40, 40]

# 팬더스의 rank 함수로 순위 변환
rank = pd.Series(x).rank()
print(rank.values)
# 시작이 1, 같은 순위가 있을 경우에는 평균 순위가 됨
# [2. 3. 4. 1. 5.5 5.5]

# 넘파이의 argsort 함수를 2회 적용하는 방법으로 순위 변환
order = np.argsort(x)
rank = np.argsort(order)
print(rank)
# 넘파이의 argsort 함수를 2회 적용하는 방법으로 순위 변환
# [1 2 3 0 4 5]
  1. RankGauss
  • 수치형 변수를 순위로 변환한 뒤 순서를 유지한 채 반강제로 정규분포가 되도록 변환하는 방법이다.
  • 신경망에서 모델을 구축할 때의 변환으로서 일반적인 표준화보다 좋은 성능을 나태낸다고 한다.

# RankGauss
# -----------------------------------
# 데이터 읽어오기
train_x, test_x = load_data()
# -----------------------------------
from sklearn.preprocessing import QuantileTransformer

# 학습 데이터를 기반으로 복수 열의 RankGauss를 통한 변환 정의
transformer = QuantileTransformer(n_quantiles=100, random_state=0, output_distribution='normal')
transformer.fit(train_x[num_cols])

# 변환 후의 데이터로 각 열을 치환
train_x[num_cols] = transformer.transform(train_x[num_cols])
test_x[num_cols] = transformer.transform(test_x[num_cols])

범주형 변수 변환

  • 범주형 변수는 많은 머신러닝 모델에서 그대로 분석에 활용할 수 없으므로 모델마다 적합한 형태로 변환해야한다.

GBDT와 같이 결정 트리에 기반을 두는 모델에서는 레이블 인코딩으로 범주형 변수를 변환하는게 가장 편리하지만, 타깃 인코딩이 더 효과적일 때가 많다. 다만 타깃 인코딩은 데이터 정보 누출의 위험이 존재한다.
그 외의 모델에서는 원-핫 인코딩이 가장 전통적인 방식이다. 신경망의 경우에는 임베딩 계층을 변수별로 구성하는게 조금 번거롭지만 어쨌든 임베딩도 유효하다. 참고링크

  1. 원-핫 인코딩
  • get_dummies 함수와 sklearn.preprocessing의 OneHotEncoder 클래스를 사용할 수 있다.
  • OneHotEncoder 클래스에서 transform 메서드의 반환값은, 입력이 데이터프레임이라도 numpy 배열로 변환되므로 원래의 컬럼명이나 레벨 정보가 사라진다. 따라서 다루기가 까다롭다. 하지만 sparse 매개변수의 값을 True로 지정하여 희소행렬을 반환하므로, 다수의 레벨을 갖는 범주형 변수의 원-핫 인코딩을 이용해 메모리를 절약할 수 있다.
  • 범주형 변수의 레벨이 많을때는 아래와 같이 대응이 가능하다.
    1. 원-핫 인코딩 이외의 다른 인코딩 방법 검토하기
    2. 임의의 규칙을 활용한 그룹화로 범주형 변수의 레벨 개수 줄이기
    3. 빈도가 낮은 범주를 모두 '기타 범주'로 모아 정리하기
# one-hot encoding
# -----------------------------------
# 데이터 읽어오기
train_x, test_x = load_data()
# -----------------------------------

# 학습 데이터와 테스트 데이터를 결합하여 get_dummies를 통한 원-핫 인코딩을 수행
all_x = pd.concat([train_x, test_x])
all_x = pd.get_dummies(all_x, columns=cat_cols)

# 학습 데이터와 테스트 데이터의 재분할
train_x = all_x.iloc[:train_x.shape[0], :].reset_index(drop=True)
test_x = all_x.iloc[train_x.shape[0]:, :].reset_index(drop=True)

from sklearn.preprocessing import OneHotEncoder

# OneHotEncoder로 인코딩
ohe = OneHotEncoder(sparse=False, categories='auto')
ohe.fit(train_x[cat_cols])

# 가변수의 컬럼명 생성
columns = []
for i, c in enumerate(cat_cols):
    columns += [f'{c}_{v}' for v in ohe.categories_[i]]

# 생성된 가변수를 데이터 프레임으로 변환
dummy_vals_train = pd.DataFrame(ohe.transform(train_x[cat_cols]), columns=columns)
dummy_vals_test = pd.DataFrame(ohe.transform(test_x[cat_cols]), columns=columns)

# 나머지 변수와의 결합
train_x = pd.concat([train_x.drop(cat_cols, axis=1), dummy_vals_train], axis=1)
test_x = pd.concat([test_x.drop(cat_cols, axis=1), dummy_vals_test], axis=1)
  1. 레이블 인코딩(label encoding)
  • 각 레벨을 단순히 정수로 변환
  • 사전 순으로 나열했을 때의 인덱스 수치는 대부분 본질적인 의미가 없다. 따라서 결정 트리 모델에 기반을 둔 방법이 아닐 경우 레이블 인코딩으로 변환한 특징을 학습에 직접 적용하는 건 그다지 적절하지 않다.
  • 한편, 결정 트리에서는 범주형 변수의 특정 레벨만 목적변수에 영향을 줄 때도 분기를 반복함으로써 예측값에 반영할 수 있으므로 학습에 활용할 수 있다. 특히 GBDT 모델에서 레이블 인코딩은 범주형 변수를 변환하는 기본적인 방법이다.
# label encoding
# -----------------------------------
# 데이터 읽어오기
train_x, test_x = load_data()
# -----------------------------------
from sklearn.preprocessing import LabelEncoder

# 범주형 변수를 for문 루프하여 반복적으로 레이블 인코딩 수행
for c in cat_cols:
    # 학습 데이터에 근거하여 정의한 후에 데이터 변환
    le = LabelEncoder()
    le.fit(train_x[c])
    train_x[c] = le.transform(train_x[c])
    test_x[c] = le.transform(test_x[c])
  1. 특징 해싱(feature hashing)
  • 원-핫 인코딩은 변환한 뒤 특징의 수는 범주의 레벨 수와 같아지는데, 특징 해싱은 그 수를 줄이는 변환이다.
  • 변환 후의 특징 수를 먼저 정해두고, 해시 함수를 이용하여 레벨별로 플래그를 표시할 위치를 결정한다. 원-핫 인코딩에서는 레벨마다 서로 다른 위치에 플래그를 표시하지만 특징 해싱에서는 변환 후에 정해진 특징 수가 범주의 레벨 수보다 적으므로, 해시 함수에 따른 계산에 의해 다른 레벨에서도 같은 위치에 플래그를 표시할 수 있다.
# feature hashing
# -----------------------------------
# 데이터 읽어오기
train_x, test_x = load_data()
# -----------------------------------
from sklearn.feature_extraction import FeatureHasher

# 범주형 변수를 반복적으로 특징 해싱 처리
for c in cat_cols:
    # FeatureHasher의 사용법은 다른 encoder와 조금 달라짐
    fh = FeatureHasher(n_features=5, input_type='string')

    # 변수를 문자열로 변환한 후 FeatureHasher 적용
    hash_train = fh.transform(train_x[[c]].astype(str).values)
    hash_test = fh.transform(test_x[[c]].astype(str).values)

    # 데이터 프레임으로 변환
    hash_train = pd.DataFrame(hash_train.todense(), columns=[f'{c}_{i}' for i in range(5)])
    hash_test = pd.DataFrame(hash_test.todense(), columns=[f'{c}_{i}' for i in range(5)])

    # 원래의 데이터 프레임과 결합
    train_x = pd.concat([train_x, hash_train], axis=1)
    test_x = pd.concat([test_x, hash_test], axis=1)

# 원래의 범주형 변수 삭제
train_x.drop(cat_cols, axis=1, inplace=True)
test_x.drop(cat_cols, axis=1, inplace=True)
  1. 프리퀀시 인코딩(frequency encoding)
  • 각 레벨의 출현 횟수 혹은 출현 빈도로 범주형 변수를 대체하는 방법
  • 각 레벨의 출현 빈도와 목적변수 간에 관련성이 있을 때 유효
  • 주의 : 동률이 생길 수 있음
# frequency encoding
# -----------------------------------
# 데이터 읽어오기
train_x, test_x = load_data()
# -----------------------------------
# for문을 이용한 변수를 반복하여 프리퀀시 인코딩 수행
for c in cat_cols:
    freq = train_x[c].value_counts()
    # 카테고리 출현 횟수로 치환
    train_x[c] = train_x[c].map(freq)
    test_x[c] = test_x[c].map(freq)
  1. 타깃 인코딩(target encoding)
  • 목적 변수를 이용하여 범주형 변수를 수치형 변수로 변환하는 방법
  • 기본적인 개념은 범주형 변수의 각 레벨 그룹에서 목적 변수의 평균값을 학습 데이터로 집계하고 그 값으로 치환한다.

    타깃 인코딩은 매우 효과적인 특징이 될 수도 있지만 데이터에 따라서 별다른 효과가 없을 수도 있다. 특히 시계열 성향이 강한 테이터에서는 범주의 출현 빈도가 시간에 따라 변화하는 경우가 있으므로, 단순한 범주별 집계만으로는 시간적인 변화를 반영하지 못해 그리 좋은 특징이 되지 않을 때가 많다. 한편으로는 목적변수의 데이터 정보를 누출할 우려가 있으므로 주의해야 한다.

  1. 임베딩(Embedding)
  • 자연어 처리에서 단어나 범주형 변수와 같은 이산적인 표표현을 실수 벡터로 변환하는 방법
  • 신경망에서는 임베딩 계층에서 단어나 범주형 변수를 실수 벡터로 변환한다. 원-핫 인코딩을 하지 않아도 임베딩 계층에 범주형 변수를 부여하여 학습할 수 있다. 또한, 반대로 학습한 후에 임베딩 계층의 가중치, 즉 각 범주형 변수를 어떤 실수 벡터로 변환할지 추려냄으로써 모델이 학습한 범주형 변수의 각 레벨이 지니는 의미로 성질을 추출할 수 있다.
  1. 순서변수의 취급
  • 결정 트리 기반 모델에서는 본래 변수의 순서에만 의존하므로 서열을 그래로 정수로 치환해 수치형 변수로 다루면 된다. 그 밖의 모델에서는 수치형 변수라 볼 수도 있고, 순서 정보를 무시하고 범주형 변수로 볼 수도 있는 만큼 이들 두가지 변환 기법을 모두 사용할 수 있다.
  1. 범주형 변숫값의 의미 추출

날짜 및 시간변수 변환

  1. 날짜변수와 시간변수의 변환 포인트
  • 1~12월이 주기성 데이터라면 원-핫 인코딩 고려할 수 있음
  • 또한, 주기성을 지니는 변수를 원형으로 배치했을 때의 위치를 2개의 변수로 나타내는 방법이 있다.
  1. 날짜변수와 시간변수로 만드는 특징
  • 연도

    1. 연도 정보를 특징에 단순하게 추가
    2. 연도 정보를 특징에 추가하되, 테스트 데이터에만 존재하는 연도를 학습 데이터의 최신 연도로 치환
    3. 연도 정보를 특징에 포함시키지 않음
    4. 연도 정보와 월 정보를 사용하여 학습 데이터로서의 사용 기간을 제한
    • 월 정보를 특징에 포함하면 1년간의 계절성을 파악할 수 있다. 다만 2년 미만의 학습 데이터 밖에 없는 경우라면 주의해야한다.
    • 월 정보는 그대로 수치 데이터로서 특징으로 사용해도 되겠지만, 경계선을 계절 변동이 적은 월로 변경하거나 또는 원-핫 인코딩 및 타깃 인코딩 등의 방법을 활용할 수 있다.
    • 일 정보를 수치 정보로서 그대로 특징으로 삼으면, 월에 주기적인 경향이 있을 때 일 정보를 토대로 그러한 경향을 파악할 수 있다.
    • 일 정보는 1~31까지 연속적으로 변화할 뿐만 아니라 월초, 월말, 월급날 등 특정 날짜에 특징이 일어나는 경우가 많다. 이때 원-핫 인코딩을 하면 변수의 수가 지나치게 많아지므로, 특징이 있을 법한 날에만 해당일인지 아닌지의 두개 값만 갖는 변수를 만드는 게 좋다.
    • 혹은 1일은 0, 31일은 1로 나머지는 0~1사이로 변환도 가능하다.
  • 연월, 월일

    1. 주기성이 없는 시간적 경향을 더 세세하게 파악하고자 할때
      • 연월 : 연 X 12 + 월
      • 연월일 : 연 X 10000 + 월 X 100 + 일
    2. 연도를 세세하게 나누어 주기적인 경향을 더 자세히 파악하려는 계산
      • 주수 : 연초부터 카운팅한 주의수(1~53)
      • 월일 : 월 X 100 + 일
      • 일수 : 연초부터 카운팅한 날짜 수(1~366)
    3. 월을 4분기에 포함시켜 표시하는 방법
      : 데이터 양이 적고 과적합을 억제하고 싶을때 유용
      • 사분기
      • 상순, 중순, 하순
  • 요일, 공휴일, 휴일

  • 특정 기념일

  • 시, 분, 초

  • 시간차

    • 데이터 별로 다른 시점을 기준으로 삼아 시간차를 산출하는 방법
    • 데이터의 공통된 시점을 기준점으로 삼아 시간차를 계산하는 방법

참고 : 데이터가 뛰어노는 AI 놀이터, 캐글

profile
데이터 분석하고 있습니다
post-custom-banner

0개의 댓글