Skip to content

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:

\[R_p^{\text{cum}} = \prod_t (1 + R_{p,t}) - 1, \qquad R_b^{\text{cum}} = \prod_t (1 + R_{b,t}) - 1\]
\[\text{excess}^{\text{cum}} = R_p^{\text{cum}} - R_b^{\text{cum}}\]

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 AttributionError with 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_identical fixture pins this directly.
  • Single period. All linkers require \(\ge 2\) periods; for one period the linking is a no-op — just use the PeriodAttribution result directly.