Changelog

0.1.0

  • Project skeleton: packaging, typed skrecsys package and the PyPI release workflow.

0.2.0

  • First working release: scikit-learn-style recommenders with fit, partial_fit, recommend and predict, backed by Rust kernels exposed through PyO3 as skrecsys._core.
  • skrecsys.recommendation: MostPopularRecommender, ItemKNNRecommender, AlternatingLeastSquares, BM25Recommender, EASE and RP3Beta.
  • skrecsys.metrics: ranking metrics (NDCG, precision, recall, hit rate, MAP, MRR) and beyond-accuracy metrics (catalog and user coverage, mean popularity, novelty), plus make_recommender_scorer for scikit-learn model selection.
  • skrecsys.model_selection.WarmStartKFold and the fetch_movielens_100k dataset loader.
  • MovieLens 100K leaderboard, checked against reference implementations (implicit, RecTools, libFM and the Dacrema et al. baselines).
  • Test CI and pre-commit hooks; macOS x86_64 dropped from the release targets.

0.3.0

  • New estimators: SLIMElasticNet and BayesianPersonalizedRanking.
  • Faster fit and recommend: identifier encoding, similarity pruning and top-k ranking moved to Rust, and neighbourhood models rank a batch of queries in one kernel without building a dense score matrix.
  • Profile-guided optimization build script (scripts/pgo.py).
  • Leaderboard reports fit and ranking timings separately, with sample counts and quantiles.

0.4.0

  • skrecsys.nn: neural recommenders HSTU, Mamba4Rec, SimpleX and XSimGCL, behind the opt-in nn extra. Fitted models unpickle and score without torch.
  • skrecsys.indexing: HNSW and QuantizedFlatIndex vector indexes with an index registry (make_index, register_index), usable by the factor and neural models.
  • Incremental fitting reworked: partial_fit grows the user and item vocabularies and updates the model from the batch instead of refitting on the full history.
  • New datasets: fetch_movielens_1m and fetch_amazon_books.
  • Core refactored into a separate rust/kernels crate with Criterion benchmarks.
  • Benchmark suite (benchmarks/run.py) with configs for the leaderboard, sequential and index benchmarks; the README is now rendered from README.md.j2.
  • Documentation of the production lifecycle (fit, partial_fit, serving modes).

0.5.0

  • skrecsys.compose: Switch and Cascade composites, built from conditions (KnownUser, MinInteractions, QueryIn, AllOf, AnyOf, Not), feature components and rankers (PointwiseRanker, GroupRanker, BlendRanker).
  • Cascade takes a list of candidate generators: their candidates are interleaved by rank, deduplicated and capped at n_retrieved per query, and every generator scores every candidate, so GeneratorScores(n_generators=...) gives the ranker one feature per generator.
  • Reciprocal rank fusion, which needs no training: ReciprocalRankFusion fuses the lists of several recommenders and stands wherever a recommender does, and ReciprocalRankRanker is a Cascade ranker fusing feature columns or other rankers by rank.
  • skrecsys.integrations: CatBoostRanker, LGBMRanker and XGBRanker for Cascade, each behind its own opt-in extra.
  • skrecsys.tune: TPE hyperparameter tuner with a Rust sampler; estimators declare their search space as annotations on __init__, and AutoTune tunes a recommender inside fit.
  • skrecsys.model_selection.ColdStartSplit for training cold-start rankers.
  • Shared parameter validation: estimators check their parameters at fit time with consistent error messages.
  • Reranking benchmark comparing Switch pipelines with and without a second-stage ranker, on MovieLens 100K and 1M split by ColdStartSplit.
  • Candidate generation benchmark: candidate recall of single and merged generators at several budgets, against the best member of each merge, and the cost of one request.
  • README split into per-topic templates under docs/.
  • skrecsys.metrics.evaluate_recommender evaluates several metrics at several cutoffs from one recommend call, and make_recommender_scorer takes lists of metrics and cutoffs to make a scikit-learn multi-metric scorer.
  • JoinDynamicFeatures takes a tuple of kinds, such as kind=("user", "item"), to compute pair features: the callback gets the distinct combinations of those columns.
  • Cascade(postprocess=...): business rules on top of the ranker. A callback receives every candidate of each query, best first, and returns the lists to serve, which it may reorder, shorten or extend with items that were not candidates. It applies in recommend only, so the ranker trains without it while evaluation and tuning measure it.
  • recommend(exclude_interactions=...) accepts an empty array, such as a request with no events since the fit, and excludes nothing instead of raising.

