Is there a method that I can call to create a random orthonormal matrix in python? Possibly using numpy? Or is there a way to create a orthonormal matrix using multiple numpy methods? Thanks.
7 Answers
Version 0.18 of scipy has scipy.stats.ortho_group
and scipy.stats.special_ortho_group
. The pull request where it was added is https://github.com/scipy/scipy/pull/5622
For example,
In [24]: from scipy.stats import ortho_group # Requires version 0.18 of scipy
In [25]: m = ortho_group.rvs(dim=3)
In [26]: m
Out[26]:
array([[-0.23939017, 0.58743526, -0.77305379],
[ 0.81921268, -0.30515101, -0.48556508],
[-0.52113619, -0.74953498, -0.40818426]])
In [27]: np.set_printoptions(suppress=True)
In [28]: m.dot(m.T)
Out[28]:
array([[ 1., 0., -0.],
[ 0., 1., 0.],
[-0., 0., 1.]])

- 757
- 6
- 8

- 110,654
- 19
- 194
- 214
-
Thank you for your response. I have noticed that the answers given are all for square matrices. Can I still use this method to obtain a d x k matrix, where k < d? – Dacion Jul 18 '16 at 15:52
-
Construct a d x d matrix and then drop extra columns – Shailesh Kumar Feb 19 '21 at 17:29
You can obtain a random n x n
orthogonal matrix Q
, (uniformly distributed over the manifold of n x n
orthogonal matrices) by performing a QR
factorization of an n x n
matrix with elements i.i.d. Gaussian random variables of mean 0
and variance 1
. Here is an example:
import numpy as np
from scipy.linalg import qr
n = 3
H = np.random.randn(n, n)
Q, R = qr(H)
print (Q.dot(Q.T))
[[ 1.00000000e+00 -2.77555756e-17 2.49800181e-16] [ -2.77555756e-17 1.00000000e+00 -1.38777878e-17] [ 2.49800181e-16 -1.38777878e-17 1.00000000e+00]]
EDIT: (Revisiting this answer after the comment by @g g.) The claim above on the QR decomposition of a Gaussian matrix providing a uniformly distributed (over the, so called, Stiefel manifold) orthogonal matrix is suggested by Theorems 2.3.18-19 of this reference. Note that the statement of the result suggests a "QR-like" decomposition, however, with the triangular matrix R
having positive elements.
Apparently, the qr
function of scipy (numpy) function does not guarantee positive diagonal elements for R
and the corresponding Q
is actually not uniformly distributed. This has been observed in this monograph, Sec. 4.6 (the discussion refers to MATLAB, but I guess both MATLAB and scipy use the same LAPACK routines). It is suggested there that the matrix Q
provided by qr
is modified by post multiplying it with a random unitary diagonal matrix.
Below I reproduce the experiment in the above reference, plotting the empirical distribution (histogram) of phases of eigenvalues of the "direct" Q
matrix provided by qr
, as well as the "modified" version, where it is seen that the modified version does indeed have a uniform eigenvalue phase, as would be expected from a uniformly distributed orthogonal matrix.
from scipy.linalg import qr, eigvals
from seaborn import distplot
n = 50
repeats = 10000
angles = []
angles_modified = []
for rp in range(repeats):
H = np.random.randn(n, n)
Q, R = qr(H)
angles.append(np.angle(eigvals(Q)))
Q_modified = Q @ np.diag(np.exp(1j * np.pi * 2 * np.random.rand(n)))
angles_modified.append(np.angle(eigvals(Q_modified)))
fig, ax = plt.subplots(1,2, figsize = (10,3))
distplot(np.asarray(angles).flatten(),kde = False, hist_kws=dict(edgecolor="k", linewidth=2), ax= ax[0])
ax[0].set(xlabel='phase', title='direct')
distplot(np.asarray(angles_modified).flatten(),kde = False, hist_kws=dict(edgecolor="k", linewidth=2), ax= ax[1])
ax[1].set(xlabel='phase', title='modified');

- 5,271
- 1
- 18
- 32
-
How do you know the resulting matrices are uniformly distributed over the manifold? And according to what measure on the manifold? – g g Apr 28 '19 at 08:22
-
@gg Thanks for the comment! Actually, the "direct" application of `qr` does not provide a uniformly distributed orthogonal matrix. Please see the edited answer for a workaround. – Stelios Apr 28 '19 at 09:48
-
-
4The random angles are not needed, just use the signs from the diagonals of R: `Q_modified = Q @ np.diag(np.sign(np.diag(R))` – Andrew Swann Apr 06 '20 at 14:09
This is the rvs
method pulled from the https://github.com/scipy/scipy/pull/5622/files, with minimal change - just enough to run as a stand alone numpy function.
import numpy as np
def rvs(dim=3):
random_state = np.random
H = np.eye(dim)
D = np.ones((dim,))
for n in range(1, dim):
x = random_state.normal(size=(dim-n+1,))
D[n-1] = np.sign(x[0])
x[0] -= D[n-1]*np.sqrt((x*x).sum())
# Householder transformation
Hx = (np.eye(dim-n+1) - 2.*np.outer(x, x)/(x*x).sum())
mat = np.eye(dim)
mat[n-1:, n-1:] = Hx
H = np.dot(H, mat)
# Fix the last sign such that the determinant is 1
D[-1] = (-1)**(1-(dim % 2))*D.prod()
# Equivalent to np.dot(np.diag(D), H) but faster, apparently
H = (D*H.T).T
return H
It matches Warren's test, https://stackoverflow.com/a/38426572/901925

- 463
- 1
- 9
- 21

- 221,503
- 14
- 230
- 353
-
This answer is good, but is now outdated; The newer answer referencing scipy.stats.ortho_group is now more appropriate. – MRule Jul 04 '22 at 16:31
An easy way to create any shape (n x m
) orthogonal matrix:
import numpy as np
n, m = 3, 5
H = np.random.rand(n, m)
u, s, vh = np.linalg.svd(H, full_matrices=False)
mat = u @ vh
print(mat @ mat.T) # -> eye(n)
Note that if n > m
, it would obtain mat.T @ mat = eye(m)
.

- 720
- 6
- 10
-
1I think that you should change `rand` into `randn` to obtain a uniform distribution of the matrices. Otherwise the best answer in my opinion. – tglas Dec 20 '19 at 21:39
from scipy.stats import special_ortho_group
num_dim=3
x = special_ortho_group.rvs(num_dim)

- 1,671
- 14
- 17
if you want a none Square Matrix with orthonormal column vectors you could create a square one with any of the mentioned method and drop some columns.

- 51
- 3
Numpy also has qr factorization. https://numpy.org/doc/stable/reference/generated/numpy.linalg.qr.html
import numpy as np
a = np.random.rand(3, 3)
q, r = np.linalg.qr(a)
q @ q.T
# array([[ 1.00000000e+00, 8.83206468e-17, 2.69154044e-16],
# [ 8.83206468e-17, 1.00000000e+00, -1.30466244e-16],
# [ 2.69154044e-16, -1.30466244e-16, 1.00000000e+00]])

- 1,891
- 1
- 12
- 4