Skip to content

How to Analyze Your Own Blood Test Results Online

Oak
A laboratory instrument aligning hundreds of backlit blood vials into one long row while a blue scanning light passes over them.

By the end of this guide you will have a single tidy table of every blood test you have ever had, one row per analyte per draw, with LOINC codes, UCUM units, the reporting lab, and the reference interval that lab used. On top of that table you will have a small set of functions that compute derived quantities yourself rather than trusting whatever the lab printed, and a statistic that tells you whether the change between two draws is larger than the assay and your own day-to-day biology can explain. You need a patient portal login, Python 3.11 or newer with pandas and pyarrow, jq, and, for the older results that exist only as scanned paper, ocrmypdf and pdfplumber. Budget an afternoon for the first extraction and about ten minutes per new draw after that.

A note on scope before the commands. Everything here is measurement and interpretation of measurement. Reference intervals are population statistics, not verdicts, and a value outside one is a prompt to ask a question, not an answer. Nothing you compute from a spreadsheet diagnoses anything, and any result that concerns you belongs in front of a clinician who can see the rest of your chart.

1. Get the data out of the portal in machine-readable form

Most large US health systems expose a FHIR R4 API to patients, because the ONC information-blocking rules require it. Epic’s MyChart, Cerner, LabCorp, and Quest all have some version of this. The fastest path is usually the Apple Health or Google Health Connect integration, which does the OAuth dance for you and gives you an XML or JSON export you can parse offline. The better path, if you want a repeatable pipeline, is to register your own SMART on FHIR app against your health system’s developer sandbox and pull Observation resources directly.

Once you have a bearer token, the query you want is the laboratory category, paged:

curl -sH "Authorization: Bearer $TOKEN" \
  -H "Accept: application/fhir+json" \
  "$BASE/Observation?patient=$PID&category=laboratory&_count=200&_sort=-date" \
  -o obs-001.json
# then follow the "next" link until it disappears
jq -r '.link[] | select(.relation=="next") | .url' obs-001.json

Flatten the bundle into TSV with jq. The important detail is that you take the LOINC coding specifically, not the first coding in the array, because many systems put a proprietary internal code first:

jq -r '
  .entry[].resource
  | select(.resourceType=="Observation")
  | select(.valueQuantity != null)
  | [ (.effectiveDateTime // .issued),
      ([.code.coding[] | select(.system=="http://loinc.org") | .code] | first // "NA"),
      .code.text,
      .valueQuantity.value,
      (.valueQuantity.code // .valueQuantity.unit),
      (.referenceRange[0].low.value // ""),
      (.referenceRange[0].high.value // ""),
      (.performer[0].display // "")
    ] | @tsv
' obs-*.json > labs_raw.tsv

Two things will be missing. Panels sometimes arrive as an Observation with hasMember references rather than a valueQuantity, so you need a second pass that resolves those references. And results older than the portal’s retention window, typically five to ten years, will not be there at all. Those you request from the lab directly as a records release, and they arrive as PDF.

2. Turn the PDFs into rows without trusting the parser

Scanned lab reports are the worst part of this job. Run ocrmypdf --deskew --rotate-pages --optimize 1 in.pdf out.pdf first, then try pdfplumber table extraction. For reports with clean ruled tables this works. For the rest, a language model doing structured extraction is genuinely the better tool, provided you constrain it to a schema and validate everything it returns.

The pattern we use: send one page at a time, demand JSON matching a Pydantic model with fields analyte_text, value, unit, ref_low, ref_high, flag, and then reject any row where the value does not appear verbatim in the page text. That last check catches the failure mode that matters, which is a model silently transposing digits or inventing a row that looks plausible for a chemistry panel. Also require the model to emit null rather than guess when a field is cut off at a page break. After extraction, map analyte_text to LOINC through a lookup table you build once and keep under version control, and route anything unmapped to a manual review file. Do not let the model assign LOINC codes freely: the 2345-7 (glucose, serum) versus 2339-0 (glucose, blood) distinction is exactly the kind of thing it gets wrong often enough to poison a five-year trend.

3. Normalize units and settle on one schema

Your table should be long, not wide, with one row per measurement:

date | loinc | analyte | value | unit_ucum | value_si | ref_low | ref_high | lab | method | fasting

Unit conversion is mechanical but unforgiving. Glucose in mg/dL times 0.05551 gives mmol/L. Total cholesterol, HDL, and LDL in mg/dL times 0.02586 give mmol/L, but triglycerides use 0.01129 because the molar mass differs. Creatinine in mg/dL times 88.4 gives µmol/L. Store both the original and the SI value, because your reference intervals came in the original units and you will want to check the conversion later.

The trap is analytes where the same LOINC has two conventional units in the wild, and worse, analytes measured in activity units or mass units depending on assay. Vitamin D, ferritin, and most hormones fall here. Write conversions as an explicit dictionary keyed on (loinc, from_unit, to_unit) and raise on any pair you have not entered. Silent pass-through is how you end up with a testosterone trend that drops by a factor of 28.8 in 2019 because the lab switched from ng/dL to nmol/L.

Record the reporting lab and, where the report states it, the assay platform. This matters more than people expect. The literature on blood-based biomarkers has spent a decade documenting how much of the apparent between-study variance is pre-analytical and platform-dependent rather than biological, to the point that standardization programs and certified reference materials were required before results could be compared across sites at all.12 The same logic applies to your own file: a LabCorp ferritin and a Quest ferritin are not interchangeable data points on one line chart.

4. Compute the derived values yourself

Several numbers on your report are not measurements. They are calculations the lab performed from measurements, and the formulas change between labs and over time. Recompute them so your history is internally consistent.

Estimated glomerular filtration rate is the clearest case. The 2021 CKD-EPI equation dropped the race coefficient that the 2009 version used, so a creatinine trend spanning that transition will show an artificial step in eGFR while the underlying creatinine did nothing:

def ckd_epi_2021(scr_mgdl: float, age: float, female: bool) -> float:
    k = 0.7 if female else 0.9
    a = -0.241 if female else -0.302
    egfr = 142 * min(scr_mgdl / k, 1) ** a \
               * max(scr_mgdl / k, 1) ** -1.200 \
               * 0.9938 ** age
    return egfr * 1.012 if female else egfr

LDL cholesterol is the second case. Most labs still report Friedewald, which subtracts triglycerides divided by five as an estimate of VLDL cholesterol in mg/dL. That fixed divisor breaks down when triglycerides exceed roughly 400 mg/dL and systematically underestimates LDL at low LDL and high triglycerides. If you have a full lipid panel with total cholesterol, HDL, and triglycerides, recompute with the Martin-Hopkins adjustable-factor method or the Sampson equation and keep all three columns so you can see how much of a change is method rather than physiology. Non-HDL cholesterol, which is simply total minus HDL, requires no estimation at all and is worth carrying alongside.

5. Decide whether a change is real

This is the step that separates a useful personal lab file from a source of anxiety. Two values differ. The question is whether they differ by more than the measurement process and your own biological rhythm would produce if nothing had changed.

The standard tool is the reference change value. Every analyte has an analytical coefficient of variation (CVa, the imprecision of the assay, which your lab will provide on request or publish in its method sheet) and a within-subject biological coefficient of variation (CVi, how much your own true value fluctuates around your set point). The European Federation of Clinical Chemistry and Laboratory Medicine maintains a free Biological Variation Database with critically appraised CVi estimates for hundreds of analytes. Then:

import math

def rcv(cva_pct: float, cvi_pct: float, z: float = 1.96) -> float:
    """Percent change that exceeds noise at the given confidence."""
    return math.sqrt(2) * z * math.sqrt(cva_pct**2 + cvi_pct**2)

Run this once per analyte and store the result next to your reference interval. The output is often surprising. For analytes with tight homeostatic control, such as sodium, albumin, or HbA1c, a change of a few percent is already meaningful. For analytes with large within-person swings, such as ALT, ferritin, or TSH, changes of thirty or forty percent can occur with nothing happening at all. If you build only one derived column, build this one, because it converts “my ferritin went from 90 to 130, should I worry” into a question with a defensible answer.

Two refinements. First, RCV assumes you are steady-state, so it is invalid across an acute illness, a change in training load, or a fast of different duration. Second, it is a two-sample statistic. With five or more points you are better served by fitting a simple model, for example an ordinary least squares slope on time with the residual standard deviation compared against your expected CVa and CVi, or a rolling median to suppress single-draw outliers. Do not fit anything fancier than a line to six points.

6. Control the pre-analytical variables you can

Your data quality ceiling is set before the tube reaches the analyzer. The biomarker methodology literature is consistent that pre-analytical handling, specimen type, and collection timing account for a large share of variance in blood-based measurements, and that these factors have to be controlled before an assay’s analytical performance is even worth discussing.3 Concretely: draw at the same time of day, because cortisol, testosterone, and iron all follow diurnal patterns with swings large enough to dominate any trend you care about. Standardize fasting duration and record it as a column. Avoid heavy exercise in the forty-eight hours before a draw if you are tracking creatine kinase, ALT, AST, or creatinine. Sit quietly for several minutes before the needle, since posture shifts plasma volume by several percent and every concentration-based analyte moves with it. Use the same lab and, where possible, the same collection site.

Hemolysis is the silent corrupter. A partly hemolyzed sample raises potassium, LDH, and AST substantially. Good labs report a hemolysis index or add a comment. If your result set has an unexplained potassium spike on one draw, check for that flag before you build a story around it.

7. Use a language model as a reader, not an oracle

Models are genuinely good at three things here: mapping free-text analyte names to LOINC candidates for your review, writing the explanation of what an analyte measures and what physiological processes move it, and generating hypotheses about which of your columns move together. They are unreliable at arithmetic on many rows, at remembering which reference interval belongs to which lab, and at resisting the pull toward a tidy narrative.

The workflow that works: compute everything numeric in code, then hand the model a compact table that already contains value, unit, reference interval, RCV-flagged change, and lab. Ask it to explain and to flag what it cannot explain, and ask explicitly for the alternative explanations, including assay change and pre-analytical artifact. Never let it do the unit conversion or the trend detection. When you see a claim you cannot trace to a row in your table, treat it as fabricated until proven otherwise.

Be especially skeptical of composite scores. Models trained on routine blood panels can predict chronological age with meaningful accuracy, which is a real finding about the information content of a standard chemistry and hematology panel.4 It does not follow that any particular “biological age” number you are shown is calibrated for you, validated prospectively, or actionable. The same caution applies to the multi-analyte cancer detection scores now marketed directly to consumers: the field has been clear for years that analytical sensitivity in a discovery cohort and useful positive predictive value in asymptomatic screening are very different problems, largely because of disease prevalence.5

8. Know where the frontier is, and where the clinician is

It is worth understanding why some blood biomarkers are moving into routine use while others stay in research. Plasma phosphorylated-tau assays for Alzheimer’s disease are the clearest current example of a blood-based marker that has crossed into practical primary care use, with prospective evidence that a blood panel combined with a brief digital cognitive test performs well in the primary care setting where most cognitive complaints first appear.6 That transition took standardized assays, defined cut-points validated across cohorts, and explicit guidance on the intended-use population, none of which happens by default.7

The practical consequence for you is a rule of thumb. When a marker has a standardized assay, a published reference interval tied to that assay, and a defined intended use, your longitudinal file is directly interpretable. When it does not, your file is still a useful record and a useful input for a clinician, and you should hold your conclusions loosely. Anything that would change what you do, anything persistently outside a reference interval, and anything with a flagged change beyond its RCV belongs in a conversation with a physician who can order confirmatory testing. The value of doing this work yourself is that you arrive at that conversation with five years of clean, unit-normalized data instead of a shoebox of PDFs.

Common problems

Duplicate rows after a portal refresh. The same Observation can appear under two IDs after a health system migration. Deduplicate on the tuple of date, LOINC, value, and unit, not on resource ID.

Mid-series assay change with no announcement. A step change of similar magnitude across several analytes on the same date, all from the same lab, is a platform change, not a physiological event. Check the method field on the report. If the lab does not print it, call and ask for the method sheet for that date.

Values reported as < or > a limit. These are censored, not missing, and not zero. Store the operator in a separate column and exclude censored points from slope fits rather than substituting the detection limit.

Reference intervals that differ between labs for the same analyte. This is expected, because intervals are derived from each lab’s own reference population and assay. Always carry the interval that came with the result rather than applying one interval retroactively to your whole history.

Timezone and date drift. effectiveDateTime is collection time, issued is result time, and they can differ by days for send-out tests. Use collection time for trends, always, and normalize to a single timezone before sorting.

Over-fitting to a single draw. One value moving inside its reference interval, or moving less than its reference change value, carries essentially no information. Resist the temptation to explain it. The discipline of writing down what you expected before the draw, then comparing, is the cheapest accuracy improvement available.

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. Harald Hampel, Sid E. O’Bryant, José L. Molinuevo, et al. Blood-based biomarkers for Alzheimer disease: mapping the road to the clinic. Nature Reviews Neurology, 2018. https://doi.org/10.1038/s41582-018-0079-7 ↩

  2. Sid E. O’Bryant, Michelle M. Mielke, Robert A. Rissman, et al. Blood‐based biomarkers in Alzheimer disease: Current state of the science and a novel collaborative paradigm for advancing from discovery to clinic. Alzheimer’s & Dementia, 2016. https://doi.org/10.1016/j.jalz.2016.09.014 ↩

  3. Samantha A. Byrnes, Bernhard H. Weigl. Selecting analytical biomarkers for diagnostic applications: a first principles approach. Expert Review of Molecular Diagnostics, 2017. https://doi.org/10.1080/14737159.2018.1412258 ↩

  4. 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 ↩

  5. Samir M. Hanash, Christina S. Baik, Olli Kallioniemi. Emerging molecular biomarkers—blood-based strategies to detect and monitor cancer. Nature Reviews Clinical Oncology, 2011. https://doi.org/10.1038/nrclinonc.2010.220 ↩

  6. Pontus Tideman, Linda Karlsson, Olof Strandberg, et al. Primary care detection of Alzheimer’s disease using a self-administered digital cognitive test and blood biomarkers. Nature Medicine, 2025. https://doi.org/10.1038/s41591-025-03965-4 ↩

  7. Jun Wang, Ming Chen, Colin L. Masters, et al. Translating blood biomarkers into clinical practice for Alzheimer’s disease: Challenges and perspectives. Alzheimer’s & Dementia, 2023. https://doi.org/10.1002/alz.13116 ↩