The pointblank package includes a built-in CDISC conformance engine that can validate SDTM datasets against the SDTMIG rule catalog. This engine runs entirely within R, so no external tools or API calls are needed at runtime. The CDISC CORE rules and SDTM Controlled Terminology are bundled as JSON files inside the package, so validation works offline and out of the box.
The conformance engine evaluates 426 rules from the SDTMIG v3.4 specification. These cover per-row value checks (e.g., codelist membership, ISO 8601 date formats), structural checks (required variables, column ordering), and domain-level requirements (required domains like DM, TS, and TA). Each rule traces back to an official CDISC CORE rule identifier, making it straightforward to cross-reference findings with published CDISC guidance.
Quick Start
The main entry point is validate_sdtmig(). It takes a
named list of data frames (one per SDTM domain) and returns a
cdisc_conformance_result object. The names in the list
correspond to domain abbreviations (e.g., "DM",
"AE", "LB") and are matched
case-insensitively.
Let’s set up a small Demographics (DM) domain with a
couple of deliberate problems. The third subject has an invalid
SEX value ("INVALID" is not in the CDISC
controlled terminology for SEX), and the second subject has a malformed
reference start date ("not-a-date" is not a valid ISO 8601
datetime string).
dm <-
dplyr::tibble(
STUDYID = rep("STUDY01", 3),
DOMAIN = rep("DM", 3),
USUBJID = c("STUDY01-001", "STUDY01-002", "STUDY01-003"),
SUBJID = c("001", "002", "003"),
SEX = c("M", "F", "INVALID"),
AGE = c(45, 62, 38),
RFSTDTC = c("2024-01-15", "not-a-date", "2024-03"),
ARMCD = c("TRT", "PBO", "TRT"),
ARM = c("Treatment", "Placebo", "Treatment"),
ACTARMCD = c("TRT", "PBO", "TRT"),
ACTARM = c("Treatment", "Placebo", "Treatment"),
COUNTRY = c("USA", "GBR", "CAN")
)Now run the validation. The engine loads the bundled rule catalog and controlled terminology automatically:
result <- validate_sdtmig(list(DM = dm))
result## <cdisc_conformance_result> SDTMIG 3-4
## 426 rules (fail=7, not_applicable=12, pass=407)
## 7 issues -- FAIL
The printed summary tells us how many of the 426 rules passed, failed, or were not applicable (rules that require inputs we didn’t provide, like Define-XML metadata or additional domains). In this case the engine flagged several issues: the two data-quality problems we planted, plus a few structural findings (missing required variables and domains that a complete submission would include).
Inspecting Results
The conformance result object has several accessor functions that make it easy to drill into the details using familiar tidyverse patterns.
Overall status
The cdisc_all_passed() function gives a single logical
value: did every rule pass without findings?
cdisc_all_passed(result)## [1] FALSE
For a more granular view, cdisc_status_counts() breaks
down the rules by status. The five possible statuses are
pass, fail, error (rule
encountered an unexpected problem), not_applicable
(required inputs missing), and not_supported (rule type not
yet implemented).
cdisc_status_counts(result)## statuses
## fail not_applicable pass
## 7 12 407
Issues summary
The cdisc_issues() function returns a data frame
containing only the rules that found problems. Each row represents one
failing rule with its ID, the affected dataset, the issue count,
severity, and message. This is the best starting point for understanding
what went wrong at a glance.
cdisc_issues(result) |>
dplyr::select(rule_id, dataset, n_issues, sensitivity, message)## rule_id dataset n_issues sensitivity
## 1 SDTM-007 DM 1 Error
## 2 SDTM-011 DM 1 Error
## 3 SDTM-032 DM 1 Error
## 4 SDTM-033 DM 1 Error
## 5 SDTM-034 DM 1 Error
## 6 SDTM-121 TS 1 Error
## 7 SDTM-122 TA 1 Warning
## message
## 1 SEX value is not in the SEX codelist.
## 2 RFSTDTC does not conform to ISO 8601 extended datetime format.
## 3 DM domain is missing one or more required demographic variables (SEX, RACE, ETHNIC, COUNTRY).
## 4 DM domain must include RFSTDTC (Subject Reference Start Date/Time) and RFENDTC (Subject Reference End Date/Time).
## 5 DM domain must include SITEID, AGE, and AGEU.
## 6 Required domain(s) missing: TS
## 7 Required domain(s) missing: TA
Notice that the issues include both the data-quality problems we introduced (SDTM-007 for the invalid SEX value, SDTM-011 for the malformed date) and structural findings (SDTM-032 through SDTM-034 for missing DM variables, SDTM-121 and SDTM-122 for the required TS and TA domains that a complete submission would contain).
Row-level findings
For rules that perform per-row checks (like
RECORD_CHECK), the engine captures detailed findings that
pinpoint exactly which subject, row, and column triggered each
violation. The cdisc_findings_df() function flattens these
into a tidy data frame:
cdisc_findings_df(result) |>
dplyr::select(rule_id, dataset, row, usubjid, checked_column, checked_value)## rule_id dataset row usubjid checked_column checked_value
## 1 SDTM-007 DM 3 STUDY01-003 SEX INVALID
## 2 SDTM-011 DM 2 STUDY01-002 RFSTDTC not-a-date
The row column uses 1-based indexing (following R
convention), so you can look up the offending row directly with
dm[row, ]. The checked_column and
checked_value fields tell you exactly which variable failed
and what value it contained, making it straightforward to trace the
problem back to source data.
Working with Multiple Domains
Real SDTM submissions contain many domains. Pass them all as entries in the named list and the engine will evaluate each rule against its applicable domain(s). Rules that check for domain presence or cross-domain consistency will also run as appropriate.
Here we add a small Adverse Events (AE) domain alongside
the DM:
ae <-
dplyr::tibble(
STUDYID = rep("STUDY01", 2),
DOMAIN = rep("AE", 2),
USUBJID = c("STUDY01-001", "STUDY01-002"),
AESEQ = c(1, 1),
AETERM = c("Headache", "Nausea"),
AEDECOD = c("Headache", "Nausea"),
AESTDTC = c("2024-02-01", "2024-02-15"),
AEENDTC = c("2024-02-03", "2024-02-17")
)
result_multi <- validate_sdtmig(list(DM = dm, AE = ae))
result_multi## <cdisc_conformance_result> SDTMIG 3-4
## 426 rules (fail=11, not_applicable=12, pass=403)
## 11 issues -- FAIL
With the AE domain present, rules that apply specifically to adverse
event data now run as well. The total rule count stays at 426 (the full
catalog is always loaded), but more rules will produce pass
or fail results rather than being skipped for lack of input
data.
We can quickly compare which rules failed across the two runs:
cdisc_issues(result_multi) |>
dplyr::select(rule_id, dataset, n_issues, message)## rule_id dataset n_issues
## 1 SDTM-007 DM 1
## 2 SDTM-011 DM 1
## 3 SDTM-032 DM 1
## 4 SDTM-033 DM 1
## 5 SDTM-034 DM 1
## 6 SDTM-037 AE 1
## 7 SDTM-038 AE 1
## 8 SDTM-121 TS 1
## 9 SDTM-122 TA 1
## 10 SDTM-162 AE 1
## 11 SDTM-289 AE 1
## message
## 1 SEX value is not in the SEX codelist.
## 2 RFSTDTC does not conform to ISO 8601 extended datetime format.
## 3 DM domain is missing one or more required demographic variables (SEX, RACE, ETHNIC, COUNTRY).
## 4 DM domain must include RFSTDTC (Subject Reference Start Date/Time) and RFENDTC (Subject Reference End Date/Time).
## 5 DM domain must include SITEID, AGE, and AGEU.
## 6 AE domain must include AETERM, AEDECOD (MedDRA preferred term), and AEBODSYS (body system).
## 7 AE domain must include AESEV (severity), AESER (serious flag), and AEOUT (outcome).
## 8 Required domain(s) missing: TS
## 9 Required domain(s) missing: TA
## 10 VISITNUM must be present in AE.
## 11 AESER must be present in AE.
Filtering by Rule Type
Sometimes you only want to focus on a particular category of checks.
The rule_types argument lets you restrict which rule types
are loaded and evaluated. This can be useful during iterative data
cleaning, where you might want to fix all per-row value issues before
moving on to structural checks.
result_records <-
validate_sdtmig(
list(DM = dm),
rule_types = "RECORD_CHECK"
)
result_records## <cdisc_conformance_result> SDTMIG 3-4
## 271 rules (fail=2, pass=269)
## 2 issues -- FAIL
The engine supports seven rule types. The first five are fully implemented and the last two are stubs that will become active when a Define-XML importer is added to the R package:
| Rule type | What it checks |
|---|---|
RECORD_CHECK |
Per-row value validation (codelist membership, date formats, consistency) |
VARIABLE_METADATA_CHECK |
Column presence and ordering within a domain |
DATASET_METADATA_CHECK |
Dataset-level attributes (sort keys, required structure) |
DATASET_CONTENTS_CHECK |
Dataset-level value constraints evaluated row-by-row |
DOMAIN_PRESENCE_CHECK |
Whether required domains are present or prohibited ones are absent |
DEFINE_ITEM_METADATA_CHECK |
Variable declarations against Define-XML (stub) |
DEFINE_CODELIST_CHECK |
Codelist values against Define-XML declarations (stub) |
What’s in the Rule Catalog?
The bundled catalog for SDTMIG v3.4 contains 426 rules translated from the CDISC CORE rule definitions. Each rule consists of:
- A core_id (e.g.,
"SDTM-007") that links back to the official CDISC CORE rule identifier, making it easy to cross-reference with published CDISC documentation and the CORE Rules Engine output. - A rule_type that determines how the rule is dispatched by the engine (record check, metadata check, etc.).
- A set of operations that compute intermediate columns before evaluation. These handle things like codelist lookups, ISO 8601 format validation, data type verification, column presence checks, and variable ordering.
- A conditions tree (a nested structure of AND/OR/NOT boolean expressions) that defines when a violation is flagged. Conditions reference the computed columns added by operations.
- A sensitivity level (
Error,Warning, orNotice) indicating the severity of a finding per CDISC guidance.
Controlled Terminology
CDISC publishes Controlled Terminology (CT) packages that define the
permitted values for coded variables like SEX,
RACE, COUNTRY, and many others. The engine
ships with a bundled SDTM CT package (sdtm-ct-2024-09-27)
that is loaded automatically when you call
validate_sdtmig().
Codelist validation is case-insensitive: a value of "m"
will match the codelist term "M". Missing values
(NA) and empty strings are always treated as valid for
codelist purposes (null-handling is covered by separate not-null
rules).
If additional CT packages are bundled in the future (e.g., newer
terminology releases or ADaM CT), you can select them explicitly with
the ct_packages argument:
result <-
validate_sdtmig(
list(DM = dm),
ct_packages = "sdtm-ct-2024-09-27"
)Current Limitations
There are a few areas where the R conformance engine differs from the full CDISC CORE Rules Engine or has functionality that is not yet implemented:
Define-XML support: The four Define-XML-aware operations (
define_var_declared,define_required_check,define_codelist_check,define_type_check) are currently stubs that always pass. They will produce real results once a Define-XML importer is added to the R package. In the meantime, rules that depend on Define-XML metadata will simply not flag any issues (they won’t produce false positives).SDTMIG version coverage: Only SDTMIG v3.4 is currently bundled. Additional standard/version combinations can be added by running the catalog generation script from the Python pointblank package and copying the resulting JSON file to the
inst/conformance/rules/directory.JSONata expressions: A small number of advanced CORE rules use JSONata expressions for complex cross-dataset logic. The JSONata evaluator is not yet ported to R. Rules that depend on it will return
not_supported. This affects a very small fraction of the catalog.