Pythonで高配当株スクリーニング|yfinance利回り実装

Python実装・コード

※本記事には広告・アフィリエイトリンクが含まれます。収益はサイト運営・検証費用に充てています。掲載コードは学習・検証目的です。

先に結論です。yfinanceのdividendYieldをそのまま100倍する記事のコードは、いま動かすと利回り350%のような値を返します。このフィールドは以前は比率(0.035)でしたが、現行のyfinanceはパーセント(3.5)で返します。バージョン間で単位が変わったため、ネット上のサンプルは大半が壊れています。

もう一つ。利回りが高い銘柄を上から並べるスクリーニングは、株価が下がったから利回りが高く見えている銘柄を優先的に拾います。いわゆる罠配当です。数字は正しくても、選び方が間違っています。

本記事では、(1) 単位問題を回避して利回りを自前計算する、(2) 特別配当を除いたTTM配当で計算する、(3) 減配履歴と配当性向と株価下落率で罠配当を機械的に落とす、という順で実装します。コードはそのままコピーして動きます。

📘 外部参考yfinance(GitHub)yfinance(PyPI)

まず単位を確認する

いきなりスクリーニングを書く前に、自分の環境でdividendYieldが何を返すかを確かめます。ここを飛ばすと、以降の全部の数字がズレます。

import yfinance as yf

t = yf.Ticker("8306.T")   # 三菱UFJフィナンシャル・グループ
info = t.info

print("yfinance version:", yf.__version__)
print("dividendYield          :", info.get("dividendYield"))
print("trailingAnnualDivYield :", info.get("trailingAnnualDividendYield"))
print("dividendRate           :", info.get("dividendRate"))
print("payoutRatio            :", info.get("payoutRatio"))
print("currentPrice           :", info.get("currentPrice") or info.get("previousClose"))

dividendYield3.4のように1を超える値ならパーセント、0.034なら比率です。trailingAnnualDividendYieldは比率のまま据え置かれていることが多く、2つのフィールドで単位が違うという状況が起こります。

フィールド単位信頼度備考
dividendYield現行はパーセントバージョンで比率だったことがある
trailingAnnualDividendYield比率Noneが返る銘柄が多い
dividendRate通貨(円)予想ベースのことがある
ticker.dividends通貨(円)の履歴実際の支払実績。自前計算の材料

結論として、infoの利回りは参考値にとどめ、ticker.dividendsから自分で計算するのが安全です。単位が変わっても壊れません。

TTM配当から利回りを自前計算する

直近12か月に実際に支払われた配当を合計し、現在株価で割ります。分子と分母の通貨が揃うので、ADRなどでもズレにくくなります。

import numpy as np
import pandas as pd
import yfinance as yf

def ttm_dividend(t: yf.Ticker) -> float:
    div = t.dividends
    if div is None or len(div) == 0:
        return 0.0
    idx = pd.to_datetime(div.index).tz_localize(None)
    s = pd.Series(div.values, index=idx).sort_index()
    cutoff = s.index.max() - pd.DateOffset(months=12)
    return float(s[s.index > cutoff].sum())

def latest_price(t: yf.Ticker) -> float:
    info = t.info
    for key in ("currentPrice", "regularMarketPrice", "previousClose"):
        v = info.get(key)
        if v:
            return float(v)
    hist = t.history(period="5d", auto_adjust=False)
    if hist.empty:
        return float("nan")
    return float(hist["Close"].dropna().iloc[-1])

def dividend_yield_pct(code: str) -> dict:
    t = yf.Ticker(code)
    px = latest_price(t)
    ttm = ttm_dividend(t)
    y = (ttm / px * 100) if px and not np.isnan(px) else float("nan")
    return {
        "code": code,
        "price": round(px, 1),
        "ttm_dividend": round(ttm, 2),
        "yield_pct": round(y, 2),
    }

print(dividend_yield_pct("8306.T"))

auto_adjust=Falseで価格を取るのがポイントです。調整済み価格は過去の配当を織り込んで遡って下方修正されるため、「現在の株価」としては未調整の終値が適切です。利回りの分母には、実際に今支払う金額を使います。

特別配当・記念配当を除く

周年記念や資産売却益による特別配当が入っていると、TTM利回りが一時的に跳ね上がります。翌年は元に戻るので、これを高配当と判定すると外します。中央値の何倍かで機械的に弾きます。

def ttm_dividend_clean(t: yf.Ticker, spike_ratio: float = 2.5) -> dict:
    div = t.dividends
    if div is None or len(div) == 0:
        return {"ttm_raw": 0.0, "ttm_clean": 0.0, "spike_removed": 0}
    idx = pd.to_datetime(div.index).tz_localize(None)
    s = pd.Series(div.values, index=idx).sort_index()

    # 過去3年の1回あたり配当の中央値を基準にする
    base_window = s[s.index > s.index.max() - pd.DateOffset(years=3)]
    median = float(base_window.median()) if len(base_window) else 0.0

    ttm = s[s.index > s.index.max() - pd.DateOffset(months=12)]
    if median > 0:
        normal = ttm[ttm <= median * spike_ratio]
    else:
        normal = ttm
    return {
        "ttm_raw": round(float(ttm.sum()), 2),
        "ttm_clean": round(float(normal.sum()), 2),
        "spike_removed": int(len(ttm) - len(normal)),
    }

spike_removedが1以上なら、その銘柄は一時的な上乗せがあったということです。ttm_rawttm_cleanの両方を表に残し、どちらで判断するかを自分で選べるようにします。

減配履歴を年ベースで見る

利回りより重要なのが、配当を維持できているかです。年間合計に直して、直近5年の推移と減配回数を数えます。

def dividend_history(code: str, years: int = 6) -> dict:
    t = yf.Ticker(code)
    div = t.dividends
    if div is None or len(div) == 0:
        return {"code": code, "years": 0, "cuts": None, "trend": None}
    idx = pd.to_datetime(div.index).tz_localize(None)
    s = pd.Series(div.values, index=idx).sort_index()
    yearly = s.groupby(s.index.year).sum()
    yearly = yearly.tail(years)

    # 前年より減った回数(最終年は期中で不完全になりやすいので除外)
    solid = yearly.iloc[:-1] if len(yearly) > 2 else yearly
    cuts = int((solid.diff() < -0.01).sum())
    trend = None
    if len(solid) >= 3:
        x = np.arange(len(solid))
        slope = np.polyfit(x, solid.values, 1)[0]
        trend = "増配" if slope > 0 else "減配" if slope < 0 else "横ばい"
    return {
        "code": code,
        "yearly": {int(k): round(float(v), 1) for k, v in yearly.items()},
        "cuts": cuts,
        "trend": trend,
    }

print(dividend_history("8306.T"))

最終年を除外しているのは、期中だと支払回数が足りず、必ず「減配」に見えてしまうためです。この処理を入れていないスクリーニングは、毎年前半に大量の偽減配を出します。

罠配当を落とすスクリーニング

ここまでの部品を組み合わせます。判定は利回りだけでなく、配当性向・株価下落率・減配回数の4条件です。

import time

CANDIDATES = ["8306.T", "8316.T", "8411.T", "9432.T", "9433.T", "5020.T", "2914.T"]

def price_drawdown_1y(t: yf.Ticker) -> float:
    h = t.history(period="1y", auto_adjust=False)
    if h.empty:
        return float("nan")
    c = h["Close"].dropna()
    return float(c.iloc[-1] / c.max() - 1)

def screen(codes: list[str], min_yield: float = 3.0, sleep: float = 1.0) -> pd.DataFrame:
    rows = []
    for code in codes:
        try:
            t = yf.Ticker(code)
            info = t.info
            px = latest_price(t)
            clean = ttm_dividend_clean(t)
            hist = dividend_history(code)
            payout = info.get("payoutRatio")
            payout_pct = round(payout * 100, 1) if payout else None
            dd = price_drawdown_1y(t)
            y = round(clean["ttm_clean"] / px * 100, 2) if px else None

            rows.append({
                "code": code,
                "name": (info.get("longName") or info.get("shortName") or "")[:16],
                "price": round(px, 1),
                "yield_pct": y,
                "payout_pct": payout_pct,
                "dd_1y_pct": round(dd * 100, 1) if not np.isnan(dd) else None,
                "cuts_5y": hist["cuts"],
                "trend": hist["trend"],
                "special": clean["spike_removed"],
            })
        except Exception as e:
            print(f"{code} 取得失敗: {e}")
        time.sleep(sleep)

    df = pd.DataFrame(rows)
    if df.empty:
        return df
    df["flag"] = ""
    df.loc[df["payout_pct"].fillna(0) > 100, "flag"] += "配当性向超過 "
    df.loc[df["dd_1y_pct"].fillna(0) < -30, "flag"] += "株価急落 "
    df.loc[df["cuts_5y"].fillna(0) >= 2, "flag"] += "減配常習 "
    return df[df["yield_pct"].fillna(0) >= min_yield].sort_values("yield_pct", ascending=False)

