CAN-SLIM の A|年間 EPS の3-5年 CAGR を Python で自動判定

Chelsea-Labs #23 サムネイル

免責事項

本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。CAN-SLIM A 要素の閾値(CAGR +25%、連続増益3年)は教育目的であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。本記事中に J-Quants API・FMP(Financial Modeling Prep)から取得した実データは掲載していません(利用規約に基づく方針)。CAN-SLIM はオニール(William J. O’Neil)著「How to Make Money in Stocks」で提唱された手法を基にエンジニア視点で再構築しています。

前回の記事#22では CAN-SLIM の C(直近1四半期の急成長)を実装しました。本記事は対になる A(Annual EPS Growth、年間 EPS 成長率の3-5年トレンド)を Python と matplotlib で自動判定するサンプルコード入門です。C が「直近の加速度」を見るのに対し、A は「長期的な成長持続性」を見ます。両者を組み合わせて初めて「本物の成長株」が抽出できます。

CAN-SLIM の A 要素は 「年間 EPS が過去3-5年で年平均 +25% 以上(CAGR ベース)の成長を持続している銘柄」を抽出する基準です。「絶対値より変化率、突発より連続性」という #22 の評価設計原則をそのまま長期軸に拡張する形で、本記事の判定器も設計します。

多くの読者がぶつかる壁:

  • 年間 EPS データの取得: 日本株は J-Quants の通期決算(TypeOfCurrentPeriod="FY")、米国株は FMP の年次 income-statement。1年分のデータを過去5期分集めるが、修正開示で複数行が来る場合の重複処理が必要
  • CAGR の計算: 単純平均ではなく 複利成長率(CAGR = (終値/始値)^(1/年数) – 1)。赤字期間を含む場合の対処と、初年度の YoY 計算不能(NaN)が連続増益判定を破壊しない設計
  • 3パネル可視化: EPS 推移、YoY 成長率、CAGR トレンドラインを1枚にまとめる。matplotlib の日本語フォント設定が抜けていると豆腐化する罠

