This vignette walks through the v0.3.0 audit framework — a
transparent evidence aggregation layer for PDAC early-marker hypothesis
prioritization. The framework summarises each gene along three
evidence axes (evidence_strength,
biological_coherence,
translational_relevance), applies two reliability
gates (leakage_gate,
heterogeneity_gate), and assigns it to one of five
audit classes (high_confidence,
supported_uncertain, penalized,
excluded, low). Unlike a supervised biomarker
predictor, the rule combines pre-specified weights with deterministic
gating and Monte Carlo uncertainty propagation, exposing cohort
heterogeneity, layer agreement, and artifact risk.
PDAC early-stage diagnosis lacks a validated ground-truth biomarker beyond CA19-9 (Mehta et al., BJS Open 2024 prediagnostic meta-analysis). Training a supervised classifier on community-curated markers risks two well-known failure modes:
pdactrace’s own pipeline outputs (e.g., the phase29
resectable list) as labels would teach the model to reproduce its own
heuristic.The audit framework therefore declines to predict and instead aggregates, ranks, and reports uncertainty. External anchors are reserved for post-freeze enrichment evaluation only.
# 1. Build a per-gene evidence graph (nodes = layers / multipliers / readouts)
g <- build_evidence_graph("LTBP1")
g$nodes
#> node_id layer node_type value_numeric weight
#> <char> <char> <char> <num> <num>
#> 1: gene gene target NA NA
#> 2: layer_presence aggregate positive_feature 1.0000000 0.2
#> 3: direction_agreement aggregate positive_feature 0.7500000 0.2
#> 4: early_pattern rna positive_feature 0.4443528 0.2
#> 5: serum_bridge serum positive_feature 0.7000000 0.1
#> 6: rescue_signal cross_layer positive_feature 1.0000000 0.1
#> 7: leakage_gate artifact multiplier 1.0000000 NA
#> 8: heterogeneity_gate cohort multiplier 0.7000000 NA
#> 9: audit_score readout readout 0.5770248 NA
#> value_label available
#> <char> <lgcl>
#> 1: LTBP1 TRUE
#> 2: 4/4 layers TRUE
#> 3: RNA/protein concordant: yes TRUE
#> 4: Early_Burst_Up TRUE
#> 5: serum detected TRUE
#> 6: eligible TRUE
#> 7: no leakage flag TRUE
#> 8: max I2 75.2, multiplier 0.70 TRUE
#> 9: audit_score 0.577 TRUE
# 2. Extract the deterministic feature components (used internally to
# compose the three axes; mapping table is in supplement S1)
extract_graph_features("LTBP1")
#> Key: <gene_symbol>
#> gene_symbol score_layer score_direction score_early score_serum score_rescue
#> <char> <num> <num> <num> <num> <int>
#> 1: LTBP1 1 0.75 0.4443528 0.7 1
#> positive_score leakage_mult het_mult audit_score_raw audit_score is_hk
#> <num> <num> <num> <num> <num> <lgcl>
#> 1: 0.7840117 1 0.7 0.4262094 0.5770248 FALSE
#> is_plasma_hi rescue_eligible layer_count cross_layer_concord
#> <lgcl> <lgcl> <int> <lgcl>
#> 1: FALSE TRUE 4 TRUE
# 3. Compute the 3-axis + 2-gate audit score and audit class
compute_audit_score("LTBP1")
#> Key: <gene_symbol>
#> gene_symbol evidence_strength biological_coherence translational_relevance
#> <char> <num> <num> <num>
#> 1: LTBP1 1 0.5971764 0.7
#> leakage_gate heterogeneity_gate positive_score audit_score
#> <num> <num> <num> <num>
#> 1: 1 0.7 0.7840117 0.5770248
#> audit_class
#> <char>
#> 1: supported_uncertain
# 4. Propagate uncertainty by Monte Carlo (N=200 here for speed)
propagate_uncertainty("LTBP1", n_mc = 200, seed = 42)
#> gene_symbol audit_score_median audit_score_lo95 audit_score_hi95
#> <char> <num> <num> <num>
#> 1: LTBP1 0.5841692 0.2508913 0.8640965
#> uncertainty_width rank_median rank_lo95 rank_hi95 confidence_class
#> <num> <num> <num> <num> <char>
#> 1: 0.6132052 136.5 3 2813.025 high_uncertain
# 5. Evaluate anchor enrichment in the top-N audit-ranked genes
evaluate_anchor_enrichment(top_n = c(50, 100, 500), tier = "secondary")
#> tier tier_key top_n n_anchor_declared n_anchor_in_atlas hits
#> <char> <char> <int> <int> <int> <int>
#> 1: SECONDARY (T1+T2) secondary 50 28 18 4
#> 2: SECONDARY (T1+T2) secondary 100 28 18 7
#> 3: SECONDARY (T1+T2) secondary 500 28 18 9
#> expect fold pval
#> <num> <num> <num>
#> 1: 0.08899436 44.94667 1.537448e-06
#> 2: 0.17798873 39.32833 2.175894e-10
#> 3: 0.88994364 10.11300 5.360423e-08
# 6. Curate a transparent case-study label table
case_study(c("LTBP1", "GAPDH", "THBS2", "ALB", "AMBP", "SERPINA1"))
#> gene_symbol rna_pattern prot_pattern scrna_origin serum_support
#> <char> <char> <char> <char> <char>
#> 1: LTBP1 Early_Burst_Up Early_Burst_Up myCAF B
#> 2: GAPDH <NA> Early_Burst_Up <NA> B
#> 3: THBS2 Early_Burst_Up Early_Burst_Up myCAF <NA>
#> 4: ALB Early_Trough Early_Burst_Up <NA> <NA>
#> 5: AMBP Early_Trough Early_Burst_Up <NA> <NA>
#> 6: SERPINA1 Early_Burst_Up Early_Burst_Up Macrophage A
#> max_I2_meta evidence_strength biological_coherence translational_relevance
#> <num> <num> <num> <num>
#> 1: 75.16350 1.00 0.5971764 0.7
#> 2: 51.79413 0.50 0.0000000 0.4
#> 3: 40.63102 0.75 0.8645668 0.3
#> 4: 56.84316 0.50 0.7335634 0.0
#> 5: 36.68052 0.50 0.5311409 0.0
#> 6: 85.07961 1.00 0.6295406 1.0
#> leakage_gate heterogeneity_gate positive_score audit_score
#> <num> <num> <num> <num>
#> 1: 1.0 0.7 0.7840117 0.5770248
#> 2: 0.0 1.0 0.3000000 0.0000000
#> 3: 1.0 1.0 0.6775984 0.7124366
#> 4: 0.5 1.0 0.4567472 0.2401153
#> 5: 1.0 1.0 0.3858993 0.4057401
#> 6: 1.0 0.7 0.8703392 0.6405610
#> audit_class is_hk is_plasma_hi rescue_eligible audit_score_median
#> <char> <lgcl> <lgcl> <lgcl> <num>
#> 1: supported_uncertain FALSE FALSE TRUE 0.5884497
#> 2: excluded TRUE FALSE FALSE 0.0000000
#> 3: high_confidence FALSE FALSE FALSE 0.7173547
#> 4: penalized FALSE TRUE FALSE 0.2346449
#> 5: supported_uncertain FALSE FALSE FALSE 0.4018386
#> 6: supported_uncertain FALSE FALSE FALSE 0.6413056
#> audit_score_lo95 audit_score_hi95 uncertainty_width rank_median rank_lo95
#> <num> <num> <num> <num> <num>
#> 1: 0.2577496 0.8645296 0.60678000 133 3.000
#> 2: 0.0000000 0.0000000 0.00000000 10098 10098.000
#> 3: 0.6780180 0.7613825 0.08336454 22 10.000
#> 4: 0.1676893 0.2461769 0.07848758 3116 2963.475
#> 5: 0.3666051 0.4335819 0.06697675 819 732.000
#> 6: 0.2708588 0.9109835 0.64012474 79 3.000
#> rank_hi95 confidence_class anchor_tier score_95ci pdactrace_call
#> <num> <char> <char> <char> <char>
#> 1: 2733.825 high_uncertain none [0.26, 0.86] rescued_candidate
#> 2: 10098.000 excluded none [0.00, 0.00] rejected_artifact
#> 3: 34.000 stable_high T1_validated [0.68, 0.76] confirmed_anchor
#> 4: 4277.000 low none [0.17, 0.25] rejected_artifact
#> 5: 1092.625 medium none [0.37, 0.43] novel_nomination
#> 6: 2437.000 high_uncertain T1_validated [0.27, 0.91] confirmed_anchorsupported_uncertain exemplarLTBP1 illustrates the framework’s most distinctive behaviour: a gene
whose pooled-RNA signal is Early_Burst_Up (rho ≈ 1.0) but
whose per-cohort meta-analysis exposes severe heterogeneity (max I² ≈ 75
%, TCGA up vs CPTAC down). The frozen rule down-weights LTBP1 via the
heterogeneity gate (0.70 in the 70-90 band), and assigns it to the
supported_uncertain audit class — protein, scRNA, and serum
layers all raise the evidence_strength axis, but the
heterogeneity gate prevents a high-confidence call.
ltbp1_score <- compute_audit_score("LTBP1")
ltbp1_score
#> Key: <gene_symbol>
#> gene_symbol evidence_strength biological_coherence translational_relevance
#> <char> <num> <num> <num>
#> 1: LTBP1 1 0.5971764 0.7
#> leakage_gate heterogeneity_gate positive_score audit_score
#> <num> <num> <num> <num>
#> 1: 1 0.7 0.7840117 0.5770248
#> audit_class
#> <char>
#> 1: supported_uncertain
ltbp1_unc <- propagate_uncertainty("LTBP1", n_mc = 200, seed = 42)
ltbp1_unc
#> gene_symbol audit_score_median audit_score_lo95 audit_score_hi95
#> <char> <num> <num> <num>
#> 1: LTBP1 0.5841692 0.2508913 0.8640965
#> uncertainty_width rank_median rank_lo95 rank_hi95 confidence_class
#> <num> <num> <num> <num> <char>
#> 1: 0.6132052 136.5 3 2813.025 high_uncertainThe wide 95% CI (roughly [0.26, 0.86] at N=500 MC
iterations) is the honest readout: LTBP1 is a credible candidate but its
uncertainty is real and traceable to cohort divergence, not buried under
a single point estimate.
GAPDH carries the strict housekeeping flag and therefore receives a
zero leakage multiplier. The score is exactly 0 in every Monte Carlo
iteration, and the gene is classified as excluded.
compute_audit_score("GAPDH")
#> Key: <gene_symbol>
#> gene_symbol evidence_strength biological_coherence translational_relevance
#> <char> <num> <num> <num>
#> 1: GAPDH 0.5 0 0.4
#> leakage_gate heterogeneity_gate positive_score audit_score audit_class
#> <num> <num> <num> <num> <char>
#> 1: 0 1 0.3 0 excluded
propagate_uncertainty("GAPDH", n_mc = 200, seed = 42)
#> gene_symbol audit_score_median audit_score_lo95 audit_score_hi95
#> <char> <num> <num> <num>
#> 1: GAPDH 0 0 0
#> uncertainty_width rank_median rank_lo95 rank_hi95 confidence_class
#> <num> <num> <num> <num> <char>
#> 1: 0 10098 10098 10098 excludedThis is the deliberate behaviour: a transparent audit framework should refuse to nominate housekeeping genes regardless of how well their trajectory shape fits an early-onset template.
External literature anchors are organised into three provenance tiers
(primary, secondary,
exploratory). Enrichment is computed with the audit_score
frozen — anchors never participate in scoring.
evaluate_anchor_enrichment(top_n = c(50, 100, 200, 500),
tier = c("primary", "secondary"))
#> tier tier_key top_n n_anchor_declared
#> <char> <char> <int> <int>
#> 1: PRIMARY (T1_validated, direct) primary 50 12
#> 2: PRIMARY (T1_validated, direct) primary 100 12
#> 3: PRIMARY (T1_validated, direct) primary 200 12
#> 4: PRIMARY (T1_validated, direct) primary 500 12
#> 5: SECONDARY (T1+T2) secondary 50 28
#> 6: SECONDARY (T1+T2) secondary 100 28
#> 7: SECONDARY (T1+T2) secondary 200 28
#> 8: SECONDARY (T1+T2) secondary 500 28
#> n_anchor_in_atlas hits expect fold pval
#> <int> <int> <num> <num> <num>
#> 1: 7 2 0.03460892 57.78857 4.952100e-04
#> 2: 7 4 0.06921784 57.78857 3.079676e-07
#> 3: 7 4 0.13843568 28.89429 4.959806e-06
#> 4: 7 5 0.34608919 14.44714 5.600381e-06
#> 5: 18 4 0.08899436 44.94667 1.537448e-06
#> 6: 18 7 0.17798873 39.32833 2.175894e-10
#> 7: 18 8 0.35597745 22.47333 7.515386e-10
#> 8: 18 9 0.88994364 10.11300 5.360423e-08A statistically significant enrichment (p ≪ 0.001) of T1+T2 anchors in the top 100 indicates that the deterministic rule recovers community-prioritized hypotheses without ever having seen them.
case_study() outputcase_study(c("LTBP1", "GAPDH", "THBS2", "ALB", "AMBP",
"SERPINA1", "ANPEP"))
#> gene_symbol rna_pattern prot_pattern scrna_origin serum_support
#> <char> <char> <char> <char> <char>
#> 1: LTBP1 Early_Burst_Up Early_Burst_Up myCAF B
#> 2: GAPDH <NA> Early_Burst_Up <NA> B
#> 3: THBS2 Early_Burst_Up Early_Burst_Up myCAF <NA>
#> 4: ALB Early_Trough Early_Burst_Up <NA> <NA>
#> 5: AMBP Early_Trough Early_Burst_Up <NA> <NA>
#> 6: SERPINA1 Early_Burst_Up Early_Burst_Up Macrophage A
#> 7: ANPEP Early_Loss_Down Early_Loss_Down Macrophage B
#> max_I2_meta evidence_strength biological_coherence translational_relevance
#> <num> <num> <num> <num>
#> 1: 75.16350 1.00 0.5971764 0.7
#> 2: 51.79413 0.50 0.0000000 0.4
#> 3: 40.63102 0.75 0.8645668 0.3
#> 4: 56.84316 0.50 0.7335634 0.0
#> 5: 36.68052 0.50 0.5311409 0.0
#> 6: 85.07961 1.00 0.6295406 1.0
#> 7: 89.42689 1.00 0.9961124 0.4
#> leakage_gate heterogeneity_gate positive_score audit_score
#> <num> <num> <num> <num>
#> 1: 1.0 0.7 0.7840117 0.5770248
#> 2: 0.0 1.0 0.3000000 0.0000000
#> 3: 1.0 1.0 0.6775984 0.7124366
#> 4: 0.5 1.0 0.4567472 0.2401153
#> 5: 1.0 1.0 0.3858993 0.4057401
#> 6: 1.0 0.7 0.8703392 0.6405610
#> 7: 1.0 0.7 0.8486393 0.6245901
#> audit_class is_hk is_plasma_hi rescue_eligible audit_score_median
#> <char> <lgcl> <lgcl> <lgcl> <num>
#> 1: supported_uncertain FALSE FALSE TRUE 0.5884497
#> 2: excluded TRUE FALSE FALSE 0.0000000
#> 3: high_confidence FALSE FALSE FALSE 0.7173547
#> 4: penalized FALSE TRUE FALSE 0.2346449
#> 5: supported_uncertain FALSE FALSE FALSE 0.4018386
#> 6: supported_uncertain FALSE FALSE FALSE 0.6413056
#> 7: supported_uncertain FALSE FALSE FALSE 0.2798504
#> audit_score_lo95 audit_score_hi95 uncertainty_width rank_median rank_lo95
#> <num> <num> <num> <num> <num>
#> 1: 0.2577496 0.8645296 0.60678000 133.0 3.000
#> 2: 0.0000000 0.0000000 0.00000000 10098.0 10098.000
#> 3: 0.6780180 0.7613825 0.08336454 22.0 10.000
#> 4: 0.1676893 0.2461769 0.07848758 3116.0 2963.475
#> 5: 0.3666051 0.4335819 0.06697675 819.0 732.000
#> 6: 0.2708588 0.9109835 0.64012474 79.0 3.000
#> 7: 0.2567785 0.6423697 0.38559130 2416.5 92.000
#> rank_hi95 confidence_class anchor_tier score_95ci pdactrace_call
#> <num> <char> <char> <char> <char>
#> 1: 2733.825 high_uncertain none [0.26, 0.86] rescued_candidate
#> 2: 10098.000 excluded none [0.00, 0.00] rejected_artifact
#> 3: 34.000 stable_high T1_validated [0.68, 0.76] confirmed_anchor
#> 4: 4277.000 low none [0.17, 0.25] rejected_artifact
#> 5: 1092.625 medium none [0.37, 0.43] novel_nomination
#> 6: 2437.000 high_uncertain T1_validated [0.27, 0.91] confirmed_anchor
#> 7: 2614.000 high_uncertain T1_validated [0.26, 0.64] confirmed_anchorEach gene receives an audit_class (5-class output of the
frozen rule) and a pdactrace_call (provenance-aware label
that combines class with anchor / tier / artifact context):
high_confidence — gates clean and audit_score ≥ 0.5
(e.g., LGALS3BP, LCN2, GGH, THBS2).supported_uncertain — heterogeneity-gated or score in
the 0.3-0.5 band (e.g., LTBP1, FN1, TGFBI, SLC16A3).penalized — plasma high-abundance (leakage_gate = 0.5;
e.g., ALB, FGB, C6).excluded — housekeeping (leakage_gate = 0; e.g.,
GAPDH).confirmed_anchor — externally validated marker that
ranks high (e.g., THBS2, TIMP1, MUC16, CEACAM1).rescued_candidate — RNA-tier ARTIFACT/OTHER but
multi-layer supported (e.g., LTBP1, VCAN, SPARC).novel_nomination — non-anchor candidate with strong
multi-layer signal and a clean leakage gate (e.g., LGALS3BP, KLK7/10,
CD55).rejected_artifact — housekeeping or plasma
high-abundance (e.g., GAPDH, ALB, C6) — surfaced only for
negative-control display.Together with the Monte Carlo confidence_class
(stable_high, high_uncertain,
medium, low, excluded) this gives
a transparent two-axis call: which class does the deterministic rule
assign and how stable is the score under realistic
perturbation.
To stay defensible, the framework explicitly does not:
pdactrace’s own
internal pipeline outputs;These are deliberate design choices recorded in the supplement and
the NEWS.md v0.3.0 entry.
sessionInfo()
#> R version 4.6.1 (2026-06-24)
#> Platform: x86_64-pc-linux-gnu
#> Running under: Ubuntu 26.04.1 LTS
#>
#> Matrix products: default
#> BLAS: /usr/lib/x86_64-linux-gnu/openblas-pthread/libblas.so.3
#> LAPACK: /usr/lib/x86_64-linux-gnu/openblas-pthread/libopenblasp-r0.3.32.so; LAPACK version 3.12.0
#>
#> locale:
#> [1] LC_CTYPE=en_US.UTF-8 LC_NUMERIC=C
#> [3] LC_TIME=en_US.UTF-8 LC_COLLATE=en_US.UTF-8
#> [5] LC_MONETARY=en_US.UTF-8 LC_MESSAGES=en_US.UTF-8
#> [7] LC_PAPER=en_US.UTF-8 LC_NAME=C
#> [9] LC_ADDRESS=C LC_TELEPHONE=C
#> [11] LC_MEASUREMENT=en_US.UTF-8 LC_IDENTIFICATION=C
#>
#> time zone: Etc/UTC
#> tzcode source: system (glibc)
#>
#> attached base packages:
#> [1] stats graphics grDevices utils datasets methods base
#>
#> other attached packages:
#> [1] data.table_1.18.6.1 pdactrace_0.99.25 rmarkdown_2.32
#>
#> loaded via a namespace (and not attached):
#> [1] sass_0.4.10 generics_0.1.4
#> [3] SparseArray_1.13.3 lattice_0.23-1
#> [5] digest_0.6.39 magrittr_2.0.5
#> [7] evaluate_1.0.5 grid_4.6.1
#> [9] RColorBrewer_1.1-3 fastmap_1.2.0
#> [11] jsonlite_2.0.0 Matrix_1.7-6
#> [13] scales_1.4.0 jquerylib_0.1.4
#> [15] abind_1.4-8 cli_3.6.6
#> [17] rlang_1.3.0 XVector_0.53.0
#> [19] Biobase_2.73.2 withr_3.0.3
#> [21] cachem_1.1.0 DelayedArray_0.39.7
#> [23] yaml_2.3.12 otel_0.2.0
#> [25] S4Arrays_1.13.1 tools_4.6.1
#> [27] dplyr_1.2.1 ggplot2_4.0.3
#> [29] SummarizedExperiment_1.43.0 BiocGenerics_0.59.12
#> [31] buildtools_1.0.0 vctrs_0.7.3
#> [33] R6_2.6.1 matrixStats_1.5.0
#> [35] stats4_4.6.1 lifecycle_1.0.5
#> [37] Seqinfo_1.3.2 S4Vectors_0.51.10
#> [39] IRanges_2.47.5 pkgconfig_2.0.3
#> [41] pillar_1.11.1 bslib_0.12.0
#> [43] gtable_0.3.6 glue_1.8.1
#> [45] tidyselect_1.2.1 tibble_3.3.1
#> [47] xfun_0.61 GenomicRanges_1.65.4
#> [49] sys_3.4.3 MatrixGenerics_1.25.0
#> [51] knitr_1.52 farver_2.1.2
#> [53] htmltools_0.5.9 labeling_0.4.3
#> [55] maketools_1.3.2 compiler_4.6.1
#> [57] S7_0.2.2supported_uncertain exemplarcase_study() output