some new features

This commit is contained in:
ilgazca
2025-07-30 17:09:11 +03:00
parent db5d46760a
commit 8019bd3b7c
20616 changed files with 4375466 additions and 8 deletions

View File

@ -0,0 +1,32 @@
"""
Package with factor rotation algorithms.
This file contains a Python version of the gradient projection rotation
algorithms (GPA) developed by Bernaards, C.A. and Jennrich, R.I.
The code is based on the Matlab version of the code developed Bernaards, C.A.
and Jennrich, R.I. and is ported and made available with permission of the
authors.
Additionally, several analytic rotation methods are implemented.
References
----------
[1] Bernaards, C.A. and Jennrich, R.I. (2005) Gradient Projection Algorithms and Software for Arbitrary Rotation Criteria in Factor Analysis. Educational and Psychological Measurement, 65 (5), 676-696.
[2] Jennrich, R.I. (2001). A simple general procedure for orthogonal rotation. Psychometrika, 66, 289-306.
[3] Jennrich, R.I. (2002). A simple general method for oblique rotation. Psychometrika, 67, 7-19.
[4] http://www.stat.ucla.edu/research/gpa/matlab.net
[5] http://www.stat.ucla.edu/research/gpa/GPderfree.txt
"""
from ._wrappers import rotate_factors
from ._analytic_rotation import target_rotation, procrustes, promax
from statsmodels.tools._test_runner import PytestTester
__all__ = ['rotate_factors', 'target_rotation', 'procrustes', 'promax',
'test']
test = PytestTester()

View File

@ -0,0 +1,152 @@
"""
This file contains analytic implementations of rotation methods.
"""
import numpy as np
import scipy as sp
def target_rotation(A, H, full_rank=False):
r"""
Analytically performs orthogonal rotations towards a target matrix,
i.e., we minimize:
.. math::
\phi(L) =\frac{1}{2}\|AT-H\|^2.
where :math:`T` is an orthogonal matrix. This problem is also known as
an orthogonal Procrustes problem.
Under the assumption that :math:`A^*H` has full rank, the analytical
solution :math:`T` is given by:
.. math::
T = (A^*HH^*A)^{-\frac{1}{2}}A^*H,
see Green (1952). In other cases the solution is given by :math:`T = UV`,
where :math:`U` and :math:`V` result from the singular value decomposition
of :math:`A^*H`:
.. math::
A^*H = U\Sigma V,
see Schonemann (1966).
Parameters
----------
A : numpy matrix (default None)
non rotated factors
H : numpy matrix
target matrix
full_rank : bool (default FAlse)
if set to true full rank is assumed
Returns
-------
The matrix :math:`T`.
References
----------
[1] Green (1952, Psychometrika) - The orthogonal approximation of an
oblique structure in factor analysis
[2] Schonemann (1966) - A generalized solution of the orthogonal
procrustes problem
[3] Gower, Dijksterhuis (2004) - Procrustes problems
"""
ATH = A.T.dot(H)
if full_rank or np.linalg.matrix_rank(ATH) == A.shape[1]:
T = sp.linalg.fractional_matrix_power(ATH.dot(ATH.T), -1/2).dot(ATH)
else:
U, D, V = np.linalg.svd(ATH, full_matrices=False)
T = U.dot(V)
return T
def procrustes(A, H):
r"""
Analytically solves the following Procrustes problem:
.. math::
\phi(L) =\frac{1}{2}\|AT-H\|^2.
(With no further conditions on :math:`H`)
Under the assumption that :math:`A^*H` has full rank, the analytical
solution :math:`T` is given by:
.. math::
T = (A^*HH^*A)^{-\frac{1}{2}}A^*H,
see Navarra, Simoncini (2010).
Parameters
----------
A : numpy matrix
non rotated factors
H : numpy matrix
target matrix
full_rank : bool (default False)
if set to true full rank is assumed
Returns
-------
The matrix :math:`T`.
References
----------
[1] Navarra, Simoncini (2010) - A guide to empirical orthogonal functions
for climate data analysis
"""
return np.linalg.inv(A.T.dot(A)).dot(A.T).dot(H)
def promax(A, k=2):
r"""
Performs promax rotation of the matrix :math:`A`.
This method was not very clear to me from the literature, this
implementation is as I understand it should work.
Promax rotation is performed in the following steps:
* Determine varimax rotated patterns :math:`V`.
* Construct a rotation target matrix :math:`|V_{ij}|^k/V_{ij}`
* Perform procrustes rotation towards the target to obtain T
* Determine the patterns
First, varimax rotation a target matrix :math:`H` is determined with
orthogonal varimax rotation.
Then, oblique target rotation is performed towards the target.
Parameters
----------
A : numpy matrix
non rotated factors
k : float
parameter, should be positive
References
----------
[1] Browne (2001) - An overview of analytic rotation in exploratory
factor analysis
[2] Navarra, Simoncini (2010) - A guide to empirical orthogonal functions
for climate data analysis
"""
assert k > 0
# define rotation target using varimax rotation:
from ._wrappers import rotate_factors
V, T = rotate_factors(A, 'varimax')
H = np.abs(V)**k/V
# solve procrustes problem
S = procrustes(A, H) # np.linalg.inv(A.T.dot(A)).dot(A.T).dot(H);
# normalize
d = np.sqrt(np.diag(np.linalg.inv(S.T.dot(S))))
D = np.diag(d)
T = np.linalg.inv(S.dot(D)).T
return A.dot(T), T

View File

