SLIM (Sparse Linear Methods, Ning &
Karypis 2011) learns a sparse, non-negative item-item weight matrix W from a
user-item matrix X and scores users with one sparse product, X @ W. Every
recommendation is explainable: "because you interacted with these items".
fastslim is a Rust implementation behind a small Python API. The solver is exact
(the returned weights satisfy the optimality conditions of the objective below),
deterministic (bit-identical output for any thread count) and fast: MovieLens 1M fits
in about a second.
pip install fastslim # or: uv add fastslimWheels for Linux (x86_64, aarch64), macOS and Windows, Python 3.10 and newer. Runtime dependencies are NumPy and SciPy.
import numpy as np
from scipy import sparse
import fastslim
rng = np.random.default_rng(0)
X = sparse.csr_matrix((rng.random((2000, 300)) < 0.03).astype(np.float64)) # users x items
W = fastslim.fit(X, lambd=2.0, beta=2.0) # items x items, sparse, zero diagonal
top = fastslim.recommend(W, X[:5], k=10) # top-10 unseen items for five users
scores = fastslim.predict(W, X[0]) # raw scores for one userThe same solver as an estimator:
from fastslim import SLIM
model = SLIM(lambd=2.0, beta=2.0).fit(X)
model.recommend(X[0], k=5)
model.weights, model.n_passes, model.converged # fitted statefit takes any SciPy sparse matrix or array, or a dense array; values must be finite
and non-negative. fastslim.metrics provides precision_at_k, recall_at_k and
ndcg_at_k against a held-out sparse matrix. The full reference is in
docs/api.md.
lambd, the L1 penalty, is compared against co-occurrence counts: itemkcan enter itemi's model only if(X.T @ X)[i, k] >= lambd. On MovieLens-sized data useful values are single digits. Raise it for a sparserW.beta, the L2 penalty, shrinks weights and conditions the problem; anybeta > 0makes the solution unique.max_iterandtolstop each item once a full pass moves no weight bytol. The defaults (1000,1e-4) converge all of MovieLens 1M. Items that run out of budget raisefastslim.ConvergenceWarning;SLIM.convergedtells which ones.n_threadschanges speed only. The result does not depend on it.
MovieLens, 80/20 split, k=10, against implicit's ALS with 64 factors. SLIM uses
lambd=2, beta=2, max_iter=50.
| Dataset | Model | Fit time | precision@10 | recall@10 | ndcg@10 |
|---|---|---|---|---|---|
| ML-100k | fastslim | 0.17 s | 0.3391 | 0.2236 | 0.4069 |
| ML-100k | implicit ALS | 0.58 s | 0.2878 | 0.2017 | 0.3433 |
| ML-1M | fastslim | 2.55 s | 0.3620 | 0.1652 | 0.4100 |
| ML-1M | implicit ALS | 4.45 s | 0.3465 | 0.1621 | 0.3915 |
Reproduce with uv sync --group bench and
uv run python benchmarks/benchmark.py --dataset 1m --baseline als --markdown.
For every item i independently, the solver minimises
0.5 * ||x_i - X w||^2 + lambd * sum_k w_k + (beta / 2) * sum_k w_k^2
subject to w >= 0, w_i = 0
by non-negative coordinate descent on the Gram matrix P = X.T @ X: only neighbours
with P_ik >= lambd can be non-zero, residuals are updated incrementally, and an active
set is iterated until a full verification pass confirms optimality. P is accumulated
in a fixed order and each item is solved by a single thread, which is what makes the
output deterministic. The derivation is in
docs/algorithm.md.
uv sync # builds the extension, installs dev tools
uv run pytest -q -m "not slow" # Python tests
cargo test # Rust testsCONTRIBUTING.md covers
layout, linting, benchmarks and releases;
CHANGELOG.md lists what
changed between versions.
Apache-2.0. See LICENSE.