Hide keyboard shortcuts

Hot-keys on this page

r m x p   toggle line displays

j k   next/prev highlighted chunk

0   (zero) top of page

1   (one) first highlighted chunk

1# -*- coding: utf-8 -*- 

2 

3 

4from ._analytic_rotation import target_rotation 

5from ._gpa_rotation import oblimin_objective, orthomax_objective, CF_objective 

6from ._gpa_rotation import ff_partial_target, ff_target 

7from ._gpa_rotation import vgQ_partial_target, vgQ_target 

8from ._gpa_rotation import rotateA, GPA 

9 

10__all__ = [] 

11 

12 

13def rotate_factors(A, method, *method_args, **algorithm_kwargs): 

14 r""" 

15 Subroutine for orthogonal and oblique rotation of the matrix :math:`A`. 

16 For orthogonal rotations :math:`A` is rotated to :math:`L` according to 

17 

18 .. math:: 

19 

20 L = AT, 

21 

22 where :math:`T` is an orthogonal matrix. And, for oblique rotations 

23 :math:`A` is rotated to :math:`L` according to 

24 

25 .. math:: 

26 

27 L = A(T^*)^{-1}, 

28 

29 where :math:`T` is a normal matrix. 

30 

31 Parameters 

32 ---------- 

33 A : numpy matrix (default None) 

34 non rotated factors 

35 method : str 

36 should be one of the methods listed below 

37 method_args : list 

38 additional arguments that should be provided with each method 

39 algorithm_kwargs : dictionary 

40 algorithm : str (default gpa) 

41 should be one of: 

42 

43 * 'gpa': a numerical method 

44 * 'gpa_der_free': a derivative free numerical method 

45 * 'analytic' : an analytic method 

46 

47 Depending on the algorithm, there are algorithm specific keyword 

48 arguments. For the gpa and gpa_der_free, the following 

49 keyword arguments are available: 

50 

51 max_tries : int (default 501) 

52 maximum number of iterations 

53 

54 tol : float 

55 stop criterion, algorithm stops if Frobenius norm of gradient is 

56 smaller then tol 

57 

58 For analytic, the supported arguments depend on the method, see above. 

59 

60 See the lower level functions for more details. 

61 

62 Returns 

63 ------- 

64 The tuple :math:`(L,T)` 

65 

66 Notes 

67 ----- 

68 What follows is a list of available methods. Depending on the method 

69 additional argument are required and different algorithms 

70 are available. The algorithm_kwargs are additional keyword arguments 

71 passed to the selected algorithm (see the parameters section). 

72 Unless stated otherwise, only the gpa and 

73 gpa_der_free algorithm are available. 

74 

75 Below, 

76 

77 * :math:`L` is a :math:`p\times k` matrix; 

78 * :math:`N` is :math:`k\times k` matrix with zeros on the diagonal and ones 

79 elsewhere; 

80 * :math:`M` is :math:`p\times p` matrix with zeros on the diagonal and ones 

81 elsewhere; 

82 * :math:`C` is a :math:`p\times p` matrix with elements equal to 

83 :math:`1/p`; 

84 * :math:`(X,Y)=\operatorname{Tr}(X^*Y)` is the Frobenius norm; 

85 * :math:`\circ` is the element-wise product or Hadamard product. 

86 

87 oblimin : orthogonal or oblique rotation that minimizes 

88 .. math:: 

89 \phi(L) = \frac{1}{4}(L\circ L,(I-\gamma C)(L\circ L)N). 

90 

91 For orthogonal rotations: 

92 

93 * :math:`\gamma=0` corresponds to quartimax, 

94 * :math:`\gamma=\frac{1}{2}` corresponds to biquartimax, 

95 * :math:`\gamma=1` corresponds to varimax, 

96 * :math:`\gamma=\frac{1}{p}` corresponds to equamax. 

97 

98 For oblique rotations rotations: 

99 

100 * :math:`\gamma=0` corresponds to quartimin, 

101 * :math:`\gamma=\frac{1}{2}` corresponds to biquartimin. 

102 

103 method_args: 

104 

105 gamma : float 

106 oblimin family parameter 

107 rotation_method : str 

108 should be one of {orthogonal, oblique} 

109 

110 orthomax : orthogonal rotation that minimizes 

111 

112 .. math:: 

113 \phi(L) = -\frac{1}{4}(L\circ L,(I-\gamma C)(L\circ L)), 

114 

115 where :math:`0\leq\gamma\leq1`. The orthomax family is equivalent to 

116 the oblimin family (when restricted to orthogonal rotations). 

117 Furthermore, 

118 

119 * :math:`\gamma=0` corresponds to quartimax, 

120 * :math:`\gamma=\frac{1}{2}` corresponds to biquartimax, 

121 * :math:`\gamma=1` corresponds to varimax, 

122 * :math:`\gamma=\frac{1}{p}` corresponds to equamax. 

123 

124 method_args: 

125 

126 gamma : float (between 0 and 1) 

127 orthomax family parameter 

128 

129 CF : Crawford-Ferguson family for orthogonal and oblique rotation which 

130 minimizes: 

131 

132 .. math:: 

133 

134 \phi(L) =\frac{1-\kappa}{4} (L\circ L,(L\circ L)N) 

135 -\frac{1}{4}(L\circ L,M(L\circ L)), 

136 

137 where :math:`0\leq\kappa\leq1`. For orthogonal rotations the oblimin 

138 (and orthomax) family of rotations is equivalent to the 

139 Crawford-Ferguson family. 

140 To be more precise: 

141 

142 * :math:`\kappa=0` corresponds to quartimax, 

143 * :math:`\kappa=\frac{1}{p}` corresponds to varimax, 

144 * :math:`\kappa=\frac{k-1}{p+k-2}` corresponds to parsimax, 

145 * :math:`\kappa=1` corresponds to factor parsimony. 

146 

147 method_args: 

148 

149 kappa : float (between 0 and 1) 

150 Crawford-Ferguson family parameter 

151 rotation_method : str 

152 should be one of {orthogonal, oblique} 

153 

154 quartimax : orthogonal rotation method 

155 minimizes the orthomax objective with :math:`\gamma=0` 

156 

157 biquartimax : orthogonal rotation method 

158 minimizes the orthomax objective with :math:`\gamma=\frac{1}{2}` 

159 

160 varimax : orthogonal rotation method 

161 minimizes the orthomax objective with :math:`\gamma=1` 

162 

163 equamax : orthogonal rotation method 

164 minimizes the orthomax objective with :math:`\gamma=\frac{1}{p}` 

165 

166 parsimax : orthogonal rotation method 

167 minimizes the Crawford-Ferguson family objective with 

168 :math:`\kappa=\frac{k-1}{p+k-2}` 

169 

170 parsimony : orthogonal rotation method 

171 minimizes the Crawford-Ferguson family objective with :math:`\kappa=1` 

172 

173 quartimin : oblique rotation method that minimizes 

174 minimizes the oblimin objective with :math:`\gamma=0` 

175 

176 quartimin : oblique rotation method that minimizes 

177 minimizes the oblimin objective with :math:`\gamma=\frac{1}{2}` 

178 

179 target : orthogonal or oblique rotation that rotates towards a target 

180 

181 matrix : math:`H` by minimizing the objective 

182 

183 .. math:: 

184 

185 \phi(L) =\frac{1}{2}\|L-H\|^2. 

186 

187 method_args: 

188 

189 H : numpy matrix 

190 target matrix 

191 rotation_method : str 

192 should be one of {orthogonal, oblique} 

193 

194 For orthogonal rotations the algorithm can be set to analytic in which 

195 case the following keyword arguments are available: 

196 

197 full_rank : bool (default False) 

198 if set to true full rank is assumed 

199 

200 partial_target : orthogonal (default) or oblique rotation that partially 

201 rotates towards a target matrix :math:`H` by minimizing the objective: 

202 

203 .. math:: 

204 

205 \phi(L) =\frac{1}{2}\|W\circ(L-H)\|^2. 

206 

207 method_args: 

208 

209 H : numpy matrix 

210 target matrix 

211 W : numpy matrix (default matrix with equal weight one for all entries) 

212 matrix with weights, entries can either be one or zero 

213 

214 Examples 

215 -------- 

216 >>> A = np.random.randn(8,2) 

217 >>> L, T = rotate_factors(A,'varimax') 

218 >>> np.allclose(L,A.dot(T)) 

219 >>> L, T = rotate_factors(A,'orthomax',0.5) 

220 >>> np.allclose(L,A.dot(T)) 

221 >>> L, T = rotate_factors(A,'quartimin',0.5) 

222 >>> np.allclose(L,A.dot(np.linalg.inv(T.T))) 

223 """ 