@ -0,0 +1,592 @@
"""
This file contains a Python version of the gradient projection rotation
algorithms (GPA) developed by Bernaards, C.A. and Jennrich, R.I.
The code is based on code developed Bernaards, C.A. and Jennrich, R.I.
and is ported and made available with permission of the authors.
References
----------
[1] Bernaards, C.A. and Jennrich, R.I. (2005) Gradient Projection Algorithms
and Software for Arbitrary Rotation Criteria in Factor Analysis. Educational
and Psychological Measurement, 65 (5), 676-696.
[2] Jennrich, R.I. (2001). A simple general procedure for orthogonal rotation.
Psychometrika, 66, 289-306.
[3] Jennrich, R.I. (2002). A simple general method for oblique rotation.
Psychometrika, 67, 7-19.
[4] http://www.stat.ucla.edu/research/gpa/matlab.net
[5] http://www.stat.ucla.edu/research/gpa/GPderfree.txt
"""
import numpy as np
def GPA(A, ff=None, vgQ=None, T=None, max_tries=501,
rotation_method='orthogonal', tol=1e-5):
r"""
The gradient projection algorithm (GPA) minimizes a target function
:math:`\phi(L)`, where :math:`L` is a matrix with rotated factors.
For orthogonal rotation methods :math:`L=AT`, where :math:`T` is an
orthogonal matrix. For oblique rotation matrices :math:`L=A(T^*)^{-1}`,
where :math:`T` is a normal matrix, i.e., :math:`TT^*=T^*T`. Oblique
rotations relax the orthogonality constraint in order to gain simplicity
in the interpretation.
Parameters
----------
A : numpy matrix
non rotated factors
T : numpy matrix (default identity matrix)
initial guess of rotation matrix
ff : function (defualt None)
criterion :math:`\phi` to optimize. Should have A, T, L as keyword
arguments
and mapping to a float. Only used (and required) if vgQ is not
provided.
vgQ : function (defualt None)
criterion :math:`\phi` to optimize and its derivative. Should have
A, T, L as keyword arguments and mapping to a tuple containing a
float and vector. Can be omitted if ff is provided.
max_tries : int (default 501)
maximum number of iterations
rotation_method : str
should be one of {orthogonal, oblique}
tol : float
stop criterion, algorithm stops if Frobenius norm of gradient is
smaller then tol
"""
# pre processing
if rotation_method not in ['orthogonal', 'oblique']:
raise ValueError('rotation_method should be one of '
'{orthogonal, oblique}')
if vgQ is None:
if ff is None:
raise ValueError('ff should be provided if vgQ is not')
derivative_free = True
Gff = lambda x: Gf(x, lambda y: ff(T=y, A=A, L=None))
else:
derivative_free = False
if T is None:
T = np.eye(A.shape[1])
# pre processing for iteration
al = 1
table = []
# pre processing for iteration: initialize f and G
if derivative_free:
f = ff(T=T, A=A, L=None)
G = Gff(T)
elif rotation_method == 'orthogonal': # and not derivative_free
L = A.dot(T)
f, Gq = vgQ(L=L)
G = (A.T).dot(Gq)
else: # i.e. rotation_method == 'oblique' and not derivative_free
Ti = np.linalg.inv(T)
L = A.dot(Ti.T)
f, Gq = vgQ(L=L)
G = -((L.T).dot(Gq).dot(Ti)).T
# iteration
for i_try in range(0, max_tries):
# determine Gp
if rotation_method == 'orthogonal':
M = (T.T).dot(G)
S = (M + M.T)/2
Gp = G - T.dot(S)
else: # i.e. if rotation_method == 'oblique':
Gp = G-T.dot(np.diag(np.sum(T*G, axis=0)))
s = np.linalg.norm(Gp, 'fro')
table.append([i_try, f, np.log10(s), al])
# if we are close stop
if s < tol:
break
# update T
al = 2*al
for i in range(11):
# determine Tt
X = T - al*Gp
if rotation_method == 'orthogonal':
U, D, V = np.linalg.svd(X, full_matrices=False)
Tt = U.dot(V)
else: # i.e. if rotation_method == 'oblique':
v = 1/np.sqrt(np.sum(X**2, axis=0))
Tt = X.dot(np.diag(v))
# calculate objective using Tt
if derivative_free:
ft = ff(T=Tt, A=A, L=None)
elif rotation_method == 'orthogonal': # and not derivative_free
L = A.dot(Tt)
ft, Gq = vgQ(L=L)
else: # i.e. rotation_method == 'oblique' and not derivative_free
Ti = np.linalg.inv(Tt)
L = A.dot(Ti.T)
ft, Gq = vgQ(L=L)
# if sufficient improvement in objective -> use this T
if ft < f-.5*s**2*al:
break
al = al/2
# post processing for next iteration
T = Tt
f = ft
if derivative_free:
G = Gff(T)
elif rotation_method == 'orthogonal': # and not derivative_free
G = (A.T).dot(Gq)
else: # i.e. rotation_method == 'oblique' and not derivative_free
G = -((L.T).dot(Gq).dot(Ti)).T
# post processing
Th = T
Lh = rotateA(A, T, rotation_method=rotation_method)
Phi = (T.T).dot(T)
return Lh, Phi, Th, table
def Gf(T, ff):
"""
Subroutine for the gradient of f using numerical derivatives.
"""
k = T.shape[0]
ep = 1e-4
G = np.zeros((k, k))
for r in range(k):
for s in range(k):
dT = np.zeros((k, k))
dT[r, s] = ep
G[r, s] = (ff(T+dT)-ff(T-dT))/(2*ep)
return G
def rotateA(A, T, rotation_method='orthogonal'):
r"""
For orthogonal rotation methods :math:`L=AT`, where :math:`T` is an
orthogonal matrix. For oblique rotation matrices :math:`L=A(T^*)^{-1}`,
where :math:`T` is a normal matrix, i.e., :math:`TT^*=T^*T`. Oblique
rotations relax the orthogonality constraint in order to gain simplicity
in the interpretation.
"""
if rotation_method == 'orthogonal':
L = A.dot(T)
elif rotation_method == 'oblique':
L = A.dot(np.linalg.inv(T.T))
else: # i.e. if rotation_method == 'oblique':
raise ValueError('rotation_method should be one of '
'{orthogonal, oblique}')
return L
def oblimin_objective(L=None, A=None, T=None, gamma=0,
rotation_method='orthogonal',
return_gradient=True):
r"""
Objective function for the oblimin family for orthogonal or
oblique rotation wich minimizes:
.. math::
\phi(L) = \frac{1}{4}(L\circ L,(I-\gamma C)(L\circ L)N),
where :math:`L` is a :math:`p\times k` matrix, :math:`N` is
:math:`k\times k`
matrix with zeros on the diagonal and ones elsewhere, :math:`C` is a
:math:`p\times p` matrix with elements equal to :math:`1/p`,
:math:`(X,Y)=\operatorname{Tr}(X^*Y)` is the Frobenius norm and
:math:`\circ`
is the element-wise product or Hadamard product.
The gradient is given by
.. math::
L\circ\left[(I-\gamma C) (L \circ L)N\right].
Either :math:`L` should be provided or :math:`A` and :math:`T` should be
provided.
For orthogonal rotations :math:`L` satisfies
.. math::
L = AT,
where :math:`T` is an orthogonal matrix. For oblique rotations :math:`L`
satisfies
.. math::
L = A(T^*)^{-1},
where :math:`T` is a normal matrix.
The oblimin family is parametrized by the parameter :math:`\gamma`. For
orthogonal rotations:
* :math:`\gamma=0` corresponds to quartimax,
* :math:`\gamma=\frac{1}{2}` corresponds to biquartimax,
* :math:`\gamma=1` corresponds to varimax,
* :math:`\gamma=\frac{1}{p}` corresponds to equamax.
For oblique rotations rotations:
* :math:`\gamma=0` corresponds to quartimin,
* :math:`\gamma=\frac{1}{2}` corresponds to biquartimin.
Parameters
----------
L : numpy matrix (default None)
rotated factors, i.e., :math:`L=A(T^*)^{-1}=AT`
A : numpy matrix (default None)
non rotated factors
T : numpy matrix (default None)
rotation matrix
gamma : float (default 0)
a parameter
rotation_method : str
should be one of {orthogonal, oblique}
return_gradient : bool (default True)
toggles return of gradient
"""
if L is None:
assert A is not None and T is not None
L = rotateA(A, T, rotation_method=rotation_method)
p, k = L.shape
L2 = L**2
N = np.ones((k, k))-np.eye(k)
if np.isclose(gamma, 0):
X = L2.dot(N)
else:
C = np.ones((p, p))/p
X = (np.eye(p) - gamma*C).dot(L2).dot(N)
phi = np.sum(L2*X)/4
if return_gradient:
Gphi = L*X
return phi, Gphi
else:
return phi
def orthomax_objective(L=None, A=None, T=None, gamma=0, return_gradient=True):
r"""
Objective function for the orthomax family for orthogonal
rotation wich minimizes the following objective:
.. math::
\phi(L) = -\frac{1}{4}(L\circ L,(I-\gamma C)(L\circ L)),
where :math:`0\leq\gamma\leq1`, :math:`L` is a :math:`p\times k` matrix,
:math:`C` is a :math:`p\times p` matrix with elements equal to
:math:`1/p`,
:math:`(X,Y)=\operatorname{Tr}(X^*Y)` is the Frobenius norm and
:math:`\circ` is the element-wise product or Hadamard product.
Either :math:`L` should be provided or :math:`A` and :math:`T` should be
provided.
For orthogonal rotations :math:`L` satisfies
.. math::
L = AT,
where :math:`T` is an orthogonal matrix.
The orthomax family is parametrized by the parameter :math:`\gamma`:
* :math:`\gamma=0` corresponds to quartimax,
* :math:`\gamma=\frac{1}{2}` corresponds to biquartimax,
* :math:`\gamma=1` corresponds to varimax,
* :math:`\gamma=\frac{1}{p}` corresponds to equamax.
Parameters
----------
L : numpy matrix (default None)
rotated factors, i.e., :math:`L=A(T^*)^{-1}=AT`
A : numpy matrix (default None)
non rotated factors
T : numpy matrix (default None)
rotation matrix
gamma : float (default 0)
a parameter
return_gradient : bool (default True)
toggles return of gradient
"""
assert 0 <= gamma <= 1, "Gamma should be between 0 and 1"
if L is None:
assert A is not None and T is not None
L = rotateA(A, T, rotation_method='orthogonal')
p, k = L.shape
L2 = L**2
if np.isclose(gamma, 0):
X = L2
else:
C = np.ones((p, p))/p
X = (np.eye(p)-gamma*C).dot(L2)
phi = -np.sum(L2*X)/4
if return_gradient:
Gphi = -L*X
return phi, Gphi
else:
return phi
def CF_objective(L=None, A=None, T=None, kappa=0,
rotation_method='orthogonal',
return_gradient=True):
r"""
Objective function for the Crawford-Ferguson family for orthogonal
and oblique rotation wich minimizes the following objective:
.. math::
\phi(L) =\frac{1-\kappa}{4} (L\circ L,(L\circ L)N)
-\frac{1}{4}(L\circ L,M(L\circ L)),
where :math:`0\leq\kappa\leq1`, :math:`L` is a :math:`p\times k` matrix,
:math:`N` is :math:`k\times k` matrix with zeros on the diagonal and ones
elsewhere,
:math:`M` is :math:`p\times p` matrix with zeros on the diagonal and ones
elsewhere
:math:`(X,Y)=\operatorname{Tr}(X^*Y)` is the Frobenius norm and
:math:`\circ` is the element-wise product or Hadamard product.
The gradient is given by
.. math::
d\phi(L) = (1-\kappa) L\circ\left[(L\circ L)N\right]
-\kappa L\circ \left[M(L\circ L)\right].
Either :math:`L` should be provided or :math:`A` and :math:`T` should be
provided.
For orthogonal rotations :math:`L` satisfies
.. math::
L = AT,
where :math:`T` is an orthogonal matrix. For oblique rotations :math:`L`
satisfies
.. math::
L = A(T^*)^{-1},
where :math:`T` is a normal matrix.
For orthogonal rotations the oblimin (and orthomax) family of rotations is
equivalent to the Crawford-Ferguson family. To be more precise:
* :math:`\kappa=0` corresponds to quartimax,
* :math:`\kappa=\frac{1}{p}` corresponds to variamx,
* :math:`\kappa=\frac{k-1}{p+k-2}` corresponds to parsimax,
* :math:`\kappa=1` corresponds to factor parsimony.
Parameters
----------
L : numpy matrix (default None)
rotated factors, i.e., :math:`L=A(T^*)^{-1}=AT`
A : numpy matrix (default None)
non rotated factors
T : numpy matrix (default None)
rotation matrix
gamma : float (default 0)
a parameter
rotation_method : str
should be one of {orthogonal, oblique}
return_gradient : bool (default True)
toggles return of gradient
"""
assert 0 <= kappa <= 1, "Kappa should be between 0 and 1"
if L is None:
assert A is not None and T is not None
L = rotateA(A, T, rotation_method=rotation_method)
p, k = L.shape
L2 = L**2
X = None
if not np.isclose(kappa, 1):
N = np.ones((k, k)) - np.eye(k)
X = (1 - kappa)*L2.dot(N)
if not np.isclose(kappa, 0):
M = np.ones((p, p)) - np.eye(p)
if X is None:
X = kappa*M.dot(L2)
else:
X += kappa*M.dot(L2)
phi = np.sum(L2 * X) / 4
if return_gradient:
Gphi = L*X
return phi, Gphi
else:
return phi
def vgQ_target(H, L=None, A=None, T=None, rotation_method='orthogonal'):
r"""
Subroutine for the value of vgQ using orthogonal or oblique rotation
towards a target matrix, i.e., we minimize:
.. math::
\phi(L) =\frac{1}{2}\|L-H\|^2
and the gradient is given by
.. math::
d\phi(L)=L-H.
Either :math:`L` should be provided or :math:`A` and :math:`T` should be
provided.
For orthogonal rotations :math:`L` satisfies
.. math::
L = AT,
where :math:`T` is an orthogonal matrix. For oblique rotations :math:`L`
satisfies
.. math::
L = A(T^*)^{-1},
where :math:`T` is a normal matrix.
Parameters
----------
H : numpy matrix
target matrix
L : numpy matrix (default None)
rotated factors, i.e., :math:`L=A(T^*)^{-1}=AT`
A : numpy matrix (default None)
non rotated factors
T : numpy matrix (default None)
rotation matrix
rotation_method : str
should be one of {orthogonal, oblique}
"""
if L is None:
assert A is not None and T is not None
L = rotateA(A, T, rotation_method=rotation_method)
q = np.linalg.norm(L-H, 'fro')**2
Gq = 2*(L-H)
return q, Gq
def ff_target(H, L=None, A=None, T=None, rotation_method='orthogonal'):
r"""
Subroutine for the value of f using (orthogonal or oblique) rotation
towards a target matrix, i.e., we minimize:
.. math::
\phi(L) =\frac{1}{2}\|L-H\|^2.
Either :math:`L` should be provided or :math:`A` and :math:`T` should be
provided. For orthogonal rotations :math:`L` satisfies
.. math::
L = AT,
where :math:`T` is an orthogonal matrix. For oblique rotations
:math:`L` satisfies
.. math::
L = A(T^*)^{-1},
where :math:`T` is a normal matrix.
Parameters
----------
H : numpy matrix
target matrix
L : numpy matrix (default None)
rotated factors, i.e., :math:`L=A(T^*)^{-1}=AT`
A : numpy matrix (default None)
non rotated factors
T : numpy matrix (default None)
rotation matrix
rotation_method : str
should be one of {orthogonal, oblique}
"""
if L is None:
assert A is not None and T is not None
L = rotateA(A, T, rotation_method=rotation_method)
return np.linalg.norm(L-H, 'fro')**2
def vgQ_partial_target(H, W=None, L=None, A=None, T=None):
r"""
Subroutine for the value of vgQ using orthogonal rotation towards a partial
target matrix, i.e., we minimize:
.. math::
\phi(L) =\frac{1}{2}\|W\circ(L-H)\|^2,
where :math:`\circ` is the element-wise product or Hadamard product and
:math:`W` is a matrix whose entries can only be one or zero. The gradient
is given by
.. math::
d\phi(L)=W\circ(L-H).
Either :math:`L` should be provided or :math:`A` and :math:`T` should be
provided.
For orthogonal rotations :math:`L` satisfies
.. math::
L = AT,
where :math:`T` is an orthogonal matrix.
Parameters
----------
H : numpy matrix
target matrix
W : numpy matrix (default matrix with equal weight one for all entries)
matrix with weights, entries can either be one or zero
L : numpy matrix (default None)
rotated factors, i.e., :math:`L=A(T^*)^{-1}=AT`
A : numpy matrix (default None)
non rotated factors
T : numpy matrix (default None)
rotation matrix
"""
if W is None:
return vgQ_target(H, L=L, A=A, T=T)
if L is None:
assert A is not None and T is not None
L = rotateA(A, T, rotation_method='orthogonal')
q = np.linalg.norm(W*(L-H), 'fro')**2
Gq = 2*W*(L-H)
return q, Gq
def ff_partial_target(H, W=None, L=None, A=None, T=None):
r"""
Subroutine for the value of vgQ using orthogonal rotation towards a partial
target matrix, i.e., we minimize:
.. math::
\phi(L) =\frac{1}{2}\|W\circ(L-H)\|^2,
where :math:`\circ` is the element-wise product or Hadamard product and
:math:`W` is a matrix whose entries can only be one or zero. Either
:math:`L` should be provided or :math:`A` and :math:`T` should be provided.
For orthogonal rotations :math:`L` satisfies
.. math::
L = AT,
where :math:`T` is an orthogonal matrix.
Parameters
----------
H : numpy matrix
target matrix
W : numpy matrix (default matrix with equal weight one for all entries)
matrix with weights, entries can either be one or zero
L : numpy matrix (default None)
rotated factors, i.e., :math:`L=A(T^*)^{-1}=AT`
A : numpy matrix (default None)
non rotated factors
T : numpy matrix (default None)
rotation matrix
"""
if W is None:
return ff_target(H, L=L, A=A, T=T)
if L is None:
assert A is not None and T is not None
L = rotateA(A, T, rotation_method='orthogonal')
q = np.linalg.norm(W*(L-H), 'fro')**2
return q

