Sapling SAP
Reference

The SAS macros

The manual of the macro library, made from the headers of the macros.

This page is the repository's own document docs/sas-macros.md, shown as it is, so that it never drifts from the product.

Made by node tools/sas_macros_doc.ts from the headers of sas/macros/ (the macro library 1.8.0); do not edit it. How a macro is written: sas-style.md; what the library computes and how a program reads: sas.md. The same as data for an agent: sas/macros/index.json (and sapling find <macro>, the MCP tool sapling_sas_macros).

Every macro a person calls checks its parameters first and stops with one line that names the parameter and the fix (ERROR: [describe] var=AGEX is not a variable of adsl: give var= a variable of data=), writes a begin and an end line in the log (%let spl_quiet = yes; keeps them out), and keeps its work data sets with debug=yes.

The global macro variables#

VariableIt is
SPL_ROOTthe folder of the study on the SAS server (the one line a program sets: %let spl_root = ...;)
SPL_STUDYthe study, set by %study
SPL_OUTthe output folder, set by %study
SPL_PROGRAMthe name of the program, set by %study
SPL_QUIETyes: no begin and end lines of the macros in the log (%let spl_quiet = yes; before the calls)

Setting up a program#

MacroIt does
%import_xptRead SAS transport files (XPORT version 5) into SAS data sets (PROC COPY)
%studyStart a program: the ADaM library, the output folder, the options

The statistics macros (one block of a table each)#

MacroIt does
%adjustThe adjusted p-values of a family of pairs of groups (PROC MULTTEST)
%ancovaThe Type III test of a term in a linear model (PROC GLM)
%anovaThe F test of a one-way analysis of variance (PROC GLM)
%ardEvery number of a table as a CSV file, with every digit (DATA steps)
%chisqThe Pearson chi-square test of groups against categories (PROC FREQ)
%cmhThe Cochran-Mantel-Haenszel test of groups against categories in strata (PROC FREQ)
%columnsThe columns of a table and the N of each (PROC SUMMARY)
%coxThe hazard ratio of pairs of groups from a Cox model (PROC PHREG)
%csvThe records of a listing as a CSV file (DATA steps)
%describeDescriptive statistics of a variable in each group (PROC MEANS)
%fisherFisher exact test of subjects with a record against those without (PROC FREQ)
%freqFrequencies of subjects in each group and category (PROC FREQ)
%interaction_genmodThe likelihood-ratio test of the treatment-by-factor interaction, counts (PROC GENMOD)
%interaction_glmThe F test of the treatment-by-factor interaction in a linear model (PROC GLM)
%interaction_logisticThe likelihood-ratio test of the treatment-by-factor interaction (PROC LOGISTIC)
%interaction_phregThe likelihood-ratio test of the treatment-by-factor interaction (PROC PHREG)
%kmThe Kaplan-Meier summary of a time in each group (PROC LIFETEST)
%layoutThe display data set of a table: its rows and a column per group (DATA steps)
%logisticThe odds ratio of pairs of groups from a logistic regression (PROC LOGISTIC)
%logrankThe log-rank test of a time between groups (PROC LIFETEST)
%lsmeansThe differences of the LS means of pairs of groups in a linear model (PROC MIXED)
%mmrmThe mixed model for repeated measures: LS means and differences at a visit (PROC MIXED)
%proportionThe percent of subjects in each category and group with its interval (PROC FREQ)
%rateThe exposure-adjusted incidence rate of each group, exact Poisson limits (PROC FREQ)
%rateratioThe rate ratio of pairs of groups, Poisson or negative binomial model (PROC GENMOD)
%riskThe risk difference, relative risk or odds ratio of pairs of groups (PROC FREQ)
%tableA table in one call: %columns, %ard, %layout and %print
%table_aeAn adverse event table in one call: subjects by the levels of var=
%table_modelA model table in one call: descriptive blocks, then ANCOVA or MMRM
%table_responseA responder table in one call: percent with interval, comparison, adjusted odds ratio
%table_shiftA shift table in one call: the category at baseline against the category after it
%table_summaryA summary table in one call: the variables, # after a categorical one
%table_tteA time-to-event table in one call: Kaplan-Meier, the log-rank test, the hazard ratio
%table_visitA table by visit in one call: each variable of vars= by parameter and visit
%termflagsThe flag Y or N of each term for every subject of the population (PROC SORT, MERGE)
%ttestThe two-sample t-test of each pair of groups (PROC TTEST)
%wilcoxonThe Wilcoxon rank-sum test and Hodges-Lehmann shift of pairs (PROC NPAR1WAY)
%worstThe records of each subject at its worst level (PROC SORT, a DATA step)

Tables and listings#

MacroIt does
%printPrint a table or a listing into RTF and a monospace listing (PROC REPORT)
%stackPut results one under another: the blocks of a table (a DATA step)

Figures (one macro per figure: docs/sas.md "Figures")#

MacroIt does
%aeplotAdverse events: percent by group and risk difference (PROC SUMMARY, PROC SGPANEL)
%barplotBars of the categories of a variable in each group (PROC FREQ, PROC SGPANEL)
%boxplotBox plots by group and visit (PROC MEANS, PROC SGPANEL)
%forestplotForest plot of results in the levels of subgroups (PROC SGPLOT)
%kmplotKaplan-Meier curves with the number at risk (PROC LIFETEST, PROC SGPANEL)
%lineplotLines through values given, with their interval (PROC SGPANEL)
%meanplotMean or median by visit with error bars (PROC MEANS, PROC SGPANEL)
%profileplotOne line per subject by visit, coloured by group (PROC MEANS, PROC SGPANEL)
%scatterplotTwo variables against each other by group (PROC REG, PROC SGPANEL)

The steps of an ADaM program#

MacroIt does
%baselineGive every record of a group the value of its baseline record (PROC SORT, MERGE)
%flag_firstFlag the first or last record of each group that meets a condition (PROC SORT, MERGE)
%flag_if_anyFlag the subjects with a record in another data set that meets a condition (PROC SORT)
%locfLast observation carried forward into the analysis windows, as new records (PROC SORT)
%merge_subjectCopy variables from another data set by subject (PROC SORT, MERGE)
%param_wideThe values of several parameters side by side, one record per group (PROC SORT, MERGE)
%pickThe value of the first or last record of each subject in another data set (PROC SORT)
%poolPool small groups into one pooled group (PROC FREQ, PROC SUMMARY, MERGE)
%relchangeNew records of the change from the record before, relative to the normal range (LAG)
%summarizeThe sum or mean of the records of each subject in another data set (PROC SUMMARY)
%summarize_groupsOne record per group with the sum, mean, largest or smallest value (PROC SUMMARY)
%tteOne time-to-event parameter: the first event, else the latest censoring (PROC SORT)
%visitsGive each record its analysis visit by its visit (a DATA step)
%windowsPut each record in its analysis window by its study day (a DATA step)

The inner parts (a person does not call them)#

MacroIt does
%_spl_argOne parameter of a call in the long program of a one-call table (DATA step statements)
%_spl_beginThe begin line of a macro in the log
%_spl_callOne call of a macro in the long program of a one-call table (DATA step statements)
%_spl_charsThe character variables among some variables of a data set
%_spl_csvWrite a data set as a CSV file (DATA steps)
%_spl_emptyThe result shape without a row: no data qualified
%_spl_endThe end line of a macro in the log
%_spl_figure_attrsThe colours, markers and line patterns of the groups of a figure
%_spl_figure_closeClose the destinations of a figure and set back what it changed
%_spl_figure_dodgeMove the groups of a figure a little apart on the x axis
%_spl_figure_labelThe label of an axis, in quotes
%_spl_figure_nodataThe figure and the result when no record qualified
%_spl_figure_openOpen the PNG and RTF destinations of a figure
%_spl_figure_outparWrite the drawing parameters of a figure macro as a data set
%_spl_figure_parRead par= and the defaults of a figure macro, and check plot=
%_spl_levelsThe planned categories of some variables as rows (DATA step statements)
%_spl_needCheck the parameters a macro needs
%_spl_nobsThe number of rows of a data set
%_spl_oneofCheck that a parameter is one of a list
%_spl_pairlistThe pairs of groups of a parameter as rows _K _A _B (DATA step statements)
%_spl_pairsThe differences of the pairs of groups asked for, from the table Diffs of a procedure
%_spl_programThe fixed parts of the long program of a one-call table (DATA step statements)
%_spl_quoteThe value of a parameter as a text in single quotes (DATA step code)
%_spl_rowsThe result shape: one row per statistic (DATA step statements)
%_spl_stackpairsThe records of each pair of groups asked for, one under the other
%_spl_textThe text of a number by its pattern (DATA step statements)
%_spl_totalThe name of the group of all groups together, from total=
%_spl_varsCheck that variables are in a data set
%_spl_windowsThe rows of windows= or visits= of a call as a data set

%_spl_arg#

One parameter of a call that a one-call table writes into its long program: its name and its value. A parameter without a value is left out.

File: sas/macros/_spl_arg.sas (13NOV2026)

ParameterDefaultIt is
_namerequiredThe name of the parameter in the call; without _value also the parameter of the calling macro whose value is written
_valueA DATA step expression with the value (a text)
%_spl_arg(data, "&rec.")

Output: CODE: DATA step statements that add the parameter to the arrays PN and PV of %_spl_program(begin).

%_spl_begin#

The begin line of a macro in the log, with the version of the macro library; it gives the time it began, for the end line (%_spl_end). The version below is the one place of the version of the library: every header says the same.

File: sas/macros/_spl_begin.sas (13NOV2026)

ParameterDefaultIt is
_macrorequiredThe name of the calling macro
%_spl_begin(describe)

Output: TEXT: the text of the time it began (seconds); the line NOTE: [describe] begins (Sapling macro library 1.1.0). in the log, unless SPL_QUIET is yes (a global macro variable: %LET spl_quiet = yes; before the calls keeps the log short).

%_spl_call#

One call of a macro in the long program of a one-call table: the parameters given by %_spl_arg since the last call, one per line, the equal signs under each other.

File: sas/macros/_spl_call.sas (13NOV2026)

ParameterDefaultIt is
_macrorequiredThe name of the macro called, without the percent sign
%_spl_call(freq)

Output: CODE: DATA step statements that write the call to the file of the long program and start the next list of parameters.

%_spl_chars#

The character variables among some variables of a data set (their names, separated by blanks). The statistics macros give them one length ($200) in their results, so that results stack without a cut value.

File: sas/macros/_spl_chars.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe data set (data set options after it are ignored)
varsrequiredThe variables
%_spl_chars(adae, TRT01A AEBODSYS AEDECOD AVISITN)

Output: TEXT: the names of the character variables among VARS, separated by blanks (a variable not in DATA is left out).

%_spl_csv#

Write a data set as a CSV file: a line with the names of the variables, then one line per row (a DATA step; values with a comma or a quote are quoted).

File: sas/macros/_spl_csv.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe data set (data set options such as KEEP= are allowed)
filerequiredThe CSV file to write
%_spl_csv(data=l14_3_1(KEEP=USUBJID AEDECOD), file=&spl_out./listings/l14_3_1.csv)

Output: FILE: a line with the names of the variables, then one line per row.

%_spl_empty#

The result of a statistics macro when no record qualifies: the result shape without a row, and a NOTE that says so. A procedure never runs on no record, so the log has no error of one.

File: sas/macros/_spl_empty.sas (12NOV2026)

ParameterDefaultIt is
keepThe group variables of the result (BY, WITHIN)
macrorequiredThe name of the calling macro
likerequiredThe records (without a row): the types and lengths of the group variables
addCharacter group variables the records do not have (VERSUS)
outrequiredThe result to write
%_spl_empty(macro=cox, out=&out., like=_cox_records, keep=&by. &within., add=versus)

Output: OUT: KEEP, ADD, GROUPS, BLOCK, STAT, VALUE and TEXT and no row; the line NOTE: [macro] No data qualified ... in the log.

%_spl_end#

The end line of a macro in the log: its run time since its begin line (%_spl_begin), and with debug=yes that its work data sets are kept.

File: sas/macros/_spl_end.sas (12NOV2026)

ParameterDefaultIt is
_macrorequiredThe name of the calling macro
_startrequiredThe time it began (%_spl_begin)
_debugThe debug= of the calling macro
%_spl_end(describe, &t0., &debug.)

Output: LOG: NOTE: [describe] ends after 0.42 seconds. in the log, unless SPL_QUIET is yes; with debug=yes always the line that names the kept work data sets.

%_spl_figure_attrs#

The look of the groups of a figure: an attribute map (DATTRMAP=) that gives each value of the group variable a colour, a marker and a line pattern, in the order of the columns of %columns (columns=; a value not in it after them), else in the order the values come in the data. Eight colours safe for colour blindness (Okabe and Ito, without the yellow, and a grey), each with another marker and line pattern, so that groups differ in black and white too; a ninth group starts the list again. ID grp has all attributes; ID grpcol the colours only, solid lines (an error bar; a marker of its own, such as the censoring mark: give it to a copy of the group variable, as SGPLOT takes one ID per variable). The data get _ORDER, the place of their group.

File: sas/macros/_spl_figure_attrs.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe rows of the figure: they get _ORDER
columnsThe columns of %columns (COL, COLVALUE)
varrequiredThe group variable
outrequiredThe attribute map
%_spl_figure_attrs(data=_kmplot_plot, columns=&columns., var=&by., out=_kmplot_attrs)

Output: OUT: the attribute map: ID, VALUE, _ORDER and the attributes; DATA: data= with _ORDER.

%_spl_figure_close#

Close the destinations of a figure (%_spl_figure_open) and set back what it changed: the RTF file closed, LISTING closed and opened again (its style LISTING) when it was open before, the options of ODS GRAPHICS reset.

File: sas/macros/_spl_figure_close.sas (12NOV2026)

ParameterDefaultIt is
listingnoyes: LISTING was open before the figure
%_spl_figure_close(listing=&listing.)

Output: CODE: the ODS statements that close the destinations.

%_spl_figure_dodge#

Move the groups a little apart on a numeric x axis, so that their error bars do not overlap: _XPLOT = x + (_ORDER - (G + 1) / 2) * STEP, G the number of groups, STEP the smaller of half the smallest distance of two x values divided by G - 1 and 3% of the range of x. The x of the data stays as it is.

File: sas/macros/_spl_figure_dodge.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe rows of the figure, with _ORDER (%_spl_figure_attrs)
xrequiredThe numeric x variable
%_spl_figure_dodge(data=_meanplot_plot, x=x)

Output: DATA: data= with _XPLOT.

%_spl_figure_label#

The label of an axis, as a text in quotes: the text given (in quotes, or put in quotes), else the label of the variable, else its name.

File: sas/macros/_spl_figure_label.sas (12NOV2026)

ParameterDefaultIt is
_datarequiredThe data set
_varThe variable of the axis
_textThe text given, or nothing
%_spl_figure_label(&data., &time., %SUPERQ(xlabel))

Output: TEXT: the label in double quotes.

%_spl_figure_nodata#

No record qualified for a figure: out= in the shape of the figure with no row, the NOTE No data qualified, and the figure that says so (a blank plot with the text, in the PNG and the RTF of file=, under the titles of the program).

File: sas/macros/_spl_figure_nodata.sas (12NOV2026)

ParameterDefaultIt is
keepThe variables of like= kept in out=
macrorequiredThe name of the calling macro
likerequiredThe records, without a row
charsCharacter variables of out= ($200)
numsNumeric variables of out=
filerequiredThe output without its extension
widthrequiredThe width of the figure
heightrequiredThe height of the figure
outThe result to write (none: no data set)
%_spl_figure_nodata(
    keep   = &by.
  , macro  = kmplot
  , like   = _kmplot_records
  , chars  = kind
  , nums   = x y n censored
  , file   = &file.
  , width  = &width.
  , height = &height.
  , out    = &out.
)

Output: OUT: no row; FILE: <file>.png and <file>.rtf with the text.

%_spl_figure_open#

Open the destinations of one figure: the PNG image (the LISTING destination with GPATH=, 300 dpi, image name and size from ODS GRAPHICS) and the RTF file with the figure on a page (the titles and footnotes of the program as text of the page: NOGTITLE NOGFOOTNOTE). The PNG carries the titles and the footnotes in the image (LISTING draws them in the graph, as SAS does by default), so a PNG alone says what it is. Both use the style HTMLBLUE: tick values 9pt, axis labels and legend 10pt, titles 11pt. It notes in the macro variable named by state= whether LISTING was open, for %_spl_figure_close.

File: sas/macros/_spl_figure_open.sas (12NOV2026)

ParameterDefaultIt is
filerequiredThe output without its extension: <file>.png and <file>.rtf
widthrequiredThe width of the figure
heightrequiredThe height of the figure
staterequiredThe name of a local macro variable of the caller: yes when LISTING was open, else no
%_spl_figure_open(file=&file., width=&width., height=&height., state=listing)

