Skip to content

Commit b148ed0

Browse files
authored
Merge pull request #1063 from mdekstrand/feature/866-accumulators
Rework the metric interface and collection to use explicit accumulators
2 parents 4c506fa + 8d71a1b commit b148ed0

22 files changed

Lines changed: 763 additions & 663 deletions

docs/guide/GettingStarted.ipynb

Lines changed: 43 additions & 41 deletions
Large diffs are not rendered by default.

docs/guide/batch.rst

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ For an example, let's start with importing things to run a quick batch:
3434
>>> from lenskit.batch import recommend
3535
>>> from lenskit.data import load_movielens
3636
>>> from lenskit.splitting import sample_users, SampleN
37-
>>> from lenskit.metrics import RunAnalysis, RBP
37+
>>> from lenskit.metrics import MeasurementCollector, RBP
3838

3939
Load and split some data:
4040

@@ -49,7 +49,7 @@ Configure and train the model:
4949

5050
Generate recommendations:
5151

52-
>>> recs = recommend(pop_pipe, split.test.keys(), n_jobs=1)
52+
>>> recs = recommend(pop_pipe, split.test.keys())
5353
>>> recs.to_df()
5454
user_id item_id score rank
5555
0 ... 1
@@ -58,13 +58,11 @@ Generate recommendations:
5858

5959
And measure their results:
6060

61-
>>> ra = RunAnalysis()
62-
>>> ra.add_metric(RBP())
63-
>>> scores = ra.measure(recs, split.test)
64-
>>> scores.list_summary() # doctest: +ELLIPSIS
65-
mean median std
66-
metric
67-
RBP 0.06... 0.02... 0.07...
61+
>>> collect = MeasurementCollector()
62+
>>> collect.add_metric(RBP())
63+
>>> collect.measure_collection(recs, split.test)
64+
>>> collect.summary_metrics() # doctest: +ELLIPSIS
65+
{... 'RBP.mean': 0.06..., ...}
6866

6967

7068
The :py:func:`predict` function works similarly, but for rating predictions.

docs/guide/documenting.rst

Lines changed: 9 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -86,22 +86,20 @@ Reporting Metrics
8686
~~~~~~~~~~~~~~~~~
8787

8888
Reporting the metrics themselves is relatively straightforward. The
89-
:py:meth:`lenskit.bulk.RunAnalysis.measure` method returns a results object
90-
contianing the metrics for individual lists, the global metrics, and easy access
91-
(through :meth:`~lenskit.bulk.RunAnalysis.list_summary`) to summary statistics
92-
of per-list metrics, optionally grouped by keys such as model name.
89+
:py:meth:`lenskit.eval.MeasurementCollector.list_metrics` and
90+
:py:meth:`lenskit.eval.MeasurementCollector.summary_metrics` methods return the
91+
individual list metrics or overall summaries for a set of lists.
9392

9493
The following code will produce a table of algorithm scores for hit rate, NDCG
9594
and MRR, assuming that your algorithm identifier is in a column named ``model``
9695
and the target list length is in ``N``::
9796

98-
rla = RunAnalysis()
99-
rla.add_metric(Hit(n=N))
100-
rla.add_metric(NDCG(n=N))
101-
rla.add_metric(RecipRank(n=N))
102-
results = rla.measure(recs, test)
103-
# group by agorithm
104-
model_metrics = results.list_summary('model')
97+
mc = MeasurementCollector()
98+
mc.add_metric(Hit(n=N))
99+
mc.add_metric(NDCG(n=N))
100+
mc.add_metric(RecipRank(n=N))
101+
mc.measure(recs, test)
102+
mc.list_metrics()
105103

106104
You can then use :py:meth:`pandas.DataFrame.to_latex` to convert ``algo_scores``
107105
to a LaTeX table to include in your paper.

docs/guide/evaluation/predictions.rst

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ There are two ways to directly call a prediction accuracy metric:
2828

2929
* Pass a single item list with scores and a ``rating`` field.
3030

31-
For evaluation, you will usually want to use :class:`~lenskit.metrics.RunAnalysis`,
31+
For evaluation, you will usually want to use :class:`~lenskit.metrics.MeasurementCollector`,
3232
which takes care of calling the prediction metric for you.
3333

3434
Missing Data

docs/guide/evaluation/rankings.rst

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -9,9 +9,9 @@ The :py:mod:`lenskit.metrics.ranking` module contains the core top-*N* ranking
99
accuracy metrics (including rank-oblivious list metrics like precision, recall,
1010
and hit rate).
1111

12-
Ranking metrics extend the :py:class:`RankingMetricBase` base class in addition
13-
to :py:class:`ListMetric` and/or :py:class:`GlobalMetric`, return a score given
14-
a recommendation list and a test rating list, both as :py:class:`item lists
12+
Ranking metrics extend the :py:class:`RankingMetricBase` base class, often in
13+
addition to :py:class:`ListMetric`, and return a score given a recommendation
14+
list and a test rating list, both as :py:class:`item lists
1515
<lenskit.data.ItemList>`; most metrics require the recommendation item list to
1616
be :py:attr:`~lenskit.data.ItemList.ordered`.
1717

docs/guide/migrating.rst

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,11 @@ fall into the following categories:
6464
also directly supports “global” metrics that are computed over an entire run
6565
instead of one list at a time.
6666

67+
.. versionchanged:: 2026.1
68+
69+
``RunAnalysis`` has been superseded by
70+
:class:`~lenskit.metrics.MeasurementCollector`.
71+
6772
.. important::
6873

6974
The default options of some metrics, particularly

docs/releases/2026.rst

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -42,7 +42,16 @@ Breaking Changes
4242
is now decomposed. All metrics based on listwise measurements or intermediate
4343
results should directly extend from ``Metric`` or :class:`~lenskit.metrics.ListMetric`.
4444
(:pr:`983`)
45-
- ``GlobalMetric`` no longer inherits from ``Metric``, and may be removed in a future release.
45+
- ``GlobalMetric`` has been removed. Code needing to compute global metrics
46+
should directly compute over item lists.
47+
- :class:`~lenskit.metrics.MeasurementCollector` no longer supports defaults
48+
when adding metrics.
49+
- :class:`~lenskit.metrics.RunAnalysis` is deprecated, and no longer supports
50+
explicit defaults or global metrics. Use
51+
:class:`~lenskit.metrics.MeasurementCollector` instead. Its global metric
52+
columns have also changed in some cases.
53+
- :class:`~lenskit.metrics.ListGini` and :class:`~lenskit.metrics.ExposureGini`
54+
now require an item vocabulary or dataset as input.
4655
- Stopped providing wheels for macOS on Intel. Users who still need to run
4756
LensKit on Intel-based Macs should use the Conda packages (available in conda-forge and
4857
`prefix.dev`_).
@@ -86,6 +95,9 @@ Performance Changes
8695
Minor Changes
8796
-------------
8897

98+
- :func:`lenskit.metrics.call_metric` has been renamed to
99+
:func:`~lenskit.metrics.measure_list`, and the old name preserved as a
100+
deprecated alias.
89101
- Pipeline type-checking for `ArrayLike` component inputs no longer works, due to
90102
a breaking change in NumPy 2.4. No LensKit components used `ArrayLike` as an
91103
input or output data type.

src/lenskit/data/accum/__init__.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
# This file is part of LensKit.
2+
# Copyright (C) 2018-2023 Boise State University.
3+
# Copyright (C) 2023-2026 Drexel University.
4+
# Licensed under the MIT license, see LICENSE.md for details.
5+
# SPDX-License-Identifier: MIT
6+
7+
"""
8+
Data accumulation support
9+
"""
10+
11+
from ._proto import Accumulator, AccumulatorFactory
12+
from ._value import ValueStatAccumulator, ValueStatistics
13+
14+
__all__ = [
15+
"Accumulator",
16+
"AccumulatorFactory",
17+
"ValueStatAccumulator",
18+
"ValueStatistics",
19+
]

src/lenskit/data/accum/_object.py

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
# This file is part of LensKit.
2+
# Copyright (C) 2018-2023 Boise State University.
3+
# Copyright (C) 2023-2026 Drexel University.
4+
# Licensed under the MIT license, see LICENSE.md for details.
5+
# SPDX-License-Identifier: MIT
6+
7+
from ._proto import Accumulator
8+
9+
10+
class ObjectListAccumulator[T](Accumulator[T, list[T]]):
11+
"""
12+
An accumulator lists of objects.
13+
"""
14+
15+
values: list[T]
16+
17+
def __init__(self):
18+
self.values = []
19+
20+
def __len__(self) -> int:
21+
return len(self.values)
22+
23+
def add(self, value: T):
24+
self.values.append(value)
25+
26+
def accumulate(self) -> list[T]:
27+
return self.values

src/lenskit/data/accum/_proto.py

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,40 @@
1+
# This file is part of LensKit.
2+
# Copyright (C) 2018-2023 Boise State University.
3+
# Copyright (C) 2023-2026 Drexel University.
4+
# Licensed under the MIT license, see LICENSE.md for details.
5+
# SPDX-License-Identifier: MIT
6+
7+
from __future__ import annotations
8+
9+
from typing import Protocol, runtime_checkable
10+
11+
12+
@runtime_checkable
13+
class AccumulatorFactory[X, R](Protocol):
14+
def create_accumulator(self) -> Accumulator[X, R]:
15+
"""
16+
Create an accumulator for the results of this object.
17+
18+
Return:
19+
An accumulator.
20+
"""
21+
...
22+
23+
24+
@runtime_checkable
25+
class Accumulator[X, R](Protocol):
26+
"""
27+
Protocol implemented by data accumulators.
28+
"""
29+
30+
def add(self, value: X) -> None:
31+
"""
32+
Add a single value to this accumulator.
33+
"""
34+
...
35+
36+
def accumulate(self) -> R:
37+
"""
38+
Compute the accumulated value from this accumulator.
39+
"""
40+
...

0 commit comments

Comments
 (0)