result = screen(CANDIDATES)
print(result.to_string(index=False))

flagが空の銘柄だけが、いちおう検討に値する候補です。フラグ付きを自動で除外せず、列として残すのは、理由を目で確認するためです。株価急落は悪材料の反映かもしれませんし、市場全体の下げかもしれません。

フラグ判定条件意味
配当性向超過payoutRatio > 100%利益を超えて配当。継続性に疑問
株価急落1年高値から-30%超利回りが高いのは分母が下がったから
減配常習直近5年で2回以上減配方針として維持されていない
special > 0特別配当を除外した来期は利回りが下がる可能性

レート制限とデータ欠損への対処

Ticker.infoは1銘柄あたり数秒かかり、連続実行すると429(Too Many Requests)が返ります。上のコードでtime.sleep(1)を入れているのはそのためです。数百銘柄を回すなら、キャッシュを挟みます。

import json
import pathlib
from datetime import date

CACHE = pathlib.Path("div_cache")
CACHE.mkdir(exist_ok=True)

def cached_screen(code: str) -> dict:
    f = CACHE / f"{code}_{date.today().isoformat()}.json"
    if f.exists():
        return json.loads(f.read_text(encoding="utf-8"))
    data = dividend_yield_pct(code)
    f.write_text(json.dumps(data, ensure_ascii=False), encoding="utf-8")
    return data

日付をファイル名に入れているので、同じ日に何度実行してもAPIは1回だけです。翌日は自動で取り直します。無配銘柄や新規上場銘柄はdividendsが空になるため、len(div) == 0の分岐を必ず入れてください。

結果をExcelで開ける形で保存する

from datetime import datetime

def save_csv(df: pd.DataFrame, prefix: str = "high_dividend") -> str:
    name = f"{prefix}_{datetime.now():%Y%m%d}.csv"
    df.to_csv(name, index=False, encoding="utf-8-sig")
    return name

utf-8-sigにするとExcelで日本語が文字化けしません。証券会社の約定CSVを読み込む側の話はSBI証券のCSVをPythonで読み込む記事にまとめています。

スクリーニング結果を鵜呑みにしない

  • 権利確定日の直前は避ける。権利落ちで配当相当分だけ株価は下がります。利回りを取りに行っても、価格で戻されます。
  • 予想配当と実績配当は違う。dividendRateは予想を含むことがあります。本記事のTTMは実績です。
  • 業種の偏り。高利回りで並べると銀行・商社・通信に集中します。相関が高くなるので、分散の観点では複数銘柄の相関分析を併用してください。
  • Yahoo Financeのデータ精度。日本株は配当情報が欠けることがあります。最終判断は企業のIR資料で確認します。

まとめ:利回りは自分で計算する

yfinanceのdividendYieldは単位が変わった実績があり、100倍するサンプルコードは現行版で壊れます。ticker.dividendsのTTM合計を未調整の現在株価で割る。この自前計算なら、ライブラリの仕様変更に振り回されません。

そのうえで、利回りだけで並べないでください。配当性向100%超、1年高値から30%超の下落、5年で2回以上の減配。この3つのフラグを列に持たせると、上位に並ぶ銘柄の見え方が変わります。

データ取得手段そのものを比較したい場合はPython株価データ取得ツール徹底比較、指標計算の型を固めたい場合は株分析を学ぶ本3選を参照してください。


【投資免責事項】本記事で紹介するコードおよび分析結果は情報提供を目的としたものであり、特定の銘柄への投資を推奨するものではありません。投資判断はご自身の責任で行ってください。

タイトルとURLをコピーしました