Skip to content

How to Track Your Own Biomarkers Over Time

Oak
A long receding row of glass-stemmed plants holding colored sap glows beside a misty river in a bioluminescent valley at dusk.

By the end of this you will have a single tidy table of every biomarker measurement you have ever had (one row per analyte per draw, units normalized, LOINC-coded), per-analyte reference change values computed from your own replicate and serial data, and a small set of trend fits that tell you when a move is larger than your own noise. You need: your historical lab PDFs or portal CSVs, Python 3.11+ with pandas, scipy, statsmodels, and pyarrow, SQLite (already on your machine), and ideally at least four draws of the same panel taken under comparable conditions. If you have CGM exports, proteomics NPX files, or whole-blood RNA-seq counts, those slot into the same schema at the end. Everything here is measurement and interpretation. Deciding whether a change matters clinically, or acting on one, is a conversation with a physician.

1. Decide what you are tracking, and why that question has no canonical answer

People search for “the five biomarkers” and “the five metabolic health markers.” There is no official five. The metabolic cluster that most clinicians and most papers converge on is fasting glucose, HbA1c (or a CGM-derived mean), triglycerides, HDL cholesterol, and blood pressure, with waist circumference often replacing one of them. Research panels are built for a specific question and are usually larger or entirely different: a five-gene blood expression set was developed for colorectal cancer detection, and it shares nothing with the metabolic five 1. Composite indices built from ordinary blood chemistry panels can predict chronological age well enough to be used as an aging readout, which tells you that the information content of a common CMP plus CBC is higher than the individual reference-range flags suggest 2.

So pick by question, not by list. A practical core for a self-tracker:

  • Metabolic: fasting glucose, insulin, HbA1c, triglycerides, HDL-C, ApoB, ALT, GGT
  • Inflammation and immune: hs-CRP, ferritin, WBC with differential, IL-6 if you have access
  • Hematology and oxygen transport: hemoglobin, hematocrit, RBC, MCV, reticulocytes, albumin
  • Renal and electrolytes: creatinine, cystatin C, eGFR, sodium, potassium
  • Endocrine: TSH, free T4, total and free testosterone or estradiol, SHBG, IGF-1
  • Nutrient status: 25-OH vitamin D, B12, folate, iron/TIBC/transferrin saturation

Cadence matters more than breadth. A biomarker measured once is a data point with unknown noise. The same biomarker measured quarterly under fixed conditions is a trajectory, and trajectories are where the signal is. In preclinical Alzheimer’s cohorts, within-person trajectories of plasma p-tau and related markers separate from stable participants years before any single value crosses a population threshold 3. The same logic applies to ferritin, ApoB, or creatinine in a healthy person: your own slope beats the population interval.

2. Build a schema that survives lab changes

The failure mode that kills most personal biomarker datasets is a wide spreadsheet with one column per test and one row per date. It breaks the first time a lab renames an analyte, changes units, or adds a test. Use long format.

CREATE TABLE measurement (
  id              INTEGER PRIMARY KEY,
  collected_at    TEXT NOT NULL,      -- ISO 8601 with offset
  analyte         TEXT NOT NULL,      -- internal canonical name
  loinc           TEXT,               -- e.g. 2093-3 for total cholesterol
  value           REAL NOT NULL,
  unit            TEXT NOT NULL,      -- UCUM, e.g. mg/dL, nmol/L, 10*9/L
  specimen        TEXT,               -- serum, plasma-EDTA, whole-blood
  lab             TEXT NOT NULL,      -- Quest, Labcorp, Boston Heart...
  method          TEXT,               -- immunoturbidimetric, LC-MS/MS...
  ref_low         REAL,
  ref_high        REAL,
  fasting_hours   REAL,
  note            TEXT,
  UNIQUE(collected_at, analyte, lab, method)
);
CREATE INDEX idx_analyte_time ON measurement(analyte, collected_at);

Store LOINC codes and UCUM units, not free text. LOINC distinguishes things you will otherwise merge by accident: 2093-3 (cholesterol, total, serum, mass/volume) is not 2089-1 (LDL-C, mass/volume) and not the calculated versus direct LDL variants. Record method because it changes the numbers. A creatinine by enzymatic assay and one by Jaffe differ systematically, and testosterone by immunoassay versus LC-MS/MS can differ by more than any change you care about detecting.

Extract from PDFs once, by hand or with a parser, then never touch the raw again:

import pandas as pd, sqlite3
df = pd.read_csv("labs_raw.csv", dtype={"loinc": "string"})
con = sqlite3.connect("biomarkers.db")
df.to_sql("measurement", con, if_exists="append", index=False)

