往診
僕の的中率は嘘をつかない——はずだった。
神崎遼一、34歳。前職は大手シンクタンクの統計アナリスト。回帰分析、ベイズ推定、主成分分析。10年間、数字だけが友人だった。2年前に「自分の手でものを作りたい」と一念発起してIT企業に転職し、独学で Perl を覚えた。
動機は不純だ。競馬予想の自動化。
5種類のアルゴリズムを組み上げた。過去走データ重視、馬場状態重視、オッズ変動追跡、血統指数、騎手相性。ハッシュのディスパッチテーブルで整理して、なかなかの的中率を叩き出していた。統計屋の面目躍如。会社の同僚にも自慢した。
しかし先月、6つ目のアルゴリズム——パドック映像スコア——を追加した瞬間、すべてが崩れた。
馬場分析の的中率が70%から23%に暴落。血統指数も奇妙な値を返すようになった。コード自体は整理してあるはずなのに、新しいロジックを追加しただけで、なぜ まったく関係ないロジック まで壊れるのか。
土曜の午後。僕は書斎のデスクで頭を抱えていた。壁には先月まで誇らしげだったオッズ変動のグラフ。デュアルモニタの左にスプレッドシート、右にターミナル。ターミナルには、何度実行しても「23%」と表示される残酷な数字。
インターホンが鳴った。
配達かと思ってドアを開けると、黒い鞄を持った無表情の男がいた。年齢は30代だろうか。白衣は着ていないが、どこか医者を思わせる佇まい。その横に、柔和な笑顔の女性。
「……どちら様ですか?」
横にいた方が一歩前に出た。
「大丈夫ですよ、ここはコード診療所です……あ、いえ、往診ですね。ナナコと申します。こちらがコードドクターです」
コード診療所? 往診? 僕は困惑したが、助手のナナコと名乗った女性の説明を聞くうちに、どうやら「コードの不具合を診る」専門家らしいと理解した。呼んだ覚えはないのだが——。
「あの、僕は依頼してないんですが……」
男が口を開いた。短く、抑揚のない声。
「コードが悲鳴を上げていた」
意味がわからない。しかし、僕はこの2週間、誰にも相談できずに一人で格闘していた。怪しい。常識的に考えれば怪しい。しかし、「コードが悲鳴を上げていた」という一言が、妙に引っかかった。この2週間、僕のコードは確かに悲鳴を上げていたからだ。藁にもすがる思いで二人を書斎に通した。
男——コードドクターと呼ばれた人物——は部屋に入るなり、壁に貼ったオッズ変動のグラフを一瞥した。……ように見えた。実際にはその視線はグラフの横のモニタ、ターミナルの画面に向いていたのかもしれない。判別がつかないほどの一瞬だった。
そして、僕のデスクの前に無言で座った。
「先生、まずは患者さん——あ、神崎さんのお話を聞いてからにしましょう」
ナナコがそう言ったが、ドクターは構わずモニタを見ていた。
「あの……僕のコードのバグを直してくれるんですか?」
「コードを見せろ」
本当に一言だった。
触診
僕はおそるおそる HorsePredictor.pm を開いた。5種類の予想アルゴリズムをハッシュのディスパッチテーブルで管理したモジュールだ。
| |
ドクターがスクロールを止めた。一瞬、沈黙。
「……悪くない」
意外だった。てっきり酷評されると身構えていた。
ナナコも少し驚いたような顔をしていた——ように、僕には見えた。
ドクターがさらにスクロールを続けた。匿名サブルーチンの中身を一つずつ追っていく。指がマウスホイールの上で止まった。
表情が変わった。眉間にかすかな皺。
「……いや。これは 免疫疾患 だ」
「免疫……? テストは通ってるんですが」
ドクターがモニタの一点を指差した。ファイルの冒頭付近、モジュールスコープの変数宣言。
| |
「ここだ。 共有汚染」
僕には何のことかわからなかった。
ナナコが微笑んで口を開いた。
「一つの冷蔵庫をみんなで使っているような状態です。誰かが牛乳を入れ替えると、お隣のおかずの味まで変わってしまう——そんな感じですね」
冷蔵庫? 僕は自分のコードを見直した。$weight_cache はモジュールスコープの変数で、すべてのアルゴリズムから参照されている。そして paddock_score の中では——。
| |
リファレンスごと差し替えている。全部消える。他のアルゴリズムが書き込んだ track_adj も bloodline も。
「でも、ディスパッチテーブルで整理したはずなんです。キーで分けてるし……」
ドクターは振り返りもせず、短く言った。
「表面だけだ。 中身は繋がっている」
ナナコが続けた。
「ディスパッチテーブルは住所録みたいなものです。誰がどこに住んでいるかは整理されていますけど、壁がない——全員が一つの大部屋で暮らしているんですよ」
住所録はあるけど壁がない。その比喩が、統計屋の脳に一つの概念を呼び起こした。
「つまり……各変量の 独立性が確保されていない ってことですか?」
ナナコが一瞬目を見開いた。
「……はい。まさにそういうことです」
ドクターが微かに頷いた——気がした。
診断
ドクターがキーボードに手を伸ばした。ターミナルに短いコマンドを打ち込み、テスト結果を表示させた。
| |
「見ろ」
ドクターが指したのは jockey_compat の値だった。paddock_score を実行しただけで、騎手相性の計算結果が変わっている。$weight_cache のリファレンスが差し替えられ、bloodline のキャッシュが消失したからだ。
「自己免疫疾患だ。臓器を足すたびに、体が壊れる」
ナナコが補足した。
「表面的には健康体なんです。定期検診——つまりテスト——でも異常は見つかりにくい。でも体内では臓器同士が不正な血管で繋がっていて、新しい臓器を移植するたびに、免疫系が暴走するんです」
僕は、自分の的中率暴落の原因をようやく理解した。パドック映像スコアを追加したこと自体が悪いのではない。追加した瞬間に、共有変数を通じて 既存のすべてのアルゴリズムの計算結果が汚染された のだ。
「処方は」
ドクターが立ち上がった。
「 臓器分離 」
外科手術
ドクターが黙々とキーボードを叩き始めた。
最初に PredictionStrategy というファイルを作った。中身を見て拍子抜けした——定義しているのは「predict を持て」、それだけだ。
| |
「予想する。それだけだ」
ナナコが僕に向き直った。
「それぞれのアルゴリズムが、同じ約束事—— predict メソッド——を持てばいいんです。中身は自由ですよ」
……待て。これ、僕が知っている概念だ。
「あ……これ、統計で言う 推定量のインターフェース と同じですね。推定方法は違っても、推定値を返すところは共通……」
言い終わる前にドクターは次のファイルを作っていた。最初の具象クラスとして Strategy::TrackCondition。
| |
「隔離完了」
僕は画面を凝視した。$weight_cache がどこにもない。@recent_results も。このクラスは外部の共有変数に一切依存していない。独立した臓器だ。
ナナコが微笑んだ。
「各アルゴリズムが自分専用の冷蔵庫を持つようになりました。もう隣の人のおかずの味は変わりませんよ」
冷蔵庫の比喩が、今度ははっきりと腹に落ちた。
「これなら僕にもできます」

