Skip to content

Model API

SRF is the main estimator in PySRF. Given a symmetric similarity matrix \(S\), it learns a sparse, non-negative embedding \(W\) such that \(S \approx WW^\top\), where each row of \(W\) holds an item's loadings on revealed dimensions and small loadings mark dimensions that are irrelevant to that item. It works directly on matrices with missing entries, so no imputation is needed. Reach for SRF whenever you want interpretable, parts-based dimensions out of a similarity matrix; see quickstart for an end-to-end walkthrough.

SRF Class

SRF

SRF(
    rank: int = 10,
    rho: float = 3.0,
    max_outer: int = 100,
    max_inner: int = 10,
    tol: float = 0.001,
    verbose: int = 0,
    init: str = "random_sqrt",
    random_state: int | None = None,
    missing_values: float | None = np.nan,
    bounds: tuple[float, float] | None = (None, None),
    check_input: bool = True,
)

Bases: TransformerMixin, BaseEstimator

Similarity-based Representation Factorization.

Factorizes a symmetric similarity matrix S into WW' where W >= 0. Handles missing entries and can enforce bounds on the reconstructed similarities.

The objective is: min_{W>=0, V} ||M * (S - V)||^2_F + rho/2 * ||V - WW'||^2_F where M is the observation mask.

Parameters:

Name Type Description Default
rank int

Number of dimensions in the embedding

10
rho float

Penalty weight controlling how closely WW' must match V

3.0
max_outer int

Maximum number of outer optimization iterations; the stall rule usually stops far earlier

100
max_inner int

Maximum iterations for the W update per outer iteration

10
tol float

