# Modeling

# Differential Methylation

Here we demonstrate the analysis of DNA methylation dependent on one or more predictors. The predictors might be tissue, cell type, sex, age, tumor/normal, case/control or a combination of these factors. The `DML` (Differential Methylation Locus) function models \(\beta\) values (DNA methylation levels) using mixed linear models. This general supervised learning framework identifies CpG loci whose differential methylation is associated with known co-variates and can be used to perform epigenome-wide association studies (EWAS). Let’s first load the needed packages:

    library(sesame)
    library(SummarizedExperiment)
    sesameDataCache() # required at new sesame installation

In the following, we will use an MM285 dataset of 10 mouse samples. This dataset contains mouse samples from different tissues and mice of different ages and sexes. The dataset is stored in a `SummarizedExperiment` object, which contains a data matrix combined with column-wise metadata accessible with `colData`:

    se = sesameDataGet("MM285.10.SE.tissue")[1:1000,] # an arbitrary 1000 CpGs
    cd = as.data.frame(colData(se)); rownames(cd) = NULL
    cd

    ##                   IDAT    tissue    sex age
    ## 1  204875570008_R06C01     Cecum Female   1
    ## 2  204875570011_R02C02     Cecum   Male   1
    ## 3  204875570008_R02C02     Colon Female   1
    ## 4  204875570008_R03C02     Colon Female   1
    ## 5  204875570008_R05C01 Esophagus Female  16
    ## 6  204875570011_R03C01 Esophagus   Male  16
    ## 7  204875570011_R01C01       Fat   Male  16
    ## 8  204875570022_R03C02       Fat Female  16
    ## 9  204875570011_R04C01     Heart   Male  31
    ## 10 204875570014_R04C02     Heart   Male  31

CRITICAL: If your data contains `NA`, it is required that you exclude CpGs with missing levels. For example, one cannot assess sex-specific DNA methylation for a CpG that only has non-NA measurement on one sex. Exclusion of such CpGs for differential methylation modeling can be done using the `checkLevels` function. Here, we will check this for both sex and tissue:

    se_ok = (checkLevels(assay(se), colData(se)$sex) &
        checkLevels(assay(se), colData(se)$tissue))
    sum(se_ok)                      # the number of CpGs that passes

    ## [1] 995

    se = se[se_ok,]

NOTE: If your model include discrete contrast variables like tissue and sex as in the current example, you should consider explicitly turning it into a factor variable with a reference level (we use `treatment coding`, see different coding systems).

For example, to use `Colon` as the reference tissue and `Female` as the reference sex, one can do the following

    colData(se)$tissue <- relevel(factor(colData(se)$tissue), "Colon")
    colData(se)$sex <- relevel(factor(colData(se)$sex), "Female")

Then we will model DNA methylation variation treating tissue and sex as covariates. To do that we will call the `DML` function and specify the R formula `~tissue + sex`. This function fits DNA methylation reading to a linear model and perform the corresponding slope test and goodness-of-fit test (F-test holding out each contrast variable). See also formula for how to specify lm/glm-like symbolic form for regression. All these results are returned in an object of class `DMLSummary`:

    smry = DML(se, ~tissue + sex)
    smry

    ## DMLSummary Object with 995 Loci, 10 samples.

    ## Contrasts: tissue, sex

You can use `DML(..., BPPARAM=BiocParallel::MulticoreParam(8))` argument to parallelize the computing.

# Test Interpretation

