Shrunk Expected Returns

PortfolioOptimisers.GrandMeanType
struct GrandMean <: AbstractShrunkExpectedReturnsTarget

Fills the shrinkage target with the grand mean of the sample expected returns.

Every element of the target holds the same value, so a shrinkage estimator pulls each asset toward the average of the whole universe. The three targets are each a multiple of the vector of ones, and only the multiplier separates them.

Mathematical definition

\[\begin{align} b_j &= \bar{\mu} = \frac{1}{N} \sum_{i=1}^{N} \hat{\mu}_i\,, \quad j = 1, \ldots, N\,. \end{align}\]

Where:

  • $b_j$: $j$-th entry of the shrinkage target vector.
  • $\hat{\boldsymbol{\mu}}$: $N \times 1$ vector of sample expected returns, whose $i$-th entry is $\hat{\mu}_i$.
  • $N$: Number of assets.

The sample covariance matrix does not enter the form, so this target is the only one of the three that a singular covariance matrix leaves untouched.

Algorithm

The branch of target_mean that this tag selects runs these steps.

  1. Take the unweighted mean of mu, giving val.
  2. Return the constant range that repeats val length(mu) times.

Constructors

GrandMean() -> GrandMean

Examples

julia> GrandMean()GrandMean()

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Equation 3.43.
  • [25] A. Meucci. Risk and Asset Allocation (Springer Berlin Heidelberg, 2005).
  • [26] Y. Feng and D. P. Palomar. A signal processing perspective of financial engineering. Foundations and Trends in Signal Processing 9, 1–231 (2016).
source
PortfolioOptimisers.VolatilityWeightedType
struct VolatilityWeighted <: AbstractShrunkExpectedReturnsTarget

Fills the shrinkage target with the inverse-covariance-weighted mean of the sample expected returns.

The inverse covariance matrix supplies the weights. Under a diagonal covariance matrix each weight is the reciprocal of the asset's variance, so a riskier asset counts for less. The name says volatility, and the form reads the whole inverse covariance matrix, so an off-diagonal entry moves the target too.

Mathematical definition

\[\begin{align} b_j &= \bar{\mu}_{\mathrm{vol}} = \frac{\boldsymbol{1}^\intercal \hat{\mathbf{\Sigma}}^{-1} \hat{\boldsymbol{\mu}}}{\boldsymbol{1}^\intercal \hat{\mathbf{\Sigma}}^{-1} \boldsymbol{1}}\,, \quad j = 1, \ldots, N\,. \end{align}\]

Where:

  • $b_j$: $j$-th entry of the shrinkage target vector.
  • $\hat{\boldsymbol{\mu}}$: $N \times 1$ vector of sample expected returns, whose $i$-th entry is $\hat{\mu}_i$.
  • $\hat{\mathbf{\Sigma}}$: Estimated covariance matrix.
  • $\boldsymbol{1}$: $N \times 1$ vector of ones.
  • $N$: Number of assets.

Algorithm

The branch of target_mean that this tag selects runs these steps.

  1. When isigma is nothing, solve sigma \ LinearAlgebra.I, giving isigma. A caller that already holds the inverse passes it, so the solve runs once per estimate at most.
  2. When mu has one row, flatten it with vec, so that the product isigma * mu is defined.
  3. Divide the sum of isigma * mu by the sum of isigma, giving val. Summing a matrix-vector product is the numerator $\boldsymbol{1}^\intercal \hat{\mathbf{\Sigma}}^{-1} \hat{\boldsymbol{\mu}}$, and summing the matrix is the denominator $\boldsymbol{1}^\intercal \hat{\mathbf{\Sigma}}^{-1} \boldsymbol{1}$.
  4. Return the constant range that repeats val length(mu) times.

Constructors

VolatilityWeighted() -> VolatilityWeighted

Examples

julia> VolatilityWeighted()VolatilityWeighted()

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Equation 3.43.
  • [25] A. Meucci. Risk and Asset Allocation (Springer Berlin Heidelberg, 2005).
  • [26] Y. Feng and D. P. Palomar. A signal processing perspective of financial engineering. Foundations and Trends in Signal Processing 9, 1–231 (2016).
