Skip to main content

Portfolio return attribution in Python: Brinson-Hood-Beebower, Brinson-Fachler, and multi-period linking.

Project description

pybrinson

CI codecov License: MIT

Portfolio return attribution in Python — with sources you can audit.

pybrinson decomposes a portfolio's excess return versus a benchmark into allocation, selection, and interaction effects, across any user-defined classification (sector, country, asset class). Every formula ships with its mathematical statement, an academic citation, and a clickable URL — readers can verify the math without leaving the file.

It targets the gap left by the existing Python finance stack: R has the pa package on CRAN and MATLAB ships brinsonAttribution, but no maintained PyPI package implements the Brinson family of models.

Status

v1.3 — adds Karnosky-Singer single-period currency attribution (4-effect decomposition: market allocation, security selection, currency allocation, interaction) under the additive convention R_p = R_L + c. Segment gains optional local_return / currency_return fields; BHB and Brinson-Fachler call sites are unchanged.

v1.2 — adds Menchero (2000, 2004) optimised multi-period linking (patent US 7,249,082 B2 expired 2024-02-18), second-source literature fixtures from Frongello (2002), and a ppar cross-validation script for the Cariño coefficient. Also fixes the v1.1 Frongello implementation to match the published convention (prefix portfolio × suffix benchmark, per the recursion on pp. 4-5 of Frongello 2002).

Methods supported

Method Reference
Single-period Brinson-Hood-Beebower (3-effect) Brinson, Hood & Beebower (1986)
Single-period Brinson-Fachler (3-effect) Brinson & Fachler (1985)
Single-period Multi-level hierarchical roll-up (any depth) Bacon (2008), chap. 5
Multi-period linking Cariño log-smoothing Cariño (1999)
Multi-period linking GRAP factors GRAP (1997)
Multi-period linking Frongello recursive Frongello (2002)
Multi-period linking Menchero optimised Menchero (2000, 2004)
Multi-period linking Geometric (Bacon) Bacon (2008), chap. 6
Currency Karnosky-Singer (4-effect, additive) Karnosky & Singer (1994)

The Brinson family is considered feature-complete as of v1.3. No standing roadmap for further additions: items that are not Brinson-family math (fixed-income Campisi, risk-attribution, factor attribution) are out of scope and belong in sibling packages. DataFrame adapters (pandas / polars) are not and will not be part of pybrinson — see the "no pandas / no numpy / no polars" rule below. If you have a concrete Brinson-family feature request, open an issue.

Install

pip install pybrinson

pybrinson has zero runtime dependencies — just the Python standard library. Requires Python 3.14+.

Absolute rule: no pandas / no numpy / no polars — ever

pybrinson will never depend on pandas, numpy, or polars. Not as runtime, not as optional, not as dev. They are banned in this repository: their cross-version-compat churn propagates to every downstream consumer and contradicts the zero-dependency positioning. If a hot path ever needs performance beyond pure Python, the performance-sensitive code is written in Rust in-repo (via PyO3 + maturin) and exposed as a native extension module — never by reaching for a DataFrame library.

Quickstart

from pybrinson import Segment, bhb

segments = [
    Segment("UK Equity", portfolio_weight=0.40, benchmark_weight=0.40,
            portfolio_return=0.20, benchmark_return=0.10),
    Segment("Japan Equity", portfolio_weight=0.30, benchmark_weight=0.20,
            portfolio_return=-0.05, benchmark_return=-0.04),
    Segment("US Equity", portfolio_weight=0.30, benchmark_weight=0.40,
            portfolio_return=0.06, benchmark_return=0.08),
]

result = bhb(segments, period="2024-Q1")
print(result)
BHB attribution — period 2024-Q1
  R_p = 8.3000%   R_b = 6.4000%   excess = 1.9000%

Segment       Allocation  Selection  Interaction     Total
------------  ----------  ---------  -----------  --------
UK Equity        0.0000%    4.0000%      0.0000%   4.0000%
Japan Equity    -0.4000%   -0.2000%     -0.1000%  -0.7000%
US Equity       -0.8000%   -0.8000%      0.2000%  -1.4000%
Total           -1.2000%    3.0000%      0.1000%   1.9000%

The identity allocation + selection + interaction == excess_return holds within 1e-9 by construction; pybrinson raises AttributionError rather than storing a silent residual when it fails.

Multi-period linking

from pybrinson import bhb, link_carino, Segment

period_attrs = [
    bhb([Segment("Equities", 0.6, 0.5, 0.10, 0.05),
         Segment("Bonds",    0.4, 0.5, 0.05, 0.10)], period="P1"),
    bhb([Segment("Equities", 0.5, 0.5, 0.20, 0.10),
         Segment("Bonds",    0.5, 0.5, 0.05, 0.10)], period="P2"),
]

print(link_carino(period_attrs))

See examples/ for runnable scripts covering BHB, Brinson-Fachler, and Cariño / GRAP / Frongello / geometric linking.

Multi-level hierarchies (v1.1+)

Each Segment may declare an immediate parent label. For chains deeper than one level, pass a parents={parent: grandparent} mapping to bhb() / fachler():

from pybrinson import Segment, bhb

leaves = [
    Segment("UK",      0.20, 0.25,  0.10,  0.08, parent="Europe"),
    Segment("Germany", 0.25, 0.20,  0.05,  0.06, parent="Europe"),
    Segment("US",      0.30, 0.35,  0.12,  0.10, parent="Americas"),
    Segment("Brazil",  0.25, 0.20, -0.04, -0.02, parent="Americas"),
]
result = bhb(leaves, parents={"Europe": "Equity", "Americas": "Equity", "Equity": None})

