KM Plot (PFS) — Figure 14.2.x.x¶

Program: f_14_2_x_x_pfs_km_plot_saffl.sas
Study: MYFAKESTUDY
Output: Figure 14.2.x.x Kaplan–Meier Plot of Progression-Free Survival by Treatment Group
Population: Safety Analysis Set (SAFFL='Y')
14_2_x_x


Purpose¶

Create the Kaplan–Meier PFS figure per SAP, including:

  • Survival S(t) or failure 1–S(t) curve (per SAP)
  • Censored observations (markers)
  • Numbers at risk at selected time points

Inputs¶

  • ADaM.ADSL — treatment, flags/denominators
  • ADaM.ADTTE — time-to-event records (PARAMCD='PFS' or per SAP)

Outputs¶

  • /outputs/rtf/f_14_2_x_x_pfs_km_plot_saffl.rtf
  • /outputs/datasets/f_14_2_x_x_kmplotdata.sas7bdat

Key Variables¶

  • ADSL: USUBJID, TRT01A / TRT01AN, SAFFL
  • ADTTE: USUBJID, PARAMCD, AVAL (time), CNSR

Methods¶

KM estimation via PROC LIFETEST (or SGRENDER from KM outputs):

  • Curve: S(t) or 1–S(t) (per SAP)
  • Censoring markers from censored records
  • At-risk counts via TIMELIST= and (template) BLOCKPLOT in INNERMARGIN
  • Group styling via attribute map (color/pattern/symbol), if required

Author: Alpha Traore
Date: <YYYY-MM-DD>
QC Program: qc_f_14_2_x_x_pfs_km_plot_saffl.sas


Change Log¶

Version Date By Description
v1.0 YYYY-MM-DD <Name> Initial version
v1.1 YYYY-MM-DD <Name> <What changed>

Main program steps (outline)

  1. Build analysis sets: merge ADSL + ADTTE(PFS), apply SAFFL and PARAMCD filter
  2. Derive censoring counts/percents by treatment
  3. KM: percentiles + survival rates at Day 0/50/100/150 (per SAP)
  4. Cox model: HR (95% CI) for treatment comparison (and p-value if required)
  5. Assemble final shell order + labels
  6. Produce RTF/PDF output with titles/footnotes
In [14]:
options mprint mlogic symbolgen; 
options validvarname=upcase;
227  ods listing close;ods html5 (id=saspy_internal) options(bitmap_mode='inline') device=svg style=HTMLBlue; ods graphics on /
227! outputfmt=png;
NOTE: Writing HTML5(SASPY_INTERNAL) Body file: sashtml13.htm
228  
229  options mprint mlogic symbolgen;
230  options validvarname=upcase;
231  ods html5 (id=saspy_internal) close;ods listing;
232  


Read the ADaM Datasets Required for Table Generation: ADSL and ADTTE

In [15]:
libname aa "/export/viya/homes/alpha@alphatraore.com/toydata";

/* proc datasets lib=aa; quit;*/   


proc sort data=aa.adsl  out = adsl  (obs=5);  by usubjid; run;
proc sort data=aa.adtte out = adtte (obs=5); by usubjid; run;

data subj;
   set adsl;
   where saffl="Y";
   trt=trt01an;
run;
 
data tte;
   set adtte;
   trt=trtan;
run;
proc sort data=subj; by usubjid; run;
proc sort data=tte; by usubjid; run;

data temp1;
    merge subj tte;
    by usubjid;
run;
proc print data=temp1(obs=5); run;
SAS Output
Obs USUBJID TRT01AN SAFFL TRT TRTAN PARAM PARAMCD AVAL CNSR PARAMN
1 01-701-1015 1 Y 1 1 Progression Free Survival PFS 2 0 1
2 01-701-1023 1 Y 1 1 Progression Free Survival PFS 3 0 1
3 01-701-1028 2 Y 2 2 Progression Free Survival PFS 3 0 1
4 01-701-1033 3 Y 3 3 Progression Free Survival PFS 28 1 1
5 01-701-1034 2 Y 2 2 Progression Free Survival PFS 58 0 1

Kaplan–Meier Cumulative Incidence (Failure) Estimate¶

  • alphaqt=: significance level for quartile (e.g., median) confidence limits
  • method= / conftype=: KM method and CI type
  • plots=: request failure plot (1–S(t)) with numbers at risk
  • timelist=: specify reporting/display time points and limit ProductLimitEstimates rows

Output: Time points and numbers at risk¶