Output: CODE: the ODS statements that open the destinations.

%_spl_figure_outpar#

The drawing parameters of a figure macro as a data set (its outpar=): one row per parameter in the order of the signature, PARAMETER its name in lower case and VALUE its value as the macro draws it (given, from par=, the default, or made by the macro: the label of an axis, the format of the ticks). par= of a later call reads it back.

File: sas/macros/_spl_figure_outpar.sas (13NOV2026)

ParameterDefaultIt is
namesrequiredThe drawing parameters of the caller, in their order (names= of %_spl_figure_par)
outThe data set to write; blank: none
%_spl_figure_outpar(names=&drawn., out=&outpar.)

Output: DATA: out= with PARAMETER ($32) and VALUE ($500).

%_spl_figure_par#

The parameters of a figure macro, before its checks. With plot= (draw the out= of a call again): data= is not given, plot= has the variables of out=, and each parameter of the computation that has a value gets a NOTE (it is not used). With par= (the outpar= of a call): each drawing parameter that the call leaves blank gets the VALUE of its row; a row that names no drawing parameter is an error. Then each parameter still blank gets its default from draw= or compute=. So a value in the call wins over a row of par=, and a row of par= over the default: the defaults of these parameters are here, not in the signature of the macro.

File: sas/macros/_spl_figure_par.sas (13NOV2026)

ParameterDefaultIt is
macrorequiredThe name of the calling macro
drawrequiredIts drawing parameters in the order of its signature, each as name or name=default
computeIts parameters of the computation that plot= does not use, each as name or name=default
shaperequiredThe variables of its out= that plot= must have
namesrequiredThe name of a local macro variable of the caller: the names of the drawing parameters
staterequiredThe name of a local macro variable of the caller: 1 when the parameters are right, else 0
%_spl_figure_par(
    macro   = kmplot
  , draw    = columns by panel atrisk=yes xlabel ylabel="Survival (%)" file
  , compute = where time cnsr=CNSR
  , shape   = kind x y lower upper n censored
  , names   = drawn
  , state   = ok
)

Output: LOG: one ERROR with its fix for a wrong plot= or par=, a NOTE for each parameter not used. The local macro variables of the caller: the parameters set from par= and from the defaults, state= and names=.

%_spl_levels#

The planned categories of some variables as rows: every combination of their values, by DO loops. A character variable gets the length $200.

File: sas/macros/_spl_levels.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe data set of the variables (it tells which ones are character)
levelsrequiredEach variable followed by its values: a text in quotes, a number without
%_spl_levels(levels=BNRIND 'LOW' 'NORMAL' 'HIGH' ANRIND 'LOW' 'NORMAL' 'HIGH', data=adlbc)

Output: CODE: DATA step statements: LENGTH of the character variables ($200), then DO loops that output every combination of the planned values (DO BNRIND = ...; DO ANRIND = ...; OUTPUT; END; END;).

%_spl_need#

Check the parameters a macro needs: each must have a value, and a data set of data=, denom=, columns= or from= must exist. For each one that does not it writes one line that names the parameter and the fix: ERROR: [describe] data= is required: give data=<value> ... ERROR: [describe] data=adsx does not exist: give data= ... It gives 1 when all are there, 0 when one is not: the caller then stops (%RETURN).

File: sas/macros/_spl_need.sas (13NOV2026)

ParameterDefaultIt is
_macrorequiredThe name of the calling macro (for the message)
_paramsrequiredThe names of its parameters that must have a value
%_spl_need(describe, data var by out)

Output: TEXT: 1 or 0, as text.

%_spl_nobs#

The number of rows of a data set (with the WHERE= of its data set options, when it has one); 0 when the data set does not exist.

File: sas/macros/_spl_nobs.sas (12NOV2026)

ParameterDefaultIt is
_datarequiredThe data set
%_spl_nobs(_describe_values)

Output: TEXT: the number, as text.

%_spl_oneof#

Check that the value of a parameter is one of a list (in any case). When it is not it writes one line that names the parameter and the fix: ERROR: [cox] ties=fast is not one of efron breslow exact ... It gives 1 or 0: the caller then stops (%RETURN).

File: sas/macros/_spl_oneof.sas (12NOV2026)

ParameterDefaultIt is
_macrorequiredThe name of the calling macro (for the message)
_paramrequiredThe parameter
_listrequiredIts values, separated by blanks
%_spl_oneof(cox, ties, efron breslow exact discrete)

Output: TEXT: 1 or 0, as text.

%_spl_pairlist#

The pairs of groups of a parameter as rows: _K (the place of the pair), _A (the group) and _B (the group it is compared with). The pairs are separated by #, one pair per line, the first group against the second; a value with a blank or a hyphen is in quotes: pairs = "Xanomeline High Dose" "Placebo" # "Xanomeline Low Dose" "Placebo" The form of version 1.0 is still read: "A"-"B" "C"-"D" (no #, every item a pair joined by a hyphen outside quotes).

File: sas/macros/_spl_pairlist.sas (12NOV2026)

ParameterDefaultIt is
listrequiredThe parameter of the calling macro that holds the pairs (pairs, family)
%_spl_pairlist(list=pairs)

Output: CODE: DATA step statements: one row (OUTPUT) per pair with _K, _A and _B ($200).

%_spl_pairs#

The differences of the pairs of groups asked for, from the table Diffs of PROC MIXED (it has each pair once, in one direction). It computes nothing: a difference the other way round is the same difference with its sign and its limits turned.

File: sas/macros/_spl_pairs.sas (12NOV2026)

ParameterDefaultIt is
byrequiredThe groups: a character variable
withinThe variables of the separate models (BY groups of PROC MIXED)
diffsrequiredThe Diffs table (the group in BY, the group it is compared with in _BY)
recordsrequiredThe records of the model (for the groups and their order)
pairsrequiredall: each later group against each earlier one, in the order of ORDER (else of the values); or the pairs separated by #, the group first, then the group it is compared with (%_spl_pairlist reads them, and the form "A"-"B" of 1.0)
orderA numeric variable of the records with one value per group
outrequiredThe data set: WITHIN, BY (the group), VERSUS (the group it is compared with), diff, diff_se, lcl, ucl, pval
%_spl_pairs(
    by      = TRT01P
  , within  =
  , diffs   = _lsmeans_diffs
  , records = _lsmeans_records
  , pairs   = all
  , order   = TRT01PN
  , out     = _lsmeans_pairs
)

Output: OUT: WITHIN, BY (the group), VERSUS (the group it is compared with), diff, diff_se, lcl, ucl, pval: one row per pair asked for (in each group of WITHIN).

%_spl_program#

The fixed parts of the DATA step with which a one-call table writes its long program: begin (the file and the variables), records (the population and the records, from the parameters ADSL, POP, DATA, WHERE and NAME of the calling macro), table (the %table call, from BY, ORDER, TOTAL, FORMAT, FILE, NAME and DEBUG), end (the routine that writes a call). It computes nothing.

File: sas/macros/_spl_program.sas (13NOV2026)

ParameterDefaultIt is
_partrequiredbegin, names, label, records, table or end
_varWith label: the variable of the block, in lower case
%_spl_program(begin)

Output: CODE: DATA step statements. It reads the macro variables REC (the records the calls read), POPN (the data set with the N of the columns), DS and NAMES of the calling macro.

%_spl_quote#

The value of a parameter as a text in single quotes, for a DATA step: each single quote in it doubled, nothing in it resolved. The code the macro writes holds the value itself (SYMGET would read it when the step runs), so that the flat program of sapling sas run --flat (the code without a macro) has it too. An empty value gives a blank text.

File: sas/macros/_spl_quote.sas (12NOV2026)

ParameterDefaultIt is
_paramrequiredThe name of the parameter (label, pairs)
%_spl_quote(label)

Output: TEXT: a text in single quotes: 'Age (years)', '"A" "B" # "C" "B"'.

%_spl_rows#

The result shape: one row per statistic of the row just read. Each statistic is a numeric variable of the same name (n, mean, pval ...); the row gets STAT (its name), VALUE (its number), PATTERN and TEXT (the number as printed, %_spl_text: a p value, pval or padj, never as 0 or 1).

File: sas/macros/_spl_rows.sas (13NOV2026)

ParameterDefaultIt is
missingThe text of a statistic without a value (default blank; %km gives NE: not estimable)
statsrequiredThe statistics in their order, each with its pattern (mean="XX.X") or without (the default pattern of the statistic below)
%_spl_rows(stats=n="XX" mean="XX.X" sd)

Output: CODE: DATA step statements: one row per statistic with STAT, VALUE and TEXT. The default patterns: XX for n, events, min, max; XX.X for mean, median, q1, q3, lsmean, diff, estimate, lcl, ucl; (XX.X%) for pct; X.XXX for pval; XX.XX for sd, se, lsmean_se, diff_se and any other statistic.

%_spl_stackpairs#

The records of each pair of groups asked for, one under the other: the pair (_PAIR), its group (_A) and the group it is compared with (_B), and the side of each record (_SIDE: 1 in _A, 2 in _B). A procedure then runs BY _PAIR _A _B on the 2 groups of each pair, the group first. It computes nothing.

File: sas/macros/_spl_stackpairs.sas (13NOV2026)

ParameterDefaultIt is
byrequiredThe groups: a character variable
recordsrequiredThe records (one per subject)
pairsrequiredall: each later group against each earlier one, in the order of ORDER (else of the values); or the pairs separated by #, the group first, then the group it is compared with (%_spl_pairlist reads them, and the form "A"-"B" of 1.0)
orderA numeric variable of the records with one value per group
macrorequiredThe macro that calls (for the message)
outrequiredThe data set: the records of each pair with _PAIR, _A, _B, _SIDE, sorted by them
%_spl_stackpairs(
    macro   = ttest
  , records = _ttest_records
  , by      = TRT01P
  , pairs   = all
  , order   = TRT01PN
  , out     = _ttest_pairs
)

Output: OUT: the records of each pair with _PAIR, _A, _B and _SIDE (1 in _A, 2 in _B), sorted by them. The macro variable PAIRSOK of the caller: 1, or 0 when a pair names a group that no record has (an error in the log).

%_spl_text#

The text of a number by its pattern. A pattern shows one number as one group of X: XX, XX.X, (XX.X%), (N=XX). The number is rounded (ROUND) to the decimals of the X group, right aligned in its width and put in its place; the text is trimmed. A p value never prints as 0 or 1: rounded to 0 it is < and the smallest step (X.XXX: <0.001), rounded to 1 it is > and one step less (>0.999). The number is unchanged.

File: sas/macros/_spl_text.sas (12NOV2026)

ParameterDefaultIt is
valuerequiredThe numeric variable
patternrequiredThe character variable with the pattern
textrequiredThe character variable to write (blank when the number is missing)
pvalue0A condition that is true when the number is a p value (STAT IN ('pval' 'padj'))
%_spl_text(
      value   = VALUE
    , pattern = PATTERN
    , text    = TEXT
    , pvalue  = STAT IN ('pval' 'padj')
  )

Output: CODE: DATA step statements: TEXT is the number of VALUE as its PATTERN shows it (9.46 with (XX.X%) gives ( 9.5%); 86 with (N=XX) gives (N=86); 3 with XX gives 3; a p value 0.00004 with X.XXX gives <0.001).

%_spl_total#

The name of the group of all groups together, from total=: nothing for no, Total for yes, else the text in quotes.

File: sas/macros/_spl_total.sas (12NOV2026)

ParameterDefaultIt is
totalrequiredThe value of total= (no, yes, or a name in quotes)
%_spl_total(%SUPERQ(total))

Output: TEXT: nothing for no (or empty), Total for yes, else the text of the quotes.

%_spl_vars#

Check that the variables of some parameters are variables of a data set. For each one that is not it writes one line that names the parameter and the fix: ERROR: [describe] var=AGEX is not a variable of adsl: give ... It gives 1 when all are there, 0 when one is not: the caller then stops (%RETURN). A data set that cannot be opened is the business of %_spl_need.

File: sas/macros/_spl_vars.sas (12NOV2026)

ParameterDefaultIt is
_macrorequiredThe name of the calling macro (for the message)
_datarequiredThe parameter that names the data set (data, denom)
_paramsrequiredThe parameters whose values are variables of it
%_spl_vars(describe, data, var by)

Output: TEXT: 1 or 0, as text.

%_spl_windows#

The rows of windows= (or visits=) of a call as a data set: one row per window, the rows of the call separated by #, the values of a row by blanks, a text in quotes, a missing number as a period.

File: sas/macros/_spl_windows.sas (13NOV2026)

ParameterDefaultIt is
_macrorequiredThe macro that calls
_namerequiredThe name of its parameter with the rows
_kindrequiredday: AVISITN AVISIT AWLO AWHI AWTARGET AWRANGE; visit: VISIT AVISITN AVISIT
_outrequiredThe data set of the rows
%_spl_windows(windows, windows, day, _windows_rows)

Output: DATA: _out, one row per window in the order of the call. The macro variable ROWSOK of the caller: 1, or 0 when a row has not its number of values (an error in the log).

%adjust#

The adjusted p-values of a family of pairs of groups (the hypotheses of a multiplicity block): the p-values (STAT = pval) of a result of pairs (%lsmeans, %cox, %logistic, %risk, %rateratio, %ttest, %wilcoxon), in the order of family=, adjusted by method= (PROC MULTTEST INPVALUES=: bonferroni, holm (STEPBON), hochberg; a DATA step: bonferroni with weights, hierarchy, the fixed sequence: the largest p-value of a hypothesis and those before it). The result is the input with a row padj for each pair of the family.

Procedure: PROC MULTTEST (INPVALUES=); a DATA step
Proven: the known answer of the method riskdiff (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts) with the adjusted p-values of a family; ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/adjust.sas (12NOV2026)

ParameterDefaultIt is
datarequiredA result of pairs: BY, VERSUS, STAT, VALUE
byrequiredThe groups: the variable of the group of each pair
familyrequiredThe pairs of the family in their order, separated by #, one pair per line, the group first, then the group it is compared with: "High" "Placebo" # "Low" "Placebo". The form "A"-"B" of version 1.0 is still read.
methodrequiredbonferroni, holm, hochberg or hierarchy
weightsThe weight of each hypothesis of a weighted Bonferroni test, in the order of family=, separated by blanks
pattern"X.XXX"The pattern of the adjusted p-value (default "X.XXX")
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_adjust_*)
%adjust(
    data   = resp_rd
  , by     = TRT01P
  , family = "High" "Placebo"
           # "Low"  "Placebo"
  , method = hochberg
  , out    = resp_rd
)

Output: OUT: the rows of data= and a row padj per pair of the family.

%aeplot#

The adverse events of the subjects as two panels side by side that share the axis of the terms: at the left the percent of the subjects of each group with the term (dots), at the right the risk difference of each group against the reference group with its interval and a line at 0. The flags are those of %termflags, the risk differences those of %risk (ci=, level=), so that the figure and a table made with them cannot differ. The terms: those of threshold=, sorted by sort=, at most max= of them.

Procedure: PROC SUMMARY, PROC SGPANEL (and PROC FREQ in %risk)
Proven: its known answer in tools/figures_sas.ts (percents and Wald intervals worked out by hand); not yet run on a SAS server
File: sas/macros/aeplot.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe adverse event records
plotDraw again: an out= of %aeplot (not with data=)
parThe drawing parameters: an outpar= of %aeplot
columnsThe columns of %columns: the order of the groups and their colours (default: the order of denom=)
denomrequiredThe population: one row per subject with by=
whereThe records to use, a condition in parentheses (TRTEMFL = 'Y')
termAEDECODThe term of a row
byrequiredThe groups: a character variable of denom=
referencerequiredThe reference group: a value of by=, in quotes
thresholdA percent: only the terms with this percent of the subjects of a group or more, in at least one group
sortdifferenceThe order of the terms from the top: difference (the risk difference of the first group that is not the reference, largest first), frequency (the percent of all subjects, largest first) or alphabetical
ciwaldThe interval of the risk difference: ci= of %risk
level0.95The confidence level
max30The most terms shown (the first in sort=)
xlabelThe heading of the right panel (default: Risk difference (%) with 95% CI, the level of level=)
filerequiredThe output without its extension: <file>.png (300 dpi) and <file>.rtf
width6.5inThe width of the figure
height4.5inThe height of the figure
outThe numbers drawn
outparThe drawing parameters: one row each
debugnoyes: keep the work data sets _aeplot_*
%aeplot(
    data      = adae
  , columns   = t14_5_1_cols
  , denom     = adsl
  , where     = (TRTEMFL = 'Y')
  , term      = AEDECOD
  , by        = TRTA
  , reference = "Placebo"
  , threshold = 5
  , file      = &spl_out./figures/f14_5_3
  , out       = f14_5_3
)

