Skip to content

Sparse Eigenvalues

scipy.sparse.linalg provides eigensolvers for large sparse matrices.

eigs - General Sparse

1. Basic Usage

import numpy as np
from scipy import sparse
from scipy.sparse import linalg as splinalg

def main():
    # Large sparse matrix
    n = 1000
    A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csr')

    # Find 6 largest eigenvalues
    eigenvalues, eigenvectors = splinalg.eigs(A, k=6)

    print("6 largest eigenvalues:")
    print(eigenvalues)

if __name__ == "__main__":
    main()

2. Which Eigenvalues

import numpy as np
from scipy import sparse
from scipy.sparse import linalg as splinalg

def main():
    n = 100
    A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csr')

    # 'LM' - largest magnitude (default)
    vals_LM, _ = splinalg.eigs(A, k=3, which='LM')

    # 'SM' - smallest magnitude
    vals_SM, _ = splinalg.eigs(A, k=3, which='SM')

    # 'LR' - largest real part
    vals_LR, _ = splinalg.eigs(A, k=3, which='LR')

    print("Largest magnitude:", vals_LM)
    print("Smallest magnitude:", vals_SM)
    print("Largest real:", vals_LR)

if __name__ == "__main__":
    main()

eigsh - Symmetric Sparse

1. Basic Usage

import numpy as np
from scipy import sparse
from scipy.sparse import linalg as splinalg

def main():
    n = 1000
    A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csr')

    # For symmetric matrices - faster
    eigenvalues, eigenvectors = splinalg.eigsh(A, k=6)

    print("6 eigenvalues (real, sorted):")
    print(eigenvalues)

if __name__ == "__main__":
    main()

2. Smallest Eigenvalues

import numpy as np
from scipy import sparse
from scipy.sparse import linalg as splinalg

def main():
    n = 100
    A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n), format='csr')

    # Smallest algebraic eigenvalues
    vals_SA, _ = splinalg.eigsh(A, k=3, which='SA')

    # Largest algebraic eigenvalues
    vals_LA, _ = splinalg.eigsh(A, k=3, which='LA')

    print("Smallest:", vals_SA)
    print("Largest:", vals_LA)

if __name__ == "__main__":
    main()

Applications

1. Graph Laplacian Spectrum

import numpy as np
from scipy import sparse
from scipy.sparse import linalg as splinalg

def main():
    # Random sparse graph
    np.random.seed(42)
    n = 500
    A = sparse.random(n, n, density=0.02, format='csr')
    A = (A + A.T) / 2  # Symmetric
    A.data[:] = 1  # Binary

    # Laplacian
    D = sparse.diags(np.array(A.sum(axis=1)).flatten())
    L = D - A

    # Find smallest eigenvalues
    vals, vecs = splinalg.eigsh(L, k=10, which='SM')

    print("Smallest Laplacian eigenvalues:")
    print(vals)

if __name__ == "__main__":
    main()

2. Spectral Clustering

import numpy as np
from scipy import sparse
from scipy.sparse import linalg as splinalg

def main():
    n = 200
    A = sparse.random(n, n, density=0.1, format='csr')
    A = (A + A.T) / 2

    D = sparse.diags(np.array(A.sum(axis=1)).flatten())
    L = D - A

    # Second smallest eigenvector for 2-way partition
    vals, vecs = splinalg.eigsh(L, k=2, which='SM')
    fiedler = vecs[:, 1]

    partition = fiedler > 0
    print(f"Partition sizes: {partition.sum()}, {(~partition).sum()}")

if __name__ == "__main__":
    main()

Summary

Function Description
splinalg.eigs(A, k) k eigenvalues of sparse A
splinalg.eigsh(A, k) k eigenvalues of symmetric sparse A
which='LM' Largest magnitude
which='SM' Smallest magnitude
which='SA' Smallest algebraic (eigsh)

Runnable Example: spectral_methods_example.py

