Skip to content

Commit 4b0f433

Browse files
author
maechler
committed
fix factanal() internal sortLoading() needs to adapt rot.matrix, too
git-svn-id: https://svn.r-project.org/R/trunk@88047 00db46b3-68df-0310-9c12-caf00c1e9a41
1 parent 7b22545 commit 4b0f433

File tree

6 files changed

+128
-66
lines changed

6 files changed

+128
-66
lines changed

doc/NEWS.Rd

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -47,7 +47,7 @@
4747
results in confusing-to-read code.
4848
}
4949
}
50-
50+
5151
\subsection{BUG FIXES}{
5252
\itemize{
5353
\item Setting \code{\link{attributes}} on primitive functions is an
@@ -810,6 +810,10 @@
810810
\item \command{R CMD check} now also reports bad symbols in
811811
package shared objects linked in from local static libraries
812812
(\PR{18789}).
813+
814+
\item \code{factanal()} now works correctly also, e.g., for
815+
\pkg{GPArotation}, \code{oblimin()} rotations, fixing \PR{18417},
816+
thanks to \I{Coen Bernaards} and others.
813817
}
814818
}
815819
}

src/library/stats/R/factanal.R

Lines changed: 49 additions & 38 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
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
@@ -16,31 +16,34 @@
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!
2219
factanal <-
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("\nLoadings:\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("\nCall:\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("\nFactor Correlations:\n")
283-
print(R, digits=digits, ...)
284-
}
292+
if (!isTRUE(all.equal(c(R), c(diag(factors))))) {
293+
cat("\nFactor Correlations:\n")
294+
print(R, digits=digits, ...)
295+
}
285296
}
286297
# end additions J. Fox, 23 June 2005
287298
if(!is.null(x$STATISTIC)) {

src/library/stats/man/factanal.Rd

Lines changed: 22 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
% File src/library/stats/man/factanal.Rd
22
% Part of the R package, https://www.R-project.org
3-
% Copyright 1995-2018 R Core Team
3+
% Copyright 1995-2025 R Core Team
44
% Distributed under GPL 2 or later
55

66
\name{factanal}
@@ -18,44 +18,46 @@ factanal(x, factors, data = NULL, covmat = NULL, n.obs = NA,
1818
rotation = "varimax", control = NULL, \dots)
1919
}
2020
\arguments{
21-
\item{x}{A formula or a numeric matrix or an object that can be
21+
\item{x}{\code{\link{formula}} or a numeric matrix or an object that can be
2222
coerced to a numeric matrix.}
23-
\item{factors}{The number of factors to be fitted.}
24-
\item{data}{An optional data frame (or similar: see
23+
\item{factors}{integer number of factors to be fitted.}
24+
\item{data}{an optional data frame (or similar: see
2525
\code{\link{model.frame}}), used only if \code{x} is a formula. By
2626
default the variables are taken from \code{environment(formula)}.}
27-
\item{covmat}{A covariance matrix, or a covariance list as returned by
27+
\item{covmat}{covariance matrix or a covariance list as returned by
2828
\code{\link{cov.wt}}. Of course, correlation matrices are covariance
2929
matrices.}
30-
\item{n.obs}{The number of observations, used if \code{covmat} is a
30+
\item{n.obs}{the number of observations, used if \code{covmat} is a
3131
covariance matrix.}
32-
\item{subset}{A specification of the cases to be used, if \code{x} is
32+
\item{subset}{a specification of the cases to be used, if \code{x} is
3333
used as a matrix or formula.}
34-
\item{na.action}{The \code{na.action} to be used if \code{x} is
35-
used as a formula.}
34+
\item{na.action}{the \code{\link{na.action}} to be used if \code{x} is
35+
used as a formula, in which case \code{\link{model.frame}()} is
36+
called.}
3637
\item{start}{\code{NULL} or a matrix of starting values, each column
3738
giving an initial set of uniquenesses.}
3839
\item{scores}{Type of scores to produce, if any. The default is none,
3940
\code{"regression"} gives Thompson's scores, \code{"Bartlett"} given
4041
Bartlett's weighted least-squares scores. Partial matching allows
4142
these names to be abbreviated.}
42-
\item{rotation}{character. \code{"none"} or the name of a function
43+
\item{rotation}{a \code{\link{function}} or \code{\link{character}}
44+
string. In the latter case, \code{"none"} or the name of a function
4345
to be used to rotate the factors: it will be called with first
4446
argument the loadings matrix, and should return a list with component
4547
\code{loadings} giving the rotated loadings, or just the rotated loadings.}
46-
\item{control}{A list of control values,
48+
\item{control}{a \code{\link{list}} of control values with components
4749
\describe{
48-
\item{nstart}{The number of starting values to be tried if
50+
\item{\code{nstart}}{The number of starting values to be tried if
4951
\code{start = NULL}. Default 1.}
50-
\item{trace}{logical. Output tracing information? Default \code{FALSE}.}
51-
\item{lower}{The lower bound for uniquenesses during
52+
\item{\code{trace}}{logical. Output tracing information? Default \code{FALSE}.}
53+
\item{\code{lower}}{The lower bound for uniquenesses during
5254
optimization. Should be > 0. Default 0.005.}
53-
\item{opt}{A list of control values to be passed to
54-
\code{\link{optim}}'s \code{control} argument.}
55-
\item{rotate}{a list of additional arguments for the rotation function.}
55+
\item{\code{opt}}{a list of control values to be passed to
56+
\code{\link{optim}}'s \code{control} argument.}
57+
\item{\code{rotate}}{a list of additional arguments for the rotation function.}
5658
}
5759
}
58-
\item{\dots}{Components of \code{control} can also be supplied as
60+
\item{\dots}{components of \code{control} can also be supplied directly as
5961
named arguments to \code{factanal}.}
6062
}
6163
\details{
@@ -128,7 +130,8 @@ factanal(x, factors, data = NULL, covmat = NULL, n.obs = NA,
128130
\item{loadings}{A matrix of loadings, one column for each factor. The
129131
factors are ordered in decreasing order of sums of squares of
130132
loadings, and given the sign that will make the sum of the loadings
131-
positive. This is of class \code{"loadings"}: see
133+
positive. For correlated factors, the correlation matrix is
134+
stored as an attribute labeled "covariance". This is of class \code{"loadings"}: see
132135
\code{\link{loadings}} for its \code{print} method.}
133136
\item{uniquenesses}{The uniquenesses computed.}
134137
\item{correlation}{The correlation matrix used.}

src/library/stats/man/loadings.Rd

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
11
% File src/library/stats/man/loadings.Rd
22
% Part of the R package, https://www.R-project.org
3-
% Copyright 1995-2014 R Core Team
3+
% Copyright 1995-2025 R Core Team
44
% Distributed under GPL 2 or later
55

66
\name{loadings}
7+
\title{Print Factor Analysis 'factanal()', Notably Loadings}
78
\alias{loadings}
89
\alias{print.loadings}
910
\alias{print.factanal}
10-
\title{Print Loadings in Factor Analysis}
1111
\description{
12-
Extract or print loadings in factor analysis (or principal
13-
components analysis).
12+
Print factor analysis result, \code{\link{factanal}()};
13+
extract or print loadings in factor analysis (or principal
14+
components analysis, PCA).
1415
}
1516
\usage{
16-
loadings(x, ...)
17+
loadings(x, \dots)
1718

1819
\method{print}{loadings}(x, digits = 3, cutoff = 0.1, sort = FALSE, \dots)
1920

@@ -31,8 +32,9 @@ loadings(x, ...)
3132
than 0.5 (in modulus) is assigned to the factor with the largest
3233
loading, and the variables are printed in the order of the factor
3334
they are assigned to, then those unassigned.}
34-
\item{\dots}{further arguments for other methods,
35-
ignored for \code{loadings}.}
35+
\item{\dots}{further arguments for other methods; for \code{factanal}
36+
printing, notably passed to \code{print(<loadings>, ...)}, hence, e.g.,
37+
\code{cutoff = <frac>}; ignored for \code{loadings}.}
3638
}
3739

3840
\details{
@@ -47,7 +49,7 @@ loadings(x, ...)
4749
The \code{print} method for class \code{"\link{factanal}"} calls the
4850
\code{"loadings"} method to print the loadings, and so passes down
4951
arguments such as \code{cutoff} and \code{sort}.
50-
52+
5153
The signs of the loadings vectors are arbitrary for both factor
5254
analysis and PCA.
5355
}
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
## Subject: [Bug 18417] factanal returns useless SS loading values
2+
## Date: Sat, 22 Mar 2025 23:21:10 +0000
3+
## --- Comment #21 from Coen Bernaards
4+
5+
## create unrotated loadings matrix
6+
data(ability.cov, package="datasets")
7+
res <- factanal(covmat = ability.cov$cov, factors = 3, rotation = "none")
8+
9+
if(FALSE) {
10+
## create the output from oblimin of GPArotation as a list;
11+
require(GPArotation)
12+
set.seed(100)
13+
obli100 <- oblimin(res$loadings, Tmat = Random.Start(3), eps = 1e-8, maxit = 10000)
14+
## keep only what's needed:
15+
obli100[setdiff(names(obli100),c("loadings", "Phi", "Th", "orthogonal"))] <- NULL
16+
saveRDS(obli100, "obli100.rds")
17+
}
18+
19+
obli100 <- readRDS("obli100.rds") # in ../stats/tests/
20+
if(getRversion() >= "4.6") { ## perform the factanal using this oblimin rotation [ rotation now may be _function_
21+
resO100 <- factanal(covmat = ability.cov$cov, factors = 3, rotation = \(...) obli100)
22+
} else { ## old R:
23+
obli_FN <- function(...) obli100
24+
resO100 <- factanal(covmat = ability.cov$cov, factors = 3, rotation = "obli_FN")
25+
}
26+
resO100 # print.factanal(..) --> print.loadings(..)
27+
28+
oriL <- res $ loadings
29+
ldns <- resO100 $ loadings
30+
rotmat <- resO100 $ rotmat
31+
# no longer can the unrotated matrix be recreated. the max abs diff between
32+
# unrotated and recreated unrotated loadings should be near zero but is not:
33+
print.default(dev <- ldns %*% solve(rotmat) - oriL)
34+
stopifnot(max(abs(dev)) < 1e-14) # max|dev| was 2.000982 (R <= 4.4.x), now 2.22e-16 [Lnx x86_64, gcc]
35+
36+
ldns # prints loadings too
37+
## check the "SS loadings .... " line
38+
L <- ldns
39+
Phi <- attr(L, "covariance")
40+
(SSld <- diag(Phi %*% crossprod(L)))
41+
## 1.896492 1.124306 1.037336
42+
stopifnot(all.equal(c(1.8964924541, 1.1243062406, 1.0373362639), SSld)) # seen 8.49e-12
548 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)