Output: OUT: TERM and BY, KIND, X, LOWER, UPPER, N. KIND percent: one row per term and group, X the percent of its subjects, N the subjects with the term. KIND difference: one row per term and group that is not the reference, X the risk difference in percent (100 x RD of %risk), LOWER and UPPER its limits in percent. OUTPAR: one row per drawing parameter in the order of the table: PARAMETER ($32) its name, VALUE ($500) its value as drawn (a label in quotes). FILE: <file>.png and <file>.rtf.

%ancova#

A linear model of a variable, one record per subject (PROC GLM): class factors, covariates and the tested term. The Type III test of the tested term (pval); its coefficient, its standard error and confidence limits (estimate, se, lcl, ucl) when stats= asks for them.

Procedure: PROC GLM
Proven: the known answer of the method ancova (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/ancova.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe response
covarsThe covariates, separated by blanks
classThe class factors, separated by blanks
testrequiredThe tested term: a numeric variable (a dose, a treatment number)
level0.95The confidence level of the limits (default 0.95)
statspval="X.XXX"The statistics, each with its pattern (default pval="X.XXX"); also estimate, se, lcl, ucl
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_ancova_*)
%ancova(
    data   = advs
  , where  = (PARAMCD = 'SYSBP' and AVISIT = 'Week 12')
  , var    = CHG
  , covars = BASE
  , class  = SITEGR1
  , test   = TRTPN
  , out    = chg_test
)

Output: OUT: one row per statistic, of all subjects together (GROUPS is blank).

%anova#

The one-way analysis of variance of a variable between groups (PROC GLM): the F test of the model (pval).

Procedure: PROC GLM
Proven: the known answer of the method anova (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/anova.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe variable
byrequiredThe groups to compare: one variable
statspval="X.XXX"The statistic and its pattern (default pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data set of the macro (_anova_overall)
%anova(
    data  = adsl
  , var   = AGE
  , by    = TRT01A
  , label = "Age (years)"
  , out   = age_test
)

Output: OUT: one row, of all groups together (GROUPS is blank).

%ard#

Every number of a table as a CSV file, with every digit: one line per number with the result it comes from, its statistic and the values of its groups (in the order of GROUPS), then the number and its text. The N of each column comes first (result N). It reads the results, never the printed page: sapling sas check compares these numbers with those of R.

Procedure: DATA steps (FILE, PUT)
Proven: the tables of the dummy study SYNHTN01 (sapling sas check: SAS CHECK PASS, 759 of 759 numbers); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/ard.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe results (%stack)
columnsThe columns (%columns): the N of each column
fileThe name of the output, without extension (default: the program)
%ard(data=t14_2_1, columns=t14_2_1_cols)

Output: FILE: <out>/ard/<file>.csv: result, stat, g1_value ... g5_value, raw, formatted; the N of each column first (result N), then every number of the results.

%barplot#

Bars of the categories of a variable in each group: the percent of the subjects of the group (denom=: the subjects of the population; a subject counts once in a category) or of its records, or the count; the groups side by side or stacked, the value on each bar. One panel per value of panel=.

Procedure: PROC FREQ, PROC SGPANEL
Proven: its known answer in tools/figures_sas.ts (counts and percents worked out by hand); not yet run on a SAS server
File: sas/macros/barplot.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
plotDraw again: an out= of %barplot (not with data=)
parThe drawing parameters: an outpar= of %barplot
columnsThe columns of %columns: the order of the groups and their colours (default: the order of the data)
denomThe population: one row per subject (USUBJID) with by=; its subjects of a group are the N of the percent (default: the records of the group)
whereThe records to use, a condition in parentheses
varrequiredThe category
byrequiredThe groups: one colour each
panelOne panel per value of this variable
statpercentpercent or count
stackednoyes: the groups stacked in one bar
labelsyesyes: the value on each bar
xlabelThe label of the x axis (default: the label of var=)
ylabelThe label of the y axis (default: Subjects (%), Records (%), Subjects or Records)
refReference lines on the y axis: values
filerequiredThe output without its extension: <file>.png (300 dpi) and <file>.rtf
width6.5inThe width of the figure
height4.5inThe height of the figure
outThe numbers drawn
outparThe drawing parameters: one row each
debugnoyes: keep the work data sets _barplot_*
%barplot(
    data    = adae
  , columns = t14_5_1_cols
  , denom   = adsl
  , where   = (TRTEMFL = 'Y')
  , var     = AESEV
  , by      = TRTA
  , file    = &spl_out./figures/f14_5_2
  , out     = f14_5_2
)

Output: OUT: one row per bar: PANEL and BY, KIND bar, X (the category as printed), Y (the percent or the count), N (the subjects or records of the bar), DENOM (the N of the percent). A category without a record in a group has no bar. OUTPAR: one row per drawing parameter in the order of the table: PARAMETER ($32) its name, VALUE ($500) its value as drawn (a label in quotes). FILE: <file>.png and <file>.rtf.

%baseline#

Give every record of a group the value of the group's baseline record (the one with the baseline flag).

Procedure: PROC SORT, MERGE
Proven: the fixtures of the step kind baseline (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/baseline.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe working data set (changed in place)
valuerequiredThe variable whose baseline value is taken (AVAL)
byrequiredThe groups (one baseline record in each)
flagABLFLThe baseline flag (default ABLFL = 'Y')
replacenoyes: NEW is in the data already and is made again (default no)
newrequiredThe variable to make (BASE)
debugnoyes: keep the work data sets _baseline_*
%baseline(
    data  = advs
  , value = AVAL
  , by    = USUBJID PARAMCD
  , new   = BASE
)

Output: DATA: NEW on every record: the VALUE of the baseline record of its group (missing for a group without one); two baseline records in a group are an ERROR that names the group. The rows keep their order.

%boxplot#

Box plots of a variable in each group (and at each visit of x=): the box from Q1 to Q3 with the median, the whiskers to the most extreme values within 1.5 times the interquartile range of the box, the values beyond as outliers, the mean as a marker. The quantiles are those of PROC MEANS QNTLDEF=5 and of VBOX (its default PERCENTILE=5; R quantile type 2). One panel per value of panel=.

Procedure: PROC MEANS, PROC SGPANEL
Proven: its known answer in tools/figures_sas.ts (quartiles and whiskers worked out by hand); not yet run on a SAS server
File: sas/macros/boxplot.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
plotDraw again: an out= of %boxplot (not with data=)
parThe drawing parameters: an outpar= of %boxplot
columnsThe columns of %columns: the order of the groups and their colours (default: the order of the data)
whereThe records to use, a condition in parentheses
yrequiredThe variable
xThe category of the x axis (the visit, AVISITN) (default: one box per group)
byrequiredThe groups: one box each
panelOne panel per value of this variable, one under the other, each with its own y axis
outliersyesyes: the values beyond the whiskers as markers
meanyesyes: the mean as a marker in the box
xformatThe format of the categories of x (default: the format of x)
xlabelThe label of the x axis (default: the label of x)
ylabelThe label of the y axis (default: the label of y)
refReference lines on the y axis: values
filerequiredThe output without its extension: <file>.png (300 dpi) and <file>.rtf
width6.5inThe width of the figure
height4.5inThe height of the figure
outThe numbers drawn
outparThe drawing parameters: one row each
debugnoyes: keep the work data sets _boxplot_*
%boxplot(
    data    = advs
  , columns = t14_3_1_cols
  , where   = (PARAMCD = 'SYSBP' and ANL01FL = 'Y')
  , y       = CHG
  , x       = AVISITN
  , by      = TRTA
  , xformat = VISIT.
  , ref     = 0
  , file    = &spl_out./figures/f14_3_1
  , out     = f14_3_1
)

Output: OUT: one row per box: PANEL, BY, KIND box, X (the category; missing without x=), N, MEAN, MEDIAN, Q1, Q3, MIN, MAX, WLOW and WHIGH (the ends of the whiskers: the smallest and the largest value within Q1 - 1.5 IQR and Q3 + 1.5 IQR). OUTPAR: one row per drawing parameter in the order of the table: PARAMETER ($32) its name, VALUE ($500) its value as drawn (a label in quotes). FILE: <file>.png and <file>.rtf.

%chisq#

The Pearson chi-square test (without continuity correction) of groups against categories, each subject once in each group and category (PROC FREQ): pval. With exact=yes Fisher's exact test of the same table.

Procedure: PROC FREQ (CHISQ, FISHER)
Proven: the known answer of the methods chisq, fisher (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/chisq.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe categories (the response): one variable
byrequiredThe groups to compare: one variable
withinOne test in each group of these variables (a parameter, a visit)
exactnoyes: Fisher's exact test (default no)
statspval="X.XXX"The statistic and its pattern (default pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_chisq_*)
%chisq(
    data  = adsl
  , var   = SEX
  , by    = TRT01A
  , label = "Sex"
  , out   = sex_test
)

Output: OUT: one row, or one per group of WITHIN (GROUPS: their names). Where PROC FREQ makes no test (a table with one row or one column) the value is missing.

%cmh#

The Cochran-Mantel-Haenszel test of groups against categories in strata (PROC FREQ), each subject once in each group, category and stratum: pval. The test of general association, or (statistic=rowmean) the test that the mean scores of the groups differ, the scores being the values of the numeric response.

Procedure: PROC FREQ (CMH)
Proven: the known answer of the method cmh (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/cmh.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe categories (the response): one variable
byrequiredThe groups to compare: one variable
strataThe stratification variables, separated by blanks
withinOne test in each group of these variables (a parameter, a visit)
statisticgeneralgeneral (default): general association; rowmean: row mean scores differ
statspval="X.XXX"The statistic and its pattern (default pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_cmh_*)
%cmh(
    data      = adqscibc
  , var       = AVAL
  , by        = TRT01P
  , strata    = SITEGR1
  , within    = AVISIT
  , statistic = rowmean
  , out       = cibic_test
)

Output: OUT: one row, or one per group of WITHIN (GROUPS: their names). Where PROC FREQ makes no test the value is missing.

%columns#

The columns of a table: each value of the column variable in the results (and in the population), in the order of ORDER (else of the values), the total last; the N of each column from the population (its subjects, the total all of them) and the header of the column with its N: Placebo|(N=86).

Procedure: PROC SUMMARY
Proven: the tables of the dummy study SYNHTN01 (sapling sas check: the N of every column the same as R); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/columns.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe results (%stack)
nThe population, one row per subject (USUBJID): its subjects of each column are the N in the header
varrequiredThe column variable: the first BY of the statistics macros
orderIts numeric twin in N= (TRT01AN): the order of the columns (default: the order of the values)
totalnoyes: the results have the group Total (total=yes of the statistics macros): the last column, its N all subjects of N=. A text in quotes is another name.
outrequiredThe columns
debugnoyes: keep the work data sets _columns_*
%columns(
    data  = t14_2_1
  , n     = adsl
  , var   = trt01a
  , order = trt01an
  , total = yes
  , out   = t14_2_1_cols
)

Output: OUT: one row per column: COL (1, 2, ...), COLVALUE (the value), BIGN (the N) and HEADER (the value, a | and (N=86): the split character of %print).

%cox#

A Cox proportional hazards model of a time, one record per subject (PROC PHREG): the groups, the covariates and the class factors, stratified by the strata (each combination of their values a stratum with its own baseline hazard). The hazard ratio of pairs of groups from one fit (hr), its Wald confidence limits (lcl, ucl) and the Wald p-value of the pair (pval). With pvalue=logrank the p-value is the log-rank test of the two groups of the pair, stratified by the same strata (PROC LIFETEST); the hazard ratio and its limits stay those of the model.

Procedure: PROC PHREG (LSMEANS); PROC LIFETEST for pvalue=logrank
Proven: the known answer of the method cox (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/cox.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe time
byrequiredThe groups of the model: a character variable
cnsrCNSRThe censoring variable: 1 = censored (default CNSR)
withinOne model in each group of these variables (the levels of a subgroup)
covarsThe covariates
classThe class factors
strataThe strata variables, separated by blanks
pairsallall: each later group against each earlier one, in the order of ORDER; or the pairs separated by #, one pair per line, the group first, then the group it is compared with (a value with a blank or a hyphen in quotes): "Drug" "Placebo" # "High" "Low". The form "A"-"B" of version 1.0 is still read.
orderA numeric variable with one value per group: the order of the groups
tiesefronThe likelihood of tied times: efron (default), breslow, or exact (the discrete likelihood, TIES=DISCRETE; R coxph calls it exact)
pvaluewaldwald (default): the Wald test of the pair in the model; logrank: the log-rank test of the two groups of the pair, in the strata
level0.95The confidence level (default 0.95)
statshr="XX.XX" lcl="XX.XX" ucl="XX.XX" pval="X.XXX"The statistics, each with its pattern (default hr="XX.XX" lcl="XX.XX" ucl="XX.XX" pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_cox_*)
%cox(
    data   = adtte
  , where  = (PARAMCD = 'PFS')
  , var    = AVAL
  , by     = TRT01P
  , strata = STRAT1 STRAT2
  , order  = TRT01PN
  , out    = pfs_hr
)

Output: OUT: one row per pair and statistic (BY: the group, VERSUS: the group it is compared with); GROUPS is BY VERSUS (and WITHIN).

%csv#

The records of a listing (or the numbers of a figure, out= of a figure macro) as a CSV file: a line with the names of the variables, then one line per record in the order of the data. sapling sas check compares it with the listing or the figure of R.

Procedure: DATA steps (FILE, PUT)
Proven: the listings of the dummy study SYNHTN01 (sapling sas check: listings the same in R and SAS); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/csv.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records of the listing
varrequiredThe variables of the listing
folderlistingsThe folder in the outputs: listings or figures
filerequiredThe name of the listing, without extension
%csv(data=adae, var=trt01a usubjid aedecod astdt, file=l14_5_1)

Output: FILE: <out>/<folder>/<file>.csv: the names of VAR, then one line per record.

%describe#

Descriptive statistics of a variable in each group: n, mean, SD, median, minimum and maximum of the values that are not missing; Q1, Q3 and the standard error when stats= asks for them.

Procedure: PROC MEANS
Proven: the known answer of the method contsumm (src/methods.ts, worked out by tools/method_answers.R); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/describe.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records: a data set
whereThe records to use, a condition in parentheses
varrequiredThe variable
byrequiredThe groups: variables. The first gives the columns of a table, the others its rows (a parameter, a visit).
keepVariables kept in the result for the names and the order of its rows (PARAM, AVISITN): the largest value in each group (the ID statement)
totalnoyes: also all groups of the first BY variable together, as the group "Total"; a text in quotes is another name for it
qntldef5The definition of the quantiles, 1 to 5 (5 is the SAS default)
statsn="XX" mean="XX.X" sd="XX.XX" median="XX.X" min="XX" max="XX"The statistics in the order of the rows, each with its pattern; also q1, q3 and se
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets _describe_*
%describe(
    data  = adsl
  , var   = AGE
  , by    = TRT01A
  , total = yes
  , label = "Age (years)"
  , out   = age
)

Output: OUT: one row per group and statistic: the BY variables (and KEEP), GROUPS (the names of the BY variables), BLOCK (the label), STAT, VALUE (the number) and TEXT (the number as printed). A group without a value has no row; no value at all: no row and the NOTE "No data qualified".

%fisher#

Fisher's exact test of "subjects with a record against subjects without" between groups (PROC FREQ): pval. Every subject of DENOM is counted, with a record or without: the usual test of an adverse event between treatments. With exact=no the Pearson chi-square test of the same table.

Procedure: PROC FREQ (FISHER, CHISQ)
Proven: the known answer of the method fisher (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/fisher.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
denomrequiredThe subjects (one row each, with BY): a data set, with options if needed
whereThe records to use, a condition in parentheses
byrequiredThe groups to compare: one variable, on DENOM
withinOne test in each group of these variables (each term)
exactyesno: the chi-square test (default yes)
statspval="X.XXX"The statistic and its pattern (default pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_fisher_*)
%fisher(
    data   = adae
  , denom  = adsl
  , where  = (TRTEMFL = 'Y')
  , by     = TRT01A
  , within = AEBODSYS AEDECOD
  , out    = pt_test
)

Output: OUT: one row, or one per group of WITHIN (GROUPS: their names). Where PROC FREQ makes no test the value is missing.

%flag_first#

Flag one record of each group: the first (or last) one, among the records that meet a condition. The other records keep a blank flag.

Procedure: PROC SORT, MERGE
Proven: the fixtures of the step kind flag.extreme (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/flag_first.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe working data set (changed in place)
whereThe records that can be chosen, a condition in parentheses
byrequiredThe groups
orderrequiredThe sort keys within a group; "descending" before a key as in a BY statement. A missing key sorts last; records with equal keys keep their order.
whichfirstfirst (default) or last
newrequiredThe flag to make (Y on the chosen record)
debugnoyes: keep the work data sets _flag_first_*
%flag_first(
    data  = advs
  , where = (not missing(AVISITN) and not missing(AVAL))
  , by    = USUBJID PARAMCD AVISITN
  , order = AWTDIFF ADY VSSEQ
  , new   = ANL01FL
)

Output: DATA: NEW: Y on the first (last) record of each group among the records that meet where=; the others keep a blank NEW. The rows keep their order.

%flag_if_any#

Flag the subjects that have a record in another data set that meets a condition.

Procedure: PROC SORT, MERGE
Proven: the fixtures of the step kinds flag.condition, flag.any (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/flag_if_any.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe working data set (changed in place)
fromrequiredThe data set of the records
whererequiredThe condition on FROM, in parentheses
elseNThe value of the other subjects: N (default) or blank
newrequiredThe flag to make: Y for a subject with such a record
debugnoyes: keep the work data sets _flag_if_any_*
%flag_if_any(
    data  = adsl
  , from  = sdtm.ds
  , where = (DSDECOD = 'COMPLETED')
  , new   = COMPLFL
)

Output: DATA: NEW: Y on every row of a subject that has such a record in FROM, else N (or blank with else=blank), in the order of its rows.

%forestplot#

The forest plot of results in the levels of subgroups: one row per level under a heading row of its factor, the estimate as a marker with its interval as a bar, a line at no difference, a log axis for a ratio, and the estimate (lower, upper) as text at the right. It draws results (of %cox, %logistic, %rateratio, %lsmeans or %risk with within=), so that the figure and the table of the same results cannot differ.

Procedure: PROC SUMMARY, PROC SGPLOT
Proven: its known answer in tools/figures_sas.ts (made-up results, arranged by hand); not yet run on a SAS server
File: sas/macros/forestplot.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe results: rows of STAT and VALUE (the result shape), with the variables of within=
plotDraw again: an out= of %forestplot (not with data=)
parThe drawing parameters: an outpar= of %forestplot
columnsThe columns of %columns: the order and colours of the groups of by=
whereThe results to use, a condition in parentheses
estimaterequiredThe statistic of the estimate (hr, or, irr, rr, rd, diff)
lowerlclThe statistic of the lower limit
upperuclThe statistic of the upper limit
withinrequiredThe variables of the levels, one per factor, in the order of the rows; a row is the level of the first of them that has a value
groupA variable with the heading of each factor (default: the label of the variable of within=)
byThe group of a pair (the results with VERSUS: the pair is by= vs VERSUS); one colour per pair
scaleloglog (a ratio) or linear (a difference)
textyesyes: the estimate (lower, upper) as text at the right under the name of the statistic (HR (CI)), and the n when the results have a statistic n
xlabel"Estimate (95% CI)"The label of the x axis
ref1The line of no difference: 1 for a ratio, 0 for a difference
filerequiredThe output without its extension: <file>.png (300 dpi) and <file>.rtf
width6.5inThe width of the figure
height4.5inThe height of the figure
outThe numbers drawn
outparThe drawing parameters: one row each
debugnoyes: keep the work data sets _forestplot_*
%forestplot(
    data     = t14_2_sg
  , estimate = hr
  , within   = SEX AGEGR1 RACE
  , by       = TRTA
  , scale    = log
  , ref      = 1
  , file     = &spl_out./figures/f14_2_sg
  , out      = f14_2_sg
)

Output: OUT: one row per level and pair: FACTOR (the variable of within=), LEVEL (its value as printed), PAIR (by= vs VERSUS; blank without by=), KIND estimate, X (the estimate), LOWER, UPPER, N (missing unless the results have n). OUTPAR: one row per drawing parameter in the order of the table: PARAMETER ($32) its name, VALUE ($500) its value as drawn (a label in quotes). FILE: <file>.png and <file>.rtf.

%freq#

Frequencies of subjects (PROC FREQ): the subjects of each category in each group (n), their percent of the subjects of the group (pct), and the records (events) when stats= asks for them. A subject is counted once in each cell. Without var= the cell is "a record at all".

Procedure: PROC FREQ
Proven: the known answer of the methods count, catsumm (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/freq.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
denomThe subjects of each group: the N of the percent and the columns (default: DATA without WHERE). A data set, with options if needed: adsl(where=(...)).
whereThe records to count, a condition in parentheses
varThe categories: variables, nested in their order (a term within its class)
byrequiredThe groups: one variable, the columns of a table
keepVariables to keep with the categories for their names and order: PARAM and PARAMN go with PARAMCD, AVISITN with AVISIT (a twin <V>N with V); the value of the first record of the category
levelsThe planned categories of some variables of var=: each variable and its values. They show also without a subject; a record of another category is not counted. The categories of the other variables come from the records counted: a record of another category of a planned variable brings none.
totalnoyes: also all groups together, as the group "Total" (a text in quotes is another name for it). Default no.
thresholdA percent: only the categories with this percent of the subjects of a group or more, in at least one group (the percent itself, not its rounded text)
orderfrequency: the rows of the variable ORDEROF in the order of their subjects, the most first (equal counts by their value), as its twin <V>N (%print orders the rows by it); default: the order of the values
orderofThe variable of VAR whose rows are ordered (default: the last of VAR; the term above a graded variable of %worst)
orderbyThe group of BY whose subjects order the rows, in quotes (default: all the groups)
statsn="XX" pct="(XX.X%)"The statistics, each with its pattern (default n="XX" pct="(XX.X%)"); also events
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_freq_*)
%freq(
    data  = adsl
  , var   = SEX
  , by    = TRT01A
  , total = yes
  , label = "Sex"
  , out   = sex
)

Output: OUT: one row per group, category and statistic: BY and VAR (and KEEP), GROUPS (their names), BLOCK, STAT, VALUE, TEXT. Every group of DENOM has every category: n = 0 when no subject has it.

%import_xpt#

Read SAS transport files (XPORT version 5) into SAS data sets.

Procedure: LIBNAME XPORT, PROC COPY
Proven: the ADaM and SDTM transport files (sapling sas run, sapling sas adam run) of the dummy study SYNHTN01 (sapling sas run: SAS RUN PASS; sapling sas check: SAS CHECK PASS, 570 of 570 numbers of R the same in SAS); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/import_xpt.sas (12NOV2026)

ParameterDefaultIt is
dirrequiredThe folder of the transport files (<member>.xpt)
membersrequiredThe names of the data sets, separated by blanks
outrequiredThe folder the data sets are written to
%import_xpt(
    dir     = /data/htn3/xpt
  , members = adsl adae advs
  , out     = /data/htn3/adam
)

Output: FILE: the data sets <out>/<member>.sas7bdat. The macro stands alone: a program includes it without the library (00_import.sas), so it calls no other macro of the library and writes no begin and end lines.

%interaction_genmod#

The test of the interaction of the treatment and a factor in a count model with its exposure, one record per subject (PROC GENMOD, the logarithm of the exposure the offset): Poisson or negative binomial (the dispersion k estimated in each model), the model of the groups, the covariates, the class factors and the factor, against the same with the treatment-by-factor terms. The likelihood-ratio test (pval): twice the difference of the full log likelihoods of the two fits, with the difference of the numbers of their estimated coefficients.

Procedure: PROC GENMOD
Proven: the known answer of the methods interaction-poisson, interaction-negbin (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/interaction_genmod.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe count
byrequiredThe groups of the model: a character variable
factorrequiredThe factor of the subgroups: a variable whose values are its levels
exposurerequiredThe exposure of each subject: a variable, or an expression of it. Above 0.
covarsThe covariates
classThe class factors
distpoissonpoisson (default) or negbin
statspval="X.XXX"The statistic and its pattern (default pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_interaction_genmod_*)
%interaction_genmod(
    data     = adsl
  , var      = EXACNT
  , by       = TRT01P
  , factor   = SEX
  , exposure = TRTDUR / 365.25
  , dist     = negbin
  , label    = "Sex"
  , out      = sex_int
)

Output: OUT: one row, of all groups and levels together (GROUPS is blank).

%interaction_glm#

The test of the interaction of the treatment and a factor in a linear model, one record per subject (PROC GLM): the variable on the groups, the covariates, the class factors, the factor and the treatment-by-factor terms; the Type III F test of the treatment-by-factor terms (pval). For the term of the highest order this is the F test of the model with the factor against the same model with the terms.

Procedure: PROC GLM
Proven: the known answer of the method interaction-lm (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/interaction_glm.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe variable
byrequiredThe groups of the model: a character variable
factorrequiredThe factor of the subgroups: a variable whose values are its levels
covarsThe covariates
classThe class factors
statspval="X.XXX"The statistic and its pattern (default pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_interaction_glm_*)
%interaction_glm(
    data   = adqsadas
  , where  = (PARAMCD = 'ACTOT' and AVISIT = 'Week 24')
  , var    = CHG
  , by     = TRTP
  , factor = SEX
  , covars = BASE
  , class  = SITEGR1
  , label  = "Sex"
  , out    = sex_int
)

Output: OUT: one row, of all groups and levels together (GROUPS is blank).

%interaction_logistic#

The test of the interaction of the treatment and a factor in a logistic regression, one record per subject (PROC LOGISTIC, the event EVENT=): the model of the groups, the covariates, the class factors and the factor, against the same with the treatment-by-factor terms. The likelihood-ratio test (pval): the difference of the likelihood-ratio chi-squares of the two fits, with the difference of their degrees of freedom.

Procedure: PROC LOGISTIC
Proven: the known answer of the method interaction-logistic (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/interaction_logistic.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe response
event"Y"The value of the response that is the event, in quotes (default "Y")
byrequiredThe groups of the model: a character variable
factorrequiredThe factor of the subgroups: a variable whose values are its levels
covarsThe covariates
classThe class factors
statspval="X.XXX"The statistic and its pattern (default pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_interaction_logistic_*)
%interaction_logistic(
    data   = adsl
  , var    = RESPFL
  , event  = "Y"
  , by     = TRT01A
  , factor = SEX
  , covars = AGE
  , label  = "Sex"
  , out    = sex_int
)

Output: OUT: one row, of all groups and levels together (GROUPS is blank).

%interaction_phreg#

The test of the interaction of the treatment and a factor in a Cox model, one record per subject (PROC PHREG): the model of the groups, the covariates, the class factors and the factor, stratified by the strata, against the same with the treatment-by-factor terms. The likelihood-ratio test (pval): the difference of the likelihood-ratio chi-squares of the two fits (twice the difference of the log partial likelihoods), with the difference of their degrees of freedom.

Procedure: PROC PHREG
Proven: the known answer of the method interaction-cox (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/interaction_phreg.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe time
byrequiredThe groups of the model: a character variable
factorrequiredThe factor of the subgroups: a variable whose values are its levels
cnsrCNSRThe censoring variable: 1 = censored (default CNSR)
covarsThe covariates
classThe class factors
strataThe strata variables, separated by blanks
tiesefronefron (default), breslow, or exact (TIES=DISCRETE)
statspval="X.XXX"The statistic and its pattern (default pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_interaction_phreg_*)
%interaction_phreg(
    data   = adtte
  , where  = (PARAMCD = 'PFS')
  , var    = AVAL
  , by     = TRT01P
  , factor = SEX
  , strata = STRAT1
  , label  = "Sex"
  , out    = sex_int
)

Output: OUT: one row, of all groups and levels together (GROUPS is blank).

%km#

The Kaplan-Meier summary of a time in each group (PROC LIFETEST): the subjects (n), the events (events), the median time (median) and its confidence limits (lcl, ucl; the limits of Brookmeyer and Crowley). A median the curve does not reach is missing. With time= the survival at that time in percent (surv; TIMELIST=, OUTSURV=) and its limits (lcl, ucl: then of the survival, not of the median).

Procedure: PROC LIFETEST
Proven: the known answer of the methods km, kmrate (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/km.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe time
byrequiredThe groups: one variable
cnsrCNSRThe censoring variable: 1 = censored (default CNSR)
timeA time: the survival at it (surv) and its limits
conftypelinearThe interval of the survival function: CONFTYPE= of PROC LIFETEST, linear (default: Sapling's stated default plain; PROC LIFETEST alone uses loglog), log, loglog
level0.95The confidence level (default 0.95)
statsn="XX" events="XX" median="XX.X" lcl="XX.X" ucl="XX.X"The statistics, each with its pattern (default n="XX" events="XX" median="XX.X" lcl="XX.X" ucl="XX.X")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_km_*)
%km(
    data  = adtte
  , where = (PARAMCD = 'TTDE')
  , var   = AVAL
  , by    = TRT01A
  , out   = ttde
)

Output: OUT: one row per group and statistic.

%kmplot#

The Kaplan-Meier curve of a time in each group: the survival in percent as steps, a mark at each censored time, the number at risk under the axis at its ticks (the subjects whose time is that time or later), in the order of the groups; with median=yes the median time of each group and its interval.

Procedure: PROC LIFETEST, PROC SUMMARY, PROC SGPANEL
Proven: its known answer in tools/figures_sas.ts (survival and number at risk worked out by hand); not yet run on a SAS server
File: sas/macros/kmplot.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records: one per subject
plotDraw again: an out= of %kmplot (not with data=)
parThe drawing parameters: an outpar= of %kmplot
columnsThe columns of %columns: the order of the groups and their colours (default: the order of the data)
whereThe records to use, a condition in parentheses
timerequiredThe time
cnsrCNSRThe censoring variable: 1 = censored
byrequiredThe groups: one curve each
panelOne panel per value of this variable
atriskyesyes: the number at risk under the axis
timesThe times of the ticks and of the number at risk, separated by blanks (default: from 0 by a step of 1, 2, 2.5 or 5 times a power of 10, to the first tick at or after the last time)
mediannoyes: the median of each group and its interval as text in the plot
conftypeloglogThe interval of the median: CONFTYPE= of PROC LIFETEST (linear, log, loglog, asinsqrt, logit)
level0.95The confidence level
xlabelThe label of the x axis (default: the label of time=)
ylabel"Survival (%)"The label of the y axis
refReference lines on the y axis: values
filerequiredThe output without its extension: <file>.png (300 dpi) and <file>.rtf
width6.5inThe width of the figure
height4.5inThe height of the figure
outThe numbers drawn
outparThe drawing parameters: one row each
debugnoyes: keep the work data sets _kmplot_*
%kmplot(
    data    = adtte
  , columns = t14_1_cols
  , where   = (PARAMCD = 'TTDE')
  , time    = AVAL
  , cnsr    = CNSR
  , by      = TRTA
  , median  = yes
  , xlabel  = "Days to the first dermatologic event"
  , file    = &spl_out./figures/f14_1
  , out     = f14_1
)

Output: OUT: one row per number drawn: PANEL and BY, KIND, X, Y, LOWER, UPPER, N, CENSORED. KIND curve: X a time (0 and each time with an event or a censoring), Y the survival in percent after it, CENSORED 1 when a subject is censored at it. KIND atrisk: X a tick, N the number at risk. KIND median: X the median, LOWER and UPPER its limits (missing: not reached). OUTPAR: one row per drawing parameter in the order of the table, PARAMETER ($32) its name and VALUE ($500) its value as drawn (a label in quotes). FILE: <file>.png and <file>.rtf.

%layout#

The display data set of a table: one row per printed line, as the shells have it. The label of a block, the levels of its groups indented by two blanks each (a term under its class, a visit under its parameter, vs B under A), the statistics of a line put in the template of the line; one column per group with its header and N from %columns, and the column All groups for a p-value of all groups; the p-value of each category (a test of each term, a block of the same label) stands in the row of its category. A category that format= names shows its label. It computes nothing: every number is the TEXT of a result. The line table (Step 3), line: label = template: 0 a category of a frequency block = {n} {pct} {events}; 1 n = {n}; 2 Events = {events}; 2.5 Patient-years = {exposure}; 3 Mean (SD) = {mean} ({sd}) [{se}] (se only: Mean (SE); sd and se: Mean (SD) [SE]); 4 Median = {median}; 5 Q1, Q3 = {q1}, {q3}; 6 Min, Max = {min}, {max}; 7 LS mean (SE) = {lsmean} ({lsmean_se}); 8 Difference (SE) = {diff} ({diff_se}); 9 the estimate by its STAT (hr Hazard ratio, or Odds ratio, irr Rate ratio, rd Risk difference, rr Relative risk, rate Rate per 100 patient-years, else Estimate) = {estimate}; 10 Confidence interval = ({lcl}, {ucl}); 11 p-value = {pval}; 11.5 Adjusted p-value = {padj}; 12 any other STAT, labelled by it = {value}. A part that is missing leaves out its piece of the template with its brackets or its comma (a confidence interval without its upper limit prints the lower limit alone); a TEXT that has the brackets of its piece already is not bracketed twice; the counts of a frequency block are right aligned to the widest count. With setup= it also keeps the %print call of OUT in a global macro variable of that name: %print(&name.) prints the display data set again after a change. The setup holds data=, by=, var=, together= and label= (row=" " and the labels of label=), and rows=, file= and nodata= when they are given here. A parameter that is not in the setup is added after it: %print(&name., file=...); one that is in it cannot be given twice (SAS stops a call with a keyword given twice): give it to %layout again, or write the %print call.

Procedure: DATA steps, PROC FORMAT, PROC SORT, PROC TRANSPOSE
Proven: the tables of the dummy study SYNHTN01 (sapling sas run: SAS RUN PASS, flat programs 12 of 12) and the cases of setup= in node tools/print_sas.ts; ran on SAS 9.4 M9, 2026-11-12; the 12 tables of the CDISC pilot (the p value of each term in T14_5_2), 2026-11-13
File: sas/macros/layout.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe results (%stack)
columnsrequiredThe columns (%columns)
varrequiredThe column variable of the results (as var= of %columns)
formatThe labels of categories: variable and format, as in a FORMAT statement (sex $sexf. race $racef.); a level of that variable shows its formatted value
labelColumn labels for %print: name="text" pairs, kept in the setup after row=" "
rowsThe rows of a page for %print, kept in the setup
fileThe output for %print without its extension, kept in the setup
nodataThe text %print prints when there is no row, kept in the setup
setupThe name of a global macro variable that keeps the %print call of OUT (blank: none is made)
outrequiredThe display data set
debugnoyes: keep the work data sets _layout_*
%layout(
    data    = t14_2_1
  , columns = t14_2_1_cols
  , var     = trt01a
  , format  = sex $sexf.
  , file    = &spl_out./tlf/t14_2_1
  , setup   = t14_2_1_print
  , out     = t14_2_1_display
)

Output: OUT: one row per printed line: GRP (the rows that stay on one page: a block, or a level of its first group), ROW (the label, indented, format $CHARw.), C1 ... Cn (the cells, labelled with the header of their column) and C99 (All groups: only when a result has a p-value of all groups). SETUP: with setup=, the global macro variable of that name: the parameters of %print for OUT, such as data=t14_2_1_display, by=row, var=c:, together=grp, label=row=" " (a NOTE in the log shows it); print with %print(&name.).

%lineplot#

Lines through values given (the LS means of %lsmeans or %mmrm by visit, a rate by month): one line per group, with the interval of lower= and upper= as error bars (the groups a little apart on the x axis) or as a band. Nothing is computed: the figure draws the values of data=. One panel per value of panel=.

Procedure: PROC SGPANEL
Proven: its known answer in tools/figures_sas.ts (the values given); not yet run on a SAS server
File: sas/macros/lineplot.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe values: one row per group and x
plotDraw again: an out= of %lineplot (not with data=)
parThe drawing parameters: an outpar= of %lineplot
columnsThe columns of %columns: the order of the groups and their colours (default: the order of the data)
whereThe rows to use, a condition in parentheses
xrequiredThe x: a numeric variable (AVISITN)
yrequiredThe value
byrequiredThe groups: one line each
panelOne panel per value of this variable, one under the other, each with its own y axis
lowerThe lower end of the interval: a variable
upperThe upper end of the interval: a variable
bandnoyes: the interval as a band; no: as error bars
xformatThe format of the ticks of x (default: the format of x)
xlabelThe label of the x axis (default: the label of x)
ylabelThe label of the y axis (default: the label of y)
refReference lines on the y axis: values
filerequiredThe output without its extension: <file>.png (300 dpi) and <file>.rtf
width6.5inThe width of the figure
height4.5inThe height of the figure
outThe numbers drawn
outparThe drawing parameters: one row each
debugnoyes: keep the work data sets _lineplot_*
%lineplot(
    data    = lsm_chg
  , columns = t14_3_1_cols
  , x       = AVISITN
  , y       = ESTIMATE
  , by      = TRTP
  , lower   = LCL
  , upper   = UCL
  , xformat = VISIT.
  , ylabel  = "LS mean change from baseline (95% CI)"
  , ref     = 0
  , file    = &spl_out./figures/f14_3_2
  , out     = f14_3_2
)

Output: OUT: one row per panel, group and x: PANEL and BY, KIND line, X, Y, LOWER, UPPER. OUTPAR: one row per drawing parameter in the order of the table: PARAMETER ($32) its name, VALUE ($500) its value as drawn (a label in quotes). FILE: <file>.png and <file>.rtf.

%locf#

Last observation carried forward into the analysis windows, as new records (ADaM: DTYPE = 'LOCF'). For each window after the first one after baseline in which a group has no analysed record: a copy of the last analysed record before it, moved to that window. Baseline is not carried.

Procedure: PROC SORT, a DATA step (SET POINT=)
Proven: the fixtures of the step kind impute (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/locf.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe working data set (changed in place; the new records are added)
windowsrequiredThe windows, as windows= of %windows (baseline and one or more after it)
whereThe records LOCF is for, a condition in parentheses
byUSUBJID PARAMCDThe groups (default USUBJID PARAMCD)
flagANL01FLThe flag of the analysed records (default ANL01FL = 'Y')
dayADYThe study day (default ADY)
length$8The length of DTYPE (default $8)
debugnoyes: keep the work data sets _locf_*
%locf(
    data    = adqs
  , windows = &advs_windows.
  , where   = (PARAMCD = 'ACTOT')
)

Output: DATA: sorted by the groups, AVISITN and DTYPE; _ROW is the new order.

%logistic#

A logistic regression of a response with two values, one record per subject (PROC LOGISTIC): the probability of the event (event=) on the groups, the covariates and the class factors. The odds ratio of pairs of groups from one fit (or), its Wald confidence limits (lcl, ucl) and the Wald p-value of the pair (pval).

Procedure: PROC LOGISTIC (LSMEANS)
Proven: the known answer of the method logistic (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/logistic.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe response: two values
event"Y"The value that is the event, in quotes (default "Y"; a number: "1"). PROC LOGISTIC alone models the first value in sort order.
byrequiredThe groups of the model: a character variable
withinOne model in each group of these variables (the levels of a subgroup)
covarsThe covariates
classThe class factors
pairsallall: each later group against each earlier one, in the order of ORDER; or the pairs separated by #, one pair per line, the group first, then the group it is compared with (a value with a blank or a hyphen in quotes): "Drug" "Placebo" # "High" "Low". The form "A"-"B" of version 1.0 is still read.
orderA numeric variable with one value per group: the order of the groups
level0.95The confidence level (default 0.95)
statsor="XX.XX" lcl="XX.XX" ucl="XX.XX" pval="X.XXX"The statistics, each with its pattern (default or="XX.XX" lcl="XX.XX" ucl="XX.XX" pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_logistic_*)
%logistic(
    data   = adrs
  , where  = (PARAMCD = 'BOR')
  , var    = CRIT1FL
  , event  = "Y"
  , by     = TRT01P
  , covars = AGE
  , class  = SEX
  , order  = TRT01PN
  , out    = resp_or
)

Output: OUT: one row per pair and statistic (BY: the group, VERSUS: the group it is compared with); GROUPS is BY VERSUS (and WITHIN).

%logrank#

The log-rank test of a time between groups (PROC LIFETEST, STRATA), one record per subject: pval. With strata= the stratified log-rank test (STRATA strata / GROUP=by): each combination of the values of the strata variables one stratum. With one group there is no test: the value is missing.

Procedure: PROC LIFETEST (STRATA)
Proven: the known answer of the method logrank (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/logrank.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe time
byrequiredThe groups to compare: one variable
cnsrCNSRThe censoring variable: 1 = censored (default CNSR)
strataThe strata variables of a stratified test, separated by blanks
statspval="X.XXX"The statistic and its pattern (default pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_logrank_*)
%logrank(
    data   = adtte
  , where  = (PARAMCD = 'TTDE')
  , var    = AVAL
  , by     = TRT01A
  , strata = STRAT1 STRAT2
  , out    = ttde_test
)

Output: OUT: one row, of all groups together (GROUPS is blank).

%lsmeans#

A linear model of a variable, one record per subject (PROC MIXED without a random effect: least squares): covariates, class factors and the groups. The differences of the LS means of pairs of groups (diff, diff_se, lcl, ucl, pval) and, when stats= asks for them, the LS mean of each group (lsmean, lsmean_se).

Procedure: PROC MIXED (LSMEANS)
Proven: the known answer of the method lsmeans (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/lsmeans.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe response
byrequiredThe groups of the model: a character variable
withinOne model in each group of these variables
covarsThe covariates
classThe class factors
pairsallall: each later group against each earlier one, in the order of ORDER; or the pairs separated by #, one pair per line, the group first, then the group it is compared with (a value with a blank or a hyphen in quotes): "Drug" "Placebo" # "High" "Low". The form "A"-"B" of version 1.0 is still read.
orderA numeric variable with one value per group: the order of the groups
level0.95The confidence level (default 0.95)
statsdiff="XX.X" diff_se="XX.XX" lcl="XX.X" ucl="XX.X" pval="X.XXX"The statistics, each with its pattern (default diff="XX.X" diff_se="XX.XX" lcl="XX.X" ucl="XX.X" pval="X.XXX"); also lsmean and lsmean_se
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_lsmeans_*)
%lsmeans(
    data   = adqsadas
  , where  = (PARAMCD = 'ACTOT' and AVISIT = 'Week 24')
  , var    = CHG
  , by     = TRT01P
  , covars = BASE
  , class  = SITEGR1
  , pairs  = all
  , order  = TRT01PN
  , out    = pairs
)

Output: OUT: the rows of the differences (BY: the group, VERSUS: the group it is compared with) and of the LS means (VERSUS blank); GROUPS is BY VERSUS WITHIN.

%meanplot#

The mean (or the median) of a variable at each visit in each group, with its error bars (SE, SD, the confidence interval of the mean, or Q1 and Q3 of the median), joined by lines; the groups a little apart on the x axis so that their bars do not overlap; with ntable=yes the n of each visit and group under the axis. One panel per value of panel=.

Procedure: PROC MEANS, PROC SGPANEL
Proven: its known answer in tools/figures_sas.ts (means, SE, SD, limits worked out by hand); not yet run on a SAS server
File: sas/macros/meanplot.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
plotDraw again: an out= of %meanplot (not with data=)
parThe drawing parameters: an outpar= of %meanplot
columnsThe columns of %columns: the order of the groups and their colours (default: the order of the data)
whereThe records to use, a condition in parentheses
yrequiredThe variable
xrequiredThe visit: a numeric variable (AVISITN); its ticks are its values, printed with xformat=
byrequiredThe groups: one line each
panelOne panel per value of this variable (PARAMCD), one under the other, each with its own y axis
statmeanmean or median
errorseThe error bars: se, sd, ci (of the mean), iqr (Q1 and Q3, of the median) or none
level0.95The confidence level of error=ci
connectyesyes: the points of a group joined by a line
ntablenoyes: the n of each visit and group under the axis
xformatThe format of the ticks of x (default: the format of x)
xlabelThe label of the x axis (default: the label of x)
ylabelThe label of the y axis (default: the statistic, the error bars and the label of y)
refReference lines on the y axis: values
filerequiredThe output without its extension: <file>.png (300 dpi) and <file>.rtf
width6.5inThe width of the figure
height4.5inThe height of the figure
outThe numbers drawn
outparThe drawing parameters: one row each
debugnoyes: keep the work data sets _meanplot_*
%meanplot(
    data    = advs
  , columns = t14_7_1_cols
  , where   = (ANL01FL = 'Y' and AVISITN > 0)
  , y       = CHG
  , x       = AVISITN
  , by      = TRTA
  , panel   = PARAM
  , ntable  = yes
  , xformat = VISIT.
  , ref     = 0
  , file    = &spl_out./figures/f14_7_1
  , out     = f14_7_1
)

Output: OUT: one row per panel, group and visit: PANEL and BY, KIND (mean or median), X (the visit), Y (the statistic), LOWER and UPPER (the ends of the error bar; missing with one value for se, sd, ci), N. OUTPAR: one row per drawing parameter in the order of the table: PARAMETER ($32) its name, VALUE ($500) its value as drawn (a label in quotes). FILE: <file>.png and <file>.rtf. The quantiles are those of PROC MEANS QNTLDEF=5 (R type 2). Draw again: plot= (an out= of %meanplot, changed or not) is drawn as it is, without a statistic (error bars where LOWER has a value), and par= (an outpar= of %meanplot) gives the drawing parameters: columns, by, panel, connect, ntable, xformat, xlabel, ylabel, ref, file, width, height. They change the picture, not the numbers; the others compute, and with plot= a NOTE says each one given is not used. A drawing parameter given in the call wins over its row of par=, the row over the default of the table.

%merge_subject#

Copy variables from another data set by subject (USUBJID). The other data set has one row per subject; the rows of DATA stay as they are, in their order.

Procedure: PROC SORT, MERGE
Proven: the fixtures of the step kinds copy, period (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/merge_subject.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe working data set (changed in place)
fromrequiredThe data set to copy from
varsrequiredThe variables, separated by blanks; NEW=OLD copies under another name
debugnoyes: keep the work data sets _merge_subject_*
%merge_subject(
    data = advs
  , from = adam.adsl
  , vars = AGE SEX TRTP=TRT01P TRTPN=TRT01PN
)

Output: DATA: the variables of FROM of its subject on every row, in the order of its rows; a subject not in FROM gets missing values.

%mmrm#

The mixed model for repeated measures, one record per subject and visit (PROC MIXED, REPEATED): at one visit the differences of the LS means of pairs of groups (diff, diff_se, lcl, ucl, pval) and, when stats= asks for them, the LS mean of each group (lsmean, lsmean_se). When the covariance structure does not converge the next one of fallback= is tried.

Procedure: PROC MIXED (REPEATED, LSMEANS)
Proven: the known answer of the method mmrm (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/mmrm.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe response
byrequiredThe groups: a character variable
visitrequiredThe repeated variable (REPEATED), a class factor
atrequiredThe visit of the LS means, in quotes
classThe class factors besides the groups and the subject (the visit is one)
subjectUSUBJIDThe subject (default USUBJID)
modelrequiredThe fixed effects: the MODEL statement after the equal sign
typeunThe covariance structure: TYPE= of REPEATED (default un)
fallbackOther structures to try in their order, when one does not converge
ddfmbwThe degrees of freedom: DDFM= of MODEL (default bw: between-within)
methodremlreml (default) or ml
pairsallall: each later group against each earlier one, in the order of ORDER; or the pairs separated by #, one pair per line, the group first, then the group it is compared with (a value with a blank or a hyphen in quotes): "Drug" "Placebo" # "High" "Low". The form "A"-"B" of version 1.0 is still read.
orderA numeric variable with one value per group: the order of the groups
level0.95The confidence level (default 0.95)
statsdiff="XX.X" diff_se="XX.XX" lcl="XX.X" ucl="XX.X" pval="X.XXX"The statistics, as in %lsmeans
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_mmrm_*)
%mmrm(
    data     = adqsadas
  , where    = (PARAMCD = 'ACTOT' and ANL01FL = 'Y')
  , var      = CHG
  , by       = TRT01P
  , visit    = AVISIT
  , at       = "Week 24"
  , class    = SITEGR1 AVISIT
  , model    = BASE SITEGR1 TRT01P AVISIT TRT01P*AVISIT BASE*AVISIT
  , type     = un
  , fallback = toep
  , ddfm     = bw
  , pairs    = all
  , order    = TRT01PN
  , out      = mmrm
)

Output: OUT: as %lsmeans: the rows of the differences (BY, VERSUS) and of the LS means.

%param_wide#

The values of several parameters side by side: one record per group of BY that has a record of every parameter of PARAMS, with the variables VARS of each parameter as <PARAMCD>_<VAR> (WEIGHT_AVAL). Two records of one parameter in a group are an ERROR.

Procedure: PROC SORT, MERGE
Proven: the fixtures of the step kind computed (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/param_wide.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records (with PARAMCD and the BY variables)
whereThe records to read, a condition in parentheses
paramsrequiredThe parameter codes
varsrequiredThe variables of each parameter to keep (AVAL)
byrequiredThe groups (the subject and the visit)
outrequiredOne record per group: BY, then <PARAMCD>_<VAR> for each parameter and variable
debugnoyes: keep the work data sets _param_wide_*
%param_wide(
    data   = work.advs
  , params = WEIGHT HEIGHT
  , vars   = AVAL
  , by     = USUBJID AVISITN
  , out    = work._bmi
)

Output: OUT: one record per group of BY that has a record of every parameter: BY, then <PARAMCD>_<VAR> for each parameter and variable. Two records of one parameter in a group are an ERROR that names the group.

%pick#

Give each subject the value of its first (or last) record in another data set.

Procedure: PROC SORT, MERGE
Proven: the fixtures of the step kinds derive.from.one, derive.from.extreme, ice (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/pick.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe working data set (changed in place)
fromrequiredThe data set of the records. Prepare it with a DATA step when the value or a sort key is not a variable yet (a date of a text, a difference).
valuerequiredThe variable of FROM that gives the value
orderThe sort keys within a subject; "descending" before a key as in a BY statement. A missing key sorts last. Without order: the first record as the data are.
whichfirstfirst (default) or last
lengthThe length of a text variable, e.g. $40
formatThe format, e.g. date9
uniquenoyes: each subject has at most one record in FROM (the plan names one record and no order); two records are an ERROR. Default no.
newrequiredThe variable to make
debugnoyes: keep the work data sets _pick_*
%pick(
    data   = adsl
  , from   = ex
  , value  = EXSTDT
  , order  = EXSTDT EXSEQ
  , which  = first
  , format = date9.
  , new    = TRTSDT
)

Output: DATA: NEW on every row of a subject: the value of its first (or last) record in FROM; missing for a subject without a record. With unique=yes two records of a subject are an ERROR that names the subject.

%pool#

Pool small groups: a group (e.g. a site) that has fewer than MIN subjects in one of the groups of BY (e.g. a treatment) goes into one pooled group.

Procedure: PROC FREQ, PROC SUMMARY, MERGE
Proven: the fixtures of the step kind pool (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/pool.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe working data set (changed in place)
varrequiredThe groups to pool (SITEID)
byrequiredThe variable whose every group must have MIN subjects (the treatment)
minrequiredThe smallest number of subjects
lengthThe length of NEW, e.g. $3
newrequiredThe variable to make: VAR, or INTO for a small group
intorequiredThe value of the pooled group
debugnoyes: keep the work data sets _pool_*
%pool(
    data   = adsl
  , var    = SITEID
  , by     = TRT01P
  , min    = 3
  , length = $3
  , new    = SITEGR1
  , into   = 900
)

Output: DATA: NEW: the value of VAR, or INTO for a group of VAR with fewer than MIN subjects in one of the groups of BY. The rows keep their order.

%print#

Print a table or a listing: one PROC REPORT into an RTF file and a monospace listing beside it. A table is printed from its display data set (%layout), a listing from its records. The BY columns come first, a repeated value shown once; the VAR columns follow, numbers right, text left as written. ODS wraps and pages the RTF; the rows of one TOGETHER value stay on one page of the listing when they fit in ROWS. The parameters of a table can come from the setup of %layout (setup=): %print(&name.) prints its display data set again after a change; a parameter that is not in the setup is added after it: %print(&name., file=...). A keyword given twice stops the call (SAS): change it in %layout.

Procedure: PROC REPORT
Proven: its cases of node tools/print_sas.ts (PRINT SAS PASS: 18 cases) and the tables and listings of the dummy study SYNHTN01 (flat programs 12 of 12); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/print.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe rows: the display data set of %layout, or the records of a listing
byThe left columns, printed first: a repeated value shown once
varrequiredThe other columns, in print order; a list such as c: or c1-c5 is allowed
togetherA variable: the rows of one value stay on one page
labelColumn labels: name="text" pairs; | splits a header (default: the labels of the variables)
rows40The rows of a page, for together=
filerequiredThe output without its extension: <file>.rtf and <file>.lst
nodataNo data qualifiedThe text printed when there is no row
debugnoyes: keep the work data sets _print_*
%print(
    data     = t14_2_1_display
  , by       = row
  , var      = c:
  , together = grp
  , label    = %STR(row=" ")
  , file     = &spl_out./tlf/t14_2_1
)

Output: FILE: <file>.rtf (style journal, landscape) and <file>.lst (the LISTING destination), under the titles and footnotes of the program; no row: the text of nodata=.

%profileplot#

The profile of each subject: one thin line per subject through its values by visit, in the colour and the line pattern of its group; with mean=yes the mean of each group at each visit as a thick line with markers. One panel per value of panel=.

Procedure: PROC MEANS, PROC SGPANEL
Proven: its known answer in tools/figures_sas.ts (the values and the means worked out by hand); not yet run on a SAS server
File: sas/macros/profileplot.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
plotDraw again: an out= of %profileplot, not with data=
parThe drawing parameters: an outpar= of %profileplot
columnsThe columns of %columns: the order of the groups and their colours (default: the order of the data)
whereThe records to use, a condition in parentheses
xrequiredThe visit: a numeric variable (AVISITN, ADY)
yrequiredThe variable
subjectUSUBJIDThe subject: one line each
byrequiredThe groups: one colour each
panelOne panel per value of this variable, one under the other, each with its own y axis
meannoyes: the mean of each group as a thick line
xformatThe format of the ticks of x (default: the format of x)
xlabelThe label of the x axis (default: the label of x)
ylabelThe label of the y axis (default: the label of y)
refReference lines on the y axis: values
filerequiredThe output without its extension: <file>.png (300 dpi) and <file>.rtf
width6.5inThe width of the figure
height4.5inThe height of the figure
outThe numbers drawn
outparThe drawing parameters: one row each
debugnoyes: keep the work data sets _profileplot_*
%profileplot(
    data    = adlbc
  , columns = t14_6_cols
  , where   = (PARAMCD = 'ALT' and ANL01FL = 'Y')
  , x       = ADY
  , y       = AVAL
  , by      = TRTA
  , mean    = no
  , ref     = 40
  , file    = &spl_out./figures/f14_6_1
  , out     = f14_6_1
)

Output: OUT: one row per point: PANEL, BY and SUBJECT, KIND (subject: a value of a subject; mean: the mean of a group, SUBJECT blank), X, Y, N (the values of a mean). OUTPAR: one row per drawing parameter in the order of the table: PARAMETER ($32) its name, VALUE ($500) its value as drawn (a label in quotes). FILE: <file>.png and <file>.rtf.

%proportion#

The subjects in each category of a variable in each group, their percent of the subjects of the group, and the confidence interval of that percent by a named method (PROC FREQ, BINOMIAL): n, pct, lcl, ucl (the limits in percent, as pct).

Procedure: PROC FREQ (BINOMIAL)
Proven: the known answer of the method propci (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/proportion.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
denomThe subjects of the percent: a data set with one row per subject and BY (default: the subjects of data=)
whereThe records to use, a condition in parentheses
varrequiredThe category
byrequiredThe groups: one variable
levelsThe categories of the plan: the variable, then its values in quotes. A category without a subject shows with n = 0.
ciclopper-pearsonThe interval: clopper-pearson (default; the exact interval, CL=EXACT), wilson (the score interval, CL=WILSON), wald (CL=WALD), agresti-coull (CL=AGRESTICOULL)
level0.95The confidence level (default 0.95)
statsn="XX" pct="(XX.X%)" lcl="XX.X" ucl="XX.X"The statistics, each with its pattern (default n="XX" pct="(XX.X%)" lcl="XX.X" ucl="XX.X")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_proportion_*)
%proportion(
    data   = adrs
  , denom  = adsl
  , where  = (PARAMCD = 'BOR')
  , var    = AVALC
  , by     = TRT01A
  , levels = AVALC 'CR' 'PR'
  , ci     = clopper-pearson
  , out    = bor_ci
)

Output: OUT: one row per group, category and statistic; GROUPS is BY VAR.

%rate#

The exposure-adjusted incidence rate of each group: the subjects with a record (n; PROC FREQ), the sum of the exposure of the subjects of the group (exposure; PROC MEANS), the rate n / exposure times per= (rate) and its exact Poisson limits (lcl, ucl: the chi-square quantiles of 2n and 2(n + 1) degrees of freedom, halved, divided by the exposure; CINV).

Procedure: PROC FREQ, PROC MEANS, CINV
Proven: the known answer of the methods eair, eventrate (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/rate.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records (the events)
denomrequiredThe subjects of the groups with their exposure: one row per subject
whereThe records to use, a condition in parentheses
byrequiredThe groups: one variable, in data= and in denom=
exposurerequiredThe exposure of a subject: a variable of denom=, or an expression of it (TRTDUR / 365.25: in years)
countsubjectssubjects (default): the subjects with a record (n); events: every record (events), the rate and its limits of the number of events
per100The rate per this many units of exposure (default 100: per 100 patient-years) (the line of %layout reads "Rate per 100 patient-years": with another per=, change LABEL in the display data set)
level0.95The confidence level (default 0.95)
statsThe statistics, each with its pattern (default n="XX" exposure="XX.X" rate="XX.XX" lcl="XX.XX" ucl="XX.XX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_rate_*)
%rate(
    data     = adae
  , denom    = adsl
  , where    = (TRTEMFL = 'Y')
  , by       = TRT01A
  , exposure = TRTDUR / 365.25
  , out      = ae_rate
)

Output: OUT: one row per group and statistic; GROUPS is BY.

%rateratio#

A count model of a count with its exposure, one record per subject (PROC GENMOD, the logarithm of the exposure the offset): Poisson or negative binomial (the dispersion k by maximum likelihood), on the groups, the covariates and the class factors. The rate ratio of pairs of groups from one fit (irr), its Wald confidence limits (lcl, ucl) and the Wald p-value of the pair (pval).

Procedure: PROC GENMOD (LSMEANS)
Proven: the known answer of the methods poisson, negbin (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/rateratio.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe count
byrequiredThe groups of the model: a character variable
exposurerequiredThe exposure of each subject: a variable, or an expression of it (TRTDUR / 365.25: in years). Above 0.
withinOne model in each group of these variables (the levels of a subgroup)
covarsThe covariates
classThe class factors
distpoissonpoisson (default) or negbin
pairsallall: each later group against each earlier one, in the order of ORDER; or the pairs separated by #, one pair per line, the group first, then the group it is compared with (a value with a blank or a hyphen in quotes): "Drug" "Placebo" # "High" "Low". The form "A"-"B" of version 1.0 is still read.
orderA numeric variable with one value per group: the order of the groups
level0.95The confidence level (default 0.95)
statsirr="XX.XX" lcl="XX.XX" ucl="XX.XX" pval="X.XXX"The statistics, each with its pattern (default irr="XX.XX" lcl="XX.XX" ucl="XX.XX" pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_rateratio_*)
%rateratio(
    data     = adexac
  , var      = AVAL
  , by       = TRT01P
  , exposure = TRTDUR / 365.25
  , covars   = BASE
  , dist     = negbin
  , order    = TRT01PN
  , out      = exac_rr
)

Output: OUT: one row per pair and statistic (BY: the group, VERSUS: the group it is compared with); GROUPS is BY VERSUS (and WITHIN).

%relchange#

New records of the change from the record before, relative to the normal range (the pilot SAP 8.3: a change larger than half the normal range is abnormal). For each record: a record of the parameter PREFIX + PARAMCD whose AVAL is (value - the value of the record before) / ((upper - lower limit) / 2), rounded as ROUND; ANRIND H above +LIMIT, L below -LIMIT, else N.

Procedure: PROC SORT, a DATA step (LAG)
Proven: the fixtures of the step kind relchange (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/relchange.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records (not changed)
whereThe records to read, a condition in parentheses (the record before is one of them)
valuerequiredThe measured value (LBSTRESN)
byUSUBJID PARAMCDThe groups (default USUBJID PARAMCD)
loA1LOThe limits (default A1LO, A1HI)
hiA1HIThe limits (default A1LO, A1HI)
orderVISITNUM ADT LBSEQThe order that gives the record before (default VISITNUM ADT LBSEQ; a missing value sorts first)
prefix_The prefix of the new PARAMCD (default _)
digits1The decimals of AVAL (default 1)
limit1ANRIND is H above +LIMIT and L below -LIMIT (default 1: half the range)
resetBASE CHG BNRIND R2A1LO R2A1HI BR2A1LO BR2A1HIThe variables set missing on the new records, those that DATA has (default BASE CHG BNRIND R2A1LO R2A1HI BR2A1LO BR2A1HI); the limits are set missing too
outrequiredThe new records: one for each record of DATA, sorted by BY and ORDER
debugnoyes: keep the work data sets _relchange_*
%relchange(
    data  = work.adlbc
  , value = LBSTRESN
  , out   = work._relchange
)

Output: OUT: has the variables of DATA. AVAL has a value on a scheduled visit (AVISITN not missing) when the value, the value before and both limits are there and the upper limit is above the lower one; it is missing on the first record of a group. PARAMCD keeps its length in DATA: it must hold the new codes (the generated programs give it the length of the specification, which lists them).

%risk#

The comparison of two proportions of each pair of groups, one record per subject (PROC FREQ): the 2 x 2 table of the two groups (the group first) and the event (event=) against the rest. measure=rd: the risk difference (rd) with the interval of ci=; measure=rr: the relative risk (rr), measure=or: the odds ratio (or), each with the Wald interval of its logarithm; the p-value of Pearson's chi-square test without continuity correction (pval). With strata= the Mantel-Haenszel risk difference (COMMONRISKDIFF, CL=MH) and the CMH test.

Procedure: PROC FREQ (RISKDIFF, RELRISK, CHISQ; COMMONRISKDIFF, CMH)
Proven: the known answer of the methods riskdiff, riskratio, oddsratio, perterm (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/risk.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe response: two values
event"Y"The value that is the event, in quotes (default "Y")
byrequiredThe groups: a character variable
withinOne comparison in each group of these variables (each term of an adverse event table: the flags of %termflags)
strataThe strata variables of the Mantel-Haenszel risk difference
pairsallall: each later group against each earlier one, in the order of ORDER; or the pairs separated by #, one pair per line, the group first, then the group it is compared with (a value with a blank or a hyphen in quotes): "Drug" "Placebo" # "High" "Low". The form "A"-"B" of version 1.0 is still read.
orderA numeric variable with one value per group: the order of the groups
measurerdrd (default), rr or or
ciwaldThe interval of a risk difference: wald (default), newcombe, miettinen-nurminen (CL=MN), agresti-caffo (CL=AC); with strata: mh
level0.95The confidence level (default 0.95)
statsThe statistics, each with its pattern (default rd="X.XXX" lcl="X.XXX" ucl="X.XXX" pval="X.XXX"; with measure=rr or or: rr= or or= "XX.XX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_risk_*)
%risk(
    data  = adrs
  , where = (PARAMCD = 'BOR')
  , var   = CRIT1FL
  , event = "Y"
  , by    = TRT01P
  , order = TRT01PN
  , ci    = newcombe
  , out   = resp_rd
)

Output: OUT: one row per pair and statistic (BY: the group, VERSUS: the group it is compared with, and WITHIN); GROUPS is BY VERSUS WITHIN.

%scatterplot#

The values of two variables against each other (a shift plot of baseline and post baseline values, two measures of a subject), one colour and marker per group; with fit=linear the least squares line of each group (its slope, intercept and R-square in out=), with fit=loess the loess curve of SGPLOT (its smoothing chosen by SGPLOT, not in out=); identity=yes the line y = x. One panel per value of panel=.

Procedure: PROC REG, PROC SGPANEL
Proven: its known answer in tools/figures_sas.ts (the points, and the slope, intercept and R-square worked out by hand); not yet run on a SAS server
File: sas/macros/scatterplot.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
plotDraw again: an out= of %scatterplot, not with data=
parThe drawing parameters: an outpar= of %scatterplot
columnsThe columns of %columns: the order of the groups and their colours (default: the order of the data)
whereThe records to use, a condition in parentheses
xrequiredThe variable of the x axis (BASE)
yrequiredThe variable of the y axis (AVAL)
byrequiredThe groups: one colour and marker each
panelOne panel per value of this variable
fitnonenone, linear (a line per group) or loess
identitynoyes: the line y = x
xlabelThe label of the x axis (default: the label of x)
ylabelThe label of the y axis (default: the label of y)
refReference lines on the y axis: values
filerequiredThe output without its extension: <file>.png (300 dpi) and <file>.rtf
width6.5inThe width of the figure
height4.5inThe height of the figure
outThe numbers drawn
outparThe drawing parameters: one row each
debugnoyes: keep the work data sets _scatterplot_*
%scatterplot(
    data     = adlbc
  , columns  = t14_6_cols
  , where    = (PARAMCD = 'ALT' and AVISITN = 24)
  , x        = BASE
  , y        = AVAL
  , by       = TRTA
  , identity = yes
  , file     = &spl_out./figures/f14_6_2
  , out      = f14_6_2
)

Output: OUT: PANEL and BY, KIND, X, Y; KIND point: one row per value; with fit=linear KIND fit: one row per group with SLOPE, INTERCEPT, RSQUARE and N (X and Y missing). OUTPAR: one row per drawing parameter in the order of the table: PARAMETER ($32) its name, VALUE ($500) its value as drawn (a label in quotes). FILE: <file>.png and <file>.rtf.

%stack#

Put results one under another: the blocks of a table, in the order given.

Procedure: a DATA step (SET INDSNAME=)
Proven: the tables of the dummy study SYNHTN01 (sapling sas run: SAS RUN PASS; sapling sas check: SAS CHECK PASS, 570 of 570 numbers of R the same in SAS); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/stack.sas (12NOV2026)

ParameterDefaultIt is
resultsrequiredThe results (of %describe, %freq, a test or a model, or a DATA step of your own in the same shape), separated by blanks
outrequiredThe table
%stack(
    age sex race
  , out = t14_2_01
)

Output: OUT: all rows, with RESULT (the name of the result each row comes from) and ORD (its place in the list: 1, 2, ...). %layout lays the blocks out in this order. Each variable has the longest length it has in the results: no text is cut.

%study#

Start a program: the ADaM library (read only), the output folder and the name of the program (in the footnote of each output), the options of the outputs.

Procedure: LIBNAME, OPTIONS
Proven: every program of the dummy study SYNHTN01 (sapling sas run: SAS RUN PASS; sapling sas check: SAS CHECK PASS, 570 of 570 numbers of R the same in SAS); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/study.sas (12NOV2026)

ParameterDefaultIt is
idrequiredThe study (first, without a keyword)
programrequiredThe name of this program: its outputs are named after it
adam&spl_root./adamThe folder of the ADaM data sets (default &spl_root./adam)
out&spl_root./outThe output folder, with the folders tlf, listings, figures and ard (default &spl_root./out)
%study(
    CDISCPILOT01
  , program = t14_2_01
)

Output: CODE: the population and the records in DATA steps, the statistics macros (%describe, %freq, %ancova, ...), then %stack and %print (a figure: its figure macro).

%summarize#

Give each subject a summary of its records in another data set: the sum or the mean of a variable. A subject without a record gets a missing value; a sum of only missing values is missing.

Procedure: PROC SUMMARY, MERGE
Proven: the fixtures of the step kind derive.from.summary (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/summarize.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe working data set (changed in place)
fromrequiredThe data set of the records (prepare the value with a DATA step)
valuerequiredThe variable of FROM to summarize. To count records, give a 0/1 variable and stat=sum; for "some record meets it" stat=max, for "every record meets it" stat=min.
statsumsum (default), mean, max or min
newrequiredThe variable to make
debugnoyes: keep the work data sets _summarize_*
%summarize(
    data  = adsl
  , from  = ex
  , value = DOSE_DAYS
  , stat  = sum
  , new   = CUMDOSE
)

Output: DATA: NEW on every row of a subject: the sum, mean, maximum or minimum of VALUE over its records in FROM; missing for a subject without a record.

%summarize_groups#

One record per group of BY with a summary of each variable of VARS over the records of the group: the sum, the mean, the largest or the smallest value (a missing value of a BY variable is a group of its own). A group without records has no record.

Procedure: PROC SUMMARY
Proven: the fixtures of the step kind parameter (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/summarize_groups.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records (prepare the values with a DATA step; a condition as 0 or 1)
byrequiredThe groups
varsrequiredThe variables to summarize
statsrequiredOne statistic per variable, in the same order: sum, mean, max or min. A sum or a mean of only missing values is missing.
outrequiredOne record per group: the BY variables and VARS (their summaries)
%summarize_groups(
    data  = _recs
  , by    = USUBJID AVISIT
  , vars  = _s1 _s2
  , stats = max sum
  , out   = _sums
)

Output: OUT: one record per group of BY with the summary of each variable of VARS (under its own name).

%table#

A table in one call: %columns, %ard (only with file=: the numbers file belongs to an output), %layout and %print, in that order, with the same parameters as the separate calls; it holds no logic of its own. It leaves the columns <data>_cols and the display data set OUT; with setup= also the global macro variable that %print(&name.) prints from, for a second print after a change of OUT. Without file= it prints nothing: change OUT, then %print(&name., file=...).

Procedure: none of its own (those of %columns, %ard, %layout, %print)
Proven: its cases of node tools/print_sas.ts (the same .lst and the same numbers file as the separate calls); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/table.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe results (%stack)
nThe population, one row per subject: the N of each column (n= of %columns)
varrequiredThe column variable of the results
orderIts numeric twin in N= (TRT01AN): the order of the columns
totalnoyes: the results have the group Total (total= of %columns)
formatThe labels of categories: variable and format, as in a FORMAT statement (format= of %layout)
labelColumn labels: name="text" pairs (label= of %print)
rows40The rows of a page, for the rows of one level
fileThe output without its extension: <file>.rtf, <file>.lst and the numbers file <out>/ard/<last part of file>.csv (blank: nothing is printed)
nodataThe text printed when there is no row
setupThe name of a global macro variable that keeps the %print call of OUT (blank: none is left)
outThe display data set (default: <data>_display)
debugnoyes: keep the work data sets of the macros it calls
%table(
    data  = t14_2_1
  , n     = adsl
  , var   = trt01a
  , order = trt01an
  , total = yes
  , file  = &spl_out./tlf/t14_2_1
  , setup = t14_2_1_print
)

Output: OUT: the display data set of %layout; DATA: <data>_cols, the columns of %columns; FILE: <file>.rtf and <file>.lst of %print and the numbers file of %ard (with file=); SETUP: with setup=, the global macro variable of that name with the parameters of %print for OUT.

%table_ae#

A table of adverse events (or other occurrences) in one call: the subjects with a record in each level of var= (system organ class, preferred term), with their percent of the population. The macro writes the long program of the table (the population, the records, one %freq, %stack, %table) and runs it; it computes nothing itself. The result <name>_ae, the table <name>, <name>_cols and <name>_display stay, and the setup <name>_print. show= gives the long program.

Procedure: none of its own (those of %freq, %table)
Proven: node tools/tables_sas.ts (the same display data set as the long program, on the made-up study SYNHTN01); ran on SAS 9.4 M9, 2026-11-13
File: sas/macros/table_ae.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records (a data set with USUBJID)
adslThe subjects, one row per subject: the population and the N of the columns; its variables go to the records (blank: the records have all they need)
popThe population: a condition on adsl= in parentheses
whereThe records to use: a condition in parentheses
varrequiredThe levels of the rows: variables, nested in their order (aebodsys aedecod)
byrequiredThe groups: a character variable, the columns
orderIts numeric twin (TRT01AN): the order of columns
sortfrequency: the rows by their subjects (order= of %freq); blank: by their names
thresholdA percent: only the rows that reach it in a group
totalnoyes: also all groups together, as Total
formatThe labels of categories: variable and format, as in a FORMAT statement (format= of %layout)
nameThe name of the table: the first part of the names of its data sets (default: the last part of file=)
shownoyes: the long program in the log; a file: the long program is written there (and run)
labelThe label of the block, in quotes
statsThe statistics, each with its pattern (stats= of %freq)
fileThe output without its extension: <file>.rtf, <file>.lst and the numbers file (blank: no print)
debugnoyes: keep the work data sets of the macros called
%table_ae(
    data  = adam.adae
  , adsl  = adam.adsl
  , pop   = (saffl = 'Y')
  , where = (trtemfl = 'Y')
  , var   = aebodsys aedecod
  , by    = trt01a
  , order = trt01an
  , sort  = frequency
  , label = "Treatment-emergent adverse events"
  , file  = &spl_out./tlf/t14_5_2
)

Output: DATA: <name>_ae (the result), <name> (the table), <name>_cols, <name>_display, <name>_pop and <name>_rec; FILE: <file>.rtf, <file>.lst and the numbers file (with file=); SETUP: the global macro variable <name>_print; SHOW: the long program.

%table_model#

A table of a continuous endpoint in one call: the descriptive statistics of the variables of describe= at the visit of at=, then the LS means and the differences of pairs of groups from an ANCOVA (%lsmeans, method=ancova) or from a mixed model for repeated measures over all visits (%mmrm, method=mmrm). The macro writes the long program of the table and runs it; it computes nothing itself. The results <name>_<variable> and <name>_ancova or <name>_mmrm, the table <name>, <name>_cols and <name>_display stay, and the setup <name>_print. show= gives the long program.

Procedure: none of its own (those of %describe, %lsmeans, %mmrm, %table)
Proven: node tools/tables_sas.ts (the same display data set as the long program, on the made-up study SYNHTN01); ran on SAS 9.4 M9, 2026-11-13
File: sas/macros/table_model.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records (a data set with USUBJID)
adslThe subjects, one row per subject: the population and the N of the columns; its variables go to the records (blank: the records have all they need)
popThe population: a condition on adsl= in parentheses
whereThe records to use: a condition in parentheses
varCHGThe response
covarsThe covariates of the model (base)
visitAVISITThe visit
byrequiredThe groups: a character variable, the columns
atThe visit of the table, in quotes ("Week 12"): the records of the descriptive blocks and the ANCOVA, the visit of the LS means of the MMRM
orderIts numeric twin (TRT01AN): the order of columns
describeVariables with a descriptive block before the model (base aval chg)
methodancovaancova (%lsmeans) or mmrm (%mmrm; at= is required)
modelThe fixed effects of the MMRM (default: covars= by= visit= by=*visit=)
typeunThe covariance structure of the MMRM
fallbackOther structures to try, in their order
pairsThe pairs compared: each group, then the group it is compared with, in quotes (blank: all pairs)
namesThe label of a block when it is not the label of the variable: chg="Change from baseline"
formatThe labels of categories: variable and format, as in a FORMAT statement (format= of %layout)
nameThe name of the table: the first part of the names of its data sets (default: the last part of file=)
shownoyes: the long program in the log; a file: the long program is written there (and run)
statsThe statistics of the model, each with its pattern (default: the LS means, their difference with its SE and limits, the p-value)
fileThe output without its extension: <file>.rtf, <file>.lst and the numbers file (blank: no print)
debugnoyes: keep the work data sets of the macros called
%table_model(
    data     = advs
  , where    = (paramcd = 'SYSBP' AND anl01fl = 'Y' AND avisitn > 0)
  , covars   = base
  , at       = "Week 12"
  , describe = base aval chg
  , by       = trtp
  , order    = trtpn
  , pairs    = "Drug X 10 mg" "Placebo"
  , file     = &spl_out./tlf/t14_3_1
)

Output: DATA: <name>_<variable> (the descriptive results), <name>_ancova or <name>_mmrm, <name> (the table), <name>_cols, <name>_display, <name>_at with at=, and <name>_pop and <name>_rec with adsl= or where=; FILE: <file>.rtf, <file>.lst and the numbers file (with file=); SETUP: the global macro variable <name>_print; SHOW: the long program.

%table_response#

A table of a binary response in one call: the subjects with the event and their percent with its interval in each group (%proportion), the comparison of pairs of groups (%risk: risk difference, relative risk or odds ratio, with its test), and with covars= the odds ratio adjusted for them (%logistic). The macro writes the long program of the table and runs it; it computes nothing itself. The results <name>prop, <name><measure> and <name>_adj, the table <name>, <name>_cols and <name>_display stay, and the setup <name>_print. show= gives the long program.

Procedure: none of its own (those of %proportion, %risk, %logistic, %table)
Proven: node tools/tables_sas.ts (the same display data set as the long program, on the made-up study SYNHTN01); ran on SAS 9.4 M9, 2026-11-13
File: sas/macros/table_response.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records (a data set with USUBJID)
adslThe subjects, one row per subject: the population and the N of the columns; its variables go to the records (blank: the records have all they need)
popThe population: a condition on adsl= in parentheses
whereThe records to use: a condition in parentheses
varrequiredThe response: a variable with two values
event"Y"The value that is the event, in quotes
byrequiredThe groups: a character variable, the columns
covarsCovariates: also the odds ratio adjusted for them (%logistic)
orderIts numeric twin (TRT01AN): the order of columns
pairsThe pairs compared: each group, then the group it is compared with, in quotes; # between pairs (blank: all pairs)
ciclopper-pearsonThe interval of the percent (ci= of %proportion)
measurerdrd, rr or or: the comparison of %risk; none: no comparison
diffciwaldThe interval of a risk difference (ci= of %risk)
formatThe labels of categories: variable and format, as in a FORMAT statement (format= of %layout)
nameThe name of the table: the first part of the names of its data sets (default: the last part of file=)
shownoyes: the long program in the log; a file: the long program is written there (and run)
fileThe output without its extension: <file>.rtf, <file>.lst and the numbers file (blank: no print)
debugnoyes: keep the work data sets of the macros called
%table_response(
    data   = adrs
  , var    = avalc
  , by     = trtp
  , order  = trtpn
  , pairs  = "Drug X 10 mg" "Placebo"
  , diffci = newcombe
  , covars = base
  , file   = &spl_out./tlf/t14_3_2
)

Output: DATA: <name>prop, <name><measure> and <name>_adj (the results), <name> (the table), <name>_cols, <name>_display, and <name>_pop and <name>_rec with adsl= or where=; FILE: <file>.rtf, <file>.lst and the numbers file (with file=); SETUP: the global macro variable <name>_print; SHOW: the long program.

%table_shift#

A shift table in one call: the subjects in each pair of categories, the category at baseline against the category after it (the two variables of var=), within the levels of within= (parameter, visit). The macro writes the long program of the table (the population, the records, one %freq, %stack, %table) and runs it; it computes nothing itself. The result <name>_shift, the table <name>, <name>_cols and <name>_display stay, and the setup <name>_print. show= gives the long program.

Procedure: none of its own (those of %freq, %table)
Proven: node tools/tables_sas.ts (the same display data set as the long program, on the made-up study SYNHTN01); ran on SAS 9.4 M9, 2026-11-13
File: sas/macros/table_shift.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records (a data set with USUBJID)
adslThe subjects, one row per subject: the population and the N of the columns; its variables go to the records (blank: the records have all they need)
popThe population: a condition on adsl= in parentheses
whereThe records to use: a condition in parentheses
varBNRIND ANRINDTwo variables: the category at baseline, then the category after it
withinThe levels above them (paramcd avisit)
byrequiredThe groups: a character variable, the columns
keepVariables kept for the names and the order of the rows (param avisitn)
orderIts numeric twin (TRT01AN): the order of columns
levelsThe planned categories of both variables, in quotes ('LOW' 'NORMAL' 'HIGH'): each is a row also without a subject
totalnoyes: also all groups together, as Total
formatThe labels of categories: variable and format, as in a FORMAT statement (format= of %layout)
nameThe name of the table: the first part of the names of its data sets (default: the last part of file=)
shownoyes: the long program in the log; a file: the long program is written there (and run)
labelThe label of the block, in quotes
statsThe statistics, each with its pattern (stats= of %freq)
fileThe output without its extension: <file>.rtf, <file>.lst and the numbers file (blank: no print)
debugnoyes: keep the work data sets of the macros called
%table_shift(
    data   = adlbc
  , where  = (anl01fl = 'Y' AND avisitn > 0)
  , within = paramcd avisit
  , levels = 'LOW' 'NORMAL' 'HIGH'
  , by     = trt01a
  , order  = trt01an
  , file   = &spl_out./tlf/t14_8_1
)

Output: DATA: <name>_shift (the result), <name> (the table), <name>_cols, <name>_display, and <name>_pop and <name>_rec with adsl= or where=; FILE: <file>.rtf, <file>.lst and the numbers file (with file=); SETUP: the global macro variable <name>_print; SHOW: the long program.

%table_summary#

A summary table (demographics, baseline) in one call: you name the variables, with # after a categorical one. The macro writes the long program of the table (the population, one %describe per continuous variable, one %freq per categorical variable, %stack, %table) and runs it; it computes nothing itself. The results <name>_<variable>_summ, the table <name>, the columns <name>_cols and the display data set <name>_display stay, and the setup <name>_print, for a change and a second %print. show= gives the long program, to read it or to go on from it by hand.

Procedure: none of its own (those of %describe, %freq, %table)
Proven: its cases of node tools/print_sas.ts (the same .lst and the same numbers file as the long program); ran on SAS 9.4 M9, 2026-11-13
File: sas/macros/table_summary.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe subjects, one row per subject (ADSL)
whereThe population: a condition in parentheses
varsrequiredThe variables in the order of the table. A name: continuous (%describe). A name and #: categorical (%freq), sex#. A name, # and a numeric variable: the categories in the order of that variable, agegr1#agegr1n
byrequiredThe groups: a character variable, the columns
orderIts numeric twin (TRT01AN): the order of columns
levelsThe planned categories of some variables: a variable, its values (levels= of %freq)
totalnoyes: also all groups together, as Total
catstatsThe statistics of a categorical variable (stats= of %freq)
namesThe label of a block when it is not the label of the variable: age="Age (years)" sex="Sex"
formatThe labels of categories: variable and format, as in a FORMAT statement (format= of %layout)
nameThe name of the table: the first part of the names of its data sets (default: the last part of file=)
shownoyes: the long program in the log; a file: the long program is written there (and run)
statsThe statistics of a continuous variable, each with its pattern (stats= of %describe)
fileThe output without its extension: <file>.rtf, <file>.lst and the numbers file (blank: nothing is printed)
debugnoyes: keep the work data sets of the macros called
%table_summary(
    data  = adsl
  , where = (saffl = 'Y')
  , vars  = age agegr1#agegr1n sex# race# ethnic#
  , by    = trt01a
  , order = trt01an
  , total = yes
  , file  = &spl_out./tlf/t14_2_1
)

Output: DATA: <name>_<variable>_summ (one result per variable), <name> (the table), <name>_cols, <name>_display, and <name>_pop with where=; FILE: <file>.rtf, <file>.lst and the numbers file (with file=); SETUP: the global macro variable <name>_print for %print(&<name>_print.); SHOW: the long program.

%table_tte#

A time-to-event table in one call: the Kaplan-Meier summary of each group (%km), the log-rank test (%logrank) and the hazard ratio of pairs of groups from a Cox model (%cox). The macro writes the long program of the table and runs it; it computes nothing itself. The results <name>_km, <name>_lr and <name>_cox, the table <name>, <name>_cols and <name>_display stay, and the setup <name>_print. show= gives the long program.

Procedure: none of its own (those of %km, %logrank, %cox, %table)
Proven: node tools/tables_sas.ts (the same display data set as the long program, on the made-up study SYNHTN01); ran on SAS 9.4 M9, 2026-11-13
File: sas/macros/table_tte.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records (a data set with USUBJID)
adslThe subjects, one row per subject: the population and the N of the columns; its variables go to the records (blank: the records have all they need)
popThe population: a condition on adsl= in parentheses
whereThe records to use: a condition in parentheses
varAVALThe time
cnsrCNSRThe censoring variable: 1 is censored
byrequiredThe groups: a character variable, the columns
timeTimes: also the percent without an event at each (time= of %km)
strataThe strata of the log-rank test and the Cox model
orderIts numeric twin (TRT01AN): the order of columns
pairsThe pairs of the hazard ratio: each group, then its comparator, in quotes (blank: all pairs)
formatThe labels of categories: variable and format, as in a FORMAT statement (format= of %layout)
nameThe name of the table: the first part of the names of its data sets (default: the last part of file=)
shownoyes: the long program in the log; a file: the long program is written there (and run)
fileThe output without its extension: <file>.rtf, <file>.lst and the numbers file (blank: no print)
debugnoyes: keep the work data sets of the macros called
%table_tte(
    data  = adam.adtte
  , adsl  = adam.adsl
  , pop   = (saffl = 'Y')
  , where = (paramcd = 'TTAE')
  , by    = trt01a
  , order = trt01an
  , pairs = "Drug X 10 mg" "Placebo"
  , file  = &spl_out./tlf/t14_6_1
)

Output: DATA: <name>_km, <name>_lr and <name>_cox (the results), <name> (the table), <name>_cols, <name>_display, <name>_pop and <name>_rec; FILE: <file>.rtf, <file>.lst and the numbers file (with file=); SETUP: the global macro variable <name>_print; SHOW: the long program.

%table_visit#

A table by visit in one call (vital signs, laboratory values): the descriptive statistics of each variable of vars= in each group, parameter and visit. The macro writes the long program of the table (the population, the records, one %describe per variable, %stack, %table) and runs it; it computes nothing itself. The results <name>_<variable>, the table <name>, <name>_cols and <name>_display stay, and the setup <name>_print. show= gives the long program.

Procedure: none of its own (those of %describe, %table)
Proven: node tools/tables_sas.ts (the same display data set as the long program, on the made-up study SYNHTN01); ran on SAS 9.4 M9, 2026-11-13
File: sas/macros/table_visit.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records (a data set with USUBJID)
adslThe subjects, one row per subject: the population and the N of the columns; its variables go to the records (blank: the records have all they need)
popThe population: a condition on adsl= in parentheses
whereThe records to use: a condition in parentheses
varsrequiredThe variables: a block of each (aval chg)
byrequiredThe groups: a character variable, the columns
paramPARAMCDThe parameter
visitAVISITThe visit
keepVariables kept for the names and the order of the rows (param paramn avisitn)
orderIts numeric twin (TRT01AN): the order of columns
postThe variables of vars= whose block has only the records of postwhere= (chg)
postwhere(ablfl NE 'Y')The records after baseline: a condition in parentheses
namesThe label of a block when it is not the label of the variable: chg="Change from baseline"
totalnoyes: also all groups together, as Total
formatThe labels of categories: variable and format, as in a FORMAT statement (format= of %layout)
nameThe name of the table: the first part of the names of its data sets (default: the last part of file=)
shownoyes: the long program in the log; a file: the long program is written there (and run)
statsThe statistics, each with its pattern (stats= of %describe)
fileThe output without its extension: <file>.rtf, <file>.lst and the numbers file (blank: no print)
debugnoyes: keep the work data sets of the macros called
%table_visit(
    data  = adam.advs
  , adsl  = adam.adsl
  , pop   = (saffl = 'Y')
  , where = (anl01fl = 'Y')
  , vars  = aval chg
  , by    = trt01a
  , order = trt01an
  , keep  = param paramn avisitn
  , post  = chg
  , file  = &spl_out./tlf/t14_7_1
)

Output: DATA: <name>_<variable> (one result per variable), <name> (the table), <name>_cols, <name>_display, <name>_pop and <name>_rec; FILE: <file>.rtf, <file>.lst and the numbers file (with file=); SETUP: the global macro variable <name>_print; SHOW: the long program.

%termflags#

The flag of each row of occurrence data for every subject: one record per subject of DENOM (with its group BY) and per term (each combination of TERMS in the records), FLAG "Y" when the subject has a record of the term and "N" when not. With threshold= only the terms whose percent of the subjects of a group reaches it in at least one group. It computes no statistic: %risk then compares the groups on FLAG within each term (within=TERMS), one record per subject, the subjects of the group the denominator.

Procedure: PROC SORT, MERGE, PROC SUMMARY
Proven: the known answer of the method perterm (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/termflags.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records (the events)
denomrequiredThe subjects of the groups: one row per subject (the population)
whereThe records to use, a condition in parentheses
termsThe variables of a row (a term within its class); none: one row of all the records (a row of an overview: any event, a serious event)
byrequiredThe groups: a variable of DENOM
thresholdA percent: only the terms with this percent of the subjects of a group or more, in at least one group (the percent itself, not its rounded text)
outrequiredThe result: USUBJID, BY, TERMS and FLAG, sorted by TERMS and USUBJID
debugnoyes: keep the work data sets of the macro (_termflags_*)
%termflags(
    data      = adae
  , denom     = adsl
  , where     = (TRTEMFL = 'Y')
  , terms     = AEBODSYS AEDECOD
  , by        = TRT01A
  , threshold = 5
  , out       = ae_flags
)

Output: OUT: one record per subject and term.

%tte#

One time-to-event parameter in one call: for each subject the first event, else the latest censoring. With end= an at-risk period from START to END: only the events and censorings in it count, else the subject is censored at the end of the period.

Procedure: PROC SORT, MERGE
Proven: the fixtures of the step kinds tte and tte.period (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/tte.sas (12NOV2026)

ParameterDefaultIt is
subjectsrequiredOne record per subject (USUBJID) with START (and END). The result keeps its variables: it must not have ADT, CNSR, AVAL or the values below.
eventsrequiredThe records the events come from: a data set
censorsThe records of the censorings: the latest date in the period (on one date the last record). Default: SUBJECTS.
eventWhich records of EVENTS are events: a condition. Default: all.
daterequiredThe date of an event, a variable of EVENTS (missing: no event)
timeThe time of an event (seconds since midnight), a variable of EVENTS, for the start day
priorityVariables of EVENTS that order the events of one date (a missing value last). Default: the order of the data.
startrequiredThe start of the period and the origin of AVAL, a date of SUBJECTS
start_timeThe time of the start, of SUBJECTS: on the start day an event counts unless both times are there and the event time is before it
endThe end of the at-risk period, a date of SUBJECTS. Without END: no period, every date counts, a date before START is START (as admiral).
censor_dateThe date of a censoring, of CENSORS (or of SUBJECTS)
seqSRCSEQ of an event: a variable of EVENTS. Default: missing.
adtfADTF of an event: the flag of its date, a variable of EVENTS
atmfATMF of an event: the flag of its time, a variable of EVENTS
keepOther variables of the record taken, in EVENTS and CENSORS both (the values of the rows that the program stacked before the call)
censor_cnsr1CNSR of a censoring: a number, or a variable of CENSORS
end_cnsr1CNSR at the end of the period
censor_descEVNTDESC of a censoring: a text in quotes, or a variable
censor_sourceSRCDOM of a censoring of SUBJECTS and of the end of the period, in quotes; SRCVAR is then the name of CENSOR_DATE (of END)
end_desc"End of at-risk period"EVNTDESC at the end of the period
event_descEVNTDESC of an event: a text in quotes, or a variable of EVENTS
sourceSRCDOM of an event, in quotes; SRCVAR is then the name of DATE
paramcdrequiredPARAMCD, in quotes
outrequiredThe result (see Output)
debugnoyes: keep the work data sets _tte_*
%tte(
    subjects      = work.adsl
  , events        = adam.adlb
  , event         = PARAMCD in ('ALT', 'AST') and R2A1HI >= 3
  , date          = ADT
  , priority      = PARAMCD
  , start         = TRTSDT
  , end           = DCUTDT
  , censor_date   = DCUTDT
  , seq           = ASEQ
  , censor_desc   = "Cutoff date"
  , censor_source = "ADSL"
  , event_desc    = "ALT/AST >= 3x ULN"
  , source        = "ADLB"
  , paramcd       = "DIL3X"
  , out           = work.dil3x
)

Output: OUT: one record per subject with an event, a censoring or (with end=) a period: the variables of SUBJECTS, PARAMCD, ADT, CNSR, AVAL (ADT - START + 1), ATM (with time=) and the values above, in the order of SUBJECTS. With end= a subject without START or without END gets no record and a WARNING that names it (USUBJID=); a subject whose START is after its END gets no record. Two records of a subject in SUBJECTS are an ERROR.

%ttest#

The two-sample t-test of each pair of groups, one record per subject (PROC TTEST, BY each pair, the group first): the difference of the means (diff: the group minus the group it is compared with), its standard error (diff_se), its confidence limits (lcl, ucl) and the two-sided p-value (pval): Student's with the pooled variance (variances=equal) or Welch's with Satterthwaite's df (variances=unequal).

Procedure: PROC TTEST
Proven: the known answer of the method ttest (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/ttest.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe variable
byrequiredThe groups: a character variable
pairsallall: each later group against each earlier one, in the order of ORDER; or the pairs separated by #, one pair per line, the group first, then the group it is compared with (a value with a blank or a hyphen in quotes): "Drug" "Placebo" # "High" "Low". The form "A"-"B" of version 1.0 is still read.
orderA numeric variable with one value per group: the order of the groups
variancesequalequal (default: the Pooled row) or unequal (the Satterthwaite row)
level0.95The confidence level (default 0.95)
statsdiff="XX.X" diff_se="XX.XX" lcl="XX.X" ucl="XX.X" pval="X.XXX"The statistics, each with its pattern (default diff="XX.X" diff_se="XX.XX" lcl="XX.X" ucl="XX.X" pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_ttest_*)
%ttest(
    data  = advs
  , where = (PARAMCD = 'SYSBP' and AVISIT = 'Week 12')
  , var   = CHG
  , by    = TRT01P
  , order = TRT01PN
  , out   = sbp_t
)

Output: OUT: one row per pair and statistic (BY: the group, VERSUS: the group it is compared with); GROUPS is BY VERSUS.

%visits#

Give each record its analysis visit by its visit: AVISIT and AVISITN. A record of a visit that is not in the list keeps them missing.

Procedure: DATA steps (SET POINT=)
Proven: the fixtures of the step kind windows.visit (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/visits.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe working data set (changed in place)
visitsrequiredThe visits, # between them; a visit is "VISIT" AVISITN "AVISIT"
%visits(
    data   = adlb
  , visits = "SCREENING" 0 "Baseline"
           # "WEEK 2"    2 "Week 2"
)

Output: DATA: AVISIT and AVISITN on each record of a visit in the list; missing on the others.

%wilcoxon#

The Wilcoxon rank-sum test of each pair of groups, one record per subject (PROC NPAR1WAY, BY each pair, the group first): the two-sided p-value of the normal approximation with the continuity correction (pval; CORRECT=), the Hodges-Lehmann estimate of the shift (estimate: the group minus the group it is compared with) and its asymptotic (Moses) confidence limits (lcl, ucl; HL).

Procedure: PROC NPAR1WAY (WILCOXON, HL)
Proven: the known answer of the method wilcoxon (src/methods.ts, worked out by tools/method_answers.R; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/wilcoxon.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe variable
byrequiredThe groups: a character variable
pairsallall: each later group against each earlier one, in the order of ORDER; or the pairs separated by #, one pair per line, the group first, then the group it is compared with (a value with a blank or a hyphen in quotes): "Drug" "Placebo" # "High" "Low". The form "A"-"B" of version 1.0 is still read.
orderA numeric variable with one value per group: the order of the groups
correctionyesyes (default): the continuity correction of the test; no
level0.95The confidence level (default 0.95)
statsestimate="XX.XX" lcl="XX.XX" ucl="XX.XX" pval="X.XXX"The statistics, each with its pattern (default estimate="XX.XX" lcl="XX.XX" ucl="XX.XX" pval="X.XXX")
labelThe label of the block in a table, in quotes
outrequiredThe result
debugnoyes: keep the work data sets of the macro (_wilcoxon_*)
%wilcoxon(
    data  = adqs
  , where = (PARAMCD = 'PAIN' and AVISIT = 'Week 12')
  , var   = CHG
  , by    = TRT01P
  , order = TRT01PN
  , out   = pain_w
)

Output: OUT: one row per pair and statistic (BY: the group, VERSUS: the group it is compared with); GROUPS is BY VERSUS.

%windows#

Put each record in its analysis window by its study day: AVISIT, AVISITN, AWLO, AWHI, AWTARGET, AWRANGE, AWU (DAYS) and AWTDIFF (the distance to the target day). A record in no window keeps them missing. The first window (in the order of AVISITN) that holds the day is taken.

Procedure: DATA steps (SET POINT=)
Proven: the fixtures of the step kind windows.day (src/adam_steps.ts; node tools/adam_steps_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/windows.sas (13NOV2026)

ParameterDefaultIt is
datarequiredThe working data set (changed in place)
windowsrequiredThe windows in the order of AVISITN, # between them; a window is AVISITN "AVISIT" AWLO AWHI AWTARGET "AWRANGE" (a period: a limit that is open)
dayADYThe study day (default ADY)
%windows(
    data    = advs
  , windows = 0 "Baseline" .  1  1  "<=1"
            # 4 "Week 4"   2  42 28 "2-42"
            # 8 "Week 8"   43 70 56 "43-70"
  , day     = ADY
)

Output: DATA: AVISIT, AVISITN, AWLO, AWHI, AWTARGET, AWRANGE, AWU and AWTDIFF on each record in a window; missing on a record in no window.

%worst#

The records of each subject at its worst level (the maximum severity or toxicity grade): for each subject and each value of BY (a term, or nothing: all its records), only the records whose VAR is the highest of LEVELS, in their order from the least to the most severe. A record whose VAR is not one of LEVELS (a missing grade) is never the worst and is left out. It computes no statistic: %freq then counts each subject once in each row, at its worst level.

Procedure: PROC SORT, a DATA step (BY, FIRST.)
Proven: the known answer of the method catsumm with worst= (src/methods.ts, counted by hand; R ars_worst gives the same, test/safety-r.test.ts; node tools/methods_sas.ts); ran on SAS 9.4 M9, 2026-11-12
File: sas/macros/worst.sas (12NOV2026)

ParameterDefaultIt is
datarequiredThe records
whereThe records to use, a condition in parentheses
varrequiredThe graded variable
byThe variables of a row (the worst of each subject in each row); default: none (the worst of all the records of the subject)
levelsrequiredIts values from the least to the most severe (texts in quotes, numbers without)
outrequiredThe records kept (all the variables of DATA)
debugnoyes: keep the work data sets of the macro (_worst_*)
%worst(
    data   = adae
  , where  = (TRTEMFL = 'Y')
  , var    = AESEV
  , by     = AEBODSYS AEDECOD
  , levels = 'MILD' 'MODERATE' 'SEVERE'
  , out    = ae_worst
)

Output: OUT: the records of DATA at the worst level of each subject and row.