確率のバグ

機械学習でロトの次回は予測できるか:勾配ブースティングと過学習の数理

読了目安 約12分

「LightGBMに過去2,000回の出目と、出現間隔や偶奇比率のような特徴量を100列与えれば、人間には見えない周期性を学習して次回を予測できるのではないか」。この問いに、実際にやって答えます。ロト6の第1〜2,129回を82,947サンプル・100次元の学習データに展開し、勾配ブースティングを訓練し、未来を見ない検証と、順番を壊したデータとの比較にかけました。先に結論を書くと、モデルが捉えたのは次回の出目の手がかりではなく、過去の有限なサンプルに偶然残った揺らぎでした。

100列の特徴量と、訓練データ上の「精度」

問題をまず機械学習の形にします。各回 t と各数字 n(1〜43)について「n は t 回の本数字6個に入るか」を予測する二値分類です。何も知らなければ正解率は毎回 6/43=13.95%。この一様な基準を、対数損失で書くと0.4041、Brierスコアで書くと0.1201になります。モデルが次回について何かを学べたなら、この数字を下回るはずです。

特徴量は、数字の予想でよく話題になる指標をひととおり入れました。直前30回の出現ラグ(30列)、最後に出てからの間隔とその二乗・対数(3列)、直近5〜200回の7つの窓での出現回数と出現率(14列)、半減期5〜50回の指数平滑(4列)、連続出現、100回内の最長空き、累積回数と順位と z 値(7列)、数字自身の属性(奇偶・十の位・下一桁・高低など11列)、前回と前々回の合計・偶奇数・高帯数・範囲・連番数・ボーナス(10列)、前回の数字との距離や共起の履歴(6列)、曜日・月・年・日・回号の剰余(6列)、間隔×出現率のような交互作用(10列)。合計100列、第201〜2,129回で82,947サンプルです。セット球はCSVに記録がないので入れていません。

ここに、わざと過学習しやすい設定の LightGBM を当てます。木2,000本、葉255、葉あたり最小5サンプル、正則化なし。訓練は第201〜1,700回。結果は次のとおりです。

同じモデルの、訓練データ上の成績と、その後の回での成績
指標一様な基準訓練(第201〜1,700回)テスト(第1,701〜2,129回)
AUC0.50001.00000.4995
対数損失(小さいほど良い)0.40410.00001.4510
Brierスコア(小さいほど良い)0.12010.00000.1396
上位6個の的中率13.95%100.00%13.60%

訓練データの上では、モデルは1回あたり6個中6.00個を当てています。1,500回分の出目を細かく記憶したのです。ここだけを見れば「AIが法則を掴んだ」と感じても不思議ではない数字です。しかし、訓練に使っていない回では AUC 0.4995、的中率13.60%。対数損失は基準の0.4041より大きい1.4510で、確率を強めに振ったぶん、一様な基準より誤差が大きくなっています。控えめな設定(木300本、葉31、最小100サンプル)でも構図は同じで、訓練AUC 0.8867に対しテストAUC 0.5068でした。

ここで起きているのは、人が出目に「流れ」を感じるのとよく似たことの、機械版です。決定木は分割点を無数に選べるので、ラベルが純粋な雑音であっても、訓練サンプルの個々の偶然に合わせて損失を下げていけます。訓練AUCの上昇は「モデルの記憶容量」の証明であって、「データに規則がある」証明ではありません。この区別を検証手順で強制するのが、次の二つの装置です。

出典・データ:宝くじ公式:ロト6のルールと当せん確率 / LightGBM公式資料:パラメータ(英語) / 集計方法とデータの範囲

Walk-Forward検証:未来を一度も見ずに、次の100回を予測し続ける

一つ目は時系列のローリング検証です。第1,001回から100回刻みでブロックを切り、各ブロックの直前までのデータだけでモデルを訓練し、そのブロックを予測します。訓練データにテスト期間の情報が一切混ざらないので、「未来を知っていたから当たった」という漏えいが構造的に起きません。scikit-learn の TimeSeriesSplit と同じ思想で、投資のバックテストではアウトオブサンプル検証と呼ばれます。合計1,129回・48,547サンプルの、未来を見ていない予測が得られます。

Walk-Forward検証の成績(ロト6、第1,001〜2,129回、1,129回のアウトオブサンプル)
指標一様な基準実測読み方
AUC0.50000.50370.5が「順位付けに情報なし」
対数損失0.40410.4091基準より大きいほど、確率の見積もりがずれている
Brierスコア0.12010.1213同上
上位6個の的中率13.95%14.28%(標準誤差±0.40%)1回に6個選んだときの平均一致率

