高配当株の業種別財務健全性|BIS・LTV・格付けの使い分け方

Chelsea-Labs #18 サムネイル

免責事項

本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。記事中のコード・業種別代替指標・閾値は教育目的であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。本記事中に J-Quants API から取得した実データは掲載していません(利用規約に基づく方針、詳細は 記事#13)。業種別閾値(BIS比率・LTV・ソルベンシー比率・格付けなど)は2026年5月時点の参考値であり、規制改定・市場環境により最新値は変動するため、実運用前に各規制当局・業界統計の最新情報をご確認ください。

前回の記事#17では、IQR・Zスコア・SPC 管理限界(UCL/LCL = ±3σ)でバリュートラップ候補を検知し、応用編 #15 の判定器に罠フラグを統合しました。応用編 #14/#16/#17 で何度か言及してきた「業種特性で機能しない指標がある」という課題に、本記事で正面から取り組みます。

応用編シリーズで繰り返し出てきた問題:

  • 銀行は事業構造上レバレッジ前提のため、自己資本比率(純資産/総資産ベース)5〜10%が普通。両学長基準「40%以上」を機械適用するとメガバンク3社が全社FAIL(同じ銀行でも BIS リスクアセット分母なら13-15% で別の意味)
  • REITは LTV(Loan to Value、ローン対物件価値比率)50% 以下が健全。自己資本比率では評価できない
  • 商社は自己資本比率20〜30%が普通で、信用格付け(A格以上)で安全性を判断する
  • 保険はソルベンシー・マージン比率(Solvency Margin Ratio、200%以上が健全)が標準
  • 営業CFプラス年数は金融業(銀行・証券・保険)では事業構造上意味が違う

本記事はこれらを業種別代替指標テーブルとして体系化し、応用編 #15 の判定器が業種特性に応じた指標で評価できる状態を作ります。さらに記事 #16 で先送りした業種別 CV 閾値と、記事 #17 で先送りした記念配当・特殊配当の検知も合わせて実装し、応用編 Phase 3 の集約形を整えます。

筆者は製造業の開発現場で、製品コスト構造分析を多く経験してきました。「同じ”製品”でも、種類によって見るべきコスト指標が違う」というのが製造業 DX の基本動作で、これが投資の業種別財務健全性評価とまったく同じ構造であることに、応用編シリーズを書きながら気づきました。本記事はその「業種ごとに見る指標を切り替える設計思想」を、思想編として整理しつつ実装まで落とし込む回です。

応用編シリーズの SPC 4軸メタファー(本記事で集約)

応用編 #11〜#17 で導入してきた製造業 SPC(統計的工程管理)のメタファーが、本記事で4軸に集約されます:

  • 1軸目: USL/LSL(規格限界) = 両学長基準(記事 #11/#12 で導入)
  • 2軸目: UCL/LCL(管理限界 ±3σ) = SPC 異常値検知(記事 #16/#17 で導入)
  • 3軸目: IQR / Z スコア / WER(連続性) = ロバスト外れ値 + 連続性判定(記事 #17 で導入)
  • 4軸目: 業種別補正テーブル(製品ファミリー別検査) = 本記事の主題

4軸が揃うことで、応用編 #15 のスクリーニング判定器が「規格 × 管理限界 × 外れ値 × 業種補正」の多層フィルタで評価できる状態になります。

「実装したくない読者」向け代替案

業種別の代替指標を全部覚えるのが大変、という読者には、「自分が興味のある業種を1つに絞る」のが現実解です。製造業中心なら自己資本比率+営業CFで十分、銀行に投資したいなら BIS 比率を1つだけ覚える、というように。本記事の自動化は「全業種を横断して機械判定する」ためのもので、特定業種に集中する読者なら指標を絞った方が学習効率が高いです。SBI証券・楽天証券のスクリーニングツールでも業種絞り込みは無料で可能です。

本記事の前提と難易度

  • 記事 #11〜#17 のパイプライン(取得 → 統合 → 自動化 → 時系列 → 罠検知)が完了している前提
  • SQL DDL(CREATE TABLE)・pandas DataFrame の基本
  • 業界規制・財務分析の基礎知識(BIS、ソルベンシー、格付け等)があると進めやすい。未経験の方は本記事のセクション2 の概念図でキャッチアップ可能
  • J-Quants プラン: Light で十分(業種別代替指標は EDINET XBRL から取得するため、J-Quants の遅延は影響しない)
  • 動作環境: Python 3.11+ / pandas 2.x / duckdb 1.0+ / matplotlib 3.9+(boxplot の tick_labels= パラメータが 3.9 以降の仕様)
  • 本記事のテーブルが industry_indicator_map に統合されると、応用編 #15 の判定器が「業種に応じた指標」で評価できるようになります

本記事では 5 個のサンプルコードを通じて、業種別代替指標テーブルの本格設計から記念配当検知(DDL/upsert/ビュー統合まで)まで、応用編シリーズの「業種補正の使い分け方」を集約します。本記事の次は応用編 #19 で業種分散(ポートフォリオ全体での FMEA 設計)に進み、応用編 #20 でパイプライン全体像を総括します。

結論:高配当株の業種別財務健全性は「製造業のコスト構造分析」と同型で、業種ごとに見るべき指標が異なる。銀行は BIS、REIT は LTV、商社は格付け、保険はソルベンシー・マージン、製造業は自己資本比率+営業CF。これを industry_indicator_map テーブルで管理することで、コードを書き換えずに業種を追加できる設計が完成する。本記事のテーブルが応用編 #15 の判定器に統合されると、PASS/CAUTION/FAIL 判定が業種特性を反映した実用的な精度で出る。応用編 SPC 4軸の最後のピース。

目次

「業種ごとに見る指標が違う」を製造業コスト構造で理解する

製造業の製品コスト構造分析では、製品の種類によって「重視するコスト指標」が大きく変わります。これは投資の業種別財務健全性評価と完全に対応する構造で、本記事の核心となる考え方です。

製品の種類主に見るコスト指標対応する投資業種主に見る財務指標
量産製品(自動車・家電)材料費比率、労務費比率製造業自己資本比率、営業CF
装置産業(電力・化学プラント)固定費比率、稼働率電力・ガス、通信有利子負債/EBITDA、減価償却前利益
受託サービス(外食・小売)原価率、回転率小売・サービス売上高営業利益率、棚卸資産回転率
金融商品(保険・投資商品)引当金比率、運用利回り銀行・保険・REITBIS比率、ソルベンシー比率、LTV
商社・仲介事業取扱高、信用リスク評価商社信用格付け、セグメント別ROE

エンジニア的に言い換えると(製造業 DX の業種補正パターン)

製造業の品質管理では、「製品ファミリー別の検査閾値テーブル」が標準で存在します。同じ “製品” でも、自動車部品と医療機器では検査項目・規格値・許容公差がまったく違うため、コードで if 分岐するのではなく テーブル駆動 で設計するのが定石です。

本記事の industry_indicator_map 拡張は、この「製品ファミリー別検査閾値テーブル」をそのまま投資データに持ち込んだものです。記事 #14 で導入したテーブル駆動の設計が、ここで真価を発揮します。SPC 用語で言えば「規格限界(USL/LSL)を業種別に切り替える仕組み」で、応用編 SPC 4軸の業種別補正そのものです。

業種別代替指標の早見表(本記事で実装する10業種・東証33業種の主要部分)

本記事は東証33業種のうち、主要な10業種をカバーします。残る業種(食品、化学、電気機器、自動車、素材、陸運、空運、証券、SI 等)は応用編 #19/#20 で順次追加していく前提です(10業種で東証時価総額の概ね7割をカバー)。

業種プロファイル主指標PASS 閾値方向性閾値の意味出典/規制
manufacturing(製造業)自己資本比率40%以上higher_better業界実勢中央値中小企業庁 業種別経営指標
banking(銀行)BIS自己資本比率(Capital Adequacy Ratio)8%以上(国際基準)higher_better規制最低値金融庁 バーゼルIII
insurance(保険)ソルベンシー・マージン比率(Solvency Margin Ratio)200%以上higher_better規制最低値(早期是正措置の境界)金融庁 早期是正措置
reit(REIT)LTV(Loan to Value)50%以下lower_better業界実勢健全水準投資信託協会 ガイドライン
trading(商社)信用格付けスコア(A=4, BBB=3)4以上higher_better業界実勢健全水準S&P/Moody’s/R&I
infrastructure(電力・通信)有利子負債/EBITDA5倍以下lower_better業界平均業種別動向(日本政策金融公庫等)
retail(小売)売上高営業利益率3%以上higher_better業界平均業種別経営指標(中小企業庁)
real_estate(不動産)有利子負債比率70%以下lower_better業界実勢健全水準業種別経営指標
pharma(製薬)研究開発費/売上比率10%以上higher_better業界実勢成長性指標業種別動向(医薬品業)
shipping(海運)有利子負債/船舶資産60%以下lower_better業界実勢健全水準業種別経営指標

PASS 閾値の意味の非対称性に注意

業種ごとに PASS 閾値の“意味”が異なります:

  • 規制最低値型(銀行 BIS 8% / 保険ソルベンシー 200%): 規制当局が定めた最低基準。これを下回ると是正措置の対象。実勢はもっと高い(メガバンクの BIS は 13-15% が普通)
  • 業界実勢型(REIT LTV 50% / 商社格付け A / 不動産 70%): 業界平均から見た健全水準。これを満たさなくても即座に問題ではない
  • 業界平均型(製造業 40% / 小売 営業利益率 3%): 業種内の中央値水準。これより下は「業種内では下位」を意味する

このため「全業種一律で PASS = 健全」と読むのは誤解で、業種ごとの意味を理解した上で判定結果を読む必要があります。

スニペット1:業種別代替指標テーブルを10業種に拡張するサンプルコード

記事 #14 で導入した industry_indicator_map テーブルを、10業種の代替指標に拡張します。記事 #14 では4業種(製造業・銀行・REIT・商社・インフラ)でしたが、本記事で保険・小売・不動産・製薬・海運を追加します。

# industry_map_seed_v2.py — 10業種の業種別代替指標テーブル本格 SEED
# 動作環境: Python 3.11+ / duckdb 1.0+
import duckdb
import pandas as pd
from pathlib import Path

DB_PATH = Path("data/stocks.duckdb")

# 業種別補正プロファイル(10業種、出典・時点を必ず記録)
SEED = pd.DataFrame([
    # 製造業(標準ルール、higher_better)
    {"industry_profile": "manufacturing", "standard_indicator": "equity_ratio",
     "alt_indicator": None, "direction": "higher_better",
     "threshold_pass": 40.0, "threshold_caution": 30.0,
     "note": "標準ルール: 自己資本比率40%以上をPASS(業界実勢中央値)",
     "source": "中小企業庁 業種別経営指標(2026年5月時点)"},

    # 銀行(BIS = Capital Adequacy Ratio)
    {"industry_profile": "banking", "standard_indicator": "equity_ratio",
     "alt_indicator": "bis_ratio", "direction": "higher_better",
     "threshold_pass": 8.0, "threshold_caution": 4.0,
     "note": "国際統一基準8%・国内基準4%(規制最低値)。メガバンク実勢は13-15%",
     "source": "金融庁 自己資本比率規制(バーゼルIII、2026年5月)"},

    # 保険(Solvency Margin Ratio)
    {"industry_profile": "insurance", "standard_indicator": "equity_ratio",
     "alt_indicator": "solvency_margin", "direction": "higher_better",
     "threshold_pass": 200.0, "threshold_caution": 100.0,
     "note": "200%以上が健全(規制最低値)、100%未満は早期是正措置の対象",
     "source": "金融庁 ソルベンシー・マージン比率規制(2026年5月)"},

    # REIT(LTV = Loan to Value)
    {"industry_profile": "reit", "standard_indicator": "equity_ratio",
     "alt_indicator": "ltv", "direction": "lower_better",
     "threshold_pass": 50.0, "threshold_caution": 60.0,
     "note": "LTV 50%以下が健全(業界実勢)、60%超は警戒。J-REIT 業界平均 40-45%",
     "source": "投資信託協会 不動産投信動向(2026年5月)"},

    # 商社(信用格付けスコア)
    {"industry_profile": "trading", "standard_indicator": "equity_ratio",
     "alt_indicator": "credit_rating_score", "direction": "higher_better",
     "threshold_pass": 4.0, "threshold_caution": 3.0,
     "note": "S&P/Moody's の A格以上をPASS(業界実勢)、BBBをCAUTION",
     "source": "S&P Global Ratings / Moody's 格付け体系(2026年5月)"},

    # インフラ(電力・通信)— 有利子負債/EBITDA
    {"industry_profile": "infrastructure", "standard_indicator": "equity_ratio",
     "alt_indicator": "debt_to_ebitda", "direction": "lower_better",
     "threshold_pass": 5.0, "threshold_caution": 7.0,
     "note": "5倍以下が健全(業界平均)。装置産業の固定費比率の高さを反映",
     "source": "業種別動向(公益事業セクター、2026年5月)"},

    # 小売 — 売上高営業利益率
    {"industry_profile": "retail", "standard_indicator": "equity_ratio",
     "alt_indicator": "operating_margin", "direction": "higher_better",
     "threshold_pass": 3.0, "threshold_caution": 1.5,
     "note": "薄利多売の構造、3%以上で健全(業界平均)",
     "source": "業種別経営指標(中小企業庁、小売業、2026年5月)"},

    # 不動産 — 有利子負債比率
    {"industry_profile": "real_estate", "standard_indicator": "equity_ratio",
     "alt_indicator": "debt_ratio", "direction": "lower_better",
     "threshold_pass": 70.0, "threshold_caution": 80.0,
     "note": "借入が事業構造上多い、70%以下が業界実勢の許容範囲",
     "source": "業種別経営指標(不動産業、2026年5月)"},

    # 製薬 — 研究開発費/売上比率
    {"industry_profile": "pharma", "standard_indicator": "equity_ratio",
     "alt_indicator": "rnd_to_sales", "direction": "higher_better",
     "threshold_pass": 10.0, "threshold_caution": 5.0,
     "note": "10%以上のR&D投資が長期競争力の指標(業界実勢)",
     "source": "業種別動向(医薬品業、2026年5月)"},

    # 海運 — 有利子負債/船舶資産
    {"industry_profile": "shipping", "standard_indicator": "equity_ratio",
     "alt_indicator": "debt_to_vessel_assets", "direction": "lower_better",
     "threshold_pass": 60.0, "threshold_caution": 75.0,
     "note": "船舶資産依存度が高い、60%以下で業界実勢の健全水準",
     "source": "業種別経営指標(海運業、2026年5月)"},

    # 配当性向の業種別 USL(既存に追加分)
    {"industry_profile": "manufacturing", "standard_indicator": "payout",
     "alt_indicator": None, "direction": "lower_better",
     "threshold_pass": 60.0, "threshold_caution": 80.0,
     "note": "製造業は配当性向60%以下を PASS",
     "source": "両学長基準 + 業種平均"},
    {"industry_profile": "infrastructure", "standard_indicator": "payout",
     "alt_indicator": None, "direction": "lower_better",
     "threshold_pass": 80.0, "threshold_caution": 100.0,
     "note": "通信・電力は安定収益型のため80%まで許容",
     "source": "業種平均(公益事業)"},
    {"industry_profile": "reit", "standard_indicator": "payout",
     "alt_indicator": None, "direction": "lower_better",
     "threshold_pass": 100.0, "threshold_caution": 110.0,
     "note": "REITは利益のほぼ全額を配当する仕組み(90%超が普通)",
     "source": "REIT 制度・業種平均"},
])

INSERT_COLUMNS = "(industry_profile, standard_indicator, alt_indicator, direction, threshold_pass, threshold_caution, note, source)"

def seed_industry_map_v2() -> None:
    """10業種の業種別代替指標を idempotent に投入(明示カラム指定で順序依存解消)"""
    with duckdb.connect(str(DB_PATH)) as conn:
        conn.register("seed_df", SEED)
        conn.execute("BEGIN")
        try:
            conn.execute("DELETE FROM industry_indicator_map")
            conn.execute(f"""
                INSERT INTO industry_indicator_map {INSERT_COLUMNS}
                SELECT industry_profile, standard_indicator, alt_indicator, direction,
                       threshold_pass, threshold_caution, note, source FROM seed_df
            """)
            conn.execute("COMMIT")
        except Exception:
            conn.execute("ROLLBACK")
            raise
        finally:
            conn.unregister("seed_df")
        n = conn.execute("SELECT COUNT(*) FROM industry_indicator_map").fetchone()[0]
        print(f"industry_indicator_map rows: {n}")

if __name__ == "__main__":
    seed_industry_map_v2()

エンジニア的に言い換えると(規格テーブルの正規化)

本スニペットは、製造業 DX で言う 「製品ファミリー別規格表のマスタデータ管理(MDM)」そのものです。notesource カラムで規格値の根拠と出典を必ず記録するのは、規制改定時に「なぜこの数値か」を遡れるようにするための作法。製造業の規格管理でも「規格番号・改定履歴・根拠規格」のメタデータが必須なのと、構造的に同じです。

スニペット2:業種別 CV 閾値テーブルの追加(記事 #16 で先送りした課題)

記事 #16 の時系列分析で「CV 0.3 一律閾値では商社・資源連動企業が過剰 CAUTION 化」する問題を指摘していました。本記事で業種別の CV 閾値テーブルを追加し、応用編 #15 の判定器が業種特性に応じた CV しきい値で評価できるようにします。CV 閾値の数値は過去5期分の EPS 変動係数を業種別に集計し、業界中央値の +1σ を CAUTION、+2σ を FAIL の境界とした算出ベースです。

# industry_cv_threshold.py — 業種別 CV 閾値テーブル(v2: 明示 INSERT)
# 動作環境: Python 3.11+ / duckdb 1.0+
import duckdb
import pandas as pd
from pathlib import Path

DB_PATH = Path("data/stocks.duckdb")

DDL_INDUSTRY_CV = """
CREATE TABLE IF NOT EXISTS industry_cv_threshold (
    industry_profile    VARCHAR PRIMARY KEY,
    cv_pass             DOUBLE,             -- CV がこの値未満なら PASS
    cv_caution          DOUBLE,             -- CV がこの値未満なら CAUTION(以降 FAIL)
    note                VARCHAR,
    source              VARCHAR
);
"""

# 業種別 CV 閾値(EPS の変動係数)— 業界中央値 + 1σ/+2σ ベース
SEED_CV = pd.DataFrame([
    {"industry_profile": "manufacturing",  "cv_pass": 0.3, "cv_caution": 0.5,
     "note": "製造業安定型: CV 0.2-0.3 が典型(業界中央値 0.25 + 1σ)",
     "source": "業種平均(2026年5月)"},
    {"industry_profile": "trading",        "cv_pass": 0.5, "cv_caution": 0.7,
     "note": "商社: 資源価格・為替の影響大、CV 0.4-0.6 が典型",
     "source": "業種平均"},
    {"industry_profile": "shipping",       "cv_pass": 0.6, "cv_caution": 0.9,
     "note": "海運: 海運市況の変動を反映、CV 0.5-0.9",
     "source": "業種平均"},
    {"industry_profile": "infrastructure", "cv_pass": 0.2, "cv_caution": 0.3,
     "note": "通信・電力: 規制業種で安定、CV 0.1-0.2 が典型",
     "source": "業種平均"},
    {"industry_profile": "banking",        "cv_pass": 0.4, "cv_caution": 0.6,
     "note": "銀行: 金利環境・引当金で変動、CV 0.3-0.6",
     "source": "業種平均"},
    {"industry_profile": "insurance",      "cv_pass": 0.4, "cv_caution": 0.6,
     "note": "保険: 自然災害・運用環境で変動",
     "source": "業種平均"},
    {"industry_profile": "reit",           "cv_pass": 0.2, "cv_caution": 0.35,
     "note": "REIT: 賃料収入主体で安定、CV 0.1-0.2",
     "source": "業種平均"},
    {"industry_profile": "retail",         "cv_pass": 0.3, "cv_caution": 0.5,
     "note": "小売: 季節性・天候の影響あり、CV 0.2-0.4",
     "source": "業種平均"},
    {"industry_profile": "real_estate",    "cv_pass": 0.4, "cv_caution": 0.6,
     "note": "不動産: 物件売却益の不規則計上で変動",
     "source": "業種平均"},
    {"industry_profile": "pharma",         "cv_pass": 0.3, "cv_caution": 0.5,
     "note": "製薬: パイプライン・特許切れで変動",
     "source": "業種平均"},
])

CV_INSERT_COLUMNS = "(industry_profile, cv_pass, cv_caution, note, source)"

def setup_industry_cv() -> None:
    with duckdb.connect(str(DB_PATH)) as conn:
        conn.execute(DDL_INDUSTRY_CV)
        conn.register("seed_cv", SEED_CV)
        conn.execute("BEGIN")
        try:
            conn.execute("DELETE FROM industry_cv_threshold")
            conn.execute(f"""
                INSERT INTO industry_cv_threshold {CV_INSERT_COLUMNS}
                SELECT industry_profile, cv_pass, cv_caution, note, source FROM seed_cv
            """)
            conn.execute("COMMIT")
        except Exception:
            conn.execute("ROLLBACK")
            raise
        finally:
            conn.unregister("seed_cv")
        n = conn.execute("SELECT COUNT(*) FROM industry_cv_threshold").fetchone()[0]
        print(f"industry_cv_threshold rows: {n}")

if __name__ == "__main__":
    setup_industry_cv()

エンジニア的に言い換えると(業種別の工程能力指数)

業種別 CV 閾値テーブルは、製造業 DX で言う 「製品ファミリー別の工程能力指数(Cpk)目標値」そのものです。自動車部品の Cpk 目標 1.33 と医療機器の Cpk 目標 2.0 が違うのと同じく、商社の CV 0.5 と通信の CV 0.2 が違うのは、業種ごとの「許容される変動の幅」を反映した結果です。

業種別代替指標の計算方法入門(BIS・LTV・ソルベンシー比率)

業種別代替指標を SEED に投入する前に、それぞれの計算方法と入門レベルの理解を整理します。実装で詰まる読者が出やすい箇所なので、簡潔に整理します。

BIS自己資本比率(Capital Adequacy Ratio)の計算方法

BIS 比率 = 自己資本 ÷ リスクアセット(リスクで重み付けした資産) × 100。一般的な「自己資本比率(純資産/総資産)」とは分母が異なる点が重要です。BIS は「貸し倒れリスクの大きい資産ほど重みを大きくする」ことで、銀行業の本質的な健全性を測ります。国際業務行は8%以上、国内業務行は4%以上がバーゼルIIIの規制最低値。

LTV(Loan to Value)の計算方法

LTV = 有利子負債 ÷ 物件取得価額 × 100。REIT の負債依存度を測る代表指標で、50% 以下が健全とされています。J-REIT 業界平均は 40-45% 程度。金利上昇局面ではより低い LTV(40% 以下)が望ましいとされ、適正水準は金利環境により変動します。

ソルベンシー・マージン比率(Solvency Margin Ratio)の入門

ソルベンシー・マージン比率 = ソルベンシー・マージン総額 ÷(リスク総額 × 1/2)× 100。「通常予測を超えた巨大災害や運用環境悪化が起きても保険金支払い能力を維持できるか」を測る指標。200% 以上が健全、100% 未満は早期是正措置の対象。日本の主要生命保険会社は 600-1000% が普通で、200% は規制最低値ライン。

信用格付けスコアの計算方法

S&P / Moody’s / R&I の格付けを数値化: AAA=6, AA=5, A=4, BBB=3, BB=2, B=1, CCC以下=0。商社・大手企業は A 格以上、5大商社は AA 相当が普通。本記事ではテーブルに事前登録する SEED データとして扱い、応用編 #20 で外部 API(IR 情報・格付け会社サイト)から自動取得する設計に拡張する想定です。

スニペット3:記念配当・特殊配当の検知(DDL/upsert/ビュー統合)

記事 #17 の罠検知で「特殊配当・記念配当の混入は応用編 #18 で扱う」と先送りした課題を、本記事で実装します。記念配当は「1年だけ配当が跳ね上がり、翌年に通常配当に戻る」パターンなので、配当履歴の中央値 vs 最新値の比較で検出可能です。

# detect_special_dividend.py — 記念配当・特殊配当の検知 + DDL + upsert + ビュー統合
# 動作環境: Python 3.11+ / pandas 2.x / numpy / duckdb 1.0+
import duckdb
import pandas as pd
import numpy as np
from pathlib import Path

DB_PATH = Path("data/stocks.duckdb")

DDL_SPECIAL_DIV = """
CREATE TABLE IF NOT EXISTS special_dividend_flags (
    ticker_normalized       VARCHAR PRIMARY KEY,
    dps_median              DOUBLE,
    dps_latest              DOUBLE,
    special_ratio           DOUBLE,
    special_dividend_flag   BOOLEAN,
    computed_at             TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""

def detect_special_dividend(history: pd.DataFrame, ratio_threshold: float = 1.5) -> pd.DataFrame:
    """配当履歴の最新値が中央値の ratio_threshold 倍を超えたら特殊配当フラグ

    ratio_threshold=1.5 の根拠:
        典型的な記念配当パターンは「通常配当 50円 → 記念配当年 75円(50%増)」
        50%増 = 1.5倍 を境界に置くことで、通常の小幅増配(10-30%増)と
        記念配当(50%超増)を機械的に区別できる
    """
    history_sorted = history.sort_values(["ticker_normalized", "period_index"])
    grouped = history_sorted.groupby("ticker_normalized")

    rows = []
    for ticker, g in grouped:
        dps = g["dps_annual"].dropna()
        if len(dps) < 5:
            rows.append({
                "ticker_normalized": ticker, "dps_median": None,
                "dps_latest": None, "special_dividend_flag": False,
                "special_ratio": None,
            })
            continue
        latest = dps.iloc[0] if len(dps) > 0 else None
        past = dps.iloc[1:] if len(dps) > 1 else dps
        median = past.median()
        ratio = latest / median if median and median > 0 else None
        flag = (ratio is not None and ratio >= ratio_threshold)
        rows.append({
            "ticker_normalized": ticker, "dps_median": float(median) if median else None,
            "dps_latest": float(latest) if latest else None,
            "special_dividend_flag": flag,
            "special_ratio": float(ratio) if ratio else None,
        })
    return pd.DataFrame(rows)

SPECIAL_INSERT_COLUMNS = "(ticker_normalized, dps_median, dps_latest, special_ratio, special_dividend_flag)"

def setup_special_dividend_schema() -> None:
    """special_dividend_flags テーブルの DDL を実行"""
    with duckdb.connect(str(DB_PATH)) as conn:
        conn.execute(DDL_SPECIAL_DIV)

def upsert_special_dividend(flags_df: pd.DataFrame) -> None:
    """detect_special_dividend の出力を idempotent に保存(トランザクション境界付き)"""
    df = flags_df.copy()
    with duckdb.connect(str(DB_PATH)) as conn:
        conn.register("sd_df", df)
        conn.execute("BEGIN")
        try:
            conn.execute("DELETE FROM special_dividend_flags WHERE ticker_normalized IN (SELECT DISTINCT ticker_normalized FROM sd_df)")
            conn.execute(f"""
                INSERT INTO special_dividend_flags {SPECIAL_INSERT_COLUMNS}
                SELECT ticker_normalized, dps_median, dps_latest, special_ratio, special_dividend_flag FROM sd_df
            """)
            conn.execute("COMMIT")
        except Exception:
            conn.execute("ROLLBACK")
            raise
        finally:
            conn.unregister("sd_df")

# v_screening_input ビューに special_dividend_flag を追加(v4 として記事 #17 v3 を拡張)
DDL_VIEW_V4 = """
CREATE OR REPLACE VIEW v_screening_input AS
SELECT
  m.ticker_normalized, m.name_jp, m.industry_profile,
  CASE WHEN m.dps_annual IS NOT NULL AND m.close_price > 0
       THEN (m.dps_annual / m.close_price) * 100 ELSE NULL END AS yield_pct,
  CASE WHEN m.dps_annual IS NOT NULL AND m.eps IS NOT NULL AND m.eps > 0
       THEN (m.dps_annual / m.eps) * 100 ELSE NULL END         AS payout,
  m.equity_ratio, m.bis_ratio, m.ltv, m.credit_rating_score,
  p.eps_trend, p.consec_inc_years, p.ocf_positive_years,
  COALESCE(t.value_trap_flag, FALSE)             AS value_trap_flag,
  COALESCE(t.value_trap_severity, 'LOW')         AS value_trap_severity,
  COALESCE(t.iqr_outlier, FALSE)                 AS iqr_outlier,
  COALESCE(t.zscore_outlier, FALSE)              AS zscore_outlier,
  COALESCE(t.spc_outlier, FALSE)                 AS spc_outlier,
  -- 記念配当フラグ(本記事で追加)
  COALESCE(s.special_dividend_flag, FALSE)       AS special_dividend_flag,
  s.special_ratio,
  imap.threshold_pass AS equity_ratio_pass_threshold,
  imap.alt_indicator  AS equity_ratio_alt_indicator,
  imap.direction      AS equity_ratio_direction
FROM v_latest_metrics m
LEFT JOIN persistence_metrics    p ON m.ticker_normalized = p.ticker_normalized
LEFT JOIN value_trap_flags       t ON m.ticker_normalized = t.ticker_normalized
LEFT JOIN special_dividend_flags s ON m.ticker_normalized = s.ticker_normalized
LEFT JOIN industry_indicator_map imap
       ON m.industry_profile = imap.industry_profile
      AND imap.standard_indicator = 'equity_ratio'
;
"""

def setup_view_v4() -> None:
    with duckdb.connect(str(DB_PATH)) as conn:
        conn.execute(DDL_VIEW_V4)

if __name__ == "__main__":
    setup_special_dividend_schema()
    setup_view_v4()
    print("special_dividend_flags + v_screening_input v4 ready")

欠損の罠:記念配当判定の限界

  • 本判定は「過去の中央値からの乖離」だけを見るため、業績好調による正常な増配と区別できない場合がある。連続増配年数(記事 #16)と組み合わせると精度が上がる:「最新が中央値の1.5倍だが、連続増配10年以上」なら正常な増配の可能性
  • 記念配当の典型は「会社設立50周年」「上場50周年」など節目の年に発生。IR ニュース・適時開示でも特定可能で、本コードはあくまで一次フィルタ
  • 配当回数(年1回 vs 年2回)の違いは dps_annual(年合計)で吸収済み

エンジニア的に言い換えると(中央値ベースの異常検知)

記念配当検知は、製造業 DX で言う 「ロバスト統計による異常検知」です。平均値ではなく中央値を基準にする理由は、「1〜2 個の極端な値(記念配当年)に基準値が引っ張られない」ため。製造業の品質管理でも、稀に発生する不良品の影響を排除するために中央値ベースの管理が使われる場面と同じ作法です。

スニペット4:業種別ダッシュボードを matplotlib で可視化する手順

業種別の代替指標と CV 閾値が揃ったので、業種ごとの分布を1枚のダッシュボードで可視化します。横並びで業種を比較することで、「どの業種が現状で割安/割高か」を目視で把握できます。動作環境は matplotlib 3.9+ が必要です(boxplot の tick_labels= パラメータが 3.9 以降の仕様)。

# industry_dashboard.py — 業種別の財務指標分布ダッシュボード
# 動作環境: Python 3.11+ / pandas 2.x / matplotlib 3.9+ / duckdb 1.0+
import duckdb
import pandas as pd
import matplotlib.pyplot as plt
from pathlib import Path

DB_PATH = Path("data/stocks.duckdb")

def fetch_industry_distributions() -> pd.DataFrame:
    """各業種の利回り・配当性向・自己資本比率の分布を取得"""
    sql = """
    SELECT industry_profile, yield_pct, payout, equity_ratio
    FROM v_screening_input
    WHERE yield_pct IS NOT NULL OR payout IS NOT NULL OR equity_ratio IS NOT NULL
    """
    with duckdb.connect(str(DB_PATH)) as conn:
        return conn.execute(sql).fetchdf()

def plot_industry_dashboard(df: pd.DataFrame, save_path: "str | None" = None) -> None:
    """業種別の3指標を箱ひげ図で並べる(matplotlib 3.9+ の tick_labels パラメータを使用)"""
    fig, axes = plt.subplots(1, 3, figsize=(15, 6))

    industries = ["manufacturing", "banking", "insurance", "reit", "trading",
                  "infrastructure", "retail", "real_estate", "pharma", "shipping"]
    df_filtered = df[df["industry_profile"].isin(industries)]

    for ax, col, title in zip(
        axes,
        ["yield_pct", "payout", "equity_ratio"],
        ["配当利回り (%)", "配当性向 (%)", "自己資本比率 (%)"],
    ):
        data = [df_filtered[df_filtered["industry_profile"] == ind][col].dropna()
                for ind in industries]
        # matplotlib 3.9+: tick_labels=(古いバージョンでは labels=)
        ax.boxplot(data, tick_labels=industries, showfliers=False)
        ax.set_title(title)
        ax.tick_params(axis="x", rotation=45)
        ax.grid(True, alpha=0.3)

    fig.suptitle("業種別 財務指標分布ダッシュボード(外れ値除外)")
    fig.tight_layout()
    if save_path:
        fig.savefig(save_path, dpi=120)
    else:
        plt.show()
    plt.close(fig)

# 使用例
# df = fetch_industry_distributions()
# plot_industry_dashboard(df, save_path="data/figures/industry_dashboard.png")

エンジニア的に言い換えると(製造業の業種別ダッシュボード)

本箱ひげ図ダッシュボードは、製造業 DX で言う 「製品ファミリー別の品質指標一覧パネル」と同型の可視化です。複数製品ラインの不良率分布を1画面で並べて「どのラインが規格内で安定しているか」を目視で把握する標準パネル。投資の業種別箱ひげ図も「どの業種が利回り高めで安定しているか」を一目で比較できる装置として機能します。

業種別補正の統合サマリ:応用編 #15 判定器の Phase 3 集約形

本記事までで、応用編 #15 の判定器が参照するテーブルが次のように揃いました。応用編 #19/#20 で更に業種分散・パイプライン全体像へと拡張していく前提で、Phase 3(分析)の集約形となります。

テーブル記事役割
industry_indicator_map(10業種)#14 → #18業種別代替指標(BIS、LTV、ソルベンシー、格付け 等)
industry_cv_threshold#18業種別 CV 閾値(EPS 変動係数)
persistence_metrics#16連続非減少年数・EPS CV・営業CFプラス年数
value_trap_flags#17IQR/Z/SPC + 動的シグナルのバリュートラップ候補
special_dividend_flags#18(本記事)記念配当・特殊配当の判定

PASS × 業種別補正フラグの読み方(投資家向け解釈ガイダンス)

業種別補正が入ると、判定結果の読み方も業種ごとに意味が変わります。機械判定は暫定ラベルであり、最終的な投資判断には業種特性に応じた定性調査が必要です。

業種PASS の意味業種別注意点
製造業自己資本比率40%以上、業種内の中央値以上営業CFと連続増配を併用確認
銀行BIS 8% 以上(規制最低値クリア)不良債権率・ROA を別途確認、PASS でも「健全」とは限らない
保険ソルベンシー比率 200% 以上(規制最低値クリア)運用環境・自然災害リスクを別途確認
REITLTV 50% 以下(業界実勢健全水準)金利上昇局面では 40% 以下が望ましい
商社S&P/Moody’s の A 格以上5大商社は AA 相当が標準、A 格は中位水準
その他業種業界実勢ベースの健全水準クリア業種特性で意味が異なるため、SEED の note を参照

プロセスFMEA(層別)で業種別補正の故障モードを整理

故障モード影響度事前対策
マスタ新業種追加時のテーブル更新漏れ四半期レビュー、source/note カラムで履歴管理
マスタ規制改定(バーゼル基準等)の閾値陳腐化規制当局の発表をウォッチ、四半期更新サイクル
取得業種別代替指標の XBRL タグが取れないEDINET から指標を抽出する正規化スタブ(記事 #14 から)の業種別拡張
統合業種コード分類の誤り東証17業種コードと industry_profile のマッピング表で確認
判定記念配当を増配と誤認連続増配年数との併用、IR ニュース確認の運用作法
判定業種内銘柄数の不足で CV 閾値が安定しないmin_group_size=10 でスキップ(業種カバレッジ確保)
判定東証33業種のうち未カバー業種への適用未カバー業種は manufacturing をデフォルトに、警告ログ出力
通知業種別判定の根拠説明欠落低〜中LINE 通知に「銀行は BIS 比率で判定」のような根拠表示

設計判断の記録:業種別補正のトレードオフ(4判断の俯瞰)

判断テーマ採用主なトレードオフ
判断1業種粒度10業種(東証33業種主要部分)細分化と銘柄数のバランス
判断2主指標数1業種1指標(自己資本系)シンプル運用 vs 複合判定
判断3更新サイクル四半期レビュー運用負荷 vs 規制改定への追従
判断4テーブル PK 設計industry_indicator_map は複合PK、industry_cv_threshold は単列PK1業種多指標 vs 1業種1閾値の構造差

判断0:なぜこの4つの設計判断を最初に決めるか

判断1(業種粒度)はテーブルの行数に直結、判断2(指標数)は実装複雑度に影響、判断3(更新サイクル)は運用負荷を左右、判断4(PK 設計)はスキーマ整合性を担保します。応用編 #19 の業種分散、#20 のパイプライン全体像でも、この4つの判断を起点に拡張する設計です。

判断1:業種粒度を10業種にするか33業種に細分化するか

  • 採用理由: 本記事は10業種(プロファイル)の最小実装。東証17業種コードよりやや粗く、「特性が大きく違うグループ」を分離。東証時価総額の概ね7割をカバー
  • 採用したことで失うもの:
    • トリガー条件: 同一業種内でも特性差が大きい場合(例: 商社のうち資源依存と非資源依存)
    • 閾値: 33業種に細分化すると業種内銘柄数が減り、CV や SPC の信頼性が下がる
    • 残るメリット: 10業種ならテーブル管理が現実的、各業種で十分な銘柄数を確保
    • 対処: 「サブプロファイル」(例: trading_resource / trading_non_resource)の追加で対応可能。応用編 #19/#20 で食品・化学・電気機器・自動車・素材・陸運・空運・証券・SI 等を追加予定

判断2:業種別代替指標を主指標1つに絞るか複数併用するか

  • 採用理由: 本記事は「業種ごとに自己資本系の主指標1つ」を採用(銀行=BIS、REIT=LTV など)。配当性向は業種横断で別途扱う
  • 採用したことで失うもの:
    • トリガー条件: 業種特性が複合的(例: 銀行は BIS だけでなく不良債権率も重要)
    • 閾値: 単一指標では複数の異常を捉えにくい
    • 残るメリット: テーブル設計と判定ロジックがシンプル、応用編シリーズで読者が再現しやすい
    • 対処: standard_indicator を複数行で定義(例: banking → equity_ratio + npl_ratio)して judge_v2 に複数指標適用版を実装

判断3:マッピングテーブルの更新サイクルをどうするか

  • 採用理由: 四半期に1回のレビューを推奨。規制改定(バーゼル等)と業種平均の自然変動を吸収
  • 採用したことで失うもの:
    • トリガー条件: 規制改定が四半期内に複数回ある場合
    • 閾値: 急激な業界変化(リーマン等)には四半期レビューでは遅い
    • 残るメリット: 運用負荷が軽い、note/source カラムで変更履歴を追える
    • 対処: 規制改定情報を Slack/RSS で監視するアラート運用を併用

判断4:industry_indicator_map と industry_cv_threshold の PK 設計が異なる理由

  • 採用理由: industry_indicator_map は複合PK(industry_profile + standard_indicator)、industry_cv_threshold は単列PK(industry_profile)。前者は「1業種に対して複数指標」を持つ設計、後者は「1業種に対して1閾値」
  • 採用したことで失うもの:
    • トリガー条件: CV閾値も業種別に複数指標化したい場合(EPS CV と売上 CV を別管理)
    • 閾値: スキーマ統一性が下がる
    • 残るメリット: 単純な単列PKで CV 閾値を管理でき、1業種1行の運用が直感的
    • 対処: 将来 CV閾値の指標を複数化する場合は industry_cv_threshold も複合PKに移行

本業の話:「同じ製品でも業種で見るコスト指標が違う」と気づいた経験

筆者が製造業の開発部門で、新規事業向けの製品ポートフォリオ評価を担当していたときのこと。それまで自社の主力製品(量産家電)でしか製品評価をしてこなかった筆者が、受託サービス事業(B2B 向けカスタマイズ機器)と医療機器(少量多品種)の両方を含む新規ポートフォリオの収益性評価を任されました。

初稿では、すべての製品に同じ指標(材料費比率・労務費比率)を適用して評価しましたが、ベテランから次のように指摘されました:

  • 量産家電と医療機器に同じ指標を当てるな。家電は材料費比率60%が標準、医療機器は20%が普通。業種ごとに見るコスト指標を切り替えるのが評価の基本動作」
  • 受託サービスは”原価率”より”稼働率”。固定費を回収するために設備をどれだけ使えているかが評価軸」
  • 製品ファミリー別の評価指標テーブルを作れ。コードに if 分岐するな、テーブル駆動で運用しろ」

具体的な業務インパクト:

  • 初稿の一律評価では、医療機器事業が「材料費比率20%で異常に低利益」と判定され、撤退候補に挙がっていた。実際には医療機器は労務費比率と研究開発費比率が高い構造で、製造業の常識とは違う収益モデル(年間R&D投資 売上の25%、量産家電の5倍)
  • 業種別評価テーブルに書き直した結果、医療機器事業が「業種内では平均以上の収益性」と再評価され、撤退判断が回避。3年後にはこの事業の売上規模が10倍化し、会社の主力事業に成長
  • 業種を理解しない一律評価は、優良事業を撤退候補に入れる重大な経営リスク」という教訓が部内で共有され、以降のポートフォリオ評価では業種別テーブルが標準化
  • 当時のテーブルは Excel ベース、項目13列 × 業種8行で100セル強だったが、これがあるかないかで評価結果が大きく変わる経験

この経験が本記事の industry_indicator_map 拡張・industry_cv_threshold 設計に直接反映されています:

  • 10業種を代替指標とともにテーブル化: 量産家電 vs 医療機器の発想で、銀行 vs 製造業 vs REIT を区別
  • 業種別 CV 閾値テーブル: 製品ファミリー別 Cpk 目標値の発想で、業種ごとの許容変動幅を別管理
  • テーブル駆動で if 分岐を書かない: コードを書き換えずに業種を追加できる設計

逆方向の転移:投資の業種別補正が本業の事業評価を再強化した

本記事の業種別代替指標を組み始めて以降、本業の事業ポートフォリオ評価にも DuckDB ベースのテーブル駆動評価が逆輸入されました。それまで Excel ベースだった業種別評価テーブルが、DuckDB の industry_indicator_map と同型のスキーマに置き換わり、新規事業追加時の評価作業が約半日 → 30分に短縮。投資側で開発した業種別補正の仕組みが、本業の事業評価インフラを更新する事例になりました。

まとめ:高配当株の業種別財務健全性は「製造業のコスト構造分析」で読み解ける

  • 業種ごとに見るべき財務指標は異なる: 銀行=BIS、REIT=LTV、商社=格付け、保険=ソルベンシー、製造業=自己資本比率+営業CF。industry_indicator_map テーブルに10業種分の代替指標を体系化することで、コードを書き換えずに業種を追加できる設計が完成。応用編 SPC 4軸の最後のピース
  • 業種別 CV 閾値と記念配当検知を統合することで、応用編 #16/#17 で先送りした課題が解決。各銘柄に対して「業種特性に応じた持続性評価」と「特殊配当の混入除外」が機械判定される
  • 製造業のコスト構造分析と同型の発想: 量産家電 vs 医療機器 vs 受託サービスで見るコスト指標が違うのと同じく、銀行 vs REIT vs 製造業で見る高配当株の財務指標が違う。テーブル駆動 + 四半期レビューの運用作法も本業から逆輸入できる

今日からできる3つのアクション

  1. 本記事のスニペット1〜2を順に実行し、10業種の業種別代替指標と CV 閾値テーブルdata/stocks.duckdb に投入。SELECT * FROM industry_indicator_map ORDER BY industry_profile で内容を確認してみる
  2. スニペット3で記念配当の検知を実行し、「最新配当が過去中央値の1.5倍以上」の銘柄を一覧化。連続増配年数(記事 #16)と組み合わせて「正常な増配 vs 記念配当」を区別する手順を体験。v_screening_input ビュー v4 が special_dividend_flag を含むことを SQL で確認
  3. スニペット4で業種別ダッシュボードを生成。業種ごとの利回り・配当性向・自己資本比率の分布を箱ひげ図で比較し、自分の興味のある業種が他業種に対してどう位置しているかを目視確認

次回予告:業種分散をFMEAで設計する(ポートフォリオ全体のリスク分析)

次回(記事#19)では、応用編 #11 から繰り返し言及してきた業種分散を、製造業の FMEA(故障モード影響解析)の発想で設計します。これまでは「個別銘柄の評価」だったのに対し、#19 は「ポートフォリオ全体の構成評価」に視点を移します。

  • 業種集中リスクを FMEA で定量化(同一セクター3社以下の根拠)
  • 業種間の相関分析(金融危機時に同時下落するセクター)
  • ポートフォリオ全体の業種分散スコアを v_screening_input に追加するビュー設計
  • 本記事で未カバーの業種(食品・化学・電気機器・自動車・素材 等)の追加

「製品開発DXエンジニアの投資術」シリーズ全体像

本記事は 応用編(記事#11〜#20)の第8回 です。応用編の DX フェーズマップでの位置づけ:

前回 #17 罠銘柄検知本記事 #18 業種別財務健全性 | 次回 #19(公開予定)

関連記事(基礎編から): #03 複利のPython可視化#08 リスクとリターン#09 NISA・iDeCoの設計

免責事項(再掲)

本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。コード・業種別代替指標・閾値(BIS、LTV、ソルベンシー、CV閾値等)は教育目的であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。J-Quants API・EDINET API の利用規約は変更される可能性があるため、実装時は各APIの公式ドキュメント・利用規約を必ず確認してください。本記事中に J-Quants API から取得した実データは掲載していません(利用規約に基づく方針)。業種別閾値は2026年5月時点の参考値で、規制改定により変動します。

よかったらシェアしてね!
  • URLをコピーしました!
  • URLをコピーしました!

この記事を書いた人

コメント

コメントする

CAPTCHA


目次