Creating a temporary SAS array of dynamic size

Arrays in SAS are incredibly useful things. For example, if you’re dealing with a concomitant medications dataset and want to check for certain medicines across multiple columns, you’d be hard-pressed to find a faster method than using arrays!

Within the SDTM.CM domain, medication names are spread across several columns, usually: CMTRT (Reported Name of Drug, Med, or Therapy), CMMODIFY (Modified Reported Name), and CMDECOD (Standardized Medication Name).

If we wanted to find and flag, for example, the following three medicines: Aspirin, Antacid, Potassium Chloride ; we could do it as follows:


data cm;
    set sdtm.cm;

    array all_meds {*} $ cmtrt cmmodify cmdecod;
    array check_meds {3} $ _temporary_ ("aspirin", "antacid", "potassium chloride");

    do i = 1 to dim(all_meds);
        do j = 1 to dim(check_meds);
            if upcase(all_meds[i]) = upcase(check_meds[j]) then occur = "Y";
        end;
    end;

    if occur ^= "Y" then delete;
run;

The above method works well to quickly check for multiple conmeds across multiple columns. However, usually a conmeds list is not restricted to only 3 items and counting the number of unique items is a slow and tedious process. If an item needs to be added or removed, then the space to be reserved needs to be updated.

Unfortunately specifying the list of conmeds in a temporary array prevents the use of the dynamic sizing, usually indicated by {*}. This is something that will hopefully be fixed in a newer version of SAS, but until such time I’ve taken it upon myself to create a macro which creates dynamically sized (sort of!) temporary arrays. It does this by counting the number of items and automatically reserving the space for it.

Note: if you are feeling lazy, you can still create a temporary array and simply oversize it, e.g., specify a size of 100 items even though you may only need half that. However, this will result in SAS posting a WARNING to the log about partial array initialization, which is not ideal in the pharmaceutical environment where we want clean logs!

If you’d prefer to avoid another O(n) loop which checks for the maximum length needed for character variables, simply set a static length, say $20.


/*
    Macro makeTempArray
    
    Purpose: To create temporary arrays without knowing the
             size of the array needed beforehand. This is a 
             limitation of the original SAS procedure for
             creating temporary arrays.
    
    Parameters:
        arrayname : an arbitrary name for your array
        ischar    : pass either Y for a character array 
                    or N for numeric
        items     : pass the list of items to be contained in
                    the array, wrapped in %str() and separated
                    by commas
*/

%macro makeTempArray(arrayname=, ischar=, items=);
    %let n=%sysfunc(countw(&items., %str(,), )); /*count the number of items to reserve space for*/
   
    %if &ischar.=Y %then %do; /*if this is a character array, we need the length of the longest item*/ 
        %let l = 1;
        %do j = 1 %to &n.;
            %let l0 = %sysfunc(length(%sysfunc(scan(&items., &j., %str(,), r))));
            %if  &l0. > &l. %then %let l = &l0.;
        %end;
    %end;
    
    array &arrayname. {&n.} %if &ischar.=Y %then $&l.; _temporary_ (
        %do i = 1 %to &n.;
            %let item = %sysfunc(scan(&items., &i., %str(,), r));
            %if &ischar.=Y %then %str("&item." ); %else &item.;
        %end;
    );
%mend makeTempArray;

With this macro, we can now modify our initial starting block of code as follows (items must be separated by commas):


data cm;
    set sdtm.cm;

    array all_meds {*} $ cmtrt cmmodify cmdecod;
    %makeTempArray(arrayname=%str(check_meds), ischar=%str(Y), items=%str(aspirin, antacid, potassium chloride));

    do i = 1 to dim(all_meds);
        do j = 1 to dim(check_meds);
            if upcase(all_meds[i]) = upcase(check_meds[j]) then occur = "Y";
        end;

        if occur = "Y" then leave;
    end;

    if occur ^= "Y" then delete;
run;

Hope this helps you next time you need to cross-check multiple items across multiple columns! Happy hacking!

Updates: My colleague, Mazi Ntintelo has rightly pointed out that the commas within the macro’s scan functions should be wrapped as %str(,) and also that the do loop checking for conmeds can be optimised with a leave statement. Thanks, Mazi!

