본문 바로가기
Project/🚀 AI Finance 2026

#22 [모듈3-1] scikit-learn, XGBoost, LightGBM — 3대 ML 라이브러리

by 로띠 2026. 3. 13.
머신러닝

scikit-learn, XGBoost, LightGBM — 3대 ML 라이브러리

주가 예측 실전 프로젝트 기반 해설

Data Collect Feature Eng. ML Training Signal

이 글은 Alchemetric 프로젝트에서 주가 예측에 사용하는 3개의 전통 ML 라이브러리 — scikit-learn, XGBoost, LightGBM — 을 ML/DL을 처음 접하는 분도 이해할 수 있도록 설명합니다. 이론보다는 "이 라이브러리가 뭐고, 우리 프로젝트에서 어떻게 쓰이는가"에 초점을 맞춥니다.

1. 왜 이 3개인가? — 간단한 배경

주가 예측은 "분류" 문제

Alchemetric은 주가가 "내일(또는 5일 후) 오를까, 내릴까?"를 예측합니다. 이것은 수치를 맞추는 "회귀(regression)"가 아니라, 상승(1) 또는 하락(0) 둘 중 하나를 고르는 "이진 분류(binary classification)" 문제입니다.

분류 문제를 잘 푸는 알고리즘은 많지만, 그중에서 정형 데이터(테이블 형태의 숫자 데이터)에 가장 강력한 3가지가 바로 RandomForest, XGBoost, LightGBM입니다. 이미지나 텍스트가 아니라 "RSI 값, 거래량, PER, 뉴스 감성 점수" 같은 숫자 피처 75개를 입력으로 쓰기 때문에, 딥러닝보다 이 3개가 더 적합한 출발점입니다.

3개를 동시에 돌리는 이유

핵심 전략: 같은 데이터(75개 피처)로 3개 알고리즘을 동시에 학습하고, 성능(F1 Score)을 비교합니다. 어떤 알고리즘이 가장 좋을지는 데이터에 따라 다르기 때문에, "다 돌려보고 비교"가 가장 현실적인 전략입니다.

각 알고리즘은 같은 문제를 다른 방식으로 접근합니다.

라이브러리 핵심 전략 한 줄 비유
scikit-learn (RandomForest) 배깅 (Bagging) 100명이 독립적으로 투표 → 다수결
XGBoost 부스팅 (Boosting) 오답노트 — 틀린 문제만 반복 학습
LightGBM 경량 부스팅 (Leaf-wise) 오답노트 + 속독법 (더 빠른 부스팅)

2. scikit-learn — ML의 스위스 아미 나이프

scikit-learn은 파이썬에서 머신러닝을 할 때 가장 먼저 배우는 라이브러리입니다. 분류, 회귀, 전처리, 평가 지표 등 ML에 필요한 거의 모든 도구가 들어 있어서 "스위스 아미 나이프"라고 불립니다. Alchemetric에서는 scikit-learn의 4가지 기능을 핵심적으로 사용합니다.

2-1. RandomForestClassifier — 다수결 투표

RandomForest는 여러 개의 결정 트리(Decision Tree)를 만들어서, 각 트리의 예측 결과를 다수결 투표로 종합하는 알고리즘입니다. 이것을 배깅(Bagging)이라고 부릅니다.

비유: 주식 투자 판단을 내릴 때, 한 명의 전문가에게만 물어보면 편향될 수 있습니다. 대신 100명의 투자자에게 각각 독립적으로 판단을 요청한 다음, 60명이 "매수"라고 하면 매수 — 이것이 RandomForest의 원리입니다. 각 투자자(트리)는 데이터의 일부만 보고, 피처의 일부만 사용하기 때문에 서로 다른 관점을 가집니다.

결정 트리(Decision Tree)가 뭔지 모르겠다면 이렇게 생각하면 됩니다. "RSI가 70 이상인가? → Yes → 거래량이 평소의 2배 이상인가? → No → 하락" 같은 if-else 분기를 데이터에서 자동으로 찾아내는 것이 결정 트리입니다. RandomForest는 이런 트리를 수백 개 만들어서 종합합니다.

Alchemetric의 ml_config.yaml에 정의된 RandomForest 설정입니다.

