NONMEM ADDL Calculation and Compression in SAS

ADDL represents the number of additional doses that are copies of the current row, with the time since first dose (RTFD) increased at the regular dosing interval (II). This allows for the compression of dose records in the NONMEM dataset. To put it more eloquently:

The NONMEM data item ADDL on a dose record expresses the number of additional implicit doses that should follow at a regular interval II. In the case where explicit doses exist, ADDL supports compacting them into ADDL/II notation.

https://rdrr.io/rforge/metrumrg/man/addl.html

The gist of calculating ADDL is the following:

  • Determine an allowed window between doses – if dosing occurs outside of this window (too soon or too late) the information should not be compressed and the modeller should be aware of it
  • Group and sort the data by subject ID, treatment, date of exposure, and dose
  • Assign a sequence number starting from 1 for each treatment (A, B, C, …) per subject
  • Determine which doses are “must-list”; the first record of every subject should be listed and any exposure attribute changes or exposure delays should be listed
  • Filter the exposure records to keep only the “must-list” data and other data required at the modeller’s discretion, e.g., the exposure records linked to PK samples
  • After filtering, the differences in SEQ values are the ADDL values

For this example, we will focus on compressing explicit doses and we will stick to date-based dosing only. The principles can be easily adjusted for date-and-time-based exposure data. Instead of defining the dosing interval (II) as 24h, we will just define it as 1 day in a macro variable.

%let allowed_dose_delay = 1;

The exposure data will be defined across two subjects, with multiple treatments, multiple dose levels, and occasional gaps between doses exceeding the 1 day window.

data ex;
	format date date9.;
	input usubjid trt $ dose date : date9.;
	cards;
1 A 50 01JAN1990
1 B 75 02JAN1990
1 A 50 02JAN1990
1 B 100 03JAN1990
1 A 50 03JAN1990
1 B 100 04JAN1990
1 A 50 05JAN1990
1 A 100 06JAN1990
1 B 50 06JAN1990
1 A 100 07JAN1990
1 A 100 08JAN1990
1 A 100 09JAN1990
2 B 50 01JAN1990
2 A 75 02JAN1990
2 B 50 02JAN1990
2 A 100 03JAN1990
2 B 50 03JAN1990
2 B 100 04JAN1990
2 B 50 05JAN1990
2 A 100 06JAN1990
2 B 50 06JAN1990
2 A 100 07JAN1990
run;

proc sort data=ex out=addl_pre;
	by usubjid trt date dose;
run;

After the initial dataset has been populated and sorted, a unique sequence number starting from 1 should be assigned to each treatment within each subject. In addition, a “must_list” variable will be populated with “Y” if:

  • the first record for the subject is encountered
  • a new treatment within subject is encountered
  • the dose level within a treatment was changed
  • a delay in dosing longer or shorter than the allowed delay window was encountered
data addl1;
	set addl_pre;
	by usubjid trt date dose;

	retain seq 1;

	if first.usubjid or first.trt then do;
		must_list = "Y";
		seq = 1;
	end;
	else do;
		seq + 1;
	end;

	if trt ^= lag(trt) then must_list = "Y";
	if dose ^= lag(dose) then must_list = "Y";
	if date - lag(date) ^= &allowed_dose_delay. then must_list = "Y";
run;

The maximum sequence per subject and treatment should be determined to calculate ADDL for the final, must-list exposure records.

proc sql noprint;
	create table addl2 as
		select *, max(seq) as max_seq
		from addl1
		group by usubjid, trt
		order by usubjid, trt, date
	;
quit;

The exposure events are filtered to keep only the “must-list” records. The remaining records will be compressed into the ADDL counter. With real-world data, you may want to add any record linked to PK sampling to the “must-list” group. This can be done by linking the exposure date and time with the sample reference dose date and time, e.g., PC.PCRFTDT.

data addl3;
	set addl2(where=(must_list = "Y"));
run;

Reverse sort the dataset to allow peeking at the next sequence records.

proc sort data=addl3 out=addl4_pre; 
	by descending usubjid descending trt descending date descending seq;
run;

data addl4;
	set addl4_pre;
	by descending usubjid descending trt descending date descending seq;

	next_seq = lag(seq);
	if usubjid ^= lag(usubjid) or trt ^= lag(trt) then next_seq = .;

	proc sort; by usubjid trt date seq;
run;

