1. 문제 출처 및 참고한 사람
문제: https://www.kaggle.com/competitions/titanic
참고:https://www.kaggle.com/code/omarelgabry/a-journey-through-titanic/notebook
2. 문제 풀이
# 필요한 라이브러리 불러오기
# 데이터 분석
import pandas as pd
import numpy as np
# 데이터 시각화
import matplotlib.pyplot as plt
import seaborn as sns
# 눈금 그리기
sns.set_style('whitegrid')
%matplotlib inline
# 머신러닝 모델 불러오기
## 로지스틱 회귀
from sklearn.linear_model import LogisticRegression
## SVC 서포터 백터 머신
from sklearn.svm import SVC
## 랜덤 포레스트
from sklearn.ensemble import RandomForestClassifier
## for validation data
from sklearn.model_selection import train_test_split
In [114]:
# 데이터 불러오기
train_df = pd.read_csv("./titanic/train.csv")
test_df = pd.read_csv("./titanic/test.csv")
In [115]:
train_df.head(10)
Out[115]:
PassengerIdSurvivedPclassNameSexAgeSibSpParchTicketFareCabinEmbarked0123456789
1 | 0 | 3 | Braund, Mr. Owen Harris | male | 22.0 | 1 | 0 | A/5 21171 | 7.2500 | NaN | S |
2 | 1 | 1 | Cumings, Mrs. John Bradley (Florence Briggs Th... | female | 38.0 | 1 | 0 | PC 17599 | 71.2833 | C85 | C |
3 | 1 | 3 | Heikkinen, Miss. Laina | female | 26.0 | 0 | 0 | STON/O2. 3101282 | 7.9250 | NaN | S |
4 | 1 | 1 | Futrelle, Mrs. Jacques Heath (Lily May Peel) | female | 35.0 | 1 | 0 | 113803 | 53.1000 | C123 | S |
5 | 0 | 3 | Allen, Mr. William Henry | male | 35.0 | 0 | 0 | 373450 | 8.0500 | NaN | S |
6 | 0 | 3 | Moran, Mr. James | male | NaN | 0 | 0 | 330877 | 8.4583 | NaN | Q |
7 | 0 | 1 | McCarthy, Mr. Timothy J | male | 54.0 | 0 | 0 | 17463 | 51.8625 | E46 | S |
8 | 0 | 3 | Palsson, Master. Gosta Leonard | male | 2.0 | 3 | 1 | 349909 | 21.0750 | NaN | S |
9 | 1 | 3 | Johnson, Mrs. Oscar W (Elisabeth Vilhelmina Berg) | female | 27.0 | 0 | 2 | 347742 | 11.1333 | NaN | S |
10 | 1 | 2 | Nasser, Mrs. Nicholas (Adele Achem) | female | 14.0 | 1 | 0 | 237736 | 30.0708 | NaN | C |
In [116]:
# 데이터의 전체적인 정보 확인
train_df.info()
print("="*60)
test_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 12 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 891 non-null int64
1 Survived 891 non-null int64
2 Pclass 891 non-null int64
3 Name 891 non-null object
4 Sex 891 non-null object
5 Age 714 non-null float64
6 SibSp 891 non-null int64
7 Parch 891 non-null int64
8 Ticket 891 non-null object
9 Fare 891 non-null float64
10 Cabin 204 non-null object
11 Embarked 889 non-null object
dtypes: float64(2), int64(5), object(5)
memory usage: 83.7+ KB
============================================================
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 11 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 418 non-null int64
1 Pclass 418 non-null int64
2 Name 418 non-null object
3 Sex 418 non-null object
4 Age 332 non-null float64
5 SibSp 418 non-null int64
6 Parch 418 non-null int64
7 Ticket 418 non-null object
8 Fare 417 non-null float64
9 Cabin 91 non-null object
10 Embarked 418 non-null object
dtypes: float64(2), int64(4), object(5)
memory usage: 36.0+ KB
여기서 자세히 봐야 할 곳은 다음과 같다.
- #### 각 데이터의 결측치 확인
- 결측치를 평균이나 중간값을 로 채울지 , 아예 column 을 drop 할지 정한다.
- cabin, age, embarked가 많이 비어있다.
- #### 데이터를 숫자로 변환 할 수 있는가
- 하다 못해 Category 형으로 만들 수 있는가
- name,PassengerId, Ticket가 걸린다.
결과 PassengerId Name Ticket 을 제거한다
단 PassengerId같은 경우 결과물에서 필요로 하므로 test에서만 지운다
In [117]:
train_df = train_df.drop(['PassengerId', 'Name', 'Ticket'], axis=1)
test_df = test_df.drop(['Name','Ticket'], axis=1)
각각의 Feature을 확인 해보자
Embarked = 승선한 항
In [118]:
# train_data 에만 2개의 값이 na 값이다.
# 이를 가장 많은 값으로 채운다...
# 솔직히 승선한 항과 생존 여부는 관련없을 것 같다...
# 그래서 drop 하기로 하였다.
train_df.drop('Embarked', axis = 1,inplace = True)
test_df.drop('Embarked', axis = 1 ,inplace = True)
fare = 요금
test_df 에서만 값이 하나가 null 값이다.
중위값으로 채우도록 하자
In [119]:
# scatter plot 으로 확인한 결과 각 값의 이상치가 많으므로 표준 편차가 크기 나온다
# 그러므로 평균값 대신 중위값을 이용하여 na 값을 채우도록 한다.
test_df['Fare'].fillna(test_df['Fare'].median() , inplace = True)
sns.scatterplot(test_df['Fare'])
plt.show()
In [120]:
# 요금의 값이 실수로 되어있는 것을 알수 있다.
# 이를 정수형으로 변환 한다.
print(test_df['Fare'].dtype)
test_df['Fare'] = test_df['Fare'].astype(int)
train_df['Fare'] = train_df['Fare'].astype(int)
float64
In [121]:
# 생존자와 사망자가 낸 요금 의 평군을 비교하여 보자
fare_survived_pivot = \
train_df.loc[:,("Fare","Survived")]\
.pivot_table(columns='Survived', aggfunc = ('std','mean'))\
.loc['Fare']\
.T.reset_index()
train_df['Fare'].plot(kind='hist', figsize=(15,3),bins=100, xlim=(0,50))
plt.show()
plt.figure(figsize=(15,10))
sns.barplot(x = 'Survived' , y= 'mean',data = fare_survived_pivot)
plt.show()
plt.figure(figsize=(15,10))
sns.barplot(x = 'Survived' , y= 'std',data = fare_survived_pivot)
plt.show()
Age = 나이
In [122]:
# Age 는 train,test 둘다 null 값이 많이 존재 한다,
# 이를 해결해보자
# 1 == 세로 개수 2== 가로 개수
fig,( origin_axis , new_axis) = plt.subplots(1,2,figsize = (20,10))
origin_axis.set_title('Oringinal Age')
new_axis.set_title('New Age')
# avg , std , na 값 개수, 값을 구한다.
avg_age_train_df = train_df['Age'].mean()
std_age_train_df = train_df['Age'].std()
age_na_count_train_df = train_df['Age'].isna().sum()
avg_age_test_df = test_df['Age'].mean()
std_age_test_df = test_df['Age'].std()
age_na_count_test_df = test_df['Age'].isna().sum()
# avg - std ~ avg + std 사이의 임의의 수를 생성한다.
rand_age_for_train = \
np.random.randint(avg_age_train_df - std_age_train_df,
avg_age_train_df + std_age_train_df,
size = age_na_count_train_df)
rand_age_for_test = \
np.random.randint(avg_age_test_df - std_age_test_df,
avg_age_test_df + std_age_test_df,
size = age_na_count_test_df)
# na값을 제화한 나머지 값들의 히스토그램을 그린다,
train_df['Age'].dropna().astype('int32').hist(bins = 60 , ax = origin_axis)
# 아까 생성한 표준과 표준편차를 이용한 값을 사용하여 구한 랜덤값 리스트를 이용하여
# na 값을 채운다.
train_df['Age'][np.isnan(train_df['Age'])] = rand_age_for_train
test_df['Age'][np.isnan(test_df['Age'])] = rand_age_for_test
# test,train 전부 나이 값이 float64 로 되어 있으므로 전부 정수형으로 바꾸어 준다
train_df['Age'] = train_df['Age'].astype('int32')
test_df['Age'] = test_df['Age'].astype('int32')
# 결측값을 채운 Age값을 히스토그램으로 그린다.
train_df['Age'].hist(bins = 60 , ax = new_axis)
/var/folders/qg/ktrysd2n07s7yy4yg05vm37h0000gn/T/ipykernel_13577/1812080846.py:33: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
train_df['Age'][np.isnan(train_df['Age'])] = rand_age_for_train
/var/folders/qg/ktrysd2n07s7yy4yg05vm37h0000gn/T/ipykernel_13577/1812080846.py:34: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
test_df['Age'][np.isnan(test_df['Age'])] = rand_age_for_test
Out[122]:
<Axes: title={'center': 'New Age'}>
In [123]:
# 계속 나이값을 시각화 해보자
# 생존자와 사망자의 나이의 피크값을 알아보자
fig, (axis1,axis2) = plt.subplots(2,1,figsize = (18,10))
sns.kdeplot(x = "Age",
data = train_df ,
hue = "Survived",
shade = True ,
ax = axis1,
)
plt.xlim(0, train_df["Age"].max())
# 생존자의 나이에 따른 생존확률
avg_age = train_df[["Age","Survived"]].groupby(["Age"], as_index=False).mean()
sns.barplot(x = "Age" , y = "Survived" , data = avg_age , ax = axis2)
/var/folders/qg/ktrysd2n07s7yy4yg05vm37h0000gn/T/ipykernel_13577/2766721832.py:6: FutureWarning:
`shade` is now deprecated in favor of `fill`; setting `fill=True`.
This will become an error in seaborn v0.14.0; please update your code.
sns.kdeplot(x = "Age",
Out[123]:
<Axes: xlabel='Age', ylabel='Survived'>
cabin = 선실
cabin의 경우 결측값이 반이상 이기 때문에 아예 제거해 버리겠다..
In [124]:
# cabin
train_df.drop("Cabin", axis = 1 , inplace = True)
test_df.drop("Cabin", axis = 1 , inplace = True)
Family = 같이탄 가족 수
여기서 Family = Parch(부모 자식) + SibSp(같이탄 형제 배우자)
In [125]:
# 가족 컬럼 추가
train_df["Family"] = train_df['Parch'] + train_df['SibSp']
test_df["Family"] = test_df['Parch'] + test_df['SibSp']
# Parch, SibSp 컬럼 제거
train_df.drop(['Parch','SibSp'] ,axis = 1, inplace = True)
test_df.drop(['Parch','SibSp'] ,axis = 1, inplace = True)
#
In [126]:
# 가족수와 생존 확률 비교
avg_family = \
train_df[['Family','Survived']].groupby('Family',as_index=False).mean()
plt.figure(figsize=(10,10))
sns.barplot(x='Family', y = 'Survived',data =avg_family)
Out[126]:
<Axes: xlabel='Family', ylabel='Survived'>
생존확률과 가족 존재 여부는 상관관계가 보이나 가족수가 많을 수록 생존확률이 많아지는 것은 아니다.
그러므로 가족 존재 여부로 바꾼다,
In [127]:
# 가족 존재 여부로 변환
train_df["Family"].loc[train_df["Family"]>0] = 1
test_df["Family"].loc[test_df["Family"]>0] = 1
/var/folders/qg/ktrysd2n07s7yy4yg05vm37h0000gn/T/ipykernel_13577/2680595693.py:2: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
train_df["Family"].loc[train_df["Family"]>0] = 1
/var/folders/qg/ktrysd2n07s7yy4yg05vm37h0000gn/T/ipykernel_13577/2680595693.py:3: SettingWithCopyWarning:
A value is trying to be set on a copy of a slice from a DataFrame
See the caveats in the documentation: https://pandas.pydata.org/pandas-docs/stable/user_guide/indexing.html#returning-a-view-versus-a-copy
test_df["Family"].loc[test_df["Family"]>0] = 1
In [128]:
# 위를 이용한 시각화
fig , (axis1 , axis2) = plt.subplots(1,2,figsize = (10,10))
# 가족 존재 여부와 생존 여부 판단.
avg_family_survived = \
train_df[['Family','Survived']]\
.groupby('Family', as_index=False).mean()
# countplot 과 barplot 사용
sns.countplot(x='Family', data = train_df , ax = axis1)
sns.barplot(x= "Family" ,y= 'Survived', data = train_df , ax = axis2)
# 라벨 설정
axis1.set_xticklabels(['Alone','With Family'])
axis2.set_xticklabels(['Alone','With Family'])
plt.show()
sex = 성
위의 나이 그래프에 따르면 성별외로 나이가 어린 사람의 생존율이 좀 더 높았다
그래서 성을 male , female , child 로 나눈다.
그 후 one-hot incoding 을 사용하여 수치화 해준다.
In [129]:
# 15세 이하의 사람을 어린이로 본다.
# 'sex' column 을 이용하여 person 을 column을 만든다.
# 단일 컬럼에는 map ,apply 을 사용할 수 있지만 , 다중 컬럼일 경우에는 apply에 axis = 1
# 인수를 주고 사용해야 한다.
# apply 에 사용하기 위한 함수 정의
def get_person(person):
age, sex = person
return sex if age>15 else 'child'
# apply를 사용하여 person 컴럼 생성
train_df['person'] = train_df[['Age','Sex']].apply(get_person,axis = 1)
test_df['person'] = test_df[['Age','Sex']].apply(get_person,axis = 1)
# sex columns drop
train_df.drop('Sex' ,axis = 1, inplace = True)
test_df.drop('Sex' ,axis = 1, inplace = True)
# 원-핫 인코딩
person_dummies_train = pd.get_dummies(train_df['person'])
person_dummies_test = pd.get_dummies(test_df['person'])
# 컬럼명 변경
person_dummies_train.columns = ['Child', 'Female', 'Male']
person_dummies_test.columns = ['Child', 'Female', 'Male']
# join 은 옆에 부치는 것 concat 은 아래에 부티는 것
train_df = train_df.join(person_dummies_train)
test_df = test_df.join(person_dummies_test)
# subplots 설정
f , (axis1,axis2) = plt.subplots(1,2,figsize = [20,20])
# person 의 카테고리별 count
sns.countplot(x = 'person' , data = train_df ,ax = axis1)
#person 의 카테고리별 생존률
avg_survived_person = \
train_df[['person', 'Survived']].groupby('person',as_index =False).mean()
sns.barplot(x = 'person' ,
y = 'Survived',
data = avg_survived_person ,
ax = axis2,
order= ['male','female','child'])
plt.show()
# 다쓴 person drop
train_df.drop('person' , axis = 1, inplace = True)
test_df.drop('person' , axis = 1, inplace = True)
# 남성이 제일 많았지만 여성과 아이의 생존 비율이 훨씬 높다.
pclass = 승객 등급
결측값은 없다.
In [130]:
# sns.factorplot('Pclass',data=train_df,kind='count')
# seaborn 0.9.0 부터 바꾸어서 아래와 같이 사용해야 한다고 한다.
# 출처 : https://stackoverflow.com/questions/56077654/trouble-using-factorplot-method-of-seaborn
sns.catplot(x = 'Pclass', y = 'Survived', data = train_df,kind = 'point')
Out[130]:
<seaborn.axisgrid.FacetGrid at 0x28851c220>
1,2,3 등급 밖에 없으므로 이 또한 one-hot-encoding 을 사용한다.
In [131]:
# one-hot-encoding 을 사용하고 컬럼 명을 class1, class2, class3 으로 바꾸어 준다.
class_dummies_train = pd.get_dummies(train_df['Pclass'])
class_dummies_train.columns = ['Class1','Class2','Class2']
class_dummies_test = pd.get_dummies(test_df['Pclass'])
class_dummies_test.columns = ['Class1','Class2','Class2']
# usless Pclass drop
train_df.drop('Pclass' ,axis = 1, inplace = True)
test_df.drop('Pclass' ,axis = 1, inplace = True)
# join dummies to df
train_df = train_df.join(class_dummies_train)
test_df = test_df.join(class_dummies_test)
In [132]:
train_df.info()
print("="*50)
test_df.info()
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 891 entries, 0 to 890
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 Survived 891 non-null int64
1 Age 891 non-null int32
2 Fare 891 non-null int64
3 Family 891 non-null int64
4 Child 891 non-null uint8
5 Female 891 non-null uint8
6 Male 891 non-null uint8
7 Class1 891 non-null uint8
8 Class2 891 non-null uint8
9 Class2 891 non-null uint8
dtypes: int32(1), int64(3), uint8(6)
memory usage: 29.7 KB
==================================================
<class 'pandas.core.frame.DataFrame'>
RangeIndex: 418 entries, 0 to 417
Data columns (total 10 columns):
# Column Non-Null Count Dtype
--- ------ -------------- -----
0 PassengerId 418 non-null int64
1 Age 418 non-null int32
2 Fare 418 non-null int64
3 Family 418 non-null int64
4 Child 418 non-null uint8
5 Female 418 non-null uint8
6 Male 418 non-null uint8
7 Class1 418 non-null uint8
8 Class2 418 non-null uint8
9 Class2 418 non-null uint8
dtypes: int32(1), int64(3), uint8(6)
memory usage: 14.0 KB
Targert 값과 Feature 값을 분리
In [138]:
# train valid 나누어서 모델 확인
train , valid = train_test_split(train_df , test_size=0.2 , random_state=42)
# Survived drop
X_train = train.drop('Survived' ,axis =1)
X_valid = valid.drop('Survived' ,axis =1)
# only survived
Y_train = train['Survived']
Y_valid = valid['Survived']
# train 과 feature 값을 맞추어 주기 위하여 PassengerId를 제거
X_test = test_df.drop('PassengerId' , axis =1).copy()
Logistic Regression (로지스틱 회귀)
In [139]:
lr_model = LogisticRegression()
lr_model.fit(X_train,Y_train)
Y_pred = lr_model.predict(X_test)
lr_model.score(X_valid,Y_valid)
/opt/homebrew/Caskroom/miniconda/base/lib/python3.10/site-packages/sklearn/linear_model/_logistic.py:458: ConvergenceWarning: lbfgs failed to converge (status=1):
STOP: TOTAL NO. of ITERATIONS REACHED LIMIT.
Increase the number of iterations (max_iter) or scale the data as shown in:
https://scikit-learn.org/stable/modules/preprocessing.html
Please also refer to the documentation for alternative solver options:
https://scikit-learn.org/stable/modules/linear_model.html#logistic-regression
n_iter_i = _check_optimize_result(
Out[139]:
0.7653631284916201
In [140]:
svx_model = SVC()
svx_model.fit(X_train , Y_train)
svx_model.score(X_valid, Y_valid)
Out[140]:
0.6536312849162011
In [141]:
rf_model = RandomForestClassifier(n_estimators=100)
rf_model.fit(X_train,Y_train)
rf_model.score(X_valid,Y_valid)
Out[141]:
0.7932960893854749
최종적으로 Random forest 가 가장 뛰어난 성능을 보이므로 해당 모델을 사용하여 결과를 제출한다.
In [151]:
X_train = train_df.drop('Survived' ,axis =1)
# only survived
Y_train = train_df['Survived']
In [153]:
rf_model = RandomForestClassifier(n_estimators=100)
rf_model.fit(X_train,Y_train)
Y_pred = lr_model.predict(X_test)
In [154]:
# 로지스틱 회귀 모델에서 각 Feature별 상관 계수를 가지고 온다.
coeff_df = pd.DataFrame(train_df.columns.delete(0))
coeff_df.columns = ['Features']
coeff_df["Coefficient Estimate"] = pd.Series(lr_model.coef_[0])
coeff_df
Out[154]:
FeaturesCoefficient Estimate012345678
Age | -0.019551 |
Fare | 0.001398 |
Family | -0.232565 |
Child | 0.519034 |
Female | 1.420083 |
Male | -1.465784 |
Class1 | 1.210193 |
Class2 | 0.218053 |
Class2 | -0.954913 |
요금과 나이는 거의 상관이 없다고 나왔다.
In [155]:
submission = pd.DataFrame({
"PassengerId": test_df["PassengerId"],
"Survived": Y_pred
})
submission.to_csv('titanic.csv', index=False)