"""
Spectral Methods with Sparse Eigenvalue Problems - Linear Algebra Foundations
Exploring Laplacian matrices, sparse eigenvalue decomposition, and spectral
analysis techniques using scipy.sparse.linalg.eigsh() and eigendecomposition.
Run this file to see spectral methods in action!
"""

import numpy as np
from scipy import sparse
from scipy import linalg

if __name__ == "__main__":

    print("=" * 70)
    print("SPECTRAL METHODS WITH SPARSE EIGENVALUE PROBLEMS")
    print("=" * 70)

    # ============================================================================
    # EXAMPLE 1: Building Laplacian Matrix from Adjacency Matrix
    # ============================================================================
    print("\n1. CONSTRUCTING LAPLACIAN MATRIX FROM ADJACENCY MATRIX")
    print("-" * 70)

    # Adjacency matrix represents connections in a graph
    # Element A[i,j] = 1 if node i connects to node j, 0 otherwise
    A = np.array([
        [0, 1, 1, 0],
        [1, 0, 1, 1],
        [1, 1, 0, 1],
        [0, 1, 1, 0]
    ], dtype=float)

    print("Adjacency matrix A (4 nodes):")
    print(A)

    # Degree matrix D: diagonal matrix with degrees on diagonal
    # degree[i] = sum of connections from node i
    degrees = np.sum(A, axis=0)
    print("\nDegree of each node:", degrees)

    # Laplacian L = D - A (undirected case)
    D = np.diag(degrees)
    L = D - A

    print("\nDegree matrix D:")
    print(D)
    print("\nLaplacian matrix L = D - A:")
    print(L)

    # ============================================================================
    # EXAMPLE 2: Making Graph Undirected and Symmetric
    # ============================================================================
    print("\n2. SYMMETRIZING ADJACENCY MATRIX FOR UNDIRECTED GRAPHS")
    print("-" * 70)

    # If the graph is undirected but stored asymmetrically, symmetrize it
    A_asymmetric = np.array([
        [0, 1, 0, 0],
        [0, 0, 1, 1],
        [1, 1, 0, 1],
        [0, 0, 0, 0]
    ], dtype=float)

    print("Asymmetric adjacency matrix:")
    print(A_asymmetric)

    # Make symmetric by averaging: C = (A + A^T) / 2
    C = (A_asymmetric + A_asymmetric.T) / 2
    print("\nSymmetrized: C = (A + A^T) / 2:")
    print(C)

    # ============================================================================
    # EXAMPLE 3: Constructing Normalized Laplacian
    # ============================================================================
    print("\n3. NORMALIZED LAPLACIAN MATRIX")
    print("-" * 70)

    # Normalized Laplacian: Q = D^(-1/2) L D^(-1/2)
    # This normalization is useful for spectral clustering
    n = C.shape[0]
    degrees_C = np.sum(C, axis=0)
    D_C = np.diag(degrees_C)

    # D^(-1/2) is the inverse square root of the degree matrix
    D_inv_sqrt = np.diag(1.0 / np.sqrt(degrees_C + 1e-10))  # add epsilon to avoid division by zero

    # Normalized Laplacian
    L_C = D_C - C
    Q = D_inv_sqrt @ L_C @ D_inv_sqrt

    print("Degrees:", degrees_C)
    print("\nNormalized Laplacian Q = D^(-1/2) L D^(-1/2):")
    print(Q)

    # ============================================================================
    # EXAMPLE 4: Computing Eigenvalues with Sparse Matrices
    # ============================================================================
    print("\n4. EIGENDECOMPOSITION: SPARSE MATRIX VERSION")
    print("-" * 70)

    # Convert to sparse format for efficiency (especially important for large matrices)
    L_sparse = sparse.csr_matrix(L_C)
    Q_sparse = sparse.csr_matrix(Q)

    print("Dense Laplacian shape:", L_C.shape)
    print("Sparse Laplacian non-zero elements:", L_sparse.nnz)

    # For small matrices, use dense eigsh; for large use sparse.linalg.eigsh
    # Dense eigendecomposition (good for comparison and understanding)
    eigvals_dense, eigvecs_dense = linalg.eigh(Q)
    print("\nEigenvalues (full, sorted ascending):")
    print(eigvals_dense)

    # Get eigenvectors sorted by eigenvalue
    idx_sorted = np.argsort(eigvals_dense)
    eigvals_sorted = eigvals_dense[idx_sorted]
    eigvecs_sorted = eigvecs_dense[:, idx_sorted]

    print("\nSmallest 2 eigenvalues:", eigvals_sorted[:2])
    print("Corresponding eigenvectors (first 2):")
    print(eigvecs_sorted[:, :2])

    # ============================================================================
    # EXAMPLE 5: Sparse Eigenvalue Solver - eigsh()
    # ============================================================================
    print("\n5. SPARSE EIGENVALUE SOLVER: scipy.sparse.linalg.eigsh()")
    print("-" * 70)

    # eigsh() finds k smallest (or largest) eigenvalues of sparse symmetric matrix
    # k: number of eigenvalues to compute
    # which='SM' means smallest magnitude
    try:
        # Request k=2 smallest eigenvalues
        eigvals_sparse, eigvecs_sparse = sparse.linalg.eigsh(
            Q_sparse, k=2, which='SM', tol=1e-5
        )
        print("Using sparse.linalg.eigsh() for k=2 smallest eigenvalues:")
        print("Eigenvalues:", eigvals_sparse)
        print("Eigenvector shape:", eigvecs_sparse.shape)
        print("First eigenvector (constant - shows overall structure):")
        print(eigvecs_sparse[:, 0])
        print("\nSecond eigenvector (spectral ordering):")
        print(eigvecs_sparse[:, 1])
    except Exception as e:
        print("Note: eigsh() for very small matrices may raise exception")
        print("Using dense eigendecomposition instead for demonstration")

    # ============================================================================
    # EXAMPLE 6: Spectral Ordering Using Second Eigenvector
    # ============================================================================
    print("\n6. SPECTRAL ORDERING - EMBEDDING NODES IN 1D")
    print("-" * 70)

    # The second eigenvector (Fiedler vector) provides optimal 1D ordering of nodes
    # Nodes are ordered by their coordinate in this eigenvector space
    x_coords = eigvecs_sorted[:, 1]  # second eigenvector
    print("Spectral coordinates (second eigenvector):")
    for i, coord in enumerate(x_coords):
        print(f"  Node {i}: position {coord:.4f}")

    # Sort nodes by this coordinate
    node_order = np.argsort(x_coords)
    print("\nNodes sorted by spectral position:")
    print("  Order:", node_order)
    print("  This ordering can reveal graph structure and community organization")

    # ============================================================================
    # EXAMPLE 7: Pseudoinverse for Solving Linear Systems
    # ============================================================================
    print("\n7. PSEUDOINVERSE: SOLVING ILL-CONDITIONED SYSTEMS")
    print("-" * 70)

    # The Laplacian is singular (rank = n-1 for connected graphs)
    # Its pseudoinverse can solve related problems
    print("Laplacian rank:", np.linalg.matrix_rank(L_C))
    print("Expected rank (n-1):", n - 1)

    # Compute pseudoinverse using linalg.pinv
    L_pinv = linalg.pinv(L_C)
    print("\nPseudoinverse shape:", L_pinv.shape)
    print("L @ L_pinv @ L ≈ L (pseudoinverse property):")
    reconstruction = L_C @ L_pinv @ L_C
    error = np.max(np.abs(reconstruction - L_C))
    print(f"Max reconstruction error: {error:.2e}")

    # ============================================================================
    # EXAMPLE 8: 2D Spectral Embedding Using Multiple Eigenvectors
    # ============================================================================
    print("\n8. SPECTRAL EMBEDDING: EMBEDDING IN 2D SPACE")
    print("-" * 70)

    # Use the 2nd and 3rd eigenvectors for 2D embedding
    x_2d = D_inv_sqrt @ eigvecs_sorted[:, 1]
    y_2d = D_inv_sqrt @ eigvecs_sorted[:, 2]

    print("2D spectral coordinates for each node:")
    for i in range(n):
        print(f"  Node {i}: ({x_2d[i]:7.4f}, {y_2d[i]:7.4f})")

    print("\nThese coordinates reflect graph topology:")
    print("- Similar graph positions map to similar coordinates")
    print("- Can be visualized as a 2D plot")
    print("- Useful for spectral clustering and visualization")

    # ============================================================================
    # EXAMPLE 9: Spectral Properties Analysis
    # ============================================================================
    print("\n9. INTERPRETING SPECTRAL PROPERTIES")
    print("-" * 70)

    # Eigenvalue properties tell us about graph structure
    print("Graph spectral analysis:")
    print(f"  Smallest eigenvalue (λ₁): {eigvals_sorted[0]:.6f}")
    print(f"  Second eigenvalue (λ₂): {eigvals_sorted[1]:.6f}")
    print(f"  Spectral gap (λ₂ - λ₁): {eigvals_sorted[1] - eigvals_sorted[0]:.6f}")

    print("\nInterpretation:")
    print("  λ₁ = 0: Always true for normalized Laplacian")
    print("  λ₂ > 0: Graph is connected")
    print("  Larger λ₂: Graph is more 'well-mixed' (faster mixing)")
    print("  Smaller λ₂: Graph may have community structure")

    # ============================================================================
    # EXAMPLE 10: Large Sparse Graph Example
    # ============================================================================
    print("\n10. PRACTICAL EXAMPLE: LARGER SPARSE GRAPH")
    print("-" * 70)

    # Create a larger sparse graph: ring network
    n_large = 100
    # Ring topology: each node connects to neighbors
    A_ring = np.zeros((n_large, n_large))
    for i in range(n_large):
        A_ring[i, (i-1) % n_large] = 1
        A_ring[i, (i+1) % n_large] = 1

    print(f"Ring graph with {n_large} nodes")
    print(f"Adjacency matrix density: {np.count_nonzero(A_ring) / (n_large**2):.4%}")

    # Convert to sparse
    A_ring_sparse = sparse.csr_matrix(A_ring)

    # Compute Laplacian
    D_ring = sparse.diags(np.array(A_ring_sparse.sum(axis=1)).ravel())
    L_ring = D_ring - A_ring_sparse

    # Compute normalized Laplacian
    D_inv_sqrt_ring = sparse.diags(1.0 / np.sqrt(np.array(D_ring.diagonal())))
    Q_ring = D_inv_sqrt_ring @ L_ring @ D_inv_sqrt_ring

    print(f"Sparse Laplacian non-zeros: {L_ring.nnz}")

    # Use sparse eigsh for efficiency
    try:
        eigvals_ring, eigvecs_ring = sparse.linalg.eigsh(Q_ring, k=3, which='SM')
        print(f"\nSmallest 3 eigenvalues of ring graph: {eigvals_ring}")
        print("Ring graphs have predictable spectral structure (cosine-like)")
    except Exception:
        # Fallback to dense for very small
        eigvals_ring = np.linalg.eigvalsh(Q_ring.todense())
        print(f"Eigenvalues: {eigvals_ring[:3]}")

    print("\n" + "=" * 70)
    print("Key takeaways:")
    print("- Laplacian L = D - A captures graph structure in matrix form")
    print("- Normalized Laplacian: Q = D^(-1/2) L D^(-1/2)")
    print("- Eigenvalues and eigenvectors reveal graph properties")
    print("- eigsh() efficiently solves sparse eigenvalue problems")
    print("- Fiedler vector (2nd eigenvector) provides optimal 1D ordering")
    print("- Spectral methods bridge linear algebra and graph structure")
    print("=" * 70)