본문 바로가기
## 오래된 게시글 (미관리) ##/Python (Linux)

24. Python - 분류 연습문제 3 [타이타닉 데이터셋 도전]

by #Glacier 2018. 12. 4.

오늘은 분류 연습문제 3번을 해보겠습니다.


연습문제 3. 타이타닉 데이터셋을 통해 승객의 속성을 기반으로 생존 여부 예측을 하는 것이 목표

먼저 Kaggle에 로그인하고, https://www.kaggle.com/c/titanic 에서  train.csv와 test.csv를 받으세요.

그리고, 우리가 이전에 /root/ml에서 모든 것을 했었죠.

마찬가지로 /root/datasets/titanic을 만들어서, train.csv와 test.csv로 훈련셋과 테스트셋을 옮깁니다.


import os

TITANIC_PATH = os.path.join("/root", "datasets", "titanic")


import pandas as pd

def load_titanic_data(filename, titanic_path=TITANIC_PATH):

    csv_path = os.path.join(titanic_path, filename)
    return pd.read_csv(csv_path)

print(TITANIC_PATH)


train_data = load_titanic_data("train.csv")

test_data = load_titanic_data("test.csv")


이렇게 불러올 수 있습니다.



train_data.head()로 데이터를 살펴봅니다. 많은 변수가 있는데, 변수에 대해 먼저 알아봅니다.

  • Survived: 타깃입니다. 0은 생존하지 못한 것이고 1은 생존을 의미합니다.
  • Pclass: 승객 등급. 1, 2, 3등석.
  • NameSexAge: 이름 그대로 의미입니다.
  • SibSp: 함께 탑승한 형제, 배우자의 수.
  • Parch: 함께 탑승한 자녀, 부모의 수.
  • Ticket: 티켓 아이디
  • Fare: 티켓 요금 (파운드)
  • Cabin: 객실 번호
  • Embarked: 승객이 탑승한 곳. C(Cherbourg), Q(Queenstown), S(Southampton)


변수는 총 9개네요.


누락된 데이터를 확인하기 위해 train_data.info() 코드를 탁~치면!



0부터 890까지, 총 891개의 index중에서 PassengerId, Survived, Pclass, Name, Sex, SibSp, Parch, Fare는 null이 없군요.

다만 Age는 714개, Cabin은 204개, Embarked는 889개로 3가지 변수에서 null이 있다는 것을 알 수 있습니다.

데이터 타입은 float64, int64, object 3가지가 있네요.


Cabin(객실 번호)는 NULL이 너무 많기 때문에 제외하고, Name과 Ticket 또한 머신러닝에서 사용할 수 있는 숫자로 변환하는 것이 까다롭기 때문에 일단 제외합니다.


train_data.describe() 를 통해 통계치를 확인하면,

38%가 생존했고, 29.7세의 평균나이, Fare 평균은 32.20파운드라는 것을 알 수 있습니다.



train_data["Survived"].value_counts() 

타깃값인 생존이 0과 1로 이루어졌는지(사망/생존) 확인합니다.


train_data["Pclass"].value_counts()

범주형 특성을 확인해봅니다.


train_data["Sex"].value_counts()

성비를 확인해봅니다.


train_data["Embarked"].value_counts()

Embarked는 탑승지인데요. C=Cherbourg, Q=Queenstown, S=Southampton입니다.


이 아래의 코드는 한번에 복사해주세요. CategoricalEncoder인데요,

사이킷런 0.20.0버전에서는 OneHotEncoder도 범주형 처리가 가능합니다.


========================================================

import numpy as np

from sklearn.base import BaseEstimator, TransformerMixin

from sklearn.utils import check_array

from sklearn.preprocessing import LabelEncoder

from scipy import sparse


