본문 바로가기

머신러닝/지도학습

[Machine Learning] Kaggle_연습사례 분석_Spaceship_titanic

320x100

 

서론


이번시간에는 Kaggle의 완전 기초, 시작단계(Getting Started) 컴피티션에 놓여져있는
Spaceship Titanic 데이터에 대해서 개인적으로 분석한 머신러닝 사례를
진행해 보고자 합니다.

먼저, 원본 데이터 링크입니다.
https://www.kaggle.com/competitions/spaceship-titanic

 

Spaceship Titanic | Kaggle

 

www.kaggle.com


제 노트북은 아래에서 보실 수 있습니다. 빠르게 요약된 버전을 보시려면, 아래 링크를
보셔도 무방합니다! (처음에 파일 로드할 시의 경로만 다릅니다)

https://www.kaggle.com/code/apatheia0/space-titanic-lightgbm-test/notebook

먼저 몇가지 결론들을 미리 알려드리자면,

사용한 머신러닝 알고리즘은 GBM기반 모델중 하나인 LightGBM 중 Classifier를
사용했습니다.

목표는 Spaceship 승선원들의 Transported가 True인지 확인하는데요.

즉, 잘 전송되었는지 여부를 분석하는 것입니다. 다시말해 종속변수가 Transported입니다.

시각화의 단계는 auc_roc 함수에 대한 혼동행렬(confusion matrix)을
heatmap으로 표현했고, 변수중요도(feature important)를
barplot으로 표현했습니다.

특별히 데이터에 대해서 생소하신 분들을 위해서 잠깐 설명드리자면,
spaceship_titanic은 현실의 데이터는 아니고,
유명한 데이터셋 중 하나인 타이타닉 생존자 파일의 'SF버전' 미래우주 시나리오입니다.
머나먼 미래, 인류는 이제 다른 성단간의 운행이 가능한 기술에 이르렀습니다.

이제 많은 사람들이 지구를 떠나 다른 성단으로도 여행을 떠날수도 있습니다!
스페이스 타이타닉호는 이름은 불길하지만, 아무튼 한 달전쯤에 13,000명 이상의
승객을 태우고 초고속 이동을 떠났습니다. 하지만 중간에 일련의 사고로 인해
탑승원들의 일부가 다른 차원(an alternate dimension)으로 Transported(전송)되고 말았습니다!

이걸 좋은 일로 받아들여야 할지, 나쁜 일로 받아들여야 할지 모르겠네요.

위에서 언급한 바와 같이, 다른 차원으로 전송된 사람들의 상황을 분석해야 합니다.
자, Dataset의 내용 자체는 꽤 흥미롭습니다.


※ 번역에 오류나 오판이 있을 수 있는점 양해 부탁드리며, 댓글 등으로 오류를 짚어 주시면 바로 수정하겠습니다.
※ 대부분 colab에서 데이터프레임을 표현할때는 원문을 가져왔으나, 데이터가 너무 큰 경우,
    부득이하게 이미지로 캡쳐했습니다.


이제 시작하겠습니다!

# 분석 알고리즘 
# 12.27 ver1_LightGBM 사용

필요한 패키지를 호출하는 것부터 시작합니다.

# 라이브러리 호출

import numpy as np
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
import sklearn
from sklearn.model_selection import train_test_split
from sklearn.metrics import accuracy_score, roc_auc_score


필자는 미리 데이터를 train, test, sample_submission 모두 다운받아서
쓰고 있습니다.

df = pd.read_csv('C:/myPyCode/data/spaceship-titanic/train.csv', index_col = 0)
df


우주로 떠난 8693명의 정보입니다.

df.head()


이부분에서 각 컬럼들의 상관관계도 확인하려 했습니다.

sns.heatmap(df.corr(), annot=True, vmax=1, vmin=-1, cmap = 'coolwarm')


데이터프레임의 기본적인 사항을 보았습니다.

df.info()
# 종속변수는 Transported
# 독립변수는 Transported 외 필요한 데이터로 하기로 함
# PassengerId 필요, Transported 필요

 

<class 'pandas.core.frame.DataFrame'>
Index: 8693 entries, 0001_01 to 9280_02
Data columns (total 13 columns):
 #   Column        Non-Null Count  Dtype  