# ml_config.yaml — RandomForest 설정
random_forest:
  classifier: sklearn.ensemble.RandomForestClassifier
  defaults:
    class_weight: "balanced"    # 상승/하락 비율 불균형 자동 보정
    random_state: 42           # 재현 가능한 결과
    n_jobs: -1                 # 모든 CPU 코어 사용
  search_space:                   # Optuna가 탐색할 파라미터 범위
    n_estimators: { low: 100, high: 500 }   # 트리 수 (많을수록 안정적)
    max_depth: { low: 5, high: 20 }        # 트리 깊이 (깊을수록 복잡한 패턴)
    min_samples_split: { low: 2, high: 20 }
    min_samples_leaf: { low: 1, high: 10 }

핵심 파라미터 해설:

파라미터 의미 프로젝트 설정
n_estimators 만들 트리 수. 100개 트리 = 100명의 투자자 100~500 탐색
max_depth 트리 깊이 제한. 너무 깊으면 과적합 5~20 탐색
class_weight 상승/하락 비율이 다를 때 자동 보정 "balanced" 고정

2-2. RobustScaler — 이상치에 강한 정규화

ML 모델에 데이터를 넣기 전에 스케일링(정규화)이 필요합니다. 예를 들어 거래량은 수백만 단위이고, RSI는 0~100 사이인데, 이 두 피처를 그대로 넣으면 거래량이 너무 커서 모델이 편향됩니다.

스케일러 종류는 여러 가지가 있는데, Alchemetric은 RobustScaler를 사용합니다.

왜 RobustScaler인가? 주식 데이터에는 급등(+30%)이나 급락(-20%) 같은 이상치(outlier)가 자주 발생합니다. 일반적인 StandardScaler는 평균과 표준편차를 사용하는데, 이상치가 있으면 평균값이 크게 왜곡됩니다. RobustScaler는 중앙값(median)IQR(사분위 범위)을 사용하기 때문에 극단적인 급등락에 영향을 거의 받지 않습니다.
# trainer.py — 실제 프로젝트 코드
from sklearn.preprocessing import RobustScaler

scaler = RobustScaler()
X_train = scaler.fit_transform(X_train)   # 학습 데이터에 맞춤 + 변환
X_val   = scaler.transform(X_val)         # 검증 데이터는 변환만

2-3. SimpleImputer — NaN(결측치) 처리

75개 피처 중 일부는 값이 없는 경우(NaN)가 있습니다. 예를 들어 신규 상장 종목은 60일 이동평균(sma_60)을 계산할 수 없습니다. XGBoost와 LightGBM은 NaN을 자체적으로 처리할 수 있지만, RandomForest는 NaN을 처리하지 못합니다.

Alchemetric에서는 모든 알고리즘에 공통으로 SimpleImputer를 적용합니다. XGBoost/LightGBM이 NaN을 자체 처리할 수 있긴 하지만, 스케일러(RobustScaler)가 NaN을 지원하지 않기 때문에 imputer를 먼저 통과시키는 것이 안전합니다.

# trainer.py — 모든 알고리즘 공통 NaN 처리
from sklearn.impute import SimpleImputer
imputer = SimpleImputer(strategy="median")
X_train = imputer.fit_transform(X_train)
X_val   = imputer.transform(X_val)

2-4. 평가 지표 — F1 Score를 메인으로 쓰는 이유

scikit-learn은 모델이 얼마나 잘 예측하는지 측정하는 평가 지표(metrics)도 제공합니다. Alchemetric에서는 5가지 지표를 동시에 계산합니다.

지표 의미 쉬운 설명
Accuracy 전체 정답률 100문제 중 몇 개 맞았나
Precision "상승"이라고 예측한 것 중 실제 상승 비율 "매수" 시그널의 신뢰도
Recall 실제 상승 중 모델이 잡아낸 비율 기회를 얼마나 놓치지 않았나
F1 Score Precision과 Recall의 조화 평균 신뢰도와 포착률의 균형
AUC-ROC 확률 예측의 종합적 품질 0.5 = 동전 던지기, 1.0 = 완벽
왜 F1 Score가 메인 지표인가? 주식 데이터는 상승과 하락의 비율이 항상 50:50이 아닙니다. 만약 상승이 70%인 기간에 모든 예측을 "상승"이라고 하면 Accuracy는 70%로 높게 나오지만, 실제로는 아무것도 학습하지 않은 것입니다. F1 Score는 Precision과 Recall의 조화 평균이기 때문에, 이런 불균형 데이터에서도 모델의 실제 능력을 정직하게 보여줍니다. 그래서 Optuna 튜닝의 최적화 목표도 F1 Score입니다.
# trainer.py — 5가지 평가 지표 동시 계산
from sklearn.metrics import (
    accuracy_score, f1_score,
    precision_score, recall_score, roc_auc_score,
)

