8. R package
8.1 What “identical” means here
FALCONAge is not a reimplementation. Every number it returns is computed by the Python core through reticulate, so the R and Python answers are the same bytes because they are the same computation. What the R package adds is an interface an R user would have written: S3 classes with print, summary, plot and as.data.frame; data.frame in and data.frame out; roxygen2 documentation; a pkgdown site; and errors that arrive as R conditions rather than Python tracebacks.
This is the design tAge uses (tAge/R/predict.R), with one difference: tAge asks the user to set RETICULATE_PYTHON by hand, and FALCONAge manages the environment itself.
8.2 Environment management
R/install.R
falconage_install(method = c("auto", "uv", "virtualenv", "conda"),
envname = "r-falconage",
version = NULL, # NULL = the version matching this R package
gpu = FALSE,
force = FALSE)
falconage_config() # what Python, what version, what device, what registry
falconage_available() # logical, does a working core exist
falconage_update()method = "auto" prefers uv when it is on the PATH, because it resolves in seconds rather than minutes, and falls back to virtualenv. gpu = TRUE installs the CUDA torch build.
version = NULL pins the Python core to the exact version that matches the R package, read from DESCRIPTION’s Config/falconage/python-version field. This is the mechanism that stops an R package from silently driving a Python core it was never tested against.
falconage_config() prints the same block the CLI prints, so a bug report from an R user and one from a Python user carry comparable information:
FALCONAge 1.0.0 (R)
python core 1.0.0 /home/x/.virtualenvs/r-falconage/bin/python
torch 2.6.0 CUDA 12.4
device cuda:0 NVIDIA RTX A4000, 16 GB
dtype float64
registry 1.1.0 (178 clocks)
cache /home/x/.cache/falconage (2.1 GB)
8.3 The bridge
R/zzz.R and R/bridge.R
.falconage <- new.env(parent = emptyenv())
.onLoad <- function(libname, pkgname) {
.falconage$py <- reticulate::import("falconage", delay_load = list(
environment = "r-falconage",
on_load = function() check_version_compatibility(),
on_error = function(e) NULL # defer the error to first use
))
set_default_options()
}delay_load matters: without it, library(FALCONAge) fails on a machine with no Python environment yet, which breaks R CMD check on CRAN’s build farm. With it, the package loads, falconage_available() returns FALSE, and every exported function that needs the core calls require_core(), which raises a condition telling the user to run falconage_install().
Type marshalling. Explicit, never implicit.
| R | Python | Direction |
|---|---|---|
data.frame |
pandas.DataFrame |
both, via reticulate |
falcon_data |
.h5ad path |
both: the object is passed as a file path, never marshalled cell by cell |
falcon_result |
.h5ad path plus a results data.frame |
both |
character(1) |
str |
both |
NULL |
None |
both |
NA |
float('nan') for numerics, None for strings |
R to Python |
matrix |
numpy.ndarray |
both |
The falcon_data rule is the important one. Passing an 850,000-column matrix through reticulate cell by cell is slow and copies twice. Instead the R object holds a path to an .h5ad file plus a cached summary (dimensions, platform, sample table), and both languages read the same file. This is also what makes a mixed-language workflow possible: preprocess in R, score in Python, plot back in R, one artefact throughout.
Error translation. bridge.R wraps every call:
py_call <- function(fn, ...) {
tryCatch(fn(...), error = function(e) {
info <- parse_python_error(e) # class, message, .remedy
rlang::abort(
message = info$message,
class = paste0("falconage_", info$class),
remedy = info$remedy,
parent = e
)
})
}A MissingUnitsError from Python becomes an R condition of class falconage_missing_units_error, so R users can tryCatch on it, and the message is the one-sentence remedy rather than a 40-line traceback. The Python traceback stays available as parent.
8.4 Public API - one-to-one with Python
Every Python verb has an R counterpart with the same name in snake_case and the same argument names. This table is the contract the conformance tests enforce (§10.5).
| Python | R | Returns |
|---|---|---|
fa.download() |
download() |
falcon_download |
fa.download(dry_run=True) |
download(dry_run = TRUE) |
data.frame |
fa.prepare() |
prepare() |
falcon_data |
fa.prepare_clinical() |
preprocess_clinical() |
falcon_data |
fa.qc() |
qc() |
falcon_qc |
fa.score() |
score() |
falcon_result |
fa.acceleration() |
acceleration() |
data.frame |
fa.cox_hazard() |
cox() |
data.frame |
fa.icc() |
icc() |
data.frame |
fa.run_benchmark() |
run_benchmark() |
falcon_benchmark |
fa.registry.list() |
list_clocks() |
data.frame |
fa.registry.get() |
clock_info() |
list |
fa.registry.cite() |
cite_clock() |
character |
fa.registry.compatible_with() |
compatible_clocks() |
data.frame |
fa.plot.ba_vs_ca() |
plot_ba_vs_ca() |
ggplot |
fa.report() |
report() |
path, invisibly |
Plotting is the one place the two diverge deliberately. Python returns matplotlib figures; R calls the Python *_data() function to get the frame and renders it with ggplot2, so an R user gets a ggplot object they can modify. The data is identical; the rendering is native to each language.
8.5 S3 classes
R/classes.R
# falcon_data
print.falcon_data # dimensions, modality, platform, species, age availability
summary.falcon_data # plus per-clock coverage
dim.falcon_data
as.data.frame.falcon_data
# falcon_result
print.falcon_result # clocks run, samples, warnings
summary.falcon_result # per-clock mean, SD, missingness, flags
as.data.frame.falcon_result # the long table
plot.falcon_result # dispatches to plot_ba_vs_ca()
`[.falcon_result` # subset by clock or sample
# falcon_qc
print.falcon_qc
plot.falcon_qcprint.falcon_result shows what a user needs before trusting the numbers:
<falcon_result> 120 samples x 5 clocks
clock unit mean sd missing flags
horvath2013 years 54.2 9.1 0.8% -
hannum years 51.7 8.4 0.0% -
phenoage years 56.9 11.2 0.0% -
grimage2 years 61.4 8.8 12.4% -
dunedinpace ratio 1.03 0.11 2.1% -
2 warnings. See warnings(x).
grimage2 is research-use-only.
8.6 DESCRIPTION
The file as it ships, not as it was specified: the differences are noted below it:
Package: FALCONAge
Title: Multiomic Biological Age and Aging Clock Scoring
Version: 1.0.0
Authors@R:
person("Bhagesh", "Hunakunti", , "bhunakun@uni-bonn.de", role = c("aut", "cre"),
comment = c(ORCID = "0000-0002-5957-8005"))
Description: Scores DNA methylation and clinical chemistry data against a
catalogue of 178 published aging clocks, and provides the downstream
statistics the field expects: age acceleration in its three conventions,
survival and association models, technical reliability, and the AA1/AA2
benchmark. Numerical work is delegated to the 'falconage' Python package
through 'reticulate', so results from R and from Python are the same bits
rather than two implementations that agree approximately. Twenty-three
clocks carry coefficients inside the package and run offline; twenty-eight
are implemented as scaffolds whose research-use-only coefficients the user
supplies.
License: GPL (>= 3)
URL: https://github.com/bhagesh-h/FALCONAge,
https://bhagesh-h.github.io/FALCONAge/,
https://bhagesh-h.github.io/FALCONAge/r
BugReports: https://github.com/bhagesh-h/FALCONAge/issues
Depends:
R (>= 4.1)
Imports:
grDevices,
reticulate (>= 1.34),
stats,
utils,
yaml
Suggests:
ggplot2,
knitr,
optparse,
patchwork,
rmarkdown,
testthat (>= 3.0.0)
VignetteBuilder: knitr
biocViews: Epigenetics, DNAMethylation, DifferentialMethylation,
MethylationArray, Software
Config/testthat/edition: 3
Encoding: UTF-8
Language: en-GB
Roxygen: list(markdown = TRUE)
RoxygenNote: 7.3.2
Four departures from the specification, each deliberate:
| Specified | Shipped | Why |
|---|---|---|
ggplot2, cli, rlang, jsonlite in Imports |
ggplot2 in Suggests; the other three absent |
Plotting returns a data frame and draws with base graphics, so grDevices is the hard dependency and ggplot2 is optional. Four fewer required packages is four fewer ways for a CRAN check to fail on a machine we do not control. |
Methods implemented include ... <doi:...> in the description |
Absent | CRAN wants DOIs for methods the package implements in R. Every method here is implemented in the Python core; the per-clock references live in the registry and come back from cite(), which is a truer place for them than a fixed list of five. |
SystemRequirements: Python (>= 3.10) ... |
Absent | On reticulate ≥ 1.41 the environment is provisioned automatically on first use, so Python is not a system requirement the user must satisfy. Still to be added before a CRAN submission, since the field is also documentation. |
Config/falconage/python-version |
Absent | The R package pins the Python core by git tag inside falconage_install(), so the field would be a second copy of a number that already exists in one place. |
Every example and every test that needs the core is wrapped in skip_if_not(falconage_available()), so R CMD check passes on a machine with no Python.
8.7 Roxygen conventions
Following the cyRAVEN register, see R/thresholds.R in that package for the model. Every exported function gets:
- an opening paragraph saying what it does, then why it does it that way when the choice is not obvious
@paramon every argument, naming the unit or the allowed values@returnon every export, describing the object rather than naming its class@examples, runnable, wrapped in\donttest{}when they need the core or the network@seealsolinking the Python equivalent by name@referenceswith the DOI for anything implementing a published method
Worked example:
#' Score biological age with one or more aging clocks
#'
#' WHAT: applies published aging-clock definitions to a prepared dataset and
#' returns one value per sample per clock, with the provenance needed to
#' interpret it.
#'
#' WHY THE DEFAULT IS `"compatible"` RATHER THAN `"all"`: the registry holds
#' clocks for several species, platforms and molecular layers, and most of them
#' do not apply to any given dataset. Running everything produces a table whose
#' majority of rows were computed almost entirely from imputed reference values,
#' which looks like a result and is not one. `"compatible"` selects the clocks
#' whose species, platform and feature coverage actually match the data, and
#' reports what it excluded.
#'
#' @param data A `falcon_data` object from [preprocess_methylation()] or
#' [preprocess_clinical()].
#' @param clocks Clock identifiers, or `"compatible"` (default) to select
#' automatically, or `"all"`. See [list_clocks()].
#' @param device `"auto"`, `"cpu"`, `"cuda"`, or a specific device such as
#' `"cuda:1"`. `"auto"` uses CUDA when it is available.
#' @param dtype `"float64"` (default) or `"float32"`. Clocks whose registry
#' entry requires double precision are promoted regardless, with a warning.
#' @param max_missing Largest fraction of a clock's features that may be absent
#' before scoring that clock is an error rather than a warning. Default 0.30.
#' @param sex_unknown What to do when a clock needs sex and it is not recorded:
#' `"error"` (default), `"impute_from_methylation"`, or `"half"`.
#'
#' @return A `falcon_result`. [as.data.frame()] gives one row per sample per
#' clock with the value, its unit, its scale type, the fraction of features
#' that had to be imputed, and any flags raised. The underlying dataset,
#' including the run manifest, is reachable through the object.
#'
#' @examples
#' \donttest{
#' if (falconage_available()) {
#' data <- preprocess_methylation(
#' system.file("extdata", "example_betas.csv", package = "FALCONAge"),
#' platform = "illumina_450k"
#' )
#' res <- score(data, clocks = c("horvath2013", "phenoage"))
#' head(as.data.frame(res))
#' }
#' }
#'
#' @seealso [acceleration()] for age acceleration, [list_clocks()] to browse the
#' registry, and the Python equivalent `falconage.score()`.
#' @references
#' Horvath S (2013). DNA methylation age of human tissues and cell types.
#' Genome Biology 14:R115. \doi{10.1186/gb-2013-14-10-r115}
#' @export
score <- function(data, clocks = "compatible", device = "auto",
dtype = "float64", max_missing = 0.30,
sex_unknown = "error", ...) { }8.8 Command-line front end
inst/scripts/falconage.R, invoked the cyRAVEN way:
Rscript "$(Rscript -e 'cat(system.file("scripts","falconage.R",package="FALCONAge"))')" \
score --input prepared.h5ad --clocks horvath2013,grimage2 --outdir results/Options are built in R/cli-options.R with optparse, and the flag names match the Python CLI exactly, so a script written against one runs against the other with only the interpreter changed.
8.9 pkgdown reference groups
r/_pkgdown.yml. Group titles are identical to the quartodoc sections in §11, which is what makes the two sites navigable as one.
url: https://bhagesh-h.github.io/FALCONAge
template:
bootstrap: 5
light-switch: true
reference:
- title: Setup
desc: >
FALCONAge computes through a Python core. These functions create and inspect
that environment; everything else assumes it exists.
contents: [falconage_install, falconage_config, falconage_available, falconage_update]
- title: Download
desc: >
Public data by accession. One verb covers GEO, ArrayExpress, SRA, Zenodo,
Figshare, Hugging Face and GDC, and returns files alongside a sample table
with age and sex extracted from whatever free-text field held them.
contents: [download, probe, cache_ls, cache_rm, cache_verify]
- title: Preprocess
desc: >
Raw intensities or a public matrix into a scoreable one. Normalisation is
selectable and recorded, because it changes the answer.
contents: [preprocess_methylation, preprocess_clinical, detect_platform,
aggregate_epicv2, estimate_sex, qc]
- title: Score
desc: One call runs any number of clocks. Every value carries its provenance.
contents: [score, acceleration]
- title: The clock registry
desc: >
What is available, what it predicts, in what unit, on what platform, and
where the published definition and the circulating coefficients disagree.
contents: [list_clocks, clock_info, compatible_clocks, cite_clock,
registry_version]
- title: Analysis
desc: >
Tests on per-sample values. Age acceleration is standardised within sex
before any hazard ratio, so effect sizes compare across clocks.
contents: [cox, c_index, associate, interactions, icc, run_benchmark]
- title: Figures
contents: [starts_with("plot_"), theme_falconage]
- title: Reporting
contents: [report, run_manifest]
articles:
- title: Get started
navbar: ~
contents: [FALCONAge, methylation]
- title: Working with data
navbar: Working with data
contents: [download, clinical, clocks]
- title: Going further
navbar: Going further
contents: [gpu, benchmarking]