224 if 'algorithm' in algorithm_kwargs: 

225 algorithm = algorithm_kwargs['algorithm'] 

226 algorithm_kwargs.pop('algorithm') 

227 else: 

228 algorithm = 'gpa' 

229 assert not ('rotation_method' in algorithm_kwargs), ( 

230 'rotation_method cannot be provided as keyword argument') 

231 L = None 

232 T = None 

233 ff = None 

234 vgQ = None 

235 p, k = A.shape 

236 # set ff or vgQ to appropriate objective function, compute solution using 

237 # recursion or analytically compute solution 

238 if method == 'orthomax': 

239 assert len(method_args) == 1, ('Only %s family parameter should be ' 

240 'provided' % method) 

241 rotation_method = 'orthogonal' 

242 gamma = method_args[0] 

243 if algorithm == 'gpa': 

244 vgQ = lambda L=None, A=None, T=None: orthomax_objective( 

245 L=L, A=A, T=T, gamma=gamma, return_gradient=True) 

246 elif algorithm == 'gpa_der_free': 

247 ff = lambda L=None, A=None, T=None: orthomax_objective( 

248 L=L, A=A, T=T, gamma=gamma, return_gradient=False) 

249 else: 

250 raise ValueError('Algorithm %s is not possible for %s ' 

251 'rotation' % (algorithm, method)) 

