このプロジェクトについて
EDINET(有価証券報告書等)の開示データをもとに、日本株の財務指標・投資判断スコアを日次で算出・公開しているサービスです。
公開サイトはこちら →
https://stocks.happy-life-design.net
前回の記事はこちら →
https://qiita.com/rightcross-ae86/items/778f8911eae0baa3dbff
今回やったこと
so what: 判定を保留していた16社分のデータを、全件クローズできた話です
個別決算と連結決算で純資産・総資産の値に大きな差がある会社は、既存の自動バックフィル処理の安全装置(unsafe_fieldsガード)が「誤検知の可能性がある」として更新を保留する仕組みになっています。この保留が16社分たまっていました。今回、新しい診断方法を実装してこの16社を1件ずつ仕分けし、全件を根拠つきでクローズしました。
前提: なぜ保留になっていたか
決算期変更や連結範囲の変動がある会社では、個別決算の値と連結決算の値が同じ年度でも大きく異なることがあります。これが「本当にそういうデータなのか」「取得・計算のどこかにバグがあって間違った値を拾っているだけなのか」を機械的に区別できず、安全側に倒して更新を止めていました。
新しい診断方法: 対象年に最も近い決算期で直接比較する
従来は対象年そのものの個別/連結を比較する方法に頼っていましたが、対象年自体にEDINET提出書類が存在しない(上場前など)ケースでは判定材料がありませんでした。そこで、対象年に最も近い既報告年の個別/連結を直接比較する診断ロジックを新規実装し、16社全社に適用しました。
仕分け結果
-
11社(12期分): 誤検知と確定 → 本番反映(12行)
対象年に最も近い年で個別/連結が完全一致、または連結タグ自体が存在しない(=当時非連結)ことを直接確認できたケース。安全な値であることが確認できたため本番データを修正しました(1社のみ対象年の書類が2期分あったため12期分)。 -
3項目: 本当の乖離と確定 → NULL維持
対象年に最も近い年で比較しても15.3〜83.8%の乖離が実在すると確認できたケース。誤検知ではなく、データを埋めずNULLのままにしておくのが正しい判断でした。 -
3社: 証拠不十分 → 追加調査でクローズ
上記の比較だけでは判定できなかった3社について、純資産をXBRLの明細(資本金・資本剰余金・利益剰余金・評価換算差額等)まで分解して再調査したところ、いずれも実体のある子会社利益の蓄積が乖離の原因と判明しました。たとえば1社は乖離額が観測できた最も古い年で数千万円規模だったものが、その後2年で倍以上に拡大しており、子会社が実在し成長している証拠として扱えると判断しました。こちらもコード変更・DB反映なしでクローズです。
実装
安全と確定した値は、既存のバックフィル処理本体(extract_prior_financials())には手を入れず、確認済みの値だけを直接登録する別経路として実装しました。既存ロジックの判定基準自体を緩めると他のデータにも影響が及ぶため、「今回個別に確認できた分だけ」を明示的なテーブルとして持たせる方式です。
# 個別調査で確定した値だけを (ticker, period_end) キーで直接登録
_CONFIRMED_SAFE_INDIVIDUAL_VALUES: dict[tuple[str, str], dict[str, float]] = {
("198A", "2023-05-31"): {"equity": 605554000.0, "total_assets": 918037000.0},
("9331", "2022-08-31"): {"equity": 1010402000.0}, # total_assetsは対象外
("5892", "2023-03-31"): {"equity": 230524000.0, "total_assets": 1341605000.0},
("5892", "2022-03-31"): {"equity": 298974000.0, "total_assets": 843481000.0},
# ...11社12期分
}
def cmd_apply_c65a_safe_overrides(conn, dry_run: bool = False):
for (ticker, period_end_str), vals in _CONFIRMED_SAFE_INDIVIDUAL_VALUES.items():
# ticker から company_id を引く処理は省略
cur.execute(
"UPDATE financials SET "
"equity = CASE WHEN equity IS NULL THEN COALESCE(%s, equity) ELSE equity END, "
"total_assets = CASE WHEN total_assets IS NULL THEN COALESCE(%s, total_assets) ELSE total_assets END "
"WHERE company_id = %s AND period_end = %s AND period_type = 'annual'",
(vals.get("equity"), vals.get("total_assets"), company_id, period_end_str),
)
CASE WHEN equity IS NULLのガードにより、万が一キーの対応関係を間違えても既存の非NULL値を上書きしてしまうことはありません。equity/total_assetsを独立した列として扱っているため、9331のように片方の値しか確定していない場合でも安全に反映できます。
検証
本番反映した11社12期分は、反映前後で全社分(54,303行)のデータをバックアップとto_jsonbで突き合わせ、変化した行・列が意図した範囲(該当12行のequity/equity_edinet/total_assets/total_assets_edinet列のみ)に収まっていることを確認しました。既存のユニットテストも実行前後で全件通過しています。
「誤検知かもしれない」という理由だけで保留し続けていたデータでも、比較の基準(どの年で比較するか)を変えるだけで機械的に仕分けられるケースが大半でした。残り少数は数値の直接比較だけでは判断できず、明細レベルまで分解して初めて実態(子会社の利益蓄積)が見えてきました。気になる点・ご質問があればコメントでお知らせください。