source
PortfolioOptimisers.MeanSquaredErrorType
struct MeanSquaredError <: AbstractShrunkExpectedReturnsTarget

Fills the shrinkage target with the trace of the covariance matrix divided by the number of observations.

Every element of the target holds the same value. The target reads a scale off the covariance matrix alone, so the sample expected returns do not enter it. It is the only one of the three targets that a shift of every asset's mean leaves where it was.

Mathematical definition

\[\begin{align} b_j &= \frac{\mathrm{tr}(\hat{\mathbf{\Sigma}})}{T}\,, \quad j = 1, \ldots, N\,. \end{align}\]

Where:

  • $b_j$: $j$-th entry of the shrinkage target vector.
  • $\mathrm{tr}(\cdot)$: Matrix trace operator.
  • $\hat{\mathbf{\Sigma}}$: Estimated covariance matrix.
  • $T$: Number of observations.
  • $N$: Number of assets.

Algorithm

The branch of target_mean that this tag selects runs these steps.

  1. Divide the trace of sigma by T, giving val. T is a required keyword of this branch alone.
  2. Return the constant range that repeats val length(mu) times.

Constructors

MeanSquaredError() -> MeanSquaredError

Examples

julia> MeanSquaredError()MeanSquaredError()

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Equation 3.43.
  • [25] A. Meucci. Risk and Asset Allocation (Springer Berlin Heidelberg, 2005).
  • [26] Y. Feng and D. P. Palomar. A signal processing perspective of financial engineering. Foundations and Trends in Signal Processing 9, 1–231 (2016).
source
PortfolioOptimisers.JamesSteinType
struct JamesStein{__T_tgt} <: AbstractShrunkExpectedReturnsAlgorithm

Blends the sample expected returns with the target under an intensity read off the covariance eigenvalues.

The intensity grows with the number of assets and falls with the distance between the sample mean and the target.

Fields

  • tgt: Shrinkage target.

Constructors

JamesStein(;    tgt::AbstractShrunkExpectedReturnsTarget = GrandMean()) -> JamesStein

Keywords correspond to the struct's fields.

Examples

julia> JamesStein()JamesStein  tgt ┴ GrandMean()

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 3.4.1.1.
  • [25] A. Meucci. Risk and Asset Allocation (Springer Berlin Heidelberg, 2005).
source
PortfolioOptimisers.BayesSteinType
struct BayesStein{__T_tgt} <: AbstractShrunkExpectedReturnsAlgorithm

Blends the sample expected returns with the target under an empirical Bayes intensity.

The intensity falls with the inverse-covariance-weighted distance between the sample mean and the target, so a short sample shrinks harder.

Fields

  • tgt: Shrinkage target.

Constructors

BayesStein(;    tgt::AbstractShrunkExpectedReturnsTarget = GrandMean()) -> BayesStein

Keywords correspond to the struct's fields.

Examples

julia> BayesStein()BayesStein  tgt ┴ GrandMean()

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 3.4.1.2.
  • [27] P. Jorion. Bayes-Stein estimation for portfolio analysis. The Journal of Financial and Quantitative Analysis 21, 279–292 (1986).
source
PortfolioOptimisers.BodnarOkhrinParolyaType
struct BodnarOkhrinParolya{__T_tgt} <: AbstractShrunkExpectedReturnsAlgorithm

Combines the sample expected returns and the target under two coefficients from random matrix theory.

The two coefficients are set separately and neither is a convex weight, so the result is not a blend between the sample mean and the target. It suits a universe whose asset count is a large fraction of its observation count, and it needs more observations than assets.

Fields

  • tgt: Shrinkage target.

Constructors

BodnarOkhrinParolya(;    tgt::AbstractShrunkExpectedReturnsTarget = GrandMean()) -> BodnarOkhrinParolya

Keywords correspond to the struct's fields.

Examples

