In-silico filter layer¶
A stackable filter layer (#149) that composes on top of the assay/enrichment groups (the sorts) and the selection rules. Each layer is a per-clone percentile predicate on an existing feature — Ppost (#143) or a GEX signature score (signatures) — so a group can be progressively refined:
CTYneg
CTYneg · Ppost^low
CTYneg · Ppost^low · GEXnaive^low
CTYneg · Ppost^low · GEXnaive^low · GEXresponse^high
Applying the full stack yields one named composite subgroup (default
label IS, for in-silico), so each assay group gains a filtered twin —
CTYneg and CTYneg+IS. N assay groups → 2N groups, and the twins feed
the cross-group Jaccard-overlap analysis.
Scoring conventions¶
- Each predicate is a lower-is-better percentile rank in
[0, 1](0 = best).direction="high"flips it so high scores win (e.g.GEXresponse^high→−antigen_response). - Tie handling keeps
Ppost == 0a valid best value and groups all equal scores together (rank(method="average"/"min")) — never dropped. - A clone passes a predicate when its percentile rank ≤ the predicate's
percentile. A missing score never passes. - With a group column, ranks are computed within each assay group, so the filter refines each group against its own distribution.
- The composite's own ranking metric is the average of the per-dimension percentile ranks → top-k or a percentile cut.
Config¶
insilico_filter:
label: IS
predicates:
- {score: log10_ppost, direction: low, percentile: 0.5}
- {score: signature_naive_stem, direction: low, percentile: 0.5}
- {score: signature_antigen_response, direction: high, percentile: 0.5}
Usage¶
from tcrsift.insilico_filter import (
predicates_from_config, apply_insilico_filter, insilico_overlap_long,
)
preds = predicates_from_config(config.insilico_filter)
# Annotate each clone with pass-flag, avg rank, and composite group name:
clones = apply_insilico_filter(clones, preds, group_col="method", label="IS")
# Jaccard overlap across base groups AND their +IS twins:
mats = insilico_overlap_long(clones, preds, group_col="method")
jaccard = mats["jaccard"] # e.g. does AIMpos+IS overlap tetpos+IS more?
insilico_filter ¶
Composable in-silico filter layer (#149).
A stackable filter layer that composes on top of the assay/enrichment groups (the sorts) and the selection rules. Each layer is a per-clone percentile predicate on an existing feature — Ppost (#143) or a GEX signature score (#141/#142/#144) — so a group can be progressively refined:
CTYneg
CTYneg · Ppost^low
CTYneg · Ppost^low · GEXnaive^low
CTYneg · Ppost^low · GEXnaive^low · GEXresponse^high
Applying the full stack yields one named composite subgroup (default
label IS for in-silico), so each assay group gains a filtered twin:
CTYneg and CTYneg+IS — N assay groups → 2N groups. These twins feed
the cross-group Jaccard-overlap analysis (:func:insilico_overlap_long) so
filtered subsets are directly comparable across assays.
Scoring conventions (from the pilot):
- Each predicate is a lower-is-better percentile rank in
[0, 1](0 = best).direction="high"flips the rank so high scores are best. - Tie handling keeps
Ppost == 0a valid best value and groups all equal scores together (pandasrankwithmethod="average"/"min") — never dropped. - A clone passes a predicate when its percentile rank ≤ the predicate's
percentile(the best fraction). Missing scores never pass. - Ranks are computed within each assay group when a group column is given, so the filter refines each group against its own distribution.
FilterPredicate
dataclass
¶
One in-silico filter dimension.
score is a per-clone column; direction is "low" (low score
is better, e.g. Ppost^low) or "high" (high is better, e.g.
GEXresponse^high); percentile is the best fraction kept
(0.5 → best half). rank_method is the pandas tie method.
Source code in tcrsift/insilico_filter.py
predicates_from_config ¶
Parse the insilico_filter.predicates list into predicates.
Each entry is a dict {score, direction?, percentile?, rank_method?}.
Source code in tcrsift/insilico_filter.py
percentile_rank ¶
Per-clone percentile rank in [0, 1] (0 = best). NaN-safe.
Lower values rank best when lower_is_better (e.g. Ppost). Ties are
grouped via pandas rank(method=...) so equal scores — including a
cluster of Ppost == 0 — share a rank rather than being dropped or
arbitrarily ordered. NaN stays NaN (and never passes a predicate).
Source code in tcrsift/insilico_filter.py
insilico_mask ¶
insilico_mask(df: DataFrame, predicates: list[FilterPredicate], *, group_col: str | None = None) -> pd.Series
Boolean Series: clones passing all predicates (composite members).
A clone passes a predicate when its percentile rank ≤ the predicate's
percentile. Clones with a missing score for any predicate are
excluded (a NaN rank never satisfies ≤). When group_col is set,
ranks are computed within each group so the filter refines each assay
group against its own distribution.
Source code in tcrsift/insilico_filter.py
average_percentile_rank ¶
average_percentile_rank(df: DataFrame, predicates: list[FilterPredicate], *, group_col: str | None = None, weights: list[float] | None = None, require_complete: bool = True) -> pd.Series
(Weighted) mean of the per-dimension percentile ranks (0 = best).
The single row-wise composite engine for in-silico ranking and PRISM. Each
predicate contributes a lower-is-better percentile rank in [0, 1]; the means
are averaged (optionally weighted by weights).
require_complete (default True) makes a row missing ANY dimension get
NaN — i.e. don't score a clone you can't fully rank. Set False for a
NaN-aware mean over the dimensions that ARE present.
Source code in tcrsift/insilico_filter.py
apply_insilico_filter ¶
apply_insilico_filter(df: DataFrame, predicates: list[FilterPredicate], *, group_col: str | None = None, label: str = DEFAULT_LABEL, pass_col: str = 'insilico_pass', rank_col: str = 'insilico_avg_rank', composite_col: str = 'insilico_group', group_sep: str = DEFAULT_GROUP_SEP) -> pd.DataFrame
Annotate df with in-silico pass flag, avg rank, and composite name.
Returns a copy with three added columns:
pass_col— bool, passes all predicates (composite membership).rank_col— average percentile rank (0 = best), for top-k ranking.composite_col— for passing rows,"<group>+<label>"(or justlabelwhen nogroup_col); NaN for non-passing rows.
Does not mutate the input.
Source code in tcrsift/insilico_filter.py
expand_insilico_twins ¶
expand_insilico_twins(df: DataFrame, predicates: list[FilterPredicate], *, group_col: str, label: str = DEFAULT_LABEL, out_group_col: str = 'overlap_group', group_sep: str = DEFAULT_GROUP_SEP) -> pd.DataFrame
Long table where each assay group gains a filtered +label twin.
Every input row keeps its base group in out_group_col; rows passing
the in-silico filter are duplicated with out_group_col set to
"<group>+<label>". N base groups → up to 2N groups, ready for
:func:tcrsift.clonotype.compute_sample_overlap_matrices with
sample_col=out_group_col.
Source code in tcrsift/insilico_filter.py
insilico_overlap_long ¶
insilico_overlap_long(df: DataFrame, predicates: list[FilterPredicate], *, group_col: str, clone_col: str = 'CDR3ab', cells_col: str = 'cells', label: str = DEFAULT_LABEL, group_sep: str = DEFAULT_GROUP_SEP) -> dict[str, pd.DataFrame]
Jaccard-overlap matrices across base groups and their +label twins.
Expands twins via :func:expand_insilico_twins, then computes the
standard clone-set and cell-weighted Jaccard matrices over the combined
group axis. The result lets you ask whether AIMpos+IS overlaps
tetpos+IS more than the unfiltered versions do.