Generation Probability (Pgen) API¶
Lightweight generation-probability estimator for TCR β/α CDR3 sequences. Used to flag likely-public CDR3s so DB matches against them can be appropriately discounted (#58).
Why a proxy, not OLGA¶
The gold-standard tool is OLGA (Sethna 2019), which fits explicit V/D/J/insertion models and computes Pgen by dynamic programming. OLGA gives the right answer but ships ~30 MB of pre-trained model files plus a runtime dependency chain.
Most tcrsift use cases (publicness discounting, ranking convergent vs. private sequences) just need the rank order, not numerically calibrated Pgens. This module provides that proxy in ~250 lines with no runtime dependency beyond numpy.
Decomposition¶
log Pgen(CDR3, V, J, chain)
≈ log P(V) # V-gene usage marginal
+ log P(J) # J-gene usage marginal
+ log P(length) # CDR3 AA length, per chain
+ log P(n_inserted) # total non-templated nt
+ log P(CDR3 | length) # uniform-AA composition baseline
The N-insertion term is the largest contributor to publicness: sequences with 0 non-templated nucleotides are convergently generatable across donors and dominate public-match noise. We model N-insertion length as geometric per junction (β has two junctions, α has one); the total N-insertion count is estimated from observed CDR3 nt length minus a typical templated V+J+D contribution.
Calibration¶
The estimator runs ~10 log units lower than real OLGA Pgens because
of the uniform-over-20 AA composition baseline. Default
publicness_score cutoffs are calibrated to this estimator's
scale (-30 / -18); pass OLGA cutoffs (-20 / -8) if feeding in
real OLGA values, or use auto_quantile=True for distribution-based
auto-calibration.
Usage¶
from tcrsift.pgen import annotate_publicness
clones = annotate_publicness(clones)
public = clones[clones["publicness"] >= 0.5]
print(f"{len(public)} likely-public clones; DB matches should be discounted")
Wired into annotate_clonotypes:
from tcrsift import annotate_clonotypes
annotated = annotate_clonotypes(
clones, vdjdb_path="vdjdb.tsv", add_publicness=True,
)
pgen ¶
Lightweight Pgen estimator for TCR β / α CDR3 sequences (#58).
The gold-standard generation-probability tool is OLGA (Sethna et al. 2019), which fits V/D/J/insertion models and computes Pgen by dynamic programming. OLGA gives the right answer but ships ~30 MB of pre-trained model files plus a runtime dependency chain.
Most tcrsift use cases (publicness discounting in DB annotation, ranking convergent vs. private sequences) just need a coarse Pgen proxy — values that rank sequences from "highly generatable" to "clearly private", not necessarily numerically calibrated. This module provides that proxy in <200 lines, with no runtime dependency beyond numpy.
Decomposition:
log Pgen(CDR3, V, J, chain)
≈ log P(V) # gene-usage marginal
+ log P(J) # gene-usage marginal
+ log P(length) # CDR3 AA length, per chain
+ log P(n_inserted) # estimated total non-templated nt
+ log P(CDR3 | length) # length-normalized AA-composition term
For an observed sequence, n_inserted is estimated from the CDR3
nucleotide length minus a typical templated boundary contribution
(see :data:_TEMPLATED_NT_TYPICAL). When the caller doesn't have
nucleotide-level data, we approximate from AA length.
Marginal parameters bundled inline (not as external data files) — they're small (<3 KB total), and inlining keeps the module self-contained and easy to audit. Sources are documented per-table.
For real Pgens use OLGA; for ranking/discounting :func:compute_pgen
is sufficient.
pgen_single ¶
pgen_single(cdr3_aa: str, v_gene: str | None, j_gene: str | None, *, chain: str = 'beta', n_inserted: int | None = None) -> float
Return log10 Pgen estimate for one sequence.
Source code in tcrsift/pgen.py
pgen_components ¶
pgen_components(cdr3_aa: str, v_gene: str | None, j_gene: str | None, *, chain: str = 'beta', n_inserted: int | None = None) -> dict[str, float]
Decompose log10 Pgen into its five terms.
Useful for debugging: shows which term is driving a sequence toward "public" or "private".
Source code in tcrsift/pgen.py
compute_pgen ¶
compute_pgen(df: DataFrame, *, cdr3_col: str = 'CDR3_beta', v_gene_col: str = 'beta_v_gene', j_gene_col: str = 'beta_j_gene', chain: str = 'beta', n_inserted_col: str | None = None, output_col: str = 'log10_pgen') -> pd.Series
Compute log10 Pgen estimate for each row in df.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
df
|
DataFrame
|
Per-clone frame with at least the |
required |
cdr3_col
|
str
|
Column with CDR3 amino-acid sequence. |
'CDR3_beta'
|
v_gene_col
|
str
|
Columns with V / J gene calls (allele suffix tolerated). |
'beta_v_gene'
|
j_gene_col
|
str
|
Columns with V / J gene calls (allele suffix tolerated). |
'beta_v_gene'
|
chain
|
str
|
|
'beta'
|
n_inserted_col
|
str | None
|
Optional column with pre-computed total N-insertion length
(nt). When None, estimated from CDR3 AA length and a typical
templated contribution from :data: |
None
|
output_col
|
str
|
Name applied to the returned Series. |
'log10_pgen'
|
Returns:
| Type | Description |
|---|---|
Series
|
log10 Pgen estimate per row, indexed like |
Source code in tcrsift/pgen.py
269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 | |
publicness_score ¶
publicness_score(log10_pgen: Series | Iterable[float], *, low_pgen_cutoff: float | None = -30.0, high_pgen_cutoff: float | None = -18.0, auto_quantile: bool = False, quantile_low: float = 0.1, quantile_high: float = 0.9) -> pd.Series
Map log10 Pgen → [0, 1] publicness in a monotone-decreasing way.
log10 Pgen ≥ high_pgen_cutoff(very generatable) → 1.0log10 Pgen ≤ low_pgen_cutoff(very private) → 0.0- linear interpolation in between.
A high publicness score means the sequence is likely to be public and DB matches against it should be discounted.
Defaults are calibrated to this module's coarse Pgen estimator, not OLGA. Typical β CDR3s land around log10 ≈ −25; very public short sequences with canonical V/J around log10 ≈ −18. The estimator is uniformly more negative than OLGA because its AA composition term is uniform-over-20 rather than position-specific. Override the cutoffs if you're feeding in real OLGA values (use ~−20 / −8 for OLGA).
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
log10_pgen
|
Series | Iterable[float]
|
Per-row log10 Pgen estimates. When a Series, its index is preserved in the returned Series. |
required |
low_pgen_cutoff
|
float | None
|
Fixed cutoffs. Ignored when |
-30.0
|
high_pgen_cutoff
|
float | None
|
Fixed cutoffs. Ignored when |
-30.0
|
auto_quantile
|
bool
|
When True, derive cutoffs from the input distribution rather than a fixed pair. Useful when the input's Pgen scale is unknown or when the fixed cutoffs would saturate the score for most rows (e.g. a narrow distribution all within ~5 log units). |
False
|
quantile_low
|
float
|
Quantile cutoffs used when |
0.1
|
quantile_high
|
float
|
Quantile cutoffs used when |
0.1
|
Source code in tcrsift/pgen.py
annotate_publicness ¶
annotate_publicness(df: DataFrame, *, cdr3_col: str = 'CDR3_beta', v_gene_col: str = 'beta_v_gene', j_gene_col: str = 'beta_j_gene', chain: str = 'beta', pgen_col: str = 'log10_pgen', publicness_col: str = 'publicness') -> pd.DataFrame
Augment df with log10_pgen + publicness columns.
Convenience wrapper combining :func:compute_pgen and
:func:publicness_score. Does not modify the input frame.