import pandas as pd
import numpy as np
import platform
import matplotlib.pyplot as plt
import seaborn as sns
# 운영체제별 한글 폰트 설정
if platform.system() == "Darwin":
plt.rcParams["font.family"] = "Apple SD Gothic Neo"
elif platform.system() == "Windows":
plt.rcParams["font.family"] = "Malgun Gothic"
else:
plt.rcParams["font.family"] = "NanumGothic"
plt.rcParams["axes.unicode_minus"] = False
# 데이터 로드
df = pd.read_csv(
"../data/titanic_train.csv", dtype_backend="pyarrow", engine="pyarrow"
)7 실습: 변환과 정제
이 실습에서는 타이타닉 승객 데이터(‘titanic_train.csv’)를 활용하여 데이터 변환과 정제 과정을 단계별로 학습합니다. 데이터 분석에서 결측치 처리, 이상치 탐지, 타입 변환, 그리고 문자열 처리는 기본적이면서도 중요한 작업입니다.
7.1 데이터 준비 및 탐색
먼저 필요한 라이브러리를 가져오고 분석할 데이터를 불러옵니다. 데이터의 전반적인 구조와 결측치 현황을 확인하는 것이 정제의 첫 단계입니다.
먼저 데이터 크기와 컬럼을 확인합니다.
데이터 크기와 컬럼 목록을 확인할 수 있습니다.
데이터가 정상적으로 로드되었다면, 각 열에 결측치가 얼마나 있는지 확인해야 합니다. 결측치의 개수와 비율을 함께 확인하여 정제 전략을 세웁니다.
결측치 개수 결측치 비율 (%)
Age 177 19.87
Cabin 687 77.10
Embarked 2 0.22
7.2 결측치 처리
타이타닉 데이터에서 Age, Cabin, Embarked 열에 결측치가 있음을 확인했습니다. 각 열의 특성에 따라 서로 다른 결측치 처리 방법을 적용합니다.
7.2.1 Embarked 열의 결측치 처리 (최빈값 대체)
Embarked(탑승 항구) 열은 결측치가 2개로 매우 적습니다. 이러한 범주형 변수의 결측치는 자주 등장하는 값인 최빈값(Mode)으로 대체하는 것이 일반적입니다.
7.2.2 Age 열의 결측치 처리 (그룹별 중앙값 대체)
Age(나이) 열은 누락된 값이 많습니다. 단순히 전체 나이의 평균이나 중앙값으로 채우기보다, 승객의 객실 등급(Pclass)과 성별(Sex)에 따라 나이 분포가 다를 수 있으므로 이를 반영하여 그룹별 중앙값으로 채우는 것이 더 정교합니다.
# Pclass와 Sex 그룹별 나이의 중앙값 확인
grouped_medians = df.groupby(
["Pclass", "Sex"], observed=True
)["Age"].median()
print("그룹별 나이 중앙값:")
print(grouped_medians)
# transform을 사용하여 각 행에 해당하는 그룹의 중앙값 매핑
age_imputed = df.groupby(["Pclass", "Sex"], observed=True)[
"Age"
].transform("median")
# 결측치를 그룹별 중앙값으로 채우기
df["Age"] = df["Age"].fillna(age_imputed)
print(
"Age 결측치 정제 후 남은 개수:",
df["Age"].isnull().sum(),
)그룹별 나이 중앙값:
Pclass Sex
1 female 35.0
male 40.0
2 female 28.0
male 30.0
3 female 21.5
male 25.0
Name: Age, dtype: double[pyarrow]
Age 결측치 정제 후 남은 개수: 0
7.2.3 Cabin 열의 결측치 처리 (임의의 범주 생성)
Cabin(선실 번호) 열은 결측치 비율이 70%가 넘습니다. 이처럼 누락된 정보가 많은 경우에는 결측치 자체를 하나의 정보로 취급하여 ‘N’(Not Specified)과 같은 새로운 범주값으로 채우는 전략을 취할 수 있습니다.
7.3 이상치 탐지 및 처리
이상치(Outlier)는 분석 결과를 왜곡할 수 있으므로 적절하게 탐지하고 처리해야 합니다. 승객 요금(Fare) 데이터를 바탕으로 이상치를 확인해 봅니다.
7.3.1 IQR 방법을 통한 이상치 탐색
IQR(사분위 범위)을 사용하여 요금 데이터의 이상치 경계를 설정하고 이상치 개수를 파악합니다.
# 요금(Fare)의 사분위수 계산
Q1 = df["Fare"].quantile(0.25)
Q3 = df["Fare"].quantile(0.75)
IQR = Q3 - Q1
# 이상치 판단 기준이 되는 상한선과 하한선 계산
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
print("요금 IQR:", round(IQR, 2))
print(
"하한선:",
round(lower_bound, 2),
"상한선:",
round(upper_bound, 2),
)
# 이상치에 해당하는 행 탐지
outliers = df[
(df["Fare"] < lower_bound) | (df["Fare"] > upper_bound)
]
print("IQR 기준 이상치 개수:", len(outliers))요금 IQR: 23.09
하한선: -26.72 상한선: 65.63
IQR 기준 이상치 개수: 116
7.3.2 이상치 정제 (Capping)
요금이 음수인 경우는 물리적으로 불가능하며, 높은 요금은 극단적인 예외 상황일 수 있습니다. 여기서는 행을 삭제하는 대신, 상한선보다 큰 요금은 상한선 값으로, 하한선보다 작은 요금은 하한선 값으로 제한하는 clip 메서드를 적용합니다.
7.3.3 분포 시각화 비교
Boxplot을 사용하여 요금 데이터 정제 전후의 분포 변화를 확인합니다.
7.3.4 가족 규모 변수 생성 및 생존율 분석
동반한 형제자매 및 배우자 수(SibSp)와 부모 및 자녀 수(Parch)에 본인을 더하여 가족 규모(FamilySize) 열을 만듭니다. 그 후, 가족 규모가 5명 이상인 대가족 승객들을 필터링하고 이들의 생존율(Survived 컬럼의 평균값)을 계산해 봅니다.
가족 규모 분포:
FamilySize
1 537
2 161
3 102
...
7 12
11 7
8 6
Name: count, Length: 9, dtype: int64[pyarrow]
5명 이상 대가족의 생존율: 16.13%
7.4 타입 변환
정제된 데이터의 가독성을 높이고 메모리 효율을 향상시키기 위해 적절한 데이터 타입으로 변환합니다.
7.4.1 PyArrow 정수형 변환
나이(Age) 열은 결측치를 대체했으나 여전히 실수형(double[pyarrow])으로 설정되어 있을 수 있습니다. 소수점을 반올림하여 정리한 뒤 결측치를 안전하게 포함할 수 있는 PyArrow 정수형(int64[pyarrow])으로 변환합니다.
7.4.2 범주형 변환
성별(Sex) 열은 male과 female 두 가지 문자열 값만 가집니다. 이를 category 타입으로 변환하여 메모리 효율을 높이고 범주형 변수로서의 특성을 명확히 합니다.
7.5 문자열 처리
텍스트 데이터로 채워진 열들에서 필요한 정보를 추출하고 문자열을 변환하는 작업을 수행합니다.
7.5.1 Name 열에서 호칭 추출하기
승객의 이름(Name)은 ’Lastname, Title. Firstname’의 규칙적인 형식으로 되어 있습니다. 정규표현식을 사용하여 승객의 호칭(Title)을 추출하고 새로운 열을 생성합니다.
7.5.2 호칭 그룹화 및 표준화
추출한 호칭 중 빈도가 낮은 호칭들을 Other로 통합하고, 문자열 공백을 제거한 후 모두 대문자로 통일합니다.
표준화된 호칭 빈도:
Title
MR 517
MISS 182
MRS 125
MASTER 40
OTHER 27
Name: count, dtype: int64[pyarrow]
7.5.3 Cabin 열에서 선실 구역(Deck) 추출
선실 번호(Cabin)의 첫 글자는 배의 구역(Deck)을 나타냅니다. Cabin 열의 첫 글자만 슬라이싱하여 새로운 Deck 열을 만듭니다.
7.5.4 티켓 번호에서 알파벳 접두사 필터링
Ticket(티켓 번호) 열에는 숫자로만 구성된 티켓이 있는 반면, 알파벳 접두사가 포함된 티켓(A/5 21171, PC 17599)도 있습니다. 문자열 메서드나 정규표현식을 사용하여 알파벳 문자가 포함된 티켓을 가진 승객들의 데이터를 필터링하고 해당 행의 개수를 출력합니다.
알파벳 문자가 포함된 티켓 개수: 230
0 A/5 21171
1 PC 17599
2 STON/O2. 3101282
10 PP 9549
12 A/5. 2151
Name: Ticket, dtype: string[pyarrow]
7.5.5 호칭별 평균 나이를 활용한 결측치 대체
앞선 결측치 처리 단계에서는 객실 등급(Pclass)과 성별(Sex) 그룹의 중앙값으로 나이 결측치를 채웠습니다. 이번에는 문자열 처리 단계에서 추출한 호칭(Title) 정보를 활용하여, 각 호칭별 평균 나이로 결측치를 채우는 대안적인 방법을 살펴봅니다. 이를 위해 먼저 Age 결측치가 존재하는 원본 데이터를 복사하여 실습을 진행합니다.
# 복사본 데이터프레임 준비 및 Title 컬럼 생성
df_alt = pd.read_csv(
"../data/titanic_train.csv", dtype_backend="pyarrow", engine="pyarrow"
)
df_alt["Title"] = df_alt["Name"].str.extract(
r",\s*(?P<Title>[A-Za-z]+)\.", expand=False
)
major_titles = ["Mr", "Miss", "Mrs", "Master"]
df_alt["Title"] = df_alt["Title"].where(df_alt["Title"].isin(major_titles), "Other")
# 호칭별 평균 나이 계산
title_age_means = df_alt.groupby("Title", observed=True)[
"Age"
].mean()
print("호칭별 평균 나이:")
print(title_age_means)
# transform을 사용해 각 호칭에 매핑되는 평균 나이로 결측치 채우기
df_alt["Age"] = df_alt["Age"].fillna(
df_alt.groupby("Title", observed=True)["Age"].transform(
"mean"
)
)
print(
"나이 결측치 정제 후 남은 개수:",
df_alt["Age"].isnull().sum(),
)호칭별 평균 나이:
Title
Master 4.574167
Miss 21.773973
Mr 32.36809
Mrs 35.898148
Other 42.384615
Name: Age, dtype: double[pyarrow]
나이 결측치 정제 후 남은 개수: 0