---  ------        --------------  -----  
 0   HomePlanet    8492 non-null   object 
 1   CryoSleep     8476 non-null   object 
 2   Cabin         8494 non-null   object 
 3   Destination   8511 non-null   object 
 4   Age           8514 non-null   float64
 5   VIP           8490 non-null   object 
 6   RoomService   8512 non-null   float64
 7   FoodCourt     8510 non-null   float64
 8   ShoppingMall  8485 non-null   float64
 9   Spa           8510 non-null   float64
 10  VRDeck        8505 non-null   float64
 11  Name          8493 non-null   object 
 12  Transported   8693 non-null   bool   
dtypes: bool(1), float64(6), object(6)
memory usage: 891.4+ KB


필자는 여기서 필요한 열과 불필요한 열을 구분해서 분석하려 했습니다.
일단 실수가 아닌, object로 이루어진 데이터 부분입니다.

# 불필요한 데이터 분석

# Name, Cabin(좌석 번호는 랜덤), 
# df.Name
# df.Cabin
df.describe(include='O')
# df.HomePlanet.value_counts()
# df.Destination.unique
	HomePlanet	CryoSleep	Cabin		Destination	VIP	Name
count	8492		8476		8494		8511		8490	8493
unique	3		2		6560		3		2	8473
top	Earth		False		G/734/S		TRAPPIST-1e	False	Gollux Reedall
freq	4602		5439		8		5915		8291	2


VIP가 무엇을 의미하는지 보았습니다.
그냥 true/false더군요.

df.VIP.unique
<bound method Series.unique of PassengerId
0001_01    False
0002_01    False
0003_01     True
0003_02    False
0004_01    False
           ...  
9276_01     True
9278_01    False
9279_01    False
9280_01    False
9280_02    False
Name: VIP, Length: 8693, dtype: object>


기본적인 사항들을 보았습니다.

df.describe()
Age	RoomService	FoodCourt	ShoppingMall	Spa	VRDeck
count	8514.000000	8512.000000	8510.000000	8485.000000	8510.000000	8505.000000
mean	28.827930	224.687617	458.077203	173.729169	311.138778	304.854791
std	14.489021	666.717663	1611.489240	604.696458	1136.705535	1145.717189
min	0.000000	0.000000	0.000000	0.000000	0.000000	0.000000
25%	19.000000	0.000000	0.000000	0.000000	0.000000	0.000000
50%	27.000000	0.000000	0.000000	0.000000	0.000000	0.000000
75%	38.000000	47.000000	76.000000	27.000000	59.000000	46.000000
max	79.000000	14327.000000	29813.000000	23492.000000	22408.000000	24133.000000


나름대로 잠깐 요약했습니다.

# 필요한 데이터 : PassengerId(아이디), HomePlanet(지구, 유로파, 화성 3종류), cryosleep(냉동수면y/n), VIP(y/n)


혹시 몰라서 데이터를 복사했습니다.

df2 = df.copy()
df2.head()
	HomePlanet	CryoSleep	Cabin	Destination	Age	VIP	RoomService	FoodCourt	ShoppingMall	Spa	VRDeck	Name	Transported
PassengerId													
0001_01	Europa	False	B/0/P	TRAPPIST-1e	39.0	False	0.0	0.0	0.0	0.0	0.0	Maham Ofracculy	False
0002_01	Earth	False	F/0/S	TRAPPIST-1e	24.0	False	109.0	9.0	25.0	549.0	44.0	Juanna Vines	True
0003_01	Europa	False	A/0/S	TRAPPIST-1e	58.0	True	43.0	3576.0	0.0	6715.0	49.0	Altark Susent	False
0003_02	Europa	False	A/0/S	TRAPPIST-1e	33.0	False	0.0	1283.0	371.0	3329.0	193.0	Solam Susent	False
0004_01	Earth	False	F/1/S	TRAPPIST-1e	16.0	False	303.0	70.0	151.0	565.0	2.0	Willy Santantines	True


기본적으로 필요한 결측치를 계산하고
일단 어떻게 처리해야 할지 고민했습니다.
거의 대부분 결측치가 있더군요.

# 결측치 확인
df.isna().mean()
# 룸서비스, 푸드코트, 쇼핑몰, 스파, vr덱 결측치는 평균값으로 계산
# 홈플래닛은 최빈값, Age는 평균값으로 처리, VIP는 F로 처리, 냉동수면은 F, 데스티네이션은 최빈값으로 처리
HomePlanet      0.023122
CryoSleep       0.024963
Cabin           0.022892
Destination     0.020936
Age             0.020591
VIP             0.023352
RoomService     0.020821
FoodCourt       0.021051
ShoppingMall    0.023927
Spa             0.021051
VRDeck          0.021627
Name            0.023007
Transported     0.000000
dtype: float64


앞으로도 절대 필요 없을것 같은 이름과, 선실 번호는 지웠습니다.

# 열 삭제 주의
df.drop(['Name','Cabin'], axis = 1, inplace =True)
df


homeplanet은 어떻게 할까요?
일단 최빈값으로 진행하려 합니다.

#결측치 처리 - homeplanet : 최빈값
df.HomePlanet.value_counts().plot(kind='bar')


진행했을 시 데이터프레임 원본이 날아가는 (얕은 복사)의 코드는
주의표시를 달아놨습니다.

# 홈플래닛 최빈값 처리 완료
# 주의
df.HomePlanet = df.HomePlanet.fillna('Earth')
df.HomePlanet.value_counts().plot(kind='bar')


데이터프레임을 안전 보관한 것입니다.
넘어갑니다.

df3 = df.copy()


룸서비스, 푸드코트, 쇼핑몰, 스파, VR덱(VRDeck)의 결측치는
모두 평균으로 묶었습니다.

혹시 추가로 합리적인 방법이 있다면 추천해주시면 감사하겠습니다.

# 결측치 처리 - 룸서비스, 푸드코트, 쇼핑몰, 스파, vr덱 : 평균
df.RoomService.fillna(df.RoomService.mean(), inplace=True)
df.FoodCourt.fillna(df.FoodCourt.mean(), inplace=True)
df.ShoppingMall.fillna(df.ShoppingMall.mean(), inplace=True)
df.Spa.fillna(df.Spa.mean(), inplace=True)
df.VRDeck.fillna(df.VRDeck.mean(), inplace=True)


결측치 재확인합니다.

df.isnull().sum()
HomePlanet        0
CryoSleep       217
Destination     182
Age             179
VIP             203
RoomService       0
FoodCourt         0
ShoppingMall      0
Spa               0
VRDeck            0
Transported       0
dtype: int64


나이의 결측치는 최빈값으로 처리했습니다.
Titanic에서도 유사하게 했던 내용입니다.

# 결측치 - age : 최빈값처리
df.Age.fillna(df.Age.median(), inplace = True)


결측치는 거의다 종료되어 더미 데이터를 남기려고 했습니다.
- 원핫 인코딩을 하기 위함이에요.

 

현재 정보입니다.

df.info()
<class 'pandas.core.frame.DataFrame'>
Index: 4277 entries, 0013_01 to 9277_01
Data columns (total 12 columns):
 #   Column                     Non-Null Count  Dtype  
---  ------                     --------------  -----  
 0   Age                        4277 non-null   float64
 1   RoomService                4277 non-null   float64
 2   FoodCourt                  4277 non-null   float64
 3   ShoppingMall               4277 non-null   float64
 4   Spa                        4277 non-null   float64
 5   VRDeck                     4277 non-null   float64
 6   CryoSleep_True             4277 non-null   uint8  
 7   Destination_PSO J318.5-22  4277 non-null   uint8  
 8   Destination_TRAPPIST-1e    4277 non-null   uint8  
 9   VIP_True                   4277 non-null   uint8  
 10  HomePlanet_Europa          4277 non-null   uint8  
 11  HomePlanet_Mars            4277 non-null   uint8  
dtypes: float64(6), uint8(6)
memory usage: 259.0+ KB


seaborn이나 matplotlib를 써도 되지만 일단 판다스의 기능인
plot.pie를 써서 전송된 사람/ 안된 사람의 비율을 나눴습니다.
1이 전송 성공, 0이 실패한 사람입니다.

# 건너간사람과 건너가지 못한 사람의 비율
plt.figure(figsize=(7,7))
df['Transported'].value_counts().plot.pie(
    explode=[0.1,0.1], autopct='%1.1f%%')


50%가 넘는군요

 

 

개별적으로 전처리는 이정도로 끝냈습니다.

다음 시간에는 spaceship의 test 파일을 가져와서 똑같이 전처리할 예정입니다😥

 

훈련셋과 시험셋을 분리해줍니다.

물론 우리가 필요한 종속변수는 Transported 입니다.

# 데이터 전처리 종료
# 훈련셋/ 시험셋 분리 시작

X = df.drop('Transported', axis = 1)
y = df['Transported']

X_train, X_test, y_train, y_test = train_test_split(X, y, test_size = 0.25, random_state = 100)

 

필요한 도구인 LightGBM을 꺼내줍니다.

LightGBM은 짧게 설명하자면 Gradient Boosting 모델의 심화된 버전이죠?

XGBoost와 함께 분류, 회귀 문제에서 간판격으로 많이 사용되고 있습니다.

다만, 데이터셋의 크기가 작을 경우 over-fitting의 가능성이 있다는 단점도 있습니다

 

▼ 자세한 설명

https://nurilee.com/2020/04/03/lightgbm-definition-parameter-tuning/

 

LightGBM 이란? 그리고 Parameter 튜닝하기

LightGBM에 관한 좋은 medium 포스트가 있어서 한글로 번역한 내용을 공유드려봅니다 :)  Pushkar Mandot의 원문 바로가기: 안녕하세요, 머신러닝은 이 세상에서 가장 빠르게 성장하는 분야입니다. 매일

nurilee.com

 

# 필요한 lightgbm 패키지 호출
import lightgbm as lgb

 

학습을 진행하겠습니다. 랜덤값은 100입니다.

 

# LightGBM 분류기 정의
model = lgb.LGBMClassifier(random_state = 100)

model
LGBMClassifier(random_state=100)

 

# Lightgbm 학습 시작

model.fit(X_train, y_train)
pred = model.predict(X_test)

 

시험셋에 대한 예측값을 보여줍니다.

 

# 예측값
pred
array([0, 0, 1, ..., 1, 1, 1], dtype=uint8)

 

해당 학습에 대한 정확도 점수를 뽑아보았는데, 크게 하이퍼패러미터를

조절하지 않았기 때문에 높진 않습니다.

 

# 정확도 점수
accuracy_score(y_test, pred) # 낮은 점수로 인해 패러미터 튜닝 시작 필요
0.7897884084636615


RandomSearchCV를 사용할 지, GridSearchCV를 사용할 지
두 검증도구를 두고 고민했는데요.
시간이 더 짧게 소요된다는 추천을 받고 RandomSearchCV를 골랐습니다.

 

# RandomSearchCV 이용

from sklearn.model_selection import RandomizedSearchCV
# # 사용할 params 딕셔너리 정의
params = {"n_estimators" : [100, 500, 1000],"learning_rate" : [0.01, 0.05, 0.1, 0.3]
          ,"lambda_l1" : [0, 10, 20],"lambda_l2" : [0, 10, 20],"max_depth" : [5, 10, 15, 20],"subsample": [0.6, 0.8, 1]}
# params = {"n_estimators" : [1000],"learning_rate" : [0.3]
#           ,"lambda_l1" : [10],"lambda_l2" : [10],"max_depth" : [20],"subsample": [1]}


밑에서 주석으로 처리한 코드는 실험한 부분입니다.

# LigthGBM 사용

model_2 = lgb.LGBMClassifier(random_state = 100)

lg = RandomizedSearchCV(model_2,
      param_distributions = params, 
      n_iter = 30, scoring = 'roc_auc', 
      random_state=100, n_jobs = -1)
lg

 

Random search CV를 적용해줍니다.

 

RandomizedSearchCV(estimator=LGBMClassifier(random_state=100), n_iter=30,
                   n_jobs=-1,
                   param_distributions={'lambda_l1': [0, 10, 20],
                                        'lambda_l2': [0, 10, 20],
                                        'learning_rate': [0.01, 0.05, 0.1, 0.3],
                                        'max_depth': [5, 10, 15, 20],
                                        'n_estimators': [100, 500, 1000],
                                        'subsample': [0.6, 0.8, 1]},
                   random_state=100, scoring='roc_auc')


