Tutorials › Biostatistics › SAS Macros for Clinical Table Generation

SAS Programming & Clinical TLFs

SAS Macros for Clinical Table Generation

A practical guide to designing reusable SAS macros for automated clinical table generation, including macro parameters, treatment-group columns, statistical summaries, PROC REPORT integration, dynamic titles and footnotes, validation, and production-ready TLF workflows.

Advanced 20 min read

What You'll Learn

  • Why macros are useful for clinical TLF production
  • How to design robust positional and keyword macro parameters
  • How to generate reusable treatment-group summaries
  • How macros interact with PROC REPORT and ODS destinations
  • How to build dynamic titles, footnotes, formats, and denominator logic
  • How to validate macro-driven tables for regulated clinical reporting

Introduction

Clinical trial reporting rarely consists of producing one table one time. A typical study may require dozens or hundreds of tables whose structure is similar but whose population, variables, treatment groups, statistics, titles, footnotes, and analysis datasets differ.

This is where SAS macros become particularly valuable. Instead of writing a separate PROC SQL, PROC FREQ, PROC MEANS, and PROC REPORT program for every table, a programmer can encapsulate recurring logic in a parameterized macro and reuse that logic across many outputs.

The objective is not simply to make SAS code shorter. A well-designed macro creates a controlled reporting component that is easier to reuse, review, test, document, and maintain.

Key idea: A production clinical macro should separate reusable reporting logic from table-specific configuration. The macro contains the stable programming pattern, while parameters identify the dataset, population, treatment variable, analysis variable, labels, statistics, and output requirements.

What Is a SAS Macro?

The SAS macro facility is a text-generation and programming mechanism that allows SAS code to be generated dynamically.

A simple macro can look like this:

%macro hello;
    %put Hello from the SAS macro facility.;
%mend hello;

%hello;

The macro processor resolves the macro invocation and generates the SAS statements needed for execution.

For clinical programming, the more important capability is parameterization. A macro can accept information from the calling program and insert that information into reusable SAS code.

%macro print_data(
    data=
);

    proc print data=&data;
    run;

%mend print_data;

%print_data(data=adam.adsl);

Why Macros Matter in Clinical TLF Production

Clinical tables, listings, and figures often share common programming patterns. For example, many tables require:

  • a treatment variable;
  • one or more analysis populations;
  • formatted treatment labels;
  • descriptive statistics;
  • consistent decimal precision;
  • missing-value handling;
  • population denominators;
  • titles and footnotes;
  • and a standardized ODS output destination.

If these features are independently coded in every program, small differences can accumulate across the reporting package.

1
Define the reporting standard: determine the required table structure, statistics, labels, formats, denominators, titles, and footnotes.
2
Identify reusable logic: separate the portions that should remain constant across tables from the parts that vary.
3
Parameterize the variation: convert dataset names, variables, labels, population flags, and options into macro parameters.
4
Validate the macro: test the macro independently before using it for production TLFs.
5
Call the macro from table-specific programs: keep the final programs concise and configuration-focused.

Positional Versus Keyword Parameters

SAS macros support both positional and keyword parameters.

Positional parameters

%macro summarize(data, variable);

    proc means data=&data;
        var &variable;
    run;

%mend summarize;

%summarize(adam.adlb, aval);

The calling sequence determines which value is assigned to each parameter. This approach is compact but can become difficult to maintain when a macro has many parameters.

Keyword parameters

For clinical reporting macros, keyword parameters are usually easier to read.

%macro summarize(
    data=,
    variable=,
    class=
);

    proc means data=&data;
        %if %length(&class) %then %do;
            class &class;
        %end;
        var &variable;
    run;

%mend summarize;

%summarize(
    data=adam.adlb,
    variable=aval,
    class=trt01an
);
Programming principle: For complex clinical macros, keyword parameters generally make the calling program more self-documenting. A reviewer can immediately see what each argument represents.

Designing a Clinical Table Macro

Consider a common clinical table showing the number and percentage of subjects experiencing at least one adverse event.

