타이타닉 데이터의 경우 이미 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 데이터를 대체해주고 제대로 적용이 됐는지 확인해줍니다.
장점: 각 범주가 독립적으로 표현되어, 순서가 중요도를 잘못 학습하는 것을 방지, 명목형 데이터에 권장 단점: 범주 개수가 많을 경우 차원이 크게 증가(차원의 저주), 모델의 복잡도 증가로 과적합 유발
수치형
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 만 진행한 코드를 실행하면 아래와 같은 결과물이 출력됩니다.
<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()