복잡한 모델은 더 나을까? 로지스틱 회귀와 트리 모델을 같은 조건에서 비교하기
근거와 검증 범위 — 모델 비교 설계
모멘텀·로지스틱 회귀·트리 모델의 공통 비교 조건을 설계합니다. ETF 가격 스냅샷·기간 경계·체결 자료·실행 기록이 제시되지 않아 모델별 정확도·수익률·낙폭의 우열을 판정하지 않습니다.
7편에서 만든 로지스틱 회귀 기준선에 이번에는 의사결정나무와 랜덤 포레스트를 더한다. 핵심은 모델 이름의 화려함이 아니다. 복잡한 모델이 의미 있으려면 단순 모멘텀·로지스틱 회귀와 같은 데이터, 같은 시간 순서, 같은 거래 규칙, 같은 비용 가정에서 비교돼야 한다.
이 글은 실제 ETF 성과를 보고하지 않는다. ETF 목록, 가격 스냅샷, 기간 경계, 체결 자료와 실행 기록이 제공되지 않았으므로, 어느 모델의 정확도·수익률·낙폭이 더 좋다고 결론 낼 수 없다. 대신 복잡도를 늘릴 때 필요한 공정한 실험 계약을 고정한다. 교육·연구 목적의 설계이며, 특정 ETF의 매수·매도를 권하지 않는다.
네 비교군을 같은 출발선에 세우기
12개월 모멘텀은 기존 규칙을 유지한다. 나머지 세 모델만 같은 ML 특징과 같은 상승/비상승 레이블을 입력으로 사용한다.
| 비교군 | 신호 생성 방식 | 복잡성 | 역할 |
|---|---|---|---|
| 12개월 모멘텀 | 기존 12개월 모멘텀 규칙 | 사전 정의한 규칙 | 단순 기준선 |
| 로지스틱 회귀 | ML 특징으로 양성 클래스 점수 추정 | 정규화된 선형 분류 기준선 [S9] | 첫 ML 기준선 |
DecisionTreeClassifier | 특징과 임계값을 순차적으로 분할 [S1] | 비선형 분할, 단일 트리 | 복잡도 증가의 첫 단계 |
RandomForestClassifier | 여러 트리의 예측을 결합 [S2] | 부분표본과 평균화 [S2] | 복잡도 증가의 두 번째 단계 |
로지스틱 회귀와 트리 계열의 predict_proba 출력은 거래 규칙에 넣기 전의 모델 점수다. 이를 보정된 투자 성공 확률이나 미래 수익의 보장으로 읽어서는 안 된다. [S1] [S2] [S9]
독자가 실험으로 답할 질문은 하나다.
복잡한 모델이 같은 조건에서 분류 성능과 비용 후 경제적 결과를 함께 개선하는가?
그 답은 실행 전에는 알 수 없다.
비선형 분할은 무엇을 더하는가
로지스틱 회귀는 특징들의 관계를 비교적 단순한 기준선으로 다룬다. 반면 의사결정나무는 Gini, entropy, log loss 같은 기준을 사용해 특징과 임계값으로 노드를 나누고, 이 분할을 반복해 더 복잡한 의사결정 영역을 표현한다. [S1] [S9]
예를 들어 트리는 다음과 같은 조건의 조합을 표현할 수 있다.
if return_20d > threshold_a:
if volatility_20d < threshold_b:
predict higher up score
else:
predict lower up score
else:
predict lower up score이는 “최근 상승 흐름이 있더라도 변동성이 높은 경우는 다르게 본다”는 가설을 모델에 넣을 수 있다는 뜻이다. 그러나 이 구조가 ETF의 미래 성과를 더 잘 예측한다는 증거는 아니다. 비선형성을 표현할 수 있다는 사실과 테스트 구간에서 우월하다는 결과는 구분해야 한다.
단일 트리의 과적합과 숲의 평균화
DecisionTreeClassifier는 기본 설정에서 완전히 성장한 비가지치기 트리를 만들 수 있다. 깊이가 깊고 리프의 표본 수가 작아질수록 학습창의 세부 패턴까지 외울 위험이 커진다. 그래서 max_depth와 min_samples_leaf는 결과를 보기 전에 제한된 후보로 정해야 한다. [S1]
RandomForestClassifier는 서로 다른 부분표본에 여러 결정트리를 적합하고 예측을 평균화하는 방식이다. scikit-learn은 이를 예측 정확도 개선과 과적합 제어를 위한 방법으로 설명하며, Breiman의 원 논문은 일반화 오차가 개별 트리의 강도와 트리 사이 상관에 의존한다고 설명한다. [S2] [S3]
여기서도 방법론과 결과를 분리한다.
- 확인된 방법론: 랜덤 포레스트는 여러 트리의 예측을 결합하고 평균화한다. [S2] [S3]
- 확인되지 않은 결과: 랜덤 포레스트가 이 ETF 실험에서 모멘텀, 로지스틱 회귀 또는 단일 트리보다 높은 테스트 성과를 낸다.
OOB 점수를 켤 수 있더라도, 그것을 시간순 학습·검증·테스트 분할의 대체물로 취급하지 않는다. [S2] 이번 근거 범위에서는 OOB가 시간 순서를 보존한다는 확인이 없다.
공정 비교 계약
복잡성의 효과만 비교하려면 아래 조건을 네 비교군에 공통으로 고정한다.
| 항목 | 네 비교군에 한 번만 고정할 조건 |
|---|---|
| 데이터·입력 | 하나의 ETF 목록·가격 스냅샷을 공통 사용한다. 모멘텀은 기존 규칙을 유지하고, 세 ML 모델에는 같은 특징과 레이블을 넣는다. |
| 시간·학습 | 학습·검증·테스트 경계는 결과 전에 날짜 단위로 정한다. 같은 signal_date의 모든 ETF 행을 함께 배정하고, label_end_date < signal_date인 행만 학습에 쓴다. 이는 시간순 원칙을 ETF 패널에 적용한 설계 판단이다. [S4] [S5] |
| 실행 | 월말 신호 뒤 다음 거래일 종가에 리밸런싱하고, 새 비중은 그 이후 수익에 적용한다. 이는 체결 마찰을 생략한 교육용 근사이며, 선택 종목은 동일가중·미선택은 현금으로 둔다. |
| 선택·평가 | 전처리는 각 학습창에서만 fit한다. 검증에서 고정한 선택을 테스트에서 다시 고르지 않으며, 기본 임계값 0.50과 편도 0/5/10/20bp 비용 시나리오에서 정확도와 비용 후 수익률·최대낙폭·turnover를 따로 본다. [S5] [S6] [S7] |
선택 편향도 비용처럼 관리하기
트리 계열의 복잡도는 깊이와 리프 크기, 숲의 트리 수와 특징 선택처럼 추가 선택지를 만든다. 후보를 늘릴수록 검증 구간에 우연히 맞는 설정을 고를 위험도 커지므로, 후보와 탐색 횟수를 결과 전에 제한하고 기록한다. [S6] [S7]
| 비교 대상 | 사전 제한할 선택지 | 기록할 내용 |
|---|---|---|
| 로지스틱 회귀 | 기존 기준선의 제한 후보 | 후보 수와 실행 횟수 |
| 의사결정나무 | max_depth, min_samples_leaf의 소수 조합 | 조합 수와 검증 선택 기준 |
| 랜덤 포레스트 | n_estimators, max_depth, min_samples_leaf, max_features의 소수 조합 | 조합 수, 시드, 실제 실행 횟수 |
| 거래 임계값 | 기본값 0.50 | 추가 탐색 여부와 횟수 |
기본 임계값 0.50과 편도 0/5/10/20bp는 시장의 참값이 아니라 사전 가정이다. 테스트 성과를 본 뒤 깊이·특징 수·임계값 후보를 추가하면, 그 작업은 새 탐색으로 분리해 기록한다. [S6] [S7]
트리와 숲을 같은 흐름에 추가하는 최소 코드
The input table contains one row per ETF and signal date. signal_date is the observation date, label_end_date is the date on which the future label becomes known, symbol identifies the ETF, feature columns must be available on the signal date, and label is binary. Monthly training windows are skipped when they cannot satisfy the shared class-1 probability output contract. The output includes each eligible model's up_probability, selected portfolio weights, and predictions for evaluation.
from sklearn.ensemble import RandomForestClassifier
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import Pipeline
from sklearn.preprocessing import StandardScaler
from sklearn.tree import DecisionTreeClassifier
FEATURES = [
"return_1d",
"return_5d",
"return_20d",
"ma_gap_20_120",
"volatility_20d",
"volume_change",
"market_return_20d",
]
THRESHOLD = 0.50
# Candidate lists must be declared before validation is inspected.
TREE_CANDIDATES = [
{"max_depth": 3, "min_samples_leaf": 20},
{"max_depth": 5, "min_samples_leaf": 40},
]
FOREST_CANDIDATES = [
{
"n_estimators": 300,
"max_depth": 5,
"min_samples_leaf": 20,
"max_features": "sqrt",
},
]
def make_models(selected_tree, selected_forest):
return {
"logistic": Pipeline(
[
("scale", StandardScaler()),
("classifier", LogisticRegression(max_iter=2_000, random_state=7)),
]
),
"tree": DecisionTreeClassifier(random_state=7, **selected_tree),
"forest": RandomForestClassifier(
random_state=7,
n_jobs=-1,
**selected_forest,
),
}
def positive_probability(model, X):
positive_index = list(model.classes_).index(1)
return model.predict_proba(X)[:, positive_index]
def select_equal_weights(frame, threshold=THRESHOLD):
selected = frame["up_probability"].ge(threshold)
frame["weight"] = 0.0
if selected.any():
frame.loc[selected, "weight"] = 1.0 / selected.sum()
return frame
def walk_forward_predictions(
samples,
test_signal_dates,
selected_tree,
selected_forest,
):
predictions = []
for signal_date in test_signal_dates:
# Only labels known before this signal date may enter training.
train = samples[
(samples["signal_date"] < signal_date)
& (samples["label_end_date"] < signal_date)
].copy()
current = samples.loc[
samples["signal_date"].eq(signal_date)
].copy()
# Skip windows that cannot satisfy the shared class-1 probability output contract.
if train["label"].nunique() != 2:
continue
# Each pipeline fits preprocessing only on the historical window.
for model_name, model in make_models(
selected_tree, selected_forest
).items():
model.fit(train[FEATURES], train["label"])
result = current[["signal_date", "symbol", "label"]].copy()
result["model"] = model_name
result["up_probability"] = positive_probability(
model, current[FEATURES]
)
predictions.append(select_equal_weights(result))
return predictionsDuring validation, select exactly one predeclared candidate for each model. Apply only those fixed selections during the test period, and do not reselect tree depth, forest settings, or thresholds. [S6] [S7]
This is a minimal contract example, not a full backtest. It loops through test periods defined by date, trains only on labels that ended before each signal date, and fits logistic-regression scaling inside the pipeline for every historical window. The three ML models receive the same feature set and label. [S1] [S2] [S5] [S9]
결과는 분류층과 경제층으로 나누어 읽기
분류층은 예측의 분포를, 경제층은 동일 거래 규칙 아래의 비용 후 결과를 확인한다.
| 관찰 | 확인할 질문 |
|---|---|
| 정확도는 높지만 비용 후 수익률은 낮다 | turnover 또는 손실 구간의 영향이 커졌는가 |
| 0bp에서만 우세하다 | 거래비용 가정에 민감한 결과인가 |
| 최대낙폭은 낮고 현금 비중이 높다 | 위험 감소가 단순 노출 감소에서 왔는가 |
| 검증에서는 좋고 테스트에서는 약하다 | 선택 편향 또는 국면 의존 가능성이 있는가 |
| 단일 트리와 숲의 결과가 다르다 | 평균화의 효과인지, 특정 표본에 맞은 우연인지 추가 검토가 필요한가 |
정확도와 비용 후 성과를 하나의 숫자로 합치면 원인을 잃기 쉽다. 실제 데이터와 실행 결과가 없으므로, 어느 모델의 우위나 비용 후 지속성을 주장할 수 없다. [S6] [S7]
복잡도를 늘리기 전에 고정할 것
트리의 비선형 분할과 랜덤 포레스트의 평균화는 로지스틱 회귀와 다른 예측을 만들 수 있지만, 우월성을 뜻하지는 않는다. [S1] [S2] [S3]
복잡도를 바꾸기 전에는 데이터·시간·비용·탐색 예산을 고정하고, 테스트 결과를 다음 선택의 근거로 쓰지 않는다.
다음 9편에서는 누수, 선택 편향, 국면 의존처럼 우연을 실력으로 오해하게 만드는 금융 ML의 실패 원인을 점검한다.
Sources
-
[S1] DecisionTreeClassifier — scikit-learn 1.9.0 documentation | scikit-learn developers | 2007–2026 | https://scikit-learn.org/stable/modules/generated/sklearn.tree.DecisionTreeClassifier.html
↩ -
[S2] RandomForestClassifier — scikit-learn 1.9.0 documentation | scikit-learn developers | 2007–2026 | https://scikit-learn.org/stable/modules/generated/sklearn.ensemble.RandomForestClassifier.html
↩ -
[S3] Random Forests | Leo Breiman | October 2001 | https://doi.org/10.1023/A:1010933404324
↩ -
[S4] TimeSeriesSplit — scikit-learn 1.9.0 documentation | scikit-learn developers | 2007–2026 | https://scikit-learn.org/stable/modules/generated/sklearn.model_selection.TimeSeriesSplit.html
↩ -
[S5] 12. Common pitfalls and recommended practices — scikit-learn 1.9.0 documentation | scikit-learn developers | 2007–2026 | https://scikit-learn.org/stable/common_pitfalls.html
↩ -
[S6] On Over-fitting in Model Selection and Subsequent Selection Bias in Performance Evaluation | Gavin C. Cawley and Nicola L. C. Talbot | July 2010 | https://www.jmlr.org/beta/papers/v11/cawley10a.html
↩ -
[S7] Nested versus non-nested cross-validation — scikit-learn 1.9.0 documentation | scikit-learn developers | 2007–2026 | https://scikit-learn.org/stable/auto_examples/model_selection/plot_nested_cross_validation_iris.html
↩ -
[S9] LogisticRegression — scikit-learn 1.9.0 documentation | scikit-learn developers | 2007–2026 | https://scikit-learn.org/stable/modules/generated/sklearn.linear_model.LogisticRegression.html
↩
오류 제보·의견 보내기
글 제목과 주소가 포함된 메일 초안을 엽니다. 내용과 받는 사람을 확인한 뒤 보내주세요.
받는 사람: [email protected]
메일 앱에서 작성메일 앱이 열리지 않으면 아래 내용을 복사해 평소 쓰는 이메일에 붙여 넣으세요.
관련 글
퀀트·데이터 연구 로지스틱 회귀는 기준선을 넘을까? 공정한 모델 비교의 조건
로지스틱 회귀와 기준 전략을 공정하게 비교하기 위한 시간축, 데이터 분리, 재학습과 비용 계산 규칙을 정리합니다. 실제 성과 검증에 앞서 고정할 실험 설계입니다.
퀀트·데이터 연구 shift(1)만으로는 부족하다: 신호·체결·레이블의 시간 계약
신호 생성, 주문 체결, 수익 귀속과 레이블 확정 시점을 구분합니다. 합성 단위테스트로 미래정보 누수와 전처리의 시간 경계를 점검합니다.