0.6.0

  • skrecsys.inspection: trace() records every stage of the recommend calls made inside a with block -- requests, Switch routes, each generator's candidates and the merged list, fusion contributions, ranker features and scores, postprocess changes and served lists -- and gives them back per query as a QueryTrace of typed steps, each named by its path in the pipeline. The trace is not stored on the estimator, so results, cloning and pickling are unchanged, and an untraced call costs one context-variable lookup. level="decisions" and a sample= fraction chosen by a process-stable hash of the query keep it cheap enough to leave on; every step has a JSON-ready to_dict().
  • explain() says why each served item was recommended, and, for items= that were not, the stage that let each go: unknown_item, excluded (naming the filter), not_retrieved, ranked_out or dropped_by_postprocess.
  • Reasons behind a leaf's score: exact per-item terms of the history for ItemKNNRecommender, BM25Recommender, RP3Beta, SLIMElasticNet and EASE; the most alike history items, approximately, for the factor and neural models; popularity for MostPopularRecommender.
  • Rankers report each feature's contribution to a score: coefficient × feature for a linear PointwiseRanker, and native SHAP values for CatBoostRanker, XGBRanker and LGBMRanker.
  • Notebook 07_inspecting_recommendations.py.
  • AugmentedRanker injects features that only one ranker sees, such as a JoinStaticFeatures or JoinDynamicFeatures, so that the rankers of a BlendRanker or a ReciprocalRankRanker can learn from different features. The Cascade fits the injected features the same way as its own and hands the candidate pairs to rankers that ask for them. A trace records the injected columns among the ranker's features and contributions, and traces each ranker of a blend or fusion at its own path, Cascade/ranker/<name>.
  • AutoTune(scoring=...) also takes a metric name such as "recall@20" or a metric such as Recall(20); skrecsys.metrics adds NDCG, Recall, Precision, MAP, MRR and HitRate, each a scorer at its cutoff, and get_scorer to resolve any of these.
  • LGBMRanker, XGBRanker and CatBoostRanker declare search ranges, so AutoTune tunes them as ranker__<name> of a Cascade, and expose the usual regularization and sampling parameters (reg_alpha, subsample, colsample_bytree, gamma, random_strength, lambdarank_pair_method and more). extra_params passes any other library parameter. XGBRanker's learning_rate, max_depth, min_child_weight and reg_lambda default to XGBoost's own values rather than None, which fits the same model.
  • LGBMRanker and XGBRanker train through the libraries' core APIs (lightgbm.train, xgboost.train) rather than their scikit-learn wrappers, as CatBoostRanker already did, so they work with any release of the library whatever scikit-learn is installed; LightGBM 4.5 no longer fails against scikit-learn 1.8+. model_ is now a lightgbm.Booster or an xgboost.Booster.
  • The integration extras' floors are each library's release from two years back: catboost>=1.2.8 (1.2.7 needs numpy<2), xgboost>=2.1.1 and lightgbm>=4.5. CI tests the integrations at those floors and at the latest releases.

