본 콘텐츠의 이미지 및 내용은 AI로 생성되었습니다.
본 콘텐츠의 이미지 및 내용을 무단으로 복제, 배포, 수정하여 사용할 경우 저작권법에 의해 법적 제재를 받을 수 있습니다.
이미지 로딩 중...
AI Generated
2025. 11. 29. · 13 Views
실전 프로젝트 Kaggle 데이터셋 EDA 보고서 완벽 가이드
Kaggle 데이터셋을 활용하여 탐색적 데이터 분석(EDA) 보고서를 작성하는 전 과정을 다룹니다. 데이터 로드부터 시각화, 인사이트 도출까지 실무에서 바로 활용할 수 있는 EDA 기법을 배웁니다.
목차
- 데이터셋_로드와_첫인상_파악
- 기술통계량으로_데이터_요약하기
- 결측값_탐지와_처리_전략
- 시각화로_데이터_분포_파악하기
- 상관관계_분석과_히트맵
- 그룹별_비교_분석
- 이상치_탐지와_처리
- EDA_보고서_구조화와_마무리
1. 데이터셋 로드와 첫인상 파악
입사 첫 주, 김개발 씨는 팀장님으로부터 첫 업무를 받았습니다. "이 Kaggle 데이터셋 분석해서 보고서로 정리해 줄 수 있어요?" 막막했습니다.
도대체 어디서부터 시작해야 할까요?
**EDA(Exploratory Data Analysis)**란 데이터를 본격적으로 분석하기 전에 데이터의 특성을 탐색하고 이해하는 과정입니다. 마치 새로운 도시에 여행 갔을 때 지도를 펼쳐 전체 지형을 파악하는 것과 같습니다.
이 과정을 통해 데이터의 구조, 결측값, 이상치 등을 미리 파악하여 분석 방향을 설정할 수 있습니다.
다음 코드를 살펴봅시다.
import pandas as pd
import numpy as np
# Kaggle 데이터셋 로드
df = pd.read_csv('titanic.csv')
# 데이터의 첫인상 파악하기
print(f"데이터 크기: {df.shape[0]}행 x {df.shape[1]}열")
print(f"\n컬럼 목록: {df.columns.tolist()}")
# 처음 5개 행 확인
print("\n=== 데이터 미리보기 ===")
print(df.head())
# 데이터 타입과 결측값 한눈에 보기
print("\n=== 데이터 정보 ===")
print(df.info())
김개발 씨는 입사 3개월 차 주니어 데이터 분석가입니다. 오늘 팀장님이 Kaggle의 타이타닉 데이터셋을 건네며 말했습니다.
"이 데이터로 간단한 EDA 보고서 작성해 볼래요? 내일 오전까지 부탁해요." 김개발 씨는 당황했습니다.
학교에서 pandas는 배웠지만, 실제로 처음 보는 데이터를 어떻게 분석해야 하는지 막막했기 때문입니다. 옆자리 선배 박시니어 씨가 다가와 조언했습니다.
"EDA는 데이터와의 첫 만남이에요. 처음 만난 사람에게 자기소개를 부탁하듯, 데이터에게도 자기소개를 부탁하면 돼요." 그렇다면 EDA란 정확히 무엇일까요?
쉽게 비유하자면, EDA는 마치 새로운 집에 이사 왔을 때 집 구석구석을 둘러보는 것과 같습니다. 방이 몇 개인지, 화장실은 어디에 있는지, 창문은 잘 열리는지 확인하는 과정이지요.
데이터도 마찬가지입니다. 몇 개의 행과 열이 있는지, 어떤 종류의 값들이 담겨 있는지, 빈 칸은 없는지 살펴보는 것입니다.
EDA 없이 바로 분석에 뛰어들면 어떻게 될까요? 한 신입 개발자가 데이터를 확인도 않고 바로 머신러닝 모델을 돌렸다가 낭패를 본 적이 있습니다.
알고 보니 데이터의 절반이 결측값이었고, 나이 컬럼에 음수 값이 들어있었습니다. 몇 시간의 작업이 허사가 되었지요.
이런 문제를 방지하기 위해 **pd.read_csv()**로 데이터를 불러온 뒤, 세 가지 메서드를 순서대로 실행합니다. 먼저 df.shape는 데이터의 크기를 알려줍니다.
891행 12열이라면, 891명의 승객 정보가 12개의 특성으로 기록되어 있다는 의미입니다. 다음으로 **df.head()**는 처음 5개 행을 보여줍니다.
실제 데이터가 어떻게 생겼는지 눈으로 확인하는 것입니다. 숫자인 줄 알았던 컬럼이 문자열일 수도 있고, 예상과 다른 형식일 수도 있습니다.
마지막으로 **df.info()**는 각 컬럼의 데이터 타입과 결측값 개수를 알려줍니다. 891개 행 중 Age 컬럼에 714개만 non-null이라면, 177개의 결측값이 있다는 뜻입니다.
박시니어 씨가 덧붙였습니다. "이 세 가지만 실행해도 데이터의 70%는 파악한 거예요.
나머지는 여기서 발견한 의문점들을 하나씩 풀어나가면 됩니다." 김개발 씨는 고개를 끄덕였습니다. EDA의 첫걸음은 생각보다 간단했습니다.
데이터에게 자기소개를 부탁하는 것, 그것이 시작이었습니다.
실전 팁
💡 - df.shape는 괄호 없이 사용합니다. 메서드가 아닌 속성이기 때문입니다.
- df.head(10)처럼 숫자를 넣으면 원하는 만큼 행을 볼 수 있습니다.
- df.tail()로 마지막 행들도 확인하여 데이터가 끝까지 잘 들어왔는지 점검하세요.
2. 기술통계량으로 데이터 요약하기
김개발 씨가 데이터의 첫인상을 파악했습니다. 이제 박시니어 씨가 물었습니다.
"그래서 이 데이터의 평균 나이는 몇 살이에요? 요금은 얼마나 다양해요?" 숫자 데이터를 한눈에 요약하는 방법이 필요했습니다.
**기술통계량(Descriptive Statistics)**은 데이터를 몇 개의 대표적인 수치로 요약하는 것입니다. 마치 여행지 리뷰를 읽을 때 평점, 방문자 수, 가격대를 먼저 확인하는 것과 같습니다.
평균, 중앙값, 표준편차 등을 통해 데이터의 중심 경향과 퍼짐 정도를 파악할 수 있습니다.
다음 코드를 살펴봅시다.
# 수치형 데이터 기술통계량
print("=== 수치형 변수 통계 ===")
print(df.describe())
# 특정 컬럼의 상세 통계
print(f"\n나이 평균: {df['Age'].mean():.2f}세")
print(f"나이 중앙값: {df['Age'].median():.2f}세")
print(f"나이 표준편차: {df['Age'].std():.2f}")
# 범주형 데이터 빈도 확인
print("\n=== 범주형 변수 분포 ===")
print(df['Pclass'].value_counts())
print(f"\n성별 분포:\n{df['Sex'].value_counts(normalize=True) * 100}")
김개발 씨는 데이터의 구조를 파악했습니다. 하지만 팀장님이 물었습니다.
"타이타닉 승객들의 평균 나이가 몇 살인지 알아요?" 당황한 김개발 씨에게 박시니어 씨가 도움의 손길을 내밀었습니다. "describe() 메서드 하나면 돼요.
수치형 데이터의 핵심 통계를 전부 보여주거든요." 기술통계량이란 무엇일까요? 쉽게 비유하자면, 학교 성적표와 같습니다.
전교생 500명의 점수를 일일이 나열하는 대신, 평균 75점, 최고점 100점, 최저점 30점이라고 요약하면 전체 상황을 한눈에 파악할 수 있습니다. 데이터 분석에서도 마찬가지입니다.
**df.describe()**를 실행하면 8개의 통계량이 한 번에 출력됩니다. count는 결측값을 제외한 데이터 개수입니다.
전체 행 수와 다르다면 결측값이 있다는 신호입니다. mean은 평균값입니다.
타이타닉 승객의 평균 나이가 29.7세라면, 대체로 젊은 층이 많았다고 해석할 수 있습니다. std는 표준편차입니다.
값이 크면 데이터가 넓게 퍼져 있고, 작으면 평균 근처에 모여 있습니다. min, 25%, 50%, 75%, max는 데이터의 분포를 보여줍니다.
50%는 중앙값으로, 평균과 차이가 크면 데이터가 한쪽으로 치우쳐 있다는 의미입니다. 김개발 씨가 질문했습니다.
"그런데 성별이나 등급 같은 건 왜 안 나오나요?" 박시니어 씨가 설명했습니다. "그건 범주형 데이터라서 평균을 구할 수 없어요.
대신 **value_counts()**로 각 범주의 개수를 세면 됩니다." 1등석, 2등석, 3등석 승객이 각각 몇 명인지, 남녀 비율은 어떤지 확인하는 것입니다. normalize=True 옵션을 주면 비율로도 확인할 수 있습니다.
실무에서는 이 통계량들을 보고서 첫 장에 넣는 경우가 많습니다. 데이터의 전체적인 모습을 독자에게 먼저 보여주는 것이지요.
주의할 점도 있습니다. 평균만 보고 판단하면 안 됩니다.
예를 들어 요금의 평균이 32달러인데 중앙값이 14달러라면, 일부 고가 티켓이 평균을 끌어올린 것입니다. 이럴 때는 중앙값이 더 대표성 있는 수치입니다.
김개발 씨는 describe()와 value_counts() 결과를 노트에 정리했습니다. 이제 데이터가 조금씩 말을 걸어오는 것 같았습니다.
실전 팁
💡 - describe(include='all')로 범주형 데이터 통계도 함께 볼 수 있습니다.
- 특정 백분위수가 필요하면 df.quantile([0.1, 0.9])처럼 사용하세요.
- 평균과 중앙값의 차이가 크면 이상치나 편향 데이터를 의심해 보세요.
3. 결측값 탐지와 처리 전략
분석을 진행하던 김개발 씨가 멈칫했습니다. 나이 컬럼에서 NaN이라는 이상한 값을 발견한 것입니다.
"선배님, 이 NaN이 뭐예요?" 데이터 분석의 가장 흔한 복병, 결측값과의 첫 대면이었습니다.
**결측값(Missing Value)**은 데이터에서 비어있는 값을 의미합니다. 마치 설문조사에서 응답하지 않고 넘어간 문항과 같습니다.
결측값을 제대로 처리하지 않으면 분석 결과가 왜곡되거나 오류가 발생할 수 있어, EDA에서 반드시 확인해야 하는 항목입니다.
다음 코드를 살펴봅시다.
# 결측값 개수 확인
print("=== 컬럼별 결측값 개수 ===")
print(df.isnull().sum())
# 결측값 비율 시각화를 위한 계산
missing_ratio = (df.isnull().sum() / len(df) * 100).round(2)
print("\n=== 컬럼별 결측값 비율(%) ===")
print(missing_ratio[missing_ratio > 0])
# 결측값 처리 전략 예시
df_cleaned = df.copy()
# 수치형: 중앙값으로 대체
df_cleaned['Age'].fillna(df['Age'].median(), inplace=True)
# 범주형: 최빈값으로 대체
df_cleaned['Embarked'].fillna(df['Embarked'].mode()[0], inplace=True)
# 너무 많은 결측값: 컬럼 삭제 고려
df_cleaned.drop('Cabin', axis=1, inplace=True)
김개발 씨는 Age 컬럼을 분석하다가 이상한 값을 발견했습니다. 숫자가 있어야 할 자리에 NaN이라고 적혀 있었습니다.
"이게 뭐지? 버그인가?" 박시니어 씨가 다가와 설명했습니다.
"NaN은 Not a Number의 약자예요. 쉽게 말해 빈 칸이에요.
원본 데이터에서 나이 정보가 누락된 승객이 있었던 거죠." 결측값은 왜 생기는 걸까요? 실제 데이터 수집 과정을 생각해 보면 이해가 쉽습니다.
타이타닉 승객 명단을 작성할 때 어떤 승객은 나이를 밝히지 않았을 수 있습니다. 또는 기록 담당자가 바빠서 빠뜨렸을 수도 있습니다.
현실의 데이터는 항상 불완전합니다. **df.isnull().sum()**을 실행하면 각 컬럼의 결측값 개수가 출력됩니다.
타이타닉 데이터에서는 Age에 177개, Cabin에 687개, Embarked에 2개의 결측값이 있습니다. 결측값을 어떻게 처리해야 할까요?
상황에 따라 세 가지 전략이 있습니다. 첫째, **대체(Imputation)**입니다.
빈 칸을 적절한 값으로 채우는 것입니다. 수치형 데이터는 평균이나 중앙값으로, 범주형 데이터는 최빈값으로 대체하는 경우가 많습니다.
Age 컬럼은 중앙값 28세로 채우면 됩니다. 둘째, 삭제입니다.
결측값이 있는 행을 삭제하거나, 결측값이 너무 많은 컬럼 자체를 삭제합니다. Cabin 컬럼은 77%가 결측값이므로 삭제하는 것이 나을 수 있습니다.
셋째, 그대로 두기입니다. 일부 분석이나 모델은 결측값을 자체적으로 처리합니다.
의도적으로 남겨두는 경우도 있습니다. 주의할 점이 있습니다.
결측값 처리는 원본 데이터를 변경하는 작업입니다. 따라서 **df.copy()**로 복사본을 만들어 작업하는 것이 안전합니다.
나중에 다른 처리 방식을 시도해 볼 수도 있기 때문입니다. 김개발 씨가 물었습니다.
"그런데 왜 평균이 아니라 중앙값을 쓰는 거예요?" "좋은 질문이에요. 나이 데이터에 어린이나 노인처럼 극단값이 있으면 평균이 왜곡될 수 있거든요.
중앙값이 더 안정적인 대표값이에요." 결측값 처리 후에는 반드시 **df.isnull().sum()**으로 다시 확인합니다. 모든 값이 0이 되었다면 성공입니다.
실전 팁
💡 - 결측값 비율이 5% 이하면 삭제, 5-30%면 대체, 30% 이상이면 컬럼 삭제를 고려하세요.
- df.dropna()로 결측값이 있는 행을 삭제할 수 있습니다.
- 결측값이 무작위인지 패턴이 있는지도 확인해 보세요. 특정 그룹에만 결측값이 몰려 있을 수 있습니다.
4. 시각화로 데이터 분포 파악하기
숫자만 보던 김개발 씨가 한숨을 쉬었습니다. "평균이 29.7이고 표준편차가 14.5인데, 이게 어떤 모양인지 감이 안 와요." 박시니어 씨가 웃으며 말했습니다.
"그래서 시각화가 필요한 거예요. 백 마디 숫자보다 한 장의 그래프가 낫거든요."
**데이터 시각화(Data Visualization)**는 숫자를 그래프로 변환하여 패턴과 분포를 직관적으로 파악하는 방법입니다. 마치 날씨를 숫자로 "기온 25도, 습도 60%"라고 말하는 것보다 온도계 그림과 구름 아이콘으로 보여주는 것이 한눈에 들어오는 것과 같습니다.
다음 코드를 살펴봅시다.
import matplotlib.pyplot as plt
import seaborn as sns
# 그래프 스타일 설정
plt.style.use('seaborn-v0_8-whitegrid')
fig, axes = plt.subplots(2, 2, figsize=(12, 10))
# 1. 히스토그램: 나이 분포
axes[0, 0].hist(df['Age'].dropna(), bins=30, edgecolor='black')
axes[0, 0].set_title('Age Distribution')
axes[0, 0].set_xlabel('Age')
# 2. 박스플롯: 등급별 요금 분포
sns.boxplot(x='Pclass', y='Fare', data=df, ax=axes[0, 1])
axes[0, 1].set_title('Fare by Passenger Class')
# 3. 막대그래프: 생존 여부
df['Survived'].value_counts().plot(kind='bar', ax=axes[1, 0])
axes[1, 0].set_title('Survival Count')
axes[1, 0].set_xticklabels(['Died', 'Survived'])
# 4. 파이차트: 성별 비율
df['Sex'].value_counts().plot(kind='pie', autopct='%1.1f%%', ax=axes[1, 1])
axes[1, 1].set_title('Gender Distribution')
plt.tight_layout()
plt.savefig('eda_distribution.png', dpi=150)
김개발 씨는 describe() 결과를 한참 들여다봤습니다. 하지만 숫자만으로는 데이터가 어떤 모양인지 상상하기 어려웠습니다.
박시니어 씨가 조언했습니다. "EDA 보고서의 꽃은 시각화예요.
그래프 하나가 숫자 백 개보다 설득력 있거든요." 시각화는 왜 필요할까요? 사람의 뇌는 숫자보다 이미지를 훨씬 빠르게 처리합니다.
"평균 30세, 표준편차 15"라는 숫자를 보면 계산이 필요하지만, 종 모양의 그래프를 보면 단번에 "아, 30세 근처에 사람이 많고 양 끝으로 갈수록 줄어드는구나"라고 이해합니다. EDA에서 가장 많이 쓰이는 그래프 네 가지를 살펴보겠습니다.
첫째, **히스토그램(Histogram)**입니다. 연속적인 수치 데이터의 분포를 보여줍니다.
나이 데이터를 히스토그램으로 그리면 20-30대가 가장 많고, 어린이와 노인은 적다는 것을 한눈에 알 수 있습니다. bins 파라미터로 막대 개수를 조절합니다.
둘째, **박스플롯(Box Plot)**입니다. 데이터의 분포와 이상치를 동시에 보여줍니다.
상자는 25%-75% 범위를, 가운데 선은 중앙값을, 수염 밖의 점들은 이상치를 나타냅니다. 등급별 요금을 비교하면 1등석 요금이 훨씬 높고 변동도 크다는 것을 알 수 있습니다.
셋째, **막대그래프(Bar Chart)**입니다. 범주별 개수나 합계를 비교할 때 사용합니다.
생존자와 사망자 수를 막대그래프로 그리면 불행히도 사망자가 더 많았다는 사실이 명확해집니다. 넷째, **파이차트(Pie Chart)**입니다.
전체 대비 비율을 보여줄 때 효과적입니다. 성별 분포를 파이차트로 그리면 남성이 약 65%로 다수였음을 직관적으로 파악할 수 있습니다.
코드를 살펴보면, matplotlib과 seaborn 두 라이브러리를 함께 사용합니다. matplotlib은 기본적인 그래프 기능을 제공하고, seaborn은 통계적 시각화에 특화되어 더 예쁜 그래프를 쉽게 그릴 수 있습니다.
**plt.subplots(2, 2)**는 2x2 격자로 네 개의 그래프를 한 화면에 배치합니다. 보고서에 넣을 때 이렇게 관련 그래프를 묶으면 보기 좋습니다.
김개발 씨가 그래프를 완성하고 감탄했습니다. "와, 숫자로 볼 때는 몰랐는데 이렇게 보니까 한눈에 들어오네요!" 박시니어 씨가 덧붙였습니다.
"보고서 받는 사람도 바쁜 사람이에요. 그래프로 핵심을 먼저 보여주고, 자세한 숫자는 부록에 넣으면 됩니다."
실전 팁
💡 - plt.savefig()로 그래프를 이미지 파일로 저장하여 보고서에 첨부하세요.
- seaborn의 countplot, distplot은 한 줄로 예쁜 그래프를 그릴 수 있습니다.
- 색맹인 독자를 위해 색상뿐 아니라 패턴으로도 구분되는 그래프를 권장합니다.
5. 상관관계 분석과 히트맵
팀장님이 김개발 씨에게 물었습니다. "나이가 많으면 요금도 비싼 건가요?
변수들 사이에 관계가 있는지 분석해 봤어요?" 김개발 씨는 고민에 빠졌습니다. 두 변수가 서로 영향을 주는지 어떻게 알 수 있을까요?
**상관관계(Correlation)**는 두 변수가 함께 변하는 정도를 나타내는 통계량입니다. 마치 키와 몸무게의 관계처럼, 하나가 증가할 때 다른 하나도 증가하는지, 감소하는지, 아니면 관계가 없는지를 수치로 표현합니다.
-1에서 1 사이의 값을 가지며, 1에 가까울수록 강한 양의 상관관계를 의미합니다.
다음 코드를 살펴봅시다.
import matplotlib.pyplot as plt
import seaborn as sns
# 수치형 컬럼만 선택
numeric_cols = df.select_dtypes(include=[np.number]).columns
df_numeric = df[numeric_cols]
# 상관계수 행렬 계산
correlation_matrix = df_numeric.corr()
print("=== 상관계수 행렬 ===")
print(correlation_matrix.round(2))
# 히트맵으로 시각화
plt.figure(figsize=(10, 8))
sns.heatmap(correlation_matrix,
annot=True, # 숫자 표시
cmap='coolwarm', # 색상 팔레트
center=0, # 0을 기준으로 색상
fmt='.2f') # 소수점 2자리
plt.title('Correlation Heatmap')
plt.tight_layout()
plt.savefig('correlation_heatmap.png', dpi=150)
김개발 씨는 지금까지 각 변수를 개별적으로 살펴봤습니다. 하지만 팀장님의 질문은 달랐습니다.
"변수들 사이에 어떤 관계가 있는지 보여줄 수 있어요?" 박시니어 씨가 설명했습니다. "그건 상관분석으로 확인해요.
두 변수가 같이 움직이는 경향이 있는지 숫자로 보여주는 거예요." 상관관계란 무엇일까요? 쉽게 비유하자면, 아이스크림 판매량과 기온의 관계를 생각해 보세요.
기온이 올라가면 아이스크림 판매량도 늘어납니다. 이런 관계를 양의 상관관계라고 합니다.
반대로 기온이 올라갈 때 핫초코 판매량이 줄어든다면 음의 상관관계입니다. 상관계수는 이 관계의 강도를 -1에서 1 사이의 숫자로 표현합니다.
1에 가까우면 강한 양의 상관관계입니다. 한 변수가 증가할 때 다른 변수도 증가합니다.
-1에 가까우면 강한 음의 상관관계입니다. 한 변수가 증가할 때 다른 변수는 감소합니다.
0에 가까우면 두 변수 사이에 선형적 관계가 없습니다. **df.corr()**은 모든 수치형 컬럼 쌍에 대해 상관계수를 계산합니다.
결과는 정사각형 행렬로, 대각선은 자기 자신과의 상관계수이므로 항상 1입니다. 이 행렬을 숫자로 보면 복잡합니다.
여기서 **히트맵(Heatmap)**이 등장합니다. 히트맵은 숫자를 색상으로 변환하여 보여줍니다.
빨간색은 양의 상관관계, 파란색은 음의 상관관계, 흰색에 가까우면 상관관계가 약합니다. 한눈에 어떤 변수 쌍이 강한 관계를 갖는지 파악할 수 있습니다.
타이타닉 데이터에서 흥미로운 발견이 있습니다. Pclass(등급)와 Fare(요금)는 -0.55의 음의 상관관계를 보입니다.
등급 숫자가 작을수록(1등석) 요금이 높기 때문입니다. 주의할 점이 있습니다.
상관관계는 인과관계가 아닙니다. 아이스크림 판매량과 익사 사고가 양의 상관관계를 보인다고 해서, 아이스크림이 익사를 유발하는 것은 아닙니다.
둘 다 여름에 증가하는 변수일 뿐입니다. 김개발 씨가 히트맵을 완성하고 보고서에 첨부했습니다.
"이거 보니까 어떤 변수가 생존과 관련있는지 힌트가 보이네요!" 박시니어 씨가 고개를 끄덕였습니다. "맞아요.
상관분석은 다음 단계 분석의 방향을 잡아주는 나침반 역할을 해요."
실전 팁
💡 - 상관계수 0.3 미만은 약한 상관, 0.3-0.7은 중간, 0.7 이상은 강한 상관으로 해석합니다.
- annot=True로 각 셀에 숫자를 표시하면 정확한 값을 확인할 수 있습니다.
- 상관관계가 높은 변수들은 머신러닝에서 다중공선성 문제를 일으킬 수 있으니 주의하세요.
6. 그룹별 비교 분석
박시니어 씨가 김개발 씨에게 새로운 과제를 던졌습니다. "단순 통계는 봤으니, 이제 그룹별로 비교해 볼까요?
남자와 여자, 1등석과 3등석, 누가 더 많이 살아남았을까요?" 데이터를 쪼개서 비교하는 방법을 배울 시간입니다.
**그룹별 분석(Group-by Analysis)**은 데이터를 특정 기준으로 나누어 각 그룹의 특성을 비교하는 방법입니다. 마치 학교에서 반별 평균 성적을 비교하는 것처럼, 범주에 따라 데이터를 분리하고 각각의 통계를 산출하여 차이점을 발견합니다.
다음 코드를 살펴봅시다.
# 성별에 따른 생존율 비교
print("=== 성별 생존율 ===")
survival_by_sex = df.groupby('Sex')['Survived'].mean() * 100
print(survival_by_sex.round(2))
# 등급별 평균 요금과 나이
print("\n=== 등급별 통계 ===")
class_stats = df.groupby('Pclass').agg({
'Fare': ['mean', 'median'],
'Age': 'mean',
'Survived': 'mean'
}).round(2)
print(class_stats)
# 복합 그룹 분석: 성별 + 등급
print("\n=== 성별-등급별 생존율 ===")
pivot_survival = pd.pivot_table(df,
values='Survived',
index='Sex',
columns='Pclass',
aggfunc='mean') * 100
print(pivot_survival.round(2))
김개발 씨가 전체 생존율을 계산했습니다. 38.4%였습니다.
하지만 박시니어 씨가 물었습니다. "전체 평균은 알겠는데, 남자와 여자의 생존율이 같았을까요?
1등석 손님과 3등석 손님은요?" 이것이 바로 그룹별 분석의 핵심입니다. 전체 평균에 숨겨진 차이를 발견하는 것입니다.
비유하자면 이렇습니다. "우리 회사 평균 연봉은 5천만 원"이라는 정보만으로는 부족합니다.
경영진 평균이 2억이고 신입 평균이 3천만 원이라면, 상황이 전혀 달라 보입니다. 데이터도 마찬가지입니다.
**groupby()**는 pandas의 핵심 기능 중 하나입니다. 데이터를 지정한 컬럼 기준으로 그룹화하고, 각 그룹에 대해 원하는 연산을 수행합니다.
코드를 살펴보겠습니다. **df.groupby('Sex')['Survived'].mean()**은 성별로 나눈 뒤 각 그룹의 생존율 평균을 계산합니다.
결과는 놀랍습니다. 여성 생존율은 74.2%, 남성은 18.9%입니다.
"여성과 어린이 먼저"라는 당시의 규칙이 데이터에 그대로 드러납니다. 더 복잡한 분석도 가능합니다.
agg() 메서드를 사용하면 여러 통계를 한 번에 계산할 수 있습니다. 등급별로 평균 요금, 중앙값 요금, 평균 나이, 생존율을 동시에 구할 수 있습니다.
**pivot_table()**은 두 개 이상의 변수로 교차 분석할 때 사용합니다. 성별과 등급을 동시에 고려하면 어떨까요?
결과를 보면 1등석 여성의 생존율은 96.8%로 거의 모두 생존했습니다. 반면 3등석 남성의 생존율은 13.5%에 불과합니다.
같은 배에 탔지만, 성별과 등급에 따라 생존 확률이 극명하게 갈렸던 것입니다. 이런 분석은 보고서에서 가장 임팩트 있는 부분이 됩니다.
단순한 숫자 나열이 아니라, 데이터에서 스토리를 발견하는 것이기 때문입니다. 김개발 씨가 감탄했습니다.
"와, 이런 차이가 있는 줄 몰랐어요. 데이터가 역사를 말해주는 것 같네요." 박시니어 씨가 웃었습니다.
"그게 바로 EDA의 매력이에요. 숫자 뒤에 숨겨진 이야기를 찾아내는 거죠."
실전 팁
💡 - groupby 결과를 reset_index()로 데이터프레임으로 변환하면 후속 작업이 편합니다.
- 시각화할 때는 seaborn의 catplot이 그룹별 비교에 최적화되어 있습니다.
- 그룹 크기가 너무 작으면 통계가 신뢰성을 잃으니 최소 30개 이상인지 확인하세요.
7. 이상치 탐지와 처리
데이터를 살펴보던 김개발 씨가 의아한 값을 발견했습니다. 요금이 512달러인 승객이 있었습니다.
다른 승객 요금이 대부분 10-50달러인데 말이죠. "이거 오타 아니에요?" 이상치를 발견한 순간이었습니다.
**이상치(Outlier)**는 다른 데이터와 동떨어진 극단적인 값을 의미합니다. 마치 초등학교 교실에 NBA 농구 선수가 한 명 있는 것처럼, 정상 범위를 벗어난 데이터입니다.
이상치는 분석 결과를 왜곡할 수 있으므로 탐지하고 적절히 처리해야 합니다.
다음 코드를 살펴봅시다.
import matplotlib.pyplot as plt
import seaborn as sns
# IQR 방법으로 이상치 탐지
def detect_outliers_iqr(data, column):
Q1 = data[column].quantile(0.25)
Q3 = data[column].quantile(0.75)
IQR = Q3 - Q1
lower_bound = Q1 - 1.5 * IQR
upper_bound = Q3 + 1.5 * IQR
outliers = data[(data[column] < lower_bound) | (data[column] > upper_bound)]
return outliers, lower_bound, upper_bound
# 요금 컬럼 이상치 확인
fare_outliers, lower, upper = detect_outliers_iqr(df, 'Fare')
print(f"요금 이상치 범위: {lower:.2f} 미만 또는 {upper:.2f} 초과")
print(f"이상치 개수: {len(fare_outliers)}개")
# 박스플롯으로 시각화
fig, axes = plt.subplots(1, 2, figsize=(12, 4))
sns.boxplot(y=df['Fare'], ax=axes[0])
axes[0].set_title('Fare with Outliers')
# 이상치 제거 후
df_no_outliers = df[(df['Fare'] >= lower) & (df['Fare'] <= upper)]
sns.boxplot(y=df_no_outliers['Fare'], ax=axes[1])
axes[1].set_title('Fare without Outliers')
plt.savefig('outlier_comparison.png', dpi=150)
김개발 씨는 요금 데이터를 분석하다가 이상한 점을 발견했습니다. 대부분의 승객이 10-50달러를 지불했는데, 한 승객은 512달러를 지불했습니다.
열 배가 넘는 금액입니다. "선배님, 이거 데이터 오류 아니에요?" 박시니어 씨가 확인해 봤습니다.
"아니요, 오류가 아니에요. 이 승객은 최고급 스위트룸을 예약했거든요.
실제로 존재하는 값이에요. 하지만 분석에 영향을 줄 수 있는 이상치인 건 맞아요." 이상치는 왜 문제가 될까요?
평균을 생각해 보세요. 9명이 각각 10달러씩 지불하고, 1명이 100달러를 지불했다면 평균은 19달러입니다.
하지만 대부분의 사람들은 10달러를 냈습니다. 한 명의 극단값이 평균을 왜곡시킨 것입니다.
이상치를 탐지하는 대표적인 방법이 IQR(Interquartile Range) 방법입니다. IQR은 25% 지점(Q1)과 75% 지점(Q3) 사이의 거리입니다.
데이터의 중간 50%가 얼마나 퍼져 있는지를 나타냅니다. 일반적으로 Q1 - 1.5 x IQR 미만이거나 Q3 + 1.5 x IQR 초과인 값을 이상치로 판단합니다.
코드의 detect_outliers_iqr() 함수가 이 작업을 수행합니다. 타이타닉 요금 데이터에서 약 116개의 이상치가 발견됩니다.
그렇다면 이상치를 무조건 삭제해야 할까요? 그렇지 않습니다.
상황에 따라 다릅니다. 첫째, 이상치가 데이터 입력 오류라면 수정하거나 삭제합니다.
나이가 200세라면 명백한 오류입니다. 둘째, 이상치가 실제 존재하는 극단값이라면 신중해야 합니다.
512달러 요금은 실제로 최고급 객실 가격이었습니다. 이런 경우 삭제하면 정보 손실이 발생합니다.
셋째, 분석 목적에 따라 결정합니다. 전체 요금 분포를 파악하는 것이 목적이라면 이상치를 포함해야 합니다.
하지만 평균 요금으로 예측 모델을 만든다면 이상치 제거를 고려할 수 있습니다. 김개발 씨가 물었습니다.
"그럼 이 데이터에서는 어떻게 해야 해요?" 박시니어 씨가 답했습니다. "보고서에 두 가지 버전을 모두 보여주세요.
이상치 포함 통계와 제외 통계를 나란히 놓으면 독자가 맥락을 이해할 수 있어요."
실전 팁
💡 - 이상치를 삭제하기 전에 왜 그 값이 발생했는지 원인을 파악하세요.
- Z-score 방법도 많이 사용됩니다. 평균에서 표준편차의 3배 이상 떨어진 값을 이상치로 봅니다.
- 이상치를 삭제 대신 다른 값(중앙값 등)으로 대체하는 winsorization 기법도 있습니다.
8. EDA 보고서 구조화와 마무리
김개발 씨가 분석을 마쳤습니다. 히스토그램, 상관분석, 그룹별 비교까지 모든 작업이 끝났습니다.
이제 남은 건 이것들을 보고서로 정리하는 일입니다. "그런데 어떤 순서로 정리해야 할까요?" 마지막 관문이 남았습니다.
EDA 보고서는 분석 결과를 체계적으로 정리하여 전달하는 문서입니다. 마치 탐험가가 여행을 마친 뒤 탐험기를 쓰는 것처럼, 데이터에서 발견한 내용을 독자가 이해하기 쉽게 구조화합니다.
좋은 보고서는 분석 과정과 인사이트를 명확하게 전달합니다.
다음 코드를 살펴봅시다.
# EDA 보고서 자동 생성 스크립트
def generate_eda_report(df, filename='eda_report.md'):
report = []
report.append("# EDA 보고서: 타이타닉 데이터셋\n")
# 1. 데이터 개요
report.append("## 1. 데이터 개요")
report.append(f"- 총 {df.shape[0]}개 행, {df.shape[1]}개 열")
report.append(f"- 수치형 변수: {df.select_dtypes(include=[np.number]).columns.tolist()}")
report.append(f"- 범주형 변수: {df.select_dtypes(include=['object']).columns.tolist()}\n")
# 2. 결측값 현황
report.append("## 2. 결측값 현황")
missing = df.isnull().sum()
for col in missing[missing > 0].index:
report.append(f"- {col}: {missing[col]}개 ({missing[col]/len(df)*100:.1f}%)")
# 3. 핵심 인사이트
report.append("\n## 3. 핵심 인사이트")
report.append(f"- 전체 생존율: {df['Survived'].mean()*100:.1f}%")
report.append(f"- 여성 생존율: {df[df['Sex']=='female']['Survived'].mean()*100:.1f}%")
report.append(f"- 남성 생존율: {df[df['Sex']=='male']['Survived'].mean()*100:.1f}%")
with open(filename, 'w', encoding='utf-8') as f:
f.write('\n'.join(report))
print(f"보고서 생성 완료: {filename}")
generate_eda_report(df)
김개발 씨의 책상에는 여러 그래프와 분석 결과가 흩어져 있었습니다. 히스토그램, 박스플롯, 상관분석 히트맵, 그룹별 통계표.
이제 이것들을 하나의 보고서로 엮어야 했습니다. 박시니어 씨가 조언했습니다.
"보고서 구조가 중요해요. 읽는 사람이 흐름을 따라가며 이해할 수 있도록 구성해야 해요." 좋은 EDA 보고서의 구조는 어떻게 될까요?
첫째, 데이터 개요로 시작합니다. 데이터가 무엇인지, 어디서 왔는지, 행과 열이 몇 개인지, 각 컬럼이 무엇을 의미하는지 설명합니다.
독자가 데이터의 배경을 이해하는 단계입니다. 둘째, 데이터 품질 점검입니다.
결측값이 얼마나 있는지, 이상치는 어떤 것들이 있는지, 데이터 타입은 올바른지 보고합니다. 이 데이터를 신뢰할 수 있는지 판단하는 근거가 됩니다.
셋째, 단변량 분석입니다. 각 변수의 분포를 개별적으로 살펴봅니다.
히스토그램, 박스플롯, 빈도표 등을 활용합니다. 넷째, 이변량 분석입니다.
변수 간의 관계를 탐색합니다. 상관분석, 그룹별 비교, 산점도 등이 여기에 해당합니다.
다섯째, 핵심 인사이트로 마무리합니다. 분석을 통해 발견한 가장 중요한 사실들을 요약합니다.
"여성의 생존율이 남성보다 4배 높았다", "1등석 승객의 생존율이 가장 높았다" 같은 문장으로 정리합니다. 코드를 보면, 보고서 생성을 자동화하는 함수를 만들었습니다.
실무에서는 이런 템플릿을 만들어 두면 반복 작업을 줄일 수 있습니다. 보고서를 작성할 때 주의할 점도 있습니다.
그래프에는 반드시 제목과 축 라벨을 붙입니다. 무엇을 보여주는 그래프인지 한눈에 알 수 있어야 합니다.
숫자만 나열하지 말고 해석을 덧붙입니다. "평균 나이는 29.7세입니다"보다 "평균 나이가 30세 미만으로, 비교적 젊은 승객이 많았습니다"가 더 의미 있습니다.
김개발 씨가 보고서를 완성하고 팀장님께 제출했습니다. 팀장님이 읽고 만족스러운 표정을 지었습니다.
"김개발 씨, 이번 분석 잘했네요. 데이터에서 스토리를 잘 뽑아냈어요." 박시니어 씨가 뒤에서 엄지를 들어 보였습니다.
김개발 씨의 첫 EDA 보고서가 무사히 완성된 순간이었습니다.
실전 팁
💡 - 보고서 맨 앞에 Executive Summary(핵심 요약)를 넣으면 바쁜 독자도 핵심을 빠르게 파악할 수 있습니다.
- Jupyter Notebook을 활용하면 코드와 설명, 시각화를 한 문서에 담을 수 있습니다.
- 보고서는 분석가 본인이 아닌 독자의 관점에서 작성해야 합니다. 전문 용어는 풀어서 설명하세요.
이상으로 학습을 마칩니다. 위 내용을 직접 코드로 작성해보면서 익혀보세요!
댓글 (0)
함께 보면 좋은 카드 뉴스
Helm 마이크로서비스 패키징 완벽 가이드
Kubernetes 환경에서 마이크로서비스를 효율적으로 패키징하고 배포하는 Helm의 핵심 기능을 실무 중심으로 학습합니다. Chart 생성부터 릴리스 관리까지 체계적으로 다룹니다.
보안 아키텍처 구성 완벽 가이드
프로젝트의 보안을 처음부터 설계하는 방법을 배웁니다. AWS 환경에서 VPC부터 WAF, 암호화, 접근 제어까지 실무에서 바로 적용할 수 있는 보안 아키텍처를 단계별로 구성해봅니다.
AWS Organizations 완벽 가이드
여러 AWS 계정을 체계적으로 관리하고 통합 결제와 보안 정책을 적용하는 방법을 실무 스토리로 쉽게 배워봅니다. 초보 개발자도 바로 이해할 수 있는 친절한 설명과 실전 예제를 제공합니다.
AWS KMS 암호화 완벽 가이드
AWS KMS(Key Management Service)를 활용한 클라우드 데이터 암호화 방법을 초급 개발자를 위해 쉽게 설명합니다. CMK 생성부터 S3, EBS 암호화, 봉투 암호화까지 실무에 필요한 모든 내용을 담았습니다.
AWS Secrets Manager 완벽 가이드
AWS에서 데이터베이스 비밀번호, API 키 등 민감한 정보를 안전하게 관리하는 Secrets Manager의 핵심 개념과 실무 활용법을 배워봅니다. 초급 개발자도 쉽게 따라할 수 있도록 실전 예제와 함께 설명합니다.