AUCは0.5037。モデルが「出やすい」と順位付けした数字は、実際にはほぼ順位どおりに出ていません。的中率は14.28%で、理論値13.95%との差は0.32ポイント、標準誤差0.40%の0.8倍です。一方、確率の当て方を測る対数損失とBrierは基準より大きく、「どの数字も6/43」とする一様な見積もりのほうが誤差は小さい結果でした。順位付けは偶然の範囲、確率の見積もりは一様な基準に届かない。これが100列の特徴量と300本の木が未来に対して残した成績です。

ただし、この表だけでは「わずかな差が偶然の範囲か」を判定できません。的中率の標準誤差は1回ごとの一致数のばらつきから計算したもので、モデルの訓練そのものが持つ揺らぎを含んでいないからです。そこで、効果がゼロだと分かっているデータで同じ手順を回し、その分布と比べます。

出典・データ:scikit-learn公式資料:TimeSeriesSplit(英語) / 集計方法とデータの範囲

Permutation Test:並び順を壊したデータと、区別がつくか

二つ目の装置は帰無データです。2,129回の出目をそのまま使い、回の並び順だけを無作為に入れ替えます。各回の6個も、数字ごとの総出現回数も変わりませんが、「前回との関係」「間隔」「直近の傾向」といった時間的な構造は完全に破壊されます。この履歴に対して同じ100列の特徴量を作り、同じWalk-Forwardを回す。これを20本。もし実データに時間的な規則があるなら、実データの成績は帰無データの分布から外れて良いはずです。

実データと、並び順を壊した帰無データ20本のWalk-Forward成績
指標実データ帰無20本の平均±標準偏差帰無の最小〜最大実データ以上に良かった帰無の割合
AUC0.50370.5012 ± 0.00470.4927〜0.509630%
対数損失0.40910.4097 ± 0.00060.4087〜0.410815%
Brierスコア0.12130.1215 ± 0.00010.1212〜0.121725%
上位6個の的中率14.28%13.94% ± 0.43%13.26%〜14.76%20%

実データの成績は、四つの指標すべてで帰無分布の内側にあります。AUC 0.5037は帰無の平均0.5012から0.51標準偏差、的中率も帰無の幅13.26%〜14.76%に収まります。帰無20本のうち、AUCで実データ以上に良かったものは30%、的中率では20%。時間的な構造を完全に壊した履歴で訓練したモデルの中に、本物の2,129回で訓練したモデルを置いても、どれが本物か分かりません。つまりこの手順は、本物の履歴と「意味のない履歴」を区別できていません。

以下は、この二つの検証を1本にまとめた骨子です。当サイトの公式結果ページからダウンロードできるCSV(列名「回」「抽せん日」「本数字1〜6」)をそのまま読めるようにしてあります。特徴量は本記事の100列から要点だけを残していますが、Walk-Forwardと帰無データの論理は同じです。Google Colabやローカルで、数分で再現できます。

walk_forward_permutation.py — Walk-Forward validation and a permutation null for Loto 6
"""Walk-forward validation and permutation test for Loto 6 number prediction.

Requires: numpy, pandas, scikit-learn, lightgbm (pip install lightgbm).
Input: the CSV downloaded from yumelotokun.com/history/#download
(columns: 回, 抽せん日, 本数字1..本数字6, ...). Runs in a few minutes.
"""
import numpy as np
import pandas as pd
from lightgbm import LGBMClassifier
from sklearn.metrics import log_loss, roc_auc_score

N, K = 43, 6
P0 = K / N  # 6/43: the probability that any given number is drawn

df = pd.read_csv("loto6-official-results-1-2137.csv")
draws = np.zeros((len(df), N), dtype=np.int8)  # one row per round, one column per number
for k in range(1, K + 1):
    draws[np.arange(len(df)), df[f"本数字{k}"].to_numpy() - 1] = 1


def features(draws, warmup=200, lags=30, windows=(5, 10, 20, 50, 100, 200)):
    """One sample per (round t, number n). Every feature uses rounds < t only."""
    cum = np.vstack([np.zeros((1, N)), np.cumsum(draws, axis=0)])
    last_seen = np.full(N, -1)
    X, y, r = [], [], []
    for t in range(len(draws)):
        if t >= warmup:
            gap = np.where(last_seen >= 0, t - last_seen, t)  # rounds since last appearance
            cols = [draws[t - k] for k in range(1, lags + 1)]  # lag indicators
            cols += [cum[t] - cum[max(0, t - w)] for w in windows]  # window counts
            cols += [gap, np.log1p(gap), np.arange(1, N + 1) % 2]  # gap, log gap, parity
            X.append(np.column_stack(cols))
            y.append(draws[t])
            r.append(np.full(N, t))
        last_seen[draws[t] == 1] = t
    return np.vstack(X), np.concatenate(y), np.concatenate(r)