class CategoricalEncoder(BaseEstimator, TransformerMixin):

    def __init__(self, encoding='onehot', categories='auto', dtype=np.float64,

                 handle_unknown='error'):

        self.encoding = encoding

        self.categories = categories

        self.dtype = dtype

        self.handle_unknown = handle_unknown


    def fit(self, X, y=None):

        """Fit the CategoricalEncoder to X.

        Parameters

        ----------

        X : array-like, shape [n_samples, n_feature]

            The data to determine the categories of each feature.

        Returns

        -------

        self

        """


        if self.encoding not in ['onehot', 'onehot-dense', 'ordinal']:

            template = ("encoding should be either 'onehot', 'onehot-dense' "

                        "or 'ordinal', got %s")

            raise ValueError(template % self.handle_unknown)


        if self.handle_unknown not in ['error', 'ignore']:

            template = ("handle_unknown should be either 'error' or "

                        "'ignore', got %s")

            raise ValueError(template % self.handle_unknown)


        if self.encoding == 'ordinal' and self.handle_unknown == 'ignore':

            raise ValueError("handle_unknown='ignore' is not supported for"

                             " encoding='ordinal'")


        X = check_array(X, dtype=np.object, accept_sparse='csc', copy=True)

        n_samples, n_features = X.shape


        self._label_encoders_ = [LabelEncoder() for _ in range(n_features)]


        for i in range(n_features):

            le = self._label_encoders_[i]

            Xi = X[:, i]

            if self.categories == 'auto':

                le.fit(Xi)

            else:

                valid_mask = np.in1d(Xi, self.categories[i])

                if not np.all(valid_mask):

                    if self.handle_unknown == 'error':

                        diff = np.unique(Xi[~valid_mask])

                        msg = ("Found unknown categories {0} in column {1}"

                               " during fit".format(diff, i))

                        raise ValueError(msg)

                le.classes_ = np.array(np.sort(self.categories[i]))


        self.categories_ = [le.classes_ for le in self._label_encoders_]


        return self


    def transform(self, X):

        """Transform X using one-hot encoding.

        Parameters

        ----------

        X : array-like, shape [n_samples, n_features]

            The data to encode.

        Returns

        -------

        X_out : sparse matrix or a 2-d array

            Transformed input.

        """

        X = check_array(X, accept_sparse='csc', dtype=np.object, copy=True)

        n_samples, n_features = X.shape

        X_int = np.zeros_like(X, dtype=np.int)

        X_mask = np.ones_like(X, dtype=np.bool)


        for i in range(n_features):

            valid_mask = np.in1d(X[:, i], self.categories_[i])


            if not np.all(valid_mask):

                if self.handle_unknown == 'error':

                    diff = np.unique(X[~valid_mask, i])

                    msg = ("Found unknown categories {0} in column {1}"

                           " during transform".format(diff, i))

                    raise ValueError(msg)

                else:

                    # Set the problematic rows to an acceptable value and

                    # continue `The rows are marked `X_mask` and will be

                    # removed later.

                    X_mask[:, i] = valid_mask

                    X[:, i][~valid_mask] = self.categories_[i][0]

            X_int[:, i] = self._label_encoders_[i].transform(X[:, i])


        if self.encoding == 'ordinal':

            return X_int.astype(self.dtype, copy=False)


        mask = X_mask.ravel()

        n_values = [cats.shape[0] for cats in self.categories_]

        n_values = np.array([0] + n_values)

        indices = np.cumsum(n_values)


        column_indices = (X_int + indices[:-1]).ravel()[mask]

        row_indices = np.repeat(np.arange(n_samples, dtype=np.int32),

                                n_features)[mask]

        data = np.ones(n_samples * n_features)[mask]


        out = sparse.csc_matrix((data, (row_indices, column_indices)),

                                shape=(n_samples, indices[-1]),

                                dtype=self.dtype).tocsr()

        if self.encoding == 'onehot-dense':

            return out.toarray()

        else:

            return out


========================================================


이제, 전처리 파이프라인을 만들어봅니다. 

DataFrame으로부터 특정 속성만 선택하기 위해, 이전에 만든 DataFrameSelector를 활용합니다.

#사이킷런이 DataFrame을 바로 사용하지 못하기 때문에 수치형/범주형 컬럼을 선택하는 클래스를 만듭니다.


from sklearn.base import BaseEstimator, TransformerMixin


class DataFrameSelector(BaseEstimator, TransformerMixin):

    def __init__(self, attribute_names):

        self.attribute_names = attribute_names

    def fit(self, X, y=None):

        return self

    def transform(self, X):

        return X[self.attribute_names]


이제, 수치형 특성을 위한 파이프라인을 만듭니다.


from sklearn.pipeline import Pipeline

from sklearn.impute import SimpleImputer


imputer = SimpleImputer(strategy="median")

