血药浓度-时间曲线图(药时曲线)是药物被给予机体后,以时间为横坐标,药物(及其代谢物)在血液中的浓度为纵坐标所绘制的血药浓度随时间变化的曲线。
药时曲线能反映体内药物浓度的经时变化过程。为设计采血点、给药方案提供依据。因此绘制药时曲线是药代动力学分析中的一个重要部分。
根据CDE颁布的《药物临床试验数据递交指导原则(试行)》中指出申报资料中的数据集通常采用SAS数据传输格式(SAS Transport Format,简称XPT),由此可见SAS软件在药品临床试验数据分析中是必不可缺。
其中SAS Graph Template Language (GTL)是绘制复杂图形的强大工具。本文将使用SAS编程呈现几种不同的药时曲线样式,探索如何绘制更协调更直观的图形,供大家共同学习讨论!
基于GTL的图形由控制图形格式和外观,并指定变量角色的模板定义生成。图形是通过关联模板与数据源来呈现。
1. 使用template过程定义STATGRAPH模板
2. 使用图形模板语言来指定图形的参数
proc template;
define statgraph tmp1;
/*利用dynamic将template中的变量、标签等内容开放出来,便于修改dynamic的值来控制输出,重复使用*/
dynamic XAXISLABEL YVAR XVAR GRPVAR LENGEDTITLE TICKMIN TICKMAX;
begingraph;
[自定义的图形模板]
endgraph;
end;
run;
3. 通过使用SGRENDER过程将数据与模板关联
proc sgrender data=[数据源] template=[GTL模板];
/*不同表仅修改dynamic即可*/
dynamic XAXISLABEL='Time(h)' YVAR='Mean' XVAR='ATPTN' LENGEDTITLE='剂量组:'
GRPVAR='ARMCD' TICKMIN='0.5' TICKMAX='13';
run;
GTL最强大的特性之一是围绕着分层语句块(即:Layout布局)构建的语法,常用布局:
1. OVERLAY : 用于在单个单元格中显示二维绘图的通用布局
2. GRIDDED : 网格图形,所有单元格独立
3. LATTICE : 升级版多单元格布局。坐标轴可以跨列或行共享,并且位于网格外部
proc template;
define statgraph Simple_Layout;
begingraph;
entrytitle 'Simple Layout';
layout lattice / columns=2 columngutter=5;
layout overlay / walldisplay=(outline) xaxisopts=(display=none) yaxisopts=(display=none);
scatterplot x=weight y=systolic / markerattrs=(size=0);
entry halign=center textattrs=(size=20) "1" / valign=center;
endlayout;
layout overlay / walldisplay=(outline) xaxisopts=(display=none) yaxisopts=(display=none);
scatterplot x=weight y=diastolic / markerattrs=(size=0);
entry halign=center textattrs=(size=20) "2" / valign=center;
endlayout;
endlayout;
endgraph;
end;
run;
proc sgrender data=sashelp.heart template=Simple_Layout;
run;
proc template;
define statgraph Complex_Layout;
begingraph;
layout lattice / columns=2 columnweights=(.6 .4) columngutter=5;
layout lattice / rows=4 rowweights=(.25 .2 .2 .35) rowgutter=5;
layout lattice / columns=2 rowweights=(.6 .4) columngutter=5;
layout overlay / walldisplay=(outline) xaxisopts=(display=none) yaxisopts=(display=none);
scatterplot x=height y=weight / markerattrs=(size=0);
entry halign=center textattrs=(size=20) "1" / valign=center;
endlayout;
layout overlay / walldisplay=(outline) xaxisopts=(display=none) yaxisopts=(display=none);
scatterplot x=height y=weight / markerattrs=(size=0);
entry halign=center textattrs=(size=20) "2" / valign=center;
endlayout;
endlayout;
layout overlay / walldisplay=(outline) xaxisopts=(display=none) yaxisopts=(display=none);
scatterplot x=height y=weight / markerattrs=(size=0);
entry halign=center textattrs=(size=20) "3" / valign=center;
endlayout;
layout overlay / walldisplay=(outline) xaxisopts=(display=none) yaxisopts=(display=none);
scatterplot x=height y=weight / markerattrs=(size=0);
entry halign=center textattrs=(size=20) "4" / valign=center;
endlayout;
layout lattice / columns=2 rowweights=(.6 .4) columngutter=5;
layout overlay / walldisplay=(outline) xaxisopts=(display=none) yaxisopts=(display=none);
scatterplot x=height y=weight / markerattrs=(size=0);
entry halign=center textattrs=(size=20) "5" / valign=center;
endlayout;
layout overlay / walldisplay=(outline) xaxisopts=(display=none) yaxisopts=(display=none);
scatterplot x=height y=weight / markerattrs=(size=0);
entry halign=center textattrs=(size=20) "6" / valign=center;
endlayout;
endlayout;
endlayout;
layout overlay / walldisplay=(outline) xaxisopts=(display=none) yaxisopts=(display=none);
scatterplot x=height y=weight / markerattrs=(size=0);
entry halign=center textattrs=(size=20) "7" / valign=center;
endlayout;
endlayout;
endgraph;
end;
run;
ods graphics / reset width=5in height=5in imagename='Complex_Layout';
proc sgrender data=sashelp.class template=Complex_Layout;
run;
准备一份不同采血点的血药浓度Mean±SD的模拟数据:
data test;
input ARMCD $10. ATPTN Mean Meanlow Meanhig;
datalines;
剂量组1 1 212.5 206.1 218.9
剂量组1 2 141.9 66.8 217.0
剂量组1 3 118.7 50.4 187.0
剂量组1 4 87.4 41.3 133.5
剂量组1 6 58.4 26.9 89.8
剂量组1 8 40.7 15.7 65.6
剂量组1 12 19.9 7.9 31.9
剂量组2 1 87.7 23.5 151.8
剂量组2 2 135.3 135.3 290.5
剂量组2 3 126.4 126.4 282.8
剂量组2 4 113.3 113.3 250.1
剂量组2 6 81.9 81.9 175.4
剂量组2 8 72.0 4.1 139.9
剂量组2 12 45.5 16.5 74.4
剂量组3 1 355.5 83.3 627.7
剂量组3 2 290.5 107.4 473.6
剂量组3 3 303.0 206.8 399.2
剂量组3 4 217.0 163.3 270.7
剂量组3 6 166.0 110.8 221.2
剂量组3 8 129.5 92.0 167.0
剂量组3 12 84.4 53.7 115.0
;
run;
药时曲线的血药浓度所对应的坐标轴通常采用线性和半对数两种不同方式呈现。
1. 使用lattice作为最外层布局
2. 内嵌两个overlay作为基础单元格布局,分别容纳线性药时曲线和半对数药时曲线
proc template;
define statgraph tmp1;
dynamic XAXISLABEL YVAR XVAR GRPVAR LENGEDTITLE TICKMIN TICKMAX;
begingraph;
layout lattice / columns=2 rows=1;
column2headers;
entry "线性药时曲线";
entry "半对数药时曲线";
endcolumn2headers;
layout overlay/
xaxisopts=(label=XAXISLABEL offsetmin=0 offsetmax=0
linearopts=(tickvaluelist=(0 1 2 3 4 6 7 8 12) viewmin=TICKMIN viewmax=TICKMAX TICKVALUEFITPOLICY=ROTATE)
tickvalueattrs=(size=7pt) labelattrs=(size=7pt))
yaxisopts=(label="Concentration (ng/mL)") walldisplay=none ;
seriesplot y=YVAR x=XVAR / group=GRPVAR name="stocks1";
scatterplot y=YVAR x=XVAR / group=GRPVAR name="stocks2"
YERRORLOWER=meanlow YERRORUPPER=meanhig markerattrs=(symbol=squarefilled size=8) ;
mergedlegend "stocks1" "stocks2" /title=LENGEDTITLE border=true across=4 DISPLAYCLIPPED=TRUE;
endlayout;
layout overlay/
xaxisopts=(label=XAXISLABEL offsetmin=0 offsetmax=0
linearopts=(tickvaluelist=(0 1 2 3 4 6 7 8 12) viewmin=TICKMIN viewmax=TICKMAX TICKVALUEFITPOLICY=ROTATE)
tickvalueattrs=(size=7pt) labelattrs=(size=7pt))
yaxisopts=(label="Concentration (ng/mL)" TYPE=LOG LOGOPTS=(base=10)) walldisplay=none ;
seriesplot y=YVAR x=XVAR / group=GRPVAR name="stocks3";
scatterplot y=YVAR x=XVAR / group=GRPVAR name="stocks4"
YERRORLOWER=meanlow2 YERRORUPPER=meanhig markerattrs=(symbol=squarefilled size=8) ;
mergedlegend "stocks3" "stocks4"/title=LENGEDTITLE /*border=true*/ across=4 DISPLAYCLIPPED=TRUE;
endlayout;
endlayout;
endgraph;
end;
run;
proc sgrender data=test template=tmp1;
dynamic XAXISLABEL='Time(h)' YVAR='Mean' XVAR='ATPTN' LENGEDTITLE='剂量组:' GRPVAR='ARMCD' TICKMIN='0.5' TICKMAX='13';
run;
将线性药时曲线和半对数曲线直接横向排列起来是最简单省力的方法,同时也有一些缺点,如:
• 图例重复
• 图形中大面积空白,缺少层次
药时曲线的消除相部分有大量空白,尝试将半对数药时曲线缩小填充到空白部分,提高图形层次感。
1. 使用overlay作为最外层布局,容纳线性药时曲线
2. 内嵌gridded作为独立的单元格布局,嵌入半对数药时曲线
解决了
• 图例重复
• 图形合并
但不同分组标记形状一致,仅以颜色来区分的效果较差。
proc template;
define statgraph tmp2;
dynamic XAXISLABEL YVAR XVAR GRPVAR LENGEDTITLE TICKMIN TICKMAX;
begingraph;
layout overlay/
xaxisopts=(label=XAXISLABEL offsetmin=0 offsetmax=0
linearopts=(tickvaluelist=(1 2 3 4 6 7 8 12) viewmin=TICKMIN viewmax=TICKMAX TICKVALUEFITPOLICY=ROTATE)
tickvalueattrs=(size=7pt) labelattrs=(size=7pt))
yaxisopts=(label="Concentration (ng/mL)" TYPE=LINEAR) ;
seriesplot y=YVAR x=XVAR / group=GRPVAR name="scatter";
scatterplot y=YVAR x=XVAR / group=GRPVAR name="scatter2"
YERRORLOWER=meanlow YERRORUPPER=meanhig ;
layout gridded / width=300px height=200px halign=right valign=top;
layout overlay/
xaxisopts=(label=XAXISLABEL offsetmin=0 offsetmax=0
linearopts=(tickvaluelist=(1 2 3 4 6 7 8 12) viewmin=TICKMIN viewmax=TICKMAX TICKVALUEFITPOLICY=ROTATE)
tickvalueattrs=(size=7pt) labelattrs=(size=7pt))
yaxisopts=(label="Concentration (ng/mL)" TYPE=LOG
LOGOPTS=(base=10 tickvaluelist=(1 10 100 1000) viewmin=0 viewmax=1000));
seriesplot y=YVAR x=XVAR / group=GRPVAR;
scatterplot y=YVAR x=XVAR / group=GRPVAR
YERRORLOWER=meanlow YERRORUPPER=meanhig ;
endlayout;
endlayout;
mergedlegend "scatter" "scatter2"/title=LENGEDTITLE across=4 DISPLAYCLIPPED=TRUE;
endlayout;
endgraph;
end;
run;
proc sgrender data=test template=tmp2 ;
dynamic XAXISLABEL='Time(h)' YVAR='Mean' XVAR='ATPTN' LENGEDTITLE='剂量组:' GRPVAR='ARMCD' TICKMIN='0.5' TICKMAX='13';
run;
1. 添加discreteattrmap块,设置不同分组的显示属性,让区分更具辨识度
discreteattrmap name="symbols" / ignorecase=true;
value "XXX" / markerattrs=() lineattrs=() TEXTATTRS=();
enddiscreteattrmap;
discreteattrvar attrvar=groupmarkers var=[分组变量] attrmap="symbols";
2. 在半对数药时曲线中使用entry语句添加类似水印效果的文字
entry "LOG SCALE" /valign=bottom TEXTATTRS=(COLOR=CXDCDCDC SIZE=15 WEIGHT=BOLD);
proc template;
define statgraph tmp3;
dynamic XAXISLABEL YVAR XVAR GRPVAR LENGEDTITLE TICKMIN TICKMAX;
begingraph;
discreteattrmap name="symbols" / ignorecase=true;
value "剂量组1" / markerattrs=(symbol=triangleFilled color=blue size=9 transparency = 0.6) lineattrs=(color=blue THICKNESS=0.5 PATTERN=4) TEXTATTRS=(size = 11pt family='Times New Roman');
value "剂量组2" / markerattrs=(symbol=circleFilled color=green size=9 transparency = 0.6) lineattrs=(color=green THICKNESS=0.5 PATTERN=4) ;
value "剂量组3" / markerattrs=(symbol=squareFilled color=firebrick size=9 transparency = 0.6) lineattrs=(color=firebrick THICKNESS=0.5 PATTERN=4);
enddiscreteattrmap;
discreteattrvar attrvar=groupmarkers var=GRPVAR attrmap="symbols";
layout overlay/
xaxisopts=(label=XAXISLABEL offsetmin=0 offsetmax=0
linearopts=(tickvaluelist=(1 2 3 4 6 7 8 12) viewmin=TICKMIN viewmax=TICKMAX TICKVALUEFITPOLICY=ROTATE)
tickvalueattrs=(size=7pt) labelattrs=(size=7pt))
yaxisopts=(label="Concentration (ng/mL)" TYPE=LINEAR) ;
seriesplot y=YVAR x=XVAR / group=groupmarkers name="scatter";
scatterplot y=YVAR x=XVAR / group=groupmarkers name="scatter2"
YERRORLOWER=meanlow YERRORUPPER=meanhig ERRORBARATTRS=(THICKNESS=0.5 PATTERN=4);
layout gridded / width=300px height=200px halign=right valign=top;
layout overlay/
xaxisopts=(label=XAXISLABEL offsetmin=0 offsetmax=0
linearopts=(tickvaluelist=(1 2 3 4 6 7 8 12) viewmin=TICKMIN viewmax=TICKMAX TICKVALUEFITPOLICY=ROTATE)
tickvalueattrs=(size=7pt) labelattrs=(size=7pt))
yaxisopts=(label="Concentration (ng/mL)" TYPE=LOG
LOGOPTS=(base=10 tickvaluelist=(1 10 100 1000) viewmin=0 viewmax=1000));
entry "LOG SCALE" /valign=bottom TEXTATTRS=(COLOR=CXDCDCDC SIZE=15 WEIGHT=BOLD);
seriesplot y=YVAR x=XVAR / group=groupmarkers;
scatterplot y=YVAR x=XVAR / group=groupmarkers
YERRORLOWER=meanlow YERRORUPPER=meanhig ERRORBARATTRS=(THICKNESS=0.5 PATTERN=4);
endlayout;
endlayout;
mergedlegend "scatter" "scatter2"/title=LENGEDTITLE across=4 DISPLAYCLIPPED=TRUE;
endlayout;
endgraph;
end;
run;
proc sgrender data=test template=tmp3 ;
dynamic XAXISLABEL='Time(h)' YVAR='Mean' XVAR='ATPTN' LENGEDTITLE='剂量组:' GRPVAR='ARMCD' TICKMIN='0.5' TICKMAX='13';
run;
当前期采血点较为密集导致不同分组的结果不易辨别时,可以将部分时间截取出来放在曲线右上方来增加区分度。
/*===========数据准备=========*/
data test2;
input ARMCD $10. ATPTN Mean Meanlow Meanhig;
datalines;
剂量组1 1 287.1 41.24 532.96
剂量组1 2 343.1 153.44 532.76
剂量组1 3 320.4 175.59 465.21
剂量组1 4 295.9 156.72 435.08
剂量组1 5 268.3 140.22 396.38
剂量组1 6 252.7 136.16 369.24
剂量组1 8 212 114.716 309.284
剂量组1 10 182.1 93.099 271.101
剂量组1 24 84.3 36.133 132.467
剂量组1 48 22.18 8.709 35.651
剂量组1 72 6.207 0.332 12.082
剂量组1 96 0.589 0.589 2.4516
剂量组2 1 388.3 166.36 610.24
剂量组2 2 655.6 390.45 920.75
剂量组2 3 647.4 350.52 944.28
剂量组2 4 608 289.94 926.06
剂量组2 5 548.1 246.31 849.89
剂量组2 6 502.7 222.03 783.37
剂量组2 8 407.7 161.17 654.23
剂量组2 10 346.7 152.92 540.48
剂量组2 24 172.9 76.032 269.768
剂量组2 48 47.56 13.489 81.631
剂量组2 72 13.48 1.219 25.741
剂量组2 96 2.37 2.37 6.5262
剂量组3 1 628.6 355.7 901.5
剂量组3 2 1047 539.45 1554.55
剂量组3 3 1060 585.8 1534.2
剂量组3 4 1008 536.09 1479.91
剂量组3 5 961.7 464.03 1459.37
剂量组3 6 891.4 409.51 1373.29
剂量组3 8 780.2 303.3 1257.1
剂量组3 10 643.9 240.93 1046.87
剂量组3 24 246.6 121.04 372.16
剂量组3 48 60.28 22.872 97.688
剂量组3 72 16.89 3.892 29.888
剂量组3 96 4.387 4.387 9.7029
剂量组4 1 457.3 272.71 641.89
剂量组4 2 924.5 735.7 1113.3
剂量组4 3 1283 818.99 1747.01
剂量组4 4 1550 329.8 2770.2
剂量组4 5 1435 331.3 2538.7
剂量组4 6 1353 365.57 2340.43
剂量组4 8 1101 333.82 1868.18
剂量组4 10 939.3 225.16 1653.44
剂量组4 24 404.8 75.29 734.31
剂量组4 48 103 25.219 180.781
剂量组4 72 24.52 9.958 39.082
剂量组4 96 6.153 1.6598 10.6462
剂量组5 1 1826 370.3 3281.7
剂量组5 2 2700 414.9 4985.1
剂量组5 3 2953 500.4 5405.6
剂量组5 4 2880 733.5 5026.5
剂量组5 5 2593 632.6 4553.4
剂量组5 6 2200 642.6 3757.4
剂量组5 8 1941 488.8 3393.2
剂量组5 10 1896 547.8 3244.2
剂量组5 24 1095 162.49 2027.51
剂量组5 48 421.4 100.4 742.4
剂量组5 72 145 16.36 273.64
剂量组5 96 69.63 9.012 130.248
;
run;
/*调整数据集*/
data test3;
merge test2 test2(where=(ATPTN le 10) rename=(Mean=Mean2 Meanlow=Meanlow2 Meanhig=Meanhig2));
by ARMCD ATPTN;
run;
proc template;
define statgraph tmp4;
dynamic XAXISLABEL YVAR XVAR GRPVAR LENGEDTITLE TICKMIN TICKMAX TICKMAX2;
begingraph;
discreteattrmap name="symbols" / ignorecase=true;
value "剂量组1" / markerattrs=(symbol=triangleFilled color=blue size=9 transparency = 0.6) lineattrs=(color=blue THICKNESS=0.5 PATTERN=4) TEXTATTRS=(size = 11pt family='Times New Roman');
value "剂量组2" / markerattrs=(symbol=circleFilled color=green size=9 transparency = 0.6) lineattrs=(color=green THICKNESS=0.5 PATTERN=4) ;
value "剂量组3" / markerattrs=(symbol=squareFilled color=firebrick size=9 transparency = 0.6) lineattrs=(color=firebrick THICKNESS=0.5 PATTERN=4);
value "剂量组4" / markerattrs=(symbol=HomeDownFilled color=Purple size=9 transparency = 0.6) lineattrs=(color=Purple THICKNESS=0.5 PATTERN=4);
value "剂量组5" / markerattrs=(symbol=StarFilled color=Orange size=9 transparency = 0.6) lineattrs=(color=Orange THICKNESS=0.5 PATTERN=4);
enddiscreteattrmap;
discreteattrvar attrvar=groupmarkers var=GRPVAR attrmap="symbols";
layout overlay/
xaxisopts=(label=XAXISLABEL offsetmin=0 offsetmax=0
linearopts=(tickvaluelist=(1 4 8 10 24 48 72 96) viewmin=TICKMIN viewmax=TICKMAX TICKVALUEFITPOLICY=ROTATE)
tickvalueattrs=(size=7pt) labelattrs=(size=7pt))
yaxisopts=(label="Concentration (ng/mL)" TYPE=LINEAR) ;
seriesplot y=YVAR x=XVAR / group=groupmarkers name="scatter";
scatterplot y=YVAR x=XVAR / group=groupmarkers name="scatter2"
YERRORLOWER=meanlow YERRORUPPER=meanhig ERRORBARATTRS=(THICKNESS=0.5 PATTERN=4);
layout gridded / width=400px height=300px halign=right valign=top;
layout overlay/
xaxisopts=(label=XAXISLABEL offsetmin=0 offsetmax=0
linearopts=(tickvaluelist=(1 2 3 4 6 8 10) viewmin=TICKMIN viewmax=TICKMAX2 TICKVALUEFITPOLICY=ROTATE)
tickvalueattrs=(size=7pt) labelattrs=(size=7pt))
yaxisopts=(label="Concentration (ng/mL)" TYPE=LINEAR) ;
seriesplot y=Mean2 x=XVAR / group=groupmarkers ;
scatterplot y=Mean2 x=XVAR / group=groupmarkers
YERRORLOWER=meanlow2 YERRORUPPER=meanhig2 ERRORBARATTRS=(THICKNESS=0.5 PATTERN=4);
endlayout;
endlayout;
mergedlegend "scatter" "scatter2"/title=LENGEDTITLE across=5 DISPLAYCLIPPED=TRUE;
endlayout;
endgraph;
end;
run;
ods graphics on /width=800px height=400px;
proc sgrender data=test3 template=tmp4 ;
dynamic XAXISLABEL='Time(h)' YVAR='Mean' XVAR='ATPTN' LENGEDTITLE='剂量组:' GRPVAR='ARMCD' TICKMIN='0.5' TICKMAX='100' TICKMAX2='12';
run;
GTL语言具有非常丰富的功能,能够实现复杂的图形绘制。仅以本文的药时曲线为例,其实在美观性的提升上仍有较大空间。
了解GTL的原理,熟练掌握layout的使用,在实践中不断学习、积累和探索,才能绘制更美观更简洁的图形!
熙宁生物|精翰生物已完成临床药理服务平台搭建,正式提供PK、PD、ADA、PK/PD等不同类型临床项目的统计分析服务。
其中统计编程团队具有丰富的统计编程经验,可完成符合CDISC标准的SDTM/ADaM数据集及其相关表格、图表和列表,与生物分析结合形成一站式服务,欢迎大家后台留言咨询交流。
[1] 国家药品监督管理局,药物临床试验数据递交指导原则(试行),2020年第16号.