Skip to content

Commit

Permalink
Merge pull request #576 from lenskit/feature/common-tests
Browse files Browse the repository at this point in the history
Implement and use common component test suites
  • Loading branch information
mdekstrand authored Dec 26, 2024
2 parents 8146c6a + 9e79952 commit 8418807
Show file tree
Hide file tree
Showing 24 changed files with 362 additions and 24 deletions.
3 changes: 2 additions & 1 deletion lenskit-funksvd/lenskit/funksvd.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,7 +238,7 @@ class FunkSVDScorer(Component, Trainable):

def __init__(
self,
features: int,
features: int = 50,
iterations: int = 100,
*,
lrate: float = 0.001,
Expand Down Expand Up @@ -314,6 +314,7 @@ def __call__(self, query: QueryInput, items: ItemList) -> ItemList:
query = RecQuery.create(query)

user_id = query.user_id
user_num = None
if user_id is not None:
user_num = self.users_.number(user_id, missing=None)
if user_num is None:
Expand Down
6 changes: 5 additions & 1 deletion lenskit-funksvd/tests/test_funksvd.py
Original file line number Diff line number Diff line change
Expand Up @@ -17,7 +17,7 @@
from lenskit.data.bulk import dict_to_df, iter_item_lists
from lenskit.funksvd import FunkSVDScorer
from lenskit.metrics import call_metric, quick_measure_model
from lenskit.testing import ml_100k, ml_ds, wantjit # noqa: F401
from lenskit.testing import BasicComponentTests, ScorerTests, wantjit

_log = logging.getLogger(__name__)

Expand All @@ -27,6 +27,10 @@
simple_ds = from_interactions_df(simple_df)


class TestFunkSVD(BasicComponentTests, ScorerTests):
component = FunkSVDScorer


def test_fsvd_basic_build():
algo = FunkSVDScorer(20, iterations=20)
algo.train(simple_ds)
Expand Down
3 changes: 2 additions & 1 deletion lenskit-hpf/lenskit/hpf.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ class HPFScorer(Component, Trainable):
items_: Vocabulary
item_features_: np.ndarray[tuple[int, int], np.dtype[np.float64]]

def __init__(self, features: int, **kwargs):
def __init__(self, features: int = 50, **kwargs):
self.features = features
self._kwargs = kwargs

Expand Down Expand Up @@ -78,6 +78,7 @@ def __call__(self, query: QueryInput, items: ItemList) -> ItemList:
query = RecQuery.create(query)

user_id = query.user_id
user_num = None
if user_id is not None:
user_num = self.users_.number(user_id, missing=None)
if user_num is None:
Expand Down
5 changes: 5 additions & 0 deletions lenskit-hpf/tests/test_hpf.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,12 +15,17 @@
from lenskit.data import ItemList, from_interactions_df
from lenskit.metrics import quick_measure_model
from lenskit.pipeline import topn_pipeline
from lenskit.testing import BasicComponentTests, ScorerTests

hpf = importorskip("lenskit.hpf")

_log = logging.getLogger(__name__)


class TestHPF(BasicComponentTests, ScorerTests):
component = hpf.HPFScorer


@mark.slow
def test_hpf_train_large(tmp_path, ml_ratings):
algo = hpf.HPFScorer(20)
Expand Down
9 changes: 9 additions & 0 deletions lenskit-implicit/tests/test_implicit.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,10 +15,19 @@
from lenskit.data import ItemList, from_interactions_df
from lenskit.implicit import ALS, BPR
from lenskit.metrics import quick_measure_model
from lenskit.testing import BasicComponentTests, ScorerTests

_log = logging.getLogger(__name__)


class TestImplicitALS(BasicComponentTests, ScorerTests):
component = ALS


class TestImplicitBPR(BasicComponentTests, ScorerTests):
component = BPR


@mark.slow
def test_implicit_als_train_rec(ml_ds):
algo = ALS(25)
Expand Down
2 changes: 1 addition & 1 deletion lenskit-sklearn/lenskit/sklearn/svd.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ class BiasedSVDScorer(Component, Trainable):

def __init__(
self,
features: int,
features: int = 50,
*,
damping: UITuple[float] | float | tuple[float, float] = 5,
algorithm: Literal["arpack", "randomized"] = "randomized",
Expand Down
5 changes: 5 additions & 0 deletions lenskit-sklearn/tests/test_svd.py
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
from lenskit.data import Dataset, ItemList, from_interactions_df
from lenskit.metrics import call_metric, quick_measure_model
from lenskit.sklearn import svd
from lenskit.testing import BasicComponentTests, ScorerTests

_log = logging.getLogger(__name__)

Expand All @@ -26,6 +27,10 @@
need_skl = mark.skipif(not svd.SKL_AVAILABLE, reason="scikit-learn not installed")


class TestBiasedSVD(BasicComponentTests, ScorerTests):
component = svd.BiasedSVDScorer


@need_skl
def test_svd_basic_build():
algo = svd.BiasedSVDScorer(2)
Expand Down
2 changes: 1 addition & 1 deletion lenskit/lenskit/als/_explicit.py
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ class BiasedMFScorer(ALSBase):

def __init__(
self,
features: int,
features: int = 50,
*,
epochs: int = 10,
reg: float | tuple[float, float] = 0.1,
Expand Down
2 changes: 1 addition & 1 deletion lenskit/lenskit/als/_implicit.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,7 +81,7 @@ class ImplicitMFScorer(ALSBase):

def __init__(
self,
features: int,
features: int = 50,
*,
epochs: int = 20,
reg: float | tuple[float, float] = 0.1,
Expand Down
4 changes: 4 additions & 0 deletions lenskit/lenskit/data/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
from .convert import from_interactions_df
from .dataset import Dataset, FieldError
from .items import ItemList
from .lazy import LazyDataset
from .matrix import MatrixDataset
from .movielens import load_movielens, load_movielens_df
from .mtarray import MTArray, MTFloatArray, MTGenericArray, MTIntArray
from .query import QueryInput, RecQuery
Expand All @@ -23,6 +25,8 @@
"Dataset",
"FieldError",
"from_interactions_df",
"LazyDataset",
"MatrixDataset",
"ID",
"NPID",
"UITuple",
Expand Down
7 changes: 5 additions & 2 deletions lenskit/lenskit/knn/item.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class ItemKNNScorer(Component, Trainable):

def __init__(
self,
nnbrs: int,
nnbrs: int = 20,
min_nbrs: int = 1,
min_sim: float = 1.0e-6,
save_nbrs: int | None = None,
Expand Down Expand Up @@ -202,7 +202,10 @@ def __call__(self, query: QueryInput, items: ItemList) -> ItemList:
ratings = query.user_items
if ratings is None:
if query.user_id is None:
raise ValueError("cannot recommend without without either user ID or items")
warnings.warn(
"cannot recommend without without either user ID or items", DataWarning
)
return ItemList(items, scores=np.nan)

upos = self.users_.number(query.user_id, missing=None)
if upos is None:
Expand Down
5 changes: 4 additions & 1 deletion lenskit/lenskit/knn/user.py
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ class UserKNNScorer(Component, Trainable):

def __init__(
self,
nnbrs: int,
nnbrs: int = 20,
min_nbrs: int = 1,
min_sim: float = 1.0e-6,
feedback: FeedbackType = "explicit",
Expand Down Expand Up @@ -155,6 +155,9 @@ def __call__(self, query: QueryInput, items: ItemList) -> ItemList:
query = RecQuery.create(query)
watch = util.Stopwatch()
log = _log.bind(user_id=query.user_id, n_items=len(items))
if len(items) == 0:
log.debug("no candidate items, skipping")
return ItemList(items, scores=np.nan)

udata = self._get_user_data(query)
if udata is None:
Expand Down
14 changes: 4 additions & 10 deletions lenskit/lenskit/testing/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,9 +8,9 @@
import os
from contextlib import contextmanager

import pytest

from ._arrays import coo_arrays, scored_lists, sparse_arrays, sparse_tensors
from ._components import BasicComponentTests, ScorerTests
from ._markers import jit_enabled, wantjit
from ._movielens import (
demo_recs,
ml_100k,
Expand All @@ -36,16 +36,10 @@
"wantjit",
"jit_enabled",
"set_env_var",
"BasicComponentTests",
"ScorerTests",
]

jit_enabled = True
if "NUMBA_DISABLE_JIT" in os.environ:
jit_enabled = False
if os.environ.get("PYTORCH_JIT", None) == "0":
jit_enabled = False

wantjit = pytest.mark.skipif(not jit_enabled, reason="JIT required")


@contextmanager
def set_env_var(var, val):
Expand Down
Loading

0 comments on commit 8418807

Please sign in to comment.