高配当株の安定性を pandas で時系列分析|連続増配・変動係数の計算

Chelsea-Labs #16 サムネイル

免責事項

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

前回の記事#15では、両学長基準の高配当株スクリーニングを Python の multiprocessing で並列化し、LINE 通知付きの自動化パイプラインを完成させました。ただし判定器の中で、EPS トレンド・連続増配年数・営業CF 5年プラスの3指標は CAUTION のまま渡していたことを覚えていますか?本記事ではその3指標を、pandas + matplotlib による時系列分析の使い方で本物の値に書き換える実装を扱います。

応用編 #11/#12 で導入した両学長スクリーニング基準のうち、6指標中3つが「持続性(時間軸の評価)」に分類されていました。「単年データのスナップショットだけでは、高配当株の持続性は判断できない」のが時系列分析の本質で、過去5〜10年の動きを見て初めて「この銘柄は減配せずに高配当を維持し続けるか」が定量化できます。本記事は応用編で扱う「高配当株のための時系列分析の入門・サンプルコード」です。

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

  • 連続増配年数の判定ロジック: 「単調増加」を SQL で書くか pandas で書くか、減配を1回でも許容するかどうかで実装が大きく変わる
  • 変動係数(CV)の閾値設計: CV 0.3 が安定と言われるが、業種で水準が違う。製造業では妥当でも、商社の資源価格連動型では上振れする
  • 可視化の用途分け: 「銘柄1社の長期推移を深掘り」と「複数銘柄を横並びで比較」では描画方式が異なる。さらに移動平均・管理図で異常を早期検知する設計も必要

筆者は製造業の開発現場で、製品の長期信頼性試験(Long-Term Reliability Test、10,000時間連続稼働試験)を多く設計してきました。この試験で学ぶのが、「平均値だけ見ても製品の信頼性は判断できない、分布・経年劣化曲線・変動の幅を見る」という時系列の見方です。高配当株データの安定性評価は、まさに製品信頼性試験そのものの構造で、過去 N 年のデータから「将来も同じ品質が続くか」を確率論的に推定する作業です。

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

本記事は pandas + matplotlib での実装を扱いますが、「時系列分析は要らない、結果だけ見たい」という読者には、SBI証券・楽天証券・マネックス証券の無料ツールでの配当履歴チャート表示で十分のケースもあります。各社の銘柄詳細ページには過去配当・EPS グラフが用意されており、目視で「高配当が右肩上がりか」「異常な落ち込みがないか」を確認するだけでも、両学長基準の持続性評価には実用的です。本記事の自動化は「全銘柄に対して一括判定する」ためのもので、興味のある数銘柄だけ見るなら無料ツールが最短です。

本記事の前提と難易度

  • SQL中級WINDOW 関数・ROW_NUMBER)の経験があると進めやすい。SQL 未経験の方は 記事#14(DuckDB データ統合)で SQL 基本を一通り通過してから戻ってくる流れを推奨
  • pandas(DataFramegroupbyrolling)と matplotlib の基本グラフが描けると進めやすい
  • 記事 #11〜#15 のパイプライン(取得 → 統合 → スクリーニング自動化)が完了している前提
  • J-Quants プランは Standard 推奨: Light(月1,650円)は5営業日遅延・通期決算サマリのみで、過去10年の四半期EPSや変動係数計算には情報が薄い。応用編 #16 以降で深掘りするなら Standard(月3,300円)が現実的。年間で約4万円
  • 本記事の時系列指標が v_screening_input に統合されると、応用編 #15 の判定器が3指標を本物の値で評価できる状態になります

本記事では 4 個のスニペットを通じて、高配当株の時系列指標を計算し、可視化し、データマートビューに統合します。本格的な異常検知・罠銘柄検知(特殊配当の混入対応など)は次回 #17 で扱います。

結論:高配当株の持続性は「過去N年の連続非減少判定 × EPS 変動係数 × rolling 移動平均によるトレンド」の3指標で定量化する。pandas の rolling と SQL Window 関数(ROW_NUMBER OVER)を組み合わせれば、東証約3,900社の連続増配年数・EPS 安定性・営業CF プラス年数を10数行のサンプルコードで計算可能。製造業の長期信頼性試験と同じ思想で、平均値ではなく「分布の安定性」を見るのが本質。本記事の指標が v_screening_input に統合されると、応用編 #15 の判定器が CAUTION で渡していた3指標を本物の PASS/CAUTION/FAIL で評価できるようになる。

目次

高配当株の安定性を測る3つの「持続性指標」

記事 #12 で整理した両学長基準6つのうち、本記事で扱うのは「持続性」分類の3指標です。これらは単年データではなく 過去5〜10年の動きから評価する必要があります。

指標計算方法PASS 閾値の例製品信頼性での対応
EPS トレンド(変動係数 + 線形回帰)過去5年 EPS の std/mean + 傾き符号CV < 0.3 かつ傾き ≥ 0 で PASS性能の経年劣化試験での合格率分散と劣化曲線
連続非減少年数(連続増配)過去 N 年の配当を 1年前と比較し、減配が無い年数10年以上で PASS、5年で CAUTION長期信頼性試験での連続稼働時間
営業CF プラス年数過去5年で営業CF > 0 の年数5年フル PASS、3-4年で CAUTION稼働中の電力収支の連続成立年数

用語の定義:「連続増配」と「連続非減少」

本記事の判定では、「同額据え置き」も連続増配の継続にカウントします。これは日本企業に多い「累進配当(減配しない方針)」を反映した設計で、英語では monotonic non-decreasing(連続非減少) と表現するのが正確です。

厳密な「毎年必ず増配する」米国アリストクラッツ型(Dividend Aristocrats、25年連続増配の S&P500 銘柄)と比べると緩い基準で、JTのように10年以上配当を据え置いている銘柄も「連続増配10年以上 = PASS」と判定される点に注意してください。米国型の厳格な評価をしたい場合は、設計判断1で議論する strict_increment モードを別途用意する必要があります。

変動係数 (CV) の業種別目安レンジ

CV 0.3 を一律の閾値にしてしまうと、業種特性の差で誤判定が起きます。実装上は応用編 #18 で業種別 CV 閾値テーブルを設計しますが、本記事の段階では次のレンジを目安に持っておくと、CAUTION/FAIL が出た時に「業種特性なのか、本当に業績不安定なのか」が判断できます(数値は2026年5月時点の参考値)。

業種カテゴリEPS CV の目安レンジ注釈
製造業(安定型)0.2 〜 0.3消費財・自動車部品など。CV 0.3 が PASS 境界
製造業(景気感応型)0.3 〜 0.5電機・機械。景気サイクル影響あり
商社0.4 〜 0.6資源価格・為替の影響大。一律基準では過剰 CAUTION
資源・素材0.5 〜 0.9商品市況に強く連動。閾値を業種別に緩める必要
通信・電力(インフラ)0.1 〜 0.2規制業種で安定。CV 低い銘柄が多い
銀行・保険0.3 〜 0.6金利環境・引当金で変動。EPS だけでは見えない安定性指標が必要

営業CFプラス年数は業種で機能しない場合がある

「過去5年で営業CFがすべてプラス」は、製造業や小売業では持続性の優れた指標ですが、銀行・証券・保険などの金融業では事業構造上の特殊性で誤判定される可能性があります。金融業の営業CFは「貸出残高の増減」「証券の売買」など事業特性に紐づくため、製造業のような「キャッシュ獲得力」を意味しません。本記事の判定器に通すと、銀行株の多くが CAUTION/FAIL に振れる可能性が高い点を認識しておいてください。応用編 #18 で業種別の代替指標(銀行はBIS自己資本比率、保険はソルベンシー・マージン比率など)に切り替える設計を扱います。

エンジニア的に言い換えると(製品信頼性試験との対応)

製造業の長期信頼性試験では、製品の合否を「累積稼働時間 × 連続稼働性 × 性能の安定性」の3指標で判定します。本記事の3指標と完全に対応します:

  • 連続非減少年数 = 連続稼働時間(Mean Time Between Failures、平均故障間隔)
  • EPS 変動係数 = 性能の経時安定性(Coefficient of Variation の試験)
  • 営業CF プラス年数 = 性能基準クリア年数(合格率の累積)

判定の対象が「製品 → 企業」に変わるだけで、思考プロセスは同じ。応用編 #11/#12 のスペックシート発想と同型です。

スニペット1:DuckDB から配当・EPS 履歴を pandas で時系列化するやり方

記事 #14 で構築した fins_statements テーブルから、銘柄ごとの過去 N 年分の通期決算データを pandas DataFrame に取り出します。Window 関数で「各銘柄の決算期を最新から N 期分」絞り込み、その上で時系列指標を計算する設計です。SQL の文字列連結ではなくパラメータバインディングを使うことで、SQL injection リスクを回避します。

# load_history.py — DuckDB から配当・EPS 履歴を時系列化(パラメータバインディング版)
# 動作環境: Python 3.11+ / duckdb 0.10+ / pandas 2.x
import duckdb
import pandas as pd
from pathlib import Path

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

def load_dividend_eps_history(n_years: int = 10) -> pd.DataFrame:
    """各銘柄の過去 n_years 期分の通期決算(配当・EPS 関連)を取得

    返却 DataFrame のカラム:
    ticker_normalized / fiscal_year_end / eps / dps_annual / period_index (0=最新)

    SQL injection 対策のため n_years は f-string ではなく ? バインディングで渡す
    """
    sql = """
    SELECT ticker_normalized, fiscal_year_end, eps, dps_annual, period_index
    FROM (
      SELECT
        CASE WHEN LENGTH(LocalCode) = 5 THEN SUBSTRING(LocalCode, 1, 4) ELSE LocalCode END AS ticker_normalized,
        DisclosedDate AS fiscal_year_end,
        CAST(EarningsPerShare AS DOUBLE) AS eps,
        CAST(ResultDividendPerShareAnnual AS DOUBLE) AS dps_annual,
        ROW_NUMBER() OVER (
          PARTITION BY (CASE WHEN LENGTH(LocalCode) = 5 THEN SUBSTRING(LocalCode, 1, 4) ELSE LocalCode END)
          ORDER BY DisclosedDate DESC
        ) - 1 AS period_index   -- 0 が最新、N-1 が n_years 前
      FROM fins_statements
      WHERE TypeOfDocument LIKE 'FY%FinancialStatements%'
    ) sub
    WHERE period_index < ?
    ORDER BY ticker_normalized, period_index
    """
    with duckdb.connect(str(DB_PATH)) as conn:
        return conn.execute(sql, [n_years]).fetchdf()

if __name__ == "__main__":
    df = load_dividend_eps_history(n_years=10)
    print(df.shape)
    print(df.head(20))
    # 銘柄ごとの行数を確認(平均が10に近いほど履歴が揃っている)
    print(df.groupby("ticker_normalized").size().describe())

形式の罠:決算データの欠損と新規上場銘柄

  • 履歴が10年に満たない銘柄(新規上場銘柄)は、変動係数や連続増配年数の信頼性が下がる。5期未満は判定スキップするルールを推奨(次のスニペット2で min_periods=5 として実装)
  • 四半期決算しか取れていない銘柄は WHERE TypeOfDocument LIKE 'FY%FinancialStatements%' でフィルタされる。J-Quants の Free / Light プランでは詳細財務が制限されるため、過去10年の高配当株分析には Standard 以上を推奨
  • EPS が NULL になる年(赤字計上時など)は NaN として保持し、std/mean 計算時は skipna=True で扱う

スニペット2:変動係数・連続非減少年数・rolling 移動平均で経年劣化を判定する

時系列データができたら、3つの持続性指標を計算します。pandas の groupby + aggapply ではなく将来安全な agg)で銘柄ごとに集約します。さらに本スニペットでは rolling 移動平均と線形回帰の傾きで経年劣化を判定するロジックを追加し、本業の長期信頼性試験で見ている「経年劣化曲線」を pandas で実装します。

# compute_persistence.py — 持続性指標(CV / 連続非減少 / 移動平均トレンド / 営業CFプラス年数)
# 動作環境: Python 3.11+ / pandas 2.x / numpy
import pandas as pd
import numpy as np

MIN_PERIODS = 5  # 履歴期数の下限(新規上場銘柄を機械的に CAUTION 化)

def compute_consec_inc_years(dps: pd.Series) -> int:
    """配当の連続非減少(同額据え置きを許容)年数を最新から遡って数える

    例: dps = [100, 95, 95, 80, 70](最新→過去) なら 100>=95 OK, 95>=95 OK ... → 4
    減配があった年で打ち切り。NaN は欠損扱いで打ち切り
    """
    arr = dps.dropna().to_numpy()
    if len(arr) < 2:
        return 0
    count = 0
    for i in range(len(arr) - 1):
        if arr[i] >= arr[i + 1]:
            count += 1
        else:
            break
    return count

def compute_eps_cv(eps: pd.Series, min_periods: int = MIN_PERIODS) -> float | None:
    """EPS の変動係数(CV = std / mean)を計算。負の平均は None"""
    arr = eps.dropna().to_numpy()
    if len(arr) < min_periods:
        return None
    mean = np.mean(arr)
    if mean <= 0:
        return None  # 赤字続きなど、CV の意味が破綻するケース
    return float(np.std(arr, ddof=1) / mean)

def compute_eps_trend_slope(eps: pd.Series, min_periods: int = MIN_PERIODS) -> float | None:
    """EPS の長期トレンド(線形回帰の傾き)を計算

    傾き > 0: 経年劣化曲線が上向き = growing
    傾き ≈ 0: 横ばい = stable
    傾き < 0: 下降 = declining

    rolling は別途可視化に使い、トレンド判定はシンプルな polyfit で
    """
    arr = eps.dropna().to_numpy()
    if len(arr) < min_periods:
        return None
    # 配列は最新→過去の順なので時間軸を反転して回帰
    x = np.arange(len(arr))[::-1]
    slope, _ = np.polyfit(x, arr, deg=1)
    return float(slope)

def compute_eps_rolling_mean(eps: pd.Series, window: int = 3) -> pd.Series:
    """3年移動平均で短期ノイズを除去した経年劣化トレンドを得る"""
    return eps.rolling(window=window, min_periods=1).mean()

def classify_eps_trend(cv: float | None, slope: float | None) -> str | None:
    """CV と傾きの組み合わせで eps_trend を分類"""
    if cv is None or slope is None:
        return None
    if cv < 0.2 and slope > 0:
        return "growing"
    if cv < 0.3:
        return "stable"
    if cv < 0.5:
        return "volatile"
    return "declining"

def compute_persistence_metrics(history: pd.DataFrame, ocf_history: pd.DataFrame) -> pd.DataFrame:
    """銘柄ごとの持続性4指標を集約する(pandas 2.2+ 対応の agg ベース)"""
    history_sorted = history.sort_values(["ticker_normalized", "period_index"])

    # groupby.agg で複数指標を同時計算(pandas 3.0 でも動く形)
    persistence = history_sorted.groupby("ticker_normalized", sort=False).agg(
        consec_inc_years=("dps_annual", compute_consec_inc_years),
        eps_cv=("eps", compute_eps_cv),
        eps_trend_slope=("eps", compute_eps_trend_slope),
    ).reset_index()
    persistence["eps_trend"] = persistence.apply(
        lambda r: classify_eps_trend(r["eps_cv"], r["eps_trend_slope"]), axis=1
    )

    # 営業CF プラス年数(過去5年で OCF > 0 の年数)
    ocf_recent = ocf_history.sort_values(["ticker_normalized", "fiscal_year_end"], ascending=[True, False])
    ocf_recent["rank"] = ocf_recent.groupby("ticker_normalized").cumcount()
    ocf_5y = ocf_recent[ocf_recent["rank"] < 5]
    ocf_summary = ocf_5y.groupby("ticker_normalized", sort=False).agg(
        ocf_positive_years=("operating_cash_flow", lambda s: int((s > 0).sum())),
    ).reset_index()

    merged = persistence.merge(ocf_summary, on="ticker_normalized", how="left")
    # NaN を含む INTEGER カラムは pandas Int64(nullable integer)で型安全化
    merged["consec_inc_years"]   = merged["consec_inc_years"].astype("Int64")
    merged["ocf_positive_years"] = merged["ocf_positive_years"].astype("Int64")
    return merged

# 使用例
# from load_history import load_dividend_eps_history
# history = load_dividend_eps_history(n_years=10)
# import duckdb
# with duckdb.connect("data/stocks.duckdb") as conn:
#     ocf_history = conn.execute(
#         "SELECT ticker_normalized, fiscal_year_end, operating_cash_flow FROM edinet_metrics"
#     ).fetchdf()
# persistence = compute_persistence_metrics(history, ocf_history)
# print(persistence.head())
# # ticker_normalized  consec_inc_years  eps_cv  eps_trend_slope  eps_trend  ocf_positive_years
# # 0001               12                0.18    1.20             growing    5

エンジニア的に言い換えると(経年劣化曲線の実装)

compute_eps_trend_slopenp.polyfit(x, arr, deg=1) は、製造業の長期信頼性試験で言う「経年劣化曲線の傾き」を線形回帰で抽出する処理です。試験データから劣化レートを定量化するのと同じ手法です。compute_eps_rolling_mean は短期ノイズを除去した「滑らかな劣化曲線」で、可視化(次のスニペット)で本業の試験チャートに重ねる移動平均線として使います。

スニペット3:matplotlib + シューハート管理図の発想で配当推移を可視化する手順

数値だけでは「なぜこの銘柄が CAUTION なのか」が分かりにくいため、複数銘柄を並べて過去10年の配当推移を1枚のグラフで比較するスニペットを示します。本記事では 3年 rolling 移動平均を重ね描きし、製造業のシューハート管理図に対応する形で USL/LSL(規格限界)を水平線で表示します。

SPC 用語の整理:USL/LSL と UCL/LCL の違い

製造業の SPC(Statistical Process Control、統計的工程管理)では、グラフに2種類の水平線を引きます。混同すると製造業エンジニア読者から信頼を失うので、本記事では明確に区別します:

  • USL/LSL(Upper/Lower Spec Limit、規格上限/下限): 仕様要件から決まる「合否境界」。本記事では両学長基準(配当性向 USL 60%、自己資本比率 LSL 40%)に対応
  • UCL/LCL(Upper/Lower Control Limit、管理上限/下限): 過程の統計から決まる「平均±3σ」の管理限界。本記事では応用編 #17 の罠銘柄検知(IQR/Zスコア外れ値)で扱う

本スニペットの可視化では USL/LSL = 両学長基準のラインを描きます。UCL/LCL(±3σ ベースの管理限界)は次回 #17 で扱います。両者を混ぜて1枚に描く可視化は応用編 #20 のまとめで統合します。

# visualize_history.py — 配当・EPS・配当性向 + 移動平均 を3パネルで可視化
# 動作環境: Python 3.11+ / pandas 2.x / matplotlib 3.x
import matplotlib.pyplot as plt
import pandas as pd
from pathlib import Path

OUT_DIR = Path("data/screening_results/figures")

def plot_history(history: pd.DataFrame, tickers: list[str], save_path: Path | None = None) -> None:
    """指定銘柄の配当・EPS・配当性向 + 3年rolling移動平均を3パネルで描画

    history は load_dividend_eps_history の出力(period_index 0 が最新)
    fiscal_year_end を x 軸の時間順にし、複数銘柄を1パネル内に重ねる
    """
    fig, axes = plt.subplots(3, 1, figsize=(10, 9), sharex=True)

    for ticker in tickers:
        g = history[history["ticker_normalized"] == ticker].sort_values("fiscal_year_end").dropna(subset=["dps_annual","eps"])
        if g.empty:
            continue
        # 配当(DPS)+ 3年移動平均
        axes[0].plot(g["fiscal_year_end"], g["dps_annual"], marker="o", label=f"{ticker} DPS")
        axes[0].plot(g["fiscal_year_end"], g["dps_annual"].rolling(3, min_periods=1).mean(),
                     linestyle="--", alpha=0.6, label=f"{ticker} 3y MA")
        # EPS + 3年移動平均(経年劣化曲線)
        axes[1].plot(g["fiscal_year_end"], g["eps"], marker="s", label=f"{ticker} EPS")
        axes[1].plot(g["fiscal_year_end"], g["eps"].rolling(3, min_periods=1).mean(),
                     linestyle="--", alpha=0.6, label=f"{ticker} 3y MA")
        # 配当性向 = DPS / EPS × 100(規格限界 USL 60% / 80% を水平線表示)
        payout = (g["dps_annual"] / g["eps"]).where(g["eps"] > 0) * 100
        axes[2].plot(g["fiscal_year_end"], payout, marker="^", label=f"{ticker} 配当性向")

    axes[0].set_ylabel("DPS (1株配当, 円)")
    axes[1].set_ylabel("EPS (1株純利益, 円)")
    axes[2].set_ylabel("配当性向 (%)")
    # USL(Upper Spec Limit)= 両学長基準。UCL/LCL(±3σ管理限界)は #17 で別途扱う
    axes[2].axhline(60, linestyle="--", color="gray", label="USL 60% (規格上限・両学長基準)")
    axes[2].axhline(80, linestyle=":", color="orange", label="USL 80% (CAUTION)")
    for ax in axes:
        ax.legend(loc="best", fontsize=9)
        ax.grid(True, alpha=0.3)
    axes[2].set_xlabel("決算期")
    fig.tight_layout()

    if save_path:
        save_path.parent.mkdir(parents=True, exist_ok=True)
        fig.savefig(save_path, dpi=120)
        print(f"saved: {save_path}")
    else:
        plt.show()
    plt.close(fig)

# 使用例(架空銘柄 XXXX, YYYY を比較)
# from load_history import load_dividend_eps_history
# history = load_dividend_eps_history(n_years=10)
# plot_history(history, tickers=["XXXX", "YYYY"], save_path=OUT_DIR / "compare_xxxx_yyyy.png")

3パネル構成にする理由は、「DPS だけ右肩上がりでも、EPS が横ばいだと配当性向が無理して上昇している」ような構造的な弱さを目視で発見できるからです。3年 rolling 移動平均を重ね描きすることで、単年のノイズを除去した滑らかな経年劣化曲線が見えます。配当性向パネルに axhline で USL 60% / CAUTION 80% のラインを引くことで、しきい値超過が一目で分かります。製造業の管理図でも、「規格限界の水平線を引く」のは標準作法です。

スニペット4:時系列指標を v_screening_input ビューに統合する手順

計算した時系列指標を、応用編 #14 の v_screening_input ビューに統合します。新たに persistence_metrics テーブルを作って、銘柄ごとの3指標を1行で持たせます。これにより、応用編 #15 のスクリーニング Runner を再実行するだけで、CAUTION で渡していた eps_trend / consec_inc_years / ocf_positive_years が本物の値で評価される状態になります(#15 の spec_sheet_judge_v2.py 側の改修は不要)。

# persistence_table.py — 持続性指標テーブルとビュー統合(v2: トランザクション境界・型安全)
# 動作環境: Python 3.11+ / duckdb 0.10+
import duckdb
import pandas as pd
from pathlib import Path

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

DDL_PERSISTENCE = """
CREATE TABLE IF NOT EXISTS persistence_metrics (
    ticker_normalized   VARCHAR PRIMARY KEY,
    consec_inc_years    INTEGER,
    eps_cv              DOUBLE,
    eps_trend_slope     DOUBLE,
    eps_trend           VARCHAR,
    ocf_positive_years  INTEGER,
    computed_at         TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
"""

