Multi-period linking#
Single-period attribution answers why this quarter. Multi-period linking answers why this year — and it is subtler than a simple sum because compounding and attribution effects interact non-trivially across periods.
The problem#
Say Q1 allocation was +0.50% and Q2 allocation was -0.30%. The cumulative allocation is not \(0.50 - 0.30 = +0.20\%\): between Q1 and Q2 the portfolio's dollar value changed, so the Q2 allocation is applied to a different base. The same applies to selection and interaction.
Compounded excess return is also not the sum of single-period excess returns:
The task of a linking method is to scale each single-period effect such that the scaled sum across time still equals the compounded arithmetic excess (additive methods) or the compounded geometric excess (geometric method).
flowchart LR
Q1["Q1<br/>A₁, S₁, I₁<br/>Rp₁, Rb₁"]
Q2["Q2<br/>A₂, S₂, I₂<br/>Rp₂, Rb₂"]
Qn["…<br/>Aₙ, Sₙ, Iₙ"]
Scale{{"Scaling<br/>per period<br/>kₜ or αₜ"}}
Out["Linked year<br/>A, S, I<br/>close compounded excess"]
Q1 --> Scale
Q2 --> Scale
Qn --> Scale
Scale --> Out
Five methods, one call#
from pybrinson import (
bhb,
link_carino, link_grap, link_frongello, link_menchero,
link_geometric,
)
periods = [bhb(segs_q1, period="Q1"), bhb(segs_q2, period="Q2"),
bhb(segs_q3, period="Q3"), bhb(segs_q4, period="Q4")]
for linker in (link_carino, link_grap, link_frongello,
link_menchero, link_geometric):
linked = linker(periods)
print(f"{linker.__name__}: excess={linked.excess_return:.4%} "
f"A={linked.allocation:.4%} "
f"S={linked.selection:.4%} "
f"I={linked.interaction:.4%}")
All five accept the same input, produce the same compounded portfolio and benchmark returns, and pin their respective identities:
- Cariño, GRAP, Frongello, Menchero — additive: \(A + S + I = R_p^{\text{cum}} - R_b^{\text{cum}}\).
- Geometric — multiplicative: \((1 + A)(1 + S) - 1 = (1 + R_p^{\text{cum}}) / (1 + R_b^{\text{cum}}) - 1\). Interaction is folded into selection.
Which one should you use?#
Short answer: whatever your firm already reports on. Mixing methods on the same fund across reports creates confusion that no library can fix.
If you are choosing from scratch:
flowchart TD
A{"Identity you<br/>need to close?"}
A -->|Geometric| G[["link_geometric"]]
A -->|Arithmetic| B{"Any period<br/>return ≤ -100%?"}
B -->|Yes| F[["link_frongello<br/>or link_grap<br/>or link_menchero"]]
B -->|No| C{"Preferred style?"}
C -->|"Log-smoothing,<br/>NA default"| Ca[["link_carino"]]
C -->|"Additive factor,<br/>FR / EU"| Gr[["link_grap"]]
C -->|"Recursive,<br/>match Frongello 2002"| Fr[["link_frongello"]]
C -->|"Uniform scaling"| Me[["link_menchero"]]
| Method | Pick if… | Trade-off |
|---|---|---|
| Cariño | You want the industry default in North America; additive identity; smooth treatment of small per-period effects. | Falls back analytically when \(R_p \approx R_b\); requires \(R > -1\) every period. |
| GRAP | You are in a European / French institutional context and reproduce what CNCEF audits expect. | Factor-based; less intuitive than Cariño. |
| Frongello | You want a recursive, path-dependent form that matches the practitioner paper numerics exactly. | Recursive: period \(t\) depends on cumulative to \(t-1\). |
| Menchero | You want uniform scaling that minimises dispersion of the scaling factors. | Formerly patented; expired 2024-02-18. |
| Geometric | You report in a geometric-excess framework (Bacon-style; common in the UK). | Different identity than the other four; interaction = 0. |
pybrinson's cross-method consistency suite verifies that all five produce compatible compounded totals — use this to demonstrate internal consistency to auditors.
Worked example#
The two_period_linking_example fixture is small enough to derive
by hand and exercises every linker:
from pybrinson import bhb, link_carino, link_geometric
from pybrinson.fixtures import two_period_linking_example
periods, expected = two_period_linking_example()
attrs = [bhb(segs, period=label) for label, segs in periods]
carino = link_carino(attrs)
assert abs(carino.excess_return - expected["arithmetic_excess"]) < 1e-12
geom = link_geometric(attrs)
assert abs(geom.excess_return - expected["geometric_excess"]) < 1e-12
Each per-period BHB closes its own identity; each linker closes its
own cumulative identity. pybrinson raises AttributionError at any
step where either closure fails, rather than absorb a residual.
Pathological cases#
- Period return \(\le -100\%\). Cariño and geometric require
\(R > -1\) (the logarithm is undefined). Both raise
AttributionErrorwith a clear message. Frongello, GRAP, and Menchero tolerate deep negatives. - All periods identical. Cariño, Frongello, and Menchero
coincide (Frongello endnote 13); the pybrinson
frongello_2002_table1_identicalfixture pins this directly. - Single period. All linkers require \(\ge 2\) periods; for
one period the linking is a no-op — just use the
PeriodAttributionresult directly.