본문 바로가기

Project/Kaggle

[Kaggle] 사이킷런 없이 Titanic 생존자 예측하기

이번 포스팅은 seabornmatplotlib 을 이용하여 EDA 를 진행하고,

이를 바탕으로 numpy pandas 만을 이용하여 예측 모델을 직접 만드는 것입니다.

 

정확도를 높이는 것이 목적이 아닌,

다양한 측면에서 데이터를 바라보며 새로운 인사이트를 발견하는 과정,

그리고 직접 모델을 만들어 봄으로써 데이터에 대한 깊은 이해를 쌓고자 합니다.

 

데이터 공부를 시작하는 여러분에게도 좋은 밑거름이 되었으면 좋겠습니다!

그럼 시작해 볼게요!

 

순서

  1. Feature 하나씩 뜯어보기
  2. EDA 를 통해 얻은 인사이트 정리
  3. 모델링 구성 짜기
  4. 모델링
  5. 테스트셋에 적용 및 결과 분석

 


 

1. Feature 하나씩 뜯어보기

1-1. 데이터 로드 및 라이브러리

# 라이브러리
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt 
%matplotlib inline  

import seaborn as sns

# 데이터 로드
train = pd.read_csv('./train.csv')
test = pd.read_csv('./test.csv')
submission = pd.read_csv('./gender_submission.csv')

# 결측치 확인
train.isnull().sum()

 

1-2. Pclass

# Survived 보기 쉽게 변환
train['Survived'] = train['Survived'].apply(lambda x: 'Yes' if x==1 else 'No')

# Cabin 알파벳만 추출하기
train['Cabin'].fillna(0, inplace=True)
train['Cabin'] = train['Cabin'].apply(lambda x: x[:1] if type(x)==str else str(x))

# plotting
f, ax = plt.subplots(1,2,figsize=(15, 5))
sns.set_theme(style="ticks")
sns.histplot(train, x="Pclass", hue="Survived", multiple="stack", palette="light:m_r", edgecolor=".3",linewidth=.8, ax=ax[0])
sns.histplot(train.query("Cabin != '0'"), x="Cabin", hue="Pclass", multiple="stack", palette="light:b_r", edgecolor=".3",linewidth=.8,shrink=0.5, ax=ax[1])
ax[0].set_xticks([1,2,3])

ax[0].set_title('Pclass per Survivied', fontsize=20)
ax[1].set_title('Cabin per Pclass', fontsize=20)

 

 

그래프 해석
- 3등석 탑승객이 가장 많지만, 생존율은 가장 낮다
- 1등석 탑승객의 경우 생존율이 가장 높다
- 객실번호가 A,B,C 는 1등석만 쓴다, D는 1,2등석 같이 쓴다, E는 1,2,3등석 같이 쓴다, F는 2,3등석 같이 쓴다, G는 3등석만 쓴다, T는 특실(?)

 

인사이트
- 좋은 class 일수록 생존율이 높아진다
- 1등석 자리를 비행기의 앞쪽이라 했을때, 객실은 알파벳 순서로 비행기 앞쪽부터 위치해 있다
- Cabin의 결측치를 채우는데 Pclass가 도움이 될 것 같다

1-3. Name

# Name 으로부터 Initial 추출
train['Initial']=0
for i in train:
    train['Initial']=train.Name.str.extract('([A-Za-z]+)\.')
train['Initial'].replace(['Mlle','Mme','Ms','Dr','Major','Lady','Countess','Jonkheer','Col','Rev','Capt','Sir','Don'],['Miss','Miss','Miss','Mr','Mr','Mrs','Mrs','Other','Other','Other','Mr','Mr','Mr'],inplace=True)

# plotting
f, ax = plt.subplots(1,2,figsize=(15, 5))
sns.set_theme(style="ticks")
sns.histplot(train, x="Initial", hue="Survived", multiple="stack", palette="light:m_r", edgecolor=".3",linewidth=.8, shrink=0.5, ax=ax[0])
sns.boxplot(data=train, x="Initial", y="Age",ax=ax[1])

ax[0].set_title('Initial per Survived', fontsize=20)
ax[1].set_title('Initial per Age', fontsize=20)

 

 

그래프 해석
- 탑승객 중 Mr 호칭이 가장 많았지만, 생존율이 매우 낮았다
- 반면 Mrs와 Miss 호칭의 탑승객은 생존율이 절반 이상이다
- Other 호칭의 탑승객 생존율은 매우 낮다
- Mr 와 Mrs 의 호칭을 가진 사람은 오른쪽으로 skew 된 분포를 보이며 대체적으로 30대라고 할 수 있다
- Miss 호칭을 가진 사람은 20대라고 볼 수 있다
- Master 호칭을 가진 사람은 0~10살 사이로 보인다
- Other의 경우, 왼쪽으로 skew 된 분포를 보이며 대체적으로 50대라고 할 수 있다

 

인사이트
- 젊은 남성(Mr),중년층(Other)의 경우 생존율이 낮고, 여성(Mrs, Miss)의 경우 생존율이 높다.
- 각 호칭별 적절한 나이 통계량 값을 이용하여 Age의 결측치를 채우는데 도움이 될 수 있다

1-4. Sex

f, ax = plt.subplots(figsize=(5, 5))
sns.set_theme(style="ticks")
sns.histplot(train, x="Sex", hue="Survived", multiple="stack", palette="light:m_r", edgecolor=".3",linewidth=.8, shrink=0.5)
ax.set_title('Sex per Survived', fontsize=20)

 

 

그래프 해석
- 남성이 여성에 비해 많다
- 남성의 생존율이 여성에 비해 확연히 낮다

 

인사이트
- 성별은 생존율을 예측하는데 큰 영향이 미친다

1-5. Age

# 결측치 채우기
# train.groupby('Initial')['Age'].mean()
train.loc[(train.Age.isnull())&(train.Initial=='Mr'),'Age']=33
train.loc[(train.Age.isnull())&(train.Initial=='Mrs'),'Age']=36
train.loc[(train.Age.isnull())&(train.Initial=='Master'),'Age']=5
train.loc[(train.Age.isnull())&(train.Initial=='Miss'),'Age']=22
train.loc[(train.Age.isnull())&(train.Initial=='Other'),'Age']=46

# plotting
f, ax = plt.subplots(figsize=(15, 5))
sns.set_theme(style="ticks")
sns.kdeplot(data=train, x="Age", hue='Survived',fill=True,alpha=0.05,ax=ax)
ax.axvline(x=10, color='r', linestyle = '--', linewidth=2)
ax.text(8.5,-0.002,'10',fontdict={'color':'red','size':18})
ax.set_title('Age per Survived', fontsize=20)

 

 

그래프 해석
- Age가 10 이후로 생존한 kde 그래프가 생존하지 못한 kde 그래프보다 높아진다

 

인사이트
- 10살 미만의 사람들은 생존할 확률이 더 높지만, 10살 이상부터는 생존하지 못할 확률이 더 높다

1-6. SibSp

# 데이터 프레임 준비
train['SibSp'] = train['SibSp'].apply(lambda x: str(x))
sib_df = train['SibSp'].value_counts().sort_index()

# 크로스탭 준비
sib_ct = pd.crosstab(train['SibSp'],train['Survived'], margins=True).drop('All',axis=0)

# plotting
plt.figure(figsize=(20,6))

plt.subplot(1,2,1)
plt.pie(sib_df, labels = sib_df.index,textprops={'fontsize': 15})
plt.title('SibSp pie', size=25)

plt.subplot(1,2,2)
plt.barh(sib_ct.index,sib_ct['Yes']/sib_ct['All'])
plt.barh(sib_ct.index,sib_ct['No']/sib_ct['All'],left = sib_ct['Yes']/sib_ct['All'])
plt.title('SibSp survival rate', size=25)
plt.legend(['Yes','No'])

# 4명 이상은 하나의 그룹으로 묶기
train['SibSp'] = train['SibSp'].apply(lambda x: str(4) if int(x)>=4 else x)

 

 

