오랑우탄의 반란

[ML] 데이터분석 예측모델링 파이썬 실습 (타이타닉) 본문

PYTHON/머신러닝

[ML] 데이터분석 예측모델링 파이썬 실습 (타이타닉)

5&2 2024. 8. 13. 18:14
반응형

 

 

지금까지 배웠던 이론을 바탕으로 실제 데이터분석의 전 과정 실습을 해보겠습니다. 

 

데이터 분석 프로세스

 

 

데이터분석 예측모델링 w. 타이타닉 데이터

지난 모델링 실습에서 활용했던 타이타닉 데이터로 분석을 진행하겠습니다. 

 

Titanic - Machine Learning from Disaster | Kaggle

 

www.kaggle.com

 

칼럼별 데이터를 체크해둡니다. 

Column Details Datatype
PassengerId  승객 ID (PK) int64
Survived (Y) 사망 (0) 생존 (1) int64
Pclass  티켓 등급 (1,2,3) int64 (범주형)
Name  이름 object
Sex  성별 object (범주형)
Age  나이 float64 
SibSp  형제와 배우자 수 int64 
Parch  부모와 자식 수 int64 
Ticket  티켓 번호 object
Fare  요금 float64  
Cabin  객실 이름 object (범주형)
Embarked 승선 항구 (C,Q,S) object (범주형)

 

 

데이터분석은 아래의 단계대로 진행하겠습니다. 

  1. 데이터 로드 및 분리 (test/train)
  2. 탐색적 데이터 분석 (EDA)
    데이터 분포 및 이상치 확인
  3. 데이터 전처리
    이상치 처리 
    결측치 처리 
    수치형/범주형 전처리 (스케일링/인코딩)
  4. 모델 학습 
  5. 모델 평가 

 

1. 데이터 로드 및 분리

사용할 라이브러리와 각각 train, test 데이터를 불러와줍니다. 

타이타닉 데이터의 경우 이미 train/test 데이터가 분리되어 있어서 train 파일로 먼저 모델 학습을 진행하고 test 파일로 평가해서 예측 결과를 submission 파일에 적용시켜주면 됩니다. 

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

train_df = pd.read_csv(r'\train.csv')
test_df = pd.read_csv(r'\test.csv')

 

왜 데이터를 분리해서 모델링을 진행하나요? 
☞ 데이터를 과도하게 학습해 해당 데이터로만 예측/분류를 할 수 있고 새로운 데이터로는 제대로 수행하지 못하는
과적합 (overfitting) 문제 발생을 방지하고자 데이터를 train (모델을 학습(fit)하기 위한 데이터) 과 test (모델을 평가 하기 위한 데이터) 로 나눠서 각각 학습을 시킵니다.

높은 복잡도의 모델 외에도 불충분한 데이터, 과도한 반복 학습 (딥러닝),
데이터 불균형 (정상환자 - 암환자의 비율이 95: 5) 의 이유로 과적합이 발생할 수 있습니다. 

 

sklearn.model_selection.train_test_split  함수를 사용해 데이터를 분리할 수 있습니다. 

 

파라미터

  • test_size: 테스트 데이터 세트 크기
  • train_size: 학습 데이터 세트 크기
  • shuffle: 데이터 분리 시 섞기
  • random_state: 호출할 때마다 동일한 학습/테스트 데이터를 생성하기 위한 난수 값. 수행할 때 마다 동일한 데이터 세트로 분리하기 위해 숫자를 고정 시켜야 함

반환 값(순서 중요)

  • X_train, X_test, y_train, y_test

 

2. EDA _ train 

train 데이터의 결측치와 이상치를 확인해줍니다. 

Age, Cabin, Embarked 데이터에 NaN이 존재하고, Fare 데이터에 과도하게 높은 값이 들어있는 것이 확인됩니다. 

또한 원본 데이터를 보존하기 위해 train 의 복사본을 만들어서 작업합니다. 

train_df.info() # 결측치 확인
train_df.describe(include='all') # 이상치 확인

train_df_2 = train_df.copy() # 작업할 복사본 생성

 

 

SibSp 와 Parch 모두 가족관계를 나타내는 칼럼이라는 점이 파악되기 때문에, 둘을 Family 라는 칼럼으로 합쳐줍니다. 

가구 수를 나타내고 싶은 것이기 때문에 마지막에 +1 을 해 1인 가구도 한 가구로 간주해줍니다. 