metrics = {
    "accuracy":  accuracy_score(y_eval, y_pred),
    "precision": precision_score(y_eval, y_pred),
    "recall":    recall_score(y_eval, y_pred),
    "f1":        f1_score(y_eval, y_pred),        # 메인 지표
    "auc_roc":   roc_auc_score(y_eval, y_proba),
}

# tuner.py — Optuna 최적화 목표도 F1
return f1_score(y_val, y_pred, average="binary")

2-5. scikit-learn 역할 요약

scikit-learn은 Alchemetric에서 4가지 역할을 동시에 수행합니다.

역할 클래스 한 줄 요약
분류 알고리즘 RandomForestClassifier 다수결 투표로 상승/하락 분류
스케일링 RobustScaler 이상치에 강한 정규화 (3개 알고리즘 공통)
결측치 처리 SimpleImputer NaN을 중앙값으로 대체 (3개 알고리즘 공통)
모델 평가 f1_score 등 5개 3개 알고리즘 모두의 성능 측정 공통 도구

3. XGBoost — Gradient Boosting의 왕

3-1. Gradient Boosting이란?

XGBoost는 eXtreme Gradient Boosting의 줄임말입니다. 핵심은 "Gradient Boosting"이라는 학습 방식에 있습니다.

RandomForest가 "100명이 동시에 각자 판단 → 다수결"이었다면, Gradient Boosting은 "1번 학생이 틀린 문제를 2번 학생이 집중 학습 → 2번이 틀린 문제를 3번이 집중 학습 → ..." 하는 순차적 개선 방식입니다.

비유 — 시험 오답노트:
1차 시험을 보고 70점을 받았습니다. 틀린 30점짜리 문제들을 집중적으로 복습합니다.
2차에서 80점. 나머지 틀린 20점짜리 문제를 또 복습합니다.
3차에서 88점. 이렇게 틀린 부분에 가중치를 줘서 반복 학습하는 것이 Boosting입니다.
"Gradient"는 "어디를 고쳐야 성능이 가장 좋아지는지"를 수학적으로 계산하는 방법(경사하강법)을 뜻합니다.

3-2. 배깅(RandomForest) vs 부스팅(XGBoost)

항목 배깅 (RandomForest) 부스팅 (XGBoost)
트리 생성 동시에 (병렬) 순서대로 (순차)
각 트리의 역할 독립적으로 판단 이전 트리의 실수를 보정
장점 과적합에 강함, 안정적 정확도가 높음
단점 정확도 한계 과적합 위험, 튜닝 필요

3-3. 핵심 파라미터

# ml_config.yaml — XGBoost 설정
xgboost:
  classifier: xgboost.XGBClassifier
  defaults:
    random_state: 42
    eval_metric: "logloss"     # 확률 예측의 정확도
    verbosity: 0               # 로그 출력 안 함
  search_space:
    n_estimators: { low: 100, high: 500 }
    max_depth: { low: 3, high: 10 }           # RF보다 얕게 (과적합 방지)
    learning_rate: { low: 0.01, high: 0.3 }    # 한 스텝의 학습량
    subsample: { low: 0.6, high: 1.0 }        # 행 샘플링 비율
    colsample_bytree: { low: 0.6, high: 1.0 } # 열 샘플링 비율
    scale_pos_weight: { low: 0.5, high: 2.0 } # 클래스 불균형 보정 가중치

파라미터 해설:

