AnpherosAnpheros PlatformDevelopers

How to normalize medical data

Tutorial: turn medical data from many sources into one comparable form — LOINC and ATC lookup, UCUM units, ISO dates, reference ranges, no duplicates on retry, and validation against the European lab profile.

Normalizing medical data means turning values that arrive in many shapes into one comparable form: the same code for the same test, the same unit system, dates in one format, every value with its reference range and its source, and no duplicates. This tutorial shows how to do that before data reaches an HL7 FHIR R4 record: looking up LOINC and ATC codes, using UCUM units, refusing ambiguous dates, keeping ranges, avoiding duplicates on retry, and checking the result against the European laboratory profile. Every response shown comes from the Anpheros API.

Why it matters

The same HbA1c result can reach you as "HbA1c 6,4 %", "Hemoglobina glicozilata: 6.4", "Glycated haemoglobin 46 mmol/mol" or a lab code only one laboratory understands. Until those are one code with one unit:

Normalization belongs at the point where data enters the record, once, not in every application that reads it later.

What a normalized value has

Part Normalized form Example
What was measured a code from a standard system LOINC 4548-4 (HbA1c), ATC A10BA02 (metformin), ICD-10 E11 (type 2 diabetes)
The value a number, or text only when the result is not numeric 6.4
The unit UCUM %, mg/dL, mm[Hg], kg
When an ISO 8601 date-time, with time zone when the time matters 2026-10-01T07:30:00Z
Normal range low and high, in the same unit 4.0–5.6
Source who produced it and from which system import from labx
Identity something that makes a repeat recognisable an idempotency key or a source identifier

Which code system goes with which resource type is listed in FHIR code systems.

Step 1: find the code

Search the platform's terminology by English or Romanian name, or by abbreviation:

curl "https://platform.anpheros.com/v1/terminology/loinc?q=hba1c" -H "Authorization: Bearer $KEY"
curl "https://platform.anpheros.com/v1/terminology/loinc?q=hemoglobina%20glicozilata" -H "Authorization: Bearer $KEY"

Both return the same concept:

{"data": [{"system": "http://loinc.org", "code": "4548-4", "display": "Hemoglobin A1c/Hemoglobin.total in Blood",
           "display_ro": "Hemoglobină glicozilată (HbA1c)", "category": "laboratory", "unit": "%"}], "total": 1}

Medications work the same way with ATC:

curl "https://platform.anpheros.com/v1/terminology/atc?q=metformin" -H "Authorization: Bearer $KEY"
{"data": [{"system": "http://www.whocc.no/atc", "code": "A10BA02", "display": "Metformin", "display_ro": "Metformin", "category": "medication", "unit": null}], "total": 1}

The unit field is the unit LOINC expects for that test. Use it to check what you received. Build the mapping from each source's own names to codes once, per source, and keep it in your integration. Do not guess at write time: when a name is ambiguous, such as "glucose" without saying whether it was blood or serum, leave it unmapped and ask the source. How to import laboratory results into a FHIR record shows how unmapped lab markers are kept and reported.

Step 2: write with codes, units and ranges

curl -X POST "https://platform.anpheros.com/v1/patients/$PID/observations" -H "Authorization: Bearer $KEY" \
     -H "Content-Type: application/json" -H "Idempotency-Key: lab-77-hba1c" -d '{
  "code": "4548-4", "category": "laboratory",
  "value": 6.4, "unit": "%", "effective_at": "2026-10-01T07:30:00Z",
  "reference_range": {"low": 4.0, "high": 5.6},
  "author_type": "import"
}'
{"id": "3296315c-…", "code": "4548-4", "system": "http://loinc.org", "display": "Hemoglobin A1c/Hemoglobin.total in Blood",
 "category": "laboratory", "effective_at": "2026-10-01T07:30:00Z", "value": 6.4, "unit": "%",
 "reference_range": {"low": 4.0, "high": 5.6}, "status": "final", "fhir": "Observation/3296315c-…"}

The REST API fills in the standard LOINC name when you send only the code, so every application reads the same display. On the FHIR API (/fhir/R4) the resource is stored exactly as you send it: include display yourself there.

