25. 실전 프로젝트 - 데이터 분석 종합 실습

25. 실전 프로젝트 - 데이터 분석 종합 실습

이전: 실험 설계

개요

실제 데이터셋을 활용한 종합 EDA 실습입니다. 데이터 로딩부터 인사이트 도출까지 전체 분석 과정을 다룹니다.


프로젝트 1: Titanic 생존 분석

1.1 데이터 로딩 및 개요

import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
import seaborn as sns

# Seaborn 내장 데이터셋 로드
titanic = sns.load_dataset('titanic')

# 데이터 개요
print("="*50)
print("데이터 기본 정보")
print("="*50)
print(f"행 수: {len(titanic)}")
print(f"열 수: {len(titanic.columns)}")
print(f"\n컬럼 목록:\n{titanic.columns.tolist()}")
print(f"\n데이터 타입:\n{titanic.dtypes}")

# 처음 5행
print("\n" + "="*50)
print("데이터 미리보기")
print("="*50)
print(titanic.head())

1.2 결측치 분석

print("="*50)
print("결측치 분석")
print("="*50)

# 결측치 현황
missing = titanic.isnull().sum()
missing_pct = (missing / len(titanic) * 100).round(2)
missing_df = pd.DataFrame({
    '결측치 수': missing,
    '결측치 비율(%)': missing_pct
}).sort_values('결측치 비율(%)', ascending=False)

print(missing_df[missing_df['결측치 수'] > 0])

# 시각화
fig, ax = plt.subplots(figsize=(10, 6))
missing_cols = missing_df[missing_df['결측치 수'] > 0].index
missing_vals = missing_df.loc[missing_cols, '결측치 비율(%)']
ax.barh(missing_cols, missing_vals, color='coral')
ax.set_xlabel('결측치 비율 (%)')
ax.set_title('결측치 현황')
for i, v in enumerate(missing_vals):
    ax.text(v + 0.5, i, f'{v}%', va='center')
plt.tight_layout()
plt.show()

1.3 타겟 변수 분석

print("="*50)
print("타겟 변수 (생존 여부) 분석")
print("="*50)

# 생존율
survival_rate = titanic['survived'].value_counts(normalize=True)
print(f"생존율: {survival_rate[1]:.1%}")
print(f"사망율: {survival_rate[0]:.1%}")

# 시각화
fig, axes = plt.subplots(1, 2, figsize=(12, 5))

# 빈도
titanic['survived'].value_counts().plot(kind='bar', ax=axes[0],
                                         color=['coral', 'steelblue'])
axes[0].set_title('생존 여부 빈도')
axes[0].set_xticklabels(['사망', '생존'], rotation=0)
axes[0].set_ylabel('인원 수')

# 비율
titanic['survived'].value_counts().plot(kind='pie', ax=axes[1],
                                         autopct='%1.1f%%',
                                         colors=['coral', 'steelblue'],
                                         labels=['사망', '생존'])
axes[1].set_title('생존 여부 비율')
axes[1].set_ylabel('')

plt.tight_layout()
plt.show()

1.4 범주형 변수별 생존율

print("="*50)
print("범주형 변수별 생존율")
print("="*50)

categorical_vars = ['sex', 'pclass', 'embarked', 'alone']

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

for ax, var in zip(axes.flat, categorical_vars):
    survival_by_var = titanic.groupby(var)['survived'].mean().sort_values(ascending=False)
    survival_by_var.plot(kind='bar', ax=ax, color='steelblue', edgecolor='black')
    ax.set_title(f'{var}별 생존율')
    ax.set_ylabel('생존율')
    ax.set_ylim(0, 1)
    ax.tick_params(axis='x', rotation=45)

    # 값 표시
    for i, v in enumerate(survival_by_var):
        ax.text(i, v + 0.02, f'{v:.1%}', ha='center', va='bottom')

plt.tight_layout()
plt.show()

# 통계 요약
print("\n성별 생존율:")
print(titanic.groupby('sex')['survived'].agg(['mean', 'count']))

print("\n객실 등급별 생존율:")
print(titanic.groupby('pclass')['survived'].agg(['mean', 'count']))

1.5 수치형 변수 분석

print("="*50)
print("수치형 변수 분석")
print("="*50)

