11# File src/library/stats/R/factanal.R
22# Part of the R package, https://www.R-project.org
33#
4- # Copyright (C) 1995-2015 The R Core Team
4+ # Copyright (C) 1995-2025 The R Core Team
55#
66# This program is free software; you can redistribute it and/or modify
77# it under the terms of the GNU General Public License as published by
1616# A copy of the GNU General Public License is available at
1717# https://www.R-project.org/Licenses/
1818
19- # # Hmm, MM thinks diag(.) needs checking { diag(vec) when length(vec)==1 !}
20- # # However, MM does not understand that factor analysis
21- # # is a *multi*variate technique!
2219factanal <-
2320 function (x , factors , data = NULL , covmat = NULL , n.obs = NA ,
2421 subset , na.action , start = NULL ,
2522 scores = c(" none" , " regression" , " Bartlett" ),
2623 rotation = " varimax" ,
2724 control = NULL , ... )
2825{
29- sortLoadings <- function (Lambda )
26+ sortLoadings <- function (Lambda , rotm = NULL )
3027 {
3128 cn <- colnames(Lambda )
3229 Phi <- attr(Lambda , " covariance" )
33- ssq <- apply(Lambda , 2L , function (x ) - sum(x ^ 2 ))
34- Lambda <- Lambda [, order(ssq ), drop = FALSE ]
35- colnames(Lambda ) <- cn
30+ io.ssq <- order(- if (is.null(Phi )) colSums(Lambda ^ 2 ) else diag(Phi %*% crossprod(Lambda )))
31+ Lambda <- Lambda [, io.ssq , drop = FALSE ]
3632 neg <- colSums(Lambda ) < 0
3733 Lambda [, neg ] <- - Lambda [, neg ]
38- if (! is.null(Phi )) {
39- unit <- ifelse(neg , - 1 , 1 )
34+ colnames(Lambda ) <- cn
35+ has.rot <- ! is.null(rotm )
36+ has.Ph <- ! is.null(Phi )
37+ if (has.rot || has.Ph ) # used for both
38+ unit <- c(1 , - 1 )[neg + 1L ] # = ifelse(neg, -1, 1)
39+ # # FIXME: <M> %*% diag(unit) can be made faster (for non-small cases) below
40+ if (has.rot )
41+ rotm <- rotm %*% diag(unit )[, io.ssq ]
42+ if (has.Ph ) {
4043 attr(Lambda , " covariance" ) <-
41- unit %*% Phi [order( ssq ), order( ssq ) ] %*% unit
44+ diag( unit ) %*% Phi [io. ssq, io. ssq ] %*% diag( unit )
4245 }
43- Lambda
46+ list ( Lambda = Lambda , rotm = rotm )
4447 }
4548 cl <- match.call()
4649 na.act <- NULL
@@ -98,6 +101,7 @@ factanal <-
98101 " %d factor is too many for %d variables" ,
99102 " %d factors are too many for %d variables" ),
100103 factors , p ), domain = NA )
104+ # # FIXME: cov2cor() shows how to do this faster (for large dim)
101105 sds <- sqrt(diag(cv ))
102106 cv <- cv / (sds %o% sds )
103107
@@ -121,8 +125,8 @@ factanal <-
121125 if (nc < 1 ) stop(" no starting values supplied" )
122126 best <- Inf
123127 for (i in 1L : nc ) {
124- nfit <- factanal.fit.mle(cv , factors , start [, i ],
125- max(cn $ lower , 0 ), cn $ opt )
128+ # # factanal.fit.mle(cmat , factors, start=NULL, lower = 0.005, control = NULL, ...)
129+ nfit <- factanal.fit.mle( cv , factors , start [, i ], max(cn $ lower , 0 ), cn $ opt )
126130 if (cn $ trace )
127131 cat(" start" , i , " value:" , format(nfit $ criteria [1L ]),
128132 " uniqs:" , format(as.vector(round(nfit $ uniquenesses , 4 ))), " \n " )
@@ -136,20 +140,25 @@ factanal <-
136140 " unable to optimize from this starting value" ,
137141 " unable to optimize from these starting values" ),
138142 domain = NA )
139- load <- fit $ loadings
140- if (rotation != " none" ) {
143+ # the following line changed by C. Bernaards, 20 December 2022
144+ load <- sortLoadings(fit $ loadings )$ Lambda
145+ if (is.function(rotation ) || rotation != " none" ) {
141146 rot <- do.call(rotation , c(list (load ), cn $ rotate ))
142147 load <- if (is.list(rot )) {
143- load <- rot $ loadings
144- fit $ rotmat <-
145- if (inherits( rot , " GPArotation " )) t(solve( rot $ Th ))
146- else rot $ rotmat
147- rot $ loadings
148- } else rot
148+ fit $ rotmat <-
149+ if (inherits( rot , " GPArotation " )) t(solve( rot $ Th ))
150+ else rot $ rotmat
151+ rot $ loadings
152+ } else rot
153+ if (is.list( rot ) && ! is.null( rot $ Phi )) attr( load , " covariance " ) <- rot $ Phi
149154 }
150- fit $ loadings <- sortLoadings(load )
155+ # the following lines added by C. Bernaards, 20 December 2022
156+ loadrot <- sortLoadings(load , fit $ rotmat )
157+ fit $ loadings <- loadrot $ Lambda
158+ if (! is.null(loadrot $ rotm )) fit $ rotmat <- loadrot $ rotm
159+ # end additions C. Bernaards, 20 December 2022
151160 class(fit $ loadings ) <- " loadings"
152- fit $ na.action <- na.act # not used currently
161+ fit $ na.action <- na.act # book keeping
153162 if (have.x && scores != " none" ) {
154163 Lambda <- fit $ loadings
155164 zz <- scale(z , TRUE , TRUE )
@@ -217,8 +226,8 @@ factanal.fit.mle <-
217226 start <- (1 - 0.5 * factors / p )/ diag(solve(cmat ))
218227 res <- optim(start , FAfn , FAgr , method = " L-BFGS-B" ,
219228 lower = lower , upper = 1 ,
220- control = c(list (fnscale = 1 ,
221- parscale = rep(0.01 , length(start ))), control ),
229+ control = c(list (fnscale = 1 ,
230+ parscale = rep(0.01 , length(start ))), control ),
222231 q = factors , S = cmat )
223232 Lambda <- FAout(res $ par , cmat , factors )
224233 dimnames(Lambda ) <- list (dimnames(cmat )[[1L ]],
@@ -248,11 +257,13 @@ print.loadings <- function(x, digits = 3L, cutoff = 0.1, sort = FALSE, ...)
248257 Lambda <- Lambda [order(mx , 1L : p ), , drop = FALSE ]
249258 }
250259 cat(" \n Loadings:\n " )
251- fx <- setNames(format(round(Lambda , digits )), NULL )
260+ fx <- setNames(format(round(Lambda , digits )), NULL ) # char matrix
252261 nc <- nchar(fx [1L ], type = " c" )
253262 fx [abs(Lambda ) < cutoff ] <- strrep(" " , nc )
254263 print(fx , quote = FALSE , ... )
255- vx <- colSums(x ^ 2 )
264+ Phi <- attr(Lambda , " covariance" )
265+ vx <- if (is.null(Phi )) colSums(Lambda ^ 2 ) else diag(Phi %*% crossprod(Lambda ))
266+ names(vx ) <- colnames(Lambda )
256267 varex <- rbind(" SS loadings" = vx )
257268 if (is.null(attr(x , " covariance" ))) {
258269 varex <- rbind(varex , " Proportion Var" = vx / p )
@@ -269,19 +280,19 @@ print.factanal <- function(x, digits = 3, ...)
269280 cat(" \n Call:\n " , deparse(x $ call ), " \n\n " , sep = " " )
270281 cat(" Uniquenesses:\n " )
271282 print(round(x $ uniquenesses , digits ), ... )
272- print(x $ loadings , digits = digits , ... )
283+ print(x $ loadings , digits = digits , ... ) # -> print.loadings()
273284 # the following lines added by J. Fox, 26 June 2005
274- if (! is.null(x $ rotmat )){
275- tmat <- solve(x $ rotmat )
276- R <- tmat %*% t( tmat )
277- factors <- x $ factors
278- rownames(R ) <- colnames(R ) <- paste0(" Factor" , 1 : factors )
285+ if (! is.null(x $ rotmat )) {
286+ tmat <- solve(x $ rotmat )
287+ R <- tcrossprod( tmat ) # t . t'
288+ factors <- x $ factors
289+ rownames(R ) <- colnames(R ) <- paste0(" Factor" , 1 : factors )
279290
280291 # the following line changed by Ulrich Keller, 9 Sept 2008
281- if (! isTRUE(all.equal(c(R ), c(diag(factors ))))) {
282- cat(" \n Factor Correlations:\n " )
283- print(R , digits = digits , ... )
284- }
292+ if (! isTRUE(all.equal(c(R ), c(diag(factors ))))) {
293+ cat(" \n Factor Correlations:\n " )
294+ print(R , digits = digits , ... )
295+ }
285296 }
286297 # end additions J. Fox, 23 June 2005
287298 if (! is.null(x $ STATISTIC )) {
0 commit comments