先に断っておくと、僕はプログラマーでもエンジニアでもありません。本業は経理で、持っている資格も簿記2級程度。「技術的バックグラウンド」と呼べるものはそれくらいです。それでも今年、Claude Codeを使って「mlb-record-finder」というMLBのマニアックな記録を発掘するCLIツールを作り、GitHubで公開しました。
きっかけは単純で、MLB.comのサラ・ラングス記者が書くような「そんな記録、誰が探しに行ったんだ」系の記事にハマったからです。同じことを自分でもやってみたくなりました。
何のデータを組み合わせているか
使っているのはすべて無料・公開データです。最初は3種類でしたが、最終的に4種類になりました。
Lahman Baseball Database(SABR公式版): 1871年まで遡る通算・シーズン成績
Retrosheet Game Logs: 1871年以降、全試合のチーム単位の結果
Retrosheet Play-by-Play: 直近シーズンの1球単位のイベントデータ
Statcast(pybaseball経由): 2015年以降の打球速度・飛距離・回転数などのトラッキングデータ
これらを1つのDuckDBファイルにまとめて、14種類の定型テンプレート(打球速度×飛距離条件での本塁打検索、球団別通算本塁打ランキング、バレル率ランキング、期待成績と実成績の乖離など)から選んで使うか、あるいは日本語・英語どちらでも普通の文章で質問を打ち込むだけで結果が返ってきます。
生データには存在しない数値を自分で作る
バレル率やxwOBAのような指標は、実はほぼタダで手に入ります。Statcastの生データにすでに計算済みの列として入っているので、あとは集計するだけです。厄介だったのはWARでした。Lahmanを見てもRetrosheetを見てもStatcastを見ても、「WAR」という数値はどこにもありません。誰も渡してくれないんです。
なので自分で作りました。打撃の価値はLahmanの成績をwOBAベースの得点貢献度計算(年度ごとの係数付き)に通して算出。守備は2016年以降はStatcastのOAA(平均的な野手と比べた創出アウト数)を使い、それ以前は簡易的なLahmanの守備指標にフォールバックします。投手の価値はFIPベース。一番面倒だったのはパークファクターで、3つのデータソースのどれにも入っていなかったため、Retrosheet Game Logsのホーム/アウェイの得点差から自分で算出しました。補正の基準となるリプレースメントレベルとポジション補正だけは、公表されている標準値をそのまま借りています。
というわけでこのWARは、FanGraphsやBaseball-Referenceの公式数値を再現したものではなく、手元にある無料データだけで組み上げた独自の簡易版です。README にも「公式サイトの数値とは一致しません」とはっきり書きました。ここをごまかすと、記録発掘ツールを作った意味そのものが崩れてしまうので。
自由質問モードは実際どんな結果を返すのか
今シーズン(2026年)からホワイトソックスに加入した村上宗隆のことが頭の片隅にあったので、自由質問モードに日本語で「2026年のハードヒット率トップ10」とだけ打ち込んでみました。返ってきたのがこちらです。
sql
SELECT (pl.name_first || ' ' || pl.name_last) AS batter_name,
COUNT(sp.launch_speed) AS batted_balls,
COUNT(CASE WHEN sp.launch_speed >= 95.0 THEN 1 END) AS hard_hit_balls,
ROUND(100.0 * COUNT(CASE WHEN sp.launch_speed >= 95.0 THEN 1 END)
/ COUNT(sp.launch_speed), 1) AS hard_hit_pct
FROM statcast_pitches sp
JOIN player_id_lookup pl ON sp.batter = pl.key_mlbam
WHERE sp.game_year = 2026 AND sp.launch_speed IS NOT NULL
GROUP BY pl.key_mlbam, pl.name_first, pl.name_last
HAVING COUNT(sp.launch_speed) >= 100
ORDER BY hard_hit_pct DESC
LIMIT 10;
打者 打球数 ハードヒット数 ハードヒット率
Angel Genao 142 53 37.3
Fernando Tatís 729 266 36.5
Nelson Velázquez 128 46 35.9
James Wood 605 213 35.2
Junior Caminero 781 274 35.1
村上宗隆 469 163 34.8
Jac Caglianone 661 229 34.6
Elly De La Cruz 629 213 33.9
Pete Alonso 845 284 33.6
Oneil Cruz 391 131 33.5
回答したのはgemini-3.8-flashで、「ハードヒット率」の定義は一切指定していません。それでもモデルは打球速度95mph以上・打球数100以上という閾値を自分で決め、その定義をきちんと言葉で説明してから結果を返してきました。生成されたSQLも毎回「検証用」として画面に表示されるので、フィルターの中身が自分の意図と合っているか目で確認できます。
日本語の名前を打ち込んでもつまずきません。裏側ではMLBAM IDを使ってplayer_id_lookupテーブルと結合しているので、表記ゆれの問題が起きないんです。MLB在籍1シーズンにも満たない村上が、469打球というサンプル数でフェルナンド・タティスやジェームズ・ウッドの隣、6位に並んでいるのを見た瞬間、「これ、ちゃんと動いてる」と実感しました。
裏側の仕組みとしては、自由質問モードはDuckDBのスキーマ全体をGemini APIに渡してSQLを生成させています。まず安いFlashモデルで試し、質問が複雑と判定された場合や最初の生成がバリデーションで弾かれた場合だけPro系モデルにエスカレーションします。生成されたSQLはSELECT/WITH以外を弾く正規表現でフィルターし、さらにEXPLAINにかけてテーブル・カラムが実在するかを確認してから初めて実行を許可する仕組みです。同じ質問は正規化したキーでローカルにキャッシュされるので、2回目は無料で返ってきます。
偶然見つけた「MLB史上初」かもしれない記録
ここが一番書きたかった部分です。ホワイトソックスは2025年、60勝102敗でアメリカンリーグ中地区最下位、リーグ全体でも最下位でした。それが2026年、9月上旬時点で75勝69敗、ガーディアンズに2ゲーム差をつけて地区首位に立っています。この打線を支えている一人が、30本塁打を突破したばかりのルーキー、村上宗隆です。
そこで自由質問モードにこう聞いてみました。
「あるチームがリーグ最下位だった翌年に首位争いをした例で、30本塁打以上を打ったルーキーがいたケースはありますか」
生成されたSQL(WITH team_ranks AS (...) から始まるCTE)は3つの条件でフィルターを組んでいました。前年に地区・リーグ最下位だったこと、翌年に首位争いをしていたこと(上位2位以内、または地区・リーグ優勝)、そしてルーキー資格(シーズン開始時点で通算打席数130以下——これはMLBの公式ルーキー基準にほぼ一致します)を満たしたうえで30本塁打以上を打ったこと。今回はgemini-3.1-pro-previewに回されました。Flashだと荷が重いと判定されたみたいです。返ってきたのは2件でした。
シーズン チーム 選手 本塁打
2007 アリゾナ・ダイヤモンドバックス クリス・ヤング 32
1986 テキサス・レンジャーズ ピート・インカビリア 30
つまり「最下位から首位争い+30本塁打ルーキー」自体は過去に2例あります。そこで質問をさらに絞り込み、「首位争い」を「実際にリーグ優勝」に変えて聞き直しました。
「あるチームがリーグ最下位だった翌年にリーグ優勝を果たした例で、30本塁打以上を打ったルーキーがいたケースはありますか」
該当0件。過去に一度もありません。
つまり、このままホワイトソックスが調子を維持してアメリカンリーグを制した場合、「最下位からリーグ優勝、しかも30本塁打ルーキー付き」というシーズンは、MLB史上一度も起きたことのない出来事になります。まだ何も決まってはいません(優勝どころか、まだ何も確定していません)が、質問の立て方ひとつで、今シーズン追いかける価値のある本物のストーリーラインに偶然行き当たったわけです。あの瞬間だけは、サラ・ラングス記者がやっていることを自分も小さく再現できた気がしました。
開発の裏側メモ
Retrosheet公式のパーサー「cwevent」はビルドにCコンパイラが必要で、手元の環境にはそれがありませんでした。結局Python製のPlay-by-Playパーサーを一から自分で書き、Retrosheet Game Logsの最終スコアと突き合わせて検証しました。結果は2430試合中2417試合が一致(99.4%)。この過程で牽制球による走者状態の崩れ、ファウルフライの誤分類、二塁打と三塁打の取り違えといった実バグを見つけて直しました。地味な作業ですが、直すたびに一致率が上がっていくのは素直に嬉しかったです。
自由質問機能は当初Anthropic APIで動かす予定でしたが、無料枠がなく、すでにGeminiの有料契約があったので、それ以上契約を増やしたくなくてGeminiに切り替えました。
gemini-2.5-proはドキュメント上はGA扱いのはずが、実際にAPIを叩くと404が返ってきたため、フォールバック先をgemini-3.1-pro-previewに変更しました。
当初Lahmanデータベースの取得元として想定していたchadwickbureauのGitHubリポジトリが、着手する頃にはすでに更新停止していました。SABR公式配布に切り替えて先に進みました。
今どういう状態か、公開サービスにしなかった理由
Phase1〜4はすべて実装・動作確認済みで、GitHubにpushしてあります。誰でも使えるホスティング型のWebサービスには、あえてしませんでした。StatcastやRetrosheetのデータ自体は無料で取得できますが、MLB.comの利用規約は自動収集データの再配布を制限しており、公開ホスティングするとそこに抵触しかねないと判断したためです。なのでコードはオープンソースとして公開し、実際に動かすかどうかは使う人に委ねています。README.mdとREADME.ja.mdには同じ2つの注意書きを入れています。19世紀データの網羅性が限定的であること、そしてここでのWARは独自の簡易算出であることです。
結局これは、野球記事に感化された経理担当が、AIの力を借りて何かを作ってみた、という話でしかありません。無料で手に入るデータを組み合わせるだけでどこまでできるか、その一例として誰かの参考になれば十分だと思っています。
GitHub(日本語版README): https://github.com/hito1120-crypto/mlb-record-finder/blob/main/README.ja.md