Produce the KM failure plot with time-point markers on the x-axis and numbers at risk shown below the x-axis at the specified timelist= time points.

In [16]:
proc lifetest data=temp1
   alphaqt=0.05
   method=km
   plots=survival(failure atrisk=0 to 200 by 25 )
   timelist=(0 to 200 by 25)
   conftype=linear ;
 
   time aval*cnsr(1);
   strata trtan;
   ods output FailurePlot  = sur_fail ProductLimitEstimates=estimates;
run;
SAS Output

The LIFETEST Procedure

 

Stratum 1: Actual Treatment (N) = 1

Product-Limit Survival Estimates
Timelist AVAL   Survival Failure Survival Standard Error Number
Failed
Number
Left
0.000 0.000   1.0000 0 0 0 86
25.000 24.000   0.8811 0.1189 0.0353 10 72
50.000 48.000   0.7812 0.2188 0.0457 18 61
75.000 70.000   0.7414 0.2586 0.0488 21 53
100.000 97.000   0.6575 0.3425 0.0540 27 47
125.000 110.000   0.6435 0.3565 0.0546 28 45
150.000 110.000   0.6435 0.3565 0.0546 28 40
175.000 110.000   0.6435 0.3565 0.0546 28 37
200.000 .   . . . 29 0

Summary Statistics for Time Variable AVAL

Quartile Estimates
Percent Point
Estimate
95% Confidence Interval
Transform [Lower Upper)
75 . LINEAR . .
50 . LINEAR . .
25 70.000 LINEAR 35.000 177.000
Mean Standard
Error
129.866 7.534

Note:The mean survival time and its standard error were underestimated because the largest observation was censored and the estimation was restricted to the largest event time.


The LIFETEST Procedure

 

Stratum 2: Actual Treatment (N) = 2

Product-Limit Survival Estimates
Timelist AVAL   Survival Failure Survival Standard Error Number
Failed
Number
Left
0.000 0.000   1.0000 0 0 0 84
25.000 25.000   0.5883 0.4117 0.0566 32 41
50.000 50.000   0.3417 0.6583 0.0576 48 21
75.000 71.000   0.1609 0.8391 0.0490 58 7
100.000 96.000   0.0919 0.9081 0.0411 61 4
125.000 96.000   0.0919 0.9081 0.0411 61 4
150.000 96.000   0.0919 0.9081 0.0411 61 4
175.000 96.000   0.0919 0.9081 0.0411 61 3
200.000 .   . . . 61 0

Summary Statistics for Time Variable AVAL

Quartile Estimates
Percent Point
Estimate
95% Confidence Interval
Transform [Lower Upper)
75 58.000 LINEAR 47.000 89.000
50 36.000 LINEAR 24.000 46.000
25 14.000 LINEAR 5.000 22.000
Mean Standard
Error
40.075 3.689

Note:The mean survival time and its standard error were underestimated because the largest observation was censored and the estimation was restricted to the largest event time.


The LIFETEST Procedure

 

Stratum 3: Actual Treatment (N) = 3

Product-Limit Survival Estimates
Timelist AVAL   Survival Failure Survival Standard Error Number
Failed
Number
Left
0.000 0.000   1.0000 0 0 0 84
25.000 25.000   0.6390 0.3610 0.0538 29 49
50.000 48.000   0.3761 0.6239 0.0565 48 23
75.000 71.000   0.2751 0.7249 0.0545 54 15
100.000 100.000   0.2018 0.7982 0.0508 58 11
125.000 119.000   0.1467 0.8533 0.0458 61 7
150.000 126.000   0.1258 0.8742 0.0438 62 6
175.000 126.000   0.1258 0.8742 0.0438 62 5
200.000 .   . . . 62 0

Summary Statistics for Time Variable AVAL

Quartile Estimates
Percent Point
Estimate
95% Confidence Interval
Transform [Lower Upper)
75 80.000 LINEAR 51.000 119.000
50 33.000 LINEAR 27.000 48.000
25 19.000 LINEAR 15.000 25.000
Mean Standard
Error
51.706 4.941

Note:The mean survival time and its standard error were underestimated because the largest observation was censored and the estimation was restricted to the largest event time.

Summary of the Number of Censored and Uncensored Values
Stratum TRTAN Total Failed Censored Percent
Censored
1 1 86 29 57 66.28
2 2 84 61 23 27.38
3 3 84 62 22 26.19
Total   254 152 102 40.16

The LIFETEST Procedure

Testing Homogeneity of Survival Curves for AVAL over Strata

