From bc6f1b401fdd1ecc4e563245487dbcfb7a614bc6 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Wed, 14 Oct 2026 21:01:58 -0400 Subject: [PATCH 1/2] Refine SDTM and ADaM variable labels --- pointblank/metadata/_adam_templates.py | 28 ++++++++++++++------------ pointblank/metadata/_sdtm_templates.py | 12 ++++++----- 2 files changed, 22 insertions(+), 18 deletions(-) diff --git a/pointblank/metadata/_adam_templates.py b/pointblank/metadata/_adam_templates.py index a67897a8c..434643097 100644 --- a/pointblank/metadata/_adam_templates.py +++ b/pointblank/metadata/_adam_templates.py @@ -341,7 +341,7 @@ def _adsl_template() -> ADaMDatasetTemplate: ), ADaMVariableSpec( "DCTREAS", - "Reason for Discontinuation from Treatment", + "Reason for Discontinuation of Treatment", "Char", core="Perm", max_length=200, @@ -475,7 +475,7 @@ def _bds_template() -> ADaMDatasetTemplate: ), ADaMVariableSpec( "ANL01FL", - "Analysis Record Flag 01", + "Analysis Flag 01", "Char", core="Perm", max_length=1, @@ -490,9 +490,9 @@ def _bds_template() -> ADaMDatasetTemplate: controlled_term="NY", ), # ── Traceability ── - ADaMVariableSpec("SRCDOM", "Source Data Domain", "Char", core="Perm", max_length=8), - ADaMVariableSpec("SRCVAR", "Source Data Variable", "Char", core="Perm", max_length=40), - ADaMVariableSpec("SRCSEQ", "Source Data Sequence Number", "Num", core="Perm"), + ADaMVariableSpec("SRCDOM", "Source Data", "Char", core="Perm", max_length=8), + ADaMVariableSpec("SRCVAR", "Source Variable", "Char", core="Perm", max_length=40), + ADaMVariableSpec("SRCSEQ", "Source Sequence Number", "Num", core="Perm"), # ── Criterion Variables ── ADaMVariableSpec("CRIT1", "Analysis Criterion 1", "Char", core="Perm", max_length=200), ADaMVariableSpec( @@ -639,12 +639,14 @@ def _adae_template() -> ADaMDatasetTemplate: ADaMVariableSpec("AENDTM", "Analysis End Datetime", "Num", core="Perm"), ADaMVariableSpec("ASTDY", "Analysis Start Relative Day", "Num", core="Perm"), ADaMVariableSpec("AENDY", "Analysis End Relative Day", "Num", core="Perm"), - ADaMVariableSpec("ADURN", "AE Duration (N)", "Num", core="Perm"), - ADaMVariableSpec("ADURU", "AE Duration Units", "Char", core="Perm", max_length=40), + ADaMVariableSpec("ADURN", "Analysis Duration (N)", "Num", core="Perm"), + ADaMVariableSpec( + "ADURU", "Analysis Duration Units", "Char", core="Perm", max_length=40 + ), # ── Flags ── ADaMVariableSpec( "TRTEMFL", - "Treatment Emergent Flag", + "Treatment Emergent Analysis Flag", "Char", core="Cond", max_length=1, @@ -721,13 +723,13 @@ def _adtte_template() -> ADaMDatasetTemplate: # ── Time-to-Event Variables ── ADaMVariableSpec("AVAL", "Analysis Value", "Num", required=True, core="Req"), ADaMVariableSpec( - "STARTDT", "Time-to-Event Origin Date", "Num", required=True, core="Req" + "STARTDT", "Time-to-Event Origin Date for Subject", "Num", required=True, core="Req" ), ADaMVariableSpec("ADT", "Analysis Date", "Num", required=True, core="Req"), ADaMVariableSpec("CNSR", "Censor", "Num", required=True, core="Req"), ADaMVariableSpec( "EVNTDESC", - "Event Description", + "Event or Censoring Description", "Char", core="Cond", max_length=200, @@ -742,9 +744,9 @@ def _adtte_template() -> ADaMDatasetTemplate: condition="Required for traceability", ), # ── Supporting Variables ── - ADaMVariableSpec("SRCDOM", "Source Data Domain", "Char", core="Perm", max_length=8), - ADaMVariableSpec("SRCVAR", "Source Data Variable", "Char", core="Perm", max_length=40), - ADaMVariableSpec("SRCSEQ", "Source Data Sequence Number", "Num", core="Perm"), + ADaMVariableSpec("SRCDOM", "Source Data", "Char", core="Perm", max_length=8), + ADaMVariableSpec("SRCVAR", "Source Variable", "Char", core="Perm", max_length=40), + ADaMVariableSpec("SRCSEQ", "Source Sequence Number", "Num", core="Perm"), ], ) diff --git a/pointblank/metadata/_sdtm_templates.py b/pointblank/metadata/_sdtm_templates.py index 8501c6ab6..989f028ad 100644 --- a/pointblank/metadata/_sdtm_templates.py +++ b/pointblank/metadata/_sdtm_templates.py @@ -617,7 +617,7 @@ def _lb_template() -> SDTMDomainTemplate: ), SDTMVariableSpec( "LBORNRLO", - "Reference Range Lower Limit-Orig Unit", + "Reference Range Lower Limit in Orig Unit", "Char", "Qualifier", max_length=40, @@ -625,7 +625,7 @@ def _lb_template() -> SDTMDomainTemplate: ), SDTMVariableSpec( "LBORNRHI", - "Reference Range Upper Limit-Orig Unit", + "Reference Range Upper Limit in Orig Unit", "Char", "Qualifier", max_length=40, @@ -1058,7 +1058,9 @@ def _ds_template() -> SDTMDomainTemplate: max_length=64, core="Exp", ), - SDTMVariableSpec("DSSTDY", "Study Day of Start of Event", "Num", "Timing", core="Perm"), + SDTMVariableSpec( + "DSSTDY", "Study Day of Start of Disposition Event", "Num", "Timing", core="Perm" + ), ], ) @@ -1154,7 +1156,7 @@ def _mh_template() -> SDTMDomainTemplate: ), SDTMVariableSpec( "MHSTDTC", - "Start Date/Time of Medical History", + "Start Date/Time of Medical History Event", "Char", "Timing", max_length=64, @@ -1162,7 +1164,7 @@ def _mh_template() -> SDTMDomainTemplate: ), SDTMVariableSpec( "MHENDTC", - "End Date/Time of Medical History", + "End Date/Time of Medical History Event", "Char", "Timing", max_length=64, From b13e024facd20c495322af21da489f93eee67de7 Mon Sep 17 00:00:00 2001 From: Richard Iannone Date: Wed, 14 Oct 2026 21:02:09 -0400 Subject: [PATCH 2/2] Test metadata template limits and IG labels --- tests/test_metadata_template_labels.py | 62 ++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) create mode 100644 tests/test_metadata_template_labels.py diff --git a/tests/test_metadata_template_labels.py b/tests/test_metadata_template_labels.py new file mode 100644 index 000000000..f17f8ef56 --- /dev/null +++ b/tests/test_metadata_template_labels.py @@ -0,0 +1,62 @@ +""" +SDTM and ADaM templates must fit SAS transport (XPT v5) limits and use the IG wording. + +XPT v5, the format required for FDA submissions, caps variable names at 8 characters and +variable and dataset labels at 40 characters. +""" + +from __future__ import annotations + +import pytest + +from pointblank.metadata._adam_templates import get_adam_dataset, list_adam_datasets +from pointblank.metadata._sdtm_templates import get_sdtm_domain, list_sdtm_domains + +TEMPLATES = [("SDTM", d, get_sdtm_domain(d)) for d in list_sdtm_domains()] + [ + ("ADaM", d, get_adam_dataset(d)) for d in list_adam_datasets() +] + + +@pytest.mark.parametrize( + "template", [t for _, _, t in TEMPLATES], ids=[f"{s}-{d}" for s, d, _ in TEMPLATES] +) +def test_template_fits_sas_transport_limits(template): + name = getattr(template, "domain", None) or template.name + assert len(name) <= 8 + assert len(template.label) <= 40, template.label + + too_long = [ + f"{v.name}: {v.label!r} ({len(v.label)})" for v in template.variables if len(v.label) > 40 + ] + assert too_long == [] + assert [v.name for v in template.variables if len(v.name) > 8] == [] + + +@pytest.mark.parametrize( + "get, dataset, variable, label", + [ + # SDTMIG 3.4 + (get_sdtm_domain, "DS", "DSSTDY", "Study Day of Start of Disposition Event"), + (get_sdtm_domain, "MH", "MHSTDTC", "Start Date/Time of Medical History Event"), + (get_sdtm_domain, "MH", "MHENDTC", "End Date/Time of Medical History Event"), + (get_sdtm_domain, "LB", "LBORNRLO", "Reference Range Lower Limit in Orig Unit"), + (get_sdtm_domain, "LB", "LBORNRHI", "Reference Range Upper Limit in Orig Unit"), + # ADaMIG 1.3 + (get_adam_dataset, "ADSL", "DCTREAS", "Reason for Discontinuation of Treatment"), + (get_adam_dataset, "BDS", "ANL01FL", "Analysis Flag 01"), + (get_adam_dataset, "BDS", "SRCDOM", "Source Data"), + (get_adam_dataset, "BDS", "SRCVAR", "Source Variable"), + (get_adam_dataset, "BDS", "SRCSEQ", "Source Sequence Number"), + # ADaM OCCDS + (get_adam_dataset, "ADAE", "ADURN", "Analysis Duration (N)"), + (get_adam_dataset, "ADAE", "ADURU", "Analysis Duration Units"), + (get_adam_dataset, "ADAE", "TRTEMFL", "Treatment Emergent Analysis Flag"), + # ADaM BDS for time-to-event + (get_adam_dataset, "ADTTE", "STARTDT", "Time-to-Event Origin Date for Subject"), + (get_adam_dataset, "ADTTE", "EVNTDESC", "Event or Censoring Description"), + (get_adam_dataset, "ADTTE", "SRCDOM", "Source Data"), + ], +) +def test_template_labels_use_ig_wording(get, dataset, variable, label): + spec = next(v for v in get(dataset).variables if v.name == variable) + assert spec.label == label