학습시키는데 시간이 걸렸습니다만, colab보다 쥬피터가 더 짧게 걸립니다.

lg.fit(X_train, y_train) # 30초 가량 소요
RandomizedSearchCV(estimator=LGBMClassifier(random_state=100), n_iter=30,
                   n_jobs=-1,
                   param_distributions={'lambda_l1': [0, 10, 20],
                                        'lambda_l2': [0, 10, 20],
                                        'learning_rate': [0.01, 0.05, 0.1, 0.3],
                                        'max_depth': [5, 10, 15, 20],
                                        'n_estimators': [100, 500, 1000],
                                        'subsample': [0.6, 0.8, 1]},
                   random_state=100, scoring='roc_auc')


학습됬다고 문구가 따로 뜨게 됩니다.

최적의 점수를 내는 패러미터입니다.

 

lg.best_params_ # 최적의 패러미터를 계산
{'subsample': 0.8,
 'n_estimators': 500,
 'max_depth': 20,
 'learning_rate': 0.01,
 'lambda_l2': 0,
 'lambda_l1': 0}

 

최고점 입니다.

 

lg.best_score_ # 최적의 점수계산
0.8765142818325253


각각의 확률입니다.

lg_proba = lg.predict_proba(X_test)
lg_proba
array([[0.94023679, 0.05976321],
       [0.81805936, 0.18194064],
       [0.10973714, 0.89026286],
       ...,
       [0.29261386, 0.70738614],
       [0.16440701, 0.83559299],
       [0.37558404, 0.62441596]])

 

auc 점수도 도출해보겠습니다.

 

k = roc_auc_score(y_test, lg_proba[:,1]) # roc_auc 점수계산
k
0.8704691174776167

 

87점까지 기록하고 있습니다.

이번에는 혼동 행렬(confusion_matrix)와 분류 리포트(classification_report)를 호출해서

f1-score를 뽑아보겠습니다.

 

 

# 혼동행렬 호출
from sklearn.metrics import confusion_matrix, classification_report
def confusion_matrix_view(y_test, pred):
    plt.figure(figsize = (10,6))
    cf_matrix = confusion_matrix(y_test, pred)
    print(cf_matrix)
    
    group_names = ['TN','FP','FN','TP']
    group_counts = ['{0:0.0f}'.format(value) for value in
                   cf_matrix.flatten()] # 평평화
    group_percentages = ['{0:.2%}'.format(value) for value in
                        cf_matrix.flatten()/np.sum(cf_matrix)]
    
    labels = [f'{v1}\n{v2}\n{v3}' for v1, v2, v3 in
             zip(group_names, group_counts, group_percentages)]
    labels = np.asarray(labels).reshape(2,2)
    
    sns.heatmap(cf_matrix, annot = labels, fmt='', cmap='coolwarm')
    
    plt.ylabel('True')
    plt.xlabel('Predicted')
    plt.show()
    # 분류 리포트 제출
    print(classification_report(y_test, pred))


따로 컨퓨전 매트릭스와 분류 리포트를 함께 뽑는 함수를 정의했습니다.

confusion_matrix_view(y_test, lg.predict(X_test))
[[834 288]
 [159 893]]

 

              precision    recall  f1-score   support

           0       0.84      0.74      0.79      1122
           1       0.76      0.85      0.80      1052

    accuracy                           0.79      2174
   macro avg       0.80      0.80      0.79      2174
weighted avg       0.80      0.79      0.79      2174

 

위의 보고서에서 점수가 반환되게 됩니다. 

높은 편은 아닙니다.


남아있는 feature들 중 중요도를 내림차순으로 전부 반환했습니다.

# 변수 중요도 확인

feature_important = pd.DataFrame({'features':X_train.columns,
                                 'values':model.feature_importances_})
plt.figure(figsize=(10,9))
sns.barplot(x='values', y='features', data=feature_important.sort_values
           (by='values',ascending = False))
plt.show()

 

Age, FoodCourt, VRDeck가 1위, 2위, 3위를 기록했네요. 😎

다음 시간에는 직접 test.csv와 sample submission파일을 불러와서

실제로 타이타닉 우주선의 전송된 승객을 예측하는 시간을 갖겠습니다.

 

728x90