複数のファンダメンタルズ指標を組み合わせて、条件に合う銘柄を抽出する処理を スクリーニング と呼びます。本記事では pandas で AND / OR 条件を組み立てる方法と、上位 N 銘柄を取り出すコード例、業種中立化の工夫を整理します。

目次

  1. スクリーニングの基本
  2. サンプル DataFrame
  3. 単純な AND 条件
  4. OR 条件と NOT 条件
  5. 上位 N 銘柄を取る
  6. 業種中立化
  7. 順位スコア(ランクスコア)
  8. 業種中立化したランクスコア
  9. 複数指標の合成スコアの注意
  10. screening 関数にまとめる
  11. 過去データで検証するときの注意

スクリーニングの基本

スクリーニングは次の 3 ステップで構成されます。

  1. 条件を 指標の組み合わせ として定義する
  2. 銘柄一覧に対して条件を評価し、True / False を出す
  3. True の銘柄(または上位 N 銘柄)を抽出する

シンプルな例から、徐々に複雑な条件に発展させていきます。

サンプル DataFrame

説明用の DataFrame を用意します。

import numpy as np
import 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)で扱うのが堅実
  • 合成スコアは過去フィットしやすい。過去データの当てはまりだけで判断しない