---
title: "TiDEomics Tutorial: Multi-Dataset/Multi-Omics Analysis"
author:
  - name: Tianen He
    affiliation:
    - University of Oxford
    email: tianen.he@ndm.ox.ac.uk
output:
  BiocStyle::html_document:
    self_contained: true
    toc: true
    toc_float:
      collapsed: true
      smooth_scroll: false
    toc_depth: 3
    number_sections: true
    code_folding: show
    highlight: tango
    df_print: paged
date: "`r BiocStyle::doc_date()`"
package: "`r BiocStyle::pkg_ver('TiDEomics')`"
vignette: >
  %\VignetteIndexEntry{TiDEomics Tutorial: Multi-Dataset/Multi-Omics Analysis}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
bibliography: references.bib
---

```{r setup, include = FALSE}
knitr::opts_chunk$set(
    collapse = TRUE,
    comment = "#>",
    crop = NULL,
    # Related to https://stat.ethz.ch/pipermail/bioc-devel/
    # 2020-April/016656.html
    dpi = 96,
    fig.width = 6,
    fig.height = 4,
    fig.keep = "all",
    max.height = "300px"
)

local({
    hook_output <- knitr::knit_hooks$get("output")
    knitr::knit_hooks$set(output = function(x, options) {
        if (!is.null(options$max.height)) {
            options$attr.output <- c(
                options$attr.output,
                sprintf('style="max-height: %s;"', options$max.height)
            )
        }
        hook_output(x, options)
    })
})
```

```{r load-packages}
library(TiDEomics)
library(org.Mm.eg.db)
```

## Introduction

