---
title: "TiDEomics Tutorial"
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}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
bibliography: references.bib
nocite: '@*'
---

```{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)
    })
})
```

## Introduction

[**TiDEomics**](https://github.com/hte123/TiDEomics) (Time-course Differential
Expression analysis of omics data) is an R package for
**multi-group time-course omics analysis** built on `SummarizedExperiment`,
especially for comparison of more than two conditions across time.

`TiDEomics` provides:

- An integrated workflow from data processing to functional interpretation, on
    different omics data types in feature x sample matrix format, with
    handling of missing values.

- An approach to separate **time-dominant**, **group-dominant**, and
    **group-specific temporal** effects through the combination of pairwise
    differential expression, variance decomposition, and co-expression module
    analysis (WGCNA).

- A filtering strategy based on **residual variance** after mixed-model
    variance decomposition, prioritising features with structured
    differential expression over unexplained variation. In comparison,
    traditional filtering by total variance or coefficient of variation is less
    appropriate when biological variation across groups and time course is
    expected.

Existing packages on time-course omics analysis are available, typically
designed for transcriptomics data. Some packages are limited to two-group
comparisons (e.g. `splineTimeR`) or expect raw sequencing counts (e.g.
`ImpulseDE2`, `TCseq`). `maSigPro` supports multi-group time-course DE analysis
via polynomial regression, complementary to `TiDEomics` with fewer
assumptions about temporal trends. `TiDEomics` also integrates QC, filtering
and downstream functional enrichment within the same workflow.

TiDEomics incorporates `Trendy` [@Trendy] which operates on single time
series to estimate breakpoints per group per feature,
and derived variance decomposition from `PALMO` [@PALMO] and introduced
residual variance as a filtering criterion.

This tutorial demonstrates a basic workflow, explains key parameters, and gives
practical tips.

Key features covered:

- *Preparing input*: Create a `SummarizedExperiment` object with the data
    matrix and sample annotation, with checks for correct formatting.

- *Quality control*: Visualise value distributions and missingness patterns.

- *Normalisation, grouping and merging replicates*: Normalise each feature to
    the starting time point when focused on changes from baseline. Transform
    the data for analyses that require single groups or single time series.

- *Sample relationships visualisation (correlation matrix, PCA, UMAP)*: Explore
    global structure and major sources of variance, check for batch effects,
    and identify features driving PCs.

- *Pairwise differential expression (by time or group)*: Identify DE features
    between pairs of time points within each group, and between pairs of groups
    at each time point.

- *Classification by feature property (randomness and overall fold change)*:
    Classify features into different categories (e.g., non-random vs. random,
    differentially expressed vs. stable) based on properties including trend
    significance and maximum fold change.

- *Segmented regression with Trendy*: Estimate breakpoints for each feature in
    each group and summarise dynamic patterns.

- *Variance decomposition modified from PALMO*: Quantify relative contributions
    from `Time`, `Group`, and `Residual`, distinguish time or group-dependent
    DE features and "noisy" features.

- *Module identification with WGCNA*: Identify co-expression modules with
    different temporal and group-specific patterns.

- *Functional enrichment (gene ontology, drugs, etc.)*: Interpret biological
    meaning of identified DE features and modules.

## Installation

```{r install, eval = FALSE}
if (!requireNamespace("BiocManager", quietly = TRUE)) {
    install.packages("BiocManager")
}

BiocManager::install("TiDEomics")
```

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

## Example input

TiDEomics expects:

- `data`: log2-transformed data.frame
    - rows: features (e.g. genes, proteins)
    - columns: First column should be feature identifiers.
        Other columns are numeric measurements of features for each sample.
    - Missing values (NA) are allowed.
- `sample_ann`: data.frame with columns:
    - `Sample`: sample names, match column names of `data`
    - `Group`: experimental group
    - `Time`: numeric time point
    - `Replicate`: replicate ID (optional, auto-generated if absent)
    - `Batch`: batch ID (optional)
    - `Subject`: biological subject ID for repeated-measures designs
        (optional, set via `subject_col`)

