目次
毎週通っていた店で構築宣言してしまったのだ。何かしらの進捗が必要だ。本業でもないのに、ネタとしての持ち出し案件だから副業にもならない。
子どもの頃からそうかもしれない。ノリで〇〇宣言してしまうことがあるのだ。「花見の開催宣言」 、「M1出場宣言」 、「火曜はノンアル宣言」。悪いクセである。
いや、予想が当たればいいのだ。当たるよこれ!それをモチベーションにして時間を作ろう。
学習データ
僕は異常値の検出自体はデータ分析の世界ではメジャーな分野であることは知っていた。セキュリティの話題で攻撃を受けたかどうかとか、いつもと違う買い物するとカードが盗まれたんじゃないかとか、そういう話だ。
何にしてもそれを判断するには、判断材料となるデータが必要となる。ボートレースに関連するデータの本家は https://www.boatrace.jp/ だ。
「泣ける」本家ページ構造
2023年12月時点では、今のようなAI・バイブコーディングの環境は存在していない。「泣ける」にはそういう時代背景的な感傷も含まれる。以下のHTMLを見ていただきたい。これは本家Webサイトのレース情報ページの一部分だ。(2026年8月時点でも変わらずノスタルジー、かと言ってこの構造が変わるとそれはそれでコードの修正が必要で厄介。)
<tbody class=" is-fs12">
<tr>
<td class="is-boatColor1 is-fs14" rowspan="4">1</td>
<td rowspan="4">
<a href="/owpc/pc/data/racersearch/profile?toban=5167">
<img src="/racerphoto/5167.jpg" width="67" height="95" alt="" />
</a>
</td>
<td rowspan="4">
<div class="is-fs11">5167 / <span class=" ">B1</span></div>
<div class="is-fs18 is-fBold">
<a href="/owpc/pc/data/racersearch/profile?toban=5167">小川 竜太朗</a>
</div>
<div class="is-fs11">埼玉/埼玉<br />26歳/52.0kg</div>
</td>
<td class="is-lineH2" rowspan="4">F0<br />L0<br />0.16</td>
<td class="is-lineH2" rowspan="4">5.35<br />33.33<br />51.28</td>
<td class="is-lineH2" rowspan="4">4.73<br />21.43<br />40.48</td>
<td class=" "> </td>
<td class=" "> </td>
<td class="is-outColor "> </td>
<!-- …この と is-outColor の空セルがこの後も延々と続く… -->
</tr>
<!-- ↑これで「1号艇ぶん」。rowspan="4" なので、実は4行で1選手を表している -->
</tbody>
見ての通り、racer_name のようなデータの意味を表す手がかりが一つも無い。あるのは is-lineH2(行の高さ?)や is-fs11(文字サイズ?)といった、見た目を調整する記号のような class ばかりだ。
.find(class_='racer_name') と一行で書けたら…と思いながら、rowspan と <br> をほぐすコードを延々と書くことになった。
ちなみに、上のHTMLがブラウザではこう見える(背景が白のセル部分)。

登録番号 5167 も、勝率の 5.35 / 33.33 / 51.28 も、F0 / L0 / 0.16 も、画面とHTMLでちゃんと一致している。人間には一目で読める表が、コードにとっては rowspan と <br> のミルフィーユなのだ。
当時、各データを抽出するコードは Python 3.9 + BeautifulSoup 4 でコーディングした痕跡が残っている。パーサーは lxml を入れるのも面倒で、標準の html.parser、今考えると僕は相当暇だったのだろうか。
そしてこのデータ抽出コーディングのツラさや嫌悪感が、後に無駄な待機時間を延々と引き起こすのだ。
本家サイトに負荷はかけないよう
本家サイトは、CDN(閲覧の負荷はエッジが肩代わりしてくれる)などの対策は万全だろうけど、さすがに集中したアクセスは避けるべきだ。というのも、CDN があってもIP単位のレート制限は別で効いてしまうのではないか。
本家に負荷をかけないために、異常と思われないために、夜間1分間に数レース分ぐらいの取得ペースにしたと記憶している。当初少なくともレース場あたり1,000レース分のデータを収集して実験をしようと考えていたと思う。つまり、データ取得だけでもそれなりの時間がかかるのだ。
ざっと見積もってみる。1レースにつき出走表と結果の2ページ。1場1,000レースで2,000リクエスト、全24場でも5万リクエストに届かない。仮に「1分に5レース」(=6秒に1回くらい)の低速ペースなら、こうなる。
- 1場(1,000レース):約3時間
- 全24場(24,000レース):約80時間 = 深夜に6時間ずつ回して2週間ほど
一度貯めてしまえば、あとは毎日の差分(全24場でも1日あたり約288レース、小一時間)を足していくだけ。そして、焼鳥屋では「いやぁ、データの収集に時間かかるんですよー。」で先延ばしできる。本当の事だ嘘ではない。
無駄な収集を行わないために
ここで1つお伝えしたい。
データを収集するならHTMLの生データも保存しておけである。
実はこの時期、僕はデータ収集の約80時間を3回経験している。とにかくデータを抽出するコーディングが面倒だったので、おそらく必要だろうと感じたデータのみ、まったくの主観で抽出していたのだ。たとえば、初期段階では大穴予想しか考えていなかったので関係なさそうな選手番号や体重、年齢なんかはデータを抽出していなかった。これは間違いだ。選手番号がなければ、選手の時系列データを分析できない。
そして、あのデータを抽出しておけば...。と思ったとき、また本家に80時間アクセスしなければならなくなるわけだ。
経験のある皆さんならもうお気づきであろう。一度HTMLの生データを保存しておけば、必要になったデータをローカルディスクからいくらでも抽出できるじゃないか!
当時利用してたAWSサービス
自分のMac Bookでローカル実行すると長時間だから不安になる。なので実行環境をクラウドサービスに移行した。データ取得コードを AWS Lambda に移植して Step Functions で回すことにした。夜間の Step Functions の起動は EventBridge、取得データの保存先はどんな感じか勉強したかったので DynamoDB に保存。
仕事でAWS環境を触ることがあっても、1からアカウント作成したことがなかったので、当初は無料枠って結構あるんだなとおどろいた記憶がある。
ただ、今どうしても思い出せないのは何故AWSを選択したかだ。最近は本業では Google Cloud のほうが作業は多い。思いだせない。最初はデータを蓄えるだけと考えてそんなに深くアーキテクチャを設計していたわけではないと思う。実験だから。こんなに長く触るシステムになるとも思っていなかった。現時点で他のクラウドに乗り換えるつもりは特にはないが。
最初に勉強がてら DynamoDB にデータを蓄積してしまったのが尾を引いて、今でも同じ AWS 環境を使用するはめになったのではないか。いくら実験だとしても計画モードは大切だ。
次は、実際の学習データ、特徴量の中身 Feature Engineering について記録しておこう。
つづく。
