Skip to contents

Performs \(L^p\) rotation to obtain sparse factor loadings.

Usage

GPForth.lp(A, Tmat = diag(ncol(A)), p = 1, normalize = FALSE, eps = 1e-05,
           maxit = 10000, gpaiter = 5)
GPFoblq.lp(A, Tmat = diag(ncol(A)), p = 1, normalize = FALSE, eps = 1e-05,
           maxit = 10000, gpaiter = 5)

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 eps as 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

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

Author

Xinyi Liu, with minor modifications for GPArotation by C. Bernaards.

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)