# 既存 v_screening_input ビューを置き換え(time-series 指標を組み込む)
# 注意: このビュー置換により、記事 #15 の v_screening_input が time-series 指標を含む
# 拡張版になる。#15 の screen_parallel.py は SELECT * で全カラム取得しているため、
# 新カラムが増えても破壊的変更なし。判定器(spec_sheet_judge_v2)も dict.get() ベース
# のため、未参照カラムは自然に無視される。
DDL_VIEW_V2 = """
CREATE OR REPLACE VIEW v_screening_input AS
SELECT
  m.ticker_normalized,
  m.name_jp,
  m.industry_profile,
  -- 配当利回り・配当性向・自己資本比率(記事 #14 から継承)
  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,
  -- 時系列指標を NULL ではなく persistence_metrics の値で
  p.eps_trend,
  p.consec_inc_years,
  p.ocf_positive_years,
  -- 業種補正の参照
  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 industry_indicator_map imap
       ON m.industry_profile = imap.industry_profile
      AND imap.standard_indicator = 'equity_ratio'
;
"""

def setup_persistence_schema() -> None:
    with duckdb.connect(str(DB_PATH)) as conn:
        conn.execute(DDL_PERSISTENCE)
        conn.execute(DDL_VIEW_V2)
        n = conn.execute("SELECT COUNT(*) FROM persistence_metrics").fetchone()[0]
        print(f"persistence_metrics rows: {n}")

def upsert_persistence(persistence_df: pd.DataFrame) -> None:
    """compute_persistence_metrics の出力を idempotent に保存(トランザクション境界付き)"""
    # Int64(pandas nullable)を NULL 可能な int に変換
    df = persistence_df.copy()
    df["consec_inc_years"]   = df["consec_inc_years"].astype("Int64")
    df["ocf_positive_years"] = df["ocf_positive_years"].astype("Int64")
    with duckdb.connect(str(DB_PATH)) as conn:
        conn.register("p_df", df)
        # トランザクション境界で DELETE→INSERT 中の例外時にデータロスを防ぐ
        conn.execute("BEGIN")
        try:
            conn.execute("DELETE FROM persistence_metrics WHERE ticker_normalized IN (SELECT DISTINCT ticker_normalized FROM p_df)")
            conn.execute("""INSERT INTO persistence_metrics
                            SELECT ticker_normalized, consec_inc_years, eps_cv, eps_trend_slope, eps_trend, ocf_positive_years,
                                   CURRENT_TIMESTAMP FROM p_df""")
            conn.execute("COMMIT")
        except Exception:
            conn.execute("ROLLBACK")
            raise
        finally:
            conn.unregister("p_df")

if __name__ == "__main__":
    setup_persistence_schema()

これで応用編 #15 の spec_sheet_judge_v2.py が、3指標を本物の値で受け取れる状態になりました。persistence_metrics テーブルは週次バッチで更新する設計(決算データの更新は四半期ですが、対象銘柄が決算発表するたびに値が変わる可能性があるため)。応用編 #15 の cron スケジュールに persistence_table.py 実行ステップを追加することで、運用がそのまま回ります。

プロセスFMEAで時系列分析の故障モード × 検出指標

応用編 #13/#14/#15 で取得層・統合層・判定層の FMEA を整理しました。本記事の時系列分析にも独自の故障モードがあります。

故障モード影響度検出指標事前対策
履歴期数の不足新規上場銘柄で誤PASSmin_periods=5 条件、5期未満は CAUTION 強制
連続非減少の判定境界JT のような10年据え置き銘柄を「連続増配 PASS」と評価本記事は「非減少」を採用、設計判断1で記録 + strict_increment モード追加検討
EPS 平均がマイナス中〜高変動係数の意味が破綻負の平均は None、判定器側で CAUTION 化
業種特性での CV 過大評価商社・不動産で CV が大きく出る業種別 CV 閾値テーブル(応用編 #18)、本記事では業種別目安レンジ表で目視確認
営業CFが業種で機能しない銀行・証券・保険で大量 CAUTION/FAIL業種別代替指標(BIS、ソルベンシー比率等)に切替(応用編 #18)
配当の特殊配当・記念配当の混入低〜中1年だけ配当が跳ね上がり、翌年「減配」誤判定配当の中央値 vs 平均値の比較で異常検知(#17 で扱う)
matplotlib 描画の欠損対応NaN を含む系列でラインが切れるplot 前に dropna()(v2 で追加済)
persistence_metrics の更新タイミング決算発表後も古い値で判定週次バッチ+決算カレンダー連動の検討
upsert 中の例外でデータロスDELETE 後の INSERT で例外発生BEGIN/COMMIT トランザクション境界(v2 で追加済)

設計判断の記録:時系列分析のトレードオフ

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

時系列分析の設計判断は、無数のバリエーションがあります。本記事では応用編シリーズの自動化で影響度が高い3つに絞りました: 判断1(連続増配の境界)は配当銘柄の選定数に直結、判断2(CV 閾値の業種一律 vs 業種別)は判定の精度に影響、判断3(履歴期数不足の扱い)は新規上場銘柄の評価を左右します。応用編 #18 では業種別 CV 閾値の本格設計を扱う前提で、本記事は最小実装に振っています。

判断1:連続増配の判定で「同額据え置き」を OK とするか

  • 採用理由: 本記事では「連続非減少(同額または増額)」を OK としてカウント。日本企業は安定配当を重視する文化が強く、業績悪化時に「据え置き」を選ぶ累進配当ポリシー銘柄が多いため、本基準は実態に合う
  • 採用したことで失うもの:
    • トリガー条件: 米国アリストクラッツ型(25年連続"増配")の厳格評価が必要な場合
    • 閾値: 「JT が10年連続据え置き → PASS」のような結果を許容できない投資スタイル
    • 残るメリット: 累進配当ポリシー銘柄を取りこぼさずに、減配リスクの低い候補をフィルタできる
    • 対処: strict_increment モード(厳密増配判定)を別フラグで追加実装(応用編 #18 で対応予定)

判断2:CV 閾値を業種一律 0.3 にするか業種別にするか

  • 採用理由: 本記事は最小実装として CV 0.3 を一律閾値で採用。応用編 #18 で業種別補正テーブルに拡張予定
  • 採用したことで失うもの:
    • トリガー条件: 商社・資源・銀行など CV が構造的に高い業種を分析対象に含める場合
    • 閾値: 5大商社の EPS は CV 0.4-0.6 が普通だが、一律基準では全社が volatile/declining 判定で振り落とされる
    • 残るメリット: 製造業・小売・通信などディフェンシブ業種では CV 0.3 が機能
    • 対処: industry_indicator_map に CV 用の業種別行を追加することで業種別運用に拡張可能(本記事のセクション2 にレンジ目安を掲載)

判断3:履歴期数の不足銘柄をどう扱うか

  • 採用理由: 5期未満は None を返し、判定器で CAUTION に分岐。新規上場銘柄を機械的に PASS させない設計
  • 採用したことで失うもの:
    • トリガー条件: IPO直後の高成長企業を高配当株候補として取り込みたい場合
    • 閾値: 上場2-4年目の優良銘柄は他指標(自己資本比率・配当性向)が優秀でも CAUTION
    • 残るメリット: 履歴データ不足による誤PASSを構造的に防止、応用編 #15 の判定器の信頼性を担保
    • 対処: 別パイプライン(IPO 銘柄向け短期評価)を必要に応じて並行運用

本業の話:10,000時間連続稼働試験で「平均値ではなく分布を見る」を学んだ経験

筆者が製造業の開発部門で、新製品の長期信頼性試験を設計していたときのこと。新型モーターの量産前評価として、100台の試作機を10,000時間(連続417日)動かし続けて、性能の経年劣化を測る試験でした。1時間に1回サンプリングする設計で、合計100台 × 10,000時間 × 1サンプル/時間 = 240万データ点を取る想定です。

初稿の試験計画では、ベテランから次のように指摘されました:

  • 平均だけ見ても信頼性は判断できない。100台の平均が規格内でも、5台がギリギリ規格外なら不良率5%。それは出荷できる品質か?」
  • 経年劣化曲線を見ろ。1,000時間で性能 95%、10,000時間で 80% に下がるなら、市場投入後の予想クレーム時期が読める」
  • 変動の幅(標準偏差・CV)を時系列で追え。最初は揃っていた100台が、5,000時間後にバラついてきたら、製造工程の品質管理に問題がある」

本業のエピソードとしての具体的な数値・業務インパクト:

  • 初稿の評価ロジックは「100台の平均値が規格内なら合格」だったが、見直し後は 「100台の80パーセンタイル値(=性能順で下位20%目の値)が規格内」に変更。設計マージンが厳しくなり、量産前の改善対象が 3点 → 11点に増加(内訳: モーター巻線の温度マージン3点、軸受の振動許容3点、制御基板の電源変動2点、外装の防塵設計3点)
  • 10,000時間試験中に4台で性能が CV 0.5 を超えてバラついたことが発見。製造工程の組立公差を調査して原因特定、量産前に改善
  • 結果として、市場投入後の初期不良率が予測値 0.5% に対し実績 0.3%に収まり、量産前評価の精度向上に貢献。年間予測クレーム件数が30件 → 18件(4割減)で、サポート工数も削減
  • 当時の Excel ベースの月次レポート集計には 2-3日の遅延が常態化していて、異常検知が後手に回っていた問題もこのプロジェクトで明らかに
  • この経験で、「平均値は最後の指標、分布と時系列が先」という時系列分析の基本動作を体得

レビューで受けた指摘は、本記事の時系列分析設計に直接反映:

  • 「平均値だけでなく分布を見る」 → 本記事の 変動係数 CV による分散評価
  • 「経年劣化曲線を見る」 → 本記事の compute_eps_trend_slope による線形回帰の傾き判定 + matplotlib の rolling 移動平均重ね描き
  • 「変動の幅を時系列で追う」 → 本記事の 連続非減少年数の判定(最新から遡って減配年が出るまで)

逆方向の転移:投資の時系列分析が本業の試験設計を改善した

本記事の時系列分析を組み始めて以降、本業の信頼性試験にも pandas + matplotlib + 管理図ベースの可視化が逆輸入されました。それまで Excel ベースだった試験データの可視化が pandas に置き換わり、240万データ点の月次集計時間が2-3日 → 数時間に短縮rolling(window=N).mean() による移動平均の使用が本業の試験設計でも標準化され、本記事の compute_eps_rolling_mean と同等のテンプレ関数が部内で共有されるようになりました。投資のスクリーニング自動化を作ることで、本業の試験設計の引き出しが拡張する、という相互作用が応用編シリーズ全体を通じて起きています。

まとめ:高配当株の持続性は「連続非減少 × CV × rolling 移動平均」で定量化する

  • 連続非減少年数 / EPS 変動係数 / rolling 移動平均トレンドの3指標で、高配当株の持続性を定量化する。pandas の groupby.agg + rolling + numpy の polyfit + DuckDB の Window 関数の組み合わせで、東証約3,900社を10数行のサンプルコードで評価可能。製造業の長期信頼性試験での「平均値ではなく分布を見る」発想と同型
  • matplotlib で DPS/EPS/配当性向の3パネル + 3年rolling移動平均を可視化すれば、「DPSだけ右肩上がりでもEPSが横ばいで配当性向が上昇している」ような構造的弱さを目視で発見できる。製造業 SPC の規格限界(USL/LSL = 両学長基準)を水平線で引き、応用編 #17 で扱う管理限界(UCL/LCL = ±3σ)と区別して使う
  • persistence_metrics テーブル + v_screening_input ビュー置換で判定器と統合。応用編 #15 の判定器を再実行するだけで CAUTION で渡していた3指標が本物の値に変わる(#15 のコード改修不要)。次回 #17 の罠銘柄検知(特殊配当・記念配当の異常値除外)で更に精度が上がる

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

  1. 本記事のスニペット1〜2を順に実行し、自分の興味のある高配当株1〜3社の連続非減少年数・EPS 変動係数・rolling 移動平均トレンド・営業CFプラス年数を計算してみる。両学長基準(連続増配10年以上、CV 0.3 未満、営業CF 5年フルプラス)に照らして、PASS 候補が浮かび上がるか確認
  2. スニペット3を実行して、その1〜3銘柄の3パネル可視化グラフ(移動平均線重ね描き)を生成。DPS/EPS の長期トレンドと配当性向の推移を目視確認することで、機械判定だけでは見えない構造的な傾向(例: 配当性向が緩やかに上昇している危険信号)を発見できます
  3. スニペット4で persistence_metrics テーブルと更新版 v_screening_input ビューを構築し、応用編 #15 のスクリーニング Runner を再実行。CAUTION で渡していた3指標が本物の PASS/CAUTION/FAIL に変わることを確認します

次回予告:罠銘柄検知(バリュートラップを統計的に見抜く)

次回(記事#17)では、本記事までで完成したスクリーニングパイプラインに「罠銘柄を統計的に見抜く異常値検知」を追加します。配当利回り 8% 超のような 「高すぎる利回り = 株価暴落のサイン」を、IQR(四分位範囲)や Z スコア、SPC の管理限界(UCL/LCL = ±3σ)で自動検知。特殊配当・記念配当の混入も同じ仕組みで除外します。

  • IQR / Zスコア / 管理限界による外れ値検知の Python 実装
  • 株価暴落と利回り急騰の同時発生を検出するロジック
  • 「高配当の罠」フラグを v_screening_input に追加するビュー設計

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

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

前回 #15 自動化パイプライン本記事 #16 時系列分析 | 次回 #17(公開予定)

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

免責事項(再掲)

本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。コード・時系列分析手法・閾値(CV 業種別レンジ等)は教育目的であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。J-Quants API・EDINET API の利用規約・料金プラン・機能は変更される可能性があるため、実装時は各APIの公式ドキュメント・利用規約を必ず確認してください。本記事中に J-Quants API から取得した実データは掲載していません(利用規約に基づく方針)。可視化サンプルはダミーデータまたは公開済み統計値に基づきます。業種別の CV 閾値・営業CFプラス年数の機能性は応用編 #18 で本格設計します。

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

この記事を書いた人

コメント

コメントする

CAPTCHA


目次