Cross-Sectional Regression
Types
PortfolioOptimisers.PseudoInverseFallback — Type
struct PseudoInverseFallback <: AbstractCrossSectionalSolveAlgorithmSolves the full-rank design directly and pseudo-inverts a rank-deficient one.
This is the default of CrossSectionalLinearRegression. It never throws on a dependent factor set: a square design that \ would refuse reaches the pseudo-inverse instead, and a non-square one reaches the same minimum-norm answer through either route. The rank test is what it costs, because the design is factorised twice whenever it passes.
Examples
julia> PseudoInverseFallback()PseudoInverseFallback()Related
PortfolioOptimisers.RankDeficiencyRefusal — Type
struct RankDeficiencyRefusal <: AbstractCrossSectionalSolveAlgorithmSolves the full-rank design directly and refuses a rank-deficient one.
The refusal names the observation, the rank it measured, the factor count and the count of eligible assets, so a caller can tell a dependent factor set apart from a cross-section that is too small.
Examples
julia> RankDeficiencyRefusal()RankDeficiencyRefusal()Related
PortfolioOptimisers.UncheckedSolve — Type
struct UncheckedSolve <: AbstractCrossSectionalSolveAlgorithmRuns no rank test and takes whatever \ returns.
It is the cheapest member, because it factorises the design once instead of twice. A rank-deficient non-square design reaches a column-pivoted QR whose solve returns the minimum-norm answer, so it agrees with MinimumNormSolve there. An exactly singular square design reaches an LU factorisation instead and throws LinearAlgebra.SingularException, which is the one case where taking the answer unchecked costs the fit.
Examples
julia> UncheckedSolve()UncheckedSolve()Related
PortfolioOptimisers.MinimumNormSolve — Type
struct MinimumNormSolve <: AbstractCrossSectionalSolveAlgorithmAlways pseudo-inverts, so it runs no rank test and takes no threshold.
Every observation takes the minimum-norm least-squares solution, whatever its rank, so the factor returns of two observations are comparable even when one of them lost a factor. It is the most expensive member. It parts from UncheckedSolve on a square design, which \ sends to an LU factorisation, and on a design that is only nearly dependent, where the pseudo-inverse truncates a singular value that \ keeps.
Examples
julia> MinimumNormSolve()MinimumNormSolve()Related
PortfolioOptimisers.CrossSectionalLinearRegression — Type
struct CrossSectionalLinearRegression{__T_alg, __T_intercept} <: AbstractCrossSectionalRegressionEstimatorFits one weighted least squares per observation across the assets, in closed form.
The solve runs on the weighted design $\sqrt{w_{t,i}} \, \boldsymbol{z}_{t,i}$ rather than on the normal matrix $\mathbf{Z}_{t}^{\intercal} \mathbf{W}_{t} \mathbf{Z}_{t}$, which halves the condition number in the exponent, and alg decides what happens when that design is rank deficient.
Algorithm
- Check
Z,XandW, and take the eligibility mask, per# Validationofcross_sectional_regression. - For each observation
t, gather the eligible assets, their weightsw, their exposuresAand their returnsy. - When
interceptistrue, subtract the weighted meansybarandxbarfromyand fromA, so the fit runs through the weighted centroid of the cross-section. - Scale
Aandybysqrt.(w), giving the weighted design and the weighted target. - Solve the weighted design through the branch
algselects, giving the rowtoff. - When
interceptistrue, set the entrytofbtoybar - dot(f[t, :], xbar). - Subtract the systematic part from
X, givingeps.
Fields
alg: Solve algorithm, anAbstractCrossSectionalSolveAlgorithm. It decides what the fit does with a rank-deficient weighted design.
intercept:intercept: Whether a per-observation intercept is fitted. Whenfalse, the regression runs through the origin of the cross-section.
Constructors
CrossSectionalLinearRegression(; alg::AbstractCrossSectionalSolveAlgorithm = PseudoInverseFallback(), intercept::Bool = false) -> CrossSectionalLinearRegressionKeywords correspond to the struct's fields.
Examples
julia> CrossSectionalLinearRegression()CrossSectionalLinearRegression alg ┼ PseudoInverseFallback() intercept ┴ Bool: falseRelated
PortfolioOptimisers.CrossSectionalTargetRegression — Type
struct CrossSectionalTargetRegression{__T_tgt, __T_intercept} <: AbstractCrossSectionalRegressionEstimatorFits one external regression model per observation across the assets.
The cross-sectional weights reach the model as observation weights, through factory and the target's own kwargs, so any target the library carries — a LinearModel or a GeneralisedLinearModel — runs here unchanged. Unlike CrossSectionalLinearRegression, the fit refuses an observation with no eligible asset, because an external model has no cross-section to read.
Algorithm
- Check
Z,XandW, and take the eligibility mask, per# Validationofcross_sectional_regression. - For each observation
t, gather the eligible assets, their weightsw, their exposuresAand their returnsy. Refuse when no asset is eligible. - When
interceptistrue, subtract the weighted meansybarandxbarfromyand fromA. The target fits no intercept column of its own, so the intercept is recovered from the centroid rather than fitted. - Build the per-observation target with
factory(tgt, StatsBase.aweights(w)), fit it toAandy, and read its coefficients into the rowtoff. - When
interceptistrue, set the entrytofbtoybar - dot(f[t, :], xbar). - Subtract the systematic part from
X, givingeps.
Fields
tgt: Regression model target.
intercept:intercept: Whether a per-observation intercept is fitted. Whenfalse, the regression runs through the origin of the cross-section.
Constructors
CrossSectionalTargetRegression(; tgt::AbstractRegressionTarget = LinearModel(), intercept::Bool = false) -> CrossSectionalTargetRegressionKeywords correspond to the struct's fields.
Examples
julia> CrossSectionalTargetRegression()CrossSectionalTargetRegression tgt ┼ LinearModel │ kwargs ┴ @NamedTuple{}: NamedTuple() intercept ┴ Bool: falseRelated
PortfolioOptimisers.CrossSectionalRegression — Type
struct CrossSectionalRegression{__T_f, __T_eps, __T_n, __T_b} <: AbstractCrossSectionalRegressionResultHolds the factor returns, the residuals, the eligible asset counts and the optional intercepts of a fitted cross-sectional regression.
The result is a sibling of Regression rather than a widening of it, because the two disagree on what an asset index means: a Regression holds one row per asset and port_opt_view slices its rows, whereas this result holds one row per observation and one column per asset, so the same index slices its columns. It carries no loadings matrix, because the exposures are the regression's input and an Exposure Estimator produces them.
Mathematical definition
\[\begin{align} \boldsymbol{f}_{t} &= \underset{\boldsymbol{f}}{\arg\min} \sum_{i = 1}^{N} w_{t,i} \left(x_{t,i} - b_{t} - \boldsymbol{z}_{t,i}^{\intercal} \boldsymbol{f}\right)^{2} \\ \boldsymbol{\varepsilon}_{t} &= \boldsymbol{x}_{t} - b_{t} \boldsymbol{1} - \mathbf{Z}_{t} \boldsymbol{f}_{t}\,. \end{align}\]
Where:
- $\boldsymbol{f}_{t}$: Factor returns of observation $t$, the $t$-th row of
f. - $\boldsymbol{x}_t$: Asset returns for observation $t$, the $t$-th row of the returns matrix.
- $\boldsymbol{\varepsilon}_{t}$: Residuals of observation $t$, the $t$-th row of
eps. - $\mathbf{Z}_{t}$: Exposure slice of observation $t$, $N \times K$, one row per asset.
- $\boldsymbol{z}_{t,i}$: Exposures of asset $i$ at observation $t$, the $i$-th row of $\mathbf{Z}_{t}$.
- $w_{t,i} \geq 0$: Cross-sectional weight of asset $i$ at observation $t$. A weight of zero excludes the pair from the fit.
- $b_{t}$: Intercept of observation $t$, the $t$-th entry of
b. The term is absent whenbis unset. - $N$: Number of assets.
- $K$: Number of factors.
Each observation is one independent problem, so a factor return is a cross-sectional quantity and never a time-series one.
Fields
f: Factor returns matrixobservations × factors. Rowtholds the coefficients of the cross-sectional fit of observationt.
eps: Residual matrixobservations × assets. It is the part of the returns the exposures do not explain, and an entry of an excluded pair is whatever the arithmetic of that pair produced, so a missing return leaves a missing residual.
n: Count of assets that entered each fit, of lengthobservations. An asset enters when its cross-sectional weight is positive.
b:b: Regression intercept vector.
Constructors
CrossSectionalRegression(; f::MatNum, eps::MatNum, n::AbstractVector{<:Integer}, b::Option{<:VecNum} = nothing) -> CrossSectionalRegressionKeywords correspond to the struct's fields.
Validation
!isempty(f),!isempty(eps)and!isempty(n).size(f, 1) == size(eps, 1) == length(n).all(x -> x >= 0, n).- If provided,
!isempty(b), andlength(b) == size(f, 1).
View parameters
CrossSectionalRegression defines its own port_opt_view method rather than deriving one from field tags.
epsis sliced on its second axis, which is the asset axis of a cross-sectional result.f,nandbpass through unchanged. Each is indexed by observation and by factor, and neither axis follows an asset selection.
Examples
julia> CrossSectionalRegression(; f = [1.0 2.0; 3.0 4.0], eps = [0.1 0.2 0.3; 0.4 0.5 0.6], n = [3, 3])CrossSectionalRegression f ┼ 2×2 Matrix{Float64} eps ┼ 2×3 Matrix{Float64} n ┼ Vector{Int64}: [3, 3] b ┴ nothingRelated
Functions
PortfolioOptimisers.cross_sectional_regression — Function
cross_sectional_regression(cre::AbstractCrossSectionalRegressionEstimator, Z::Arr3Num,
X::MatNum, W::MatNum) -> CrossSectionalRegression
cross_sectional_regression(csr::CrossSectionalRegression, args...) -> CrossSectionalRegressionFit one regression per observation across the assets, or return a fitted result unchanged.
The verb is its own rather than a fourth argument of regression, because regression(re::Regression, args...) is a greedy passthrough that returns its first argument for any trailing arguments, so a time-series result handed to a cross-sectional call would return silently instead of raising.
The weight matrix W is an argument rather than a field, because a two-pass weighting scheme calls the estimator twice on one design with two different weight matrices, and a policy stored on the estimator would force a second estimator object or a mutation.
Algorithm
- Take the eligibility mask through
cross_sectional_design_mask. - For each observation
t, gather the eligible assets, their weightsw, their exposuresAand their returnsy, and record their count inn. - When
cre.interceptistrue, take the weighted meansybarandxbarofyand ofA, and subtract them. An observation with no eligible asset takes zero for both. - Take the factor returns of the observation through
cross_sectional_coefficients, and write them into the rowtoff. An observation with no eligible asset takes zero factor returns underCrossSectionalLinearRegression, except underRankDeficiencyRefusal, whose rank test reads an empty design as rank zero and refuses it by name. - When
cre.interceptistrue, writeybar - dot(f[t, :], xbar)into the entrytofb. - Subtract the systematic part, through
cross_sectional_systematic, fromX, givingeps.
Arguments
cre: Cross-sectional regression estimator.csr: A cross-sectional regression result.Z::Arr3Num: Exposure tensorobservations × assets × factors.X::MatNum: Asset returns matrixobservations × assets.W::MatNum: Cross-sectional weights matrixobservations × assets.args...: Additional positional arguments (ignored by the passthrough).
Validation
- The rules of
cross_sectional_design_mask.
Returns
csr::CrossSectionalRegression: The fitted result, or the input result unchanged.
Examples
julia> Z = reshape([1.0, 0.0, 0.5, 0.0, 1.0, 0.5], 1, 3, 2);julia> cross_sectional_regression(CrossSectionalLinearRegression(), Z, [1.0 2.0 1.5], ones(1, 3))CrossSectionalRegression f ┼ 1×2 Matrix{Float64} eps ┼ 1×3 Matrix{Float64} n ┼ Vector{Int64}: [3] b ┴ nothingRelated
StatsAPI.predict — Method
StatsAPI.predict(csr::CrossSectionalRegression, Z::Arr3Num) -> MatNumReturn the systematic part of a fitted cross-sectional regression, observations × assets.
The residuals the result already carries are X - predict(csr, Z) for the X the fit saw, so this method earns its place on an exposure tensor the fit did not see.
Algorithm
- Call
cross_sectional_systematicwithcsr.f,csr.bandZ.
Arguments
csr: A cross-sectional regression result.Z::Arr3Num: Exposure tensorobservations × assets × factors. The asset axis may differ from the one the fit saw; the observation and factor axes may not.
Validation
- The rules of
cross_sectional_systematic.
Returns
Xh::MatNum: Systematic returns,observations × assets.
Examples
julia> Z = reshape([1.0, 0.0, 0.5, 0.0, 1.0, 0.5], 1, 3, 2);julia> csr = cross_sectional_regression(CrossSectionalLinearRegression(), Z, [1.0 2.0 1.5], ones(1, 3));julia> predict(csr, Z)1×3 Matrix{Float64}: 1.0 2.0 1.5Related
PortfolioOptimisers.cross_sectional_r2 — Function
cross_sectional_r2(csr::CrossSectionalRegression, Z::Arr3Num, X::MatNum,
W::MatNum) -> VecNumReturn the weighted coefficient of determination of every observation.
An observation whose weighted total sum of squares is zero has no defined ratio, and its entry is NaN. mean_cross_sectional_r2 is the scalar summary that skips those entries.
Mathematical definition
\[\begin{align} R^{2}_{t} &= 1 - \frac{\sum_{i} w_{t,i} \left(x_{t,i} - \hat{x}_{t,i}\right)^{2}}{\sum_{i} w_{t,i} \left(x_{t,i} - \bar{x}_{t}\right)^{2}} \\ \bar{x}_{t} &= \frac{\sum_{i} w_{t,i} x_{t,i}}{\sum_{i} w_{t,i}}\,. \end{align}\]
Where:
- $R^{2}_{t}$: Weighted coefficient of determination of observation $t$.
- $x_{t,i}$: Return of asset $i$ at observation $t$.
- $\hat{x}_{t,i}$: Systematic return of asset $i$ at observation $t$.
- $\bar{x}_{t}$: Weighted mean return of observation $t$.
- $w_{t,i} \geq 0$: Cross-sectional weight of asset $i$ at observation $t$. Both sums run over the eligible assets alone.
Algorithm
- Take the eligibility mask through
cross_sectional_design_mask. - Take the systematic returns through
StatsAPI.predict(csr::CrossSectionalRegression, Z::Arr3Num). - For each observation, sum the weighted squared residuals and the weighted squared deviations from the weighted mean, over the eligible assets alone.
- Return
1 - rss / tssper observation, andNaNwheretssis not positive.
Arguments
csr: A cross-sectional regression result.Z::Arr3Num: Exposure tensorobservations × assets × factors.X::MatNum: Asset returns matrixobservations × assets.W::MatNum: Cross-sectional weights matrixobservations × assets.
Validation
- The rules of
cross_sectional_design_mask.
Returns
r2::VecNum: Coefficient of determination of every observation, of lengthobservations, in the type a division of the inputs lands in.
Examples
julia> Z = reshape([1.0, 0.0, 0.5, 0.0, 1.0, 0.5], 1, 3, 2);julia> csr = cross_sectional_regression(CrossSectionalLinearRegression(), Z, [1.0 2.0 1.5], ones(1, 3));julia> cross_sectional_r2(csr, Z, [1.0 2.0 1.5], ones(1, 3))1-element Vector{Float64}: 1.0Related
PortfolioOptimisers.mean_cross_sectional_r2 — Function
mean_cross_sectional_r2(csr::CrossSectionalRegression, Z::Arr3Num, X::MatNum,
W::MatNum) -> NumberReturn the mean weighted coefficient of determination across the observations.
The mean skips every observation whose ratio is undefined, and it is NaN when no observation defines one.
Algorithm
- Take the per-observation vector through
cross_sectional_r2. - Return the mean of its finite entries, and
NaNwhen it holds none.
Arguments
csr: A cross-sectional regression result.Z::Arr3Num: Exposure tensorobservations × assets × factors.X::MatNum: Asset returns matrixobservations × assets.W::MatNum: Cross-sectional weights matrixobservations × assets.
Validation
- The rules of
cross_sectional_design_mask.
Returns
r2::Number: Mean coefficient of determination across the observations.
Examples
julia> Z = reshape([1.0, 0.0, 0.5, 0.0, 1.0, 0.5], 1, 3, 2);julia> csr = cross_sectional_regression(CrossSectionalLinearRegression(), Z, [1.0 2.0 1.5], ones(1, 3));julia> mean_cross_sectional_r2(csr, Z, [1.0 2.0 1.5], ones(1, 3))1.0Related
PortfolioOptimisers.port_opt_view — Method
port_opt_view(csr::CrossSectionalRegression, i, args...)Return a view of a CrossSectionalRegression result, selecting only the assets indexed by i.
Algorithm
- Take a column view of
epsoveri, giving the residuals of the selected assets. The asset axis of a cross-sectional result is the second one, because a row ofepsis one observation. - Build a new
CrossSectionalRegressionfrom that view and the three untouched fields, which re-runs every guard of the constructor.
Arguments
csr: A cross-sectional regression result.i: Indices of the assets to select.args...: Additional positional arguments (ignored).
Returns
csr::CrossSectionalRegression: A new result whose residuals are restricted to the selected assets.
Examples
julia> csr = CrossSectionalRegression(; f = [1.0 2.0], eps = [0.1 0.2 0.3], n = [3])CrossSectionalRegression f ┼ 1×2 Matrix{Float64} eps ┼ 1×3 Matrix{Float64} n ┼ Vector{Int64}: [3] b ┴ nothingjulia> PortfolioOptimisers.port_opt_view(csr, [1, 3])CrossSectionalRegression f ┼ 1×2 Matrix{Float64} eps ┼ 1×2 SubArray{Float64, 2, Matrix{Float64}, Tuple{Base.Slice{Base.OneTo{Int64}}, Vector{Int64}}, false} n ┼ Vector{Int64}: [3] b ┴ nothingRelated