Eggs Sunny Side Up
본문 바로가기
Computer Engineering/머신러닝

타이타닉 데이터 분류

by guswn100059 2023. 6. 13.
### 문제정의
- 타이타닉 데이터를 활용하여 생존과 사망을 구분하는 머신러닝 모델을 만들자
- kaggle 사이트에 올려서 순위도 확인해보자(내림차순)

### 데이터 수집
- kaggle 사이트에서 데이터를 다운로드

# train, test 데이터 가져오기
# train 변수 : train.csv 파일 가져오기
# test 변수 : test.csv 파일 가져오기

import pandas as pd
train = pd.read_csv('./data/titanic/train.csv')
test = pd.read_csv('./data/titanic/test.csv')

print(train.shape)
print(test.shape)
# train : 891개 데이터, 12개 특성
# test : 418개 데이터, 11개 특성
# train과 test의 특성의 차이는 survived 컬럼의 차이 
# => 머신러닝 학습을 통해서 찾은 규칙을 기반으로 예측해야 하는 값

- 컬럼값들의 특성
![image.png](attachment:image.png)

### 데이터 전처리
- 결측치
- 이상치

#### 결측치 확인
- info()

train.info()
# 결측치 컬럼 : Age, Cabin, Embarked
# 학습을 위해 숫자형 자료로 변경이 필요한 컬럼 : Name, Sex, Ticket, Cabin, Embarked

test.info()
#결측치 컬럼 : Age, Fare, Cabin
# 학습을 위해 숫자형 자료로 변경이 필요한 컬럼 : Name, Sex, Ticket, Cabin, Embarked

#### 이상치 확인
- describe()

train.describe()
# PassengerId : 승객의 번호를 의미. 필요없는 데이터 => 삭제
# Survived : (0, 1) => 38% 생존
# Pclass : 객실 등급(1, 2, 3) 평균의 의미가 없음
# Age : 평균값이 의미 O, 20~30대 승객이 많음
# SibSp : 형제자매/배우자 수 => 이상치 의미가 없음
# Parch : 부모/자식 수 => 이상치 의미가 없음
# Fare : 평균이 의미 있음
#      : 평균값과 중앙값을 비교했을 때 중앙값에 비해 평균값이 높다면?
#         1. 티켓의 가격이 잘못
#         2. 데이터 수집이 잘못
#         3. 진짜로 1등급을 넘어서는 가격이 존재

# 크게 이상치가 있다고 볼 수 없음

test.describe()

#### Embarked 결측치 채우기
- 탑승항구 : S, Q, C 정보를 담고 있는 컬럼
- 결측치 수가 상대적으로 작기(2)때문에 먼저 처리

train['Embarked'].value_counts()
# 결측치를 채울 때 주의할 점
# - 어떤 이유로 결측치를 채울 것인가 확인
# 탑승하는 항구의 값을 채우기 위해 다른 컬럼이나 추가적인 정보 확인 필요
# 탑승하는 항구별로 티켓의 가격이 다른지 확인하는 것도 의미가 있을 것 같음.

# 결측치가 2개 

# 결측치 2개를 최빈값(S)으로 채우기
# 전체 결측치를 한 번에 채워주는 함수 : fillna(입력값)

train['Embarked'].fillna('S', inplace=True)

train.info()

#### Fare 결측치 채우기

test['Fare']

# Fare는 연속적인 수치값으로 최빈값이 있다고 해도 큰 의미가 없음.
# 중앙값을 사용해서 결측치를 채우기
test['Fare'].fillna(14.454200, inplace=True)

test.info()

#### Age 결측치 채우기
- 다른 컬럼들과의 상관관계를 고려해서 결측치 채우기

train.corr()
# 수치형 컬럼들간의 상관관계를 나타내는 함수 : corr()
# -1 ~ 1 사이의 숫자로 표현
# -1에 가까울수록 반비례 - 나이가 많을수록 객실의 등급은 1등급에 가까울 것이다. 나이 ↑, 객실등급 ↓
# 1에 가까울수록 (정)비례

# 수치 앞의 부호로 정비례/반비례를 판단
# 절대값(부호를 제외한 값)이 '1'에 가까울수록 상관관계가 높다.

# Pclass, SibSp, Parch 컬럼은 Age와 상관이 있을 것이다.

# Age 컬럼을 Pclass 컬럼과 비교하기
age_table = train[['Pclass', 'Sex', 'Age']].groupby(by=['Pclass', 'Sex']).median()

# 각 등급에 탑승한 승객들의 나이의 중앙값을 확인
# Pclass 는 숫자형 자료이지만, 객실의 등급을 나타내는 범주형 처리
# Age 컬럼을 Sex컬럼을 추가해서 비교해보기
age_table # 사용하기 위해서 table로 저장

train.iloc[0] # person

# 1. 하나의 행을 가지고 온다 => 한 사람의 데이터
# 2. Age가 결측치가 아니면 => 원래 가지고 있던 값을 그대로 사용
# 3. Age가 결측치라면 Pclass, Sex을 확인하고 해당하는 값을 age_table에서 검색 후
# 4. 해당하는 값으로 결측치를 채우기
# train : 891, test : 418

# 함수 만들기
# 꺼내오는 하나의 행 정보가 넘파이 배열 형태이므로 넘파이 라이브러리 필요
import numpy as np

# 행과 열 단위로 데이터를 처리하는 함수 : apply()
# 함수로 만들 때 주의할 점 : apply()를 적용시킬 때는
# 행/열 단위로(현재는 행) 데이터가 들어올 것을 예상하고 만들기

def fill_age(person):
    if np.isnan(person['Age']): # Age 컬럼이 결측치이다.
        return age_table.loc[person['Pclass'], person['Sex']]['Age']
        # 결측치라면 age_table의 객실 등급과 성별을 확인하고 Age값 적용
    else : # Age 컬럼이 결측치가 아니다
        return person['Age'] # 가지고 있던 값을 사용

# apply()로 age 결측치 채우기
train.apply(fill_age, axis=1) # 행단위로 출력
# 결측치가 채워진 Age 특성이 출력

train['Age'] = train.apply(fill_age, axis=1)
test['Age'] = test.apply(fill_age, axis=1)

test.info()

#### Cabin 결측치 채우기

train['Cabin'].unique()
# 앞의 알파벳 : 타이타닉 호의 구역
# 뒤 숫자 : 방번호
# 앞의 알파벳만 사용하여 공통적인 특성으로 그룹화

# 앞에 붙어있는 알파벳 하나만 가져오기 => 문자열 인덱싱
train['Cabin'].str[0] # 알파벳 하나만 가져옴
train['Cabin'] = train['Cabin'].str[0]
test['Cabin'] = test['Cabin'].str[0]

test['Cabin'].unique()

test['Cabin'].value_counts()

# 결측치가 가장 많은 컬럼
# 결측치를 하나의 데이터로 생각
# -> 결측치가 사망자의 데이터로 데이터를 받지 못해서

train['Cabin'].fillna('N', inplace=True)
test['Cabin'].fillna('N', inplace=True)

train['Cabin'].value_counts()

train.info()

test.info()

#### PassengerId 삭제
- 승객의 탑승번호는 생존과 연관이 없으므로 필요없는 데이터

# 행이나 열단위로 데이터를 삭제 : drop()
train.drop('PassengerId', axis=1, inplace=True)
test.drop('PassengerId', axis=1, inplace=True)

train.shape, test.shape

### 탐색적 데이터 분석(EDA)
- 타이타닉 데이터의 통계치가 별 의미가 없었으므로 데이터를 그래프로 확인

# 그래프를 쉽게 그려주는 라이브러리
import seaborn as sns

train.info()

#### 범주형 데이터
- object 타입
- Name, Sex, Ticket, Cabin, Embarked, Pclass
- bar 차트 : 특성의 값이 몇 개씩 있는지 나눠서 보여주는 차트 => Name, Ticket 컬럼 제외
- Pclass : 정수형 데이터타입이지만, 연속된 숫자가 아닌 객실의 등급을 나타내므로 범주형으로 처리

##### Cabin 시각화
- 객실의 구역

sns.countplot(data=train, x='Cabin', hue='Survived')

# bar차트는 맞는데, 하나의 x축에 2개의 값이 존재
# countplot : 값의 갯수, y
# X : 객실의 구역 -> Cabin
# hue : x축에서 생존자/사망자의 수를 count => Survived

- N의 값을 가지고 있는 사람들이 상대적으로 많이 사망
- 임의로 채운 데이터(N)이지만 어느 정도 의미가 있어 보임
- N의 값을 분석에 사용해도 될 거 같다.

##### Pclass 시각화
- 객실의 등급

sns.countplot(data=train, x='Pclass', hue='Survived')

- 객실 등급이 높아질수록 생존율이 올라간다.

##### Cabin과 Pclass 시각화
- 객실의 구역과 등급을 함께 시각화

sns.countplot(data=train, x='Cabin', hue='Pclass')

- N구역 사람들은 3등급 객실의 사람들이 많았기 때문에 사망율도 높았다.
- A, B, C, D, E 구역은 1등급 객실의 사람들이 많음

- countplot 함수를 통한 시각화는 확인하고 싶은 데이터를 가지고 와서 어떻게 분포가 되어있는지 확인
- 범주형 데이터를 시각화하는데 사용하는 함수

##### Embarked와 Pclass 시각화

sns.countplot(data=train, x='Embarked', hue='Pclass')

- Q라는 도시는 3등급 객실의 탑승객이 많은 것을 보면 S, C에 비해 낙후된 도시라고 추측
- 각 탑승항구에 따른 객실의 등급은 연관성을 생각해 볼 필요가 있다.

#### 숫자형 데이터
- 구간을 나눠서 데이터의 분포를 추정하는 그래프
- 히스토그램

train.head()

##### Age, Sex, Survived 시각화

# 나이별 생존자와 사망자의 비율을 'Sex'로 구분하여 확인하기
sns.violinplot(data=train, x='Sex', y='Age', hue='Survived', split=True)
# split=True : 하나의 성별에 따른 생존/사망을 함께 표현

- 20 ~ 40대 사이의 사망률이 높고,
- 어린아이 중에서는 남자 아이가 여자아이에 비해 많이 생존

##### Fare, Sex, Survived 시각화

# 요금별 생존자와 사망자의 비율을 성별로 구분하여 확인하기
sns.violinplot(data=train, x='Sex', y='Fare', hue='Survived', split=True)

- 요금이 싼 사람은 상대적으로 많이 사망했다.

#### 특성 공학
- 가지고 있는 특성을 기반으로 새로운 특성을 만드는 것

##### 가족의 수 특성 만들기
- SibSp, Parch를 더해서 가족의 숫자라는 새로운 컬럼을 생성
- SibSp(형제자매/배우자수) + Parch(부모자식수) + 1인 => family_size(가족의 수)

train['Family_size'] = train['SibSp'] + train['Parch'] + 1
test['Family_size'] = test['SibSp'] + test['Parch'] + 1

train.head() # 특성 추가 확인

# 수치로 되어있지만 연속적인 수치의 의미가 아닌 범주형 자료로
# countplot으로 가족의 수를 생존별로 확인
sns.countplot(data=train, x='Family_size', hue='Survived')

- 전체 데이터가 3개의 구간으로 구분할 수 있음
- 1 : 사망률이 높은 비율 - Alone
- 2 ~ 4 : 생존율 높은 비율 - Small
- 5 ~ 11 : 사망률이 높은 비율 - Big
- 구간별로 수치형 데이터를 범주형으로 변경 => Binning

