# 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.

Source: https://developers.anpheros.com/guides/normalize-medical-data

**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:

- a chart of the patient's HbA1c over time shows three unrelated lines;
- an alert on "HbA1c above 6.5 %" misses two of the values;
- an AI model sees four facts instead of one trend;
- another system receiving the record cannot interpret it.

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](https://developers.anpheros.com/guides/fhir-code-systems).

## Step 1: find the code

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

```bash
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:

```json
{"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:

```bash
curl "https://platform.anpheros.com/v1/terminology/atc?q=metformin" -H "Authorization: Bearer $KEY"
```

```json
{"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](https://developers.anpheros.com/guides/import-lab-results-fhir) shows how unmapped lab markers are kept and reported.

## Step 2: write with codes, units and ranges

```bash
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"
}'
```

```json
{"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:

- **Do not convert silently.** If a laboratory reports glucose in `mmol/L` and your app displays `mg/dL`, store what the laboratory measured and convert when you display it. The stored value stays traceable to the report.
- **Check the unit against the code.** LOINC codes are specific: HbA1c in `%` is `4548-4`, while HbA1c in `mmol/mol` is a different LOINC code. A value whose unit does not match the code's expected unit is a mapping error, not a conversion problem.

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:

```json
{"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.

- **Retries:** send an `Idempotency-Key` that identifies the value in the source, for example the report id and the test. Sending the same request again with the same key returns the first response and stores nothing new. The SDKs add one to every create.
- **The same report twice:** the lab connector builds a stable identifier per row from the report, code, date and value, and skips rows already imported.
- **Two routes:** store the source system's own id with `X-Anpheros-Origin-Id` (or in `identifier` on FHIR resources), and search for it before writing values that can come back through another path.

## 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:

```bash
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:

```json
{"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](https://developers.anpheros.com/guides/medical-data-provenance).

## Checklist

- [ ] Each source has a mapping from its names to LOINC / ATC / ICD-10, kept in your integration
- [ ] Ambiguous names stay unmapped and are reported, never guessed
- [ ] UCUM units, matching the code; no silent conversion
- [ ] ISO 8601 dates, with time zones where the time matters
- [ ] Reference ranges kept with lab values
- [ ] Original wording kept (`code.text`)
- [ ] Idempotency keys or source ids on every write
- [ ] `author_type` and source system on every write
- [ ] `$validate` in tests for lab results

## 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

- [FHIR code systems](https://developers.anpheros.com/guides/fhir-code-systems)
- [How to import laboratory results into a FHIR record](https://developers.anpheros.com/guides/import-lab-results-fhir)
- [Medical data provenance explained](https://developers.anpheros.com/guides/medical-data-provenance)
- [Healthcare data interoperability](https://developers.anpheros.com/guides/interoperability)
- [Medical data API](https://developers.anpheros.com/guides/medical-data-api)
