AI 헬스케어 초격차 캠프

과정별 2주 완성,
AI 헬스케어
커리큘럼

파이썬 기초부터 데이터 분석, 데이터베이스까지 — 각 과정마다 2주 커리큘럼으로 의료 데이터를 다루는 실력을 키워요.

커리큘럼 보기 →
5
학습 과정
14
인터랙티브 가이드
8주
전체 커리큘럼

학습 로드맵

클릭하면 해당 과정으로 이동해요

과정별 커리큘럼

각 카드와 Day를 클릭하면 인터랙티브 가이드가 열려요
Step 01

Python 기초

2주 · 14일
Week 1파이썬 기초 문법
Week 2파이썬 심화 & 날개 달기
Step 02

수학 & 통계

2주 · 14일
Week 1수학 기초 & NumPy
Week 2의료 AI 기초 통계
Step 03

데이터 분석

2주 · 14일
Week 1pandas 기초 & EDA
Week 2전처리 & 시각화
Step 04

데이터베이스

2주 · 14일
Week 1RDBMS · SQL · SQLAlchemy
Week 2SQLAlchemy 심화 · NoSQL · MongoDB
프로젝트Step 04 종합 실전 프로젝트
Step 05

머신러닝

준비중
Project

실전 프로젝트

5일차 · Step 04 수료 후

📚 참고 가이드

프로젝트와 함께 보면 좋은 자료
Project 2

흡연 여부 데이터 분석

데이터 분석 & 의료 가설 검증

왜 이 사이트를
만들었나요?

AI 헬스케어를 공부하고 싶은데 어디서부터 시작해야 할지 막막한 분들을 위해 만들었어요.

딱딱한 이론 대신, 실제 의료 데이터 예시와 인터랙티브 가이드로 직접 손을 움직이며 배울 수 있도록 구성했어요.

