JV-Linkデータの落とし穴集 — ハマってから知ること
JV-Linkデータの落とし穴集 — ハマってから知ること
JV-Linkのデータをデータベースに取り込み、スキーマを眺めながらSELECTを書けば、コードは一応動きます。しかし「動いた」と「正しい」は別物です。JV-Linkのデータには、仕様書を隅まで読まないと気づけない暗黙のルールがいくつもあり、知らずに集計するとエラーは出ないのに結果が静かに間違っているという最悪のパターンに陥ります。この記事では、実際にハマってから「先に知りたかった」と思うタイプの落とし穴を6つ紹介します。
落とし穴1: オッズは「10倍した整数の文字列」
odds1_tanshoのoddsカラムを見ると、'0124' のような値が入っています。これは124倍ではなく12.4倍です。JV-Linkのオッズは小数点を持たず、実際の値を10倍した整数をゼロ詰めの文字列として格納しています。
さらに厄介なのは無効値の存在です。
| 格納値 | 意味 |
|---|---|
'0124' |
12.4倍 |
'0010' |
1.0倍 |
'0000' |
オッズなし(無効) |
'****' |
取消・除外などで発売なし |
float(odds) / 10 と素朴に書くと、'****' で例外が飛ぶか、'0000' を0.0倍として通してしまいます。変換は防御的な関数を1つ作って必ずそこを通すのが鉄則です。
def parse_odds(raw: str | None) -> float | None:
"""JV-Linkのオッズ文字列をfloatに変換する。無効値はNoneを返す。"""
if raw is None:
return None
s = raw.strip()
if not s.isdigit():
return None # '****' など発売なしのパターン
v = int(s)
if v == 0:
return None # '0000' はオッズなし
return v / 10.0
この関数を通せば、無効値は一律 None(pandasではNaN)になり、後段の集計から自然に除外されます。複勝オッズ(最低・最高の2値)や馬連・三連単のオッズも同じ形式なので、すべて同じ関数を通します。
落とし穴2: 馬番はゼロ詰め文字列 — intで結合すると突合できない
umagoto_race_johoのumabanには '01' '02' ... '18' というゼロ詰め2桁の文字列が入っています。ここで「数値なんだからintにしよう」と片方のテーブルだけ変換すると、事故が起きます。
# 事故の例: 片方だけintにして結合
results['umaban'] = results['umaban'].astype(int) # 1, 2, ...
merged = results.merge(odds, on=['race_code', 'umaban']) # oddsは'01'のまま
# → 1行もマッチせずmergedが空。エラーは出ない
エラーにならず単に0行マッチになるのがこの罠の怖いところです。INNER JOINなら結果が消え、LEFT JOINなら全行NULLになり、どちらも「オッズが取れていない」ことに気づくのが遅れます。
対策はキー正規化の関数を1つ定義し、結合キーに触るすべての箇所で同じ関数を使うことです。
def norm_umaban(v) -> str:
"""馬番を'01'形式のゼロ詰め2桁文字列に正規化する。"""
return str(int(str(v).strip())).zfill(2)
results['umaban'] = results['umaban'].map(norm_umaban)
odds['umaban'] = odds['umaban'].map(norm_umaban)
SQL側で揃えるなら LPAD(umaban::text, 2, '0') が同じ役割を果たします。race_codeやketto_toroku_bangoなど他のコード系カラムも同様に「文字列のまま扱う」と決めてしまうのが安全です。
落とし穴3: data_kubun — 同じテーブルに速報と確定が混ざる
JV-Linkのレコードにはdata_kubun(データ区分)があり、同じレースのデータが速報・成績確定など複数の段階で配信されます。取り込み方によっては、1レースに対して複数世代の行がテーブルに共存します。
これを知らずに集計すると、レース前の中間オッズや速報段階の着順を「確定値」として掴んでしまいます。オッズの時系列分析をしたいなら中間データはむしろ宝ですが、成績集計や学習データ作成では確定データだけにフィルタする必要があります。
-- 確定データのみを対象にする(区分値は必ず仕様書で確認する)
SELECT race_code, umaban, kakutei_chakujun
FROM umagoto_race_joho
WHERE data_kubun = '5';
区分値の意味はレコード種別ごとに定義が異なるため、'5' を鵜呑みにせず自分が使うレコード種別の仕様書ページで必ず確認してください。まず SELECT data_kubun, COUNT(*) FROM ... GROUP BY data_kubun で手元のデータにどの区分が何件あるかを把握するのが先決です。
落とし穴4: 交流レースでは騎手・調教師コードが '00000' になることがある
地方競馬とのいわゆる交流レースでは、地方所属の騎手・調教師が出てきます。彼らは中央のマスタに登録がないため、kishu_codeやchokyoshi_codeが '00000' のようなダミー値になることがあります。
この状態で騎手マスタとINNER JOINすると、地方所属騎手の行がごっそり消えます。LEFT JOINにしても騎手の勝率などの特徴量はNULLになり、「騎手情報なし」の馬が交流レースに偏って発生します。頼れるのは名前カラム(kishumei_ryakushoなど)だけですが、名前での突合は表記揺れとの戦いになります。
設計段階で決めておくべきことは次の2点です。
- 地方場開催(keibajo_codeが30番台以降)を分析対象に含めるか。中央4場・ローカル場だけを対象にするなら、最初からWHERE句で除外してしまうのが最も単純です
- 含めるなら、コード '00000' を「未知の騎手」として明示的に扱う(落とし穴を踏んだ後で気づくのではなく、ETL時点でフラグ化する)
落とし穴5: 同着・取消・除外 — 特殊値と複数行払戻
kakutei_chakujunは '01'〜'18' だけだと思っていると裏切られます。出走取消・競走除外・競走中止などの馬は '00' などの特殊値になり、異常の内訳はijo_kubun_code(異常区分コード)側に入ります。着順を数値化する前に、「完走した行」だけに絞るフィルタを必ず挟みます。
# 完走馬のみを学習対象にする
df = df[df['kakutei_chakujun'].between('01', '18')].copy()
df['chakujun'] = df['kakutei_chakujun'].astype(int)
もう1つの罠が同着です。同着が発生すると、同じ着順の行が複数できるだけでなく、払戻テーブル(haraimodoshi)では同じ券種に複数セットの組番・払戻金が並びます。単勝の払戻カラムが1〜3のセットで定義されているのはこのためです。「1レース1券種1行」を前提に最初のセットだけ読むコードを書くと、同着レースの回収額計算が狂います。払戻を扱うときは、全セットをループして有効なもの(組番が空でないもの)をすべて拾う実装にしてください。
落とし穴6: track_codeの範囲で芝・ダート・障害を判定する
track_codeは2桁のコードで、おおまかに次の範囲で分かれています。
| 範囲 | 種別 |
|---|---|
| 10〜22 | 芝 |
| 23〜29 | ダート |
| 51以降 | 障害 |
ここでよくあるのが track_code LIKE '1%' で芝を判定するコードです。一見正しそうですが、20〜22の芝コース(外回り・内→外などのバリエーション)を取りこぼします。逆に障害を LIKE '5%' で取ると意図通りに見えて、将来コードが追加されたときの挙動が読めません。
ゼロ詰め2桁の文字列は辞書順と数値順が一致するので、範囲比較で書くのが正確です。
SELECT race_code,
CASE
WHEN track_code BETWEEN '10' AND '22' THEN '芝'
WHEN track_code BETWEEN '23' AND '29' THEN 'ダート'
WHEN track_code >= '51' THEN '障害'
ELSE 'その他'
END AS surface
FROM race_shosai;
「芝のつもりでダートが混ざる」「外回りコースだけ抜け落ちる」といったバグは集計結果からは見抜きにくいので、判定ロジックは1か所に集約し、GROUP BY surface で件数の妥当性を一度確認しておくと安心です。
まとめ
- オッズは10倍した整数の文字列。
'0000'と'****'を弾く変換関数を1つ作って必ず通す - umabanなどコード系カラムはゼロ詰め文字列のまま扱う。intと文字列の混在joinは「エラーなしで0件マッチ」する
- data_kubunで速報と確定が同居する。成績集計は確定データにフィルタ(区分値は仕様書で確認)
- 交流レースでは騎手・調教師コードが解決できないことがある。地方場開催の扱いを設計時に決める
- kakutei_chakujunの特殊値と、払戻の同着複数セットを前提に実装する
- 芝・ダート・障害はtrack_codeの範囲比較で判定する。
LIKE '1%'は芝の一部を取りこぼす
どれも「知っていれば1行で防げるが、知らないと数日溶かす」類のものです。共通する教訓は、コード系カラムは必ず仕様書と突き合わせ、変換・判定ロジックは関数として1か所に集約することです。
JvLink To Importer を使えば、この記事で扱ったテーブル群をGUI操作だけでデータベースに取り込めます。取り込んだ後の落とし穴は、この記事をチェックリスト代わりにしてください。
この記事をシェア