numeric_vars = ['age', 'fare']

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

for i, var in enumerate(numeric_vars):
    # 히스토그램 (생존 여부별)
    for survived, label, color in [(0, '사망', 'coral'), (1, '생존', 'steelblue')]:
        data = titanic[titanic['survived'] == survived][var].dropna()
        axes[i, 0].hist(data, bins=30, alpha=0.6, label=label, color=color)
    axes[i, 0].set_title(f'{var} 분포 (생존 여부별)')
    axes[i, 0].set_xlabel(var)
    axes[i, 0].legend()

    # 박스플롯
    titanic.boxplot(column=var, by='survived', ax=axes[i, 1])
    axes[i, 1].set_title(f'{var} (생존 여부별)')
    axes[i, 1].set_xlabel('생존 여부')

plt.suptitle('')
plt.tight_layout()
plt.show()

# 통계 요약
print("\n나이별 생존 통계:")
print(titanic.groupby('survived')['age'].describe())

1.6 다변량 분석

print("="*50)
print("다변량 분석")
print("="*50)

# 성별 & 객실 등급별 생존율
pivot = pd.pivot_table(titanic, values='survived',
                       index='pclass', columns='sex', aggfunc='mean')
print("성별 & 객실 등급별 생존율:")
print(pivot)

# 히트맵
fig, ax = plt.subplots(figsize=(8, 6))
sns.heatmap(pivot, annot=True, cmap='RdYlGn', fmt='.1%',
            vmin=0, vmax=1, ax=ax)
ax.set_title('성별 & 객실 등급별 생존율')
plt.show()

# 나이 그룹 생성
titanic['age_group'] = pd.cut(titanic['age'],
                              bins=[0, 12, 18, 35, 60, 100],
                              labels=['어린이', '청소년', '청년', '중년', '노년'])

# 나이 그룹별 생존율
age_survival = titanic.groupby('age_group')['survived'].mean()
print("\n나이 그룹별 생존율:")
print(age_survival)

1.7 통계 검정

from scipy import stats

print("="*50)
print("통계 검정")
print("="*50)

# 성별에 따른 생존율 차이 (카이제곱 검정)
contingency = pd.crosstab(titanic['sex'], titanic['survived'])
chi2, p_value, dof, expected = stats.chi2_contingency(contingency)
print(f"\n성별-생존 카이제곱 검정:")
print(f"χ² = {chi2:.4f}, p-value = {p_value:.4f}")

# 생존 여부에 따른 나이 차이 (t-검정)
survived_age = titanic[titanic['survived'] == 1]['age'].dropna()
died_age = titanic[titanic['survived'] == 0]['age'].dropna()

stat, p_value = stats.ttest_ind(survived_age, died_age)
print(f"\n나이-생존 t-검정:")
print(f"t = {stat:.4f}, p-value = {p_value:.4f}")
print(f"생존자 평균 나이: {survived_age.mean():.1f}")
print(f"사망자 평균 나이: {died_age.mean():.1f}")

1.8 인사이트 정리

print("="*50)
print("주요 인사이트")
print("="*50)

insights = """
1. 전체 생존율: 약 38%

2. 성별:
   - 여성 생존율(74%)이 남성(19%)보다 현저히 높음
   - "여성과 아이 먼저" 원칙의 영향

3. 객실 등급:
   - 1등석(63%) > 2등석(47%) > 3등석(24%)
   - 상위 등급일수록 생존율 높음

4. 나이:
   - 어린이 생존율이 가장 높음
   - 생존자 평균 나이가 사망자보다 약간 낮음

5. 동반자:
   - 혼자 탑승한 승객의 생존율이 낮음

6. 운임:
   - 높은 운임을 지불한 승객의 생존율이 높음
   - (객실 등급과 상관관계)
"""
print(insights)

프로젝트 2: 팁 데이터 분석

2.1 데이터 탐색

tips = sns.load_dataset('tips')

print("="*50)
print("Tips 데이터셋 개요")
print("="*50)
print(tips.info())
print("\n기술 통계:")
print(tips.describe())

2.2 팁 금액 분석