The desired table might contain:

Statistic Placebo Drug A Drug B Total
Subjects in population 100 102 101 303
Any AE 61 (61.0%) 69 (67.6%) 64 (63.4%) 194 (64.0%)

The table-generating macro should not hard-code these treatment groups. Instead, it should receive the relevant treatment variable and population information as parameters.

A Basic Treatment-Group Macro

A useful starting point is a macro that accepts a dataset, treatment variable, and population variable.

%macro make_ae_table(
    data=,
    trtvar=,
    where=
);

    proc freq data=&data;

        %if %length(&where) %then %do;
            where &where;
        %end;

        tables &trtvar / missing;

    run;

%mend make_ae_table;

The important point is that the macro does not know whether the caller is using ADSL, ADAE, a treatment-emergent population, or another analysis dataset. That information is supplied by the calling program.

Adding Population Control

Clinical tables frequently use analysis-population flags such as SAFFL, ITTFL, or FASFL.

Rather than embedding a particular population flag inside the macro, make the population expression configurable.

%macro make_ae_table(
    data=,
    trtvar=,
    popvar=,
    popval=Y
);

    proc freq data=&data;

        where &popvar="&popval";

        tables &trtvar / missing;

    run;

%mend make_ae_table;

%make_ae_table(
    data=adam.adae,
    trtvar=trt01an,
    popvar=SAFFL
);

Now the same macro can be used with a different population without changing its internal logic.

Macro Variables and Indirect References

One of the most useful techniques in macro-driven reporting is using macro variables to control the values inserted into generated SAS code.

%let analysis_ds = adam.adae;
%let treatment   = trt01an;
%let population  = SAFFL;

%make_ae_table(
    data=&analysis_ds,
    trtvar=&treatment,
    popvar=&population
);

This approach can be extended to control titles, footnotes, output paths, study identifiers, treatment labels, and analysis parameters.

Building a Reusable Summary Component

A production macro often needs to generate a standardized count and percentage.

For a treatment group with denominator \(N_j\), the percentage for a category with count \(n_j\) is:

\[ 100\times\frac{n_j}{N_j} \]

The important programming issue is not the formula itself but ensuring that the correct denominator is used for every treatment group and population.

Clinical reporting warning: A macro that calculates percentages correctly for one table can still be wrong if it assumes that every table uses the same denominator. Denominator logic should therefore be an explicit part of the macro design.

Creating a Count-and-Percentage String

A common reporting convention is to display the count and percentage in one cell, such as 61 (61.0%).

length result $40;

if denominator > 0 then
    result = cats(
        put(count, 8.),
        ' (',
        put(100*count/denominator, 5.1),
        '%)'
    );
else
    result = '0 (0.0%)';

In production programming, the formatting rule should generally be centralized rather than repeated independently throughout every table program.

Using PROC REPORT

PROC REPORT is particularly useful for clinical tables because it provides substantial control over column structure, labels, ordering, statistics, and ODS output.

proc report data=final_table nowd split='|';

    column parameter placebo drug_a drug_b total;

    define parameter / display
        "Parameter"
        style(column)=[cellwidth=2.2in];

    define placebo / display
        "Placebo|(N=100)";

    define drug_a / display
        "Drug A|(N=102)";

    define drug_b / display
        "Drug B|(N=101)";

    define total / display
        "Total|(N=303)";

run;

The macro can generate the surrounding PROC REPORT structure while the table-specific program supplies the dataset and column configuration.

Dynamic Treatment Headers

A common challenge is that treatment labels and denominators change from study to study.

Instead of hard-coding:

define placebo / display "Placebo|(N=100)";
define drug_a  / display "Drug A|(N=102)";

the labels can be constructed dynamically.

%let trt1_label = Placebo;
%let trt1_n     = 100;

%let trt2_label = Drug A;
%let trt2_n     = 102;

define trt1 / display
    "&trt1_label|(N=&trt1_n)";

