Cross-Sectional Regression

Types

PortfolioOptimisers.PseudoInverseFallbackType
struct PseudoInverseFallback <: AbstractCrossSectionalSolveAlgorithm

Solves 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

source
PortfolioOptimisers.RankDeficiencyRefusalType
struct RankDeficiencyRefusal <: AbstractCrossSectionalSolveAlgorithm

Solves 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

source
PortfolioOptimisers.UncheckedSolveType
struct UncheckedSolve <: AbstractCrossSectionalSolveAlgorithm

Runs 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

source
PortfolioOptimisers.MinimumNormSolveType
struct MinimumNormSolve <: AbstractCrossSectionalSolveAlgorithm

Always 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

source
PortfolioOptimisers.CrossSectionalLinearRegressionType
struct CrossSectionalLinearRegression{__T_alg, __T_intercept} <: AbstractCrossSectionalRegressionEstimator

Fits 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

  1. Check Z, X and W, and take the eligibility mask, per # Validation of cross_sectional_regression.
  2. For each observation t, gather the eligible assets, their weights w, their exposures A and their returns y.
  3. When intercept is true, subtract the weighted means ybar and xbar from y and from A, so the fit runs through the weighted centroid of the cross-section.
  4. Scale A and y by sqrt.(w), giving the weighted design and the weighted target.
  5. Solve the weighted design through the branch alg selects, giving the row t of f.
  6. When intercept is true, set the entry t of b to ybar - dot(f[t, :], xbar).
  7. Subtract the systematic part from X, giving eps.

Fields

  • intercept: intercept: Whether a per-observation intercept is fitted. When false, the regression runs through the origin of the cross-section.

Constructors

CrossSectionalLinearRegression(;    alg::AbstractCrossSectionalSolveAlgorithm = PseudoInverseFallback(),    intercept::Bool = false) -> CrossSectionalLinearRegression

Keywords correspond to the struct's fields.

Examples

julia> CrossSectionalLinearRegression()CrossSectionalLinearRegression        alg ┼ PseudoInverseFallback()  intercept ┴ Bool: false

Related

source
PortfolioOptimisers.CrossSectionalTargetRegressionType
struct CrossSectionalTargetRegression{__T_tgt, __T_intercept} <: AbstractCrossSectionalRegressionEstimator

Fits 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

  1. Check Z, X and W, and take the eligibility mask, per # Validation of cross_sectional_regression.
  2. For each observation t, gather the eligible assets, their weights w, their exposures A and their returns y. Refuse when no asset is eligible.
  3. When intercept is true, subtract the weighted means ybar and xbar from y and from A. The target fits no intercept column of its own, so the intercept is recovered from the centroid rather than fitted.
  4. Build the per-observation target with factory(tgt, StatsBase.aweights(w)), fit it to A and y, and read its coefficients into the row t of f.
  5. When intercept is true, set the entry t of b to ybar - dot(f[t, :], xbar).
  6. Subtract the systematic part from X, giving eps.

Fields

  • tgt: Regression model target.
  • intercept: intercept: Whether a per-observation intercept is fitted. When false, the regression runs through the origin of the cross-section.

Constructors

CrossSectionalTargetRegression(;    tgt::AbstractRegressionTarget = LinearModel(),    intercept::Bool = false) -> CrossSectionalTargetRegression

Keywords correspond to the struct's fields.

Examples

julia> CrossSectionalTargetRegression()CrossSectionalTargetRegression        tgt ┼ LinearModel            │   kwargs ┴ @NamedTuple{}: NamedTuple()  intercept ┴ Bool: false

Related

source
PortfolioOptimisers.CrossSectionalRegressionType
struct CrossSectionalRegression{__T_f, __T_eps, __T_n, __T_b} <: AbstractCrossSectionalRegressionResult

Holds 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 when b is 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 matrix observations × factors. Row t holds the coefficients of the cross-sectional fit of observation t.
  • eps: Residual matrix observations × 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 length observations. 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) -> CrossSectionalRegression

Keywords 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), and length(b) == size(f, 1).

View parameters

CrossSectionalRegression defines its own port_opt_view method rather than deriving one from field tags.

  • eps is sliced on its second axis, which is the asset axis of a cross-sectional result.
  • f, n and b pass 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 ┴ nothing

