Hierarchical Risk Parity in Python (2026): Run It with PyPortfolioOpt and skfolio, and Check It Before You Trust It

Hierarchical Risk Parity has become the default answer whenever a quant asks “what do I run instead of mean-variance?” In 2026 you can get HRP weights in a handful of lines from either PyPortfolioOpt or skfolio. The harder question is whether the weights that come out mean what you think they mean — because the two libraries describe the mechanics differently, and they ship with different defaults.

E-E-A-T and method note. This guide was written by the QuantBrainAI research desk and is desk research, not a lab report. This review is based on official documentation and published sources — we did not run these libraries against live data in-house. Every number below is either attributed to a named source or explicitly labeled as an illustrative snapshot. Documentation pages were fetched on 2026-10-13.

What HRP Is and Why Quants Reach for It

Hierarchical Risk Parity (HRP) is an allocation method from Marcos López de Prado that builds weights from a hierarchical clustering of assets instead of a covariance-matrix inversion. The PyPortfolioOpt docs claim HRP “does not require the inversion of the covariance matrix” and “seems to produce diverse portfolios that perform well out of sample.”

That second sentence is the entire pitch, and it is worth reading carefully. It is the library’s wording, not an established result — and the word “seems” is doing real work. Mean-variance optimization is fragile because it inverts an estimated covariance matrix, so small estimation errors in correlations get amplified into large, unstable weights. HRP sidesteps the inversion by clustering assets first and allocating capital down the tree. Whether that produces better live allocations is exactly the thing you should test on your own data rather than inherit from a docstring.

How HRP Actually Works: The Four-Step Pipeline

HRP runs in four steps: build a distance matrix from asset correlations, cluster assets into a hierarchical tree, apply seriation to reorder the leaves so adjacent leaves are close, then allocate top-down by recursive bisection. At each split, sub-cluster risk is computed by inverse-risk allocation and a weighting factor updates the cluster weight.

The steps, in the order the skfolio example page presents them:

  1. Distance matrix. Convert the correlation matrix into a distance matrix, so that highly correlated assets sit close together in metric space.
  2. Hierarchical tree clustering. Run hierarchical clustering on that distance matrix to build a dendrogram of asset relationships.
  3. Seriation. Reorder the leaves of the tree so that adjacent leaves are as close as possible. This is the step people skip when they summarize HRP as “just cluster and weight” — it is what makes the recursive pass well-behaved.
  4. Recursive bisection. Allocate top-down. At each split, each sub-cluster’s total risk is computed as an inverse-risk allocation, and a weighting factor derived from the two sub-cluster risks updates the cluster weight.

Note that step 4 is a risk allocation, not an optimizer. There is no objective function being maximized and no expected-return input anywhere in the pipeline. That is the structural reason HRP is more stable than mean-variance — and also why it cannot express a view about which assets will outperform.

Two Libraries, Two Descriptions of the Same Algorithm

PyPortfolioOpt describes HRP as forming minimum-variance mini-portfolios within each branch and combining them node by node. skfolio describes seriation followed by recursive bisection with inverse-risk allocation. Both cite López de Prado (2016). The two write-ups use different vocabulary, and the docs do not say whether the two bisection steps reduce to the same calculation, so check the weights rather than assume they match.

The PyPortfolioOpt “Other Optimizers” docs (v1.5.4) lay it out as: form a distance matrix from asset correlations; cluster assets into a tree via hierarchical clustering; within each branch, form the minimum-variance portfolio (normally between two assets); then iterate over each level, combining mini-portfolios at each node.

The skfolio page describes the same lineage in the seriation-plus-bisection language above. Both point back to the same paper — López de Prado, M. (2016), “Building Diversified Portfolios that Outperform Out of Sample,” The Journal of Portfolio Management, 42(4), 59–69. The PyPortfolioOpt docs also carry the note “Code reproduced with permission from Marcos Lopez de Prado (2016).”

If you are comparing HRP outputs across libraries, you are comparing two implementations whose docs describe their internals differently. Treat that as a prompt to inspect weights rather than a reason to pick a side. One caveat on sourcing: the SSRN abstract page for the original paper (abstract_id=2708678) could not be fetched for this review, so we cite only the journal reference and the PyPortfolioOpt docs.

Quick Start: HRP with PyPortfolioOpt (Runnable Code)

PyPortfolioOpt exposes HRP as HRPOpt(returns=...). Call optimize(linkage_method='single') to cluster with SciPy, then clean_weights() for rounded weights and portfolio_performance(verbose=True, risk_free_rate=0.02, frequency=252) for annualized metrics. All hierarchical classes share an EfficientFrontier-like API, and save_weights_to_file() writes results to disk.

import pandas as pd
import yfinance as yf
from pypfopt import HRPOpt

TICKERS = ["NVDA", "AVGO", "AMD", "MU", "TSM", "ASML", "AMAT", "LRCX",
           "KLAC", "INTC", "QCOM", "ARM", "MRVL", "WDC", "SMCI"]

prices = yf.download(TICKERS, start="2019-01-01", auto_adjust=True)["Close"]
returns = prices.pct_change().dropna()