define trt2 / display
    "&trt2_label|(N=&trt2_n)";

This becomes particularly powerful when combined with standardized treatment metadata.

Using Metadata to Drive the Table

A mature TLF framework can maintain study-specific treatment metadata in a control dataset rather than embedding treatment labels throughout the code.

TRT01AN TRT01A Display Order
0 Placebo 1
1 Drug A 2
2 Drug B 3

The macro can then retrieve the treatment labels and use them to construct the final report headers.

Macro Loops

Macro loops are useful when the same block of generated SAS code must be repeated for multiple treatment groups or analysis variables.

%macro treatment_columns(ntrt=3);

    %do i=1 %to &ntrt;

        define trt&i / display
            "Treatment &i";

    %end;

%mend treatment_columns;

%treatment_columns(ntrt=3);

The macro processor generates the repeated SAS statements before the SAS compiler executes them.

Conditional Macro Logic

Clinical tables frequently require optional features. For example, a table may contain a Total column in one study but not another.

%macro table_columns(
    total=yes
);

    define placebo / display "Placebo";
    define drug_a  / display "Drug A";

    %if %upcase(&total)=YES %then %do;

        define total / display "Total";

    %end;

%mend table_columns;

This allows one macro to support multiple standardized table configurations without creating unrelated versions of the same program.

Generating Titles and Footnotes

Titles and footnotes are part of the clinical output specification and should be treated as controlled metadata rather than informal text.

%macro table_header(
    number=,
    title=,
    population=
);

title1 "Table &number";
title2 "&title";
footnote1 "Population: &population";

%mend table_header;

%table_header(
    number=14.3.1,
    title=Treatment-Emergent Adverse Events,
    population=Safety Population
);
Best practice: Keep title and footnote generation centralized when possible. This reduces the risk that nearly identical tables will contain inconsistent population definitions, analysis descriptions, or study terminology.

Combining Macros Into a TLF Framework

A useful clinical reporting framework separates responsibilities among several macros.

1
Population macro: creates or applies the required analysis population.
2
Summary macro: calculates counts, percentages, means, standard deviations, medians, or other required statistics.
3
Formatting macro: applies standardized display formats and character construction.
4
Report macro: constructs the PROC REPORT layout.
5
Output macro: controls ODS RTF, PDF, Excel, or other approved destinations.

A More Complete Clinical Table Macro

The following example illustrates the architecture of a reusable table macro. It intentionally separates configuration from reporting mechanics.

%macro clinical_table(
    data=,
    where=,
    treatment=,
    variable=,
    title=,
    footnote=,
    outfile=
);

    %local condition;

    %if %length(&where) %then
        %let condition = where &where;;

    proc sort data=&data out=_tbl_input;
        by &treatment;
        &condition
    run;

    proc freq data=_tbl_input noprint;

        tables &treatment*&variable /
            out=_tbl_freq
            missing;

    run;

    ods rtf file="&outfile";

    title "&title";

    proc report data=_tbl_freq nowd;

        column &treatment &variable count percent;

        define &treatment / group "Treatment";
        define &variable  / group "Category";
        define count       / display "n";
        define percent     / display "Percent";

    run;

    footnote "&footnote";

    ods rtf close;

    proc datasets library=work nolist;
        delete _tbl_input _tbl_freq;
    quit;

%mend clinical_table;

This example is intentionally simplified. A production implementation would typically require more extensive handling of treatment metadata, denominators, formats, ordering, missing categories, output paths, titles, footnotes, logging, and validation.

Why Temporary Datasets Matter

Reusable macros often create intermediate datasets. These datasets should have controlled names and should not accidentally overwrite permanent study data.

A common convention is to use temporary WORK datasets:

_tbl_input
_tbl_freq
_tbl_summary
_tbl_final

At the end of the macro, temporary objects should be removed when appropriate.

proc datasets library=work nolist;
    delete
        _tbl_input
        _tbl_freq
        _tbl_summary
        _tbl_final;
quit;

Macro Variable Scope