気がつくと、僕の手が動いていた。2つ目のアルゴリズム、Strategy::PastData を自分で書き始めていた。
| |
$weight_cache を $self->weight_cache に変えた。インスタンス変数だ。他のクラスからは触れない。
ドクターが僕のコードを見た。一瞬手を止めて、画面を見つめた。
「……使える」
短い評価だった。でも、僕には十分だった。
残りのアルゴリズム——OddsMovement、Bloodline、JockeyCompat——も同じパターンで書き進めた。そして最後にドクターが作ったのが PredictionEngine。すべてを束ねるコンテキストだ。
| |
「 add_strategy で登録して、predict で実行する——レースのたびに戦略を差し替えられるんですね」
僕が言うと、ナナコが嬉しそうに頷いた。
「その通りです。 Strategy パターン ——アルゴリズムの交換可能性。これが今回の処方箋です」
術後経過
テストを実行した。
| |
全アルゴリズムが独立して正しい結果を返した。馬場分析の的中率が70%に復帰。
僕は興奮していた。
「あの——今追加しようとしていた6つ目、パドック映像スコアのロジック。今すぐ試していいですか?」
「やってみろ」
僕は Strategy::PaddockScore を新規作成した。既存のファイルには一切触れない。
| |
PredictionEngine に登録。テスト実行。
| |
「一つも壊れない……! 本当に独立してる……」
独立変数の重要性。統計学で嫌というほど学んだはずのことだった。多重共線性を排除し、各変量の独立性を確保する。基本中の基本だ。
なのに、自分のコードではそれを見落としていた。
ドクターが帰り支度を始めた。鞄を手に取り——そして、僕のデスクに置いてあったペンを一本、無造作に胸ポケットに差し込んだ。
一瞬、「あ、僕のペンを……」と思った。だが違った。よく見ると、僕が使っているのとは違うメーカーのペンだ。どうやら診察中にドクターが無意識に置いていたらしい。
ナナコが淡々と言った。
「すみません、先生よくペンを置き忘れるんです」
それだけのことだった。それだけのことなのに、あの無造作な手の動きに——なんだろう、不思議な温かみのようなものを感じたのは、きっと気のせいだ。
ドクターが玄関に向かった。振り返らずに、一言。
「感謝は、このコードに」
ナナコが深くお辞儀をして続いた。
「お大事にしてくださいね。次のアルゴリズムを追加するときも、もう怖くないはずです」
二人が去った後、僕はデスクに戻った。ターミナルには的中率のサマリが表示されている。6つのアルゴリズムが、互いに干渉することなく、それぞれの精度を保っている。
そして画面の隅に、小さく表示された文字。
| |
独立性の確保は、統計でも、コードでも、同じだった。
僕が一番基本的なことを忘れていたのだ。
処方箋まとめ
| 症状 | 適用すべき | 経過観察 |
|---|---|---|
| 条件分岐(if-elsif)でアルゴリズムを切り替えている | ✓ | |
| ディスパッチテーブルで管理しているが共有状態がある | ✓ | |
| アルゴリズム追加時にメインモジュールの修正が必要 | ✓ | |
| 分岐が2〜3個で今後増える見込みがない | ✓ | |
| 各アルゴリズムが完全に独立している(共有状態なし) | ✓ |
治療のステップ
- Strategy ロールの定義:
Moo::Roleで共通インターフェース(predict等)を定義する - 具象クラスの分離: 各アルゴリズムを独立したクラスに移植し、共有変数への依存を排除する
- Context クラスの作成: Strategy を登録・実行する
PredictionEngineを構築する - 独立性のテスト: 各 Strategy を個別にテストし、他の Strategy の実行結果に影響されないことを確認する
- 拡張テスト: 新しい Strategy を追加しても既存の結果が変わらないことを検証する
助手より
統計の知識をお持ちの神崎さんなら、きっとすぐに馴染めると思います。「各変量の独立性を確保する」——神崎さんご自身がおっしゃった言葉が、Strategy パターンの本質そのものです。新しいアルゴリズムを思いついたら、クラスを一つ作るだけ。もう既存のコードが壊れることはありません。先生はああいう方ですが、神崎さんのコードを「使える」と評価したのは、本当に珍しいことなんですよ。
——ナナコ
