Open In Colab

데이터 불러오기

from google.colab import drive
drive.mount('/content/drive')
Drive already mounted at /content/drive; to attempt to forcibly remount, call drive.mount("/content/drive", force_remount=True).
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

from sklearn.preprocessing import StandardScaler

import warnings
warnings.filterwarnings("ignore")

path = '/content/drive/MyDrive/Penguin/'

train = pd.read_csv(path + 'train.csv')
test = pd.read_csv(path + 'test.csv')
sample_submission = pd.read_csv(path + 'sample_submission.csv')
train.head()
id Species Island Clutch Completion Culmen Length (mm) Culmen Depth (mm) Flipper Length (mm) Sex Delta 15 N (o/oo) Delta 13 C (o/oo) Body Mass (g)
0 0 Gentoo penguin (Pygoscelis papua) Biscoe Yes 50.0 15.3 220 MALE 8.30515 -25.19017 5550
1 1 Chinstrap penguin (Pygoscelis antarctica) Dream No 49.5 19.0 200 MALE 9.63074 -24.34684 3800
2 2 Gentoo penguin (Pygoscelis papua) Biscoe Yes 45.1 14.4 210 FEMALE 8.51951 -27.01854 4400
3 3 Gentoo penguin (Pygoscelis papua) Biscoe Yes 44.5 14.7 214 FEMALE 8.20106 -26.16524 4850
4 4 Gentoo penguin (Pygoscelis papua) Biscoe No 49.6 16.0 225 MALE 8.38324 -26.84272 5700

혹시 이 코드를 그대로 사용하신다면 path만 파일이 저장되어있는 주소로 바꾸시면 됩니다.

print(train.shape)
print(test.shape)
(114, 11)
(228, 10)

우선 id포함 변수 개수가 10개이며 데이터 개수가 114, 228개로 다른 대회에 비해 적은 것을 알 수 있어요.

복잡한 모델을 사용해 과적합이 되는 것을 많이 조심해야겠습니다.

또 하나 특이한 점은 테스트 데이터가 약 2배 더 많네요.

기본변수 설명

train['Species'].value_counts()
Gentoo penguin (Pygoscelis papua)            48
Adelie Penguin (Pygoscelis adeliae)          41
Chinstrap penguin (Pygoscelis antarctica)    25
Name: Species, dtype: int64
train['Island'].value_counts()
Biscoe       57
Dream        44
Torgersen    13
Name: Island, dtype: int64
train['Sex'].value_counts()
MALE      56
FEMALE    55
Name: Sex, dtype: int64
train.drop(['id'], axis = 1, inplace = True)
test.drop(['id'], axis = 1, inplace = True)

discrete_names = ['Species', 'Island', 'Clutch Completion', 'Sex']
continuous_names = ['Culmen Length (mm)', 'Culmen Depth (mm)', 'Flipper Length (mm)', 'Delta 15 N (o/oo)', 'Delta 13 C (o/oo)']

id : 데이터를 구분하기 위한 고유값, Species : 펭귄의 종(총 3개 종이 있음), Island : 샘플이 수집된 근처 섬(총 3개 섬이 있음)

Clutch Completion : 팽귄 둥지 알 개수가 2개인 경우 Yes, Culmen Length/Culmen Depth : 펭귄 부리의 가로/세로 길이

Flipper Length : 펭귄 날개 길이, Sex : 펭귄 성별, Delta 15 N : 토양과 관련된 동위원소 비율, Delta 13 C : 먹이에 관련된 동위원소 비율

id는 당연히 의미가 없으니 제외하고 Species/Island/Sex은 이산형 변수, 나머지 값은 연속형 변수 입니다.

결측치 여부

train.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 114 entries, 0 to 113
Data columns (total 10 columns):
 #   Column               Non-Null Count  Dtype  
