Skip to content
Gladyshev-LabPublic

About

R package for transcriptomic biological age analysis

Resources

Stars

29 stars

Watchers

0 watching

Forks

Repository files navigation

tAge

tAge logo

R package for transcriptomic biological age prediction from gene expression data, implementing the transcriptomic clocks from Tyshkovskiy et al. (2026), Nature.

The clocks predict two kinds of biological age from bulk RNA-seq (or pseudobulk single-cell) expression: chronological age and expected mortality. tAge handles preprocessing, normalisation, gene mapping across species, and prediction with the pre-trained Elastic Net (EN) and Bayesian Ridge (BR) models.

Installation

# install.packages("devtools")
devtools::install_github("Gladyshev-Lab/tAge")

Python dependency

tAge calls Python (via reticulate) to run the model predictions. Create a virtual environment with the packages listed in inst/python/requirements.txt:

python -m venv .venv
.venv/bin/pip install -r inst/python/requirements.txt   # joblib pandas scikit-learn numpy

Point R at that interpreter before calling predict_tAge():

# Linux / macOS
Sys.setenv(RETICULATE_PYTHON = ".venv/bin/python")
# Windows
Sys.setenv(RETICULATE_PYTHON = ".venv/Scripts/python.exe")

Clock models

Models are published on Zenodo (record 22166800): 60 composite clocks as single files and the rodent and multispecies module clocks as two archives. The bundled registry mirrors the record; list and download from R:

library(tAge)

# Browse available clocks (filter by type / outcome / species / tissue / scaling)
list_clocks(type = "EN", outcome = "Mortality")

# Download a chosen set; returns the table with a `path` column
clocks <- list_clocks(type = "EN", outcome = "Mortality",
                      species = "Multispecies", tissue = "Multi-Tissue")
clocks <- download_clocks(clocks, dest_dir = "clocks")

model_paths <- list(
  scaled_diff = clocks$path[clocks$scaling == "Scaled"],
  yugene_diff = clocks$path[clocks$scaling == "YuGene"]
)

# Module clocks: one file per co-expression module, fetched as one archive per
# set ("Rodents" or "Multispecies") and unpacked into dest_dir
modules <- download_clocks(list_module_clocks(outcome = "Mortality", species = "Rodents"),
                           dest_dir = "clocks")

Quick start

Bulk RNA-seq

library(tAge)
Sys.setenv(RETICULATE_PYTHON = ".venv/bin/python")

# 1. Load raw counts (genes x samples) and sample metadata (samples x variables)
exprs_data <- read.csv("expression_matrix.csv", row.names = 1)
metadata   <- read.csv("metadata.csv", row.names = 1)
eset <- make_ExpressionSet(exprs_data, metadata)

# 2. Preprocess: filter, map genes, RLE-normalise, log, scale, YuGene, centre.
#    Gene identifiers (Ensembl / Gene.Symbol / Entrez) are detected automatically.
#    With several tissues, preprocess and centre each on its own controls:
tAge_eset <- tAge_preprocessing(
  eset,
  species = "mouse",
  split_by = "Tissue",
  control_group_column = "Genotype", control_group_label = "WT"
)

# 3. Predict (the species was recorded by tAge_preprocessing). The clock table
#    from download_clocks() can be passed as is: one column per clock, named
#    after the model file, as in the Python package.
results <- predict_tAge(tAge_eset, clocks)
head(results)
attr(results, "tage_units")   # unit of every prediction column

A named list, list(scaled_diff = ..., yugene_diff = ...), works too: with one model per representation the columns are scaled_diff_EN_tAge and yugene_diff_EN_tAge, with several they are named after the model files.

species is the species of the samples and only sets the maximum lifespan used to express chronological-age clocks in months (rodents) or years (primates); it is not the species group a clock was trained on. age_units forces months or years, and normalized_age = "percent" reports normalized-age clocks as a percentage of the expected maximum lifespan, as the paper does.

Single-cell (pseudobulk)

library(Seurat)

eset <- aggregate_on_obs_columns(
  seurat_obj,
  obs_column_names = c("sample_id", "tissue"),
  coverage_threshold = 1e7
)
eset <- flag_outliers(eset, split_by = "tissue")   # adds eset$tage_outlier
eset_clean <- eset[, !eset$tage_outlier]

results <- tAge_by_group(
  eset_clean,
  split_by = "tissue",
  model_paths = model_paths,
  species = "mouse",
  mode = "EN",
  control_group_column = "age",
  control_group_label  = "young"
)

Statistics

tage_compare_groups() and friends implement the statistical models of the paper's clock analyses; the R and Python packages agree numerically. Elastic net clocks are compared with estimated marginal-mean contrasts of value ~ group + covariates (emmeans on an lm); Bayesian ridge clocks go through a REML meta-regression (metafor::rma.uni) that weights each sample by its own prediction standard deviation and reports z-tests.

# BR predictions carry a matching <column>_sd column.
results <- predict_tAge(tAge_eset, model_paths, species = "mouse", mode = "BR")