그래프 해석
- SibSp는 0 이 약 70%를 차지하고, 그 다음 한 명이 20%, 그 다음으로는 비슷하다
- 생존 비율을 보자면 SibSp 가 1에서 가장 높고, 높아질수록 생존 비율은 낮아진다

 

인사이트
- 오버피팅을 막기 위해 4명 이상은 하나의 그룹으로 묶는게 낫겠다
- 형제 자매 혹은 배우자 1~2명과 함께 타는 것이 혼자 타는 것보다 생존율이 높다
- 하지만 동승자(형제 자매 혹은 배우자)가 3명 이상일 경우 생존율이 오히려 낮아지고, 5명 이상일 경우는 생존자가 없다

1-7. Parch

# 데이터 프레임 준비
train['Parch'] = train['Parch'].apply(lambda x: str(x))
par_df = train['Parch'].value_counts().sort_index()

# 크로스탭 준비
par_ct = pd.crosstab(train['Parch'],train['Survived'], margins=True).drop('All',axis=0)

# plotting
plt.figure(figsize=(20,6))

plt.subplot(1,2,1)
plt.pie(par_df, labels = par_df.index,textprops={'fontsize': 15})
plt.title('Parch pie', size=25)

plt.subplot(1,2,2)
plt.barh(par_ct.index,par_ct['Yes']/par_ct['All'])
plt.barh(par_ct.index,par_ct['No']/par_ct['All'],left = par_ct['Yes']/par_ct['All'])
plt.title('Parch survival rate', size=25)
plt.legend(['Yes','No'])

# 3명 이상은 하나의 그룹으로 묶기
train['Parch'] = train['Parch'].apply(lambda x: str(3) if int(x)>=3 else x)

 

 

그래프 해석
- Parch의 경우 0이 압도적으로 많고, 1,2가 비슷하다
- 3 이상은 거의 없다
- 3 이상은 표본이 너무 적기 때문에 비율을 보는건 무의미하다
- 0,1,2만 비교해봤을때, 0보다는 1,2가 높고, 1이 가장 높다

 

인사이트
- 오버피팅을 예방하기 위해 3 이상은 하나의 그룹으로 묶는게 낫다
- 혼자 타는것보다 동승자(부모,자녀)가 1~3명 있는 경우 생존율이 높다
- SibSp와 마찬가지로 너무 많은 경우에는 생존율이 현저하게 떨어진다

1-8. Ticket

# Ticket에서 숫자만 추출
tic_num = train['Ticket'].apply(lambda x: x.split(' ')[-1])
train['Ticket'] = tic_num.replace('LINE',0)
train['Ticket'] = train['Ticket'].apply(lambda x: int(x))

# plotting
plt.figure(figsize=(15,10))

plt.subplot(2,2,(1,2))
sns.kdeplot(data=train, x="Ticket", hue='Pclass',palette = 'Set1',fill=True,alpha=0.05)
plt.text(250000, -0.0000003,"↓",fontdict={'color':'red','size':25})
plt.text(2800000, -0.0000003,"↓",fontdict={'color':'red','size':25})
plt.xticks([])
plt.title('Ticket Number per Pclass', size=15)

plt.subplot(2,2,3)
sns.scatterplot(data=train, x="Ticket",y="Fare",hue="Pclass", palette="Set1")
plt.xlim(0,500000)
plt.title('Ticket num : 0 ~ 500000', size=15)

plt.subplot(2,2,4)
sns.scatterplot(data=train, x="Ticket",y="Fare",hue="Pclass", palette="Set1")
plt.xlim(3000000,)
plt.title('Ticket num : 3000000 ~ ', size=15)

 

 

그래프 해석
- ticket 번호의 kde 분포가 pclass별로 특징이 나타남
- 0 ~ 1e5 구간에는 모든 pclass가 분포하고, 1e5 ~ 2e5 구간에는 pclass가 1, 2e5 ~ 3e5 구간에는 2, 3e5 ~ 4e5 구간에는 3이 분포함
- 3e6 부근에는 pclass가 3인 데이터만 분포함

 

인사이트
- ticket 번호의 첫번째자리 숫자는 Pclass 를 나타냄
- 0 ~ 1e5 구간의 ticket 번호는 Pclass와 별개로 다양하게 분포해있음// 현장결제의 티켓번호로 추측

 

 

# plotting
plt.figure(figsize=(13,10))

plt.subplot(2,2,1)
sns.scatterplot(data=train, x="Ticket",y="Fare",hue="Survived", palette="Set1")
plt.xlim(0,70000)
plt.ylim(0,300)
plt.title('1,2,3 class (Ticket num : 0 ~ 100000)', size=15)

plt.subplot(2,2,2)
sns.scatterplot(data=train, x="Ticket",y="Fare",hue="Survived", palette="Set1")
plt.xlim(110000,114000)
plt.ylim(0,300)
plt.title('1 class (Ticket num : 100000 ~ 200000)', size=15)

plt.subplot(2,2,3)
sns.scatterplot(data=train, x="Ticket",y="Fare",hue="Survived", palette="Set1")
plt.xlim(300000,400000)
plt.ylim(0,50)
plt.title('2 class (Ticket num : 300000 ~ 400000)', size=15)

plt.subplot(2,2,4)
sns.scatterplot(data=train, x="Ticket",y="Fare",hue="Survived", palette="Set1")
plt.xlim(3101250,3101325)
plt.ylim(0,50)
plt.title('3 class (Ticket num : 3000000 ~ )', size=15)

plt.subplots_adjust(hspace=0.3)
plt.suptitle('Ticket Number per Survival', size=30)

 

 

인사이트
- 티켓 번호에 좌석정보도 포함되어 있다면, 가운데 좌석의 생존율이 떨어질것으로 가정하고 plotting
- 티켓 번호에서는 좌석 등급을 제외한 별다른 규칙성을 찾을 수 없기 때문에 생존 여부를 판단하기 어려워보임

1-9. Fare

# 인당 가격 컬럼 추가 (per_fare)
per_fare = train['Fare'].groupby(train['Ticket']).agg({('fare','mean'),('num','count')})
per_fare['per_fare'] = per_fare['fare']/per_fare['num']
per_fare.drop(['fare','num'], axis=1,inplace=True)
train = pd.merge(train, per_fare, on='Ticket')

# plotting
f, ax = plt.subplots(2,2,figsize=(10,10))
sns.scatterplot(data=train, x='Age',y='Fare', hue='Survived',ax=ax[0,0])
sns.scatterplot(data=train, x='Age',y='Fare', hue='Pclass',ax=ax[0,1])
sns.scatterplot(data=train, x='Age',y='per_fare', hue='Survived',ax=ax[1,0])
sns.scatterplot(data=train, x='Age',y='per_fare', hue='Pclass',ax=ax[1,1])
ax[0,0].set_ylim(0,250)
ax[0,1].set_ylim(0,250)
ax[1,0].set_ylim(0,250)
ax[1,1].set_ylim(0,250)

ax[0,0].set_title('Ticket price per Suvival', size=15)
ax[0,1].set_title('Ticket price per Pclass', size=15)
ax[1,0].set_title('Fare(1 person) per Suvival', size=15)
ax[1,1].set_title('Fare(1 person) per Pclass,', size=15)

plt.subplots_adjust(hspace=0.3)
plt.suptitle('Fare(1 person) VS Ticket price(Team)', size=30)

 

 

그래프 해석
- 기존의 Fare 컬럼을 기준으로 생존여부를 분리했을때, 제대로 분리되어 있지 않음
- 기존의 Fare 컬럼을 기준으로 Pclass를 분리했을때, 제대로 분리되어 있지 않음
- 티켓가격을 일인당 좌석 요금으로 변경하니 생존여부와 Pclass를 제대로 분리하는 모습을 보임 

 

인사이트
- 1등석 중에서도 비싼 1등석이 있고, 저렴한 1등석이 있을거다. 
- 이를 Fare 컬럼이 보완해야하는데, 티켓 가격으로는 제대로된 보완을 하지 못함
- 일인당 좌석 요금 컬럼을 생성하여 분류하니 제대로 분리함

1-10. Cabin

# plotting
f, ax = plt.subplots(1,2,figsize=(15, 5))
sns.set_theme(style="ticks")
sns.histplot(train.query("Cabin != '0'"), x="Cabin", hue="Survived", multiple="stack", palette="light:m_r", edgecolor=".3",linewidth=.8,shrink=0.5, ax=ax[0])
sns.histplot(train.query("Cabin != '0'"), x="Cabin", hue="Pclass", multiple="stack", palette="light:b_r", edgecolor=".3",linewidth=.8,shrink=0.5, ax=ax[1])

ax[0].set_title('Cabin per Survivied', fontsize=20)
ax[1].set_title('Cabin per Pclass', fontsize=20)

 

 

그래프 해석
- A, B, C, E 객실의 생존율이 높게 나타남
- A, B, C, E 객실은 1등석 탑승객이 이용함

 

인사이트
- 객실별로 생존율 차이가 나긴 하지만, 이는 좌석 등급에 의한 차이로 보임
- 객실의 위치 자체로 생존율에 큰 영향을 주지 않고, 결측치가 약 77% 이기 때문에 삭제하는것이 나을것같다

1-11. Embarked

fig, ax = plt.subplots(1,2,figsize=(12,6))
sns.histplot(train[train['Embarked'].notnull()], x='Embarked',hue="Pclass", multiple="stack", ax=ax[0])
sns.histplot(train[train['Embarked'].notnull()], x='Embarked',hue="Survived", multiple="stack", ax=ax[1])
ax[0].set_title('Embarked per Pclass', size=20)
ax[1].set_title('Embarked per Survival', size=20)
plt.subplots_adjust(wspace=0.3)

# 결측치 처리
train['Embarked'].fillna('S', inplace=True)

 

 

그래프 해석
- S 선착장에서 탄 사람이 가장 많다
- Q 선착장에서 탄 사람은 대부분 3등석 탑승객이다

 

인사이트
- 결측치 2개는 가장 비율이 높은 S 선착장으로 채워도 무방할 것 같다

 

2. EDA 를 통해 얻은 인사이트 정리

2-1. 내가 찾은 생존율이 높은 조건

- Pclass : 1 > 2 > 3 // 높은 클래스일수록 생존율이 높다  
- Name(Initaial) : Miss > Mrs > Master > Mr > Other // 여성일수록 생존율이 높고, 남성과 중년층은 생존율이 낮다  
- Sex : female > male  
- Age : 10살 미만 어린이  
- SibSp : 1 > 2 > 0 > 3 > 4이상 // 혼자보다는 동승자가 있을수록 생존율이 높지만, 동승자가 3명 이상이라면 생존율이 급격히 낮아진다  
- Parch : 1 > 2 > 0 > 3이상  
- Fare(per_fare) : 높을수록 생존율이 높다  

 

2-2. 내가 찾은 신기한 정보들

- 기존의 Fare는 티켓 가격으로, 동승자가 많을수록 높은 가격으로 책정됨. 따라서 한 사람당 가격으로 변경해주어야함 (1등석 중에서도 비싼 1등석의 생존율이 더 높을 것으로 예상되기 때문)  
- 객실은 알파벳 순서로 Pclass 별로 배정 (A,B,C 는 1등석만 쓴다, D는 1,2등석 같이 쓴다, E는 1,2,3등석 같이 쓴다, F는 2,3등석 같이 쓴다, G는 3등석만 쓴다, T는 특실(?))  
- Q 선착장이 있는 지역은 평균 소득이 낮을 것으로 보임 (3등석 탑승객이 대부분이기 때문)  
- Ticket Number의 첫번째 숫자는 Pclass를 의미함, 좌석 정보도 포함되었는지 EDA를 진행해본 결과 포함되지 않은 것으로 결론남 

 

2-3. 삭제한 컬럼들

- PassengerId : 탑승객 고유의 ID이기 때문에 생존예측에는 도움이 되지 않음  
- Ticket : 티켓 정보에는 Pclass 정보만이 포함되어 있지만, Pclass 컬럼이 있기 때문에 불필요한 컬럼이라고 판단  
- Fare : 티켓 가격은 단순하게 동승자가 많을수록 높아지기 때문에 한 사람당 가격(per_fare)을 구하고, 티켓가격(Fare)은 삭제  
- Cabin : 온전히 객실번호만으로 생존율에 끼치는 영향은 없다고 EDA를 통해 알게됨, 무엇보다 결측치가 70%가 넘기 때문에 삭제  
- Embarked : EDA를 통해 선착장의 장소에 따른 생존율 변화는 발견할 수 없었음  
- SibSp, Parch : 두 컬럼을 더한 SibSp_Parch 컬럼 생성

3. 모델링 구성 짜기

# SibSp와 Parch 합 생성
train['SibSp'] = train['SibSp'].apply(lambda x: int(x))
train['Parch'] = train['Parch'].apply(lambda x: int(x))
train['SibSp_Parch'] = train['SibSp'] + train['Parch'] 

# 불필요한 컬럼 삭제
train.drop(['PassengerId','Name','Ticket','Fare','Cabin','SibSp','Parch','Embarked'], axis=1, inplace=True)

3-1. EDA에서 생존율 차이를 가장 많이 보인 'Pclass'와 'Sex' 를 기준으로 가장 먼저 나눔 

* 그럼 'Pclass' 와 'Sex' 중 어떤 걸 먼저?

 

- 아래 크로스탭을 보면 Pclass 의 경우 1등석과 3등석에서는 생존여부를 잘 나누지만, 2등석에서는 잘 나누지 못한다.
- 그에 반해 Sex 의 경우 여성과 남성에 따라 생존여부가 극명히 갈리기 때문에 Sex를 기준으로 가장 먼저 나누고, Pclass로 나누는 것을 선택함.

 

display(pd.crosstab(train['Pclass'],train['Survived']))
display(pd.crosstab(train['Sex'],train['Survived']))

 

 

3-2. EDA에서 그 다음으로 생존여부를 잘 분리한 특성은? 'Initial', 'SibSp_Parch'

* 'Initial', 'SibSp_Parch' 중 어떤 걸 먼저?

 

in_first_df = pd.crosstab([train['Sex'],train['Pclass'],train['Initial']],train['Survived'])
in_first = ((in_first_df['Yes']/in_first_df['No'] == 0)|(in_first_df['No']/in_first_df['Yes'] == 0)).sum()/len(in_first_df)
print(in_first)
# 0.26666666666666666

sp_first_df = pd.crosstab([train['Sex'],train['Pclass'],train['SibSp_Parch']],train['Survived'])
sp_first = ((sp_first_df['Yes']/sp_first_df['No'] == 0)|(sp_first_df['No']/sp_first_df['Yes'] == 0)).sum()/len(sp_first_df)
print(sp_first)
# 0.3611111111111111

 

- 완전히 다 분류한 경우(모두 생존했거나, 모두 생존하지 않았거나)의 비율을 비교   
- Initial 을 먼저 분류 했을 때, 생존 여부중 하나라도 0이 있는 경우(완전히 다 분류한 경우)의 비율, SibSp_Parch 을 먼저 분류 했을 때, 하나라도 0이 있는 경우(완전히 다 분류한 경우)의 비율을 비교해봤더니 SibSp_Parch 을 먼저 분류 했을 때가 더 높았음. 따라서 SibSp_Parch 을 먼저 분류함  

 

3-3. 남은 특성은? 'Age', 'per_fare'

* 'Age' 와 'per_fare' 중 어떤 걸 먼저?

 

'Age'와 'per_fare'모두 연속형 변수이기 때문에 최적의 분류기준을 찾기 어려움.
따라서 두 가지를 동시에 적용함. 
scatterplot을 이용하여 가장 잘 분류하는 지점을 찾아냄

 

