Choosing a clock

By the question, by the scale, and by whether the coefficients transfer at all

There is no best clock. There is a clock that answers the question you asked, and a much larger number that answer a different one in the same units.

Five generations of clock, and what the output of each one actually is

The generations are not successive improvements on one measurement. Each was trained against a different target, so each returns a different quantity: a predicted age whose signal is the residual, a mortality composite rescaled into year-like units, a rate, a pair of scores with no shared origin, and a division count. A later generation is not a better answer to the earlier question, and the first generation is still the right choice when the question really is how old a sample looks.

Choosing by research question

Question Ready to score Needs a licensed file No coefficients traced Scale
How old does this sample look? altumage, cage, corticalclock, hannum, horvath2013, intrinclock, lin, meer, and 11 more pchannum, pchorvath2013, pcskinandblood, retroclock, systemsageageprediction abec, cabec, ctsliver, eabec, encen100, encen40, ensembleagehumanmouse, gliasin, and 20 more age_years, relative_score
How old is this newborn, gestationally? bohlin, knight, leecontrol, leerefinedrobust, leerobust, mayne — epicga gestational_weeks
How fast is this person aging? dunedinpoam38 dunedinpace ensembleagestatic, ensembleagestatictop age_years, pace_ratio
Which organ system is aging fastest? — systemsage, systemsageblood, systemsagebrain, systemsageheart, systemsagehormone, systemsageimmune, systemsageinflammation, systemsagekidney, and 4 more dnamfitage age_years
Who is at risk of dying sooner, or is frailer? dnamfili, dnamic, dnamphenoage, hd, hrsinchphenoage, kdm, linage2, metabohealth, and 2 more cpgptgrimage3, cpgptpcgrimage3, dnamemrage, grimage, grimage2, omicmage, pcgrimage, pcphenoage fiage, mammalianlifespan, senmortalityage age_years, mortality_log_hazard, relative_score
Is damage separable from adaptation? yingadaptage, yingdamage — — age_years_relative
How much has this tissue divided? cellpopage, epicmithyper, epicmithypo, epitoc1, epitoc2, epitoc3, hypoclock, replitali, and 2 more — replitalinorm, sencultureage divisions, relative_score
What is the blood’s cell composition? deconvolutebloodepicbcell, deconvolutebloodepiccd4tcell, deconvolutebloodepiccd8tcell, deconvolutebloodepicmonocyte, deconvolutebloodepicneutrophil, deconvolutebloodepicnkcell twelvecelldeconvolutebloodepicbas, twelvecelldeconvolutebloodepicbmem, twelvecelldeconvolutebloodepicbnv, twelvecelldeconvolutebloodepiccd4mem, twelvecelldeconvolutebloodepiccd4nv, twelvecelldeconvolutebloodepiccd8mem, twelvecelldeconvolutebloodepiccd8nv, twelvecelldeconvolutebloodepiceos, and 4 more — proportion
How long are the telomeres? dnamtl pcdnamtl — telomere_kb
What is this person exposed to, or how do they live? dnamstress, mccartneyalcohol, mccartneybmi, mccartneybodyfat, mccartneyeducation, mccartneyhdlcholesterol, mccartneyldlcholesterol, mccartneysmoking, and 3 more grimage2packyrs, pcgrimagepackyrs dnamfitagegaitf, dnamfitagegaitm, dnamfitagegripf, dnamfitagegripm, dnamfitagevo2max, reedbmi gestational_weeks, relative_score
What is a specific protein or lab value likely to be? downsyndrome grimage2adm, grimage2b2m, grimage2cystatinc, grimage2gdf15, grimage2leptin, grimage2loga1c, grimage2logcrp, grimage2pai1, and 8 more bocklandt, compil6, garagnani relative_score
Is a named disease more likely? depressionbarbu, prostatecancerkirby — adbahadosingh, cvdwesterman, hepatoxu relative_score
What is the sample’s chromosomal sex, or its species? — — mammalianfemale, xchrom, ychrom relative_score

Every one of the 178 catalogued clocks appears above, routed by its declared predicts field. The full table with paper links is the clock catalogue.

“Ready to score” means the coefficients ship in the package. The rest are catalogued and need a file; see Availability below.

reg = fa.registry.load()
reg.filter(generation="second", availability="bundled")
reg.compatible_with(d)
list_clocks(generation = "second", tier = "bundled")
compatible_clocks(d)

Scale type and permitted operations

Every clock carries a scale_type, and it decides which downstream operations are defined.

Scale Clocks Permitted Refused, and why
age_years 80 acceleration, correlate, difference, mean, residual —
relative_score 53 correlate, rank everything except correlation and rank: there is no external unit to difference or average
proportion 18 compositional, correlate, difference, mean anything ignoring the sum-to-one constraint
divisions 10 correlate, difference, mean, rank acceleration: a division count is not elapsed time
gestational_weeks 8 acceleration, correlate, difference, mean, residual mixing with an age in years
mortality_log_hazard 3 correlate, hazard ratio, mean, rank acceleration: a log-hazard has no zero on the age scale
telomere_kb 2 acceleration, correlate, difference, mean comparison with an age in years; and higher is the younger direction here, unlike every age clock beside it
pace_ratio 2 correlate, difference, mean, rank acceleration: it is already a rate
age_years_relative 2 correlate, difference, mean, residual acceleration: the clock tracks age with a slope near one but its origin moves between cohorts, so predicted minus chronological measures the cohort

acceleration() enforces this. Naming a clock whose scale does not support it is an error; leaving the clock list empty quietly excludes them.

Coverage and validity

A clock trained on 450K runs perfectly well on EPIC data that carries its probes, and fails on 450K data filtered to 20,000 probes. Only the feature list can answer whether a clock is usable, which is what compatible_clocks() checks.

But high coverage does not make a result meaningful. The mammalian methylation array was designed around CpGs conserved across mammals, and carries 96% of Horvath2013’s 353 probes, so a zebra scores at higher coverage than many human 450K datasets and returns a confident number from a clock fitted on people. Nothing in the arithmetic can notice. Set species and FALCONAge warns:

d <- falcon_data(betas, modality = "dna_methylation", species = "Equus grevyi")
res <- score(d, clocks = "horvath2013")
manifest(res)$warnings
#> trained on Homo sapiens, scored on Equus grevyi. Feature coverage says nothing
#> about whether the coefficients transfer across species.

Feature count and coefficient mass

Coverage has two forms and they disagree exactly when it matters: how many of a clock’s features are present, and what share of the model’s total |coefficient| those present features carry. Check both before committing to a clock on a given array.

fa.probe_loss(d, clocks="scoreable")
#> horvath2013   coverage 0.9632   mass_coverage 0.6117   cg16867657 (14.1%), ...

Horvath 2013 on one EPIC v2 array: probes present against model weight present

Ninety-six per cent of probes present, sixty-one per cent of the model. The scoring floor applies to both, so this is refused with the heaviest absent probes named rather than scored into a confident number. The gap opens because elastic-net coefficients are far from uniform: a handful of probes carry a disproportionate share of the total weight, and losing four per cent of the probes loses thirty-five per cent of the model when the missing four per cent are those.

→ The science, §19.5 prices the same loss in years, per clock per platform, and explains why the PC clocks barely move where first-generation clocks shift by double digits.

Reliability

“Reliable” means two different things, and they do not track together:

What is repeated What it measures
Technical ICC the same DNA, assayed twice the assay, and the model’s sensitivity to array noise
Biological ICC the same person, sampled again days later whether the number is a property of the person or of the morning

Nearly every clock is technically excellent; biological ICCs are substantially lower. GrimAge2 and DunedinPACE are among the most biologically fragile, and they are the two most often used to claim an intervention worked. res.interpretation() carries both columns, with None meaning “not established” rather than “fine”.

The rule: measuring change within a person, prefer a clock with a published biological ICC, or a principal-component version. → The science, §17.7.

Measure it on your own cohort

A published ICC is somebody else’s laboratory. Measure yours:

se = fa.technical_se(res, data)
se.diagnostics[["median_se", "se_over_sd", "implied_cohort_icc"]]
se_over_sd
measurement error as a fraction of the spread between your own samples. Above 1, the clock cannot distinguish two of your samples however different their point estimates look.
implied_cohort_icc
the reliability this cohort implies. Often below the published figure, because a narrow age range leaves less between-sample variance for the same assay noise.

For scale: on this package’s test corpus Horvath 2013 comes out at ±1.58 years, DunedinPoAm38 is least repeatable at 0.72, and the 319,607-probe BLUP clock reaches 1.00. With technical duplicates, fa.icc_from_replicates(data, "subject") replaces the published per-probe table with your own plates. → The science, §19.2 for the propagation itself.

Then ask how many samples you need

fa.power("horvath2013", effect=0.5, result=pilot)

At ICC 0.9 a tenth of the sample size exists only to average out the instrument, and the function says so. It refuses to default the standard deviation, because n scales with its square.

Specimen type

The same 91 donors, scored from saliva and from buffy coat

Saliva clock ages ran 3.83–16.46 years above buffy coat in the same 91 people, while still correlating with them at Spearman 0.45–0.69. A correlation check passes and the output is a decade out, because correlation is not agreement. The left panel is the whole argument: every point sits above the line of identity, which is a bias, and the scatter around it stays tight, which is what a correlation coefficient reports. The paired lines on the right show the same donors shifting in the same direction, so no amount of averaging removes it.

The rule: set obs["tissue"] on every dataset. FALCONAge then compares it against what each clock was fitted on. Pan-tissue clocks say nothing, most warn with the measured discordance, and twelve refuse outright. Cell-free DNA is refused by every clock. No tissue column gets one warning saying the check did not run.

→ The science, §19.4 for the twelve refusals and the measurements behind each. Reliability, technical against biological, is [39].

Availability

Availability n What you do
bundled 69 Nothing. Coefficients ship inside the package, or the clock is a formula with none.
untraced 52 Catalogued, but no primary source has been established, so no coefficients ship.
licensed 57 Obtain a coefficient file and register it. The architecture is implemented and tested.
list_clocks(tier = "C")     # the ones needing author permission
list_clocks(untraced = TRUE)

The distinction between B and C matters. C is a licensing decision by the authors. B is an honesty one by us: copying a coefficient set out of another package without knowing where that package got it is how the known paper-versus-implementation discrepancies spread in the first place, and a traced extractor is a welcome contribution.

The clock catalogue

reg = fa.registry.load()
reg.filter(availability="bundled")
reg.search("mortality")
reg.get("horvath2013")
reg.get("dnamphenoage").cite("bibtex")
list_clocks(tier = "bundled")
list_clocks(search = "mortality")
clock_info("horvath2013")
cite_clock("dnamphenoage", "bibtex")

Forty clocks are licensed scaffolds: the model, the feature list, the transform chain and the expected shapes all ship, but the coefficients are research-use-only or have no public source FALCONAge may redistribute. Asking about one says why, where to obtain a file, and which open clocks answer the same question.

fa.registry.register_local_weights("grimage2", "~/licensed/grimage2_coefs.csv")
fa.score(d, clocks=["grimage2"])
register_local_weights("grimage2", "~/licensed/grimage2_coefs.csv")
score(d, clocks = "grimage2")