num_pipeline = Pipeline([

    ("select_numeric", DataFrameSelector(["Age", "SibSp", "Parch", "Fare"])),

    ("imputer", SimpleImputer(strategy="median")),

])


num_pipeline.fit_transform(train_data)


그리고, fit_trainsform()메서드를 이용하면 수치형 특성을 반환받습니다.


문자열로 구성된 범주형 열을 위해 별도의 Imputer 클래스가 필요합니다.

일반 Imputer는 이를 처리하지 못하기 때문입니다!


class MostFrequentImputer(BaseEstimator, TransformerMixin):

    def fit(self, X, y=None):

        self.most_frequent_ = pd.Series([X[c].value_counts().index[0] for c in X], index = X.columns)

        return self

    def transform(self,X,y=None):

        return X.fillna(self.most_frequent_)


그리고, 범주형 특성을 위한 파이프라인도 만듭니다.

저자 깃허브에 보면, CategoricalEncoder를 사용하여 원-핫 벡터로 변경하는 것보다 0.20버전에서 OneHotEncoder가 더 낫다고 합니다.


from sklearn.preprocessing import OneHotEncoder


cat_pipeline = Pipeline([

        ("select_cat", DataFrameSelector(["Pclass", "Sex", "Embarked"])),

        ("imputer", MostFrequentImputer()),

        ("cat_encoder", OneHotEncoder(sparse=False)),

    ])


마찬가지로, fit_transform()메서드를 이용합니다.


cat_pipeline.fit_transform(train_data)



이제, FeatureUnion을 이용한 숫자와 범주형 파이프라인을 연결합니다.

from sklearn.pipeline import FeatureUnion


preprocess_pipeline = FeatureUnion(transformer_list = [

("num_pipeline", num_pipeline),

("cat_pipeline", cat_pipeline),

])


이제 원본 데이터를 받아 머신러닝 모델에 주입할 숫자 입력 특성을 출력하는 전처리 파이프라인을 완성했습니다.


X_train = preprocess_pipeline.fit_transform(train_data)


y_train = train_data["Survived"]

잘 합쳐진 것을 볼 수 있죠. 진짜 데이터값인 레이블데이터를 가져옵니다.


분류기를 훈련시키는 데에 가장 먼저 SVC를 사용합니다. (SupportVectorClassifier)


from sklearn.svm import SVC


svm_clf = SVC()

svm_clf.fit(X_train, y_train)

자동적으로 default값들이 지정되었습니다.

모델이 잘 훈련된 것 같고, 이를 사용해서 테스트 세트에 대해 예측을 만듭니다.


X_test = preprocess_pipeline.transform(test_data)

y_pred = svm_clf.predict(X_test)


이 예측 결과를 교차 검증으로 모델이 얼마나 좋은지 평가합니다.


from sklearn.model_selection import cross_val_score


svm_scores = cross_val_score(svm_clf, X_train, y_train, cv=10) # 10-fold 교차 검증

svm_scores.mean()

이 결과 SVM 모델의 정확도는 73.65%로 나옵니다. 엄청 높지는 않네요.

캐글에서 타이타닉 경연대회의 리더보드는 상위10%이내가 80%이상의 정확도를 내고 있다고 합니다.

어떤 사람은 100%.;;


80%이상의 모델을 위해 RandomForestClassifier를 사용해보고, SVM모델과 비교합니다.


from sklearn.ensemble import RandomForestClassifier


forest_clf = RandomForestClassifier(random_state=42)

forest_scores = cross_val_score(forest_clf, X_train, y_train, cv=10)

forest_scores.mean()


결과로 0.8115 즉, 81.2% 정도의 정확도를 달성한 것을 알 수 있습니다.

이렇게 10-폴드 교차 검증에 대한 평균 정확도를 보는 대신, 모델에서 얻은 10개의 점수를 다르게 표현해봅니다.

1사분위, 3사분위를 명료하게 표현하는 box-and-whisker(상자-수염 그림) 혹은 box-plot 이라 불리는 그래프로 표현합니다.

IQR = Q3-Q1, Q1-1.5*IQR 이하 Q3+1.5*IQR 이상은 이상치로 간주 (이 그림은 다들 아실 거라 믿어요.)


from matplotlib import pyplot as plt


plt.figure(figsize=(8,4))

