Why not login to Qiita and try out its useful features?

We'll deliver articles that match you.

You can read useful information later.

0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

EDINET有報XBRLの名寄せで踏んだ地雷と、30ルールの品質ロジック

0
Posted at

EDINET DBを作る過程で、有価証券報告書のXBRLデータと格闘した。3,848社×最大6年分の財務データを、会計基準をまたいで名寄せする。

やってみてわかったのは、XBRLは「構造化データ」と謳っているわりに、実運用上は半構造化データに近いということだ。会計基準が3種類あり、企業独自の拡張タクソノミがあり、連結と単体が混在している。

この記事では、名寄せエンジンの設計と、データ品質を担保するために組んだ30ルールの中身を書く。公開ドキュメント(edinetdb.jp/docs/data-quality)の裏側にある、実装上の判断とハマりポイントの記録だ。

問題の構造

「売上高」をAPIで返したい。それだけのことに、これだけの障壁がある。

会計基準ごとにelement_idが違う

JP GAAP(2,933社):

jpcrp_cor:NetSalesSummaryOfBusinessResults

IFRS(295社):

jpcrp_cor:RevenueIFRSSummaryOfBusinessResults

US GAAP(8社):

jpcrp_cor:RevenueUSGAAPSummaryOfBusinessResults

これは公式タクソノミに定義された標準element_idの話で、ここまではドキュメントを読めばわかる。

企業が独自拡張したelement_idがある

IFRS企業の一部は、標準element_idではなく独自拡張を使う。

jpcrp030000-asr_E02144-000:RevenueFromContractsWithCustomers...SummaryOfBusinessResults

これはトヨタの例。jpcrp030000-asr_EXXXXX というプレフィックスで、企業ごとに命名規則が異なる。標準マッピングでは捕捉できない。

連結と単体が混在している

同じelement_idでも、context_idによって連結か単体かが変わる。

context_id: CurrentYearDuration                        → 連結
context_id: CurrentYearDuration_NonConsolidatedMember   → 単体

さらに、配当金や資本金は逆で、NonConsolidatedMemberの方が正しい値になる。一律に「NonConsolidated除外」とやると、配当金が取れなくなる。

2層構造の名寄せエンジン

この問題を、Layer 1(標準マッピング)とLayer 2(正規表現フォールバック)の2層で解決した。

Layer 1: 141パターンの標準マッピング

24の財務指標について、3会計基準×複数のelement_idバリエーションをハードコードしたマッピングテーブルを作った。

FINANCIAL_ITEM_MAP = {
    # 売上高
    "jpcrp_cor:NetSalesSummaryOfBusinessResults": ("revenue", "売上高"),
    "jpcrp_cor:RevenueIFRSSummaryOfBusinessResults": ("revenue", "売上高"),
    "jpcrp_cor:RevenueUSGAAPSummaryOfBusinessResults": ("revenue", "売上高"),
    # 営業利益
    "jppfs_cor:OperatingIncome": ("operating_income", "営業利益"),
    "jpigp_cor:OperatingProfitLossIFRS": ("operating_income", "営業利益"),
    # ...全141パターン
}

指標は売上高、営業利益、純利益のようなP/L項目に加えて、総資産、純資産のB/S項目、営業CF/投資CF/財務CFのCF項目、EPS、BPS、PER、配当金など合計24指標。

ここで1つ地雷があった。営業利益のelement_id問題だ。

JP GAAPでは、SummaryOfBusinessResults(経営指標等の推移)に営業利益の公式element_idが存在しない。代わりにP/L本表のjppfs_cor:OperatingIncomeを使う必要がある。これは他の指標と取得元が異なるため、優先順位の制御が必要になる。

実装では、Key Financial Data(KFD)を最優先で採用し、次に会計基準に一致するelement_idを選ぶという優先度ベースの値選定ロジックを組んだ。

Layer 2: 正規表現フォールバック

Layer 1では約95%のデータをカバーできるが、残り約5%の企業固有拡張element_idを取りこぼす。IFRSのトヨタやソニーなど、大型銘柄がここに含まれるので無視できない。

# 売上高の正規表現
revenue_regex = r'(?i)(NetSales|Revenue|OperatingRevenue|BusinessRevenue|GrossSales).*(?:SummaryOfBusinessResults|KeyFinancialData)'
revenue_exclude = r'(?i)(Intersegment|Cost|Gain|Loss|Allowance|Commission|Refund|Proceeds)'

# 営業利益の正規表現
oi_regex = r'(?i)(OperatingProfitLoss|OperatingIncome|OperatingProfit|BusinessProfit|CoreOperatingIncome).*(?:SummaryOfBusinessResults|KeyFinancialData)'

# 純利益の正規表現
ni_regex = r'(?i)(?:^|:)[^:]*Profit.*SummaryOfBusinessResults'
ni_exclude = r'(?i)(Ordinary|Operating|BeforeTax|GrossProfit|Equity|Earnings|Business)'

ポイントは除外パターンの方にある。「Revenue」にマッチするelement_idは大量にあるが、セグメント間売上、売上原価、売却益なども含まれる。除外条件を厳密に定義しないと、全く別の数字を売上高として返すことになる。

純利益は特に罠が深い。ProfitSummaryOfBusinessResultsにマッチさせたいが、OrdinaryProfit(経常利益)やOperatingProfit(営業利益)やGrossProfit(売上総利益)も全部Profitを含む。除外パターンで7種類を排除してようやく純利益だけが残る。

2パスの連結/単体フォールバック

context_idの処理は2パス構成にした。

Pass 1: 連結データを取得する。NonConsolidatedMemberを含むcontext_idを除外。ただし配当金・資本金は例外で、NonConsolidatedMemberから取る。

Pass 2: Pass 1で値が取れなかった企業(約525社)について、単体データにフォールバックする。NonConsolidatedMemberのcontext_idから値を取得。

ただし全指標をフォールバックするわけではない。EPSと設備投資額と従業員数はフォールバック対象外にしている。連結EPSと単体EPSは意味が違うし、混在させるとスクリーニングの精度が落ちる。

-- EPS, capex, num_employeesはPass 2から除外
AND b.itemName NOT IN ('eps', 'capex', 'num_employees')

30ルールの品質ロジック

名寄せでデータを統一しても、値そのものがおかしいケースがある。単位誤り、株式分割による非連続、業種固有の開示特性。これらを4段階のパイプラインで処理する。

STEP 1: Bronze → Silver(16ルール)

入力フィルタリングとLayer 1/2の名寄せ。ここまでに書いた内容がこのステップに相当する。

  • マッピング済み141element_idのみ通過
  • 数値型データのみ抽出
  • 過去5年度分に限定
  • 孤立行(企業コードor決算期がNULL)を排除
  • Layer 1/2の名寄せ適用
  • 2パス連結/単体フォールバック

STEP 2: Silver → Gold集計(1ルール)

行持ちデータをピボットして、1企業×1年度=1行の横持ちに変換する。24指標が列になる。

STEP 3: 比率算出とサニタイズ(11ルール)

ここが一番地雷が多い。

EPS異常値の検出:

CASE
  WHEN ABS(eps) > 1000000 THEN NULL       -- 単位誤り(100万円超はありえない)
  WHEN raw_prev_eps IS NOT NULL
       AND ABS(SAFE_DIVIDE(eps, raw_prev_eps)) > 10  -- 前年比10倍以上は株式分割
  THEN NULL
  ELSE eps
END AS eps

EPSが100万円を超えるのは、ほぼ確実に単位誤り(円単位のところに千円単位で入っている等)。前年比10倍以上の変動は株式分割or株式併合の影響で、時系列比較に使えないからNULL化する。

BPSも同様のロジック:

CASE
  WHEN ABS(bps) > 10000000 THEN NULL      -- 1,000万円超
  WHEN raw_prev_bps IS NOT NULL
       AND ABS(SAFE_DIVIDE(bps, raw_prev_bps)) > 10
  THEN NULL
  ELSE bps
END AS bps

ROEの債務超過ガード:

