高配当株ポートフォリオの業種分散|FMEA でセクター集中リスクを設計

Chelsea-Labs #19 サムネイル

免責事項

本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。記事中のコード・FMEA設計・閾値(HHI・相関・RPN等)は教育目的であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。本記事中に J-Quants API から取得した実データは掲載していません(利用規約に基づく方針、詳細は 記事#13)。可視化・サンプル値はダミーデータまたは公開済み統計値に基づきます。

前回の記事#18では、業種別代替指標テーブル(10業種)と CV 閾値、記念配当検知を統合し、応用編 SPC 4軸(規格 USL/LSL + 管理 UCL/LCL + 異常値 IQR/Z + 業種別補正)を集約しました。本記事はその視点を1段上げて、これまで個別銘柄の評価が中心だったところから、「ポートフォリオ全体の構成評価」に視点を移します。

応用編 #11 で「業種分散(同一セクター3社以下)」と提示してから、応用編 #12〜#18 では個別銘柄の判定と業種別補正を扱ってきました。本記事は応用編シリーズで予告し続けてきたポートフォリオ全体の業種分散を製造業の FMEA(Failure Mode and Effects Analysis、故障モード影響解析)で設計する回です。

多くの読者がぶつかる壁は次の3つです:

  • 業種集中度の定量化: 「同一セクター3社以下」のような単純ルールでは「5業種に均等 vs 10業種に偏在」のような分散の質的差を捉えられない。市場集中度指数(HHI)で1次元の数値に集約する
  • 業種間相関の見落とし: 異なる業種に分散しているつもりでも「金融危機時に同時下落するセクター」がある。過去株価データの相関行列で隠れた連動性を可視化する
  • FMEA 視点でのリスク優先順位: 業種ごとの影響度・発生頻度・検出難度を RPN(Risk Priority Number)で集約することで、ポートフォリオ全体のリスクをスコア化する

筆者は製造業の開発現場で、サプライチェーン FMEAを多く設計してきました。「同一サプライヤーに3部品以上依存していたら冗長化を検討する」というサプライチェーン設計の発想は、ポートフォリオの業種分散と完全同型です。本業の FMEA で部品供給リスクを定量化する作法が、投資のセクター集中リスクの評価にそのまま転用できます。

応用編シリーズの SPC 4軸 + FMEA の集約点(#11→#19 の連続性)

応用編 #11 で「FMEA でリスク分散を設計」と最初に提示した枠組みが、本記事で完全な形に集約されます:

  • SPC 4軸(個別銘柄の評価): USL/LSL + UCL/LCL + IQR/Z + 業種別補正(記事 #11〜#18)
  • FMEA(ポートフォリオ全体の評価): 故障モード × 影響度 × 発生頻度 × 検出難度 → RPN(本記事)

SPC が「個別品質管理」、FMEA が「全体リスク管理」と役割分担。両者を組み合わせることで、応用編シリーズの「両学長スクリーニング基準による自動化」が完全な品質保証システムとして稼働する状態に到達します。

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

業種分散の定量化が大変、という読者には、「業種分散の最低ルール3つだけ守る」のが現実解です: (1) 同一業種は3社以下、(2) 単一業種への投資比率を30%以下、(3) 金融・REIT・通信・製造の異なる4セクター以上に分散。本記事の自動化は「数十〜百銘柄のポートフォリオに対して機械的に分散度を算出する」用途で、5-10銘柄程度なら最低ルール3つで十分です。SBI証券・楽天証券のポートフォリオ画面でも業種比率は無料で表示されます。

本記事の前提と難易度

  • 記事 #11〜#18 のパイプライン(取得 → 統合 → 自動化 → 時系列 → 罠検知 → 業種別補正)が完了している前提
  • pandas(corrgroupby)と numpy の統計関数の基本
  • FMEA の基礎知識があると進めやすい。未経験の方は本記事のセクション2 の概念図でキャッチアップ可能
  • 動作環境: Python 3.11+ / pandas 2.x / duckdb 1.0+ / matplotlib 3.9+
  • J-Quants プラン: Light で十分(業種分類・株価過去履歴は Light でカバー、相関分析にはやや遅延の影響あり)
  • 本記事のスコアが v_screening_input に統合されると、ポートフォリオ単位での業種分散判定の基盤が整います(具体的な JOIN スキーマは応用編 #20 で整理)

本記事では 4 個のサンプルコードを通じて、ポートフォリオ業種分散の3指標(HHI・相関・RPN)を算出します。応用編 #20 のまとめで、本記事までのすべての要素を統合したパイプライン全体像を整理します。

結論:高配当株ポートフォリオの業種分散は「HHI(集中度指数)× 業種間相関 × FMEA RPN」の3指標で定量化する。SPC 4軸が個別銘柄の品質管理、FMEA がポートフォリオ全体のリスク管理という役割分担で、両者を組み合わせることで応用編シリーズの自動化システムが完全な品質保証になる。本記事の業種分散スコアが v_screening_input に統合されると、応用編 #15 の判定器が「個別 PASS かつ ポートフォリオ業種分散 OK」の複合判定の基盤が整う。

目次

業種分散を「集中度 × 相関 × 故障モード」の3観点で整理する

業種分散と一口に言っても、構造的には3つの観点に分かれます。それぞれ検出方法が異なるため、本記事では3つの異なる手法を組み合わせます。

観点典型的な失敗検出手法本記事のスニペット
集中度30社のうち15社が同じ業種HHI(Herfindahl-Hirschman Index、市場集中度指数)
業種間相関異業種に分散したつもりが金融危機で同時下落過去株価の相関行列(pandas corr
故障モード1業種の規制改定で全保有銘柄が影響FMEA RPN(影響度 × 発生頻度 × 検出難度)

エンジニア的に言い換えると(製造業のサプライチェーンFMEAとの対応)

本3観点は、製造業 DX で言う 「サプライチェーンFMEA の3軸」そのものです:

  • 集中度 = 単一サプライヤー依存度: 同一サプライヤーから何部品調達しているか
  • 相関 = サプライヤー間の連動性: 自然災害・地政学リスクで複数サプライヤーが同時停止
  • 故障モード = 部品調達リスクの優先順位: RPN で発生頻度・影響度・検出難度を集約

製造業では「単一サプライヤーで3部品以上ならセカンドソース確保、地震多発地域に集中していたら別地域への分散検討」という運用が標準。投資のセクター集中も全く同じ思考プロセスで設計可能です。

スニペット1:HHI(市場集中度指数)でポートフォリオ業種集中度を計算する

HHI(Herfindahl-Hirschman Index) は、市場の集中度を測る指標で、米国司法省・FTC が独占禁止法の判定に使う標準指標です。各構成要素の市場シェアの2乗和で計算し、0(完全分散)〜10000(完全独占)の値を取ります。投資ポートフォリオに適用すると、業種別保有比率の2乗和でポートフォリオの業種集中度を1つの数値に集約できます。

HHI 閾値の投資視点での再設定(独禁法基準そのままだと厳しすぎる)

独禁法 HHI は数十〜数百社の市場で測られる指標で、5-15業種規模の投資ポートフォリオに機械的に流用すると「実は分散しているのに高集中」と誤判定するリスクがあります。例えば5業種均等で HHI=2000、10業種均等で HHI=1000 なので、独禁法の「高分散 1500未満」は投資ポートフォリオでは構造的に達成困難です。本記事では2系統の閾値を併記します:

レベル独禁法基準投資ポートフォリオ視点
高分散HHI < 1500HHI < 2000(10業種以上に分散相当)
中集中1500 ≤ HHI < 25002000 ≤ HHI < 3000(5-10業種程度)
高集中HHI ≥ 2500HHI ≥ 3000(要警戒、業種シフトのトリガー)

本コードは 投資ポートフォリオ視点の閾値を採用しています。独禁法基準で運用したい場合は thresholds=(1500, 2500) を引数で渡せる設計です。

# compute_hhi.py — ポートフォリオの業種集中度を HHI で計算(v2: 投資視点閾値)
# 動作環境: Python 3.11+ / pandas 2.x / numpy
import pandas as pd
import numpy as np
from typing import TypedDict

class HhiResult(TypedDict):
    hhi: float                              # 0-10000
    industry_shares: pd.Series              # 業種別シェア (%)
    verdict: str                            # DIVERSIFIED / MODERATELY_CONCENTRATED / HIGHLY_CONCENTRATED

def compute_industry_hhi(portfolio: pd.DataFrame,
                          thresholds: tuple[float, float] = (2000.0, 3000.0)) -> HhiResult:
    """ポートフォリオの業種集中度を HHI で算出(投資視点の閾値をデフォルトに)

    portfolio: ticker_normalized / industry_profile / market_value (時価評価額)
    thresholds: (high_diversified_upper, moderately_concentrated_upper)
                投資視点デフォルト: (2000, 3000)
                独禁法基準で運用: (1500, 2500)
    """
    if portfolio["market_value"].sum() == 0:
        return {"hhi": 0.0, "industry_shares": pd.Series(dtype=float), "verdict": "N/A"}
    industry_value = portfolio.groupby("industry_profile")["market_value"].sum()
    total = industry_value.sum()
    shares_pct = (industry_value / total) * 100
    hhi = float((shares_pct ** 2).sum())
    if hhi < thresholds[0]:
        verdict = "DIVERSIFIED"
    elif hhi < thresholds[1]:
        verdict = "MODERATELY_CONCENTRATED"
    else:
        verdict = "HIGHLY_CONCENTRATED"
    return {"hhi": hhi, "industry_shares": shares_pct.sort_values(ascending=False),
            "verdict": verdict}

# 使用例(架空のポートフォリオ)
# portfolio = pd.DataFrame([
#     {"ticker_normalized": "XXXX", "industry_profile": "manufacturing", "market_value": 1_000_000},
#     {"ticker_normalized": "YYYY", "industry_profile": "manufacturing", "market_value":   500_000},
#     {"ticker_normalized": "ZZZZ", "industry_profile": "banking",       "market_value":   500_000},
#     {"ticker_normalized": "WWWW", "industry_profile": "reit",          "market_value":   500_000},
#     {"ticker_normalized": "VVVV", "industry_profile": "trading",       "market_value":   500_000},
# ])
# result = compute_industry_hhi(portfolio)
# print(f"HHI: {result['hhi']:.0f} ({result['verdict']})")
# print(result["industry_shares"])
# # HHI: 3333 (HIGHLY_CONCENTRATED)
# # manufacturing 50.0    (シェア50%)→ 50² = 2500
# # banking       16.7    (シェア16.67%)→ 16.67² ≈ 278
# # reit          16.7
# # trading       16.7    合計: 2500 + 278×3 ≈ 3333

エンジニア的に言い換えると(サプライヤー集中度の管理)

HHI は、製造業 DX で言う 「サプライヤー集中度の Cpk」そのものです。「全部品の40%を同一サプライヤーから調達 = HHI 1600+」のように、単一サプライヤー依存度を1つの数値に集約することで、調達戦略の見直しトリガーにできます。投資の業種集中も同じ思考で、HHI 3000 を超えたら業種シフトのトリガーとして運用します。

スニペット2:業種間相関分析で「隠れた連動性」を見抜く手順

異なる業種に分散しているつもりでも、金融危機やコロナショック時に同時下落するセクターがあります。これは過去株価データの相関係数で可視化できます。pandas の corr() で業種別平均株価変動率の相関行列を計算し、heatmap で可視化します。

相関係数の解釈ガイダンス(典型レンジ)

レンジ意味運用判断
0.9以上ほぼ同一指標同一セクターと見なし、HHI で重複カウントしない
0.7〜0.9強い連動(同サブセクター・同テーマ株)「実質同一セクター扱い」。例: 銀行×保険、商社×海運
0.6〜0.85サブセクター内・系列内の関連HHI 計算で集約も検討
0.3〜0.5平常時の典型レンジ(同一市場で正の弱い相関)独立業種として扱える
-0.3〜0.3ほぼ無相関分散効果が大きい組合せ
-0.5以下負の相関(ディフェンシブ vs 景気敏感)ポートフォリオ全体の変動を緩和

注意:危機時には平常時の 0.3-0.5 が 0.7-0.9 に跳ね上がる「テールリスクの相関」が発生。過去24ヶ月平均と直近6ヶ月の両方を見るのが定石です。

# compute_industry_correlation.py — 業種間相関分析と heatmap 可視化(v2)
# 動作環境: Python 3.11+ / pandas 2.x / numpy / matplotlib 3.9+ / duckdb 1.0+
import duckdb
import pandas as pd
import numpy as np
import matplotlib.pyplot as plt
from pathlib import Path

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

def fetch_industry_returns(months: int = 24) -> pd.DataFrame:
    """業種別の月次平均リターンを取得(過去 months ヶ月)

    返却カラム: month / industry_profile / monthly_return_pct

    DuckDB の ARG_MIN/ARG_MAX で月初・月末の Close を取得(FIRST/LAST より公式推奨)
    """
    sql = """
    WITH monthly AS (
      SELECT
        c.industry_profile,
        DATE_TRUNC('month', p.Date) AS month,
        p.Code AS ticker,
        ARG_MIN(p.Close, p.Date) AS month_start_close,
        ARG_MAX(p.Close, p.Date) AS month_end_close
      FROM prices_daily p
      JOIN companies c ON c.ticker_normalized =
                    CASE WHEN LENGTH(p.Code) = 5 THEN SUBSTRING(p.Code,1,4) ELSE p.Code END
      WHERE p.Date >= CURRENT_DATE - CAST(? AS INTEGER) * INTERVAL '1' DAY
      GROUP BY c.industry_profile, DATE_TRUNC('month', p.Date), p.Code
    )
    SELECT
      industry_profile, month,
      AVG((month_end_close - month_start_close) / NULLIF(month_start_close, 0) * 100) AS monthly_return_pct
    FROM monthly
    GROUP BY industry_profile, month
    ORDER BY industry_profile, month
    """
    days = months * 31
    with duckdb.connect(str(DB_PATH)) as conn:
        return conn.execute(sql, [days]).fetchdf()

def compute_correlation_matrix(returns_df: pd.DataFrame) -> pd.DataFrame:
    """業種別月次リターンから相関行列を計算"""
    pivot = returns_df.pivot(index="month", columns="industry_profile", values="monthly_return_pct")
    return pivot.corr(method="pearson")

def plot_correlation_heatmap(corr: pd.DataFrame, save_path: "str | None" = None) -> None:
    """相関行列を heatmap で可視化(matplotlib 3.9+ で完全動作)"""
    fig, ax = plt.subplots(figsize=(10, 8))
    im = ax.imshow(corr.values, cmap="RdBu_r", vmin=-1, vmax=1, aspect="auto")
    ax.set_xticks(range(len(corr.columns)))
    ax.set_xticklabels(corr.columns, rotation=45, ha="right")
    ax.set_yticks(range(len(corr.index)))
    ax.set_yticklabels(corr.index)
    for i in range(len(corr.index)):
        for j in range(len(corr.columns)):
            text_color = "white" if abs(corr.iloc[i, j]) > 0.6 else "black"
            ax.text(j, i, f"{corr.iloc[i, j]:.2f}",
                    ha="center", va="center", color=text_color, fontsize=8)
    fig.colorbar(im, ax=ax, label="相関係数")
    ax.set_title("業種間月次リターン相関行列(24ヶ月)")
    fig.tight_layout()
    if save_path:
        fig.savefig(save_path, dpi=120)
    else:
        plt.show()
    plt.close(fig)

# 使用例
# returns = fetch_industry_returns(months=24)
# corr = compute_correlation_matrix(returns)
# # 「相関 0.7 以上のペア」が「実質同一セクター扱い」
# high_corr_pairs = []
# for i in corr.index:
#     for j in corr.columns:
#         if i < j and corr.loc[i, j] >= 0.7:
#             high_corr_pairs.append((i, j, corr.loc[i, j]))
# print(f"相関0.7以上のペア: {len(high_corr_pairs)}")
# plot_correlation_heatmap(corr, save_path="data/figures/industry_corr.png")

エンジニア的に言い換えると(製造業の連動性試験)

業種間相関分析は、製造業 DX で言う 「サプライヤー間の連動性試験(Common Cause Failure 分析)」そのものです。同じ地域の複数サプライヤーが地震で同時停止する可能性、同じ素材原料に依存する複数部品が原料高騰で同時値上がりする可能性—これらを過去データから定量評価するのが連動性試験。投資の業種間相関分析は、その思考をそのまま株価データに適用しています。

スニペット3:FMEA RPN(Risk Priority Number)でリスク優先順位を算出する計算方法

製造業の FMEA で標準的に使われる RPN(Risk Priority Number、リスク優先度指数)は、影響度(Severity) × 発生頻度(Occurrence) × 検出難度(Detection)の3軸を 1〜10 のスコアで評価し、その積(最大1000)でリスク優先順位を決めます。本記事ではこれを業種別に適用します。

S/O/D の定量基準(業績影響 % との対応)

S/O/D のスコア値は、AIAG-VDA FMEA Handbook 等の標準ガイドラインで推奨される定量基準を本記事の文脈に合わせて整理しました(業界実例は2026年5月時点の参考値)。

スコアSeverity(業績影響)Occurrence(発生頻度)Detection(検出難度)業界実例
1-2業績影響 0-5%10年に1回未満簡単に検出可規制業種(電力)
3-45-15%5-10年に1回標準指標で検出インフラ・通信
5-615-30%3-5年に1回複合指標が必要製造業・小売
7-830-50%1-3年に1回事前検出困難銀行・保険・REIT
9-1050%超1年以内に1回事後でしか検出不可海運・資源
# fmea_rpn.py — 業種別 FMEA RPN(Risk Priority Number)算出(v2)
# 動作環境: Python 3.11+ / pandas 2.x / numpy
import pandas as pd
import numpy as np
import logging

logger = logging.getLogger(__name__)

# 業種別 FMEA 評価表(RPN = Severity × Occurrence × Detection、各 1-10)
# S/O/D のスコア値の根拠は本文の定量基準テーブル参照
INDUSTRY_FMEA = pd.DataFrame([
    # S=5(15-30%影響) × O=4(5-10年に1回) × D=5(複合指標必要) = 100
    {"industry_profile": "manufacturing",  "severity": 5, "occurrence": 4, "detection": 5,
     "note": "景気循環影響(業績影響15-30%)、経営状態は財務指標で検出可"},
    # S=8(30-50%) × O=5(3年) × D=7(事前検出困難) = 280
    {"industry_profile": "banking",        "severity": 8, "occurrence": 5, "detection": 7,
     "note": "金融危機時の影響大(業績影響30-50%)、不良債権の事前検出が困難"},
    {"industry_profile": "insurance",      "severity": 7, "occurrence": 4, "detection": 7,
     "note": "自然災害・運用環境影響大(30-40%)、ソルベンシー悪化検出は難しい"},
    {"industry_profile": "reit",           "severity": 7, "occurrence": 5, "detection": 4,
     "note": "金利上昇影響大(30-40%)、LTV で事前検出可"},
    # S=8(30-50%) × O=6(1-3年) × D=6(複合指標が必要) = 288
    {"industry_profile": "trading",        "severity": 8, "occurrence": 6, "detection": 6,
     "note": "資源価格・地政学影響大(30-50%)、セグメント別ROEで事前検出可"},
    {"industry_profile": "infrastructure", "severity": 4, "occurrence": 3, "detection": 4,
     "note": "規制業種で安定(5-15%)、料金規制改定は事前察知可能"},
    {"industry_profile": "retail",         "severity": 5, "occurrence": 5, "detection": 4,
     "note": "消費トレンド変化影響(15-30%)、売上指標で検出可"},
    {"industry_profile": "real_estate",    "severity": 7, "occurrence": 4, "detection": 5,
     "note": "金利・物件価値変動影響(30-40%)、LTV で部分検出可"},
    {"industry_profile": "pharma",         "severity": 7, "occurrence": 5, "detection": 8,
     "note": "特許切れ・薬価改定影響大(30-40%)、パイプライン評価が困難"},
    # S=9(50%超) × O=7(年に1回近く) × D=5(複合指標必要) = 315
    {"industry_profile": "shipping",       "severity": 9, "occurrence": 7, "detection": 5,
     "note": "海運市況の急変動(業績影響50%超)、需給バランスは事前指標あり"},
])

class RpnTableEntry(pd.Series):
    pass

def compute_industry_rpn() -> pd.DataFrame:
    """業種別 RPN を計算(include_lowest=True で RPN=0 が NaN にならない)"""
    df = INDUSTRY_FMEA.copy()
    df["rpn"] = df["severity"] * df["occurrence"] * df["detection"]
    df["risk_level"] = pd.cut(
        df["rpn"],
        bins=[0, 100, 200, 1000],
        labels=["LOW", "MEDIUM", "HIGH"],
        include_lowest=True,  # 0 を含めて分類
    )
    return df.sort_values("rpn", ascending=False)

def compute_portfolio_weighted_rpn(portfolio: pd.DataFrame) -> dict:
    """ポートフォリオの加重平均 RPN を計算(未カバー業種は警告ログ出力)"""
    industry_rpn = compute_industry_rpn().set_index("industry_profile")
    industry_value = portfolio.groupby("industry_profile")["market_value"].sum()
    total = industry_value.sum()
    if total == 0:
        return {"weighted_rpn": 0.0, "industry_breakdown": pd.DataFrame()}

    # 未カバー業種を警告(サイレント無視を防ぐ)
    uncovered = set(industry_value.index) - set(industry_rpn.index)
    if uncovered:
        logger.warning(f"未カバー業種を無視しました: {uncovered}(応用編 #20 で追加予定)")

    shares = industry_value / total
    weighted = (shares * industry_rpn["rpn"]).dropna().sum()
    breakdown = pd.DataFrame({
        "share_pct": shares * 100,
        "rpn": industry_rpn["rpn"],
        "contribution": shares * industry_rpn["rpn"],
    }).dropna().sort_values("contribution", ascending=False)
    return {"weighted_rpn": float(weighted), "industry_breakdown": breakdown,
            "uncovered_industries": list(uncovered)}

# 使用例
# rpn_table = compute_industry_rpn()
# print(rpn_table[["industry_profile", "severity", "occurrence", "detection", "rpn", "risk_level"]])
#
# portfolio = pd.DataFrame([...])  # スニペット1 の例と同じ
# result = compute_portfolio_weighted_rpn(portfolio)
# print(f"Portfolio Weighted RPN: {result['weighted_rpn']:.1f}")
# print(result["industry_breakdown"])

エンジニア的に言い換えると(製造業のFMEAテーブル)

本 INDUSTRY_FMEA は、製造業 DX で言う 「故障モード一覧表(FMEA Worksheet)」そのものです。製造業では「不良モード」「発生工程」「影響度」「検出難度」を1表に整理し、RPN 200 以上は対策必須という運用が標準。投資のセクターリスクも同じ作法で、RPN ベースで「優先的にウォッチすべき業種」を機械的に序列化します。

スニペット4:業種分散スコアをビューに統合 + 5業種拡張で計15業種に

仕上げに、ポートフォリオ業種分散スコア用のテーブルを作成し、応用編 #18 で先送りした食品・化学・電気機器・自動車・素材の5業種を industry_indicator_map に追加して計15業種にカバレッジを拡張します。

# portfolio_scores.py — 業種分散スコアテーブル + 5業種拡張
# 動作環境: Python 3.11+ / duckdb 1.0+
import duckdb
import pandas as pd
from pathlib import Path

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

DDL_PORTFOLIO_SCORES = """
CREATE TABLE IF NOT EXISTS portfolio_scores (
    score_id            INTEGER PRIMARY KEY,
    portfolio_name      VARCHAR,
    hhi                 DOUBLE,
    weighted_rpn        DOUBLE,
    high_corr_pair_count INTEGER,
    score_summary       VARCHAR,
    computed_at         TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""

# 5業種拡張(食品・化学・電気機器・自動車・素材)
ADDITIONAL_INDUSTRIES = pd.DataFrame([
    {"industry_profile": "food", "standard_indicator": "equity_ratio",
     "alt_indicator": None, "direction": "higher_better",
     "threshold_pass": 50.0, "threshold_caution": 35.0,
     "note": "食品業: 安定収益・在庫回転率高、自己資本比率50%以上が業界実勢",
     "source": "中小企業庁 業種別経営指標 + 産業別経営動向調査(食品業、2026年5月)"},
    {"industry_profile": "chemicals", "standard_indicator": "equity_ratio",
     "alt_indicator": None, "direction": "higher_better",
     "threshold_pass": 45.0, "threshold_caution": 30.0,
     "note": "化学: 設備投資が大きく自己資本比率45%以上で健全",
     "source": "中小企業庁 業種別経営指標(化学業、2026年5月)"},
    {"industry_profile": "electronics", "standard_indicator": "equity_ratio",
     "alt_indicator": None, "direction": "higher_better",
     "threshold_pass": 50.0, "threshold_caution": 35.0,
     "note": "電気機器: 技術陳腐化リスク高、自己資本比率50%以上推奨",
     "source": "中小企業庁 業種別経営指標(電気機器業、2026年5月)"},
    {"industry_profile": "automotive", "standard_indicator": "equity_ratio",
     "alt_indicator": None, "direction": "higher_better",
     "threshold_pass": 40.0, "threshold_caution": 30.0,
     "note": "自動車: 設備投資・研究開発が大きく、40%以上で業界中央値",
     "source": "中小企業庁 業種別経営指標(自動車業、2026年5月)"},
    {"industry_profile": "materials", "standard_indicator": "equity_ratio",
     "alt_indicator": None, "direction": "higher_better",
     "threshold_pass": 35.0, "threshold_caution": 25.0,
     "note": "素材: 商品市況の影響大、自己資本比率35%以上で業界実勢",
     "source": "中小企業庁 業種別経営指標(鉄鋼・非鉄業、2026年5月)"},
])

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

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

def add_additional_industries() -> None:
    """5業種を industry_indicator_map に追加(既存10業種は変更しない、idempotent)"""
    with duckdb.connect(str(DB_PATH)) as conn:
        conn.register("add_df", ADDITIONAL_INDUSTRIES)
        conn.execute("BEGIN")
        try:
            conn.execute("""
                DELETE FROM industry_indicator_map
                WHERE (industry_profile, standard_indicator) IN
                    (SELECT industry_profile, standard_indicator FROM add_df)
            """)
            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 add_df
            """)
            conn.execute("COMMIT")
        except Exception:
            conn.execute("ROLLBACK")
            raise
        finally:
            conn.unregister("add_df")
        n = conn.execute("SELECT COUNT(*) FROM industry_indicator_map").fetchone()[0]
        print(f"industry_indicator_map rows (after expansion): {n}")

def upsert_portfolio_score(score_id: int, portfolio_name: str,
                            hhi: float, weighted_rpn: float,
                            high_corr_pair_count: int, score_summary: str) -> None:
    """ポートフォリオスコアを保存(idempotent)"""
    with duckdb.connect(str(DB_PATH)) as conn:
        conn.execute("BEGIN")
        try:
            conn.execute("DELETE FROM portfolio_scores WHERE score_id = ?", [score_id])
            conn.execute("""
                INSERT INTO portfolio_scores (score_id, portfolio_name, hhi, weighted_rpn,
                       high_corr_pair_count, score_summary)
                VALUES (?, ?, ?, ?, ?, ?)
            """, [score_id, portfolio_name, hhi, weighted_rpn, high_corr_pair_count, score_summary])
            conn.execute("COMMIT")
        except Exception:
            conn.execute("ROLLBACK")
            raise

if __name__ == "__main__":
    setup_portfolio_scores_schema()
    add_additional_industries()
    print("portfolio_scores + 15 industries ready")

エンジニア的に言い換えると(製品ラインアップの分散度評価)

ポートフォリオスコアテーブルは、製造業 DX で言う 「製品ラインアップの分散度評価表」そのものです。製造業では「主力製品の売上比率」「製品ファミリー数」「需要相関」を1つのスコアにまとめて、「単一製品依存リスク」を評価します。投資のポートフォリオ業種分散も同じ作法で、HHI + 相関 + RPN を1つのスコアシートに集約します。

3指標統合の解釈マトリクス(HHI × 相関 × RPN の読み方)

3指標は独立した情報を持つため、組合せパターンで意味が変わります。下記マトリクスで自分のポートフォリオがどのパターンに該当するかを確認してください。

パターンHHI相関 0.7+ ペア数加重 RPN意味推奨アクション
① 理想分散< 20000-2件< 150多業種に均等、独立性高、リスク低個別 PASS 銘柄の定性調査に集中
② 表面分散< 20005+件< 150業種数は多いが連動性高、危機時に同時下落負の相関業種を追加 or 高相関ペアを集約
③ 集中分散2000-30000-2件< 2005-10業種に集中、独立性は確保HHI 高い業種の銘柄を別業種にシフト
④ 高リスク集中2000-30003+件200-300業種偏在 + 連動性 + 高リスク業種比率ポートフォリオの大幅再編検討
⑤ 危険集中≥ 30003+件≥ 300少業種への集中 + 業種間連動 + 高リスク原則として避ける構成。即時の業種シフト
⑥ 少数高リスク≥ 30000-2件≥ 3003-4業種で高リスク業種に集中業種数を増やすか、低 RPN 業種を追加

プロセスFMEA(層別)でポートフォリオ業種分散の故障モードを整理

故障モード影響度事前対策
マスタ業種コード分類の境界曖昧(コングロマリット企業)主力事業セグメントを採用、複数業種への分散登録は避ける
取得株価履歴の欠損で相関分析が不安定履歴24ヶ月未満の銘柄は分析から除外、min_periods=18 で安定性確保
判定業種カバレッジ不足(15業種以外)中〜高未カバー業種は logger.warning + manufacturing 集約、応用編 #20 で全業種網羅
判定負の相関銘柄組合せの最適化未対応低〜中本記事は相関係数の表示のみ、最適化(マルコビッツ)は応用編 #20 で扱う
判定テールリスク相関(危機時の同時下落)見落とし過去24ヶ月平均と直近6ヶ月の両方を計算、危機局面のサブセット分析
判定独禁法基準そのままの誤適用投資視点閾値 (2000/3000) を default に、独禁法基準 (1500/2500) は引数で切替可能
通知ポートフォリオスコアの通知連携欠落低〜中応用編 #15 の LINE 通知に「HHI 値・risk_level」を追加表示

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

判断テーマ採用主なトレードオフ
判断1集中度指標HHI(市場集中度指数、投資視点閾値)シンプル vs 多軸評価
判断2相関分析の期間過去24ヶ月(月次)長期傾向 vs テールリスク
判断3RPN スコア重み等倍積(S × O × D)標準FMEA作法 vs 業種特性差
判断4ポートフォリオ単位個別銘柄評価とは別テーブルスキーマ統一 vs 役割分離

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

判断1(集中度指標)はスコアの読みやすさに直結、判断2(相関期間)は危機時の捉え方に影響、判断3(RPN重み)は業種差の表現精度を左右、判断4(テーブル設計)はスキーマ整合性を担保します。応用編 #20 のまとめでも、この4判断を起点にパイプライン全体のスコアリング設計を整理する予定です。

判断1:集中度指標を HHI にするか別の指標にするか

  • 採用理由: HHI は独占禁止法・FTC で標準的に使われる指標で、計算がシンプル(業種シェアの2乗和)。投資視点向け閾値 (2000/3000) を default に、独禁法基準 (1500/2500) も引数で切替可能
  • 採用したことで失うもの:
    • トリガー条件: 業種数が極端に少ない(3業種以下)ポートフォリオで HHI が過小評価される可能性
    • 閾値: 「均等分散 vs 偏った分散」のニュアンスは HHI 1次元では不足
    • 残るメリット: 1指標で全体把握、独占禁止法基準の理解で投資中級者にも納得感
    • 対処: 業種数(unique count)と HHI を併用する2軸評価を応用編 #20 で検討
  • 本業 DX との同型対応: サプライチェーン FMEA でも「サプライヤー数 vs HHI」のトレードオフは同じ。1指標に集約する利点と多軸評価の精緻さのバランスは、製造業の品質管理でも繰り返し登場するテーマ

判断2:相関分析の期間を24ヶ月にするか別の期間にするか

  • 採用理由: 24ヶ月は2年分で景気サイクルの一部を捉えられる長さ。pandas の corr() で計算負荷も軽い
  • 採用したことで失うもの:
    • トリガー条件: 過去24ヶ月に大きなショックがなかった場合、テールリスク相関を捕捉できない
    • 閾値: 平常時の相関 0.4 が危機時に 0.8 に跳ね上がる現象を見落とす
    • 残るメリット: J-Quants Light の遅延データでも実用的に運用可能
    • 対処: 過去24ヶ月平均 + 直近6ヶ月の両方を計算、危機局面(リーマン・コロナ等)のサブセット分析を別途実施
  • 本業 DX との同型対応: サプライヤー連動性試験でも「過去2年の通常稼働データ vs 災害時データ」の使い分けが定石。テールリスク評価の難しさは投資もサプライチェーンも共通課題

判断3:RPN スコアを等倍積にするか加重平均にするか

  • 採用理由: 等倍積(Severity × Occurrence × Detection)は AIAG-VDA FMEA Handbook の標準作法。3軸の相互作用を反映
  • 採用したことで失うもの:
    • トリガー条件: 業種特性で「Severity だけ極端に高い」場合の表現が単調
    • 閾値: 各軸の重み調整ができない
    • 残るメリット: 標準作法で読者が再現しやすい、製造業エンジニアに馴染みあり
    • 対処: 業種特性に応じた重み付き積(例: Severity^1.5 × Occurrence × Detection)を応用編 #20 で検討
  • 本業 DX との同型対応: 製造業の FMEA でも「等倍積 vs 加重積 vs Action Priority」のトレードオフは AIAG-VDA 改定(2019)で議論された論点。業界標準と業界特性のバランスは同じ構造で検討される

判断4:portfolio_scores を別テーブルにするか v_screening_input に統合するか

  • 採用理由: ポートフォリオ単位の評価は個別銘柄評価と粒度が違うため、別テーブル portfolio_scores を採用。複数ポートフォリオを管理する設計
  • 採用したことで失うもの:
    • トリガー条件: 単一ポートフォリオしか持たない場合、テーブル設計が過剰
    • 閾値: スキーマ統一性が下がる(ticker_normalized 主キー vs score_id 主キー)
    • 残るメリット: 複数ポートフォリオ(実験用 / 本運用 / バックテスト用)を並行管理可能
    • 対処: 単一運用の場合は score_id=1 固定で運用、必要時に拡張。応用編 #15 判定器との JOIN 仕様は応用編 #20 で整理
  • 本業 DX との同型対応: 製造業のサプライチェーン管理でも「個別部品マスタ vs プロジェクト単位の評価表」を別テーブルで管理するのが定石。粒度の異なるデータを同一スキーマに混ぜると保守が破綻する経験は同型

本業の話:サプライチェーン FMEA で「単一供給依存」を10件→1件に削減した経験

筆者が製造業の開発部門で、新製品のサプライチェーン設計を担当していたときのこと。新製品には約200点の部品が使われており、初稿のサプライヤー設計では「コスト最適化」だけを見て、各部品で最も安いサプライヤーを選定していました。レビュー会議で、ベテランから次のように指摘されました:

  • 同一サプライヤーから何点調達しているか集計しろ。コスト最適化の名のもとに、200点中50点を1社に依存していた。そのサプライヤーが地震・倒産・契約変更で停止したら新製品が出荷できない
  • サプライヤー集中度を FMEA の RPN で評価しろ。発生頻度(自然災害・経営リスク) × 影響度(部品供給停止時の生産影響) × 検出難度(リスク予兆の事前察知)で序列化する」
  • RPN 200 以上の組合せはセカンドソース確保を必須化。多少コストが上がっても冗長性を確保する」

具体的な業務インパクトと現場感の質的記述:

  • 初期データ整備の苦労: 200点部品 × サプライヤー × 地域 × 単価情報を Excel テーブル(列: 部品名・サプライヤー名・国・地域・年間調達額・年間数量・代替候補数 の7列)で集計するのに約2週間。Excel は便利だが、集計関数の入れ子が複雑化するとデバッグが困難で、半分の期間が「集計式のバグ取り」
  • サプライヤー集中度を HHI で再算定すると HHI 4500(高集中状態)。15社が単一サプライヤー依存(同一サプライヤーから10点以上)
  • FMEA RPN で序列化した結果、RPN 200 以上のリスク部品が15点セカンドソース確保プロセスは半年がかり: (1) 候補サプライヤー10社に技術仕様書送付・回答受領(1.5ヶ月)、(2) 試作品評価(2ヶ月)、(3) 価格交渉・契約締結(1ヶ月)、(4) 量産品質検証(1.5ヶ月)。この期間中、設計部門・購買・品質保証・法務が連携して進めた
  • 結果として、単一供給依存は10件→1件に削減(残り1件は技術的にセカンドソースが存在しない特殊部品で、在庫を6ヶ月分確保することで代替対策)
  • 翌年に発生した東南アジアの地震で、当該地域のサプライヤー1社が3週間停止。セカンドソース確保済みだったため、3日以内に代替サプライヤーへの切替を完了し出荷遅延ゼロ。この3週間の対応では、緊急生産シフト調整・品質保証チームの代替部品検証・社内承認の特急手続きを並行し、品質リスクを最小化しながら切替を完遂
  • コスト追加(年間約2,000万円)は出荷遅延回避(試算3億円規模)と比べ、約15倍のリターンを発生
  • この経験から「コスト最適化と分散リスクのトレードオフ」を FMEA で定量化する作法が部内で標準化、新規プロジェクトの立ち上げ時にサプライチェーン FMEA が必須プロセスになった

本記事の業種分散設計は、この経験から直接来ています:

  • HHI でポートフォリオ業種集中度を測る: サプライヤー集中度の評価作法をそのまま転用
  • RPN で業種別リスクを序列化: サプライヤー集中度評価の標準作法を投資に応用
  • 業種間相関で「テールリスクの連動」を検出: 地震多発地域に集中していたサプライヤーリスクの発想

逆方向の転移:投資の業種分散分析が本業のサプライチェーン FMEA に逆輸入された

本記事の業種分散分析を組み始めて以降、本業のサプライチェーン FMEA にも pandas + DuckDB ベースの定量化フレームワークが逆輸入されました。それまで Excel ベースだったサプライヤー集中度の集計が、HHI + 相関分析 + RPN の自動計算スクリプトに置き換わり、サプライチェーンレビューの定例会議の準備時間が約3日 → 半日に短縮。投資側で開発した業種分散の定量化フレームワークが、本業のリスク管理インフラを再強化する事例として応用編シリーズで最も具体的な相互作用例になりました。

まとめ:応用編シリーズ「SPC 4軸 + FMEA = 5層構成」の完成

本記事で応用編シリーズの製造業 DX メタファーが 5層構成として完成しました:

  • 第1層 USL/LSL(規格限界、両学長基準): 記事 #11 + #12
  • 第2層 UCL/LCL(管理限界 ±3σ、SPC 異常値): 記事 #16
  • 第3層 IQR / Z スコア / WER(外れ値・連続性): 記事 #17
  • 第4層 業種別補正テーブル(製品ファミリー別検査): 記事 #18
  • 第5層 FMEA(ポートフォリオ全体のリスク管理): 本記事 #19
  • 個別銘柄評価(SPC 4軸)+ ポートフォリオ評価(FMEA)の役割分担で、応用編シリーズの自動化システムが完全な品質保証になる。SPC が「製品の品質管理」、FMEA が「サプライチェーンのリスク管理」と同型
  • HHI(市場集中度指数、投資視点閾値 2000/3000)でポートフォリオ業種集中度を1次元の数値に集約。業種間相関分析で「金融危機時の同時下落」のような隠れた連動性を可視化。FMEA RPN(影響度 × 発生頻度 × 検出難度)で業種別リスク優先順位を序列化
  • 5業種拡張で計15業種カバー: 食品・化学・電気機器・自動車・素材を追加し、東証時価総額の概ね9割をカバー。応用編 #20 で残り業種を追加してカバレッジ完成

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

  1. 本記事のスニペット1〜2を順に実行し、自分の保有銘柄ポートフォリオの HHI と業種間相関を計算してみる。HHI 3000 を超えていたら業種シフトを検討、相関 0.7 以上のペアがあれば「実質同一セクター」として再分類
  2. スニペット3で FMEA RPN テーブルを実行し、自分のポートフォリオの加重平均 RPN を算出。各業種の貢献度を industry_breakdown で確認することで、リスク削減の優先順位(どの業種を減らすか)が見える
  3. セクション4 の3指標統合解釈マトリクスを見ながら、自分のポートフォリオが①〜⑥のどのパターンに該当するかを確認。⑤危険集中・⑥少数高リスクの場合は、応用編 #20 のまとめ回までに業種シフト計画を立てておく

次回予告:応用編シリーズ全体のパイプライン総括(#11〜#19)

次回(記事#20)は応用編シリーズの最終回です。#11〜#19 で構築してきたパイプラインの全体像を整理し、応用編 → 発展編(記事#21〜#30、CAN-SLIM × 成長株分析)への接続点を提示します。

  • 応用編パイプラインの全体図(取得 → 統合 → 自動化 → 時系列 → 罠検知 → 業種別補正 → 業種分散)
  • 残りの東証業種(証券・SI・陸運・空運 等)の追加で全業種カバー
  • 市場ベースライン(TOPIX)併用、マルコビッツ最適化、portfolio_scores と v_screening_input の JOIN 仕様など応用編で先送りした課題の整理
  • 発展編(CAN-SLIM × 成長株)への接続点

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

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

前回 #18 業種別財務健全性本記事 #19 業種分散の FMEA | 次回 #20(応用編シリーズ最終回・公開予定)

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

免責事項(再掲)

本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。コード・FMEA設計・閾値(HHI・相関・RPN等)は教育目的であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。J-Quants API・EDINET API の利用規約は変更される可能性があるため、実装時は各APIの公式ドキュメント・利用規約を必ず確認してください。本記事中に J-Quants API から取得した実データは掲載していません(利用規約に基づく方針)。

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

この記事を書いた人

コメント

コメントする

CAPTCHA


目次