SAS (Base SAS)+
*==============================================================================;
* Sample data: 3 subjects, 3 visits each
*==============================================================================;
data advs;
infile datalines dlm='|' dsd missover;
input USUBJID : $7. VISITNUM PARAMCD : $6. AVAL;
datalines4;
101-001|1|SYSBP|130
101-001|2|SYSBP|125
101-001|3|SYSBP|120
101-002|1|SYSBP|145
101-002|2|SYSBP|140
101-002|3|SYSBP|138
101-003|1|SYSBP|118
101-003|2|SYSBP|122
101-003|3|SYSBP|119
;;;;
run;
proc sort data=advs;
by USUBJID VISITNUM;
run;
*==============================================================================;
* BUG: LAG does not reset at BY-group boundaries
*==============================================================================;
* On the first row of 101-002, prev_aval picks up 120 (the last AVAL
* from 101-001) instead of missing. The change value is therefore wrong.
*------------------------------------------------------------------------------;
data bug;
set advs;
by USUBJID VISITNUM;
prev_aval = lag(AVAL);
change = AVAL - prev_aval;
run;
proc print data=bug noobs;
title 'BUG: LAG leaks across subjects';
run;
*==============================================================================;
* FIX: Guard with first.USUBJID
*==============================================================================;
* We still call LAG unconditionally (so the queue always advances), but
* we blank out the result on the first row of each subject.
*------------------------------------------------------------------------------;
data fixed;
set advs;
by USUBJID VISITNUM;
prev_aval = lag(AVAL);
if first.USUBJID then do;
prev_aval = .;
change = .;
end;
else do;
change = AVAL - prev_aval;
end;
run;
proc print data=fixed noobs;
title 'FIXED: LAG guarded by first.USUBJID';
run;- We sort ADVS by USUBJID and VISITNUM so each subject's visits appear in chronological order.
- In the BUG example,
prev_aval = lag(AVAL)is called on every row, but we never check whether the row is the start of a new subject. - On the first row of 101-002 (VISITNUM 1, AVAL 145), LAG returns 120 — the last AVAL from 101-001. The computed change (145 − 120 = 25) is clinically meaningless because it spans two different subjects.
- In the FIX, we still call
lag(AVAL)unconditionally on every row so the queue advances properly. - We then check
first.USUBJID: when it is TRUE, we set both prev_aval and change to missing, discarding the cross-subject leak. - The
elsebranch only fires for rows that are not the first in their subject, so the change calculation is safe. - This guard-with-first-dot pattern is the standard clinical SAS idiom for any LAG operation that must respect BY groups.
- After running, we compare BUG and FIXED side by side to confirm that VISITNUM 1 rows now show missing for prev_aval and change.