Hierarchical

PortfolioOptimisers.ClusterNodeType
struct ClusterNode{tid, tl, tr, td, tcnt} <: AbstractResult

Binds 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, 1 for a leaf. It is the fourth column of a linkage matrix, and pre_order sizes its traversal stack from it.

Constructors

ClusterNode(    id,    left::Option{<:ClusterNode} = nothing,    right::Option{<:ClusterNode} = nothing,    height::Number = 0.0,    level::Int = 1) -> ClusterNode

Arguments 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: 1

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).
  • [4] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 12.1.1, Equation 12.5.
source
PortfolioOptimisers.PreorderTreeByIDType
struct PreorderTreeByID <: AbstractPreorderBy

Collects 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).
source
PortfolioOptimisers.pre_orderFunction
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 is a.level.

Related

source
PortfolioOptimisers.to_treeFunction
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}: All 2N - 1 nodes, leaves first, then merges in merge order. The vector is not sorted by height; a caller that needs that ordering sorts it, as optimal_number_clusters does.

Related

source
PortfolioOptimisers.optimal_number_clustersFunction
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 stated k, lowered to the ceiling. If that k is 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 to valid_k_clusters. The dispersion is onc.alg.alg applied to one cluster's pairwise distances, summed over clusters.
    • onc::OptimalNumberClusters{<:Any, <:SilhouetteScore}: Scores each count by onc.alg.alg applied to the vector of per-asset silhouettes, then hands the scores to valid_k_clusters.
  • res: Hierarchical clustering object.

  • D: Distance matrix the clustering was run on.

Returns

  • onc::Integer: Selected optimal number of clusters.

Related

source
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 fixed k directly, clamped to max_k.
    • onc::OptimalNumberClusters{<:Any, <:SecondOrderDifference}: Scores each count by the two-difference gap statistic of onc.alg.alg applied to that run's per-point costs, and takes the argmax.
    • onc::OptimalNumberClusters{<:Any, <:SilhouetteScore}: Scores each count by onc.alg.alg applied 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

source
PortfolioOptimisers.clusteriseMethod
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

source
PortfolioOptimisers.AbstractPreorderByType
abstract type AbstractPreorderBy <: AbstractAlgorithm

Abstract 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 that a contributes 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

source
PortfolioOptimisers.validate_k_valueFunction
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: true if k is a valid number of clusters, false otherwise.

Related

source
PortfolioOptimisers.valid_k_clustersFunction
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.

Warning

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 to typemin(eltype(arr)) so that the next iteration skips it.

Returns

  • k::Integer: Valid number of clusters.

Related

source

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).