julia> BodnarOkhrinParolya()BodnarOkhrinParolya  tgt ┴ GrandMean()

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 3.4.1.3.
  • [28] T. Bodnar, O. Okhrin and N. Parolya. Optimal shrinkage estimator for high-dimensional mean vector. Journal of Multivariate Analysis 170, 63–79 (2019).
source
PortfolioOptimisers.ShrunkExpectedReturnsType
struct ShrunkExpectedReturns{__T_me, __T_ce, __T_alg} <: AbstractShrunkExpectedReturnsEstimator

Shrinks the sample expected returns toward a target chosen by the shrinkage algorithm.

It holds the three parts the shrinkage needs: a mean estimator, a covariance estimator and a shrinkage algorithm, which carries the target.

Fields

  • me: Expected returns estimator.
  • ce: Covariance estimator.
  • alg: Expected returns shrinkage algorithm.

Constructors

ShrunkExpectedReturns(;    me::AbstractExpectedReturnsEstimator = SimpleExpectedReturns(),    ce::StatsBase.CovarianceEstimator = PortfolioOptimisersCovariance(),    alg::AbstractShrunkExpectedReturnsAlgorithm = JamesStein()) -> ShrunkExpectedReturns

Keywords correspond to the struct's fields.

Propagated parameters

When factory is called on this type, the following @fprop-tagged fields are automatically propagated:

  • me: Recursively updated via factory.
  • ce: Recursively updated via factory.

View parameters

When port_opt_view is called on this type, the following @vprop-tagged fields are automatically subset to the selected indices:

Examples

julia> ShrunkExpectedReturns()ShrunkExpectedReturns   me ┼ SimpleExpectedReturns      │   w ┴ nothing   ce ┼ PortfolioOptimisersCovariance      │   ce ┼ Covariance      │      │    me ┼ SimpleExpectedReturns      │      │       │   w ┴ nothing      │      │    ce ┼ GeneralCovariance      │      │       │   ce ┼ StatsBase.SimpleCovariance: StatsBase.SimpleCovariance(true)      │      │       │    w ┴ nothing      │      │   alg ┼ FullMoment()      │      │     w ┴ nothing      │   mp ┼ MatrixProcessing      │      │     pdm ┼ Posdef      │      │         │      alg ┼ UnionAll: NearestCorrelationMatrix.Newton      │      │         │   kwargs ┴ @NamedTuple{}: NamedTuple()      │      │      dn ┼ nothing      │      │      dt ┼ nothing      │      │     alg ┼ nothing      │      │   order ┴ NTuple{4, Symbol}: (:pdm, :dn, :dt, :alg)  alg ┼ JamesStein      │   tgt ┴ GrandMean()

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 3.4.1.
source
Statistics.meanMethod
Statistics.mean(me::ShrunkExpectedReturns, X::MatNum; dims::Int = 1, kwargs...)

Compute shrunk expected returns using the specified estimator.

This method applies a shrinkage algorithm to the sample expected returns, pulling them toward a target to reduce estimation error, especially in high-dimensional settings. No method of this family clamps its coefficients. JamesStein and BayesStein write (1 - alpha) * mu + alpha * b, and nothing holds alpha inside $[0, 1]$, so the result can sit outside the segment that joins the sample mean and the target. BodnarOkhrinParolya sets its two coefficients separately and they do not sum to one.

Mathematical definition

James-Stein shrinkage of the sample expected returns toward the target:

\[\begin{align} \hat{\boldsymbol{\mu}}_{JS} &= (1 - \alpha)\, \hat{\boldsymbol{\mu}} + \alpha\, \boldsymbol{b}\,, \\ \alpha &= \frac{N \bar{\lambda} - 2 \lambda_{\max}}{T \, \lVert \hat{\boldsymbol{\mu}} - \boldsymbol{b} \rVert_2^2}\,. \end{align}\]