# 기초가공: family 변수 생성
def get_family(df):
    df['Family'] = df['SibSp'] + df['Parch'] + 1 #가구
    return df
get_family(train_df_2).head(3)

 

 

좀 더 구체적으로 숫자형 변수들의 분포와 이상치가 어떻게 배치되어 있는지 pairplot으로 확인해줍니다. 

sns.pairplot(train_df_2[['Age','Fare','Family']])

 

3. 데이터 전처리 _ train 

이상치 처리 (Fare)

이론적으로는 ESD 나 IQR 방식으로 이상치 처리를 하지만, 정규분포를 따르지 않는 비대칭 데이터나 샘플크기가 너무 작을 때는 부적절합니다. 또한 현실에서는 도메인과 비즈니스 맥락에 따라 이상치 처리 방식이 모두 다를 것이기 때문에 데이터를 잘 이해하고 이상치를 어떻게 처리할지 생각하는 것이 중요합니다. 

 

타이타닉 데이터의 경우 Fare 가 400 이상인 경우가 있을 수도 있겠지만 같은 Pclass 1등급 임에도 요금 차이가 과도하게 나는 것은 오류라고 볼 수 있겠습니다. 실제 데이터를 확인했을 때 전체 데이터 891개 중에 3개뿐이기 때문에 해당 데이터 행 전체를 삭제해서 이상치를 처리합니다. 

 

Fare이 400 보다 작은 데이터로 train_df_2 데이터를 대체해주고 제대로 적용이 됐는지 확인해줍니다. 

train_df_2 = train_df_2[train_df_2['Fare'] < 400]
train_df_2.shape 
>>> (888, 13) #원본 데이터 891개

 

 

 

결측치 처리 (Age, Embarked)

다음으로 앞서 확인했던 결측치가 있는 데이터를 처리해줍니다. 

Age 의 경우 평균값을 대체해주고, Embarked의 경우 최빈값 'S' 로 채워줍니다. 

# 결측치처리
def get_non_missing(df):
    Age_mean = train_df_2['Age'].mean()
    df['Age'] = df['Age'].fillna(Age_mean)
    df['Embarked'] = df['Embarked'].fillna('S')
    return df
get_non_missing(train_df_2).info()

cabin 은 해당 분석에서 제외로 미처리

 

 

데이터 전처리 (인코딩, 스케일링)

간단한 이상치와 결측치 처리가 완료 되었으면, 각각 사용될 범주형과 수치형 자료에 대한 전처리를 진행합니다.

범주형 자료에 대한 전처리는 인코딩, 수치형 자료에 대한 전처리는 스케일링이라고 부르는데요, 

이를 진행하는 이유는 머신러닝 모델의 원활한 학습을 위해서입니다.  

자료형 사용함수 전처리 방식
범주형 sklearn.preprocessing.LabelEncoder Label Encoding 
문자열 범주형 값을 고유한 숫자로 할당

장점: 모델이 처리하기 쉬운 수치형으로 데이터 변환
단점: 순서 간 크기에 의미가 부여될 수 있음
  pd.get_dummies
sklearn.preprocessing.OneHotEncoder
One-Hot Encoding
각 범주를 이진 형식으로 변환하는 기법

장점: 각 범주가 독립적으로 표현되어, 순서가 중요도를 잘못 학습하는 것을 방지, 명목형 데이터에 권장
단점: 범주 개수가 많을 경우 차원이 크게 증가(차원의 저주), 모델의 복잡도 증가로 과적합 유발
수치형 sklearn.preprocessing.StandardScaler
Standardization 표준화
각 데이터에 평균을 빼고 표준편차를 나누어 평균을 0 표준편차를 1로 조정하는 방법

장점: 이상치가 있거나 분포가 치우쳐져 있을 때 유용, 모든 특성의 스케일을 동일하게 맞춤. 많은 알고리즘에서 좋은 성능
단점: 데이터의 최소-최대 값이 정해지지 않음 
  sklearn.preprocessing.MinMaxScaler
Normalization 정규화
데이터를 0과 1사이 값으로 조정(최소값 0, 최대값 1)

장점: 이상치 없을 때 유용, 모든 특성의 스케일을 동일하게 맞춤, 최대-최소 범위가 명확
단점: 이상치에 영향을 많이 받을 수 있음
  sklearn.preprocessing.RobustScaler
