開発中のプロジェクトです。掲載している画面は、表示を確認するための合成データ(架空のクラブ)で動かしたものです。
なぜ作ったか
対象は、B.LEAGUE のファン、特定のクラブのサポーター、そしてデータが好きなバスケファンです。
試合前に勝敗の見込みを数字で示し、なぜその予測になったのかを日本語で説明します。さらに、自分の予測がどれだけ当たったかをモデルの版ごとに公開し、外れた試合も隠しません。「よく当たる」と言い張るのではなく、「70%と言った試合が実際に約70%勝っている」ことを見せて信頼してもらう、という考え方で作っています。賭けに関する機能は持ちません。
主な機能
- 勝敗確率:両チームの勝つ確率を示します。
- 予想スコア:得点差と合計得点の予測から、試合のスコアを示します。
- 個人スタッツの予測:出場時間・得点・リバウンド・アシストから始め、段階的に広げます。
- 予測の根拠:チーム力の差、休養日数、主力の欠場などの要因ごとに、どちらに有利に働いたかを示します。
- 的中率の公開:モデルの版ごとの推移と、予測した確率がどれだけ正確だったかを公開します。
設計の判断
試合が始まったら、予測を二度と書き換えられない構造
- 課題
- 試合の後で予測を直せるなら、「的中率を隠さず公開する」という約束そのものが成り立たない。アプリのバグや操作ミスでも過去の予測が変わってはいけない。
- 選んだ方法と理由
- データベース(D1)のトリガで、確定済みの予測の行に対する更新と削除を拒否する。予測本体と子テーブル4つのすべてが対象。毎時の Cron で、試合開始を迎えた予測を子から親の順に確定させる。開始時刻を過ぎた試合への書き込みは API が拒否し、予測をやり直すときも上書きせず、版を1つ上げた新しい行として追加する。書き込みは必ず Workers を通し、データベースを直接触る経路は作らない。
- 結果
- アプリ側に不具合があっても、データベースの層で「過去の予測は変わらない」ことを保証できるようになった。本番のデータベースに適用済み。
「よく当たる」ではなく「確率が正確」かで評価する
- 課題
- 勝ち負けの的中率だけでは、「70%」と言った予測を信じてよいかは分からない。また、評価に使える試合数では、的中率の差を統計的に見分けにくい。
- 検討した選択肢
- 的中率でモデルの採用を判定する
- 確率の正確さ(較正の誤差 ECE と Brier スコア)で判定する
- 選んだ方法と理由
- 採用の判定には的中率を使わず、較正の誤差(ECE)を使うことにした。合格ラインは、偶然のばらつきの大きさを見積もって決めた。的中率のページでは、予測した確率の帯ごとに実際の勝率を並べ、外れた試合も隠さない。
- 結果
- 試しに4シーズン分で評価したところ(評価用765試合)、LightGBM は ECE 0.058 で合格ライン 0.087 を下回った。一方で、チーム力の指標(Elo)だけの単純なモデルを有意には上回らないことも分かったため、採用の判定は全シーズンのデータがそろってから行う。
未来の情報が混ざらない特徴量の作り方
- 課題
- 試合開始の時点ではまだ確定していない情報を学習に混ぜてしまうと、検証の成績だけが良く見え、実際の予測では当たらなくなる。
- 検討した選択肢
- 基準時刻をずらすと結果が変わるかを確かめる検査
- データベースをわざと書き換えても、基準時刻が同じなら結果が変わらないことを確かめる検査
- 選んだ方法と理由
- 特徴量を作る関数はすべて基準時刻(as_of)を引数に取り、その時刻までに終わった試合だけを使う。検証は時系列で分割する。基準時刻をずらす検査は、基準時刻を無視する実装を見逃してしまうため採らず、データベースを撹乱しても結果が変わらないことをテストで確かめる方式にした。
- 結果
- 的中率が70.3%と「リークを疑うべき」水準だったため精査し、リークの検査10件がすべて通ることを確認した。
年間の運用費を0円に抑える配信の仕組み
- 課題
- 個人の非公式ツールとして長く続けるには、費用を0円に抑え、上限のない従量課金も避ける必要があった。
- 検討した選択肢
- Workers の有料プラン(年約9,000円)を使う
- ページを都度生成し直す仕組み(ISR)を使う
- R2 や KV にデータを置いて配信する
- バッチが書き出した静的な JSON を配信する
- 選んだ方法と理由
- 予測の結果はバッチが静的な JSON として書き出し、よく見られる画面は Workers もデータベースも通さずに配信する。静的に生成するのは直近3シーズンまでに絞り、最初に読み込む JavaScript は gzip で180KB 以下に抑える。
- 結果
- これらの上限は CI で毎回検査しており、上限を超える変更は取り込めないようになっている。
技術構成
| 技術 | 役割 |
|---|---|
| Next.js 16 | 画面。静的に書き出し、Cloudflare Pages で配信する |
| TypeScript | 画面と API の実装言語 |
| Cloudflare Workers / Hono | 公開 API、予測を登録する内部 API、毎時の Cron で予測を確定する処理 |
| Cloudflare D1 | 正本のデータベース。トリガで確定済みの予測を凍結する |
| Python 3.12 | データの取り込み、特徴量の生成、学習のバッチ |
| LightGBM | 勝敗を予測するモデル |
| GitHub Actions | CI、データの取り込み、パーサの日次監視 |
品質と運用
- テスト:データ取り込み・特徴量・学習のバッチ(Python)と API(TypeScript)に自動テストがあります。
- CI:Python は ruff・mypy・pytest、API は型チェック・lint・Vitest を実行します。画面は型チェックと lint に加えて、文字色のコントラスト(4.5:1 以上)、JavaScript の容量、書き出すファイルの数、リンク切れを検査します。テスト用のデータに実在サイト由来の文字列が紛れていないかも確かめています。
- データ取り込みの作法:robots.txt と利用規約の内容を照合し、変わっていたら取得を止めます。リクエストの間隔は3秒以上、1日3,000リクエストまでとし、429 や 503 が返ったら中止します。取得した HTML はそのまま保存しません。
- 監視:パーサが今も正しく動くかを、毎日決まった時刻に確かめています。
これから
取り込むシーズンを増やしてからモデルの採用を判定し、予測の登録、画面と実データの結線、予想スコア、要因ごとの説明(SHAP)、的中率ページと進めます。公開の前には、正式な名称の決定と商標の調査、法律の確認、B.LEAGUE への事前の連絡を行う予定です。


