JvLink To Importer

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操作だけでデータベースに取り込めます。取り込んだ後の落とし穴は、この記事をチェックリスト代わりにしてください。

この記事をシェア

Post