학습 가이드
의료 데이터에서 "이상치 = 제거 대상"이라는 공식은 틀렸어요. 이상치가 환자의 긴급 상황일 수 있어요. 항상 원인을 먼저 파악한 후 처리 방법을 결정하세요.
CSV 데이터 분석 전체 흐름 — 5단계 의료 데이터 분석 표준 순서
데이터를 받았을 때 어떤 순서로 접근해야 하는지 정리했어요. 순서를 지키는 것 자체가 분석의 신뢰도를 높여줘요.
단계명칭주요 코드의료적 의미
1데이터 파악df.shape / df.dtypes / df.describe()몇 명 환자? 어떤 지표? 정상 범위인가?
2품질 점검df.isnull().sum() / df.duplicated()측정 누락, 중복 기록 탐지
3분포 시각화df.hist() / sns.boxplot()수치 범위, 이상치, 왜도 확인
4가설 설정 & 검증stats.ttest_ind() / pearsonr()"Running이 심박수에 영향 주는가?"
5결과 해석 & 보고data_quality_report() / 시각화임상적 의미, 한계점 명시
기초 탐색 코드 — 데이터를 처음 받았을 때
import pandas as pd import numpy as np df = pd.read_csv('biometric_wearable_dataset.csv') # ① 전체 구조 파악 print(f"행: {df.shape[0]:,} / 열: {df.shape[1]}") print(df.dtypes) # ② 기술통계 (숫자형) df.describe().round(2) # ③ 결측치 비율 missing = df.isnull().sum() / len(df) * 100 print(missing[missing > 0]) # ④ 범주형 분포 print(df['activity'].value_counts()) print(df['is_tampered'].value_counts(normalize=True).mul(100).round(1))
항상 describe()를 먼저 실행하세요. heart_rate 최솟값이 0이거나 blood_oxygen이 150%라면 센서 오류예요. 분석 전에 반드시 이상 값을 잡아야 해요.
시각화 ① — 히스토그램 (분포 파악) 용도: 데이터 분포 전체 이해
언제 쓰나: 수치형 변수 하나의 분포를 볼 때. "심박수가 대부분 얼마 범위에 있는가?" 에 답해요.
import matplotlib.pyplot as plt import seaborn as sns df_clean = df[df['is_tampered'] == 0] fig, axes = plt.subplots(1, 3, figsize=(15, 4)) # 심박수 분포 axes[0].hist(df_clean['heart_rate'], bins=40, color='#1d6e56', edgecolor='white', alpha=0.85) axes[0].axvline(150, color='red', linestyle='--', label='위험 기준 150bpm') axes[0].set_title('심박수 분포'); axes[0].legend() # SpO₂ 분포 axes[1].hist(df_clean['blood_oxygen'], bins=30, color='#3b82f6', edgecolor='white', alpha=0.85) axes[1].axvline(94, color='red', linestyle='--', label='저산소 기준 94%') axes[1].set_title('산소포화도 분포'); axes[1].legend() # 체온 분포 axes[2].hist(df_clean['body_temp'], bins=30, color='#f97316', edgecolor='white', alpha=0.85) axes[2].axvline(38.0, color='red', linestyle='--', label='발열 기준 38°C') axes[2].set_title('체온 분포'); axes[2].legend() plt.suptitle('웨어러블 생체 지표 분포 (센서 오류 제외)', fontsize=14) plt.tight_layout(); plt.show()
이 시각화가 보여주는 것: 심박수가 정규분포를 따르는지, 왼쪽/오른쪽으로 치우쳤는지(왜도)를 한눈에 파악해요. 빨간 점선 기준 오른쪽이 위험군이에요.
의학적 인사이트: 심박수 분포가 오른쪽 꼬리(Right skew)라면 격렬한 운동 데이터가 많다는 의미예요. 정상 안정 심박수(60~100bpm) 구간에 데이터가 집중돼야 정상 생활 패턴이에요.
시각화 ② — 박스플롯 (그룹 비교 + 이상치) 용도: 활동별 분포 비교
언제 쓰나: 그룹 간 수치 비교 + 이상치 탐지를 동시에 할 때. "활동 종류에 따라 심박수가 다른가?"
fig, axes = plt.subplots(1, 2, figsize=(14, 5)) # 활동별 심박수 박스플롯 activity_order = ['Resting', 'Walking', 'Cycling', 'Running'] sns.boxplot(data=df_clean, x='activity', y='heart_rate', order=activity_order, palette='Set2', ax=axes[0]) axes[0].axhline(150, color='red', linestyle='--', alpha=0.7, label='위험 150bpm') axes[0].set_title('활동별 심박수 분포'); axes[0].legend() axes[0].set_xlabel('활동'); axes[0].set_ylabel('심박수 (bpm)') # 활동별 산소포화도 박스플롯 sns.boxplot(data=df_clean, x='activity', y='blood_oxygen', order=activity_order, palette='Blues', ax=axes[1]) axes[1].axhline(94, color='red', linestyle='--', alpha=0.7, label='저산소 94%') axes[1].set_title('활동별 산소포화도 분포'); axes[1].legend() plt.tight_layout(); plt.show()
이 시각화가 보여주는 것: 박스 안의 선이 중앙값, 박스 상하가 Q1~Q3 (IQR), 점들이 이상치예요. Running의 중앙값이 Resting 대비 얼마나 높은지 직관적으로 비교돼요.
의학적 인사이트: Running에서 심박수 이상치(점)가 많으면 격렬한 운동 중 심폐 과부하가 발생했다는 신호예요. Resting에서 SpO₂가 낮다면 수면 무호흡 의심 가능성이 있어요.
시각화 ③ — 히트맵 (상관관계) 용도: 변수 간 연관성 전체 파악
언제 쓰나: 여러 수치형 변수 간의 상관관계를 한 번에 볼 때. "심박수가 오르면 호흡수도 같이 오르는가?"
numeric_cols = ['heart_rate', 'blood_oxygen', 'body_temp', 'resp_rate', 'step_count'] corr = df_clean[numeric_cols].corr() plt.figure(figsize=(8, 6)) sns.heatmap(corr, annot=True, fmt='.2f', cmap='RdYlGn', vmin=-1, vmax=1, square=True, linewidths=0.5) plt.title('생체 지표 간 상관관계', fontsize=14) plt.tight_layout(); plt.show() # 특정 변수와 상관이 높은 것 정렬 print(corr['heart_rate'].sort_values(ascending=False))
이 시각화가 보여주는 것: 초록(양의 상관), 빨강(음의 상관). heart_rate↑ ↔ resp_rate↑ 면 심폐 연동 반응이에요.
의학적 인사이트: heart_rate와 step_count의 강한 양의 상관(r≥0.7)은 웨어러블 데이터가 신뢰성 있게 측정됐다는 증거예요. blood_oxygen과 heart_rate의 음의 상관(r≤-0.3)은 빈맥 시 산소 공급 효율이 떨어진다는 생리학적 패턴이에요.
시각화 ④ — 시계열 라인 차트 용도: 시간에 따른 변화 패턴
언제 쓰나: timestamp가 있을 때. "하루 중 어느 시간대에 심박수가 가장 높은가?"
df_clean['timestamp'] = pd.to_datetime(df_clean['timestamp']) df_clean['hour'] = df_clean['timestamp'].dt.hour # 시간대별 평균 집계 hourly = df_clean.groupby('hour').agg( avg_hr = ('heart_rate', 'mean'), avg_spo2 = ('blood_oxygen', 'mean'), avg_temp = ('body_temp', 'mean'), ).round(2) fig, ax1 = plt.subplots(figsize=(12, 5)) ax2 = ax1.twinx() ax1.plot(hourly.index, hourly['avg_hr'], 'g-o', linewidth=2, label='평균 심박수') ax1.fill_between(hourly.index, hourly['avg_hr'], alpha=0.15, color='green') ax2.plot(hourly.index, hourly['avg_spo2'], 'b--s', linewidth=2, label='평균 SpO₂') ax1.set_xlabel('시간대'); ax1.set_ylabel('심박수 (bpm)', color='green') ax2.set_ylabel('산소포화도 (%)', color='blue') plt.title('24시간 시간대별 생체 지표 변화', fontsize=14) fig.legend(loc='upper right') plt.tight_layout(); plt.show()
이 시각화가 보여주는 것: 인체의 24시간 생체 리듬(Circadian Rhythm)을 데이터로 증명해요.
의학적 인사이트: 오전 6~9시와 오후 5~8시에 심박수가 피크를 이루면 출퇴근/운동 시간대로 해석돼요. 새벽 0~5시에 SpO₂가 낮아지는 패턴은 수면 무호흡의 생리학적 증거로 볼 수 있어요. 이 패턴이 임상적으로 의미 있으려면 의사의 검토가 필요해요.
시각화 ⑤ — 산점도 + 추세선 용도: 두 변수의 관계 시각화
언제 쓰나: 두 수치형 변수의 선형 관계를 확인할 때. "걸음수가 많을수록 심박수가 정말 높아지는가?"
fig, axes = plt.subplots(1, 2, figsize=(13, 5)) # 걸음수 vs 심박수 sns.scatterplot(data=df_clean.sample(500), x='step_count', y='heart_rate', hue='activity', alpha=0.6, ax=axes[0], palette='Set1') # 추세선 추가 m, b = np.polyfit(df_clean['step_count'], df_clean['heart_rate'], 1) x_range = np.linspace(df_clean['step_count'].min(), df_clean['step_count'].max(), 100) axes[0].plot(x_range, m*x_range+b, 'k--', linewidth=2, label='추세선') axes[0].set_title('걸음수 vs 심박수'); axes[0].legend() # 호흡수 vs 산소포화도 sns.scatterplot(data=df_clean.sample(500), x='resp_rate', y='blood_oxygen', hue='activity', alpha=0.6, ax=axes[1], palette='Set2') axes[1].axhline(94, color='red', linestyle='--', alpha=0.7) axes[1].set_title('호흡수 vs 산소포화도') plt.tight_layout(); plt.show()
의학적 인사이트: 걸음수-심박수 양의 상관은 유산소 운동 효과를 증명해요. 호흡수 증가 시 SpO₂가 낮아지는 패턴은 과호흡(Hyperventilation) 의심 신호예요. Running 군에서 점들이 경계선(94%) 아래에 몰려있다면 격렬한 운동 중 산소 요구량 초과를 나타내요.
의료 가설 검증 — t-검정 실전 코드
통계적 가설 검증은 "차이가 있다"는 주장의 근거를 수치로 만드는 과정이에요. p-value < 0.05이면 우연이 아닌 실제 차이로 해석해요.
from scipy import stats # 가설 1: "Running은 Resting보다 심박수가 유의미하게 높다" running = df_clean[df_clean['activity']=='Running']['heart_rate'] resting = df_clean[df_clean['activity']=='Resting']['heart_rate'] t, p = stats.ttest_ind(running, resting) print(f"Running 평균: {running.mean():.1f} bpm") print(f"Resting 평균: {resting.mean():.1f} bpm") print(f"t-통계량: {t:.3f}, p-value: {p:.6f}") print("결론:", "통계적으로 유의미한 차이" if p < 0.05 else "차이 없음") # 가설 2: "체온이 높을수록 심박수도 높다" (피어슨 상관) r, p_corr = stats.pearsonr(df_clean['body_temp'], df_clean['heart_rate']) print(f"\n체온-심박수 상관계수: r={r:.3f}, p={p_corr:.4f}") # |r| 해석: 0.1=약함, 0.3=중간, 0.5+=강함 # 가설 3: "센서 오류(is_tampered=1)는 특정 활동에 더 많이 발생하는가?" (카이제곱) ct = pd.crosstab(df['activity'], df['is_tampered']) chi2, p_chi, dof, _ = stats.chi2_contingency(ct) print(f"\n카이제곱: {chi2:.3f}, p={p_chi:.4f}") print("결론:", "활동과 센서 오류 연관 있음" if p_chi < 0.05 else "연관 없음")
복합 위험군 시각화 — 의료 인사이트 도출
# 위험 등급 분류 def classify_risk(row): score = 0 if row['heart_rate'] > 150: score += 2 if row['blood_oxygen'] < 94: score += 3 # SpO₂ 가중치 높음 if row['body_temp'] > 38.5: score += 1 if row['resp_rate'] > 20: score += 1 if score >= 4: return '🔴 위험' elif score >= 2: return '🟡 주의' else: return '🟢 정상' df_clean['risk_level'] = df_clean.apply(classify_risk, axis=1) # 활동별 위험등급 스택 바 차트 risk_pivot = pd.crosstab(df_clean['activity'], df_clean['risk_level'], normalize='index') * 100 risk_pivot.plot(kind='bar', stacked=True, figsize=(10, 5), color=['#ef4444', '#22c55e', '#eab308']) plt.title('활동별 위험 등급 비율 (%)', fontsize=14) plt.ylabel('비율 (%)'); plt.xlabel('활동') plt.xticks(rotation=0); plt.legend(loc='upper right') plt.tight_layout(); plt.show() # 위험등급 분포 출력 print(df_clean['risk_level'].value_counts())
이 시각화가 보여주는 것: 어떤 활동에서 위험 등급이 집중되는지 한눈에 보여요.
의학적 인사이트: Running의 🔴 위험 비율이 높다고 해서 "Running이 위험하다"고 결론 내리면 안 돼요. 격렬한 운동 중 일시적인 생리적 반응일 수 있어요. Resting에서 🔴 위험이 발생하면 그게 진짜 임상적 위험 신호예요. 맥락(Context)이 핵심이에요.
분석 결과 해석 체크리스트
분석 항목시각화 방법의학적 해석 포인트
생체 지표 분포히스토그램정상 범위(60~100bpm) 비율, 위험 구간 비율
활동별 차이박스플롯중앙값 차이, 이상치 위치, IQR 너비
변수 간 관계히트맵 / 산점도r>0.5 강한 상관, 임상적 의미 설명
24시간 패턴라인 차트서카디안 리듬, 수면 시간대 이상 여부
위험군 분포스택 바 차트활동별 위험 비율, Resting 위험이 핵심
가설 검증p-value 수치p<0.05: 유의미, 상관≠인과 항상 명시
모든 분석 결과에는 반드시 한계점을 명시하세요: ① 표본 편향 ② 교란변수 ③ 센서 오류 가능성 ④ 임상적 검증 필요. "데이터상 패턴이 발견됨"과 "의학적으로 확정된 사실"은 완전히 달라요.