---  ------               --------------  -----  
 0   Species              114 non-null    object 
 1   Island               114 non-null    object 
 2   Clutch Completion    114 non-null    object 
 3   Culmen Length (mm)   114 non-null    float64
 4   Culmen Depth (mm)    114 non-null    float64
 5   Flipper Length (mm)  114 non-null    int64  
 6   Sex                  111 non-null    object 
 7   Delta 15 N (o/oo)    111 non-null    float64
 8   Delta 13 C (o/oo)    111 non-null    float64
 9   Body Mass (g)        114 non-null    int64  
dtypes: float64(4), int64(2), object(4)
memory usage: 9.0+ KB
train[train['Sex'].isnull()]
Species Island Clutch Completion Culmen Length (mm) Culmen Depth (mm) Flipper Length (mm) Sex Delta 15 N (o/oo) Delta 13 C (o/oo) Body Mass (g)
6 Adelie Penguin (Pygoscelis adeliae) Torgersen Yes 42.0 20.2 190 NaN 9.13362 -25.09368 4250
8 Adelie Penguin (Pygoscelis adeliae) Torgersen Yes 34.1 18.1 193 NaN NaN NaN 3475
70 Gentoo penguin (Pygoscelis papua) Biscoe Yes 46.2 14.4 214 NaN 8.24253 -26.81540 4650
train[train['Delta 15 N (o/oo)'].isnull()]
Species Island Clutch Completion Culmen Length (mm) Culmen Depth (mm) Flipper Length (mm) Sex Delta 15 N (o/oo) Delta 13 C (o/oo) Body Mass (g)
8 Adelie Penguin (Pygoscelis adeliae) Torgersen Yes 34.1 18.1 193 NaN NaN NaN 3475
18 Adelie Penguin (Pygoscelis adeliae) Dream No 39.8 19.1 184 MALE NaN NaN 4650
109 Adelie Penguin (Pygoscelis adeliae) Torgersen Yes 36.6 17.8 185 FEMALE NaN NaN 3700

우선 변수들의 결측치 여부를 info 함수를 이용해서 알아봤습니다.

3개 행에서 Sex와 2개의 Delta 변수가 결측치로 관측되었습니다.

6,8,70 번 관측치가 Sex변수가 NULL 값이고 8, 18, 109번 관측치가 2개의 Delta 변수에 대해서 NULL 값이 나왔습니다.

test.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 228 entries, 0 to 227
Data columns (total 9 columns):
 #   Column               Non-Null Count  Dtype  
---  ------               --------------  -----  
 0   Species              228 non-null    object 
 1   Island               228 non-null    object 
 2   Clutch Completion    228 non-null    object 
 3   Culmen Length (mm)   228 non-null    float64
 4   Culmen Depth (mm)    228 non-null    float64
 5   Flipper Length (mm)  228 non-null    float64
 6   Sex                  222 non-null    object 
 7   Delta 15 N (o/oo)    219 non-null    float64
 8   Delta 13 C (o/oo)    220 non-null    float64
dtypes: float64(5), object(4)
memory usage: 16.2+ KB

테스트 데이터에서도 위에 언급한 3개의 변수들이 일부 NULL 값을 가진걸 확인 했습니다.

소규모 데이터이기 때문에 조금 신중하게 접근할 필요가 있겠습니다.

연속형 변수 관찰

train['Body Mass (g)'].hist()
<matplotlib.axes._subplots.AxesSubplot at 0x7fe29d33c710>

반응변수인 몸무게의 히스토그램입니다. 이상치가 있어보이진 않고, 정규분포와 꽤 유사해보입니다.

plt.figure(figsize=(20,15))
plt.suptitle("Histogram", fontsize=40)

for i in range(len(continuous_names)):
    plt.subplot(2,3,i+1)
    plt.title(continuous_names[i])
    plt.hist(train[continuous_names[i]])

(데이콘 운영자님이 작성하신 코드를 일부 참고했습니다.)

연속형 변수의 히스토그램을 그렸을 때 이상치도 크게 없어보이고 나름(?) 정규성을 근접하게라도 따르는 것 같습니다.

하지만 각 변수 마다 스케일이 다르기 때문에 표준화가 필요할 것 같아요.

plt.figure(figsize=(15,10))
ax = sns.heatmap(train.drop(discrete_names, axis = 1).corr(), annot=True)
plt.show()

모든 변수가 반응변수(Body Mass)와 꽤 높은 상관관계를 가지고 있습니다. 딱히 버릴 변수는 없을 것 같아요.

또한 설명변수끼리도 상관관계가 어느정도 있는 것 같아요. 추후에 차원축소(PCA)를 적용할 수도 있겠습니다.

이산형 변수 관찰

plt.figure(figsize=(20,15))

for i in range(len(discrete_names)):
    plt.subplot(2,2,i+1)
    plt.xlabel(discrete_names[i])
    plt.ylabel('Body Mass (g)')
    sns.violinplot(x= train[discrete_names[i]], y= train['Body Mass (g)'])
plt.tight_layout(rect=[0, 0.03, 1, 0.95])
plt.show()

(데이콘 운영자님의 코드를 참고했습니다.)

각각의 이산형 변수로 구분한 반응변수를 그래프로 나타냈습니다. 쉽게 얘기해서 이산형 변수와 반응변수의 관계를 알 수 있습니다.

네 변수 모두 유의미하게 반응변수에 영향을 끼친다고 볼 수 있습니다. 즉 버릴변수는 없어보입니다.

데이터 전처리

train.fillna(train.mean(), inplace = True)
test.fillna(train.mean(), inplace = True)
train = pd.get_dummies(train)
test = pd.get_dummies(test)

print(train.shape)
print(test.shape)
(114, 16)
(228, 15)

두 Delta 변수 결측값은 연속형이기 때문에 평균값으로 채우기로 했습니다.

그리고 이산형 변수는 모두 get_dummies 함수를 사용해서 원-핫 인코딩 처리 했습니다.

라벨인코딩에 경우 간단하나, 이 변수들은 순서형 자료가 아닌 범주형 자료이기 때문에 사용이 부적절하다고 생각했습니다.

이 경우 Sex 변수 결측값은 Sex_men, Sex_women 어느 경우에도 속하지 않기 때문에 두 행 전부 0으로 처리됩니다.

from sklearn.preprocessing import StandardScaler

scaler = StandardScaler()

train_scaler = scaler.fit_transform(train[continuous_names])
train[continuous_names] = pd.DataFrame(data=train_scaler, columns=continuous_names)

test_scaler = scaler.transform(test[continuous_names])
test[continuous_names] = pd.DataFrame(data=test_scaler, columns=continuous_names)

train[continuous_names].head()
Culmen Length (mm) Culmen Depth (mm) Flipper Length (mm) Delta 15 N (o/oo) Delta 13 C (o/oo)
0 1.016685 -0.887255 1.161653 -0.775548 0.630951
1 0.922318 1.027037 -0.209242 1.601553 1.629486
2 0.091884 -1.352893 0.476205 -0.391149 -1.533908
3 -0.021357 -1.197680 0.750384 -0.962206 -0.523568
4 0.941191 -0.525091 1.504376 -0.635514 -1.325731

앞서 말한대로 연속형 변수에 경우 변수간 스케일을 맞춰주기 위해 StandardScaler를 사용했습니다.

StandardScaler는 평균 0, 분산 1이 되도록 값을 바꿔주기 때문에 변수간 스케일이 동등해집니다.

여기서 저는 '테스트 데이터(test.csv)를 학습에 사용' 하지 않기 위해 트레인 데이터만 스케일링에 사용했는데요.

테스트 데이터를 스케일링에 사용된다면 엄밀히 말해서 data leakage에 해당할 수도 있을거 같습니다.

이 부분은 제가 문의게시판에 올려보겠습니다.

(윗 코드 결측치를 평균 대치 한 부분도 train 평균 값만을 사용했는데 같은 이치입니다.)

train_label = train['Body Mass (g)']
train.drop(['Body Mass (g)'], axis = 1, inplace = True)

