Streaming OMP Reference
The shapleyx.utilities.streaming module provides a memory-efficient
regression pipeline for RS-HDMR basis expansions that are too large to
materialise in RAM. Instead of building the full \((n \times p)\) design
matrix, the pipeline stores only the primitive Legendre terms
(\(n \times d\cdot\max(\text{polys})\)) plus compact integer recipes
describing how to combine them into each basis function. Columns are
computed on demand via Numba-compiled kernels.
New method strings that activate the streaming path:
| Method string | Description |
|---|---|
'omp_stream' |
Streaming Orthogonal Matching Pursuit — computes basis columns on the fly, never builds the dense matrix. |
'omp_cv_stream' |
Streaming OMP with K-fold cross-validation and retrospective sparsity selection. Supports n_jobs parallelism per fold. |
These are drop-in replacements for 'omp' and 'omp_cv' — the
downstream API (run_all(), predict(), get_pawnx(), etc.) is
identical.
Core Classes
FeatureRecipes
class FeatureRecipes:
feature_names: list[str] # E.g. "X0_3*X1_2"
n_features: int # Number of candidate basis terms
prim_indices: np.ndarray # (n_features, max_factors) int32
n_factors: np.ndarray # (n_features,) int32
max_factors: int
Compact integer representation of every candidate basis function.
For a term like \(\phi_i(\mathbf{x}) = L_3(x_0) \cdot L_2(x_1)\),
prim_indices[i] stores the flat indices into the primitives array
so that the hot inner loop is prod *= primitives[j, idx].
LazyBasisMatrix
class LazyBasisMatrix:
primitives: np.ndarray # (n_samples, n_prim) float64
recipes: FeatureRecipes
columns: list[str] # column labels
shape: tuple[int, int] # (n_samples, n_features)
n_features: int
n_samples: int
A Legendre basis matrix that never materialises the full design
matrix. The primitives array contains pre-computed shifted Legendre
values for every (variable, degree) pair:
Key methods:
dot_with_residual(residual)— Compute \(X^\top r\) (the correlation of every feature with the residual vector). This is the OMP hot path. For 16K features and 2K samples, a single scan takes ~40 ms (Numba, parallelised viaprange).
active_submatrix(indices) — Build the design submatrix for a small set of features (used by the OMP least-squares step).
column(idx) / column_batch(indices) — Compute individual columns on demand.
StreamingOMP
class StreamingOMP:
lazy_basis: LazyBasisMatrix
n_nonzero_coefs: int # max features to select
fit_intercept: bool # default True (matches sklearn OMP)
tol: float # early stopping tolerance
coef_: np.ndarray # (n_features,) sparse coefficients
intercept_: float # fitted intercept
active_: np.ndarray # indices of selected features
n_nonzero_coefs_: int # actual number selected
n_iter_: int # iterations performed
OMP that operates on a LazyBasisMatrix. At each iteration:
- Scan all features via
dot_with_residualto find the one most correlated with the residual. - Solve the least-squares problem on the (small) active submatrix.
- Update the residual.
When fit_intercept=True (default), the algorithm centres \(y\) for the
correlation scan and solves the augmented problem
\([\,\mathbf{1} \mid X_{\text{active}}\,]\,\mathbf{w} \approx \mathbf{y}\),
matching sklearn's OrthogonalMatchingPursuit.
Methods: fit(y), predict(lazy_basis)
StreamingOMPCV
class StreamingOMPCV:
lazy_basis: LazyBasisMatrix
cv: int # number of CV folds (default 10)
max_iter: int # max non-zero coefs to consider
fit_intercept: bool
scoring: str # 'r2' or 'mse'
random_state: int
n_jobs: int # parallel fold evaluation (default 1)
coef_: np.ndarray
intercept_: float
active_: np.ndarray
n_nonzero_coefs_: int # optimal sparsity level
best_cv_score_: float
cv_scores_: list # [(n_nz, mean_score, std_score), ...]
Cross-validated OMP with retrospective sparsity selection. Evaluates
the full sparsity path (1 … max_iter) via K-fold CV and selects the
level with the best mean score.
Parallel CV: When n_jobs > 1 and joblib is installed, folds are
evaluated in parallel. Each fold computes the full sparsity path
independently; scores are then aggregated. On Linux, the shared
primitives array is accessed read-only across forked workers
(zero-copy). The warm-up JIT compilation is shared via Numba's
disk cache.
Methods: fit(y), predict(lazy_basis)
Memory Scaling
For \(d\) inputs, polys=[8,6,2], and \(n\) samples:
| Component | Formula | Size at \(d{=}20\), \(n{=}10\,000\) |
|---|---|---|
| Primitives | \(d \cdot 8 \cdot n \cdot 8\) B | 12.8 MB |
| Feature recipes | \(\sum \binom{d}{k} \cdot p_k \cdot k \cdot 4\) B | ~1 MB |
| Full design matrix (dense) | \(\sum \binom{d}{k} \cdot p^k \cdot n \cdot 8\) B | ~1.5 GB |
| Active submatrix (sparse) | \(\approx 100 \cdot n \cdot 8\) B | ~8 MB |
The streaming path uses ~22 MB instead of ~1.5 GB — a 70× reduction. The gap widens with higher-dimensional expansions.
Benchmarks
Sobol G-function, \(d{=}20\), polys=[8,6,2], \(n{=}2\,000\), 80 non-zero
coefficients. Numba 0.65.1, Intel i7 (8 cores):
| Method | Time | Memory | R² |
|---|---|---|---|
Dense OMP (method='omp') |
49.5 s | 258 MB | 0.9803 |
Streaming OMP (method='omp_stream') |
4.5 s | 3 MB | 0.9802 |
| Streaming OMP-CV 10-fold, seq | 51.0 s | 3 MB | 0.8271 (CV) |
| Streaming OMP-CV 10-fold, 4 jobs | 35.3 s | 3 MB | 0.8271 (CV) |
Streaming OMP is 4.4× faster than dense OMP — the primitives fit in L3 cache while the dense matrix is RAM-bandwidth-bound. CV parallelisation adds 1.4× on top (or more for larger \(n\)).
How to Use
from shapleyx import rshdmr
# Streaming OMP — drop-in for method='omp'
model = rshdmr(df, polys=[8, 6, 2], method='omp_stream', n_iter=80)
sobol, shapley, total = model.run_all()
# Streaming OMP with CV and 4-fold-parallel evaluation
model = rshdmr(df, polys=[8, 6, 2], method='omp_cv_stream',
n_iter=100, n_jobs=4)
sobol, shapley, total = model.run_all()