RecoMetrics

Library-agnostic evaluation framework for implicit-feedback recommender systems that are based on low-rank matrix factorization models or latent embeddings. Calculates per-user metrics based on the ranking of items produced by the model, using efficient multi-threaded routines. Also provides functions for generating train-test splits of data. Writen in C++ with interfaces for Python and R.


For a longer introduction, see:

Evaluating implicit-feedback recommendations

When evaluating recommender systems built from implicit-feedback data (e.g. numer of times that a user played each song in a music service), one usually wants to evaluate the quality of the produced recommendations according to how well they rank the available pool of items for each user.

This is done by setting aside some fraction of the data for testing purposes, then building a model on the remainder of the data, and producing top-K recommended lists for each user that exclude the items already consumed by him/her in this non-held-out data. The recommended lists are evaluated according to how they rank the held-out items in comparison to the items that the user did not consume, using classification or ranking metrics such as precision, NDCG, AUC, among others. The held-out items are considered to be "positive" entries, while the items which were not consumed by the user are considered "negative" entries.

Compared to metrics used for explicit-feedback recommendations such as RMSE, metrics for implicit-feedback are much slower to compute (might well be slower than fitting the model itself), since they require generating a ranking of a large number of items for each user separately and iterating over ranked lists. Many libraries for recommender systems provide their own functionality for automatically setting aside some data and evaluating these metrics while fitting models, but there are some issues with such approach:

This library, in contrast:

Supported metrics

This package does NOT deal with other more specialized metrics evaluating e.g. "serendipity", "discoverability", diversity of recommendations, etc.

Installation

Note: requires C/C++ compilers configured for Python. See this guide for instructions.

pip install recometrics

Or if that fails:

pip install --no-use-pep517 recometrics

Note for macOS users: on macOS, the Python version of this package might compile without multi-threading capabilities. In order to enable multi-threading support, first install OpenMP:

brew install libomp

And then reinstall this package: pip install --upgrade --no-deps --force-reinstall recometrics.


IMPORTANT: the setup script will try to add compilation flag -march=native. This instructs the compiler to tune the package for the CPU in which it is being installed (by e.g. using AVX instructions if available), but the result might not be usable in other computers. If building a binary wheel of this package or putting it into a docker image which will be used in different machines, this can be overriden either by (a) defining an environment variable DONT_SET_MARCH=1, or by (b) manually supplying compilation CFLAGS as an environment variable with something related to architecture. For maximum compatibility (but slowest speed), it's possible to do something like this:

export DONT_SET_MARCH=1
pip install recometrics

or, by specifying some compilation flag for architecture:

export CFLAGS="-march=x86-64"
export CXXFLAGS="-march=x86-64"
pip install recometrics

Note that, if not using -march=native, it will rely on the BLAS library provided by SciPy for calculations.


Note: This package greatly benefits from extra optimizations that aren't enabled by default for R packages. See this guide for instructions on how to enable them.

install.packages("recometrics")

For better performance, it's recommended to compile the package from source with extra optimizations -O3 and -march=native - in Linux, this can be done by creating a file ~/.R/Makevars containing this line: CXX11FLAGS += -O3 -march=native (plus an empty line at the end) (this file should be created before installing recometrics).

Library is a self-contained templated header file (src/recometrics.hpp). Can be copied into other projects and used by #include "..."'ing it.

Documentation

Examples

Applied examples with public data and different libraries for fitting models:

Sample usage

import numpy as np
from scipy.sparse import csr_matrix, random as sprandom
import recometrics

### User-item interactions (e.g. number of video views)
n_users = 100
n_items = 50
rng = np.random.default_rng(seed=123)
X = sprandom(n_users, n_items, density=0.2,
             data_rvs=lambda n: rng.integers(1, 100, n),
             format="csr")

### Creating a fit + train-test split
X_fit, X_train, X_test, test_users = \
    recometrics.split_reco_train_test(
        X, split_type="separated",
        users_test_fraction=0.1,
        items_test_fraction=0.3,
        min_items_pool=10, min_pos_test=2,
        seed=123
    )

### Model would be fit to 'X_fit' (non-test users)
### e.g. model = Model(...).fit(X_fit)
latent_dim = 5
Item_Factors = rng.standard_normal((n_items, latent_dim))

### Then it would produce user factors for 'X_train'
### (users to which the model was not fit)
User_Factors = rng.standard_normal((X_train.shape[0], latent_dim))

### And then the metrics would be calculated
df_metrics_by_user = \
    recometrics.calc_reco_metrics(
        X_train, X_test,
        User_Factors, Item_Factors,
        k=5,
        precision=True,
        average_precision=True,
        ndcg=True,
        nthreads=-1
    )
df_metrics_by_user.head(3)
   P@5      AP@5    NDCG@5
0  0.0  0.000000  0.000000
1  0.0  0.000000  0.000000
2  0.2  0.333333  0.610062
library(Matrix)
library(recometrics)

### User-item interactions (e.g. number of video views)
n_users <- 100
n_items <- 50
set.seed(123)
X <- rsparsematrix(n_users, n_items, density=0.2, repr="R",
                   rand.x=function(n) sample(100, n, replace=TRUE))

### Creating a fit + train-test split
temp <- create.reco.train.test(
    X, split_type="separated",
    users_test_fraction=0.1,
    items_test_fraction=0.3,
    min_items_pool=10, min_pos_test=2,
    seed=1
)
X_train <- temp$X_train
X_test <- temp$X_test
X_fit <- temp$X_rem
rm(temp)

### Model would be fit to 'X_fit' (non-test users)
### e.g. model <- reco_model(X_fit, ...)
latent_dim <- 5
Item_Factors <- matrix(rnorm(n_items*latent_dim), ncol=n_items)

### Then it would produce user factors for 'X_train'
### (users to which the model was not fit)
User_Factors <- matrix(rnorm(nrow(X_train)*latent_dim), ncol=nrow(X_train))

### And then the metrics would be calculated
df_metrics_by_user <- calc.reco.metrics(
    X_train, X_test,
    User_Factors, Item_Factors,
    k=5,
    precision=TRUE,
    average_precision=TRUE,
    ndcg=TRUE,
    nthreads=parallel::detectCores()
)
head(df_metrics_by_user, 3)
  p_at_5 ap_at_5 ndcg_at_5
1    0.0   0.000 0.0000000
2    0.2   0.125 0.3047166
3    0.2   0.125 0.1813742