데이터 동아리 DF(Data First) 봄 단기 프로젝트(머신러닝 기초 7주 과정)의 최종 산출물입니다. 매주 배운 내용을 따로 연습하는 데서 그치지 않고, 7주치를 하나의 문제에 합쳐서 적용해보는 것이 목표였습니다.
| 노트북 | 문제 | 데이터 |
|---|---|---|
DF_Classification_Project.ipynb |
이진 분류 | 심장병 진단 |
DF_Regression_Project.ipynb |
회귀 | 주택 가격 예측 |
| 주차 | 배운 내용 | 이 프로젝트에서 |
|---|---|---|
| 1–2주 | NumPy · pandas | 전처리 · EDA |
| 3주 | 교차검증 · GridSearchCV · 인코딩 · 스케일링 | StratifiedKFold · GridSearchCV · LabelEncoder / OneHotEncoder |
| 4주 | 오차행렬 · 정밀도/재현율 · threshold · ROC-AUC | 지표 5종 동시 평가 · Binarizer로 임계값 조정 |
| 5주 | 결정트리 · 랜덤포레스트 · GBM · XGBoost · LightGBM · 앙상블(Voting) | 4개 모델 비교 → 튜닝된 3개로 Soft Voting |
| 6주 | 선형회귀 · 경사하강법 · 규제 | 회귀 노트북 |
| 7주 | PCA · K-Means · GMM | 차원축소 후 군집 변수 실험 |
각 주차의 내용을 개별 실습으로 끝내지 않고, 한 데이터셋 위에서 순서대로 쌓아 올려 단계마다 성능이 어떻게 변하는지 비교했습니다.
cp(chest pain type)는 0~3 숫자로 들어 있지만 의학적으로는 성격이 다른 범주입니다.
| 값 | 의미 |
|---|---|
| 0 | 전형적 협심증 — 가슴을 짓누르거나 쥐어짜는 통증 |
| 1 | 비전형적 협심증 — 찌르는 듯한 통증 |
| 2 | 비협심증성 통증 — 근육통·소화불량·스트레스 등 |
| 3 | 무증상 |
그런데 cp를 그대로 쓰면 모델이 0 < 1 < 2 < 3 이라는 순서로 읽습니다. 실제로는 0이 가장 위험하고 3이 가장 덜 위험해서 순서가 반대에 가깝습니다.
그래서 위험도 기준으로 파생 변수를 만들어 원본 컬럼을 대체했습니다.
def cp_risk_group(cp):
if cp == 0: return 'high_risk' # 전형적 협심증
elif cp in [1,2]: return 'mid_risk'
else: return 'low_risk' # 무증상
df['cp_risk_group'] = df['cp'].apply(cp_risk_group)
df_final = df_final.drop(columns=['cp'])도메인 해석을 글로만 적어두지 않고 피처 엔지니어링으로 연결한 부분입니다.
EDA → 파생변수·인코딩 → 결정트리 / 랜덤포레스트
→ 부스팅 (XGBoost, LightGBM) → GridSearchCV 튜닝
→ 튜닝된 3개로 Voting 앙상블
→ PCA → K-Means / GMM 군집 변수 추가 → 전후 비교
분할 단계부터 stratify=y를 줘서 레이블 비율을 학습/테스트에 동일하게 유지했습니다.
| 단계 | 성능 |
|---|---|
| 결정 트리 | 정확도 0.9167 |
| 랜덤 포레스트 | 정확도 0.9363 |
| KFold 교차검증 (5) | 평균 0.9047 |
| StratifiedKFold 교차검증 (5) | 평균 0.9126 |
| GridSearchCV 최적화 | CV 최고 0.9484 |
| 최종 테스트 세트 | 정확도 0.9706 · 정밀도 0.9626 · 재현율 0.9810 · F1 0.9717 · AUC 0.9703 |
최적 하이퍼파라미터 — RandomForest max_depth=7, min_samples_split=2, n_estimators=100 / XGBoost colsample_bytree=0.7, learning_rate=0.1 외
같은 데이터에 KFold(0.9047)와 StratifiedKFold(0.9126)를 나란히 돌려 층화 추출의 차이를 수치로 확인했습니다.
7주차에 배운 PCA·K-Means·GMM으로 군집 라벨을 새 피처로 넣으면 성능이 오를까를 실험했습니다.
| 조건 | Voting 앙상블 정확도 |
|---|---|
| 원본 데이터 | 0.9383 |
| K-Means 군집 변수 추가 | 0.9292 |
| GMM 군집 변수 추가 | 0.9292 |
둘 다 떨어졌습니다. 비지도 학습으로 만든 파생 변수가 항상 이득은 아니라는 걸 직접 확인했고, 결과를 지우는 대신 그대로 남겼습니다.
cp_risk_group(도메인 기반)은 살아남고 군집 라벨(통계 기반)은 밀린 셈이라, 파생 변수는 만드는 방식보다 근거가 있는지가 중요하다는 쪽으로 이해하고 있습니다.
StandardScaler → 파생변수(building_age_group) 인코딩 → LinearRegression → KFold(5) → GridSearchCV
| 지표 | 값 |
|---|---|
| 평균 RMSE | 14,091.14 |
| 평균 R² | 0.7981 |
| CV 최고 R² | 0.7990 |
분류 노트북에 비해 설명이 얕습니다. 회귀는 기본 파이프라인을 한 번 통과시켜보는 데 목적을 뒀고, 정리는 분류 쪽에 집중했습니다. 스케일링을 분할 전에 전체 데이터에 적용한 점은 데이터 누수 관점에서 다시 짚어볼 부분으로 남겨둡니다.
Python pandas NumPy scikit-learn XGBoost LightGBM matplotlib seaborn
- 전처리
StandardScaler·LabelEncoder·OneHotEncoder - 검증
train_test_split(stratify)·KFold·StratifiedKFold·cross_val_score - 튜닝
GridSearchCV - 평가
accuracy·precision·recall·f1_score·roc_auc_score·confusion_matrix·Binarizer - 앙상블
VotingClassifier - 차원축소·군집
PCA·KMeans·GaussianMixture