Transcript Mapping

Strand-aware, locus-based mapping of GT to prediction transcripts.

This module provides the core logic for associating ground-truth (GT) and predicted transcripts from GFF/GTF files using a gffcompare-style algorithm:

  1. Locus grouping — GT transcripts are clustered into loci (sets of mutually reachable overlapping transcripts) by an O(n log n) coordinate sweep.

  2. Junction-F1 scoring — every predicted transcript in a locus is scored against all GT transcripts using a continuous junction-F1 metric: 2 × |shared junctions| / (|pred junctions| + |ref junctions|). Single-exon fallback: reciprocal overlap fraction.

  3. Optimal 1:1 assignment — the Hungarian algorithm (scipy.optimize.linear_sum_assignment) maximises the total junction-F1 across all (GT, pred) pairs in a locus. Each GT transcript and each prediction is assigned at most once per predictor.

Two locus matching modes are supported (see LocusMatchingMode):

  • FULL_DISCOVERY — maximise the number of 1:1 matches per locus. Unmatched GT transcripts yield null-prediction FN pairs; unmatched predictions yield null-GT FP pairs. Best for multi-isoform tools.

  • BEST_PER_LOCUS — keep only the single highest-scoring (GT, pred) pair per locus and drop all others. Best for single-transcript tools (e.g. Augustus).

Public API

class gene_calling_benchmark.transcript_mapping.LocusMatchingMode(*values)[source]

Bases: str, Enum

Controls how GT and prediction transcripts are paired within a locus.

FULL_DISCOVERY

All GT transcripts participate; the optimal set of 1:1 matches (maximising total junction-F1) is computed via the Hungarian algorithm. Unmatched GT transcripts contribute null-prediction FN pairs; unmatched predictions contribute null-GT FP pairs.

Use this mode for multi-isoform tools (Helixer, SegmentNT, …).

BEST_PER_LOCUS

Every GT locus is scored exactly once per predictor, so a predictor is never rewarded for emitting more isoforms nor penalised for emitting fewer than the reference. The per-transcript denominator is therefore the full set of GT loci and is directly comparable across predictors. Each (locus, predictor) pair resolves to one of three entries:

  • Case A — match. The predictor’s single highest-scoring (GT, pred) pair in the locus is kept; all other isoforms and predictions in that locus are dropped.

  • Case C — overlap, wrong structure. The predictor matched nothing in the locus but has a transcript overlapping it (no shared junction). That prediction is paired against the real GT isoform it best overlaps (junction_f1 = 0), so a confidently-wrong call is penalised on its coinciding bases rather than silently dropped.

  • Case B — clean miss. The predictor has no transcript overlapping the locus at all: a locus-level FN against the representative (longest) isoform, paired with a null prediction.

Use this mode for single-transcript tools (Augustus, …).

Each predictor accumulates independently, and Case C only fires for a predictor that matched nothing in the locus, so the GT isoform it scores against is never also counted in a Case A entry for that same predictor — no GT base is double-counted within a predictor’s per-transcript numbers.

Precision note: predictions that overlap no GT locus become intergenic FP entries (null GT). But an extra prediction that overlaps a locus the predictor already matched (Case A) is dropped from the per-transcript view — counting it would force a second entry for that locus and double-count the GT bases the matched pair already covers. Those dropped bases are still charged by the global nucleotide precision (union over all predicted bases), so per-transcript precision can read slightly optimistically relative to the global numbers; pair the two views. Scoring every overlapping isoform is what FULL_DISCOVERY is for.

class gene_calling_benchmark.transcript_mapping.MatchClass(*values)[source]

Bases: str, Enum

Structural relationship between a predicted and a GT transcript.

Listed in priority order (highest quality first).

EXACT = 'exact'

Identical intron chains — every splice site matches exactly.

CONTAINED = 'contained'

Pred intron chain ⊆ GT intron chain and pred span ⊆ GT span.

CONTAINS = 'contains'

GT intron chain ⊆ pred intron chain and GT span ⊆ pred span.

SHARED_JUNCTION = 'shared_junction'

At least one shared splice junction between GT and prediction.

OVERLAPPING = 'overlapping'

Coordinate overlap only — no shared junctions (includes single-exon).

class gene_calling_benchmark.transcript_mapping.PredictionMatch(**data)[source]

Bases: BaseModel

A single predicted transcript matched to a GT transcript.

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

Parameters:
model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

base_overlap: int

Number of overlapping bases between GT and prediction spans.

junction_f1: float

Continuous junction-F1 score used for assignment (0–1).

class gene_calling_benchmark.transcript_mapping.TranscriptMapping(**data)[source]

Bases: BaseModel

Association between one GT transcript and its matched predictions.

Parameters:
seqid

Chromosome / scaffold identifier.

Type:

str

strand

'+' or '-'.

Type:

str

gt_id

Ground-truth transcript identifier. For unmatched predictions this starts with _UNMATCHED_PRED_PREFIX.

Type:

str

gt_start, gt_end

Genomic coordinates of the evaluation window (1-based, inclusive).

Type:

int

is_unmatched_prediction

True when no GT transcript was assigned to this prediction. The GT array is null (all-background) for unmatched predictions.

Type:

bool

matched_predictions

Per-predictor matches. Empty when no predictor was matched to this GT transcript.