3. Normalize units in one place

Write a unit conversion table, not inline arithmetic. Molar conversions are analyte-specific and getting one wrong silently corrupts a series.

CONV = {
    ("glucose", "mg/dL", "mmol/L"): 0.05551,
    ("cholesterol", "mg/dL", "mmol/L"): 0.02586,
    ("triglyceride", "mg/dL", "mmol/L"): 0.01129,
    ("creatinine", "mg/dL", "umol/L"): 88.4,
    ("testosterone", "ng/dL", "nmol/L"): 0.03467,
    ("25OH-vitD", "ng/mL", "nmol/L"): 2.496,
    ("ferritin", "ng/mL", "ug/L"): 1.0,
}

def to_canonical(row, targets):
    tgt = targets[row.analyte]
    if row.unit == tgt:
        return row.value
    k = (row.analyte, row.unit, tgt)
    if k not in CONV:
        raise KeyError(f"no conversion {k}")
    return row.value * CONV[k]

Keep the original value and unit in the database. Compute canonical values in a view or a derived column so a bad factor is one line to fix rather than a re-extraction.

4. Estimate your own noise before you interpret any change

This is the step almost every consumer dashboard skips, and it is the one that makes the rest useful. Every measurement carries analytical variation (CV_A, the assay) and within-person biological variation (CV_I, your own physiological fluctuation around your set point). A change smaller than the combined noise means nothing.

The reference change value is:

RCV = Z * sqrt(2) * sqrt(CV_A^2 + CV_I^2)

with Z = 1.96 for a two-sided 95% criterion (so the multiplier is 2.77) or Z = 1.65 for a one-sided 95% test of increase or decrease. Published within-subject CVs for immunological and inflammatory blood markers span a wide range, and the derived analytical quality goals differ by more than an order of magnitude between analytes 4. That is why a single generic “±10% is noise” rule is wrong: albumin moves a few percent, ferritin and CRP move tens of percent.

You can estimate your own CVs with two designs.

Analytical CV: split one draw into two tubes and run both, same lab, same day, ideally blinded. Do this for the handful of analytes you care most about. The difference between duplicates gives CV_A directly.

Within-person CV: take four or more draws over weeks under fixed preanalytical conditions (see step 5), then compute the residual CV after removing any linear trend.

import numpy as np
from scipy import stats

def cv_within(times_days, values):
    sl, ic, *_ = stats.theilslopes(values, times_days)
    resid = values - (ic + sl * np.asarray(times_days))
    return float(np.std(resid, ddof=1) / np.mean(values))

def rcv(cv_a, cv_i, z=1.96):
    return z * np.sqrt(2) * np.sqrt(cv_a**2 + cv_i**2)

# example: hs-CRP with CV_A ~ 5%, personal CV_I ~ 40%
rcv(0.05, 0.40)   # -> 1.12, i.e. a ~112% change is the 95% threshold

That hs-CRP number is the point. A CRP going from 0.8 to 1.4 mg/L is inside your own noise. A ferritin going from 40 to 75 µg/L probably is not. Store RCV per analyte in a table and use it as the alert threshold instead of the lab’s reference interval.

The index of individuality (CV_I / CV_G, your variation over population variation) tells you whether population reference ranges are informative for you at all. When it is below about 0.6, your personal set point is narrow relative to the population spread, and you can sit well inside the “normal” range while having moved a long way from your own baseline.

5. Fix the preanalytical variables, or your trend is a hydration trend

Concentration-based measurements are ratios, and the denominator moves. Plasma volume shifts with hydration, posture, heat, and recent endurance training, and plasma, red cell, and total blood volume can be estimated from ordinary blood test parameters, which is exactly why those parameters drift when volume drifts 5. A 10% plasma volume expansion dilutes hemoglobin, albumin, and total protein by roughly the same fraction with no change in total body content.

Protocol we use for every serial draw:

  • Same time of day, ideally 07:00–09:00 (cortisol, testosterone, and iron all have diurnal swings; morning testosterone can run 20–30% above late afternoon)
  • 12 hours fasted, water allowed, recorded in fasting_hours
  • No vigorous exercise for 48 hours (CK, AST, ALT, ferritin, CRP all respond)
  • Seated 10 minutes before venipuncture, no prolonged tourniquet (potassium and calcium rise with stasis)
  • Same lab, same assay, same specimen type
  • Log alcohol in prior 72h, sleep, illness, menstrual cycle day, and any supplement (biotin above ~5 mg/day interferes with many immunoassays, including TSH and troponin, in assay-specific directions)

