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

GPForth.lp(A, Tmat=diag(rep(1, ncol(A))), p=1, normalize=FALSE, eps=1e-05, 
            maxit=10000, gpaiter=5) 
     GPFoblq.lp(A, Tmat=diag(rep(1, 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 \(=<\) 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 GPA rotation. The goal is to decrease the objective value, not optimize the inner loop. Warnings may appear, but they 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. If randomStarts were used, this contains the loadings with the lowest criterion value.

Th

Rotation matrix, satisfying loadings %*% t(Th) = A.

Table

Matrix recording iteration details during optimization.

method

String indicating the rotation objective function.

orthogonal

Logical indicating whether the rotation is orthogonal.

convergence

Logical indicating whether convergence was achieved. Convergence is controlled element-wise by 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 p promotes sparsity in the loading matrix but increases computational difficulty. For guidance on choosing p, see the Concluding Remarks in the references.

Since the \(L^p\) function is nonsmooth, a different optimization method is required compared to smooth rotation criteria. Two new functions, GPForth.lp and GPFoblq.lp, replace GPForth and GPFoblq for orthogonal and oblique \(L^p\) rotations, respectively.

The optimization method follows an iterative reweighted least squares (IRLS) approach. It approximates the nonsmooth objective function with a smooth weighted least squares function in the main loop and optimizes it using GPA in the inner loop.

Normalization is not recommended for \(L^p\) rotation. Its use may have unexpected results.

See also

References

Liu, X., Wallin, G., Chen, Y., & Moustaki, I. (2023). Rotation to sparse loadings using \(L^p\) losses and related inference problems. Psychometrika, 88(2), 527–553.

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 from random position
  fa.lpT1 <- GPForth.lp(loadings(fa.unrotated), p = 1)
  # 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, 50% at the same lowest minimum.
#> Random starts converged to 2 different local minima
#> Loadings at lowest minimum:
#>              Factor1  Factor2
#> NL1       0.66539031 -0.42787
#> TV2       0.65607038 -0.52479
#> NL3       0.64841375 -0.42123
#> RTL4      0.11492304 -0.69264
#> RTL5      0.07431037 -0.75643
#> Veronica -0.00980093 -0.81783
#> SBS6      0.00046327 -0.72669
#> 
#>                Factor1 Factor2
#> SS loadings    1.31244 2.88478
#> Proportion Var 0.18749 0.41211
#> Cumulative Var 0.18749 0.59960
#> 
#> Rotating matrix:
#>          [,1]     [,2]
#> [1,]  0.48590 -0.87401
#> [2,] -0.87401 -0.48590
#> 
#> Iteration table:
#>         iter       f  time
#> elapsed   20 0.93384 0.017
  
  p <- 1
  # Oblique rotation
  # Single start
  fa.lpQ1 <- GPFoblq.lp(loadings(fa.unrotated), p = p)
  # 10 random starts
  fa.lpQ <- lpQ(loadings(fa.unrotated), p = p, randomStarts = 10)
  summary(fa.lpQ, Structure = TRUE)
#> Oblique rotation method Lp rotation, p=1 converged in 13 iterations.
#> Pattern (loadings):
#>            Factor1  Factor2
#> NL1       0.000863 -0.79155
#> TV2      -0.101454 -0.78160
#> NL3      -0.003413 -0.77140
#> RTL4     -0.614923 -0.14354
#> RTL5     -0.704348 -0.09622
#> Veronica -0.819252  0.00257
#> SBS6     -0.722079 -0.00857
#> 
#> 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


  # this functions ensures consistent ordering of factors of a
  # GPArotation object for cleaner comparison
  # Inspired by fungible::orderFactors and fungible::faSort functions
  sortFac <- function(x){
    # Only works for object of class GPArotation 
    if (!inherits(x, "GPArotation")) {stop("argument not of class GPArotation")}
    cln <- colnames(x$loadings) 
    # ordering for oblique slightly different from orthogonal
    ifelse(x$orthogonal, vx <- colSums(x$loadings^2), 
      vx <- diag(x$Phi %*% t(x$loadings) %*% x$loadings) )
    # sort by squared loadings from high to low
    vxo <- order(vx, decreasing = TRUE)
    vx <- vx[vxo]
    # maintain the right sign
    Dsgn <- diag(sign(colSums(x$loadings^3))) [ , vxo]
    x$Th <- x$Th %*% Dsgn 
    x$loadings <- x$loadings %*% Dsgn
    if (match("Phi", names(x))) { 
            # If factor is negative, reverse corresponding factor correlations
            x$Phi <- t(Dsgn) %*% x$Phi %*% Dsgn
      }
    colnames(x$loadings) <- cln 
    x 
  } 
  
  # seed set to see the results of sorting
  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 ordered factor loadings
  res <- round(cbind(sortFac(fa.lpQ1)$loadings, sortFac(fa.lpQ0.5)$loadings, 
    sortFac(fa.geo)$loadings),3)
  print(c("oblique-  Lp 1           Lp 0.5         geomin")); print(res)
#> [1] "oblique-  Lp 1           Lp 0.5         geomin"
#>          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 ordered factor loadings
  res <- round(cbind(fa.lpQ1$loadings, fa.lpQ0.5$loadings, fa.geo$loadings),3)
  print(c("oblique-  Lp 1           Lp 0.5         geomin")); print(res)
#> [1] "oblique-  Lp 1           Lp 0.5         geomin"
#>          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)