From 58f187e16514adce3ce5014d86428aeca36105a8 Mon Sep 17 00:00:00 2001 From: Pip Liggins Date: Thu, 26 Mar 2026 13:33:04 +0000 Subject: [PATCH] Update tests for units in ARC to catch known bug --- tests/test_units.py | 583 +++++++++++++++++++++++++------------ units/unit_conversion.json | 480 ++++++++++++++++++++++++------ 2 files changed, 789 insertions(+), 274 deletions(-) diff --git a/tests/test_units.py b/tests/test_units.py index 425918d..1d5045a 100644 --- a/tests/test_units.py +++ b/tests/test_units.py @@ -1,4 +1,6 @@ import pathlib +import re +import json import pytest import pandas as pd import numpy as np @@ -14,22 +16,117 @@ EXCEPTIONS = ["demog_age_units", "medi_units"] +# --- Fixtures --- + + +@pytest.fixture(scope="module") +def arc_df(): + """Load full ARC.csv with all columns needed for unit tests.""" + return pd.read_csv( + ARC_PATH, + dtype="object", + usecols=[ + "Variable", + "Type", + "Validation", + "Answer Options", + "Question", + "Minimum", + "Maximum", + ], + ) + + +@pytest.fixture(scope="module") +def conversion_registry(): + """Load the unit conversion registry from JSON.""" + return ConversionRegistry().load_from_json( + path=UNITS_PATH, + schema_path=SCHEMA_PATH, + ) + + +@pytest.fixture(scope="module") +def unit_conversion_json(): + """Load raw unit_conversion.json as dict list.""" + with open(UNITS_PATH, "r") as f: + return json.load(f) + + +@pytest.fixture(scope="module") +def unit_fields_info(arc_df): + """ + Extract unit field information from ARC. + + Returns dict with: + - units_field_names: list of variables with Validation='units' + - field_names: list of base field names (without '_units' suffix) + - units_field_names_dict: mapping from field_name to units_field_name + - unit_specific_field_names: dict mapping field_name to list of unit-specific vars + """ + units_idx = arc_df["Validation"] == "units" + units_field_names = arc_df.loc[units_idx, "Variable"].tolist() + field_names = [x.split("_units")[0] for x in units_field_names] + + unit_specific_field_names = { + x: arc_df.loc[ + arc_df["Variable"].str.startswith(x + "_") + & ~arc_df["Variable"].str.endswith("_units"), + "Variable", + ].tolist() + for x in field_names + } + + return { + "units_field_names": units_field_names, + "field_names": field_names, + "units_field_names_dict": dict(zip(field_names, units_field_names)), + "unit_specific_field_names": unit_specific_field_names, + } + + +# --- Helper functions --- + + +def parse_answer_options(options_str, include_values=False): + """ + Parse Answer Options string. + + Args: + options_str: String like '1, cm | 2, in' + include_values: If True, return [(1, 'cm'), (2, 'in')]. + If False, return ['cm', 'in']. + """ + if pd.isna(options_str) or not options_str.strip(): + return [] + result = [] + for option in options_str.split("|"): + option = option.strip() + if "," in option: + value, label = option.split(",", 1) + if include_values: + result.append((int(value.strip()), label.strip())) + else: + result.append(label.strip()) + return result + + +# --- Tests --- + + @pytest.mark.high -def test_arc_units_correct_type_validation(): +def test_arc_units_correct_type_validation(arc_df): """ Variable names ending in "_units" must have Type="radio" and Validation="units", except the variables "demog_age_units" and "medi_units" which are special cases. """ - arc = pd.read_csv( - ARC_PATH, dtype="object", usecols=["Variable", "Type", "Validation"] - ) condition = ( - ~arc["Variable"].str.endswith("_units") - | (arc["Type"].isin(["radio"]) & arc["Validation"].isin(["units"])) - | arc["Variable"].isin(EXCEPTIONS) + ~arc_df["Variable"].str.endswith("_units") + | (arc_df["Type"].isin(["radio"]) & arc_df["Validation"].isin(["units"])) + | arc_df["Variable"].isin(EXCEPTIONS) ) if not condition.all(): - invalid = arc.loc[~condition].set_index("Variable").to_dict(orient="index") + invalid = arc_df.loc[~condition].set_index("Variable").to_dict(orient="index") pytest.fail( """Variable names ending in "_units" must have Type="radio" and """ f"""Validation="units".Variables: {invalid}""" @@ -37,85 +134,195 @@ def test_arc_units_correct_type_validation(): @pytest.mark.high -def test_arc_numeric_variables_exist(): +def test_arc_units_have_numeric_variable(arc_df, unit_fields_info): """ - Variable names ending in "_units" must have be related to numeric variable - without this suffix, plus the same number of unit-specific numeric variables - as in the Answer Options. This doesn't check that the unit-specific variables - actually match the Answer Options though. + Variable names ending in "_units" must have a corresponding numeric variable + without this suffix (e.g., labs_neutrophil for labs_neutrophil_units). """ - arc = pd.read_csv( - ARC_PATH, - dtype="object", - usecols=["Variable", "Validation", "Answer Options"], - ) + units_field_names = unit_fields_info["units_field_names"] + field_names = unit_fields_info["field_names"] + all_arc_variables = arc_df["Variable"].tolist() - units_idx = arc["Validation"] == "units" - units_field_names = arc.loc[units_idx, "Variable"].tolist() - field_names = [x.split("_units")[0] for x in units_field_names] - - condition = [x in arc["Variable"].tolist() for x in field_names] + condition = [x in all_arc_variables for x in field_names] if not np.all(condition): invalid = [x for x, y in zip(units_field_names, condition) if not y] pytest.fail( f"Unit variables {invalid} do not have corresponding numeric variables" ) - unit_specific_field_names = { - x: arc.loc[ - arc["Variable"].str.startswith(x + "_") - & ~arc["Variable"].str.endswith("_units"), - "Variable", - ].tolist() - for x in field_names - } - unit_answers = { - x: arc.set_index("Variable").loc[x + "_units", "Answer Options"].split("|") +@pytest.mark.high +def test_arc_json_field_synchronization(unit_fields_info, conversion_registry): + """ + Bidirectional synchronization check between ARC and unit_conversion.json: + - All ARC fields with Validation='units' must exist in JSON as 'field_name's + - All JSON field_name entries must exist in ARC + - The units_field_name in JSON must match the ARC naming convention + """ + field_names = unit_fields_info["field_names"] + units_field_names_dict = unit_fields_info["units_field_names_dict"] + entries = conversion_registry.conversion_entries + + # Check ARC → JSON: all ARC unit fields exist in JSON + missing_in_json = [ + x for x in field_names + if x not in entries.keys() and x + "_units" not in EXCEPTIONS + ] + if missing_in_json: + pytest.fail( + f"ARC variables {missing_in_json} need to be added to units " + f"conversion JSON file {UNITS_PATH}" + ) + + # Check JSON → ARC: all JSON entries exist in ARC + entries_filtered = { + k: v for k, v in entries.items() if v.units_field_name not in EXCEPTIONS } - unit_answers = {k: [x.strip() for x in v] for k, v in unit_answers.items()} + missing_in_arc = [x for x in entries_filtered.keys() if x not in field_names] + if missing_in_arc: + pytest.fail( + f"Conversion JSON file {UNITS_PATH} contains variables " + f"{missing_in_arc} that are not in ARC." + ) - condition = [ - len(unit_answers[x]) == len(unit_specific_field_names[x]) for x in field_names + # Check units_field_name in JSON matches ARC naming + inconsistent_units_field_names = [ + units_field_names_dict[x] + for x in field_names + if x in entries and entries[x].units_field_name != units_field_names_dict[x] ] - if not np.all(condition): - invalid = [x for x, y in zip(field_names, condition) if not y] + if inconsistent_units_field_names: pytest.fail( - "The number of Answer Options for the radio units variable does not " - f"match the number of unit-specific ARC variables. Variables: {invalid}" + f"ARC variables {inconsistent_units_field_names} does not match the entry in " + f"conversion JSON file {UNITS_PATH}" ) @pytest.mark.high -def test_arc_consistent_min_max(): +def test_unit_specific_variables_exist(arc_df, unit_fields_info, conversion_registry): """ - Variables with multiple units must have consistent min-max values. + For each unit_field_name defined in unit_conversion.json, there must be a + corresponding variable in ARC. Conversely, unit-specific variables in ARC + (e.g., labs_neutrophil_pcnt) must be defined in the JSON. """ - arc = pd.read_csv( - ARC_PATH, - dtype="object", - usecols=["Variable", "Validation", "Minimum", "Maximum"], - ) + field_names = unit_fields_info["field_names"] + unit_specific_field_names = unit_fields_info["unit_specific_field_names"] + unit_conversion = conversion_registry.conversion_entries + all_arc_variables = arc_df["Variable"].tolist() + + missing_unit_vars = [] + for field_name in field_names: + if field_name + "_units" in EXCEPTIONS: + continue + if field_name not in unit_conversion: + # Caught by test_arc_json_field_synchronization + continue + + actual_unit_vars = unit_specific_field_names[field_name] + + # Expected variables from unit_field_name in JSON (may be None for some units) + json_units = unit_conversion[field_name].units + expected_unit_vars = [ + u.unit_field_name for u in json_units.units if u.unit_field_name is not None + ] - units_idx = arc["Validation"] == "units" - units_field_names = arc.loc[units_idx, "Variable"].tolist() - field_names = [x.split("_units")[0] for x in units_field_names] + missing = [v for v in expected_unit_vars if v not in all_arc_variables] + extra = [v for v in actual_unit_vars if v not in expected_unit_vars] + + if missing or extra: + missing_unit_vars.append( + { + "field": field_name, + "missing": missing, + "extra": extra, + } + ) + + if missing_unit_vars: + msg = "Unit-specific variables in ARC.csv do not match unit_conversion.json:\n" + for m in missing_unit_vars: + if m["missing"]: + msg += f" - {m['field']}: missing variables {m['missing']}\n" + if m["extra"]: + msg += f" - {m['field']}: unexpected variables {m['extra']}\n" + pytest.fail(msg) + + +@pytest.mark.high +def test_arc_answer_options_match_unit_conversion_json( + arc_df, unit_fields_info, conversion_registry +): + """ + Answer Options for unit selection variables must match exactly with the + unit values and labels in the unit_conversion.json file. + """ + field_names = unit_fields_info["field_names"] + unit_conversion = conversion_registry.conversion_entries + + arc_indexed = arc_df.set_index("Variable") + arc_answer_options = { + x: parse_answer_options( + arc_indexed.loc[x + "_units", "Answer Options"], include_values=True + ) + for x in field_names + } + + # Check values and labels match between ARC and conversion JSON + mismatches = [] + for field_name in field_names: + if field_name + "_units" in EXCEPTIONS: + continue + if field_name not in unit_conversion: + # This is caught by test_arc_json_field_synchronization + continue + + arc_options = arc_answer_options[field_name] + json_units = unit_conversion[field_name].units + + # Build expected pairs from JSON + json_options = [(u.unit_value, u.unit_label) for u in json_units.units] + + if arc_options != json_options: + mismatches.append( + { + "field": field_name, + "arc_options": arc_options, + "json_options": json_options, + } + ) + + if mismatches: + msg = "Answer Options in ARC.csv do not match unit_conversion.json:\n" + for m in mismatches: + msg += ( + f" - {m['field']}: ARC has {m['arc_options']} " + f"but JSON has {m['json_options']}\n" + ) + pytest.fail(msg) + + +@pytest.mark.high +def test_arc_consistent_min_max(arc_df, unit_fields_info): + """ + Variables with multiple units must have consistent min-max values. + """ + field_names = unit_fields_info["field_names"] values = [ { - "min": float(arc.set_index("Variable").loc[x, "Minimum"]), - "max": float(arc.set_index("Variable").loc[x, "Maximum"]), - "min_of_min": arc.loc[ - arc["Variable"].str.startswith(x + "_") - & ~arc["Variable"].str.endswith("_units"), + "min": float(arc_df.set_index("Variable").loc[x, "Minimum"]), + "max": float(arc_df.set_index("Variable").loc[x, "Maximum"]), + "min_of_min": arc_df.loc[ + arc_df["Variable"].str.startswith(x + "_") + & ~arc_df["Variable"].str.endswith("_units"), "Minimum", ] .astype(float) .min(skipna=False), - "max_of_max": arc.loc[ - arc["Variable"].str.startswith(x + "_") - & ~arc["Variable"].str.endswith("_units"), + "max_of_max": arc_df.loc[ + arc_df["Variable"].str.startswith(x + "_") + & ~arc_df["Variable"].str.endswith("_units"), "Maximum", ] .astype(float) @@ -145,137 +352,19 @@ def test_arc_consistent_min_max(): @pytest.mark.high -def test_fields_in_arc_exist_in_conversions(): - arc = pd.read_csv( - ARC_PATH, - dtype="object", - usecols=["Variable", "Validation", "Answer Options"], - ) - - units_idx = arc["Validation"] == "units" - units_field_names = arc.loc[units_idx, "Variable"].tolist() - field_names = [x.split("_units")[0] for x in units_field_names] - - units_field_names = dict(zip(field_names, units_field_names)) - unit_specific_field_names = { - x: arc.loc[ - arc["Variable"].str.startswith(x + "_") - & ~arc["Variable"].str.endswith("_units"), - "Variable", - ].tolist() - for x in field_names - } - - conversion_registry = ConversionRegistry().load_from_json( - path=UNITS_PATH, - schema_path=SCHEMA_PATH, - ) - entries = conversion_registry.conversion_entries - - missing_field_names = [x for x in field_names if x not in entries.keys()] - if missing_field_names: - pytest.fail( - f"ARC variables {missing_field_names} need to be added to units " - f"conversion JSON file {UNITS_PATH}" - ) - - inconsistent_units_field_names = [ - units_field_names[x] - for x in field_names - if entries[x].units_field_name != units_field_names[x] - ] - if inconsistent_units_field_names: - pytest.fail( - f"ARC variables {inconsistent_units_field_names} does not match the entry in " - f"conversion JSON file {UNITS_PATH}" - ) - - missing_unit_specific_field_names = [ - x - for x in field_names - if not set(unit_specific_field_names[x]).issubset( - x.unit_field_name for x in entries[x].units.units - ) - ] - if missing_unit_specific_field_names: - pytest.fail( - f"ARC variables {missing_unit_specific_field_names} have units not listed in " - f"conversion JSON file {UNITS_PATH}. Please update the JSON file." - ) - - -@pytest.mark.medium -def test_fields_in_conversions_exist_in_arc(): - arc = pd.read_csv( - ARC_PATH, - dtype="object", - usecols=["Variable", "Validation"], - ) - - units_idx = arc["Validation"] == "units" - units_field_names = arc.loc[units_idx, "Variable"].tolist() - field_names = [x.split("_units")[0] for x in units_field_names] - - units_field_names = dict(zip(field_names, units_field_names)) - unit_specific_field_names = { - x: arc.loc[ - arc["Variable"].str.startswith(x + "_") - & ~arc["Variable"].str.endswith("_units"), - "Variable", - ].tolist() - for x in field_names - } - - conversion_registry = ConversionRegistry().load_from_json( - path=UNITS_PATH, - schema_path=SCHEMA_PATH, - ) - entries = conversion_registry.conversion_entries - entries = {k: v for k, v in entries.items() if v.units_field_name not in EXCEPTIONS} - - missing_field_names = [x for x in entries.keys() if x not in field_names] - if missing_field_names: - pytest.fail( - f"Conversion JSON file {UNITS_PATH} contains variables " - f"{missing_field_names} that are not in ARC." - ) - - missing_unit_specific_field_names = [ - x - for x in field_names - if not set(x.unit_field_name for x in entries[x].units.units).issubset( - unit_specific_field_names[x] - ) - ] - if missing_unit_specific_field_names: - pytest.fail( - f"ARC variables {missing_unit_specific_field_names} does not include units " - f"listed in conversion JSON file {UNITS_PATH}. Please update the JSON file." - ) - - -@pytest.mark.high -def test_valid_conversions_for_min_max(): +def test_valid_conversions_for_min_max(arc_df, unit_fields_info, conversion_registry): """ Test that the min/max values for unit-specific variables align with conversion functions. """ - arc = pd.read_csv( - ARC_PATH, - dtype="object", - usecols=["Variable", "Validation", "Minimum", "Maximum"], - ) - - units_idx = arc["Validation"] == "units" - units_field_names = arc.loc[units_idx, "Variable"].tolist() - field_names = [x.split("_units")[0] for x in units_field_names] + field_names = unit_fields_info["field_names"] + entries = conversion_registry.conversion_entries - units_field_names = dict(zip(field_names, units_field_names)) arc_subset = pd.concat( [ - arc.loc[ - arc["Variable"].str.startswith(x + "_") - & ~arc["Variable"].str.endswith("_units"), + arc_df.loc[ + arc_df["Variable"].str.startswith(x + "_") + & ~arc_df["Variable"].str.endswith("_units"), ["Variable", "Minimum", "Maximum"], ] .assign(field_name=x) @@ -288,16 +377,9 @@ def test_valid_conversions_for_min_max(): float ) - conversion_registry = ConversionRegistry().load_from_json( - path=UNITS_PATH, - schema_path=SCHEMA_PATH, - ) - - entries = conversion_registry.conversion_entries - # Ignore fields not yet in the JSON file, these are highlighted by an earlier test arc_subset = arc_subset.loc[arc_subset["field_name"].isin(entries.keys())] - field_names = arc_subset["field_name"].unique().tolist() + field_names_filtered = arc_subset["field_name"].unique().tolist() def get_from_unit(x: pd.DataFrame): try: @@ -352,12 +434,12 @@ def convert(x: pd.DataFrame, values_column="Minimum"): invalid_min = [ x - for x in field_names + for x in field_names_filtered if x in arc_subset.loc[invalid_min, "field_name"].tolist() ] invalid_max = [ x - for x in field_names + for x in field_names_filtered if x in arc_subset.loc[invalid_max, "field_name"].tolist() ] if invalid_min + invalid_max: @@ -367,3 +449,120 @@ def convert(x: pd.DataFrame, values_column="Minimum"): "Max values for unit-specific variables are not consistent with " f"the conversion functions for {invalid_max}. Fix ARC or conversion JSON." ) + + +@pytest.mark.high +def test_unit_labels_match_question_text(arc_df, unit_conversion_json): + """ + Unit labels in unit_conversion.json must match the units displayed in + the Question text (in parentheses) for each unit-specific variable in ARC.csv. + + This ensures consistency between the question text and the internal conversion + logic. + + Example: + For labs_neutrophil_pcnt with Question "Neutrophils (%)", the unit_label + in unit_conversion.json must be "%" (not "percent" or "pcnt"). + """ + + arc = arc_df.set_index("Variable") + + mismatches = [] + + for conversion_entry in unit_conversion_json: + if conversion_entry["units_field_name"] in EXCEPTIONS: + continue + + for unit_option in conversion_entry.get("units", []): + unit_label = unit_option.get("unit_label") + unit_field_name = unit_option.get("unit_field_name") + if unit_field_name is None: + continue + + question = arc.loc[unit_field_name, "Question"] + + # Extract unit from parentheses at the end of the question + # e.g., "CD4 cell count (cells/mm^3)" -> "cells/mm^3" + match = re.search(r"\(([^)]+)\)\s*$", question) + if not match: + continue + + question_unit = match.group(1) + + if question_unit != unit_label: + mismatches.append( + { + "variable": unit_field_name, + "question_unit": question_unit, + "conversion_unit": unit_label, + "question": question, + } + ) + + if mismatches: + msg = "Unit labels in unit_conversion.json do not match Question text in ARC.csv:\n" + for m in mismatches: + msg += ( + f" - {m['variable']}: Question has '{m['question_unit']}' " + f"but conversion JSON has '{m['conversion_unit']}'\n" + ) + pytest.fail(msg) + + +@pytest.mark.high +def test_question_units_match_answer_options(arc_df): + """ + For unit-specific fields (e.g., labs_neutrophil_pcnt), the unit in the + Question text must match one of the Answer Options in the corresponding + unit selection field (e.g., labs_neutrophil_units). + + This is a direct ARC-only consistency check that doesn't rely the conversion registry. + """ + + arc = arc_df.set_index("Variable") + + # Get all unit selection fields + units_fields = arc[arc["Validation"] == "units"].index.tolist() + units_fields = [x for x in units_fields if x not in EXCEPTIONS] + + mismatches = [] + + for units_field in units_fields: + field_name = units_field.rsplit("_units", 1)[0] + answer_options = parse_answer_options(arc.loc[units_field, "Answer Options"]) + + # Find unit-specific variables (e.g., labs_neutrophil_pcnt for labs_neutrophil) + unit_specific_vars = [ + v + for v in arc.index + if v.startswith(field_name + "_") and not v.endswith("_units") + ] + + for var in unit_specific_vars: + question = arc.loc[var, "Question"] + + # Extract unit from parentheses at the end of the question + match = re.search(r"\(([^)]+)\)\s*$", question) + if not match: + continue + + question_unit = match.group(1) + + if question_unit not in answer_options: + mismatches.append( + { + "variable": var, + "units_field": units_field, + "question_unit": question_unit, + "answer_options": answer_options, + } + ) + + if mismatches: + msg = "Question units do not match Answer Options in unit selection field:\n" + for m in mismatches: + msg += ( + f" - {m['variable']}: Question has '{m['question_unit']}' " + f"but {m['units_field']} Answer Options are {m['answer_options']}\n" + ) + pytest.fail(msg) diff --git a/units/unit_conversion.json b/units/unit_conversion.json index c281339..8c41eb6 100644 --- a/units/unit_conversion.json +++ b/units/unit_conversion.json @@ -3,9 +3,21 @@ "field_name": "demog_age", "units_field_name": "demog_age_units", "units": [ - { "unit_label": "Days", "unit_value": 1, "unit_field_name": null }, - { "unit_label": "Months", "unit_value": 2, "unit_field_name": null }, - { "unit_label": "Years", "unit_value": 3, "unit_field_name": null } + { + "unit_label": "Days", + "unit_value": 1, + "unit_field_name": null + }, + { + "unit_label": "Months", + "unit_value": 2, + "unit_field_name": null + }, + { + "unit_label": "Years", + "unit_value": 3, + "unit_field_name": null + } ], "preferred_unit": "Years", "conversion_rules": [ @@ -31,8 +43,16 @@ "field_name": "demog_height", "units_field_name": "demog_height_units", "units": [ - { "unit_label": "cm", "unit_value": 1, "unit_field_name": "demog_height_cm" }, - { "unit_label": "in", "unit_value": 2, "unit_field_name": "demog_height_in" } + { + "unit_label": "cm", + "unit_value": 1, + "unit_field_name": "demog_height_cm" + }, + { + "unit_label": "in", + "unit_value": 2, + "unit_field_name": "demog_height_in" + } ], "preferred_unit": "cm", "conversion_rules": [ @@ -50,8 +70,16 @@ "field_name": "demog_weight", "units_field_name": "demog_weight_units", "units": [ - { "unit_label": "kg", "unit_value": 1, "unit_field_name": "demog_weight_kg" }, - { "unit_label": "lb", "unit_value": 2, "unit_field_name": "demog_weight_lb" } + { + "unit_label": "kg", + "unit_value": 1, + "unit_field_name": "demog_weight_kg" + }, + { + "unit_label": "lb", + "unit_value": 2, + "unit_field_name": "demog_weight_lb" + } ], "preferred_unit": "kg", "conversion_rules": [ @@ -69,8 +97,16 @@ "field_name": "comor_hba1c", "units_field_name": "comor_hba1c_units", "units": [ - { "unit_label": "mmol/mol", "unit_value": 1, "unit_field_name": "comor_hba1c_mmolmol" }, - { "unit_label": "%", "unit_value": 2, "unit_field_name": "comor_hba1c_pcnt" } + { + "unit_label": "mmol/mol", + "unit_value": 1, + "unit_field_name": "comor_hba1c_mmolmol" + }, + { + "unit_label": "%", + "unit_value": 2, + "unit_field_name": "comor_hba1c_pcnt" + } ], "preferred_unit": "mmol/mol", "conversion_rules": [ @@ -88,8 +124,16 @@ "field_name": "infa_brtwei", "units_field_name": "infa_brtwei_units", "units": [ - { "unit_label": "g", "unit_value": 1, "unit_field_name": "infa_brtwei_g" }, - { "unit_label": "lb", "unit_value": 2, "unit_field_name": "infa_brtwei_lb" } + { + "unit_label": "g", + "unit_value": 1, + "unit_field_name": "infa_brtwei_g" + }, + { + "unit_label": "lb", + "unit_value": 2, + "unit_field_name": "infa_brtwei_lb" + } ], "preferred_unit": "g", "conversion_rules": [ @@ -107,8 +151,16 @@ "field_name": "vital_highesttem", "units_field_name": "vital_highesttem_units", "units": [ - { "unit_label": "°C", "unit_value": 1, "unit_field_name": "vital_highesttem_c" }, - { "unit_label": "°F", "unit_value": 2, "unit_field_name": "vital_highesttem_f" } + { + "unit_label": "°C", + "unit_value": 1, + "unit_field_name": "vital_highesttem_c" + }, + { + "unit_label": "°F", + "unit_value": 2, + "unit_field_name": "vital_highesttem_f" + } ], "preferred_unit": "°C", "conversion_rules": [ @@ -126,14 +178,26 @@ "field_name": "vital_fio2spo2", "units_field_name": "vital_fio2spo2_units", "units": [ - { "unit_label": "Fraction, 0.21-1.0", "unit_value": 1, "unit_field_name": "vital_fio2spo2_02110" }, - { "unit_label": "%", "unit_value": 2, "unit_field_name": "vital_fio2spo2_pcnt" }, - { "unit_label": "Highest L/min", "unit_value": 3, "unit_field_name": "vital_fio2spo2_ltr" } + { + "unit_label": "Fraction, 0.21-1.0", + "unit_value": 1, + "unit_field_name": "vital_fio2spo2_02110" + }, + { + "unit_label": "%, 21-100", + "unit_value": 2, + "unit_field_name": "vital_fio2spo2_pcnt" + }, + { + "unit_label": "Highest L/min", + "unit_value": 3, + "unit_field_name": "vital_fio2spo2_ltr" + } ], "preferred_unit": "Fraction, 0.21-1.0", "conversion_rules": [ { - "from_unit": "%", + "from_unit": "%, 21-100", "to_unit": "Fraction, 0.21-1.0", "type": "linear", "multiplier": 0.01, @@ -152,8 +216,16 @@ "field_name": "treat_pao2", "units_field_name": "treat_pao2_units", "units": [ - { "unit_label": "mmHg", "unit_value": 1, "unit_field_name": "treat_pao2_mmhg" }, - { "unit_label": "kPa", "unit_value": 2, "unit_field_name": "treat_pao2_kpa" } + { + "unit_label": "mmHg", + "unit_value": 1, + "unit_field_name": "treat_pao2_mmhg" + }, + { + "unit_label": "kPa", + "unit_value": 2, + "unit_field_name": "treat_pao2_kpa" + } ], "preferred_unit": "mmHg", "conversion_rules": [ @@ -171,13 +243,21 @@ "field_name": "treat_highfio", "units_field_name": "treat_highfio_units", "units": [ - { "unit_label": "Fraction, 0.21-1.0", "unit_value": 1, "unit_field_name": "treat_highfio_02110" }, - { "unit_label": "%", "unit_value": 2, "unit_field_name": "treat_highfio_pcnt" } + { + "unit_label": "Fraction, 0.21-1.0", + "unit_value": 1, + "unit_field_name": "treat_highfio_02110" + }, + { + "unit_label": "%, 21-100", + "unit_value": 2, + "unit_field_name": "treat_highfio_pcnt" + } ], "preferred_unit": "Fraction, 0.21-1.0", "conversion_rules": [ { - "from_unit": "%", + "from_unit": "%, 21-100", "to_unit": "Fraction, 0.21-1.0", "type": "linear", "multiplier": 0.01, @@ -190,8 +270,16 @@ "field_name": "labs_haemo", "units_field_name": "labs_haemo_units", "units": [ - { "unit_label": "g/dL", "unit_value": 1, "unit_field_name": "labs_haemo_gdl" }, - { "unit_label": "g/L", "unit_value": 2, "unit_field_name": "labs_haemo_gl" } + { + "unit_label": "g/dL", + "unit_value": 1, + "unit_field_name": "labs_haemo_gdl" + }, + { + "unit_label": "g/L", + "unit_value": 2, + "unit_field_name": "labs_haemo_gl" + } ], "preferred_unit": "g/dL", "conversion_rules": [ @@ -209,8 +297,16 @@ "field_name": "labs_neutrophil", "units_field_name": "labs_neutrophil_units", "units": [ - { "unit_label": "%", "unit_value": 1, "unit_field_name": "labs_neutrophil_pcnt" }, - { "unit_label": "10^9/L", "unit_value": 2, "unit_field_name": "labs_neutrophil_109l" } + { + "unit_label": "%", + "unit_value": 1, + "unit_field_name": "labs_neutrophil_pcnt" + }, + { + "unit_label": "10^9/L", + "unit_value": 2, + "unit_field_name": "labs_neutrophil_109l" + } ], "preferred_unit": "%", "conversion_rules": [ @@ -229,8 +325,16 @@ "field_name": "labs_lymphocyte", "units_field_name": "labs_lymphocyte_units", "units": [ - { "unit_label": "%", "unit_value": 1, "unit_field_name": "labs_lymphocyte_pcnt" }, - { "unit_label": "10^9/L", "unit_value": 2, "unit_field_name": "labs_lymphocyte_109l" } + { + "unit_label": "%", + "unit_value": 1, + "unit_field_name": "labs_lymphocyte_pcnt" + }, + { + "unit_label": "10^9/L", + "unit_value": 2, + "unit_field_name": "labs_lymphocyte_109l" + } ], "preferred_unit": "%", "conversion_rules": [ @@ -249,8 +353,16 @@ "field_name": "labs_eosinophil", "units_field_name": "labs_eosinophil_units", "units": [ - { "unit_label": "%", "unit_value": 1, "unit_field_name": "labs_eosinophil_pcnt" }, - { "unit_label": "10^9/L", "unit_value": 2, "unit_field_name": "labs_eosinophil_109l" } + { + "unit_label": "%", + "unit_value": 1, + "unit_field_name": "labs_eosinophil_pcnt" + }, + { + "unit_label": "10^9/L", + "unit_value": 2, + "unit_field_name": "labs_eosinophil_109l" + } ], "preferred_unit": "%", "conversion_rules": [ @@ -269,8 +381,16 @@ "field_name": "labs_hematocrit", "units_field_name": "labs_hematocrit_units", "units": [ - { "unit_label": "%", "unit_value": 1, "unit_field_name": "labs_hematocrit_pcnt" }, - { "unit_label": "L/L", "unit_value": 2, "unit_field_name": "labs_hematocrit_ll" } + { + "unit_label": "%", + "unit_value": 1, + "unit_field_name": "labs_hematocrit_pcnt" + }, + { + "unit_label": "L/L", + "unit_value": 2, + "unit_field_name": "labs_hematocrit_ll" + } ], "preferred_unit": "%", "conversion_rules": [ @@ -288,8 +408,16 @@ "field_name": "labs_platelets", "units_field_name": "labs_platelets_units", "units": [ - { "unit_label": "10^9/L", "unit_value": 1, "unit_field_name": "labs_platelets_109l" }, - { "unit_label": "10^3/µL", "unit_value": 2, "unit_field_name": "labs_platelets_103ul" } + { + "unit_label": "10^9/L", + "unit_value": 1, + "unit_field_name": "labs_platelets_109l" + }, + { + "unit_label": "10^3/µL", + "unit_value": 2, + "unit_field_name": "labs_platelets_103ul" + } ], "preferred_unit": "10^9/L", "conversion_rules": [ @@ -307,8 +435,16 @@ "field_name": "labs_erymeancorhgb", "units_field_name": "labs_erymeancorhgb_units", "units": [ - { "unit_label": "g/L", "unit_value": 1, "unit_field_name": "labs_erymeancorhgb_gl" }, - { "unit_label": "g/dL", "unit_value": 2, "unit_field_name": "labs_erymeancorhgb_gdl" } + { + "unit_label": "g/L", + "unit_value": 1, + "unit_field_name": "labs_erymeancorhgb_gl" + }, + { + "unit_label": "g/dL", + "unit_value": 2, + "unit_field_name": "labs_erymeancorhgb_gdl" + } ], "preferred_unit": "g/L", "conversion_rules": [ @@ -326,8 +462,16 @@ "field_name": "labs_fibrinogen", "units_field_name": "labs_fibrinogen_units", "units": [ - { "unit_label": "g/L", "unit_value": 1, "unit_field_name": "labs_fibrinogen_gl" }, - { "unit_label": "mg/dL", "unit_value": 2, "unit_field_name": "labs_fibrinogen_mgdl" } + { + "unit_label": "g/L", + "unit_value": 1, + "unit_field_name": "labs_fibrinogen_gl" + }, + { + "unit_label": "mg/dL", + "unit_value": 2, + "unit_field_name": "labs_fibrinogen_mgdl" + } ], "preferred_unit": "g/L", "conversion_rules": [ @@ -345,8 +489,16 @@ "field_name": "labs_ddimer", "units_field_name": "labs_ddimer_units", "units": [ - { "unit_label": "µg/L", "unit_value": 1, "unit_field_name": "labs_ddimer_ugl" }, - { "unit_label": "mg/L", "unit_value": 2, "unit_field_name": "labs_ddimer_mgl" } + { + "unit_label": "µg/L", + "unit_value": 1, + "unit_field_name": "labs_ddimer_ugl" + }, + { + "unit_label": "mg/L", + "unit_value": 2, + "unit_field_name": "labs_ddimer_mgl" + } ], "preferred_unit": "µg/L", "conversion_rules": [ @@ -364,8 +516,16 @@ "field_name": "labs_bilirubin", "units_field_name": "labs_bilirubin_units", "units": [ - { "unit_label": "µmol/L", "unit_value": 1, "unit_field_name": "labs_bilirubin_umoll" }, - { "unit_label": "mg/dL", "unit_value": 2, "unit_field_name": "labs_bilirubin_mgdl" } + { + "unit_label": "µmol/L", + "unit_value": 1, + "unit_field_name": "labs_bilirubin_umoll" + }, + { + "unit_label": "mg/dL", + "unit_value": 2, + "unit_field_name": "labs_bilirubin_mgdl" + } ], "preferred_unit": "µmol/L", "conversion_rules": [ @@ -383,8 +543,16 @@ "field_name": "labs_albumin", "units_field_name": "labs_albumin_units", "units": [ - { "unit_label": "g/L", "unit_value": 1, "unit_field_name": "labs_albumin_gl" }, - { "unit_label": "mmol/L", "unit_value": 2, "unit_field_name": "labs_albumin_mmoll" } + { + "unit_label": "g/L", + "unit_value": 1, + "unit_field_name": "labs_albumin_gl" + }, + { + "unit_label": "mmol/L", + "unit_value": 2, + "unit_field_name": "labs_albumin_mmoll" + } ], "preferred_unit": "g/L", "conversion_rules": [ @@ -402,9 +570,21 @@ "field_name": "labs_glucose", "units_field_name": "labs_glucose_units", "units": [ - { "unit_label": "mmol/L", "unit_value": 1, "unit_field_name": "labs_glucose_mmoll" }, - { "unit_label": "mg/dL", "unit_value": 2, "unit_field_name": "labs_glucose_mgdl" }, - { "unit_label": "g/L", "unit_value": 3, "unit_field_name": "labs_glucose_gl" } + { + "unit_label": "mmol/L", + "unit_value": 1, + "unit_field_name": "labs_glucose_mmoll" + }, + { + "unit_label": "mg/dL", + "unit_value": 2, + "unit_field_name": "labs_glucose_mgdl" + }, + { + "unit_label": "g/L", + "unit_value": 3, + "unit_field_name": "labs_glucose_gl" + } ], "preferred_unit": "mmol/L", "conversion_rules": [ @@ -430,8 +610,16 @@ "field_name": "labs_ureanitro", "units_field_name": "labs_ureanitro_units", "units": [ - { "unit_label": "mg/dL", "unit_value": 1, "unit_field_name": "labs_ureanitro_mgdl" }, - { "unit_label": "mmol/L", "unit_value": 2, "unit_field_name": "labs_ureanitro_mmoll" } + { + "unit_label": "mg/dL", + "unit_value": 1, + "unit_field_name": "labs_ureanitro_mgdl" + }, + { + "unit_label": "mmol/L", + "unit_value": 2, + "unit_field_name": "labs_ureanitro_mmoll" + } ], "preferred_unit": "mg/dL", "conversion_rules": [ @@ -449,8 +637,16 @@ "field_name": "labs_creatinine", "units_field_name": "labs_creatinine_units", "units": [ - { "unit_label": "µmol/L", "unit_value": 1, "unit_field_name": "labs_creatinine_umoll" }, - { "unit_label": "mg/dL", "unit_value": 2, "unit_field_name": "labs_creatinine_mgdl" } + { + "unit_label": "µmol/L", + "unit_value": 1, + "unit_field_name": "labs_creatinine_umoll" + }, + { + "unit_label": "mg/dL", + "unit_value": 2, + "unit_field_name": "labs_creatinine_mgdl" + } ], "preferred_unit": "µmol/L", "conversion_rules": [ @@ -468,9 +664,21 @@ "field_name": "labs_calcium", "units_field_name": "labs_calcium_units", "units": [ - { "unit_label": "mmol/L", "unit_value": 1, "unit_field_name": "labs_calcium_mmoll" }, - { "unit_label": "mEq/L", "unit_value": 2, "unit_field_name": "labs_calcium_meql" }, - { "unit_label": "mg/dL", "unit_value": 3, "unit_field_name": "labs_calcium_mgdl" } + { + "unit_label": "mmol/L", + "unit_value": 1, + "unit_field_name": "labs_calcium_mmoll" + }, + { + "unit_label": "mEq/L", + "unit_value": 2, + "unit_field_name": "labs_calcium_meql" + }, + { + "unit_label": "mg/dL", + "unit_value": 3, + "unit_field_name": "labs_calcium_mgdl" + } ], "preferred_unit": "mmol/L", "conversion_rules": [ @@ -496,8 +704,16 @@ "field_name": "labs_chlor", "units_field_name": "labs_chlor_units", "units": [ - { "unit_label": "mmol/L", "unit_value": 1, "unit_field_name": "labs_chlor_mmoll" }, - { "unit_label": "mg/dL", "unit_value": 2, "unit_field_name": "labs_chlor_mgdl" } + { + "unit_label": "mmol/L", + "unit_value": 1, + "unit_field_name": "labs_chlor_mmoll" + }, + { + "unit_label": "mg/dL", + "unit_value": 2, + "unit_field_name": "labs_chlor_mgdl" + } ], "preferred_unit": "mmol/L", "conversion_rules": [ @@ -515,8 +731,16 @@ "field_name": "labs_magn", "units_field_name": "labs_magn_units", "units": [ - { "unit_label": "mmol/L", "unit_value": 1, "unit_field_name": "labs_magn_mmoll" }, - { "unit_label": "mg/dL", "unit_value": 2, "unit_field_name": "labs_magn_mgdl" } + { + "unit_label": "mmol/L", + "unit_value": 1, + "unit_field_name": "labs_magn_mmoll" + }, + { + "unit_label": "mg/dL", + "unit_value": 2, + "unit_field_name": "labs_magn_mgdl" + } ], "preferred_unit": "mmol/L", "conversion_rules": [ @@ -534,8 +758,16 @@ "field_name": "labs_procalcito", "units_field_name": "labs_procalcito_units", "units": [ - { "unit_label": "ng/mL", "unit_value": 1, "unit_field_name": "labs_procalcito_ngml" }, - { "unit_label": "µg/L", "unit_value": 2, "unit_field_name": "labs_procalcito_ugl" } + { + "unit_label": "ng/mL", + "unit_value": 1, + "unit_field_name": "labs_procalcito_ngml" + }, + { + "unit_label": "µg/L", + "unit_value": 2, + "unit_field_name": "labs_procalcito_ugl" + } ], "preferred_unit": "ng/mL", "conversion_rules": [ @@ -553,8 +785,16 @@ "field_name": "labs_crp", "units_field_name": "labs_crp_units", "units": [ - { "unit_label": "mg/L", "unit_value": 1, "unit_field_name": "labs_crp_mgl" }, - { "unit_label": "mg/dL", "unit_value": 2, "unit_field_name": "labs_crp_mgdl" } + { + "unit_label": "mg/L", + "unit_value": 1, + "unit_field_name": "labs_crp_mgl" + }, + { + "unit_label": "mg/dL", + "unit_value": 2, + "unit_field_name": "labs_crp_mgdl" + } ], "preferred_unit": "mg/L", "conversion_rules": [ @@ -572,8 +812,16 @@ "field_name": "labs_lactate", "units_field_name": "labs_lactate_units", "units": [ - { "unit_label": "mmol/L", "unit_value": 1, "unit_field_name": "labs_lactate_mmoll" }, - { "unit_label": "mg/dL", "unit_value": 2, "unit_field_name": "labs_lactate_mgdl" } + { + "unit_label": "mmol/L", + "unit_value": 1, + "unit_field_name": "labs_lactate_mmoll" + }, + { + "unit_label": "mg/dL", + "unit_value": 2, + "unit_field_name": "labs_lactate_mgdl" + } ], "preferred_unit": "mmol/L", "conversion_rules": [ @@ -591,8 +839,16 @@ "field_name": "labs_paco2", "units_field_name": "labs_paco2_units", "units": [ - { "unit_label": "mmHg", "unit_value": 1, "unit_field_name": "labs_paco2_mmhg" }, - { "unit_label": "kPa", "unit_value": 2, "unit_field_name": "labs_paco2_kpa" } + { + "unit_label": "mmHg", + "unit_value": 1, + "unit_field_name": "labs_paco2_mmhg" + }, + { + "unit_label": "kPa", + "unit_value": 2, + "unit_field_name": "labs_paco2_kpa" + } ], "preferred_unit": "mmHg", "conversion_rules": [ @@ -610,9 +866,21 @@ "field_name": "labs_cd4", "units_field_name": "labs_cd4_units", "units": [ - { "unit_label": "cells/mm^3", "unit_value": 1, "unit_field_name": "labs_cd4_celmm" }, - { "unit_label": "10^6 cells/L", "unit_value": 2, "unit_field_name": "labs_cd4_106cell" }, - { "unit_label": "cells/µL", "unit_value": 3, "unit_field_name": "labs_cd4_celul" } + { + "unit_label": "cells/mm^3", + "unit_value": 1, + "unit_field_name": "labs_cd4_celmm" + }, + { + "unit_label": "10^6 cells/L", + "unit_value": 2, + "unit_field_name": "labs_cd4_106cell" + }, + { + "unit_label": "cells/µL", + "unit_value": 3, + "unit_field_name": "labs_cd4_celul" + } ], "preferred_unit": "cells/mm^3", "conversion_rules": [ @@ -638,8 +906,16 @@ "field_name": "labs_tropi", "units_field_name": "labs_tropi_units", "units": [ - { "unit_label": "ng/L", "unit_value": 1, "unit_field_name": "labs_tropi_ngl" }, - { "unit_label": "ng/mL", "unit_value": 2, "unit_field_name": "labs_tropi_ngml" } + { + "unit_label": "ng/L", + "unit_value": 1, + "unit_field_name": "labs_tropi_ngl" + }, + { + "unit_label": "ng/mL", + "unit_value": 2, + "unit_field_name": "labs_tropi_ngml" + } ], "preferred_unit": "ng/L", "conversion_rules": [ @@ -657,8 +933,16 @@ "field_name": "labs_protein", "units_field_name": "labs_protein_units", "units": [ - { "unit_label": "g/L", "unit_value": 1, "unit_field_name": "labs_protein_gl" }, - { "unit_label": "g/dL", "unit_value": 2, "unit_field_name": "labs_protein_gdl" } + { + "unit_label": "g/L", + "unit_value": 1, + "unit_field_name": "labs_protein_gl" + }, + { + "unit_label": "g/dL", + "unit_value": 2, + "unit_field_name": "labs_protein_gdl" + } ], "preferred_unit": "g/L", "conversion_rules": [ @@ -676,8 +960,16 @@ "field_name": "nborn_birthweight", "units_field_name": "nborn_birthweight_units", "units": [ - { "unit_label": "g", "unit_value": 1, "unit_field_name": "nborn_birthweight_g" }, - { "unit_label": "lb", "unit_value": 2, "unit_field_name": "nborn_birthweight_lb" } + { + "unit_label": "g", + "unit_value": 1, + "unit_field_name": "nborn_birthweight_g" + }, + { + "unit_label": "lb", + "unit_value": 2, + "unit_field_name": "nborn_birthweight_lb" + } ], "preferred_unit": "g", "conversion_rules": [ @@ -695,8 +987,16 @@ "field_name": "nborn_length", "units_field_name": "nborn_length_units", "units": [ - { "unit_label": "cm", "unit_value": 1, "unit_field_name": "nborn_length_cm" }, - { "unit_label": "in", "unit_value": 2, "unit_field_name": "nborn_length_in" } + { + "unit_label": "cm", + "unit_value": 1, + "unit_field_name": "nborn_length_cm" + }, + { + "unit_label": "in", + "unit_value": 2, + "unit_field_name": "nborn_length_in" + } ], "preferred_unit": "cm", "conversion_rules": [ @@ -714,8 +1014,16 @@ "field_name": "nborn_infhc", "units_field_name": "nborn_infhc_units", "units": [ - { "unit_label": "cm", "unit_value": 1, "unit_field_name": "nborn_infhc_cm" }, - { "unit_label": "in", "unit_value": 2, "unit_field_name": "nborn_infhc_in" } + { + "unit_label": "cm", + "unit_value": 1, + "unit_field_name": "nborn_infhc_cm" + }, + { + "unit_label": "in", + "unit_value": 2, + "unit_field_name": "nborn_infhc_in" + } ], "preferred_unit": "cm", "conversion_rules": [ @@ -733,8 +1041,16 @@ "field_name": "nborn_deathage", "units_field_name": "nborn_deathage_units", "units": [ - { "unit_label": "Days", "unit_value": 1, "unit_field_name": "nborn_deathage_days" }, - { "unit_label": "Months", "unit_value": 2, "unit_field_name": "nborn_deathage_months" } + { + "unit_label": "Days", + "unit_value": 1, + "unit_field_name": "nborn_deathage_days" + }, + { + "unit_label": "Months", + "unit_value": 2, + "unit_field_name": "nborn_deathage_months" + } ], "preferred_unit": "Days", "conversion_rules": [ @@ -748,4 +1064,4 @@ } ] } -] +] \ No newline at end of file