0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Flutter × Supabase で競馬AI予想パイプラインを全自動化した話 — JRA+NAR両対応・前走情報まで実装

0
Posted at

Flutter × Supabase で競馬AI予想パイプラインを全自動化した話 — JRA+NAR両対応・前走情報まで実装

はじめに

「AIで競馬予想ができたら面白い」という発想から始まり、Flutter UI・Supabase Edge Function・GitHub Actions をつなぎ合わせた完全自動化パイプラインを構築しました。

JRA (中央競馬) だけでなく NAR (地方競馬) 15場にも対応し、さらに前走情報・馬体重・年齢性別の詳細データも表示できるようになりました。


アーキテクチャ全体像

[JRA/NAR データ取得]  → fetch_horse_racing.py (Python)
        ↓
[tools-hub EF]        → horseracing.today / predict_all / predictions / accuracy
        ↓
[Supabase DB]         → horse_races / horse_results テーブル
        ↓
[horse-racing-update.yml] → 1時間毎に自動実行 (GitHub Actions)
        ↓
[Flutter UI]          → horse_racing_predictor_page.dart (3タブ構成)

データ取得: JRA + NAR 完全対応

Python スクリプト fetch_horse_racing.py が JRA と NAR 両方のデータを EUC-JP エンコーディングで取得します。

# JRA: netkeiba.com から EUC-JP でデータ取得
# NAR: 地方競馬 15場 (大井・川崎・船橋など) に対応

response = requests.get(url, headers=headers, timeout=10)
# EUC-JP の文字化けを回避: errors='replace' で確定デコード
content = response.content.decode('euc-jp', errors='replace')

詰まりポイント: Windows 環境の Python は CP932 エンコードがデフォルト。EUC-JP のバイト列をそのまま読もうとすると文字化けします。errors='replace' で unknown バイトを置換することで安定化しました。


Edge Function: tools-hub のアクション分岐

EF ハードキャップ (50本以下) を守るため、競馬機能は全て tools-hubaction 分岐で実装:

// tools-hub/index.ts
switch (action) {
  case 'horseracing.today':
    return await getHorseRacingToday(supabase);
  case 'horseracing.predict_all':
    return await predictAllRaces(supabase, body);
  case 'horseracing.predictions':
    return await getPredictions(supabase, body);
  case 'horseracing.accuracy':
    return await getAccuracyStats(supabase);
}

認証ゾーンの設計

GitHub Actions から JWT なしで呼び出せるよう、horseracing.todayhorseracing.predictionsNO_AUTH_ACTIONS に配置:

const NO_AUTH_ACTIONS = ['horseracing.today', 'horseracing.predictions'];

当初 auth 必須ゾーンに入れていたため GitHub Actions から 401 エラーが発生。no_auth に移動して解消しました。

horse_results の 500 エラー解消

全レース結果を一括 SELECT するとタイムアウト。Promise.all で並列個別クエリに変更:

// After: 個別クエリ並行実行
const results = await Promise.all(
  raceIds.map(id =>
    supabase.from('horse_results').select('*').eq('race_id', id)
  )
);

GitHub Actions: 1時間毎の完全自動実行

# .github/workflows/horse-racing-update.yml
on:
  schedule:
    - cron: "0 * * * *"  # 毎時00分

jobs:
  update:
    steps:
      - name: Run all modes
        run: |
          python fetch_horse_racing.py --mode today
          python fetch_horse_racing.py --mode predict
          python fetch_horse_racing.py --mode accuracy

--mode all を追加したことで、1 回のジョブ実行でデータ取得・AI予想・的中率更新の全工程を完了させられるようになりました。


Flutter UI: 3タブ + 前走情報

horse_racing_predictor_page.dart は3タブ構成:

タブ 内容
今日のレース ExpansionTile でレースカード展開、グレードバッジ (G1=赤/G2=青/G3=緑)
予想履歴 過去のAI予想一覧、日付フィルタ
的中率 ヒット率%・総予想数・最高払戻し

グレードカラーバッジ

Color _gradeColor(String grade) => switch (grade) {
  'G1' => Colors.red.shade700,
  'G2' => Colors.blue.shade700,
  'G3' => Colors.green.shade700,
  _    => Colors.grey.shade600,
};

前走情報 (最新追加)

直近セッション (Windows版#68 → VSCode版#70) で前走情報・馬体重・年齢性別を追加:

// 前走情報カード
ListTile(
  title: Text('前走: ${horse.prevRaceName}'),
  subtitle: Text(
    '前走着順: ${horse.prevRaceRank}位 | '
    '馬体重: ${horse.weight}kg | '
    '${horse.age}${horse.sex}'
  ),
)

マイグレーション:

ALTER TABLE horse_races
  ADD COLUMN prev_race_name  text,
  ADD COLUMN prev_race_rank  int,
  ADD COLUMN horse_weight    int,
  ADD COLUMN horse_age       int,
  ADD COLUMN horse_sex       text;

まとめ

機能 実装状況
JRA データ取得 ✅ netkeiba.com EUC-JP対応
NAR 地方競馬 15場 ✅ 大井/川崎/船橋 etc.
AI予想生成 ✅ tools-hub EF
1時間毎自動更新 ✅ GitHub Actions cron
前走情報・馬体重 ✅ VSCode版#70 で追加
的中率ダッシュボード ✅ Flutter 3タブUI

競馬予想の精度向上は今後の課題ですが、データ取得→AI予想→UI表示まで完全自動化という基盤は完成しました。


自分株式会社: https://my-web-app-b67f4.web.app/
#FlutterWeb #Supabase #個人開発 #buildinpublic #競馬AI

0
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
0
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?