Relative convergence tolerance. Fitting stops when the relative fit ||X - WW'||_F / ||X||_F over observed entries falls below tol or improves by less than tol across the last 10 iterations (Xu et al., 2012). With missing data, the ADMM splitting must additionally agree: ||V - WW'||_F <= tol * max(||V||_F, ||WW'||_F).

1e-3
verbose int

Whether to print optimization progress

0
init str

Embedding initialization method ('random', 'random_sqrt')

'random_sqrt'
random_state int or None

Random seed for reproducible initialization

None
missing_values float or None

Sentinel value marking missing entries in the similarity matrix

np.nan
bounds tuple of (float, float) or None

(lower, upper) bounds on the reconstructed similarities. For example, (0, 1) for similarities normalized to [0, 1].

(None, None)
check_input bool

Whether to verify that the input matrix and missing mask are symmetric. Set to False for large matrices where the input is known to be valid, to avoid allocating temporary arrays during the symmetry check.

True

Attributes:

Name Type Description
w_ ndarray of shape (n_samples, rank)

Learned embedding matrix

components_ ndarray of shape (n_samples, rank)

Alias for w_ (sklearn compatibility)

n_iter_ int

Number of outer iterations performed

history_ dict

Optimization metrics per iteration

Examples:

>>> from pysrf import SRF
>>> model = SRF(rank=10, random_state=42)
>>> w = model.fit_transform(similarity_matrix)
>>> reconstruction = w @ w.T
>>> # With missing data
>>> similarity_matrix[mask] = np.nan
>>> model = SRF(rank=10, missing_values=np.nan)
>>> w = model.fit_transform(similarity_matrix)
Source code in pysrf/model.py
def __init__(
    self,
    rank: int = 10,
    rho: float = 3.0,
    max_outer: int = 100,
    max_inner: int = 10,
    tol: float = 1e-3,
    verbose: int = 0,
    init: str = "random_sqrt",
    random_state: int | None = None,
    missing_values: float | None = np.nan,
    bounds: tuple[float, float] | None = (None, None),
    check_input: bool = True,
) -> None:
    self.rank = rank
    self.rho = rho
    self.max_outer = max_outer
    self.max_inner = max_inner
    self.tol = tol
    self.verbose = verbose
    self.init = init
    self.random_state = random_state
    self.missing_values = missing_values
    self.bounds = bounds
    self.check_input = check_input

_fit_loop

_fit_loop(steps: Iterator[tuple[ndarray, Step]]) -> SRF

Drive the outer iterations; steps yields one measurement each.

Source code in pysrf/model.py
def _fit_loop(self, steps: Iterator[tuple[np.ndarray, Step]]) -> SRF:
    """Drive the outer iterations; steps yields one measurement each."""
    history = defaultdict(list)

    recent = deque(maxlen=PROGRESS_WINDOW)
    pbar = self._progress_bar()
    for i, (w, step) in zip(pbar, steps):
        earlier = recent[0] if recent else None
        metrics = step.metrics(self._total_ss)
        metrics["converged"] = step.converged(earlier, self.tol)
        for key, value in metrics.items():
            history[key].append(value)

        pbar.set_postfix(
            rec_error=f"{metrics['rec_error']:.3f}",
            evar=f"{metrics['evar']:.3f}",
        )

        if metrics["converged"]:
            logger.info("Converged at iteration %d", i)
            break
        recent.append(step)
    else:
        warnings.warn(
            f"Maximum number of outer iterations ({self.max_outer}) "
            "reached without convergence. Increase max_outer or tol.",
            ConvergenceWarning,
        )

    self.w_ = w
    self.components_ = w
    self.n_iter_ = i
    self.history_ = history

    return self

_validate_input

_validate_input(x: ndarray) -> np.ndarray

Validate the similarity matrix and derive the observation stats.

Allocates only the one contract copy of x and the observation mask; all statistics are computed without materializing the observed entries.

Source code in pysrf/model.py
def _validate_input(self, x: np.ndarray) -> np.ndarray:
    """Validate the similarity matrix and derive the observation stats.

    Allocates only the one contract copy of x and the observation
    mask; all statistics are computed without materializing the
    observed entries.
    """
    self._validate_params()
    _validate_bounds(self.bounds)

    x = validate_data(
        self,
        x,
        reset=True,
        ensure_all_finite=(
            "allow-nan" if is_nan_marker(self.missing_values) else True
        ),
        ensure_2d=True,
        dtype=np.float64,
        copy=True,
    )

    self._observed_mask = observation_mask(x, self.missing_values)
    if not np.any(self._observed_mask):
        raise ValueError(
            "No observed entries found in the data. All values are missing."
        )

    if is_nan_marker(self.missing_values) or self.missing_values is None:
        np.nan_to_num(x, copy=False, nan=0.0, posinf=0.0, neginf=0.0)
    else:
        x[~self._observed_mask] = 0.0
    if self.check_input:
        check_symmetric(self._observed_mask, raise_exception=True)
        x = check_symmetric(x, raise_exception=True, tol=1e-10)

    # Zero-filled missing entries drop out of the sums, so the
    # variance identity avoids materializing the observed values
    n_observed = np.count_nonzero(self._observed_mask)
    observed_mean = np.sum(x) / n_observed
    observed_sumsq = np.einsum("ij,ij->", x, x)
    self._total_ss = observed_sumsq - n_observed * observed_mean**2

    if self.verbose:
        n_threads = os.environ.get("OMP_NUM_THREADS", "1")
        logger.info(
            "Using %s BLAS thread(s). For faster fitting, set "
            "OMP_NUM_THREADS before importing pysrf "
            "(e.g. export OMP_NUM_THREADS=4)",
            n_threads,
        )

    return x

fit

fit(x: ndarray, y: ndarray | None = None) -> SRF

Fit the model to a similarity matrix.

Parameters:

Name Type Description Default
x array-like of shape (n_samples, n_samples)

Symmetric similarity matrix. Missing values should be marked according to the missing_values parameter.

required
y Ignored

Not used, present for sklearn API compatibility.

None

Returns:

Name Type Description
self SRF

Fitted estimator.

Source code in pysrf/model.py
def fit(self, x: np.ndarray, y: np.ndarray | None = None) -> SRF:
    """Fit the model to a similarity matrix.

    Parameters
    ----------
    x : array-like of shape (n_samples, n_samples)
        Symmetric similarity matrix. Missing values should be marked
        according to the ``missing_values`` parameter.
    y : Ignored
        Not used, present for sklearn API compatibility.

    Returns
    -------
    self : SRF
        Fitted estimator.
    """
    x = self._validate_input(x)
    w = _initialize_w(x, self.rank, self.init, self.random_state)

    if self._observed_mask.all():
        steps = _complete_steps(x, w, self.max_inner)
    else:
        steps = _missing_steps(
            x, self._observed_mask, w, self.rho, self.bounds, self.max_inner
        )
    return self._fit_loop(steps)

fit_transform

fit_transform(
    x: ndarray, y: ndarray | None = None
) -> np.ndarray

Fit the model and return the learned embedding matrix W.

Parameters:

Name Type Description Default
x array-like of shape (n_samples, n_samples)

Symmetric similarity matrix

required
y Ignored

Not used, present for sklearn API compatibility.

None

Returns:

Name Type Description
w ndarray of shape (n_samples, rank)

Embedding matrix

Source code in pysrf/model.py
def fit_transform(self, x: np.ndarray, y: np.ndarray | None = None) -> np.ndarray:
    """Fit the model and return the learned embedding matrix W.

    Parameters
    ----------
    x : array-like of shape (n_samples, n_samples)
        Symmetric similarity matrix
    y : Ignored
        Not used, present for sklearn API compatibility.

    Returns
    -------
    w : ndarray of shape (n_samples, rank)
        Embedding matrix
    """
    return self.fit(x, y).transform(x)

reconstruct

reconstruct(w: ndarray | None = None) -> np.ndarray

Reconstruct the similarity matrix as WW'.

Parameters:

Name Type Description Default
w ndarray of shape (n_samples, rank) or None

Embedding matrix. If None, uses the fitted embedding.

None

Returns:

Name Type Description
s_hat ndarray of shape (n_samples, n_samples)

Reconstructed similarity matrix

Source code in pysrf/model.py
def reconstruct(self, w: np.ndarray | None = None) -> np.ndarray:
    """Reconstruct the similarity matrix as WW'.

    Parameters
    ----------
    w : ndarray of shape (n_samples, rank) or None
        Embedding matrix. If None, uses the fitted embedding.

    Returns
    -------
    s_hat : ndarray of shape (n_samples, n_samples)
        Reconstructed similarity matrix
    """
    if w is None:
        check_is_fitted(self)
        w = self.w_

    return w @ w.T

score

score(x: ndarray, y: ndarray | None = None) -> float

Negative MSE on observed entries (higher is better).

Parameters:

Name Type Description Default
x array-like of shape (n_samples, n_samples)

Symmetric similarity matrix. Missing values should be marked according to the missing_values parameter.

required
y Ignored

Not used, present for sklearn API compatibility.

None

Returns:

Name Type Description
score float

Negative mean squared reconstruction error on observed entries.

Source code in pysrf/model.py
def score(self, x: np.ndarray, y: np.ndarray | None = None) -> float:
    """Negative MSE on observed entries (higher is better).

    Parameters
    ----------
    x : array-like of shape (n_samples, n_samples)
        Symmetric similarity matrix. Missing values should be marked
        according to the ``missing_values`` parameter.
    y : Ignored
        Not used, present for sklearn API compatibility.

    Returns
    -------
    score : float
        Negative mean squared reconstruction error on observed entries.
    """
    check_is_fitted(self)

    x = validate_data(
        self,
        x,
        reset=False,
        ensure_2d=True,
        dtype=np.float64,
        ensure_all_finite="allow-nan"
        if is_nan_marker(self.missing_values)
        else True,
    )
    observed_mask = observation_mask(x, self.missing_values)
    s_hat = self.reconstruct()
    mse = np.mean((x[observed_mask] - s_hat[observed_mask]) ** 2)
    return -mse

transform

transform(x: ndarray) -> np.ndarray

Return the learned embedding matrix W.

Parameters:

Name Type Description Default
x array-like of shape (n_samples, n_samples)

Ignored, present for sklearn API compatibility.

required

Returns:

Name Type Description
w ndarray of shape (n_samples, rank)

Embedding matrix

Source code in pysrf/model.py
def transform(self, x: np.ndarray) -> np.ndarray:
    """Return the learned embedding matrix W.

    Parameters
    ----------
    x : array-like of shape (n_samples, n_samples)
        Ignored, present for sklearn API compatibility.

    Returns
    -------
    w : ndarray of shape (n_samples, rank)
        Embedding matrix
    """
    check_is_fitted(self)
    return self.w_