Rank Statistics
TRTAN Log-Rank Wilcoxon
1 -45.268 -6456.0
2 25.627 3984.0
3 19.641 2472.0
Covariance Matrix for the Log-Rank Statistics
TRTAN 1 2 3
1 34.8938 -15.5885 -19.3053
2 -15.5885 25.8988 -10.3102
3 -19.3053 -10.3102 29.6155
Covariance Matrix for the Wilcoxon Statistics
TRTAN 1 2 3
1 984202 -458582 -525619
2 -458582 828329 -369746
3 -525619 -369746 895366
Test of Equality over Strata
Test Chi-Square DF Pr >
Chi-Square
Log-Rank 60.2696 2 <.0001
Wilcoxon 43.8983 2 <.0001
-2Log(LR) 100.9378 2 <.0001
Product-Limit Failure Curves with Number of Subjects at Risk
In [17]:
title "FailurePlot";
proc print data=sur_fail (obs=5); run;
SAS Output

FailurePlot

Obs TIME SURVIVAL ATRISK EVENT CENSORED TATRISK STRATUM STRATUMNUM _1_SURVIVAL_ _1_CENSORED_
1 0 1.00000 86 0 . . 1 1 0.000000 .
2 0 . 86 . . 0 1 1 . .
3 1 0.98837 86 1 . . 1 1 0.011628 .
4 2 0.97674 85 1 . . 1 1 0.023256 .
5 3 0.95349 84 2 . . 1 1 0.046512 .
In [18]:
title "ProductLimitEstimates";
proc print data=estimates (obs=5); run;
SAS Output

ProductLimitEstimates

Obs STRATUM TRTAN TIMELIST AVAL CENSOR SURVIVAL FAILURE STDERR FAILED LEFT
1 1 1 0.000 0.000 0 1.0000 0 0 0 86
2 1 1 25.000 24.000 0 0.8811 0.1189 0.0353 10 72
3 1 1 50.000 48.000 0 0.7812 0.2188 0.0457 18 61
4 1 1 75.000 70.000 0 0.7414 0.2586 0.0488 21 53
5 1 1 100.000 97.000 0 0.6575 0.3425 0.0540 27 47

Select the number of subjects at risk¶

Definition: Number at risk = count of subjects who have not experienced the event and have not been censored immediately prior to a given time point.

Programming note: Display the number at risk at selected time points on the Kaplan–Meier (failure/cumulative incidence) plot.

In [19]:
data est(rename=(timelist=time stratum=stratumnum));
   set estimates(keep=stratum timelist left );
run;

proc print data=est (obs=8); run;
SAS Output

ProductLimitEstimates

Obs STRATUMNUM TIME LEFT
1 1 0.000 86
2 1 25.000 72
3 1 50.000 61
4 1 75.000 53
5 1 100.000 47
6 1 125.000 45
7 1 150.000 40
8 1 175.000 37

Output: Time-point block plot and numbers at risk¶

Generate a Kaplan–Meier figure that includes:

  • Time-point block plot (tick/marker display):
    Marks the requested time points along the x-axis for consistent visual reference.

  • Numbers at risk:
    Displays the number of subjects at risk immediately prior to each selected time point beneath the x-axis.

Implementation notes

  • Use PLOTS= to request the failure plot and the at-risk table.
  • Use TIMELIST= to align the displayed/reporting time points across the plot and output datasets.
In [20]:
proc sort data=sur_fail;
   by stratumnum time;
run;
 
data new_survival (drop=j);
   merge est sur_fail ;
   length param_t $100;
   by stratumnum time;
      do j = 0 to 200 by 25;
          if time = j  then do;
            blkrsk = put(left,3.);
            blkx   = time;
         end;
      end;
    trtan = stratumnum;
    param_t="parameter name";
    paramn=1;
run;
 
proc sort data=new_survival;
   by trtan time;
run;

title "Time-point block plot and numbers at risk";
proc print data=new_survival (obs=10); run;
SAS Output

Time-point block plot and numbers at risk

