Skip to content

QR Decomposition

Decompose a matrix into orthogonal and triangular components.

np.linalg.qr

1. Basic Usage

import numpy as np

def main():
    A = np.array([[1, 2],
                  [3, 4],
                  [5, 6]])

    Q, R = np.linalg.qr(A)

    print("A =")
    print(A)
    print()
    print("Q (orthogonal) =")
    print(Q.round(4))
    print()
    print("R (upper triangular) =")
    print(R.round(4))

if __name__ == "__main__":
    main()

2. Mathematical Form

\[A = QR\]
  • \(Q\): Orthogonal matrix (\(Q^TQ = I\))
  • \(R\): Upper triangular matrix

3. Verify Result

import numpy as np

def main():
    A = np.array([[1, 2],
                  [3, 4],
                  [5, 6]])

    Q, R = np.linalg.qr(A)

    # Q @ R should equal A
    A_reconstructed = Q @ R

    print("A =")
    print(A)
    print()
    print("Q @ R =")
    print(A_reconstructed.round(10))
    print()
    print(f"Match: {np.allclose(A, A_reconstructed)}")

if __name__ == "__main__":
    main()

Properties

1. Q is Orthogonal

import numpy as np

def main():
    A = np.array([[1, 2],
                  [3, 4],
                  [5, 6]])

    Q, R = np.linalg.qr(A)

    # Q^T @ Q should be identity
    QtQ = Q.T @ Q

    print("Q^T @ Q =")
    print(QtQ.round(10))
    print()
    print(f"Is identity: {np.allclose(QtQ, np.eye(QtQ.shape[0]))}")

if __name__ == "__main__":
    main()

2. R is Upper Triangular

import numpy as np

def main():
    A = np.array([[1, 2, 3],
                  [4, 5, 6],
                  [7, 8, 10]])

    Q, R = np.linalg.qr(A)

    print("R =")
    print(R.round(4))
    print()

    # Check lower triangle is zero
    lower = np.tril(R, k=-1)
    print("Lower triangle (should be zeros):")
    print(lower.round(10))

if __name__ == "__main__":
    main()

3. Orthonormal Columns

import numpy as np

def main():
    A = np.random.randn(5, 3)

    Q, R = np.linalg.qr(A)

    # Check column norms are 1
    col_norms = np.linalg.norm(Q, axis=0)
    print(f"Column norms: {col_norms}")

    # Check columns are orthogonal
    for i in range(Q.shape[1]):
        for j in range(i + 1, Q.shape[1]):
            dot = Q[:, i] @ Q[:, j]
            print(f"Q[:, {i}] ยท Q[:, {j}] = {dot:.10f}")

if __name__ == "__main__":
    main()

Mode Options

1. Reduced QR (default)

import numpy as np

def main():
    A = np.random.randn(5, 3)

    Q, R = np.linalg.qr(A, mode='reduced')

    print(f"A shape: {A.shape}")
    print(f"Q shape: {Q.shape}")
    print(f"R shape: {R.shape}")

if __name__ == "__main__":
    main()

2. Full QR

import numpy as np

def main():
    A = np.random.randn(5, 3)

    Q, R = np.linalg.qr(A, mode='complete')

    print(f"A shape: {A.shape}")
    print(f"Q shape: {Q.shape}")  # Square
    print(f"R shape: {R.shape}")

if __name__ == "__main__":
    main()

3. R Only

import numpy as np

def main():
    A = np.random.randn(5, 3)

    R = np.linalg.qr(A, mode='r')

    print(f"A shape: {A.shape}")
    print(f"R shape: {R.shape}")
    print()
    print("R =")
    print(R.round(4))

if __name__ == "__main__":
    main()

Least Squares

1. QR Solution

import numpy as np
from scipy import linalg

def main():
    # Overdetermined system
    A = np.array([[1, 1],
                  [1, 2],
                  [1, 3]])
    b = np.array([1, 2, 2])

    Q, R = np.linalg.qr(A)

    # Solve R @ x = Q^T @ b
    x = linalg.solve_triangular(R, Q.T @ b)

    print(f"Least squares solution: {x}")
    print(f"Residual: {np.linalg.norm(A @ x - b):.4f}")

if __name__ == "__main__":
    main()

2. Compare with lstsq

import numpy as np
from scipy import linalg

def main():
    A = np.array([[1, 1],
                  [1, 2],
                  [1, 3],
                  [1, 4]])
    b = np.array([1, 2.1, 2.9, 4.2])

    # QR method
    Q, R = np.linalg.qr(A)
    x_qr = linalg.solve_triangular(R, Q.T @ b)

    # lstsq method
    x_lstsq, _, _, _ = np.linalg.lstsq(A, b, rcond=None)

    print(f"QR solution:    {x_qr}")
    print(f"lstsq solution: {x_lstsq}")
    print(f"Match: {np.allclose(x_qr, x_lstsq)}")

if __name__ == "__main__":
    main()

3. Why Use QR

  • More numerically stable than normal equations
  • Efficient for multiple right-hand sides

Applications

1. Orthonormal Basis

import numpy as np

def main():
    # Linearly independent vectors (as columns)
    A = np.array([[1, 1],
                  [1, 2],
                  [0, 1]])

    Q, R = np.linalg.qr(A)

    print("Original columns (not orthogonal):")
    print(A)
    print()
    print("Orthonormal basis for column space:")
    print(Q.round(4))

if __name__ == "__main__":
    main()

2. Projection Matrix

import numpy as np

def main():
    # Subspace spanned by columns of A
    A = np.array([[1, 0],
                  [1, 1],
                  [0, 1]])

    Q, R = np.linalg.qr(A)

    # Projection matrix: P = Q @ Q^T
    P = Q @ Q.T

    print("Projection matrix:")
    print(P.round(4))
    print()

    # Project a vector
    v = np.array([1, 1, 1])
    v_proj = P @ v
    print(f"v = {v}")
    print(f"Projection of v: {v_proj.round(4)}")

if __name__ == "__main__":
    main()

3. Matrix Rank

import numpy as np

def main():
    # Rank-deficient matrix
    A = np.array([[1, 2, 3],
                  [2, 4, 6],
                  [1, 1, 1]])

    Q, R = np.linalg.qr(A)

    # Rank = number of non-zero diagonal elements in R
    diag_R = np.abs(np.diag(R))
    rank_qr = np.sum(diag_R > 1e-10)

    print("R diagonal:", diag_R.round(4))
    print(f"Rank via QR: {rank_qr}")
    print(f"np.linalg.matrix_rank: {np.linalg.matrix_rank(A)}")

if __name__ == "__main__":
    main()

scipy.linalg.qr

1. Pivoting

Column pivoting for better numerical stability.

import numpy as np
from scipy import linalg

def main():
    A = np.array([[1, 2, 3],
                  [4, 5, 6],
                  [7, 8, 9]])

    # QR with column pivoting
    Q, R, P = linalg.qr(A, pivoting=True)

    print(f"Permutation: {P}")
    print()
    print("R diagonal:", np.diag(R).round(4))

if __name__ == "__main__":
    main()

2. Economic Mode

import numpy as np
from scipy import linalg

def main():
    A = np.random.randn(100, 10)

    # Economic QR (like NumPy's reduced)
    Q, R = linalg.qr(A, mode='economic')

    print(f"A shape: {A.shape}")
    print(f"Q shape: {Q.shape}")
    print(f"R shape: {R.shape}")

if __name__ == "__main__":
    main()

3. Raw Mode

Returns Householder reflectors for advanced use.

Best Practices

1. Use for Least Squares

QR is the recommended method for solving least squares problems.

2. Reduced by Default

Use reduced (economic) mode unless you need the full Q.

3. Pivoting for Rank

Use pivoted QR when rank determination is important.