모델 적합을 위해 반응변수를 따로 label로 빼줍니다.

간단한 모델 적합

from sklearn.linear_model import Ridge
from sklearn.model_selection import cross_val_score

alphas = [0,0.1,1,10,100]

for alpha in alphas:
    ridge = Ridge(alpha=alpha)
    
    neg_mse_scores = cross_val_score(ridge, train, train_label, scoring = 'neg_mean_squared_error', cv = 5)
    avg_rmse = np.mean(np.sqrt(-neg_mse_scores))
    print('alpha 값 ', alpha, '일때 평균 rmse :', np.round(avg_rmse,4))
alpha 값  0 일때 평균 rmse : 339.4875
alpha 값  0.1 일때 평균 rmse : 337.4481
alpha 값  1 일때 평균 rmse : 330.4727
alpha 값  10 일때 평균 rmse : 329.2807
alpha 값  100 일때 평균 rmse : 405.4389

릿지 회귀 모델을 사용하였습니다. 교차검증을 통해 최적의 알파(하이퍼파라미터)를 찾는 과정입니다.

릿지 회귀를 간단히 설명하면 일반 선형 회귀에 응용으로 회귀계수 크기를 패널티로 부여한 모델입니다.

전반적으로 회귀계수 크기가 수축하는 효과를 보이며 과적합을 방지할 수 있습니다.

다만 릿지 회귀는 입력 특성 스케일에 민감하기에 앞서 스케일링 작업을 해주었습니다.

ridge = Ridge(alpha = 10)
ridge.fit(train, train_label)
sample_submission['Body Mass (g)'] = ridge.predict(test)

sample_submission.to_csv('Penguin_final_1.csv',index=False)

앞서 교차검증으로 나온 최적의 알파 값은 10이기 때문에 이를 대입해서 모델을 만듭니다.

이 모델에 Public MSE 값은 309.92 정도가 나옵니다.

from sklearn.linear_model import Lasso

alphas = [0,0.1,1,10,100]

for alpha in alphas:
    lasso = Lasso(alpha=alpha)
    
    neg_mse_scores = cross_val_score(lasso, train, train_label, scoring = 'neg_mean_squared_error', cv = 5)
    avg_rmse = np.mean(np.sqrt(-neg_mse_scores))
    print('alpha 값 ', alpha, '일때 평균 rmse :', np.round(avg_rmse,4))
alpha 값  0 일때 평균 rmse : 339.5967
alpha 값  0.1 일때 평균 rmse : 338.9466
alpha 값  1 일때 평균 rmse : 335.2101
alpha 값  10 일때 평균 rmse : 326.6552
alpha 값  100 일때 평균 rmse : 396.6299
lasso = Lasso(alpha = 10)
lasso.fit(train, train_label)
sample_submission['Body Mass (g)'] = lasso.predict(test)

sample_submission.to_csv('Penguin_final_2.csv',index=False)
from sklearn.linear_model import ElasticNet

alphas = [0,0.1,1,10,100]
#alphas = [0.07,0.1,0.5,1,3]

for alpha in alphas:
    elasticNet = ElasticNet(alpha=alpha)
    
    neg_mse_scores = cross_val_score(elasticNet, train, train_label, scoring = 'neg_mean_squared_error', cv = 5)
    avg_rmse = np.mean(np.sqrt(-neg_mse_scores))
    print('alpha 값 ', alpha, '일때 평균 rmse :', np.round(avg_rmse,4))
alpha 값  0 일때 평균 rmse : 339.5967
alpha 값  0.1 일때 평균 rmse : 327.6324
alpha 값  1 일때 평균 rmse : 358.7528
alpha 값  10 일때 평균 rmse : 573.6431
alpha 값  100 일때 평균 rmse : 754.224
elasticNet = ElasticNet(alpha = 0.1)
elasticNet.fit(train, train_label)
sample_submission['Body Mass (g)'] = elasticNet.predict(test)

sample_submission.to_csv('Penguin_final_3.csv',index=False)