The code below is evaluated
only when the package’s data layers are available – bundled, via
options(cpgdirection.data_dir = ), or through the
cpgdirectionData package. The text and code are complete either
way.
An epigenome-wide study gives you CpG sites. It rarely gives you matched RNA, so for most sites you do not know whether higher methylation goes with higher or lower expression of the nearby gene. That sign is not fixed: methylation is not uniformly repressive, and pooled estimates across studies can average to zero simply because opposing signs cancel.
This package assigns the sign, and tells you how much to trust it.
library(cpgdirection)
#> cpgdirection 2.99.5
#> cpg_expression_direction(my_cpgs) all methods; see best_direction
#> cpgd_selftest() verify the installation
#> Predictions are conditional on the CpG being an eQTM; see ?cpg_expression_direction
my_cpgs <- c("Zcg00335286_TC21_MC2R", "cg02079741_TC21_POMC", "cg00000029")
res <- cpg_expression_direction(my_cpgs, universal = FALSE, verbose = FALSE)
#> see ?cpgdirectionData and browseVignettes('cpgdirectionData') for documentation
#> Error while performing HEAD request.
#> Proceeding without cache information.
#> loading from cache
#> see ?cpgdirectionData and browseVignettes('cpgdirectionData') for documentation
#> downloading 1 resources
#> retrieving 1 resource
#> loading from cache
#> see ?cpgdirectionData and browseVignettes('cpgdirectionData') for documentation
#> downloading 1 resources
#> retrieving 1 resource
#> loading from cache
#> see ?cpgdirectionData and browseVignettes('cpgdirectionData') for documentation
#> loading from cache
#> Error while performing HEAD request.
#> Proceeding without cache information.
res[, c("cpg_id", "best_direction", "best_evidence"), with = FALSE]
#>
#> cpgdirection: 3 CpGs, all methods
#>
#> best_direction came from:
#> catalogue_consensus 1
#> catalogue_single 1
#> no_evidence 1
#>
#> direction assigned : 2 of 3 (67%)
#> +1 0 -1 2
#>
#> 1 CpGs have no direction. To fill them from a tissue you name
#> in advance (on biology, before seeing the answers):
#> cpg_expression_direction(my_cpgs, target_tissue = "solid_tissue")
#>
#> Read best_evidence alongside best_direction: measured is worth ~1.00,
#> distance_only ~0.62. All of it is conditional on the CpG being an eQTM.
#>
#> cpg_id best_direction best_evidence
#> 1 cg00000029 -1 catalogue_single
#> 2 cg00335286 NA no_evidence
#> 3 cg02079741 -1 catalogue_consensusmy_cpgs can be a character vector, a file path or a data
frame column. Identifiers carrying array suffixes or analysis prefixes
are reduced to their canonical form automatically, so
Zcg00335286_TC21_MC2R is understood, and the gene embedded
in the name is used as the target. (universal = FALSE skips
the distance-only fallback layer here, which needs the Bioconductor gene
annotation packages on first use.)
| column | meaning |
|---|---|
best_direction |
+1, -1, or NA where the
evidence does not support a call |
best_evidence |
which layer supplied it |
Always read them together. A direction worth 0.96
and a direction worth 0.62 are both a +1.
best_expected_accuracy carries the validated figure.
best_evidence |
accuracy | what it means |
|---|---|---|
measured |
~1.00 | a catalogue measured this exact pair |
smr_high |
0.96 | Mendelian randomisation, several instruments agreeing |
catalogue_consensus |
0.77–0.87 | genomic-context models, tissues agreeing |
smr_moderate |
0.85 | Mendelian randomisation, one instrument |
catalogue_single |
0.55–0.87 | one tissue only |
smr_weak |
0.70 | instruments disagree; a warning, not evidence |
distance_only |
0.60–0.65 | distance to the TSS, nothing else |
no_evidence |
— | nothing available; a correct answer, not a failure |
A CpG is not obliged to regulate one gene.
cpg_gene_pairs() discovers every supported target
automatically – bare identifiers suffice – and resolves direction
separately for every CpG x gene pair, excluding probes flagged as
unreliable (cross-hybridizing or degenerately mapped) by default:
p <- cpg_gene_pairs(c("cg26261055", "cg06495038"), verbose = FALSE)
#> see ?cpgdirectionData and browseVignettes('cpgdirectionData') for documentation
#> loading from cache
#> Building the gene TSS annotation from TxDb (once per machine)...
#> 24 genes were dropped because they have exons located on both strands of the
#> same reference sequence or on more than one reference sequence, so cannot be
#> represented by a single genomic range.
#> Use 'single.strand.genes.only=FALSE' to get all the genes in a GRangesList
#> object, or use suppressMessages() to suppress this message.
#> 'select()' returned 1:1 mapping between keys and columns
#> wrote /tmp/RtmpHvxjA0/file15657c774c4b.csv (28617 genes)
#> cached 28617 genes at /github/home/.cache/R/cpgdirection/gene_tss_hg19.csv.gz
#> see ?cpgdirectionData and browseVignettes('cpgdirectionData') for documentation
#> downloading 1 resources
#> retrieving 1 resource
#> loading from cache
#> Error while performing HEAD request.
#> Proceeding without cache information.
p[, c("cpg_id", "target_gene", "mapping_primary", "best_direction",
"best_evidence", "usable"), with = FALSE]
#>
#> cpgdirection automatic CpG-gene discovery
#>
#> submitted inputs: 2
#> unique canonical CpGs: 2
#>
#> target discovery (candidate pairs per source):
#> EPIC-v2 manifest targets: 2
#> tissue lookup targets: 2
#> measured eQTM targets: 0
#> SMR targets: 1
#>
#> CpGs with >=1 target: 2
#> CpGs with >1 target/co-effect: 2
#> total unique CpG-gene pairs: 0
#>
#> pair-level direction:
#> usable: 3
#> abstain/no direction: 2
#>
#> best_evidence:
#> distance_only 2
#> distance_tissue_conflict 2
#> smr_moderate 1
#>
#> probe QC (mask / position verification):
#> excluded as unreliable: 0 CpGs
#>
#> Mapping provenance (mapping_sources) says how a gene entered the
#> candidate set; best_evidence says what the direction rests on. They
#> are different facts and are kept apart. A CpG can carry opposite
#> directions for different genes; read pairs, not CpGs.
#>
#> cpg_id target_gene mapping_primary best_direction
#> 1 cg26261055 CRHBP EPICv2_manifest -1
#> 2 cg26261055 S100Z tissue_lookup NA
#> 3 cg06495038 CRHBP EPICv2_manifest -1
#> 4 cg06495038 F2RL1 SMR -1
#> 5 cg06495038 S100Z tissue_lookup NA
#> best_evidence usable
#> 1 distance_only TRUE
#> 2 distance_tissue_conflict FALSE
#> 3 distance_only TRUE
#> 4 smr_moderate TRUE
#> 5 distance_tissue_conflict FALSEmapping_sources records how each gene entered the
candidate set (manifest annotation, catalogue, measured eQTM, SMR);
best_evidence records what the direction rests on. They are
different facts and are kept apart.
Where the evidence is insufficient the package declines rather than guessing. That is why accuracy is high on the sites it does call. If you need a complete column, name the tissue your question is about:
Declare that tissue on biological grounds before looking at the answers. Choosing the tissue that gives the result you wanted is selecting on the outcome.
cpgd_direction_balance(res)
#>
#> cpgdirection: balance of directions in blood
#> predicted in the shipped table : 26.2% +1
#> measured (ground truth) : 39.5% +1
#>
#> The model predicts +1 less often than it truly occurs (26.2% vs 39.5%).
#> It shrinks towards the majority class, so a +1 is conservative and a
#> -1 carries less weight than its count suggests.
#>
#> your run: 2 catalogue-derived calls
#> +1 0 -1 2 (0.0% +1)
#> exact binomial vs the 26.2% base rate: p = 1
#> -> consistent with the base rate; no anomaly to explain.Inverse associations are the majority everywhere, so a
mostly--1 result is expected. This tests whether yours is
more one-sided than the base rate explains.
Every accuracy figure is conditional on the CpG being an eQTM at all. The catalogues behind the models contain only significant associations, so nothing here tells you whether a CpG affects expression. Feed it a site with no real effect and you will get a confident direction for something that is not there.
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] cpgdirection_2.99.5 cpgdirectionData_0.99.1 ExperimentHub_3.3.2
#> [4] AnnotationHub_4.3.2 BiocFileCache_3.3.0 dbplyr_2.6.0
#> [7] BiocGenerics_0.59.12 generics_0.1.4 rmarkdown_2.32
#>
#> loaded via a namespace (and not attached):
#> [1] KEGGREST_1.53.6
#> [2] httr2_1.3.0
#> [3] rjson_0.2.23
#> [4] xfun_0.61
#> [5] bslib_0.12.0
#> [6] Biobase_2.73.2
#> [7] lattice_0.23-1
#> [8] vctrs_0.7.3
#> [9] tools_4.6.1
#> [10] bitops_1.1-0
#> [11] stats4_4.6.1
#> [12] curl_8.0.0
#> [13] parallel_4.6.1
#> [14] tibble_3.3.1
#> [15] AnnotationDbi_1.75.2
#> [16] RSQLite_3.53.3
#> [17] blob_1.3.0
#> [18] pkgconfig_2.0.3
#> [19] BiocBaseUtils_1.15.1
#> [20] Matrix_1.7-6
#> [21] data.table_1.18.6.1
#> [22] S4Vectors_0.51.10
#> [23] cigarillo_1.3.1
#> [24] lifecycle_1.0.5
#> [25] compiler_4.6.1
#> [26] Rsamtools_2.29.0
#> [27] Biostrings_2.81.9
#> [28] Seqinfo_1.3.2
#> [29] codetools_0.2-20
#> [30] htmltools_0.5.9
#> [31] sys_3.4.3
#> [32] buildtools_1.0.0
#> [33] sass_0.4.10
#> [34] RCurl_1.98-1.20
#> [35] yaml_2.3.12
#> [36] TxDb.Hsapiens.UCSC.hg19.knownGene_3.22.1
#> [37] pillar_1.11.1
#> [38] crayon_1.5.3
#> [39] jquerylib_0.1.4
#> [40] BiocParallel_1.47.0
#> [41] cachem_1.1.0
#> [42] org.Hs.eg.db_3.23.1
#> [43] tidyselect_1.2.1
#> [44] digest_0.6.39
#> [45] purrr_1.2.2
#> [46] dplyr_1.2.1
#> [47] BiocVersion_3.24.0
#> [48] restfulr_0.0.17
#> [49] maketools_1.3.2
#> [50] fastmap_1.2.0
#> [51] grid_4.6.1
#> [52] cli_3.6.6
#> [53] magrittr_2.0.5
#> [54] GenomicFeatures_1.65.0
#> [55] XML_3.99-0.25
#> [56] withr_3.0.3
#> [57] rappdirs_0.3.4
#> [58] filelock_1.0.3
#> [59] bit64_4.8.6
#> [60] XVector_0.53.0
#> [61] httr_1.4.9
#> [62] bit_4.6.0
#> [63] otel_0.2.0
#> [64] png_0.1-9
#> [65] memoise_2.0.1
#> [66] evaluate_1.0.5
#> [67] knitr_1.52
#> [68] GenomicRanges_1.65.4
#> [69] IRanges_2.47.5
#> [70] BiocIO_1.23.3
#> [71] rtracklayer_1.73.0
#> [72] rlang_1.3.0
#> [73] glue_1.8.1
#> [74] DBI_1.3.0
#> [75] BiocManager_1.30.27
#> [76] jsonlite_2.0.0
#> [77] R6_2.6.1
#> [78] GenomicAlignments_1.49.2