The `DMLSummary` object is a list of slightly expanded `summary.lm` objects as is typically returned by `summary(lm())`. The `summaryExtractTest` function extracts some key test statistics from the `DMLSummary` object and store them in a data frame. Rows of the data frame correspond to CpGs/loci and columns contain the slopes and p-values of each variable.

    test_result = summaryExtractTest(smry)
    colnames(test_result) # the column names, show four groups of statistics

    ##  [1] "Probe_ID"             "Est_X.Intercept."     "Est_tissueCecum"
    ##  [4] "Est_tissueEsophagus"  "Est_tissueFat"        "Est_tissueHeart"
    ##  [7] "Est_sexMale"          "Pval_X.Intercept."    "Pval_tissueCecum"
    ## [10] "Pval_tissueEsophagus" "Pval_tissueFat"       "Pval_tissueHeart"
    ## [13] "Pval_sexMale"         "FPval_tissue"         "FPval_sex"
    ## [16] "Eff_tissue"           "Eff_sex"

    head(test_result)

    ## # A tibble: 6 × 17
    ##   Probe_ID    Est_X.Intercept. Est_tissueCecum Est_tissueEsophagus Est_tissueFat
    ##   <chr>                  <dbl>           <dbl>               <dbl>         <dbl>
    ## 1 cg00101675…            0.936        -0.00674             -0.0534       -0.149
    ## 2 cg00116289…            0.864        -0.0102              -0.0223       -0.0702
    ## 3 cg00211372…            0.888        -0.0232              -0.0167       -0.0112
    ## 4 cg00531009…            0.883        -0.00284             -0.0326       -0.0121
    ## 5 cg00747726…            0.670        -0.00417              0.0119        0.108
    ## 6 cg00896209…            0.884         0.00508             -0.0277       -0.0203
    ## # ℹ 12 more variables: Est_tissueHeart <dbl>, Est_sexMale <dbl>,
    ## #   Pval_X.Intercept. <dbl>, Pval_tissueCecum <dbl>,
    ## #   Pval_tissueEsophagus <dbl>, Pval_tissueFat <dbl>, Pval_tissueHeart <dbl>,
    ## #   Pval_sexMale <dbl>, FPval_tissue <dbl>, FPval_sex <dbl>, Eff_tissue <dbl>,
    ## #   Eff_sex <dbl>

With the exception of the `Intercept`, there are four groups of columns, each starting with “Est_”, “Pval_”, “FPval_”, and “Eff_” as prefix. Here are what they represent:

- Est_* : The slope estimate (aka the \(\beta\) coefficient, not to be confused with the DNA methylation \(\beta\)-value though) for continuous variable. DNA methylation difference of the current level with respect to the reference level for nominal contrast variables. Each suffix is concatenated from the contrast variable name (e.g., tissue, sex) and the level name if the contrast variable is discrete (e.g, Cecum, Esophagus, Fat). For example, `Est_tissueFat` should be interpreted as the estimated methylation level difference of Fat compared to the reference tissue (which is `Colon`, as set above). If reference is not set, the first level in the alphabetic order is used as the reference level. There is a special column named `Est_`(Intercept)``. It corresponds to the base-level methylation of the reference (in this case a Female Colon sample).

- Pval_* : The unadjusted p-values of t-testing the slope. This represents the statistical significance of the methylation difference. For example, `Pval_tissueFat` tests whether `Fat` is significantly different from `Colon` (the reference level) in DNA methylation. The `Pval_`(Intercept)`` tests whether the reference level is significantly different from zero.

- FPval_* : The unadjusted p-value of the F-test contrasting the full model against a reduced model with the labeled contrast variable held out. Note that “Pval_” and “FPval_” are equivalent when the contrast variable is a 2-level factor, i.e., in the case of a pairwise comparison.

- Eff_* : The effect size of each normial contrast variable. This is equivalent to the maximum slope subtracted by the minimum level including the reference level (0).

Multiple-testing adjustment can be done afterwards using R’s `p.adjust` function. It is integrated to the `DMR` function by default (see below).

# Goodness of Fit

One may want to ask a question like

> Is the CpG methylation tissue-specific?

rather than

> Is the CpG more methylated in fat compared to liver?

The first question ask about the contrast variable as a whole while the second question concerns only a specific level in the contrast variable. To answer this question, we can use an F-test contasting the full model with a reduced model with the target contrast held out. By default, this statistics is already computed in the `DML` function. The test result is recorded in the FPval_ columns. For example, to get all CpGs that are methylated specific to sex,

    library(dplyr)
    library(tidyr)
    test_result %>% dplyr::filter(FPval_sex < 0.05, Eff_sex > 0.1) %>%
        select(FPval_sex, Eff_sex)

    ## # A tibble: 5 × 2
    ##    FPval_sex Eff_sex
    ##        <dbl>   <dbl>
    ## 1 0.00000491   0.402
    ## 2 0.0000742    0.298
    ## 3 0.0108       0.149
    ## 4 0.0180       0.117
    ## 5 0.0467       0.118

