Clinical chemistry and survival
Clinical chemistry
d <- read_clinical("nhanes.csv", units = list(
albumin = "g/L", creatinine = "umol/L", glucose = "mmol/L", crp = "mg/dL",
lymphocyte_percent = "%", mean_cell_volume = "fL",
red_cell_distribution_width = "%", alkaline_phosphatase = "U/L",
white_blood_cell_count = "10^3/uL", age = "years"))
score(d, clocks = "phenoage")Units are required and never guessed. Albumin at 4.2 is g/dL and albumin at 42 is g/L; both are clinically normal, and PhenoAge fitted on one returns nonsense for the other. No range check can separate them, so the only correct behaviour is to ask.
KDM and homeostatic dysregulation have no coefficients of their own: each is defined relative to a reference cohort. FALCONAge ships one such fit, BioAge’s NHANES III KDM (fa.models.clinical.kdm_bioage); for any other reference, fit it:
ref <- fit_kdm(nhanes3, markers = markers)
score(d, clocks = "kdm", reference = ref)LinAge2
LinAge2 (Fong et al. 2025) is trained on survival rather than age: examination, blood and urine inputs in NHANES variable names and units, three scores from health questionnaire items, and a Cox model on principal components, one per sex, returned as the age at which a model on age alone gives the same risk. The inputs are named as in the authors’ codebook, with age and sex in obs:
d = fa.FalconData(X=nhanes_style_inputs, obs=obs[["age", "sex"]], modality="clinical_chemistry")
fa.score(d, clocks=["linage2"])Cotinine (LBXCOT) in ng/mL is binned into the authors’ four smoking categories. A missing lipid sets LDL to 0, as the authors’ code does, and the result names the samples that happened to; any other missing input leaves that sample without a score. The parameters are what the authors’ linAge2.R fits from the NHANES file it ships with, and FALCONAge reproduces its output to 5e-10 years on the 2,344 subjects of their test wave.
Blood-count ratios
fa.blood_count_ratios(cbc, units={"neutrophils": "10^9/L", "lymphocytes": "10^9/L",
"monocytes": "10^9/L", "platelets": "10^9/L"})NLR, LMR, PLR and SII from a complete blood count. Units are declared, as everywhere; counts are converted to 10^9/L. Percentages give the two ratios that do not depend on the total (NLR, LMR) and not the two that involve platelets, and a zero lymphocyte count gives no ratio rather than infinity.
Which marker carries an association
A composite clinical clock associated with an outcome does not say which of its markers carries the association. fa.leave_one_marker_out(res, "kdm", data=d, reference=ref, test="outcome") recomputes KDM, HD or PhenoAge without each marker in turn (KDM and HD are refitted; for PhenoAge the marker is held at the reference mean) and reports how far the score and the association moved.
Survival and association
Two figures that most aging-clock papers carry, and that need the analysis functions above.
Survival
fa.cox_hazard(res, time_col="time", event_col="status") # HR per unit (per year for an age clock)
fa.plot.kaplan_meier(res, "horvath2013",
time_col="time", event_col="status") # the figurecox_hazard(res, time_col = "time", event_col = "status")
plot_kaplan_meier(res, "horvath2013", time_col = "time", event_col = "status")The curves compare the fastest-ageing tenth with the slowest, not two halves. The middle of the acceleration distribution is where a clock discriminates least, so a median split dilutes the comparison; the tails are the published convention. The log-rank p is in the subtitle.
If the curves cross: the hazards are not proportional and the single hazard ratio beside them is an average over a period during which the ordering changed. That is worth saying in a caption rather than leaving for a reader to notice.
Association
assoc = fa.associate(res, "crp", covariates=("age", "sex"))
fa.plot.volcano(assoc, fdr=0.05)assoc <- associate(res, "crp", covariates = c("age", "sex"))
plot_volcano(assoc, fdr = 0.05)The dashed line is the Benjamini-Hochberg threshold at the stated FDR, read off the q column rather than recomputed. This matters more than it looks: across a few hundred tests a raw 0.05 admits roughly one in twenty of the nulls, and drawing that line instead labels them significant.