Finally, ADDL is calculated as the difference between MAX_SEQ and SEQ in the case of the final record, otherwise (NEXT_SEQ – (SEQ + 1)) to ensure we have exclusive bounds between the current and next exposure sequence numbers.

data addl5;
	set addl4;

	if missing(next_seq) then addl = max_seq - seq;
	else addl = next_seq - (seq + 1);

	keep usubjid trt date dose addl;
run;
While not explicitly specified here, our data has II defined as 24 hours or 1 day. In real-world data an II column would be visible in the data.

Provided that your data is well-structured and clean, compressing the exposure records and calculating ADDL is a straightforward task!

SAS and the Forward Re-scan Rule

The “Forward Re-scan Rule” (FRR) is used by SAS to resolve macro variables over several passes. This is especially useful when having one macro variable point to another macro variable, or when trying to resolve numbered macro variables.

The SAS Advanced Prep Guide summarises the FRR as follows:

  • When multiple ampersands or percent signs precede a name token, the macro processor resolves two ampersands (&&) to one ampersand (&), and re-scans the reference.
  • To re-scan a reference, the macro processor scans and resolves tokens from left to right from the point where multiple ampersands or percent signs are coded, until no more triggers can be resolved.

Example: numbered list of macro variables

To illustrate this example for a numbered list of macro variables, we can load each unique car manufacturer from SASHELP.CARS into a unique macro variable:

data cars;
	set sashelp.cars;
run;

proc sql;
	select distinct make into :car1-:car999
	from cars;
quit;

%put Number of obs = &sqlobs;

SAS will not create more macro variables than necessary. We have accounted for the possibility of 999 distinct manufacturers, but in reality the dataset contains only 38. SAS will only reserve the variables car1 to car38.

We can loop through the variables we’ve just created, by using the FRR. To further illustrate, also turn on the MPRINT, MLOGIC, and SYMBOLGEN options. Note the use of the double ampersand (&&).

options mprint mlogic symbolgen;
%macro printCars;
	%do i = 1 %to &sqlobs;
		%put &&car&i;
	%end;
%mend printCars;
%printCars;
 MLOGIC(PRINTCARS):  %DO loop beginning; index variable I; start value is 1; stop value is 38; by value is 1.  
 MLOGIC(PRINTCARS):  %PUT &&car&i
 SYMBOLGEN:  && resolves to &.
 SYMBOLGEN:  Macro variable I resolves to 1
 SYMBOLGEN:  Macro variable CAR1 resolves to Acura

From the log output, we can trace the FRR resolution as follows:

  1. &&car&i
  2. &car1
  3. Acura

Example: nested macro variables

Suppose we declare the following macro variables:

%let one = two;
%let two = three;
%let three = one;

To test your understanding of the FRR, can you accurately predict the resolution of these macro variables?

%put &one;
%put &&one;
%put &&&one;
%put &&&&one;
%put &&&&&one;
%put &&&&&&one;
%put &&&&&&&one;
%put &&&&&&&&one;
%put &&&&&&&&&one;
%put &&&&&&&&&&one;

The FRR will process from left to right. Any double ampersand (&&) will be resolved to a single ampersand (&) and any instances of a single ampersand will be resolved.

Let’s work through two examples together.

%put &&&&&one;

To better organise our desk-checking of the code, we can rewrite it in a more human-readable, and -friendly format:

&& && &one;

Each double ampersand (&&) resolves to a single ampersand, and the remaining single ampersand and macro reference is resolved.

& & two;

The remaining two ampersands are resolved to a single ampersand:

&two;

Which resolves to:

three

And upon checking the SAS log, we can see our result has been confirmed:

%put &&&&&one;
three

As a final example, let’s work through:

%put &&&&&&&&one;

Organise the ampersands into a more human-readable format, group and resolve double ampersands, and finally resolve remaining single ampersands:

&&&&&&&&one;
&& && && && one;
& & & & one;     compress ==>  && && one;
& & one;         compress ==>  && one;
&one;
two

Our result is once again confirmed by the SAS log:

%put &&&&&&&&one;
two

Tarjan’s Cycle Enumeration Algorithm – Python Implementation

from copy import deepcopy

G = []
cycles = []

point_stack = []
marked = []
marked_stack = []