Type:

list[PredictionMatch]

fn_for_predictors

When set, this is a BEST_PER_LOCUS Case-B (clean miss) entry that counts as a miss only for the named predictor(s) — those with no prediction overlapping the locus. Other predictors skip it, so every GT locus is counted once per predictor without charging a predictor that matched (Case A) or overlapped (Case C) the same locus.

Type:

list[str]

Create a new model by parsing and validating input data from keyword arguments.

Raises [ValidationError][pydantic_core.ValidationError] if the input data cannot be validated to form a valid model.

self is explicitly positional-only to allow self as a field name.

model_config: ClassVar[ConfigDict] = {'frozen': True}

Configuration for the model, should be a dictionary conforming to [ConfigDict][pydantic.config.ConfigDict].

gene_calling_benchmark.transcript_mapping.map_transcripts(gt_path, pred_paths, label_config=None, *, gt_df=None, pred_dfs=None, transcript_types=None, gt_feature_role_map=None, pred_feature_role_maps=None, exclude_features=None, locus_matching_mode=LocusMatchingMode.FULL_DISCOVERY)[source]

Map GT transcripts to predicted transcripts across multiple predictors.

Uses a gffcompare-style locus-based algorithm:

  1. Group GT transcripts into loci by coordinate overlap.

  2. Score each predicted transcript against all GT transcripts in the same locus using continuous junction-F1.

  3. Assign 1:1 pairings via the Hungarian algorithm (FULL_DISCOVERY) or take the single best pair (BEST_PER_LOCUS).

Parameters:
  • gt_path (str | Path) – Path to the ground-truth GFF/GTF file.

  • pred_paths (dict[str, str | Path]) – {predictor_name: path} for each prediction file.

  • label_config (LabelConfig | None) – Label semantics used for later array construction. Needed here so feature-role maps can be validated against the active annotation mode. When omitted, mapping falls back to EXON_INTRON defaults.

  • gt_df (DataFrame | None) – Pre-parsed GT GFF DataFrame. When given, gt_path is not re-parsed (avoids a redundant parse when the caller already has the DataFrame).

  • pred_dfs (dict[str, DataFrame] | None) – Pre-parsed prediction DataFrames keyed by predictor name. When given, pred_paths is not re-parsed.

  • transcript_types (list[str] | None) – Feature types that define transcript boundaries. Defaults to DEFAULT_TRANSCRIPT_TYPES.

  • gt_feature_role_map (dict[str, str] | None) – Maps GT GFF/GTF feature types to benchmark roles. When None, the mode-specific default is used.

  • pred_feature_role_maps (dict[str, str] | dict[str, dict[str, str]] | None) – Per-predictor feature-role maps. None means use the GT role map.

  • exclude_features (list[str] | None) – Feature types to ignore entirely (e.g. ["gene"]).

  • locus_matching_mode (LocusMatchingMode) – Assignment strategy. FULL_DISCOVERY (default) maximises the number of 1:1 matches per locus. BEST_PER_LOCUS keeps only the single best-scoring pair per locus and drops the rest.

Returns:

In FULL_DISCOVERY mode: one entry per GT transcript (including unmatched) plus sentinel entries for unmatched predictions. In BEST_PER_LOCUS mode: only matched pairs are returned.

Return type:

list[TranscriptMapping]

gene_calling_benchmark.transcript_mapping.build_paired_arrays(mapping, gt_df, pred_dfs, label_config, transcript_types=None, gt_feature_role_map=None, pred_feature_role_maps=None, *, _gt_index=None, _pred_indices=None, _gt_role_map=None, _pred_role_maps=None)[source]

Build the GT and per-predictor annotation arrays for one mapping.

GT array

  • Matched GT transcript → built from that transcript’s child features.

  • Unmatched prediction (is_unmatched_prediction=True) → null array (all-background), representing a pure FP with no reference signal.

Prediction arrays

  • Predictor has a match → built from the matched prediction’s child features (transcript-specific, not a region union).

  • Predictor has no match for this GT → null array (all-background), representing a missed transcript.

Using transcript-specific arrays (rather than region-based union) ensures that each matched pair is evaluated in isolation: coverage from other transcripts in the same locus does not contaminate either array.

Parameters:
Returns:

(gt_array, {predictor_name: pred_array}). The dict contains an entry for every predictor in pred_dfs.

Return type:

tuple[ndarray, dict[str, ndarray]]

Notes

The leading-underscore keyword parameters are a private fast path for callers that build arrays in a loop (e.g. the pipeline): pre-built (seqid, parent) indices and pre-normalized role maps are reused across mappings instead of being rebuilt on every call. Stand-alone callers omit them and the per-call build/normalize path runs as before.

gene_calling_benchmark.transcript_mapping.export_mapping_table(mappings, output_path)[source]

Write the mapping list to a human-readable TSV for debugging.

Columns: seqid, strand, gt_id, gt_start, gt_end, is_unmatched_prediction, predictor, pred_id, pred_start, pred_end, match_class, base_overlap, junction_f1.

Each row represents one GT-prediction pair. GT transcripts with multiple matched predictions produce multiple rows. GT transcripts with no matches produce a single row with empty prediction columns.

Parameters:
Returns:

The path the file was written to.

Return type:

Path