\(L^p\) Rotation
lp.RdPerforms \(L^p\) rotation to obtain sparse factor loadings.
Arguments
- A
Initial factor loadings matrix to be rotated.
- Tmat
Initial rotation matrix.
- p
Component-wise \(L^p\) where 0 < p \(\le\) 1.
- normalize
Not recommended for \(L^p\) rotation.
- eps
Convergence is assumed when the norm of the gradient is smaller than
eps.- maxit
Maximum number of iterations allowed in the main loop.
- gpaiter
Maximum iterations for the inner GPA rotation loop. The goal is to decrease the objective value, not fully optimize the inner loop. Warnings may appear but can be ignored if the main loop converges.
Value
A GPArotation object, which is a list containing:
- loadings
Rotated loadings matrix, with one column per factor.
- Th
Rotation matrix, satisfying
loadings %*% t(Th) = A.- Table
Data frame recording iteration details: iteration count, objective value, and elapsed time.
- method
String indicating the rotation objective function.
- orthogonal
Logical indicating whether the rotation is orthogonal.
- convergence
Logical indicating whether convergence was achieved. Convergence is assessed element-wise using
epsas tolerance.- Phi
Covariance matrix of rotated factors,
t(Th) %*% Th.
Details
These functions optimize an \(L^p\) rotation objective where
0 < p <= 1. A smaller value of p promotes greater sparsity
in the loading matrix but increases computational difficulty. For
guidance on choosing p, see the Concluding Remarks in
Liu et al. (2023).
The user-facing wrapper functions lpT and lpQ
provide random start functionality on top of GPForth.lp and
GPFoblq.lp respectively, analogous to how GPFRSorth
and GPFRSoblq wrap GPForth and GPFoblq for
standard rotation criteria. For most users lpT and lpQ
are the recommended entry points.
Since the \(L^p\) objective is nonsmooth, a different optimization
method is required compared to smooth rotation criteria. GPForth.lp
and GPFoblq.lp replace GPForth and GPFoblq for
orthogonal and oblique \(L^p\) rotations, respectively.
The optimization uses an iterative reweighted least squares (IRLS) approach.
The nonsmooth objective is approximated by a smooth weighted least squares
function in the outer loop, which is then optimized using GPA in the inner
loop (gpaiter controls the maximum inner iterations).
Normalization is not recommended for \(L^p\) rotation and may produce unexpected results.
See also
lpT,
lpQ,
vgQ.lp.wls
References
Liu, X., Wallin, G., Chen, Y., and Moustaki, I. (2023). Rotation to sparse loadings using \(L^p\) losses and related inference problems. Psychometrika, 88(2), 527–553. doi: 10.1007/s11336-023-09911-y
Examples
data("WansbeekMeijer", package = "GPArotation")
fa.unrotated <- factanal(factors = 2, covmat = NetherlandsTV, rotation = "none")
options(warn = -1)
# Orthogonal rotation — single start
fa.lpT1 <- GPForth.lp(loadings(fa.unrotated), p = 1)
# Orthogonal rotation — 10 random starts
fa.lpT <- lpT(loadings(fa.unrotated), Tmat = Random.Start(2), p = 1,
randomStarts = 10)
print(fa.lpT, digits = 5, sortLoadings = FALSE, Table = TRUE, rotateMat = TRUE)
#> Orthogonal rotation method Lp rotation, p=1 converged at lowest minimum.
#> Of 10 random starts 100% converged, 40% at the same lowest minimum.
#> Random starts converged to 2 different local minima.
#> Loadings at lowest minimum:
#> Factor1 Factor2
#> NL1 -0.66539 0.42787
#> TV2 -0.65607 0.52479
#> NL3 -0.64842 0.42123
#> RTL4 -0.11493 0.69264
#> RTL5 -0.07432 0.75643
#> Veronica 0.00980 0.81783
#> SBS6 -0.00047 0.72669
#>
#> Factor1 Factor2
#> SS loadings 1.31245 2.88476
#> Proportion Var 0.18749 0.41211
#> Cumulative Var 0.18749 0.59960
#>
#> Rotating matrix:
#> [,1] [,2]
#> [1,] -0.48591 0.87401
#> [2,] 0.87401 0.48591
#>
#> Iteration table:
#> iter f time
#> elapsed 14 0.93384 0.012
# Oblique rotation — single start
fa.lpQ1 <- GPFoblq.lp(loadings(fa.unrotated), p = 1)
# Oblique rotation — 10 random starts
fa.lpQ <- lpQ(loadings(fa.unrotated), p = 1, randomStarts = 10)
summary(fa.lpQ, Structure = TRUE)
#> Oblique rotation method Lp rotation, p=1 converged in 13 iterations.
#> Pattern (loadings):
#> Factor1 Factor2
#> NL1 -0.001 0.792
#> TV2 0.101 0.782
#> NL3 0.003 0.771
#> RTL4 0.615 0.144
#> RTL5 0.704 0.096
#> Veronica 0.819 -0.003
#> SBS6 0.722 0.009
#>
#> Structure:
#> Factor1 Factor2
#> NL1 0.422 0.791
#> TV2 0.519 0.836
#> NL3 0.415 0.773
#> RTL4 0.692 0.472
#> RTL5 0.756 0.472
#> Veronica 0.818 0.435
#> SBS6 0.727 0.394
# Compare Lp (p=1), Lp (p=0.5), and Geomin oblique rotations
set.seed(1020)
fa.lpQ1 <- lpQ(loadings(fa.unrotated), p = 1, randomStarts = 10)
fa.lpQ0.5 <- lpQ(loadings(fa.unrotated), p = 0.5, randomStarts = 10)
fa.geo <- geominQ(loadings(fa.unrotated), randomStarts = 10)
# With factor ordering using internal sortGPALoadings helper
res <- round(cbind(GPArotation:::.sortGPALoadings(fa.lpQ1)$loadings,
GPArotation:::.sortGPALoadings(fa.lpQ0.5)$loadings,
GPArotation:::.sortGPALoadings(fa.geo)$loadings), 3)
print(c("oblique -- Lp p=1 Lp p=0.5 Geomin"))
#> [1] "oblique -- Lp p=1 Lp p=0.5 Geomin"
print(res)
#> Factor1 Factor2 Factor1 Factor2 Factor1 Factor2
#> NL1 -0.001 0.792 -0.002 0.792 -0.020 0.802
#> TV2 0.101 0.782 0.101 0.781 0.085 0.788
#> NL3 0.003 0.771 0.002 0.772 -0.015 0.782
#> RTL4 0.615 0.144 0.618 0.138 0.627 0.119
#> RTL5 0.704 0.096 0.708 0.089 0.719 0.067
#> Veronica 0.819 -0.003 0.824 -0.011 0.839 -0.038
#> SBS6 0.722 0.009 0.726 0.002 0.740 -0.023
# Without factor ordering
res <- round(cbind(fa.lpQ1$loadings, fa.lpQ0.5$loadings, fa.geo$loadings), 3)
print(c("oblique -- Lp p=1 Lp p=0.5 Geomin"))
#> [1] "oblique -- Lp p=1 Lp p=0.5 Geomin"
print(res)
#> Factor1 Factor2 Factor1 Factor2 Factor1 Factor2
#> NL1 0.792 -0.001 -0.792 0.002 0.020 -0.802
#> TV2 0.782 0.101 -0.781 -0.101 -0.085 -0.788
#> NL3 0.771 0.003 -0.772 -0.002 0.015 -0.782
#> RTL4 0.144 0.615 -0.138 -0.618 -0.627 -0.119
#> RTL5 0.096 0.704 -0.089 -0.708 -0.719 -0.067
#> Veronica -0.003 0.819 0.011 -0.824 -0.839 0.038
#> SBS6 0.009 0.722 -0.002 -0.726 -0.740 0.023
options(warn = 0)