Feature Distance: private API
PortfolioOptimisers.AbstractCollapseAlgorithm — Type
abstract type AbstractCollapseAlgorithm <: AbstractAlgorithmAbstract supertype for all collapse algorithms.
A collapse algorithm is the aggregator applied along the observation axis of a window of time-varying features. It is consumed through AggregateFeatures and AggregateDistances, which differ in what they aggregate, not in how.
Related
PortfolioOptimisers.AbstractFeatureCollapseAlgorithm — Type
abstract type AbstractFeatureCollapseAlgorithm <: AbstractAlgorithmAbstract supertype for all feature collapse algorithms.
A feature collapse algorithm reduces a window of time-varying features, observations × assets × features, to a single assets × assets distance matrix. It is the FeatureDistance alg field, and is inert when the feature matrix is 2-D — a static feature matrix has no observation axis to collapse. At observations == 1 every algorithm in the family agrees exactly.
Related
PortfolioOptimisers.assert_metric_domain — Function
assert_metric_domain(metric::Distances.SemiMetric, Z::ArrNum, sym::Symbol = :Z)Assert that Z lies in metric's domain. The fallback is a no-op: most metrics accept any finite real input, and a blanket non-negativity check would reject signed factor loadings and the FeatureDistance default metric alike.
Distances.Jaccard (the Ruzicka form), Distances.BrayCurtis and Distances.ChiSqDist are the exceptions, all defined only on non-negative reals. The check matters most for Distances.Jaccard, which fails silently: it returns values up to 2 on signed input, with no error, straight into a clustering routine.
Algorithm
- Select the method by the type of
metric. The three metrics above own one method between them; every other metric reaches theDistances.SemiMetricmethod, which is a no-op and returns immediately. - On that method, check
Zfor non-negativity withassert_nonneg, which raises aDomainErrornamingsymwhen an entry is negative.
Arguments
metric: Distance metric whose domainZmust lie in.Z: Feature matrixassets × featuresifdims = 1,features × assetswhendims = 2. May also be a 3-D array of time-varying features, in which case the observation axis always leads:observations × assets × featuresifdims = 1,observations × features × assetswhendims = 2.sym::Symbol = :Z: Name that the error message gives toZ.
Validation
- Under
Distances.Jaccard,Distances.BrayCurtisandDistances.ChiSqDist:all(x -> x >= 0, Z).
Returns
nothing.
Related
PortfolioOptimisers.assert_feature_matrix — Function
assert_feature_matrix(de::FeatureDistance, Z::ArrNum, dims::Integer)Validate a feature matrix at the distance/cor_and_dist entry point: dims selects a valid axis, Z is non-empty, every entry is finite, and Z lies in the metric's domain.
Non-finite entries are rejected because no metric produces a usable distance from them — the Minkowski family gives Inf and the cosine family gives NaN — and neither can be clustered. Structurally degenerate inputs that a metric can handle are admitted: zero feature vectors are given a documented convention (see AngularDist), and duplicate or constant features are legitimate.
Algorithm
- Check
dimswithassert_dims. - Check that
Zis non-empty withassert_nonempty. - Check that every entry of
Zis finite withassert_all_finite. - Check that
Zlies inde.metric's domain withassert_metric_domain.
Arguments
de: Feature distance estimator, read for itsmetric.Z: Feature matrixassets × featuresifdims = 1,features × assetswhendims = 2. May also be a 3-D array of time-varying features, in which case the observation axis always leads:observations × assets × featuresifdims = 1,observations × features × assetswhendims = 2.dims: Dimension along which to perform the computation.
Validation
dims in (1, 2).!isempty(Z).all(isfinite, Z).Zlies inde.metric's domain (seeassert_metric_domain).
Returns
nothing.
Related
PortfolioOptimisers.zero_feature_vectors — Function
zero_feature_vectors(Z::MatNum, dims::Integer)Boolean mask of the assets whose feature vector is entirely zero, in the layout declared by dims.
Algorithm
- Select the asset axis from
dims: the rows ofZatdims = 1, and its columns atdims = 2. - Test each asset's feature vector with
all(iszero, ...), giving one entry of the mask per asset.
Arguments
Z: Feature matrixassets × featuresifdims = 1,features × assetswhendims = 2. May also be a 3-D array of time-varying features, in which case the observation axis always leads:observations × assets × featuresifdims = 1,observations × features × assetswhendims = 2.dims: Dimension along which to perform the computation.
Returns
z::Vector{Bool}: Mask, one entry per asset, true where that asset's feature vector is entirely zero.
Related
PortfolioOptimisers.patch_zero_feature_vectors! — Function
patch_zero_feature_vectors!(D::MatNum, Z::MatNum, dims::Integer)Apply the zero-feature-vector convention to D in place: two zero vectors are at distance 0, a zero vector and a non-zero one at distance 1.
Only entries the metric left as NaN are rewritten. A zero feature vector is structurally valid input, so construction-time validation cannot catch it, but it is undefined for the metrics normalised by a norm — the cosine family gives NaN against anything, and Distances.Jaccard/Distances.BrayCurtis give NaN between two zero vectors. It is perfectly well defined for the Minkowski family, which places it at the origin; restricting the patch to NaN entries fixes the former without corrupting the latter.
The convention is the one that keeps $S = \cos(\pi D)$ true on every entry, so AngularSimilarity yields +1 between two zero vectors and -1 against a non-zero one, with a unit diagonal. Distances.pairwise always writes an exact zero diagonal, so self-distance needs no patching.
Algorithm
- Build the zero mask
zofZwithzero_feature_vectors. - Return
Dunchanged when no asset is masked, which is the common case. - Otherwise visit every off-diagonal entry of
Dand rewrite it only when the metric left it asNaNand at least one of its two assets is masked: tozero(T)when both are masked, and toone(T)when exactly one is.
Arguments
D: Distance matrixassets × assets, rewritten in place.Z: Feature matrixassets × featuresifdims = 1,features × assetswhendims = 2. May also be a 3-D array of time-varying features, in which case the observation axis always leads:observations × assets × featuresifdims = 1,observations × features × assetswhendims = 2.dims: Dimension along which to perform the computation.
Returns
D::MatNum: Distance matrixassets x assets, in the units the distance algorithm defines.
Related
PortfolioOptimisers.feature_distance — Function
feature_distance(metric::Distances.SemiMetric, Z::MatNum, dims::Integer)Turn a 2-D feature matrix into a distance matrix. This is the shared kernel behind every FeatureDistance entry point: the collapse algorithms differ only in the matrix they hand it, except for AggregateDistances, which calls it once per observation and aggregates the results.
Algorithm
- Apply
metricto every pair of assets ofZwithDistances.pairwise, along the axisdimsnames, givingD. - Apply the zero-feature-vector convention to
Din place withpatch_zero_feature_vectors!.
Arguments
metric: Distance metric applied to the assets ofZ.Z: Feature matrixassets × featuresifdims = 1,features × assetswhendims = 2. May also be a 3-D array of time-varying features, in which case the observation axis always leads:observations × assets × featuresifdims = 1,observations × features × assetswhendims = 2.dims: Dimension along which to perform the computation.
Returns
D::MatNum: Distance matrixassets x assets, in the units the distance algorithm defines.
Related
feature_distance(de::FeatureDistance, Z::Arr3Num, dims::Integer)Turn a window of time-varying features into a distance matrix, by the collapse algorithm in de.alg.
Algorithm
The type of de.alg selects one of four methods. Each is stated on its own type, and the branch is:
LastObservation: hand the last slice of the observation axis to the 2-D kernel.StackObservations: hand the stacked matrix fromstack_observationsto the 2-D kernel, along its first axis.AggregateFeatures: resolve the weights withcollapse_weights, collapse the window withcollapse_features, and hand the collapsed matrix to the 2-D kernel.AggregateDistances: resolve the weights withcollapse_weights, then accumulate one weighted distance matrix per observation and divide by the weight total.
Arguments
de: Feature distance estimator, read for itsmetricand itsalg.Z: Feature matrixassets × featuresifdims = 1,features × assetswhendims = 2. May also be a 3-D array of time-varying features, in which case the observation axis always leads:observations × assets × featuresifdims = 1,observations × features × assetswhendims = 2.dims: Dimension along which to perform the computation.
Returns
D::MatNum: Distance matrixassets x assets, in the units the distance algorithm defines.
Related
PortfolioOptimisers.collapse_features — Function
collapse_features(alg::AbstractCollapseAlgorithm, Z::Arr3Num, w::Option{<:VecNum})Aggregate a window of time-varying features along its leading observation axis, returning a matrix with the two trailing axes of Z unchanged. Used by AggregateFeatures.
The element type of the result follows the aggregate, not the window: the mean and the median of a window of integers are both fractional, and a window with an even observation count has a fractional median even when every value in it is whole. Both routes therefore build their result from the values they compute.
Algorithm
The MeanCollapse route:
- Reduce the leading axis of
ZwithStatistics.mean, weighted bywwhenwis notnothing. - Drop the reduced axis.
The MedianCollapse route:
- For each asset
jand each featurek, take the observation seriesview(Z, :, j, k). - Reduce that series with
Statistics.median, weighted bywwhenwis notnothing, giving one entry of the result.
Arguments
alg: Collapse algorithm, the aggregator applied along the observation axis.Z: Feature matrixassets × featuresifdims = 1,features × assetswhendims = 2. May also be a 3-D array of time-varying features, in which case the observation axis always leads:observations × assets × featuresifdims = 1,observations × features × assetswhendims = 2.w: Resolved observation weights, one entry per observation, ornothingfor an unweighted collapse.
Returns
Zc::Matrix{<:Number}: Collapsed feature matrix, the two trailing axes ofZunchanged.
Related
PortfolioOptimisers.stack_observations — Function
stack_observations(Z::Arr3Num, dims::Integer)Reshape a window of time-varying features into an assets × (observations · features) matrix, whose rows are the assets whichever trailing axis dims says they occupy.
Algorithm
- Permute
Zso the asset axis leads:(2, 1, 3)atdims = 1, and(3, 1, 2)atdims = 2. Both leave the observation axis second and the feature axis third. - Reshape the permuted array to
assets × (observations · features), giving one long feature vector per asset.
Arguments
Z: Feature matrixassets × featuresifdims = 1,features × assetswhendims = 2. May also be a 3-D array of time-varying features, in which case the observation axis always leads:observations × assets × featuresifdims = 1,observations × features × assetswhendims = 2.dims: Dimension along which to perform the computation.
Returns
Za::Matrix{<:Number}: Stacked feature matrix,assets × (observations · features).
Related
PortfolioOptimisers.collapse_weights — Function
collapse_weights(w::Option{<:ObsWeights}, Z::Arr3Num)Resolve the observation weights of a collapse algorithm against a window of time-varying features.
Z is matricised to observations × (assets · features) first, because get_observation_weights's documented interface is VecNum/MatNum and a raw 3-D array matches neither — a user's correct MatNum method would otherwise never fire. There is no caller-side nothing guard: get_observation_weights raises ObservationWeightsError itself when a DynamicAbstractWeights cannot resolve, so nothing here means only that no weights were requested.
Cross-fold weighting requires a DynamicAbstractWeights. It resolves against the Z it is handed, so it is fold-local and correct automatically. A static AbstractWeights is fixed at construction and outlives the fold: a longer one used to be read positionally by AggregateDistances, giving the oldest weights to the newest observations with no bounds error, and a shorter one gave a bare BoundsError. The length check makes both loud.
Algorithm
- Reshape
Ztoobservations × (assets · features), so the observation axis leads a matrix. - Resolve
wagainst that matrix withget_observation_weights, along its first axis. - Check the resolved length against the observation count of
Z, unless the resolution gavenothing.
Arguments
w: Optional observation weights vectorobservations × 1, or a concrete subtype ofDynamicAbstractWeights. Ifnothing, the computation is unweighted.Z: Feature matrixassets × featuresifdims = 1,features × assetswhendims = 2. May also be a 3-D array of time-varying features, in which case the observation axis always leads:observations × assets × featuresifdims = 1,observations × features × assetswhendims = 2.
Validation
length(w) == size(Z, 1)once resolved. Raises aDimensionMismatchnaming both lengths.
Returns
w::Option{<:VecNum}: Resolved observation weights, one entry per observation, ornothingwhen no weights were requested.
Related
PortfolioOptimisers.collapse_rows — Function
collapse_rows(alg::AbstractFeatureCollapseAlgorithm, pnl::AssetPanel)Name the observation rows a collapse algorithm reads, so the kernel stacks those rows alone.
A time-varying Feature Matrix is stacked from an Asset Panel and then collapsed along its observation axis, and a collapse that reads one row has no use for the others. LastObservation reads the last row, so it names it, and the stack it is handed is a window of one observation: a lifted static Panel Field, whose values are a RepeatedLeading, is then read once rather than once per observation, and the kernel's cost under the default collapse is the assets × features slice it measures. Every other member answers Colon(), every row. The two aggregates resolve their weights against the stacked window itself (see collapse_weights), so a window they could cut is not known before the stack exists, and StackObservations reads the whole stack by definition.
A static panel has no observation axis, so every member answers Colon() on one, LastObservation included.
Algorithm
The method that Julia selects is the algorithm.
LastObservationon a time-varying panel: the last observation,nobs:nobs, wherenobsis the observation countpanel_axesreads.- Every other case:
Colon().
Arguments
alg: The collapse algorithm.pnl: The Asset Panel the Feature Matrix is stacked from.
Returns
rows: A one-observation range, orColon(). Therowskeyword offeature_matrix.
Related