Obs STRATUMNUM TIME LEFT SURVIVAL ATRISK EVENT CENSORED TATRISK STRATUM _1_SURVIVAL_ _1_CENSORED_ PARAM_T BLKRSK BLKX TRTAN PARAMN
1 1 0.000 86 1.00000 86 0 . . 1 0.000000 . parameter name 86 0 1 1
2 1 0.000 86 . 86 . . 0 1 . . parameter name 86 0 1 1
3 1 1.000 . 0.98837 86 1 . . 1 0.011628 . parameter name   . 1 1
4 1 2.000 . 0.97674 85 1 . . 1 0.023256 . parameter name   . 1 1
5 1 3.000 . 0.95349 84 2 . . 1 0.046512 . parameter name   . 1 1
6 1 7.000 . 0.94186 82 1 . . 1 0.058140 . parameter name   . 1 1
7 1 8.000 . . 81 0 0.94186 . 1 . 0.058140 parameter name   . 1 1
8 1 9.000 . 0.93009 80 1 . . 1 0.069913 . parameter name   . 1 1
9 1 12.000 . . 79 0 0.93009 . 1 . 0.069913 parameter name   . 1 1
10 1 13.000 . . 78 0 0.93009 . 1 . 0.069913 parameter name   . 1 1

Build PROC TEMPLATE for the Kaplan–Meier Graph¶

  • Use dynamic statements to parameterize titles, labels, and display options.
  • Use ENTRYTITLE to define the graph title (and optional subtitle).
  • Apply attribute mapping for consistent group styling:
    • Line: color and pattern
    • Marker: color and symbol
  • Display numbers at risk in the inner margin, printed with BLOCKPLOT at selected time points.
  • Customize axes (line, ticks, tick values, labels) and set the appropriate axis type with its type-specific options.
  • In the KM plot section, layer two plot types:
    • STEPPLOT for the KM curve
    • SCATTERPLOT for censored observations (and/or time-point markers)
In [21]:
proc template;
   define statgraph kmplot;
   dynamic x_var y_var1 y_var2 ;
    begingraph;
         *Graph title;
          entrytitle "Category = Time to Event"  /
          textattrs=(size=9pt ) pad=(bottom=20px);
 
         *Colour map;
         discreteattrmap name='colors' / ignorecase=true;
            value 'Placebo'  / lineattrs=(color=blue pattern=shortdash) markerattrs=(color=blue symbol=trianglefilled);
            value 'Low Dose'     / lineattrs=(color=red  pattern=shortdash) markerattrs=(color=red symbol=circlefilled);
            value 'High Dose'     / lineattrs=(color=green  pattern=shortdash) markerattrs=(color=green symbol=squarefilled);
         enddiscreteattrmap;
 
         discreteattrvar attrvar=gmarker var=trtan attrmap='colors';
 
            %*Start KM plot*;
            layout overlay /
               xaxisopts=(Label="Time at Risk (days)"
               display=(tickvalues line label ticks )
               type=linear
               linearopts=(tickvaluesequence=(start=0 end=200 increment=25)
                    viewmin=0 viewmax=200))
               yaxisopts=( Label="Cumulative Incidence of Subjects with Event"
               type=linear  linearopts= (viewmin=0 viewmax=1) );
 
               StepPlot X=x_var Y=y_var1 / primary=true Group=gmarker
                  LegendLabel="Cumulative Incidence of Subjects with Event" NAME="STEP";
 
               %*Censored observations are suppressed but can be added here*;
               scatterPlot X=x_var Y=y_var2 / Group=gmarker markerattrs=(symbol=plus)
                    LegendLabel="Censored" NAME="SCATTER";
 
               Mergedlegend "STEP" "SCATTER"/
                  location=inside halign=left valign=top across=1 valueattrs=(family="Arial" size=8pt);
               referenceline y=0;
 
            %*Start at risk value plot;
            innermargin;
               axistable x=blkx value=blkrsk / class=gmarker colorgroup=gmarker
                  display=(label)
                  labelattrs=(family="Arial" size=8pt)
                  valueattrs=(family="Arial" size=8pt);
 
 
            endinnermargin;
            endlayout;
      endgraph;
   end;
