Metadata-Version: 2.4
Name: calissta
Version: 0.1.0
Summary: Calibrated recommendations via sparse-autoencoder steering and top-k (D'Hondt) aggregation.
Project-URL: Homepage, https://github.com/pdokoupil/CALISSTA
Project-URL: Repository, https://github.com/pdokoupil/CALISSTA
Project-URL: Paper, https://doi.org/10.1145/3774935.3806154
Project-URL: Reproducibility, https://osf.io/8en5u
Author: Patrik Dokoupil
License: MIT
License-File: LICENSE
Keywords: beyond-accuracy,calibration,diversity,interpretability,recommender-systems,sparse-autoencoders
Classifier: Development Status :: 3 - Alpha
Classifier: Intended Audience :: Science/Research
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
Requires-Python: >=3.9
Requires-Dist: numpy>=1.21
Provides-Extra: dev
Requires-Dist: pytest>=7; extra == 'dev'
Provides-Extra: examples
Requires-Dist: huggingface-hub>=0.20; extra == 'examples'
Requires-Dist: pandas>=1.3; extra == 'examples'
Requires-Dist: polars>=0.20; extra == 'examples'
Requires-Dist: scipy>=1.7; extra == 'examples'
Requires-Dist: torch>=1.13; extra == 'examples'
Provides-Extra: torch
Requires-Dist: huggingface-hub>=0.20; extra == 'torch'
Requires-Dist: torch>=1.13; extra == 'torch'
Description-Content-Type: text/markdown

<div align="center">

# CALISSTA

**Calibrated recommendations, in the retrieval phase.** Make a recommender's output
match a target mix of concepts (genres, tags, categories) — *60% drama, 30% comedy,
10% action* — by **steering a sparse autoencoder**, not by expensive post-hoc
re-ranking.

<!-- Badges light up once the repo is pushed and the package is published to PyPI. -->
[![PyPI](https://img.shields.io/pypi/v/calissta.svg)](https://pypi.org/project/calissta/)
[![CI](https://github.com/pdokoupil/CALISSTA/actions/workflows/ci.yml/badge.svg)](https://github.com/pdokoupil/CALISSTA/actions/workflows/ci.yml)
[![License: MIT](https://img.shields.io/badge/License-MIT-blue.svg)](LICENSE)
[![Paper](https://img.shields.io/badge/paper-UMAP'26-b31b1b.svg)](https://doi.org/10.1145/3774935.3806154)
[![OSF](https://img.shields.io/badge/OSF-reproducibility-blue.svg)](https://osf.io/8en5u)

</div>

Most calibration methods re-rank a large candidate pool after the fact — accurate
but slow. `calissta` calibrates **during candidate retrieval**: it puts a sparse
autoencoder (SAE) on your recommendation backbone, *steers* the user's sparse
embedding toward each target concept (e.g. genre), decodes one candidate list per concept, and
interleaves them with **D'Hondt** proportional allocation. In the paper this was
~an order of magnitude faster than re-ranking calibration, at comparable or better
calibration and relevance.

> **Algorithm:** Patrik Dokoupil, Ludovico Boratto & Ladislav Peska (UMAP '26, see
> [Citing](#citing)). **This implementation:** Patrik Dokoupil.
> Full paper reproduction (training, all datasets, ablations) lives on
> [OSF](https://osf.io/8en5u); this package is the streamlined, install-able library.

## Install

```bash
pip install calissta                 # core — NumPy only, bring your own backbone/SAE
pip install "calissta[torch]"        # + reference ELSA backbone & SAE, from_pretrained
pip install "calissta[examples]"     # + everything to run the ML25M example (polars, …)
```

| Extra | Adds | For |
|---|---|---|
| *(none)* | NumPy | the algorithm with your own models |
| `[torch]` | torch, huggingface_hub | reference ELSA/SAE, `from_pretrained` |
| `[examples]` | + polars, pandas, scipy | running the ML25M example & retraining scripts |

## Zero-setup on MovieLens-25M

With `calissta[torch]` and a published bundle (checkpoints + `concepts.npz`), one call
gets you a ready calibrator:

```python
from calissta import CalisstaCalibrator

cal = CalisstaCalibrator.from_pretrained("path/to/bundle")   # local dir or a HF repo id
recs = cal.recommend(user_history, target_distribution=[0.6, 0.3, 0.1], alpha=0.5, k=10)
```

(Build a bundle from your own trained models with `examples/build_ml25m_bundle.py`.)

## Quick start

You bring three things: a **backbone** (any recommender that encodes a user to a
dense embedding and decodes back to item scores), an **SAE** on top of it, and your
dataset's **concepts**. Then ask for a calibrated list:

```python
from calissta import CalisstaCalibrator, Concepts
from calissta.steering import fit_steering_vectors

# concepts: which items belong to which genre/tag/category, + a steering direction each
s_g = fit_steering_vectors(item_sparse_embeddings, item_concept_matrix)   # (n_concepts, sae_dim)
concepts = Concepts(concept_names, item_concept_matrix, s_g)

cal = CalisstaCalibrator(backbone, sae, concepts)
recs = cal.recommend(
    user_history,                      # item indices the user interacted with
    target_distribution=[0.6, 0.3, 0.1],
    alpha=0.5,                         # calibration strength in [0, 1]
    k=10,
)
```

`python examples/quickstart.py` runs the whole pipeline on toy identity models
(pure NumPy, <1 s) and shows the recommended list's concept mix tracking the target:

```
target D_u          ->  achieved concept mix over the top-10
  [0.6, 0.3, 0.1]   ->  [0.6 0.3 0.1]
```

## Bring your own model (it's model-agnostic)

The backbone and SAE are just two tiny protocols
([`calissta.interfaces`](src/calissta/interfaces.py)) — encode/decode. The paper
uses **ELSA** + an **SAE**, but anything works (matrix factorization, a neural
recommender, …) as long as small embedding perturbations don't wreck its output.
There is **no hard PyTorch dependency**; the reference ELSA/SAE ship in the
`[torch]` extra, everything else is NumPy.

```python
class Backbone:            # yours
    def encode(self, history): ...   # -> dense user embedding
    def decode(self, dense):   ...   # -> scores over all items

class SparseAutoencoder:   # yours
    def encode(self, dense):          ...   # -> (sparse, context)
    def decode(self, sparse, context): ...  # -> dense
```

## Reference models (running on real data)

Don't want to wire up your own? `calissta[torch]` ships clean
implementations so you can run the whole thing:

```python
from calissta.models import ELSA, ELSABackbone, TopKSAE, SAEAdapter, item_sparse_embeddings_ub

backbone = ELSABackbone(trained_elsa)       # ELSA scoring incl. ReLU(recon − interactions)
sae      = SAEAdapter(trained_topk_sae)
s_g      = fit_steering_vectors(item_sparse_embeddings_ub(trained_elsa, trained_sae), M)  # UB source
```

`examples/train_and_calibrate.py` trains ELSA + a Top-k SAE on synthetic data and
calibrates end-to-end in a few seconds.

> **Attribution.** Neither model is CALISSTA's contribution — please cite them if you
> use them. **ELSA:** Vančura et al., *Scalable Linear Shallow Autoencoder for
> Collaborative Filtering*, RecSys 2022. **SAE-for-CF:** Spišák et al., *From Knots to
> Knobs: Towards Steerable Collaborative Filtering Using Sparse Autoencoders*
> (arXiv:2601.11182, 2026), building on the k-sparse autoencoder (Makhzani & Frey 2013;
> Gao et al. 2024).

## Evaluation & the ML25M example

`calissta.metrics` provides the paper's metrics: **`genre_exposure_error`** (GEE — the
calibration metric: MAE between target and achieved concept exposure) and
**`ndcg_at_k`** (relevance). Both are pure NumPy and take a produced recommendation
list.

`examples/genre_calibration_ml25m.py` *demonstrates* the calibration/relevance
trade-off on MovieLens-25M with the paper's checkpoints — Genre Exposure Error falls as
calibration strength rises, at a relevance cost (editorial scenario, illustrative
300-user sample; numbers vary with the sample):

| alpha | GEE ↓ | nDCG@10 |
|---|---|---|
| 0.0 | 0.27 | 0.34 |
| 0.6 | 0.19 | 0.28 |
| 0.9 | 0.17 | 0.26 |

This *mimics* the experiment; it is **not** an exact reproduction (different sample /
seed / split). For the paper's exact numbers and full reproduction use
[OSF](https://osf.io/8en5u). Get the models from OSF **or** retrain with the paper's
config (`examples/train_elsa.py`, `examples/train_sae.py`); `load_elsa`/`load_sae`
read both. See [`examples/README.md`](examples/README.md) for the runbook.

## Any dataset (it's concept-agnostic)

Calibration concepts are described entirely by the `Concepts` object: an
`item_matrix` `M` (`n_concepts × n_items`, which items are in which concept) and a
steering vector per concept. Genres for movies, tags for music, categories for
products — same code, different `M`.

`fit_steering_vectors(item_sparse_embeddings, M)` builds each concept's **contrastive**
direction — `normalize(mean(items in concept) − mean(items not in concept))` — and
orthogonalizes them. The item sparse
embeddings can be **user-based (UB)** — ELSA item factors projected through the SAE
encoder (what the paper used) — or **item-based (IB)** — one-hot items pushed through
ELSA→SAE; you compute whichever and pass it in.

## What you can tune

- **`alpha`** — global calibration strength in `[0, 1]`.
- **`alpha_mapping`** — how `alpha` becomes per-concept: `"discounted"` (default;
  the paper's correction that steers rare concepts more and dominant ones less),
  `"fixed"`, `"proportional"`, `"multiplied"`.
- **`steer_mode`** — `"direction"` (default; `e + α_g·‖e‖·s_g`, the
  `direction_orthogonalized` setting the paper's experiments used) or `"convex"`
  (`(1-α)e + α·s_g`, Algorithm 1's pseudocode form).
- **`n_candidates`**, **`k`**, **`exclude_seen`**.

## How it works

Steering happens in the SAE's **sparse, largely monosemantic** space, so pushing
toward a concept is a clean, local edit. Each concept yields its own candidate
list; D'Hondt then fills `k` seats so the concept composition matches `D_u`. See
the [paper](https://doi.org/10.1145/3774935.3806154) for the algorithm, the
history-aware `alpha` correction, complexity analysis, and experiments.

## Citing

If you use this software, please cite the paper (GitHub's "Cite this repository"
reads [`CITATION.cff`](CITATION.cff)):

```bibtex
@inproceedings{dokoupil2026calissta,
  author    = {Dokoupil, Patrik and Boratto, Ludovico and Peska, Ladislav},
  title     = {CALISSTA: Calibrated Candidate Retrieval Using Sparse Autoencoders and Top-k Aggregation},
  booktitle = {Proceedings of the 34th ACM Conference on User Modeling, Adaptation and Personalization},
  series    = {UMAP '26}, pages = {345--350}, year = {2026},
  doi       = {10.1145/3774935.3806154}
}
```

## License & attribution

The **`calissta` package is MIT-licensed** — see [`LICENSE`](LICENSE). That covers our
code: the calibrator, steering, D'Hondt aggregation, metrics, and the reference-model
code under `calissta.models`.

The `examples/` and reference models **build on others' methods and data** — our code is
an original (MIT) reimplementation, but please credit the sources:

- **ELSA** backbone — Vančura et al., *Scalable Linear Shallow Autoencoder for
  Collaborative Filtering*, RecSys 2022.
- **SAE-for-CF** — Spišák et al., *From Knots to Knobs: Towards Steerable Collaborative
  Filtering Using Sparse Autoencoders*, arXiv:2601.11182 (2026); k-sparse SAE: Makhzani &
  Frey 2013, Gao et al. 2024.
- **MovieLens-25M** (genre-calibration example) — GroupLens; **not redistributed** here (the
  download script fetches it from GroupLens under their license).

Reproducibility artifacts (trained checkpoints, full ablations) are on
[OSF](https://osf.io/8en5u).