Record all of it in the note field as structured key-value text so you can regress against it later. Comorbid conditions also shift whole panels systematically: in obstructive sleep apnea, inflammatory and metabolic blood markers track with disease severity, which means an untreated sleep problem shows up as a persistent offset across several of your series rather than a single flagged value 6.

Three layers, in order of increasing ambition.

Rolling median plus RCV flag, which is what you will use daily:

import pandas as pd

def flag(series: pd.Series, rcv_val: float) -> pd.DataFrame:
    base = series.expanding(min_periods=3).median().shift(1)
    delta = (series - base) / base
    return pd.DataFrame({"value": series, "baseline": base,
                         "pct_delta": delta,
                         "exceeds_rcv": delta.abs() > rcv_val})

Theil-Sen slope for direction, because it is insensitive to one bad draw:

sl, ic, lo, hi = stats.theilslopes(v, t_days, alpha=0.95)
per_year = sl * 365.25
significant = not (lo <= 0 <= hi)

Report slope per year with its confidence interval. ApoB rising 8 mg/dL/year with a CI excluding zero is a finding worth a clinician’s attention. ApoB with a CI spanning zero is four draws of noise.

Multi-analyte state, when you have twenty or more analytes across eight or more draws. Z-score each analyte against your own baseline mean and SD, then track the Mahalanobis distance of each visit from your personal centroid. This catches coordinated small moves that no single-analyte threshold would flag. Fit the covariance on your own history, not a population estimate, and expect it to be unstable until you have more draws than analytes in the subset you use.

7. Add CGM as a continuous layer

Export raw CGM as CSV (Dexcom Clarity gives 5-minute readings; Libre gives 15-minute). Compute the standard metrics yourself so they are comparable across devices and firmware changes:

g = pd.read_csv("cgm.csv", parse_dates=["timestamp"]).set_index("timestamp")
g = g["glucose_mgdl"].resample("5min").mean().interpolate(limit=3)

mean_g = g.mean()
cv     = g.std() / mean_g                      # target < 0.36
tir    = ((g >= 70) & (g <= 140)).mean()       # non-diabetic range
gmi    = 3.31 + 0.02392 * mean_g               # % HbA1c equivalent
daily  = g.resample("1D").mean()
modd   = g.groupby(g.index.time).diff().abs().mean()

Compare GMI against measured HbA1c. A persistent gap of more than ~0.4 percentage points usually means altered red cell turnover, a hemoglobin variant, or a sensor calibration bias, and it is worth checking your reticulocyte count and haptoglobin before trusting either number. Report CGM summaries over fixed 14-day windows so they align with your quarterly draws.

8. Add proteomics and transcriptomics without fooling yourself

Affinity proteomics (Olink) arrives as NPX, a log2-relative scale. NPX is not comparable across panel versions or across batches without bridging samples, so if you run a second timepoint, insist on bridge normalization and keep the bridging sample IDs. Do not convert NPX to concentrations. Track deltas in log2 space and require at least 0.3–0.5 NPX (roughly 25–40%) before calling a move, unless you have replicate data showing tighter assay CV for that specific protein.

Whole-blood RNA-seq is dominated by blood cell composition. Globin and erythroid transcripts (HBB, HBA, ALAS2, SLC4A1) are so specific to blood that forensic work uses them to identify a sample as blood 7, and they will consume a large share of your reads unless globin depletion was used. A shift in your neutrophil-to-lymphocyte ratio will move hundreds of genes, and the same caution applies to circulating microRNAs, where measured plasma levels track blood cell counts closely enough that apparent biomarker changes can be cell-composition artifacts 8. So:

  • Quantify with salmon quant -l A -i gencode_v44_index -1 R1.fq.gz -2 R2.fq.gz --validateMappings --gcBias -p 8 -o out/
  • Deconvolve cell fractions (CIBERSORTx LM22 or a similar reference) and store them as measurements in the same table
  • Include cell fractions as covariates in any within-person differential expression model, and filter globin genes before normalizing

For brain-derived or other tissue-specific markers now measurable in plasma, the same discipline applies. GFAP, for example, shows a consistent group-level elevation in Alzheimer’s disease across studies, with meaningful between-study heterogeneity in the absolute values reported 9. Assay platform and matrix matter, and neuroinflammatory marker panels are still being characterized for how they move within individuals over time 10. Track the number, hold the interpretation, and bring it to a neurologist rather than to a dashboard.

Common problems

Changing labs mid-series. The most common way to invent a trend. Treat lab or method change as a hard break: fit a step offset, or run one split-sample draw at both labs to measure the bias before you merge the series. If you cannot, keep them as separate series.