Where:

  • $\hat{\boldsymbol{\mu}}_{JS}$: James-Stein shrunk expected returns.
  • $\hat{\boldsymbol{\mu}}$: $N \times 1$ vector of sample expected returns, whose $i$-th entry is $\hat{\mu}_i$.
  • $\boldsymbol{b}$: $N \times 1$ shrinkage target vector, every entry of which holds the same value.
  • $\alpha$: Shrinkage intensity, the weight the blend gives the target.
  • $\bar{\lambda}$: Mean eigenvalue of the covariance matrix.
  • $\lambda_{\max}$: Maximum eigenvalue of the covariance matrix.
  • $T$: Number of observations.
  • $N$: Number of assets.

Two consequences of the form bound where it is usable.

  • $N \bar{\lambda}$ is the trace of the covariance matrix, so $N \bar{\lambda} \leq 2 \lambda_{\max}$ whenever $N \leq 2$. The intensity is then negative and the blend extrapolates away from the target rather than toward it.
  • The denominator is zero when the target equals the sample mean. GrandMean and VolatilityWeighted both reduce to the sample mean at $N = 1$, so a one-asset sample raises a DomainError under either of them. MeanSquaredError does not read the sample mean, so it stays finite there.

Algorithm

  1. Compute the sample expected returns with me.me, giving mu.
  2. Compute the covariance matrix with me.ce, giving sigma.
  3. Read T and N off size(X), and swap them when dims is 2.
  4. Compute the shrinkage target with target_mean, giving b, and transpose it into a row when dims is 1.
  5. Eigendecompose sigma, giving evals.
  6. Subtract b from mu, giving mb, and form the intensity alpha from evals, mb, N and T.
  7. Return the blend (1 - alpha) * mu + alpha * b.

Arguments

  • me: Shrunk expected returns estimator.

    • me::ShrunkExpectedReturns{<:Any, <:Any, <:JamesStein}: Use the James-Stein algorithm.
    • me::ShrunkExpectedReturns{<:Any, <:Any, <:BayesStein}: Use the Bayes-Stein algorithm.
    • me::ShrunkExpectedReturns{<:Any, <:Any, <:BodnarOkhrinParolya}: Use the Bodnar-Okhrin-Parolya algorithm.
  • X: Data matrix observations × assets if the dims keyword does not exist or dims = 1, assets × observations when dims = 2.

  • dims: Dimension along which to perform the computation.

  • kwargs...: Additional keyword arguments passed to the mean and covariance estimators.

Validation

  • !iszero(dot(mu - b, mu - b)). The JamesStein intensity divides by this denominator, and it is exactly zero when the target equals the sample mean. GrandMean and VolatilityWeighted both do so at $N = 1$, and the other two overloads state their own rules for raising.

Returns

  • mu::ArrNum: Expected returns vector assets x 1 if the dims keyword does not exist or dims = 2, 1 x assets if dims = 1.

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 3.4.1.1.
  • [25] A. Meucci. Risk and Asset Allocation (Springer Berlin Heidelberg, 2005).
source
Statistics.meanMethod
mean(
    me::ShrunkExpectedReturns{<:Any, <:Any, <:BayesStein},
    X::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}};
    dims,
    kwargs...
) -> Any

BayesStein overload of mean(me::ShrunkExpectedReturns, X::MatNum; dims::Int = 1, kwargs...). Shrinks sample returns toward the target using a Bayesian formula with inverse covariance weighting.

Mathematical definition

Bayes-Stein shrinkage of the sample expected returns toward the target:

\[\begin{align} \alpha &= \frac{N + 2}{(N + 2) + T \, (\hat{\boldsymbol{\mu}} - \boldsymbol{b})^\intercal \hat{\mathbf{\Sigma}}^{-1} (\hat{\boldsymbol{\mu}} - \boldsymbol{b})}\,, \\ \hat{\boldsymbol{\mu}}_{BS} &= (1 - \alpha)\hat{\boldsymbol{\mu}} + \alpha \boldsymbol{b}\,. \end{align}\]

Where:

  • $\alpha$: Shrinkage intensity, the weight the blend gives the target.
  • $\hat{\boldsymbol{\mu}}_{BS}$: Bayes-Stein shrunk expected returns.
  • $\hat{\boldsymbol{\mu}}$: $N \times 1$ vector of sample expected returns, whose $i$-th entry is $\hat{\mu}_i$.
  • $\boldsymbol{b}$: $N \times 1$ shrinkage target vector, every entry of which holds the same value.
  • $\hat{\mathbf{\Sigma}}$: Estimated covariance matrix.
  • $T$: Number of observations.
  • $N$: Number of assets.

Two consequences of the form separate this intensity from the James-Stein one.

  • The quadratic form is non-negative whenever $\hat{\mathbf{\Sigma}}$ is positive semidefinite, so $\alpha$ then lies in $(0, 1]$. This is the only one of the three algorithms whose coefficient is a convex weight without a clamp, and it returns the target exactly when the quadratic form is zero. A covariance estimator that returns an indefinite matrix breaks the bound.
  • The quadratic form uses the inverse of the covariance matrix that me.ce returns. Equation 3.44 of [5] states the same intensity over the bias-corrected matrix $\bar{\mathbf{\Sigma}} = \frac{T-1}{T-N-1} \hat{\mathbf{\Sigma}}$; this method uses $\hat{\mathbf{\Sigma}}$. The correction raises $\alpha$.

Algorithm

  1. Compute the sample expected returns with me.me, giving mu.
  2. Compute the covariance matrix with me.ce, giving sigma.
  3. Read T and N off size(X), and swap them when dims is 2.
  4. Solve sigma \ LinearAlgebra.I, giving isigma, and pass it to target_mean so that the VolatilityWeighted branch does not solve a second time.
  5. Compute the shrinkage target, giving b, and transpose it into a row when dims is 1.
  6. Flatten mu - b with vec, giving mb, and form the intensity alpha from mb, isigma, N and T.
  7. Return the blend (1 - alpha) * mu + alpha * b.

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 3.4.1.2, Equation 3.44.
  • [27] P. Jorion. Bayes-Stein estimation for portfolio analysis. The Journal of Financial and Quantitative Analysis 21, 279–292 (1986).
source
Statistics.meanMethod
mean(
    me::ShrunkExpectedReturns{<:Any, <:Any, <:BodnarOkhrinParolya},
    X::AbstractMatrix{<:Union{var"#s136", var"#s53"} where {var"#s136"<:Number, var"#s53"<:AbstractJuMPScalar}};
    dims,
    kwargs...
) -> Any

BodnarOkhrinParolya overload of mean(me::ShrunkExpectedReturns, X::MatNum; dims::Int = 1, kwargs...). Shrinks sample returns toward the target using the Bodnar-Okhrin-Parolya formula, designed for robust high-dimensional estimation. It needs $T > N$: the term $N/(T-N)$ is undefined at $T = N$ and changes sign below it, so a square or wide returns matrix raises a DomainError.

Mathematical definition

Three inverse-covariance-weighted quadratic forms carry the sample mean and the target:

\[\begin{align} u &= \hat{\boldsymbol{\mu}}^\intercal \hat{\mathbf{\Sigma}}^{-1} \hat{\boldsymbol{\mu}}\,, \\ v &= \boldsymbol{b}^\intercal \hat{\mathbf{\Sigma}}^{-1} \boldsymbol{b}\,, \\ w &= \hat{\boldsymbol{\mu}}^\intercal \hat{\mathbf{\Sigma}}^{-1} \boldsymbol{b}\,. \end{align}\]

The two coefficients and the combination follow from them:

\[\begin{align} \alpha &= \frac{(u - N/(T-N))v - w^2}{uv - w^2}\,, \\ \beta &= \frac{(1-\alpha) w}{u}\,, \\ \hat{\boldsymbol{\mu}}_{BOP} &= \alpha \hat{\boldsymbol{\mu}} + \beta \boldsymbol{b}\,. \end{align}\]

Where:

  • $u$, $v$, $w$: Inverse-covariance-weighted quadratic forms.
  • $\alpha$, $\beta$: Shrinkage coefficients.
  • $\hat{\boldsymbol{\mu}}_{BOP}$: Bodnar-Okhrin-Parolya shrunk expected returns.
  • $\hat{\boldsymbol{\mu}}$: $N \times 1$ vector of sample expected returns, whose $i$-th entry is $\hat{\mu}_i$.
  • $\boldsymbol{b}$: $N \times 1$ shrinkage target vector, every entry of which holds the same value.
  • $\hat{\mathbf{\Sigma}}$: Estimated covariance matrix.
  • $T$: Number of observations.
  • $N$: Number of assets.

Three consequences of the form separate this algorithm from the other two.

  • $\alpha$ and $\beta$ are set separately and do not sum to one, so the result is not a point on the segment that joins $\hat{\boldsymbol{\mu}}$ and $\boldsymbol{b}$. Cancelling the $w^2$ term rewrites the coefficient as $\alpha = 1 - \frac{N}{T-N} \frac{v}{uv - w^2}$, so $\alpha < 1$ always, and $\alpha < 0$ exactly when $\frac{N}{T-N} v > uv - w^2$. The combination then extrapolates away from the sample mean.
  • $uv - w^2$ is a Cauchy-Schwarz gap in the inner product $\langle \boldsymbol{x}, \boldsymbol{y} \rangle = \boldsymbol{x}^\intercal \hat{\mathbf{\Sigma}}^{-1} \boldsymbol{y}$, so it vanishes exactly when the target is a multiple of the sample mean. At $N = 1$ every vector is such a multiple, so a one-asset sample raises a DomainError under all three targets.
  • Every target of this file is a multiple of the vector of ones, so writing $\boldsymbol{b} = c \boldsymbol{1}$ makes $v$ and $w$ scale with $c^2$ and $c$. The factor cancels in $\alpha$, which is therefore the same for the three targets on one sample, and survives in $\beta \boldsymbol{b}$, which is not.

Algorithm

  1. Compute the sample expected returns with me.me, giving mu.
  2. Compute the covariance matrix with me.ce, giving sigma.
  3. Read T and N off size(X), and swap them when dims is 2.
  4. Solve sigma \ LinearAlgebra.I, giving isigma, and pass it to target_mean so that the VolatilityWeighted branch does not solve a second time.
  5. Compute the shrinkage target, giving b, and transpose it into a row when dims is 1.
  6. Flatten mu and b into the vectors vm and vb, which the quadratic forms need whichever way dims orients the data.
  7. Form the three quadratic forms u, v and w from vm, vb and isigma.
  8. Form alpha from u, v, w, N and T, then beta from alpha, w and u.
  9. Return the combination alpha * mu + beta * b.

Validation

  • T > N, the estimator's own published condition. The term $N/(T-N)$ is undefined at $T = N$ and negative below it.
  • !iszero(u * v - w^2). Both coefficients divide by this Cauchy-Schwarz gap, and it is exactly zero when the target is a multiple of the sample mean. Every vector is such a multiple at $N = 1$.

Related

References

  • [5] D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025). Section 3.4.1.3.
  • [28] T. Bodnar, O. Okhrin and N. Parolya. Optimal shrinkage estimator for high-dimensional mean vector. Journal of Multivariate Analysis 170, 63–79 (2019).
source

References

[5]
D. Cajas. Advanced Portfolio Optimization: A Cutting-edge Quantitative Approach (Springer Nature Switzerland, 2025).
[25]
A. Meucci. Risk and Asset Allocation (Springer Berlin Heidelberg, 2005).
[26]
Y. Feng and D. P. Palomar. A signal processing perspective of financial engineering. Foundations and Trends in Signal Processing 9, 1–231 (2016).
[27]
P. Jorion. Bayes-Stein estimation for portfolio analysis. The Journal of Financial and Quantitative Analysis 21, 279–292 (1986).
[28]
T. Bodnar, O. Okhrin and N. Parolya. Optimal shrinkage estimator for high-dimensional mean vector. Journal of Multivariate Analysis 170, 63–79 (2019).