Why Sparse Matrices
Sparse matrices efficiently represent matrices with mostly zero entries.
Memory Efficiency
1. Dense vs Sparse Storage
import numpy as np
from scipy import sparse
import sys
def main():
n = 10000
density = 0.001 # 0.1% nonzero
# Dense storage
A_dense = np.zeros((n, n))
k = int(n * n * density)
rows = np.random.randint(0, n, k)
cols = np.random.randint(0, n, k)
A_dense[rows, cols] = np.random.randn(k)
# Sparse storage
A_sparse = sparse.csr_matrix(A_dense)
dense_size = A_dense.nbytes
sparse_size = A_sparse.data.nbytes + A_sparse.indices.nbytes + A_sparse.indptr.nbytes
print(f"Matrix size: {n} x {n}")
print(f"Nonzeros: {k} ({density*100}%)")
print(f"Dense storage: {dense_size / 1e6:.1f} MB")
print(f"Sparse storage: {sparse_size / 1e6:.3f} MB")
print(f"Compression: {dense_size / sparse_size:.0f}x")
if __name__ == "__main__":
main()
2. Complexity Analysis
| Storage |
Memory |
| Dense |
\(O(mn)\) |
| Sparse |
\(O(k)\) where \(k\) = nonzeros |
3. When Sparse Wins
import numpy as np
from scipy import sparse
def main():
n = 1000
for density in [0.001, 0.01, 0.1, 0.5]:
k = int(n * n * density)
dense_mem = n * n * 8 # 8 bytes per float64
sparse_mem = k * 8 + k * 4 + (n + 1) * 4 # data + indices + indptr
ratio = dense_mem / sparse_mem
winner = "Sparse" if ratio > 1 else "Dense"
print(f"Density {density*100:5.1f}%: {winner} wins ({ratio:.1f}x)")
if __name__ == "__main__":
main()
Computational Efficiency
1. Matrix-Vector Multiply
import numpy as np
from scipy import sparse
import time
def main():
n = 5000
density = 0.01
# Create sparse matrix
A_sparse = sparse.random(n, n, density=density, format='csr')
A_dense = A_sparse.toarray()
x = np.random.randn(n)
# Dense multiplication
start = time.perf_counter()
for _ in range(10):
y_dense = A_dense @ x
dense_time = time.perf_counter() - start
# Sparse multiplication
start = time.perf_counter()
for _ in range(10):
y_sparse = A_sparse @ x
sparse_time = time.perf_counter() - start
print(f"Dense: {dense_time:.4f} sec")
print(f"Sparse: {sparse_time:.4f} sec")
print(f"Speedup: {dense_time/sparse_time:.1f}x")
if __name__ == "__main__":
main()
2. Operation Complexity
| Operation |
Dense |
Sparse |
| \(Ax\) (matrix-vector) |
\(O(n^2)\) |
\(O(k)\) |
| \(A + B\) |
\(O(n^2)\) |
\(O(k_A + k_B)\) |
| Memory |
\(O(n^2)\) |
\(O(k)\) |
Where Sparse Matrices Arise
1. PDEs (Finite Differences)
import numpy as np
from scipy import sparse
def main():
# 1D Laplacian: tridiagonal
n = 100
A = sparse.diags([-1, 2, -1], [-1, 0, 1], shape=(n, n))
print(f"1D Laplacian ({n}x{n})")
print(f"Nonzeros: {A.nnz}")
print(f"Density: {A.nnz / (n*n) * 100:.2f}%")
if __name__ == "__main__":
main()
2. Graphs (Adjacency)
import numpy as np
from scipy import sparse
def main():
# Social network: each person has ~150 connections
n_people = 1_000_000
avg_connections = 150
# Theoretical storage
dense_gb = n_people * n_people * 8 / 1e9
sparse_gb = n_people * avg_connections * 12 / 1e9 # data + indices
print(f"Network: {n_people:,} people, {avg_connections} avg connections")
print(f"Dense storage: {dense_gb:,.0f} GB (impossible)")
print(f"Sparse storage: {sparse_gb:.2f} GB (feasible)")
if __name__ == "__main__":
main()
3. Machine Learning (Features)
import numpy as np
from scipy import sparse
def main():
# Text classification: bag of words
n_documents = 100_000
vocabulary = 50_000
avg_words_per_doc = 200
# Feature matrix
dense_gb = n_documents * vocabulary * 8 / 1e9
sparse_gb = n_documents * avg_words_per_doc * 12 / 1e9
print(f"Documents: {n_documents:,}")
print(f"Vocabulary: {vocabulary:,}")
print(f"Dense: {dense_gb:.1f} GB")
print(f"Sparse: {sparse_gb:.2f} GB")
if __name__ == "__main__":
main()
Summary
1. Use Sparse When
- Most entries are zero (density < 10%)
- Matrix is too large for dense storage
- Structure enables sparse operations
2. Use Dense When
- Matrix is small
- High density (> 30%)
- Need all dense operations