Extending fastcompare¶
fastcompare is extended by subclassing a base class. Put your class either directly in
server/plugins/fastcompare/algo/*.py or in a separate plugin folder
server/plugins/<yourplugin>/*.py — EasyStudy discovers subclasses automatically by walking the
plugins/ tree. All base classes live in
server/plugins/fastcompare/algo/algorithm_base.py.
Every component implements name() (a unique display name) and parameters() (fields the researcher
can set in the study-creation UI). Parameters are declared with Parameter(...) and become clickable
inputs automatically.
Optional dependencies
If your component needs a heavy library (TensorFlow, LensKit, …), import it lazily / behind
try/except and declare it as an extra in pyproject.toml. Discovery skips modules whose
optional dependency is missing, so this keeps the lightweight core runnable.
Algorithms¶
Subclass AlgorithmBase and implement fit(), predict(selected_items, filter_out_items, k),
name(), parameters(). Worked example — the built-in NumPy EASE (no heavy deps):
import numpy as np
import pandas as pd
from plugins.fastcompare.algo.algorithm_base import AlgorithmBase, Parameter, ParameterType
class EASE(AlgorithmBase):
def __init__(self, loader, positive_threshold, l2, **kwargs):
self._loader = loader
self._all_items = loader.ratings_df.item.unique()
self._rating_matrix = (
loader.ratings_df.pivot(index="user", columns="item", values="rating")
.fillna(0).values
)
self._threshold = positive_threshold
self._l2 = l2
self._items_count = self._rating_matrix.shape[1]
self._weights = None
def fit(self): # one-time training
X = np.where(self._rating_matrix >= self._threshold, 1, 0).astype(np.float32)
G = X.T @ X + self._l2 * np.identity(self._items_count, dtype=np.float32)
P = np.linalg.inv(G)
B = P / (-np.diag(P))
np.fill_diagonal(B, 0.0)
self._weights = B
def predict(self, selected_items, filter_out_items, k): # per-participant recommendation
candidates = np.setdiff1d(self._all_items, list(selected_items))
candidates = np.setdiff1d(candidates, filter_out_items)
if not selected_items:
return np.random.choice(candidates, size=k, replace=False).tolist()
user_vector = np.zeros((self._items_count,))
for i in selected_items:
user_vector[i] = 1.0
preds = np.tensordot(user_vector.astype(np.float32), self._weights, axes=1)
return [c for _, c in sorted(((preds[c], c) for c in candidates), reverse=True)][:k]
@classmethod
def name(cls):
return "EASE"
@classmethod
def parameters(cls):
return [
Parameter("l2", ParameterType.FLOAT, 500, help="L2-norm regularization"),
Parameter("positive_threshold", ParameterType.FLOAT, 2.5,
help="Threshold to binarize ratings into positive/negative."),
]
predict receives the participant's selected_items, items to filter_out (already seen), and k
(how many to return). It must return k item ids, none of them filtered out.
Existing wrappers (plugins/fastcompare/algo/wrappers/) show how to wrap LensKit and TF-Recommenders
models — a good reference if your algorithm needs an extra. plugins/recbole/algorithms.py is a good
reference for wrapping a family of models: it declares one AlgorithmBase subclass per RecBole
model (BPR, LightGCN, NGCF, NeuMF, DMF) so each exposes only its own hyperparameters in the UI, sharing
training/prediction logic via a plain (non-AlgorithmBase) mixin class that discovery ignores.
Datasets¶
Subclass DataLoaderBase. In addition to name()/parameters(), implement load_data() and the
accessors EasyStudy uses to render items and compute metrics:
- Data frames / matrices:
ratings_df,items_df,items_df_indexed,rating_matrix,distance_matrix. - Item lookups:
get_item_index(item_id),get_item_id(item_index),get_item_index_description(...)/get_item_id_description(...),get_item_index_categories(...),get_all_categories(). - Images:
get_item_id_image_url(item_id)/get_item_index_image_url(item_index).
Get inspired by GoodbooksDataLoader / MLDataLoaderWrapper in plugins/utils/.
For images, return a static URL:
from flask import url_for
url_for('static', filename=f'datasets/goodbooks-10k/img/{item_id}.jpg')
server/static/datasets/.
If your deployment only lets you write inside plugins/, ship assets in your plugin and address them
with pm.emit_assets('yourPluginName', filename) (import pm from app, and check
has_app_context() first). Returning remote http://… URLs works but slows the study down.
Preference elicitation¶
Subclass PreferenceElicitationBase (implement fit(), the elicitation logic, name(),
parameters()). Popularity sampling (plugins/utils/popularity_sampling.py) is a dependency-free
reference; the TFRS-based method requires the tensorflow extra.
Metrics¶
Subclass EvaluationMetricBase and implement evaluate(shown_items, selected_items) + name(). The
built-ins are nDCG@K, Precision@K, ILD, and Selection Count — new metrics appear automatically in the
study Results view.
Testing your component¶
Add a test under server/plugins/fastcompare/tests/ (or your plugin). See
test_algorithm.py for the fixture pattern (it skips cleanly if the dataset isn't downloaded). Run:
cd server && pytest -q