fig, ax = plt.subplots(figsize=(10,10))
sns.scatterplot(data=train, x='Age',y='per_fare', hue='Survived',ax=ax)
ax.axhline(y=65, color='r', linestyle = '--', linewidth=3)
ax.axhline(y=23, color='r', linestyle = '--', linewidth=3)
ax.vlines(x=10, ymin = 0,ymax=23, color='g', linestyle = '--', linewidth=3)
ax.vlines(x=43, ymin = 23,ymax=65, color='g', linestyle = '--', linewidth=3)
ax.text(-8,62, "65",fontdict={'color':'red','size':18})
ax.text(-8,20, "23",fontdict={'color':'red','size':18})
ax.text(8,-15, "10",fontdict={'color':'green','size':18})
ax.text(41.5,-10, "43",fontdict={'color':'green','size':18})
ax.set_ylim(0,)

 

 

4. 모델링

아래 크로스탭에서 완전 분리된 기준(생존 여부 레이블 중 하나라도 0이 있는 기준)은 생존여부를 바로 예측하고,  
완전히 분리하지 못한 기준은 'age'와 'per_fare'의 scatterplot에서 해당되는 영역으로 생존여부를 판단한다

 

pd.set_option('display.max_rows',500)
pd.crosstab([train['Sex'],train['Pclass'],train['Initial'],train['SibSp_Parch']],train['Survived'])

 

아래 생략

4-1. 함수 만들기

# age와 fare를 기준으로 나눈 함수
def age_fare(data):
    if data['per_fare'] >= 65:
        return 1
    else:
        if data['per_fare'] >= 23:
            if data['Age'] >= 43:
                return 0
            else:
                return 1
        else:
            if data['Age'] >= 10:
                return 0
            else:
                return 1
          
# 생존 여부 예측 모델
def predict_survival(data):
    if data['Sex'] == 'female':
        if data['Pclass'] == 1:
            if data['Initial'] == 'Miss':
                if data['SibSp_Parch'] in [1,2,4,5]:
                    return 1
                else:
                    return age_fare(data)
            elif data['Initial'] == 'Mr':
                return 1
            elif data['Initial'] == 'Mrs':
                if data['SibSp_Parch'] in [0,1,2]:
                    return 1
                else:
                    return age_fare(data)
            else:
                return age_fare(data)
                
        elif data['Pclass'] == 2:
            if data['Initial'] == 'Miss':
                if data['SibSp_Parch'] in [1,2,3]:
                    return 1
                else:
                    return age_fare(data)
            elif data['Initial'] =='Mrs':
                if data['SibSp_Parch'] in [3,4,5]:
                    return 1
                else:
                    return age_fare(data)
            else:
                return age_fare(data)
                
        else: # Pclass 3
            if data['Initial'] == 'Miss':
                if data['SibSp_Parch'] in [4,5]:
                    return 0
                else:
                    return age_fare(data)
            elif data['Initial'] == 'Mrs':
                return age_fare(data)
            else:
                return age_fare(data)
    
    else: # male
        if data['Pclass'] == 1:
            if data['Initial'] == 'Master':
                return 1
            elif data['Initial'] == 'Mr':
                if data['SibSp_Parch'] == 3:
                    return 1
                elif data['SibSp_Parch'] in [4,5]:
                    return 0
                else:
                    return age_fare(data)
            else:
                return age_fare(data)
                
        elif data['Pclass'] == 2:
            if data['Initial'] == 'Master':
                return 1
            elif data['Initial'] == 'Mr':
                if data['SibSp_Parch'] in [2,3]:
                    return 0
                else:
                    return age_fare(data)
            elif data['Initial'] == 'Other':
                return 0
            else:
                return age_fare(data)
        
        else: # Pclass 3
            if data['Initial'] == 'Master':
                if data['SibSp_Parch'] in [1,2,3]:
                    return 1
                elif data['SibSp_Parch'] in [4,5]:
                    return 0
                else:
                    return age_fare(data)
            elif data['Initial'] == 'Master':
                if data['SibSp_Parch'] in [3,4,5,6]:
                    return 0
                else:
                    return age_fare(data)
            else:
                return age_fare(data)          
         

4-2. 예측하기

# 예측하기
pred_list = []
for i in range(len(train)):
    pred = predict_survival(train.iloc[i,:])
    pred_list.append(pred)

pred = np.array(pred_list)