hrp = HRPOpt(returns=returns)
weights = hrp.optimize(linkage_method="single")  # SciPy hierarchical clustering
cleaned = hrp.clean_weights()
print(cleaned)

hrp.portfolio_performance(verbose=True, risk_free_rate=0.02, frequency=252)
hrp.save_weights_to_file("hrp_weights.csv")  # optional

The linkage_method='single' argument is not decoration — it is a real modeling choice, and it is the default here. Hold that thought for the defaults section.

Quick Start: HRP with CVaR in skfolio (Runnable Code)

skfolio follows the scikit-learn estimator pattern: construct HierarchicalRiskParity with a risk measure and portfolio parameters, call .fit(X_train), read .weights_, then .predict(...) on new data. The documented example uses CVaR rather than variance, which is where the library’s flexibility shows.

from sklearn.model_selection import train_test_split
from skfolio import RiskMeasure
from skfolio.datasets import load_sp500_dataset
from skfolio.optimization import HierarchicalRiskParity
from skfolio.preprocessing import prices_to_returns

prices = load_sp500_dataset()
X = prices_to_returns(prices)

X_train, X_test = train_test_split(X, test_size=0.33, shuffle=False)

model = HierarchicalRiskParity(
    risk_measure=RiskMeasure.CVAR,
    portfolio_params=dict(name="HRP-CVaR-Ward-Pearson"),
)
model.fit(X_train)
print(model.weights_)

pred = model.predict(X_test)
model.plot_contribution(measure=RiskMeasure.CVAR)
model.seriation_estimator_.hierarchical_clustering_estimator_.plot_dendrogram(
    heatmap=False
)

Two things are worth pulling out of that setup. The split is chronological (shuffle=False), which is the correct choice for anything time-series shaped. And the risk measure is CVaR, not variance — meaning skfolio is running a materially different allocation problem than the classic 2016 formulation.

From the example page’s own run, the 20 training weights sum to 1.0 (first weight 0.0503, maximum 0.0862). The page’s comparison table also lists effective-number-of-assets values of 17.3–17.6 in its first four columns and 20.0 in the last; the column labels on that page determine what the gap means, so check them before reading anything into it. These are illustrative numbers from that page’s run, attributed to it — not results we reproduced.

Defaults That Diverge from the Original Paper

The original paper used variance as the risk measure and single-linkage clustering. PyPortfolioOpt’s optimize() defaults to linkage_method='single', matching that. skfolio generalizes to multiple risk measures and linkages and defaults to Ward variance minimization, which its docs call “more stable and has better properties than the single-linkage method.”

So the two libraries ship with different defaults for the same-named algorithm, and neither doc explains why. That is the single most important practical fact in this post:

  • Original paper: variance risk measure, single-linkage clustering.
  • PyPortfolioOpt: optimize(linkage_method='single') per the v1.5.4 docs — consistent with the paper.
  • skfolio: defaults to Ward linkage, which minimizes within-cluster variance, and its docs describe Ward as more stable than single linkage.

The skfolio page also notes that single linkage “is equivalent to the minimum spanning tree, which suffers from the chaining effect.” Chaining means clusters can be linked by a single pair of nearby points, producing long, stringy clusters that may not reflect genuine structure in your correlation matrix. If you accept skfolio’s default and then wonder why your weights look different from a PyPortfolioOpt run, the linkage method is the first place to look.

HRP vs. Mean-Variance vs. Risk Parity: Comparison Table

Dimension HRP Mean-variance (Markowitz) Risk parity
Covariance-matrix inversion Not required — the PyPortfolioOpt docs claim HRP “does not require the inversion of the covariance matrix” Required Depends on the formulation
Allocation logic Distance matrix → hierarchical clustering → seriation → recursive bisection with inverse-risk allocation at each split Single global optimization over expected returns and covariance Inverse-risk weighting, often inverse-volatility across assets
Out-of-sample diversity The PyPortfolioOpt docs claim HRP “seems to produce diverse portfolios that perform well out of sample” — a library claim, not verified here Sensitive to estimation error, particularly in expected returns Not addressed by a comparable claim on the pages reviewed here
Python support pypfopt.HRPOpt; skfolio.optimization.HierarchicalRiskParity pypfopt.EfficientFrontier and skfolio’s mean-risk estimators Not covered by the two documentation pages reviewed here
Tunable defaults PyPortfolioOpt: linkage_method='single'. skfolio: Ward linkage, configurable risk measure Expected-return estimator and covariance estimator Risk-measure choice
Inputs needed Returns (or covariance); no expected-return forecast Expected returns + covariance Return covariance / volatilities

What to Check Before Trusting HRP Output

Before allocating on HRP weights, verify five things: which linkage method your library is actually using; which risk measure is active; whether the dendrogram shows chaining; the effective number of assets versus a naive baseline; and whether the “out-of-sample” claim came from documentation rather than your own testing.

Walk the checklist:

(a) Which linkage method is running? Single linkage equals the minimum spanning tree and suffers the chaining effect per the skfolio page. Ward minimizes within-cluster variance and skfolio’s docs call it more stable. PyPortfolioOpt defaults to single. These are not cosmetic differences — they change which assets end up in which cluster, and therefore the weights.