# pd.cut()
# 필요한 정보
# 1. 구간에 대한 정보 : bins
# 2. 구간에 대한 범주(라벨) 이름 : labels
bins = [0, 1, 4, 15] # 0초과 1이하, 1초과 4이하, 4초과 15이하
labels = ['Alone', 'Small', 'Big']

train['Family_size'].head()

train['Family_group'] = pd.cut(train['Family_size'], bins=bins, labels=labels)
test['Family_group'] = pd.cut(test['Family_size'], bins=bins, labels=labels)

train.head()

#### Family_group 시각화

# countplot 사용하여 시각화
sns.countplot(data=train, x='Family_group', hue='Survived')

# 데이터 분석에 불필요한 Name, Ticket 컬럼을 삭제
train.drop('Name', axis=1, inplace=True)
train.drop('Ticket', axis=1, inplace=True)
test.drop('Name', axis=1, inplace=True)
test.drop('Ticket', axis=1, inplace=True)

# 삭제 후 확인
train.columns

test.columns

test.info()

#### 글자 데이터를 숫자 데이터로 변환
- one_hot 인코딩 사용

# Sex, Cabin, Embarked, Family_group
cat_feature = ['Sex', 'Cabin', 'Embarked', 'Family_group']

# 현재 가지고 있는 데이터는 train과 test로 나누어져 있음
# 두 데이터가 다를 수 있으므로 데이터를 확인이 필요
# 따라서 두 데이터(train, test)를 합쳐서 원핫인코딩

# 기존 실습에서 다뤘던 데이터는 train과 test가 분리되어 있지 않아서
# 데이터가 다를 수 있는 문제가 없었다.

##### train에 있는 Survived 분리
- y_train

# 훈련용 정답 데이터
y_train = train['Survived']

# train 데이터 세트에서 Survived 컬럼 삭제
train.drop('Survived', axis=1, inplace=True)

train.columns

test.columns

# 데이터 합치기
combined = pd.concat([train, test], ignore_index=True)
# concat은 기본적으로 아래(행)으로 합쳐주는 함수, 인덱스 번호 확인이 필요
# 기존에 가지고 있는 데이터의 인덱스 번호를 사용하지 않고,
# 새로운 인덱스 번호를 사용하겠다. => ignore_index = True
combined

# 원핫인코딩 : pd.get_dummies()
# cat_feature 데이터(범주형 데이터)
# 컬럼 안의 데이터 갯수만큼 컬럼이 생성

# train과 test를 합한 이유
# 1. 만약에 train과 test에 있는 같은 컬럼의 값이 데이터의 양에 따라 다를 경우
# 2. 원핫인코딩을 진행하면 컬럼의 갯수가 달라지게 되기 때문에 사전에 방지를 위함

one_hot = pd.get_dummies(combined[cat_feature])

one_hot.shape

# 필요가 없어진 글자형 데이터(cat_feature) 삭제
combined.drop(cat_feature, axis=1, inplace=True)

combined

# 숫자데이터 + 원핫인코딩 결과 합치기
total = pd.concat([combined, one_hot], axis=1)

total.shape

### 모델 선택 및 하이퍼 파라미터 튜닝

#### Decision Tree 모델 불러오기

from sklearn.tree import DecisionTreeClassifier
tree_model = DecisionTreeClassifier()

#### 데이터 나누기

# y_train, total
# X_train, X_test, y_train, y_test
# y_test는 처음부터 없었음 => 모델 학습을 통해서 만들어내야 하는 결과

# total 데이터를 X_train, X_test로 구분
X_train = total.iloc[:891]
X_test = total.iloc[891:]

X_train.shape, X_test.shape

### 학습

tree_model.fit(X_train, y_train)

#### 교차검증

from sklearn.model_selection import cross_val_score

# 사용할 모델, 문제데이터, 정답데이터, 데이터분할 수(cv)
cross_val_score(tree_model, X_train, y_train, cv=5)

cross_val_score(tree_model, X_train, y_train, cv=5).mean()

### 평가

tree_model.score(X_train, y_train)

#### 예측

