Tutorials › Biostatistics › SAS PROC REPORT for TLF Development

Clinical Trial Programming

SAS PROC REPORT for TLF Development

A practical guide to using PROC REPORT for production clinical-trial Tables, Listings, and Figures, including report columns, treatment-arm layouts, ACROSS variables, spanning headers, summary statistics, COMPUTE blocks, BREAK and RBREAK statements, formatting, pagination, ODS destinations, and reusable TLF programming patterns.

Intermediate 22 min read

What You'll Learn

  • How PROC REPORT fits into clinical TLF development
  • How COLUMN, DEFINE, ORDER, GROUP, and ACROSS work
  • How to build treatment-arm summary tables
  • How to use COMPUTE blocks and calculated report columns
  • How BREAK and RBREAK control report structure
  • How to create production-ready ODS PDF and RTF output

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
Key idea: PROC REPORT is not simply a prettier PRINT procedure. It is a report-building framework in which the programmer defines the report structure, identifies which variables control grouping, specifies which columns are statistics, and controls how the resulting report is rendered.

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.

1
Source data: SDTM, ADaM, or other validated analysis datasets.
2
Population selection: identify the subjects belonging in the TLF.
3
Derivations: calculate analysis flags, categories, changes, and endpoints.
4
Summarization: calculate counts, percentages, means, medians, standard deviations, minimums, and maximums as required.
5
Report construction: use PROC REPORT to arrange the summarized data into the required table structure.
6
ODS rendering: generate PDF, RTF, HTML, or another controlled output.
7
Validation: compare the output against specifications and independent checks.

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:

  • USUBJID
  • AGE
  • SEX

The NOWD option suppresses the interactive REPORT window and is commonly used in batch-oriented clinical programming.

Production principle: In regulated or submission-oriented programming, PROC REPORT should generally be treated as a deterministic reporting procedure rather than an interactive exploration tool.

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.

GROUP versus DISPLAY: DISPLAY preserves detail rows. GROUP collapses identical values into report groups and changes how PROC REPORT processes the underlying observations. Choosing the wrong type can therefore change both the appearance and the meaning of a table.

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:

Do the complicated derivation before PROC REPORT whenever possible. PROC REPORT is excellent at arranging and presenting a well-structured summary dataset. It is generally easier to validate a table when statistical derivations and analysis flags are created upstream.

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.

Important: ACROSS variables are processed horizontally. This is fundamentally different from using a GROUP variable, which organizes observations vertically.

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:

Figure 1. Typical Treatment-Arm Header Structure
Illustrative output for a clinical-trial summary table.
Demographic Characteristics
Safety Population
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.

TLF programming rule: Do not assume that an ACROSS variable will contain exactly the categories specified in the shell. Control the analysis dataset and category ordering before PROC REPORT whenever a fixed production layout is required.

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.

Separation of concerns: Derivation determines what the numbers are. PROC REPORT determines how the numbers are presented. Keeping these responsibilities separate generally makes TLF programming easier to maintain and validate.

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.

Use conditional formatting carefully. In clinical-trial reporting, formatting conventions should come from the approved shell, reporting standards, or statistical programming specification. A programmer should not introduce visually meaningful emphasis merely because a value happens to cross a conventional statistical threshold.

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.

For listings, be especially careful with GROUP. If the requirement is to show every observation, GROUP may be inappropriate because PROC REPORT's grouping behavior can suppress repeated displayed 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
Practical rule: Use DATA step, SQL, SUMMARY, or other analytical procedures to construct and validate the data needed for the table. Use PROC REPORT to turn that validated data into the specified report.

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.

Critical AE principle: For many adverse-event tables, the numerator is the number of unique subjects with an event, not the number of raw AE records. The counting rule must come from the SAP or TLF specification.

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.

1
SAP: defines the statistical method.
2
TLF shell: defines what the output should look like.
3
Analysis programming: derives the required values.
4
PROC REPORT: organizes and formats the values.
5
QC: confirms the output against independent expectations.

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:

Analyze numerically; present as character when necessary. Do not convert data to character merely to make programming easier if the underlying value still needs to be statistically analyzed.

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.

Always validate the denominator. A perfectly formatted table with the wrong denominator is still an incorrect clinical-trial result.

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.

Validate structure, not just numbers. TLF QC must confirm column order, row order, labels, pagination, formatting, and footnotes in addition to numerical correctness.

Production TLF Validation

A PROC REPORT program should undergo multiple levels of QC.

1
Validate the analysis dataset.
2
Validate denominators and subject counts.
3
Validate statistical derivations independently.
4
Validate row and column ordering.
5
Validate treatment assignment.
6
Validate formatting and decimal places.
7
Validate titles and footnotes.
8
Review rendered PDF/RTF/HTML output.

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

  1. Using GROUP when DISPLAY is required. This can suppress repeated detail values and change the report structure.
  2. Allowing treatment categories to determine column order accidentally. The input data should support the required shell order.
  3. Calculating statistics inside presentation logic without validation. Derivations should generally be independently checked upstream.
  4. Using the wrong denominator. The denominator must follow the SAP and TLF specification.
  5. Counting records instead of subjects. This is particularly dangerous in adverse-event reporting.
  6. Ignoring zero-count categories. Required categories may need to be explicitly represented even when no subjects meet the criterion.
  7. Hard-coding output text everywhere. Reusable presentation variables and centralized formats can make programs more maintainable.
  8. Testing only in HTML. PDF and RTF can render widths, pagination, and styles differently.
  9. Failing to inspect the final rendered output. Successful SAS execution does not guarantee a correct TLF.
  10. 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.

TLF distinction: PROC REPORT is primarily a T/L tool. For production statistical graphics, ODS Graphics and the SG procedures are usually more appropriate.

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.

1
Input layer: validated ADaM data.
2
Analysis layer: derive flags and endpoint variables.
3
Summary layer: produce counts and statistics.
4
Presentation layer: create report-ready character values and ordering.
5
Rendering layer: PROC REPORT plus ODS destination.

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:

  • PDF
  • 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.

1
Check the input: Is the expected data present?
2
Check sorting: Is the input ordered correctly?
3
Check variable type: DISPLAY, ORDER, GROUP, ANALYSIS, or ACROSS?
4
Check statistics: Is PROC REPORT calculating the intended statistic?
5
Check COMPUTE: Is presentation logic changing the output?
6
Check ODS: Is the destination affecting the rendering?

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.

Debug upstream first. Many apparent PROC REPORT problems are actually data-preparation problems.

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:

Never calculate a Total column merely by adding visible treatment percentages. The Total value should be derived according to the statistical definition of the table.

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

1
Confirm the correct source ADaM dataset.
2
Confirm the analysis population.
3
Confirm subject-level counting rules.
4
Confirm denominators.
5
Confirm row ordering.
6
Confirm treatment-column ordering.
7
Confirm numeric precision.
8
Confirm missing and zero-value presentation.
9
Confirm titles, subtitles, and footnotes.
10
Review the actual rendered output.

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

1
Read the TLF shell before writing PROC REPORT code.
2
Identify the exact analysis population and denominator.
3
Determine whether each report variable should be DISPLAY, ORDER, GROUP, ANALYSIS, ACROSS, or COMPUTED.
4
Create explicit row-order variables when shell order is not alphabetical.
5
Control treatment-arm ordering explicitly.
6
Perform complicated derivations upstream.
7
Use COMPUTE blocks for appropriate report-level calculations and formatting.
8
Use BREAK and RBREAK deliberately for subtotals and totals.
9
Validate both the intermediate dataset and final rendered TLF.
10
Keep reusable ODS styles, formats, and reporting conventions centralized.

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.

Bottom line: Build the analysis correctly first. Create a controlled summary or presentation dataset second. Then use PROC REPORT to translate that dataset into the exact row structure, treatment columns, labels, statistics, formatting, and pagination required by the TLF shell. When used this way, PROC REPORT becomes a highly reusable component of a production clinical-trial reporting framework rather than a one-off table-generation procedure.

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.