def walk_forward(draws, first_test=1000, step=100, seed=0):
    """Train on rounds < start, predict rounds [start, start + step). Never look ahead."""
    X, y, r = features(draws)
    prob = np.full(len(y), np.nan)
    for start in range(first_test, r.max() + 1, step):
        train = r < start
        test = (r >= start) & (r < start + step)
        model = LGBMClassifier(
            n_estimators=300, learning_rate=0.05, num_leaves=31, min_child_samples=100,
            subsample=0.8, subsample_freq=1, colsample_bytree=0.8,
            random_state=seed, verbose=-1,
        )
        model.fit(X[train], y[train])
        prob[test] = model.predict_proba(X[test])[:, 1]
    m = ~np.isnan(prob)
    y, r, prob = y[m], r[m], prob[m]
    # Hit rate: pick the 6 numbers with the highest predicted probability in each round.
    hits = [y[r == t][np.argsort(-prob[r == t])[:K]].mean() for t in np.unique(r)]
    return dict(auc=roc_auc_score(y, prob), log_loss=log_loss(y, prob), hit_rate=np.mean(hits))


baseline_log_loss = -(P0 * np.log(P0) + (1 - P0) * np.log(1 - P0))  # 0.4041
real = walk_forward(draws)

# Permutation null: shuffle the ORDER of the rounds. Marginals stay, time structure dies.
rng = np.random.default_rng(20260916)
null = [walk_forward(draws[rng.permutation(len(draws))], seed=i + 1) for i in range(20)]

print(f"baseline  auc 0.5000  log_loss {baseline_log_loss:.4f}  hit_rate {P0:.4f}")
print(f"real      auc {real['auc']:.4f}  log_loss {real['log_loss']:.4f}  hit_rate {real['hit_rate']:.4f}")
for key in ("auc", "log_loss", "hit_rate"):
    values = np.array([n[key] for n in null])
    print(f"null {key:9s} mean {values.mean():.4f}  sd {values.std(ddof=1):.4f}  "
          f"min {values.min():.4f}  max {values.max():.4f}")

出典・データ:scikit-learn公式資料:permutation_test_score(英語) / 当サイト:公式結果とCSVダウンロード / 集計方法とデータの範囲

バックテスト過剰適合:200個試せば、乱数からも「勝てるモデル」が出る

ここまでは「1つのモデルを正しく検証する」話でした。しかし実際の予想モデル開発では、特徴量の組み合わせ、木の深さ、シード、期間を何百通りも試し、いちばん良かったものを発表します。金融工学ではこれを「バックテスト過剰適合」と呼び、Bailey・Borwein・López de Prado・Zhu は2014年に、独立な試行を N 回行えば、シグナルが皆無のホワイトノイズからでも最大のシャープレシオが √(2 ln N) 程度まで育つことを示しました。N=200 なら正規近似で平均から約2.75標準偏差。「探せば必ず見つかる」は比喩ではなく、期待値の式です。

同じことをAUCで再現します。100列から30列を無作為に選び、葉の数を7・15・31から選び、シードも変えた200構成を、第201〜1,500回で訓練し、第1,501〜1,800回の検証ブロックで採点します。その中で最良だった1構成を、まだ誰も見ていない第1,801〜2,129回で再評価します。

200構成の検証AUCと、最良構成のその後
項目値
200構成の検証AUC:平均 ± 標準偏差0.5040 ± 0.0059
検証AUCの範囲0.4832〜0.5197
正規近似での最大値の期待値(平均+2.75σ)0.5201
検証AUCが0.505を超えた構成の数84個
最良構成の検証AUC0.5197
同じ構成のホールドアウトAUC0.5088
同じ構成のホールドアウト的中率14.54%(理論13.95%)

200構成の最大AUCは0.5197。「AUC 0.520のAIモデルを発見」と書くには十分な数字ですが、正規近似で予想される200個の最大値0.5201と並べれば、それは探索が作った値だと分かります。そしてその最良構成は、ホールドアウトではAUC 0.5088、的中率14.54%へ後退し、前節の帰無の幅(0.4927〜0.5096)の内側に戻りました。選抜に使った期間の成績は、選抜した瞬間に上方へ偏ります。公開されている「精度」を読むときは、この選抜バイアスのぶんを差し引いて考えると見通しがよくなります。

I(X; Y) = H(Y) − H(Y | X) = 0 (各回の抽せんが独立同分布なら、過去の任意の関数 X と次回の出目 Y は独立)