Keep the original wording too. For a lab report, code.text on the FHIR Observation holds the laboratory's name ("Hemoglobina glicozilata (HbA1c)") next to the LOINC coding. The patient still recognises their paper report, and nothing is lost if the mapping is ever questioned.

Step 3: units

Use UCUM codes: mg/dL, mmol/L, %, mm[Hg], kg, cm, /min, Cel. Two rules prevent most errors:

Anpheros does not convert units for you; it stores the unit you send.

Step 4: dates

Convert dates to ISO 8601 before writing, using the source's known format. 01.10.2026 is 1 October in Romania and many European countries, but 10/01/2026 is 1 October only in the United States. The API refuses anything that is not ISO, rather than guessing:

{"error": {"type": "invalid", "message": "Input should be a valid datetime or date, invalid character in year (at effective_at)", "field": "effective_at"}}

The lab connector is the exception: it accepts DD.MM.YYYY and DD/MM/YYYY because it knows it is reading a European report.

For measurements where the time of day matters (blood pressure, glucose, medication doses), send the time with its time zone. A reading at 23:30 in Bucharest is on the previous day in UTC, and the patient expects it on the day they took it.

Step 5: no duplicates

Duplicates come from retries, from the same report sent twice and from the same value arriving through two routes.

Step 6: check against a profile

For laboratory results, ask the platform how a resource measures up to the HL7 Europe laboratory report profile before you rely on it:

curl -X POST "https://platform.anpheros.com/fhir/R4/Observation/\$validate?profile=eu-lab" -H "Authorization: Bearer $KEY" \
     -H "Content-Type: application/fhir+json" -d '{
  "resourceType": "Observation", "status": "final",
  "category": [{"coding": [{"system": "http://terminology.hl7.org/CodeSystem/observation-category", "code": "laboratory"}]}],
  "code": {"text": "HbA1c"}, "subject": {"reference": "Patient/'$PID'"},
  "valueQuantity": {"value": 6.4, "unit": "%"}
}'

This un-normalized result gets a precise list:

{"resourceType": "OperationOutcome", "issue": [
  {"severity": "error", "code": "invalid", "diagnostics": "Observation.code must carry a LOINC coding.", "expression": ["Observation.code"]},
  {"severity": "error", "code": "required", "diagnostics": "Observation.effective[x] is required.", "expression": ["Observation.effective"]},
  {"severity": "warning", "code": "business-rule", "diagnostics": "valueQuantity should carry a UCUM system and code.", "expression": ["Observation.valueQuantity"]},
  {"severity": "warning", "code": "informational", "diagnostics": "No referenceRange: interpretation will need the laboratory's ranges.", "expression": ["Observation.referenceRange"]},
  {"severity": "information", "code": "informational", "diagnostics": "No performer (laboratory) recorded.", "expression": ["Observation.performer"]},
  {"severity": "information", "code": "informational", "diagnostics": "No issued time (when the laboratory released the result).", "expression": ["Observation.issued"]}]}

Each issue maps to one of the steps above. Run $validate in your integration tests, with real samples from each source.

Step 7: keep the source

Normalization changes how a value is written, never where it came from. Send author_type (device, import, practitioner, patient) and X-Anpheros-Source-System with every write. That is what lets a doctor, an auditor or an AI model tell a laboratory result from a value typed at home. See Medical data provenance explained.

Checklist

Frequently asked questions

What is medical data normalization?

Converting health data from different sources into one standard form: shared codes (LOINC for tests and measurements, ICD-10 for diagnoses, ATC for medications), UCUM units, ISO dates, reference ranges and a recorded source, so that values from different systems can be compared, combined and exchanged.

Should I convert all values to one unit?

Store the unit the source measured in, as long as it is a valid UCUM unit that matches the code, and convert when displaying or analysing. That keeps every stored value traceable to its original report.

Can normalization be fully automatic?

Mostly. Codes in the source, known names and well-formed units can be mapped automatically. Ambiguous names and unknown local codes need a human decision once per source, after which the mapping is automatic.

Related