# 팁 비율 계산
tips['tip_pct'] = tips['tip'] / tips['total_bill'] * 100

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 팁 금액 분포
axes[0, 0].hist(tips['tip'], bins=30, edgecolor='black', alpha=0.7)
axes[0, 0].axvline(tips['tip'].mean(), color='red', linestyle='--',
                   label=f'평균: ${tips["tip"].mean():.2f}')
axes[0, 0].set_title('팁 금액 분포')
axes[0, 0].set_xlabel('팁 ($)')
axes[0, 0].legend()

# 팁 비율 분포
axes[0, 1].hist(tips['tip_pct'], bins=30, edgecolor='black', alpha=0.7)
axes[0, 1].axvline(tips['tip_pct'].mean(), color='red', linestyle='--',
                   label=f'평균: {tips["tip_pct"].mean():.1f}%')
axes[0, 1].set_title('팁 비율 분포')
axes[0, 1].set_xlabel('팁 비율 (%)')
axes[0, 1].legend()

# 요일별 팁
tips.groupby('day')['tip'].mean().plot(kind='bar', ax=axes[1, 0],
                                        color='steelblue', edgecolor='black')
axes[1, 0].set_title('요일별 평균 팁')
axes[1, 0].set_ylabel('평균 팁 ($)')
axes[1, 0].tick_params(axis='x', rotation=45)

# 시간대별 팁
tips.groupby('time')['tip'].mean().plot(kind='bar', ax=axes[1, 1],
                                         color='coral', edgecolor='black')
axes[1, 1].set_title('시간대별 평균 팁')
axes[1, 1].set_ylabel('평균 팁 ($)')
axes[1, 1].tick_params(axis='x', rotation=0)

plt.tight_layout()
plt.show()

print(f"평균 팁: ${tips['tip'].mean():.2f}")
print(f"평균 팁 비율: {tips['tip_pct'].mean():.1f}%")

2.3 청구 금액과 팁의 관계

fig, axes = plt.subplots(1, 2, figsize=(14, 5))

# 산점도
sns.scatterplot(data=tips, x='total_bill', y='tip', hue='time',
                size='size', ax=axes[0])
axes[0].set_title('청구 금액 vs 팁')

# 회귀선
sns.regplot(data=tips, x='total_bill', y='tip', ax=axes[1],
            scatter_kws={'alpha': 0.5})
axes[1].set_title('청구 금액 vs 팁 (회귀선)')

plt.tight_layout()
plt.show()

# 상관계수
corr, p_value = stats.pearsonr(tips['total_bill'], tips['tip'])
print(f"상관계수: {corr:.4f} (p-value: {p_value:.4f})")

2.4 그룹별 비교

fig, axes = plt.subplots(2, 2, figsize=(14, 10))

# 성별
sns.boxplot(data=tips, x='sex', y='tip_pct', ax=axes[0, 0])
axes[0, 0].set_title('성별 팁 비율')

# 흡연 여부
sns.boxplot(data=tips, x='smoker', y='tip_pct', ax=axes[0, 1])
axes[0, 1].set_title('흡연 여부별 팁 비율')

# 요일
sns.boxplot(data=tips, x='day', y='tip_pct', ax=axes[1, 0])
axes[1, 0].set_title('요일별 팁 비율')

# 인원수
sns.boxplot(data=tips, x='size', y='tip_pct', ax=axes[1, 1])
axes[1, 1].set_title('인원수별 팁 비율')

plt.tight_layout()
plt.show()

# 통계 검정: 성별 차이
male_tip = tips[tips['sex'] == 'Male']['tip_pct']
female_tip = tips[tips['sex'] == 'Female']['tip_pct']
stat, p_value = stats.ttest_ind(male_tip, female_tip)
print(f"\n성별 팁 비율 t-검정: t={stat:.4f}, p={p_value:.4f}")

프로젝트 3: 분석 보고서 템플릿

def generate_eda_report(df, target=None):
    """
    EDA 보고서 자동 생성 함수

    Parameters:
    -----------
    df : DataFrame
        분석할 데이터프레임
    target : str, optional
        타겟 변수명
    """
    print("="*60)
    print("           탐색적 데이터 분석 (EDA) 보고서")
    print("="*60)

    # 1. 기본 정보
    print("\n1. 데이터 기본 정보")
    print("-"*40)
    print(f"   행 수: {len(df):,}")
    print(f"   열 수: {len(df.columns)}")
    print(f"   메모리 사용량: {df.memory_usage(deep=True).sum() / 1024**2:.2f} MB")

    # 2. 데이터 타입
    print("\n2. 데이터 타입 요약")
    print("-"*40)
    dtype_counts = df.dtypes.value_counts()
    for dtype, count in dtype_counts.items():
        print(f"   {dtype}: {count}개")

    # 3. 결측치
    print("\n3. 결측치 현황")
    print("-"*40)
    missing = df.isnull().sum()
    missing_pct = (missing / len(df) * 100)
    for col, (cnt, pct) in zip(df.columns, zip(missing, missing_pct)):
        if cnt > 0:
            print(f"   {col}: {cnt}개 ({pct:.1f}%)")
    if missing.sum() == 0:
        print("   결측치 없음")

    # 4. 수치형 변수
    print("\n4. 수치형 변수 통계")
    print("-"*40)
    numeric_cols = df.select_dtypes(include=[np.number]).columns
    if len(numeric_cols) > 0:
        for col in numeric_cols[:5]:  # 상위 5개만
            print(f"\n   [{col}]")
            print(f"   평균: {df[col].mean():.2f}, 중앙값: {df[col].median():.2f}")
            print(f"   표준편차: {df[col].std():.2f}")
            print(f"   범위: [{df[col].min():.2f}, {df[col].max():.2f}]")

    # 5. 범주형 변수
    print("\n5. 범주형 변수 요약")
    print("-"*40)
    categorical_cols = df.select_dtypes(include=['object', 'category']).columns
    for col in categorical_cols[:5]:  # 상위 5개만
        print(f"\n   [{col}]")
        print(f"   고유값 수: {df[col].nunique()}")
        print(f"   최빈값: {df[col].mode().values[0]}")

    # 6. 타겟 변수 (있는 경우)
    if target and target in df.columns:
        print(f"\n6. 타겟 변수 ({target}) 분석")
        print("-"*40)
        if df[target].dtype in ['int64', 'float64']:
            print(f"   평균: {df[target].mean():.2f}")
            print(f"   분포: 연속형")
        else:
            print(f"   클래스 분포:")
            for val, cnt in df[target].value_counts().items():
                print(f"   - {val}: {cnt} ({cnt/len(df)*100:.1f}%)")

    print("\n" + "="*60)
    print("                     보고서 끝")
    print("="*60)

# 사용 예시
# generate_eda_report(titanic, target='survived')

분석 체크리스트

## 데이터 분석 체크리스트

### 1단계: 데이터 이해
- [ ] 데이터 출처와 수집 방법 확인
- [ ] 각 변수의 의미 파악
- [ ] 비즈니스 문맥 이해

### 2단계: 데이터 품질 확인
- [ ] 결측치 확인 및 처리 계획
- [ ] 이상치 탐지
- [ ] 데이터 타입 확인
- [ ] 중복 데이터 확인

### 3단계: 단변량 분석
- [ ] 수치형 변수 분포 확인
- [ ] 범주형 변수 빈도 확인
- [ ] 기술통계량 계산

### 4단계: 이변량/다변량 분석
- [ ] 변수 간 상관관계 분석
- [ ] 그룹별 비교 분석
- [ ] 타겟 변수와의 관계 분석

### 5단계: 통계 검정
- [ ] 적절한 검정 방법 선택
- [ ] 가정 검증
- [ ] 결과 해석

### 6단계: 인사이트 도출
- [ ] 주요 발견 정리
- [ ] 비즈니스 의미 해석
- [ ] 추가 분석 제안

요약

단계 주요 작업 도구/함수
데이터 로딩 CSV/Excel/DB 로드 pd.read_*()
개요 파악 형태, 타입 확인 info(), describe()
결측치 확인 및 처리 isna(), fillna()
단변량 분포, 빈도 분석 histplot(), countplot()
이변량 관계 분석 scatterplot(), boxplot()
다변량 패턴 발견 heatmap(), pairplot()
통계 검정 유의성 검정 scipy.stats
인사이트 결과 정리 마크다운 보고서
to navigate between lessons