컴페티션 목표 : 머신러닝을 이용하여 타이타닉 승객들의 생존 여부를 예측
Survived 정보를 담은 csv파일을 제출하여 정답과의 일치율을 확인
작성자 : 김소륜
작성일 : 2023-03-31
파트 : 전처리
- sex
- embarked
- Pcalss
# data analysis and wrangling
import pandas as pd
import numpy as np
import random as rnd
# visualization
import seaborn as sns
import matplotlib.pyplot as plt
%matplotlib inline
# machine learning
from sklearn.linear_model import LogisticRegression
from sklearn.svm import SVC, LinearSVC
from sklearn.ensemble import RandomForestClassifier
from sklearn.neighbors import KNeighborsClassifier
from sklearn.naive_bayes import GaussianNB
from sklearn.linear_model import Perceptron
from sklearn.linear_model import SGDClassifier
from sklearn.tree import DecisionTreeClassifier
기본적인 모듈을 임포트 해줍니다.
raw data 불러오기
전처리
train_df = pd.read_csv('/content/drive/MyDrive/CODE/kaggle/binary classification/titanic/train.csv')
test_df = pd.read_csv('/content/drive/MyDrive/CODE/kaggle/binary classification/titanic/test.csv')
combine = [train_df, test_df]
print(train_df.columns.values)
출력값 : ['PassengerId' 'Survived' 'Pclass' 'Name' 'Sex' 'Age' 'SibSp' 'Parch' 'Ticket' 'Fare' 'Cabin' 'Embarked']
전처리를 위해 데이터 종류를 확인합니다.
- 범주형
- 명목형 : Survived, Sex, Embarkd
- 순서형 : PClass
- 수치형
- 이산형 : SibSp, Parch
- 연속형 : Age, Fare
수치형 데이터 분포
train_df.describe(include="all")
- 891명의 샘플로 총 인원의 40%가 데이터로 주어집니다.
- Survived는 0과 1로 이루어진 범주형 데이터입니다.
- 대부분의 승객(>75%)은 부모 또는 자식과 함께 타지 않았습니다.
- 30%의 승객은 형제자매나 배우자와 함께 탔습니다.
- Fares(요금)의 경우, 매우 적은 수의 승객이 512$ 보다 많이 지불했습니다.
- 나이가 많은 승객(65-80)의 수는 1% 미만입니다.
범주형 데이터의 분포
train_df.describe(include=['O'])
- 이름(Name)은 모두 다릅니다.
- 성별(Sex)은 577/891이 남자로 약 65%를 차지합니다.
- 호실(Cabin)은 양도 적고, 중복도 많이됩니다. 일부 손님들이 호실을 공유한 것으로 보입니다.
- 승선(Embarked)은 총 3개의 종류가 있고, S가 제일 많습니다.
- 티켓(Ticket)은 중복이 매우 적습니다.
가정
- The upper-class passengers (Pclass=1)이 더 많이 생존했습니다.
- Women (Sex=female)이 더 많이 생존했습니다.
- Embarked은 생존 여부와 관계가 있습니다. (고로 다른 특성과 연관이 있으므로 결측값을 채워넣어야 합니다.)
Pclass
train_df[['Pclass', 'Survived']].groupby(['Pclass'], as_index=False).mean().sort_values(by='Survived', ascending=False)
Pclass=1인 승객에 대해서 생존 확률이 0.5보다 높은 것을 알 수 있습니다. 이는 모델에 특성으로 넣으면 됩니다.
grid = sns.FacetGrid(train_df, col='Survived', row='Pclass', height=2.2, aspect=1.6)
grid.map(plt.hist, 'Age', alpha=.5, bins=20)
grid.add_legend();
- Pclass=3인 승객수가 가장 많습니다. 하지만 대다수가 생존하지 못했습니다.
- Pclass=2, Pclass=3에서 영아 승객은 대부분 생존했습니다.
- Pclass=1인 승객들은 대부분 생존했습니다.
- Pclass에 따라 승객의 나이 분포는 다릅니다.
-> Pclass는 모델에 필요한 특성입니다.
Sex
train_df[["Sex", "Survived"]].groupby(['Sex'], as_index=False).mean().sort_values(by='Survived', ascending=False)
실제로 여성 승객이 생존 확률이 74%로 높은 수치임을 알 수 있습니다.
Sex 특성 변환하기
for dataset in combine:
dataset['Sex'] = dataset['Sex'].map( {'female': 1, 'male': 0} ).astype(int)
train_df.head()
female=1, male=0으로 변환합니다.
Embarked
# grid = sns.FacetGrid(train_df, col='Embarked')
grid = sns.FacetGrid(train_df, row='Embarked', height=2.2, aspect=1.6)
grid.map(sns.pointplot, 'Pclass', 'Survived', 'Sex', palette='deep')
grid.add_legend()
- 여성 승객은 높은 비율로 생존했습니다.
- 예외적으로 Embarked=C인 남성 승객의 생존 비율은 높습니다. 이는 Pclass와의 상관관계일 수 있으며, 특성들은 꼭Survived와 직접적으로 연관될 필요는 없습니다.
- Sex 특성은 모델에 필요합니다.
- Embarked 특성은 빈 부분을 채워야 하고, 모델에 있어 필요한 특성입니다.
# grid = sns.FacetGrid(train_df, col='Embarked', hue='Survived', palette={0: 'k', 1: 'w'})
grid = sns.FacetGrid(train_df, row='Embarked', col='Survived', height=2.2, aspect=1.6)
grid.map(sns.barplot, 'Sex', 'Fare', alpha=.5, ci=None)
grid.add_legend()
- 높은 요금을 낸 승객은 생존율이 더 높았습니다.
Embarked 특성 채우기
Embarked 특성은 S, Q, C값을 가집니다. 하지만 훈련 데이터셋에 총 2개의 값이 Null값입니다. 최빈값으로 이를 채웁니다.
freq_port = train_df.Embarked.dropna().mode()[0]
freq_port
출력값 : 'S'
for dataset in combine:
dataset['Embarked'] = dataset['Embarked'].fillna(freq_port)
train_df[['Embarked', 'Survived']].groupby(['Embarked'], as_index=False).mean().sort_values(by='Survived', ascending=False)
Embarked 특성 변환하기 (수치형)
for dataset in combine:
dataset['Embarked'] = dataset['Embarked'].map( {'S': 0, 'C': 1, 'Q': 2} ).astype(int)
train_df.head()
범주형 특성을 수치형으로 변환합니다.
작성자 : 박세진, 하서진
작성일 : 2023-04-01
파트 : 전처리
- Name
- Age
- SibSp, Parch
Name
Name 특성 확인
dataset['Name']
- 이름에서 Mr, Mrs 등과 같은 호칭인 단어들을 다수 확인할 수 있습니다.
- 이러한 단어들은 그 사람의 성별과 나이대와 관련이 있다고 판단할 수 있습니다.
따라서 호칭인 단어들을 출력하여 Title열을 새로 만들어서 정리합니다.
for dataset in combine:
dataset['Title'] = dataset.Name.str.extract(' ([A-Za-z]+)\.', expand=False) #이름을 .으로 분리하여 .앞의 글자(호칭)만 추출
pd.crosstab(train_df['Title'], train_df['Sex']) #이름에서 나타나는 성별 특성 구분
- 호칭을 통해 성별의 유추가 가능합니다.
등장 빈도가 적은 호칭은 Rare로 묶어주고 동일한 의미의 단어들은 한 단어로 통일합니다.
for dataset in combine:
dataset['Title'] = dataset['Title'].replace(['Lady', 'Countess','Capt', 'Col',\
'Don', 'Dr', 'Major', 'Rev', 'Sir', 'Jonkheer', 'Dona'], 'Rare') #소수인 것은 묶어서 rare로 바꿈
dataset['Title'] = dataset['Title'].replace('Mlle', 'Miss')
dataset['Title'] = dataset['Title'].replace('Ms', 'Miss')
dataset['Title'] = dataset['Title'].replace('Mme', 'Mrs')
train_df[['Title', 'Survived']].groupby(['Title'], as_index=False).mean()
Title 숫자 부여
호칭의 종류에 따라 각각 숫자를 부여합니다. 호칭이 없는 열들은 0을 부여합니다.
title_mapping = {"Mr": 1, "Miss": 2, "Mrs": 3, "Master": 4, "Rare": 5} #호칭을 숫자로 변환
for dataset in combine:
dataset['Title'] = dataset['Title'].map(title_mapping)
dataset['Title'] = dataset['Title'].fillna(0) #결측값 0으로 채우기
train_df.head()
Age
Age 특성 확인
grid = sns.FacetGrid(train_df, col='Survived', row='Pclass',height=2.2,aspect=1.6)
grid.map(plt.hist, 'Age', alpha=.5, bins=20)
grid.add_legend();
- 객실 등급 별로 나이 대 분포가 다르고 이에 따른 생존 여부도 다르다는 것을 알 수 있습니다.
- 결측값을 채우기 위해 객실 등급 정보를 이용해야 하는 것을 알 수 있습니다.
grid = sns.FacetGrid(train_df, row='Pclass', col='Sex', height=2.2, aspect=1.6)
grid.map(plt.hist, 'Age', alpha =.5, bins=20)
grid.add_legend()
- 성별과 객실 등급에 따라서 나이 분포가 다르다는 것을 확인할 수 있습니다.
- 성별과 객실 등급을 고려하며 나이의 결측값을 채우도록 하겠습니다.
Age 결측값 채우기
나이의 결측값을 채우기 위해 우선 (2,3)의 빈 행렬을 만들어 줍니다.
guess_ages = np.zeros((2,3)) #(2,3)의 행렬 생성
guess_ages
for문을 이용하여 결측값을 제외한 성별과 객실등급에 따른 나이 정보를 guess_df에 저장합니다. Age_guess에 저장한 나이 정보의 중앙값을 저장합니다. 0.5단위로 나이를 반올림하여 미리 만들어둔 빈 행렬에 저장한 뒤, 나이의 결측값에 해당하는 성별과 객실등급의 나이의 중앙값을 채웁니다.
for dataset in combine: #성별과 객실 등급을 고려하여 나이의 결측값을 채움
for i in range(0, 2):
for j in range(0, 3):
guess_df = dataset[(dataset['Sex'] == i) & \
(dataset['Pclass'] == j+1)]['Age'].dropna()
age_guess = guess_df.median() # 중앙값 사용
# 0.5단위의 나이로 반올림
#ex23.3-->23.5, 34.12-->34.0
guess_ages[i,j] = int( age_guess/0.5 + 0.5 ) * 0.5
for i in range(0, 2): #값이 비어있으면 구한 중앙값 넣기
for j in range(0, 3):
dataset.loc[ (dataset.Age.isnull()) & (dataset.Sex == i) & (dataset.Pclass == j+1),\
'Age'] = guess_ages[i,j]
dataset['Age'] = dataset['Age'].astype(int) #정수형으로 변환
train_df.head()
Age 구간 분류
AgeBand열을 새로 만들어 나이를 10개 구간으로 나누어 각 구간에 따른 생존률을 구합니다.
train_df['AgeBand'] = pd.cut(train_df['Age'], 10) #전체 나이를 10등분
train_df[['AgeBand', 'Survived']].groupby(['AgeBand'], as_index=False).mean().sort_values(by='AgeBand', ascending=True)
- 나이 대 역시 생존률에 영향을 준다는 것을 알 수 있습니다
Age 숫자 부여
나이 구간 별로 각각 숫자를 부여합니다.
for dataset in combine: #각 나이대별로 숫자 부여
dataset.loc[ dataset['Age'] <= 8, 'Age'] = 0
dataset.loc[(dataset['Age'] > 8) & (dataset['Age'] <= 16), 'Age'] = 1
dataset.loc[(dataset['Age'] > 16) & (dataset['Age'] <= 24), 'Age'] = 2
dataset.loc[(dataset['Age'] > 24) & (dataset['Age'] <= 32), 'Age'] = 3
dataset.loc[(dataset['Age'] > 32) & (dataset['Age'] <= 40), 'Age'] =4
dataset.loc[(dataset['Age'] > 40) & (dataset['Age'] <= 48), 'Age'] =5
dataset.loc[(dataset['Age'] > 48) & (dataset['Age'] <= 56), 'Age'] = 6
dataset.loc[(dataset['Age'] > 56) & (dataset['Age'] <= 64), 'Age'] = 7
dataset.loc[(dataset['Age'] > 64) & (dataset['Age'] <= 72), 'Age'] = 8
dataset.loc[ dataset['Age'] > 72, 'Age'] = 9
train_df.head()
AgeBand열을 제거해줍니다.
train_df = train_df.drop(['AgeBand'], axis=1) #AgeBand 제거
combine = [train_df, test_df]
train_df.head()
SibSp, Parch
SibSp, Parch 특성 확인
가족 구성원의 수를 구하기 위하여 형제자매의 수를 뜻하는 SibSp 열과 부모자식의 수를 뜻하는 Parch 열과 본인을 더하여 FamilySize라는 열에 저장합니다. 가족 구성원 수에 따른 생존률을 확인합니다.
for dataset in combine: #가족 구성원의 수 구하기
dataset['FamilySize'] = dataset['SibSp'] + dataset['Parch'] + 1 #본인포함
train_df[['FamilySize', 'Survived']].groupby(['FamilySize'], as_index=False).mean().sort_values(by='Survived', ascending=False)
a=train_df.groupby('FamilySize').agg(survived=('Survived','mean'))
a.plot(kind='bar') #bar차트로 시각화
IsAlone 활용하기
가족 구성원 수가 1명인 사람의 특성을 활용하겠습니다. IsAlone열을 만들어 FamilySize가 1인 경우 1을 저장하고 아닌 경우 0을 저장합니다. IsAlone에 따른 생존률을 확인합니다.
for dataset in combine: #FamilySize가 1인 사람과 아닌 사람 구분
dataset['IsAlone'] = 0
dataset.loc[dataset['FamilySize'] == 1, 'IsAlone'] = 1
train_df[['IsAlone', 'Survived']].groupby(['IsAlone'], as_index=False).mean()
Parch, SibSp, FamilySize열을 제거해줍니다.
train_df = train_df.drop(['Parch', 'SibSp', 'FamilySize'], axis=1) #Parch, SibSp, FamilySize 제거
test_df = test_df.drop(['Parch', 'SibSp', 'FamilySize'], axis=1)
combine = [train_df, test_df]
이 과정을 모두 거친 train_df는 다음과 같습니다.
train_df.head()
작성자 : 송정현
작성일 : 2023-03-31
파트 : 데이터 전처리
앞의 전처리에서 Name부분을 Title로 대체했으므로, Name Column을 제거해줍니다.
train_df = train_df.drop(['Name'], axis=1) #Name 제거
test_df = test_df.drop(['Name'], axis=1)
combine = [train_df, test_df]
Ticket
Ticket 특성 확인
print(train_df['Ticket'].isna().any()) # 결측값 확인
print(train_df['Ticket'].value_counts())
출력값을 보면 Ticket column의 결측치는 없으나, 값들이 분류할 수 없을 정도로 제각각임을 알 수 있습니다.
Ticket 외에 Fare(티켓 가격) Cabin(선실 등급) 등 Ticket의 정보를 대체할 수 있는 데이터가 많으므로 Ticket column을 제거해 줍니다.
train_df = train_df.drop(['Ticket'], axis=1) # Ticket 제거
test_df = test_df.drop(['Ticket'], axis=1)
combine = [train_df, test_df]
train_df.head()
결과값을 출력하면 다음과 같습니다.
Fare & Cabin
Fare 특성 확인
# train 데이터
print(train_df['Fare'].describe())
nan_count = train_df['Fare'].isna().sum()
print("Number of NaN values:", nan_count)
# test 데이터
print(test_df['Fare'].describe())
nan_count = test_df['Fare'].isna().sum()
print("Number of NaN values:", nan_count)
Fare의 경우 연속형 자료로써 다음과 같은 평균, 4분위 값 등의 정보를 알 수 있습니다.
또한 train_df에서는 결측치가 없는 반면, test_df에는 결측치가 한 개 있음을 알 수 있습니다.
Cabin 특성 확인
먼저 Cabin column의 특성을 파악하기 위해 다음과 같은 작업을 진행합니다.
# train data
print(train_df['Cabin'].describe())
nan_count = train_df['Cabin'].isna().sum()
print("Number of NaN values:", nan_count)
# test data
print(test_df['Cabin'].describe())
nan_count = test_df['Cabin'].isna().sum()
print("Number of NaN values:", nan_count)
describe 메서드를 사용하여 데이터의 개수, 고유값 등 범주형 데이터의 전반적인 내용을 파악할 수 있습니다.
이를 통해 train데이터에서는 전체 891개의 데이터 중, nan(결측값)을 제외한 값이 204개(count)임을,
test 데이터에서는 전체 418개의 데이터 중, nan(결측값)을 제외한 값이 91개(count)임을 파악할 수 있습니다.
따라서, cabin자료형을 이용하기 위해서는 위의 대부분의 결측치를 채워 넣어야 합니다.
어떻게?
Cabin(객실)은 Pclass(승객의 등급)에 따라 구분될 수 밖에 없고, Pclass data의 결측치가 없는 것을 앞에서 보았습니다.
Cabin과 Pclass간의 연관성이 있지 않을까?
이에 대해 Chat gpt한테 물어봤습니다 : "타이타닉에서 Cabin과 Pclass의 연관성을 Cabin의 각 층을 기준으로 설명해줘"
타이타닉 호의 객실은 A 데크부터 G 데크까지 총 7층에 걸쳐 배치되어 있습니다. 각 층은 다양한 객실 등급의 객실을 수용하고 있으며, 객실의 위치와 크기는 객실 등급에 따라 달랐습니다.
A 데크: A 데크는 가장 상층에 위치하며, 1등실 객실과 스위트룸, 그리고 상류층 승객을 위한 공용실과 라운지가 위치해 있습니다.
B 데크: B 데크는 1등실 객실과 스위트룸이 주로 위치한 층입니다. 또한, 승무원의 숙소와 상류층 승객을 위한 다양한 시설과 공용실이 위치해 있습니다.
C 데크: C 데크는 1등실과 2등실 객실이 위치한 층입니다. 이 층에는 상류층 승객을 위한 라운지와 다양한 시설, 그리고 무대와 연주실도 있습니다.
D 데크: D 데크는 2등실 객실이 위치한 층입니다. 이 층에는 상류층 승객을 위한 공용실과 라운지가 위치해 있습니다.
E 데크: E 데크는 1등실, 2등실, 그리고 3등실 객실이 모두 위치한 층입니다. 이 층에는 다양한 시설과 공용실이 위치해 있습니다.
F 데크: F 데크는 2등실과 3등실 객실이 위치한 층입니다. 이 층에는 다양한 시설과 공용실이 위치해 있습니다.
G 데크: G 데크는 가장 하층에 위치하며, 3등실 객실과 승무원의 숙소가 위치해 있습니다.
따라서, 타이타닉 호의 각 층은 객실 등급과 승객들의 지위와 관련이 있으며, 이는 생존율과도 연관이 있었습니다.
위 대답에서 각 Cabin의 승객 분포를 Pclass를 기준으로 나타내면 다음과 같습니다.
Pclass1 : Cabin A,B,C
Pclass2 : Cabin C,D,E
Pclass3 : Cabin D,E,F
위의 Fare와 Pclass의 정보를 통해 Cabin의 결측치를
다음과 같은 순서로 채워 넣을 수 있습니다.
1. Pclass 등급별 Fare의 분포 파악 및 이상치 제거
2. Fare 값을 3등분한 값을 각 Pclass별 Cabin에 적용
- 이에 대해 Chat gpt한테 물어봤습니다 : "수치형 데이터를 삼등분하기 위해서는 어떤 방법을 써야 하지?"
수치형 데이터를 세 부분으로 나누기 위해서는 데이터의 사분위수를 사용할 수 있습니다. 사분위수는 데이터를 네 개의 동일한 부분으로 나눕니다. 첫 번째 사분위수(Q1)는 25% 백분위수를 나타내며, 두 번째 사분위수는 50% 백분위수(중앙값)를 나타내며, 세 번째 사분위수(Q3)는 75% 백분위수를 나타냅니다.
데이터를 세 부분으로 나누기 위해서는 Q1과 Q3 값을 분할 지점으로 사용할 수 있습니다. Q1 이하의 데이터 포인트는 첫 번째 부분에 속하고, Q1보다 크지만 Q3 이하인 데이터 포인트는 두 번째 부분에 속하고, Q3보다 큰 데이터 포인트는 세 번째 부분에 속합니다.
이 방법은 대략적으로 정규 분포되거나 대칭적으로 분포하는 데이터를 분석하는 데 유용합니다. 그러나 데이터가 심하게 치우친 경우에는 이 방법이 적절하지 않을 수 있으며, 백분위수 또는 분위수와 같은 다른 방법을 대신 사용할 수 있습니다.
3. train_df, test_df에 동일한 작업 진행 및 결측값 채우기
위 정보를 바탕으로 train data부터 분석을 진행합니다.
df_ncabin = train_df[train_df['Cabin'].isnull()] #train_df의 cabin column에서 nan값만 추출
train_df.groupby('Pclass').agg(Fare_mean=('Fare','mean'),
Fare_mid=('Fare','median'),
Fare_max=('Fare','max'),
Fare_min=('Fare', 'min'))
다음과 같이 Pclass 별로 Fare의 평균, 중간값, 최대/최소 값을 파악합니다.
Fare값을 3등분하는데 활용할 함수 두 가지를 정의해 줍니다.
첫 번째 함수는 Fare를 삼등분 하는데 있어서의 기준을 하한선(lower_bound) : q1(전체 값의 25%), 상한선(upper_bound) : q3(전체 값의 75%)로 지정해주는 함수입니다.
두 번째 함수는 Fare의 이상치를 제거하는 함수로써, 3등분의 기준에 대한 신뢰성을 높이는 역할을 하는 함수입니다.
# Fare를 3등분하는 함수
# lower : 1분할 값
# upper :3분할 값
def data_devision(data):
df = data
q1 = np.percentile(df, 25)
q3 = np.percentile(df, 75)
upper_bound = q3
lower_bound = q1
return [lower_bound, upper_bound]
# 이상값 제거를 위한 함수
def choose_index(df, c):
data = df
choose_index = []
for i in range(len(data)):
if i <= c:
choose_index.append(i)
return max(choose_index)
위 함수를 이용하여 Pclass 1,2,3의 Fare값을 분석합니다.
# Pclass 1
sns.distplot(df_ncabin[df_ncabin['Pclass'] == 1]['Fare'])
plt.show()
# Pclass 2
sns.distplot(df_ncabin[df_ncabin['Pclass'] == 2]['Fare'])
plt.show()
# Pclass 3
sns.distplot(df_ncabin[df_ncabin['Pclass'] == 3]['Fare'])
plt.show()
Pclass 별 Fare값을 분포도로 나타내면 다음과 같습니다
각 그림에서 이상치를 시각적으로 확인할 수 있습니다.
# Pclass 1
Fare_list_p1 = sorted(df_ncabin[df_ncabin['Pclass'] == 1]['Fare'])
Fare_list_p1 = Fare_list_p1[:choose_index(Fare_list_p1, 200)] #이상치 제거
devision_p1 = data_devision(Fare_list_p1) #fare를 삼등분하는 upper_bound, lower_bound 생성
# Pclass 2
Fare_list_p2 = sorted(list(df_ncabin[df_ncabin['Pclass'] == 2]['Fare']))
Fare_list_p2 = Fare_list_p2[:choose_index(Fare_list_p2, 60)] #이상치 제거
devision_p2 = data_devision(Fare_list_p2)
# Pclass 3
Fare_list_p3 = sorted(list(df_ncabin[df_ncabin['Pclass'] == 3]['Fare']))
Fare_list_p3 = Fare_list_p3[:choose_index(Fare_list_p3, 40)] #이상치 제거
devision_p3 = data_devision(Fare_list_p3)
앞에서 정의한 함수를 이용하여 이상치를 제거하고 Fare를 삼등분하는 upper/lower bound를 생성합니다.
# 각 Pclass에 해당하는 상한(upper)/ 하한(lower)선과 추정되는 Cabin의등급을 dict로 저장
devision_dict = {1 : [devision_p1, ['A', 'B', 'C']],
2 : [devision_p2,['C', 'D', 'E']],
3 : [devision_p3, ['E', 'F', 'G']]}
Pclass 1,2,3에 대해 작업이 반복되므로 Pclass에 대한 Fare 구분선과 Cabin 값을 위처럼 dict 자료형으로 정리합니다.
# Cabin_list 만들기
Cabin_list = [None] * len(train_df) #train_df의 길이와 동일한 빈 리스트 생성
for i in devision_dict.keys():
df = df_ncabin[df_ncabin['Pclass'] == i] # Pclass 지정
for j in df.index:
if df['Fare'][j] <= devision_dict[i][0][0]:# lower_bound
Cabin_list[j] = devision_dict[i][1][2] # Cabin list의 우측에 있는 값
elif df['Fare'][j] <= devision_dict[i][0][1]: # upper_bound
Cabin_list[j] = devision_dict[i][1][1] # Cabin list의 중간에 있는 값
elif df['Fare'][j] > devision_dict[i][0][1]: # upper_bound
Cabin_list[j] = devision_dict[i][1][0] # Cabin list의 중간에 있는 값
dict의 요소를 활용한 이중 for문으로 Cabin이 결측치로 나타나 있는 인덱스에 값을 추가합니다.
지금까지 train_df에서 Cabin 값이 결측치인 인덱스만 가지고 작업하였습니다. 따라서 이 결괏값을 Cabin 값이 존재하는 기존의 자료와 합쳐야 합니다.
df_Cabin_notnan = train_df['Cabin'].dropna(axis=0, how='any') #nan 값 제거
for i in df_Cabin_notnan.index:
Cabin_list[i] = df_Cabin_notnan[i][0] #각 자리의 알파벳만 따옴
train_df에서 Cabin 값이 nan이 아닌 부분만 가져옵니다. 앞서 봤듯이 Cabin의 값은 각 Cabin의 알파벳과 숫자로 이루어져 있습니다. 따라서 각 값의 첫 번째 값(알파벳)만을 값으로 가져와서 앞의 Cabin리스트와 합칩니다.
train_df['Cabin'] = Cabin_list
set(train_df['Cabin'])
출력값 : {'A', 'B', 'C', 'D', 'E', 'F', 'G', 'T'} : 출력값 'T'가 섞여있으므로 이를 제거해줍니다.
train_df[train_df['Cabin']=='T']
Cabin이 T인 자료형을 확인해보니, Pclass는 1이고 Fare는 35.5였습니다. 이에, 앞서 정해놨던 기준에 따라 해당 값을 'B'로 바꿔주었습니다.
train_df['Cabin'].value_counts()
test data도 동일한 방식으로 진행합니다.
df_ncabin1 = test_df[test_df['Cabin'].isnull()] #df_train의 nan값만 추출
print(test_df.groupby('Pclass').agg(Fare_mean=('Fare','mean'),
Fare_mid=('Fare','median'),
Fare_max=('Fare','max'),
Fare_min=('Fare', 'min')))
print(test_df[['Pclass', 'Fare']].groupby(['Pclass'], as_index=False).apply(lambda x: x.mode().iloc[0]).sort_values(by='Fare', ascending=False))
# Fare를 기준을 가지고 3등분하는 함수
def data_devision(data):
df = data
q1 = np.percentile(df, 25)
q3 = np.percentile(df, 75)
upper_bound = q3
lower_bound = q1
return [lower_bound, upper_bound]
# 이상값 제거를 위한 함수
def choose_index(df, c):
data = df
choose_index = []
for i in range(len(data)):
if i <= c:
choose_index.append(i)
return max(choose_index)
Pclass 1,2,3의 Fare값을 분석합니다.
sns.distplot(df_ncabin1[df_ncabin1['Pclass'] == 1]['Fare'])
plt.show()
sns.distplot(df_ncabin1[df_ncabin1['Pclass'] == 2]['Fare'])
plt.show()
sns.distplot(df_ncabin1[df_ncabin1['Pclass'] == 3]['Fare'])
plt.show()
set(df_ncabin1[df_ncabin1['Pclass'] == 3]['Fare'])
출력값 : {nan, 3.1708, 6.4375, 6.4958, 6.95, 7.0, 7.05, 7.225, 7.2292, 7.25, 7.2833, 7.55, 7.575, 7.5792, 7.6292, 7.65, 7.7208, 7.725, 7.7333, 7.75, 7.775, 7.7792, 7.7958, 7.8208, 7.8292, 7.85, 7.8542, 7.8792, 7.8875, 7.8958, 7.925, 8.05, 8.1125, 8.5167, 8.6625, 8.7125, 8.9625, 9.225, 9.325, 9.35, 9.5, 12.1833, 12.2875, 13.4167, 13.775, 13.9, 14.1083, 14.4, 14.4542, 14.4583, 14.5, 15.1, 15.2458, 15.5, 15.55, 15.7417, 15.9, 16.1, 17.4, 18.0, 20.2125, 20.25, 20.575, 21.075, 21.6792, 22.025, 22.3583, 22.525, 23.25, 23.45, 24.15, 25.4667, 29.125, 31.3875, 34.375, 39.6875, 46.9, 56.4958, 69.55}
*Pclass3의 경우 시각적으로 바로 판단이 어려워 직접 값을 보고 이상치를 제거하였습니다.
# Pclass 1
Fare_list_p1_1 = sorted(df_ncabin1[df_ncabin1['Pclass'] == 1]['Fare'])
Fare_list_p1_1 = Fare_list_p1[:choose_index(Fare_list_p1, 100)]
devision_p1_1 = data_devision(Fare_list_p1_1)
# Pclass 2
Fare_list_p2_1 = sorted(list(df_ncabin1[df_ncabin1['Pclass'] == 2]['Fare']))
Fare_list_p2_1 = Fare_list_p2[:choose_index(Fare_list_p2, 60)] #이상치 제거
devision_p2_1 = data_devision(Fare_list_p2_1)
# Pclass 3
Fare_list_p3_1 = sorted(list(df_ncabin1[df_ncabin1['Pclass'] == 3]['Fare']))
Fare_list_p3_1 = Fare_list_p3_1[:choose_index(Fare_list_p3_1, 34)] #이상치 제거
devision_p3 = data_devision(Fare_list_p3)
# 각 Pclass에 해당하는 상한(upper)/ 하한(lower)선과 추정되는 Cabin의등급을 dict로 저장
devision_dict = {1 : [devision_p1, ['A', 'B', 'C']],
2 : [devision_p2,['C', 'D', 'E']],
3 : [devision_p3, ['E', 'F', 'G']]}
# Cabin_list 만들기
Cabin_list_1 = [None] * len(test_df)
for i in devision_dict.keys():
df = df_ncabin1[df_ncabin1['Pclass'] == i]
for j in df.index:
if df['Fare'][j] <= devision_dict[i][0][0]:
Cabin_list_1[j] = devision_dict[i][1][2]
elif df['Fare'][j] <= devision_dict[i][0][1]:
Cabin_list_1[j] = devision_dict[i][1][1]
elif df['Fare'][j] > devision_dict[i][0][1]:
Cabin_list_1[j] = devision_dict[i][1][0]
# 기존의 데이터와 합치기
df_Cabin_notnan_1 = test_df['Cabin'].dropna(axis=0, how='any') #nan 값 제거
for i in df_Cabin_notnan_1.index:
Cabin_list_1[i] = df_Cabin_notnan_1[i][0] #각 자리의 알파벳만 따옴
test_df['Cabin'] = Cabin_list_1
완성된 두 데이터를 다시 한번 살펴보았습니다.
for data in combine:
print('Data : ', len(data))
print(data['Cabin'].describe())
print(data['Cabin'].isna().sum())
다음과 같이 test_df에서 None값이 1개 존재했습니다.
for i in test_df['Cabin'].index:
if test_df['Cabin'][i] == None:
print(i)
None이 어디서 났는지 살펴보았습니다.
출력값 : 152
test_df.loc[152,]
train_df[['Pclass', 'Cabin']].groupby('Pclass').agg(lambda x: x.mode().values[0]) # pclass당 cabin의 최빈값
test_df.loc[152,'Cabin'] = 'E'
# 확인
for data in combine:
print(data['Cabin'].isna().sum())
출력 :0, 0
None의 위치의 데이터를 살펴보고, Pclass3에서 Cabin의 최빈값으로 None값을 채워넣었고, none값이 모두 사라졌음을 확인했습니다.
전처리 마무리
train_df = train_df.drop(['Fare'], axis=1) #Fare 제거
test_df = test_df.drop(['Fare'], axis=1)
combine = [train_df, test_df]
# Cabin값 숫자형으로 변형
for dataset in combine:
dataset['Cabin'] = dataset['Cabin'].map( {'A': 0, 'B': 1, 'C': 2,
'D' : 3, 'E' : 4, 'F' : 5, 'G' : 6
} ).astype(int)
Fare 컬럼을 제거하고 Cabin의 문자형(알파벳)자료형을 모두 숫자형으로 변경해줍니다.
다음과 같이 데이터가 완성되었습니다.
작성자 : 이유진
작성일 : 2023-04-03
파트 : 머신러닝을 통한 생존율 예측
모델,예측과 해결
이제 모델 훈련할 준비는 모두 끝났습니다. 예측 모델 알고리즘은 60개가 넘기 때문에 정확히 이해하고 적절한 알고리즘을 선택할 줄 알아야 합니다. 이 문제는 분류 문제로 지도학습에 속하는 문제입니다. 지도학습에 사용되는 알고리즘은 다음과 같습니다.
- Logistic Regression
- KNN or k-Nearest Neighbors
- Support Vector Machines
- Naive Bayes classifier
- Decision Tree
- Random Forrest
- Perceptron
- Artificial neural network
- RVM or Relevance Vector Machine
훈련,테스트 세트
위에서 전처리한 데이터셋을 알고리즘에 적용하기 위해 train, test 데이터셋으로 적절하게 나누어줘야 합니다. Train 세트를 만들어줄때 결과값인 생존여부를 제외한 특성들은 X에, 생존여부를 Y에 넣어줍니다.
X_train = train_df.drop(["Survived", "PassengerId"], axis=1)
Y_train = train_df["Survived"]
X_test = test_df.drop("PassengerId", axis=1).copy()
X_train.shape, Y_train.shape, X_test.shape
상당히 많은 머신러닝 모델들을 이용해 문제를 해결할 수 있겠지만, 너무 많기 때문에 여기서는 성능이 좋았거나 살펴볼 만한 모델 몇 가지만 자세히 다뤄보도록 하겠습니다.
# Logistic Regression
logreg = LogisticRegression()
logreg.fit(X_train, Y_train)
Y_pred = logreg.predict(X_test)
acc_log = round(logreg.score(X_train, Y_train) * 100, 2)
acc_log
출력값 : 80.13
첫 번째로 적용해볼 머신러닝 알고리즘은 Logistic Regression입니다. 이는 시작 단계에서 돌려보기 좋은 모델입니다. 이름과는 다르게 분류 알고리즘으로, 회귀를 사용해 데이터가 어떤 범주에 속할 확률을 0에서 1 사이의 값으로 예측하고, 그 확률에 따라 더 높은 법주에 속하는 것으로 분류해주는 지도 학습 알고리즘입니다. 어떤 데이터가 a에 속할 확률이 0.5 이상이면 a 로 분류하고, 확률이 0.5보다 작으면 not a로 분류하는 것으로, 이렇게 데이터가 2개의 범주 중 하나에 속하도록 결정하는 것을 2진분류(binary classification) 이라고 합니다.
로지스틱 회귀를 이해하기 위해선 우선 선형 회귀(Linear Regression)에 대한 개념을 먼저 익혀야만 합니다. 예를 들어 어떤 학생이 공부하는 시간에 따라 시험에 합격할 확률이 달라진다고 할 때, 선형 회귀를 사용하면 아래와 같은 그림으로 나타납니다.
공부한 시간이 적으면 시험에 통과하지 못하고, 시간이 많으면 시험에 통과하는 것을 알 수 있습니다. 그러나 회귀선을 살펴보면 확률이 음과 양의 방향으로 무한대까지 뻗어갑니다. 그렇기 때문에 공부시간이 적으면 통과할 확률이 음수가 되는 비논리적인 상황이 펼쳐집니다.
만약 선형 회귀 대신 로지스틱 회귀를 사용한다면 아래와 같이 나타납니다.
시험에 합격할 확률이 0에서 1사이의 값으로 Sigmoid 함수의 형태로 나타나게 됩니다. 실제 데이터에 로지스틱 회귀 알고리즘을 사용하기 위해선 복잡한 과정을 거쳐야 하는데, 이를 간단하게 요약해보면 다음과 같습니다.
1.모든 속성들의 계수와 절편을 0으로 초기화.
2.각 속석들의 값에 계수를 곱해서 log odds를 구한다.
3.lod odds를 sigmoid 함수에 넣어서 0과 1사이의 확률을 구한다.
데이터가 어떤 범주에 속할지 말지 결정할 확률 컷오프를 Treshold(임계값)이라 하고 기본값은 0.5이지만 데이터의 특성이나 상황에 따라 변할 수 있습니다.
coeff_df = pd.DataFrame(train_df.columns.delete(0))
coeff_df.columns = ['Feature']
coeff_df["Correlation"] = pd.Series(logreg.coef_[0])
coeff_df.sort_values(by='Correlation', ascending=False)
로지스틱 회귀를 사용하면 각 특성이 미치는 영향을 알아볼 수 있습니다. 결정함수에서 사용되는 계수 값들을 알 수 있는 것입니다. 양의 계수를 확률을 증가시키고, 음의 계수를 확률을 감소시킵니다. 위의 표를 살펴보면 어떤 속성들이 생존이라는 결과와 가장 관계가 있는지 쉽게 살펴볼 수 있습니다.
SVM
다음은 Support Vector Machine, SVM이라고 불리는 지도학습 모델을 사용해보았습니다. 서포트 벡터 머신은 결정경계(Decision Boundary), 즉 분류를 위한 선을 정의하는 모델입니다. 그래서 분류되지 않은 새로운 점이 나타나면 경계의 어느 쪽에 속하는지 확인해서 분류 과제를 수행할 수 있게 됩니다.
결국 이 결정 경계라는 것을 어떻게 정의하고 계산하는지 이해하는게 중요합니다.
위 그림에서 실선은 빨간색, 파란색 두 개의 클래스를 정확하게 분류합니다. 선 밖에 새로운 훈련 샘플이 추가되어도 결정경계에는 전혀 영향을 미치지 못하지만 경계에 걸쳐있는 샘플은 그렇지 않습니다. 점선에 걸쳐있는 샘플들은 boundary가 더 밖으로 밀려나게끔 합니다.
이렇게 결정경계에 위치해 Boundary에 결정적 영향을 미치는 샘플을서포트 벡터라고 부르며, 결정경계와 서포트 벡터 사이의 거리를 Margin이라고 합니다.
위의 그래프에서 클래스를 분류하는 결정 경계 중 어떤 것이 가장 이상적일까요? 당연히 초록색 선입니다. 노란색 선은 파란색 클래스에 너무 가깝고 파란색 선은 주황색 클래스에 너무 가깝습니다. 두 클래스와 거리가 가장 먼 decision boundary가 가장 이상적으로 최적의 결정경계로 마진을 최대화합니다. 그렇기 때문에 SVM을 라지 마진 분류라고도 합니다.
# Support Vector Machines
svc = SVC()
svc.fit(X_train, Y_train)
Y_pred = svc.predict(X_test)
acc_svc = round(svc.score(X_train, Y_train) * 100, 2)
acc_svc
# knn
knn = KNeighborsClassifier(n_neighbors = 3)
knn.fit(X_train, Y_train)
Y_pred = knn.predict(X_test)
acc_knn = round(knn.score(X_train, Y_train) * 100, 2)
acc_knn
출력값 : 80.13
# Gaussian Naive Bayes
gaussian = GaussianNB()
gaussian.fit(X_train, Y_train)
Y_pred = gaussian.predict(X_test)
acc_gaussian = round(gaussian.score(X_train, Y_train) * 100, 2)
acc_gaussian
# Perceptron
perceptron = Perceptron()
perceptron.fit(X_train, Y_train)
Y_pred = perceptron.predict(X_test)
acc_perceptron = round(perceptron.score(X_train, Y_train) * 100, 2)
acc_perceptron
# Linear SVC
linear_svc = LinearSVC()
linear_svc.fit(X_train, Y_train)
Y_pred = linear_svc.predict(X_test)
acc_linear_svc = round(linear_svc.score(X_train, Y_train) * 100, 2)
acc_linear_svc
# Stochastic Gradient Descent
sgd = SGDClassifier()
sgd.fit(X_train, Y_train)
Y_pred = sgd.predict(X_test)
acc_sgd = round(sgd.score(X_train, Y_train) * 100, 2)
acc_sgd
의사결정나무(Decision Tree)
다음은 의사결정나무(Decision Tree) 모델의 개념에 대해 설명해보겠습니다. 의사결정나무는, 특정 속성들에 대한 질문을 기반으로 데이터를 분리하는 방법입니다. 이는 사람들이 일상생활에서 어떤 의사 결정을 내리는 과정과 매우 비슷합니다. 의사결정나무는 여러개의 예/아니요 질문을 이어가면서 학습을 진행합니다. 매, 펭귄, 돌고래, 곰을 구분한다고 생각해보았을 때, 매와 펭귄은 날개가 있고, 돌고래와 곰은 날개가 없습니다. ‘날개가 있나요?’라는 질문을 통해서 매, 펭귄/ 돌고래, 곰을 나눌 수 있습니다. 매와 펭귄은 ‘날 수 있나요?’라는 질문으로 나눌 수 있고, 돌고래와 곰을 ‘지느러미가 있나요?’라는 질문으로 나눌 수 있습니다.
이렇게 특정 기준에 따라 데이터를 구분하는 모델을 의사결정나무라고 하며 한번의 분기 때마다 변수 영역을 두 개로 구분합니다. 이때 질문이나 정답을 담은 네모 상자를 노드(Node)라고 합니다. 가장 처음 분류 기준을 Root Node라고 하고, 맨 마지막 노드를 Terminal node라고 합니다.
의사결정나무 알고리즘의 프로세스를 간단히 알아보겠습니다.
먼저 위와 같이 데이터를 가장 잘 구분할 수 있는 질문은 기준으로 나눕니다.
나뉜 각 범주에서 또 다시 데이터를 가장 잘 구분할 수 있는 질문을 기준으로 나눕니다. 이를 지나치겜 많이하면 아래와 같이 오버피팅(과학습)이 됩니다. 의사결정나무에 아무런 파라미터도 주지 않고 모델링 하면 과학습이 발생합니다.
가지치기
오버피팅을 막기 위한 전략으로 가지치기(Pruning)라는 기법이 있습니다. 트리에 가지가 너무 많다면 오버피팅이라 볼 수 있습니다. 가지치기란 나무의 가지를 치는 작업을 말합니다. 즉, 최대 깊이나 터미널 노드의 최대 개수, 혹은 한 노드가 분할하기 위한 최소 데이터 수를 제한하는 것입니다. min_sample_split 파라미터를 조정하여 한 노드에 들어있는 최소 데이터 수를 정해줄 수 있습니다. min_sample_split = 10이면 한 노드에 10개의 데이터가 있다면 그 노드는 더 이상 분기를 하지 않습니다. 또한, max_depth를 통해서 최대 깊이를 지정해줄 수도 있습니다. max_depth = 4이면, 깊이가 4보다 크게 가지를 치지 않습니다.
# Decision Tree
decision_tree = DecisionTreeClassifier()
decision_tree.fit(X_train, Y_train)
Y_pred = decision_tree.predict(X_test)
acc_decision_tree = round(decision_tree.score(X_train, Y_train) * 100, 2)
acc_decision_tree
마지막으로 랜덤 포레스트 모델에 대해 간단하게 설명해보도록 하겠습니다. '무작위 숲'이라는 이름 그 자체처럼 랜덤 포레스트는 훈련을 통해 구성해놓은 다수의 의사결정나무들로부터 분류 결과를 취합해서 결론을 얻는 일종의 인기 투표로 이해할 수 있습니다.
위 그림에서 Yes 라고 답한 나무가 NO 라고 답한 나무가 많으므로 다수결의 원칙에 의해 Yes 라는 결과가 나오게 됩니다.
그렇다면 각각의 나무들은 어떻게 구성될까요.
만약 하나의 나무에 모든 요소(Feature)들은 기반으로 결과를 예측하게 된다면, 분명 과적합 현상이 일어날 것입니다.
만약 30 개의 요소가 있다고 할때, 모든 요소들을 기반으로 하나의 결정나무를 만든다면 가지가 많아질 것이고 이는 과적합 현상으로 이어집니다.
따라서 과적합 현상을 피하기 위해 Random Forest 는 전체 요소 중 랜덤으로 일부만을 선택해 하나의 결정나무를 만들고, 또 전체 요소 중 랜덤으로 일부 요소를 선택해 또 다른 결정나무를 만들며,여러 개의 의사결정 나무를 만드는 방식으로 구성됩니다. 이렇게 만들어진 의사결정나무마다 하나의 예측 값을 내놓습니다.
이렇게 여러 개의 의사결정나무들이 내린 예측 값들 중 가장 많이 나온 값을 최종 예측값으로 정합니다.
# Random Forest
random_forest = RandomForestClassifier(n_estimators=100)
random_forest.fit(X_train, Y_train)
Y_pred = random_forest.predict(X_test)
random_forest.score(X_train, Y_train)
acc_random_forest = round(random_forest.score(X_train, Y_train) * 100, 2)
acc_random_forest
Kaggle competition 제출파일 만들기
models = pd.DataFrame({
'Model': ['Support Vector Machines', 'KNN', 'Logistic Regression',
'Random Forest', 'Naive Bayes', 'Perceptron',
'Stochastic Gradient Decent', 'Linear SVC',
'Decision Tree'],
'Score': [acc_svc, acc_knn, acc_log,
acc_random_forest, acc_gaussian, acc_perceptron,
acc_sgd, acc_linear_svc, acc_decision_tree]})
models.sort_values(by='Score', ascending=False)
모델 비교
위에서 했던 머신러닝의 결과를 한 표로 정리하였습니다. Random Forest 와 Decision Tree 의 점수가 같지만, Random Forest 가 과적합이 더 적게 일어나므로 Random Forest를 선택하는 것이 적절합니다.
# Random Forest
random_forest = RandomForestClassifier(n_estimators=100)
random_forest.fit(X_train, Y_train)
Y_pred = random_forest.predict(X_test)
random_forest.score(X_train, Y_train)
acc_random_forest = round(random_forest.score(X_train, Y_train) * 100, 2)
print(acc_random_forest)
print(Y_pred)
88.33
[0 0 0 0 1 0 0 0 1 0 0 0 1 0 1 1 0 1 0 1 0 0 1 0 1 0 1 1 0 0 0 0 1 1 0 1 0 0 0 0 0 0 0 1 1 0 0 0 1 1 0 0 1 1 0 0 0 0 0 1 0 0 0 1 1 1 1 0 1 1 1 0 0 1 1 1 0 1 0 1 1 0 0 0 0 0 0 0 1 1 1 0 1 0 1 0 1 0 0 0 1 0 0 0 1 0 0 0 0 0 0 1 1 1 1 0 0 1 1 1 1 0 1 0 0 0 0 1 0 0 0 1 1 0 0 0 0 0 0 0 0 1 1 0 0 0 1 0 0 0 1 1 0 1 0 0 1 0 0 0 1 1 1 0 0 0 0 0 1 0 0 0 0 1 0 1 1 0 1 1 0 1 1 0 1 0 1 0 0 0 0 0 0 0 1 0 1 0 0 0 1 0 0 1 0 1 0 0 1 0 0 0 0 1 0 0 1 0 1 0 1 0 1 0 1 1 0 1 0 0 0 1 0 0 0 0 0 1 1 1 1 1 1 0 0 1 1 0 1 1 1 0 0 0 0 0 0 0 0 0 0 0 1 0 0 0 0 0 0 0 1 0 1 1 1 1 0 0 0 0 0 0 1 0 0 0 0 0 0 0 0 0 1 1 0 0 1 0 0 0 0 0 0 0 1 1 0 0 0 0 0 1 0 0 1 1 1 0 0 0 0 0 0 0 1 0 1 0 0 0 1 0 1 1 0 1 0 0 0 1 0 0 0 1 0 0 0 1 0 1 1 0 0 0 0 0 1 0 0 0 0 1 1 0 1 1 0 0 1 0 0 1 0 0 1 1 0 0 0 0 0 0 0 1 0 1 0 0 0 0 0 1 0 0 0 1 0 1 0 0 1 0 1 0 1 0 0 0 1 0 1 1 0 0 1 0 0 1]
0,1로 나타난 생존율 예측값과, test_df의 PassengerId값을 하나의 csv파일로 만들어 제출합니다.
submission = pd.DataFrame({
"PassengerId": test_df["PassengerId"],
"Survived": Y_pred
})
submission.to_csv('/content/drive/MyDrive/CODE/kaggle/binary classification/titanic/titanic.csv', index=False)
다음과 같은 결과값을 얻으면 competiton이 종료됩니다.
'Study > Kaggle competition' 카테고리의 다른 글
Kaggle Competition #4 (1) | 2023.04.04 |
---|---|
kaggle competition #3 (0) | 2023.04.03 |
Kaggle competition #2 (0) | 2023.04.03 |
Kaggle competition #5 : House Price prediction (0) | 2023.03.23 |