ポートフォリオ(portfolio、複数銘柄の組み合わせ)を組むときの 重み付け方法 には、大きく分けて等加重(equal-weight)と時価総額加重(market-cap weight)があります。本記事では、同じ銘柄群を 2 つの重みでバックテストし、リターン・リスク・回転率の違いを Python で確認します。

目次

  1. 重み付け方法の違い
  2. 数式での整理
  3. サンプルデータの準備
  4. 等加重ポートフォリオ
  5. 時価総額加重ポートフォリオ
  6. 累積リターンの比較
  7. 評価指標の比較表
  8. 回転率(turnover)の比較
  9. 取引コストの組み込み
  10. どちらを使うか

重み付け方法の違い

代表的な 2 つの方式を整理します。

方式重み特徴主な利用例
等加重wi=1/Nw_i = 1 / N全銘柄を同じ金額で持つスマートベータ ETF、シンプルな比較ベース
時価総額加重wi=MVi/jMVjw_i = \text{MV}_i / \sum_j \text{MV}_j大型株の比重が高い多くの市場指数(TOPIX など)

ここで MVi\text{MV}_i は銘柄 ii の時価総額(market value)です。

等加重の特徴

  • 小型株の影響が相対的に大きい(銘柄数で割るだけのため)
  • 期間ごとに リバランス(再均等化) が必要 → 取引コスト・回転率が高い
  • 時価総額加重に比べて、サイズファクターの効きを取り込みやすい

時価総額加重の特徴

  • 大型株が大きな比重を持つ。指数連動型に近い動き
  • リバランスを頻繁に行わなくても自然に重みが追従する → 回転率が低い
  • 銘柄数を増やしても、超大型銘柄の動きで結果が左右されやすい

数式での整理

ポートフォリオの当日リターン rpr_p は、各銘柄リターン rir_i の重み付き平均です。

rp,t=i=1Nwi,t1ri,tr_{p,t} = \sum_{i=1}^{N} w_{i,t-1} \cdot r_{i,t}
  • wi,t1w_{i,t-1} : 前日終値時点でのウェイト(先読み防止のため 1 日 shift)
  • ri,tr_{i,t} : 当日のリターン

時価総額加重の場合、リバランスを行わなければ重みは銘柄リターンに従って自然に変化します。等加重では毎期間ごとにリバランスして 1/N に戻します。

サンプルデータの準備

10 銘柄・約 5 年の日次リターンと、各銘柄の時価総額(初日の値)を用意します。実データを使う場合は、J-Quants の終値と時価総額(またはその代理として上場株式数 × 株価)を組み合わせます。

import numpy as np
import pandas as pd
rng = np.random.default_rng(seed=83)
n_days = 1250
n_tickers = 10
tickers = [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 で持つ。月次リバランスでウェイトを戻す
  • 時価総額加重は大型株の比重が高く、リバランス無しでも自然に追従する
  • リバランス頻度・取引コスト・生存者バイアスで結果は大きく動く
  • 「正解の重み付け」は無く、目的に応じて選ぶ
  • ファクター戦略のベンチマークとしては等加重が分かりやすい