筆者は製造業の研究開発現場で、製品の世代別改善率(Gen1→Gen5、5世代分の長期トレンド)を CAGR で評価し、量産投資の意思決定に使っていました。「直近1世代の改善率(≒ #22 C)」と「過去5世代の年平均改善率(≒ 本記事 A)」の両方を見る作法は、CAN-SLIM の C×A 複合判定と同じ枠組みです。本業の経験を投資データに転用する典型例として、本記事のエピソードで詳しく整理します。

本記事で扱う専門用語の予習

  • CAGR(Compound Annual Growth Rate): 複利年成長率。 (終期EPS / 始期EPS)^(1/年数) - 1。単純平均と異なり、複利の効いた実質的な年率成長を表す
  • 年間 EPS: 通期決算の1株あたり純利益。J-Quants は TypeOfCurrentPeriod="FY" でフィルタ、FMP は period=annual で取得
  • 3-5年トレンド: CAN-SLIM 原典で推奨される評価期間。3年は最低ライン、5年が望ましい
  • 連続増益年数: 過去の年間 EPS が連続で前年比プラスだった年数。本物の成長持続性のシグナル
  • ターンアラウンド: 赤字→黒字転換銘柄。CAGR 評価では除外し、別ロジックで判定
  • GAAP EPS vs Non-GAAP EPS: GAAP は会計原則準拠、Non-GAAP は一過性費用を除いた調整後。米国テック成長株は GAAP では長期赤字のため、本記事の CAN-SLIM A 判定では大半が CAUTION に倒れる構造的問題あり(後述)
  • 修正開示(restatement): J-Quants の通期決算で、企業が後日訂正を公開した場合、同じ会計年度に対して複数行が返る。最新版を採用する dedup が必要

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

「CAGR を自分で計算するのは大変」という読者には、米国 IBD の “EPS 5-Year Growth Rate”マネックス証券の銘柄スクリーナー「過去5年 EPS 成長率」フィルタSBI証券の Morningstar データで同等機能が利用可能です。本記事の自動判定は「自分の閾値で日米両市場の年間 EPS 5年トレンドをスクリーニングしたい」用途で、結果だけ見たい読者には不要です。

本記事の前提と難易度

  • 応用編 #11〜#20、発展編 #21〜#22 を読んでいると進めやすい(必須ではない)
  • pandas(groupbyshiftrolling)と matplotlib の基本
  • 動作環境: Python 3.11+ / pandas 2.x / numpy / matplotlib 3.9+ / duckdb 1.0+ / requests 2.31+
  • データソース: 日本株 J-Quants Light 以上、米国株 FMP Starter プラン(#22 から継続、年間約3.4万円)
  • 戦略軸 vs 戦術軸: 発展編 #21 戦略軸(CAN-SLIM 採用方針)の延長として、#22 と本記事は戦術軸(C 要素・A 要素の個別実装)。発展編 #24 で C×A の複合判定に進む

成長株投資 固有のリスク(A 要素を運用する前に)

CAN-SLIM A の自動判定は「過去の成長持続性」のシグナルを取る仕組みで、未来の成長を保証するものではありません。成長株投資には次の固有リスクがあります:

  • 成長失速リスク: 過去5年 CAGR +30% でも、市場飽和や競合参入で翌年に成長率が一桁台に落ちるケースが頻繁にある
  • 高 PER リスク: 成長株は PER 30倍以上が常態。成長率が市場予想を下回ると株価が30-50%急落することも
  • 業種集中リスク: 成長株は IT・ヘルスケア・半導体に偏りがちで、ポートフォリオが業種集中する
  • 為替リスク(米国株): 円換算リターンが為替変動で大きくぶれる

応用編 #19 の業種集中度(HHI)と #16 の安定性指標を併用するのが、これらリスクへの実装上の対策です。

本記事では 4個のサンプルコードで、年間 EPS 取得から CAGR 計算、matplotlib 3パネル可視化、応用編 #15 → 発展編 #22 で構築した screen_parallel_v2 への A 要素統合まで自動判定を実装します。

結論:CAN-SLIM の A 要素は「年間 EPS の3-5年 CAGR ≥ +25% + 連続増益3年以上」を機械的に自動判定する。データ取得は J-Quants(通期決算 + 修正開示 dedup)と FMP(年次 income-statement)、CAGR は複利公式 (終期/始期)^(1/年数) - 1 で算出(赤字期間含むと CAUTION)、可視化は matplotlib 3パネル(EPS推移 / YoY / CAGR トレンドライン)。判定器は #22 の screen_parallel_v2 に A 要素として追加し、#24 で C×A 複合判定に発展させる。「絶対値より変化率、突発より連続性」原則を3-5年の長期軸に拡張するのが本記事の核心。

目次

CAN-SLIM A 要素の役割と「3-5年 CAGR ≥ +25%」閾値の意味(自動判定の手順)

CAN-SLIM の A は Annual EPS Growth、年間 EPS の長期成長率を見ます。オニール原典の中核:

  • 過去3-5年の年間 EPS が CAGR で +25% 以上の持続的成長
  • 年単位での 連続増益(最低3年連続)の事実
  • 直近1年だけの突発成長ではなく、本業の成長エンジンが安定稼働していること(突発より連続性)

「3-5年 CAGR +25%」閾値と PASS 通過率(運用前の期待値設定)

2026年5月時点の参考値(市場環境で変動):

  • 米国 S&P500: 過去5年 CAGR ≥ +25% を満たす銘柄は概ね3-7%。連続増益3年要件を加えると 2-4%
  • 東証プライム: 同条件で1-3%。日本企業の会計保守主義 + 為替影響で長期成長率が抑制傾向
  • 閾値 +15% に緩和: 通過率が +5〜+10pt(米国 8-15%、日本 5-10%)に上昇。日本市場運用なら緩和を検討する余地
  • GAAP / Non-GAAP の影響(重要): 米国テック成長株(Tesla 黎明期型)は GAAP EPS だと長期赤字 → CAGR 計算不能 → 全銘柄 CAUTION に倒れる構造的問題あり。Non-GAAP EPS への切替を検討

これらは運用開始前に「自分のスクリーニングで何件 PASS が出るか」の期待値を持つための参考。閾値は引数で柔軟に調整可能な設計にしています。

エンジニア的に言い換えると(製品世代別改善率の評価作法)

CAN-SLIM A は、製造業 DX で言う 「製品の世代別 CAGR 改善率」重なります:

  • 3-5年 = 製品3-5世代分: Gen1→Gen5 の改善トレンドを CAGR で見る
  • +25% の意味: 「世代をまたいだ持続的な性能向上」の最低ライン
  • 連続増益3年: 「3世代連続で改善」が本物の R&D 体制のシグナル
  • 絶対値より変化率、突発より連続性: #22 で確立した原則を5年スパンに拡張、本記事の自動判定を貫く設計原理

スニペット1:年間 EPS データの取得サンプルコード入門(J-Quants 通期 + FMP 年次)

#22 の fetch_quarterly_eps と並列で、年間 EPS 取得関数を実装します。J-Quants は TypeOfCurrentPeriod="FY"(通期決算)でフィルタ、FMP は period=annual パラメータで年次データを取得。修正開示の dedup と LocalCode の5桁正規化が公開ブロッカー級の重要ポイントです。

# fetch_annual_eps.py — 日米両市場の年間 EPS を統一フォーマットで取得(v2: dedup + LocalCode 5桁化)
# 動作環境: Python 3.11+ / pandas 2.x / requests 2.31+
import os
import requests
import pandas as pd
from typing import Literal

JQUANTS_BASE = "https://api.jquants.com/v1"
FMP_BASE = "https://financialmodelingprep.com/api/v3"

def fetch_annual_eps_jp(id_token: str, code: str) -> pd.DataFrame:
    """日本株の年間 EPS を取得(J-Quants /fins/statements、TypeOfCurrentPeriod=FY)

    返却カラム: ticker_normalized (5桁) / fiscal_year / period_end / eps / market='JP'

    v2 修正:
    - LocalCode を5桁0埋めで ticker_normalized に格納(応用編 #14 と整合)
    - 修正開示の重複は (LocalCode, fiscal_year) で keep="last" の dedup
    """
    headers = {"Authorization": f"Bearer {id_token}"}
    params = {"code": code}
    resp = requests.get(f"{JQUANTS_BASE}/fins/statements", headers=headers, params=params, timeout=60)
    resp.raise_for_status()
    rows = resp.json().get("statements", [])
    df = pd.DataFrame(rows)
    if df.empty:
        return pd.DataFrame(columns=["ticker_normalized", "fiscal_year", "period_end", "eps", "market"])
    annual = df[df["TypeOfCurrentPeriod"] == "FY"].copy()
    annual["period_end"] = pd.to_datetime(annual["CurrentPeriodEndDate"])
    annual["fiscal_year"] = annual["period_end"].dt.year
    # 応用編 #14 の ticker_normalized は5桁0埋め。LocalCode が4桁なら左0埋め、5桁なら維持
    annual["ticker_normalized"] = annual["LocalCode"].astype(str).str.zfill(5)
    # 修正開示の dedup: 同じ (LocalCode, fiscal_year) の最新行を残す
    annual = annual.sort_values(["ticker_normalized", "fiscal_year", "DisclosedDate"]).drop_duplicates(
        subset=["ticker_normalized", "fiscal_year"], keep="last"
    )
    return pd.DataFrame({
        "ticker_normalized": annual["ticker_normalized"],
        "fiscal_year": annual["fiscal_year"],
        "period_end": annual["period_end"],
        "eps": pd.to_numeric(annual["EarningsPerShare"], errors="coerce"),
        "market": "JP",
    })

def fetch_annual_eps_us(symbol: str, fmp_apikey: str, periods: int = 7) -> pd.DataFrame:
    """米国株の年間 EPS を取得(FMP /income-statement/{symbol} 年次版)

    v2 修正: calendarYear カラムは v3 stable で廃止傾向、period_end.dt.year に統一
    注意: FMP の eps は GAAP ベース。米国テック成長株は Non-GAAP EPS を別経路で取得を検討
    """
    url = f"{FMP_BASE}/income-statement/{symbol}"
    params = {"period": "annual", "limit": periods, "apikey": fmp_apikey}
    resp = requests.get(url, params=params, timeout=60)
    resp.raise_for_status()
    rows = resp.json()
    df = pd.DataFrame(rows)
    if df.empty:
        return pd.DataFrame(columns=["ticker_normalized", "fiscal_year", "period_end", "eps", "market"])
    df["period_end"] = pd.to_datetime(df["date"])
    df["fiscal_year"] = df["period_end"].dt.year  # v2: calendarYear からの脱却
    return pd.DataFrame({
        "ticker_normalized": symbol,  # 米国株はティッカーをそのまま正規化キーに
        "fiscal_year": df["fiscal_year"].astype(int),
        "period_end": df["period_end"],
        "eps": pd.to_numeric(df["eps"], errors="coerce"),
        "market": "US",
    })

def fetch_annual_eps(market: Literal["JP", "US"], identifier: str, **kwargs) -> pd.DataFrame:
    """統一インターフェイス(#22 fetch_quarterly_eps と同パターン、自動判定の入口)"""
    if market == "JP":
        id_token = kwargs.get("id_token") or os.environ["JQUANTS_ID_TOKEN"]
        return fetch_annual_eps_jp(id_token=id_token, code=identifier)
    elif market == "US":
        apikey = kwargs.get("fmp_apikey") or os.environ["FMP_APIKEY"]
        periods = kwargs.get("periods", 7)
        return fetch_annual_eps_us(symbol=identifier, fmp_apikey=apikey, periods=periods)
    else:
        raise ValueError(f"Unsupported market: {market}")

形式の罠:会計年度のズレ + 修正開示 + GAAP/Non-GAAP

  • 日本企業: 多くが3月期決算。Apple は9月期、Microsoft は6月期など個別差あり
  • 修正開示: J-Quants は同じ会計年度に複数行(初回開示 + 訂正版)が来ることがある。DisclosedDate 順に sort して keep=”last” で最新版採用
  • 5年分の取得: periods=7 で7期取得しておくと、欠損や上場初期の不安定期を1-2期分バッファできる
  • GAAP/Non-GAAP の使い分け: FMP の eps は GAAP。米国テック成長株は GAAP だと長期赤字 → 全銘柄が CAUTION に倒れるため、別経路で Non-GAAP を取得する選択肢を検討

スニペット2:CAGR と連続増益年数の計算サンプルコード(複利公式 + 連続性自動判定)

年間 EPS データから CAGR(複利年成長率)連続増益年数を計算します。「絶対値より変化率、突発より連続性」原則を CAGR + 連続増益で実装します。初年度の YoY 計算不能(NaN)が連続増益判定を破壊しないよう、is_up を nullable 化するのが v2 の重要修正です。

# compute_annual_cagr.py — 年間 EPS の CAGR と連続増益年数を計算 + DuckDB 保存
# 動作環境: 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_ANNUAL_EPS_WITH_CAGR = """
CREATE TABLE IF NOT EXISTS annual_eps_with_cagr (
    ticker_normalized          VARCHAR NOT NULL,
    fiscal_year                INTEGER NOT NULL,
    period_end                 DATE NOT NULL,
    market                     VARCHAR NOT NULL,
    eps                        DOUBLE,
    yoy_growth_pct             DOUBLE,
    cagr_3y_pct                DOUBLE,
    cagr_5y_pct                DOUBLE,
    consecutive_up_years       INTEGER,
    computed_at                TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
    PRIMARY KEY (ticker_normalized, fiscal_year)
);
"""

def _cagr_vec(start: pd.Series, end: pd.Series, years: int) -> pd.Series:
    """ベクトル化 CAGR 計算: (end/start)^(1/years) - 1。両端正値のみ計算(v2: apply 廃止)"""
    valid = (start > 0) & (end > 0) & start.notna() & end.notna()
    result = np.where(valid, (end / start) ** (1.0 / years) - 1.0, np.nan) * 100.0
    return pd.Series(result, index=start.index)

def compute_annual_cagr(df: pd.DataFrame) -> pd.DataFrame:
    """年間 EPS の YoY、3年/5年 CAGR、連続増益年数を計算(自動判定の中核)

    入力: ticker_normalized / fiscal_year / period_end / eps / market

    v2 修正:
    - apply(_cagr) を _cagr_vec のベクトル化演算に
    - 連続増益判定で初年度 NaN が「減益」と誤判定される罠を修正(is_up を nullable に)
    - 「絶対値より変化率、突発より連続性」原則を YoY + 連続増益年数で同時実装
    """
    df = df.sort_values(["ticker_normalized", "fiscal_year"]).copy()
    df["eps_prev"] = df.groupby("ticker_normalized")["eps"].shift(1)

    valid = df["eps_prev"].notna() & (df["eps_prev"] != 0)
    df["yoy_growth_pct"] = np.where(
        valid,
        ((df["eps"] - df["eps_prev"]) / df["eps_prev"].abs()) * 100,
        np.nan,
    )

    # ベクトル化 CAGR(H3: apply 廃止で性能 + 型整合改善)
    eps_3y_ago = df.groupby("ticker_normalized")["eps"].shift(3)
    eps_5y_ago = df.groupby("ticker_normalized")["eps"].shift(5)
    df["cagr_3y_pct"] = _cagr_vec(eps_3y_ago, df["eps"], 3)
    df["cagr_5y_pct"] = _cagr_vec(eps_5y_ago, df["eps"], 5)

    # 連続増益年数(C2 修正: 初年度 NaN を Int64 nullable で扱い、減益誤判定を回避)
    is_up = pd.Series(pd.NA, index=df.index, dtype="Int64")
    is_up[df["yoy_growth_pct"] > 0] = 1
    is_up[df["yoy_growth_pct"] <= 0] = 0
    df["_is_up"] = is_up
    # streak group: ticker 内で is_up が変化した境界を新グループに
    df["_streak_group"] = (df["_is_up"].fillna(-1) != df.groupby("ticker_normalized")["_is_up"].shift(1).fillna(-1)).cumsum()
    df["consecutive_up_years"] = df.groupby(["ticker_normalized", "_streak_group"]).cumcount() + 1
    df.loc[df["_is_up"] != 1, "consecutive_up_years"] = 0
    df = df.drop(columns=["_is_up", "_streak_group", "eps_prev"])
    return df

def upsert_annual_eps_with_cagr(df: pd.DataFrame) -> None:
    """年次 CAGR データを idempotent に保存"""
    DB_PATH.parent.mkdir(parents=True, exist_ok=True)
    target = df[
        ["ticker_normalized", "fiscal_year", "period_end", "market",
         "eps", "yoy_growth_pct", "cagr_3y_pct", "cagr_5y_pct", "consecutive_up_years"]
    ]
    with duckdb.connect(str(DB_PATH)) as conn:
        conn.execute(DDL_ANNUAL_EPS_WITH_CAGR)
        conn.register("annual_df", target)
        conn.execute("BEGIN")
        try:
            conn.execute("""
                DELETE FROM annual_eps_with_cagr
                WHERE (ticker_normalized, fiscal_year) IN
                      (SELECT ticker_normalized, fiscal_year FROM annual_df)
            """)
            conn.execute("""
                INSERT INTO annual_eps_with_cagr
                    (ticker_normalized, fiscal_year, period_end, market,
                     eps, yoy_growth_pct, cagr_3y_pct, cagr_5y_pct, consecutive_up_years)
                SELECT * FROM annual_df
            """)
            conn.execute("COMMIT")
        except Exception:
            conn.execute("ROLLBACK")
            raise
        finally:
            conn.unregister("annual_df")

欠損の罠:CAGR 計算で赤字期を含む場合 + 連続性判定の NaN ハンドリング

  • 始期または終期が負値(赤字): 複利公式が定義されないため、本コードでは NaN で返す
  • 初年度の YoY 計算不能: shift(1) で前年データなし → NaN。従来の (NaN > 0) は False に評価され、初年度が「減益」と同じ扱いになる罠。v2 では Int64 nullable で扱い、初年度を「未判定」状態にする
  • 業務上の意味: 赤字期を含む銘柄や上場初期銘柄は CAN-SLIM A では捉えられない。ターンアラウンド戦略は別ロジック(#29 ハイブリッド戦略で検討)

エンジニア的に言い換えると(絶対値より変化率、突発より連続性の長期版)

本コードは「絶対値より変化率」の原則を 絶対値分母 YoY + CAGR で、「突発より連続性」を 連続増益年数(min_consecutive_years=3)で実装します。製造業の品質管理での運用と同じ枠組みです。#22(四半期軸)と本記事(年次軸)で同じ原則を異なる時間スケールに適用しているのが、シリーズ全体を貫く設計原理です。

スニペット3:matplotlib 3パネル可視化サンプルコード(EPS推移 / YoY / CAGR トレンド)

1銘柄の長期成長持続性を1枚の図で評価できるように、matplotlib で 3パネル可視化を作ります。応用編 #16 の時系列分析(CV + 線形回帰)の発展形で、3-5年トレンドを直感的に判断するための作図パターンです。日本語フォントの rcParams 設定 + polyfit の点数ガード強化が v2 の改修ポイント。「絶対値より変化率、突発より連続性」を視覚で確認できる設計です。

# plot_annual_eps_trend.py — 年間 EPS の長期トレンドを 3パネル可視化(v2: フォント + polyfit ガード強化)
# 動作環境: Python 3.11+ / matplotlib 3.9+ / pandas 2.x / numpy
import matplotlib
import matplotlib.pyplot as plt
import pandas as pd
import numpy as np
from pathlib import Path

# v2 H5 修正: 日本語フォントの rcParams 設定(macOS / Windows / Linux 環境対応)
# 環境に応じて存在するフォントを優先採用
matplotlib.rcParams["font.family"] = ["Hiragino Sans", "Yu Gothic", "Meiryo", "Noto Sans CJK JP", "DejaVu Sans"]
matplotlib.rcParams["axes.unicode_minus"] = False

def _r_squared(x: np.ndarray, y: np.ndarray, coef: np.ndarray) -> float:
    """線形回帰の決定係数 R²(H4: トレンドラインの信頼度を示す)"""
    y_pred = np.polyval(coef, x)
    ss_res = np.sum((y - y_pred) ** 2)
    ss_tot = np.sum((y - y.mean()) ** 2)
    return 1.0 - ss_res / ss_tot if ss_tot > 0 else 0.0

def plot_annual_eps_trend(df: pd.DataFrame, ticker: str, out_dir: Path = Path("data/plots")) -> Path:
    """1銘柄の年間 EPS トレンドを 3パネル可視化(自動判定結果を視覚化)

    Panel 1 (上): 年間 EPS の絶対値推移(棒グラフ + トレンドライン + R²)
    Panel 2 (中): YoY 成長率(折れ線、+25% / +10% の閾値ライン)
    Panel 3 (下): 3年 CAGR / 5年 CAGR の推移(折れ線、+25% 閾値ライン)

    出力: data/plots/{ticker}_annual_eps_trend.png
    """
    out_dir.mkdir(parents=True, exist_ok=True)
    sub = df[df["ticker_normalized"] == ticker].sort_values("fiscal_year").copy()
    if sub.empty:
        raise ValueError(f"No data for ticker {ticker}")

    fig, axes = plt.subplots(3, 1, figsize=(10, 9), sharex=True)

    # Panel 1: EPS 絶対値推移
    axes[0].bar(sub["fiscal_year"], sub["eps"], color="#805ad5", alpha=0.7, label="Annual EPS")
    x = sub["fiscal_year"].values.astype(float)
    y = sub["eps"].values.astype(float)
    mask = ~np.isnan(y)
    if mask.sum() >= 3:  # v2 H4: 2点 → 3点以上に強化(統計的意味の確保)
        coef = np.polyfit(x[mask], y[mask], 1)
        r2 = _r_squared(x[mask], y[mask], coef)
        axes[0].plot(x, np.polyval(coef, x), "r--",
                     label=f"Trend (slope={coef[0]:.2f}, R²={r2:.2f})")
    axes[0].set_ylabel("EPS")
    axes[0].set_title(f"{ticker} — Annual EPS Trend (3-5Y CAGR-based)")
    axes[0].legend()
    axes[0].grid(alpha=0.3)

    # Panel 2: YoY 成長率(変化率の視覚化)
    axes[1].plot(sub["fiscal_year"], sub["yoy_growth_pct"], "o-", color="#3182ce", label="YoY (%)")
    axes[1].axhline(25, color="green", linestyle="--", alpha=0.5, label="PASS (+25%)")
    axes[1].axhline(10, color="orange", linestyle="--", alpha=0.5, label="CAUTION (+10%)")
    axes[1].axhline(0, color="gray", linestyle="-", alpha=0.3)
    axes[1].set_ylabel("YoY Growth (%)")
    axes[1].legend()
    axes[1].grid(alpha=0.3)

    # Panel 3: CAGR 推移(連続性の視覚化)
    axes[2].plot(sub["fiscal_year"], sub["cagr_3y_pct"], "s-", color="#38a169", label="3Y CAGR (%)")
    axes[2].plot(sub["fiscal_year"], sub["cagr_5y_pct"], "^-", color="#d69e2e", label="5Y CAGR (%)")
    axes[2].axhline(25, color="green", linestyle="--", alpha=0.5, label="PASS (+25%)")
    axes[2].axhline(0, color="gray", linestyle="-", alpha=0.3)
    axes[2].set_xlabel("Fiscal Year")
    axes[2].set_ylabel("CAGR (%)")
    axes[2].legend()
    axes[2].grid(alpha=0.3)

    fig.tight_layout()
    out_path = out_dir / f"{ticker}_annual_eps_trend.png"
    fig.savefig(out_path, dpi=120)
    plt.close(fig)
    return out_path

可視化の罠:日本語豆腐化 + polyfit 過小サンプル

  • 日本語フォント未設定: 読者がラベルを日本語化すると豆腐化(□表示)。rcParams["font.family"] でプラットフォーム別に主要フォントを優先指定
  • polyfit 2点だけで線形回帰: 統計的に意味なし(必ず100% フィット)。3点以上必須に + R² も併記
  • Panel 1 / Panel 3 の読み方: Panel 1 は絶対値(変化が見えにくい)、Panel 3 が変化率の累積(成長加速度の視覚化)。「絶対値より変化率」原則の視覚化

エンジニア的に言い換えると(生産技術の3層トレンド分析の作法)

本コードは、製造業 DX で言う 「製品性能の3層トレンド分析」の作法と共通しています。Panel 1 が「絶対性能の経年推移」、Panel 2 が「世代間の改善率(=変化率)」、Panel 3 が「中長期トレンドの加速 / 減速(=連続性)」。3層を1枚にまとめると量産化判定者が一目で総合判断でき、「絶対値より変化率、突発より連続性」原則を視覚で確認できます。

スニペット4:A 要素判定器を screen_parallel_v2 に統合する手順(自動判定パイプライン完成)

#22 で構築した screen_parallel_v2 に A 要素判定を追加し、1回の Runner 起動で「高配当 / CAN-SLIM C / CAN-SLIM A」3判定が自動で出る形に拡張します。次回 #24 で C×A 複合判定(両方 PASS の銘柄を成長株候補として抽出)に発展させる土台です。

# can_slim_a_judge.py — CAN-SLIM A 要素の判定器(CAGR + 連続増益年数の自動判定、v2: TypedDict 整理)
# 動作環境: Python 3.11+ / pandas 2.x
from typing import Literal, TypedDict, NotRequired

Verdict = Literal["PASS", "CAUTION", "FAIL"]

# v2 H6 修正: total=False を撤去、NotRequired のみで欠損許容を表現
class AElementMetrics(TypedDict):
    cagr_3y_pct: NotRequired[float | None]
    cagr_5y_pct: NotRequired[float | None]
    consecutive_up_years: NotRequired[int | None]

def judge_a_element(metrics: AElementMetrics,
                     threshold_pass: float = 25.0,
                     threshold_caution: float = 15.0,
                     min_consecutive_years: int = 3,
                     prefer_5y: bool = True) -> Verdict:
    """CAN-SLIM A 要素を3段階で自動判定(突発より連続性原則)

    threshold_pass=25.0 (原典準拠), threshold_caution=15.0
    min_consecutive_years=3 (原典準拠の連続増益最低ライン)
    prefer_5y=True なら5年 CAGR 優先、未取得なら3年 CAGR にフォールバック
    """
    cagr = metrics.get("cagr_5y_pct") if prefer_5y else metrics.get("cagr_3y_pct")
    if cagr is None:
        cagr = metrics.get("cagr_3y_pct")
    if cagr is None:
        return "CAUTION"

    if cagr < threshold_caution:
        return "FAIL"
    if cagr < threshold_pass:
        return "CAUTION"

    # CAGR PASS の上で連続性チェック(突発より連続性)
    consec = metrics.get("consecutive_up_years") or 0
    if consec < min_consecutive_years:
        return "CAUTION"
    return "PASS"

# screen_parallel_v3.py への統合(差分のみ、自動判定パイプラインの拡張)
# from can_slim_a_judge import judge_a_element
#
# def judge_one_ticker(metrics_with_eps: dict) -> dict:
#     ...(既存の高配当 + C 判定)
#     a_result = judge_a_element({
#         "cagr_3y_pct": metrics_with_eps.get("cagr_3y_pct"),
#         "cagr_5y_pct": metrics_with_eps.get("cagr_5y_pct"),
#         "consecutive_up_years": metrics_with_eps.get("consecutive_up_years"),
#     })
#     return {
#         ...,
#         "can_slim_a_verdict": a_result,
#     }
#
# SQL 拡張(直近年の CAGR を ROW_NUMBER で取得、#22 と同パターン):
# LEFT JOIN (
#     SELECT ticker_normalized, cagr_3y_pct, cagr_5y_pct, consecutive_up_years
#     FROM (
#         SELECT *, ROW_NUMBER() OVER (PARTITION BY ticker_normalized
#                                       ORDER BY fiscal_year DESC) AS rn
#         FROM annual_eps_with_cagr
#     ) ranked WHERE rn = 1
# ) a ON s.ticker_normalized = a.ticker_normalized

エンジニア的に言い換えると(複合検査ライン + 連続性原則の継承)

本コードは、#22 で構築した「高配当 + CAN-SLIM C」検査ラインに「CAN-SLIM A」検査機を追加する拡張です。応用編で確立した判定器パターン(direction、threshold、欠損 → CAUTION)を踏襲し、判定対象を「YoY → CAGR」に入れ替えただけ。「絶対値より変化率、突発より連続性」原則は min_consecutive_years=3 の引数で運用に落とし込み、CAN-SLIM の他要素(N/S/L/I/M)も同パターンで自動判定可能です。

設計判断の記録:A 要素実装の3判断 + 応用編〜発展編 #23 の俯瞰表(30件)

判断1:CAGR を3年と5年どちらを優先するか

  • 採用理由: 5年 CAGR 優先 + 3年 CAGR フォールバック。代替案との比較:
    • 5年のみ: 新規上場銘柄(5期未満)は全て CAUTION 化、抽出機会損失
    • 3年のみ: CAN-SLIM 原典の長期持続性意図から外れる
    • 採用:5年優先 + 3年フォールバック: 原典準拠と機会損失回避を両立
  • 採用したことで失うもの: 直近の成長加速度(5年だと過去の安定期も平均化される)
  • トリガー条件: 直近の急成長を強調したい運用 → prefer_5y=False で3年優先に切替
  • 残るメリット: 長期持続性(CAN-SLIM の本来意図)に忠実な自動判定
  • 本業 DX 同型対応: 製造業の長期 R&D 評価でも「直近3世代より過去5世代の改善率」を重視するのが定石

判断2:赤字期を含む銘柄を CAGR 計算でどう扱うか

  • 採用理由: 始期・終期どちらかが負値以下なら CAGR を None で返し、判定器で CAUTION 扱い
  • 採用したことで失うもの: ターンアラウンド銘柄 + 米国テック成長株(GAAP で長期赤字)の機械抽出機会
  • トリガー条件: ターンアラウンド戦略中心 → 別 Runner で「赤字→黒字転換 + 直近3年連続黒字」判定。米国テック株中心 → Non-GAAP EPS への切替
  • 残るメリット: CAN-SLIM A の本来意図(長期成長中の銘柄)に絞った精度確保
  • 製造業の品質管理での運用: 「失敗世代を含む製品ライン」と「連続成功ライン」は別評価枠組みで管理するのが定石

判断3:3パネル可視化を1銘柄ずつ生成するか、PASS 銘柄まとめて生成するか

  • 採用理由: 判定 PASS 銘柄のみを後段で可視化(screen_parallel の判定結果からフィルタ)
  • 採用したことで失うもの: FAIL 銘柄の可視化(理由分析がしにくい)
  • トリガー条件: 「なぜ FAIL だったか」を個別調査したい → 銘柄指定で個別実行可能なインターフェイス
  • 残るメリット: 全銘柄可視化の処理時間(数千銘柄 × 数秒/枚 で数時間)を回避
  • 生産技術の作法: 「全プロトタイプ可視化」より「量産候補のみ詳細レポート」が標準

応用編 #13〜#21 + 発展編 #22〜#23 の主要設計判断 俯瞰表(30件)

応用編で確立した俯瞰表に、発展編 #22 の2件 + 本記事 #23 の3件を追加して累計30件に拡張します:

記事主要判断採用
#13データソース選定J-Quants + EDINET(公式 API 優先)
#14DB / アーキテクチャDuckDB + ELT パターン + マスタ駆動
#14業種別補正設計industry_indicator_map テーブル駆動 + direction
#14正規化キーticker_normalized 5桁0埋め
#15並列化技術multiprocessing(initializer パターン)
#15通知メディアLINE Messaging API
#15スケジューラcron → GitHub Actions
#16連続増配判定連続非減少(同額据え置きOK)
#16EPS 安定性指標変動係数 CV + 線形回帰
#16新規上場銘柄の扱い5期未満は CAUTION 強制
#17異常値検知の組合せIQR + Z + SPC + WER の3-4層フィルタ
#17市場全体ショック対応絶対値 + #20 で TOPIX 相対値併用
#18業種粒度10業種 → #20 で25業種
#18業種別主指標数1業種1主指標
#18マスタ更新サイクル四半期レビュー
#18テーブル PK 設計複合PK / 単列PK の使い分け
#19集中度指標HHI(投資視点閾値 2000/3000)
#19相関分析期間過去24ヶ月(月次) + 直近6ヶ月併用
#19FMEA RPN 重み等倍積(S × O × D)
#19ポートフォリオテーブル個別銘柄評価とは別テーブル
#20master_runner 方式NotImplementedError による委譲
#20業種カバレッジ25業種で東証時価総額の概ね9割
#21成長株フレームワークCAN-SLIM
#21パイプライン再利用応用編流用、判定層のみ拡張
#21市場カバレッジ日米両市場(FMP + SEC EDGAR)
#22YoY 計算ロジック絶対値分母
#22連続性判定min_consecutive=2
#23CAGR 期間優先順位5年優先 + 3年フォールバック
#23赤字期 CAGR 扱いNone で返し CAUTION
#23可視化粒度PASS 銘柄のみ詳細可視化

本業の話:製品の世代別 CAGR 改善率で量産投資の意思決定をした経験

筆者が研究開発部門で製品の世代別性能トレンド分析を担当していたとき、ベテランから次の指導を受けました:

  • 直近1世代の改善率だけで量産投資を決めるな。過去5世代分の CAGR 改善率を見ろ。CAGR が +25% 以上なら『成長エンジン稼働中』、+15-25% なら『安定成長』、+15% 未満なら『成熟』」
  • 連続改善年数を必ずチェック。CAGR が高くても、5年中2年が大幅赤字なら本物の R&D 体制とは言えない(絶対値より変化率、突発より連続性)」
  • 3パネル可視化で量産化判定者に提示。Panel 1 で絶対性能、Panel 2 で世代間改善率、Panel 3 で中長期トレンドを一目で判断できるようにする」

具体的な業務インパクトと4-5年スパンのマイルストーン:

  • 1年目(指摘): 初稿の量産化判定では「直近世代の改善率」のみを基準にしていた。ベテラン指摘で 5年 CAGR + 連続増益年数の2軸に切替
  • 2年目(改修): 過去10年分の世代別データを再構築し、CAGR 評価を実装。量産化判定の精度が約4割向上(測定方法: 量産後12ヶ月の継続改善率を「改良判定で量産化された製品 vs 旧基準で量産化されていたであろう製品」で比較、サンプル 過去2年分の量産化案件 約20件 のシミュレーション再評価)
  • 3年目(運用): 3パネル可視化レポートが量産化会議の標準資料に。「絶対値より変化率、突発より連続性」原則が部内で浸透
  • 4-5年目(市場投入): 採用された世代の市場ライフサイクルが平均1.5倍に延長(測定方法: 後継機投入までの期間を新基準採用前後の量産化製品で比較、サンプル 新基準採用後3年分 約8件 を旧基準時代の同期間比較)。連続改善年数で除外した「単発で改善した世代」がもし量産化されていたら、ライフサイクル短縮で投資回収できなかったケースが事後分析で複数判明

本記事の CAN-SLIM A 判定器は、この本業の経験と同じ枠組みです。CAGR + 連続増益年数 + 3パネル可視化の組合せが、長期成長の本質を見極める評価設計の核心で、「絶対値より変化率、突発より連続性」原則を5年スパンに拡張した自動判定です。

逆方向の転移:投資の CAGR 計算が本業の長期トレンド分析を強化

本記事のベクトル化 CAGR 計算と、初年度 NaN を nullable Int64 で扱う連続性判定パターンを整理した結果、本業の世代別性能トレンド分析でも同等の改善が期待されます。本業ではこれまで「世代をまたぐ初年度のデータ欠損」を「マイナス成長」と扱ってきましたが、本記事の nullable パターンを逆輸入することで初期世代の評価精度向上の見込みです。応用編で確立した双方向の知識循環が発展編でも続いています。

まとめ:CAN-SLIM A は CAGR ≥ +25% + 連続増益3年で自動判定、#22 → #23 の進化3点

  • CAN-SLIM A 要素は「年間 EPS の3-5年 CAGR ≥ +25% + 連続増益3年以上」で機械的に自動判定。J-Quants(通期決算 + 修正開示 dedup + LocalCode 5桁正規化)と FMP(年次 income-statement、calendarYear 廃止対応)で年間 EPS を取得し、ベクトル化 CAGR 計算で複利成長率を算出。赤字期含む場合は CAUTION 扱い、初年度 NaN は nullable Int64 で誤判定回避。matplotlib 3パネル可視化(日本語フォント rcParams + polyfit ≥3点ガード)で長期トレンドを一目で判断。「絶対値より変化率、突発より連続性」原則を5年スパンに拡張するのが本記事の核心
  • #22 screen_parallel_v2 への統合で、1 Runner 起動で「高配当 / CAN-SLIM C / CAN-SLIM A」3判定が自動で出る形に拡張。応用編で確立した判定器パターン・initializer パターン・DuckDB トランザクション境界を継承
  • #22 → #23 の構造的進化3点: (1) 四半期 → 年次への時間軸拡張、(2) YoY 単発 → CAGR 複利、(3) 数値判定のみ → matplotlib 3パネル可視化追加。「絶対値より変化率、突発より連続性」原則は同一で、時間スケールと表現形式が拡張

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

  1. 本記事スニペット1〜2で、自分の興味のある日本株1銘柄と米国株1銘柄の過去5年の年間 EPS と CAGR + 連続増益年数を計算してみる。+25% 基準を満たすか、初年度 NaN が連続増益判定を破壊しないか確認
  2. スニペット3 の plot_annual_eps_trend3パネル可視化を生成し、3-5年トレンドの直感的判断ができるかチェック。日本語フォント設定が機能しているか合わせて確認
  3. スニペット4 で screen_parallel_v2 に A 要素を統合し、「C のみ PASS」「A のみ PASS」「C×A 両方 PASS」の銘柄数を比較。米国 S&P500 で C×A 両方 PASS は概ね 1-3%、東証プライムで 0.5-1% 程度を想定すると、自分の運用での期待値が掴める

次回予告:CAN-SLIM C×A 複合自動判定で本物の成長株を抽出

次回(記事#24)では、本記事の A(長期成長持続性)と #22 の C(直近の急成長)を複合自動判定し、「直近加速 + 長期持続」両方を満たす本物の成長株を抽出する手順を扱います。さらに #29 のハイブリッド戦略(高配当 × 成長 × 業種分散)への接続が見えてきます。

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

本記事は 発展編(記事#21〜#30)の第3回 です。

  • 基礎編(#01〜#10): 完了
  • 応用編(#11〜#20): 完了 ✅
  • 発展編(#21〜#30): 進行中

発展編 ロードマップ:

  • 導入#21 なぜ成長株か
  • CAN-SLIM 実装#22 C 当期EPS▶ イマココ #23 A 年間EPS成長(本記事) → #24 C×A 複合
  • 事業構造:#25 製品アーキテクト視点
  • 機械学習・統計:#26 PCA → #27 エントロピー → #28 NLP(N要素)
  • ハイブリッド + 総括:#29 配当 × 成長 戦略 → #30 シリーズ総括

前回 #22 CAN-SLIM C本記事 #23 CAN-SLIM A | 次回 #24(C×A 複合自動判定、公開予定)

関連記事(応用編から): #15 全銘柄スクリーニング自動化#16 配当推移の安定性#19 業種分散 FMEA

免責事項(再掲)

本記事は投資助言を目的としたものではなく、技術・分析手法の紹介です。CAN-SLIM A 要素の閾値(CAGR +25%、連続増益3年)は教育目的であり、特定の銘柄・金融商品の売買を推奨するものではありません。投資判断はご自身の責任で行ってください。J-Quants API・FMP の利用規約は変更される可能性があるため、実装時は各サービスの公式ドキュメント・利用規約を必ず確認してください。本記事中に J-Quants/FMP から取得した実データは掲載していません。

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

この記事を書いた人

コメント

コメントする

CAPTCHA


目次