CASE
  WHEN netAssets < 0 THEN NULL   -- 純資産がマイナス → ROE計算が意味をなさない
  ELSE SAFE_DIVIDE(netIncome, netAssets)
END AS roe

純資産がマイナス(債務超過)の企業でROEを計算すると、赤字なのにROEがプラスになるという逆転が起きる。会計的に無意味なので排除。

保険業・銀行業のクロススコープガード:

CASE
  WHEN industry IN ('保険業', '銀行業')
       AND netIncome > 0 AND netIncome > revenue
  THEN NULL
  ELSE SAFE_DIVIDE(operatingIncome, revenue)
END AS operatingMargin

保険持株会社は「売上高」の定義が一般事業会社と異なる。連結純利益が連結売上高を上回るケースがあり、これは開示スコープの不一致を示すシグナル。営業利益率をNULLにして誤表示を防ぐ。

配当性向のキャップ:

CASE
  WHEN eps > 0 AND dividendPerShare > 0
       AND SAFE_DIVIDE(dividendPerShare, eps) <= 2.0  -- 200%超はNULL
  THEN SAFE_DIVIDE(dividendPerShare, eps)
END AS payoutRatio

EDINETのDPS(1株当たり配当金)は株式分割を遡及調整しない。過去のDPSが見かけ上膨れて、配当性向が1000%になるケースがある。200%を上限としてそれ以上はNULL化。

STEP 4: モニタリング(2ルール)

Transform実行後に、4つのコア指標(revenue, net_income, total_assets, net_assets)の欠損を自動検知する。

# 欠損企業をWARNINGログに出力
logger.warning(
    "DATA GAP DETECTED: %s (%s, %s) missing: %s",
    row.filerName, row.edinetCode, row.accountingStandard,
    list(row.missing_items)
)

NULL率が20%を超える指標は警告、30%を超えると致命的アラートを出す。現状、検知される欠損企業はかんぽ生命(保険持株会社で売上高なし)、サンバイオ(開発ステージで売上なし)など、開示形式に起因する正常な欠損ばかりだ。

カバレッジの結果

この30ルールで、以下のカバレッジを達成した。

指標 カバレッジ 取得社数 / 全体
売上高 99.6% 3,831 / 3,845
営業利益 97.2% 3,736 / 3,845
純利益 99.9% 3,840 / 3,845
総資産 99.9% 3,841 / 3,845

営業利益の97.2%は他より低いが、内訳を見ると銀行83社、保険10社、IFRS総合商社5社、US GAAP企業6社など、制度的にその指標を開示しない企業が大半。取りこぼしではなく正常な欠損だ。

設計判断のまとめ

  1. ハードコード141パターン+正規表現フォールバックの2層構造: 安定性と網羅性のバランス。新しい企業や会計基準変更があってもLayer 2で吸収できる

  2. 2パス連結/単体フォールバック: 単体決算のみの企業もカバーしつつ、連結/単体の混在を防止。指標によってはフォールバックしない制御が必要

  3. サニタイズは閾値ベースでNULL化: 怪しいデータは「直す」のではなく「消す」方針。間違った数字を返すよりNULLの方が害が少ない

  4. 業種固有の例外処理: 一律ルールだけでは保険・銀行・IFRSで破綻する。業種コードによる分岐は避けられない

ロジックの全文は公開ドキュメントに記載している。


EDINET DB: edinetdb.jp
APIキー発行(無料): edinetdb.jp/developers

0
1
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
edinetdb

@edinetdb(EDINET DB)

全上場3,848社の有報データを構造化し、REST APIとMCPサーバーで提供するサービス、EDINET DBを公開中。 3会計基準を24指標に名寄せ。信用スコア算出ロジック、名寄せルールを全公開しています。 データ基盤の設計、MCP、API設計まわりの技術記事を書きます。 データを民主化したい。

Comments

No comments

Let's comment your feelings that are more than good

Being held Article posting campaign

2025年、生成AIを使ってみてどうだった?

2026-01-19 ~ 2026-02-27

View details
0
1

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Login to continue?

Login or Sign up with social account

Login or Sign up with your email address