Example: subset of
[GSE263759](https://www.ncbi.nlm.nih.gov/geo/query/acc.cgi?acc=GSE263759)
data set published in @Traxler2025.

- Ensembl IDs were mapped to symbols, genes with all zero counts were excluded.
- Use time 0 untreated samples for other groups' time 0.
- Sample 500 random genes.
- Normalised to `log2(CPM + 1)`

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

### Preprocessing for different data types

TiDEomics expects log-transformed, normalised data as input. The core workflow
is compatible with any quantitative omics data in a feature x sample matrix.
Enrichment analysis (`enrichGO_list`, `enrichGO_rank`, `enrichR_list`,
`enrich_msigdb`) requires gene identifiers and is not applicable to non-gene
features (e.g., metabolites, lipids).

Batch correction should be applied before input if batch effects are present.

For the most common scenarios in transcriptomics and proteomics:

- RNA-seq data should be normalised for sequencing depth and composition bias
    before TiDEomics, e.g. with TMM. The tutorial uses `log2(CPM + 1)` for
    simplicity. For pairwise differential expression, `trend = TRUE` can be used
    to account for the mean-variance relationship. See below section on
    [pairwise differential expression](#pairwise-de) for details.

- MS-based proteomics data processing software may include normalisation when
    producing protein x sample matrix. Depending on software and settings,
    manual normalisation and log transformation may be required, e.g. median
    normalisation. Missing values are accepted in most TiDEomics functions,
    and a basic imputation function `impute_groups()` is provided.

## Workflow

We present the workflow as below sections, explaining the usage and parameters.

### Data preparation

```{r create-input}
data_obj <- create_input(
    data = tutorial_data,
    sample_ann = tutorial_sample_info)

data_obj
```

The `SummarizedExperiment` stores data in named **assays**: `"orig"`
(assay 1) for the original input and `"norm"` (assay 2) for time-0-normalised
values (added by `normalise_to_start()`). Downstream functions accept either the
name or the index.

```{r view-data}
assays(data_obj)[["orig"]] |> as.data.frame() |> utils::head()
```

```{r coldata}
colData(data_obj) # sample annotation
```

### Optional: custom color palette

By default, TiDEomics generates a default color palette (`scales::pal_hue()`)
based on the number of groups. A custom palette for each group can be set with
`set_custom_palette()`, which will be used in all subsequent plotting functions
where applicable.

```{r palette}
custom_palette <- c(
    "untreated" = "#1b9e77", "IFNbeta" = "#d95f02",
    "IFNgamma" = "#7570b3", "LPS" = "#e7298a"
)
set_custom_palette(custom_palette)
```

### Quality control

```{r abundance}
plot_distribution(data_obj, facet_by = "Group")
```

If distributions differ significantly, consider global normalisation (e.g.,
quantile, median) *before* using TiDEomics.

For data with missing values (e.g., proteomics), `plot_ID()` and
`plot_missing()` can be used to check missingness patterns.

### Normalisation to starting time point

`normalise_to_start()` subtracts the baseline value from each feature at
starting time point (referred to as time 0) or the first available time point
if the feature is missing at time 0.

Two modes are available for defining the baseline:

- `by_subject = FALSE` (default): group-level baseline, mean of all samples at
    starting time point in the group. This is used when no subject-level
    information is available or when between-subject baseline differences are
    of interest.

- `by_subject = TRUE`: subject-level baseline, mean of all samples at starting
    time point for each subject. This is used when subject-level information
    is available and the focus is on subject-specific changes from baseline.

```{r norm0}
data_obj <- normalise_to_start(data_obj)
```

Both original and time-0 normalised data are stored in the
`SummarizedExperiment` object and downstream functions allow the user to specify
which to use for each analysis (`assay = 1 or 'orig'` for original,
`assay = 2 or 'norm'` for time-0 normalised).

Use original data when absolute abundance
differences across groups are of interest (e.g., constitutively different
baseline levels). Use time-0 normalised data when relative changes from
baseline are of interest.

### Splitting groups and merging replicates

Use `split_groups()` to obtain group-specific `SummarizedExperiment` objects
and `merge_replicates()` to average replicates for analyses / visualisation
that require single time series for features, including
`calc_feature_property()`, `run_Trendy()`, `plot_modules_v()` and
`plot_modules_h()`.

```{r split}
data_obj_list <- split_groups(data_obj)
```

```{r merge-rep}
data_obj_merged_list <- merge_replicates(data_obj_list)
```

```{r merge-groups}
data_obj_merged <- merge_groups(data_obj_merged_list)
```

### Sample relationships (correlation, PCA, UMAP)

Correlation matrix can be plotted with `plot_cor_matrix()` to check sample
relationships and potential batch effects.

```{r cormat}
plot_cor_matrix(data_obj,
    method = "spearman",
    label_rep = TRUE, label_batch = TRUE,
    cellwidth = 2, cellheight = 2
)
```

High within-replicate correlation and clustering by `Group` or `Time` increase
confidence in data quality and downstream analysis. If batch effects are
present, consider batch correction before using TiDEomics.

There are multiple functions for PCA and UMAP:

- Use `plot_pca()` to visualise sample relationships in 2D. Output loadings
    can be used to identify features driving the principal components.

- Use `plot_pca_3D()` to visualise sample relationships in 3D.

- Use `PCAtools::eigencorplot()` to correlate PCs with Group and Time.

- Use `plot_umap()` for UMAP and set `seed` for reproducibility.

Features with missing values (NA) are automatically excluded from PCA and
UMAP. Consider imputation (e.g., group-wise minimum imputation with
`split_groups()`, `impute_groups()` and `merge_groups()`) to include features
with missing values in PCA and UMAP.

```{r pca}
PC <- plot_pca(data_obj,
    # pc1 = 1, pc2 = 2, # default to plot PC1 and PC2
    plot_screeplot = TRUE,
    plot_loadings = FALSE,
    plot_morepc = TRUE,
    circle = FALSE,
    morepc = 1:5
)
PC$p_list
```

```{r pca-3D}
plot_pca_3D(PC$pca, pcs = 1:3)
```

Note: the 3D plot may not display properly in some html, but should work in an
interactive R session.

```{r pca-eigencor}
PCAtools::eigencorplot(PC$pca,
    metavars = c("Group", "Time"),
    components = paste0("PC", 1:5),
    col = colorRampPalette(c("#3C5488FF", "white", "#E64B35FF"))(100),
    colCorval = "black"
)
```

```{r umap}
umap <- plot_umap(data_obj, seed = 1234)
umap$p_list
```

PCA and UMAP can also be performed separately on each group to explore
within-group structure and dynamics.

By default, when plotting PCA by group, circles and arrows are added to show
the trajectory of samples along time course. Set `circle = FALSE` or
`arrow = FALSE` to remove circles and arrows.

```{r pca-umap-group, fig.width = 8, fig.height = 2.5}
plot_pca_by_group(data_obj, circle = TRUE, arrow = TRUE, legend_pos = "top")
# also accepts a list: plot_pca_by_group(data_obj_list)

plot_umap_by_group(data_obj, seed = 1234, legend_pos = "top")
# also accepts a list: plot_umap_by_group(data_obj_list)
```

### Pairwise differential expression {#pairwise-de}

The two DE functions for pairwise comparison between time points and groups are
built based on the limma package [@limma].

`DE_between_time()`:

- Compare pairs of time points within each group.

- Output: Nested lists of DE statistics tables, including all features or
    only significant features.

- Use `plot_DE_between_time()` to summarise number of DE features per pair of
    time points per group.

- When a Subject column is provided via `subject_col` in `create_input()`,
    `DE_between_time()` automatically uses a paired design via
    `limma::duplicateCorrelation(block = Subject)`.

`DE_between_group()`:

- Compare pairs of groups at each time point.

- Output: Nested lists of DE statistics tables, including all features or
    only significant features.

- Use `plot_DE_between_group()` to summarise number of DE features per pair of
    groups with line plots.

Common parameters for both functions:

- `assay`: index of assay to use (1 = original, 2 = time0-normalised if
    available)

- `filter`: minimal number of non-NA replicates for the feature to be included
    in DE testing (e.g., `filter = 1` means requiring features to have at least
    1 non-NA value at both time points in the specific group, or in both groups
    at the specific time point).

- `trend`: passed to `limma::eBayes()`. Set to `TRUE` for RNA-seq
    count-derived data to model the mean-variance trend.
    `FALSE` (default) is usually appropriate for other types of data,
    where the trend is typically weak.
    The trend can be visualised with `limma::plotSA()` with elements of
    `$fit_list` of both functions' output.

- Default thresholds for DE are `p.adj < 0.05` and `|log2FC| > 1`.

- `plot_DE_between_time()` and `plot_DE_between_group()` allow re-filtering
    DE by setting new thresholds.

Please note that the example data only has two replicates per time point.
More replicates are recommended for robust pairwise DE analysis.

DE_between_time():

```{r de-between-time, fig.width = 8, fig.height = 2, warning = FALSE}
DE_between_time_out <- DE_between_time(data_obj, assay = 1,
    filter = 1, trend = FALSE)
```

```{r de-between-time-all}
DE_between_time_out$all_list$IFNbeta$`t2-t0` |> utils::head()
```

```{r de-between-time-de}
DE_between_time_out$de_list$IFNbeta$`t2-t0` |> utils::head()
```

```{r de-between-time-plot, fig.width = 7, fig.height = 2}
# Filtered with thresholds in `DE_between_time()`
plot_DE_between_time(DE_between_time_out,
    fontsize = 8, value = FALSE, nrow = 1, heatmap_width = 3
)

# Re-filtering with new thresholds
plot_DE_between_time(DE_between_time_out,
    fontsize = 8, value = FALSE, nrow = 1, heatmap_width = 3,
    adjP_thres = 0.01, logFC_thres = 1
)
```

DE_between_group():

```{r de-between-group}
DE_between_group_out <- DE_between_group(data_obj, assay = 2,
    filter = 1, trend = TRUE)
```

```{r de-between-group-plot, fig.width = 3, fig.height = 2, out.width = "70%"}
# Filtered with thresholds in `DE_between_group()`
plot_DE_between_group(DE_between_group_out)

# Re-filtering with new thresholds
plot_DE_between_group(DE_between_group_out, adjP_thres = 0.01, logFC_thres = 1)
```

```{r de-between-group-all}
DE_between_group_out$all_list$`IFNgamma-untreated`$`24` |> utils::head()
```

```{r de-between-group-de}
DE_between_group_out$de_list$`IFNgamma-untreated` |> utils::head()
```

Output of `DE_between_group()`and `DE_between_time()` can be plotted with
`plot_volcano()` to visualise DE features of selected group(s) and time
point(s).

```{r volcano}
plot_volcano(DE_between_group_out,
    group1 = "untreated", group2 = "IFNgamma", time = 24,
    logFC_thres = 0.5, adjP_thres = 0.05, label = TRUE)
```

### Feature properties and classification

`calc_feature_property()` computes per-feature properties:

- `P_trend`: calculated with `randtests::bartels.rank.test()`, testing for
    non-randomness of the time profile (i.e., whether the feature shows a
    significant overall trend across time points).

- `Max_FC`: maximum fold change across time points, calculated as the
    difference between the feature's maximum and minimum abundance across time
    points. This captures the overall magnitude of change across the time
    course, regardless of the specific time points at which changes occur.

- `Max_FC_time`: the difference between time points with maximum and minimum
    abundance, providing information about the direction (up or down) and
    duration of changing.

- `Exp_threshold`: user-set value threshold for a feature to be considered
    expressed in a sample. This can be used to filter features based on
    expression level, e.g. setting `threshold = 0` means that only 
    values > 0 (log2(CPM + 1) > 0, i.e. CPM > 0 in the tutorial dataset) are 
    considered expressed. The default is NULL, which means all non-NA
    values are considered expressed.

- `T_total`, `T_exp` and `Exp_ratio`: number of total time points, number of
    expressed time points with values > `Exp_threshold` (or non-NA values if
    `threshold = NULL`), and proportion of expressed time points (`T_exp` /
    `T_total`).

- `Rho_time`: Spearman correlation with time (positive = up, negative = down).
    Requires at least 3 expressed time points.

- `Peak_ratio`: proportion of time points that are local maxima (0 = monotonic,
    higher = more oscillatory).

- `Log2_CV`: `log2(1 + SD / |mean|)`, coefficient of variation for relative
    temporal variability.

- `AUC`: area under the time-0-normalised curve from assay 2. Positive =
    net increase from baseline, negative = net decrease. `NA` if assay 2 is not
    available (run `normalise_to_start()` first).

The property values can be used to classify features into different categories
(e.g., non-random vs. random, differentially expressed vs. stable) for
downstream analysis and interpretation. For example, features with
`P_trend < 0.05` and `Max_FC >= 1` can be candidates for non-randomly
overall-changing DE features.

Note:

- When replicates are present, the input should be the mean of replicates at
    each time point, output of `merge_replicates()`.

- The function is set to require at least 3 values > threshold to
    calculate `P_trend`.

```{r property}
data_obj_merged_list <- calc_feature_property(data_obj_merged_list,
    threshold = 0)
property_tb <- summarise_feature_property(data_obj_merged_list)

utils::head(property_tb)
```

Features expressed in only one group or a subset of groups can be extracted
with `group_specific_features()` based on the output table of
`summarise_feature_property()`.

```{r group-specific}
group_specific_features(property_tb, groups = c("untreated"),
    genename = FALSE, GO = FALSE
)
```

### Segmented regression analysis

`run_Trendy` fits segmented linear models to the time course for each feature
to estimate breakpoints with Trendy package [@Trendy], see
[Trendy](https://bioconductor.org/packages/release/bioc/html/Trendy.html)
for details.

**Notes**:

- The function requires complete input (no NA). When data contains missing
    values (e.g. proteomics), use `impute_groups()` to impute (default: group
    minimum; pass `fun = function(x) min(x) / 2` for half-minimum,
    `fun = median` for median imputation, etc.), or restrict to features with
    complete data.

- If `feature` is not specified, the function will run on all features
    filtered by expression / missing rate, which requires running
    `calc_feature_property()` before `impute_groups()`.

- Number of time points needed = [# segments] x [minimum number of samples in
    a segment]. For example, when # segments = 2 (maxK = 1) and
    minNumInSeg = 2, at least 4 time points are needed.

```{r impute}
data_obj_merged_imp_list <- impute_groups(data_obj_merged_list)
```

A subset of features is used for demonstration as `run_Trendy()` can be
time-consuming.

```{r trendy-run}
set.seed(1234)
random_features <- sample(rownames(data_obj_merged_imp_list[[1]]), 50)

example_res_list <- run_Trendy(data_obj_merged_imp_list,
    feature = random_features,
    minExp = 0.5,
    maxK = 1,
    minNumInSeg = 2, meanCut = 0, NCores = 2
)
```

Use `plot_segments()`to plot the fitted segments and breakpoints for
selected features.

```{r trendy-plot, fig.height = 2.5, fig.width = 8}
plot_segments(data_obj_merged_imp_list,
    example_res_list,
    feature = c("Slc25a51", "Aunip"), # example features
    ylab = "Log2(CPM + 1)"
)
```

Plot the distribution of breakpoints across time points with
`plot_breakpoints()` in each group, and summarise the temporal patterns
(combination of trends of each segment) of features with `summarise_Trendy()`
and `extract_segment_trends()`.

```{r trendy-summary, fig.height = 2, fig.width = 4}
plot_breakpoints(example_res_list)

trendy_summary <- summarise_Trendy(example_res_list)
trendy_summary |> utils::head()
```

```{r trendy-list}
trendy_list <- extract_segment_trends(trendy_summary)
trendy_list$IFNbeta
```

### Variance decomposition

Variance of each feature is decomposed by linear mixed models (LMM) into
contributions from `Group`, `Subject` (when present), `Time`, `Residual`, and
optionally `Group:Time` (or `Subject:Time`) interaction, which captures
group-specific temporal patterns. This helps to identify time-dependent
features, group-dependent features, and "noisy" features with high residual
variance.

**Interpreting the components:**

- High `Time`: time-dependent features, dynamic along time course consistently
    across groups. Good candidates for `run_Trendy()` or other time-focused
    analysis.

- High `Group`: group-dependent features, stable along time but differentially
    expressed between groups. May be baseline biological markers.

- High `Subject` (when present): features with baseline differences between
    individuals, biologically meaningful in patient studies.

- High `Residual`: unexplained variation, consider excluding for
    downstream analysis.

The function `decomp_variance()` is inspired by `PALMO::lmeVariance()`[@PALMO].
Group and Time are always included (auto-skipped if only 1 group or time point
present). `Subject` is included when present in colData. Set
`interaction = TRUE` to add `Group:Time` (or `Subject:Time` with Subject) as a
variance component capturing group-specific temporal patterns. Sufficient
replicates per combination are required for stable estimates.

```{r variance-decomposition, fig.height = 3}
# filter genes for variance decomposition:
# at least 50% values > 0 in at least 2 groups
decomp_filter_genes <- group_specific_features(property_tb,
    filter_ratio = 0.5,
    group_pct = 2 / 4,
    GO = FALSE, genename = FALSE
)$features

var_decomp <- decomp_variance(data_obj,
    features = decomp_filter_genes,
    assay = 1, core = 2
)

plot_variance(var_decomp, rank = "Time", top_n = 20)
plot_variance(var_decomp, rank = "Group", top_n = 20)
```

### WGCNA (co-expression modules)

Detailed tutorials of weighted gene correlation network analysis
([WGCNA](https://cran.r-project.org/web/packages/WGCNA/index.html))
[@WGCNA_1; @WGCNA_2] can be found
[online](https://github.com/edo98811/WGCNA_official_documentation/).
The following is a brief demonstration of how to prepare input and run WGCNA
with TiDEomics functions.

**Filtering strategies**:

- Features with too many missing values will be automatically removed with
    `WGCNA::goodGenes()` included in the `prepare_WGCNA()` function, and can
    also be pre-filtered with the calculated feature property (output of
    `calc_feature_property()` and `summarise_feature_property()`).

- Residual variance can be used to exclude noisy features.

- It is not recommended to filter by differential expression before WGCNA, see
[WGCNA FAQ](https://edo98811.github.io/WGCNA_official_documentation/faq.html).

```{r wgcna-prepare}
# Example filtering by residual variance < Q3
var_res_q3 <- stats::quantile(var_decomp$Residual, 0.75, na.rm = TRUE)
filter_wgcna <- var_decomp |> dplyr::filter(Residual < var_res_q3) |>
    dplyr::pull(Feature)

data_obj_wgcna <- data_obj[filter_wgcna, ]
```

**Prepare data format and choose power**:

`prepare_WGCNA()` prepares the input for WGCNA and helps to choose the
soft-thresholding power. Check the scale-free topology fit indices to confirm
that the chosen power is appropriate.

Reasonable powers are less than 15 for unsigned or signed hybrid networks, and
less than 30 for signed networks, to reach scale-free topology fit index > 0.8,
and mean connectivity (mean.k) not too high (in the hundreds or above). See
[WGCNA FAQ](https://edo98811.github.io/WGCNA_official_documentation/faq.html)
for details.

```{r choose-power}
wgcna_input <- prepare_WGCNA(data_obj_wgcna, assay = 2,
    powers = seq(1, 20),
    networkType = "signed", RsquaredCut = 0.8
)
wgcna_input$plot
picked_power <- wgcna_input$powerEstimate
picked_power
```

**Run WGCNA**:

Use `run_WGCNA()` with the output of `prepare_WGCNA()`, and visualise the
resulting modules with `plot_WGCNA()`.

`run_WGCNA()` is a wrapper of `WGCNA::blockwiseModules()`, which runs the WGCNA
analysis and returns the module assignment for each feature. Parameters for
`WGCNA::blockwiseModules()` can be set with `run_WGCNA()`, e.g., `corType`
for correlation method.

```{r run-wgcna}
net <- run_WGCNA(wgcna_input,
    power = picked_power,
    # corType = "pearson", # other option is "bicor"
    numericLabels = TRUE
)

plot_WGCNA(net, fontsize = 8)
```

**Extract modules**: show module sizes

```{r extract-wgcna-modules}
gene_module <- WGCNA_module(net, exclude_grey = TRUE)

gene_module |>
    dplyr::group_by(Module) |>
    dplyr::summarise(n = dplyr::n())
```

Module metrics (size, mean kME, etc.) can be summarised with
`summarise_module_metrics()`:

```{r module-metrics}
summarise_module_metrics(net)
```

**Plot module profiles**:

```{r plot-wgcna-modules, fig.width = 6, fig.height = 6}
plot_modules_v(gene_module,
    data_obj_merged, scale = TRUE,
    ylabel = "Z-score of log2(CPM + 1)",
    height_ratio = 2
)
```

### Functional enrichment

TiDEomics wrap `clusterProfiler::enrichGO()` and
`clusterProfiler::gseGO()`[@clusterProfiler_1; @clusterProfiler_2;
@clusterProfiler_3; @clusterProfiler_4]
to run GO enrichment efficiently on gene sets and ranked gene list.

For ranked gene list (e.g., ranked by Time or Group contribution from variance
decomposition), use `enrichGO_rank()` and plot with `enrichplot::gseaplot2()`.

```{r go_rank, fig.width=5, fig.height=3}
gse_group <- enrichGO_rank(var_decomp,
    gene_rank_by = "Group",
    OrgDb = org.Mm.eg.db,
    keyType = "SYMBOL", category = "BP",
    go_rank_by = "p.adjust")

enrichplot::gseaplot2(gse_group[["BP"]], geneSetID = 1:3, base_size = 8)
```

For multiple defined gene sets (e.g. WGCNA modules, DE genes by feature
classification), use `enrichGO_list()` and plot with `plot_GO()`.

Optionally, use `simplify = TRUE` with `enrichGO_list()` to simplify the GO
results by removing redundant terms (default: FALSE).

```{r go-example, fig.width = 6}
background_wgcna <- colnames(wgcna_input$data)

go_list <- enrichGO_list(
    gene_list = gene_module, OrgDb = org.Mm.eg.db,
    universe = background_wgcna,
    pvalueCutoff = 0.9, # get more results for demonstration
    qvalueCutoff = 0.9,
    simplify = FALSE,
    keyType = "SYMBOL"
)

plot_GO(go_list$all, plot_dotplot = TRUE,
    plot_emapplot = FALSE,
    plot_cnetplot = FALSE,
    showCategory_dotplot = 3)
```

Enrichment can also be performed against the Molecular Signatures Database
([MSigDB](https://www.gsea-msigdb.org/gsea/msigdb)) via `enrich_msigdb()`,
which queries the gene sets with `msigdbr::msigdbr()`
and runs a hypergeometric test across
user-selected collections (Hallmark, GO, etc.) or specific gene sets.

```{r enrich-msigdb}
# Mouse Hallmark gene sets
hallmark_msigdb <- enrich_msigdb(gene_module, universe = background_wgcna,
    minGSSize = 5, category = "MH", species = "Mus musculus", db_species = "MM")

# Human C2 (chemical and genetic perturbations)
perturb_msigdb <- enrich_msigdb(gene_module, universe = background_wgcna,
    category = "C2", subcategory = "CGP", species = "Mus musculus")

# Specific gene sets (category not required)
ifn_msigdb <- enrich_msigdb(gene_module, universe = background_wgcna,
    gene_sets = c("HALLMARK_INTERFERON_ALPHA_RESPONSE",
        "HALLMARK_INTERFERON_GAMMA_RESPONSE"),
    minGSSize = 5, species = "Mus musculus", db_species = "MM")
```

Enrichment of other gene sets (e.g., drug signatures, TF targets, pathways) can
be performed with `enrichR_list()`, which wraps `enrichR::enrichr()`.
Available databases can be checked with `enrichR::listEnrichrDbs()`.

`enrichGO_list()`, `enrich_msigdb()` and `enrichR_list()` all
return a named list of data.frames (one per category for `enrichGO_list`,
one for `enrich_msigdb()`, one per database for `enrichR_list()`) with
`Cluster` and `Description` columns, compatible with `plot_modules_h()`.

### Integrated visualisation of WGCNA modules and functional enrichment

Enriched terms can be added to the module profile plot with
`plot_modules_h()`, which is similar to `plot_modules_v()` above but
horizontally aligned with annotation of the modules with their top enriched
GO terms. The plotting function is adapted from ClusterGVis
package [@ClusterGVis].

```{r plot-wgcna-modules-go, fig.width = 8, fig.height = 5}
plot_modules_h(gene_module,
    data_obj_merged, scale = TRUE,
    ylabel = "Z-score of log2(CPM + 1)",
    enrich_list = go_list$all,
    enrich_category = "BP",
    heatmap_width = 6,
    heatmap_height = 8
)
```

Multiple enrichment categories (e.g. GO BP, CC, MF, MSigDB collections,
enrichR databases) can be displayed side-by-side
by passing a vector to `enrich_category`. Features of interest can
be marked on the heatmap or shown as an annotation column via
`mark_features` and `enrich_category = "Hub features"`. The
`enrich_p_threshold` parameter controls per-category p-value colour-coding
of enrichment terms; set to `NA` for "Hub features", which use auto-assigned
colours instead.

Hub features can be extracted by WGCNA module membership (the correlation of
the feature with the corresponding module eigengene) with `extract_hubs()`

```{r plot-wgcna-modules-multi, fig.width = 15, fig.height = 5}
hub_features <- extract_hubs(net, top_n = 3)

plot_modules_h(gene_module,
    data_obj_merged, scale = TRUE,
    ylabel = "Z-score of log2(CPM + 1)",
    enrich_list = c(go_list$all, hallmark_msigdb),
    enrich_category = c("BP", "CC", "MH"),
    enrich_p_threshold = c(0.05, 0.05, 0.05),
    mark_features = hub_features,
    heatmap_width = 6,
    heatmap_height = 8
)
```

### Universal: plot features of interest

Selected features can be plotted with `plot_trend()`, which shows the mean and
standard deviation of replicates at each time point.

```{r plot-feature}
plot_trend(data_obj, assay = 1,
    features = c("Abtb1", "Dram1", "Ifi27", "Nufip1"),
    title = "Example features")

# Or pre-calculate mean and sd with calc_mean_sd()
table_mean_sd_orig <- calc_mean_sd(data_obj)$orig
plot_trend(table_mean_sd_orig,
    features = c("Abtb1", "Dram1", "Ifi27", "Nufip1"),
    title = "Example features")
```

Visualisation of features with high & low residual variance justify the
filtering strategy for WGCNA input.

```{r plot-residual}
plot_trend(data_obj, assay = 1,
    features = var_decomp |>
        dplyr::arrange(Residual) |> utils::head(12) |> dplyr::pull(Feature),
    title = "Features with lowest residual variance"
)

plot_trend(data_obj, assay = 1,
    features = var_decomp |>
        dplyr::arrange(dplyr::desc(Residual)) |>
        utils::head(12) |> dplyr::pull(Feature),
    title = "Features with highest residual variance"
)
```

### One-step preprocessing with `prepare_tide()`

`prepare_tide()` is a wrapper function that runs multiple preprocessing steps
in a single call, including input creation, normalisation, merging replicates,
variance decomposition, and feature filtering. It is convenient for standard
analyses, while running each step individually allows for more control over
parameters.

```{r prepare-tide}
tide <- prepare_tide(
    data = tutorial_data,
    sample_ann = tutorial_sample_info,
    filter_ratio = 0.5,
    min_groups = 2,
    keep = "below_quantile",
    residual_threshold = 0.75
)
tide$filter_summary
```

Variance decomposition and filtering are run on both `"orig"` and `"norm"`
assays. The returned list `tide` contains:

| Element | Description | Use for | Step-by-step equivalent in the tutorial |
|---------|-------------|---------|-------------------------|
| `tide$se` | Unmerged SE, all features | Correlation matrix, PCA, UMAP, Pairwise DE, plot_trend | `data_obj` |
| `tide$se_filtered$orig/norm` | Unmerged SE, filtered features | WGCNA | `data_obj_wgcna` |
| `tide$merged_list` | Per-group merged SEs, all features | calc_feature_property, run_Trendy | `data_obj_merged_list` |
| `tide$merged_list_filtered$orig/norm` | Per-group merged SEs, filtered features | - | - |
| `tide$merged_se` | Single merged SE, all features | plot_modules_v/h | `data_obj_merged` |
| `tide$merged_se_filtered$orig/norm` | Single merged SE, filtered features | plot_modules_v/h | - |
| `tide$variance$orig/norm` | Variance decomposition | plot_variance, enrichGO_rank | `var_decomp` |
| `tide$feature_property` | Feature property summary | group_specific_features | `property_tb` |
| `tide$filter_summary` | Per-stage filtering stats | QC | - |
| `tide$group_specific_filter` | Features removed by cross-group filter | Review group-specific features | - |
| `tide$DE` | DE results placeholder | Centralised storage | `DE_between_time/group_out`|
| `tide$enrichment` | Enrichment results placeholder | Centralised storage | `go_list`, `gse_group`, `hallmark_msigdb` etc. |
| `tide$WGCNA` | WGCNA results placeholder | Centralised storage | `net` |

Pairwise DE, WGCNA and enrichment can be run on the returned elements, and the
results can be appended to the list for centralised storage.

```{r tide-de}
tide$DE <- list(
    between_group = DE_between_group_out,
    between_time  = DE_between_time_out
)
tide$WGCNA <- net

tide$enrichment$GO_modules <- go_list
tide$enrichment$MSigDB <- hallmark_msigdb
tide$enrichment$GO_group <- gse_group
```

## Interoperability within Bioconductor ecosystem

TiDEomics uses SummarizedExperiment as the central data container. It keeps
the feature x sample matrices, sample annotation, and feature metadata
synchronized during normalisation, splitting, and filtering. The use of
multiple assays within the same object also allows original ("orig") and
baseline-normalized ("norm") data to remain linked throughout the workflow.

As SummarizedExperiment is a standard Bioconductor container, TiDEomics
data can be passed to other Bioconductor tools for further custom analysis
and visualisation. For example, [iSEE](https://bioconductor.org/packages/iSEE)
interactive SummarizedExperiment browser can be used to explore the data
in an interactive Shiny app.

```{r isee, eval = FALSE}
iSEE::iSEE(data_obj)
```

`prepare_tide()` output can be converted to a
[`DeeDeeExperiment`](https://bioconductor.org/packages/DeeDeeExperiment)
object for formatted summary. The nested DE and enrichment
results can be flattened with `flatten_DE()` or `flatten_enrich()` before
passing to the `DeeDeeExperiment` constructor.

```{r deedee}
# Flatten nested results
de_flat <- flatten_DE(tide$DE)
enrich_flat <- flatten_enrich(tide$enrichment)

# Build DeeDeeExperiment
dde <- DeeDeeExperiment::DeeDeeExperiment(
    sce = tide$se,
    de_results = de_flat,
    enrich_results = enrich_flat
)
```

## Common usage scenarios

TiDEomics supports two experimental designs, auto-detected from the sample
annotation:

- **Cell culture / independent samples**: `create_input(data, sample_ann)`
    -- each sample is independent. LMM uses `(1|Group) + (1|Time)`. DE uses
    unpaired designs.
- **Patient / repeated-measures**:
    `create_input(data, sample_ann, subject_col = "PatientID")` -- same
    subjects tracked across time points. LMM adds `(1|Subject)`.
    `DE_between_time()` uses paired design via `limma::duplicateCorrelation()`.
    `normalise_to_start(by_subject = TRUE)` subtracts each subject's own
    baseline. `merge_replicates()` and `plot_trend()` compute statistics across
    subjects.

Users can ask different biological questions and use different functions in
TiDEomics to answer them. For example:

- Which features change over time in each sample group? -> `DE_between_time()`,
    `calc_feature_property()`, `run_Trendy()`.

- Which features differ between groups at the same time point? ->
    `DE_between_group()` (For starting time point, use `assay = 1`; for later
    time points, use `assay = 1` for original input data, or use `assay = 2` for
    change-from-baseline).

- What is the temporal pattern for individual features? -> `run_Trendy()` +
    `summarise_Trendy()` + `extract_segment_trends()`.

- Which features are noisy vs biologically driven? -> `decomp_variance()` +
    `plot_variance()`, noisiness can be represented by `Residual` variance.

- What co-expression modules are present and what are their temporal profiles?
    -> `prepare_WGCNA()` + `run_WGCNA()` + `plot_modules_v()` or
    `plot_modules_h()`.

- What biological processes are associated with the genes of interest (e.g.
    co-expression modules, differentially expressed genes in each sample
    group)? -> `enrichGO_list()` + `plot_GO()`.

- What biological processes are associated with the genes ranked by their
    property (e.g. time or group contributed variance)? -> `enrichGO_rank()`.

- How to normalise the data? -> `normalise_to_start()` to focus on changes
    from baseline; apply global normalisation (quantile, median) and batch
    correction when appropriate, before `create_input()`.

- How to handle missing values? -> `impute_groups()` for `run_Trendy`.
    `plot_pca()` and `plot_umap()` automatically exclude features with missing
    values. `prepare_WGCNA()` filter out features with too many missing values.

## Session information

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

## References
