K-Stuff+ 구위모델 만들기

KBO 리그를 위한 구위 모델 만들기의 여정

by randahlia

WBC중계를 하며, tjStuff+ 라는 것을 알게 되었다. 공 하나하나 마다 '구위 지수'가 표시되었다. 누가 얼마나 좋은 공을 던지는지를 표시해 주는게 매우 신선했다. Pitchfx 초기 시절에 MLB는 Nasty index라는 것을 만든적이 있는데, 코스나 볼배합에 따른 일종의 구위 지수였다. 지금은 사장된것 같다.

어쨋든, tjStuff가 유명해진 이유는 간단했다.

'한국 선수의 정보들이 나왔기 때문'

조병현의 포심 패스트볼은 WBC 데이터를 기준으로도 110을 상회하는 - 평균이 100 - 뛰어난 구위 수치를 보였다. 이 얼마나 직관적이고 훌륭한 지표인가.

다행히도 tjStuff의 원작자 Thomas Nestico가 3.0 모델(2024년 버전) 까지에 대해서는 개발 과정을 본인의 블로그에 공개해둔 터라, 상당히 많은 힌트를 얻을 수 있었다. 바로 한국의 스터프+를 만들어 보기로 했다.


첫번째 벽 : 데이터가 다르다


MLB에는 Statcast가 있다. 회전수(spin rate), 회전축(spin axis), 3차원 트래킹 데이터가 완비되어 있다. tjStuff+는 이 풍부한 데이터 위에 세워진 모델이다. (비상업적 활용을 전제로)KBO리그 데이터를 통해서도 구속, 가속도, 릴리스 포인트, 초기 속도 벡터를 확보할 수 있다. 하지만 회전수와 회전축이 없다. 이것은 단순히 변수 하나가 빠진 수준이 아니다. 스핀은 공의 무브먼트를 결정하는 핵심 원인이기 때문이다.


그래서 우회로를 찾았다. 두 가지 방법으로 회전 정보를 *간접 추정*한다.

첫째, Magnus 가속도 역산. PITCHf/x 물리학에 따르면, 공에 작용하는 가속도는 `중력 + Magnus(스핀) + Drag(공기저항)`으로 분해할 수 있다. 트래킹 데이터에는 총 가속도(ax, az)가 측정되어 있고, 공기저항은 y방향 가속도(ay)로부터 추정할 수 있다.(y방향(투수→포수)에는 공기저항 외에 작용하는 힘이 거의 없다. 이 특성을 이용해 공기저항 계수를 추출하고, 전체 가속도에서 중력과 공기저항을 빼면 순수 스핀 효과만 남는다. 물론 자이로스핀 타입의 구질을 제대로 측정할수 없다는 단점이 있다) 중력은 상수다. 세 가지를 빼면 순수 스핀 효과만 남는다. 회전수 자체는 모르지만, 회전이 공의 궤적에 미친 영향은 일정 수준 이상의 신뢰도로 역산할 수 있다.(100% 정확하다고 할 수는 없는 것이, 유체의 역학에는 매우 다양한 요소가 작용하기 때문이다)

둘째, VAA(Vertical Approach Angle). 공이 홈플레이트를 통과하는 순간, 수직으로 얼마나 가파른 각도로 들어오는지를 나타내는 값이다. 릴리스 시점의 속도와 가속도를 사용해, 등가속도 운동 모델로 플레이트 도착 시점의 실제 속도 벡터를 계산한다.

대부분의 투구는 마운드와 홈플레이트 간의 높이 차이와 중력의 영향으로 음수(-)의 VAA를 갖는다. 값이 0에 가까울수록 공이 'flat'하다고 표현한다. 뚝 떨어지는 유인구는 VAA의 절대값이 크고(가파름), 라이징 패스트볼은 VAA가 작다(플랫함). 실제로 떠오르는 공이 있다면 양수가 되겠지만, 인간이 던지는 공에서 그런 일은 거의 발생하지 않는다.(아래에서 위로 의도적으로 던지지 않는 이상)


모델은 무엇을 예측하는가


"좋은 공"이란 무엇일까? 삼진을 잡는 공? 헛스윙을 유도하는 공? 약한 타구를 만드는 공?

K-Stuff 모델은 이 질문에 실점 기여도(Run Value)로 답한다. 야구에는 24가지 상황(주자 × 아웃카운트 조합)이 있고, 각 상황의 기대 실점이 계산되어 있다. 무사 1루의 기대 실점은 약 0.85점이고, 2사 주자 없음은 약 0.10점이다. 투구 하나하나가 이 기대 실점을 얼마나 바꾸는지 — 이것이 delta Run Expectancy다.

스트라이크를 하나 잡으면 기대 실점이 줄어든다(투수에게 좋다). 볼을 하나 던지면 늘어난다(투수에게 나쁘다). 안타를 맞으면 크게 늘어나고, 삼진을 잡으면 크게 줄어든다.