Jan’s Golden Curry Chicken Ramen

Regular readers (hah!) of this blog will know that my topics have always been programming and photography focused. However, I’ve recently come up with a ramen recipe of my own that I want to write down, not only to share but also to preserve it for my own fragile and failing memory. It is far from an authentic Japanese recipe, but it does scratch the itch.

I have dropped some links to the ingredients I normally use. I don’t get any kickbacks from them.

You will need the following ingredients per person that you are cooking for:

What if I am a vegetarian? Firstly allow me to offer my condolences and deepest sympathies with your condition. I would then suggest to switch out the chicken stock with vegetable stock and find a suitable alternative to the chicken. Perhaps tofu or a plant-based meat.

Start cooking!

Cooking is like love. It should be entered into with abandon, or not at all.

Julia Child

Chicken

Coat the chicken fillets in tonkatsu sauce and sprinkle some mild chilli powder on top. Pop them into the airfryer (12 to 14 minutes) or oven grill them until done.

Eggs and Broth

You can do the broth in parallel with the eggs !

Eggs

Bring a small pot of water to boil. The eggs should be cooked in the boiling water for roughly 6.5 to 8.5 minutes, depending on how runny you prefer it to be. I normally strike a nice balance at 7.5 minutes.

After 7.5 minutes, put the eggs into an ice bath.

Broth

Bring the 500 ml (per person) of water to boil. Add the chicken stock and miso paste. Finely grate the golden curry cube(s) as they do not dissolve well otherwise.

Switch to a medium heat and leave to simmer for at least 30 minutes. Stir occasionally to make sure the curry cubes are dissolving.

Ramen

As soon as the eggs are in the ice bath, drop the ramen into the boiling water (you can use the same water that you used for the eggs). Cook the ramen for ~4 minutes, ensuring that it does not cook too soft.

Mushrooms

You may decide to use proper shiitake or other mushrooms for this recipe, but baby button mushrooms work well and you can find them anywhere. Grab about 5 baby mushrooms per person and fry them in a pan coated with a shallow layer of butter. You can add some herbs (italian herbs, rosemary, etc.) if you want, but it’s not necessary.

Plating

You’ll want bowls of a decent size as the portions are fairly large.

Drop the noodles into the bowl and peel the eggshells from the eggs. Split the eggs lengthwise and place on top of the noodles. Add the spinach and sweetcorn (drain the can first!) ; keep in mind that the spinach will wilt to a much smaller size! Place the chicken and mushrooms on top of the spinach.

Now pour the broth into the bowl, making sure beforehand that the curry cubes have dissolved properly. Chop up some spring onions and sprinkle onto the broth.

Bon appetit!

Loughcrew Cairns

Loughcrew or Lough Crew (Irish: Loch Craobh, meaning ‘lake of the tree’) is an area of historical importance near Oldcastle, County Meath, Ireland. It is home to a group of ancient tombs from the 4th millennium BC, some decorated with rare megalithic art, which sit on top of a range of hills.

https://en.wikipedia.org/wiki/Loughcrew

During my brother Ian’s visit to Ireland, we decided to go see the Loughcrew Cairns. Below are some pictures from the short, but steep (!), hike up to the tombs.

There was something special about this stone…
Fantastic views of the rolling hills in the Irish countryside.
The climb is a lot steeper than it looks! Be warned!
At the top!
The entrance to the largest tomb is gated.
Several smaller burial mounds surround the largest one.
While not really visible in this picture, the stones are decorated with megalithic art.
It is fantastic that one can walk so close to such a significant part of human history.
The lawn was being maintained by the most eco-friendly lawnmower ever invented.
Proper “Windows XP wallpaper” vibes.

That’s it! Definitely a sight to see if you’re visiting Ireland and it’s a special opportunity to walk so close to monuments that are almost 6000 years old.

Significant Figures in SAS

For three significant figures, the SAS Institute provides the following code snippet to accomplish the task.

However, it is often useful to round to more or less than 3 significant figures. I’ve developed a macro to do so for my own use and am sharing the code below.

