3. Audit framework: 3 axes + 2 gates + 4 classes (v0.3.0)

Goal

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.

library(pdactrace)
library(data.table)

1. Why a hand-engineered rule (and not a supervised model)

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:

  1. Internal circularity — using pdactrace’s own pipeline outputs (e.g., the phase29 resectable list) as labels would teach the model to reproduce its own heuristic.
  2. Anchor circularity — using literature-curated anchors as training labels collapses the tool into a “literature mimic”: it learns to prioritize what other papers already prioritized, with no genuine discovery capability.

The audit framework therefore declines to predict and instead aggregates, ranks, and reports uncertainty. External anchors are reserved for post-freeze enrichment evaluation only.

2. The 6-function API at a glance

# 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_anchor

3. LTBP1 — the supported_uncertain exemplar

LTBP1 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_uncertain

The 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.

4. GAPDH — the housekeeping negative control

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         excluded

This 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.

5. Anchor enrichment evaluation (post-freeze)

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-08

A 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.

6. Reading the case_study() output

case_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_anchor

Each 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):

  • audit_class
    • 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).
  • pdactrace_call
    • 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.

7. What the audit framework does not claim

To stay defensible, the framework explicitly does not:

  • predict a probability of being a “true PDAC early marker” (no ground truth exists);
  • learn weights from anchors or from pdactrace’s own internal pipeline outputs;
  • collapse high-heterogeneity signals into a single point estimate without a confidence interval;
  • treat housekeeping or plasma high-abundance proteins as candidates, even when their trajectory shape is suggestive.

These are deliberate design choices recorded in the supplement and the NEWS.md v0.3.0 entry.

Session info

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.2