Allocation, selection and interaction roll up additively at every level: each parent equals the sum of its descendants, and the root parent equals the period total.

Reusable cited fixtures (v1.1+)

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

The same fixtures back the test suite — there is one source of truth.

Design principles

  • Pure Python first. Zero runtime dependencies. Realistic attribution inputs are small (≤1k segments × ≤1k periods); a NumPy dependency would not pay for itself.
  • Specification-driven, externally verified. Every function carries its formula, an academic citation, and a clickable URL. The math is cross-checked against published worked examples.
  • No silent residuals. Identity failures raise. Bad inputs raise. pybrinson never imputes, rescales or swallows residuals.
  • Typed and tested. Public API is fully type-annotated; ships py.typed; tests cover both pinned worked examples and randomised identity checks.

Positioning vs ppar / fincore

ppar fincore pybrinson
BHB no yes yes
Brinson-Fachler 3-effect 2-effect only no yes
Cariño linking yes no yes
GRAP linking no no yes
Frongello linking no no yes
Menchero linking no no yes
Geometric linking no no yes
Multi-level hierarchical roll-up no no yes
Public fixture pack of cited examples no no yes
Cross-method consistency suite no no yes
Inline source citations no no mandatory
Identity failure handling n/a silent residual raises
Runtime dependencies 9 2 0

See docs/implementation-v1.md for the audited findings against pinned upstream commits.

Development

This project uses uv and targets Python 3.14.

uv sync                  # install deps, fetch Python 3.14 if needed
uv run pytest            # full test suite
uv run pytest -k bhb     # subset
uv build                 # sdist + wheel into dist/

References

Primary papers cited in the source:

  • Brinson, G. P., Hood, L. R., & Beebower, G. L. (1986). "Determinants of Portfolio Performance." Financial Analysts Journal, 42(4). DOI
  • Brinson, G. P., & Fachler, N. (1985). "Measuring Non-U.S. Equity Portfolio Performance." Journal of Portfolio Management, 11(3). DOI
  • Cariño, D. R. (1999). "Combining Attribution Effects Over Time." Journal of Performance Measurement, 3(4).
  • Groupe de Recherche en Attribution de Performance (1997). Synthèse des modèles d'attribution de performance.
  • Frongello, A. (2002). "Linking Single Period Attribution Results." Journal of Performance Measurement, 6(3).
  • Frongello, A. (2002). "Attribution Linking: Proofed and Clarified." Journal of Performance Measurement, 7(1). Author PDF
  • Menchero, J. (2000). "An Optimized Approach to Linking Attribution Effects Over Time." Journal of Performance Measurement, 5(1).
  • Menchero, J. (2004). "Multiperiod Arithmetic Attribution." Financial Analysts Journal, 60(4). DOI
  • Bacon, C. R. (2008). Practical Portfolio Performance Measurement and Attribution, 2nd ed., Wiley. Wiley page
  • Bacon, C. R. (2019). Performance Attribution: History and Progress. CFA Institute Research Foundation. Free PDF

License

MIT — see LICENSE.

Project details


Download files

Download the file for your platform. If you're not sure which to choose, learn more about installing packages.

Source Distribution

pybrinson-1.3.1.tar.gz (51.2 kB view details)

Uploaded Source

Built Distribution

If you're not sure about the file name format, learn more about wheel file names.

pybrinson-1.3.1-py3-none-any.whl (47.4 kB view details)

Uploaded Python 3

File details

Details for the file pybrinson-1.3.1.tar.gz.

File metadata

  • Download URL: pybrinson-1.3.1.tar.gz
  • Upload date:
  • Size: 51.2 kB
  • Tags: Source
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pybrinson-1.3.1.tar.gz
Algorithm Hash digest
SHA256 e034c0effd674a1e12b5d7a3fd2adc30e432627c9fddbe68227ad1c7f5ea8dcd
MD5 1a8227f15c6829e7b99c1c2e2549de23
BLAKE2b-256 912e000c6f7caf55331cfec05c90f2e056ff4a7fa50858c9bf849034f5999c46

See more details on using hashes here.

Provenance

The following attestation bundles were made for pybrinson-1.3.1.tar.gz:

Publisher: release.yml on gghez/pybrinson

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

File details

Details for the file pybrinson-1.3.1-py3-none-any.whl.

File metadata

  • Download URL: pybrinson-1.3.1-py3-none-any.whl
  • Upload date:
  • Size: 47.4 kB
  • Tags: Python 3
  • Uploaded using Trusted Publishing? Yes
  • Uploaded via: twine/6.1.0 CPython/3.13.12

File hashes

Hashes for pybrinson-1.3.1-py3-none-any.whl
Algorithm Hash digest
SHA256 2401ba2dfaffab5fd28368527be0d9537b2fa3826bc8abc06ccd006384ea532e
MD5 a350d04995ed72b928e03a70ae29ec98
BLAKE2b-256 074e9ee00880ad82c5ff494983d159d1c6bfe9e7896f5a455619595ab9386633

See more details on using hashes here.

Provenance

The following attestation bundles were made for pybrinson-1.3.1-py3-none-any.whl:

Publisher: release.yml on gghez/pybrinson

Attestations: Values shown here reflect the state when the release was signed and may no longer be current.

Supported by

AWS Cloud computing and Security Sponsor Datadog Monitoring Depot Continuous Integration Fastly CDN Google Download Analytics Pingdom Monitoring Sentry Error logging StatusPage Status page