SAS编程PROC FCMP 笔记
什么是PROC FCMP
FCMP全称是Function compiler,可以自定义函数和函数里的子程序,可以简单理解成R里面的fuction,Python里的def,能够写一些自定义函数,在其他步骤中像内置函数比如SUM(),MEANS(),SUBSTR()一样调用。
这个PROC语句,可能是大多数人在工作中都很少用到的内容,因为它和Macro功能很像但略有不同,本篇文章是对PROC FCMP的学习笔记。

基础用法示例
主要分三步:
用proc fcmp定义函数;
用options cmplib指定函数保存位置;
在程序里调用。
/* 1. 写cal_bmi函数 */
proc fcmp outlib=work.myfuncs.math;
function cal_bmi(weight_lb, height_in);
weight_kg = weight_lb * 0.453592;
height_m = height_in * 0.0254;
if height_m > 0 then
bmi = weight_kg / (height_m ** 2);
else
bmi = .;
return(bmi);
endsub;
run;
/* 2. options 指定函数位置 */
options cmplib=(work.myfuncs);
/* 3. 在data步中调用 */
data class;
set sashelp.class;
bmi = cal_bmi(weight, height);
run;function部分属于是主函数定义,return只能填写函数返回值;一看便知,这function基本上和其他编程语言里的自定义函数一个意思。但是除了指返回一个值的function,FCMP里还可以用subroutine定义多个子程序,用outargs定义多个返回值,增强了功能性,示例为输入in,返回inv,inm两个变量:
/* 定义subroutine子函数 */
proc fcmp outlib=work.funcs.temp;
subroutine inverse(in,inv,inm) group="generic";
outargs inv, inm;
if in=0 then do;
inv=.;
inm=.;
end;
else do;
inv=1/in;
inm = inv + 1;
end;
endsub;
run;
options cmplib=work.funcs;
/* 在data步用call 子函数名调用 */
data a;
x=5;
call inverse(x, y, z);
put x= y= z=;
run;
x=5 y=0.2 z=1.2PROC FCMP的意义
从上面的示例,可以看出PROC FCMP的意义在于将算法模块化,之后能够重复调用,这样封装复杂逻辑就能起到:
让主代码结构保持简洁、优雅;
简化代码维护,如果后续计算规则改变,就只需要修改FCMP里的函数定义;
跨过程步调用,除了DATA步之外,还可以在许多PROC步中调用,让程序更加灵活。