# 계산을 위해 생존여부를 숫자로 변환
train['Survived'] = train['Survived'].apply(lambda x: 1 if x=='Yes' else 0)

# 정확도 계산
acc = (train.shape[0] - (train[(train['Survived'] - pred) !=0].shape[0]))/train.shape[0]
print('Train Set 에서의 정확도 : {}'.format(acc))

# Train Set 에서의 정확도 : 0.7609427609427609

 

5. 테스트셋에 적용 및 결과 분석

5-1. 테스트셋 전처리

# Initial 컬럼 추가
test['Initial']=0
for i in test:
    test['Initial']=test.Name.str.extract('([A-Za-z]+)\.')
test['Initial'].replace(['Mlle','Mme','Ms','Dr','Major','Lady','Countess','Jonkheer','Col','Rev','Capt','Sir','Don','Dona'],['Miss','Miss','Miss','Mr','Mr','Mrs','Mrs','Other','Other','Other','Mr','Mr','Mr','Mrs'],inplace=True)

# Age 결측치 처리
# test.groupby('Initial')['Age'].mean()
test.loc[(test.Age.isnull())&(test.Initial=='Mr'),'Age']=32
test.loc[(test.Age.isnull())&(test.Initial=='Mrs'),'Age']=39
test.loc[(test.Age.isnull())&(test.Initial=='Master'),'Age']=7
test.loc[(test.Age.isnull())&(test.Initial=='Miss'),'Age']=22
test.loc[(test.Age.isnull())&(test.Initial=='Other'),'Age']=43

# Fare 결측치 처리
test['Fare'].fillna(0, inplace=True)

# 인당 가격 컬럼 추가 (per_fare)
per_fare = test['Fare'].groupby(test['Ticket']).agg({('fare','mean'),('num','count')})
per_fare['per_fare'] = per_fare['fare']/per_fare['num']
per_fare.drop(['fare','num'], axis=1,inplace=True)
test = pd.merge(test, per_fare, on='Ticket')

# SibSp와 Parch 합 생성
test['SibSp'] = test['SibSp'].apply(lambda x: int(x))
test['Parch'] = test['Parch'].apply(lambda x: int(x))
test['SibSp_Parch'] = test['SibSp'] + test['Parch'] 

# 불필요한 컬럼 삭제
test.drop(['PassengerId','Name','Ticket','Fare','Cabin','SibSp','Parch','Embarked'], axis=1, inplace=True)

5-2. 예측하기

pred_list = []
for i in range(len(test)):
    pred = predict_survival(test.iloc[i,:])
    pred_list.append(pred)

pred = np.array(pred_list)

# 결과파일 만들기
submission['Survived'] = pred
submission.to_csv('submission.csv', index=False)

5-3. 결과

캐글에 제출해보니 정확도가 0.52631 로 측정됩니다.

이정도 정확도면 랜덤으로 예측한것과 다름없죠.. 

모델의 성능면에서는 볼품없지만, 이번 포스팅을 준비하면서 배운점은 많습니다.

 

일단 seaborn 과 matplotlib 를 사용하는데에 훨씬 능숙해졌습니다.

이전에는 오류가 날때마다 구글링을 했지만, 지금은 혼자서도 잘 고칩니다 ㅎㅎ

 

그리고 제가 모델링한 방법이 결국은 트리기반 알고리즘의 개념이었다는 점이 흥미로웠습니다.

지니계수와 복잡도(엔트로피)등이 필요한 이유, 그리고 feature importance 를 그려봤을 때 상위권에 위치한 특성의 특징등을 좀 더 가깝게 이해할 수 있었습니다.

 

또한 EDA를 통해 잘 알려지지 않은 숨겨진 정보 (ex. Cabin의 규칙, Ticket의 규칙)를 찾았을 땐, 

정말이지 피쳐 엔지니어링의 잠재력은 끝이 없다는 것을 알게됐습니다.

 

많은 시간을 투자한 포스팅인 만큼 많은 사람에게 도움이 됐으면 좋겠네요 ㅎㅎ

데이터 공부하시는 분들 모두 화이팅 입니다!!