💻 준비 코드
Show code cell source
import pandas as pd
train = pd.read_csv('./train.csv')
test = pd.read_csv('./test.csv')
submission = pd.read_csv('./gender_submission.csv')
for df in [train, test]:
df['Gender'] = df['Sex'].map({'male': 0, 'female': 1})
df.loc[df['Embarked'].isnull(), 'Embarked'] = 'S'
3. 사라진 요금을 찾아서…#
데이터 과학 동아리 - 여섯 번째 모임
프롬: 다안 선배! 지난 시간에 승선 항구 정보를 추가했더니 예측 정확도가 또 올랐잖아요. 이제 약 75%의 승객들의 생존 여부를 정확하게 예측할 수 있게 됐네요. 오늘은 또 어떤 변수를 살펴볼 예정인가요?
다안: 오늘은 승객들이 지불한 요금(Fare) 데이터를 더 자세히 살펴볼 거야. 요금은 이미 우리 모델에 포함되어 있는 수치형 변수 중 하나지만, test 데이터에 결측치가 존재하여 추가적인 처리가 필요해.
코더블: 숫자형 변수의 결측치라… 이전 승선 항구의 문자형 결측치와는 다른 접근이 필요할 것 같아요. 그런데 요금은 객실 등급과 관련이 있을 것 같은데, 그런 관계도 분석해볼 수 있을까요?
다안: 좋은 질문이야! 맞아, 요금은 객실 등급(Pclass)과 밀접한 관련이 있을 거야. 게다가 같은 등급의 객실이라도 승선 항구(Embarked)에 따라 요금이 달랐을 수도 있어. 이러한 관계들을 분석하여 가장 적절한 값으로 결측치를 채워보도록 하자.
Fare 결측치 현황 파악#
프롬: 그럼 먼저 얼마나 많은 결측치가 있는지부터 확인해야 할 것 같아요. train 데이터와 test 데이터에서 Fare의 결측치가 몇 개나 있는지 확인해볼게요!
# 프롬프트
train과 test 데이터의 Fare 컬럼 결측치 개수를 계산해줘
코더블: 저는 isnull() 함수와 sum() 함수를 사용해서 결측치 개수를 계산해볼게요.
print(f"Number of missing values in Fare (train): {train['Fare'].isnull().sum()}")
print(f"Number of missing values in Fare (test): {test['Fare'].isnull().sum()}")
Number of missing values in Fare (train): 0
Number of missing values in Fare (test): 1
프롬: 오, train 데이터에는 결측치가 없고 test 데이터에만 1개가 있네요! 단 한 개의 결측치라… 간단해 보이지만, 정확한 예측을 위해서는 이것도 채워넣어야겠죠?
다안: 맞아! 비록 하나의 결측치지만, 정확한 예측을 위해서는 적절한 값으로 채워넣는 게 좋아. 머신러닝 알고리즘은 결측치가 있으면 제대로 작동하지 않는 경우가 많거든. 아무리 작은 문제라도 해결해야 전체 시스템이 제대로 돌아가는 법이지.
결측치 승객 정보 확인#
코더블: 그럼 그 한 명의 승객은 누구인지 확인해볼까요? 그 사람의 다른 정보들을 알면 적절한 요금을 추정하는 데 도움이 될 것 같아요.
프롬: 네, 저도 궁금해요! 그 승객의 정보를 확인해볼게요.
# 프롬프트
Fare가 결측치인 승객의 모든 정보를 보여줘
코더블: 저는 이렇게 코드를 작성해볼게요. test 데이터에서 Fare가 결측치인 행만 선택해서 보는 거죠.
test[test.Fare.isna()]
PassengerId | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | Gender | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
152 | 1044 | 3 | Storey, Mr. Thomas | male | 60.5 | 0 | 0 | 3701 | NaN | NaN | S | 0 |
다안: 좋아, 이제 요금 정보가 누락된 승객에 대해 알게 됐네. 이 승객(Storey, Mr. Thomas)의 특징을 정리해보면:
3등석(Pclass=3) 승객이야
남성(Sex=’male’) 승객이고
Southampton(Embarked=’S’)에서 승선했어
가족은 동반하지 않았어(SibSp=0, Parch=0)
이런 정보는 결측치를 채울 때 정말 유용해. 특히 같은 등급, 같은 항구에서 승선한 다른 승객들의 요금을 참고하면 좋을 것 같아.
프롬: 그럼 3등석이면서 Southampton에서 승선한 다른 승객들의 요금은 얼마였는지 알아봐야겠네요!
등급과 항구별 승객 통계 분석#
다안: 좋은 생각이야! 객실 등급(Pclass)과 승선 항구(Embarked)의 각 조합별로 승객 수와 요금의 중앙값을 함께 확인해보자. 이렇게 하면 더 신뢰할 수 있는 참고 값을 얻을 수 있을 거야.
프롬: 그럼 확인해볼게요!
# 프롬프트
객실 등급과 탑승 항구별로 그룹을 나누고, 각 그룹의 승객 수와 요금 중앙값을 계산해줘
코더블: 저는 groupby 함수를 사용해서 Pclass와 Embarked로 그룹을 나누고, 각 그룹의 통계를 계산해볼게요.
passenger_stats = train.groupby(['Pclass', 'Embarked'])['Fare'].agg(['count', 'median'])
passenger_stats
count | median | ||
---|---|---|---|
Pclass | Embarked | ||
1 | C | 85 | 78.2667 |
Q | 2 | 90.0000 | |
S | 129 | 52.5542 | |
2 | C | 17 | 24.0000 |
Q | 3 | 12.3500 | |
S | 164 | 13.5000 | |
3 | C | 66 | 7.8958 |
Q | 72 | 7.7500 | |
S | 353 | 8.0500 |
프롬: 와, 객실 등급과 승선 항구별로 요금이 확실히 차이가 나네요! 특히 1등석은 정말 비싸고, 같은 등급이라도 승선 항구에 따라 요금이 다르네요.
다안: 그래, 결과를 보면 각 조합에 대한 상세 정보를 알 수 있어:
승객 수를 통해 각 그룹의 표본 크기를 확인할 수 있어
특히 3등석 Southampton의 경우 353명으로 가장 많은 승객 수를 보여 중앙값의 신뢰도가 높을 거야
중앙값을 보면 같은 등급이라도 승선 항구에 따라 요금 차이가 있음을 알 수 있어
이 통계를 바탕으로 Storey씨의 결측치를 채울 적절한 값을 결정할 수 있을 것 같아.
코더블: 3등석 Southampton 승객들의 요금 중앙값은 8.05파운드네요. 이 값이 가장 적절한 추정치로 보여요. 그런데 같은 등급이라도 승선 항구에 따라 요금이 다르다는 건 흥미로워요. 예를 들어, 3등석도 Cherbourg에서는 약 7.90 파운드인데, Queenstown에서는 7.75파운드예요.
다안: 좋은 관찰이야! 항구별로 요금이 달랐던 이유는 여러 가지가 있을 수 있어. 요금은 단순히 객실 등급뿐만 아니라 동승한 가족 수, 객실의 정확한 위치, 심지어는 예약 시기에 따라서도 달라졌을 거야. 각 항구마다 승선한 승객들의 이런 요인들이 달랐기 때문에 통계값에도 차이가 생긴 거지. 예를 들어, Southampton에서는 가족 단위 승객이 많았을 수도 있고, Cherbourg에서는 상대적으로 배 안쪽의 객실을 더 많이 예약했을 수도 있어. 또한 각 항구마다 티켓 판매 정책이 조금씩 달랐을 가능성도 있지. 이런 작은 차이들이 모여서 항구별로 다른 요금 패턴을 만들어냈을 거야.
자, 이제 Storey씨의 결측치를 채워보자. train 데이터에서 3등석이면서 Southampton에서 승선한 승객들의 요금 중앙값을 구해서 그 값으로 결측치를 채우는 게 좋을 것 같아. 즉, train 데이터의 통계값을 사용해서 test 데이터의 결측치를 채우는 거지.
결측치 채우기#
프롬: 궁금한게 있는데요. “test 데이터의 결측치를 채우는데, 왜 train 데이터의 중앙값을 사용하나요? test 데이터의 중앙값이나 전체 데이터의 중앙값을 쓰는 게 맞지 않나요?”
다안: 아주 좋은 질문이야! 머신러닝에서 정말 중요한 원칙과 관련된 거야. 바로 ‘데이터 유출(Data Leakage)’을 방지하기 위한 거지.
쉽게 설명해볼게. 머신러닝은 시험을 준비하는 것과 비슷해. 우리는 기출문제(train data)로 공부하고, 실제 시험(test data)을 봐. 만약 시험 문제를 미리 보고 공부한다면? 그건 일종의 부정행위지!
test 데이터의 정보를 사용하여 전처리하는 것도 마찬가지야. 이건 아직 알 수 없는 미래의 정보를 사용하는 것과 같아. 그래서 우리는 항상 train 데이터만 가지고 전처리 방법을 결정하고, 그 방법을 test 데이터에 적용해야 해.
코더블: 실제 서비스 환경을 생각해보면 더 이해가 쉬울 것 같아요. 예를 들어, 내일 타이타닉호에 새로운 승객이 승선한다고 가정해볼까요? 그 승객의 요금 정보가 누락됐다면, 우리는 과거의 데이터(train data)를 참고해서 채워야겠죠. 미래의 데이터는 아직 알 수 없으니까요.
프롬: 아하! 이제 이해가 됐어요. 그럼 3등석, Southampton에서 승선한 승객들의 요금 중앙값으로 결측치를 채워볼게요!
# 프롬프트
1. 3등석이면서 Southampton에서 승선한 승객들의 요금 중앙값을 구해서
2. 그 값으로 결측치를 채운 후
3. 해당 승객(PassengerId=1044)의 정보를 다시 출력해줘
코더블: 저는 코드로 구현해볼게요. 먼저 조건에 맞는 중앙값을 계산하고, 그 값으로 결측치를 채워보겠습니다.
median_fare = train[(train['Pclass'] == 3) & (train['Embarked'] == 'S')]['Fare'].median()
test['Fare'] = test['Fare'].fillna(median_fare)
test[test['PassengerId'] == 1044]
PassengerId | Pclass | Name | Sex | Age | SibSp | Parch | Ticket | Fare | Cabin | Embarked | Gender | |
---|---|---|---|---|---|---|---|---|---|---|---|---|
152 | 1044 | 3 | Storey, Mr. Thomas | male | 60.5 | 0 | 0 | 3701 | 8.05 | NaN | S | 0 |
프롬: 이제 요금 정보가 채워졌네요! 8.05파운드예요. 3등석 Southampton 승객들의 중앙값과 정확히 일치하네요.
다안: 훌륭해! 머신러닝 프로젝트에서 test 데이터의 결측치를 채울 때는 반드시 train 데이터를 기준으로 해야 한다는 중요한 원칙을 배웠네. 이건 실전에서도 매우 중요한 개념이야.
프롬: 그런데 이렇게 한 사람의 결측치를 추정하면서 문득 의문이 들었어요. 혹시 이 결측치에 어떤 특별한 의미가 있는 건 아닐까요? 그러니까… 요금을 지불하지 않았을 수도 있지 않을까요?
다안: 정말 흥미로운 질문이야! 단순한 데이터 누락이 아니라 더 깊은 의미가 있을 수 있다는 접근은 좋은 데이터 과학자의 자세야. 실제로 데이터 분석에서는 ‘결측의 이유’가 중요한 정보가 되기도 해. 혹시 Storey씨가 특별한 이유로 무임승선했던 건 아닐까?
코더블: 그럼 다음 시간에는 요금이 0인 승객들이 있는지도 확인해보면 좋을 것 같아요! 그런 패턴이 있다면 Storey씨의 결측치를 0으로 채우는 것도 하나의 방법이 될 수 있겠네요.
다안: 좋은 생각이야! 오늘은 수치형 결측치를 어떻게 채우는지, 특히 train 데이터를 기준으로 결측치를 채워야 한다는 중요한 원칙을 배웠어. 다음 시간에는 이 결측치의 의미를 좀 더 깊이 탐구해보고, 혹시 무임승객이 실제로 존재했는지도 살펴보자. 데이터에는 항상 우리가 미처 생각하지 못한 이야기가 숨어있거든!
프롬: 정말 기대돼요! 타이타닉호에 무임승객이 있었다면 그건 또 다른 흥미로운 이야기가 될 것 같아요. 오늘도 많은 걸 배웠어요, 다안 선배!