def tarjan(s, v):
    global cycles
    f = False
    point_stack.append(v)
    marked[v] = True
    marked_stack.append(v)
    for w in G[v]:
        if w < s:
            G[w] = []
        elif w == s:
            points_keeper = list(deepcopy(point_stack))
            if points_keeper not in cycles:
                cycles.append(points_keeper)
            f = True
        elif marked[w] == False:
            g = tarjan(s,w)
            f = f or g

    if f == True:
        while marked_stack[len(marked_stack) - 1] != v:
            u = marked_stack.pop()
            marked[u] = False
        marked_stack.pop()
        marked[v] = False

    point_stack.pop()
    return f

def entry_tarjan(G_):
    global G, cycles, marked, marked_stack, point_stack
    G = []
    cycles = []

    point_stack = []
    marked = []
    marked_stack = []

    G = deepcopy(G_)

    marked = [False for x in xrange(0, len(G_))]

    for i in range(len(G)):
        tarjan(i, i)
        while marked_stack:
            u = marked_stack.pop()
            marked[u] = False

    return cycles

Tarjan’s Cycle Enumeration Algorithm – Go Implementation

package main

import "fmt"

var (
	G            [][]int
	point_stack  []int
	marked       []bool
	marked_stack []int
)

func main() {
	//Example graph - adjacency list
	G = [][]int{[]int{1}, []int{10}, []int{0}, []int{0}, []int{3}, []int{8}, []int{9}, []int{4, 5}, []int{2}, []int{6}, []int{7}}
	entry_tarjan(G)
}

func entry_tarjan(G [][]int) {
	marked = make([]bool, len(G))

	for i := 0; i < len(G); i++ {
		tarjan(i, i)
		for len(marked_stack) > 0 {
			u := marked_stack[len(marked_stack)-1]
			marked_stack = marked_stack[:len(marked_stack)-1]
			marked[u] = false
		}
	}
}

func tarjan(s int, v int) bool {
	f := false
	point_stack = append(point_stack, v)
	marked[v] = true
	marked_stack = append(marked_stack, v)

	for _, w := range G[v] {
		cb := make(chan bool, len(G[v]))
		go branch(s, v, w, cb)
		f = <-cb
	}

	if f == true {
		for marked_stack[len(marked_stack)-1] != v {
			u_ := marked_stack[len(marked_stack)-1]
			marked_stack = marked_stack[:len(marked_stack)-1]
			marked[u_] = false
		}
		marked_stack = marked_stack[:len(marked_stack)-1]
		marked[v] = false
	}

	point_stack = point_stack[:len(point_stack)-1]
	return f
}

func branch(s int, v int, w int, cb chan bool) {
	f_ := false
	if w < s {
		G[w] = []int{}
	} else if w == s {
		fmt.Println(point_stack)
		f_ = true
	} else if marked[w] == false {
		g_ := tarjan(s, w)
		f_ = f_ || g_
	}

	cb <- f_
}

SAS LOCF For Multiple Variables

It is often necessary to replace missing measurements with the closest, previous measurement. This technique is referred to as LOCF (last observation carried forward).

In this example, we will create a dataset with 4 columns: subject ID, visit number, body weight, and systolic blood pressure.

data have;
	input ID VISIT WT SBP;
	cards;
1 10 85 125
1 20 84 .
1 30 86 .
1 40 . 130
1 50 . 128
1 60 85 .
2 10 . 110
2 20 90 .
2 30 91 .
2 40 91 123
2 50 . .
2 60 . 130
;
run;
Input dataset, with subject ID, visit number, body weight, and systolic blood pressure

Thereafter, we will sort the dataset to ensure it is in the order we expect. Never assume that your input will be appropriately sorted.

proc sort data=have;
	by id visit;
run;

Now we will define 2 macro variables, which are simply lists of variables. The first contains the original variables available in the dataset, which will not be altered, and the second names the variables which will contain the LOCF values.

%let origvars = %str(WT  SBP);
%let locfvars = %str(WT_ SBP_);

This brings us to our final block of code. The lists of variables defined above are loaded into arrays and a loop performs the LOCF operation across all the variables defined.

data want(drop = j);
	set have;
	by id visit;

	/*Create arrays of the variable lists*/
	array orig[*] &origvars.;
	array locf[*] &locfvars.;
	retain 	      &locfvars.;

	do j = 1 to dim(orig);
		if first.id then do;
			/*Set a placeholder value for initial missings*/
			if orig(j) = . then locf(j) = -99;
		end;

		/*Replace retained value with latest non-missing value*/
		if not missing(orig(j)) then locf(j) = orig(j);
	end;
run;
The final dataset with LOCF’ed variables, WT_ and SBP_