Hierarchical
PortfolioOptimisers.ClusterNode — Type
struct ClusterNode{tid, tl, tr, td, tcnt} <: AbstractResultBinds one merge of a dendrogram to the two clusters it joined.
The tree form of a linkage matrix: to_tree turns a Clustering.Hclust into one of these per merge, plus one per asset, and the last one built is the root. A leaf carries left and right as nothing, which is what is_leaf tests.
level counts leaves, it does not measure depth
level is the number of assets in the subtree below the node — 1 for a leaf, and the sum of the two children's counts for a merge. It is the fourth column of a linkage matrix, not a position in the tree, and on an eight-asset universe the two disagree: the root carries level = 8 where its depth is 5.
pre_order sizes its traversal stack as 2 * a.level, so a depth would undersize it.
Fields
id: Node identifier.
left: Left child node.
right: Right child node.
height: Height of the node in the dendrogram.
level: Number of leaves in the subtree rooted at the node,1for a leaf. It is the fourth column of a linkage matrix, andpre_ordersizes its traversal stack from it.
Constructors
ClusterNode( id, left::Option{<:ClusterNode} = nothing, right::Option{<:ClusterNode} = nothing, height::Number = 0.0, level::Int = 1) -> ClusterNodeArguments correspond to the struct's fields. A node given children ignores the level argument and takes left.level + right.level instead, so only a leaf's level comes from the caller.
Examples
julia> ClusterNode(1)ClusterNode id ┼ Int64: 1 left ┼ nothing right ┼ nothing height ┼ Float64: 0.0 level ┴ Int64: 1Related
References
- [52] P. Virtanen, R. Gommers, T. E. Oliphant, M. Haberland, T. Reddy, D. Cournapeau, E. Burovski, P. Peterson, W. Weckesser, J. Bright, S. J. van der Walt, M. Brett, J. Wilson, K. J. Millman, N. Mayorov, A. R. Nelson, E. Jones, R. Kern, E. Larson, C. J. Carey, İ. Polat, Y. Feng, E. W. Moore, J. VanderPlas, D. Laxalde, J. Perktold, R. Cimrman, I. Henriksen, E. A. Quintero, C. R. Harris, A. M. Archibald, A. H. Ribeiro, F. Pedregosa and P. van Mulbregt. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods 17, 261–272 (2020).
- [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 12.1.1, Equation 12.5.
PortfolioOptimisers.VecClN — Type
const VecClN = AbstractVector{<:ClusterNode}Alias for a vector of ClusterNode objects.
Represents an ordered collection of cluster nodes, typically used in hierarchical tree traversal and linkage computation.
Related
PortfolioOptimisers.is_leaf — Function
is_leaf(a::ClusterNode)Is this node an asset, or a merge of two clusters?
Tests left alone. A ClusterNode is built with both children or with neither, so one test settles it.
Arguments
a: The node to check.
Returns
flag::Bool:truewhen the node has no children.
Examples
julia> PortfolioOptimisers.is_leaf(ClusterNode(1))trueRelated
PortfolioOptimisers.PreorderTreeByID — Type
struct PreorderTreeByID <: AbstractPreorderByCollects each leaf's id, which for a leaf is its asset index.
The default strategy, and the only one that ships. to_tree numbers the leaves 1:N in the order of the clustering's own asset axis, so a pre_order under this strategy returns asset indices ready to index a returns matrix with.
Related
References
- [52] P. Virtanen, R. Gommers, T. E. Oliphant, M. Haberland, T. Reddy, D. Cournapeau, E. Burovski, P. Peterson, W. Weckesser, J. Bright, S. J. van der Walt, M. Brett, J. Wilson, K. J. Millman, N. Mayorov, A. R. Nelson, E. Jones, R. Kern, E. Larson, C. J. Carey, İ. Polat, Y. Feng, E. W. Moore, J. VanderPlas, D. Laxalde, J. Perktold, R. Cimrman, I. Henriksen, E. A. Quintero, C. R. Harris, A. M. Archibald, A. H. Ribeiro, F. Pedregosa and P. van Mulbregt. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods 17, 261–272 (2020).
PortfolioOptimisers.pre_order — Function
pre_order(a::ClusterNode, preorder_by::AbstractPreorderBy = PreorderTreeByID())List the leaves below a node, left to right.
Walks the subtree rooted at a in preorder and collects one property per leaf; an internal node contributes nothing but the order it imposes on its two children. The property collected is get_node_property's, so preorder_by is what a caller changes to collect something other than the node's id.
preorder_by is positional, not a keyword.
Arguments
a: Root node of the subtree to walk.preorder_by: Traversal strategy, deciding which property each leaf contributes.
Returns
res::Vector{Int}: One property per leaf, in left-to-right order. Its length isa.level.
Related
PortfolioOptimisers.to_tree — Function
to_tree(a::Hclust)Rebuild a linkage matrix as a tree of ClusterNode objects.
Reads a Clustering.Hclust from Clustering.jl and builds 2N - 1 nodes: one leaf per asset, numbered 1:N in the clustering's own asset order, then one node per merge, numbered N+1 upward in the order the merges happened. The last merge is therefore the root.
Arguments
a: Hierarchical clustering object.
Returns
root::ClusterNode: Root of the tree, which is the node of the last merge.nodes::Vector{ClusterNode}: All2N - 1nodes, leaves first, then merges in merge order. The vector is not sorted by height; a caller that needs that ordering sorts it, asoptimal_number_clustersdoes.
Related
PortfolioOptimisers.optimal_number_clusters — Function
optimal_number_clusters(onc::OptimalNumberClusters{<:Any, <:Integer}, res::Hclust,
args...)
optimal_number_clusters(onc::OptimalNumberClusters{<:Any, <:SecondOrderDifference},
res::Hclust, D::MatNum)
optimal_number_clusters(onc::OptimalNumberClusters{<:Any, <:SilhouetteScore},
res::Hclust, D::MatNum)Cut a dendrogram at the number of clusters onc selects.
Scores every candidate count up to the ceiling onc sets, then takes the highest-scoring count the tree can actually be cut at. A count no node of the dendrogram supports is rejected by validate_k_value and the next-highest score is tried, so the answer is the best valid count rather than the best score.
Arguments
onc: Optimal number of clusters estimator.onc::OptimalNumberClusters{<:Any, <:Integer}: Takes the statedk, lowered to the ceiling. If thatkis not valid, searches upward and downward for the nearest valid count and takes the nearer of the two; a tie goes to whichever side has more room left.onc::OptimalNumberClusters{<:Any, <:SecondOrderDifference}: Scores each count by the two-difference gap statistic of the within-cluster dispersions, then hands the scores tovalid_k_clusters. The dispersion isonc.alg.algapplied to one cluster's pairwise distances, summed over clusters.onc::OptimalNumberClusters{<:Any, <:SilhouetteScore}: Scores each count byonc.alg.algapplied to the vector of per-asset silhouettes, then hands the scores tovalid_k_clusters.
res: Hierarchical clustering object.D: Distance matrix the clustering was run on.
Returns
onc::Integer: Selected optimal number of clusters.
Related
optimal_number_clusters(onc::OptimalNumberClusters{<:Any, <:Integer},
alg::AbstractNonHierarchicalClusteringAlgorithm, D::MatNum)
optimal_number_clusters(onc::OptimalNumberClusters{<:Any, <:SecondOrderDifference},
alg::AbstractNonHierarchicalClusteringAlgorithm, D::MatNum)
optimal_number_clusters(onc::OptimalNumberClusters{<:Any, <:SilhouetteScore},
alg::AbstractNonHierarchicalClusteringAlgorithm, D::MatNum)Run a non-hierarchical algorithm at every candidate k and keep the best one.
Clusters the distance matrix once per candidate count, scores the results, and returns the winning clustering together with its k. Both come back because a flat partition cannot be re-cut: unlike the hierarchical branch, the clustering is the choice of k.
No validity test, and no tree to run one against
valid_k_clusters has no counterpart here. It rejects a count the dendrogram cannot be cut at, and a flat partition has no dendrogram, so the argmax is taken as it stands.
The dispersion under SecondOrderDifference is also a different quantity from the hierarchical branch's: it is onc.alg.alg applied to the k-means per-point costs, not to within-cluster pairwise distances. That vector has one entry per asset whatever the cut, so a cut never reduces a one-value vector here.
Arguments
onc: Optimal number of clusters estimator.onc::OptimalNumberClusters{<:Any, <:Integer}: Uses a fixedkdirectly, clamped tomax_k.onc::OptimalNumberClusters{<:Any, <:SecondOrderDifference}: Scores each count by the two-difference gap statistic ofonc.alg.algapplied to that run's per-point costs, and takes the argmax.onc::OptimalNumberClusters{<:Any, <:SilhouetteScore}: Scores each count byonc.alg.algapplied to the vector of per-asset silhouettes, and takes the argmax.
alg: Non-hierarchical clustering algorithm (e.g.,KMeansAlgorithm).D: Pairwise distance matrix.
Returns
(res, k): The clustering result and optimal number of clusters.
Related
PortfolioOptimisers.clusterise — Method
clusterise(cle::ClustersEstimator{<:Any, <:Any, <:HClustAlgorithm, <:Any},
X::MatNum; branchorder::Symbol = :optimal, dims::Int = 1,
kwargs...)Run hierarchical clustering and return the result as a Clusters object.
This function applies the specified clustering estimator to the input data matrix, computes the similarity and distance matrices, performs hierarchical clustering, and selects the optimal number of clusters. The result is returned as a Clusters object.
Arguments
cle: Clustering estimator.X: Data matrix (observations × assets).branchorder: Branch ordering strategy for hierarchical clustering.dims: Dimension along which to perform the computation.kwargs...: Additional keyword arguments.
Returns
res::Clusters: Result object containing clustering, similarity, distance matrices, and number of clusters.
Related
PortfolioOptimisers.get_node_property — Function
get_node_property(preorder_by::PreorderTreeByID, a::ClusterNode)Get the property of a node used for preorder traversal.
For PreorderTreeByID, this returns the node's id.
Arguments
preorder_by: Preorder traversal strategy.a: The node.
Returns
- The node's identifier.
Related
PortfolioOptimisers.AbstractPreorderBy — Type
abstract type AbstractPreorderBy <: AbstractAlgorithmAbstract supertype for all preorder traversal strategies.
All concrete and/or abstract types implementing specific preorder traversal logic should be subtypes of AbstractPreorderBy.
A strategy decides which property a leaf contributes to pre_order's output. It does not change the order of the walk, which is always left subtree before right.
Interfaces
In order to implement a new traversal strategy that works seamlessly with the library, subtype AbstractPreorderBy and implement the following method:
Required method
get_node_property(preorder_by::MyPreorderBy, a::ClusterNode): Return the property thatacontributes when it is reached as a leaf.
Arguments
preorder_by: The concrete traversal strategy.a: Node reached by the walk.
Returns
- The property to collect.
Related
PortfolioOptimisers.validate_k_value — Function
validate_k_value(res::Clustering.Hclust, nodes::VecClN, k::Integer)Can this tree be cut into exactly k clusters?
Cuts the tree at k, then walks the k - 1 tallest nodes. Each non-leaf node among them must have at least one of the k clusters contained wholly within one of its two subtrees; a node for which no cluster does makes k invalid.
Arguments
res: Hierarchical clustering object.nodes: Vector of nodes in the clustering tree, sorted by descending height.k: Number of clusters to validate.
Returns
flag::Bool:trueifkis a valid number of clusters,falseotherwise.
Related
PortfolioOptimisers.valid_k_clusters — Function
valid_k_clusters(res::Hclust, arr::VecNum)Take the highest-scoring number of clusters the tree can be cut at.
Takes a candidate, tests it with validate_k_value, and on failure blanks that entry to typemin(eltype(arr)) and takes the next. The candidate is argmax(arr), or length(arr) when no entry of arr is finite. It returns 1 instead when a failed candidate leaves every entry infinite.
The scores are trusted as they arrive, and a NaN is not rejected. argmax returns the index of the first NaN in an array that carries one, ahead of every real score; an array that is NaN throughout instead takes the length(arr) branch, because a NaN is not finite and not infinite either. SecondOrderDifference's default measure produces such an array whenever a cluster holds exactly two assets, and the k that comes back is then the length of the array rather than a maximiser.
Arguments
res: Hierarchical clustering object.arr: Score for each candidate number of clusters. Modified in place: a candidate that fails validation is set totypemin(eltype(arr))so that the next iteration skips it.
Returns
k::Integer: Valid number of clusters.
Related
Clustering.assignments — Function
assignments(
clr::Clusters{<:Clustering.ClusteringResult}
) -> Any
Return the cluster assignments for a non-hierarchical Clusters result.
Related
References
- [4]
- D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
- [52]
- P. Virtanen, R. Gommers, T. E. Oliphant, M. Haberland, T. Reddy, D. Cournapeau, E. Burovski, P. Peterson, W. Weckesser, J. Bright, S. J. van der Walt, M. Brett, J. Wilson, K. J. Millman, N. Mayorov, A. R. Nelson, E. Jones, R. Kern, E. Larson, C. J. Carey, İ. Polat, Y. Feng, E. W. Moore, J. VanderPlas, D. Laxalde, J. Perktold, R. Cimrman, I. Henriksen, E. A. Quintero, C. R. Harris, A. M. Archibald, A. H. Ribeiro, F. Pedregosa and P. van Mulbregt. SciPy 1.0: fundamental algorithms for scientific computing in Python. Nature Methods 17, 261–272 (2020).