pre = tree_model.predict(X_test)

# kaggle에 업로드할 답안지 파일 만들기
gender_sub = pd.read_csv('./data/titanic/gender_submission.csv')
gender_sub['Survived'] = pre

gender_sub

# csv 파일로 내보내기
gender_sub.to_csv('phjsub01.csv', index=False)

- 점수 : 0.7177

### 성능(점수) 개선하기
- 하이퍼 파라미터 튜닝
- 데이터 전처리
- 다른 모델 사용하기

#### 하이퍼 파라미터 튜닝하기
- 모델이 학습용 데이터엔 과대적합이 걸렸으니 이를 해소해보자
- 하이퍼 파라미터 튜닝 -> 학습 -> train, test score 확인 과정
- 최종 train, test의 score의 차이가 크지 않다면 => 잘 만들어진 모델이라고 판단

for i in range(1, 20):
    # 하이퍼 파라미터 튜닝
    tree_model = DecisionTreeClassifier(max_depth=i)

    # 학습 => 기존엔 fit을 사용했지만 여기선 사용 X
    # 교차검증 : 학습 + 검증
    result = cross_val_score(tree_model, X_train, y_train, cv=5).mean()
    print(f"max_depth : {i}, score : {result}")

# 하이퍼 파라미터 조정 전 : 0.7177
# max_depth 조정 후 : 0.8035

#### 한 번에 여러 개의 하이퍼 파라미터 조정해보기 : GridSearch
- 하이퍼 파라미터들끼리 서로 영향을 주기 때문에

# max_depth & max_leaf_nodes
#     max_depth : 1 ~ 20
#         for i in range(1, 21):
#         max_leaf_nodes : 10 ~ 20
#             for j in range(10, 21):
# 20 * 10 = 200번 반복
# 최적의 조합을 찾기 위해서 여러 개의 for문이 필요 --> 복잡, 작업이 번거로움
# ==> 한 번에 여러 개의 하이퍼 파라미터를 튜닝해주는 기술인 GridSearch를 사용

                

# 관심있는(궁금한) 하이퍼 파라미터를 지정
# max_depth : 1 ~ 20
# max_leaf_nodes : 10 ~ 20

# 파라미터를 딕셔너리 형태로 지정
param = {
    'max_depth' : [3, 6, 9, 12, 15, 18],
    'max_leaf_nodes' : [10, 12, 14, 16, 18, 20]
}

##### GridSearchCV 함수 사용

from sklearn.model_selection import GridSearchCV
# 사용할 모델, 사용할 하이퍼 파라미터, 데이터 분할 수(cv)
grid_model = GridSearchCV(DecisionTreeClassifier(), param, cv=5)

# 학습
grid_model.fit(X_train, y_train)

##### 최적의 조합 확인

# 찾아낸 최고의 조합(max_depth, max_leaf_nodes)
grid_model.best_params_

# 최고의 조합일 때의 점수
grid_model.best_score_

# 자세하게 조합 찾기
param = {
    'max_depth' : [12, 13, 15, 16, 17],
    'max_leaf_nodes' : [16, 17, 18, 19, 20]
}
grid_model = GridSearchCV(DecisionTreeClassifier(), param, cv=5)
grid_model.fit(X_train, y_train)

grid_model.best_params_

grid_model.best_score_

# 최고의 하이퍼 파라미터를 적용한 학습
# 현재까지 진행한 코드를 기준으로 기존의 tree_model로 학습 진행 필요
tree_model = DecisionTreeClassifier(max_depth=15, max_leaf_nodes=18)
tree_model.fit(X_train, y_train)

# 학습이 완료되면 점수 확인을 위해서 Kaggle 사이트에 올려보기

pre = tree_model.predict(X_test)
gender_sub = pd.read_csv('./data/titanic/gender_submission.csv')
gender_sub['Survived'] = pre
gender_sub.to_csv('phjsub02.csv', index=False)

Kaggle 대회에서 점수 확인

 

댓글