情報理論の言葉で言い直します。過去の出目から作った特徴量 X が、次回の出目 Y について持つ情報量は相互情報量 I(X; Y) です。各回が独立に同じ分布から引かれているなら、X は Y と独立で、I(X; Y) は近似的にではなく厳密に 0。機械学習モデルは、この量が 0 より大きいかを有限サンプルから推定する装置と見ることができます。推定には必ず揺らぎがあり、揺らぎの上側だけを拾う手順は「発見」を生みやすくなります。ただし独立同分布は仮定であり、この記事の検定が示すのは「独立同分布と区別できなかった」ことです。どれほどの偏りまでなら見えたかは、次に確かめます。

出典・データ:Bailey, Borwein, López de Prado, Zhu (2014) Pseudo-Mathematics and Financial Charlatanism, Notices of the AMS(英語) / 集計方法とデータの範囲

陰性を書く資格:本物の効果を仕込んだデータで、検出できるか

「差がなかった」と書く検定は、「差があれば見えた」ことを示す義務があります。そこで独立・等確率の抽せんに、間隔効果だけを人工的に加えた履歴を作りました。10回以上出ていない数字の当たる重みを1.1倍・1.2倍・1.4倍にして2,129回を生成し、同じ100列、同じWalk-Forwardにかけます。

合成データの陽性対照(間隔10回以上の数字の重みを引き上げた履歴)
出遅れた数字の重みAUC対数損失(基準0.4041)上位6個の的中率
1.1倍0.50530.409314.38% ± 0.39%
1.2倍0.50820.408614.95% ± 0.40%
1.4倍0.52300.408115.93% ± 0.43%

1.4倍の世界ではAUC 0.5230、的中率15.93%で、帰無の幅(AUC 0.4927〜0.5096、的中率13.26%〜14.76%)を明確に超えます。1.2倍ではAUC 0.5082と帰無の最大値の内側に留まり、的中率14.95%だけが帰無の最大をわずかに超える、境界線上の結果です。1.1倍ではAUC 0.5053、的中率14.38%で、どちらも帰無の中です。この手順の検出限界は、間隔効果に限れば「1.2倍と1.4倍の間」にあります。したがって実データの結論は「規則は存在しない」ではなく、「出遅れた数字が1.4倍出やすくなる程度の規則があれば確実に見えたが、見えなかった」であり、1.2倍程度の弱い偏りまで否定するものではありません。流れの記事で回帰が置いた1.02倍の下限よりはるかに粗いのは、機械学習が問いを絞らないぶん、検出力を払っているからです。

出典・データ:集計方法とデータの範囲

機械学習が力を発揮できるのは、球ではなく人の選び方

ここまでの結果は、機械学習が無力だという話ではありません。何に向けるかで結果が変わるという話です。物理的な抽せん機が作る出目は、この記事のあらゆる手順に対して独立同分布と区別できませんでした。いっぽう、同じロト6のデータには、機械学習が確実に学べる部分があります。人間の側です。

当サイトの群衆の選好の記事で示したとおり、出目の合計値と1等の当せん口数には ρ = −0.42 という強い負の相関があり、誕生日の範囲に収まる数字、きれいな並び、ゾロ目は口数が数倍に膨らみます。売上口数、払戻金額、曜日、キャリーオーバーの有無を特徴量にして「次回、人々はどの数字に群がるか」を予測するモデルは、相互情報量がゼロではない対象を相手にしているので、Walk-Forwardでも帰無データを明確に上回ります。ここでモデルが学習しているのは球の物理ではなく、人の選び方です。

期待値を分解すると構図が見えます。当せん確率は1組あたり 1/6,096,454 で、どの組でも同じです。組によって違うのは、当せんしたときに何人と分け合うかです。機械学習が扱える変数があるとすれば、それは「出やすい数字」ではなく「人が選びやすい数字・選びにくい数字」であり、それは次回の予測というより、分け前の仕組みを読む作業です。

出典・データ:集計方法とデータの範囲

結び:シグナルとノイズを見分ける手順

機械学習は、データの中のシグナルとノイズを見分けるための工学です。シグナルがほとんど見つからない対象に向けると、モデルはノイズを細かく記憶する方向へ進みます。「AI予想」の数字が良く見えやすいのは、訓練データ上の成績、選抜後の成績、未来を含んだ分割という三つの漏えいが、いずれも数字を上へ押し上げるからです。

自分で確かめるときの手順は四つで足ります。①訓練データ上の成績は、未来の成績とは分けて扱う。②分割は必ず時系列で行い、テスト期間の情報を特徴量にもハイパーパラメータ選びにも使わない。③並び順を壊した帰無データで同じ手順を回し、実データがその分布から外れるかで判定する。④何通り試したかを申告し、「差がない」と書くときは、どれほどの効果なら見えたかを陽性対照で添える。この記事はその四つを、ロト6の全履歴に対して実行した記録です。

モデルの限界を先に認める態度は、予測をあきらめることではありません。予測できるものと、できないものを、データに決めさせることです。

出典・データ:集計方法とデータの範囲