TL;DR
- NBA 경기 예측은 "승패를 맞히는 분류"보다 확률 품질(LogLoss/Brier)과 캘리브레이션이 제품 관점에서 더 중요합니다.
- 구축 순서는 Elo 베이스라인(빠른 검증) → 피처 확장 + GBDT/ML → 캘리브레이션 → 운영 모니터링이 가장 안전합니다.
- NBA는 일정/휴식 영향이 큽니다. 백투백·휴식일·이동이 승률/퍼포먼스에 영향을 준다는 연구 결과가 있습니다.
- Elo는 "업데이트 규칙"과 "시즌 평균회귀(리셋)"를 명시해야 재현 가능한 파이프라인이 됩니다(538은 시즌 시작 시 평균으로 1/4 회귀 예시를 공개).
- 인게임 확장은 PBP(Play-by-Play) 이벤트 스트림 → 상태 피처 → 온라인 추론 구조로 별도 파이프라인을 두는 게 일반적이며, 관련 베이지안 접근 연구도 있습니다.
본문
1) 목표 정의: “NBA 경기 예측 ML 모델”을 제품으로 만든다는 의미
이 글에서의 목표는 단순히 “오늘 경기 맞히기”가 아니라, 지속적으로 배포/모니터링 가능한 ML 제품입니다.
- 프리게임(pre-game): 경기 시작 전
P(home_win)을 예측(배치 스코어링 중심) - 인게임(in-game) 확장: 경기 중 시간/점수/포제션/파울 등 상태를 받아 승리확률 업데이트(스트리밍/저지연 추론)
그리고 출력은 라벨(승/패)이 아니라 확률입니다. 확률이면 평가도 Accuracy가 아니라 LogLoss/Brier(Proper Scoring Rule)가 중심이 됩니다.
Why it matters: 운영에서 중요한 건 "맞았냐"가 아니라 "55%라고 말한 경기들이 정말 55% 수준인가"입니다. 이게 무너지면 추천/알림/리스크 정책이 다 무너집니다.
2) 데이터 소스/컴플라이언스: NBA 데이터는 “안정성”과 “이용 조건”이 먼저다
NBA 데이터는 경로가 다양합니다.
- (프로토타입)
nba_api같은 클라이언트로 stats.nba.com 계열 엔드포인트를 호출해 빠르게 실험하는 경우가 많습니다. - 다만 NBA 디지털 플랫폼 이용은 NBA.com Terms of Use 영향을 받습니다(서비스/상용화 목적이면 특히 중요).
실무 팁: "사내 PoC"는 빠른 소스로 시작하더라도, 서비스화가 목표면 초기에 라이선스/피드 안정성까지 포함한 전환 계획을 세우는 게 비용이 적게 듭니다.
Why it matters: 모델 성능보다 먼저, 데이터 수급이 끊기면 파이프라인이 죽습니다. 또한 이용 조건 문제는 나중에 해결할수록 리팩토링 비용이 커집니다.
3) 누수 방지 원칙: “as-of time(예측 시점)”이 스키마에 박혀 있어야 한다
스포츠 예측의 가장 흔한 실패는 미래 정보 누수입니다.
- “최근 10경기 지표” 계산에 예측 대상 경기 이후 경기 포함
- 선수 결장/라인업을 경기 후 확정 데이터로 사용
- 시즌 누적 지표를 예측 시점 이전으로 컷하지 않음
따라서 추천 스키마는 최소한 아래처럼 갑니다.
games(game_id, game_datetime_utc, home_team_id, away_team_id, home_score, away_score, season, is_playoff, ...)team_game_stats(game_id, team_id, is_home, ...boxscore metrics...)features_snapshot(game_id, as_of_datetime_utc, feature_json, label_home_win, ...)
Why it matters: 누수는 오프라인 점수를 올리지만 실전에서 무너집니다. “좋은 모델”이 아니라 “나쁜 평가 방법”이 만든 착시입니다.
4) Elo 베이스라인: 업데이트 수식 + 시즌 평균회귀(리셋)까지 명시하자
Elo는 “팀 강도”를 빠르게 만들 수 있는 베이스라인입니다. 중요한 건 수식을 문서화해서 재현 가능하게 만드는 것입니다.
4.1 프리게임 승리확률(기본 Elo 기대값)
전통 Elo는 기대 승률을 아래 형태로 씁니다(점수 스케일 400 기준).
E_home = 1 / (1 + 10^(-(R_home - R_away)/400))
NBA에서는 홈코트 보정(HCA)을 Elo 차이에 더해주는 패턴이 흔합니다. FiveThirtyEight는 Elo 차이에 홈코트 보정 100을 더해 스프레드를 만드는 예시를 공개했습니다.
4.2 경기 후 Elo 업데이트(핵심)
업데이트는 보통 아래 형태입니다.
R_home' = R_home + K * MOV_mult * (S_home - E_home)R_away' = R_away - K * MOV_mult * (S_home - E_home)S_home은 홈팀이 이기면 1, 지면 0
FiveThirtyEight는 NBA에서 K=20을 사용한다고 명시했습니다.
또한 마진(점수차)을 반영하기 위해 "margin of victory multiplier"를 사용하며, 계산식을 구체적으로 공개했습니다.
MOV_mult = ((|MOV| + 3)^0.8) / (7.5 + 0.006 * elo_diff)elo_diff는 홈코트 보정까지 포함한 Elo 차이- 언더독이 이긴 경우
elo_diff부호 처리 규칙까지 설명되어 있음
4.3 시즌 리셋/평균회귀(운영에서 매우 중요)
시즌이 바뀌면 로스터/전술/부상 상태가 바뀌므로 “지난 시즌 Elo를 그대로” 가져가면 드리프트가 커질 수 있습니다.
FiveThirtyEight의 "pure Elo"는 시즌 시작 시 각 팀 Elo를 평균 1505로 1/4 만큼 회귀(revert)한다고 명시합니다.
R_team_start = R_team_end_prev + 0.25 * (1505 - R_team_end_prev)
이 아이디어는 “오프시즌 변화”를 단순히 흡수하는 최소한의 장치입니다.
4.4 NBA 특화 보정(선택): 휴식/이동/고도
FiveThirtyEight 2015-16 설명에서는 홈 보정(약 92점), 백투백 피로 페널티(약 46점), 장거리 이동 페널티(예: 보스턴→LA 2000마일에 약 16점), 덴버 등 고도 보정 같은 예시를 공개했습니다.
이걸 그대로 쓰지 않더라도, 휴식/이동/고도가 유의미한 신호라는 걸 "피처 설계 우선순위"로 가져갈 근거가 됩니다.
Why it matters: Elo는 "모델"이기 전에 "데이터 제품"입니다. 업데이트 규칙과 시즌 리셋을 문서화하지 않으면 팀원이 바뀌는 순간 재현이 깨지고, 운영 모니터링도 어려워집니다.
5) NBA 프리게임 피처 50개: 정의·계산·누수 방지 포인트
아래 피처는 “프리게임 P(home_win)”을 기준으로 합니다.
모든 rolling/누적 피처는 반드시 예측 대상 경기 이전 경기만 사용합니다(as-of time).
5.1 레이팅/강도 피처 (10)
elo_home/ 2.elo_away: 경기 직전 Eloelo_diff = elo_home - elo_awayelo_diff_hca = (elo_home + HCA) - elo_away(HCA는 상수 또는 시즌별 학습)elo_recent_change_home: 최근 N경기 Elo 변화량(추세)elo_recent_change_awayelo_winprob_base: Elo 기대값E_homeelo_spread_proxy: (Elo diff + HCA) / 28 형태의 스프레드 근사(538 예시)season_revert_applied: 시즌 시작 평균회귀 적용 여부(0/1)is_playoff: 플레이오프 여부(정규 vs PO는 분포가 달라질 수 있음)
누수 방지: 시즌 전체를 한 번에 계산하지 말고, 시간순으로 Elo를 업데이트하면서 그 시점의 Elo를 피처로 스냅샷 저장.
5.2 일정/휴식/이동 피처 (14)
rest_days_home: 홈팀 이전 경기 이후 휴식일(0이면 백투백)rest_days_awayrest_diff = rest_days_home - rest_days_awayb2b_home(0/1) / 15.b2b_awaygames_last_7_home: 최근 7일 경기 수games_last_7_awaythree_in_four_home: 4일 내 3경기 여부(0/1)three_in_four_awayfour_in_six_home/ 21.four_in_six_awaytravel_km_home: 홈팀이 직전 경기 도시→이번 경기 도시 이동거리(가능 시)travel_km_awaytimezone_change_away: 원정팀 시간대 변경(가능 시)altitude_home: 경기장 고도(가능 시) (덴버 등 특이)
근거(일정/휴식 효과): 백투백 대비 하루 휴식이 승리 가능성을 유의하게 높였다는 분석이 보고된 바 있습니다.
누수 방지: 일정 피처는 경기 날짜/도시만으로 계산 가능해야 하며, “시즌 종료 후 정리된 데이터”를 그대로 쓰지 않도록 주의.
5.3 팀 성과 Rolling 피처 (20)
아래는 팀 박스스코어에서 가장 흔히 쓰는 축입니다.
(A) 득실/마진 기반 (8)
home_pts_roll_N: 홈팀 최근 N경기 평균 득점home_opp_pts_roll_N: 홈팀 최근 N경기 평균 실점home_margin_roll_N = home_pts_roll_N - home_opp_pts_roll_Naway_pts_roll_Naway_opp_pts_roll_Naway_margin_roll_Nmargin_diff_roll_N = home_margin_roll_N - away_margin_roll_Nhome_winrate_roll_N/ 34.away_winrate_roll_N
(B) 페이스/효율 기반 (12)
home_ortg_roll_N: 최근 N경기 팀 공격 효율(포제션 100당 득점)home_drtg_roll_N: 최근 N경기 팀 수비 효율(포제션 100당 실점)home_nrtg_roll_N = ortg - drtgaway_ortg_roll_Naway_drtg_roll_Naway_nrtg_roll_Nnrtg_diff_roll_N = home_nrtg - away_nrtghome_pace_roll_N/ 43.away_pace_roll_Npace_diff = home_pace - away_pacehome_homecourt_nrtg_roll_N: 홈경기만 필터한 net ratingaway_road_nrtg_roll_N: 원정경기만 필터한 net rating
정의 근거: ORtg는 "100포제션당 득점"으로 설명됩니다.
누수 방지: N경기 rolling은 game_id 기준으로 정렬 후 shift(1) 적용(현재 경기 포함 금지).
5.4 선수/가용성 피처 (6, 선택)
선수 관련은 데이터 신뢰성이 핵심입니다(예측 시점에 알 수 있었던 정보만).
home_inactive_count: 홈팀 결장/의심(Questionable) 인원 수(가능하면 상태별 분리)away_inactive_counthome_top_minutes_out: 직전 N경기 평균 출전시간 상위 선수 중 결장 여부(0/1)away_top_minutes_out
누수 방지: “경기 후 확정된 결장 사유/라인업”을 끌어오면 거의 확실히 누수입니다. 반드시 “경기 전 공개된 상태”만.
6) 학습/검증/캘리브레이션: 시간 기준 분할 + 확률 품질 중심
6.1 시간 분할(권장)
- 시즌 홀드아웃: 예) 2021–2024 학습 → 2024–2025 검증
- 워크포워드: 학습/검증 윈도우를 조금씩 전진시키며 반복
6.2 지표
log_loss: 확률을 과신했다가 틀리면 크게 벌점brier_score_loss: 예측확률과 실제의 평균제곱차, strictly proper scoring rule
6.3 캘리브레이션(필수)
scikit-learn은 확률 보정(calibration)의 필요성과 방법(Platt/Isotonic)을 정리합니다.
Why it matters: NBA 예측에서 "0.62라고 말한 경기들이 실제로 62%로 이기느냐"가 맞아야, 임계값 정책(예: 0.60 이상만 추천)이 제품화됩니다.
7) 프리게임 파이프라인 Mermaid (배치 중심)
flowchart LR
subgraph S[Sources]
S1[Schedule / Game Results]
S2[Team Box Score]
S3[Player Availability optional]
end
subgraph I[Ingestion]
I1[Batch Collector]
I2[(Raw Storage)]
I3[(Warehouse)]
end
subgraph F[Feature Engineering - as-of time]
F1[Data Quality Checks]
F2[Elo Updater\n(time-ordered)]
F3[Rolling Team Metrics\nshift(1)]
F4[Rest/Travel Features]
F5[(Feature Store - Offline)]
end
subgraph T[Training]
T1[Time Split\n(season holdout / walk-forward)]
T2[Baseline\nElo+Logistic]
T3[GBDT Model\n(XGB/LGBM/HistGB)]
T4[Calibration\n(Platt/Isotonic)]
T5[Eval\nLogLoss/Brier/Calibration]
T6[(Model Registry)]
end
subgraph P[Prediction]
P1[Daily Batch Scoring\n(upcoming games)]
P2[(Predictions DB)]
P3[Dashboard/API]
end
subgraph M[Monitoring]
M1[Data Drift]
M2[Score Drift\n(LogLoss/Brier rolling)]
M3[Calibration Drift]
M4[Retrain Trigger]
end
S --> I1 --> I2 --> I3
I3 --> F1 --> F2 --> F3 --> F4 --> F5
F5 --> T1 --> T2 --> T3 --> T4 --> T5 --> T6
T6 --> P1 --> P2 --> P3
P2 --> M1 --> M4
P2 --> M2 --> M4
P2 --> M3 --> M4
8) 인게임 승리확률 확장: 상태 피처 + 스트리밍 파이프라인
인게임은 "경기 상태(state)"가 핵심입니다. 연구에서도 베이지안 방식으로 인게임 승리확률을 추정하는 방법을 제안합니다.
8.1 인게임 최소 피처(권장 시작점)
time_remaining(또는quarter,seconds_left_in_q)score_diff = home_score - away_scorepossession(공격권: home/away)- (추가)
team_fouls,bonus,timeouts_left,free_throw_shooter_pct등
"시간+점수차" 기반 로지스틱/스무딩 접근은 오래된 실무/분석 글에서도 반복적으로 등장합니다.
8.2 인게임 실시간 파이프라인 Mermaid (스트리밍/저지연)
flowchart LR
subgraph Live[Live Game Stream]
L1[PBP Events\n(score, clock, possession, fouls...)]
end
subgraph Stream[Streaming Layer]
K1[Message Bus\n(Kafka/Kinesis/PubSub)]
K2[Stream Processor\n(Flink/Spark Streaming)]
end
subgraph State[State Store]
S1[(Game State Store\nRedis/Key-Value)]
end
subgraph OnlineF[Online Feature Builder]
O1[State Update\nper event]
O2[Feature Vector\n(time, score_diff, possession...)]
end
subgraph Serve[Online Inference]
M1[Low-latency Model\n(Logistic/GBDT)]
M2[Online Calibration\n(optional)]
M3[(WinProb Output DB)]
M4[UI/API\nLive Win%]
end
Live --> K1 --> K2 --> O1 --> S1
S1 --> O2 --> M1 --> M2 --> M3 --> M4
Why it matters: 인게임은 "데이터 엔지니어링 난이도"가 급상승합니다. 그래서 프리게임 배치 파이프라인을 먼저 안정화하고, 그 다음 스트리밍을 별도 레인으로 확장하는 게 실패 확률이 낮습니다.
9) 구현 스니펫: Elo 업데이트 + 누수 없는 rolling 피처 생성
9.1 Elo 업데이트(538 공개 요소 반영 예시)
아래는 538이 공개한 K=20, MOV multiplier 아이디어를 반영해 구현 가능한 뼈대입니다.
import math
def expected_score(r_home, r_away, hca=0.0):
diff = (r_home + hca) - r_away
return 1.0 / (1.0 + 10 ** (-diff / 400.0))
def mov_multiplier(mov, elo_diff_hca):
# FiveThirtyEight MOV multiplier 구조 참고
# (|MOV|+3)^0.8 / (7.5 + 0.006*elo_diff_hca)
# elo_diff_hca는 홈코트 보정 포함 Elo 차이
return ((abs(mov) + 3) ** 0.8) / (7.5 + 0.006 * elo_diff_hca)
def update_elo(r_home, r_away, home_score, away_score, k=20, hca=0.0):
s_home = 1.0 if home_score > away_score else 0.0
e_home = expected_score(r_home, r_away, hca=hca)
mov = home_score - away_score
elo_diff_hca = (r_home + hca) - r_away
mult = mov_multiplier(mov, elo_diff_hca)
shift = k * mult * (s_home - e_home)
return r_home + shift, r_away - shift
9.2 시즌 평균회귀(538 pure Elo 예시)
시즌 시작 시 "평균 1505로 1/4 회귀" 예시는 아래처럼 구현할 수 있습니다.
def season_revert(r_end_prev, mean=1505.0, revert_frac=0.25):
return r_end_prev + revert_frac * (mean - r_end_prev)
9.3 누수 없는 rolling 피처 핵심 패턴
# df_team_game: team_id, game_date, pts, opp_pts ... (시간 오름차순)
df_team_game = df_team_game.sort_values(["team_id", "game_date"])
N = 10
df_team_game["pts_roll10"] = (
df_team_game.groupby("team_id")["pts"]
.apply(lambda s: s.shift(1).rolling(N).mean())
)
df_team_game["opp_pts_roll10"] = (
df_team_game.groupby("team_id")["opp_pts"]
.apply(lambda s: s.shift(1).rolling(N).mean())
)
df_team_game["margin_roll10"] = df_team_game["pts_roll10"] - df_team_game["opp_pts_roll10"]
결론 (요약 정리)
- NBA 프리게임 예측은 Elo 베이스라인으로 파이프라인을 먼저 검증하고, 일정/휴식/효율 지표로 피처를 확장한 뒤 GBDT로 고도화하는 흐름이 안전합니다.
- 일정/휴식(백투백/휴식일/이동)은 성과에 영향을 준다는 연구 보고가 있어, 초기 피처 우선순위로 넣을 가치가 큽니다.
- 확률 모델의 제품화는 LogLoss/Brier + 캘리브레이션이 핵심이며, scikit-learn 문서가 방법론을 정리합니다.
- 인게임 확장은 별도 스트리밍 파이프라인을 두고, 상태 피처(시간/점수차/포제션)부터 시작하는 게 현실적입니다.
References
- (How We Calculate NBA Elo Ratings, 2015-05-21)[https://fivethirtyeight.com/features/how-we-calculate-nba-elo-ratings/]
- (How Our NBA Predictions Work, FiveThirtyEight)[https://fivethirtyeight.com/methodology/how-our-nba-predictions-work/]
- (How Our 2015-16 NBA Predictions Work, 2015-12-07)[https://fivethirtyeight.com/features/how-our-2015-16-nba-predictions-work/]
- (Probability calibration, scikit-learn Docs)[https://scikit-learn.org/stable/modules/calibration.html]
- (log_loss, scikit-learn Docs)[https://scikit-learn.org/stable/modules/generated/sklearn.metrics.log_loss.html]
- (brier_score_loss, scikit-learn Docs)[https://scikit-learn.org/stable/modules/generated/sklearn.metrics.brier_score_loss.html]
- (Terms of Use, NBA.com)[https://www.nba.com/termsofuse]
- (nba_api NBA.com API client, GitHub)[https://github.com/swar/nba_api]
- (Effect of travel and rest on performance of professional basketball players, 1997-01-01)[https://pubmed.ncbi.nlm.nih.gov/9381060/]
- (Basketball performance is affected by the schedule congestion cycles, 2021-03-10)[https://pubmed.ncbi.nlm.nih.gov/32172667/]
- (Glossary, Basketball-Reference)[https://www.basketball-reference.com/about/glossary.html]
- (Bayesian estimation of in-game home team win probability for college basketball, 2022-04-26)[https://arxiv.org/pdf/2204.11777]
- (Elo rating system, Wikipedia)[https://en.wikipedia.org/wiki/Elo_rating_system]
- (Modeling Win Probability for a College Basketball Game, Wages of Wins Journal)[https://dberri.wordpress.com/2009/03/05/modeling-win-probability-for-a-college-basketball-game-a-guest-post-from-brian-burke/]
'AI > Trend' 카테고리의 다른 글
| Coforge-Encora 23.5억달러 인수, AI 엔지니어링 판이 커진다 (4) | 2025.12.28 |
|---|---|
| 중국 ‘인간처럼 상호작용하는 AI’ 규제 초안 핵심 정리(과몰입 경고·개입 의무) (1) | 2025.12.28 |
| GPT-5.2-Codex·Gemini 3 Flash 출시와 ‘AI의 사회적 파장’ (4) | 2025.12.27 |
| 이탈리아 반독점 당국, Meta에 WhatsApp 타사 AI 챗봇 차단 중단 명령 (2) | 2025.12.27 |
| 존 캐리루 저작권 소송: 6대 AI 기업 LLM 훈련 데이터 쟁점 (4) | 2025.12.27 |