Sequences API

Interaction Histories

compresso_recsys.ItemSequences is the chronological view of a split: for each row, the catalog indices that row interacted with, in order, with duplicates preserved. It is the counterpart to the csr_matrix views, holding exactly the two things a sparse row cannot express.

A CSR row is a set. Sorting its indices discards order, and its data array holds one value per distinct column, so re-watching a film is a larger number rather than a second event. Both are precisely what a sequential model learns from, so a separate representation is needed rather than a convention on top of the matrix.

What ItemSequences deliberately does not hold is padding, PAD/MASK tokens, a maximum length, or any truncation. Those are modelling decisions that architectures disagree about — a GRU pads right, a causal transformer pads left, and context windows differ — so they belong in a trainer rather than in a checkpoint that every model reads. compresso_recsys.models.SequenceBatcher owns them.

Rows may be empty, and carry no identity of their own: row i of a sequence addresses the same user as row i of the matrix beside it, exactly as the existing csr_matrix sources are aligned. Buffers are read-only, so a sequence handed to a trainer cannot be modified underneath it.

class compresso_recsys.ItemSequences(values, indptr, n_items)[source]

Per-row chronological item histories, oldest first.

values holds catalog indices for every row concatenated, and indptr marks where each row begins, exactly as CSR does — row i is values[indptr[i]:indptr[i + 1]]. Unlike CSR there is no data array, because a history has no weights, and no column sorting, because the order is the information.

Rows may be empty. A user with no history is a real prediction case, and refusing to represent one would only push the special case into every caller.

Rows carry no identity. csr_matrix does not name its rows either, and evaluation aligns sample_ids positionally against whichever source it was given; carrying identifiers on one source type and not the other would mean two alignment stories instead of one.

Parameters:
  • values (ndarray)

  • indptr (ndarray)

  • n_items (int)

classmethod from_rows(rows, *, n_items)[source]

Build from one array per row, in row order.

Return type:

ItemSequences

Parameters:
  • rows (list[ndarray] | list[list[int]])

  • n_items (int)

property n_rows: int

Number of histories.

property row_lengths: ndarray

Interactions per row.

Also the slicing-invariant description of the structure: indptr is rebased by take_rows(), so anything identifying these sequences must hash lengths rather than offsets.

row(index)[source]

One history, oldest first, as a read-only view.

Return type:

ndarray

Parameters:

index (int)

take_rows(start, stop)[source]

Rows start:stop as a new instance, with indptr rebased.

Half-open, as Python slicing is. The rebasing is why indptr cannot identify a slice: every slice starts at zero regardless of where it came from.

Return type:

ItemSequences

Parameters:
  • start (int)

  • stop (int)

select_rows(indices)[source]

Rows named by indices, in the order given.

The counterpart to take_rows() for a non-contiguous selection, which is what shuffling a training set needs.

Return type:

ItemSequences

Parameters:

indices (ndarray | list[int])

compresso_recsys.save_item_sequences(path, sequences)[source]

Write sequences to a single compressed .npz.

Compressed to match scipy.sparse.save_npz, which the matrix views already use: a checkpoint should not store one view cheaply and the other expensively.

Return type:

None

Parameters:
compresso_recsys.load_item_sequences(path)[source]

Read sequences written by save_item_sequences().

Return type:

ItemSequences

Parameters:

path (str | Path)

Which Split Modes Produce Them

Sequences require an ordering to preserve, so only the chronological split modes build them. leave_last_out and temporal payloads carry x_train_sequences together with train_source_sequences, val_source_sequences and test_source_sequences; user_split and item_split carry none, and loading a checkpoint built by those modes — or one built before sequences existed — yields None for each key. Every matrix model still loads complete.

Each sequence addresses the same rows and columns as the matrix it accompanies, per stage: a temporal split gives each window its own catalog, so the relationship holds within a stage rather than across the checkpoint.

from compresso_recsys import load_recsys_split, read_checkpoint

with read_checkpoint("ml1m-llo") as root:
    split = load_recsys_split(root)

sequences = split["x_train_sequences"]
sequences.n_rows              # rows, matching split["x_train"].shape[0]
sequences.n_items             # catalog width, matching its column space
sequences.row(0)              # one history, in order, repeats intact
sequences.row_lengths         # events per row
sequences.take_rows(0, 256)   # a contiguous batch
sequences.select_rows([7, 2]) # an arbitrary selection, for shuffling