INVESTMENT #10

配当利回りスクリーナーを作る(pandas × CSV)

Python経験者向けの投資分析シリーズ第10回。架空25銘柄のCSVを、配当利回り・PER・PBRで絞り込むCLIツールを作ります。NaNは比較すると必ずFalse割り算の丸めで境界の行が落ちるpandas 3.0の連鎖代入は静かに失敗する——絞り込みが「効いているつもり」になる場所を実行結果つきで。

🎯 対象: Python経験者・投資分析 📦 Python 3.12.10 / pandas 3.0.3 / matplotlib 3.11.1 / yfinance 1.4.1 🧪 2026-09-04 に実行して確認 ⏱️ 読了: 約24分

1. この記事のゴールと連載の位置づけ

本記事は「Python経験者が、自分の投資ポートフォリオを管理・可視化するアプリを作る」連載の第10回です。第9回の最後に「次は配当利回りでの絞り込み」と書いた宿題がここ。CSVに並んだ銘柄データから、配当利回り・PER・PBRの条件に合う行だけを機械的に抜き出すCLIツールを作ります。

コマンドラインの実行結果。適用した条件として配当利回り3.5%以上とPBR1.0倍以下が表示され、条件を満たした5件・満たさなかった17件・判定できなかった3件・合計25件という内訳が並び、その下にSMPL-22からSMPL-23までの5行が表として出力されている(すべて架空のサンプルデータ)
▲ 完成形。適用した条件と件数の内訳を必ず刷る(銘柄コード・社名・株価はすべて架空。3.5%・1.0倍は動作確認用の仮の値)
⚠️
先に決めておくこと:スクリーニングは投資判断ではない

スクリーニングは、条件に合う行を機械的に絞る作業です。本記事が作るのは絞り込みの道具で、どの条件が良いかは扱いません。抽出された行が良い投資対象であることを意味しません。当サイトは投資助言業(金融商品取引業)の登録をしておらず、個別銘柄の推奨や売買タイミングの助言は行いません。「利回り3.5%以上」のような数値はすべて動作確認のための仮の値です。

🎯
この回のゴール

架空25行のCSVを読み、配当利回り・PER・PBRを計算して条件に合う行を抜き出し、件数の内訳と一緒に標準出力とCSVに書き出すところまで。本文は要点のコードだけで、完成版は dividend_screener.py(270行)にあります。書き方だけ急ぐなら4章→8章で足ります。5〜7章は、そこで踏む地雷の先回りです。

第8回・第9回との住み分け

ここまでの2回はすでに持っている株が題材でした。第8回で配当の入金を記録する側を作り、第9回でそれをカレンダーにした。本記事だけがまだ持っていない候補を扱います。第8回で決めた金額を銭単位の整数で持つ方針は6章で効いてきますが、dividends.db は使いません。DBが無くても動きます。

📡
通信が発生するのは9章だけ

CSVを読んで絞り込んで書き出す処理(2〜8章)は完全にローカルで、外部との通信はありません。通信するのは9章のyfinanceを使う部分だけ、送るのは銘柄コードだけで、CSVの中身も絞り込みの条件も送信しません。9章は使わなくてもツールは完成します

🧪
検証環境

本文の実行結果は冒頭の環境(Windows 11)で 2026年9月4日に確認しました。numexpr は入れていません(4章で効きます)。サンプルの銘柄コード・社名・株価・配当額などはすべて架空で、実データは9章のyfinance出力だけです。

2. サンプルを架空銘柄にした理由と、データ入手という別の壁

「全銘柄の配当利回りとPERの一覧を読み込んで、条件で絞る」——そう考えたときの最大の難所は、その一覧をどこから持ってくるかです。個人が規約違反なしに上場全銘柄の指標を一括で得る簡単な方法は見当たりません(Webページから機械的に集めるなら第2回のとおり利用規約とrobots.txtの確認が先)。そこで本記事はデータ入手の問題を切り離し、架空25行のCSVで絞り込みロジックだけを作ります

架空の銘柄コードに「4桁の数字」も「130A」も使えない

数字4桁は当然だめですが、「英字が混ざっていれば架空だと分かるだろう」も違います。2024年1月以降の新規上場分から、銘柄コードに英文字が入るようになりました(「130A」のような形式・楽天証券松井証券のお知らせ。既存のコードは数字のみで変更なし)。つまり 130A 形式は実在します

そこでサンプルは SMPL-01SMPL-25 に。桁数も区切りも実在のコード体系と一致しないので、将来どのコードが割り当てられても衝突しません。第8回では実在の社名とコードに架空の金額を入れていましたが、今回は出力そのものが「銘柄の絞り込み結果」になるので、コードから社名・業種まで架空にしています。

CSVには「計算済みの指標」を持たせない

列は code / name / sector / price / dividend(1株あたり年間配当額)/ dividend_prev(前期の配当)/ eps / bps / dividend_note の9つ。25行に0(無配)と空欄(データ無し)マイナスのEPS(赤字)マイナスのBPS(債務超過)、前期より配当が少ない行、記念配当・特別配当と注記された行を混ぜてあります。以降の章で踏む地雷は全部この25行の中にあります。

配当利回り・PER・PBRの列はCSVに入れず読み込んだあとにコードで計算します。計算済みの値を配るといつの株価で、どの配当(予想か実績か)で作られた値なのかが分からなくなるからです。

    df = pd.read_csv(path, dtype={"code": str, "name": str, "sector": str,
                                  "dividend_note": str})
    for col in NUMERIC_COLS:
        # 「-」「—」などが混ざると列がobjectになるので、数値以外はNaNに落とす
        df[col] = pd.to_numeric(df[col], errors="coerce")

dtype={"code": str} は、コードが全部数字の体系だったときに整数として読まれて先頭のゼロが消えるのを防ぐため。to_numeric(errors="coerce") は、証券サイトからコピーしたデータでよくある「」「N/A」を NaN に落とします。これを飛ばすと列全体が object になり、数値の比較のつもりが文字列の比較になります(CSVの文字コードやdtypeは第3回で)。

サンプルCSVは配布コードに埋め込んであるので、python dividend_screener.py --init-sample の1行で手元に作れます。同名ファイルがあるときは上書きせずに止まります(--force を付けたときだけ上書き)。

3. 3つの指標の定義を先に揃える

絞り込みを書く前に、3つの指標が何を何で割った数字なのかを確定させます。

指標算出できない条件
配当利回り(%)1株あたり年間配当 ÷ 株価 × 100配当額が無い/株価が0以下
PER(倍)株価 ÷ 1株当たり当期純利益(EPS)EPSが0以下(赤字)/EPSが無い
PBR(倍)株価 ÷ 1株当たり純資産(BPS)BPSが0以下(債務超過)/BPSが無い

配当利回りは「株価に対する年間配当金の割合を示す指標」で、「一株当たりの年間配当金を、現在の株価で割って求める」(野村證券 証券用語解説集)。同じ解説に「投資をするときは、年間配当金の予想値で計算し、判断材料とする」とあるとおり、実務で目にする利回りは予想配当ベースが多く、実績配当で計算した値とは別物です。本記事のサンプルは税引前の配当額で計算しており、手取りの割合ではありません。自分のCSVがどちらの配当なのかは列名で分けておくのが安全です。

PERは「株価が『1株当たりの当期純利益』の何倍になっているかを示す指標」、PBRは「株価が直前の本決算期末の『1株当たり純資産』の何倍になっているかを示す指標」(金融経済教育推進機構J-FLECの用語集:PERPBR)。PERも「今後の決算の予想の1株益を使用します」とあり、予想と実績で数字が変わる点は利回りと同じです。

赤字のPERを「小さい順」に並べてはいけない

実装で効いてくるのはEPSがマイナスのとき。素直に price / eps を計算すると負のPERが出ます。負の数はどんな正の数より小さいので、「PERの小さい順」に並べると赤字の行が先頭に集まる算出不能はNaNにするのが定義に忠実です。

def add_metrics(df: pd.DataFrame) -> pd.DataFrame:
    """配当利回り・PER・PBR を計算する。算出できない行はNaNにする。"""
    df = df.copy()
    price = df["price"].where(df["price"] > 0)
    df["yield_pct"] = df["dividend"] / price * 100
    df["per"] = price / df["eps"].where(df["eps"] > 0)    # 赤字(eps<=0)は算出不能
    df["pbr"] = price / df["bps"].where(df["bps"] > 0)    # 債務超過(bps<=0)は算出不能
    return df

Series.where(cond)条件を満たさない要素をNaNに置き換えるメソッドです。eps が0以下の行だけNaNになり、その行のPERも自動的にNaNになる。

もう1つ、式そのものから読める性質を。配当利回りは分母が株価なので、配当額が変わらなくても株価が下がれば利回りは上がります。利回りが高いという事実だけからは、配当が増えたのか株価が下がったのかを区別できません。

なおPBRについては、2023年3月31日に東証がプライム市場・スタンダード市場の全上場会社に「資本コストや株価を意識した経営の実現に向けた対応」を要請しています。これは上場会社に対する経営面の要請であって、投資家への推奨ではありません。本記事は制度の背景として触れるだけで、水準の良し悪しは扱いません。

4. 1条件で絞る — boolean indexing と query()

pandasで行を抜き出す書き方は主に2つ。どちらも結果は同じです。

th = 3.5                                   # 動作確認用の仮の値
df[df["yield_pct"] >= th]                  # boolean indexing
df.query("yield_pct >= @th")               # query(@でローカル変数を渡す)

条件が増えると df[(df["yield_pct"] >= 3.5) & (df["pbr"] <= 1.0)] より df.query("yield_pct >= 3.5 and pbr <= 1.0") のほうが目で追えます。ただし条件を動的に組み立てるなら、文字列を連結するより真偽値のSeriesを & で合成するほうが安全です(8章でそうしています)。

条件を2つにすると、まず括弧で転ぶ

複数条件をつなぐときは各条件を括弧で囲みます& は比較演算子より優先順位が高いので、括弧を省いた df[df["per"] <= 15 & df["pbr"] <= 1.0](15・1.0は仮の値)は 15 & df["pbr"] を先に計算しようとして TypeErrorand に替えても、Seriesは真偽値1つに畳めないので ValueError: The truth value of a Series is ambiguous. になります(3.0.3で実測)。query() の中でだけ and が書けるのは、pandasが式を & に変換しているからです。

列名にカッコや空白を入れると、内部で名前が書き換えられる

query() の文字列はPythonの式として解釈されるので、列名がPythonの識別子として妥当かどうかで扱いが変わります。日本語の列名は妥当なのでそのまま通ります。

d = df.rename(columns={"yield_pct": "配当利回り"})
print(len(d.query("配当利回り >= 3.5")))     # 8 … バッククォート無しで通る

一方 利回り(%) のようにカッコや空白が入った列名はバッククォートで囲む必要があり、内部で使える名前に書き換えられます('dividend yield''BACKTICK_QUOTED_STRING_dividend_yield''利回り(%)' にいたっては 'BACKTICK_QUOTED_STRING__UNICODE_u5229_..._PERCENT__RPAR_')。この変換を通す価値はありません。列名は yield_pct のようなASCIIのsnake_caseにしておき、日本語の見出しは rename() で出力の直前に当てれば足ります。

「query()は速い」が当てはまらない環境がある

query() の速さは numexpr が入っているかどうかで決まります。pandasのdocstringは engine の既定を 'numexpr' と書いていますが、実装上の既定は None=「numexprを試して、無ければpythonにフォールバック」。入っていない環境では警告も出さずpythonエンジンで実行されます

from pandas.core.computation.check import NUMEXPR_INSTALLED
print(NUMEXPR_INSTALLED)          # False … この環境ではpythonエンジンで動く

engine="numexpr" と明示すれば ImportError: 'numexpr' is not installed or an unsupported version. で止まるので気づけますが、既定のままでは気づけません。速度を理由に query() を選ぶなら、まずこの1行で環境を確かめてください。pandasの集計の書き方は第4回で扱っています。

5. つまずき実演①:NaNは比較すると必ずFalse

25行のCSVを「利回り3.5%以上」と「3.5%未満」の2つに分けて、件数を足してみます。

print(len(df.query("yield_pct >= 3.5")))   # 8
print(len(df.query("yield_pct < 3.5")))    # 15
print(len(df))                             # 25

8 + 15 = 23 で、25になりません。消えた2行は、配当額が空欄でNaNになった行です。pandasの公式ドキュメントは「np.nan との比較は常にFalseを返す」(当サイト訳)と明記していて、>= でも < でも同じようにFalseになる。だからどちらの結果にも現れません

厄介なのはエラーが出ないことです。「利回り3.5%以上は8件でした」という出力は、それらしく見えます。実際には25件のうち2件を判定していないだけ。絞り込みの結果を見ているつもりで、判定できなかった行を見落としている——スクリーナーで一番よくある間違いです。

「条件で落ちた」と「データが無い」を分けて数える

対処は難しくありません。条件を満たすか(ok)そもそも判定できるか(usable)を別々に持てばいい。

def screen(df: pd.DataFrame, conds: dict):
    """通過・条件落ち・判定不能の3つに分ける。この3つの合計が必ず全件になる。"""
    ok_all = pd.Series(True, index=df.index)
    usable_all = pd.Series(True, index=df.index)
    for ok, usable in conds.values():
        ok_all &= ok
        usable_all &= usable
    passed = usable_all & ok_all
    dropped = usable_all & ~ok_all
    unknown = ~usable_all
    return passed, dropped, unknown

passeddroppedunknown は排他で、3つの件数を足すと必ず全件になります。この不変条件があると、出力を見ただけで数え漏れに気づけます。各条件の usable は、その条件が使う列が揃っているかで決める。利回りの条件なら usable = df["dividend"].notna() & (df["price"] > 0) です。

欠損を0で埋めると、嘘の情報になる

fillna(0) でNaNを消せば件数は合います。でもそれは「利回り0%」という実在しない情報を作る行為です。サンプルCSVでは、無配の行に dividend = 0 を、データが無い行に空欄を入れて区別しています。「配当を出していない」と「配当額を知らない」は違う。前者は0%として正しく判定でき、後者は判定不能として除外するのが誠実な扱いです。

実行するとこの3分割が出ます(1章の画面写真がその出力です)。判定できなかった3件は、配当額が空欄の2行と、BPSがマイナス(債務超過)でPBRを算出できない1行。この3件は「条件に合わない」のではなく「判定していない」

グラフにすると、消えた行の存在が見える

架空25銘柄の配当利回りとPBRの散布図。横軸PBR、縦軸配当利回りで、条件を満たした5点が緑の大きな丸、条件で落ちた17点が灰色の丸として描かれ、しきい値を示す赤い破線が引かれている。判定できなかった3件は座標を持たないため描かれていないという注記が入る(すべて架空のサンプルデータ)
▲ 散布図に描けるのは22点だけ。破線は動作確認用の仮のしきい値で、推奨値ではありません(架空のサンプルデータ)

散布図は両方の軸に値がある行しか打てません。判定できなかった3件は座標を持たないので1点も現れない。グラフを見て「25銘柄の分布はこうか」と読むと、すでに3件ぶん間違っています。

6. つまずき実演②:割り算で比べると境界の行が落ちる

次は、エラーも警告も出ないうえに件数の合計も合ってしまう種類の間違いです。サンプルCSVには、利回りがちょうど3.5%になる行が2つあります。

コード配当株価手計算Pythonの計算結果>= 3.5
SMPL-0522.4円640円3.5%3.4999999999999996False
SMPL-233.57円102円3.5%3.4999999999999996False

「利回り3.5%以上」で絞ったのに、ちょうど3.5%の2行が落ちます。Pythonの公式チュートリアルが説明するとおり「ほとんどの10進小数は2進小数として正確に表せない」(当サイト訳)ため、割り算の結果がわずかに下振れする(0.1 + 0.1 + 0.1 == 0.3False になるのと同じ理屈)。珍しくもありません。株価100〜10,000円・しきい値1.0〜5.0%を0.5%刻みで総当たりすると、数学的にはちょうどしきい値なのに >= がFalseになる組み合わせが1,644件ありました(2026-09-04実測)。しきい値ちょうどの行は、いちばん落としたくない行です。

割らずに、掛けて比べる

解決は式の変形です。price > 0 なら、

dividend / price * 100 >= 3.5      ⇔      dividend * 10000 >= 350 * price

両辺に price を掛けて割り算を消す。しきい値の3.5%も350ベーシスポイントという整数にすれば、左辺も右辺も整数の掛け算になります。金額を銭(1/100円)の整数で持てば、誤差が入る隙がありません。第8回で配当額をINTEGERの銭で持つと決めたのと同じ方針です。

def sen(series: pd.Series) -> pd.Series:
    """円を銭(1/100円)の整数に直す。比較を整数で行い、割り算の誤差を持ち込まない。"""
    return series.mul(100).round().astype("Float64").astype("Int64")


def cond_min_yield(df: pd.DataFrame, value: float):
    """配当利回り >= value(%)。除算をせず、両辺に掛けて整数で比べる。"""
    bp = round(value * 100)                               # 3.5% -> 350(ベーシスポイント)
    usable = df["dividend"].notna() & (df["price"] > 0)
    ok = (sen(df["dividend"]) * 10000 >= bp * sen(df["price"])).fillna(False)
    return ok.astype(bool) & usable, usable

2つだけ補足を。sen() の変換先が大文字の Int64 なのは、これが欠損を <NA> のまま持てる整数型だからです。小文字の int64 にすると、欠損のある列は IntCastingNaNError で止まります。手前の Float64欠損を扱える実数型で、入力の型をここで揃えてから整数にします(3.0.3では float64 から直接 Int64 にしても結果は同じでした)。もう1つ、fillna(False)比較できなかった行をFalseに倒すだけで、「条件を満たさなかった」のか「判定できなかった」のかは区別しません。それを持つのが usable なので、両方を返して5章の3分割に渡します

これで SMPL-05SMPL-23 は通過します。同じCSV・同じしきい値3.5%で、割り算のまま比べると8件、掛けて比べると10件。2件の差がちょうど境界の行です。先ほどの1,644件も、交差乗算に置き換えると全件が解消しました。

表示は割り算の結果を丸めて出して構いません。丸めるのは表示、比較は整数と決めておけば混ざらない。ただし round(y, 2) >= 3.5 のように丸めてから比べるのは避けます。Pythonの round() は偶数丸め(round(0.5)round(2.5)2)で、round(2.675, 2)2.67 になるように丸め自体が浮動小数点の影響を受けるからです。正確な10進計算なら decimal標準ライブラリ一覧)もありますが、pandasのベクトル演算から外れます。

7. つまずき実演③:連鎖代入は例外ではなく警告で失敗する

絞り込んだ行に印を付けたくなったとき、こう書いてしまうことがあります。

d2[d2["yield_pct"] >= 3.5]["flag"] = True     # 効かない
print("flag" in d2.columns)                   # False … 列すら作られない

pandas 3.0では Copy-on-Write が既定になり、あらゆるインデックス操作がコピーとして振る舞います。上の書き方は「コピーに代入している」ので、もとの d2 は変わりません。厄介なのは止まらないこと。実行しても例外は上がらず、警告が1件出るだけでした(3.0.3・2026-09-04実測)。

A value is being set on a copy of a DataFrame or Series through chained assignment.
Such chained assignment never works to update the original DataFrame or Series ...
Try using '.loc[row_indexer, col_indexer] = value' instead ...

ChainedAssignmentError という名前が付いていますが、既定の設定では例外ではなく警告として出ます(例外クラス自体は pandas.errors に定義されています)。正しくは1手で書きます。

d2.loc[d2["yield_pct"] >= 3.5, "flag"] = True   # これは効く

あわせて、SettingWithCopyWarning はpandas 3.0で削除されました。「警告を消すために .copy() を付ける」という対処を書いた記事は大量に残っています。

📖
次に読むなら:ライブラリの「既定値」を疑う癖

ここまでの罠はどれもエラーにならないぶん、既定の挙動を知っているかどうかだけが分かれ目でした。numexprの有無、NaNの扱い、コピーの意味論——ドキュメントの隅にある既定値が結果を静かに変えます。この読み方を鍛えるならEffective Python 第3版が近道です(当サイトのおすすめ本ランキング総合4位・全572ページ)。リンク先は当サイトの書籍紹介ページで、アフィリエイト広告を含みます。

8. しきい値に既定値を置かない設計と、自分のCSVへの差し替え

条件をコマンドラインから受け取ります。ここにこの記事でいちばん強い設計判断を1つ入れました。しきい値に既定値を置きません。

    # しきい値に default は置かない(既定値を置くと推奨値に見えるため)
    p.add_argument("--min-yield", type=float, help="配当利回りの下限(%%)")
    p.add_argument("--max-per", type=float, help="PERの上限(倍)")
    p.add_argument("--max-pbr", type=float, help="PBRの上限(倍)")

default=3.0 と書いた瞬間、その数字は「とりあえずの推奨値」として読まれます。どの水準で絞るかは使う人が決めることで、道具が決めることではありません。だから3つとも省略可能にしたうえで、1つも指定されなければ parser.error()実行を断りますrequired=True を3つに付けると「3つ全部必須」になるので、「少なくとも1つ」は自分で書きます。argparse の位置づけは標準ライブラリ一覧に)。引数なしで起動すると、usageに続いてこう出て終了コード2で止まります。

dividend_screener.py: error: しきい値を少なくとも1つ指定してください(--min-yield / --max-per / --max-pbr)。既定値は用意していません。

指定された条件だけを組み立てる

条件は {"配当利回り >= 3.5%": (ok, usable), ...} というdictに集めます。指定されなかった条件は評価しません(「PERの指定が無い」を「PERの上限なしの条件を満たした」と扱うと、PERが欠損している行の扱いが変わってしまいます)。

    conds = {}
    if args.min_yield is not None:
        conds[f"配当利回り >= {args.min_yield}%"] = cond_min_yield(df, args.min_yield)
    if args.max_per is not None:
        conds[f"PER <= {args.max_per}倍"] = cond_max_per(df, args.max_per)

キーを人が読める文字列にしておくと、そのまま出力の見出しに使えます。条件と結果が同じ場所から出てくるので、表示だけ古いという食い違いが起きません。結果CSVの先頭にも、適用した条件と件数をコメント行として刷ります。

# source,sample_stocks.csv
# total_rows,25
# matched_rows,5
# condition_1,配当利回り >= 3.5%
# condition_2,PBR <= 1.0倍
# note,条件に合う行を機械的に抽出しただけのファイルです。投資判断ではありません。
code,name,sector,price,yield_pct,per,pbr,dividend,dividend_prev,dividend_note

出力の書き出しと並べ替え

読み戻すときは pd.read_csv(path, comment="#") でコメント行を飛ばせます。書き出しは to_csv(f, index=False, float_format="{:.2f}")(f-string書式はpandas 3.0から)。encoding="utf-8-sig" は先頭にBOM(EF BB BF)を付ける指定で、日本語環境の表計算ソフトで開くときの文字化け対策の定番です。

並べ替えは --sort で列を選べます。sort_values()na_position は既定が "last" で、ascending の指定に関係なくNaNは末尾——昇順で並べても最終行は最大値ではありません。そして出力にはこの1行を添えています——「並び順は指標の大小であって、投資対象としての順位ではありません」。上に来た行が優れているという読み方を、ツール自身が否定しておくためです。

🔒
出力CSVの置き場所

結果のCSVは「自分がどの条件で、どの銘柄を見ているか」の記録です。共有フォルダ・クラウド同期・Webサーバーの公開ディレクトリには置かないでください。人に渡すなら条件のコメント行を外すほうが安全です。

自分のCSVに差し替える