Macro variable scope becomes increasingly important as the reporting framework grows.

A macro variable created with %let may have local or global implications depending on where it is created and whether the macro facility finds an existing symbol table.

For reusable production macros, explicitly declaring local variables helps prevent unintended interaction with the calling program.

%macro example(
    data=
);

    %local condition i result;

    /* Macro logic */

%mend example;
Production principle: A macro should avoid unexpectedly changing macro variables belonging to the calling program. Explicit local declarations make macro behavior more predictable.

Handling Optional Parameters

A well-designed macro should provide sensible defaults whenever a parameter has a natural default.

%macro report_table(
    data=,
    treatment=trt01an,
    decimals=1,
    total=yes
);

    /* Reporting logic */

%mend report_table;

The caller can therefore specify only the information that differs from the standard configuration.

%report_table(
    data=adam.adae
);

Macro Validation

Macro validation is more than checking whether the generated SAS program runs. A macro can execute without errors and still produce a clinically incorrect table.

Validation should therefore address at least four levels:

Validation Level Question
Macro syntax Does the macro generate valid SAS code?
Statistical logic Are counts, percentages, and statistics calculated correctly?
Clinical logic Is the intended population, treatment, and analysis variable used?
Output validation Does the final TLF match the approved specification?

Inspecting the Generated SAS Code

One of the most important debugging techniques is to inspect the code generated by the macro processor.

The SAS options MPRINT, MLOGIC, and SYMBOLGEN are particularly useful during development.

options mprint mlogic symbolgen;

For example, MPRINT shows the SAS statements generated by a macro, while SYMBOLGEN can show how macro variable references resolve.

Development tip: Use macro debugging options during development and troubleshooting, but consider the amount of diagnostic information they produce before enabling them in large production runs.

Common Macro Failure Modes

1. Unquoted special characters

Macro parameters containing special characters may require careful handling, particularly when they are inserted into WHERE expressions or generated code.

2. Unexpected macro resolution

Text containing ampersands or percent signs can be interpreted by the macro processor rather than treated as literal text.

3. Incorrect denominator logic

This is one of the most consequential clinical reporting errors. A table may look visually correct while using an inappropriate denominator.

4. Treatment ordering errors

Treatment columns should follow the approved study order rather than alphabetical order unless alphabetical ordering is explicitly intended.

5. Hidden dependencies

A macro that assumes a global macro variable, format catalog, library, option, or dataset exists can become difficult to reuse safely.

Macro Design: What Should Be Parameterized?

A useful rule is to parameterize information that legitimately varies while keeping the core reporting algorithm stable.

Good Parameter Reason
Input dataset Different tables use different analysis datasets
Treatment variable Different analyses may use different treatment structures
Population condition Safety, ITT, FAS, and other populations differ
Analysis variable The same reporting framework may summarize different endpoints
Title / footnote Table-specific metadata changes
Decimal precision Display requirements differ by statistic
Output destination Production environments may use different paths or ODS destinations

What Should Usually Not Be Parameterized?

Over-parameterization can make a macro harder to understand than the individual programs it was designed to replace.

If every statement can be turned on or off through a parameter, the macro may become a configuration language rather than a reusable reporting component.

Rule of thumb: Parameterize meaningful differences in the reporting requirement, not every line of SAS syntax.

A Practical Table-Generation Architecture

A mature clinical programming environment can be organized into layers.

1
Raw clinical data: SDTM domains and other source data.
2
Analysis datasets: ADSL, ADAE, ADLB, ADTTE, and other ADaM datasets.
3
Statistical summaries: counts, percentages, descriptive statistics, confidence intervals, and inferential results.
4
Reusable macros: standardized formatting, table construction, titles, footnotes, and output.
5
Table-specific programs: supply configuration and analysis-specific parameters.
6
Validated TLF: final RTF, PDF, or other approved output.

Example: Calling a Production-Style Macro

The final table program should ideally be relatively short because most of the reusable programming logic has already been validated.