Robust Scaling 
중앙값과 IQR을 사용하여 스케일링

장점: 이상치의 영향에 덜 민감
단점: 표준화와 정규화에 비해 덜 사용됨

 

 

스케일링 

먼저 수치형 데이터 Age, Fare, Family 에 대한 스케일링을 진행합니다. 

Fare 는 분포가 치우쳐있고 값이 크기 때문에 StandardScaler 를 사용해주고, Age 는 분포가 균일하기 때문에, Family는 분포가 균일하지 않지만 전체적으로 값이 작기 때문에 둘은 MinMaxScaler 를 사용해줍니다. 

sklearn에서 함수를 불러와주고 변수에 저장해준 후 fit으로 적용시켜서 각각 새로운 칼럼에 결과를 저장해줍니다. 

 

이때 함수에서 학습은 train_df_2 에 대해 진행하고 실제 결과물의 저장은 df (인풋 데이터) 로 되게끔 설계하는 것이 중요합니다. 이렇게 하는 이유는 train 학습 후 test 로도 학습을 진행해야 하기 때문입니다. (일 두 번 하지 않기 ^^)

def get_numeric_sc(df):
    #sd_sc: Fare, mm_sc: Age, Family
    from sklearn.preprocessing import StandardScaler, MinMaxScaler
    sd_sc = StandardScaler()
    mm_sc = MinMaxScaler()
    
    sd_sc.fit(train_df_2[['Fare']])
    df['Fare_sd_sc'] = sd_sc.transform(df[['Fare']]) #학습은 train 기준, 적용은 들어오는 데이터에 

    mm_sc.fit(train_df_2[['Age','Family']])
    df[['Age_mm_sc','Family_mm_sc']] = mm_sc.transform(df[['Age','Family']])

    return df

train_df_2 = get_numeric_sc(train_df_2) # 함수 적용

 

스케일링한 대로 데이터가 잘 변환돼서 들어갔는지 확인해줍니다.

 

 

인코딩

다음으로 범주형 데이터 전처리를 진행합니다. 

Pclass 는 등급이기 때문에 (순서형) 그리고 Sex는 두개밖에 없기 때문에  LabelEncoder를 사용하고 Embarked 는 순서형 데이터가 아니기 때문에 OneHotEncoder를 사용해줍니다. 

def get_category(df):
    from sklearn.preprocessing import LabelEncoder, OneHotEncoder
    le = LabelEncoder()
    le2 = LabelEncoder()
    oe = OneHotEncoder()

    le.fit(train_df_2[['Pclass']])
    df['Pclass_le'] = le.transform(df['Pclass'])

    le2.fit(train_df_2[['Sex']])
    df['Sex_le'] = le2.transform(df['Sex'])

    #index reset을 위한 구문 추가 -> 안할 시 test에서 오류 발생 
    df = df.reset_index()

    oe.fit(train_df_2[['Embarked']])
    embarked_csr = oe.transform(df[['Embarked']]).toarray()
    embarked_csr_df = pd.DataFrame(embarked_csr, columns= oe.get_feature_names_out()) 
    df = pd.concat([df, embarked_csr_df], axis=1)

    return df

train_df_2 = get_category(train_df_2)

 

OneHotEncoder 는 사용 시 주의사항이 있는데요, 다른 함수와 다르게 transform 으로 적용시키기만 하면 오류가 발생합니다. 데이터 형태가 맞지 않기 때문인데요, 함수 적용 후 array 로 변환해서 DataFrame 으로 저장해서 기존 데이터에 붙여줘야 합니다. 이 내용을 단계별로 살펴볼까요? 

 

우선 단순히 transform 만 진행한 코드를 실행하면 아래와 같은 결과물이 출력됩니다. 

from sklearn.preprocessing import OneHotEncoder
oe = OneHotEncoder()

oe.fit(train_df_2[['Embarked']])
embarked_csr = oe.transform(train_df_2[['Embarked']])
embarked_csr
<Compressed Sparse Row sparse matrix of dtype 'float64'
with 888 stored elements and shape (888, 3)>

 

OneHotEncoder는 각 값에 대해 [0, 0, 1], [0, 1, 0] 등과 같이 대부분의 원소가 0인 행렬 형태로 데이터를 변환해줍니다. 이를 Compressed Sparse Row (CSR) 라고 부르는데, 많은 양의 sparse한 데이터를 효율적으로 저장하는 형태입니다. 

