PER(株価収益率、Price Earnings Ratio)は業種によって平均水準が大きく違います。本記事は、プライム市場の銘柄を業種マスタと結合し、業種別 PER 中央値ランキング を作るまでの流れを、コード・結果・落とし穴の順に通しで公開します。
過去の集計結果は将来の株価や利益を保証しません。本記事は「業種別の比較を行うときの集計手順」の学習を目的にしています。
目次
- PER の定義をそろえる
- 集計のゴール
- 必要なライブラリ
- なぜ中央値か
- コード(コピペで動く)
- 実行結果(イメージ)
- 外れ値の扱い
- サンプル出力で納得感を確かめる
- 落とし穴
PER の定義をそろえる
本記事では次の式で固定します。
- 株価: 集計日の前営業日の終値
- 予想 EPS: 直近本決算で開示された通期予想 EPS(Earnings Per Share)
- EPS が 0 以下(赤字予想)の銘柄は PER の比較対象から外す
赤字予想銘柄を含めると PER が極端な負値になり、ランキングが壊れます。「比較できる土俵」に乗らないものは、ランキングの前段でフィルタするのが基本です。
集計のゴール
最終的に出すのは次の 2 表です。
- 業種別 PER 中央値ランキング(昇順 / 降順)
- 各業種の上位・下位 3 銘柄を並べたサンプル(納得感を確かめる用)
必要なライブラリ
pip install pandas検証バージョン: Python 3.12.5 / pandas 2.2.3
なぜ中央値か
業種ごとの代表値として平均ではなく中央値を使う理由は次の通りです。
- 外れ値耐性: PER は分布が右に長い裾を引くため、平均は数銘柄に引っ張られやすい
- 解釈のしやすさ: 「半数の銘柄がこの値より上 / 下」という意味付けが直感的
- 赤字スレスレの銘柄: 微小な利益 → PER が異常に大きくなる挙動を緩和できる
参考までに、平均 PER も併記して 2 つを比較する形にします。
コード(コピペで動く)
"""prime_per_ranking.pyプライム市場の銘柄を業種マスタに結合し、業種別 PER 中央値ランキングを作る。"""from __future__ import annotations
import pandas as pd
LISTED_PATH = "listed_info.csv" # Code, CoName, MktNm, S33NmSTATEMENTS_PATH = "statements.csv" # Code, DiscDate, FEPSPRICES_PATH = "prices.csv" # Code, Date, CPRIME_MARKET_LABEL = "プライム"PER_LOWER, PER_UPPER = 0.0, 200.0 # 比較対象の PER レンジ
def load_latest_eps(path: str) -> pd.DataFrame: df = pd.read_csv(path, parse_dates=["DiscDate"]) df = df.dropna(subset=["FEPS"]) # 銘柄ごとに最新開示の 1 行 df = df.sort_values(["Code", "DiscDate"]).groupby("Code", as_index=False).tail(1) return df.rename(columns={"FEPS": "eps"})[["Code", "eps"]]
def load_latest_close(path: str) -> pd.DataFrame: df = pd.read_csv(path, parse_dates=["Date"]) df = df.sort_values(["Code", "Date"]).groupby("Code", as_index=False).tail(1) return df[["Code", "C"]]
def main() -> None: listed = pd.read_csv(LISTED_PATH) listed = listed[listed["MktNm"].str.contains(PRIME_MARKET_LABEL, na=False)]
eps = load_latest_eps(STATEMENTS_PATH) close = load_latest_close(PRICES_PATH)
# statements は Code、listed / prices は Code。結合時に対応づける df = listed.merge(eps, on="Code").merge(close, on="Code") # 赤字予想と外れ値を除外してから PER を作る df = df[df["eps"] > 0].copy() df["per"] = df["C"] / df["eps"] df = df[(df["per"] > PER_LOWER) & (df["per"] < PER_UPPER)]
# 業種別の集計 by_sector = ( df.groupby("S33Nm") .agg( n=("Code", "size"), per_median=("per", "median"), per_mean=("per", "mean"), ) .round(2) .sort_values("per_median") )
print("=== 業種別 PER 中央値ランキング(昇順)===") print(by_sector) by_sector.to_csv("sector_per_ranking.csv")
# 業種ごとの上位 / 下位 3 銘柄サンプル samples = ( df.sort_values(["S33Nm", "per"]) .groupby("S33Nm", group_keys=False) .apply(lambda g: pd.concat([g.head(3), g.tail(3)])) ) samples[["S33Nm", "Code", "CoName", "C", "eps", "per"]].to_csv( "sector_per_samples.csv", index=False )
if __name__ == "__main__": main()実行結果(イメージ)
=== 業種別 PER 中央値ランキング(昇順)=== n per_median per_meanS33Nm銀行業 80 9.45 11.32保険業 10 10.10 12.05鉄鋼 30 11.80 14.66卸売業 110 12.10 15.40建設業 90 12.55 16.02...情報・通信業 180 22.40 30.18医薬品 40 24.05 33.72小売業 150 19.80 27.55サービス業 200 21.55 32.40PER 中央値が低い業種は銀行・保険・鉄鋼・卸売など、利益水準が安定しつつ成長期待が控えめな業種が並びがちです。逆に高い側には情報・通信業や医薬品など、成長期待を織り込みやすい業種が並びます。
外れ値の扱い
外れ値除去は 集計の前 に置きます。後で気づいて除去すると、削除する銘柄の選び方に意図が混じりやすいためです。
| 状況 | 対処 |
|---|---|
| 赤字予想(EPS ≦ 0) | 比較対象から除外 |
| 微小利益で PER が 1000 倍超 | PER_UPPER でカット |
| 一時利益で EPS が肥大 | 過去 3 期の平均 EPS を使うバージョンを別に作る |
| 子会社売却益などの特別損益 | 開示資料の脚注を確認し、必要なら手で除外 |
サンプル出力で納得感を確かめる
ランキングだけ見て終わらせず、業種ごとに上位 / 下位 3 銘柄を並べる sector_per_samples.csv を作っておきます。「銀行業の中央値が低いのは妥当か」を、自分の知っている銘柄で確かめるためです。
サンプル列の例。
S33Nm Code CoName C eps per銀行業 ... サンプル銀行A 1024 125.0 8.19銀行業 ... サンプル銀行B 845 98.0 8.62銀行業 ... サンプル銀行C 960 108.5 8.85...銀行業 ... サンプル地銀X 1850 150.0 12.33銀行業 ... サンプル地銀Y 2100 168.0 12.50銀行業 ... サンプル地銀Z 1620 125.0 12.96落とし穴
- EPS の定義: 通期予想 EPS / 実績 EPS / 連結 / 単体 で値が変わる。集計の冒頭で明示する
- 業種マスタの粒度: 33 業種分類と 17 業種分類で結果が変わる。どちらを使ったか記録する
- 市場区分の表記揺れ: 「プライム」「Prime」「Prime Market」など、データ提供元で違うことがある
- 平均との乖離: 平均 PER と中央値の乖離が大きい業種は、上位の数銘柄に引っ張られている。生データを必ず確認
まとめ
- 業種別 PER ランキングは「赤字除外 + 上限カット + 中央値」が基本
- 平均 PER は外れ値に弱く、業種比較には中央値が向く
- 結果はランキングだけで信用せず、業種別のサンプル表で納得感を確かめる
- EPS と業種分類のバージョンを記録しておくと、後で再現できる
過去の集計は将来の利益・株価を保証しません。