自分のデータに移る手順は1つです。--init-sample で書き出したCSVを開き、9つの列名はそのままに、行を自分の候補へ置き換えて同じコマンドに渡す。指標の列は作りません(3章のとおりコード側で計算します)。一括で集める手段は2章のとおり見当たらないので、20〜30行を手で埋めるのが現実的です。1件ずつの確かめ方は9章に。

  • --csv:入力CSV(既定 sample_stocks.csv
  • --init-sample / --force:サンプルCSVを書き出す/既存を上書きする
  • --min-yield / --max-per / --max-pbr:しきい値(既定なし・1つ以上必須)
  • --sort:並べ替える列(yield_pct / per / pbr / price・既定は yield_pct
  • --out:結果CSVの書き出し先(省略すると画面表示だけ)
  • --fetch:1銘柄の生データを取得(9章・唯一通信する)

9. (オプション)候補ティッカーの生データを取る — この章だけ通信する

📡
この章だけ外部と通信します

以下は 第1回で使ったyfinance 経由で Yahoo! Finance に問い合わせます。送るのは銘柄コードだけで、手元のCSVも絞り込みの条件も送りません。この章を飛ばしてもツールは完成します。

自分の候補で試したくなったとき、指標を1件ずつ取る口が Ticker.info です。配布コードでは --fetch に隔離してあります(出力は抜粋)。

> python dividend_screener.py --fetch 7203.T
  currentPrice      : 3099.0
  previousClose     : 3117.0
  dividendYield     : 3.21
  dividendRate      : 100.0
  trailingPE        : 8.821771
  priceToBook       : 0.98362345
  currency          : JPY
  financialCurrency : JPY
  dividendRate / currentPrice * 100 = 3.2268

この出力は2026年9月4日に取得した実データです。銘柄コードは実在のものですが推奨銘柄ではなく、数値は取得時点のもので変動します。良し悪しは論じません。

dividendYield の単位を、使う前に必ず確かめる

いちばん危ないのがこの1行です。dividendYield3.21 は、3.21%なのか、それとも小数表記で321%なのか。実測では dividendRate / currentPrice * 100 の自己計算が 3.2268 でほぼ一致したので、取得時点では%表記だと分かります。ここを取り違えると、小数で返ると思い込んで >= 0.035 で絞ればほぼ全件が残り、逆に小数の値を >= 3.5 で絞れば1件も残りません。しきい値が100倍ずれてもエラーは出ません

大事なのはこの単位をyfinanceが保証しているわけではない点です。1.4.1のソースを検索すると dividendYield という文字列は1回も出てきませんinfo はYahooが返したJSONをそのまま渡すだけで、単位の正規化をしていない。つまり単位を決めているのはデータ提供元で、「バージョンいくつで変わった」という形では追えません。使う前に一度printして確かめるのが自衛策です。より確実なのは実額から自分で計算すること。dividendRate は1株あたりの年間配当額(上の例では100.0円)で返るので解釈が一意です。

    rate, price = info.get("dividendRate"), info.get("currentPrice")
    if rate is not None and price:
        print(f"  dividendRate / currentPrice * 100 = {rate / price * 100:.4f}")

キーが「まるごと無い」ことがある

同じ形式で別の米国上場銘柄(ADR)を取ると、dividendYielddividendRatepayoutRatiotrailingPE4つのキーが存在しませんでした(2026-09-04時点。trailingEps はマイナスの値で返り、forwardPE だけが存在した。これは取得したデータの状態であって、その企業の決算内容を述べるものではありません)。番外編のとおり info["trailingPE"] と直接書けば KeyError.get() なら None。そして None は数値と比較できません。

info.get("trailingPE") > 15      # 15は動作確認用の仮の値
# TypeError: '>' not supported between instances of 'NoneType' and 'int'

5章のNaNは黙って落ちるのに、こちらは例外で止まります。取得した辞書をそのままDataFrameに流すなら、None をNaNに変換してから比較する(そして5章の3分割で数える)ほうが一貫します。

ほかにも、info["previousClose"]fast_info["previousClose"]別の作り方をした別の値で、実測した5銘柄すべてで一致しませんでした(7203.Tは 3117.03133.0)。利回りの分母をどちらにするかで結果が変わります。currencyfinancialCurrency が違う銘柄では、価格と1株純資産の通貨が揃っていないこともある。yfinanceはPyPIの説明で「調査・教育目的を意図している」(当サイト訳)と明示しており、値の正確性は保証されません。指標は自分で検算してください。

そもそもyfinanceには組み込みのスクリーナーがある

公平のために書いておくと、yfinanceには yf.screen()EquityQuery が最初から入っていて、1.4.1でも実際に動きました。それでも自分で書く理由は3つ:実行が完全にローカルで再現できること、条件を自分の言葉で定義できること、使えるフィールド名がバージョンで変わること。既製品で足りるなら、そちらのほうが速い場面も当然あります。

10. つまずき一覧と、スクリーナーで分からないこと

自分のデータを入れると出てくる問題です。最後の1つ以外はエラーで止まりません

つまずき何が起きるか対処
NaNを含む列を >= で絞る欠損行が結果にも「非該当」にも現れず、件数の合計が全件にならないokとusableを分けて3分割で数える(5章)
dividend / price * 100 >= th で比べるしきい値ちょうどの行が丸めで落ちる両辺に掛けて整数で比べる(6章)
df[cond]["flag"] = True例外ではなく警告。列も作られず静かに失敗する.loc[cond, "flag"] = value(7章)
sort_values(ascending=True) の末尾を取る最大値ではなくNaNをつかむ(既定は na_position="last"並べ替え前にNaNを外す(8章)
欠損を fillna(0) で埋める「利回り0%」という実在しない情報ができる無配の0とデータ無しを区別する(5章)
info["trailingPE"] と直接書くキーごと無い銘柄で KeyError.get() でも None との比較で TypeErrorNone 前提で書きNaNに寄せる(9章)

スクリーナーで分かること/分からないこと

このツールで分かることこのツールでは分からないこと
入力したデータの中で、条件に合う行がどれかその条件が妥当かどうか
条件で落ちた件数と、判定できなかった件数の内訳欠損している値が本当はいくつか
利回りが高い行と低い行(指標の大小)利回りが高い理由(増配か、株価の下落か)
前期と今期の配当額の差次の期に配当がどうなるか
dividend_note の注記の有無注記が無い行に一過性の配当が含まれていないか

右の列が本記事の結論です。サンプルの実行では、条件を満たした5件のうち2件は前期より配当額が少ない行でした。配当利回りは前期の実績でも今期の予想でも計算できてしまうので、数字だけ見ると同じ「4%」に見える。だから配布コードは「前期より配当額が少ない行が何件、一過性の配当を含む行が何件」という参考情報を判定とは別枠で表示します。判定には使いません——それをどう読むかはツールの仕事ではないからです。配当は会社の決定で増配・減配・無配になり得ます。過去や予想の配当額から計算した利回りは、将来を保証しません。

証券会社のスクリーナーと、自作の使い分け

見る観点証券会社・情報サイトのスクリーナー本記事の自作
対象の銘柄上場銘柄をほぼ網羅自分が用意したCSVの行だけ
データの鮮度・整備提供元が更新している自分で入れた時点のまま
条件の自由度用意された項目の範囲式を自分で書ける
絞り込みの中身結果だけが表示される欠損で落ちた件数まで見える
手間かからないデータ準備とコードが要る

銘柄を広く探す用途なら、既製のスクリーナーのほうが速く、データも整っています。自作が勝てるのは「どう絞ったか」を自分で説明できるところだけ。

次回:株主優待カレンダー

第11回では株主優待のカレンダーアプリを扱う予定です。連載の全体像は投資×Python シリーズ一覧にあります。

📌
この連載のスタンス

本連載は「自分の資産を自分で管理・可視化するツールを、Pythonの学習題材として自作する」ことが目的です。特定銘柄の売買や投資手法を勧めるものではなく、記事中の銘柄コード・社名・株価・配当額・業種はすべて架空(9章のyfinance出力のみ取得時点の実データで、推奨銘柄ではありません)、しきい値は動作確認用の例です。当サイトは投資助言業(金融商品取引業)の登録を行っておらず、個別銘柄の推奨・売買タイミングの助言・税務相談には応じられません。税制の適用や確定申告の要否は国税庁の情報か税理士にご確認ください。投資判断はご自身の責任で(詳細は免責事項)。

11. よくある質問(FAQ)

Q. 利回りは何%以上を条件にすればいいですか?

本記事では扱いません。どの水準で絞るかは投資判断そのもので、当サイトは投資助言業の登録をしておらず、個別の助言はできません。記事とコードに出てくる「3.5%」「1.0倍」はツールが動くことを確かめるための仮の値です。ツールにしきい値の既定値を置いていないのも同じ理由で(8章)、既定値は「推奨」として読まれてしまうからです。

Q. 実在の全銘柄のデータはどこで手に入りますか?

個人が規約違反なしに一括で手に入れる簡単な手段は見当たりません(2章)。証券会社のスクリーナーの書き出し可否は各社の規約しだいで、Webページから機械的に集めるなら第2回で扱った利用規約とrobots.txtの確認が先です。

Q. query()df[条件] はどちらが速いですか?

query() が速いのは numexpr が入っている環境の話です。入っていなければ警告も出さずpythonエンジンで動き、速度の優位は消えます(pandas.core.computation.check.NUMEXPR_INSTALLED で確認できます・4章)。読みやすさと、条件を動的に組み立てやすいかで選ぶほうが実用的です。

Q. 無配の銘柄はどう扱うのが正しいですか?

「配当が0円」と「配当額のデータが無い」を分けるのが正解です。前者は利回り0%として判定でき、後者は判定不能として件数を別に数えます。fillna(0) でまとめて埋めると、データが無いだけの行が「利回り0%」という実在しない情報になります(5章)。

Q. 赤字の会社のPERはどうなりますか?

1株当たり利益がマイナスだとPERは算出できません。計算式のまま price / eps を通すと負のPERが出て、「PERの小さい順」に並べたときに先頭へ来ます。本記事は eps > 0 以外をNaNにしています(3章)。数値として計算はできても、その値を「割安・割高」の比較に使うことはできません。

Q. Excelのオートフィルタで足りるのでは?

条件に合う行を出すところまでは同じです。自作で足しているのは2つ。判定できなかった行を数えて件数の内訳を閉じること(5章)と、比較を割り算のままにせず整数で行うこと(6章)です。この2つが要らない規模なら、フィルタで十分な場面も当然あります。

Q. このツールは外部と通信しますか?

CSVを読んで絞り込むだけなら通信しません。通信するのは --fetch を付けたときだけで、送るのは銘柄コードのみ。絞り込みの条件もCSVの中身も送信しません。

Q. 出力したCSVを人に見せても大丈夫ですか?

中身は自分が探している条件と、その結果の銘柄リストです。共有フォルダ・クラウド同期・公開ディレクトリには置かないでください。渡すなら先頭の条件コメント行を外すほうが安全です(8章)。

🧰
読み終えたら:次に手を動かす3本

まずは完成版 dividend_screener.py を保存して、--init-sample でサンプルCSVを作るところまで。あとは --min-yield 3.5 のような仮の値で動かせば、5章の3分割がそのまま出ます。

データを取る側は第1回のyfinance、pandasの集計は第4回次回(第11回)は株主優待のカレンダーで、続きは投資×Python シリーズ一覧(ブックマーク推奨)か新着記事のRSSで追えます。

本記事は、生成AIを活用して下書きし、運営者が内容を確認・編集したうえで公開しています。掲載しているコード・出力・画像は Windows 11 / Python 3.12.10 / pandas 3.0.3 / matplotlib 3.11.1 / yfinance 1.4.1 で 2026年9月4日に実行して確認したものです。サンプルの銘柄コード・社名・業種・株価・配当額・1株利益・1株純資産はすべて架空で、実在の企業や運用実績とは関係がありません(9章のyfinanceの出力のみ取得時点の実データで、銘柄コードは実在しますが推奨銘柄ではありません)。しきい値は動作確認用の例であり推奨値ではありません。本記事はプログラミングの情報提供を目的としたもので、銘柄の売買・投資手法の推奨でも税務上の助言でもありません。投資判断はご自身の責任で。AIの利用方針は免責事項をご覧ください。