組合せ最適化による競馬必勝法

## これなに

「[競馬必勝法をプログラム化](https://qiita.com/naonao_py/items/019b15876e3ef02d92f2)」を組合せ最適化で解いてみました。

- 考え方は「[うまめし.com 競馬必勝法](https://www.umameshi.com/info/0011.html)」を参考にしてます。
- プログラムについては、「[原油の製油の最適化 - モデル化のポイント](https://qiita.com/SaitoTsutomu/items/e3ef218b78fd859b4aeb#%E3%83%A2%E3%83%87%E3%83%AB%E5%8C%96%E3%81%AE%E3%83%9D%E3%82%A4%E3%83%B3%E3%83%88)」を参考にしてください。

## 元データ

2013年9月20日名古屋競馬1レースの単勝と馬単の表です。
この表の各行ごとに馬券を何枚買うかを求めます。

```py
from ortoolpy import addbinvars, lpSum, model_min, pd


def sample_data_frame() -> pd.DataFrame:
    """2013年9月20日名古屋競馬1レースの表

    :return: 「First=1着番号、Second=2着番号（-1なら単勝）、Odds=オッズ」の表
    """
    ls = list(range(1, 8))
    df1 = pd.DataFrame({  # 単勝
        "First": ls,
        "Second": [-1] * 7,
        "Odds": [14.7, 72.6, 1.4, 2.3, 151.8, 18.0, 66.8],
    })
    df2 = pd.DataFrame({  # 馬単
        'First': [i for i in ls for _ in ls[1:]],
        'Second': [j for i in ls for j in ls if i != j],
        'Odds': [
            347.2, 37.4, 54.0, 662.7, 169.6, 607.5, 455.6, 177.8, 197.1,
            1457.9, 331.4, 2429.8, 23.0, 86.8, 5.4, 50.7, 13.0, 177.8, 10.3,
            49.3, 1.9, 90.0, 5.9, 123.6, 1214.9, 1457.9, 74.4, 455.6, 560.8,
            3644.7, 119.5, 347.2, 27.6, 43.2, 486.0, 455.6, 1041.4, 2429.8,
            1214.9, 911.2, 3644.7, 911.2
        ]
    })
    return pd.concat([df1, df2]).reset_index(drop=True)


df = sample_data_frame()
# First=1着番号、Second=2着番号（-1なら単勝）、Odds=オッズ
df[:3]  # 先頭3行
```

|    |   First |   Second |   Odds |
|---:|--------:|---------:|-------:|
|  0 |       1 |       -1 |   14.7 |
|  1 |       2 |       -1 |   72.6 |
|  2 |       3 |       -1 |    1.4 |

## 最適化

```py
def solve(df: pd.DataFrame, num: int, alpha: float) -> pd.DataFrame:
    """購買数を求める

    :param df: 変数表
    :param num: 購買数上限
    :param alpha: リターン比
    :return: 変数表（Val列が購買数）
    """
    n = len(df[df.Second == -1])  # 単勝の行数
    m = model_min(dfi=df)  # 数理モデル
    df["Mono"] = None  # 単勝を買うかどうか
    df.loc[: n - 1, "Mono"] = addbinvars(n)
    m += lpSum(df.Var)  # 目的関数（総購買数）
    m += lpSum(df.Var) <= num  # 購買数上限
    for row in df[:n].itertuples():
        m += row.Odds * row.Var >= num * alpha * row.Mono
        df.loc[df.First == row.First, "Mono"] = row.Mono
    for row in df[n:].itertuples():
        m += row.Odds * row.Var >= num * alpha * (1 - row.Mono)
    m.solve()
    df["Prize"] = df.Odds * df.Val * 100
    return df[df.Val > 0] if m.status == 1 else None


res = solve(df, num=1000, alpha=1.08)
res
```

|    |   First |   Second |   Odds | Var     | Mono    |   Val |   Prize |
|---:|--------:|---------:|-------:|:--------|:--------|------:|--------:|
|  1 |       2 |       -1 |   72.6 | v000002 | v000051 |    15 |  108900 |
|  3 |       4 |       -1 |    2.3 | v000004 | v000053 |   470 |  108100 |
|  4 |       5 |       -1 |  151.8 | v000005 | v000054 |     8 |  121440 |
|  5 |       6 |       -1 |   18   | v000006 | v000055 |    60 |  108000 |
|  7 |       1 |        2 |  347.2 | v000008 | v000050 |     4 |  138880 |
|  8 |       1 |        3 |   37.4 | v000009 | v000050 |    29 |  108460 |
|  9 |       1 |        4 |   54   | v000010 | v000050 |    20 |  108000 |
| 10 |       1 |        5 |  662.7 | v000011 | v000050 |     2 |  132540 |
| 11 |       1 |        6 |  169.6 | v000012 | v000050 |     7 |  118720 |
| 12 |       1 |        7 |  607.5 | v000013 | v000050 |     2 |  121500 |
| 19 |       3 |        1 |   23   | v000020 | v000052 |    47 |  108100 |
| 20 |       3 |        2 |   86.8 | v000021 | v000052 |    13 |  112840 |
| 21 |       3 |        4 |    5.4 | v000022 | v000052 |   200 |  108000 |
| 22 |       3 |        5 |   50.7 | v000023 | v000052 |    22 |  111540 |
| 23 |       3 |        6 |   13   | v000024 | v000052 |    84 |  109200 |
| 24 |       3 |        7 |  177.8 | v000025 | v000052 |     7 |  124460 |
| 43 |       7 |        1 | 1041.4 | v000044 | v000056 |     2 |  208280 |
| 44 |       7 |        2 | 2429.8 | v000045 | v000056 |     1 |  242980 |
| 45 |       7 |        3 | 1214.9 | v000046 | v000056 |     1 |  121490 |
| 46 |       7 |        4 |  911.2 | v000047 | v000056 |     2 |  182240 |
| 47 |       7 |        5 | 3644.7 | v000048 | v000056 |     1 |  364470 |
| 48 |       7 |        6 |  911.2 | v000049 | v000056 |     2 |  182240 |

- 購買数の上限は1000枚（`num`）です。1枚100円なので、軍資金は10万円です。
- どの馬が勝っても最低10万8千円（軍資金×`alpha`）の配当になるように制約条件をつけます。
- 結果として999枚買います（`res.Val.sum()`）。
- 1から7のどの馬が来ても`Prize`列が10万8千円を超えています。

`alpha = 1.08`は2分探索で求めると良いでしょう。
机上の空論でなく、100%儲かるところがすごいですね。

以上