与Macro相比的区别
虽然Macro也有模块化、重复调用的能力,但两者还是有一些区别:
最直接的区别:
Macro是代码生成器,目的是把提前编译好的Macro里的代码文本写在程序里运行,不管里面用了多少宏变量、DATA/PROC步或者只是单行代码,都只是运行预编写的程序而已,这就是为什么用MPRINT能打印Macro的语句;
而FCMP是直接封装了逻辑成为一个内置函数,用MPRINT是无法打印内容的。
什么时候用Macro
需要把一串包含DATA步、PROC步的完整流程封装起来,用Macro;
需要动态生成变量名,如tmp1、tmp2、tmp3...用Macro;
需要执行条件判断、循环执行或者指定的单行/多行代码;
什么时候用FCMP
需要用复杂计算公式计算变量、并且重复调用时;
需要简化DATA步代码,避免过多IF-THEN-ELSE时;
需要和一些PROC语句如REPORT、FORMAT连用时;
运行几万行甚至更多数据,用Macro处理速度比FCMP慢时(解释性Macro语句运行会比编译好的FCMP慢)。
PROC FCMP的应用场景
作为处理临床试验数据的sp,工作内容中任何【行内复杂逻辑计算且需要在不同数据集或项目中复用】的地方,都可以使用FCMP处理数据、减少代码量、降低Validation风险。比如:
ADaM编程中复杂的日期处理和填补;
LB里衍生CTCAE;
eGFR肾小球滤过率的医学公式计算,涉及到性别、种族、年龄以及肌酐值等;
需要把FCMP写成的函数,像内置函数一样用在一些PROC语句里的时候。
实操
官方文档里PROC FCMP里的高级应用其实和实际工作有点远、要么就是能用Macro替代导致学习成本略高。实操这里举三个例子,一个是官方示例,计算study day
计算Study day
proc fcmp outlib=sasuser.funcs.trial;
function study_day(intervention_date, event_date);
n=event_date - intervention_date;
if n >= 0 then
n=n + 1;
return(n);
endsub;
options cmplib=sasuser.funcs;
data _null_;
start='15Feb2010'd;
today='27Mar2010'd;
sd=study_day(start, today);
put sd=;
run;虽然工作中普遍在Macro里计算study day,但是这个案例可以更好地帮助理解PROC FCMP的运作机制。
完成日期填补
用宏的方式写,之后可以改成FCMP,在做ADaM时就可以反复调用,让主程序清爽一点。这个操作可以用Macro也可以用FCMP,只要有自己一套固定的代码流程能够应对大部分工作情况、遇到不一样的情况能修改适配即可。
proc delete data=work._all_;run;quit;
proc fcmp outlib=work.funcs.adam_date;
/* impute astdt */
subroutine impute_astdt(stdtc $, trtsdt, astdt, astdtf $);
outargs astdt, astdtf; /* return this two variable */
astdt = .;
astdtf = "";
/* 1. Complete date (YYYY-MM-DD) */
if length(strip(stdtc)) >= 10 then do;
astdt = input(substr(stdtc,1,10), yymmdd10.);
astdtf = "";
end;
/* 2. Missing day (YYYY-MM) */
else if length(strip(stdtc)) = 7 then do;
dt_year = input(substr(stdtc,1,4), best.);
dt_month = input(substr(stdtc,6,2), best.);
if not missing(trtsdt) and dt_year = year(trtsdt) and dt_month = month(trtsdt) then do;
astdt = trtsdt;
end;
else do;
astdt = input(strip(stdtc) || "-01", yymmdd10.);
end;
astdtf = "D";
end;
/* 3. Missing month and day (YYYY) */
else if length(strip(stdtc)) = 4 then do;
dt_year = input(substr(stdtc,1,4), best.);
if not missing(trtsdt) and dt_year = year(trtsdt) then do;
astdt = trtsdt;
end;
else do;
astdt = input(strip(stdtc) || "-01-01", yymmdd10.);
end;
astdtf = "M";
end;
endsub;
/* impute aendt */
subroutine impute_aendt(endtc $, dthdt, aendt, aendtf $);
outargs aendt, aendtf; /* return this two variable */
aendt = .;
aendtf = "";
aendt_temp = .;
/* 1. Complete date */
if length(strip(endtc)) >= 10 then do;
aendt_temp = input(substr(endtc,1,10), yymmdd10.);
aendtf = "";
end;
/* 2. Missing day */
else if length(strip(endtc)) = 7 then do;
aendt_temp = intnx('month', input(strip(endtc) || "-01", yymmdd10.), 0, 'e');
aendtf = "D";
end;
/* 3. Missing month and day */
else if length(strip(endtc)) = 4 then do;
aendt_temp = input(strip(endtc) || "-12-31", yymmdd10.);
aendtf = "M";
end;
/* 4. select min date in aendt_temp, dthdt */
if not missing(aendt_temp) then do;
aendt = min(aendt_temp, dthdt);
end;
endsub;
run;
options cmplib=work.funcs;
/* 生成adae的主程序 */
data adae1;
length astdt aendt trtsdt dthdt 8. astdtf aendtf $1.;
if _N_ = 1 then do;
dcl hash mergeadsl(dataset:'adam.adsl');
mergeadsl.definekey('usubjid');
mergeadsl.definedata('trtsdt', 'dthdt');
mergeadsl.definedone();
call missing(of trtsdt dthdt astdt aendt astdtf aendtf);
end;
set sdtm.ae;
rc = mergeadsl.find();
call impute_astdt(aestdtc, trtsdt, astdt, astdtf);
call impute_aendt(aeendtc, dthdt, aendt, aendtf);
format astdt aendt yymmdd10.;
run;衍生CTCAE
这可能是SP工作中比较合适用PROC FCMP来做的内容,因为即使是写成Macro,这部分的运行也会造成巨量的文本解析,但是如果用FCMP就能更少文本解析、更快运行速度、更清晰代码结构,工作中根据SAP修改FCMP里面的内容即可,用function输出grade或者用subroutine可以输出更多参数,根据自己的习惯来就好。
proc fcmp outlib=work.funcs.ctcae;
subroutine calc_ctcae(lbtestcd $, val, uln, lbtoxgr $, lbtoxdir $, lbtox $);
/* 输出下面三个参数 */
outargs lbtoxgr, lbtoxdir, lbtox;
/* 前置条件 */
if n(val, uln) < 2 or uln <= 0 then return;
/* 开始计算CTCAE */
if lbtestcd = 'ALT' then do;
r = val / uln; /* 只计算一次倍数,减少重复运算量 */
/* 布尔逻辑输出grade */
g_num = 4 * (r > 20) +
3 * (5 < r <= 20) +
2 * (3 < r <= 5) +
1 * (1 < r <= 3) +
0 * (r <= 1)
;
lbtoxgr = put(g_num, 1.);
if g_num > 0 then do;
lbtoxdir = 'INC';
lbtox = 'Alanine aminotransferase increased';
end;
else do;
call missing(lbtoxdir, lbtox);
end;
end;
endsub;
run;
options cmplib=work.funcs;
/* 主程序调用 */
data lb1;
length lbtoxgr $6 lbtoxdir $3 lbtox $200;
set sdtm.lb(keep=usubjid subjid lbtestcd lbstresn lbstnrhi);
call missing(lbtoxgr, lbtoxdir, lbtox);
call calc_ctcae(lbtestcd, lbstresn, lbstnrhi, lbtoxgr, lbtoxdir, lbtox);
run;