This tutorial demonstrates the **multi-dataset/multi-omics** workflow of
[**TiDEomics**](https://bioconductor.org/packages/TiDEomics). It complements the
single-dataset workflow in the `TiDEomics Tutorial` vignette
(`browseVignettes("TiDEomics")`).

Key features covered:

- *Preparation of multiple datasets*: prepare, filter and combine datasets,
    retaining shared and dataset-specific features.

- *Multi-dataset exploratory visualisation*: plot temporal profiles, PCA, UMAP,
    and sample correlation matrices across datasets or omics layers.

- *Consensus co-expression module analysis*: identify and interpret consensus
    WGCNA modules across datasets, including integrated visualisation with
    functional enrichment.

- *Feature level comparison*: compare feature properties or feature set
    overlaps within or across datasets, with optional functional annotation.

In TiDEomics, a dataset is one `SummarizedExperiment` object. Multiple
datasets or omics layers are provided as a named list, e.g.
`list(Exp1 = se1, Exp2 = se2)`.

## Example input

The tutorial dataset was derived from GSE263759 [@Traxler2025] as described
in the `TiDEomics Tutorial` vignette. `tutorial_data` and
`tutorial_sample_info` provide the sample expression and annotations:

```{r load-example}
data(tutorial_data, package = "TiDEomics")
data(tutorial_sample_info, package = "TiDEomics")

tutorial_data[1:5, 1:5]
```

Exp1 uses `tutorial_data` directly. Exp2 is generated from the same table
by adding noise and suffixing its first 50 feature names with `_Exp2`, to
simulate differences between datasets or omics layers.

```{r input-data}
tutorial_data_2 <- tutorial_data
exp1_values <- as.matrix(tutorial_data[, -1])
gene_sd <- apply(exp1_values, 1, sd)

set.seed(5678)
noise <- matrix(
    rnorm(length(exp1_values)),
    nrow = nrow(exp1_values)
)
tutorial_data_2[, -1] <- exp1_values + noise * 0.5 * gene_sd
tutorial_data_2$Feature[seq_len(50)] <- paste0(
    tutorial_data_2$Feature[seq_len(50)], "_Exp2"
)
```

The two datasets each have 50 dataset-specific features, while all other
features are shared:

```{r overlap-summary}
shared_features <- intersect(tutorial_data$Feature, tutorial_data_2$Feature)
length(shared_features)
```

## Workflow

### Multi-dataset preparation

`combine_tide()` accepts a list of `prepare_tide()` outputs and returns the
filtered objects including all features or shared features only.

```{r combine-tide}
tide_results <- list()
tide_results[["Exp1"]] <- prepare_tide(
    data = tutorial_data, sample_ann = tutorial_sample_info,
    feature_threshold = 0, keep = "threshold", residual_threshold = 100)
tide_results[["Exp2"]] <- prepare_tide(
    data = tutorial_data_2, sample_ann = tutorial_sample_info,
    feature_threshold = 0, keep = "threshold", residual_threshold = 100)

combined <- combine_tide(tide_results, assay = 2)
names(combined)
combined$filter_summary
```

The returned `filter_summary` reports the number and percentage of features
retained in each dataset. The `se_list_filtered` and `se_list_merged` objects
include dataset specific features, while `se_list_filtered_shared` and
`se_list_merged_shared` contain only features retained by every dataset.

### Multi-dataset visualisation

`plot_trend()` plots the mean and standard deviation of selected
features over time. The `multi_layout` argument
controls the arrangement of datasets and features, while `independent_y`
controls whether panels use independent or shared y-axis ranges:

```{r trend, fig.width = 5}
plot_trend(combined$se_list_filtered,
    features = combined$shared_features[1:4], assay = 2,
    multi_layout = "wrap", independent_y = FALSE, ncol = 2
)

plot_trend(combined$se_list_filtered,
    features = combined$shared_features[1:4], assay = 2,
    multi_layout = "overlay", independent_y = TRUE, ncol = 2
)

plot_trend(combined$se_list_filtered,
    features = combined$shared_features[1:4], assay = 2,
    multi_layout = "vertical", independent_y = FALSE
)

plot_trend(combined$se_list_filtered,
    features = combined$shared_features[1:4], assay = 2,
    multi_layout = "horizontal", independent_y = TRUE
)
```

`plot_pca()`, `plot_umap()`, and `plot_cor_matrix()` accept the dataset list
and plot the datasets side by side. With `shared_only = TRUE`, PCA, UMAP, and
correlation matrices are computed on the features shared by all datasets,
otherwise all eligible features are used.

```{r pca, fig.width = 8, fig.height = 3}
PC <- plot_pca(combined$se_list_filtered,
    assay = 2,
    shared_only = TRUE,
    plot_screeplot = TRUE,
    plot_loadings = FALSE,
    plot_morepc = FALSE
)
PC$plot
```

```{r umap, fig.width = 8, fig.height = 3}
umap_out <- plot_umap(combined$se_list_filtered,
    seed = 1234, assay = 2,
    shared_only = FALSE
)
umap_out$plot
```

```{r cormat, fig.width = 8, fig.height = 4}
plot_cor_matrix(combined$se_list_filtered,
    method = "spearman",
    assay = 2,
    shared_only = FALSE,
    label_rep = TRUE, label_batch = TRUE,
    cellwidth = 1.5, cellheight = 1.5
)
```

When datasets contain the same samples, `plot_cor_matrix()` reports
clustering similarity using correlation of cophenetic distances.

The other TiDEomics functions can be applied separately to each dataset
(e.g. `combined$se_list_filtered$Exp1` and `combined$se_list_filtered$Exp2`),
as described in the main tutorial.

### Consensus WGCNA

Consensus WGCNA identifies co-expression modules that are present
across all or most datasets [@WGCNA_0; @WGCNA_1; @WGCNA_2].
Only features shared by all datasets are included,
so the `se_list_filtered_shared` from `combine_tide()` output is used.

First, scale-free topology fit is assessed and the suitable power is picked
for each dataset with `prepare_WGCNA()`:

```{r wgcna-prep, fig.width = 4}
inp1 <- prepare_WGCNA(combined$se_list_filtered_shared$Exp1,
    assay = 2, powers = seq(1, 20),
    networkType = "signed", RsquaredCut = 0.8
)
inp1$plot

inp2 <- prepare_WGCNA(combined$se_list_filtered_shared$Exp2,
    assay = 2, powers = seq(1, 20),
    networkType = "signed", RsquaredCut = 0.8
)
inp2$plot
```

`run_consensus_WGCNA()` then computes consensus network and modules across the
datasets.

```{r consensus}
cons <- run_consensus_WGCNA(
    nets = list(Exp1 = inp1, Exp2 = inp2),
    powers = c(
        Exp1 = inp1$custom_powerEstimate,
        Exp2 = inp2$custom_powerEstimate
    ),
    minModuleSize = 10
)
```

The features with the highest module membership (kME) per module
can be extracted with `extract_consensus_hubs()`. The result is a table
containing consensus and dataset-specific hub feature sets:

```{r consensus-hubs}
hubs <- extract_consensus_hubs(cons, top_n = 1)
head(hubs)
```

Module feature counts and module membership summaries can be
reported with `summarise_module_metrics()`:

```{r consensus-metrics}
module_metrics <- summarise_module_metrics(cons)
module_metrics$Consensus
```

### Visualisation of co-expressed modules

The feature-module assignment can be extracted with
`WGCNA_module()` and visualised by heatmap, using the replicate-merged
expression data of each dataset, as the `se_list_merged_shared` output
from `combine_tide()`:

```{r module-table}
cons_module <- WGCNA_module(cons$network, exclude_grey = TRUE)
```

`plot_modules_v()` stacks the per-dataset module profiles vertically:

```{r modules-v, fig.width = 8, fig.height = 9}
plot_modules_v(cons_module, combined$se_list_merged_shared,
    assay = 2, scale = TRUE,
    height_ratio = 2)
```

`plot_modules_h()` provides the horizontal layout with optional
enrichment annotations, similar to the single dataset usage. Module
profile y-axis scaling, group annotations, marked features, and legend
position can also be customised.

In the below example, the hub features are coloured by categories,
and the module profiles use shared y-axis across all modules and datasets.

```{r modules-h, fig.width = 10, fig.height = 5}
hub_colours <- c(
    "Consensus hub" = "#0072B2",
    "Exp1 hub" = "#D55E00",
    "Exp2 hub" = "#009E73"
)

plot_modules_h(cons_module, combined$se_list_merged_shared,
    assay = 2, scale = TRUE,
    mark_features = hubs,
    feature_colours = hub_colours,
    profile_scale = "shared", # or "dataset", "free"
    group_annotation = "both", # or "name", "colour", "none"
    legend_position = "right", # or "bottom"
    heatmap_width = 5, heatmap_height = 8,
    profile_width = 3
)
```

`plot_modules_h()` also supports selection of specific modules to display
with `selected_modules` and wrapping long enrichment terms with
`enrich_wrap_width`:

```{r modules-h-selected, fig.width = 10, fig.height = 5}
module_background <- WGCNA_module(cons$network, exclude_grey = FALSE)$Feature

go_modules <- enrichGO_list(cons_module,
    OrgDb = org.Mm.eg.db,
    universe = module_background,
    category = "BP",
    pvalueCutoff = 1, qvalueCutoff = 1) # keep all terms for visualisation

plot_modules_h(cons_module, combined$se_list_merged_shared,
    assay = 2, scale = TRUE,
    mark_features = hubs,
    feature_colours = hub_colours,
    enrich_list = go_modules$all,
    enrich_category = c("BP", "Features"),
    enrich_wrap_width = 4,
    profile_scale = "free", # or "dataset", "shared"
    group_annotation = "colour", # or "name", "both", "none"
    legend_position = "right",
    heatmap_width = 3, heatmap_height = 8,
    profile_width = 3,
    selected_modules = c(2, 5)
)
```

### Feature overlap or property comparison

Feature overlap or property comparisons can be across datasets or omics layers,
or within a single dataset. For example, one can compare specific contrasts
or properties of different modules from one dataset.

The feature overlap can be visualised with `plot_upset_h()` and
`plot_upset_v()`, based on `ComplexHeatmap::UpSet()`
[@complexheatmap_1; @complexheatmap_2].

For `plot_upset_h()`, functional enrichment terms
[@clusterProfiler_1; @clusterProfiler_2; @clusterProfiler_3; @clusterProfiler_4]
for the feature set intersections can be added as a horizontally
aligned textbox:

```{r upset-h, fig.width = 7, fig.height = 3.5}
set_list <- lapply(combined$se_list_filtered, rownames)
background_features <- unique(unlist(set_list))

feature_sets <- list(
    Exp1_Exp2 = intersect(set_list$Exp1, set_list$Exp2),
    Exp1 = setdiff(set_list$Exp1, set_list$Exp2),
    Exp2 = setdiff(set_list$Exp2, set_list$Exp1)
)

go_all <- enrichGO_list(feature_sets,
    OrgDb = org.Mm.eg.db,
    universe = background_features,
    category = "BP",
    pvalueCutoff = 1, qvalueCutoff = 1) # keep all terms for visualisation

plot_upset_h(set_list,
    enrich_list = go_all$all,
    enrich_category = "BP",
    enrich_top_n = 2,
    enrich_wrap_width = 7
)
```

`plot_upset_v()` allows a vertically aligned enrichment term dot matrix
below the Upset plot. The term ordering and colours can be customised with
`enrich_term_order` and `enrich_term_colours`, and `term_wrap_width`
controls term wrapping (similar to `enrich_wrap_width` for `plot_upset_h()`).

```{r upset-v, fig.width = 5, fig.height = 5}
bp_terms <- unique(as.data.frame(go_all$all$BP)$ID)
enrich_term_colours <- stats::setNames(
    rep(c("#0072B2", "#D55E00", "#009E73", "black"),
        length.out = length(bp_terms)),
    bp_terms
)

plot_upset_v(set_list,
    enrich_list = go_all$all,
    enrich_category = "BP",
    enrich_top_n = 2,
    term_wrap_width = 50,
    enrich_term_order = "intersection", # or "rank"
    enrich_term_colours = enrich_term_colours,
    include_all = FALSE # see clusterProfiler::dotplot `includeAll` usage
)
```

Per-feature properties can be compared with `plot_scatter()`.
Here the feature mean abundance is compared between the two datasets:

```{r scatter, fig.width = 5, fig.height = 4}
exp1_stat <- data.frame(
    Feature = rownames(combined$se_list_filtered$Exp1),
    Mean = rowMeans(
        SummarizedExperiment::assay(combined$se_list_filtered$Exp1, 2),
        na.rm = TRUE
    )
)
exp2_stat <- data.frame(
    Feature = rownames(combined$se_list_filtered$Exp2),
    Mean = rowMeans(
        SummarizedExperiment::assay(combined$se_list_filtered$Exp2, 2),
        na.rm = TRUE
    )
)

plot_scatter(exp1_stat, exp2_stat,
    col1 = "Mean", col2 = "Mean",
    name1 = "Exp1 mean abundance",
    name2 = "Exp2 mean abundance",
    colour_threshold = c(x = 1, y = 1),
    label_threshold = c(x = 2.5, y = 2.5)
)
```

## Session information

```{r session-info}
sessionInfo()
```

## References