K-Stuff+ 모델의 학습 목표는 각 투구의 실점 기여도(Run Value)다. 이것은 FanGraphs의 Pitch Value와 같은 뿌리를 가진다 — 24가지 주자/아웃 상황에서 투구 하나가 기대 실점을 얼마나 바꾸는지를 수치화한 것이다. 다만 Pitch Value는 실제 결과를 그대로 반영하는 반면, K-Stuff+는 이 값을 정답지로만 사용한다. 모델은 공의 물리적 특성만 보고 '이런 공이라면 평균적으로 실점 기여도가 이 정도일 것이다'를 학습한다. 그래서 같은 공이라도 Pitch Value는 결과에 따라 매번 달라지지만, K-Stuff+는 항상 같은 점수를 준다. 결과가 아니라 공 자체를 평가하기 때문이다.


모델은 어떤 정보를 사용하는가


모델에 투입되는 15개의 변수는 크게 세 층위로 나뉜다.


1층 : 원시 물리량 - "이 공은 어떻게 움직이는가"

- 구속, 수평/수직 가속도, 릴리스포인트, 스핀에 의한 총 가속도 크기(추산), 수직 변위(pfx_z)


2층 : 터널링 피처 - "같은 투수의 패스트볼 대비 얼마나 다른가"

- 투수의 패스트볼 평균값과 해당 투구의 구속 차

- 수직가속도 차이

- 수평가속도 차이

- 스핀 효과의 차이


체인지업의 절대 구속이 130km라는 사실보다, 이 투수의 패스트볼 대비 20km 느리다는 정보가 타자의 체감 난이도를 더 잘 설명할 수 있다. tjStuff+의 토마스 네스티코도 이 점을 강조했다.

시퀀싱의 핵심은 상대적 차이다.


3층 : 궤적 피처 - "공이 어떤 각도로 들어오는가"

- VAA 잔차 : 같은 높이의 평균 공 대비 이 공의 진입 각도는 얼마나 다른가

- 패스트볼 대비 진입각 차이 : 쉽게 말하면 '터널링 진입각의 차이' 라고 할수 있다.

- 존 상단/하단 도달 여부


VAA를 그대로 쓰지 않고 잔차를 사용하는 이유가 있다. 높은 존으로 들어오는 공은 자연스럽게 진입이 완만하고, 낮은 존으로 들어오는 공은 자연스럽게 가파르다. 이 물리적 당연함을 제거해야 '같은 높이인데 유독 플랫하게 들어오는 패스트볼'이나 '같은 높이인데 유독 급격히 떨어지는 커브'를 구분할 수 있다. 폴 스킨스가 던지는 하이 패스트볼과 고영표가 던지는 하이 패스트볼은 같은 높이에 도달하더라도 진입 각도가 다르다. VAA잔차는 이 차이를 잡아내기 위한 장치다.


모델은 어떻게 학습되는가


스터프+는 lightGBM모델을 사용한다. 단순화 하자면 아래와 같다.

1. 첫번째 트리가 대략적인 예측을 한다

2. 잔차에 대해 두번째 트리가 보정을 한다

3. 그 잔차를 세번째 트리가 다시 보정한다

4. 이것을 800번 반복한다.


아래 그림은 하나의 트리에 대한 예시이다(실제 모델으 수치가 아니다)

스크린샷 2026-04-03 20.29.02.png

각 트리는 "구속이 145이상이고 VAA가 -5보다 작으면 실점 기여도가 낮다"와 같은 규칙을 학습한다. 800개의 트리가 협력하면 개별 규칙으로는 잡을 수 없는 복잡한 패턴을 포착할 수 있다. tjStuff+도 동일한 LightGBM을 사용한다. 팬그래프는 Neural Network, 드라이브라인 피칭봇은 Mixture of Experts를 사용하지만, 핵심 아이디어는 모두 같다.

물리량 -> 실점 기여도 예측


하이퍼파라미터 튜닝 : Optuna

모델에는 "몇 개의 트리를 쓸 것인가", "각 트리의 깊이는 얼마인가", "얼마나 빠르게 학습할 것인가"등에 대한 설정들이 저장되어 있다. 이것을 하이퍼파라미터라고 한다. Optuna라는 자동 최적화 도구를 사용해 최적의 조합을 찾았다. 2021-2023년 데이터를 학습시켜 2024 데이터로 검증하는 형태로 진행했다.(2025년 구속측정시스템 변화로 구속이 갑자기 1.6km/h 정도 상승했다. 이 부분에 대해서는 추후 재학습을 통해 보정해 나가야 한다)

스크린샷 2026-04-03 20.29.48.png


점수는 어떻게 매겨지는가


모델이 예측한 실점 기여를 바로 컨텐츠화 하면 받아들이는 팬의 입장에서는 매우 이해하기 어렵다. 매 투구당 실점 기여도는 매우 작은 숫자이기 때문이다.

