LOINC Laboratory and Observation Codes
LOINC (Logical Observation Identifiers Names and Codes) is a universal, freely licensed terminology of numeric codes that uniquely identify clinical laboratory tests, vital signs, clinical observations, survey instruments, and document types, enabling health systems and research networks to exchange and aggregate results without mapping through local lab-code silos.
In plain language
Every laboratory test, vital sign, and clinical measurement can arrive from different hospitals using completely different internal naming systems, making it impossible to combine data across sites without a shared lookup key. LOINC (Logical Observation Identifiers Names and Codes) solves this by assigning a universal numeric code — for example, 2160-0 for serum creatinine — to each unique combination of analyte, specimen type, and measurement method, so that a result from one hospital can be matched instantly to the same test at another hospital. For a researcher building a study that needs lab values (kidney function, blood sugar, hemoglobin), the most common trap is assuming that one LOINC code covers all versions of a test — it does not, and mixing results coded in different units (like milligrams per deciliter versus micromoles per liter) can produce numbers that are off by a factor of 88, silently corrupting every threshold-based calculation.
LOINC (Logical Observation Identifiers Names and Codes)
is a universal terminology maintained by the Regenstrief Institute that assigns a stable numeric code to every distinct clinical observation — laboratory tests, vital signs, clinical findings, survey-instrument items, and document types. First published in 1994 and updated approximately twice a year, it now contains more than 100,000 terms and is used in over 170 countries. LOINC is freely downloadable after registration and acceptance of the Regenstrief license; users may incorporate codes in systems and research but may not bulk-reproduce the database tables without permission (see https://loinc.org/kb/license/).
The six-part fully specified name: why it matters for RWE
The semantics of a LOINC code are entirely encoded in its fully specified name, a structured six-part axis. Understanding these six axes is the single most important conceptual task for a researcher building lab-based phenotypes:
1. Component (analyte) — what is being measured: Glucose, Creatinine, Hemoglobin A1c, Troponin I, etc. 2. Property — the physical or chemical type of quantity: mass concentration (MCnc, e.g., mg/dL), molar concentration (SCnc, e.g., mmol/L), arbitrary units, presence/absence (Prid). 3. Time — the collection timing: point-in-time (Pt) versus 24-hour collection (24H) versus other periods. 4. System (specimen type) — the biological source: serum or plasma (Ser/Plas), whole blood (Bld), urine (Urine), cerebrospinal fluid (CSF), etc. 5. Scale — the measurement type: quantitative (Qn), ordinal (Ord), nominal (Nom), narrative (Nar), or document. 6. Method (when it matters) — the analytical technique when it materially distinguishes the result: Jaffe vs. enzymatic for creatinine; immunoassay vs. HPLC for HbA1c. Method is omitted from the code name when it does not change the interpretive meaning.
Why "glucose" is not one code
Fasting versus random glucose, serum versus whole-blood glucose, mass-concentration versus molar-concentration property — each combination is a different LOINC code with different reference ranges and clinical interpretation. A researcher who writes "glucose" as a free-text filter and expects to capture all glucose results will mix these conceptually different measurements. The discipline of building an explicit LOINC code list — naming every axis for every code included — is what makes a lab-based phenotype reviewable, reproducible, and defensible.
Code format
LOINC codes are numeric identifiers with a mod-10 Luhn check digit appended after a hyphen, for example 2160-0 (serum/plasma creatinine, SCnc, Pt, Ser/Plas, Qn) or 4548-4 (HbA1c/total hemoglobin, MFr, Pt, Bld, Qn). The digits are meaningless identifiers — all meaning lives in the fully specified name. The check digit detects single-character transcription errors.
Units are not part of the code; UCUM is the companion standard
LOINC codes specify the property dimension (mass vs molar concentration) but do not encode the specific unit. The Unified Code for Units of Measure (UCUM) is the companion standard for units, and results must carry UCUM-coded units alongside the LOINC code. In multi-site EHR or network studies, the same LOINC code will often arrive in different unit flavors (mg/dL vs µmol/L for creatinine; mmol/L vs mg/dL for glucose) that require explicit unit harmonization before any pooling or threshold-based phenotyping. Forgetting this step is among the most common hidden arithmetic errors in distributed network studies.
The local-code problem — the dominant data-quality issue for lab-based RWE
Most clinical laboratory information systems (LIS) record results using internal, site-specific local codes rather than LOINC codes. LOINC mapping is applied (or not) by the health system's informatics team before data reaches a research data warehouse or common data model. Mapping completeness varies enormously across sites — from near-complete to below 50% for less common analytes — and mapping accuracy is a separate concern: a local code for "creatinine, enzymatic" may be incorrectly mapped to the Jaffe LOINC code, introducing systematic method misclassification invisible to downstream analysts. In multi-site PCORnet or OHDSI/OMOP networks, differential unmapping across sites means that a cohort built on LOINC-mapped records is effectively selecting for better-resourced sites, biasing lab-dependent eGFR or HbA1c cohorts toward the site demographics that happen to have completed their mapping. Documenting the LOINC mapping rate per site and per analyte is a required feasibility step for any multi-site lab-dependent study.
Method heterogeneity and reference-range awareness
Even when LOINC codes are correctly mapped, the same analyte LOINC may aggregate results from multiple analytical methods with different inter-method biases. Serum creatinine by Jaffe (2160-0) and by enzymatic assay (38483-4) produce systematically different numeric values, which affects eGFR calculations. An eGFR-based CKD cohort that pools Jaffe and enzymatic creatinine without adjustment will introduce differential misclassification by the method distribution across sites. Lab-based phenotypes must either restrict to a single method code, apply method-specific conversion, or sensitivity-analyze the method mix.
Scope beyond the clinical laboratory
Although LOINC originated in laboratory medicine, the terminology has expanded substantially: - Vital signs: heart rate (8867-4), systolic blood pressure (8480-6), BMI (39156-5) — all have LOINC codes used in EHR observation tables. - Clinical findings and assessments: clinical observations recorded by providers are increasingly assigned LOINC codes in EHR systems. - Survey and patient-reported outcome instruments: PHQ-9 depression screen items each have individual LOINC codes (e.g., 44250-9 for item 1), enabling structured extraction of PRO data from EHRs. - Document types: clinical note categories (discharge summary, pathology report, operative note) have LOINC document-type codes used in NLP pipelines and document management systems. - Radiology: imaging order and result types also have LOINC representation used in radiology information systems.
LOINC in the OMOP CDM
In the OHDSI OMOP Common Data Model, LOINC is the standard vocabulary for the Measurement domain (lab results, vital signs, clinical observations) and for the Observation domain (survey items, clinical assessments). When a site transforms data to OMOP, local lab codes are mapped to LOINC via the CONCEPT and CONCEPT_RELATIONSHIP tables; results land in the MEASUREMENT table with `measurement_concept_id` drawn from LOINC. A site with incomplete LOINC mapping will have observations in MEASUREMENT with concept_id = 0 (unmapped) — a leading indicator of data quality that should be audited before any analytic use.
LOINC and SNOMED CT collaboration
Regenstrief and SNOMED International maintain a formal harmonization effort; SNOMED CT provides the ontological backbone for representing the component and system axes, while LOINC provides the operational identity for test ordering and result reporting. In OMOP, conditions and clinical findings use SNOMED; measurements and labs use LOINC.
CPT versus LOINC: claims visibility vs EHR result capture
CPT (Current Procedural Terminology) codes identify the billed procedure — the laboratory service ordered and reimbursed. LOINC codes identify the resulted observation — the specific analyte and measurement reported. In a Medicare FFS claims dataset, a CPT 80053 (comprehensive metabolic panel) appears as a claim but does not tell you the individual constituent results (glucose, creatinine, ALT, etc.) or their numeric values; those live only in EHR or lab feed data coded with LOINC. This claims-vs-EHR split is a major practical constraint: whether a patient had an HbA1c above 8% in a given quarter is knowable from EHR/LOINC-coded data, invisible in claims. A study that needs lab values for outcome ascertainment or covariate definition requires EHR access; CPT codes from claims can only confirm whether a test was ordered, not its result.
Pros, cons, and trade-offs
- vs local codes only (no LOINC mapping): Local-only systems cannot exchange, aggregate, or benchmark lab results across institutions. LOINC costs significant informatics effort to map and maintain, but without it multi-site studies and common data model participation are infeasible. Prefer LOINC: a site without LOINC mapping is, for practical purposes, excluded from most large-scale EHR network research. - vs free-text analyte name matching: Matching on "creatinine" as a string in the lab test name field is fast but conflates Jaffe and enzymatic methods, serum and urine, and point-in-time and timed collections. LOINC matching is more work up front (explicit code list) but produces a reproducible, method-specific, specimen-specific cohort definition. Prefer LOINC for any phenotype where method or specimen matters. - vs ICD codes for lab findings: ICD diagnosis codes can capture that a patient has chronic kidney disease (N18.) but cannot represent the measured creatinine value, the eGFR trajectory, or the staging date. LOINC captures the quantitative measurement; ICD captures the clinical conclusion. Use both: ICD for broad cohort screening, LOINC for value-level phenotype precision (confirmed eGFR < 60 on two occasions ≥ 90 days apart). - vs SNOMED CT for observations: SNOMED CT models the clinical concept with rich ontological relationships; LOINC models the test identity* for ordering and reporting. OMOP uses LOINC for Measurement and SNOMED for Observation and Condition. Use both in appropriate domains; do not substitute one for the other in the OMOP context.
When to use
- Building any lab-based phenotype (eGFR for CKD staging, HbA1c for glycemic control, troponin for ACS adjudication, viral load for HIV suppression, CBC for cytopenias). - Harmonizing lab results across multiple EHR systems or data partners in a distributed research network (PCORnet, OHDSI, Sentinel). - Extracting vital-sign trajectories (blood pressure, BMI, heart rate) from OMOP Measurement tables. - Linking structured PRO instrument responses (PHQ-9, GAD-7) from EHR data into an outcomes analysis. - Auditing OMOP data quality by inspecting unmapped (concept_id = 0) measurement records. - Specifying the exact observation set for a regulatory-submission RWE study where the FDA or payer reviewer must be able to replicate the code list without ambiguity.
When NOT to use — and when it is actively misleading or dangerous
- When you treat a single LOINC code as covering all variants of an analyte. Selecting only 2160-0 for creatinine and missing 38483-4 (enzymatic) and site-specific Jaffe variants will undercount the lab record population and introduce method-based selection — any creatinine result captured by a Jaffe method at a site that mapped to a different LOINC will be lost. Build a code list, not a single code. - When LOINC mapping completeness has not been audited. If 30% of a site's creatinine results are in unmapped records (concept_id = 0), a LOINC-filtered query is effectively discarding 30% of the data. Using such a query to compute eGFR-based staging will produce systematically wrong stage distributions. Audit mapping rate before any lab-based analysis. - When you apply a threshold in mg/dL to a mix of mg/dL and µmol/L results. This is one of the most dangerous arithmetic errors in multi-site lab studies: a creatinine of 88.4 µmol/L is 1.00 mg/dL, not 88.4 mg/dL. A threshold that treats all results as mg/dL will classify nearly all µmol/L results (which arrive in the 50–300 range) as severe renal impairment. Unit harmonization must precede any numeric threshold application. - When using LOINC codes from claims data alone. Standard CPT-coded claims do not carry LOINC codes or result values. LOINC is a feature of EHR and LIS data, not of billing claims. Assuming a lab result is captured because a CPT billing code appears in claims is an ascertainment error. - For drug exposure ascertainment. Medication orders may appear in the EHR as observations, but exposure ascertainment from LOINC-coded observations is unreliable — use RxNorm-coded orders/administrations and pharmacy dispense records, not LOINC observation records, for drug exposure definition.
Data-source operational depth
- EHR / OMOP Measurement table: LOINC codes appear as `measurement_concept_id`; unmapped local codes appear with concept_id = 0 and the local code in `measurement_source_value`. The numeric result is in `value_as_number` with `unit_concept_id` from UCUM. Always filter on unit concept as well as LOINC concept to avoid unit-mixing errors. Include records with `measurement_concept_id = 0` in an audit query to quantify the unmapped fraction before excluding them. - PCORnet LAB_RESULT_CM table: Contains `LAB_LOINC` (the mapped LOINC code) and `RAW_LAB_CODE` (the local code). Sites with low mapping completeness will have many populated `RAW_LAB_CODE` records with missing `LAB_LOINC`. The network's data quality reporting should include LAB_LOINC fill rate per site per common analyte. - Claims (CPT): CPT panel codes (e.g., 80053 comprehensive metabolic panel) identify that a test was ordered and billed; individual constituent LOINC codes and numeric results are unavailable. Use claims for test utilization analysis (was the test ordered?), not for result-level phenotyping. - Registry: Disease registries often include key lab values (PSA for prostate cancer, tumor markers, staging labs) as structured fields; they may or may not carry LOINC codes, depending on registry design. Verify code presence before assuming LOINC alignment. - Linked EHR-claims: The ideal substrate for combining LOINC-coded lab values (from EHR) with complete medication utilization and CPT test ordering (from claims). Linkage enables: CPT confirms the test was ordered in the claims (utilization); LOINC provides the result value in the EHR (phenotyping). Reconcile by matching CPT service date to measurement date within a clinically sensible window (e.g., ± 3 days).
Maintenance and licensing
LOINC is maintained by the Regenstrief Institute at Indiana University and released approximately twice per year (typically February and August). The terminology is freely available for download and use after user registration; the license explicitly prohibits bulk reproduction of the full LOINC table in competing products. Small illustrative code lists with attribution ("Source: Regenstrief Institute LOINC, loinc.org") are permitted in publications and protocols.
Worked example
Scenario
A pharmacoepidemiology team is building a chronic kidney disease (CKD) cohort from a three-site OMOP network. The outcome definition requires two serum creatinine measurements of 1.50 mg/dL or higher, at least 90 days apart, to confirm CKD stage 3 or worse. The analyst queries the MEASUREMENT table across all three sites and discovers that creatinine results arrive under four different identifiers: two different LOINC codes (one for the Jaffe method, one for the enzymatic method), one unmapped local code, and a mix of units (mg/dL at sites A and C; µmol/L at site B). The worked example below shows the raw data, the unit conversion, and how the threshold is applied correctly after harmonization.
Dataset
Raw creatinine rows from the OMOP MEASUREMENT table across three sites. Site B reports in µmol/L; sites A and C report in mg/dL. Site C has one unmapped local code with a NULL LOINC.
| person_id | site | measurement_date | loinc_code | local_code | value_as_number | unit |
|---|---|---|---|---|---|---|
| 1001 | A | 2023-03-01 | 2160-0 | CREAT-S | 1.2 | mg/dL |
| 1001 | A | 2023-07-15 | 2160-0 | CREAT-S | 1.55 | mg/dL |
| 1002 | B | 2023-04-10 | 38483-4 | KREA | 132.6 | umol/L |
| 1002 | B | 2023-08-22 | 38483-4 | KREA | 168.96 | umol/L |
| 1003 | C | 2023-02-28 | LB-CREAT | 0.95 | mg/dL | |
| 1003 | C | 2023-09-05 | 2160-0 | CREAT-S | 1.62 | mg/dL |
Steps
Step 1 — Build the LOINC code list: serum creatinine is represented by at least two LOINC codes in this network: 2160-0 (Jaffe method, Ser/Plas) and 38483-4 (enzymatic method, Ser/Plas). Both measure the same analyte in the same specimen, so both belong in the code list. The local code LB-CREAT at site C is unmapped (NULL LOINC); include it via measurement_source_value matching after confirming with the site data manager that it represents serum creatinine.
Step 2 — Unit harmonization: site B reports in µmol/L. The conversion to mg/dL is: value_mg_dL = value_umol_L / 88.4. For person 1002: 132.6 / 88.4 = 1.50 mg/dL (first measurement) and 168.96 / 88.4 = 1.91 mg/dL (second measurement).
Step 3 — Apply the threshold to harmonized values. The 1.50 mg/dL threshold is applied after unit conversion. Harmonized creatinine values per person: Person 1001 (site A): 1.20 mg/dL (2023-03-01) and 1.55 mg/dL (2023-07-15). Second value meets threshold; gap = 136 days >= 90 days. Person 1002 (site B): 1.50 mg/dL (2023-04-10) and 1.91 mg/dL (2023-08-22). Both values meet threshold; gap = 134 days >= 90 days. Person 1003 (site C): 0.95 mg/dL (2023-02-28, unmapped local code, included after site confirmation) and 1.62 mg/dL (2023-09-05). Only the second value meets threshold; gap = 219 days but only one qualifying measurement, so person 1003 does not meet the two-value CKD criterion.
Step 4 — Apply the two-measurement CKD criterion (both >= 1.50 mg/dL, >= 90 days apart): person 1001 qualifies (1 out of 2 measurements above threshold? No — only the second is >= 1.50); re-check: 1.20 < 1.50 (fails) and 1.55 >= 1.50 (passes) — only one qualifying value, so person 1001 does NOT meet the criterion. Person 1002: both 1.50 >= 1.50 and 1.91 >= 1.50, gap = 134 days >= 90. Person 1002 QUALIFIES. Person 1003: only one qualifying value (1.62 on 2023-09-05). Does NOT qualify.
Step 5 — Without unit harmonization (the error case): if the analyst applies the 1.50 mg/dL threshold directly to site B's µmol/L values, person 1002's results (132.6 and 168.96) would appear to be extreme outliers far above any reasonable creatinine (normal range in mg/dL is 0.5–1.2), causing them to be either winsorized, excluded, or flagged incorrectly. The unit error renders the site B data unusable for threshold-based staging without conversion.
Result
After LOINC code-list expansion (including both 2160-0 and 38483-4), local-code inclusion via site confirmation, and unit harmonization (132.6 / 88.4 = 1.50 mg/dL; 168.96 / 88.4 = 1.91 mg/dL), exactly one patient (person 1002) meets the CKD criterion of two creatinine values >= 1.50 mg/dL at least 90 days apart. Person 1001 has only one qualifying value. Person 1003 also has only one qualifying value (the earlier unmapped-code record is below threshold). Cohort size = 1 of 3 patients. Without unit harmonization, site B data would be unusable and person 1002 would be lost entirely.
Runnable example
python implementation
Two utilities for LOINC-based lab harmonization in a pandas DataFrame representing an OMOP-style Measurement table. The first validates that a LOINC code string is syntactically correct using the mod-10 Luhn check-digit algorithm (detects single-character...
import pandas as pd
# ------------------------------------------------------------------ #
# 1. LOINC check-digit validator (mod-10 Luhn variant) #
# ------------------------------------------------------------------ #
# LOINC uses a Luhn mod-10 check digit appended after a hyphen,
# e.g. "2160-0" or "38483-4".
# Algorithm (per Regenstrief spec):
# - Take the numeric prefix digits.
# - Double every second digit from the right (rightmost prefix digit
# is position 1, not doubled).
# - Subtract 9 from doubled values > 9.
# - Sum all digits.
# - Check digit = (10 - (sum % 10)) % 10.
def validate_loinc(code: str) -> bool:
"""Return True if the LOINC code passes the mod-10 check digit.
Args:
code: LOINC code string, e.g. '2160-0' or '38483-4'.
Returns:
True if valid format and check digit matches; False otherwise.
"""
if not isinstance(code, str):
return False
parts = code.strip().split("-")
if len(parts) != 2:
return False
numeric_part, check_str = parts
if not numeric_part.isdigit() or not check_str.isdigit():
return False
check_digit = int(check_str)
digits = [int(d) for d in numeric_part]
# Double every second digit from the right (index from right: 0-based)
# rightmost digit of numeric_part has position 0 (not doubled)
total = 0
for i, d in enumerate(reversed(digits)):
if i % 2 == 1: # even positions from right (1-indexed) -> double
d *= 2
if d > 9:
d -= 9
total += d
expected = (10 - (total % 10)) % 10
return expected == check_digit
# Spot-check known codes
assert validate_loinc("2160-0"), "serum creatinine (Jaffe) should pass"
assert validate_loinc("38483-4"), "serum creatinine (enzymatic) should pass"
assert validate_loinc("4548-4"), "HbA1c should pass"
assert not validate_loinc("2160-1"), "wrong check digit should fail"
assert not validate_loinc("ABCD-0"), "non-numeric prefix should fail"
# ------------------------------------------------------------------ #
# 2. Creatinine harmonization across LOINC codes and units #
# ------------------------------------------------------------------ #
# Serum creatinine LOINC codes used in this example:
# 2160-0 Creatinine [Mass/volume] in Serum or Plasma (Jaffe method)
# 38483-4 Creatinine [Mass/volume] in Serum or Plasma (Enzymatic method)
# Conversion: mg/dL = umol/L / 88.4
CREATININE_LOINCS = {"2160-0", "38483-4"}
# Simulated OMOP Measurement table (as would be extracted from a CDM)
raw_data = {
"person_id": [1001, 1001, 1002, 1002, 1003, 1003],
"site": ["A", "A", "B", "B", "C", "C"],
"measurement_date": ["2023-03-01", "2023-07-15",
"2023-04-10", "2023-08-22",
"2023-02-28", "2023-09-05"],
"loinc_code": ["2160-0", "2160-0",
"38483-4", "38483-4",
None, "2160-0"],
"local_code": ["CREAT-S", "CREAT-S",
"KREA", "KREA",
"LB-CREAT","CREAT-S"],
"value_as_number": [1.20, 1.55, 132.6, 168.96, 0.95, 1.62],
"unit": ["mg/dL", "mg/dL",
"umol/L", "umol/L",
"mg/dL", "mg/dL"],
}
df = pd.DataFrame(raw_data)
df["measurement_date"] = pd.to_datetime(df["measurement_date"])
# --- Step 1: LOINC code validation for all non-null codes -------- #
loinc_valid = df["loinc_code"].dropna().apply(validate_loinc)
assert loinc_valid.all(), f"Invalid LOINC codes detected: {df.loc[~loinc_valid.reindex(df.index, fill_value=True), 'loinc_code'].tolist()}"
# --- Step 2: Audit unmapped records (NULL LOINC) ------------------ #
unmapped = df[df["loinc_code"].isna()].copy()
print(f"Unmapped records (NULL LOINC): {len(unmapped)}")
print(unmapped[["person_id", "site", "local_code", "value_as_number", "unit"]])
# In production: confirm with site data manager that LB-CREAT = serum creatinine
# and include after confirmation. Here we include it as a simplification.
# --- Step 3: Filter to creatinine records (LOINC + confirmed local) #
mask_loinc = df["loinc_code"].isin(CREATININE_LOINCS)
mask_local = df["local_code"].isin({"LB-CREAT"}) # site-confirmed
creatinine = df[mask_loinc | mask_local].copy()
# --- Step 4: Unit harmonization to mg/dL ------------------------- #
UMOL_PER_MGDL = 88.4 # 1 mg/dL creatinine = 88.4 umol/L
def harmonize_creatinine(row):
v = row["value_as_number"]
u = (row["unit"] or "").lower().strip()
if u in ("umol/l", "µmol/l", "umol/L"):
return v / UMOL_PER_MGDL
elif u in ("mg/dl", "mg/dL"):
return v
else:
return float("nan") # unknown unit -> flag for review
creatinine["creatinine_mgdl"] = creatinine.apply(harmonize_creatinine, axis=1)
# Verify the unit conversion arithmetic (checked by gate):
# 132.6 / 88.4 = 1.50 mg/dL
# 168.96 / 88.4 = 1.91 mg/dL
import math
assert math.isclose(132.6 / 88.4, 1.50, rel_tol=0.01), "132.6/88.4 should equal 1.50"
assert math.isclose(168.96 / 88.4, 1.91, rel_tol=0.01), "168.96/88.4 should equal ~1.91"
# --- Step 5: Apply CKD phenotype threshold ----------------------- #
THRESHOLD_MGDL = 1.50
DAYS_APART = 90
creatinine = creatinine.sort_values(["person_id", "measurement_date"])
creatinine["meets_threshold"] = creatinine["creatinine_mgdl"] >= THRESHOLD_MGDL
qualifying = []
for pid, group in creatinine.groupby("person_id"):
above = group[group["meets_threshold"]].copy()
if len(above) < 2:
continue
above = above.sort_values("measurement_date")
for i in range(len(above) - 1):
gap = (above.iloc[i + 1]["measurement_date"] - above.iloc[i]["measurement_date"]).days
if gap >= DAYS_APART:
qualifying.append(pid)
break
print(f"\nCKD cohort (two creatinine >= {THRESHOLD_MGDL} mg/dL, >= {DAYS_APART} days apart):")
print(f"Qualifying person_ids: {qualifying}")
# Expected: [1002] only
assert qualifying == [1002], f"Expected [1002], got {qualifying}"
# Show harmonized values for verification
print("\nHarmonized creatinine table:")
print(creatinine[["person_id", "site", "measurement_date",
"loinc_code", "value_as_number", "unit", "creatinine_mgdl"]])r implementation
R implementation of the same two utilities: a LOINC check-digit validator using the mod-10 Luhn algorithm, and a creatinine harmonization workflow that builds a code list, applies unit conversion (µmol/L to mg/dL), and identifies patients meeting a CKD...
library(dplyr)
# ------------------------------------------------------------------ #
# 1. LOINC check-digit validator (mod-10 Luhn variant) #
# ------------------------------------------------------------------ #
validate_loinc <- function(code) {
# Returns TRUE if the LOINC code passes the mod-10 check digit.
# code: character string, e.g. "2160-0" or "38483-4"
if (!is.character(code) || is.na(code)) return(FALSE)
parts <- strsplit(trimws(code), "-", fixed = TRUE)[[1]]
if (length(parts) != 2) return(FALSE)
numeric_part <- parts[1]
check_str <- parts[2]
if (grepl("[^0-9]", numeric_part) || grepl("[^0-9]", check_str)) return(FALSE)
check_digit <- as.integer(check_str)
digits <- as.integer(strsplit(numeric_part, "")[[1]])
# Double every second digit from the right (position 1 from right = not doubled)
n <- length(digits)
for (i in seq_along(digits)) {
pos_from_right <- n - i # 0-based position from right
if (pos_from_right %% 2 == 1) { # odd position from right -> double
d <- digits[i] * 2
if (d > 9) d <- d - 9
digits[i] <- d
}
}
expected <- (10 - (sum(digits) %% 10)) %% 10
return(expected == check_digit)
}
# Spot-checks
stopifnot(validate_loinc("2160-0")) # serum creatinine Jaffe
stopifnot(validate_loinc("38483-4")) # serum creatinine enzymatic
stopifnot(validate_loinc("4548-4")) # HbA1c
stopifnot(!validate_loinc("2160-1")) # wrong check digit
stopifnot(!validate_loinc("ABCD-0")) # non-numeric
# ------------------------------------------------------------------ #
# 2. Creatinine harmonization across LOINC codes and units #
# ------------------------------------------------------------------ #
# Conversion constant: 1 mg/dL creatinine = 88.4 umol/L
UMOL_PER_MGDL <- 88.4
# Simulated OMOP Measurement tibble
df <- tibble(
person_id = c(1001, 1001, 1002, 1002, 1003, 1003),
site = c("A", "A", "B", "B", "C", "C"),
measurement_date = as.Date(c("2023-03-01","2023-07-15",
"2023-04-10","2023-08-22",
"2023-02-28","2023-09-05")),
loinc_code = c("2160-0", "2160-0",
"38483-4", "38483-4",
NA, "2160-0"),
local_code = c("CREAT-S","CREAT-S",
"KREA", "KREA",
"LB-CREAT","CREAT-S"),
value_as_number = c(1.20, 1.55, 132.6, 168.96, 0.95, 1.62),
unit = c("mg/dL","mg/dL",
"umol/L","umol/L",
"mg/dL","mg/dL")
)
# LOINC codes for serum creatinine (Jaffe + enzymatic)
CREATININE_LOINCS <- c("2160-0", "38483-4")
LOCAL_CREATININE <- c("LB-CREAT") # site-confirmed local codes
# Step 1: Validate non-null LOINC codes
valid_loinc <- df |>
filter(!is.na(loinc_code)) |>
pull(loinc_code) |>
sapply(validate_loinc)
stopifnot(all(valid_loinc))
# Step 2: Audit unmapped records
unmapped <- df |> filter(is.na(loinc_code))
cat("Unmapped records (NULL LOINC):", nrow(unmapped), "\n")
# Step 3: Filter to creatinine (LOINC + site-confirmed local codes)
creatinine <- df |>
filter(loinc_code %in% CREATININE_LOINCS | local_code %in% LOCAL_CREATININE)
# Step 4: Unit harmonization to mg/dL
# Conversion: mg/dL = umol/L / 88.4
creatinine <- creatinine |>
mutate(
creatinine_mgdl = case_when(
tolower(trimws(unit)) %in% c("umol/l", "µmol/l") ~ value_as_number / UMOL_PER_MGDL,
tolower(trimws(unit)) == "mg/dl" ~ value_as_number,
TRUE ~ NA_real_
)
)
# Verify conversion arithmetic: 132.6 / 88.4 = 1.50 mg/dL
# 168.96 / 88.4 = 1.91 mg/dL
stopifnot(abs(132.6 / UMOL_PER_MGDL - 1.50) < 0.01)
stopifnot(abs(168.96 / UMOL_PER_MGDL - 1.91) < 0.01)
# Step 5: Apply CKD threshold (>= 1.50 mg/dL on >= 2 occasions >= 90 days apart)
THRESHOLD <- 1.50
DAYS_APART <- 90
ckd_cohort <- creatinine |>
filter(creatinine_mgdl >= THRESHOLD) |>
arrange(person_id, measurement_date) |>
group_by(person_id) |>
summarise(
n_above = n(),
first_date = min(measurement_date),
last_date = max(measurement_date),
gap_days = as.integer(max(measurement_date) - min(measurement_date)),
.groups = "drop"
) |>
filter(n_above >= 2, gap_days >= DAYS_APART)
cat("\nCKD cohort (two creatinine >= 1.50 mg/dL, >= 90 days apart):\n")
print(ckd_cohort)
# Expected: person_id 1002 only (gap = 134 days, both values >= 1.50)
stopifnot(nrow(ckd_cohort) == 1 && ckd_cohort$person_id[1] == 1002)
cat("\nHarmonized creatinine table:\n")
print(creatinine |>
select(person_id, site, measurement_date, loinc_code,
value_as_number, unit, creatinine_mgdl))