Algorithm and implementation notes
Source:vignettes/algorithm-and-implementation.Rmd
algorithm-and-implementation.RmdThis vignette documents the algorithm behind mspca(),
the implementation choices that make it fast, and how to set its
parameters. Readers who want the theoretical analysis should consult
Cory-Wright and Pauphilet (2026).
The problem
The goal of sparse PCA is to identify loading vectors that collectively explain a large share of the variance in the data, while each vector involves only a small number of the original features. In the single-component case () this is
for which many efficient algorithms exist (dβAspremont et al. 2007; Yuan and Zhang 2013; Bertsimas et al. 2022). The challenge in the -component case is coordinating the components so that they are non-redundant.
In standard dense PCA, non-redundancy is ensured by requiring the
leading eigenvectors of
to be mutually orthogonal and their projections to be uncorrelated.
These two properties hold simultaneously for eigenvectors but generally
cannot both be enforced in the sparse setting. msPCA
therefore supports either type of coupling constraint, selected with
feasibilityConstraintType:
-
Orthogonality
(
feasibilityConstraintType = 0, the default): the loading vectors are required to be mutually orthogonal, for all . This is the direct geometric analogue of standard PCA. -
Zero pairwise correlation
(
feasibilityConstraintType = 1): the projected components are required to be uncorrelated in the data, for all . This ensures each component captures statistically distinct information.
Writing
to encode the constraint type, the
-component
problem solved by msPCA is
Orthogonality corresponds to and zero correlation to . In the zero-correlation case the implementation uses divided by the total variance . This leaves the feasible set unchanged and makes the violation measure invariant to a rescaling of the data.
The objective is the sum of per-component variances. Most approaches for sparse PCA with multiple PCs use this objective (Zou et al. 2006; JournΓ©e et al. 2010; Lu and Zhang 2012; Vu et al. 2013; Benidis et al. 2016; Cory-Wright and Pauphilet 2026). It corresponds to the variance of the orthogonal projection onto the span of only when the loading vectors are orthogonal; in general it is the sum of the marginal variances of the sparse components.
Evaluation metrics
Variance explained. The cumulative fraction of total variance explained by is
computed by fraction_variance_explained(). Because
loading vectors may not be orthogonal, interpret this as a cumulative
component-variance score rather than the variance of the orthogonal
projection onto the span of
.
Per-component contributions are returned by
fraction_variance_explained_perPC() and, unnormalized, by
variance_explained_perPC().
Feasibility. The constraint violation measures how far the returned solution is from satisfying the coupling constraints. Under orthogonality it is
and under zero pairwise correlation
both computed by feasibility_violation_off(). The second
is normalized by the total variance
:
the loading vectors are unit-norm, so
is homogeneous of degree one in
and the unnormalized sum would depend on the units of the data. After
normalization each pairwise term reads as a fraction of the total
variance, hence is scale-invariant. The same convention is used inside
the solver, and for the nonredundancy matrices stored on
the fitted object.
Lagrangian alternating maximization
The key algorithmic idea is to handle the coupling constraints in (1) via a quadratic penalty in the objective. Introducing non-negative penalty parameters for each pair (with ) gives the penalized objective
For fixed components , , and fixed penalties, the subproblem for reduces to a non-convex single-component sparse PCA problem against the perturbed covariance matrix
This decomposition holds for both constraint types: with the perturbation is , and with it is .
Most methods for computing the leading sparse eigenvector require the matrix to be positive semidefinite. If is not, we add a diagonal shift , which does not change the optimal solution because all feasible vectors have unit norm. The shift used is , which bounds the deflation term by for every and so guarantees . It is the smallest shift of this form, which preserves the eigengap the power method relies on, and it is recomputed at each inner step from the current components, so no estimate of the spectrum of is needed. Under orthogonality and it reduces to .
Iterating over and progressively increasing the penalties to drive constraint violations toward zero yields the following scheme.
Algorithm 1: Lagrangian alternating maximization for problem (1)
Require: covariance matrix , number of components , sparsity budgets , constraint matrix , iterations , feasibility tolerance
- Initialize for all ; set for all
- for do
- Β Β for do
- Β Β Β Β Compute
- Β Β Β Β Compute via Algorithm 2 applied to
- Β Β end for
- Β Β if then
- Β Β Β Β Record as feasible; update best solution if the objective improves
- Β Β end if
- Β Β Increase the values (see below)
- end for
- return best feasible solution found, or the last iterate if none was found
Each single-component subproblem is solved via the truncated power method (TPM, Yuan and Zhang 2013), which alternates between a power step (multiplying by ) and a truncation step (retaining only the largest-magnitude entries). In practice TPM often finds high-quality solutions around two orders of magnitude faster than certifiably optimal methods (Berk and Bertsimas 2019; Behdin and Mazumder 2026). Each call starts from the current iterate and then draws random restarts, which guard against poor local optima.
Algorithm 2: truncated power method with random restarts (Yuan and Zhang 2013)
Require: matrix , sparsity budget , iteration limit , time limit
- repeat
- Β Β Draw
- Β Β repeat
- Β Β Β Β Β (power step)
- Β Β Β Β Zero out all but the entries of largest in absolute value Β (truncation step)
- Β Β Β Β
- Β Β until converges
- Β Β if then ; reset iteration count
- until time limit exceeded or no improvement after iterations
- return
The scheme resembles an iterative deflation procedure (Mackey 2008) in which a single-component sparse PCA problem is solved against a surrogate matrix at each iteration. The key difference is that the deflated matrix is induced by an explicit penalty on the non-redundancy constraints, which is progressively increased throughout the algorithm.
Implementation details
Penalty update. The penalty parameters are initialized to zero and increased progressively across outer iterations, letting the algorithm explore freely at first and gradually tightening the feasibility requirement. During the first 15% of iterations the increment is proportional to the total constraint violation ; for the remaining iterations we switch to a ratio-based update proportional to the ratio of the current objective to the current constraint violation, which produces larger and more decisive increases; during the last 25% of iterations the step-size coefficient is further increased by a factor of 5 to accelerate final convergence to feasibility. See Cory-Wright and Pauphilet (2026) for a full description and theoretical justification of the update rule.
Penalty weights. We write , with a single scalar carrying the schedule above and a per-component weight fixed at the first iteration. The weight is
the same expression for both constraint types. Its effect is that the penalty a component can contribute is bounded by , i.e.Β times the variance that component explains, whichever is in force. The scalar is thus a dimensionless penalty-to-objective ratio and the schedule behaves the same way under both constraints. Under orthogonality and , so is simply the variance explained by component ; under zero correlation and the factor offsets the shrinkage that normalizing by its trace would otherwise apply to the penalty. Numerator and denominator are homogeneous of the same degree in , so the weights are invariant to a rescaling of the data. With this weight the PSD shift of the previous section also takes the common form .
Termination. Algorithm 1 stops when the number of
outer iterations reaches maxIter (default 200), or earlier
at any iteration where the current solution is feasible and the change
in objective value since the previous iteration falls below
stallingTolerance (default 1e-8).
Feasibility tracking. At each iteration Algorithm 1
checks whether the current solution satisfies the coupling constraint up
to feasibilityTolerance (default 1e-4). The best feasible
solution encountered across all iterations is returned. If no feasible
solution is found within the iteration budget, the algorithm returns the
solution with the smallest observed constraint violation.
Software implementation. All computations are
carried out in C++ via the Rcpp (Eddelbuettel and FranΓ§ois 2011) and
RcppEigen (Bates and Eddelbuettel
2013) interfaces, with a lightweight R wrapper providing the
user-facing API. To avoid materializing the
perturbed matrix
at each inner-loop step, the C++ back-end represents it implicitly: each
product
is evaluated as
where collects the previously computed components (, for orthogonal loadings; , , for uncorrelated PCs) and contains the corresponding scaled penalty coefficients. This eliminates the matrix-build cost per component update while keeping the per-step cost at .
When the raw data matrix
is provided (type = "X"), the product
is replaced by the two-pass evaluation
at cost
instead of
,
which is substantially more scalable when
and avoids forming the
covariance matrix entirely. After the first outer iteration the previous
iterate serves as a warm start for Algorithm 2, substantially reducing
the number of random restarts required.
Computational complexity
The dominant cost of Algorithm 1 per outer iteration is
calls to Algorithm 2. With the implicit matrix-vector representation
above, applying
to a vector costs
with type = "Sigma" and
with type = "X". In both cases the
deflation term is negligible for moderate
.
Each call performs at most
such products, giving a worst-case per-outer-iteration cost of
.
In practice, warm-start initialization and early convergence detection
reduce the effective number of TPM iterations substantially, so the
empirical cost is much closer to
per outer iteration.
Guidance on parameter choices
Choosing the sparsity budgets ks
The budgets
are the primary tuning parameters. A practical approach is to run
mspca() over a range of values and plot the trade-off
between FVE and sparsity:
library("msPCA")
Sigma <- cor(datasets::mtcars)
ks_grid <- seq(2, 10, by = 1)
trade_off <- sapply(ks_grid, function(k) {
set.seed(42)
res <- mspca(Sigma, r = 3, ks = rep(k, 3), verbose = FALSE)
fraction_variance_explained(Sigma, res$x_best)
})
plot(ks_grid, trade_off, type = "b",
xlab = "sparsity budget k", ylab = "fraction of variance explained")Domain knowledge often provides a natural guide: if each PC is expected to represent a distinct thematic cluster of features, setting to the anticipated cluster size is a good starting point.
Choosing the constraint type
Orthogonality (feasibilityConstraintType = 0) is
appropriate when the loading vectors are to be used as a projection
basis, or when the geometric structure of the components matters. Zero
pairwise correlation (feasibilityConstraintType = 1) is
preferable when the primary goal is statistical decorrelation of the
projected data. In our experience the two options yield similar results
when
is close to the identity, but can differ noticeably for strongly
correlated datasets. See
vignette("case-study-snp500", package = "msPCA") for a
worked comparison.
Both sets of pairwise violations are computed at fit time and stored
in nonredundancy, so a solution can be scored under the
definition that was not enforced without a refit:
Iteration and restart budgets
maxIter (default 200) caps the number of outer
iterations. Lowering it speeds up large problems at some risk of
returning a less-refined solution; the case study uses
maxIter = 100 on a 423-variable problem without noticeable
loss. maxRestartTPM and minRestartTPM control
the number of random restarts in the inner TPM call at the first and
subsequent outer iterations respectively; the defaults (30 and 20) are
conservative and can be reduced when runtime matters more than guarding
against poor local optima.