그렇기 때문에 이를 array 형식으로 변환해서 보면 아래와 같습니다. 

이 결과물을 그대로 DataFrame 으로 저장해주면 사용 가능한 데이터 형식이 됩니다. 

이때 메소드로 columns= oe.get_feature_names_out() 를 사용하면 칼럼명이 알아보기 쉽게 들어갑니다. 

 

concat을 사용해서 기존 데이터에 붙여주면 인코딩은 완료됐습니다. 

 

 

4. 모델 학습 _ train

이제 모델 훈련을 합니다. 데이터 처리에 비해 매우 간단합니다. 

종속변수와 독립변수를 정의하고 로지스틱회귀 분석을 진행해줍니다. 결과물을 기반으로 예측값을 구합니다. 

def get_model(df):
    from sklearn.linear_model import LogisticRegression
    model_lor = LogisticRegression()
    X = df[['Age_mm_sc','Fare_sd_sc','Family_mm_sc','Pclass_le','Sex_le','Embarked_C','Embarked_Q','Embarked_S']]
    y = df[['Survived']]
    return model_lor.fit(X,y)
    
model_output = get_model(train_df_2) #모델훈련

y_pred = model_output.predict(X) #예측값

 

 

 

5. 모델 평가 _ train

결과적으로 높은 정확도와 f1_score 가 나옵니다. 

from sklearn.metrics import accuracy_score, f1_score
print(accuracy_score(train_df_2['Survived'],y_pred))
print(f1_score(train_df_2['Survived'],y_pred))

 

 

 

 

이제 test 데이터로 모델 훈련과 평가를 진행합니다. 

 

 

1. EDA _ test

먼저 데이터를 확인해주는데, train에는 없었던 Fare 칼럼의 결측치가 존재합니다. 

 

이걸 처리해주는 코드를 기존 get_non_missing 함수에 추가해줍니다. 

def get_non_missing(df):
    Age_mean = train_df_2['Age'].mean()
    Fare_mean = train_df_2['Fare'].mean()
    df['Age'] = df['Age'].fillna(Age_mean)
    # train data 에는 불필요하지만 test data 에 결측치 존재하기 때문 
    df['Fare'] = df['Fare'].fillna(Fare_mean)
    df['Embarked'] = df['Embarked'].fillna('S')
    return df
get_non_missing(train_df_2).info()

 

2. 데이터 전처리 _ test

앞서 함수로 과정을 다 저장해뒀기 때문에 함수 적용만 해주면 됩니다. 

test_df_2 = get_family(test_df)
test_df_2 = get_non_missing(test_df_2)
test_df_2 = get_numeric_sc(test_df_2)
test_df_2 = get_category(test_df_2)

 

 

3. 모델 학습 _ test

test 데이터도 마찬가지로 모델 훈련을 진행시켜주고 예측값을 구합니다. 

y_test_pred 가 최종 survived 에 대한 예측값이 되어 0 또는 1 로 저장된 형태가 되겠습니다. 

즉, X의 칼럼들을 기반으로 Survived가 어떻게 나올지를 예측한 분석 결과입니다. 

test_X = test_df_2[['Age_mm_sc','Fare_sd_sc','Family_mm_sc','Pclass_le','Sex_le','Embarked_C','Embarked_Q','Embarked_S']]
y_test_pred = model_output.predict(test_X)

 

 

4. 예측값 대체 후 제출

해당 대회에서 제출할 파일인 gender_submission 을 불러와줍니다. 

이때 Survived 의 0, 1 은 Sex 를 기반으로 Male 은 죽고 0 Female 은 살았을 것이다 1 라는 가설에 대해 입력된 값이고, 이번 데이터 분석에서 도출한 예측값으로 대체해줍니다. 

sub_df = pd.read_csv(r'C:\Users\김예진\data_python\ml\titanic\gender_submission.csv')

sub_df['Survived'] = y_test_pred # 대체

sub_df.to_csv('./result.csv',index=False) # 인덱스 제거

 

 

캐글 타이타닉 대회 페이지 우측상단에 Submit Prediction 을 클릭 후 제출 파일을 첨부하면 점수가 뜹니다. 

 

 

 




오랑우탄이 영어를 하고 오랑이가 데이터분석가가 되는 그날까지~

 

 

 

반응형