Introduction
Clinical-trial statistical programming produces three major classes of deliverables: Tables, Listings, and Figures, commonly referred to as TLFs.
Although SAS provides many procedures that can contribute to TLF production, PROC REPORT is one of the most versatile tools for producing formatted tabular output.
PROC REPORT can combine characteristics of a data step, a summary procedure, and a reporting procedure in a single framework.
It can:
- Display detail-level observations
- Group observations into categories
- Calculate summary statistics
- Create treatment-arm columns
- Generate derived report columns
- Insert subtotals and totals
- Apply formats and labels
- Control column widths and alignment
- Generate spanning headers
- Produce output through ODS destinations
Where PROC REPORT Fits in a TLF Workflow
PROC REPORT is usually the presentation layer of a larger programming workflow.
A typical clinical-trial table begins with raw or analysis data and passes through several transformations before reaching PROC REPORT.
A Simple PROC REPORT
The simplest PROC REPORT invocation looks like this:
proc report data=adam.adsl nowd; column usubjid age sex; run;
This tells SAS to display three variables:
USUBJIDAGESEX
The NOWD option suppresses the interactive REPORT
window and is commonly used in batch-oriented clinical programming.
The COLUMN Statement
The COLUMN statement defines the variables and
report items that appear in the output.
proc report data=adam.adsl nowd; column usubjid age sex race; run;
The order in the COLUMN statement determines the basic order in the report.
For example:
column usubjid sex age race;
produces a different column arrangement than:
column usubjid age sex race;
This distinction becomes extremely important when developing TLFs because the shell often specifies an exact column order.
The DEFINE Statement
The DEFINE statement describes how each report
item behaves.
proc report data=adam.adsl nowd; column usubjid age sex; define usubjid / display "Subject ID"; define age / display "Age (years)"; define sex / display "Sex"; run;
The DEFINE statement can control:
- Column type
- Display label
- Format
- Width
- Alignment
- Grouping behavior
- Ordering behavior
- Statistical analysis
DISPLAY Variables
A DISPLAY variable shows the value associated with
each detail row.
define usubjid / display "Subject ID"; define age / display "Age"; define sex / display "Sex";
DISPLAY is useful for listings and for descriptive report structures where every input observation should remain represented.
ORDER Variables
An ORDER variable sorts the report according to
that variable and displays its value in the report.
define trt01p / order "Treatment"; define avisitn / order "Visit Number";
ORDER variables are particularly useful when constructing reports with hierarchical sorting.
GROUP Variables
A GROUP variable groups observations with the same
value.
define trt01p / group "Treatment"; define sex / group "Sex";
GROUP is one of the most important PROC REPORT concepts for TLF development.
When a variable is defined as GROUP, PROC REPORT can collapse multiple observations into a single displayed group and calculate statistics across the grouped observations.
ANALYSIS Variables
An ANALYSIS variable is used with a statistical
statistic such as:
- SUM
- MEAN
- MEDIAN
- MIN
- MAX
- N
- NMISS
- STD
For example:
proc report data=adam.adsl nowd;
column age;
define age /
analysis mean
"Mean Age";
run;
PROC REPORT calculates the requested statistic rather than simply printing each underlying value.
Common PROC REPORT Variable Types
| Type | Typical Use |
|---|---|
| DISPLAY | Show detail-level values |
| ORDER | Sort and display ordered values |
| GROUP | Collapse observations into groups |
| ANALYSIS | Calculate statistics |
| ACROSS | Create columns across categories such as treatment |
| COMPUTED | Create report columns from other report items |
Building a Basic Demographics Table
A typical demographics table might display:
Treatment N Placebo 125 Drug A 128 Age Mean 58.4 SD 9.7 Median 59.0 Min, Max 31, 79 Sex Male 72 (57.6%) Female 53 (42.4%)
The final table may look simple, but producing it correctly requires careful data preparation.
A useful principle is:
A Treatment-Arm Table
Suppose the summary dataset contains:
| Parameter | Statistic | Placebo | Drug A | Total |
|---|---|---|---|---|
| Age | Mean | 57.8 | 58.9 | 58.4 |
| Age | SD | 9.4 | 10.0 | 9.7 |
| Male | n (%) | 70 (56.0) | 76 (59.4) | 146 (57.7) |
PROC REPORT can arrange this dataset into the desired table structure.
ACROSS Variables
The ACROSS usage is one of the most powerful
PROC REPORT features for TLFs.
Suppose:
define trt01p / across "Treatment";
PROC REPORT can create a column for each treatment category.
For example:
Placebo Drug A Total
The actual number of columns depends on the report items nested beneath the ACROSS variable.
ACROSS With a Statistic
A common pattern is:
proc report data=summary nowd; column parameter trt01p,n; define parameter / group "Parameter"; define trt01p / across "Treatment"; define n / analysis n "N"; run;
The resulting report can create treatment-specific N columns.
Nested Columns With ACROSS
A common clinical-trial structure is:
Placebo Drug A Total
n % n % n %
Parameter
Category 1
Category 2
PROC REPORT can create this structure by nesting multiple report items under an ACROSS variable.
proc report data=summary nowd; column parameter trt01p,(n pct); define parameter / group "Parameter"; define trt01p / across "Treatment"; define n / analysis sum "n"; define pct / analysis mean "Percent"; run;
The exact implementation depends on how the summary dataset is structured and how the percentage variable has been derived.
Spanning Headers
Clinical tables frequently require multi-level headers.
For example:
| Parameter | Placebo | Drug A | ||
|---|---|---|---|---|
| n | % | n | % | |
| Male | 70 | 56.0 | 76 | 59.4 |
| Female | 55 | 44.0 | 52 | 40.6 |
| Total | 125 | 100.0 | 128 | 100.0 |
Illustrative report layout; actual production formatting depends on the study shell and ODS destination.
Using COLUMN Groups
PROC REPORT allows a parenthesized group of report items in the COLUMN statement.
column parameter trt01p,(n pct);
This is conceptually:
Treatment ├── n └── pct
If treatment has two levels, PROC REPORT can produce:
Placebo Drug A n % n %
This pattern appears frequently in clinical-trial tables.
Why ACROSS Tables Can Be Tricky
ACROSS processing is powerful but can be confusing because the number and order of generated columns depend on the values present in the data.
For example, if the summary dataset unexpectedly contains:
Placebo Drug A Drug B Unknown
PROC REPORT may generate additional columns.
Preloading Formats for Controlled Columns
A formatted treatment variable can be used to control category presentation.
proc format;
value $trtf
"P" = "Placebo"
"A" = "Drug A";
run;
Then:
data summary; set summary; format trt01p $trtf.; run;
For some report structures, the combination of formats and PRELOADFMT can help ensure that formatted
categories are represented consistently.
Statistics in PROC REPORT
PROC REPORT supports many common descriptive statistics.
| Statistic | Example |
|---|---|
| N | Number of nonmissing observations |
| NMISS | Number of missing observations |
| SUM | Sum |
| MEAN | Arithmetic mean |
| MEDIAN | Median |
| STD | Standard deviation |
| MIN | Minimum |
| MAX | Maximum |
For example:
define aval / analysis mean format=8.2 "Mean";
Mean, Standard Deviation, and Median
A common descriptive table may need:
proc report data=adam.adtte nowd; column aval; define aval / analysis mean "Mean"; define aval / analysis median "Median"; define aval / analysis std "SD"; run;
However, repeatedly referencing the same variable in a production report can require careful consideration of report structure and labels.
In many TLF programs, a summary dataset containing one row per parameter and statistic is easier to control.
Summary Dataset Strategy
Instead of asking PROC REPORT to perform every calculation, one robust approach is to create a presentation-ready dataset.
PARAMETER STAT COL1 COL2 Age Mean 57.8 58.9 Age SD 9.4 10.0 Age Median 58.0 59.0 Age Min-Max 31-79 29-81
PROC REPORT can then focus primarily on presentation.
Character Statistics
Clinical tables often need values such as:
58.4 (9.7) or 31, 79 or 70 (56.0%)
These are often best represented as character presentation variables.
For example:
data summary;
length result $40;
result = cats(
put(n,8.),
" (",
put(pct,5.1),
"%)"
);
run;
The resulting character variable can then be displayed directly.
Why Character Presentation Variables Are Common in TLFs
A production table often needs exact control over:
- Decimal places
- Parentheses
- Percent signs
- Missing-value symbols
- Confidence-interval notation
- Footnote markers
- Statistical significance symbols
Representing the final cell as character text can make this presentation logic explicit.
COMPUTE Blocks
A COMPUTE block allows PROC REPORT to calculate or
modify report output.
compute age;
if age < 18 then
call define(
_col_,
"style",
"style=[font_weight=bold]"
);
endcomp;
COMPUTE blocks can be used for:
- Conditional formatting
- Derived values
- Text manipulation
- Column-level presentation logic
- Conditional styles
Computed Columns
A computed report item can be declared in the COLUMN statement and defined
as COMPUTED.
proc report data=summary nowd;
column n pct result;
define n / display "N";
define pct / display "Percent";
define result / computed "Result";
compute result / character length=30;
result = cats(
put(n,8.),
" (",
put(pct,5.1),
"%)"
);
endcomp;
run;
Computed columns are useful when the final report cell depends on multiple existing report items.
COMPUTE BLOCKs and Conditional Formatting
Suppose a p-value should be emphasized when it is below 0.05.
compute pvalue;
if pvalue < 0.05 then
call define(
_col_,
"style",
"style=[font_weight=bold]"
);
endcomp;
This allows formatting decisions to depend on the value being reported.
CALL DEFINE
CALL DEFINE is frequently used within COMPUTE
blocks to modify the current report item.
call define( _col_, "style", "style=[font_weight=bold]" );
The first argument identifies the report item, while the second identifies the attribute being changed.
For example:
call define( "result", "style", "style=[foreground=red]" );
The exact style syntax depends on the ODS destination and reporting standard.
BREAK Statements
The BREAK statement inserts formatting or summary
behavior when the value of a GROUP or ORDER variable changes.
break after trt01p / summarize;
This can create subtotals after each treatment group.
RBREAK Statements
The RBREAK statement applies a break at the end
of the entire report.
rbreak after / summarize;
This is often useful for overall totals.
BREAK Versus RBREAK
| Statement | Purpose |
|---|---|
| BREAK AFTER group | Acts when a GROUP or ORDER value changes |
| RBREAK AFTER | Acts at the end of the report |
For example:
break after trt01p / summarize; rbreak after / summarize;
could conceptually produce:
Placebo Category A Category B Placebo Total Drug A Category A Category B Drug A Total Overall Total
Using COMPUTE AFTER
PROC REPORT also supports COMPUTE blocks associated with breaks.
compute after trt01p; line " "; endcomp;
This can be used to insert lines or perform formatting after a group.
LINE Statements
The LINE statement can insert text into report
output.
compute after; line "Source: ADaM ADSL"; line "Population: Safety"; endcomp;
This can be useful for controlled report annotations, although many production programmers prefer ODS text or other standardized mechanisms for titles and footnotes.
Formatting Numbers
Clinical TLFs require precise numeric formatting.
define age / analysis mean format=8.1 "Mean Age";
A format of 8.1 means the value is displayed with
one decimal place.
For percentages:
format=6.1
may display:
56.0
rather than:
56
The appropriate format should always follow the table shell.
Missing Values
Missing values need explicit consideration in clinical reporting.
Possible presentation conventions include:
- Blank
- NA
- NE
- ND
- Not applicable
- Not estimable
PROC REPORT should not make this decision implicitly.
A production program should have an explicit rule for the relevant endpoint and table.
Labels
Column labels should normally match the approved shell.
define age / display "Age at Baseline (years)";
Labels can also contain multiple lines through SAS label syntax or ODS-specific formatting.
Column Widths
Widths can be controlled using the WIDTH= attribute.
define usubjid / display "Subject ID" style(column)=[cellwidth=1.2in];
The exact behavior can differ by ODS destination, so production development should always validate the final rendered output.
Alignment
Text and numeric columns often need different alignment.
define parameter / display style(column)=[just=left]; define result / display style(column)=[just=right];
Consistent alignment is particularly important when a TLF contains many treatment columns.
Indentation
Clinical tables frequently use hierarchical rows:
Age Mean SD Median Min, Max Sex Male Female
Indentation can be implemented using spaces, style attributes, or presentation variables depending on the reporting framework.
A simple character-variable approach is:
data summary; length label $100; label = " Mean"; run;
A more structured reporting framework may instead use cell-width and indentation styles.
Using the FLOW Option
Long text such as adverse-event descriptions may require wrapping.
define preferred_term / display flow "Preferred Term";
FLOW allows text to wrap within the available column width.
PROC REPORT for Listings
PROC REPORT is also useful for patient listings.
proc report data=adam.adae nowd; column usubjid aedecod aestdt aesev; define usubjid / order "Subject"; define aedecod / display "Preferred Term"; define aestdt / display "Start Date"; define aesev / display "Severity"; run;
A listing differs from a summary table because the goal is generally to preserve patient-level detail.
Ordering a Listing
Listings often require multiple sorting levels.
proc sort data=adam.adae out=ae_sorted; by usubjid aebodsys aedecod aestdt; run; proc report data=ae_sorted nowd; column usubjid aebodsys aedecod aestdt aesev; define usubjid / order "Subject"; define aebodsys / order "System Organ Class"; define aedecod / order "Preferred Term"; define aestdt / display "Start Date"; define aesev / display "Severity"; run;
Sorting before reporting makes the intended ordering explicit and easier to validate.
Grouping Adverse Events
A listing may instead group events by patient.
define usubjid / group "Subject";
The choice between ORDER and GROUP should be deliberate because GROUP can collapse repeated values.
Pagination
Clinical reports frequently span multiple pages.
PROC REPORT provides options and ODS controls for pagination.
A common pattern is:
options orientation=landscape;
proc report data=summary nowd
split='|';
column parameter trt01p,(n pct);
define parameter /
group
"Parameter";
run;
The actual page dimensions depend on the ODS destination, orientation, margins, font, and other settings.
Landscape Output
Wide treatment-arm tables often require landscape orientation.
options orientation=landscape;
This is particularly common for:
- Laboratory summaries
- Vital-sign tables
- Shift tables
- Adverse-event summaries
- Exposure summaries
- Efficacy tables with many treatment groups
ODS PDF
PROC REPORT can generate PDF output through ODS PDF.
ods pdf file="demographics.pdf" style=journal; proc report data=summary nowd; column parameter result; define parameter / display "Parameter"; define result / display "Result"; run; ods pdf close;
In production environments, the style, margins, fonts, page numbering, headers, and footers are typically controlled by the study's reporting framework.
ODS RTF
RTF remains common for clinical-trial deliverables because it can be reviewed and incorporated into document-based reporting workflows.
ods rtf file="demographics.rtf" style=journal; proc report data=summary nowd; column parameter result; define parameter / display "Parameter"; define result / display "Result"; run; ods rtf close;
ODS HTML
HTML is useful for development, review, and web-based presentation.
ods html5 file="demographics.html" style=htmlblue; proc report data=summary nowd; column parameter result; define parameter / display "Parameter"; define result / display "Result"; run; ods html5 close;
The HTML output is particularly useful during development because it can make it easier to inspect report structure before generating final PDF or RTF deliverables.
Titles and Footnotes
A production TLF normally needs controlled titles and footnotes.
title1 "Table 14.1.1"; title2 "Demographic Characteristics"; title3 "Safety Population"; footnote1 "Source: ADSL"; footnote2 "Percentages are based on the number of subjects in each treatment group."; proc report data=summary nowd; column parameter result; define parameter / display "Parameter"; define result / display "Result"; run;
Titles and footnotes should generally be driven by the approved TLF shell rather than written ad hoc during programming.
Dynamic Titles
Macro variables can be used to construct reusable titles.
%let population = Safety Population; title1 "Table 14.1.1"; title2 "Demographic Characteristics"; title3 "&population";
This can be particularly useful when a single program produces related tables for multiple analysis populations.
Using SPLIT Characters
PROC REPORT can split column headings across lines.
proc report data=summary nowd
split='|';
column usubjid age sex;
define usubjid /
display
"Subject|Identifier";
define age /
display
"Age at|Baseline";
run;
The SPLIT character is especially useful when a shell contains compact, multi-line headers.
Spanning Headers With ACROSS
One of the classic TLF layouts is:
Treatment
-------------------------
Placebo Drug A
n % n %
Parameter
Category 1
Category 2
A PROC REPORT structure might be:
proc report data=summary nowd
split='|';
column parameter trt01p,(n pct);
define parameter /
group
"Parameter";
define trt01p /
across
"Treatment";
define n /
analysis sum
"n";
define pct /
analysis mean
"%";
run;
The exact statistic definitions depend on the summary dataset.
PROC REPORT and Pre-Summarized Data
A very common production strategy is to summarize the analysis data before calling PROC REPORT.
For example:
proc summary data=adam.adsl nway;
class trt01p sex;
output out=sex_summary
n=n;
run;
The resulting dataset can then be formatted for reporting.
data sex_summary;
set sex_summary;
pct = 100 * n / total_n;
length result $30;
result = cats(
put(n,8.),
" (",
put(pct,5.1),
"%)"
);
run;
PROC REPORT can then simply display the resulting values.
PROC REPORT Versus PROC TABULATE
Both procedures can produce complex tables, but their strengths differ.
| Feature | PROC REPORT | PROC TABULATE |
|---|---|---|
| Flexible row structure | Excellent | Good |
| Listings | Excellent | Limited |
| COMPUTE blocks | Yes | No equivalent |
| Conditional formatting | Excellent | More limited |
| ACROSS-style tables | Yes | Yes |
| Complex presentation logic | Excellent | Moderate |
PROC REPORT is often preferred when the report requires substantial presentation logic.
PROC REPORT Versus PROC SQL
PROC SQL is primarily a data manipulation and querying procedure.
PROC REPORT is primarily a reporting procedure.
A common TLF workflow is therefore:
PROC SQL / DATA STEP
↓
Analysis dataset
↓
PROC SUMMARY / MEANS
↓
Presentation dataset
↓
PROC REPORT
↓
ODS PDF / RTF / HTML
Creating an Adverse-Event Summary
Adverse-event tables are among the most common uses of PROC REPORT.
A typical structure is:
System Organ Class Preferred Term Placebo Drug A Headache 18 (14.4%) 25 (19.5%) Nausea 9 (7.2%) 17 (13.3%) Fatigue 12 (9.6%) 21 (16.4%)
The source data may contain one or more records per subject and event.
Before PROC REPORT, the programmer typically determines the appropriate subject-level counting rule.
Counting Distinct Subjects
PROC REPORT itself should not be expected to infer the correct clinical counting rule.
For example, if a subject experiences headache three times, the table may need to count:
1 subject
rather than:
3 events
A common strategy is to derive a subject-level flag before summarization.
proc sort data=adam.adae out=ae_unique nodupkey; by usubjid trt01p aebodsys aedecod; run;
The resulting data can then be summarized using counts of subjects.
Nested AE Hierarchies
Adverse-event tables often contain:
- System Organ Class
- Preferred Term
- Subject count
- Percentage
A presentation dataset may contain rows such as:
SOC: Nervous System Disorders Headache Dizziness SOC: Gastrointestinal Disorders Nausea Vomiting
PROC REPORT can display the hierarchy using ORDER/GROUP variables or by creating explicit display labels.
Ordering Preferred Terms
Clinical tables often require alphabetical ordering of SOC and preferred term.
proc sort data=summary; by aebodsys aedecod; run; proc report data=summary nowd; column aebodsys aedecod trt01p,result; define aebodsys / order "System Organ Class"; define aedecod / order "Preferred Term"; define trt01p / across "Treatment"; define result / display "n (%)"; run;
In other cases, a controlled numeric sort variable is preferable to relying on alphabetic order.
Controlled Row Ordering
Suppose the required order is:
All Subjects Male Female Unknown
Alphabetical sorting would not necessarily produce that order.
A dedicated ordering variable is safer:
roword=1 All Subjects roword=2 Male roword=3 Female roword=4 Unknown
Then:
define roword / order noprint; define label / display "Sex";
This technique is widely useful in production TLF programming.
Suppressing Helper Variables
A variable can be used for ordering while hidden from the final report.
define roword / order noprint;
The variable controls the report but does not appear as a visible column.
NOPRINT Variables
NOPRINT is useful for technical variables that support:
- Ordering
- Grouping
- COMPUTE logic
- Page breaks
- Derived report calculations
This is an important technique for separating report logic from visible presentation.
Page Breaks
Some reports require a page break when a major group changes.
A BREAK statement can be configured to force a new page.
break after aebodsys / page;
This can be useful for lengthy listings or hierarchical reports.
However, excessive page breaks can produce inefficient output, so the final PDF or RTF should always be reviewed.
Repeated Headers
Multi-page reports need readable column headers on every page.
ODS destinations generally handle repeated report headers, but the final behavior should be validated in the actual destination.
This is especially important for:
- Patient listings
- Laboratory listings
- Exposure listings
- Adverse-event listings
- Long efficacy tables
Style Overrides
PROC REPORT allows cell-level style control.
define result /
display
style(column)=
[just=right
cellwidth=1.1in];
Header styles can also be controlled:
define result /
display
style(header)=
[font_weight=bold
just=center];
Using STYLE(REPORT)
Report-level styling can be applied with:
proc report data=summary nowd
style(report)=
[cellspacing=0
cellpadding=3];
run;
Production reporting frameworks often centralize these settings rather than repeating them in every program.
Reusable Reporting Macros
Once a programming team has several TLFs, repeated PROC REPORT logic can be encapsulated in macros.
%macro make_report(
data=,
outfile=
);
ods rtf file="&outfile.";
proc report data=&data. nowd;
column parameter result;
define parameter /
display
"Parameter";
define result /
display
"Result";
run;
ods rtf close;
%mend;
The macro can then be called repeatedly.
%make_report( data=demographics, outfile=table_14_1_1.rtf ); %make_report( data=exposure, outfile=table_14_3_1.rtf );
In real production systems, macros are usually more sophisticated and parameterized.
Macro Variables for TLF Metadata
Study-level metadata can be centralized.
%let studyid = ABC123; %let treatment = Safety Population; %let version = Final; title1 "&studyid"; title2 "Demographic Characteristics"; title3 "&treatment";
This reduces hard-coded repetition across a large TLF package.
A Reusable PROC REPORT Skeleton
A useful starting template is:
proc report
data=summary
nowd
split='|';
column
roword
parameter
trt01p,(result);
define roword /
order
noprint;
define parameter /
display
"Parameter"
style(column)=[just=left];
define trt01p /
across
"Treatment";
define result /
display
"n (%)"
style(column)=[just=right];
run;
This skeleton can be adapted to many categorical clinical tables.
Example: A Complete Categorical TLF
proc report
data=summary
nowd
split='|';
column
roword
parameter
trt01p,result;
define roword /
order
noprint;
define parameter /
display
"Parameter"
style(column)=[just=left];
define trt01p /
across
"Treatment"
order=data;
define result /
display
"n (%)"
style(column)=[just=right];
run;
The ORDER=DATA behavior is useful when the desired
treatment order has already been established in the input data.
Why TLF Shells Matter
A PROC REPORT program should not be developed in isolation.
The programmer should have the approved shell or specification describing:
- Table number
- Title
- Population
- Row structure
- Column structure
- Treatment order
- Decimal places
- Statistical methods
- Footnotes
- Denominators
- Missing-data conventions
PROC REPORT is the mechanism for implementing the reporting design; it is not the source of the statistical definition.
PROC REPORT and the Statistical Analysis Plan
The SAP determines statistical methodology.
The TLF shell translates that methodology into a reporting specification.
The programming dataset implements the analysis.
PROC REPORT presents the resulting values.
Handling P-Values
Inferential statistics may be calculated upstream and then displayed through PROC REPORT.
data summary;
length pvalue_c $20;
if pvalue < 0.001 then
pvalue_c = "<0.001";
else
pvalue_c = put(pvalue, pvalue6.4);
run;
The presentation variable can then be displayed:
define pvalue_c / display "P-value";
This is often easier to validate than embedding complicated p-value formatting directly inside PROC REPORT.
Confidence Intervals
Confidence intervals are frequently represented as a single report cell.
For example:
24.5 (18.2, 30.8)
A presentation variable can be created:
ci = cats(
put(estimate,6.1),
" (",
put(lower,6.1),
", ",
put(upper,6.1),
")"
);
PROC REPORT then displays ci as a character
column.
Statistical Significance Markers
Some shells require markers such as:
24.5* 24.5** <0.001***
If such conventions are specified, the marker should be generated through controlled programming logic.
if pvalue < 0.001 then result = cats(result,"***"); else if pvalue < 0.01 then result = cats(result,"**"); else if pvalue < 0.05 then result = cats(result,"*");
The actual convention must come from the study specification.
Character Versus Numeric Report Columns
| Approach | Advantage | Potential Issue |
|---|---|---|
| Numeric | Statistics and numeric formatting remain available | Complex cell formatting can be harder |
| Character | Exact cell presentation is easy to control | Numeric analysis must occur upstream |
For production TLFs, both approaches are valid.
A useful distinction is:
Zero Counts
A common clinical-table issue is the display of categories with zero subjects.
Suppose treatment groups contain:
Placebo 0 Drug A 3
The shell may require:
0 (0.0%) 3 (2.3%)
rather than leaving the Placebo cell blank.
The summary dataset should therefore explicitly represent required zero-count categories.
Structural Zeros Versus Missing Values
A zero count and a missing value do not mean the same thing.
| Value | Possible Interpretation |
|---|---|
| 0 | No subjects/events observed |
| . | Missing numeric value |
| Blank | Potentially not applicable or intentionally suppressed |
The reporting program should preserve the distinction required by the analysis and shell.
Denominator Control
Percentages in clinical tables can use different denominators.
For example:
- All randomized subjects
- All treated subjects
- Subjects in a treatment group
- Subjects with a baseline measurement
- Subjects with a particular assessment
PROC REPORT cannot determine the correct denominator from the data structure alone.
Patient-Level Listings and Page Organization
For long listings, it can be useful to organize output by subject.
proc report data=listing nowd; column usubjid avisit aval; define usubjid / order "Subject"; define avisit / display "Visit"; define aval / display "Result"; break after usubjid / page; run;
This creates a page-oriented structure in which a new subject can begin on a new page.
Whether this is desirable depends on the listing specification.
PROC REPORT for Laboratory Listings
A laboratory listing may contain:
Subject Visit Collection Date Parameter Result Unit Reference Range Flag
A basic implementation is:
proc report data=adam.adlb nowd;
column
usubjid
avisit
adt
param
aval
avalu
anrlo
anrhi
anrind;
define usubjid / order "Subject";
define avisit / order "Visit";
define adt / display "Collection Date";
define param / display "Parameter";
define aval / display "Result";
define avalu / display "Unit";
define anrlo / display "Lower Ref.";
define anrhi / display "Upper Ref.";
define anrind / display "Flag";
run;
Conditional Formatting for Laboratory Flags
A laboratory listing may visually emphasize abnormal results.
compute anrind;
if anrind ne "" then
call define(
_col_,
"style",
"style=[font_weight=bold]"
);
endcomp;
Again, whether such emphasis is appropriate depends on the approved reporting standard.
Using Formats in PROC REPORT
Formats can be specified directly in DEFINE statements.
define adt / display format=date9. "Collection Date";
For numeric values:
define aval / display format=8.2 "Result";
Custom formats are often preferable for controlled terminology.
Dates
Clinical datasets commonly store dates as numeric SAS dates.
The display format can be changed without changing the underlying value.
format=ddmmyy10.
or:
format=date9.
The selected convention should follow the study's reporting standards.
Sorting Dates
Do not convert dates to character simply to control display order.
Prefer:
define adt / order format=date9. "Date";
The underlying numeric date remains sortable while the displayed value is formatted.
Handling Multiple Treatment Arms
A study might contain:
Placebo Low Dose High Dose Total
PROC REPORT can use an ACROSS variable for this structure.
column parameter trt01p,result; define parameter / group "Parameter"; define trt01p / across "Treatment"; define result / display "n (%)";
The key is ensuring that the input dataset contains the required treatment categories in the required order.
Why Treatment Order Matters
A clinical shell may require:
Placebo | Drug A 50 mg | Drug A 100 mg | Total
An accidental sort could produce:
Drug A 100 mg | Drug A 50 mg | Placebo | Total
The numbers may all be correct, but the table would still fail the shell.
Production TLF Validation
A PROC REPORT program should undergo multiple levels of QC.
Independent QC
An independent QC programmer may reproduce key values using a different programming approach.
For example, the production program might use PROC REPORT after a summary dataset is generated with PROC SQL, while QC independently calculates:
n percent mean median standard deviation minimum maximum
The two results can then be compared.
Comparing Output With a Shell
A useful QC checklist is:
| Item | QC Question |
|---|---|
| Title | Does it exactly match the specification? |
| Population | Is the correct analysis population used? |
| Treatment order | Are columns in the required order? |
| Row order | Are categories in the required sequence? |
| Denominator | Is each percentage based on the correct population? |
| Decimals | Are all values displayed to the specified precision? |
| Missing values | Are missing values represented correctly? |
| Footnotes | Are required notes present and correct? |
| Pagination | Are page breaks and headers acceptable? |
Common PROC REPORT Mistakes
- Using GROUP when DISPLAY is required. This can suppress repeated detail values and change the report structure.
- Allowing treatment categories to determine column order accidentally. The input data should support the required shell order.
- Calculating statistics inside presentation logic without validation. Derivations should generally be independently checked upstream.
- Using the wrong denominator. The denominator must follow the SAP and TLF specification.
- Counting records instead of subjects. This is particularly dangerous in adverse-event reporting.
- Ignoring zero-count categories. Required categories may need to be explicitly represented even when no subjects meet the criterion.
- Hard-coding output text everywhere. Reusable presentation variables and centralized formats can make programs more maintainable.
- Testing only in HTML. PDF and RTF can render widths, pagination, and styles differently.
- Failing to inspect the final rendered output. Successful SAS execution does not guarantee a correct TLF.
- Embedding too much statistical logic in COMPUTE blocks. COMPUTE is powerful, but complicated derivations can become difficult to validate.
PROC REPORT Is Not a Statistical Procedure
This distinction is important.
PROC REPORT can calculate descriptive statistics, but it should not be considered a substitute for the statistical programming required to define clinical endpoints.
For example, a time-to-event endpoint may require:
- Censoring rules
- Event definitions
- Analysis dates
- Stratification
- Handling of ties
- Kaplan-Meier methodology
- Confidence intervals
PROC REPORT can present the resulting estimates, but the endpoint derivation belongs upstream.
Using PROC REPORT With ADaM
A common architecture is:
ADSL ↓ Subject population ↓ BDS / OCCDS / other ADaM ↓ Analysis derivations ↓ Summary dataset ↓ PROC REPORT ↓ TLF
The exact ADaM dataset depends on the analysis.
Examples include:
- ADSL for subject-level characteristics
- ADAE for adverse events
- ADLB for laboratory data
- ADVS for vital signs
- ADTTE for time-to-event endpoints
- ADRS for response endpoints
Example: Demographic Table Workflow
/* Step 1: Select population */
data adsl_pop;
set adam.adsl;
if saffl="Y";
run;
/* Step 2: Create summary */
proc summary data=adsl_pop nway;
class trt01p sex;
output out=sex_summary
n=n;
run;
/* Step 3: Calculate percentages */
proc sql;
create table summary as
select
trt01p,
sex,
n,
100*n / sum(n) as pct
from sex_summary
group by trt01p;
quit;
/* Step 4: Create display value */
data summary;
set summary;
length result $30;
result = cats(
put(n,8.),
" (",
put(pct,5.1),
"%)"
);
run;
/* Step 5: Report */
proc report data=summary nowd;
column sex trt01p,result;
define sex /
group
"Sex";
define trt01p /
across
"Treatment";
define result /
display
"n (%)";
run;
Improving the Architecture
For production work, it is often better to separate:
01_population.sas 02_derivation.sas 03_summary.sas 04_report.sas 05_qc.sas
This makes it easier to identify whether a discrepancy originates from:
- Population selection
- Derivation
- Summary calculation
- Report construction
- Rendering
Using PROC REPORT for Figures
PROC REPORT is primarily a table/listing procedure.
It is not generally the main procedure used to create statistical figures.
Figures are more commonly created using procedures and tools such as:
- PROC SGPLOT
- PROC SGPANEL
- PROC SGRENDER
- ODS Graphics
However, PROC REPORT can still contribute to a figure package by producing supporting data tables or figure annotations.
Combining PROC REPORT With PROC SGPLOT
A clinical efficacy package might therefore use:
PROC REPORT
↓
Efficacy table
PROC SGPLOT
↓
Waterfall plot
PROC SGPLOT
↓
Spider plot
PROC LIFETEST
↓
Kaplan-Meier analysis/plot
Each procedure is used for the type of output it handles best.
Reusable TLF Design Pattern
A robust PROC REPORT program can often be organized into five layers.
Why This Pattern Scales
A study may contain hundreds of TLFs.
If every PROC REPORT program contains completely independent derivation, summary, formatting, and rendering logic, maintenance becomes difficult.
A layered architecture allows common components to be reused.
For example:
study_setup.sas formats.sas population_macros.sas summary_macros.sas report_macros.sas ods_styles.sas
Individual TLF programs can then focus primarily on the table-specific requirements.
Production ODS Setup
A production environment may centralize ODS configuration.
options
orientation=landscape
nodate
nonumber;
ods escapechar="^";
ods pdf
file="table.pdf"
style=clinical;
title1 "Study ABC123";
title2 "Table 14.2.1";
proc report
data=summary
nowd;
column parameter result;
define parameter /
display
"Parameter";
define result /
display
"Result";
run;
ods pdf close;
The exact options and style definitions should be standardized within the organization or study.
Output Consistency
A major advantage of a centralized TLF framework is consistency.
Every table can share:
- Font
- Font size
- Margins
- Header appearance
- Column spacing
- Footnote formatting
- Page numbering
- Study identifiers
This is one reason production TLF environments frequently use custom ODS styles.
Custom ODS Styles
An organization may define a custom style:
proc template;
define style styles.clinical;
parent=styles.rtf;
style fonts from fonts /
'TitleFont' = ("Arial", 10pt)
'DocFont' = ("Arial", 9pt)
'HeadingFont' = ("Arial", 9pt, bold);
end;
run;
The exact style definition can become much more extensive in a production framework.
Development Versus Production Output
During development, HTML can be convenient because it is quick to generate and inspect.
For final deliverables, the study may require:
- RTF
- DOCX through a controlled workflow
- HTML
- Other validated formats
The development environment should therefore test the final destination before the TLF is considered complete.
Debugging PROC REPORT
When PROC REPORT output looks wrong, inspect the problem in layers.
Inspect the Intermediate Dataset
One of the most effective debugging techniques is to temporarily inspect the dataset immediately before PROC REPORT.
proc print data=summary(obs=50); run;
If the summary dataset is wrong, changing PROC REPORT syntax will not solve the underlying problem.
Inspect the SAS Log
Production TLF programming should always review the SAS log for:
- Errors
- Warnings
- Uninitialized variables
- Unexpected missing values
- Format errors
- Unexpected observations
A program that produces a PDF successfully can still contain warnings that indicate a data or programming issue.
Common Warning: Unexpected Missing Values
Suppose a COMPUTE block assumes a value exists:
compute result;
result = cats(
put(n,8.),
" (",
put(pct,5.1),
"%)"
);
endcomp;
If N or PCT is missing,
the final display may not match the intended output.
The programmer should explicitly define missing-value behavior.
Table Shell to PROC REPORT Translation
A useful skill for clinical programmers is translating a table shell into PROC REPORT concepts.
| Shell Element | PROC REPORT Concept |
|---|---|
| Row hierarchy | GROUP / ORDER / DISPLAY |
| Treatment columns | ACROSS |
| Summary statistic | ANALYSIS |
| Calculated display cell | COMPUTED |
| Subtotal | BREAK |
| Grand total | RBREAK |
| Hidden sort variable | NOPRINT |
| Wrapped text | FLOW |
| Cell formatting | STYLE / CALL DEFINE |
A Practical Translation Example
Suppose the shell says:
Parameter Placebo Drug A Age, Mean (SD) 57.8 (9.4) 58.9 (10.0) Sex, n (%) Male 70 (56.0) 76 (59.4) Female 55 (44.0) 52 (40.6)
The programmer can translate this into:
- Parameter → DISPLAY/GROUP
- Treatment → ACROSS
- Result → DISPLAY
- Sex categories → ORDER
- Age summary → pre-derived presentation value
That conceptual translation is often more important than memorizing individual PROC REPORT statements.
A Production-Oriented Example
proc report
data=tlf_demog
nowd
split='|';
column
roword
parameter
trt01p,result;
define roword /
order
noprint;
define parameter /
display
"Parameter"
style(column)=[just=left];
define trt01p /
across
"Treatment"
order=data;
define result /
display
"n (%)"
style(column)=[just=right];
run;
The strength of this architecture is that the report dataset already contains the correct values and ordering.
Handling Overall Columns
Many tables require:
Placebo Drug A Total
The Total column can be generated upstream and included as a report category, or handled separately depending on the structure.
For example:
trt01p --------- Placebo Drug A Total
A carefully constructed summary dataset can then allow PROC REPORT to display all three categories consistently.
Why the Total Column Deserves Special Attention
The Total denominator is not always simply the sum of treatment denominators.
For example, if a subject belongs to only one treatment group, totals may be straightforward.
But for more complex analyses involving subsets, missing assessments, or different populations, the overall denominator may need independent derivation.
Therefore:
Using PROC REPORT for Shift Tables
Shift tables are another common clinical application.
A typical structure might be:
Post-Baseline Baseline Normal Low High Normal 80 5 3 Low 7 18 2 High 4 1 15
The table can be built using:
- Baseline category
- Post-baseline category
- Treatment
- Count
- Percentage
PROC REPORT then becomes the presentation layer for the cross-classified summary dataset.
PROC REPORT for Vital-Sign Summaries
A vital-sign table may contain:
Parameter Visit Statistic Placebo Drug A
For example:
Systolic Blood Pressure
Week 4
Mean
SD
Median
Min, Max
Week 8
Mean
SD
Median
Min, Max
A presentation dataset with explicit row ordering is often the most maintainable solution.
PROC REPORT for Efficacy Tables
Efficacy tables may require:
- Response categories
- Number of responders
- Response percentages
- Risk differences
- Confidence intervals
- P-values
PROC REPORT can display all of these once the statistical calculations have been derived.
PROC REPORT and Time-to-Event Results
A survival table may contain:
Treatment N Events Censored Median 95% CI P-value
For example:
Placebo 125 88 37 8.4 (7.1, 10.2) Drug A 128 71 57 12.7 (10.4, 15.6)
PROC LIFETEST or other statistical procedures may produce the estimates, while PROC REPORT formats them into the final table.
Reusable Presentation Variables
A useful production technique is to create standardized display variables.
length
n_pct $30
mean_sd $30
ci $50
pvalue_c $20;
n_pct = cats(
put(n,8.),
" (",
put(pct,5.1),
"%)"
);
mean_sd = cats(
put(mean,6.1),
" (",
put(sd,6.1),
")"
);
ci = cats(
put(estimate,6.1),
" (",
put(lower,6.1),
", ",
put(upper,6.1),
")"
);
This makes the final PROC REPORT code relatively simple.
Keeping PROC REPORT Readable
A production program can become difficult to maintain if every DEFINE statement contains a large amount of style logic.
For example, avoid unnecessarily repeating:
style(column)= [font_face="Arial" font_size=9pt just=right cellwidth=1.1in]
across dozens of columns.
Instead, use centralized styles or macros where appropriate.
Modular TLF Programming
A mature clinical programming environment often separates:
Study setup
↓
Formats
↓
Population macros
↓
Analysis derivations
↓
Summary macros
↓
Presentation datasets
↓
PROC REPORT
↓
ODS style
↓
Final TLF
This architecture reduces duplication and makes standards easier to enforce.
PROC REPORT Quality-Control Checklist
Best Practices for Clinical Programmers
- Keep analysis and presentation separate.
- Use explicit row-order variables.
- Control treatment order deliberately.
- Pre-calculate complex statistical results.
- Use character presentation variables when exact cell formatting is required.
- Use COMPUTE blocks primarily for report-level presentation logic.
- Centralize formats and ODS styles.
- Validate both data and rendered output.
- Test the final ODS destination.
- Design reusable macros only after the underlying reporting pattern is understood.
The Most Important PROC REPORT Concepts
| Concept | Remember |
|---|---|
| COLUMN | Defines the report structure |
| DEFINE | Controls how report items behave |
| DISPLAY | Shows detail values |
| ORDER | Sorts and displays ordered values |
| GROUP | Groups identical values |
| ANALYSIS | Calculates statistics |
| ACROSS | Creates horizontal categories |
| COMPUTED | Creates derived report items |
| COMPUTE | Adds report-time calculation or formatting logic |
| BREAK | Controls group-level breaks and summaries |
| RBREAK | Controls report-level breaks and summaries |
| NOPRINT | Uses a variable without displaying it |
| ODS | Controls final output destination and rendering |
The Core Mental Model
PROC REPORT becomes much easier to understand if you think of it as a report grammar.
The programmer defines:
WHAT appears?
↓
COLUMN
HOW does each item behave?
↓
DEFINE
HOW are categories arranged horizontally?
↓
ACROSS
HOW are values grouped?
↓
GROUP / ORDER
HOW are statistics calculated?
↓
ANALYSIS
HOW are custom cells calculated or styled?
↓
COMPUTE
WHERE are subtotals inserted?
↓
BREAK
WHERE is the overall total inserted?
↓
RBREAK
WHERE does the output go?
↓
ODS
Once these concepts are understood, even complicated PROC REPORT programs become much easier to read.
Example of a Complete TLF Architecture
/*=========================================================
Table 14.1.1
Demographic Characteristics
=========================================================*/
/* Population */
data adsl_pop;
set adam.adsl;
if saffl="Y";
run;
/* Summary */
proc summary data=adsl_pop nway;
class trt01p sex;
output out=summary_raw
n=n;
run;
/* Presentation */
data tlf_demog;
set summary_raw;
length parameter result $100;
parameter = sex;
/* Derive denominator and percentage upstream */
pct = 100*n / denom;
result = cats(
put(n,8.),
" (",
put(pct,5.1),
"%)"
);
run;
/* Output */
title1 "Table 14.1.1";
title2 "Demographic Characteristics";
title3 "Safety Population";
proc report
data=tlf_demog
nowd
split='|';
column
roword
parameter
trt01p,result;
define roword /
order
noprint;
define parameter /
display
"Parameter";
define trt01p /
across
"Treatment"
order=data;
define result /
display
"n (%)";
run;
Why This Architecture Is Effective
Each layer has a clearly defined responsibility.
| Layer | Responsibility |
|---|---|
| ADaM | Validated analysis variables and populations |
| Derivation | Clinical/statistical definitions |
| Summary | Counts and statistical calculations |
| Presentation | Final labels, ordering, and display strings |
| PROC REPORT | Report layout and rendering logic |
| ODS | Output destination and presentation environment |
Final Practical Checklist
Bottom Line
SAS PROC REPORT is one of the most useful procedures for clinical-trial TLF development because it sits directly between validated analysis data and the final human-readable report.
Its real strength is not simply producing a table. It is the ability to combine:
- Detailed listings
- Grouped summaries
- Analysis statistics
- Horizontal treatment columns
- Spanning headers
- Calculated report items
- Conditional formatting
- Subtotals and totals
- Pagination
- ODS rendering
For clinical programmers, the most important concepts are understanding the difference between DISPLAY, ORDER, GROUP, ANALYSIS, and ACROSS, knowing when to use COMPUTE, and keeping statistical derivation separate from presentation logic.
A strong PROC REPORT program should therefore not be thought of as a collection of formatting statements. It is the final implementation of a reporting specification.
References
SAS Institute Inc. SAS 9.4 Procedures Guide: Base SAS Procedures.
PROC REPORT documentation and related reporting procedures.
SAS Institute Inc. SAS 9.4 Output Delivery System: User's Guide.
Documentation for ODS destinations, styles, and output management.
SAS Institute Inc. SAS 9.4 SQL Procedure User's Guide.
Documentation for data preparation and summary workflows commonly used before
PROC REPORT.
CDISC. Analysis Data Model (ADaM) Implementation Guide.
Guidance relevant to the analysis datasets commonly used as sources for
clinical-trial TLF development.
ICH. ICH E9: Statistical Principles for Clinical Trials.
Principles relevant to the statistical analyses that underlie clinical-trial
tables and reports.