Portfolio return attribution in Python: Brinson-Hood-Beebower, Brinson-Fachler, and multi-period linking.
Project description
pybrinson
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.0 — single-period BHB and Brinson-Fachler plus Cariño / GRAP / geometric multi-period linking. Single-level classification only; nested hierarchies are scheduled for v1.1.
Methods supported
| Method | Reference | |
|---|---|---|
| Single-period | Brinson-Hood-Beebower (3-effect) | Brinson, Hood & Beebower (1986) |
| Single-period | Brinson-Fachler (3-effect) | Brinson & Fachler (1985) |
| Multi-period linking | Cariño log-smoothing | Cariño (1999) |
| Multi-period linking | GRAP factors | GRAP (1997) |
| Multi-period linking | Geometric (Bacon) | Bacon (2008), chap. 6 |
Deferred to v1.1+: Menchero / Frongello linking, multi-level (nested) classification, currency attribution.
Install
pip install pybrinson
pybrinson has zero runtime dependencies — just the Python standard
library. Requires Python 3.14+.
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 / geometric linking.
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 |
| Geometric linking | 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.
- 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
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file pybrinson-1.0.0.tar.gz.
File metadata
- Download URL: pybrinson-1.0.0.tar.gz
- Upload date:
- Size: 19.2 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
338d910348b1da85f620c3ded93ede364b6fb4c8900896305893537d39714fb7
|
|
| MD5 |
03573f1c273ff5e1575f65828b7d79b7
|
|
| BLAKE2b-256 |
2b218b86c40f35a6935a001930016a5be43d110425a79b62bb98f03d46fa191e
|
Provenance
The following attestation bundles were made for pybrinson-1.0.0.tar.gz:
Publisher:
release.yml on gghez/pybrinson
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pybrinson-1.0.0.tar.gz -
Subject digest:
338d910348b1da85f620c3ded93ede364b6fb4c8900896305893537d39714fb7 - Sigstore transparency entry: 1280178553
- Sigstore integration time:
-
Permalink:
gghez/pybrinson@31f0ce003cb7ebaba2c94cf2432442a2d367edfa -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/gghez
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@31f0ce003cb7ebaba2c94cf2432442a2d367edfa -
Trigger Event:
push
-
Statement type:
File details
Details for the file pybrinson-1.0.0-py3-none-any.whl.
File metadata
- Download URL: pybrinson-1.0.0-py3-none-any.whl
- Upload date:
- Size: 22.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
37a571660b326c5c6606435372655e068b98b0ebbcb2e6cbc1312344c2b32dc5
|
|
| MD5 |
90a9d5de85bd3a65f6fd50ff81f52090
|
|
| BLAKE2b-256 |
c20466f1f50982c45a4f5db916adac29a60f539d64fd286cd159f0516ffe14fc
|
Provenance
The following attestation bundles were made for pybrinson-1.0.0-py3-none-any.whl:
Publisher:
release.yml on gghez/pybrinson
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
pybrinson-1.0.0-py3-none-any.whl -
Subject digest:
37a571660b326c5c6606435372655e068b98b0ebbcb2e6cbc1312344c2b32dc5 - Sigstore transparency entry: 1280178556
- Sigstore integration time:
-
Permalink:
gghez/pybrinson@31f0ce003cb7ebaba2c94cf2432442a2d367edfa -
Branch / Tag:
refs/tags/v1.0.0 - Owner: https://github.com/gghez
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@31f0ce003cb7ebaba2c94cf2432442a2d367edfa -
Trigger Event:
push
-
Statement type: