Channels: reading a signature#

Twenty names that turn “the model found something” into a sentence with a noun in it.

A signature is a wide vector — 152, 688 or 1,403 columns depending on the tier. Wide vectors score well and explain badly. Channels are the interpretive layer over them: a small, fixed, purely structural vocabulary, the same for every sample anyone emits, in which a finding can be stated and compared across labs.

What a channel is#

A signature column is named <sig>:<channel>:<locus>:<feature>. The second field is the channel: the named group of columns that measures one thing.

vsig : div : TRB : 1D_c
──┬──  ─┬─   ─┬─   ──┬──
  │     │     │      └── feature — the individual number
  │     │     └───────── locus
  │     └─────────────── channel — WHAT IS BEING MEASURED
  └───────────────────── half: statistics (vsig) or geometry (rsig)

Channels are disjoint and exhaustive: every emitted column belongs to exactly one, so a set of per-channel shares sums over the whole vector with nothing left over. That is what makes them usable as an accounting unit rather than a filing convenience.

The vocabulary is a property of the contract, not of any corpus or any cohort. The same twenty names describe every sample anyone emits, at every tier, in every locus. Two labs therefore compare not just numbers but findings.

Why the vocabulary exists#

Classical repertoire analysis answers “which summary statistic separates my groups?” by running a fixed menu of named statistics side by side — diversity, clonality, junction length, V usage — one test per statistic, and reading off which one moved. The menu is the explanation: every number has a name, so a result is a sentence.

A wide feature vector scores better and explains worse. signature() returns 688 anonymous columns; a model trained on them reports “it separates the groups”, which has no noun in it. The channel vocabulary restores the noun without giving up the vector:

without channels   the classifier reaches AUC 0.78
with channels      the classifier reaches AUC 0.78, carried by IGH diversity and
                   IGH isotype composition; junction length adds nothing

The second sentence is a hypothesis someone can test with a different assay. The first is not.

The twenty channels#

Printable at any time, from the library or the command line, reading no input at all:

mir signature --channels                 # one row per channel, and what it measures
mir signature --channels --tier full        # sized for the tier you will emit

Channel

Half

What it measures

mask

vsig

Which loci and which statistics this sample can support at all.

qc

vsig

How far the annotation had to reach: unrecognised V/J calls, non-standard residues.

depth

vsig

How much was sequenced — reads, observed richness, unseen-species mass.

div

vsig

Diversity at a fixed coverage level: Hill numbers, clonality, d50.

clon

vsig

Clonal dominance — the size of the largest clones as a share of the sample.

len

vsig

Junction length distribution: mean, spread, asymmetry.

pair

vsig

Relative yield between loci from one library — the α/β, γ/δ and B/T ratios.

iso

vsig

Isotype composition of the IGH repertoire (class-switch state).

shm

vsig

Somatic hypermutation load — mean V identity to germline.

pgen

vsig

How typical the repertoire’s rearrangements are under the V(D)J model.

kmer

vsig

V-gene-and-junction k-mer composition, projected onto a frozen basis.

aa

vsig

Amino-acid composition of the junction.

pchem

vsig

Physicochemical profile of the junction (charge, hydrophobicity, bulk).

depth

rsig

Depth as the geometry sees it: effective clone count and observed mass.

div

rsig

Sequence-aware dispersion — Rao entropy, which a Hill number cannot express.

band

rsig

Shares of the repertoire held by clone-size bands and by isotype.

contrast

rsig

Signed deviation from unselected V(D)J output — the selection imprint.

phiv / phij / phic

rsig

Where the repertoire sits in V-gene, J-gene and junction coordinates.

Two channel names appear on both halves — depth and div — and they are different measurements of the same idea, not duplicates. vsig:div is a Hill number of the clone-size vector; rsig:div is Rao entropy in embedding coordinates, which can see that two clonotypes are one substitution apart. A channel key therefore always carries its half: vsig:div, never div.

Attributability: which channels can name clonotypes#

Every channel declares, at build time, whether it has a clonotype pre-image — whether “which clones drive this” is a well-posed question.

  • rsig:contrast, rsig:phiv, rsig:phij, rsig:phic are attributable. They are linear functionals of a kernel-mean over clonotypes, so the question has an answer and the library will compute it.

  • Everything else is not. A Hill number is a summary of a distribution and a read fraction is a ratio of totals; neither is a sum over clonotypes, so asking which clones drive it is a category error rather than an unanswered question.

This is declared, never inferred from the name. Asking an unattributable channel for its drivers raises instead of returning a plausible-looking list.

Per-locus channels#

A channel spans loci — vsig:div is the diversity channel of all seven. Ask for the finer grouping when the locus is part of the finding, which it usually is:

from mir.signature import channel_spec, channels

channels("standard")                      # {"vsig:div": [21, 22, ...], ...}
channel_spec("standard", per_locus=True)  # keys are "vsig:div:TRB", ...

“IGH diversity moved and TRB diversity did not” is a different claim from “diversity moved”, and usually the more useful one.

Using a channel map#

channel_spec() hands mir.explain the name → column-index map that a bare matrix does not carry. An ablation against any scorer of yours then reports channel names rather than column indices:

from mir.explain import channel_report
from mir.signature import channel_spec, signature_cohort

F = signature_cohort(samples, tier="standard")
X = F.drop("sample_id").to_numpy()
spec = channel_spec("standard", columns=F.columns[1:])

rep = channel_report(X, spec, lambda B: cv_auc(B, y), base=0.5, mode="both")
rep.frame()        # one row per channel: score, delta_in, delta_out, rank
rep.best           # -> "vsig:div"

The scorer is yours — a cross-validated AUC, a Cox C-index, anything that maps a column block to a number where higher is better. The library never sees your labels and ships no scorers of its own, because the model choice belongs to the analysis.

For an attributable channel, the last hop goes all the way back to sequences:

from mir.explain import channel_drivers

channel_drivers(space, samples, y, spec, "rsig:phic")   # the clonotypes behind the channel

On an unattributable channel that call raises — see above.

Read the two deltas together:

delta_in

Does this channel carry signal on its own? Marginal, so it is inflated by correlation between channels — two redundant channels both look important.

delta_out

Is this channel’s signal anywhere else? Conditional, and deflated by the same correlation.

High in / high out means irreplaceable. High in / near-zero out means redundant — the signal is duplicated elsewhere in the vector, which is itself a finding.

Channels and the reference#

Channels are orthogonal to which scale reference you use. Choosing blood over deep-tcr changes the location and scale a column is standardised against; it does not move a column into a different channel, add a channel, or remove one. The vocabulary is fixed by the contract, and the contract is frozen — see The portable repertoire signature for what is fitted on data and what is not.