지난 포스트에 이어, 이번 글에서는 데이터의 특성, feature의 개요를 EDA를 통해 살펴보겠습니다.
지난 글:
[ML 대회 해설] Regression with an Abalone Dataset Public 8등 풀이 - 소개 (1)
전복 좋아하시나요? 저는 별로 안 좋아하는데요, 시장 갈 때 마다 가격보고 기함하는 식재료 중 하나입니다.특히 큰 놈은 부르는게 값이더라구요. 한국에서는 전복의 크기를 표현할 때, kg에
here-lives-mummy.tistory.com
Overview
전복의 나이는 어떻게 알 수 있을까요?
전복 패각의 나이테를 확인하면 됩니다.
전복은 나무처럼 해를 거듭할수록 패각에 나이테가 늘어납니다.
때문에 속살을 긁어내 패각 안쪽 나이테를 세어보면 나이를 알 수 있지요.
하지만 활전복을 출하하면서 나이를 알아보겠다고 패각을 뜯어볼 수도, 나이테를 일일이 세고 있을 수도 없는 노릇입니다.
따라서, 이 대회에선 전복의 여러 외적인 특성을 기반으로 전복의 나이 (= 패각 나이테 개수)를 예측하는 모델을 요구합니다.
주요 Feature
주어진 train.csv, test.csv의 feature는 다음과 같습니다:
feature name | dtype | 결측치 (train) | 결측치 (test) | 설명 |
id | int | 0 | 0 | 데이터 row별 식별 id |
Sex | object | 0 | 0 | 전복 성별 |
Length | float | 0 | 0 | 전복 패각 길이 |
Diameter | float | 0 | 0 | 전복 패각 직경 |
Height | float | 0 | 0 | 전복 패각의 높이 |
Whole weight | float | 0 | 0 | 전복 전체 무게 |
Whole weight.1 | float | 0 | 0 | 전복 무게 (처리 후) |
Whole weight.2 | float | 0 | 0 | 전복 무게 (처리 후 2) |
Shell weight | float | 0 | 0 | 패각 무게 |
Rings | int | 0 | X | 패각 나이테 개수 (=전복나이) (Target Feature) test.csv에선 공란 |
각 데이터의 row번호인 'id'와 target인 'Rings'를 제외하면 총 9개의 feature가 있습니다.
우리의 목표는 이 9의 feature로, 전복의 나이에 해당하는 'Rings'를 예측할 것입니다.
이 중 'Sex'를 제외하면 모두 numerical feature로 보입니다.
간혹 int, float type로 되어있으나 실제로는 categorical feature인 것들이 있는데요
(ex. 학급번호: 1, 2, 3... 으로 표시되어있으나 서순이 없고, 단순 categorize해서 번호를 매겼을 가능성이 높음)
다행히도 이번 대회에서는 없는 것 같습니다.
이번 대회에는 특히 길이와 무게 단위 feature가 많은데요,
이럴 경우 각 feature의 단위를 꼭 확인해야합니다.
데이터 출처 홈페이지에 들어가, 각 데이터의 단위를 살펴보겠습니다.
다행히도 원본 데이터에선 모든 feature의 단위가 통일되어있었습니다.
그래도 가끔 Kaggle로 데이터가 넘어오며 단위가 변경되는 경우가 있으니 EDA과정에서 다시 확인해보겠습니다.
표면 상으로는 모든 feature에 결측치가 없지만, 이는 데이터를 분석하며 다시 확인해보겠습니다.
실제로는 결측치이지만 '0'이나 'null', 'n/a' 등 문자열로 직접 표현해두었을 수도 있기 때문입니다.
이제 Feature Engineering을 위한 Insight를 얻기 위해, EDA를 해봅시다.
탐색적 데이터 분석 (EDA)
상관계수 분석
먼저, numeric feature와 target feature 간의 상관계수를 보며 insight를 얻어보겠습니다.
여느 대회들과 달리 매우 특이한 상관계수 분포를 가졌네요.
Target인 Rings와, row번호인 id를 제외한 모든 feature가 서로 아주아주 높은 상관계수를 보입니다.
이는 즉슨 feature 들이 모델을 만들 때, 서로 비슷한 역할을 해버려 모델이 과적합될 위험이 있습니다.
물론 상관계수가 높다고 하더라도 각 feature의 미세한 변동성으로 인해 좋은 feature가 될 수 있으므로 언제나 drop은 신중히 선택해야합니다.
Insight 1. 상관계수가 높은 feature 중에서는 몇 개를 drop해야할 수도 있다.
Target 분포
본격적으로 모든 feature를 살피기 전에 target의 분포를 보겠습니다.
unique 값부터 살펴보겠습니다.
전부 정수값이고, 특별한 이상치는 없는 듯 합니다.
다음은 Target값에 따른 데이터 수의 분포를 살펴보겠습니다.
그래프가 전반적으로 7~10 사이에 많이 몰려있고, target 값이 증가함에 따라 오른쪽으로 꼬리가 길게 남 걸 볼 수 있습니다.
잘 보면 약간 y = -xe^x 그래프와도 닮았습니다.
여기서 저는 target feature에 로그를 씌운다면 정규분포에 더 가깝게 표현할 수 있을 것 이라는 아이디어를 얻었습니다.
logscale을 씌워 다시 분포를 찍어보겠습니다.
*주의: Target값이 0일 때에 대비해 log가 아닌, log1p ( =log(x+1) ) 함수를 씌워야합니다. prediction값에는 역함수인 expm1 ( = e^(x-1) )를 씌우는 것도 잊지 말아야 하고요.
평균이 더 중앙에 가까워지며 정규분포에 가까운 형태를 보입니다.
일반적으로 머신러닝 모델의 경우, target의 분포가 고를수록 좋은 성능을 보입니다.
그러므로 target이 편향된 데이터라도, 전처리를 통해 그 분포를 조정한다면 모델의 성능을 대폭 향상하기도 합니다.
딥러닝에서는 보통 normalize를 하는데요,
Boosting 계열 모델을 사용한 머신러닝 대회에서는 이런 로그/지수함수 씌우기를 더 많이 사용합니다.
따라서, 저는 target에 np.log1p()를 씌워 모델이 log값을 예측하게 한 뒤 그 값에 다시 np.expm1()을 씌워보겠습니다.
Insight 2-1. target에 log를 씌우면 성능이 개선될 수도 있다.
Numeric Feature Overview
모든 feature의 value count, 평균, 최솟값, 중앙값, 최댓값 등을 살펴보겠습니다.
train.describe().T
'Length'와 'Diameter', 'Height'의 모든 값들은 scale이 비슷한 것으로 미루어보아, Kaggle에서 제공한 데이터에서도 셋의 단위가 동일한 듯 합니다. (원본데이터를 다운받아 살펴보면 더 확실히 알 수 있습니다)
'Whole weight', 'Whole weight.1', 'Whole weight.2', 'Shell weight' 도 모든 값들의 scale이 비슷한 것으로 보아, 넷의 단위가 동일한 것 같습니다.
이걸 토대로 어쩌면 'Whole weight.1', 'Whole weight.2'가 원본에 있던 ' Shucked_weight', 'Viscera_weight'가 아닌가 하는 추측도 할 수 있습니다.
* 실제로는 'Whole weight.1'이 'Shucked_weight' (속살 무게), 'Whole weight.2'가 'Viscera_weight' (내장 무게)입니다. 하지만 대회에서 일부러 feature 이름을 바꿔가며 숨긴만큼, 이번 해설에서는 이를 모른다는 전제 하에 진행해보겠습니다.
성별 (Sex)
전복의 성별입니다.
각 unique값 별 데이터 수, 그리고 unique에 따른 target의 평균값을 살펴보겠습니다.
F는 암컷(female), M은 수컷(Male)일 것입니다.
I는 UC Irvine의 원본 데이터를 찾아보니 치어(Infant)라고 합니다.
암컷과 수컷 간의 target mean은 그리 크지 않은 반면,
치어와 성체 간에는 차이가 매우 큽니다.
그렇다면, 암컷과 수컷을 합치고 치어를 따로 두어 새로운 feature 'Adult'를 만들어볼 수도 있을 것입니다.
Insight 2-2. 'Sex' feature에서 암컷과 수컷을 합쳐 'Adult' feature를 만들 수 있다.
Length
전복의 길이입니다.
'Length'에 따른 target 값의 count와 mean값 분포를 살펴보겠습니다.
Target의 평균값은 'Length'가 증가함에 따라 증가하는 추세를 보입니다.
특이하게 'Length'값이 0.553일 때 target값이 확 튀는 모습을 보여주는데요, 이 때의 데이터를 한 번 뽑아보겠습니다:
해당되는 데이터는 단 한 건이므로, 노이즈로 볼 수도 있을 것 같습니다.
따라서 이 때의 target 값을 잘 보정해준다면 모델성능의 소폭상승을 노려볼 수도 있을 것 같습니다.
Insight 2-3. Length=0.553일 때의 target값을 보정해줄 수 있을 것이다.
Diameter
전복의 직경입니다.
'Diameter'에 따른 target 값의 count와 mean값 분포를 살펴보면, 'Length'와 모습이 거의 비슷한 걸 볼 수 있습니다.
이 경우 둘 다 모델에 넣었을 때 과적합이 발생할 수도 있음을 고려해야 합니다.
Insight 2-4. 'Diameter'와 'Length' feature 모두 모델에 넣을 경우 과적합이 발생할 수도 있다.
Height
껍질을 떼지 않은 상태에서의 전복 높이입니다.
'Height'에 따른 target 값의 count와 평균값 분포를 살펴보겠습니다.
데이터가 'Height'가 특정값일 때에 몰려있습니다.
소수의 데이터가 중앙값을 훨씬 상회하는 곳에 분포하고 있는데요,
이는 일종의 이상치로 작용할 가능성이 있습니다.
따라서, 이를 적당히 보정하거나 데이터를 drop해볼 수 있을 것 같습니다. 물론 drop의 경우 언제나 신중히 해야합니다.
Insight 2-5. Height의 이상치를 보정하거나 아예 drop할 수 있을 것이다.
Whole weight
'Whole weight'는 패각과 속살을 합친 총 무게입니다.
'Whole weight' 기준 target 분포를 살펴보겠습니다.
'Whole weight'가 증가함에 따라 target역시 증가하는 모습을 보이고 있지만, 상당히 노이즈가 심합니다.
제거해주기 위해 round 함수를 사용해 소숫점 자리를 정리해보겠습니다.
df = train.copy()
df['Whole weight'] = df['Whole weight'].round(2)
# 결과 plot 출력할 것
이전보다 훨씬 깔끔한 target 분포를 보여주고, 어느정도 비슷한 값들 안에서 데이터를 grouping되었습니다.
Insight 2-6. Whole weight의 소숫점 밑 자리를 정리해서 노이즈를 제거해줄 수 있다.
Whole weight. 1
'Whole weight.1' 기준 target 분포를 보면 'Whole weight'와 마찬가지로 해당 feature가 증가함에 따라 target역시 증가하는 모습을 보이고 있지만, 노이즈가 상당합니다.
이것 역시 앞과 동일하게 round()를 사용해서 소숫점 밑 자리를 정리해보겠습니다.
df = train.copy()
df['Whole weight.1'] = df['Whole weight.1'].round(2)
# 결과 plot 출력할 것
역시 이전보다 훨씬 깔끔한 target 분포를 보여주고, 어느정도 비슷한 값들 안에서 데이터를 grouping되었습니다.
Insight 2-7. Whole weight.1의 소숫점 밑 자리를 정리해서 노이즈를 제거해줄 수 있다.
Whole weight. 2
'Whole weight.2' 기준도 앞의 두 feature와 동일합니다. 아 feautre 값이 증가함에 따라 target역시 증가하는 모습을 보이고 있지만, 노이즈가 적지 않은 것 역시 확인할 수 있습니다.
마찬가지로 round()를 사용해서 소숫점 밑 자리를 정리해보겠습니다.
df = train.copy()
df['Whole weight.2'] = df['Whole weight.2'].round(2)
# 결과 plot 출력할 것
이전보다 깔끔한 분포를 보입니다.
소숫점 둘째자리에서 반올림했을 때 디테일이 다소 날아가 완만한 곡선의 모양을 보이는데, 이는 feature를 그룹핑하여 모델에 좋은 영향을 끼칠수도, 디테일이 없어 모델에 악영향을 끼칠 수도 있을 것 같습니다.
특히 Average 그래프의 세로축을 보면 이전의 다른 Weight 그래프들보다 scale이 훨씬 낮기 때문에 조심해야 합니다.
다른 feature들보다 변동폭이 좁다보니 조금만 디테일이 날아가도 나쁜 feature가 될 수 있습니다.
Insight 2-8. Whole weight.2의 소숫점 밑 자리를 정리해서 노이즈를 제거해줄 수 있다.
Shell weight
물기를 제거했을 때 패각의 무게입니다.
Shell weight 역시 Average 값을 보면 노이즈가 끼어있습니다.
마찬가지로 rounding을 적용시켜보겠습니다.
df = train.copy()
df['Shell weight'] = df['Shell weight'].round(2)
# 결과 plot 출력할 것
두 경우 모두, 이전보다 깔끔한 모습을 보입니다.
Weight.2와 같이 소숫점 둘째자리에서 반올림했을 때 디테일이 많이 날아간 것 같아 보입니다만 Whole weight.2만큼 변동폭이 좁은 것은 아니기 때문에 비교적 걱정을 덜 해도 될 것 같습니다.
Insight 2-9. Shell weight의 소숫점 밑 자리를 정리해서 노이즈를 제거해줄 수 있다.
Weight feature 들을 보면 그래프 오른쪽 꼬리 부분에 위치하는 소수의 데이터에서 Average 값이 팍 떨어지는 모습을 보이는데요,
감이 좋으신 분이라면 이게 조금 이상함을 눈치채셨을 겁니다.
이렇게 간혹 소수의 극값이 이상치를 가지고 있을 때, 그 이상치가 평균에 영향을 끼쳐 prediction을 방해할 때가 있습니다.
예를들어, Shell weight 가 최대일 때의 데이터와, Shell weigth >= 0.749이상일 때의 데이터 44개를 뽑아보면 다음과 같습니다:
train[train['Shell weight'] >= 1]
train[train['Shell weight'] >= 0.749]
상위 44개의 데이터에선 대부분 target값이 10대 중반정도로 보이는데, max값일 때의 데이터는 9 하나 뿐이라, average값 역시 9가 되었습니다.
이럴 경우 이 값을 보정하거나 이 데이터를 drop하는 방식을 선택할 수 있습니다.
Insight 2-10. Weight들의 이상치를 보정하거나 아예 drop할 수 있을 것이다.
여기까지 Regression with an Abalone Dataset 대회의 EDA입니다.
다음 포스트에선 Feature Engineering 과정과 모델 튜닝 결과를 가져올게요.
다음 글:
[ML 대회 해설] Regression with an Abalone Dataset Public 8등 풀이 - Feature Engineering & Model Tuning
이번 글에서는 EDA에서 얻었던 Insights를 토대로 최종 선택한 Feature Engineering과 모델 튜닝을 보여드리겠습니다. 이전 글: [ML 대회 해설] Regression with an Abalone Dataset Public 8등 풀이 - EDA지난 포스트에
here-lives-mummy.tistory.com
도움이 되었다면 하트 눌러주세요 :)
구독하시면 더 많은 데이터사이언스 정보와 대회풀이를 보실 수 있습니다!