파라미터 쉬운 설명
learning_rate 한 번에 얼마나 고칠지. 0.01이면 조금씩 천천히, 0.3이면 많이 빠르게. 작을수록 정밀하지만 느림
max_depth 트리 깊이. 부스팅에서는 RF보다 얕게 설정 (3~10). 각 트리가 약한 학습기 역할
subsample 각 트리가 데이터의 60~100%만 랜덤 사용. 과적합 방지
colsample_bytree 각 트리가 피처의 60~100%만 랜덤 사용. RF의 랜덤 피처 선택과 유사
scale_pos_weight 상승 vs 하락 비율이 다를 때 보정. RF의 class_weight와 같은 역할
eval_metric: "logloss" 내부 학습 과정에서 확률 예측의 정확도를 측정. 예측 확률이 실제와 얼마나 가까운지

3-4. XGBoost가 강력한 3가지 이유

1) 정규화(Regularization) 내장: L1(Lasso)과 L2(Ridge) 정규화가 내장되어 있어서, 모델이 학습 데이터에 지나치게 맞춰지는 "과적합"을 자체적으로 억제합니다. RandomForest에는 이 기능이 없습니다.

2) 결측치(NaN) 자체 처리: XGBoost는 NaN 값을 별도 처리 없이 그대로 학습할 수 있습니다. 내부적으로 NaN 데이터를 왼쪽 또는 오른쪽 자식 노드로 보내는 최적 방향을 자동으로 학습합니다. 다만 Alchemetric에서는 스케일러 호환성을 위해 모든 알고리즘에 SimpleImputer를 공통 적용합니다.

3) 병렬 처리: 부스팅은 트리를 순차적으로 만들지만, 각 트리 안에서 분기점을 찾는 작업은 병렬로 처리합니다. 덕분에 단순 Gradient Boosting보다 훨씬 빠릅니다.

4. LightGBM — 빠르고 가벼운 부스팅

LightGBM은 Microsoft가 개발한 Gradient Boosting 프레임워크입니다. XGBoost와 같은 "부스팅" 원리지만, 트리를 성장시키는 방식이 근본적으로 다릅니다.

4-1. Level-wise vs Leaf-wise — 핵심 차이

XGBoost와 LightGBM의 가장 큰 차이는 트리를 만드는 방식입니다.

방식 동작 원리 특징
Level-wise (XGBoost) 1층 전체 → 2층 전체 → 3층 전체 순서로 모든 노드를 균등하게 확장 균형 잡힌 트리, 과적합에 안전
Leaf-wise (LightGBM) 오차(loss)가 가장 큰 리프 노드부터 우선 확장 비대칭 트리, 같은 깊이에서 더 높은 정확도
비유: Level-wise는 건물을 지을 때 1층을 전부 완성하고 2층으로 올라가는 방식입니다. Leaf-wise는 "가장 약한 부분부터 집중 보강"하는 방식이라서, 같은 노력(같은 수의 리프)으로 더 좋은 성능을 낼 수 있습니다. 다만 한쪽에만 집중하면 전체 구조가 무너질 수 있어서(과적합), num_leaves로 제한을 걸어야 합니다.

4-2. Histogram-based Splitting — 왜 빠른가

LightGBM이 XGBoost보다 빠른 또 다른 이유는 히스토그램 기반 분할입니다.

일반적으로 트리의 분기점을 찾으려면 모든 데이터 값을 하나씩 확인해야 합니다. RSI 값이 [14.2, 28.7, 33.1, 45.6, 52.3, 67.8, 71.2, 89.4] 이런 식으로 있으면, 가능한 모든 분기점(14.2와 28.7 사이, 28.7과 33.1 사이, ...)을 다 계산합니다.

LightGBM은 이 연속 값들을 256개 구간(bin)으로 미리 나눕니다. [0~12.5 = bin 0, 12.5~25 = bin 1, ...] 이렇게 변환한 다음 bin 단위로만 분기점을 찾습니다. 계산량이 크게 줄어서 대용량 데이터에서 XGBoost 대비 2~5배 빠릅니다.

4-3. 핵심 파라미터

# ml_config.yaml — LightGBM 설정
lightgbm:
  classifier: lightgbm.LGBMClassifier
  defaults:
    is_unbalance: true          # 클래스 불균형 자동 처리
    random_state: 42
    verbose: -1                 # 로그 끄기
  search_space:
    n_estimators: { low: 100, high: 500 }
    num_leaves: { low: 15, high: 63 }          # max_depth 대신 리프 수로 복잡도 제어
    learning_rate: { low: 0.01, high: 0.3 }
    feature_fraction: { low: 0.6, high: 1.0 }   # XGBoost의 colsample_bytree와 동일
    bagging_fraction: { low: 0.6, high: 1.0 }   # XGBoost의 subsample과 동일
    bagging_freq: { low: 1, high: 7 }            # 몇 라운드마다 행 샘플링할지

XGBoost와 대응하는 파라미터:

LightGBM XGBoost 대응 설명
num_leaves max_depth Leaf-wise라서 깊이 대신 리프 수로 복잡도 제어
feature_fraction colsample_bytree 트리당 사용 피처 비율 (이름만 다를 뿐 같은 역할)
bagging_fraction subsample 행 샘플링 비율
is_unbalance scale_pos_weight 클래스 불균형 처리 (LightGBM은 bool, XGBoost는 비율값)
참고: LightGBM도 XGBoost와 마찬가지로 NaN을 자체적으로 처리할 수 있지만, Alchemetric에서는 스케일러(RobustScaler)와의 호환성을 위해 모든 알고리즘에 SimpleImputer를 공통 적용합니다.

5. 3개 비교 총정리

항목 RandomForest XGBoost LightGBM
학습 방식 배깅 (독립 다수결) 부스팅 (순차 개선) 부스팅 (Leaf-wise)
트리 성장 Level-wise Level-wise Leaf-wise
학습 속도 보통 보통 빠름
메모리 사용 많음 보통 적음
NaN 자체 처리 불가 (Imputer 필요) 가능 가능
정규화 내장 없음 L1/L2 L1/L2
과적합 리스크 낮음 중간 중간~높음
불균형 처리 class_weight scale_pos_weight is_unbalance
복잡도 핵심 파라미터 max_depth max_depth num_leaves
개발사 scikit-learn (커뮤니티) DMLC (오픈소스) Microsoft

어떤 걸 써야 하나?

정답: "3개 다 쓰고 비교"

어떤 알고리즘이 최고인지는 데이터에 따라 달라집니다. Kaggle 대회에서도 우승 솔루션이 때로는 XGBoost, 때로는 LightGBM, 때로는 RandomForest입니다. 특히 금융 데이터처럼 노이즈가 많고 시장 상황에 따라 패턴이 변하는 경우, 동일 조건에서 3개를 돌려보고 F1 Score가 가장 높은 모델을 활성화하는 것이 가장 현실적입니다.

6. 프로젝트 적용 — Alchemetric에서의 실전 흐름

6-1. YAML 선언형 설정

Alchemetric은 알고리즘 설정을 파이썬 코드에 하드코딩하지 않고, ml_config.yaml 파일에 선언합니다. 새 알고리즘을 추가하려면 YAML에 몇 줄만 추가하면 됩니다. 코드 수정이 필요 없습니다.

# ml_config_loader.py — YAML에서 분류기 클래스를 동적 임포트
def get_classifier_class(algorithm: str):
    config = get_algorithm_config(algorithm)
    # "xgboost.XGBClassifier" → xgboost 모듈에서 XGBClassifier 클래스 로드
    module_path, class_name = config["classifier"].rsplit(".", 1)
    module = importlib.import_module(module_path)
    return getattr(module, class_name)

이 구조 덕분에 trainer.py는 알고리즘 이름("random_forest", "xgboost", "lightgbm")만 바꿔주면 동일한 파이프라인이 그대로 돌아갑니다.

6-2. 동일 조건, 공정한 비교

3개 알고리즘이 완전히 동일한 조건에서 학습됩니다.

조건 설정
입력 피처 Phase 7까지 계산된 75개 피처 (기술지표 + 재무 + 거시 + 뉴스 + 공시 + 수급 + 섹터 + 대안)
데이터 분할 시계열 기반 85% 학습 / 15% 검증 (미래 데이터 누출 방지)
스케일링 RobustScaler (3개 공통)
평가 지표 F1, Accuracy, Precision, Recall, AUC-ROC (3개 공통)
하이퍼파라미터 튜닝 Optuna 50회 (각 알고리즘별 탐색 공간은 YAML 정의)

6-3. 전체 학습~예측 흐름

# 학습 흐름 (trainer.py)

# 1. feature_store에서 75개 피처 + 타겟(target_class_1d) 로드
df = _load_data(market="KOSPI", features=PHASE7_FEATURE_COLUMNS)