Here we used 0.1 as the effect size threshold. This means DNA methylation difference under 0.1 (10%) is considered not biologically meaningful. This can be a valid assumption for homogenous cell composition as most cells would be either biallelically methylated, unmethylated or monoallelically methylated. But different threshold can be used in different analysis scenarios.

We can define CpG methylation as sex-specific, tissue-specific or both, by:

    test_result %>%
        mutate(sex_specific =
            ifelse(FPval_sex < 0.05 & Eff_sex > 0.1, TRUE, FALSE)) %>%
        mutate(tissue_specific =
            ifelse(FPval_tissue < 0.05 & Eff_tissue > 0.1, TRUE, FALSE)) %>%
        select(sex_specific, tissue_specific) %>% table

    ##             tissue_specific
    ## sex_specific FALSE TRUE
    ##        FALSE   891   99
    ##        TRUE      5    0

As you can see from the result, some probes are sex-specific and others are tissue-specific. There is no overlap between probes whose methylation reading is differential along both contrasts.

# Pairwise Comparison

From the test result, we can also ask whether the DNA methylation is different between two sexes or between two specific tissues. For example, `Est_sexMale` compares male from females. The following code creates a volcano plot.

    library(ggplot2)
    ggplot(test_result) + geom_point(aes(Est_sexMale, -log10(Pval_sexMale)))

Likewise, we can ask whether DNA methylation might be different between fat and colon. We can do

    ggplot(test_result) + geom_point(aes(Est_tissueFat, -log10(Pval_tissueFat)))

# Continuous Predictors

The variable tested in the `DML` function can be continuous. Suppose we are interested in `age` besides `sex`. We will call the same function but with the following formula:

    smry2 = DML(se, ~ age + sex)
    test_result2 = summaryExtractTest(smry2) %>% arrange(Est_age)

Let’s verify the CpGs positively associated with age.

    test_result2 %>% dplyr::select(Probe_ID, Est_age, Pval_age) %>% tail

    ## # A tibble: 6 × 3
    ##   Probe_ID        Est_age Pval_age
    ##   <chr>             <dbl>    <dbl>
    ## 1 cg28140103_TC21  0.0208  0.0120
    ## 2 cg19175169_TC21  0.0222  0.00919
    ## 3 cg28121306_BC11  0.0242  0.00539
    ## 4 cg28115955_TC21  0.0242  0.00104
    ## 5 cg03755602_TC21  0.0263  0.00396
    ## 6 cg28117416_TC21  0.0269  0.00113

    df = data.frame(Age = colData(se)$age,
        BetaValue = assay(se)[test_result2$Probe_ID[nrow(test_result2)],])
    ggplot(df, aes(Age, BetaValue)) + geom_smooth(method="lm") + geom_point()

    ## `geom_smooth()` using formula = 'y ~ x'

# DMR

For a given contrast, one can merge neighboring CpGs that show consistent methylation variation into differentially methylated regions (DMRs). For example, we can merge sex-specific differential methylation identified above to chromosome X regions that show X-inactivation-related methylation difference. To do this, we need to pick a contrast:

    dmContrasts(smry)                       # pick a contrast from below

    ## [1] "X.Intercept."    "tissueCecum"     "tissueEsophagus" "tissueFat"
    ## [5] "tissueHeart"     "sexMale"

    merged = DMR(se, smry, "sexMale", platform="MM285") # merge CpGs to regions

    ## Merging correlated CpGs ... Done.
    ## Generated 509 segments.
    ## Combine p-values ...
    ##  - 25 significant segments.
    ##  - 1 significant segments (after BH).
    ## Done.

    merged %>% dplyr::filter(Seg_Pval_adj < 0.01)

    ##   Seg_ID Seg_Chrm Seg_Start   Seg_End   Seg_Est     Seg_Pval Seg_Pval_adj
    ## 1    501     chrX  94123156 101643027 -0.349981 3.146868e-09 1.601756e-06
    ## 2    501     chrX  94123156 101643027 -0.349981 3.146868e-09 1.601756e-06
    ##          Probe_ID   Estimate Std. Error   t value     Pr(>|t|)
    ## 1 cg06019215_BC11 -0.4020152 0.01210943 -33.19852 4.909686e-06
    ## 2 cg08310277_BC21 -0.2979468 0.01777330 -16.76373 7.420544e-05