run;
328  ods listing close;ods html5 (id=saspy_internal) options(bitmap_mode='inline') device=svg style=HTMLBlue; ods graphics on /
328! outputfmt=png;
NOTE: Writing HTML5(SASPY_INTERNAL) Body file: sashtml20.htm
329  
330  proc template;
331     define statgraph kmplot;
332     dynamic x_var y_var1 y_var2 ;
333      begingraph;
334           *Graph title;
335            entrytitle "Category = Time to Event"  /
336            textattrs=(size=9pt ) pad=(bottom=20px);
337  
338           *Colour map;
339           discreteattrmap name='colors' / ignorecase=true;
340              value 'Placebo'  / lineattrs=(color=blue pattern=shortdash) markerattrs=(color=blue symbol=trianglefilled);
341              value 'Low Dose'     / lineattrs=(color=red  pattern=shortdash) markerattrs=(color=red symbol=circlefilled);
342              value 'High Dose'     / lineattrs=(color=green  pattern=shortdash) markerattrs=(color=green symbol=squarefilled);
343           enddiscreteattrmap;
344  
345           discreteattrvar attrvar=gmarker var=trtan attrmap='colors';
346  
347              %*Start KM plot*;
348              layout overlay /
349                 xaxisopts=(Label="Time at Risk (days)"
350                 display=(tickvalues line label ticks )
351                 type=linear
352                 linearopts=(tickvaluesequence=(start=0 end=200 increment=25)
353                      viewmin=0 viewmax=200))
354                 yaxisopts=( Label="Cumulative Incidence of Subjects with Event"
355                 type=linear  linearopts= (viewmin=0 viewmax=1) );
356  
357                 StepPlot X=x_var Y=y_var1 / primary=true Group=gmarker
358                    LegendLabel="Cumulative Incidence of Subjects with Event" NAME="STEP";
359  
360                 %*Censored observations are suppressed but can be added here*;
361                 scatterPlot X=x_var Y=y_var2 / Group=gmarker markerattrs=(symbol=plus)
362                      LegendLabel="Censored" NAME="SCATTER";
363  
364                 Mergedlegend "STEP" "SCATTER"/
365                    location=inside halign=left valign=top across=1 valueattrs=(family="Arial" size=8pt);
366                 referenceline y=0;
367  
368              %*Start at risk value plot;
369              innermargin;
370                 axistable x=blkx value=blkrsk / class=gmarker colorgroup=gmarker
371                    display=(label)
372                    labelattrs=(family="Arial" size=8pt)
373                    valueattrs=(family="Arial" size=8pt);
374  
375  
376              endinnermargin;
377              endlayout;
378        endgraph;
379     end;
NOTE: Overwriting existing template/link: Kmplot
NOTE: STATGRAPH 'Kmplot' has been saved to: WORK.TEMPLAT
380  run;
NOTE: PROCEDURE TEMPLATE used (Total process time):
      real time           0.00 seconds
      cpu time            0.00 seconds
      

381  ods html5 (id=saspy_internal) close;ods listing;
382  


Set options for the graph¶

  • Turn on ODS Graphics and set output quality (DPI), size (height/width), and border.
  • Define titles and footnotes for the figure.
  • Choose the output destination (inline for Jupyter, or file output such as PNG/RTF/PDF) and apply the desired style.
  • Set image naming/path options when saving files.

Create a format for treatment display¶

  • Create a user-defined format to present treatment groups consistently in outputs.
  • Use the stratum (treatment) numeric values when building the format so labels align with the KM STRATA ordering and report presentation.
In [22]:
proc format;
   value trtanf
   1="Placebo"
   2="TRT A"
   3="TRT B"
   ;
run;
383  ods listing close;ods html5 (id=saspy_internal) options(bitmap_mode='inline') device=svg style=HTMLBlue; ods graphics on /
383! outputfmt=png;
NOTE: Writing HTML5(SASPY_INTERNAL) Body file: sashtml21.htm
384  
385  proc format;
386     value trtanf
387     1="Placebo"
388     2="TRT A"
389     3="TRT B"
390     ;
NOTE: Format TRTANF is already on the library WORK.FORMATS.
NOTE: Format TRTANF has been output.
391  run;

NOTE: PROCEDURE FORMAT used (Total process time):
      real time           0.00 seconds
      cpu time            0.00 seconds
      

392  ods html5 (id=saspy_internal) close;ods listing;
393  


In [23]:
ods listing image_dpi=300;

ods graphics on / reset=all border=off
  height=13cm width=14.72cm
  antialiasmax=20000;

title1 "Cumulative Incidence Plot";
title2 "Safety Analysis Set";


/* Generate the graph using sgrender procedure */
proc sgrender data=new_survival template=kmplot;
   dynamic x_var="time" y_var1="_1_survival_" y_var2="_1_censored_" ;
   format trtan trtanf.;
run;
ods graphics off;
ods rtf close;

title;
footnote;
ods graphics off;
SAS Output

Cumulative Incidence Plot

Safety Analysis Set

svgtitle The SGRender Procedure 86 72 61 53 47 45 40 37 0 84 41 21 7 4 4 4 3 0 84 49 23 15 11 7 6 5 0 0 25 50 75 100 125 150 175 200 Time at Risk (days) 0.0 0.2 0.4 0.6 0.8 1.0 Cumulative Incidence of Subjects with Event Placebo TRT A TRT B TRT B TRT A Placebo Category = Time to Event The SGRender Procedure
In [ ]:

In [ ]: