複数のファンダメンタルズ指標を組み合わせて、条件に合う銘柄を抽出する処理を スクリーニング と呼びます。本記事では pandas で AND / OR 条件を組み立てる方法と、上位 N 銘柄を取り出すコード例、業種中立化の工夫を整理します。
目次
- スクリーニングの基本
- サンプル DataFrame
- 単純な AND 条件
- OR 条件と NOT 条件
- 上位 N 銘柄を取る
- 業種中立化
- 順位スコア(ランクスコア)
- 業種中立化したランクスコア
- 複数指標の合成スコアの注意
- screening 関数にまとめる
- 過去データで検証するときの注意
スクリーニングの基本
スクリーニングは次の 3 ステップで構成されます。
- 条件を 指標の組み合わせ として定義する
- 銘柄一覧に対して条件を評価し、True / False を出す
- True の銘柄(または上位 N 銘柄)を抽出する
シンプルな例から、徐々に複雑な条件に発展させていきます。
サンプル DataFrame
説明用の DataFrame を用意します。
import numpy as npimport pandas as pd
df = pd.DataFrame({ "Code": ["1301", "1302", "1303", "1304", "1305", "1306", "1307", "1308"], "S33Nm": ["製造業", "銀行業", "化学", "製造業", "化学", "情報・通信業", "情報・通信業", "製造業"], "C": [2900, 1500, 4900, 1200, 3500, 13000, 8000, 2200], "per": [12.5, 9.0, 18.0, np.nan, 22.0, 28.0, 17.5, 11.0], "pbr": [1.0, 0.6, 1.5, 0.8, 2.5, 5.0, 2.8, 0.9], "roe": [10.5, 6.5, 13.5, 4.0, 9.0, 22.0, 18.0, 8.0], "equity_ratio": [55, 7, 65, 30, 50, 70, 60, 45], "yield_pct": [3.0, 4.5, 1.5, 5.0, 0.0, 0.5, 1.0, 3.5],})print(df)PER が NaN(赤字 / 計算不能)の銘柄が混じっている前提で組み立てます。
単純な AND 条件
「PER が 15 倍以下、ROE が 8% 以上、自己資本比率が 30% 以上」を AND で書きます。
mask = ( (df["per"] <= 15) & (df["roe"] >= 8) & (df["equity_ratio"] >= 30))print(df[mask][["Code", "S33Nm", "per", "roe", "equity_ratio"]])pandas で boolean を組み合わせるときは &(AND)・|(OR)・~(NOT)を使います。and / or / not のキーワードは Series に使えないので注意します。各条件は 括弧で囲む のが慣例です。
NaN を含む条件は 常に False と評価されます。PER が NaN の銘柄は自動的に除外されます。
OR 条件と NOT 条件
「PER 10 倍以下 OR PBR 0.8 倍以下」のような割安系の OR 条件はこう書きます。
cheap = (df["per"] <= 10) | (df["pbr"] <= 0.8)print(df[cheap][["Code", "per", "pbr"]])「銀行業ではない」のような NOT は次の通りです。
not_bank = ~(df["S33Nm"] == "銀行業")# あるいはnot_bank = df["S33Nm"] != "銀行業"isin を使えば「製造業 OR 化学」のような複数等価条件も簡潔に書けます。
target_sector = df["S33Nm"].isin(["製造業", "化学"])上位 N 銘柄を取る
条件に合う銘柄が多すぎるとき、ある指標で並び替えて上位 N を取ります。
candidates = df[mask].copy()top = ( candidates.sort_values("roe", ascending=False) .head(3) .reset_index(drop=True))print(top[["Code", "S33Nm", "per", "roe", "equity_ratio"]])並び替えのキーは目的に応じて変えます。割安系なら PER / PBR を昇順、収益力系なら ROE 降順、配当系なら配当利回り降順、というように使い分けます。
業種中立化
業種で指標の水準が違うため、業種ごとに上位 N を取る ほうが偏りが減ります。
def top_by_sector(df: pd.DataFrame, by: str, n: int = 2) -> pd.DataFrame: """業種ごとに `by` 列で上位 n 件を取る。""" return ( df.sort_values(["S33Nm", by], ascending=[True, False]) .groupby("S33Nm", group_keys=False) .head(n) .reset_index(drop=True) )
print(top_by_sector(df[mask], by="roe", n=2))group_keys=False を入れることで、グループ列を二重に追加しない形で結果を取れます。
順位スコア(ランクスコア)
複数の指標を ランク に変換し、合算してスコアを作る方法です。指標間の単位を気にせず、ざっくり比較できます。
# 割安(PER / PBR が小さいほど良い)・収益(ROE が大きいほど良い)を合成work = df.dropna(subset=["per"]).copy()work["rank_per"] = work["per"].rank(ascending=True) # 小さいほど 1 位work["rank_pbr"] = work["pbr"].rank(ascending=True)work["rank_roe"] = work["roe"].rank(ascending=False) # 大きいほど 1 位work["score"] = work[["rank_per", "rank_pbr", "rank_roe"]].mean(axis=1)
ranked = work.sort_values("score").head(5)print(ranked[["Code", "S33Nm", "per", "pbr", "roe", "score"]])ランクスコアは外れ値に強い反面、指標間の重み を全部均等に置いてしまいます。重み付けは mean の前に各 rank を掛けるなどで調整します。
業種中立化したランクスコア
業種ごとに rank を取ってから合算すると、業種要因を抑えた相対比較ができます。
work = df.dropna(subset=["per"]).copy()for col, asc in [("per", True), ("pbr", True), ("roe", False)]: work[f"rank_{col}"] = ( work.groupby("S33Nm")[col] .rank(ascending=asc, method="average") )work["score"] = work[[c for c in work.columns if c.startswith("rank_")]].mean(axis=1)print(work.sort_values("score").head(5)[["Code", "S33Nm", "score"]])業種ごとの rank を合算するため、銀行業のように指標の水準が違う業種でも、相対的に良い銘柄が拾えます。
複数指標の合成スコアの注意
合成スコアは作りやすい反面、次の落とし穴があります。
- 指標の単位が違う(PER は倍、ROE は %、利回りは %)。素の値で平均すると偏る
- 欠損 が多い指標は、ランクで上位に出やすくなる(欠損を除外する処理が必要)
- 重み付け は恣意性が残る。等重でもバイアスの一形態
- 過去フィット しやすい。過去データで最適化したスコアは将来でも有効と限らない
「シンプルなスコアでも、過去最高に当てはまるよう調整した瞬間」に、過去フィットが始まっています。
screening 関数にまとめる
実用的なスクリーニング関数の例です。
def screen( df: pd.DataFrame, per_max: float = 20, pbr_max: float = 2.0, roe_min: float = 8, equity_ratio_min: float = 30, top_n: int = 5, sort_by: str = "roe",) -> pd.DataFrame: """ファンダ条件で銘柄を絞り込み、上位 N 件を返す。
Args: df: Code, S33Nm, per, pbr, roe, equity_ratio を含む DataFrame。 per_max: PER 上限。 pbr_max: PBR 上限。 roe_min: ROE 下限(%)。 equity_ratio_min: 自己資本比率下限(%)。 top_n: 抽出件数。 sort_by: 並び替え列(降順)。
Returns: 条件を満たす上位 top_n 件の DataFrame。 """ cond = ( (df["per"] <= per_max) & (df["pbr"] <= pbr_max) & (df["roe"] >= roe_min) & (df["equity_ratio"] >= equity_ratio_min) ) return ( df[cond] .sort_values(sort_by, ascending=False) .head(top_n) .reset_index(drop=True) )
print(screen(df, top_n=3))引数化しておくと、条件をチューニングしながら何度も呼び出せます。Streamlit のスライダーから条件を渡すような使い方にも展開できます(#12-3「Streamlit 入門 — Python だけでウェブアプリを作る」)。
過去データで検証するときの注意
スクリーニング条件を過去データで検証する場合は、データリークに気を付けます。
- 各時点で その時点の利用可能な情報のみ で条件を評価する(発表前の決算は使わない)
- 上場廃止銘柄を除外しない(生存者バイアス)
- 取引コスト・スリッページの考慮
- 期間を分割して 学習期間 / 検証期間 で振る舞いが安定するか確認
これらは#7-7「生成AIに分析を「設計」してもらう使い方」(分析の設計)・記事 10 系列(統計・定量分析)で詳しく扱います。
生成AI へのプロンプト例
業種中立スクリーニング関数の依頼例です。
入力 DataFrame に次の列があります(銘柄識別列は J-Quants API に準拠)。- Code (str)- S33Nm (str)- per (float, 計算不能は NaN)- pbr (float)- roe (float, %)- equity_ratio (float, %)
依頼: 業種中立のスクリーニング関数 sector_neutral_screen(df, n) を書いてください。
仕様:- 業種ごとに per 昇順 rank, pbr 昇順 rank, roe 降順 rank を出す- 3 つの rank の単純平均をスコアとする- スコアが小さい順に各業種から最大 n 件抽出- per が NaN の銘柄は除外
要件: pandas 2.2 系。docstring は日本語。まとめ
- スクリーニングは「条件定義 → 評価 → 抽出」の 3 ステップ
- pandas では
&|~で条件を組み合わせる(and/orは使えない) - 業種で水準が違うため、業種中立(業種ごとの rank)で扱うのが堅実
- 合成スコアは過去フィットしやすい。過去データの当てはまりだけで判断しない