CDISC Data Import
Comprehensive guide for importing CDISC/SDTM and ADaM formatted data in Python.
Overview
The neopkpd.data module provides utilities for importing data from CDISC (Clinical Data Interchange Standards Consortium) formats.
from neopkpd.data import import_cdisc
data = import_cdisc(
pc_path="pc.csv",
ex_path="ex.csv",
dm_path="dm.csv"
)
Quick Start
Basic Import
from neopkpd.data import import_cdisc
# Import from CSV files
data = import_cdisc(
pc_path="pc.csv", # Pharmacokinetic Concentrations
ex_path="ex.csv", # Exposure (Dosing)
dm_path="dm.csv" # Demographics
)
# Access data
print(f"Subjects: {data.n_subjects}")
print(f"Observations: {data.n_observations}")
Import from SAS Transport (XPT)
data = import_cdisc(
pc_path="pc.xpt",
ex_path="ex.xpt",
dm_path="dm.xpt",
format="xpt"
)
Supported Domains
PC Domain (Pharmacokinetic Concentrations)
Required Variables
| Variable |
Label |
Type |
Description |
| STUDYID |
Study ID |
str |
Study identifier |
| USUBJID |
Subject ID |
str |
Unique subject identifier |
| PCTESTCD |
Test Code |
str |
Short test name |
| PCSTRESN |
Numeric Result |
float |
Standardized numeric result |
| PCSTRESU |
Unit |
str |
Units (ng/mL, µg/L, etc.) |
| PCELTM |
Elapsed Time |
str |
Time from reference (ISO 8601) |
Optional Variables
| Variable |
Label |
Description |
| PCLLOQ |
Lower LOQ |
Lower limit of quantification |
| PCSTAT |
Status |
Completion status |
| PCBLFL |
Baseline Flag |
"Y" for baseline |
| PCSPEC |
Specimen |
PLASMA, SERUM, etc. |
| PCDTC |
Datetime |
ISO 8601 datetime |
| PCDY |
Study Day |
Study day |
Example Data
import pandas as pd
pc_data = pd.DataFrame({
'STUDYID': ['STUDY01'] * 7,
'USUBJID': ['SUBJ001'] * 7,
'PCTESTCD': ['DRUGA'] * 7,
'PCSTRESN': [0.0, 125.3, 89.7, 52.1, 28.4, 12.1, 3.2],
'PCSTRESU': ['ng/mL'] * 7,
'PCELTM': ['PT0H', 'PT1H', 'PT2H', 'PT4H', 'PT8H', 'PT12H', 'PT24H'],
'PCLLOQ': [0.5] * 7,
'PCSPEC': ['PLASMA'] * 7
})
EX Domain (Exposure)
Required Variables
| Variable |
Label |
Type |
Description |
| STUDYID |
Study ID |
str |
Study identifier |
| USUBJID |
Subject ID |
str |
Unique subject identifier |
| EXDOSE |
Dose |
float |
Administered dose |
| EXDOSU |
Units |
str |
Dose units (mg, µg) |
| EXROUTE |
Route |
str |
Route of administration |
| EXSTDTC |
Start Datetime |
str |
Dose start (ISO 8601) |
Optional Variables
| Variable |
Label |
Description |
| EXENDTC |
End Datetime |
Dose end (infusions) |
| EXDOSFRM |
Dose Form |
TABLET, CAPSULE, etc. |
| EXDOSFRQ |
Frequency |
QD, BID, Q12H |
| EXDUR |
Duration |
ISO 8601 duration |
| EXTRT |
Treatment |
Treatment name |
| EXDY |
Study Day |
Study day |
Route Mapping
| EX Route |
NeoPKPD Route |
| ORAL |
"oral" |
| INTRAVENOUS |
"iv_bolus" |
| INTRAVENOUS INFUSION |
"iv_infusion" |
| SUBCUTANEOUS |
"subcutaneous" |
| INTRAMUSCULAR |
"intramuscular" |
DM Domain (Demographics)
Required Variables
| Variable |
Label |
Type |
Description |
| STUDYID |
Study ID |
str |
Study identifier |
| USUBJID |
Subject ID |
str |
Unique subject identifier |
| RFSTDTC |
Reference Start |
str |
Reference start date |
Optional Variables
| Variable |
Label |
Description |
| AGE |
Age |
Age at screening |
| AGEU |
Age Units |
YEARS, MONTHS |
| SEX |
Sex |
M, F, U |
| RACE |
Race |
Racial designation |
| ETHNIC |
Ethnicity |
Ethnic group |
| ARMCD |
Arm Code |
Treatment arm code |
| ARM |
Arm |
Arm description |
| COUNTRY |
Country |
Country |
| SITEID |
Site ID |
Study site |
Import Functions
import_cdisc()
from neopkpd.data import import_cdisc
def import_cdisc(
pc_path: str,
ex_path: str,
dm_path: str,
*,
format: str = "csv",
blq_handling: str = "zero",
lloq: float | None = None,
time_reference: str = "first_dose"
) -> CDISCData:
"""
Import CDISC data from PC, EX, and DM domains.
Parameters
----------
pc_path : str
Path to PC domain file
ex_path : str
Path to EX domain file
dm_path : str
Path to DM domain file
format : str
"csv" or "xpt"
blq_handling : str
"zero", "lloq_half", or "missing"
lloq : float, optional
Lower limit of quantification
time_reference : str
"first_dose" or "rfstdtc"
Returns
-------
CDISCData
Imported data object
"""
Individual Domain Functions
from neopkpd.data import read_cdisc_pc, read_cdisc_ex, read_cdisc_dm
# Read individual domains
pc = read_cdisc_pc("pc.csv")
ex = read_cdisc_ex("ex.csv")
dm = read_cdisc_dm("dm.csv")
# Combine manually
from neopkpd.data import cdisc_to_observed_data
data = cdisc_to_observed_data(pc, ex, dm)
CDISCData Class
Structure
@dataclass
class CDISCData:
"""Container for imported CDISC data."""
# Study information
study_id: str
analyte: str
units: str
time_units: str
# Subject data
subjects: list[SubjectData]
# Validation
validation_warnings: list[str]
# Properties
@property
def n_subjects(self) -> int: ...
@property
def n_observations(self) -> int: ...
@property
def subject_ids(self) -> list[str]: ...
# Methods
def get_subject(self, subject_id: str) -> SubjectData: ...
def to_dataframe(self) -> pd.DataFrame: ...
def to_estimation_data(self) -> EstimationData: ...
SubjectData Class
@dataclass
class SubjectData:
"""Individual subject data."""
subject_id: str
times: list[float] # Hours from first dose
observations: list[float] # Concentrations
doses: list[DoseEvent] # Dose events
covariates: dict[str, Any] # Demographics
lloq: float # LLOQ value
blq_flags: list[bool] # BLQ indicators
Accessing Data
from neopkpd.data import import_cdisc
data = import_cdisc("pc.csv", "ex.csv", "dm.csv")
# Iterate through subjects
for subj in data.subjects:
print(f"Subject: {subj.subject_id}")
print(f" Time points: {len(subj.times)}")
print(f" Max conc: {max(subj.observations):.2f}")
print(f" Age: {subj.covariates.get('age', 'N/A')}")
print(f" Doses: {len(subj.doses)}")
# Get specific subject
subj = data.get_subject("SUBJ001")
# Convert to DataFrame
df = data.to_dataframe()
print(df.head())
Time Handling
Elapsed Time Parsing
# PCELTM format (ISO 8601 duration)
# PT0H → 0.0 hours
# PT1H → 1.0 hours
# PT2H30M → 2.5 hours
# P1DT2H → 26.0 hours
# Parsed automatically during import
data = import_cdisc("pc.csv", "ex.csv", "dm.csv")
# Times are relative to first dose (in hours)
for subj in data.subjects:
print(f"Time range: {min(subj.times)} to {max(subj.times)} h")
Reference Time Options
# Times relative to first dose (default)
data = import_cdisc("pc.csv", "ex.csv", "dm.csv",
time_reference="first_dose"
)
# Times relative to reference start date (RFSTDTC)
data = import_cdisc("pc.csv", "ex.csv", "dm.csv",
time_reference="rfstdtc"
)
BLQ Handling
Detection
BLQ values are detected from:
- PCSTRESN == 0 or missing
- PCSTAT == "NOT DONE"
- PCORRES contains "<", "BLQ", "BLOQ"
- PCSTRESN < PCLLOQ
# Check BLQ flags
for subj in data.subjects:
n_blq = sum(subj.blq_flags)
if n_blq > 0:
print(f"Subject {subj.subject_id}: {n_blq} BLQ samples")
Handling Options
# Set BLQ to zero (default)
data = import_cdisc("pc.csv", "ex.csv", "dm.csv",
blq_handling="zero"
)
# Set BLQ to LLOQ/2
data = import_cdisc("pc.csv", "ex.csv", "dm.csv",
blq_handling="lloq_half",
lloq=0.5
)
# Keep as NaN (missing)
data = import_cdisc("pc.csv", "ex.csv", "dm.csv",
blq_handling="missing"
)
Validation
Automatic Validation
data = import_cdisc("pc.csv", "ex.csv", "dm.csv")
# Check for warnings
if data.validation_warnings:
print("Validation warnings:")
for warning in data.validation_warnings:
print(f" ⚠️ {warning}")
Manual Validation
from neopkpd.data import validate_cdisc_dataset
validation = validate_cdisc_dataset(
pc_path="pc.csv",
ex_path="ex.csv",
dm_path="dm.csv"
)
if validation.valid:
print("✓ Dataset passes validation")
else:
print("❌ Validation failed:")
for error in validation.errors:
print(f" - {error}")
Validation Checks
| Check |
Description |
| Required columns |
All required variables present |
| Data types |
Correct types for each column |
| Subject consistency |
IDs match across domains |
| Dose availability |
All PK subjects have dosing data |
| Concentration values |
No negative concentrations |
| Time sequence |
Valid time progression |
Standard Covariates
# Covariates extracted from DM domain:
# - age (AGE)
# - sex (SEX → "M", "F")
# - race (RACE)
# - ethnicity (ETHNIC)
# - treatment arm (ARM, ARMCD)
data = import_cdisc("pc.csv", "ex.csv", "dm.csv")
for subj in data.subjects:
print(f"Subject {subj.subject_id}:")
print(f" Age: {subj.covariates.get('age', 'N/A')}")
print(f" Sex: {subj.covariates.get('sex', 'N/A')}")
print(f" Weight: {subj.covariates.get('weight', 'N/A')}")
Additional Covariates from VS
# Include vital signs for weight, height
data = import_cdisc(
pc_path="pc.csv",
ex_path="ex.csv",
dm_path="dm.csv",
vs_path="vs.csv", # Optional vital signs
covariates=["weight", "height", "bmi"]
)
Export Options
To DataFrame
df = data.to_dataframe()
# DataFrame columns:
# SUBJID, TIME, DV, AMT, EVID, MDV, CMT, AGE, SEX, WT, ...
print(df.head())
from neopkpd.data import export_nonmem_data
export_nonmem_data(data, "nonmem_data.csv")
# Creates NONMEM-compatible dataset with:
# ID, TIME, DV, AMT, EVID, MDV, CMT, ...
# Direct use with NCA
from neopkpd.nca import run_population_nca
pop_result = run_population_nca(
data.to_dataframe(),
subject_col='SUBJID',
time_col='TIME',
conc_col='DV',
dose_col='AMT'
)
Complete Example
import pandas as pd
from neopkpd.data import import_cdisc, validate_cdisc_dataset
# Step 1: Validate data
print("Validating CDISC data...")
validation = validate_cdisc_dataset(
pc_path="pc.csv",
ex_path="ex.csv",
dm_path="dm.csv"
)
if not validation.valid:
print("Validation failed:")
for error in validation.errors:
print(f" - {error}")
exit(1)
print("✓ Validation passed")
# Step 2: Import data
print("\nImporting CDISC data...")
data = import_cdisc(
pc_path="pc.csv",
ex_path="ex.csv",
dm_path="dm.csv",
blq_handling="lloq_half",
lloq=0.5
)
# Step 3: Display summary
print("\n" + "=" * 50)
print("CDISC Data Import Summary")
print("=" * 50)
print(f"\nStudy: {data.study_id}")
print(f"Analyte: {data.analyte}")
print(f"Units: {data.units}")
print(f"\nSubjects: {data.n_subjects}")
print(f"Total observations: {data.n_observations}")
# Step 4: Subject details
print("\n--- Subject Details ---")
for subj in data.subjects[:5]: # First 5 subjects
n_obs = len(subj.times)
n_doses = len(subj.doses)
age = subj.covariates.get('age', 'N/A')
sex = subj.covariates.get('sex', 'N/A')
n_blq = sum(subj.blq_flags)
print(f"\n{subj.subject_id}:")
print(f" Observations: {n_obs}")
print(f" Time range: {min(subj.times):.1f} - {max(subj.times):.1f} h")
print(f" Doses: {n_doses}")
print(f" Age: {age}, Sex: {sex}")
if n_blq > 0:
print(f" BLQ samples: {n_blq}")
# Step 5: Check warnings
if data.validation_warnings:
print("\n--- Warnings ---")
for w in data.validation_warnings:
print(f" ⚠️ {w}")
# Step 6: Use with NCA
print("\n--- Running Population NCA ---")
from neopkpd.nca import run_population_nca, summarize_population_nca
df = data.to_dataframe()
pop_result = run_population_nca(
df,
subject_col='SUBJID',
time_col='TIME',
conc_col='DV',
dose_col='AMT'
)
summary = summarize_population_nca(pop_result)
print(f"\nCmax: {summary['cmax']['mean']:.2f} ± {summary['cmax']['sd']:.2f} {data.units}")
print(f"AUC: {summary['auc_0_inf']['mean']:.2f} ± {summary['auc_0_inf']['sd']:.2f} {data.units}·h")
# Step 7: Export
print("\n--- Exporting Data ---")
df.to_csv("analysis_dataset.csv", index=False)
print("Saved to analysis_dataset.csv")
ADaM Import
ADPC Dataset
from neopkpd.data import import_adam_adpc
adpc = import_adam_adpc("adpc.csv")
# ADaM-specific variables:
# AVAL - Analysis value
# ATPT - Analysis timepoint
# ATPTN - Numeric timepoint
# TRTP - Planned treatment
# TRTA - Actual treatment
ADSL Dataset
from neopkpd.data import import_adam_adsl
adsl = import_adam_adsl("adsl.csv")
Combined Import
from neopkpd.data import import_adam
data = import_adam(
adpc_path="adpc.csv",
adsl_path="adsl.csv"
)
PP Domain (PK Parameters)
Import pre-calculated PK parameters:
from neopkpd.data import read_cdisc_pp
pp = read_cdisc_pp("pp.csv")
# Common parameter codes:
# CMAX - Maximum concentration
# TMAX - Time to max
# AUCLST - AUC to last
# AUCIFO - AUC extrapolated
# LAMZHL - Half-life
See Also