# Fleet

> CDISC SDTM/ADaM derivation toolkit for Polars DataFrames.

Fleet adds chainable verb pipelines directly onto every Polars DataFrame so you can build standards-compliant CDISC datasets with minimal boilerplate. Importing the package registers `.sdtm` and `.adam` namespaces automatically.

## Install

```
pip install fleet
```

## Quickstart

```python
import fleet
import polars as pl

dm = (
    raw_df.sdtm
    .hardcode_no_ct("DOMAIN",  value="DM")
    .hardcode_no_ct("STUDYID", value="MYSTUDY01")
    .assign_no_ct("USUBJID",   source="patient_id")
    .assign_ct("SEX", source="sex_raw",
               ct_codelist="C66731", ct_mapping={"M": "M", "F": "F"})
    .to_frame()
)
```

Nothing runs until `.to_frame()`. All verbs accumulate into a single `df.with_columns(...)` pass.

## Namespaces

- `df.sdtm` — 14 SDTM derivation verbs (domain mapping, CT lookup, datetime normalisation)
- `df.adam` — 22 ADaM BDS verbs (study day, baseline, change, flags, BSA, MAP, QTc, LOCF, summary records, derived parameters)
- `df.adam.adsl` — 8 patient-level verbs (treatment dates, duration, population flags, AE counts, age conversion, periods)
- `df.adam.occds` — 5 occurrence verbs (first/last date, worst severity, any-flag)

### Therapeutic-area extensions (via `df.adam.<ta>`)

- `df.adam.onco` — Oncology: AVAL response, BOR, filter_pd
- `df.adam.ophtha` — Ophthalmology: ETDRS/LogMAR, study eye derivation
- `df.adam.peds` — Pediatrics: age derivations, prematurity flag
- `df.adam.vaccine` — Vaccine: fever flag, seroconversion, ADIS AVAL
- `df.adam.metabolic` — Metabolic: waist-to-hip ratio, body fat %
- `df.adam.neuro` — Neuroscience: Centiloid, MMSE, CDR-SoB

## SDTM verbs

- `assign_no_ct(target, source)` — copy raw column, no CT validation
- `hardcode_no_ct(target, value)` — constant literal for all rows
- `assign_ct(target, source, ct_codelist, ct_mapping)` — recode through CT codelist
- `assign_datetime(target, source, mode)` — normalise to ISO 8601 date/datetime
- `derivation(target, expression)` — arbitrary Polars expression
- `if_then_else(target, when, else_value)` — multi-branch conditional
- `to_frame()` — materialise

## ADaM verbs

- `source(target)` — pass-through column
- `derivation(target, expression)` — arbitrary Polars expression
- `assigned(target, value)` — constant literal
- `study_day(target, analysis_date, reference_date)` — CDISC study-day formula
- `flag(target, criteria, groups)` — per-group boolean flag
- `baseline(target, aval, baseline_flag)` — carry baseline AVAL within groups
- `chg(target, aval, base)` — change from baseline
- `pchg(target, aval, base)` — percent change from baseline
- `bmi(target, weight, height)` — Body Mass Index
- `nrind(target, value, low, high)` — normal range indicator (LOW/NORMAL/HIGH)
- `on_treatment(target, adt, trtsdt, trtedt)` — on-treatment flag
- `shift(target, baseline_cat, post_cat)` — categorical shift string
- `bsa(target, weight, height, method)` — Body Surface Area (Mosteller, DuBois, Haycock, Gehan-George)
- `map(target, systolic, diastolic)` — Mean Arterial Pressure
- `qtc(target, qt, rr, method)` — Corrected QT interval (Fridericia or Bazett)
- `derive_summary_records(by_vars, analysis_var, set_values_to, stat)` — aggregate and append summary rows
- `derive_locf_records(by_vars, analysis_var, order_by)` — forward-fill (LOCF) within patient groups
- `derive_param(by_vars, set_values_to, parameter_filter, analysis_value)` — derive new parameter rows
- `to_frame()` — materialise

## ADSL verbs

- `source(target)` — pass-through column
- `first_date(target, from_dataset, date_col, where)` — earliest event date per subject
- `last_date(target, from_dataset, date_col, where)` — latest event date per subject
- `duration(target, start_date, end_date)` — integer day count
- `eligibility_flag(target, criteria)` — "Y"/null flag based on row condition
- `count_events(target, from_dataset, where)` — integer event count per subject
- `age_years(target, age_col, age_unit_col)` — unit-aware age conversion (YEARS, MONTHS, WEEKS, DAYS)
- `derive_period(periods, start_date)` — create Subperiod/Phase variables (APxxSDT, APxxEDT, TRTxxP)
- `to_frame()` — materialise

## define.yaml

A single YAML file is the source of truth for a study: it declares output variables, derivation algorithms, codelists, and structural metadata. The same file drives Define-XML 2.1 generation, pipeline execution, and post-dataset checks.

### Derivation algorithms

- `assigned` → `.assigned(target, value=...)`
- `source` → `.source(target, from_col=...)`
- `derivation` → `.derivation(target, expression=...)`
- `first_date` → `.first_date(target, from_dataset=..., date_col=..., where=...)`
- `last_date` → `.last_date(...)`
- `duration` → `.duration(target, start_date=..., end_date=...)`
- `flag` → `.eligibility_flag(target, criteria=...)`
- `count` → `.count_events(target, from_dataset=..., where=...)`

### Spec-driven pipeline

```python
from fleet import load_study_context

ctx = load_study_context("specs/cdiscpilot01/adam/define.yaml")
adsl = ctx.build("ADSL")
```

### Define-XML generation

```python
from fleet import DefineXMLGenerator

gen = DefineXMLGenerator("specs/cdiscpilot01/adam/define.yaml")
gen.write("outputs/cdiscpilot01/define_adam.xml")
```

## Controlled Terminology

```python
from fleet import CTValidator

ct = CTValidator.get_instance()
ct.load_ct_file("specs/ct/sdtm_ct.yaml")
ok, msg = ct.validate_value("C66731", "M")
```

## Documentation

Full docs: https://realslimmahdi.github.io/fleet-cdisc/

- Getting started: https://realslimmahdi.github.io/fleet-cdisc/getting-started/installation/
- define.yaml guide: https://realslimmahdi.github.io/fleet-cdisc/concepts/define-yaml/
- API reference: https://realslimmahdi.github.io/fleet-cdisc/api/
- ADSL example notebook: https://realslimmahdi.github.io/fleet-cdisc/examples/adsl_pipeline/