plt.plot([1]*10, svm_scores, ".")

plt.plot([2]*10, forest_scores, ".")

plt.boxplot([svm_scores, forest_scores], labels=("SVM", "Random Forest"))

plt.ylabel("Accuracy", fontsize=14)

plt.show()




보시다시피, SVM에서는 정확도 IQR범위 외에도 이상치 점이 보이지만, RandomForest는 이상치 점이 보이지 않습니다.

따라서 SVM 모델보다 RandomForest 모델이 정확도가 더 높게 평가될 것입니다.


# 이 결과를 더 향상시키려면, 교차 검증과 그리드 탐색을 사용해 더 많은 모델 비교하며 하이퍼 파라미터 튜닝을 해야함.

# 또, 특성공학을 시도한다. (Sibsp와 Parch 특성의 합으로 바꾼다던지..)

# Survived 특성과 관련된 이름을 구별해보기. (이름에 "Countess"가 있는 경우 생존 가능성이 높다)

# 수치 특성을 범주형 특성으로 바꾸어보세요. (나이대가 다른 경우 다른 생존 비율이 있을 수 있다.)

# 또, 생존자의 30%가 혼자 여행하는 사람이기 때문에 이들을 위한 특별한 범주를 만드는 것도 도움



예를 들어서, 이렇게 AgeBucket을 만듭니다. train_data의 Age를 몫연산하는데, 주의할 점은

// 225가 아니라는 점입니다. 15로 Age가 몫연산되고, 다시 15가 곱해지는 겁니다(괄호없음).

그러면, 15 30 45 60 75 90처럼 나이가 나눠지는 몪에 따라 연령대가 구분되고, 그걸 다시 15로 곱해줘서 표현하는 겁니다.

그러면, 몫이 5였던, 즉 75세 이상의 사람들은 생존율이 1로 나타나고, 몫이 1 이하인 0 부분을 보면,

75세 이상을 제외하면 가장 높은 0.57이란 수치를 볼 수 있습니다. 연세드신 분과 어린 아이들을 먼저 구한 것일까? 라는 생각도 해볼 수 있겠죠.


하지만 애석하게도 75세 이상은 1명 뿐이었고, 대부분이 15~30세 사이, 30~45세 사이라는 점을 알 수 있죠.

노인의 표본이 적어서 먼저 구했다고 결론지을 수 없고, 15세 이하의 어린 아이들이 먼저 구해졌다는 것은 어느 정도 신빙성이 있을 수도 있겠네요!


SibSp : 함께 탑승한 형제, 배우자의 수

Parch : 함께 탑승한 자녀, 부모의 수


쓰고 있는데도 가슴이 아프네요. 확실히 두 변수는 연관이 있어보입니다. 모두 소중하다는 점에서요!

두 변수를 합치고, 두 변수와 합친 변수 모두를 살펴봅니다.



형제, 배우자가 함께 탑승한 경우가 적긴 하지만 함께 타신 분들도 있습니다.

함께 탑승한 자녀, 부모님 수는 2명 이상부터 더 많네요. 

이 두 변수를 합쳐보면 소중한 사람들과 탑승한 경우가 되겠죠.



위의 결과를 다시 가져와서 보면, 지나치게 대가족이 온 경우는 생존율이 낮았네요.

하지만 혼자 여행하는 것 보다 적절히 도와줄 수 있는 1~3명 사이의 관계가 있는 인물들이 있을 경우 생존율이 높아짐을 볼 수 있습니다. 관계가 있는 사람이 3명일 경우는 생존율이 꽤 높네요.


자 이렇게 타이타닉 데이터셋을 훑어봤습니다. 흥미진진하네요 !!! 

이제 다음은 스팸분류기 만들어보는 예제를 보겠습니다.


 블로그 

출처


이 글의 상당 부분은  [핸즈온 머신러닝, 한빛미디어/오렐리앙 제롱/박해선] 서적을 참고하였습니다.


나머지는 부수적인 함수나 메서드에 대해 부족한 설명을 적어두었습니다.

학습용으로 포스팅 하는 것이기 때문에 복제보다는 머신러닝에 관심이 있다면 구매해보시길 추천합니다.


도움이 되셨다면 로그인 없이 가능한

아래 하트♥공감 버튼을 꾹 눌러주세요!