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.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.
Metadata
Release files for pybrinson 1.3.1
For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.
Source distribution (sdist)
| File | Size | Uploaded | |
|---|---|---|---|
| pybrinson-1.3.1.tar.gz | 51.2 kB | Details |
Built distribution (wheel)
| File | Interpreter | ABI | Platform | Reset |
|---|---|---|---|---|
| pybrinson-1.3.1-py3-none-any.whl | Python 3 | none | any | Details |
Total release size: 98.5 kB
Release files / pybrinson-1.3.1.tar.gz
| Download URL | pybrinson-1.3.1.tar.gz |
|---|---|
| Size | 51.2 kB |
| Tags | Source |
|
SHA-256 checksum How to use checksums |
e034c0effd674a1e12b5d7a3fd2adc30e432627c9fddbe68227ad1c7f5ea8dcd
|
|
BLAKE2b-256 checksum How to use checksums |
912e000c6f7caf55331cfec05c90f2e056ff4a7fa50858c9bf849034f5999c46
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 12, 2026.
Transparency logRelease files / pybrinson-1.3.1-py3-none-any.whl
| Download URL | pybrinson-1.3.1-py3-none-any.whl |
|---|---|
| Size | 47.4 kB |
| Tags | Python 3 |
|
SHA-256 checksum How to use checksums |
2401ba2dfaffab5fd28368527be0d9537b2fa3826bc8abc06ccd006384ea532e
|
|
BLAKE2b-256 checksum How to use checksums |
074e9ee00880ad82c5ff494983d159d1c6bfe9e7896f5a455619595ab9386633
|
| Upload date | |
|
Uploaded using Trusted Publishing? What is trusted publishing? |
Yes |
| Uploaded via |
twine/6.1.0 CPython/3.13.12
|
Provenance
Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.
PyPI Publish Attestation
PyPI verified that this artifact, at this checksum, originated from the publisher listed below.
Signed by GitHub Actions, verified by PyPI on Apr 12, 2026.
Transparency log