# 2. 시계열 분할 (85% 학습 / 15% 검증)
train_df, val_df, test_df = _split_data(df, train_ratio=0.85)

# 3. NaN 처리 (3개 알고리즘 공통 — 스케일러 호환성)
imputer = SimpleImputer(strategy="median")
X_train = imputer.fit_transform(X_train)

# 4. RobustScaler로 정규화 (3개 공통)
scaler = RobustScaler()
X_train = scaler.fit_transform(X_train)

# 5. Optuna로 하이퍼파라미터 최적화 (F1 Score 최대화)
best_params = tune_hyperparameters(algorithm, X_train, y_train, X_val, y_val)

# 6. 최적 파라미터로 학습
model = _get_classifier(algorithm, best_params)   # YAML에서 클래스 동적 로드
model.fit(X_train, y_train)

# 7. 평가 + 저장 (모델 + 스케일러 + 피처 목록을 joblib으로 함께 저장)
joblib.dump({"model": model, "scaler": scaler, "feature_columns": features})
# 예측 흐름 (predictor.py)

# 1. 활성 모델 3개 (RF, XGB, LGBM) 로드
models = repo.get_active_models(market="KOSPI")

# 2. 각 모델이 동일 종목에 대해 독립적으로 예측
for ml_model in models:
    saved = joblib.load(ml_model.model_path)
    X_scaled = saved["scaler"].transform(X)
    probability_up = saved["model"].predict_proba(X_scaled)[0][1]

    # 3. 확률 → BUY / SELL / HOLD 시그널 변환
    if probability_up >= 0.6:   signal = "BUY"
    elif probability_up <= 0.4: signal = "SELL"
    else:                       signal = "HOLD"

결과적으로 하나의 종목에 대해 3개의 독립적인 시그널이 생성됩니다. 예를 들어 삼성전자에 대해 RandomForest는 BUY(62%), XGBoost는 HOLD(55%), LightGBM은 BUY(68%)를 예측하면, 투자자는 이 3개 의견을 종합적으로 판단할 수 있습니다.

6-4. 피처 중요도 — 모델이 무엇을 중요하게 보는가

3개 알고리즘 모두 feature_importances_ 속성을 제공합니다. 학습 후 "어떤 피처가 예측에 가장 큰 영향을 미쳤는가"를 확인할 수 있습니다. 같은 데이터라도 알고리즘에 따라 중요하게 보는 피처가 다를 수 있고, 이 차이 자체가 투자 인사이트가 됩니다.

# trainer.py — 학습 후 피처 중요도 추출
if hasattr(model, "feature_importances_"):
    for fname, imp in zip(features, model.feature_importances_):
        feature_importance[fname] = round(float(imp), 6)

# 결과 예시:
# RandomForest → rsi_14: 0.045, volume_ratio: 0.038, macd: 0.032
# XGBoost     → return_1d: 0.052, bb_pctb: 0.041, foreign_ratio: 0.035
# LightGBM    → volatility_20d: 0.048, price_to_sma20: 0.039, rsi_14: 0.034

정리

Alchemetric의 전통 ML 파이프라인은 3개의 서로 다른 접근 방식으로 동일한 문제를 풀고, 그 결과를 비교합니다.

라이브러리 한 줄 요약
scikit-learn ML의 기본 도구이자, 전처리(RobustScaler, SimpleImputer)와 평가(F1 등)의 공통 인프라. RandomForest로 안정적인 배깅 분류도 담당. SimpleImputer는 3개 알고리즘 공통 적용
XGBoost Gradient Boosting의 대표. 오답노트 방식으로 정확도를 극대화. 정규화 내장 + NaN 자체 처리
LightGBM XGBoost의 빠른 대안. Leaf-wise 성장 + 히스토그램 분할로 속도와 메모리 효율을 크게 개선

YAML 기반 선언형 설정 + Optuna 자동 튜닝 + 동일 피처/분할 조건 아래에서, 3개 알고리즘이 공정하게 경쟁하며 각각 BUY/SELL/HOLD 시그널을 생성합니다. "어떤 알고리즘이 항상 최고"가 아니라 "다 돌려보고 데이터가 답을 말하게 하는" 것이 Alchemetric의 ML 전략입니다.