Clusters
PortfolioOptimisers.Clusters — Type
struct Clusters{__T_res, __T_S, __T_D, __T_P, __T_k} <: AbstractClusteringResultCarries a clustering of the asset universe together with the matrices it was computed from.
Holds the result of either family, because res takes both shapes clusterise produces: a Clustering.Hclust from a hierarchical algorithm and a Clustering.ClusteringResult from a non-hierarchical one. k is the number of clusters selected by an AbstractOptimalNumberClustersEstimator, and assignments is what reads a cluster label per asset off the pair — cutting the tree at k on the hierarchical branch, reading the field on the other.
D is what the clustering saw, unless P is present
S and D are the similarity and distance matrices of the universe, and an ordinary ClustersEstimator clusters D itself and leaves P as nothing. A NetworkClustersEstimator instead accumulates a pseudo-distance matrix out of the network structure and clusters that, so it fills P and res is the clustering of P. Both matrices are kept because a consumer wants the network's clustering and the universe's own distances.
Fields
res: Clustering result.
S: Similarity matrix.
D: Distance matrix.
P: Pseudo-distance matrix the clustering was run on,nothingwhen the clustering ran onDitself. ANetworkClustersEstimatorbuilds it by accumulating the network structure out of the distance or similarity matrix; seeclusterise.
k: Optimal number of clusters.
Constructors
Clusters(; res::ClTypes, S::MatNum, D::MatNum, P::Option{<:MatNum} = nothing, k::Integer) -> ClustersKeywords correspond to the struct's fields.
Validation
!isempty(S).!isempty(D).size(S) == size(D).- If
Pis notnothing,!isempty(P)andsize(S) == size(P). k >= 1.
Related
PortfolioOptimisers.clusterise — Method
clusterise(cle::AbstractClusteringResult, args...; kwargs...)Return the clustering result cle unchanged.
Identity pass-through, so that every consumer takes an estimator or a precomputed result through one call. A ClustersEstimator reaching this function has already been run; a result reaching it is not run again.
Arguments
cle: Clustering result.args...: Additional positional arguments, ignored.kwargs...: Additional keyword arguments, ignored.
Returns
cle::AbstractClusteringResult: The input, unchanged.
Related
PortfolioOptimisers.SecondOrderDifference — Type
struct SecondOrderDifference{__T_alg} <: AbstractOptimalNumberClustersAlgorithmPicks the number of clusters at which the within-cluster dispersion curve bends most sharply.
The two-difference gap statistic. It reads the second-order difference of the within-cluster dispersion across cluster counts, so it finds the elbow of that curve without the Monte Carlo simulation the original gap statistic needs.
Mathematical definition
\[\begin{align} c^{\star} &= \underset{c}{\arg\max} \left(W_{c+2} + W_{c} - 2 W_{c+1}\right)\,,\\ &\mathrm{s.t.} \quad 1 \leq c \leq \sqrt{N}\,,\\ W_{c} &= \sum_{j=1}^{c} g\left(\left\{d_{uv} : u, v \in \mathcal{C}_{j},\, u < v\right\}\right)\,. \end{align}\]
Where:
- $c^{\star}$: Selected number of clusters.
- $W_{c}$: Within-cluster dispersion of a cut into $c$ clusters.
- $\mathcal{C}_{j}$: Set of assets in the $j$-th cluster of that cut.
- $d_{uv}$: Entry of the distance matrix the clustering ran on.
- $g$: Vector-to-scalar measure
alg, applied to one cluster's pairwise distances. A cluster of one asset has no pairwise distance and contributes $0$. - $N$: Number of assets.
The selected $c$ is the left end of the triple, not its centre. That is the source's own statement of the problem, and the code maximises it as written.
The measure decides what "dispersion" means, and the default is not the source's
The source takes $W_{c}$ as the mean of a cluster's pairwise distances, which is alg = MeanValue(). The default alg = StandardisedValue() divides that mean by the corrected standard deviation of the same distances, which is a different statistic and selects a different $c$: on a 400x40 sample the two answer 6 and 4.
A cluster of exactly two assets carries one pairwise distance, and a single value has no corrected standard deviation. StandardisedValue divides by $1$ in that case, so such a cluster contributes its mean pairwise distance to $W_{c}$. The gap series stays finite, and the selected $c$ is a real maximiser. The two measures still differ on every larger cluster.
Fields
alg: The measure used to evaluate clustering quality.
Constructors
SecondOrderDifference(; alg::Num_VecToScaM = StandardisedValue()) -> SecondOrderDifferenceKeywords correspond to the struct's fields.
Propagated parameters
When factory is called on this type, the following @fprop-tagged fields are automatically propagated:
alg: Recursively updated viafactory.
Examples
julia> SecondOrderDifference()SecondOrderDifference alg ┼ StandardisedValue │ mv ┼ MeanValue │ │ w ┴ nothing │ sv ┼ StdValue │ │ w ┼ nothing │ │ corrected ┴ Bool: trueRelated
AbstractOptimalNumberClustersAlgorithmOptimalNumberClustersSilhouetteScoreVectorToScalarMeasureMeanValueStandardisedValueoptimal_number_clustersfactory
References
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 12.2.1.2, Equation 12.15.
- [47] S. Yue, X. Wang and M. Wei. Application of two-order difference to gap statistic. Transactions of Tianjin University 14, 217–221 (2008).
- [48] R. Tibshirani, G. Walther and T. Hastie. Estimating the number of clusters in a data set via the gap statistic. Journal of the Royal Statistical Society: Series B (Statistical Methodology) 63, 411–423 (2001).
PortfolioOptimisers.SilhouetteScore — Type
struct SilhouetteScore{__T_alg} <: AbstractOptimalNumberClustersAlgorithmPicks the number of clusters whose assets sit best inside their own cluster.
Each asset gets a silhouette, which compares how far it sits from its own cluster against how far it sits from the nearest other one. alg reduces the whole vector of them to one number per cluster count, and the count with the largest number wins.
Mathematical definition
\[\begin{align} s_{i} &= \frac{b_{i} - a_{i}}{\max\left(a_{i},\, b_{i}\right)}\,,\\ a_{i} &= \sum_{j \in \mathcal{C}_{I},\, j \neq i} \frac{d_{ij}}{\left|\mathcal{C}_{I}\right| - 1}\,,\\ b_{i} &= \underset{J \neq I}{\min} \sum_{j \in \mathcal{C}_{J}} \frac{d_{ij}}{\left|\mathcal{C}_{J}\right|}\,,\\ c^{\star} &= \underset{c}{\arg\max} \; g\left(\boldsymbol{s}\right)\,, \quad 1 \leq c \leq \sqrt{N}\,. \end{align}\]
Where:
- $s_{i}$: Silhouette of asset $i$, in $\left[-1,\, 1\right]$. It is $1$ when the asset is clustered well and $-1$ when it is clustered wrongly.
- $a_{i}$: Mean distance from asset $i$ to the other members of its own cluster $\mathcal{C}_{I}$.
- $b_{i}$: Smallest mean distance from asset $i$ to the members of any other cluster.
- $d_{ij}$: Entry of the distance matrix the clustering ran on.
- $\boldsymbol{s}$: Vector of silhouettes over all assets, one per asset.
- $g$: Vector-to-scalar measure
alg. - $c^{\star}$: Selected number of clusters.
- $N$: Number of assets.
The default reduction is the source's standardised score
alg = StandardisedValue() divides the mean of $\boldsymbol{s}$ by its corrected standard deviation, which is the source's quality measure term for term. The standardisation is what makes scores comparable across cluster counts, so it is the reduction the selection wants; the field is a knob only because a caller may want the plain mean instead. The two disagree in general, and the standardised form is the one this default computes.
Unlike SecondOrderDifference, the reduction here runs over one vector of length $N$, never over one cluster at a time, so no cluster size makes it undefined.
Fields
alg: The measure used to evaluate clustering quality.
Constructors
SilhouetteScore(; alg::Num_VecToScaM = StandardisedValue()) -> SilhouetteScoreKeywords correspond to the struct's fields.
Propagated parameters
When factory is called on this type, the following @fprop-tagged fields are automatically propagated:
alg: Recursively updated viafactory.
Examples
julia> SilhouetteScore()SilhouetteScore alg ┼ StandardisedValue │ mv ┼ MeanValue │ │ w ┴ nothing │ sv ┼ StdValue │ │ w ┼ nothing │ │ corrected ┴ Bool: trueRelated
AbstractOptimalNumberClustersAlgorithmOptimalNumberClustersSecondOrderDifferenceVectorToScalarMeasureStandardisedValueoptimal_number_clustersfactory
References
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 12.2.1.1, Equations 12.13-12.14.
- [49] P. J. Rousseeuw. Silhouettes: a graphical aid to the interpretation and validation of cluster analysis. Journal of Computational and Applied Mathematics 20, 53–65 (1987).
- [50] M. López de Prado and M. J. Lewis. Detection of false investment strategies using unsupervised learning methods. Quantitative Finance 19, 1555–1565 (2019).
PortfolioOptimisers.OptimalNumberClusters — Type
struct OptimalNumberClusters{__T_max_k, __T_alg} <: AbstractOptimalNumberClustersEstimatorDecides how many clusters to cut a dendrogram or a partition into.
Pairs a selection rule with a ceiling on the answer. alg is either an AbstractOptimalNumberClustersAlgorithm, which chooses the count from the data, or an Integer, which states it outright.
A stated k is a request, not a guarantee
max_k caps every branch, and the cap is itself capped: the ceiling in force is min(floor(Int, sqrt(N)), max_k), so a max_k above $\sqrt{N}$ buys nothing. A stated k above that ceiling is lowered to it.
A stated k is also checked against the tree on the hierarchical branch. A cut into k clusters that no node of the dendrogram supports is not honoured; optimal_number_clusters searches upward and downward for the nearest k that is, and takes the nearer of the two. The data-driven branches apply the same test through valid_k_clusters, which walks the scoring array from its largest entry down until one passes.
Fields
max_k: Maximum number of clusters to consider. Ifnothing, computed as thefloor(Int, sqrt(assets)).
alg: Algorithm for selecting the optimal number of clusters. If an integer, defines the number of clusters directly.
Constructors
OptimalNumberClusters(; max_k::Option{<:Integer} = nothing, alg::Int_ONC = SecondOrderDifference()) -> OptimalNumberClustersKeywords correspond to the struct's fields.
Validation
- If
max_kis notnothing,max_k >= 1. - If
algis anInteger,alg >= 1.
Propagated parameters
When factory is called on this type, the following @fprop-tagged fields are automatically propagated:
alg: Recursively updated viafactory.
Examples
julia> OptimalNumberClusters(; max_k = 10)OptimalNumberClusters max_k ┼ Int64: 10 alg ┼ SecondOrderDifference │ alg ┼ StandardisedValue │ │ mv ┼ MeanValue │ │ │ w ┴ nothing │ │ sv ┼ StdValue │ │ │ w ┼ nothing │ │ │ corrected ┴ Bool: trueRelated
AbstractOptimalNumberClustersEstimatorAbstractOptimalNumberClustersAlgorithmSecondOrderDifferenceSilhouetteScoreoptimal_number_clustersvalid_k_clustersfactory
References
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 12.2.1.
PortfolioOptimisers.HClustAlgorithm — Type
struct HClustAlgorithm{__T_linkage} <: AbstractHierarchicalClusteringAlgorithmBuilds a dendrogram by merging the two nearest clusters until one remains.
linkage is the criterion that says how near two clusters are once they hold more than one asset. :single takes the smallest distance between their members and :complete the largest; :average takes the mean over all cross pairs; :ward merges the pair that raises the total within-cluster variance least. The symbol is passed straight to Clustering.hclust, so every criterion that package accepts is available.
Fields
linkage: Linkage method for hierarchical clustering fromClustering.jl.
Constructors
HClustAlgorithm(; linkage::Symbol = :ward) -> HClustAlgorithmKeywords correspond to the struct's fields.
Examples
julia> HClustAlgorithm(; linkage = :average)HClustAlgorithm linkage ┴ Symbol: :averageRelated
References
PortfolioOptimisers.ClustersEstimator — Type
struct ClustersEstimator{__T_ce, __T_de, __T_alg, __T_onc} <: AbstractClustersEstimatorTurns a return matrix into a clustering of the asset universe.
Holds the four steps in the order they run: ce estimates the covariance, de turns it into a distance matrix, alg clusters that matrix, and onc decides how many clusters to keep. clusterise runs them and returns a Clusters.
The universe is clustered, not the observations
Every step reads an assets x assets matrix, so the number of observations leaves the picture at ce. That is why alg and onc carry no @fprop tag: an ObsWeights weights observations, and there is no observation left for it to weight once de has run. ce and de are tagged, and they are where a weight belongs.
Fields
ce: Covariance estimator.
de: Distance matrix estimator.
alg: Clustering algorithm.
onc: Optimal number of clusters estimator.
Constructors
ClustersEstimator(; ce::StatsBase.CovarianceEstimator = PortfolioOptimisersCovariance(), de::AbstractDistanceEstimator = Distance(; alg = CanonicalDistance()), alg::AbstractClustersAlgorithm = HClustAlgorithm(), onc::AbstractOptimalNumberClustersEstimator = OptimalNumberClusters()) -> ClustersEstimatorKeywords correspond to the struct's fields.
Propagated parameters
When factory is called on this type, the following @fprop-tagged fields are automatically propagated:
Examples
julia> ClustersEstimator()ClustersEstimator ce ┼ PortfolioOptimisersCovariance │ ce ┼ Covariance │ │ me ┼ SimpleExpectedReturns │ │ │ w ┴ nothing │ │ ce ┼ GeneralCovariance │ │ │ ce ┼ StatsBase.SimpleCovariance: StatsBase.SimpleCovariance(true) │ │ │ w ┴ nothing │ │ alg ┴ FullMoment() │ mp ┼ MatrixProcessing │ │ pdm ┼ Posdef │ │ │ alg ┼ UnionAll: NearestCorrelationMatrix.Newton │ │ │ kwargs ┴ @NamedTuple{}: NamedTuple() │ │ dn ┼ nothing │ │ dt ┼ nothing │ │ alg ┼ nothing │ │ order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg) de ┼ Distance │ power ┼ nothing │ alg ┴ CanonicalDistance() alg ┼ HClustAlgorithm │ linkage ┴ Symbol: :ward onc ┼ OptimalNumberClusters │ max_k ┼ nothing │ alg ┼ SecondOrderDifference │ │ alg ┼ StandardisedValue │ │ │ mv ┼ MeanValue │ │ │ │ w ┴ nothing │ │ │ sv ┼ StdValue │ │ │ │ w ┼ nothing │ │ │ │ corrected ┴ Bool: trueRelated
AbstractClustersEstimatorAbstractHierarchicalClusteringAlgorithmAbstractNonHierarchicalClusteringAlgorithmAbstractOptimalNumberClustersEstimatorClustersclusterisefactory
References
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 12.1.1.
PortfolioOptimisers.AbstractClustersEstimator — Type
abstract type AbstractClustersEstimator <: AbstractPhylogenyEstimatorAbstract supertype for all clustering estimator types.
All concrete and/or abstract types implementing clustering-based estimation algorithms should be subtypes of AbstractClustersEstimator.
Related
PortfolioOptimisers.AbstractClustersAlgorithm — Type
abstract type AbstractClustersAlgorithm <: AbstractPhylogenyAlgorithmAbstract supertype for all clustering algorithm types.
All concrete and/or abstract types implementing specific clustering algorithms should be subtypes of AbstractClustersAlgorithm.
Related
PortfolioOptimisers.factory — Method
factory(
alg::AbstractClustersAlgorithm,
args...;
kwargs...
) -> AbstractClustersAlgorithm
Return the clustering algorithm alg unchanged.
Identity pass-through used when a clustering algorithm is provided in a context that calls factory.
Related
PortfolioOptimisers.AbstractHierarchicalClusteringAlgorithm — Type
abstract type AbstractHierarchicalClusteringAlgorithm <: AbstractClustersAlgorithmAbstract supertype for all hierarchical clustering algorithm types.
All concrete and/or abstract types implementing hierarchical clustering algorithms (such as hierarchical agglomerative clustering or DBHT) should be subtypes of AbstractHierarchicalClusteringAlgorithm.
Related
PortfolioOptimisers.AbstractNonHierarchicalClusteringAlgorithm — Type
abstract type AbstractNonHierarchicalClusteringAlgorithm <: AbstractClustersAlgorithmAbstract supertype for all non-hierarchical clustering algorithm types.
All concrete and/or abstract types implementing non-hierarchical clustering algorithms (such as k-means) should be subtypes of AbstractNonHierarchicalClusteringAlgorithm.
Related
PortfolioOptimisers.AbstractOptimalNumberClustersEstimator — Type
abstract type AbstractOptimalNumberClustersEstimator <: AbstractEstimatorAbstract supertype for all optimal number of clusters estimator types.
All concrete and/or abstract types implementing algorithms to estimate the optimal number of clusters should be subtypes of AbstractOptimalNumberClustersEstimator.
Related
PortfolioOptimisers.AbstractOptimalNumberClustersAlgorithm — Type
abstract type AbstractOptimalNumberClustersAlgorithm <: AbstractAlgorithmAbstract supertype for all optimal number of clusters algorithm types.
All concrete and/or abstract types implementing specific algorithms for determining the optimal number of clusters should be subtypes of AbstractOptimalNumberClustersAlgorithm.
Related
PortfolioOptimisers.AbstractClusteringResult — Type
abstract type AbstractClusteringResult <: AbstractPhylogenyResultAbstract supertype for all clustering result types.
All concrete and/or abstract types representing the result of a clustering estimation should be subtypes of AbstractClusteringResult.
Related
PortfolioOptimisers.Int_ONC — Type
const Int_ONC = Union{<:Integer, <:AbstractOptimalNumberClustersAlgorithm}Alias for an integer or optimal number of clusters algorithm.
Matches either a plain integer (specifying the number of clusters directly) or an AbstractOptimalNumberClustersAlgorithm (which determines the optimal number automatically).
Related
PortfolioOptimisers.ClTypes — Type
const ClTypes = Union{<:Clustering.ClusteringResult, <:Clustering.Hclust}Alias for clustering result types from the Clustering.jl package.
Matches either a Clustering.ClusteringResult or Clustering.Hclust. Used internally to accept output from the Clustering.jl library for both flat and hierarchical clustering results.
Related
PortfolioOptimisers.ClE_Cl — Type
const ClE_Cl = Union{<:AbstractClustersEstimator, <:AbstractClusteringResult}Alias for a clustering estimator or result.
Matches either an AbstractClustersEstimator or an AbstractClusteringResult. Used for dispatch in phylogeny and network estimation workflows that accept either form.
Related
References
- [4]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
- [47]
- S. Yue, X. Wang and M. Wei. Application of two-order difference to gap statistic. Transactions of Tianjin University 14, 217–221 (2008).
- [48]
- R. Tibshirani, G. Walther and T. Hastie. Estimating the number of clusters in a data set via the gap statistic. Journal of the Royal Statistical Society: Series B (Statistical Methodology) 63, 411–423 (2001).
- [49]
- P. J. Rousseeuw. Silhouettes: a graphical aid to the interpretation and validation of cluster analysis. Journal of Computational and Applied Mathematics 20, 53–65 (1987).
- [50]
- M. López de Prado and M. J. Lewis. Detection of false investment strategies using unsupervised learning methods. Quantitative Finance 19, 1555–1565 (2019).
- [51]
- D. Müllner. Modern hierarchical, agglomerative clustering algorithms, arXiv preprint arXiv:1109.2378 (2011).