PBR・PERスクリーニングをPythonで自動化して割安製造メーカー株を探す

Python実装・コード

先月、製造メーカーの決算ラッシュで「この銘柄、安くなってる気がするんだけど割安なのかどうか全然わからない」という状況が続いた。毎回証券会社のサイトでPBRを手動で調べていて、正直しんどかった。。。どうせPythonで株価取得してるなら、ファンダメンタルズ指標のスクリーニングも自動化しようと思い立った。

なぜPBR・PERなのか

PBR(株価純資産倍率)とPER(株価収益率)は、株が「割安かどうか」を測る基本指標だ。製造メーカーは設備・在庫などの資産が多く、PBRが比較的低い傾向がある。東証が「PBR1倍割れ企業への改善要請」を出してから、PBR1倍以下の銘柄が市場で注目されるようになった。

PBR = 株価 ÷ 1株純資産(BPS)
PER = 株価 ÷ 1株利益(EPS)

一般的に、PBR1倍以下は「解散価値以下」、PER15倍以下は「利益に対して安い」と判断されることが多い。ただしセクターや成長率によって基準は変わるので、あくまでスクリーニングの入口として使う。

yfinanceでファンダメンタルズデータを取得する

yfinanceのTicker.infoには財務指標が含まれている。ただし日本株の場合、データが取得できない銘柄や古いデータが入っていることがあるので注意が必要だ。

import yfinance as yf
import pandas as pd
import time

# 製造メーカー銘柄リスト(例:トヨタ、デンソー、ファナック、小松製作所、ホンダ等)
tickers_jp = [
    "7203.T",  # トヨタ自動車
    "6902.T",  # デンソー
    "6954.T",  # ファナック
    "6301.T",  # 小松製作所
    "7267.T",  # ホンダ
    "6752.T",  # パナソニック
    "6501.T",  # 日立製作所
    "7011.T",  # 三菱重工
    "6326.T",  # クボタ
    "7013.T",  # IHI
    "6103.T",  # オークマ
    "6113.T",  # アマダ
]

def get_fundamentals(ticker_symbol):
    """yfinanceから財務指標を取得する"""
    try:
        t = yf.Ticker(ticker_symbol)
        info = t.info

        return {
            "ticker":      ticker_symbol,
            "name":        info.get("longName", info.get("shortName", "")),
            "price":       info.get("currentPrice", info.get("regularMarketPrice")),
            "pbr":         info.get("priceToBook"),
            "per":         info.get("trailingPE"),
            "forward_per": info.get("forwardPE"),
            "dividend_yield": info.get("dividendYield"),
            "market_cap":  info.get("marketCap"),
            "roe":         info.get("returnOnEquity"),
        }
    except Exception as e:
        print(f"  エラー {ticker_symbol}: {e}")
        return None

# データ取得(レート制限対策で0.5秒待機)
records = []
for t in tickers_jp:
    print(f"取得中: {t}")
    data = get_fundamentals(t)
    if data:
        records.append(data)
    time.sleep(0.5)

df = pd.DataFrame(records)
print(df[["ticker", "name", "price", "pbr", "per"]].to_string(index=False))

スクリーニング条件を適用する

データが揃ったら条件フィルタを掛ける。PBRとPERはfloatで取得されるが、欠損(None/NaN)があるので dropna() を先にやる。

df_screen = df.dropna(subset=["pbr", "per"]).copy()

# 条件定義
PBR_MAX = 1.5    # PBR 1.5倍以下(1.0倍以下に絞るとかなり厳しいので少し緩めた)
PER_MAX = 15.0   # PER 15倍以下
ROE_MIN = 0.05   # ROE 5%以上(収益性を担保するため)

mask = (
    (df_screen["pbr"] <= PBR_MAX) &
    (df_screen["per"] <= PER_MAX) &
    (df_screen["roe"] >= ROE_MIN)
)

result = df_screen[mask].sort_values("pbr").reset_index(drop=True)

# 表示フォーマット整理
result["pbr"]           = result["pbr"].map("{:.2f}".format)
result["per"]           = result["per"].map("{:.1f}".format)
result["roe"]           = result["roe"].map("{:.1%}".format)
result["dividend_yield"] = result["dividend_yield"].map(
    lambda x: f"{x:.2%}" if pd.notna(x) else "-"
)

print(f"\n=== スクリーニング結果(PBR≤{PBR_MAX}, PER≤{PER_MAX}, ROE≥{ROE_MIN:.0%})===")
print(result[["ticker", "name", "price", "pbr", "per", "roe", "dividend_yield"]].to_string(index=False))

結果をCSVに保存して毎朝自動実行する

スクリーニング結果を日付付きCSVに保存するようにして、Windowsのタスクスケジューラで毎朝8時に実行する設定にした。

from datetime import date

# CSVに保存
output_path = f"screening_{date.today().strftime('%Y%m%d')}.csv"
result.to_csv(output_path, index=False, encoding="utf-8-sig")
print(f"結果を保存しました: {output_path}")

# ===== タスクスケジューラ設定(参考・Windowsのみ) =====
# schtasks /create /tn "StockScreening" /tr "python C:\scripts\screening.py"
#          /sc DAILY /st 08:00 /f

スクリーニング結果をチャートで確認する

PBRとPERの散布図にプロットすると、どの銘柄がどのゾーンにいるか一目でわかる。

import matplotlib.pyplot as plt

fig, ax = plt.subplots(figsize=(10, 7))
fig.patch.set_facecolor("#0d1117")
ax.set_facecolor("#0d1117")

# 全銘柄をプロット
df_plot = df_screen.copy()
df_plot["pbr"] = pd.to_numeric(df_plot["pbr"], errors="coerce")
df_plot["per"] = pd.to_numeric(df_plot["per"], errors="coerce")

ax.scatter(df_plot["per"], df_plot["pbr"],
           color="#38bdf8", alpha=0.6, s=80, zorder=3)

# 割安ゾーンを強調
ax.axvline(PER_MAX, color="#f87171", linewidth=0.8, linestyle="--", alpha=0.7)
ax.axhline(PBR_MAX, color="#f87171", linewidth=0.8, linestyle="--", alpha=0.7)
ax.fill_between([0, PER_MAX], [0, 0], [PBR_MAX, PBR_MAX],
                color="#34d399", alpha=0.08, label="割安ゾーン")

# ティッカーラベル
for _, row in df_plot.dropna(subset=["per","pbr"]).iterrows():
    ax.annotate(row["ticker"].replace(".T",""),
                (row["per"], row["pbr"]),
                textcoords="offset points", xytext=(6, 4),
                fontsize=8, color="#8b949e")

ax.set_xlabel("PER(倍)", color="#8b949e")
ax.set_ylabel("PBR(倍)", color="#8b949e")
ax.set_title("製造メーカー銘柄 PBR × PER マップ", color="#f0f6fc", fontsize=13)
ax.tick_params(colors="#8b949e")
ax.spines[:].set_color("#30363d")
ax.legend(facecolor="#161b22", labelcolor="#e6edf3")
plt.tight_layout()
plt.savefig("pbr_per_map.png", dpi=150, facecolor="#0d1117")
plt.show()

使ってみてわかった注意点

yfinanceの財務データは証券会社のデータと若干ずれることがある。特に日本株は決算発表直後に数値が更新されないことがあった。「PERが異常に低い」と思って調べたら前期の利益が使われていた、ということが実際にあった。。。スクリーニング結果はあくまで「候補リスト」として扱い、最終確認は証券会社や決算短信で行うのがマストだ。

あとPBR1倍以下でもROEが低い(利益をほとんど生んでいない)銘柄は罠になりやすい。だからROEの足切り条件(5%以上)を入れた。これだけで候補数がぐっと減る。

まとめ

yfinanceのTicker.infoを使えば、PBR・PER・ROEなどのファンダメンタルズ指標を一括取得してスクリーニングできる。製造メーカー株のような「資産が厚い・PBRが低め」な銘柄を探すのにうってつけの手法だ。個人的には毎朝の自動スクリーニングを実装してから、手動調査の時間がかなり減った。次はJ-Quantsの決算データと組み合わせて、より精度の高いスクリーニングに発展させたいと考えている。

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