(b) Which risk measure? Variance is the 2016 default. skfolio lets you pass RiskMeasure.CVAR, and the example does. CVaR-based clustering and allocation will not match a variance-based run, even on identical inputs.

(c) Inspect the dendrogram. Call plot_dendrogram(heatmap=False) via seriation_estimator_.hierarchical_clustering_estimator_ and actually look at it. Long, stringy branches are the visual signature of chaining. If your tree looks like a chain rather than a hierarchy, seriation is reordering noise.

(d) Count effective assets. Compute effective-N for your own weights and compare it with a naive baseline. The skfolio example page reports effective-N values of 17.3–17.6 and 20.0 in its comparison table, but only its column labels say which model is which.

(e) Re-read the claim. “Seems to produce diverse portfolios that perform well out of sample” is the PyPortfolioOpt docs’ wording. It is a claim, not a reproduced result. The same discipline applies here as anywhere else: run your own walk-forward test before capital moves.

FAQ

This section answers the three questions readers raise most often after reading the two library docs side by side — covering covariance inversion, the linkage-default mismatch, and whether you can swap variance for another risk measure.

Does HRP require inverting the covariance matrix?

No — per the PyPortfolioOpt docs, HRP “does not require the inversion of the covariance matrix as with traditional mean-variance optimization.” That is the library’s claim about the method. It is also structurally true of the algorithm as described: recursive bisection computes sub-cluster risk via inverse-risk allocation, so no matrix inverse appears in the pipeline. The practical consequence is that HRP is less exposed to the estimation-error amplification that makes mean-variance weights unstable.

Why does skfolio default to Ward linkage while PyPortfolioOpt defaults to single linkage?

The docs do not give a reason for the split. skfolio’s docs describe Ward as “more stable and has better properties than the single-linkage method,” while noting single linkage “is equivalent to the minimum spanning tree, which suffers from the chaining effect.” PyPortfolioOpt’s documented optimize(linkage_method='single') follows the original paper’s single-linkage setup more closely. Neither default is wrong — but if you do not set linkage_method explicitly, you are inheriting a choice you did not make.

Can I use a risk measure other than variance with HRP?

Yes in skfolio. Its estimator takes risk_measure=RiskMeasure.CVAR, and the official example runs exactly that under the portfolio name “HRP-CVaR-Ward-Pearson.” The original paper used variance, so CVaR is a generalization the library adds rather than a reproduction of the 2016 method. PyPortfolioOpt’s HRPOpt is built around the returns/covariance interface instead; the PyPortfolioOpt docs page reviewed here does not list a risk-measure parameter.

The Bottom Line

Both libraries make HRP a five-minute job; the real work is verifying which variant you actually ran, because the two ship with different defaults and describe the same algorithm differently.

skfolio is the more flexible choice if you want non-variance risk measures like CVaR, alternative linkage methods, and a scikit-learn-shaped API (fit/predict/weights_) that slots into existing pipelines. Per its GitHub README, it is built on top of scikit-learn, licensed BSD-3-Clause, sits at roughly 2.5k stars, and has a companion arXiv paper — arXiv:2507.04176 (Nicolini, Manzi, Delatte, 2025). PyPortfolioOpt is the faster path if you already use its EfficientFrontier-style API and want the documented single-linkage behavior that tracks the original paper.

What you should not do is treat either output as validated. HRP avoids a known failure mode of mean-variance — covariance inversion under estimation error — and that is a genuine structural argument. The diversification and out-of-sample benefits, however, come to you as documentation claims. Run your own walk-forward backtest, check the dendrogram, and confirm your linkage method and risk measure before you size a position.

How This Guide Was Built

This is desk research based on the documentation and sources listed below, fetched on 2026-10-13. No in-house backtest was run, no code was executed against live data, and no out-of-sample performance numbers were reproduced. The SSRN abstract page for the original paper could not be fetched, so the paper is cited only via its journal reference. The 2026-10-13 semiconductor prices in the PyPortfolioOpt example are a live yfinance snapshot used purely as an illustration of the ticker set — they are not returns and not a performance claim.

Sources consulted (fetched 2026-10-13):

  1. skfolio — HRP with CVaR example (primary source)
  2. PyPortfolioOpt — “Other Optimizers”, v1.5.4 (primary source)
  3. skfolio GitHub README
  4. skfolio — HierarchicalRiskParity API reference (primary source)
  5. skfolio home page
  6. PyPortfolioOpt GitHub repository (primary source)
  7. PyPortfolioOpt HRP cookbook notebook
  8. skfolio: Portfolio Optimization in Python, arXiv:2507.04176 (primary source)
  9. SciPy — scipy.cluster.hierarchy.linkage (primary source)
  10. SciPy — hierarchical clustering module
  11. López de Prado, M. (2016). “Building Diversified Portfolios that Outperform Out of Sample.” The Journal of Portfolio Management, 42(4), 59–69. (Journal citation only; the SSRN page could not be fetched.)
← Back to all posts