지난 글에서는 EDA를 통해 각 데이터의 특성을 분석하여
데이터 전처리 방법과 파생 Feature 아이디어를 얻어냈습니다.
지난 글:
[ML 대회 해설] Dacon 아파트 실거래가 예측 AI 경진대회 2등 풀이 - EDA (2): Object type Feature EDA
이번 포스트에선 object type feature의 EDA를 수행해보겠습니다. 지난 글: [ML 대회 해설] Dacon 아파트 실거래가 예측 AI 경진대회 3등 풀이 - EDA (1): Numeric Feature EDA오늘은 저번 포스트에서 뽑은 Numer
here-lives-mummy.tistory.com
이 글에서는 이를 바탕으로, 제가 제출한 최종 답안에서 Feature Engineering을 어떻게 하였는지를 설명하겠습니다.
실패한 Feature Engineering과 원인 분석은 추후에 다른 포스트로 가져오겠습니다.
개요
Numeric Feature의 EDA에서 얻었던 아이디어는 다음과 같습니다 (이전 글 참고) :
Insight 2-1. target에 log를 씌우면 성능이 개선될 수도 있다.
Insight 2-2. exclusive_use_area를 적절한 자리에서 반올림/버림/올림 하여 노이즈를 제거한다.
Insight 2-3. Target Feature / exclusive_use_area = 평당가격 feature을 만들어볼 수 있을 것이다.
Insight 2-4. transaction_year - year_of_completion = 아파트연식 을 파생 feature로 만들 수 있을 것이다.
Insight 2-5. apartment_id는 target feature 기준으로 그룹핑된 것이 아니기에 상대적으로 성능을 대폭 하락시킬 확률이 매우 낮다.
Insight 2-6. transaction_year_month를 각각 transaction_year, transaction_month로 나눌 수 있을 것이다.
Object Type Feature의 EDA에서 얻었던 아이디어는 다음과 같습니다 (이전 글 참고):
Insight 3-1. City는 서울을 1로, 부산을 0으로 encoding한다.
Insight 3-2. Dong을 City + Dong 형식으로 바꾸어, 동명이동이 하나로 묶이는 것을 방지한다.
Insight 3-3. Jibun은 addr_kr과 공유하는 속성이 있어, 둘 중 하나를 골라야 할 수도 있다.
Insight 3-4. Addr_kr 는 dong + jibun + apt와 거의 똑같은 패턴을 가진 feature이다. addr_kr과 다른 세 feature 중 하나를 택해야 할 수 있다.
Insight 3-5. apt는 속성값이 다양하고 count가 1인 것들이 많아, 비슷한 값들끼리 그룹핑을 해줄 필요가 있다.
Insight 3-6. Transaction_date는 1~10, 11~20, 21~말일로 이루어져있으나, 매달 말일이 달라 실제보다 많은 unique값을 갖게 되었으니 이를 하나로 그룹핑해줄 수 있다.
여기서 잠시, Feature Engineering을 하지 않은, 날 것의 numeric feaure만 넣은 Baseline Validation score은 다음과 같습니다: (이전 글 참고)
LightGBM | XGBoost | CatBoost |
9,697.93027 | 6,990.93174 | 13,356.58443 |
이전 글에서도 언급했듯이, 저는 GPU를 사용하기 위해 LightGBM을 제외한, XGBoost와 CatBoost 두 가지 모델만 사용하겠습니다.
Feature Engineering
1. Object Type Feature 추가
City 추가
Insight 3-1. City는 서울을 1로, 부산을 0으로 encoding한다.
에서 영감을 받아, city를 sklearn이 제공하는 TargetEncoder를 활용해 target의 평균값으로 encoding 해주었습니다.
Q. Category의 unique값이 2개일 때, Binary Encoding과 Target encoding 중 어느 것이 더 좋은 성능을 내는가?
A. Boosting 계열 모델에서는 둘 다 똑같은 성능을 냅니다. 특정 pivot 기준으로 Binary tree를 그리는 Boosting model 특성상 1/0으로 encoding하든, 0.5/0.2로 encoding하든 트리는 똑같이 형성될 것이기 때문입니다.
* 예외: Target encoding 결과 서로 같은 값으로 encoding 될 때.
적용 시, Validation score는 다음과 같이 나옵니다:
Feature 수 | XGBoost | CatBoost |
6 | 5,937.77468 | 10,960.81271 |
두 모델에서 모두 성능이 개선된 것을 확인할 수 있습니다.
Dong 수정 & 추가
Insight 3-2. Dong을 City + Dong 형식으로 바꾸어, 동명이동이 하나로 묶이는 것을 방지한다.
Dong을 City + Dong 형태로 바꾸어 중복을 피한 후, encoding해주겠습니다.
Dong의 unique value는 477개에 달하므로 One-hot encoding은 적절치 않습니다.
그렇다고 Label encoding을 사용한다면 서로 관계없는 행정동 간의 vector 거리차로 인한 편향이 생길 수 있습니다.
따라서, city와 마찬가지로 sklearn.TargetEncoder를 사용해 encoding 해주었습니다.
적용한 결과 validation score:
Feature 수 | XGBoost | CatBoost |
7 | 4,425.36267 | 7,012.81725 |
두 모델에서 모두 성능이 대폭 개선되었습니다.
2. 파생 Feature Engineering
Exclusive_use_area 노이즈 제거
Insight 2-2. exclusive_use_area를 적절한 자리에서 반올림/버림/올림 하여 노이즈를 제거한다.
를 기반으로, exclusive_use_area를 정리한 새 feature area_round 를 추가해주었습니다.
정리하는 방법은 크게 세 가지가 있습니다:
1. 반올림
2. 버림
3. 올림
또, 어떤 자릿수까지 정리할 것인지를 선택하는 것도 매우 중요합니다.
실험 결과, 10의 자리 미만을 버릴 때 성능이 가장 좋았습니다.
적용한 결과 validation score:
Feature 수 | XGBoost | CatBoost |
8 | 4,111.71911 | 6,688.78941 |
그런데, 무언가 이상합니다.
눈치가 빠른 분들이라면 다음과 같은 의문이 생길 거예요 -
"Exclusive_use_area와 area_round는 유사한 속성을 갖는 feature가 되는 것 아닌가? 그렇다면 모델이 비슷한 feature 두 개를 참고하는 꼴이 되는데... 과적합의 위험이 있는 것은 아닌가? 둘 중 하나 Drop해야 하는 것은 아닌가?"
이에 대한 답변은 다음과 같습니다:
Q. 왜 exclusive_use_area를 area_round로 대체하는 것이 아니라, exclusive_use_area와 area_round 모두를 유지했는가?
A. 보통 서로 높은 상관관계를 갖는 feature가 두 개 이상 존재하면 모델의 과적합 및 특정 feature의 feature 중요도 감소를 우려하곤 합니다. 때문에 이 경우 feature를 drop하기도 하지요.
하지만, Feature engineering을 할 때, 일반적으로 feature drop은 맨 마지막에 해주는 것이 좋습니다.
이는 feature drop 역시 하나의 데이터유실로 볼 수 있기 때문입니다.
(이상치/오류치로 보이는 데이터 drop을 모델학습 가장 마지막의 마지막 단계에 해보는 것과 같은 이치입니다.)
실제로 이 버전에서 두 모델의 Feature 중요도를 살피면 우려와 달리 생각보다 area_round가 모델에서 큰 역할을 차지하고 있다는 걸 알 수 있습니다.
평당 가격
Insight 2-3. Target Feature / exclusive_use_area = 평당가격 feature을 만들어볼 수 있을 것이다.
(집값 / 아파트 면적)을 계산하면, 그 아파트의 평당 가격을 구할 수 있습니다.
하지만, test 데이터셋은 train과 달리 Target값인 집값이 비어있기 때문에, 이런 식으로 평당 가격을 찾기는 불가능합니다.
따라서, 저는 아파트 단위로 평당가격을 책정한 feature use_area_price를 만들어주었습니다.
1. trainset에서 모든 row의 평당가격인 'use_area_price_train'을 구한다.
2. trainset을 apartment_id값으로 그룹핑하여 각 아파트의 'use_area_price_train' 평균값인 'use_area_price'를 구한다.
3. apartment_id를 기준으로 testset에 'use_area_price'를 할당한다.
적용한 결과 validation score:
Feature 수 | XGBoost | CatBoost |
10 | 3,492.32625 | 4,286.83756 |
3. 수치 조정하기
Apt_id로 Target Encoding
apt_id 별로 묶어서 Target Encoding을 해주겠습니다.
그 전에, trainset에는 없는 apartment_id가 testset에는 있는 지를 확인해보겠습니다.
train_ids = set(train.apartment_id.unique())
test_ids = set(test.apartment_id.unique())
unique_in_test = test_ids - train_ids
len(unique_in_test)
49개나 있는 것을 확인할 수 있습니다.
이 결측치를 채우기 위해 target encoding 과정에서 smoothing을 할 수도 있겠으나,
저는 조금 더 데이터와 연관 있는 feature로 만들기 위해 결측치를 dong으로 target encoding 한 값으로 대체하였습니다.
Apt Grouping
Insight 3-5. apt는 속성값이 다양하고 count가 1인 것들이 많아, 비슷한 값들끼리 그룹핑을 해줄 필요가 있다.
train을 apt 값으로 그룹핑하여 각 unique값에서 데이터 개수를 기준으로 apt 값을 정렬해보았습니다.
train.apt.value_counts().head(50)
df = train.apt.value_counts().reset_index()
df.columns = ['apt', 'count']
df['count'] = df['count'].astype(int)
df[df['count'] < 100].tail(60)
모든 값을 특정 기준으로 grouping하는 것은 되려 데이터의 특성을 해칠 수 있으므로, 상대적으로 count 수가 적은 (= count가 n개 미만인) 아파트를 count가 큰 아파트에 편입시키는 방법은 선택하였습니다.
따라서, 최적의 성능을 낼 수 있는 그룹핑 기준값 n과 편입시킬 수 있는 아파트 리스트를 실험을 통해 찾아내었습니다.
기준값 n은 여러 숫자를 grid-search하며 찾아내면 됩니다.
아파트 리스트를 작성할 때 '현대'와 '현대3' 처럼 서로 유사해 보이는 것은 하나로 묶어버리는 실수를 범할 수 있는데요,
target의 mean값으로 이 둘을 비교해보면 실제로는 서로 완전히 다른 것을 알 수 있습니다.
따라서, 저는 아파트 이름이 비슷하더라도 오직 target mean 값을 기준으로 unique 값을 구분하여 리스트를 작성했습니다.
또한, 아파트 count가 작은 순으로 데이터를 정렬해보면 유독 빌라 매물이 많은 것 을 알 수 있습니다.
df = train[train['apt'].str.contains('빌라', na=False)]
graph_count_mean('apt', df)
정리해보면
- 이름에 '빌', '빌라' 가 들어가있음.
- 대체적으로 거래 건수가 적음.
- 아파트에 비해 낮은 거래가를 보임.
따라서, 저는 이들 중 count값이 n인 것들은 모두 '빌라'로 한 번에 그룹핑할 수 있도록 바꾸었습니다.
여기서 주의해야 할 것이 있는데, 이름에 '빌라'가 포함되었는데, 빌라가 아닌 매물이 존재합니다.
'빌라트'는 검색해보면 아파트인 걸 알 수 있는데요,
거래건수가 상당히 적은 데다가 거래가격이 '빌라'로 표기되는 다른 매물들과 가격차가 크게 없어 무시하였습니다.
('빌'이 들어가는 매물 중에서는 빌라가 아닌 것이 꽤 많아 제외하였습니다.)
제가 찾아낸 최적의 기준값 n과 아파트 리스트는 다음과 같습니다:
# 기준 거래건수
n = 60
# 아파트 리스트
li = ['빌라', '현대1','현대2', '현대3', '현대', '한신', '대우', '신동아', '동아', '두산', '주공',
'래미안', '롯데캐슬', '롯데', '아크로' , '엘에이치', 'e-편한세상', '훼미리',
'성원', '미성', '엘지', '한양', '리센츠', '중앙하이츠']
# 거래건수가 적은 매물의 apt값을 보정
low_count_apt = train['apt'].value_counts()[train['apt'].value_counts() < n].index
for df in [train, test]:
for apt_name in li:
df.loc[
df['apt'].isin(low_count_apt) & df['apt'].str.contains(apt_name, na=False),
'apt'
] = apt_name
이제 이 apt에 sklearn.TargetEncoder을 적용시켜주었습니다.
결과는 다음과 같습니다:
Feature 수 | XGBoost | CatBoost |
11 | 3,482.44039 | 4,246.18452 |
두 모델에서 성능이 소폭 올랐습니다.
여기에서 다시 한 번, Feature 중요도와 상관계수를 확인해보겠습니다.
Feature 중요도
두 모델에서 공통적으로 apt는 큰 기여도를 보이고 있지 않으나, 작은 비율이나마 차지하며 제값을 잘 하고 있는 걸 볼 수 있습니다.
apt feature의 경우 너무 세분화가 되어있고, count값이 1인 것이 너무 많아 모델에 활용하는 것이 불가능에 가까웠기 때문에, 이 정도만 해도 장족의 발전이라고 볼 수 있을 겁니다.
상관계수
apt_id_mean은 target feature와 0.93으로 매우 높은 상관계수를 보이고 있습니다.
하지만 이는 되려 과적합을 일으킬 수 있으므로 유의해야합니다.
요약
정리하면, 제가 선택한 방법은 다음과 같습니다:
1. city target encoding
2. dong을 city + dong으로 변환하여 target encoding
3. use_area_price
2. exclusive_use_area를 10의 자리 미만을 버림한 area_round feature 추가
5. apt_id_mean = add_mean_fts로
6. count수가 적은 apt 값을 count가 큰 apt 중 유사한 것으로 바꾸고, 빌라는 '빌라'로 그룹핑한다.
여기까지 따라오시느라 수고 많았습니다!
다음 글에서는 Feature Engineering으로 완성한 모델을 어떻게 튜닝하였는지를 설명하겠습니다.
다음 글:
[ML 대회 해설] Dacon 아파트 실거래가 예측 AI 경진대회 2등 풀이 - Model Tuning
그간 따라오시느라 고생 많으셨습니다! 이제 거의 다 왔습니다. 지난 글에서는 EDA로 얻었던 insight를 기반으로 Feature Engineering을 진행했습니다.지난 글: [ML 대회 해설] Dacon 아파트 실거래가 예
here-lives-mummy.tistory.com
도움이 되었다면 하트를 눌러주세요 :)
구독하시면 더 많은 데이터사이언스 정보와 대회풀이를 보실 수 있습니다!