%macro _nsigfig(varin=, varout=, n=);
if &varin. = 0 then &varout. = 0;
else do;
	if int(&varin.) ^= 0 then do;
	    &varout. = round(&varin., 10**(int(log10(abs(&varin.))) + (1 - &n.)));
	end;
	else do;
	    &varout. = round(&varin., 10**(-1*(abs(int(log10(abs(&varin.)))) + &n.)));
	end;
end;
%mend _nsigfig;

NONMEM Dataset Example (free, with R code)

I’m making available here a basic NONMEM dataset example, along with the R code used to create it. Hopefully it will be helpful to someone in future!

CSV download: http://jvdl.me/downloads/nonmem/dummy_nonmem.csv

XPT download: http://jvdl.me/downloads/nonmem/dummy_nonmem.xpt

library(dplyr)
library(tidyverse)

library(linpk)
library(haven)

rm(list = ls())

# basic structure
stdy <- c(1, 2, 3)
sex <- c(0, 1)
race <- c(1, 2, 3)
ntad <- c(0, 0.5, 1, 2, 4, 8, 12, 16, 24, 48, 72)
# end basic structure

# create dummy foundation with random wt, ht, bmi, age
foundation <- data.frame(stdy) %>% 
    full_join(data.frame(sex), by = character()) %>%
    full_join(data.frame(race), by = character()) %>%
    mutate(
        c = NA,
        id = row_number(),
        wt = runif(row_number(), 70, 100),
        ht = runif(row_number(), 150, 200),
        bmi = wt / ((ht / 100) ^ 2),
        age = runif(row_number(), 30, 60)
    ) %>% 
    mutate_at(
        vars(wt, ht, bmi), list(~ round(., 2))
    ) %>%
    mutate_at(
        vars(age), list(~ trunc(.))
    ) %>%
    full_join(data.frame(ntad), by = character())

# prepare pc
foundation.pc <- foundation %>%
    mutate(
        evid = 0L,
        cmt = 2L,
        mdv = 0, 
        amt = NA
    )

# prepare ex
foundation.ex <- foundation %>%
    filter(ntad == 0) %>%
    mutate(
        evid = 1L,
        cmt = 1L,
        addl = NA,
        ii = NA, 
        mdv = 1,
        amt = case_when(
            stdy == 1 ~ 100L,
            stdy == 2 ~ 200L,
            stdy == 3 ~ 300L,
            TRUE      ~ -99L
        )
    )

# set pc and ex and locf amt as dose
foundation.nonmem <- bind_rows(foundation.pc, foundation.ex) %>%
    arrange(stdy, id, ntad, evid) %>%
    mutate(
        dose = amt,
        tad = case_when(
            ntad == 0 & evid == 0 ~ -0.05,
            TRUE                  ~ ntad
        ),
    ) %>%
    group_by(stdy, id) %>%
    tidyr::fill(dose, .direction = c("downup")) %>%
    ungroup() 

# populate dummy concs with pklin::pkprofile
foundation.nonmem$dv = pkprofile(foundation.nonmem$tad, cl=0.5, vc=11, ka=1.3, dose = list(amt = foundation.nonmem$dose))

#introduce some randomness to pk concs
foundation.nonmem <- foundation.nonmem %>%
    mutate(
        dv = as.numeric(dv) * runif(row_number(), 1.1, 1.25) * (dose / 100),
        dv = ifelse(evid == 0, dv, NA), 
        lndv = case_when(
            dv == 0 ~ 0,
            TRUE    ~ log(dv)
        )
    ) %>%
    mutate_at(vars(dv, lndv), list(~ round(., digits = 2)))

# housekeeping, replace NAs, column names to uppercase, etc.
foundation.nonmem.final <- foundation.nonmem %>% 
    mutate(
        across(everything(), ~replace_na(.x, "."))
    ) %>%
    select(c, stdy, id, tad, ntad, dose, amt, addl, ii, evid, cmt, dv, lndv, mdv, sex, race, age, wt, ht, bmi)

names(foundation.nonmem.final) <- stringr::str_to_upper(names(foundation.nonmem.final))

# export to csv and xpt
setwd("~/R")
write_csv(foundation.nonmem.final, file = "dummy_nonmem.csv")
write_xpt(foundation.nonmem.final, path = "dummy_nonmem.xpt", name = "NM", version = 5)