%clinical_table(
    data      = adam.adae,
    where     = saffl="Y" and trtemfl="Y",
    treatment = trt01an,
    variable  = aedecod,
    title     = Treatment-Emergent Adverse Events by Preferred Term,
    footnote  = Safety population; treatment-emergent events,
    outfile   = "&outdir./table_14_3_1.rtf"
);

This style makes the purpose of the program immediately visible. The caller defines what should be produced; the macro controls how the standardized table is generated.

Macro Libraries in Clinical Programming

Large clinical programming groups often maintain reusable macro libraries rather than embedding all macro definitions inside individual table programs.

A library might contain components for:

  • population filtering;
  • treatment metadata;
  • count and percentage formatting;
  • continuous descriptive statistics;
  • categorical summaries;
  • confidence intervals;
  • p-values;
  • PROC REPORT layouts;
  • ODS RTF and PDF output;
  • titles and footnotes;
  • standardized validation checks.

Macros and Automated TLF Pipelines

Macros become especially powerful when combined with metadata-driven programming.

Instead of manually writing one program for every table, a control dataset can describe the required outputs.

Table Dataset Population Analysis
14.1.1 ADSL Safety Demographics
14.2.1 ADAE Safety Adverse Events
14.3.1 ADLB Safety Laboratory Parameters
14.4.1 ADTTE ITT Time-to-Event

A driver program can then read the metadata and invoke the appropriate macros.

Macros Do Not Replace Statistical Programming

Automation does not eliminate the need for statistical understanding.

A macro can reliably calculate the instructions it has been given, but it cannot determine whether those instructions represent the correct clinical estimand, population, analysis set, or statistical method.

Important distinction: Macro programming automates the implementation of an analysis specification. It does not determine the analysis specification itself.

Production Checklist

Before using a macro-driven clinical table in a production submission workflow, review the following:

  • Is the macro specification documented?
  • Are all parameters clearly defined?
  • Are defaults appropriate and documented?
  • Are local macro variables explicitly controlled?
  • Are population and denominator rules correct?
  • Are treatment groups in the approved order?
  • Are formats and decimal places correct?
  • Are missing values handled according to the specification?
  • Are titles and footnotes correct?
  • Has the generated SAS code been reviewed?
  • Has the resulting output been independently checked?
  • Are temporary datasets isolated from permanent study data?
  • Is the macro version controlled?
  • Can the same macro be reproduced in a clean environment?

Key Takeaways

Principle Practical Meaning
Parameterize variation Keep table-specific information outside reusable macro logic.
Centralize standards Use shared macros for recurring formats, layouts, titles, and reporting conventions.
Control denominators Make population and denominator logic explicit.
Use keyword parameters Improve readability and maintainability of complex macro calls.
Inspect generated code Use MPRINT and related tools during development and debugging.
Validate the output Execution without errors does not guarantee clinical correctness.
Version control the framework Treat macros as production software rather than disposable snippets.

Conclusion

SAS macros provide one of the most effective mechanisms for standardizing clinical table generation. The real value is not simply reducing the number of lines of SAS code. The value comes from creating reusable, controlled, and testable reporting components.

A strong macro framework separates the stable mechanics of table generation from the study-specific configuration. Population definitions, treatment groups, analysis variables, titles, footnotes, and display requirements can then be supplied as parameters while the underlying reporting logic remains consistent.

For clinical programmers, the progression from individual table programs to reusable macros is an important step toward scalable TLF automation. The next step is to combine these macros with metadata-driven programming, version control, automated validation, and standardized ADaM-based analysis workflows.

References

SAS Institute Inc. SAS Macro Language: Reference. SAS Institute Inc.
SAS Institute Inc. SAS 9.4 Procedures Guide: PROC REPORT. SAS Institute Inc.
SAS Institute Inc. SAS 9.4 Output Delivery System: User's Guide. SAS Institute Inc.
CDISC. Analysis Data Model (ADaM) Implementation Guide.
FDA. Study Data Technical Conformance Guide.