ポートフォリオ(portfolio、複数銘柄の組み合わせ)を組むときの 重み付け方法 には、大きく分けて等加重(equal-weight)と時価総額加重(market-cap weight)があります。本記事では、同じ銘柄群を 2 つの重みでバックテストし、リターン・リスク・回転率の違いを Python で確認します。
目次
- 重み付け方法の違い
- 数式での整理
- サンプルデータの準備
- 等加重ポートフォリオ
- 時価総額加重ポートフォリオ
- 累積リターンの比較
- 評価指標の比較表
- 回転率(turnover)の比較
- 取引コストの組み込み
- どちらを使うか
重み付け方法の違い
代表的な 2 つの方式を整理します。
| 方式 | 重み | 特徴 | 主な利用例 |
|---|---|---|---|
| 等加重 | 全銘柄を同じ金額で持つ | スマートベータ ETF、シンプルな比較ベース | |
| 時価総額加重 | 大型株の比重が高い | 多くの市場指数(TOPIX など) |
ここで は銘柄 の時価総額(market value)です。
等加重の特徴
- 小型株の影響が相対的に大きい(銘柄数で割るだけのため)
- 期間ごとに リバランス(再均等化) が必要 → 取引コスト・回転率が高い
- 時価総額加重に比べて、サイズファクターの効きを取り込みやすい
時価総額加重の特徴
- 大型株が大きな比重を持つ。指数連動型に近い動き
- リバランスを頻繁に行わなくても自然に重みが追従する → 回転率が低い
- 銘柄数を増やしても、超大型銘柄の動きで結果が左右されやすい
数式での整理
ポートフォリオの当日リターン は、各銘柄リターン の重み付き平均です。
- : 前日終値時点でのウェイト(先読み防止のため 1 日
shift) - : 当日のリターン
時価総額加重の場合、リバランスを行わなければ重みは銘柄リターンに従って自然に変化します。等加重では毎期間ごとにリバランスして 1/N に戻します。
サンプルデータの準備
10 銘柄・約 5 年の日次リターンと、各銘柄の時価総額(初日の値)を用意します。実データを使う場合は、J-Quants の終値と時価総額(またはその代理として上場株式数 × 株価)を組み合わせます。
import numpy as npimport pandas as pd
rng = np.random.default_rng(seed=83)n_days = 1250n_tickers = 10tickers = [f"S{i:02d}" for i in range(n_tickers)]
# 銘柄ごとの平均・ボラに差を付けるmu = rng.normal(0.0003, 0.0004, size=n_tickers)sigma = rng.uniform(0.012, 0.026, size=n_tickers)
returns = pd.DataFrame( rng.normal(mu, sigma, size=(n_days, n_tickers)), index=pd.date_range("2021-01-04", periods=n_days, freq="B"), columns=tickers,)
# 初日の時価総額(対数正規分布で大きな差)market_cap_initial = pd.Series( np.exp(rng.normal(loc=12, scale=1.0, size=n_tickers)), index=tickers, name="mcap",)print(market_cap_initial.round(0))等加重ポートフォリオ
毎月初に等加重にリバランスする実装を書きます。リバランス日を「月初の最初の営業日」と定義します。
def equal_weight_returns(returns: pd.DataFrame) -> pd.Series: """月初リバランスの等加重ポートフォリオの日次リターンを返す。""" n = returns.shape[1] rebalance_dates = ( returns.index.to_series().groupby(returns.index.to_period("M")).min() ) weights = pd.DataFrame(0.0, index=returns.index, columns=returns.columns)
current_w = pd.Series(1 / n, index=returns.columns) for d in returns.index: if d in rebalance_dates.values: current_w = pd.Series(1 / n, index=returns.columns) # 等加重に戻す weights.loc[d] = current_w # 翌日に向けて、当日のリターンで重みを自然に動かす next_w = current_w * (1 + returns.loc[d]) current_w = next_w / next_w.sum()
# 先読み防止: 前日のウェイト * 当日のリターン port_ret = (weights.shift(1) * returns).sum(axis=1) return port_ret.iloc[1:]
eq_ret = equal_weight_returns(returns)print(eq_ret.tail())weights.shift(1) で「前日終値時点のウェイト」を当日リターンに掛ける形にしています。リバランス日は「当日終値で実行 → 翌日から新しい等加重で運用」という仮定です。
時価総額加重ポートフォリオ
時価総額は時間とともに変化します。シンプルには「初日の時価総額をベースに、その後はリターンで自然に動く」と考えれば、リバランス無しの実装になります。
def cap_weight_returns(returns: pd.DataFrame, mcap0: pd.Series) -> pd.Series: """初日時価総額ベースの cap-weighted ポートフォリオの日次リターン。""" weights = pd.DataFrame(index=returns.index, columns=returns.columns, dtype=float) current_w = (mcap0 / mcap0.sum()).reindex(returns.columns) for d in returns.index: weights.loc[d] = current_w next_w = current_w * (1 + returns.loc[d]) current_w = next_w / next_w.sum()
port_ret = (weights.shift(1) * returns).sum(axis=1) return port_ret.iloc[1:]
cap_ret = cap_weight_returns(returns, market_cap_initial)print(cap_ret.tail())実際の指数では、構成銘柄の入れ替えや浮動株調整があるため、より複雑な計算になります。本記事は単純化したモデルです。
累積リターンの比較
両ポートフォリオの資産曲線を重ねます。
import matplotlib.pyplot as plt
eq_eq = (1 + eq_ret).cumprod()cap_eq = (1 + cap_ret).cumprod()
fig, ax = plt.subplots(figsize=(9, 5))ax.plot(eq_eq.index, eq_eq, label="equal-weight", color="tab:blue")ax.plot(cap_eq.index, cap_eq, label="cap-weighted", color="tab:orange")ax.set_title("Equal-weight vs Cap-weighted")ax.set_ylabel("Eq (start = 1.0)")ax.set_xlabel("Date")ax.grid(alpha=0.3)ax.legend()plt.tight_layout()plt.savefig("ew_vs_cw.png", dpi=120)plt.close(fig)
評価指標の比較表
TRADING_DAYS = 252
def summarize(ret: pd.Series) -> dict[str, float]: eq = (1 + ret.fillna(0)).cumprod() n = len(ret) years = n / TRADING_DAYS cum = eq.iloc[-1] - 1 cagr = eq.iloc[-1] ** (1 / years) - 1 if years > 0 else float("nan") sharpe = (ret.mean() / ret.std(ddof=0)) * np.sqrt(TRADING_DAYS) if ret.std(ddof=0) > 0 else float("nan") mdd = ((eq - eq.cummax()) / eq.cummax()).min() return {"cum": cum, "cagr": cagr, "sharpe": sharpe, "mdd": mdd}
summary = pd.DataFrame({"equal_weight": summarize(eq_ret), "cap_weighted": summarize(cap_ret)}).round(4)print(summary)回転率(turnover)の比較
回転率(turnover)は、リバランスごとの「重みの変化量の合計」です。等加重は月次リバランスで重みを 1/N に戻すため、回転率が高くなります。
def monthly_turnover(weights: pd.DataFrame) -> pd.Series: """月初時点のウェイト変化量の合計を月次で返す。""" monthly = weights.resample("MS").first() diff = monthly.diff().abs().sum(axis=1) return diff
# 等加重ポートフォリオのウェイト履歴を再計算(可視化用)def equal_weights_history(returns: pd.DataFrame) -> pd.DataFrame: n = returns.shape[1] rebalance_dates = ( returns.index.to_series().groupby(returns.index.to_period("M")).min() ) weights = pd.DataFrame(0.0, index=returns.index, columns=returns.columns) current_w = pd.Series(1 / n, index=returns.columns) for d in returns.index: if d in rebalance_dates.values: current_w = pd.Series(1 / n, index=returns.columns) weights.loc[d] = current_w next_w = current_w * (1 + returns.loc[d]) current_w = next_w / next_w.sum() return weights
eq_weights = equal_weights_history(returns)print(monthly_turnover(eq_weights).head(12))時価総額加重ではリバランスを行わない設計なので、月次の差分はリターンに従う自然な変動だけになります。等加重に比べて、月初での 強制的なウェイト戻し が無いぶん回転率は低くなります。
取引コストの組み込み
回転率に片道コストを掛けて、ポートフォリオリターンから差し引きます。
COST = 0.001 # 片道 0.1%
def apply_cost(ret: pd.Series, weights_history: pd.DataFrame, cost: float = COST) -> pd.Series: """ウェイトの変化量にコストを掛けて、リターンから差し引く。""" daily_change = weights_history.diff().abs().sum(axis=1).fillna(0) return ret - daily_change.reindex(ret.index).fillna(0) * cost
eq_ret_after = apply_cost(eq_ret, eq_weights)print(summary)print("after cost (eq):", summarize(eq_ret_after))時価総額加重では weights_history の差分が小さいため、コスト控除後でも結果がほとんど動きません。等加重では月次リバランスのコスト分だけ CAGR が押し下げられます。
どちらを使うか
シンプルな目安は次の通りです。
| 目的 | 推奨 |
|---|---|
| 市場全体の動きを取り込む | 時価総額加重(指数連動の発想) |
| 銘柄選定の効果を素直に見る | 等加重(重みの偏りで結果が歪まない) |
| ファクター戦略のベンチマーク | 等加重(銘柄群の中身を平均的に見る) |
| 取引コストを抑えたい | 時価総額加重(自然に追従するためリバランス少) |
「正解」はありません。同じ銘柄群でも目的によって適切な重みは変わります。
注意点
- 時価総額の取得: 株式分割・自社株買いで時価総額が動きます。当日に観測可能な値を使うこと
- 生存者バイアス: 上場廃止銘柄を含めずに過去を再現すると、結果は過大評価になります(#10-7「バックテストの落とし穴(俯瞰)」)
- 外れ値への感度: 等加重は構成銘柄の極端な動きを 1/N 受ける。時価総額加重は超大型銘柄の動きが支配的
- リバランス頻度: 日次・月次・四半期で結果が動きます。月次が現実的な出発点
本記事のコードは学習目的のシミュレーションで、実発注 API は呼びません。
生成AI へのプロンプト例
複数の重み付け方式をまとめて評価したい場合の例です。
入力:- returns: pd.DataFrame, index=date, columns=ticker, 値=日次リターン- mcap0: pd.Series, index=ticker, 初日の時価総額- rebalance: str, "M"(月次)/ "Q"(四半期)/ "Y"(年次)
次の関数 weighting_comparison(returns, mcap0, rebalance="M") を書いてください。
仕様:- equal-weight, cap-weighted の 2 系統を計算- 各リバランス日にウェイトを戻す(equal-weight は 1/N、cap-weighted は最新時価総額比)- 戻り値: dict[str, pd.Series] で日次戦略リターン
要件:- pandas 2.2 系- 先読み防止のため、ウェイトは shift(1) してリターンに掛ける- docstring を日本語で書くまとめ
- 等加重は構成銘柄を 1/N で持つ。月次リバランスでウェイトを戻す
- 時価総額加重は大型株の比重が高く、リバランス無しでも自然に追従する
- リバランス頻度・取引コスト・生存者バイアスで結果は大きく動く
- 「正解の重み付け」は無く、目的に応じて選ぶ
- ファクター戦略のベンチマークとしては等加重が分かりやすい