See Supplemental Vignette for track-view visualization of the data.

# Session Info

    sessionInfo()

    ## R version 4.6.0 (2026-04-24)
    ## Platform: x86_64-pc-linux-gnu
    ## Running under: Ubuntu 24.04.4 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.26.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] stats4    stats     graphics  grDevices utils     datasets  methods
    ## [8] base
    ##
    ## other attached packages:
    ##  [1] ggplot2_4.0.3               tidyr_1.3.2
    ##  [3] dplyr_1.2.1                 SummarizedExperiment_1.42.0
    ##  [5] Biobase_2.72.0              GenomicRanges_1.64.0
    ##  [7] Seqinfo_1.2.0               IRanges_2.46.0
    ##  [9] S4Vectors_0.50.0            MatrixGenerics_1.24.0
    ## [11] matrixStats_1.5.0           sesame_1.30.0
    ## [13] sesameData_1.29.10          ExperimentHub_3.2.0
    ## [15] AnnotationHub_4.2.0         BiocFileCache_3.2.0
    ## [17] dbplyr_2.5.2                BiocGenerics_0.58.0
    ## [19] generics_0.1.4
    ##
    ## loaded via a namespace (and not attached):
    ##  [1] tidyselect_1.2.1      farver_2.1.2          blob_1.3.0
    ##  [4] filelock_1.0.3        Biostrings_2.80.0     S7_0.2.2
    ##  [7] fastmap_1.2.0         digest_0.6.39         lifecycle_1.0.5
    ## [10] KEGGREST_1.52.0       RSQLite_2.4.6         magrittr_2.0.5
    ## [13] compiler_4.6.0        rlang_1.2.0           sass_0.4.10
    ## [16] tools_4.6.0           utf8_1.2.6            yaml_2.3.12
    ## [19] knitr_1.51            labeling_0.4.3        S4Arrays_1.12.0
    ## [22] bit_4.6.0             curl_7.1.0            DelayedArray_0.38.1
    ## [25] plyr_1.8.9            RColorBrewer_1.1-3    abind_1.4-8
    ## [28] BiocParallel_1.46.0   withr_3.0.2           purrr_1.2.2
    ## [31] sys_3.4.3             grid_4.6.0            preprocessCore_1.74.0
    ## [34] wheatmap_0.2.0        colorspace_2.1-2      MASS_7.3-65
    ## [37] scales_1.4.0          dichromat_2.0-0.1     cli_3.6.6
    ## [40] rmarkdown_2.31        crayon_1.5.3          reshape2_1.4.5
    ## [43] httr_1.4.8            tzdb_0.5.0            DBI_1.3.0
    ## [46] cachem_1.1.0          stringr_1.6.0         splines_4.6.0
    ## [49] parallel_4.6.0        AnnotationDbi_1.74.0  BiocManager_1.30.27
    ## [52] XVector_0.52.0        vctrs_0.7.3           Matrix_1.7-5
    ## [55] jsonlite_2.0.0        hms_1.1.4             bit64_4.8.0
    ## [58] maketools_1.3.2       jquerylib_0.1.4       glue_1.8.1
    ## [61] codetools_0.2-20      stringi_1.8.7         gtable_0.3.6
    ## [64] BiocVersion_3.23.1    tibble_3.3.1          pillar_1.11.1
    ## [67] rappdirs_0.3.4        htmltools_0.5.9       R6_2.6.1
    ## [70] httr2_1.2.2           evaluate_1.0.5        lattice_0.22-9
    ## [73] readr_2.2.0           png_0.1-9             memoise_2.0.1
    ## [76] BiocStyle_2.40.0      bslib_0.10.0          Rcpp_1.1.1-1.1
    ## [79] nlme_3.1-169          SparseArray_1.12.1    mgcv_1.9-4
    ## [82] xfun_0.57             buildtools_1.0.0      pkgconfig_2.0.3