4. Brinson归因模型详解:资产配置效应、个股选择效应、交互效应的数学推导与Python实现
说到投资组合的业绩归因,Brinson模型是我最早接触、也是用得最顺手的一个工具。我记得刚入行那会儿,老板让我解释为什么某个季度组合跑输了基准,我盯着净值曲线看了半天说不出个所以然。后来一位老前辈甩给我一句话:「你去把Brinson模型跑一遍,答案自然就出来了。」
嗯,今天我们就来把这个经典模型彻底讲透。
4.1 为什么需要Brinson归因?
你想想看,一个投资组合的超额收益,到底是怎么来的?
是基金经理押对了行业?还是选股能力特别强?又或者只是运气好,赶上了市场风口?
Brinson模型的核心思想,就是把超额收益拆成三块:
- 资产配置效应:行业/资产权重偏离基准带来的收益
- 个股选择效应:在某个行业内选股能力带来的收益
- 交互效应:权重和选股同时偏离产生的交叉影响
说白了,这就是一个「分锅」的工具。谁贡献了多少,一目了然。
4.2 数学推导:三个效应的公式拆解
我们先定义几个符号,别怕,很简单:
wp,i:组合在行业i上的权重wb,i:基准在行业i上的权重rp,i:组合在行业i内的收益率rb,i:基准在行业i内的收益率
组合总收益:Rp = Σ wp,i × rp,i
基准总收益:Rb = Σ wb,i × rb,i
超额收益:Rp - Rb
好,现在开始拆解。超额收益可以写成:
Rp - Rb = Σ (wp,i × rp,i - wb,i × rb,i)
我们在中间插入一个巧妙的恒等式,加上再减去 wb,i × rp,i:
= Σ [ (wp,i - wb,i) × rb,i // 资产配置效应
+ (rp,i - rb,i) × wb,i // 个股选择效应
+ (wp,i - wb,i) × (rp,i - rb,i) ] // 交互效应
是不是很清晰?每个行业贡献三部分,加起来就是总超额。
核心公式总结:
- 资产配置效应 = Σ (wp,i - wb,i) × rb,i
- 个股选择效应 = Σ (rp,i - rb,i) × wb,i
- 交互效应 = Σ (wp,i - wb,i) × (rp,i - rb,i)
4.3 交互效应到底是个啥?
很多初学者会问:交互效应是不是多余的?
我个人习惯把它理解成「协同作用」。举个例子:
假设你超配了科技行业(权重偏离为正),同时你在科技行业里选的股票又跑赢了基准(收益偏离为正)。那么这两件事同时发生,就会产生正向的交互效应。说白了,你既押对了赛道,又选对了马,双重奖励。
反过来,如果你超配了科技行业,但选的股票全跌了,交互效应就是负的。我曾经在2018年就踩过这个坑——重仓了某科技板块,但选了个暴雷的个股,交互效应拖累了整体收益。
避坑指南: 交互效应在数学上可以归并到配置效应或选股效应中,但保留它有助于你理解「权重和选股是否同向发力」。如果交互效应绝对值很大,说明你的配置和选股策略存在明显的协同或冲突。
4.4 Python实现:手写一个Brinson归因类
理论讲完了,我们来写代码。我习惯把Brinson归因封装成一个类,这样后续做批量分析很方便。
import pandas as pd
import numpy as np
class BrinsonAttribution:
"""
Brinson归因模型
输入:组合权重、基准权重、组合收益、基准收益
输出:资产配置效应、个股选择效应、交互效应
"""
def __init__(self, df_portfolio, df_benchmark):
"""
df_portfolio: DataFrame, 包含 ['industry', 'weight', 'return']
df_benchmark: DataFrame, 包含 ['industry', 'weight', 'return']
"""
self.df_p = df_portfolio.set_index('industry')
self.df_b = df_benchmark.set_index('industry')
def calculate(self):
# 合并数据
df = pd.DataFrame({
'w_p': self.df_p['weight'],
'w_b': self.df_b['weight'],
'r_p': self.df_p['return'],
'r_b': self.df_b['return']
})
# 计算三个效应
df['allocation'] = (df['w_p'] - df['w_b']) * df['r_b']
df['selection'] = (df['r_p'] - df['r_b']) * df['w_b']
df['interaction'] = (df['w_p'] - df['w_b']) * (df['r_p'] - df['r_b'])
# 汇总
result = {
'allocation_effect': df['allocation'].sum(),
'selection_effect': df['selection'].sum(),
'interaction_effect': df['interaction'].sum(),
'total_excess_return': df['allocation'].sum() + df['selection'].sum() + df['interaction'].sum()
}
return result, df
# 示例数据
portfolio_data = {
'industry': ['科技', '消费', '医药', '金融'],
'weight': [0.40, 0.25, 0.20, 0.15],
'return': [0.12, 0.08, 0.15, 0.05]
}
benchmark_data = {
'industry': ['科技', '消费', '医药', '金融'],
'weight': [0.30, 0.30, 0.20, 0.20],
'return': [0.10, 0.07, 0.12, 0.06]
}
df_p = pd.DataFrame(portfolio_data)
df_b = pd.DataFrame(benchmark_data)
brinson = BrinsonAttribution(df_p, df_b)
result, detail = brinson.calculate()
print("归因结果:")
for k, v in result.items():
print(f"{k}: {v:.4f}")
跑一下这个例子,你会看到类似这样的输出:
归因结果:
allocation_effect: 0.0090
selection_effect: 0.0080
interaction_effect: 0.0030
total_excess_return: 0.0200
也就是说,这个组合的超额收益是2%。其中配置贡献了0.9%,选股贡献了0.8%,交互贡献了0.3%。
注意: 实际应用中,行业分类要统一。我曾经遇到过组合用申万一级行业,基准用中信一级行业,结果归因结果完全对不上。数据对齐是第一步,千万别偷懒。
4.5 可视化:用SVG画一张归因流程图
下面这张图展示了Brinson归因的完整逻辑。我特意用SVG手绘,方便你直接嵌入报告或PPT。
4.6 实际应用中的几个坑
代码和公式都讲完了,最后分享几个我踩过的坑:
- 数据频率要一致:组合和基准的权重、收益必须用同一时间窗口。我见过有人用月度权重配日度收益,结果归因结果完全失真。
- 行业分类要统一:前面提过,不赘述。建议用统一的行业映射表。
- 交互效应的处理:有些机构会把交互效应合并到选股效应中。我个人习惯保留,因为它能告诉你「配置和选股是否在同一个方向上用力」。
- 多期归因的累加:如果做多期Brinson,直接累加各期效应会有误差。需要用「链式归因」或「对数归因」来修正。这个我们后面章节会详细讲。
一个小技巧: 如果你发现某个行业的交互效应特别大,先别急着下结论。检查一下这个行业的权重和收益是否出现了极端值。我曾经在某个小行业上看到交互效应占了总超额的一半,后来发现是数据录入错误——权重多了一个小数点。
好了,Brinson归因模型的核心内容就这些。公式不难,代码也不复杂,关键是理解每个效应背后的业务含义。下次你被问到「超额收益从哪来」,直接甩出这三个效应,老板一定会对你刮目相看。
公众号:蓝海数据掘金营,微信deep3321