252 elif method == 'oblimin': 

253 assert len(method_args) == 2, ('Both %s family parameter and ' 

254 'rotation_method should be ' 

255 'provided' % method) 

256 rotation_method = method_args[1] 

257 assert rotation_method in ['orthogonal', 'oblique'], ( 

258 'rotation_method should be one of {orthogonal, oblique}') 

259 gamma = method_args[0] 

260 if algorithm == 'gpa': 

261 vgQ = lambda L=None, A=None, T=None: oblimin_objective( 

262 L=L, A=A, T=T, gamma=gamma, return_gradient=True) 

263 elif algorithm == 'gpa_der_free': 

264 ff = lambda L=None, A=None, T=None: oblimin_objective( 

265 L=L, A=A, T=T, gamma=gamma, rotation_method=rotation_method, 

266 return_gradient=False) 

267 else: 

268 raise ValueError('Algorithm %s is not possible for %s ' 

269 'rotation' % (algorithm, method)) 

270 elif method == 'CF': 

271 assert len(method_args) == 2, ('Both %s family parameter and ' 

272 'rotation_method should be provided' 

273 % method) 

274 rotation_method = method_args[1] 

275 assert rotation_method in ['orthogonal', 'oblique'], ( 

276 'rotation_method should be one of {orthogonal, oblique}') 

277 kappa = method_args[0] 

278 if algorithm == 'gpa': 

279 vgQ = lambda L=None, A=None, T=None: CF_objective( 

280 L=L, A=A, T=T, kappa=kappa, rotation_method=rotation_method, 

281 return_gradient=True) 