Related

source

Functions

PortfolioOptimisers.cross_sectional_regressionFunction
cross_sectional_regression(cre::AbstractCrossSectionalRegressionEstimator, Z::Arr3Num,
                           X::MatNum, W::MatNum) -> CrossSectionalRegression
cross_sectional_regression(csr::CrossSectionalRegression, args...) -> CrossSectionalRegression

Fit 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

  1. Take the eligibility mask through cross_sectional_design_mask.
  2. For each observation t, gather the eligible assets, their weights w, their exposures A and their returns y, and record their count in n.
  3. When cre.intercept is true, take the weighted means ybar and xbar of y and of A, and subtract them. An observation with no eligible asset takes zero for both.
  4. Take the factor returns of the observation through cross_sectional_coefficients, and write them into the row t of f. An observation with no eligible asset takes zero factor returns under CrossSectionalLinearRegression, except under RankDeficiencyRefusal, whose rank test reads an empty design as rank zero and refuses it by name.
  5. When cre.intercept is true, write ybar - dot(f[t, :], xbar) into the entry t of b.
  6. Subtract the systematic part, through cross_sectional_systematic, from X, giving eps.

Arguments

  • cre: Cross-sectional regression estimator.
  • csr: A cross-sectional regression result.
  • Z::Arr3Num: Exposure tensor observations × assets × factors.
  • X::MatNum: Asset returns matrix observations × assets.
  • W::MatNum: Cross-sectional weights matrix observations × assets.
  • args...: Additional positional arguments (ignored by the passthrough).

Validation

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 ┴ nothing

Related

source
StatsAPI.predictMethod
StatsAPI.predict(csr::CrossSectionalRegression, Z::Arr3Num) -> MatNum

Return 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

  1. Call cross_sectional_systematic with csr.f, csr.b and Z.

Arguments

  • csr: A cross-sectional regression result.
  • Z::Arr3Num: Exposure tensor observations × assets × factors. The asset axis may differ from the one the fit saw; the observation and factor axes may not.

Validation

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

Related

source
PortfolioOptimisers.cross_sectional_r2Function
cross_sectional_r2(csr::CrossSectionalRegression, Z::Arr3Num, X::MatNum,
                   W::MatNum) -> VecNum

Return 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

  1. Take the eligibility mask through cross_sectional_design_mask.
  2. Take the systematic returns through StatsAPI.predict(csr::CrossSectionalRegression, Z::Arr3Num).
  3. For each observation, sum the weighted squared residuals and the weighted squared deviations from the weighted mean, over the eligible assets alone.
  4. Return 1 - rss / tss per observation, and NaN where tss is not positive.

Arguments

  • csr: A cross-sectional regression result.
  • Z::Arr3Num: Exposure tensor observations × assets × factors.
  • X::MatNum: Asset returns matrix observations × assets.
  • W::MatNum: Cross-sectional weights matrix observations × assets.

Validation

Returns

  • r2::VecNum: Coefficient of determination of every observation, of length observations, 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.0

Related

source
PortfolioOptimisers.mean_cross_sectional_r2Function
mean_cross_sectional_r2(csr::CrossSectionalRegression, Z::Arr3Num, X::MatNum,
                        W::MatNum) -> Number

Return 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

  1. Take the per-observation vector through cross_sectional_r2.
  2. Return the mean of its finite entries, and NaN when it holds none.

Arguments

  • csr: A cross-sectional regression result.
  • Z::Arr3Num: Exposure tensor observations × assets × factors.
  • X::MatNum: Asset returns matrix observations × assets.
  • W::MatNum: Cross-sectional weights matrix observations × assets.

Validation

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

Related

source
PortfolioOptimisers.port_opt_viewMethod
port_opt_view(csr::CrossSectionalRegression, i, args...)

Return a view of a CrossSectionalRegression result, selecting only the assets indexed by i.

Algorithm

  1. Take a column view of eps over i, giving the residuals of the selected assets. The asset axis of a cross-sectional result is the second one, because a row of eps is one observation.
  2. Build a new CrossSectionalRegression from 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 ┴ nothing

Related

source