이번 투구는 0.001점을 아끼는 효과가 있는 구위다

라는 말이 와닿을 수가 없다. 그래서 이러한 수치들에 대해서 Z-score 정규화를 적용하게 된다. 100점이 해당 구종의 리그 평균. 110이면 상위 16%, 120이면 상위 2.5%가 된다. 그래서 대부분의 투수가 90점에서 110점 사이에 위치하며, 거의 구분하기 힘들어진다. 지표 공개후 잠시 probit 스케일링을 적용하여 이것을 80-140기반으로 업스케일링 하였으나 오히려 분포가 20~170점이라는 예상치 못한 수준의 극단적 결과가 나와서 z-score 형태로 모두 되돌렸다.

10_probit_vs_zscore.png

비슷한 분포지만, 20~200인 상태와 70~130인 상태는 받아들이는 입장에서 매우 다를것이라고 본다. 콘텐츠로서의 극적효과를 위해 조정했던 스코어가 오히려 콘텐츠의 신뢰도를 낮추게 되는 결과를 초래했다.


실제 스터프와 로케이션 지수에 대한 분포는 아래와 같다.

01_season_distribution.png


이 모델은 얼마나 믿을 수 있는가


지표는 결국 '신뢰도'에 대한 답을 내놓아야 한다. 나는 이것을 세가지 기준으로 검증했다.

검증1 : 재현성 - 올해 좋은점수를 받은 투수가 내년에도 좋은 점수를 받는가?

02_yoy_stickiness.png

실제 YoY Stickness는 평균적으로 0.80을 기록했다. tjStuff+의 연도간 상관계수는 0.85 수준으로, 회전수 및 회전축 데이터 없이 이정도 수치를 기록했다는 점에서 충분한 성과라고 생각한다. 현재의 스터프+ 지수의 높은 재현성에 대해 개인적으로는 '현재 데이터로 뽑아낼 수 있는 한계치' 정도라고 판단한다.


검증2 : 설명력 - 같은 시즌에 높은점수의 투수가 실제로 좋은 성적을 내는가?

어찌보면 가장 중요한 검증지표일 것이다. 이 단계는 아직 일어나지 않은 미래에 대한 예측력을 검증하는 것이다.

11_predictive_correlation.png

다음 시즌 삼진율에 대한 상관계수는 0.42, K-BB%대해서는 0.35의 상관계수를 기록했다. 올해 스터프+로 내년 삼진율의 17%를 설명(0.42 * 0.42 = 0.17)할 수 있다는 뜻이다. 대단해 보이지 않을 수 있지만, 대부분의 클래식 지표들이 이 이상의 설명력을 갖지 못한다. ERA의 상관계수가 0.3, SIERA가 0.45 수준이다.


100점이 평균이면, 분포가 너무 좁지 않은가?


이 질문은 반드시 짚고 넘어갈 부분이다. 매 투구마다 스터프/로케이션을 계산하는데, 투구별 표준편차는 10 정도로 매우 넓은 편이다. 하지만 시즌 전체 평균에서는 대부분의 투수가 95~105사이에 모인다. 108이면 최상위, 93이면 최하위 수준이다.

06_scaling_levels.png

한 구 한구는 변동이 크지만, 시즌 수백 수천개의 공을 평균하면 자연스레 평균에 수렴학 된다. 숫자 자체는 좁아 보이지만, 그 안에서의 차이는 꽤나 큰 편이다.


이 지표가 말하는 것, 그리고 말하지 않는 것.


K-Stuff+는 이 투수가 던지는 공 자체가 타자에게 얼마나 어려운가를 말하는 지표다. 구속, 무브먼트, 릴리스포인트, 터널링 효과, 진입각도 등 물리적 요소를 조합해서 학습시킨 모델이 판단한 공의 품질이다. 하지만 이것으로

- 제구력 : 좋은 공을 어디에 던지는가? 하는 질문에 답하지 못한다

- 멘탈/상황대응 : 풀카운트, 득점권에 강한 투수와 그렇지 못한 투수를 구분하지 못한다

- 타자 품질에 대한 보정 : 엘리트 타자와 2군급 타자를 상대한 결과가 동일하게 취급된다

- 시퀀싱의 전체 : 패스트볼 대비 차이는 반영하지만 "3구 연속 같은 코스에 던진뒤 빠지는 공"같은 고차원적 시퀀싱은 포착하지 못한다


이 지표를 신뢰하는 것은 여러분의 자유다. 하지만 적어도, 어떤 이론적 기반으로 어떠한 기술을 통해, 어떠한 검증을 거쳐 만들었는지 정도는 밝히고 싶었다.


지표는 www.kbostuff.app 에서 확인할 수 있다.

매거진의 이전글WBC 해설을 준비하며