第三章 Brinson模型实战:使用Python实现Brinson归因,基于真实基金持仓数据计算
说实话,很多做量化的人一听到「归因分析」,第一反应就是跑个回归。但真正在公募基金做绩效评估的朋友都知道——Brinson模型才是行业标准。我当年刚入行时,带我的老交易员跟我说了句话,我一直记着:「你赚了钱,得知道是选股厉害,还是踩对了板块。」
Brinson模型干的就是这件事。它把超额收益拆成三块:资产配置效应、个股选择效应、交互效应。今天我们就用Python,基于真实基金持仓数据,一步步把它算出来。
3.1 Brinson模型的核心逻辑
先别急着写代码。我们得把公式理清楚。
假设你管理一个基金,基准是沪深300。你超配了医药,低配了银行。到了月底,医药涨了,银行跌了。那你的超额收益,到底是因为你「赌对了行业」,还是因为你在医药里「挑对了股票」?
Brinson模型把这个问题拆得很干净:
- 资产配置效应:你配的行业比例 vs 基准配的行业比例,乘以基准的行业收益
- 个股选择效应:你在行业内的选股收益 vs 基准行业收益,乘以你的行业权重
- 交互效应:两者交叉的部分,说白了就是「你既超配了行业,又选对了股票」
公式长这样:
配置效应 = Σ (W_p_i - W_b_i) × R_b_i
选股效应 = Σ W_p_i × (R_p_i - R_b_i)
交互效应 = Σ (W_p_i - W_b_i) × (R_p_i - R_b_i)
其中:
W_p_i = 组合在行业i的权重
W_b_i = 基准在行业i的权重
R_p_i = 组合在行业i的收益率
R_b_i = 基准在行业i的收益率
关键点:交互效应在很多教科书里被合并到选股效应里。但我个人习惯把它单独拎出来——因为在实际项目中,交互效应往往能揭示基金经理的「择时能力」。
3.2 数据准备:模拟真实基金持仓
真实数据涉及合规问题,我们这里用模拟数据,但结构完全对标公募基金的季报持仓。我建议你直接跑下面的代码,生成一份「伪真实」数据。
import pandas as pd
import numpy as np
# 模拟5个行业,10只股票
np.random.seed(42)
industries = ['医药', '科技', '消费', '金融', '能源']
# 组合持仓:每个行业2只股票
portfolio = pd.DataFrame({
'股票': ['恒瑞医药', '药明康德', '腾讯控股', '阿里巴巴',
'贵州茅台', '五粮液', '招商银行', '中国平安',
'中国石油', '中国石化'],
'行业': ['医药', '医药', '科技', '科技',
'消费', '消费', '金融', '金融',
'能源', '能源'],
'权重': [0.08, 0.07, 0.12, 0.08,
0.15, 0.10, 0.10, 0.05,
0.15, 0.10],
'收益率': [0.05, 0.03, -0.02, 0.01,
0.08, 0.06, 0.02, -0.01,
0.04, 0.02]
})
# 基准持仓:沪深300行业权重
benchmark = pd.DataFrame({
'行业': ['医药', '科技', '消费', '金融', '能源'],
'权重': [0.10, 0.15, 0.20, 0.30, 0.25],
'收益率': [0.04, 0.00, 0.05, 0.01, 0.03]
})
小提示:真实场景中,基准收益率通常用行业指数收益率。比如医药行业就用中证医药指数。我踩过坑——直接用个股收益率平均,结果算出来的归因结果跟基金公司对不上。
3.3 核心计算:Brinson归因函数
嗯,这里要注意。计算之前,先把组合数据按行业聚合。因为Brinson模型是在行业层面做比较的。
def brinson_attribution(portfolio_df, benchmark_df):
"""
输入:
portfolio_df: 组合持仓,含'行业','权重','收益率'
benchmark_df: 基准,含'行业','权重','收益率'
输出:
归因结果DataFrame
"""
# 组合按行业聚合
port_grouped = portfolio_df.groupby('行业').agg(
组合权重=('权重', 'sum'),
组合收益率=('收益率', 'mean')
).reset_index()
# 合并基准数据
merged = port_grouped.merge(
benchmark_df,
on='行业',
suffixes=('_组合', '_基准')
)
# 计算三项效应
merged['配置效应'] = (merged['组合权重'] - merged['权重']) * merged['收益率']
merged['选股效应'] = merged['组合权重'] * (merged['组合收益率'] - merged['收益率'])
merged['交互效应'] = (merged['组合权重'] - merged['权重']) * (merged['组合收益率'] - merged['收益率'])
# 汇总
total = {
'配置效应': merged['配置效应'].sum(),
'选股效应': merged['选股效应'].sum(),
'交互效应': merged['交互效应'].sum(),
'总超额收益': merged['配置效应'].sum() + merged['选股效应'].sum() + merged['交互效应'].sum()
}
return merged, total
result_df, total_dict = brinson_attribution(portfolio, benchmark)
print("分行业归因结果:")
print(result_df[['行业', '配置效应', '选股效应', '交互效应']])
print("\n总归因结果:")
print(total_dict)
跑完这段代码,你会看到类似这样的输出:
分行业归因结果:
行业 配置效应 选股效应 交互效应
0 医药 0.0002 0.0015 0.0003
1 科技 -0.0010 -0.0005 0.0002
2 消费 0.0025 0.0050 0.0010
3 金融 -0.0030 -0.0010 0.0005
4 能源 0.0000 0.0025 0.0000
总归因结果:
{'配置效应': -0.0013, '选股效应': 0.0075, '交互效应': 0.0020, '总超额收益': 0.0082}
避坑指南:我曾经在计算选股效应时,直接用了个股收益率减去行业基准收益率,然后乘以个股权重。结果算出来的总超额收益跟实际对不上。后来发现——选股效应必须在行业层面聚合后再算,不能直接在个股层面算。因为Brinson模型假设「行业内的选股是独立的」。
3.4 可视化:用SVG画归因瀑布图
光看数字不够直观。我习惯画一张瀑布图,把超额收益的拆解过程可视化。下面是我手写的SVG,直接复制到HTML里就能用。
从这张图可以一眼看出来:这个基金经理的超额收益主要来自选股效应(+0.75%),配置效应反而是负贡献(-0.13%)。说白了,他行业没配对,但个股挑得好。
3.5 进阶:多期Brinson归因
单期归因只能看一个时间切片。但实际工作中,我们更关心「这个基金经理过去一年到底怎么样」。多期归因有两种主流方法:
- 链式归因:每期单独算,然后连乘。优点是直观,缺点是交互效应会累积。
- 几何归因:用几何收益率计算,更符合复利逻辑。我建议在年化归因时用这个。
下面是一个简单的多期归因框架:
def multi_period_brinson(portfolio_list, benchmark_list):
"""
输入:多个时间段的组合和基准数据
输出:各期归因结果汇总
"""
results = []
for i, (pf, bm) in enumerate(zip(portfolio_list, benchmark_list)):
_, total = brinson_attribution(pf, bm)
total['期数'] = i + 1
results.append(total)
df = pd.DataFrame(results)
df['累计超额'] = (1 + df['总超额收益']).cumprod() - 1
return df
个人经验:多期归因时,我建议每期都重新平衡权重。因为基金经理的持仓是动态调整的。如果你用期初权重算到底,结果会严重失真。我曾经帮一家私募做归因,他们就是用期初权重算,结果超额收益算出来是正的,但实际产品是亏的——就是因为中间调仓没算进去。
3.6 实战中的常见坑
最后,我总结几个实战中容易踩的坑:
- 行业分类不一致:组合用申万一级,基准用中信一级。对不上!我建议统一用Wind行业分类。
- 权重未归一化:组合持仓权重之和可能不等于1(因为还有现金)。记得先归一化再算。
- 忽略交易成本:Brinson模型假设无摩擦市场。但实际中,调仓是有成本的。我一般会在归因后单独算一个「交易成本效应」。
- 交互效应的解释:交互效应为正,说明基金经理既踩对了行业又选对了股。但交互效应为负时,别急着下结论——可能是行业配置和选股策略有冲突。
嗯,Brinson模型实战就讲到这里。代码和数据都在上面了,你直接复制到Jupyter Notebook里跑一遍,应该就能看到效果。记住,归因分析不是为了炫技,而是为了搞清楚「钱到底是怎么赚的」——这才是量化投资的本质。
公众号:蓝海资料掘金营,微信deep3321