0.7.0

  • Query context: columns after the system ones in X are the context of each interaction, [user, item, context...] or [user, item, time, context...] with time=True, such as the shelf, device or search a request came from. recommend(X) takes a vector of users, as before, or a matrix [user, context...] with one row per query. Every recommender accepts context and ignores what it cannot use; Switch passes it on to its branches.
  • Cascade hands the context to its features and ranker, beside the candidate pairs. Its generators still retrieve by user. It trains the ranker with the context of each held-out user's first held-out interaction, fills a query asked without context with NaN, and checks the width against n_context_. predict reads context columns too.
  • JoinDynamicFeatures takes the key part "context": "context" passes the context rows to the callback, and "item-context" computes features of a candidate and its request together, for context of any type.
  • Breaking: the feature component contract is now transform(pairs, *, scores=None, context=None). Cascade, ConcatFeatures and AugmentedRanker always pass context=, None when there is none, so a custom component must take the keyword, ignoring it if it has no use for it. Every built-in component does. The Candidates a ranker receives carry the context too.
  • evaluate_recommender and make_recommender_scorer ask each user with the context of their first held-out row. WarmStartKFold and ColdStartSplit split an X with context columns as they split it without. trace and explain take queries with context.
  • skrecsys.utils.validation adds check_queries, interaction_context and stack_columns.
  • Behaviour change: without time=True, a third column of X is context, no longer an error. check_interactions accepts any number of columns after the system ones, and check_optionally_timed is no longer used where context may be present, since it cannot tell time from context.
  • Identifiers keep their type next to time and context columns. A list or a DataFrame that mixes integer identifiers with float or datetime columns no longer turns the identifiers into floats, which merged identifiers past 253. A float array whose identifiers are whole numbers gives them back as integers, and one whose identifiers are past 253 is rejected.
  • Times keep their nanoseconds. A pandas.Timestamp was read to the microsecond, and a time beside identifiers of another type was stored as a datetime. Breaking: a JoinDynamicFeatures callback keyed by time now gets such times as datetime64 scalars, NaT where missing, instead of datetime objects and None.
  • Notebook 08, 08_query_context.py: query context on MovieLens 100K.

0.8.0

  • Lists put together from several recommenders, in skrecsys.compose. Backfill fills each list from its first recommender and tops it up from the next, so a list is as personal as the data allows and full for every user; skip_insufficient=True leaves out a recommender that cannot be fitted yet. ReservedSlots gives the last n_slots of the first head positions to another recommender's items, for exploration. ItemListRecommender recommends from a fixed list, which may hold items no interaction names, serves unknown users and fits on no interactions; rotate=True starts each user at a place of their own, as a function of the user and random_state, with no state changed by recommend.
  • MostPopularRecommender(exposure=..., smoothing=...) scores an item by its popularity per exposure, popularity / (exposure + smoothing), from a table of [item, exposure]. The new item_counts_ holds the raw counts, partial_fit stays exact, and it reads exposure again on every call.
  • skrecsys.model_selection.LatestInteractionsSplit(test_size, max_users=...) holds out the latest rows of every user, or of the most recently active max_users only, which bounds the groups a Cascade trains its ranker on.
  • ProfileAffinity compares each candidate's attributes with those of the items its user interacted with: their share of the user's history, and the cosine. It and InteractionCounts take interactions=, a second kind of event to read instead of X, such as dislikes.
  • skrecsys.exceptions.InsufficientDataError, a ValueError, is what Cascade.fit raises when its ranker has nothing to learn from, and what any recommender raises for interactions without rows, in place of scikit-learn's message about 0 samples. A wrong argument never raises it.
  • skrecsys.utils.validation.stable_unit_hash takes a salt.

0.9.0

  • skrecsys.typing is public: the protocols that describe estimators and composite components (Recommender, FittedRecommender, Ranker, Features, Condition, ...), RandomStateLike, RankingMetric, clone_as and the override and TypeIs backports. It replaces the private skrecsys._typing.
  • skrecsys.inspection exports the Level, Sample and Status types.
  • skrecsys.integrations.batching: MicroBatcher and AsyncRecommender rank the requests of an asyncio service that are in flight together in one recommend call. No extra needed.
  • benchmarks/batching.py and the Rust load generator benchmarks/loadgen measure when micro-batching pays off: one model served with and without it under rising open-loop load.