경영선수(競艇)의 확률 모델 구축 경험: 시장을 이기는 특징량과 그렇지 못한 특징량
요약
경영선수 레이스 확률 모델 구축 경험을 공유하며, 시장(Market)이라는 강력한 기준선을 넘어서는 특징량 설계의 중요성을 강조합니다. 120가지 조합의 확률 분포를 예측하기 위해 LightGBM 기반의 다단계 분류 구조와 사전/직전 버전 학습 전략을 적용했습니다.
핵심 포인트
- 시장보다 나은 가치에 집중하는 특징량 시각화가 핵심입니다.
- 다단계 이진 분류(LightGBM)를 조합하여 120가지 확률 분포를 예측합니다.
- 사전 모델과 당일 전시 기록 기반의 추가 학습을 분리하여 안정성을 확보했습니다.
개인적으로 경영선수(竞艇)의 모든 레이스 확률을 제공하는 사이트 '이치마크(イチマーク)'를 만들고 있습니다. 첫 번째 글에서는 확률이 액면가와 정확히 일치했던 경험에 대해 다루었고, 두 번째 글에서는 '시장보다 정확하지만 배팅으로는 이길 수 없는' 이야기를 썼습니다. 이번에는 모델을 어떻게 구축했는지에 대해 쓰겠습니다.
기계 학습(Machine Learning)을 하는 분들을 위한 내용입니다. 공영 경기 예측은 답맞히기 상대로서 '시장(Market)'이라는 강력한 기준선(baseline)이 매 레이스마다 공개되는, 다소 독특한 문제입니다. 그 부분이 가장 흥미로웠습니다. 일반적인 Kaggle 대회에서는 '기준선을 초과하는 것'으로 끝날 수 있지만, 여기서는 처음부터 '시장을 초과하는 부분만이 가치'라는 잣대가 정해져 있어 특징량(feature)을 보는 시각 자체가 달라졌습니다.
문제
6척(艇) 중 1착부터 3착까지의 순서를 맞히는 삼연단(三連単, 3-win combination)은 총 120가지 조합이 있습니다. 레이스는 연간 약 5만 건입니다. 우리가 구해야 하는 것은 이 120가지 조합 각각의 확률 분포입니다.
d이터는 공식적인 과거 성적을 1997년부터 전부 공개되어 있어, 프로그램표(전날 나오는 출주표), 경주 성적(착순, 진입 코스, 스타트 타이밍, 결정 기술, 날씨)을 가져올 수 있습니다. 당일에는 전시 항주(本番前試走, 본 경기 전 시범 주행) 기록이 마감 15분경에 나오고, 그 외에는 마감 전의 배당률(odds) 정보가 있습니다.
평가는 로그 손실(log loss)을 사용합니다. 맞춘 조합에 부여했던 확률의 음의 로그 평균입니다. 확정된 배당률에서 역산한 시장의 확률이 약 3.63이고, 균일 분포(uniform distribution)라면 4.79가 되며, 모델은 '시장보다 낮은지'를 기준으로 판단합니다. 회수율로 측정하지 않는 이유는, 같은 정확도를 가진 모델 두 개를 만들어서 과거 데이터로 회수율을 비교했을 때 각각 82%와 127%가 되었기 때문입니다. 베팅 조합이 소수의 고배당에 의해 좌우되기 때문에, 1~2개의 적중으로 모든 것이 바뀝니다. 따라서 모델 비교에는 사용할 수 없습니다.
구조
120가지 조합을 직접 분류하는 것은 포기하고, 세 단계로 나누었습니다.
- 각 척의 1착률 (6척 기준으로 정규화)
- 1착을 고정했을 때, 나머지 5척의 2착률
- 1착과 2착을 고정했을 때, 나머지 4척의 3착률
각각 LightGBM의 이진 분류(binary classification)를 사용하고, 이를 곱하여 120가지 조합을 만듭니다. 2착과 3착 모델에는 '남은 척 중에서의 상대값'을 넣었는데, 이는 거의 효과가 없었습니다. 1착 모델이 가진 정보와 중복되는 것 같습니다.
또 다른 축은 사전 버전(事前版)과 직전 버전(直前版)입니다. 사전 버전은 전날의 정보만으로, 프로그램표와 과거 성적에서 만든 특징량입니다. 1997년부터의 모든 레이스에서 학습할 수 있습니다. 직전 버전은 여기에 당일 전시 기록을 추가한 것이며, 전시 데이터는 2024년 3월 이후에나 얻을 수 있어 학습에 사용할 수 있는 건수만 약 4.4만 건밖에 없습니다.
그래서 직전 버전은 사전 버전 위에 쌓는 형태로 만들었습니다. 사전 버전의 예측 로짓(logit)을 특징량으로 넣고, 전시 열(column)을 추가하여 적은 데이터로 '추가 학습'만 하도록 합니다. 여기서 한 단계 더 나아간 점은, 기반 모델과 추가 학습 기간을 분리해야 한다는 것입니다. 기반 모델이 추가 학습 기간을 보고 학습하면, 추가 학습 부분이 기반 모델의 오차에 맞춰지려다가 본 경기에서 무너집니다. 따라서 기반 모델은 2024년 3월 이전 데이터로 학습하고, 추가 학습은 그 이후부터 직전 2개월을 제외(打ち切り) 및 보정(較正)에 사용하도록 분리하는 방식으로 결정했습니다.
특징량
사전 버전에는 약 110개의 열이 있습니다. 틀(艇番과 예상 코스), 선수(급별, 승률, 연령, 체중, 직전 코스별 성적, 평균 ST와 그 분산, F 보유 여부, 전진 경향, 장소와의 적성), 모터(프로그램표의 2연대율에 더해, 기수력을 제외한 성적과 과거 전시 타임의 6척 평균과의 차이), 이번 절의 착순 배열, 레이스 종류나 거리나 나이트어 여부, 장소의 코스별 1착률, 그리고 위의 각 항목들에 대한 '6척 중에서의 순위와 평균과의 차이'입니다.
직전 버전에서 추가하는 것은 전시 타임, 스타트 전시 시 진입과 ST, 장소가 제공하는 독자적인 전시 데이터(한 바퀴, 돌아가기 발, 직선), 틸트(tilt), 부품 교체, 조정 중량, 같은 날의 전 경기, 해수 기상입니다.
여기까지는 아마 누가 만들어도 비슷한 결과가 나올 것입니다.
효과를 본 것은 '평소와의 차이'
전시 타임은 누구나 볼 수 있고, 6척 중에서 빠르거나 느린지도 누구나 알 수 있습니다. 그래서 시장은 이를 이미 반영하고 있습니다. 모델에 전시 타임의 순위를 넣어도, 모델 단독으로는 좋아지지만, 시장과 결합했을 때 개선이 거의 나타나지 않았습니다. 이 사실을 깨닫기까지 시간이 좀 걸렸습니다.
효과를 본 것은 전시 타임을 '그 선수의 평소'와 비교한 열입니다. 전시 타임의 6척 평균과의 차이에서, 그 선수의 과거 180일 동안 같은 값의 평균을 빼줍니다. 전날까지의 기록으로, 출전 횟수가 적은 선수는 0으로 축소합니다. 비슷한 방식으로 모터 이력, 한 바퀴/돌아가기 발/직선, 스타트 전시 ST에서도 만들었습니다.
| 기준 | 평소와의 차이를 추가 | |
|---|---|---|
| 모델 단독의 로그 손실 | 3.7352 | 3.7296 |
| 시장과 결합한 것 − 시장 | -0.0144 | -0.0164 |
차이는 −0.0020(95% 범위에서 −0.0029〜−0.0011)입니다. 수치로는 작지만, 이 문제에서 '시장과의 차이'를 0.002 움직일 특징량은 드뭅니다. 상乗세 모델의 이득을 분해해보니, 6%가 이 열(선수의 평소와 전시 시간의 차이)에서 나왔습니다.
논리는 나중에 붙인 것이지만, 아마도 이렇습니다. '전시가 빠르다'는 시장에서도 보는 것입니다. '이 선수치고는 빠르다'는 180일 분량의 기록이 없으면 알 수 없고, 시장 참여자 대부분은 그렇게 가지고 있지 않습니다. 시장과의 차이를 벌린 것은 긴 기록에서 만드는 그 선수・그 모터의 '평소'였습니다.
옆 배와의 차이는 모델에는 효과가 있지만 시장에는 효과가 없다
각 배에 안쪽과 바깥쪽 옆 배와의 차이(자신 − 옆)를 더했습니다. 사전 버전이라면 평균 ST나 코스별 성적, 직전 버전이라면 전시 시간이나 출발 전시의 ST입니다. ST는 '자신보다 바깥에서 가장 빠른 배와의 차이'도 포함했습니다.
모델 단독으로는 −0.0073으로, 시도한 것 중 가장 큰 개선이었습니다. 기뻤지만, 시장과 섞으니 차이가 거의 벌어지지 않았습니다. 옆과의 관계는 경주권을 사는 사람들이 보통 보는 것이었다는 뜻입니다. 모델 단독의 개선과, 시장과의 차이의 개선은 별개로 다루어야 합니다. 이것을 알게 된 것은 이 특징량 덕분이었습니다.
섞는 방법과 상乗세 설정
모델의 확률 p와 시장의 확률 q는 c ∝ q^b × (p/q)^a 로 섞고 있습니다. b≈1.06, a≈0.48입니다. 시장을 토대로, 모델과 시장의 차이를 절반만 채택하는 형태입니다. 처음에는 섞는 비율을 0.1 간격으로 정했다가 연속적인 값으로 다시 정했을 뿐인데, 시장과의 차이가 −0.0144에서 −0.0159로 바뀌었습니다. 사소하지만, 섞는 방법의 최적화는 특징량 하나 분량의 가치가 있었습니다.
상乗세 모델은 4.4만 경주에서만 학습할 수 있기 때문에, 설정은 단순한 것이 좋았습니다. Leaf63・학습률0.03에서 Leaf31・학습률0.01로 변경하여, 시장과의 차이가 −0.0164 → −0.0174가 되었습니다. 설정을 고르는 것은 조정 기간일 뿐이고, 평가 기간에서는 선택한 것 하나만 보는 것을 지켰습니다.
효과가 없었던 것들
여기가 사실 가장 쓰고 싶은 부분입니다. '모델 단독을 좋게 하는 것'과 '시장과의 차이를 벌리는 것'은 별개이며, 후자를 움직이는 것은 매우 적습니다.
| 시도한 것 | 모델 단독 | 시장과의 차이 | 무슨 일이 일어나고 있었는가 |
|---|---|---|---|
| 옆 배와의 차이 | −0.0073 | 거의 0 | 시장이 이미 반영함 |
| ... | |||
| 풍은 조금 아쉬워서 보충합니다. 공식 성적으로 55만 경주를 집계하면, 풍속 6m 이상에서 1코스 1착률은 58%에서 48%로 떨어지며, 순풍일 경우 2코스, 맞바람일 경우 3~6코스가 유리합니다. 장소별 차이도 커서, 와카마츠는 무풍에서 5m 이상일 때 −15.7pt, 에도가와는 −1.4pt입니다. 단발로 보면 명확하게 강한 상관관계가 있습니다. 그래도 모델에 넣으면 효과가 보이지 않습니다. 장소×코스별 1착률의 열이 그 장소의 전형적인 바람 영향을 이미 포함하고 있어서, 남는 것은 '그날의 바람이 평소와 다른 부분'만입니다. 그것은 작았습니다. |
조위는 기상청의 조석표를 전장 분량으로 모아서 시도했지만, 결과는 '장소마다 효과가 달라서 정리되지 않는다'였습니다. 모은 노력에 비해 남은 것은, 조의 시간만으로는 의미가 없고 그 시간대의 수위가 필요하다는 교훈뿐이었습니다.
난수 씨앗을 바꾼 5개 모델의 평균이 완전히 0이었던 것은 오히려 놀라웠습니다. LightGBM이 이 데이터에서는 씨앗에 거의 변하지 않을 정도로 안정적이었다고 생각합니다.
평가에서 빠진 함정
같은 기간을 20번 봤다. 하룻밤 사이에 20개 정도 실험을 돌려서, 전부 2025년 후반으로 평가했습니다. 당연히 이것은 선택 편향(selection bias)이며, 최종 모델(B3라고 부릅니다)의 '시장과의 차이 −0.019'에는 낙관론이 조금 들어 있습니다. 대책으로, 2024년 9월부터 2025년 12월을 두 달 간격으로 8 기간에 나누어, 기간별로 그 이전 데이터를 가지고 재학습하는 시계열 평가를 나중에 만들었습니다. B3의 시장과의 차이는 8 기간 모두에서 −0.019〜−0.024였습니다. 시기에 따라 변하지 않는 것은 안심할 만한 자료였습니다.
게다가, 2026년 1월~9월 데이터는 봉인했습니다. 사전 등록 문서에 'B3를 2026년 데이터로 한 번만 측정한다'고 적고, 평가 스크립트는 명시적인 플래그가 없으면 작동하지 않도록 했습니다. 모델을 더 이상 만지지 않기로 결정한 후에, 딱 한 번 열 예정이며, 아직 열지 않았습니다.
행의 순서가 실행마다 달랐다. 확률 행렬을 레이스별로 저장하고, 다른 실행의 행렬과 더했더니 결과가 이상했습니다. 반나절 동안 쫓아간 끝에 보니, groupby의 순서가 보장되어 있지 않아서, 행의 순서가 실행마다 달랐던 것입니다. 따로 만든 표는 레이스의 키(key)로 행을 맞춘 다음에 더합니다. 당연한 일이지만, polars에서 group_by에 maintain_order=True를 붙이지 않은 부분이 하나 남아있었기 때문에 발생했습니다.
회수율에 여러 번 속았다. 처음에는 회수율(回収率)로 특징량(feature)을 골랐고, '효과가 있었다'고 생각했던 것이 다음 주에는 사라지는 것을 반복했습니다. 로그 손실(log loss), 게다가 '시장과의 차이를 일 단위의 부트스트랩으로 폭넓게'에 초점을 맞춘 후에야 비로소 실험 결과가 재현되기 시작했습니다.
정체기 (頭打ち)
여기까지 시장과의 차이는 −0.0144에서 −0.0194까지 벌어졌습니다. 약 35% 정도입니다. 그 이후의 실험은 건당 ±0.0005였고, 95% 폭이 0을 넘나드는 것들뿐이었습니다. '같은 정보를 재배열하는' 특징량은 다 써버렸고, 남아있는 것은 '시장이 가지고 있지 않은 정보'를 만드는 것입니다.
후보로는 세 가지가 있습니다. 마감 전 오즈의 시계열(수집 시작 후 3주), 전시 항주 영상(아무도 수치화하지 않았지만 만들기는 무거운 작업), 그리고 단순히 시간입니다. 정확도에 대한 것은 당분간 미루기로 했습니다.
도구 (道具)
Python 3.12, polars, LightGBM을 사용합니다. 특징량 집계는 모두 polars로 처리하며, 1997년부터의 약 130만 대(艇) 분량이 수십 초 만에 끝납니다. 실험은 각각 experiment_*.py로 만들고, 결과는 장부(Markdown 표)에 남깁니다. 효과가 없었던 것도 모두 적어두면 나중에 '저거 시도했었나' 하는 일이 없어집니다.
실제 운영 환경은 EC2의 t4g.small 1대와 S3 + CloudFront의 정적 사이트입니다. 월 약 1,000엔 정도 비용이 듭니다. 수집은 마감일로부터 역산한 일정표에 따라 움직이고 있으며, 마감 전 12 시점의 오즈와 전시 데이터를 계속 가져가고 있습니다.
요약 같은 것 (まとめのようなもの)
시장을 벤치마크로 삼으면, 특징량의 가치는 '모델이 좋아지는가'가 아니라 '시장이 모르는 것을 더할 수 있는가'로 측정됩니다. 이 자(物差し)로 보면, 대부분의 특징량은 시장의 재진술에 불과했고, 효과를 본 것은 긴 히스토리에서 만드는 '평소' 데이터뿐이었습니다.
얻게 된 −0.02는 확률적으로는 진짜이지만, 두 번째에 쓴 것처럼 경마표(舟券)로 이기기에는 부족합니다. 그럼에도 불구하고 공영 경기(公営競技)는 '정답과 비교되는 예측 문제'로서 매우 좋은 연습장이라고 생각합니다.
Discussion

AI 자동 생성 콘텐츠
본 콘텐츠는 Zenn ML의 원문을 AI가 자동으로 요약·번역·분석한 것입니다. 원 저작권은 원저작자에게 있으며, 정확한 내용은 반드시 원문을 확인해 주세요.
원문 바로가기