tage_compare_groups(
  results,
  value_columns   = "yugene_diff_BR_tAge",
  group_column    = "Genotype",
  reference_group = "WT",
  covariates      = "Sex",
  split_by        = "Tissue",
  se_columns      = "yugene_diff_BR_tAge_sd",   # BR clocks only
  p_adjust        = "BH"
)

One row per contrast, with estimate always meaning group2 - group1, plus se, a 95% interval in ci_low / ci_high, statistic, df, p_value, p_adjusted and a label holding the usual *** ** * ^ stars.

Function Description
tage_compare_groups() marginal-mean contrasts between groups
tage_regress_continuous() slope of tAge against a numeric predictor
tage_module_stats() per-module effect sizes and p-values for module clocks
tage_adjust_covariates() covariate-adjusted values matching what the model tested
tage_significance_stars() *** ** * ^ labels

Two figures are built directly on those tests, and return the statistics as the "tage_stats" attribute:

Function Description
tage_clock_forest() one clock per row, effect with 95% CI, filled = survives the correction
tage_module_heatmap() module effects as a heatmap, modules × strata; module_set ("rodent", "multispecies", "human") picks the row annotation
p <- tage_clock_forest(
  results, clocks_meta = clocks,
  group_column = "Genotype", reference_group = "WT", split_by = "Tissue"
)

Every figure is drawn with theme_tage() from one set of colour tokens (tage_colors()), the same as the Python package: groups take colour-vision-safe categorical slots in a fixed order with the reference group in grey (tage_series_colors()), each clock outcome keeps its colour across figures, signed effects use one blue–grey–red scale centred on zero, and text never wears a data colour. Add theme_tage() to your own ggplots to match.

A ggplot carries no size of its own, so the figure functions record the size they were designed for and grow it with the number of clocks, modules and panels. tage_save_plot() uses that size; tage_fig_size() reads it back for a knitr chunk:

tage_fig_size(p)                       # width and height in inches
tage_save_plot(p, "forest.png")        # saved at the recorded size
tage_save_plot(p, "forest.pdf", width = 7)   # override just the width

# or fix the size up front
tage_clock_forest(..., width = 7, height = 4.5)
tage_clock_forest(..., row_height = 0.22, panel_width = 4.0)
tage_module_heatmap(..., cell_height = 0.28, cell_width = 0.9)

variance_strata chooses whether the residual variance comes from the two compared groups ("subset") or from every group in the stratum ("all_data"); p_adjust_scope chooses the correction family — "within_column" across comparisons and strata of one clock, "across_columns" across clocks or modules within a comparison and stratum, or "global".

tage_boxplot() uses this engine by default (stat_method = "emmeans"); pass stat_method = "t.test" (or another ggpubr::stat_compare_means method) for a plain two-sample test.

Interpreting the output

The units of the prediction depend on the clock outcome:

Outcome Column Units Notes
Chronological *_tAge months (rodents) or years (primates); age_units overrides normalised age rescaled by species max lifespan (tage_species())
Mortality *_tAge log10(hazard ratio) not an age; higher = higher expected mortality
Normalized age *_tAge fraction of max lifespan, or % with normalized_age = "percent" age / expected maximum lifespan of the experimental model

Only chronological clocks are rescaled to age units. Mortality clocks output a log10 hazard ratio and must not be interpreted in years — tAge derives this automatically from the clock registry (and from the file name for models outside it). The "tage_units" attribute of the result records the unit of every column.

Relative (differential) clocks and centring

All distributed clocks are relative (_scaleddiff / _yugenediff) models: they operate on expression centred against a reference group. tAge_preprocessing() centres the data automatically:

  • No reference group given (default): centres on all samples (overall per-gene median). Use this for a general age/mortality readout of a cohort.
  • Reference group given: pass control_group_column and control_group_label to centre on age-/sex-matched controls within your design (recommended when comparing treatment vs control).
  • Several tissues, datasets or cell types: pass split_by. The clocks were trained on expression centred within each dataset and tissue, so filtering, normalisation and centring must happen per stratum; a single reference pooled across tissues mixes tissue differences into the signal. tAge_by_group() does the same and predicts in one call.

The bundled example is the Klotho-knockout dataset of the paper (kidney and skeletal muscle, WT vs KO). Note that the distributed clocks contain the Klotho gene itself, whereas the paper's Klotho analysis used clocks retrained without it; expect the KO effect here to be somewhat larger.

Supported species

Mouse, human, rat, monkey (tage_species()). Non-mouse species are mapped to mouse orthologs internally, so all clocks operate in the mouse Entrez gene space. Input identifiers may be Ensembl (with or without version suffix), gene symbols or Entrez IDs; map_genes() detects which.

Citation

Tyshkovskiy et al. (2026) Universal Transcriptomic Hallmarks of Mammalian Ageing and Mortality. Nature.

License

This package and the associated transcriptomic clock models are distributed under the MGB Open Access License 1.0. These materials may be used for non-commercial academic purposes, subject to the license terms. Commercial uses require a separate commercial license or agreement with Mass General Brigham.

For questions about commercial use, please contact MGB Innovation / Business Development & Licensing:

About

R package for transcriptomic biological age analysis

Resources

Stars

29 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages