결측 데이터 전략 — Graceful NULL + 이중 Forward-Fill
ML 피처 파이프라인에서 NULL을 다루는 세 가지 원칙
이 문서는 AI_Finance(ALCHEMETRIC) 프로젝트의 피처 엔지니어링 파이프라인에서 결측 데이터(NULL)를 다루는 세 가지 원칙을 다룹니다. 350+ 종목 × 7개 데이터 소스를 병합하는 과정에서, 데이터가 없는 종목에 대해 0으로 채우지 않고 NULL을 유지하는 전략과 월별 지표에 대한 이중 Forward-Fill 기법을 설명합니다.
1. 문제 정의 — 350+ 종목, 7개 소스, 불가피한 결측
Alchemetric의 피처 엔지니어링 파이프라인은 단일 종목에 대해 7개 데이터 소스를 병합하여 최대 75개 피처를 생성합니다. 그러나 350+ 종목 모두가 7개 소스의 데이터를 완전히 갖추고 있지는 않습니다.
| 데이터 소스 | Phase | 결측 발생 원인 |
| 가격/기술적 지표 | Phase 1 | lookback 부족 (상장 60일 미만) |
| 펀더멘털/재무제표 | Phase 2 | 비상장 전환, DART 미등록, 스팩 |
| 거시경제지표 | Phase 3 | 월별/분기별 발표 → 거래일 갭 |
| 뉴스 센티먼트 | Phase 4 | 소형주는 뉴스 자체가 없음 |
| 공시 | Phase 5A | DART 미등록, 공시 이력 없는 종목 |
| 수급 (공매도/프로그램) | Phase 5B | 소형주 공매도 데이터 미제공 |
| 대안 데이터 (트렌드/커뮤니티) | Phase 7 | Google Trends 미수집, 커뮤니티 미존재 |
이때 발생하는 두 가지 핵심 문제:
문제 1: 컬럼 누락 → 스키마 에러 종목 A: [close, return_1d, ..., per, pbr, news_sentiment, ...] # 75 cols 종목 B: [close, return_1d, ..., ...] # per, pbr 컬럼 자체가 없음 → ML 학습 시 "KeyError: 'per'" → 파이프라인 중단 문제 2: 0-fill → 거짓 시그널 disclosure_count_30d = 0 # "공시 0건" (실제로 확인했지만 없었음) disclosure_count_30d = 0 # "데이터 없음" (확인 자체를 못 함) → 두 상황을 같은 값으로 처리하면 ML 모델이 잘못된 패턴을 학습
핵심 질문: 데이터가 없는 종목의 피처를 어떻게 채워야 ML 모델이 올바르게 학습할 수 있는가? 0으로 채우면 거짓 시그널이 되고, 컬럼을 빼면 스키마가 깨진다. 정답은 NULL을 유지하되 컬럼은 반드시 존재하게 하는 것이다.
2. 원칙 1 — Graceful NULL 보장
Alchemetric의 모든 _merge_*() 함수는 동일한 패턴을 따릅니다: 데이터가 없어도 컬럼은 반드시 존재하며, 값은 NULL(None)로 채웁니다.
2.1 공통 패턴 — 컬럼 보장 루프
모든 merge 함수의 마지막에는 동일한 구조의 컬럼 보장 코드가 존재합니다.
# 공통 패턴: 모든 _merge_*() 함수의 마지막 for col in EXPECTED_COLUMNS: if col not in df.columns: df[col] = None return df
이 패턴이 적용된 실제 코드를 각 Phase별로 살펴보겠습니다.
2.2 Phase 2 — _merge_fundamental_features()
def _merge_fundamental_features(df, market, code, session): """재무 피처 병합 (Phase 2)""" # --- 1) KIS 기초정보 병합 --- fund_rows = fund_repo.get_fundamentals(market=market, code=code) if fund_rows: fund_df = DataFrame([...]) df = df.merge(fund_df, on="date", how="left") else: # 데이터 없음 → 컬럼만 추가 (NULL) for col in ["per", "pbr", "eps", "market_cap", "foreign_ratio", "inst_net_buy", "foreign_net_buy"]: if col not in df.columns: df[col] = None # --- 2) DART 재무제표 (forward-fill) --- stmt_rows = fund_repo.get_financial_statements_for_features(...) if stmt_rows: # 분기별 데이터를 일별로 forward-fill ... else: for col in ["roe", "debt_ratio"]: if col not in df.columns: df[col] = None
2.3 Phase 5A — _merge_disclosure_features()
공시 함수는 이중 방어를 적용합니다: 데이터가 아예 없으면 조기 리턴하면서 NULL을 채우고, 데이터가 있더라도 마지막에 컬럼 보장을 수행합니다.
_DISC_COLS = [
"disclosure_count_30d", "days_since_disclosure",
"disclosure_sentiment", "disclosure_type_score",
"disclosure_volume_change",
]
# 방어 1: 데이터 자체가 없으면 조기 리턴
if not disc_rows:
for col in _DISC_COLS:
df[col] = None
return df
# ... 계산 로직 ...
df = df.merge(disc_feat_df, on="date", how="left")
# 방어 2: merge 후에도 컬럼 보장
for col in _DISC_COLS:
if col not in df.columns:
df[col] = None
2.4 Phase 7 — _merge_alternative_features()
_ALTERNATIVE_FEATURE_COLUMNS = [
"google_trend_score", "google_trend_momentum",
"community_post_volume", "community_comment_volume",
"community_engagement_ratio", "alternative_activity_index",
]
# 대안 데이터가 없으면 → 6개 피처 모두 NULL
if not alt_rows:
for col in _ALTERNATIVE_FEATURE_COLUMNS:
df[col] = None
return df
# ... 계산 로직 ...
# 마지막 컬럼 보장
for col in _ALTERNATIVE_FEATURE_COLUMNS:
if col not in df.columns:
df[col] = None
설계 의도: XGBoost와 LightGBM은 NULL(NaN)을 자연스럽게 처리하는 내장 메커니즘을 갖고 있습니다. 데이터가 없을 때 NULL을 유지하면 모델이 “정보 부재”라는 사실 자체를 학습할 수 있습니다. 반면 0이나 평균으로 채우면 모델이 실제 값과 대체 값을 구분할 수 없게 됩니다.
3. 원칙 2 — 이중 Forward-Fill
거시경제지표 중 월별/분기별로 발표되는 지표(예: US CPI)는 일별 주가 데이터와 주기가 다릅니다. CPI가 매월 1회 발표되면, 나머지 ~20 거래일은 값이 비어 있게 됩니다. 이 문제를 해결하기 위해 이중 Forward-Fill 전략을 사용합니다.
3.1 Forward-Fill 대상 컬럼
# 월별/분기별 지표 — forward-fill 대상 (일별 지표는 left join만으로 충분) _MACRO_FFILL_COLUMNS = {"us_cpi"}
현재 us_cpi가 Forward-Fill 대상입니다. fed_rate 등 다른 월별 지표도 이 Set에 추가하면 자동으로 이중 ffill이 적용됩니다. krw_usd, vix 같은 일별 지표는 left join만으로 충분하므로 ffill 대상에 포함하지 않습니다.
3.2 왜 두 단계가 필요한가?
Forward-Fill을 한 번만 하면 해결되지 않는 이유는, 데이터가 두 개의 다른 시간축에 존재하기 때문입니다.
━━━ Level 1: 피벗 테이블 내 ffill ━━━ macro_indicator 테이블 (원본): ┌────────────┬──────────┬───────┐ │ date │ name │ value │ ├────────────┼──────────┼───────┤ │ 2026-01-14 │ US_CPI │ 3.2 │ ← 1월 발표 │ 2026-01-14 │ KRW_USD │ 1320 │ │ 2026-01-15 │ KRW_USD │ 1318 │ │ 2026-02-12 │ US_CPI │ 3.1 │ ← 2월 발표 └────────────┴──────────┴───────┘ 피벗 후 (Level 1 ffill 전): ┌────────────┬─────────┬────────┐ │ date │ us_cpi │ krw_usd│ ├────────────┼─────────┼────────┤ │ 2026-01-14 │ 3.2 │ 1320 │ │ 2026-01-15 │ NaN │ 1318 │ ← CPI는 1/14에만 존재 │ ... │ NaN │ ... │ │ 2026-02-12 │ 3.1 │ 1325 │ └────────────┴─────────┴────────┘ Level 1 ffill: 피벗 테이블 안에서 us_cpi를 아래로 채움 ┌────────────┬─────────┬────────┐ │ 2026-01-14 │ 3.2 │ 1320 │ │ 2026-01-15 │ 3.2 │ 1318 │ ← 채워짐 │ ... │ 3.2 │ ... │ │ 2026-02-12 │ 3.1 │ 1325 │ └────────────┴─────────┴────────┘
━━━ Level 2: merge 후 거래일 갭 ffill ━━━ 문제: 피벗에는 거시지표가 존재하는 날짜만 있음 주식 거래일과 거시지표 날짜가 다를 수 있음 feature_store (left join 후): ┌────────────┬────────┬─────────┬────────┐ │ date │ close │ us_cpi │ krw_usd│ ├────────────┼────────┼─────────┼────────┤ │ 2026-01-13 │ 50000 │ NaN │ 1319 │ ← CPI 발표 전 │ 2026-01-14 │ 50500 │ 3.2 │ 1320 │ ← CPI 발표일 │ 2026-01-15 │ 50300 │ NaN │ 1318 │ ← 거래일이지만 피벗에 없음 │ 2026-01-16 │ 50800 │ NaN │ 1321 │ └────────────┴────────┴─────────┴────────┘ Level 2 ffill: merge된 df에서 us_cpi를 다시 아래로 채움 ┌────────────┬────────┬─────────┬────────┐ │ 2026-01-13 │ 50000 │ NaN │ 1319 │ ← 발표 전이므로 NULL 유지 (정확!) │ 2026-01-14 │ 50500 │ 3.2 │ 1320 │ │ 2026-01-15 │ 50300 │ 3.2 │ 1318 │ ← 채워짐 │ 2026-01-16 │ 50800 │ 3.2 │ 1321 │ ← 채워짐 └────────────┴────────┴─────────┴────────┘
3.3 실제 구현 코드
def _merge_macro_features(df, session): """거시 피처 병합 (Phase 3)""" macro_rows = MacroRepository(session).get_all_by_date_range(...) if macro_rows: # 피벗: (date) x (indicator_name) → value pivot = macro_df.pivot_table( index="date", columns="indicator_name", values="value", aggfunc="first", ) pivot = pivot.rename(columns=rename) # ━━━ Level 1: 피벗 테이블 내 ffill ━━━ for col in _MACRO_FFILL_COLUMNS: if col in pivot.columns: pivot[col] = pivot[col].ffill() df = df.merge(pivot, on="date", how="left") # ━━━ Level 2: merge 후 거래일 갭 ffill ━━━ for col in _MACRO_FFILL_COLUMNS: if col in df.columns: df[col] = df[col].ffill() # 컬럼 보장 (거시 데이터가 아예 없어도 컬럼은 존재) for col in _MACRO_COLUMN_MAP.values(): if col not in df.columns: df[col] = None
Level 1만으로 부족한 이유: 피벗 테이블에는 거시지표가 기록된 날짜만 존재합니다. 주식 거래일(feature_store의 date)과 거시지표 날짜가 다를 수 있습니다. 예를 들어, 거시지표가 1/14에만 기록되었지만 주식은 1/13~1/16 모두 거래됩니다. left join 후 1/15, 1/16의 us_cpi는 NaN이 되므로, Level 2에서 한 번 더 ffill이 필요합니다.
3.4 이중 Forward-Fill 흐름도
이중 Forward-Fill 흐름 ┌──────────────────┐ │ macro_indicator │ 원본 (date, name, value) └────────┬─────────┘ ▼ ┌──────────────────┐ │ pivot_table() │ 가로 전환 (date × indicator) └────────┬─────────┘ ▼ ┌──────────────────┐ │ Level 1 ffill │ 피벗 내 월별 지표 갭 채움 │ pivot[col].ffill()│ (CPI: 1/14 → 2/12 사이 채움) └────────┬─────────┘ ▼ ┌──────────────────┐ │ df.merge(pivot) │ 주가 DataFrame과 left join └────────┬─────────┘ ▼ ┌──────────────────┐ │ Level 2 ffill │ merge 후 거래일 갭 채움 │ df[col].ffill() │ (피벗에 없는 거래일의 NaN 채움) └────────┬─────────┘ ▼ ┌──────────────────┐ │ 컬럼 보장 │ 11개 거시 컬럼 존재 보장 └──────────────────┘
4. 원칙 3 — NULL vs 0의 의미론
NULL과 0은 근본적으로 다른 의미를 가집니다. 이 구분을 유지하는 것이 ML 모델의 예측 품질에 직접적인 영향을 줍니다.
4.1 의미론적 차이
| 값 | 의미 | 트리 모델 처리 |
| NULL (None / NaN) | “데이터 없음” — 관측 자체가 불가능 | 별도 분기 경로 (default direction) |
| 0 | “실제로 0” — 관측했지만 값이 0 | 일반 분기 (threshold 비교) |
4.2 disclosure_count_30d 예시
공시 피처에서 이 차이가 가장 극명하게 드러납니다.
시나리오 A: DART에 등록된 종목, 최근 30일 공시 0건 disclosure_count_30d = 0 # 실제로 0건 (확인했지만 없었음) days_since_disclosure = 45 # 45일 전 마지막 공시 disclosure_sentiment = None # 30일 내 공시 없으니 센티먼트도 없음 → 모델 해석: "이 종목은 최근 조용함, 45일 전 마지막 공시" 시나리오 B: DART에 미등록된 종목 (데이터 소스 자체 없음) disclosure_count_30d = None # 데이터 자체가 없음 days_since_disclosure = None # 알 수 없음 disclosure_sentiment = None # 알 수 없음 → 모델 해석: "이 종목은 공시 데이터 추적 불가" 시나리오 C: 만약 0-fill을 했다면 (잘못된 접근) disclosure_count_30d = 0 # A와 B가 동일한 값! days_since_disclosure = 0 # "오늘 공시했다"라는 거짓 시그널 disclosure_sentiment = 0.0 # "중립 센티먼트"라는 거짓 시그널 → 모델이 "공시 0건"과 "데이터 없음"을 구분 불가 → 잘못된 패턴 학습
경고: days_since_disclosure = 0으로 채우면 “오늘 공시가 있었다”는 강력한 거짓 시그널이 됩니다. 공시 당일은 주가 변동이 큰 날이므로, 모델이 이 잘못된 패턴을 학습하면 예측 정확도가 크게 하락합니다.
4.3 다른 피처의 NULL vs 0
| 피처 | NULL 의미 | 0 의미 |
| per | 펀더멘털 데이터 없음 | PER이 0 (적자 전환) |
| news_sentiment | 뉴스 자체가 없는 종목 | 센티먼트가 정확히 중립 |
| short_selling_ratio | 공매도 데이터 미제공 | 공매도 비율 0% (매우 긍정적) |
| google_trend_score | 트렌드 데이터 미수집 | 검색량 0 (관심 없음) |
| roe | 재무제표 미공시 | ROE 0% (수익 없음) |
5. 구현 패턴 비교 — Phase별 merge 함수
각 Phase의 merge 함수가 NULL을 처리하는 방식을 비교합니다. 모든 함수는 Graceful NULL 보장 패턴을 공유하지만, 데이터 특성에 따라 세부 전략이 다릅니다.
| Phase | 함수 | 피처 수 | NULL 전략 | Forward-Fill |
| Phase 2 | _merge_fundamental_features() | 9개 | 조기 리턴 + 컬럼 보장 | roe/debt_ratio만 분기별 ffill |
| Phase 3 | _merge_macro_features() | 11개 | 컬럼 보장 | 이중 ffill (Level 1 + 2) |
| Phase 4 | _merge_news_features() | 5개 | 컬럼 보장 | 없음 (일별 데이터) |
| Phase 5A | _merge_disclosure_features() | 5개 | 이중 방어 (조기 리턴 + 컬럼 보장) | 없음 (롤링 윈도우) |
| Phase 5B | _merge_supply_demand_features() | 5개 | 조기 리턴 + 컬럼 보장 | 없음 (일별 데이터) |
| Phase 6A | _merge_sector_features() | 7개 | 다단계 방어 (섹터 없음/피어 부족/데이터 없음) | 없음 (일별 계산) |
| Phase 6B | _merge_news_refined_features() | 3개 | 컬럼 보장 | 없음 |
| Phase 7 | _merge_alternative_features() | 6개 | 조기 리턴 + 컬럼 보장 | 없음 |
5.1 Phase 6A의 다단계 방어
섹터/상대강도 피처는 NULL이 발생할 수 있는 경로가 3가지입니다. 각 경로에서 모두 Graceful NULL을 보장합니다.
def _merge_sector_features(df, market, code, session): # 방어 1: 섹터 정보 없음 sector = stock_repo.get_sector_for_code(code, market) if not sector: for col in _SECTOR_FEATURE_COLUMNS: df[col] = None return df # 방어 2: 동일 섹터 피어가 2개 미만 sector_codes = stock_repo.get_codes_in_sector(sector, market) if len(sector_codes) < 2: for col in _SECTOR_FEATURE_COLUMNS: df[col] = None return df # 방어 3: feature_store에 피어 데이터 없음 sector_rows = ml_repo.get_sector_features_bulk(...) if not sector_rows: for col in _SECTOR_FEATURE_COLUMNS: df[col] = None return df # ... 계산 로직 ... # 최종 컬럼 보장 for col in _SECTOR_FEATURE_COLUMNS: if col not in df.columns: df[col] = None
5.2 Phase 5B의 파생 피처 NULL 처리
수급 피처에서 program_net_volume은 program_buy_volume - program_sell_volume으로 파생됩니다. buy와 sell이 모두 NULL인 경우, net도 NULL로 설정합니다.
# program_net_volume 파생 supply_df["program_net_volume"] = ( supply_df["program_buy_volume"].fillna(0) - supply_df["program_sell_volume"].fillna(0) ) # buy/sell 모두 None인 경우 net도 None both_null = ( supply_df["program_buy_volume"].isna() & supply_df["program_sell_volume"].isna() ) supply_df.loc[both_null, "program_net_volume"] = None
핵심 원칙: 파생 피처도 원본이 모두 NULL이면 파생 값도 NULL이어야 합니다. buy=NULL, sell=NULL일 때 net=0으로 계산하면 “프로그램 순매수 0”이라는 거짓 시그널이 됩니다.
6. 트리 모델과 NULL
Alchemetric은 예측 모델로 XGBoost와 LightGBM을 사용합니다. 이 두 모델은 NULL(NaN)을 자체적으로 처리하는 메커니즘을 내장하고 있어, Graceful NULL 전략과 완벽하게 호환됩니다.
6.1 XGBoost의 Default Direction
XGBoost는 트리의 각 노드에서 default direction을 학습합니다. 특정 피처가 NULL인 샘플이 왔을 때, 좌측/우측 중 어느 방향으로 보낼지 학습 데이터에서 최적의 방향을 찾습니다.
XGBoost 트리 분기 예시 disclosure_count_30d < 3 ? / \ Yes (0,1,2건) No (3건 이상) / \ 낮은 위험 리프 disclosure_sentiment < 0 ? / \ 높은 위험 리프 중간 위험 리프 NULL이 오면? disclosure_count_30d = NaN → default direction으로 분기 (학습 시 최적 방향 결정) → 0-fill과 다른 경로를 탈 수 있음!
6.2 LightGBM의 use_missing
LightGBM은 기본적으로 use_missing=True로 설정되어, NULL 값을 별도의 bin으로 분류합니다. 이를 통해 모델은 “값이 있는 경우”와 “값이 없는 경우”를 다른 분기로 처리할 수 있습니다.
6.3 NULL vs 0-fill vs Mean-fill 비교
| 전략 | 장점 | 단점 | 트리 모델 호환 |
| NULL 유지 | 의미 보존, 별도 분기 학습 | 선형 모델에서 사용 불가 | 최적 |
| 0-fill | 단순, 모든 모델 호환 | 거짓 시그널, 의미 왜곡 | 비추 |
| Mean-fill | 분포 유지 | 정보 손실, 분산 축소 | 비추 |
| Indicator 변수 추가 | 명시적 결측 표시 | 피처 수 2배 증가 | 불필요 (트리 모델이 자체 처리) |
Alchemetric의 선택: XGBoost/LightGBM은 NULL을 자체적으로 최적 분기에 배치합니다. 별도의 결측 지시 변수(indicator variable)를 추가할 필요가 없으므로, 75개 피처로 충분합니다. 0-fill이나 mean-fill은 모델이 “데이터 없음”이라는 정보를 학습할 기회를 박탈합니다.
7. 파이프라인 컨텍스트 — 75개 피처와 NULL 빈도
7.1 전체 피처 엔지니어링 호출 순서
compute_features() 내부 호출 순서 Phase 1 _compute_all_features() # 가격/기술지표 25개 Phase 2 _merge_fundamental_features() # 펀더멘털/재무 9개 Phase 3 _merge_macro_features() # 거시경제지표 11개 Phase 4 _merge_news_features() # 뉴스 센티먼트 5개 Phase 5A _merge_disclosure_features() # 공시 피처 5개 Phase 5B _merge_supply_demand_features() # 수급 피처 5개 Phase 7 _merge_alternative_features() # 대안 데이터 6개 Phase 6A _merge_sector_features() # 섹터/상대강도 7개 Phase 6B _merge_news_refined_features() # 뉴스 정제 3개 ────────────────────────────────────────────── 합계 76개 (+ target 4개)
7.2 NULL 발생 가능 피처 분류
| NULL 빈도 | 피처 그룹 | 피처 수 | 원인 |
| 거의 없음 | Phase 1 (가격/기술지표) | 25개 | 주가 데이터는 필수 조건 |
| 거의 없음 | Phase 3 (거시지표) | 11개 | 종목 무관, 공통 데이터 |
| 중간 | Phase 2 (펀더멘털) | 9개 | 스팩, 신규상장, DART 미등록 |
| 중간 | Phase 4 (뉴스) | 5개 | 소형주 뉴스 부재 |
| 중간 | Phase 6 (섹터/뉴스 정제) | 10개 | 섹터 미분류, 피어 부족 |
| 높음 | Phase 5 (공시/수급) | 10개 | 소형주 공매도 미제공, 공시 미등록 |
| 높음 | Phase 7 (대안 데이터) | 6개 | Google Trends/커뮤니티 미수집 |
7.3 NULL 발생 가능 피처 전체 목록
Phase 2 (펀더멘털) — 9개 per, pbr, eps, market_cap, foreign_ratio, inst_net_buy, foreign_net_buy, roe, debt_ratio Phase 4 (뉴스) — 5개 news_sentiment, news_volume, news_sentiment_std, market_sentiment, market_news_volume Phase 5A (공시) — 5개 disclosure_count_30d, days_since_disclosure, disclosure_sentiment, disclosure_type_score, disclosure_volume_change Phase 5B (수급) — 5개 short_selling_volume, short_selling_ratio, program_buy_volume, program_sell_volume, program_net_volume Phase 6A (섹터/상대강도) — 7개 sector_return_1d, sector_return_5d, relative_strength_1d, relative_strength_5d, relative_strength_20d, sector_momentum_rank, sector_breadth Phase 6B (뉴스 정제) — 3개 news_relevance_ratio, news_sentiment_filtered, sector_news_sentiment Phase 7 (대안 데이터) — 6개 google_trend_score, google_trend_momentum, community_post_volume, community_comment_volume, community_engagement_ratio, alternative_activity_index NULL 가능 피처 합계: 40개 / 전체 76개 (52.6%)
7.4 DB 저장 시 NULL 처리
compute_features()의 마지막 단계에서 DataFrame의 NaN을 Python None으로 변환하여 DB에 저장합니다. PostgreSQL의 NULL과 pandas의 NaN이 정확히 매핑됩니다.
# feature_store 레코드 생성 시 NaN → None 변환 for _, row in features_df.iterrows(): record = {"market": market, "code": code, "date": row["date"]} for col in features_df.columns: if col == "date": continue val = row[col] if pd.isna(val): record[col] = None # NaN → NULL elif isinstance(val, np.integer): record[col] = int(val) elif isinstance(val, np.floating): record[col] = float(val) else: record[col] = val records.append(record)
7.5 전략 요약
Alchemetric 결측 데이터 전략 (3원칙) 원칙 1: Graceful NULL 보장 ┌──────────────────────────────────────────────────┐ │ 데이터 없음 → 컬럼은 존재 + 값은 NULL │ │ for col in COLS: │ │ if col not in df.columns: df[col] = None │ └──────────────────────────────────────────────────┘ 원칙 2: 이중 Forward-Fill ┌──────────────────────────────────────────────────┐ │ Level 1: 피벗 내 ffill (월별 갭 채움) │ │ Level 2: merge 후 ffill (거래일 갭 채움) │ │ 대상: _MACRO_FFILL_COLUMNS = {"us_cpi"} │ └──────────────────────────────────────────────────┘ 원칙 3: NULL vs 0 의미 보존 ┌──────────────────────────────────────────────────┐ │ NULL = "데이터 없음" → 트리 모델 별도 분기 │ │ 0 = "실제로 0" → 일반 threshold 비교 │ │ 0-fill 금지, mean-fill 금지 │ └──────────────────────────────────────────────────┘
결론: 76개 피처 중 52.6%(40개)가 NULL 가능합니다. 이 결측은 데이터 품질 문제가 아니라, 350+ 종목의 이질적인 데이터 커버리지를 반영하는 정상적인 현상입니다. Graceful NULL + 이중 Forward-Fill + NULL/0 의미 보존, 이 세 가지 원칙이 XGBoost/LightGBM의 내장 결측 처리 메커니즘과 결합되어, 데이터 결측이 모델 성능 저하로 이어지지 않도록 합니다.
'Project > 🚀 AI Finance 2026' 카테고리의 다른 글
| #20 [모듈2-4] 피어 비교 피처 — 섹터 상대강도 + 복합 인덱스 (0) | 2026.03.13 |
|---|---|
| #19 [모듈2-3] 증분 파이프라인 + Lookback 버퍼 (0) | 2026.03.13 |
| #17 [모듈2-1] 2-Pass 아키텍처 피처 엔지니어링 (0) | 2026.03.13 |
| #16 [모듈2] Feature Engineering (0) | 2026.03.13 |
| #15 [모듈1-9] pytrends + BS4 — 대안 데이터 (0) | 2026.03.13 |