Using the reference interval as your threshold. Reference intervals are population 95% intervals. They tell you where most people sit, not where you sit. Use RCV against your own baseline, and use the interval only as a secondary check.

Too few points. Theil-Sen with three points is decoration. Aim for four draws minimum before you fit anything, six or more before you believe a slope.

Multiple comparisons. A 60-analyte panel will flag about three values outside a 95% interval by chance every single time. If you interpret every flag, you will chase noise indefinitely. Pre-specify the ten analytes you care about, correct the rest (Benjamini-Hochberg on your per-analyte RCV z-scores works fine), and require two consecutive draws in the same direction.

Biotin and other assay interferences. High-dose biotin supplements break streptavidin-biotin immunoassays. Stop them 72 hours before a draw and record it.

Seasonal structure read as trend. Vitamin D has a large annual cycle in most latitudes, and hemoglobin and lipids have smaller ones. With two years of data you can fit a sine term and remove it. With one year you cannot, so compare same-season to same-season.

Fasting glucose treated as a metabolic summary. A single fasting glucose is one sample of a distribution that a CGM shows you in full. Use both, and use the CGM CV and time-in-range as the metabolic variability readout.

Interpreting a flagged result alone. An RCV-exceeding move in ferritin, creatinine, a liver enzyme, a hematology parameter, or any tumor- or neuro-associated marker is a reason to see a physician, with your full series in hand. Bring the numbers, the units, the lab, the method, and the dates.

Oak builds longitudinal molecular profiles of individuals: whole-genome sequencing, RNA sequencing, proteomics, blood biomarkers, and continuous glucose data, integrated into one model of you. Build your profile.

Footnotes

  1. Mark Han, Choong Tsek Liew, Hong Wei Zhang, et al. Novel Blood-Based, Five-Gene Biomarker Set for the Detection of Colorectal Cancer. Clinical Cancer Research, 2008. https://doi.org/10.1158/1078-0432.ccr-07-1801 ↩

  2. Evgeny Putin, Polina Mamoshina, Alexander Aliper, et al. Deep biomarkers of human aging: Application of deep neural networks to biomarker development. Aging, 2016. https://doi.org/10.18632/aging.100968 ↩

  3. Yara Yakoub, Nicholas J. Ashton, Thomas K Karikari, et al. Longitudinal Blood Biomarker Trajectories in Preclinical Alzheimer’s Disease. Alzheimer’s & Dementia, 2022. https://doi.org/10.1002/alz.064763 ↩

  4. Najib Aziz, Roger Detels, Joshua J. Quint, et al. Biological variation of immunological blood biomarkers in healthy individuals and quality goals for biomarker tests. BMC Immunology, 2019. https://doi.org/10.1186/s12865-019-0313-0 ↩

  5. Louisa Margit Lobigs, Pierre‐Edouard Sottas, Pitre Collier Bourdon, et al. The use of biomarkers to describe plasma‐, red cell‐, and blood volume from a simple blood test. American Journal of Hematology, 2016. https://doi.org/10.1002/ajh.24577 ↩

  6. Ivan Guerra de Araújo Freitas, Pedro Felipe Carvalhedo de Bruin, Lia Bittencourt, et al. What can blood biomarkers tell us about cardiovascular risk in obstructive sleep apnea?. Sleep and Breathing, 2015. https://doi.org/10.1007/s11325-015-1143-9 ↩

  7. C. Haas, E. Hanson, A. Kratzer, et al. Selection of highly specific and sensitive mRNA biomarkers for the identification of blood. Forensic Science International: Genetics, 2011. https://doi.org/10.1016/j.fsigen.2010.09.006 ↩

  8. Colin C. Pritchard, Evan Kroh, Brent Wood, et al. Blood Cell Origin of Circulating MicroRNAs: A Cautionary Note for Cancer Biomarker Studies. Cancer Prevention Research, 2012. https://doi.org/10.1158/1940-6207.capr-11-0370 ↩

  9. Ka Young Kim, Ki Young Shin, Keun-A Chang. GFAP as a Potential Biomarker for Alzheimer’s Disease: A Systematic Review and Meta-Analysis. Cells, 2023. https://doi.org/10.3390/cells12091309 ↩

  10. Simone Lista, Bruno P. Imbimbo, Margherita Grasso, et al. Tracking neuroinflammatory biomarkers in Alzheimer’s disease: a strategy for individualized therapeutic approaches?. Journal of Neuroinflammation, 2024. https://doi.org/10.1186/s12974-024-03163-y ↩