View File

@ -0,0 +1,350 @@
from ._analytic_rotation import target_rotation
from ._gpa_rotation import oblimin_objective, orthomax_objective, CF_objective
from ._gpa_rotation import ff_partial_target, ff_target
from ._gpa_rotation import vgQ_partial_target, vgQ_target
from ._gpa_rotation import rotateA, GPA
__all__ = []
def rotate_factors(A, method, *method_args, **algorithm_kwargs):
r"""
Subroutine for orthogonal and oblique rotation of the matrix :math:`A`.
For orthogonal rotations :math:`A` is rotated to :math:`L` according to
.. math::
L = AT,
where :math:`T` is an orthogonal matrix. And, for oblique rotations
:math:`A` is rotated to :math:`L` according to
.. math::
L = A(T^*)^{-1},
where :math:`T` is a normal matrix.
Parameters
----------
A : numpy matrix (default None)
non rotated factors
method : str
should be one of the methods listed below
method_args : list
additional arguments that should be provided with each method
algorithm_kwargs : dictionary
algorithm : str (default gpa)
should be one of:
* 'gpa': a numerical method
* 'gpa_der_free': a derivative free numerical method
* 'analytic' : an analytic method
Depending on the algorithm, there are algorithm specific keyword
arguments. For the gpa and gpa_der_free, the following
keyword arguments are available:
max_tries : int (default 501)
maximum number of iterations
tol : float
stop criterion, algorithm stops if Frobenius norm of gradient is
smaller then tol
For analytic, the supported arguments depend on the method, see above.
See the lower level functions for more details.
Returns
-------
The tuple :math:`(L,T)`
Notes
-----
What follows is a list of available methods. Depending on the method
additional argument are required and different algorithms
are available. The algorithm_kwargs are additional keyword arguments
passed to the selected algorithm (see the parameters section).
Unless stated otherwise, only the gpa and
gpa_der_free algorithm are available.
Below,
* :math:`L` is a :math:`p\times k` matrix;
* :math:`N` is :math:`k\times k` matrix with zeros on the diagonal and ones
elsewhere;
* :math:`M` is :math:`p\times p` matrix with zeros on the diagonal and ones
elsewhere;
* :math:`C` is a :math:`p\times p` matrix with elements equal to
:math:`1/p`;
* :math:`(X,Y)=\operatorname{Tr}(X^*Y)` is the Frobenius norm;
* :math:`\circ` is the element-wise product or Hadamard product.
oblimin : orthogonal or oblique rotation that minimizes
.. math::
\phi(L) = \frac{1}{4}(L\circ L,(I-\gamma C)(L\circ L)N).
For orthogonal rotations:
* :math:`\gamma=0` corresponds to quartimax,
* :math:`\gamma=\frac{1}{2}` corresponds to biquartimax,
* :math:`\gamma=1` corresponds to varimax,
* :math:`\gamma=\frac{1}{p}` corresponds to equamax.
For oblique rotations rotations:
* :math:`\gamma=0` corresponds to quartimin,
* :math:`\gamma=\frac{1}{2}` corresponds to biquartimin.
method_args:
gamma : float
oblimin family parameter
rotation_method : str
should be one of {orthogonal, oblique}
orthomax : orthogonal rotation that minimizes
.. math::
\phi(L) = -\frac{1}{4}(L\circ L,(I-\gamma C)(L\circ L)),
where :math:`0\leq\gamma\leq1`. The orthomax family is equivalent to
the oblimin family (when restricted to orthogonal rotations).
Furthermore,
* :math:`\gamma=0` corresponds to quartimax,
* :math:`\gamma=\frac{1}{2}` corresponds to biquartimax,
* :math:`\gamma=1` corresponds to varimax,
* :math:`\gamma=\frac{1}{p}` corresponds to equamax.
method_args:
gamma : float (between 0 and 1)
orthomax family parameter
CF : Crawford-Ferguson family for orthogonal and oblique rotation which
minimizes:
.. math::
\phi(L) =\frac{1-\kappa}{4} (L\circ L,(L\circ L)N)
-\frac{1}{4}(L\circ L,M(L\circ L)),
where :math:`0\leq\kappa\leq1`. For orthogonal rotations the oblimin
(and orthomax) family of rotations is equivalent to the
Crawford-Ferguson family.
To be more precise:
* :math:`\kappa=0` corresponds to quartimax,
* :math:`\kappa=\frac{1}{p}` corresponds to varimax,
* :math:`\kappa=\frac{k-1}{p+k-2}` corresponds to parsimax,
* :math:`\kappa=1` corresponds to factor parsimony.
method_args:
kappa : float (between 0 and 1)
Crawford-Ferguson family parameter
rotation_method : str
should be one of {orthogonal, oblique}
quartimax : orthogonal rotation method
minimizes the orthomax objective with :math:`\gamma=0`
biquartimax : orthogonal rotation method
minimizes the orthomax objective with :math:`\gamma=\frac{1}{2}`
varimax : orthogonal rotation method
minimizes the orthomax objective with :math:`\gamma=1`
equamax : orthogonal rotation method
minimizes the orthomax objective with :math:`\gamma=\frac{1}{p}`
parsimax : orthogonal rotation method
minimizes the Crawford-Ferguson family objective with
:math:`\kappa=\frac{k-1}{p+k-2}`
parsimony : orthogonal rotation method
minimizes the Crawford-Ferguson family objective with :math:`\kappa=1`
quartimin : oblique rotation method that minimizes
minimizes the oblimin objective with :math:`\gamma=0`
quartimin : oblique rotation method that minimizes
minimizes the oblimin objective with :math:`\gamma=\frac{1}{2}`
target : orthogonal or oblique rotation that rotates towards a target
matrix : math:`H` by minimizing the objective
.. math::
\phi(L) =\frac{1}{2}\|L-H\|^2.
method_args:
H : numpy matrix
target matrix
rotation_method : str
should be one of {orthogonal, oblique}
For orthogonal rotations the algorithm can be set to analytic in which
case the following keyword arguments are available:
full_rank : bool (default False)
if set to true full rank is assumed
partial_target : orthogonal (default) or oblique rotation that partially
rotates towards a target matrix :math:`H` by minimizing the objective:
.. math::
\phi(L) =\frac{1}{2}\|W\circ(L-H)\|^2.
method_args:
H : numpy matrix
target matrix
W : numpy matrix (default matrix with equal weight one for all entries)
matrix with weights, entries can either be one or zero
Examples
--------
>>> A = np.random.randn(8,2)
>>> L, T = rotate_factors(A,'varimax')
>>> np.allclose(L,A.dot(T))
>>> L, T = rotate_factors(A,'orthomax',0.5)
>>> np.allclose(L,A.dot(T))
>>> L, T = rotate_factors(A,'quartimin',0.5)
>>> np.allclose(L,A.dot(np.linalg.inv(T.T)))
"""
if 'algorithm' in algorithm_kwargs:
algorithm = algorithm_kwargs['algorithm']
algorithm_kwargs.pop('algorithm')
else:
algorithm = 'gpa'
assert not ('rotation_method' in algorithm_kwargs), (
'rotation_method cannot be provided as keyword argument')
L = None
T = None
ff = None
vgQ = None
p, k = A.shape
# set ff or vgQ to appropriate objective function, compute solution using
# recursion or analytically compute solution
if method == 'orthomax':
assert len(method_args) == 1, ('Only %s family parameter should be '
'provided' % method)
rotation_method = 'orthogonal'
gamma = method_args[0]
if algorithm == 'gpa':
vgQ = lambda L=None, A=None, T=None: orthomax_objective(
L=L, A=A, T=T, gamma=gamma, return_gradient=True)
elif algorithm == 'gpa_der_free':
ff = lambda L=None, A=None, T=None: orthomax_objective(
L=L, A=A, T=T, gamma=gamma, return_gradient=False)
else:
raise ValueError('Algorithm %s is not possible for %s '
'rotation' % (algorithm, method))
elif method == 'oblimin':
assert len(method_args) == 2, ('Both %s family parameter and '
'rotation_method should be '
'provided' % method)
rotation_method = method_args[1]
assert rotation_method in ['orthogonal', 'oblique'], (
'rotation_method should be one of {orthogonal, oblique}')
gamma = method_args[0]
if algorithm == 'gpa':
vgQ = lambda L=None, A=None, T=None: oblimin_objective(
L=L, A=A, T=T, gamma=gamma, return_gradient=True)
elif algorithm == 'gpa_der_free':
ff = lambda L=None, A=None, T=None: oblimin_objective(
L=L, A=A, T=T, gamma=gamma, rotation_method=rotation_method,
return_gradient=False)
else:
raise ValueError('Algorithm %s is not possible for %s '
'rotation' % (algorithm, method))
elif method == 'CF':
assert len(method_args) == 2, ('Both %s family parameter and '
'rotation_method should be provided'
% method)
rotation_method = method_args[1]
assert rotation_method in ['orthogonal', 'oblique'], (
'rotation_method should be one of {orthogonal, oblique}')
kappa = method_args[0]
if algorithm == 'gpa':
vgQ = lambda L=None, A=None, T=None: CF_objective(
L=L, A=A, T=T, kappa=kappa, rotation_method=rotation_method,
return_gradient=True)
elif algorithm == 'gpa_der_free':
ff = lambda L=None, A=None, T=None: CF_objective(
L=L, A=A, T=T, kappa=kappa, rotation_method=rotation_method,
return_gradient=False)
else:
raise ValueError('Algorithm %s is not possible for %s '
'rotation' % (algorithm, method))
elif method == 'quartimax':
return rotate_factors(A, 'orthomax', 0, **algorithm_kwargs)
elif method == 'biquartimax':
return rotate_factors(A, 'orthomax', 0.5, **algorithm_kwargs)
elif method == 'varimax':
return rotate_factors(A, 'orthomax', 1, **algorithm_kwargs)
elif method == 'equamax':
return rotate_factors(A, 'orthomax', 1/p, **algorithm_kwargs)
elif method == 'parsimax':
return rotate_factors(A, 'CF', (k-1)/(p+k-2),
'orthogonal', **algorithm_kwargs)
elif method == 'parsimony':
return rotate_factors(A, 'CF', 1, 'orthogonal', **algorithm_kwargs)
elif method == 'quartimin':
return rotate_factors(A, 'oblimin', 0, 'oblique', **algorithm_kwargs)
elif method == 'biquartimin':
return rotate_factors(A, 'oblimin', 0.5, 'oblique', **algorithm_kwargs)
elif method == 'target':
assert len(method_args) == 2, (
'only the rotation target and orthogonal/oblique should be provide'
' for %s rotation' % method)
H = method_args[0]
rotation_method = method_args[1]
assert rotation_method in ['orthogonal', 'oblique'], (
'rotation_method should be one of {orthogonal, oblique}')
if algorithm == 'gpa':
vgQ = lambda L=None, A=None, T=None: vgQ_target(
H, L=L, A=A, T=T, rotation_method=rotation_method)
elif algorithm == 'gpa_der_free':
ff = lambda L=None, A=None, T=None: ff_target(
H, L=L, A=A, T=T, rotation_method=rotation_method)
elif algorithm == 'analytic':
assert rotation_method == 'orthogonal', (
'For analytic %s rotation only orthogonal rotation is '
'supported')
T = target_rotation(A, H, **algorithm_kwargs)
else:
raise ValueError('Algorithm %s is not possible for %s rotation'
% (algorithm, method))
elif method == 'partial_target':
assert len(method_args) == 2, ('2 additional arguments are expected '
'for %s rotation' % method)
H = method_args[0]
W = method_args[1]
rotation_method = 'orthogonal'
if algorithm == 'gpa':
vgQ = lambda L=None, A=None, T=None: vgQ_partial_target(
H, W=W, L=L, A=A, T=T)
elif algorithm == 'gpa_der_free':
ff = lambda L=None, A=None, T=None: ff_partial_target(
H, W=W, L=L, A=A, T=T)
else:
raise ValueError('Algorithm %s is not possible for %s '
'rotation' % (algorithm, method))
else:
raise ValueError('Invalid method')
# compute L and T if not already done
if T is None:
L, phi, T, table = GPA(A, vgQ=vgQ, ff=ff,
rotation_method=rotation_method,
**algorithm_kwargs)
if L is None:
assert T is not None, 'Cannot compute L without T'
L = rotateA(A, T, rotation_method=rotation_method)
return L, T