282 elif algorithm == 'gpa_der_free': 

283 ff = lambda L=None, A=None, T=None: CF_objective( 

284 L=L, A=A, T=T, kappa=kappa, rotation_method=rotation_method, 

285 return_gradient=False) 

286 else: 

287 raise ValueError('Algorithm %s is not possible for %s ' 

288 'rotation' % (algorithm, method)) 

289 elif method == 'quartimax': 

290 return rotate_factors(A, 'orthomax', 0, **algorithm_kwargs) 

291 elif method == 'biquartimax': 

292 return rotate_factors(A, 'orthomax', 0.5, **algorithm_kwargs) 

293 elif method == 'varimax': 

294 return rotate_factors(A, 'orthomax', 1, **algorithm_kwargs) 

295 elif method == 'equamax': 

296 return rotate_factors(A, 'orthomax', 1/p, **algorithm_kwargs) 

297 elif method == 'parsimax': 

298 return rotate_factors(A, 'CF', (k-1)/(p+k-2), 

299 'orthogonal', **algorithm_kwargs) 

300 elif method == 'parsimony': 

301 return rotate_factors(A, 'CF', 1, 'orthogonal', **algorithm_kwargs) 

302 elif method == 'quartimin': 

303 return rotate_factors(A, 'oblimin', 0, 'oblique', **algorithm_kwargs) 

304 elif method == 'biquartimin': 

305 return rotate_factors(A, 'oblimin', 0.5, 'oblique', **algorithm_kwargs) 

306 elif method == 'target': 

307 assert len(method_args) == 2, ( 

308 'only the rotation target and orthogonal/oblique should be provide' 

309 ' for %s rotation' % method) 

310 H = method_args[0] 

311 rotation_method = method_args[1] 

312 assert rotation_method in ['orthogonal', 'oblique'], ( 

313 'rotation_method should be one of {orthogonal, oblique}') 

314 if algorithm == 'gpa': 

315 vgQ = lambda L=None, A=None, T=None: vgQ_target( 

316 H, L=L, A=A, T=T, rotation_method=rotation_method) 

317 elif algorithm == 'gpa_der_free': 

318 ff = lambda L=None, A=None, T=None: ff_target( 

319 H, L=L, A=A, T=T, rotation_method=rotation_method) 

320 elif algorithm == 'analytic': 

321 assert rotation_method == 'orthogonal', ( 

322 'For analytic %s rotation only orthogonal rotation is ' 

323 'supported') 

324 T = target_rotation(A, H, **algorithm_kwargs) 

325 else: 

326 raise ValueError('Algorithm %s is not possible for %s rotation' 

327 % (algorithm, method)) 

328 elif method == 'partial_target': 

329 assert len(method_args) == 2, ('2 additional arguments are expected ' 

330 'for %s rotation' % method) 

331 H = method_args[0] 

332 W = method_args[1] 

333 rotation_method = 'orthogonal' 

334 if algorithm == 'gpa': 

335 vgQ = lambda L=None, A=None, T=None: vgQ_partial_target( 

336 H, W=W, L=L, A=A, T=T) 

337 elif algorithm == 'gpa_der_free': 

338 ff = lambda L=None, A=None, T=None: ff_partial_target( 

339 H, W=W, L=L, A=A, T=T) 

340 else: 

341 raise ValueError('Algorithm %s is not possible for %s ' 

342 'rotation' % (algorithm, method)) 

343 else: 

344 raise ValueError('Invalid method') 

345 # compute L and T if not already done 

346 if T is None: 

347 L, phi, T, table = GPA(A, vgQ=vgQ, ff=ff, 

348 rotation_method=rotation_method, 

349 **algorithm_kwargs) 

350 if L is None: 

351 assert T is not None, 'Cannot compute L without T' 

352 L = rotateA(A, T, rotation_method=rotation_method) 

353 return L, T