Cited fixture pack#
pybrinson.fixtures exposes worked examples drawn directly from
the primary literature. The same fixtures back pybrinson's own
test suite — one source of truth. Use them to:
- Validate your own in-house attribution engine against published numbers.
- Cross-check a vendor report (Bloomberg, Barra, FactSet) when the numbers feel off.
- Regression-test a legacy spreadsheet before you retire it.
- Reproduce an academic paper's worked example end-to-end.
If a number disagrees with the fixture's published source, file an issue — the fixture is the bug, not your implementation.
Available fixtures#
from pybrinson.fixtures import (
bacon_2008_ch5_bhb,
bacon_2008_ch5_fachler,
frongello_2002_table1_identical,
frongello_2002_table3,
karnosky_singer_3segment,
two_period_linking_example,
)
bacon_2008_ch5_bhb()#
3-sector UK / Japan / US example from Bacon (2008) Chapter 5. Canonical BHB teaching vehicle; also cites the 1986 Brinson-Hood- Beebower paper and the CFA Institute free reprint.
from pybrinson import bhb
from pybrinson.fixtures import bacon_2008_ch5_bhb
segments, expected = bacon_2008_ch5_bhb()
result = bhb(segments)
assert abs(result.excess_return - expected["excess_return"]) < 1e-12
assert abs(result.allocation - expected["allocation"]) < 1e-12
bacon_2008_ch5_fachler()#
Same input segments as above, with the Brinson-Fachler allocation formula and its own expected numbers.
frongello_2002_table3()#
3-period 2-sector "challenge" example from Frongello (2002) pp. 10-12. The paper hand-derives Frongello-linked allocation as 57.39475% and total excess as 88.9805% — pybrinson reproduces both to \(10^{-10}\).
from pybrinson import bhb, link_frongello
from pybrinson.fixtures import frongello_2002_table3
periods, expected = frongello_2002_table3()
attrs = [bhb(segs, period=label) for label, segs in periods]
linked = link_frongello(attrs)
assert abs(linked.allocation - expected["frongello_linked_allocation"]) < 1e-10
frongello_2002_table1_identical()#
3 identical periods from Frongello (2002) Table 1. Frongello endnote 13 pins this as the special case where the Frongello, Cariño, and Menchero methods produce identical results — pybrinson's consistency tests verify exactly this.
karnosky_singer_3segment()#
3-region US / UK / Japan example for the currency attribution 4-effect decomposition. Constructed for pedagogical clarity so the additive consistency \(R_p = R^L + c\) holds exactly.
from pybrinson import currency_attribution
from pybrinson.fixtures import karnosky_singer_3segment
segments, expected = karnosky_singer_3segment()
result = currency_attribution(segments)
assert abs(result.excess_return - expected["excess_return"]) < 1e-12
two_period_linking_example()#
2-period 2-sector example small enough to derive every linking
method by hand. Backs the cross-method consistency suite in
tests/consistency/.
Each per-period BHB closes exactly; compounded portfolio return is 21.5%, benchmark 18.25%, arithmetic excess 3.25%, geometric excess ≈ 2.7484%.
Return shape#
Each fixture returns a 2-tuple:
(segments, expected)for single-period fixtures.(periods, expected)for multi-period fixtures, whereperiodsistuple[(period_label, segments), ...].
expected is a dict with the published numbers — totals,
per-segment breakdowns, and linking outcomes depending on the
fixture.
Adding your own#
The fixture module is pure data. If you reproduce a worked example from the literature that is not yet covered, the pattern is:
def my_paper_table_4() -> tuple[tuple[Segment, ...], dict[str, float]]:
"""3-sector example from Smith & Jones (2020), Table 4.
Source: Smith, J., & Jones, K. (2020). ...
DOI: 10.1234/jpm.2020.xyz
https://doi.org/10.1234/jpm.2020.xyz
"""
segments = (
Segment("A", ...),
...
)
expected = {
"portfolio_return": ...,
"benchmark_return": ...,
"excess_return": ...,
# Numbers taken DIRECTLY from the paper, not recomputed by pybrinson.
"allocation": ...,
"selection": ...,
"interaction": ...,
}
return segments, expected
The rule is that expected values come from the paper, never
from what pybrinson produces — otherwise the fixture is circular
and has no value.