View File

@ -0,0 +1,584 @@
import unittest
import numpy as np
from statsmodels.multivariate.factor_rotation._wrappers import rotate_factors
from statsmodels.multivariate.factor_rotation._gpa_rotation import (
ff_partial_target, vgQ_partial_target, ff_target, vgQ_target, CF_objective,
orthomax_objective, oblimin_objective, GPA)
from statsmodels.multivariate.factor_rotation._analytic_rotation import (
target_rotation)
class TestAnalyticRotation(unittest.TestCase):
@staticmethod
def str2matrix(A):
A = A.lstrip().rstrip().split('\n')
A = np.array([row.split() for row in A]).astype(float)
return A
def test_target_rotation(self):
"""
Rotation towards target matrix example
http://www.stat.ucla.edu/research/gpa
"""
A = self.str2matrix("""
.830 -.396
.818 -.469
.777 -.470
.798 -.401
.786 .500
.672 .458
.594 .444
.647 .333
""")
H = self.str2matrix("""
.8 -.3
.8 -.4
.7 -.4
.9 -.4
.8 .5
.6 .4
.5 .4
.6 .3
""")
T = target_rotation(A, H)
L = A.dot(T)
L_required = self.str2matrix("""
0.84168 -0.37053
0.83191 -0.44386
0.79096 -0.44611
0.80985 -0.37650
0.77040 0.52371
0.65774 0.47826
0.58020 0.46189
0.63656 0.35255
""")
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
T = target_rotation(A, H, full_rank=True)
L = A.dot(T)
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
def test_orthogonal_target(self):
"""
Rotation towards target matrix example
http://www.stat.ucla.edu/research/gpa
"""
A = self.str2matrix("""
.830 -.396
.818 -.469
.777 -.470
.798 -.401
.786 .500
.672 .458
.594 .444
.647 .333
""")
H = self.str2matrix("""
.8 -.3
.8 -.4
.7 -.4
.9 -.4
.8 .5
.6 .4
.5 .4
.6 .3
""")
vgQ = lambda L=None, A=None, T=None: vgQ_target(H, L=L, A=A, T=T)
L, phi, T, table = GPA(A, vgQ=vgQ, rotation_method='orthogonal')
T_analytic = target_rotation(A, H)
self.assertTrue(np.allclose(T, T_analytic, atol=1e-05))
class TestGPARotation(unittest.TestCase):
@staticmethod
def str2matrix(A):
A = A.lstrip().rstrip().split('\n')
A = np.array([row.split() for row in A]).astype(float)
return A
@classmethod
def get_A(cls):
return cls.str2matrix("""
.830 -.396
.818 -.469
.777 -.470
.798 -.401
.786 .500
.672 .458
.594 .444
.647 .333
""")
@classmethod
def get_quartimin_example(cls):
A = cls.get_A()
table_required = cls.str2matrix("""
0.00000 0.42806 -0.46393 1.00000
1.00000 0.41311 -0.57313 0.25000
2.00000 0.38238 -0.36652 0.50000
3.00000 0.31850 -0.21011 0.50000
4.00000 0.20937 -0.13838 0.50000
5.00000 0.12379 -0.35583 0.25000
6.00000 0.04289 -0.53244 0.50000
7.00000 0.01098 -0.86649 0.50000
8.00000 0.00566 -1.65798 0.50000
9.00000 0.00558 -2.13212 0.25000
10.00000 0.00557 -2.49020 0.25000
11.00000 0.00557 -2.84585 0.25000
12.00000 0.00557 -3.20320 0.25000
13.00000 0.00557 -3.56143 0.25000
14.00000 0.00557 -3.92005 0.25000
15.00000 0.00557 -4.27885 0.25000
16.00000 0.00557 -4.63772 0.25000
17.00000 0.00557 -4.99663 0.25000
18.00000 0.00557 -5.35555 0.25000
""")
L_required = cls.str2matrix("""
0.891822 0.056015
0.953680 -0.023246
0.929150 -0.046503
0.876683 0.033658
0.013701 0.925000
-0.017265 0.821253
-0.052445 0.764953
0.085890 0.683115
""")
return A, table_required, L_required
@classmethod
def get_biquartimin_example(cls):
A = cls.get_A()
table_required = cls.str2matrix("""
0.00000 0.21632 -0.54955 1.00000
1.00000 0.19519 -0.46174 0.50000
2.00000 0.09479 -0.16365 1.00000
3.00000 -0.06302 -0.32096 0.50000
4.00000 -0.21304 -0.46562 1.00000
5.00000 -0.33199 -0.33287 1.00000
6.00000 -0.35108 -0.63990 0.12500
7.00000 -0.35543 -1.20916 0.12500
8.00000 -0.35568 -2.61213 0.12500
9.00000 -0.35568 -2.97910 0.06250
10.00000 -0.35568 -3.32645 0.06250
11.00000 -0.35568 -3.66021 0.06250
12.00000 -0.35568 -3.98564 0.06250
13.00000 -0.35568 -4.30635 0.06250
14.00000 -0.35568 -4.62451 0.06250
15.00000 -0.35568 -4.94133 0.06250
16.00000 -0.35568 -5.25745 0.06250
""")
L_required = cls.str2matrix("""
1.01753 -0.13657
1.11338 -0.24643
1.09200 -0.26890
1.00676 -0.16010
-0.26534 1.11371
-0.26972 0.99553
-0.29341 0.93561
-0.10806 0.80513
""")
return A, table_required, L_required
@classmethod
def get_biquartimin_example_derivative_free(cls):
A = cls.get_A()
table_required = cls.str2matrix("""
0.00000 0.21632 -0.54955 1.00000
1.00000 0.19519 -0.46174 0.50000
2.00000 0.09479 -0.16365 1.00000
3.00000 -0.06302 -0.32096 0.50000
4.00000 -0.21304 -0.46562 1.00000
5.00000 -0.33199 -0.33287 1.00000
6.00000 -0.35108 -0.63990 0.12500
7.00000 -0.35543 -1.20916 0.12500
8.00000 -0.35568 -2.61213 0.12500
9.00000 -0.35568 -2.97910 0.06250
10.00000 -0.35568 -3.32645 0.06250
11.00000 -0.35568 -3.66021 0.06250
12.00000 -0.35568 -3.98564 0.06250
13.00000 -0.35568 -4.30634 0.06250
14.00000 -0.35568 -4.62451 0.06250
15.00000 -0.35568 -4.94133 0.06250
16.00000 -0.35568 -6.32435 0.12500
""")
L_required = cls.str2matrix("""
1.01753 -0.13657
1.11338 -0.24643
1.09200 -0.26890
1.00676 -0.16010
-0.26534 1.11371
-0.26972 0.99553
-0.29342 0.93561
-0.10806 0.80513
""")
return A, table_required, L_required
@classmethod
def get_quartimax_example_derivative_free(cls):
A = cls.get_A()
table_required = cls.str2matrix("""
0.00000 -0.72073 -0.65498 1.00000
1.00000 -0.88561 -0.34614 2.00000
2.00000 -1.01992 -1.07152 1.00000
3.00000 -1.02237 -1.51373 0.50000
4.00000 -1.02269 -1.96205 0.50000
5.00000 -1.02273 -2.41116 0.50000
6.00000 -1.02273 -2.86037 0.50000
7.00000 -1.02273 -3.30959 0.50000
8.00000 -1.02273 -3.75881 0.50000
9.00000 -1.02273 -4.20804 0.50000
10.00000 -1.02273 -4.65726 0.50000
11.00000 -1.02273 -5.10648 0.50000
""")
L_required = cls.str2matrix("""
0.89876 0.19482
0.93394 0.12974
0.90213 0.10386
0.87651 0.17128
0.31558 0.87647
0.25113 0.77349
0.19801 0.71468
0.30786 0.65933
""")
return A, table_required, L_required
def test_orthomax(self):
"""
Quartimax example
http://www.stat.ucla.edu/research/gpa
"""
A = self.get_A()
vgQ = lambda L=None, A=None, T=None: orthomax_objective(
L=L, A=A, T=T, gamma=0, return_gradient=True)
L, phi, T, table = GPA(A, vgQ=vgQ, rotation_method='orthogonal')
table_required = self.str2matrix("""
0.00000 -0.72073 -0.65498 1.00000
1.00000 -0.88561 -0.34614 2.00000
2.00000 -1.01992 -1.07152 1.00000
3.00000 -1.02237 -1.51373 0.50000
4.00000 -1.02269 -1.96205 0.50000
5.00000 -1.02273 -2.41116 0.50000
6.00000 -1.02273 -2.86037 0.50000
7.00000 -1.02273 -3.30959 0.50000
8.00000 -1.02273 -3.75881 0.50000
9.00000 -1.02273 -4.20804 0.50000
10.00000 -1.02273 -4.65726 0.50000
11.00000 -1.02273 -5.10648 0.50000
""")
L_required = self.str2matrix("""
0.89876 0.19482
0.93394 0.12974
0.90213 0.10386
0.87651 0.17128
0.31558 0.87647
0.25113 0.77349
0.19801 0.71468
0.30786 0.65933
""")
self.assertTrue(np.allclose(table, table_required, atol=1e-05))
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
# oblimin criterion gives same result
vgQ = lambda L=None, A=None, T=None: oblimin_objective(
L=L, A=A, T=T, gamma=0, rotation_method='orthogonal',
return_gradient=True)
L_oblimin, phi2, T2, table2 = GPA(A, vgQ=vgQ,
rotation_method='orthogonal')
self.assertTrue(np.allclose(L, L_oblimin, atol=1e-05))
# derivative free quartimax
out = self.get_quartimax_example_derivative_free()
A, table_required, L_required = out
ff = lambda L=None, A=None, T=None: orthomax_objective(
L=L, A=A, T=T, gamma=0, return_gradient=False)
L, phi, T, table = GPA(A, ff=ff, rotation_method='orthogonal')
self.assertTrue(np.allclose(table, table_required, atol=1e-05))
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
def test_equivalence_orthomax_oblimin(self):
"""
These criteria should be equivalent when restricted to orthogonal
rotation.
See Hartman 1976 page 299.
"""
A = self.get_A()
gamma = 0 # quartimax
vgQ = lambda L=None, A=None, T=None: orthomax_objective(
L=L, A=A, T=T, gamma=gamma, return_gradient=True)
L_orthomax, phi, T, table = GPA(
A, vgQ=vgQ, rotation_method='orthogonal')
vgQ = lambda L=None, A=None, T=None: oblimin_objective(
L=L, A=A, T=T, gamma=gamma, rotation_method='orthogonal',
return_gradient=True)
L_oblimin, phi2, T2, table2 = GPA(A, vgQ=vgQ,
rotation_method='orthogonal')
self.assertTrue(np.allclose(L_orthomax, L_oblimin, atol=1e-05))
gamma = 1 # varimax
vgQ = lambda L=None, A=None, T=None: orthomax_objective(
L=L, A=A, T=T, gamma=gamma, return_gradient=True)
L_orthomax, phi, T, table = GPA(
A, vgQ=vgQ, rotation_method='orthogonal')
vgQ = lambda L=None, A=None, T=None: oblimin_objective(
L=L, A=A, T=T, gamma=gamma, rotation_method='orthogonal',
return_gradient=True)
L_oblimin, phi2, T2, table2 = GPA(
A, vgQ=vgQ, rotation_method='orthogonal')
self.assertTrue(np.allclose(L_orthomax, L_oblimin, atol=1e-05))
def test_orthogonal_target(self):
"""
Rotation towards target matrix example
http://www.stat.ucla.edu/research/gpa
"""
A = self.get_A()
H = self.str2matrix("""
.8 -.3
.8 -.4
.7 -.4
.9 -.4
.8 .5
.6 .4
.5 .4
.6 .3
""")
vgQ = lambda L=None, A=None, T=None: vgQ_target(H, L=L, A=A, T=T)
L, phi, T, table = GPA(A, vgQ=vgQ, rotation_method='orthogonal')
table_required = self.str2matrix("""
0.00000 0.05925 -0.61244 1.00000
1.00000 0.05444 -1.14701 0.12500
2.00000 0.05403 -1.68194 0.12500
3.00000 0.05399 -2.21689 0.12500
4.00000 0.05399 -2.75185 0.12500
5.00000 0.05399 -3.28681 0.12500
6.00000 0.05399 -3.82176 0.12500
7.00000 0.05399 -4.35672 0.12500
8.00000 0.05399 -4.89168 0.12500
9.00000 0.05399 -5.42664 0.12500
""")
L_required = self.str2matrix("""
0.84168 -0.37053
0.83191 -0.44386
0.79096 -0.44611
0.80985 -0.37650
0.77040 0.52371
0.65774 0.47826
0.58020 0.46189
0.63656 0.35255
""")
self.assertTrue(np.allclose(table, table_required, atol=1e-05))
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
ff = lambda L=None, A=None, T=None: ff_target(H, L=L, A=A, T=T)
L2, phi, T2, table = GPA(A, ff=ff, rotation_method='orthogonal')
self.assertTrue(np.allclose(L, L2, atol=1e-05))
self.assertTrue(np.allclose(T, T2, atol=1e-05))
vgQ = lambda L=None, A=None, T=None: vgQ_target(
H, L=L, A=A, T=T, rotation_method='oblique')
L, phi, T, table = GPA(A, vgQ=vgQ, rotation_method='oblique')
ff = lambda L=None, A=None, T=None: ff_target(
H, L=L, A=A, T=T, rotation_method='oblique')
L2, phi, T2, table = GPA(A, ff=ff, rotation_method='oblique')
self.assertTrue(np.allclose(L, L2, atol=1e-05))
self.assertTrue(np.allclose(T, T2, atol=1e-05))
def test_orthogonal_partial_target(self):
"""
Rotation towards target matrix example
http://www.stat.ucla.edu/research/gpa
"""
A = self.get_A()
H = self.str2matrix("""
.8 -.3
.8 -.4
.7 -.4
.9 -.4
.8 .5
.6 .4
.5 .4
.6 .3
""")
W = self.str2matrix("""
1 0
0 1
0 0
1 1
1 0
1 0
0 1
1 0
""")
vgQ = lambda L=None, A=None, T=None: vgQ_partial_target(
H, W, L=L, A=A, T=T)
L, phi, T, table = GPA(A, vgQ=vgQ, rotation_method='orthogonal')
table_required = self.str2matrix("""
0.00000 0.02559 -0.84194 1.00000
1.00000 0.02203 -1.27116 0.25000
2.00000 0.02154 -1.71198 0.25000
3.00000 0.02148 -2.15713 0.25000
4.00000 0.02147 -2.60385 0.25000
5.00000 0.02147 -3.05114 0.25000
6.00000 0.02147 -3.49863 0.25000
7.00000 0.02147 -3.94619 0.25000
8.00000 0.02147 -4.39377 0.25000
9.00000 0.02147 -4.84137 0.25000
10.00000 0.02147 -5.28897 0.25000
""")
L_required = self.str2matrix("""
0.84526 -0.36228
0.83621 -0.43571
0.79528 -0.43836
0.81349 -0.36857
0.76525 0.53122
0.65303 0.48467
0.57565 0.46754
0.63308 0.35876
""")
self.assertTrue(np.allclose(table, table_required, atol=1e-05))
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
ff = lambda L=None, A=None, T=None: ff_partial_target(
H, W, L=L, A=A, T=T)
L2, phi, T2, table = GPA(A, ff=ff, rotation_method='orthogonal')
self.assertTrue(np.allclose(L, L2, atol=1e-05))
self.assertTrue(np.allclose(T, T2, atol=1e-05))
def test_oblimin(self):
# quartimin
A, table_required, L_required = self.get_quartimin_example()
vgQ = lambda L=None, A=None, T=None: oblimin_objective(
L=L, A=A, T=T, gamma=0, rotation_method='oblique')
L, phi, T, table = GPA(A, vgQ=vgQ, rotation_method='oblique')
self.assertTrue(np.allclose(table, table_required, atol=1e-05))
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
# quartimin derivative free
ff = lambda L=None, A=None, T=None: oblimin_objective(
L=L, A=A, T=T, gamma=0, rotation_method='oblique',
return_gradient=False)
L, phi, T, table = GPA(A, ff=ff, rotation_method='oblique')
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
self.assertTrue(np.allclose(table, table_required, atol=1e-05))
# biquartimin
A, table_required, L_required = self.get_biquartimin_example()
vgQ = lambda L=None, A=None, T=None: oblimin_objective(
L=L, A=A, T=T, gamma=1/2, rotation_method='oblique')
L, phi, T, table = GPA(A, vgQ=vgQ, rotation_method='oblique')
self.assertTrue(np.allclose(table, table_required, atol=1e-05))
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
# quartimin derivative free
out = self.get_biquartimin_example_derivative_free()
A, table_required, L_required = out
ff = lambda L=None, A=None, T=None: oblimin_objective(
L=L, A=A, T=T, gamma=1/2, rotation_method='oblique',
return_gradient=False)
L, phi, T, table = GPA(A, ff=ff, rotation_method='oblique')
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
self.assertTrue(np.allclose(table, table_required, atol=1e-05))
def test_CF(self):
# quartimax
out = self.get_quartimax_example_derivative_free()
A, table_required, L_required = out
vgQ = lambda L=None, A=None, T=None: CF_objective(
L=L, A=A, T=T, kappa=0, rotation_method='orthogonal',
return_gradient=True)
L, phi, T, table = GPA(A, vgQ=vgQ, rotation_method='orthogonal')
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
# quartimax derivative free
ff = lambda L=None, A=None, T=None: CF_objective(
L=L, A=A, T=T, kappa=0, rotation_method='orthogonal',
return_gradient=False)
L, phi, T, table = GPA(A, ff=ff, rotation_method='orthogonal')
self.assertTrue(np.allclose(L, L_required, atol=1e-05))
# varimax
p, k = A.shape
vgQ = lambda L=None, A=None, T=None: orthomax_objective(
L=L, A=A, T=T, gamma=1, return_gradient=True)
L_vm, phi, T, table = GPA(A, vgQ=vgQ, rotation_method='orthogonal')
vgQ = lambda L=None, A=None, T=None: CF_objective(
L=L, A=A, T=T, kappa=1/p, rotation_method='orthogonal',
return_gradient=True)
L_CF, phi, T, table = GPA(A, vgQ=vgQ, rotation_method='orthogonal')
ff = lambda L=None, A=None, T=None: CF_objective(
L=L, A=A, T=T, kappa=1/p, rotation_method='orthogonal',
return_gradient=False)
L_CF_df, phi, T, table = GPA(A, ff=ff, rotation_method='orthogonal')
self.assertTrue(np.allclose(L_vm, L_CF, atol=1e-05))
self.assertTrue(np.allclose(L_CF, L_CF_df, atol=1e-05))
class TestWrappers(unittest.TestCase):
@staticmethod
def str2matrix(A):
A = A.lstrip().rstrip().split('\n')
A = np.array([row.split() for row in A]).astype(float)
return A
def get_A(self):
return self.str2matrix("""
.830 -.396
.818 -.469
.777 -.470
.798 -.401
.786 .500
.672 .458
.594 .444
.647 .333
""")
def get_H(self):
return self.str2matrix("""
.8 -.3
.8 -.4
.7 -.4
.9 -.4
.8 .5
.6 .4
.5 .4
.6 .3
""")
def get_W(self):
return self.str2matrix("""
1 0
0 1
0 0
1 1
1 0
1 0
0 1
1 0
""")
def _test_template(self, method, *method_args, **algorithms):
A = self.get_A()
algorithm1 = 'gpa' if 'algorithm1' not in algorithms else algorithms[
'algorithm1']
if 'algorithm`' not in algorithms:
algorithm2 = 'gpa_der_free'
else:
algorithms['algorithm1']
L1, T1 = rotate_factors(A, method, *method_args, algorithm=algorithm1)
L2, T2 = rotate_factors(A, method, *method_args, algorithm=algorithm2)
self.assertTrue(np.allclose(L1, L2, atol=1e-5))
self.assertTrue(np.allclose(T1, T2, atol=1e-5))
def test_methods(self):
"""
Quartimax derivative free example
http://www.stat.ucla.edu/research/gpa
"""
# orthomax, oblimin and CF are tested indirectly
methods = ['quartimin', 'biquartimin',
'quartimax', 'biquartimax', 'varimax', 'equamax',
'parsimax', 'parsimony',
'target', 'partial_target']
for method in methods:
method_args = []
if method == 'target':
method_args = [self.get_H(), 'orthogonal']
self._test_template(method, *method_args)
method_args = [self.get_H(), 'oblique']
self._test_template(method, *method_args)
method_args = [self.get_H(), 'orthogonal']
self._test_template(method, *method_args,
algorithm2='analytic')
elif method == 'partial_target':
method_args = [self.get_H(), self.get_W()]
self._test_template(method, *method_args)