Skip to contents

This is written for someone who knows %summarytable and the tp.dc.* templates, and does not want to learn gtsummary beyond what the port requires. It ends at the summary object; rendering it is covered in vignette("hvtiRtables").

One macro call becomes three

%summarytable computes the table and writes the document. Its RTFFILE=, TBLTITLE=, ADDFN=, STYLE= and PAGE= parameters sit in the same call as CON1= and CLASS=. This package splits that work, so the parameters you know are distributed across separate calls:

Stage Function Macro parameters that land here
Compute stats and tests hv_tbl_summary() DATA=, CLASS=, CON*=, CAT*=, ORD1=, LIST=, PP=, PVALUES=, ASD=, TOTALCOL=, CUTEXACT=
Shape the table hv_man_table() STYLE= is style = in hv_man_table(), and a manuscript table’s layout = chooses compare or describe; PAGE= has none (pages are US Letter)
Write the file hv_man_table_save() RTFFILE=, PDFFILE=, XMLFILE=, TBLTITLE=, ADDFN=, PRINTFN=
Verify the file hv_check_docx() (no analogue)

If you pass a macro parameter to the wrong stage, the error says which function takes it:

hv_tbl_summary(
  gtsummary::trial, by = "trt",
  groups = list(Demography = "age"), continuous = "age",
  tbltitle = "Table 1"
)
#> Error:
#> ! `tbltitle` is a `%summarytable` parameter this package handles at a different stage: pass `caption =` to `hv_man_table_save()`. Note: the manuscript style requires a caption and the report style takes an optional one; it is written in bold above the table.

Defaults that differ

Five places where a faithful-looking port produces different numbers.

Macro Default there Default here Fix
CON1= mean +/- SD, one-way ANOVA median (percentiles), Kruskal-Wallis list the variable in normal as well as continuous
PP= 16 84 nominal; house templates use both 15 85 and 16 84 percentiles = c(15, 85) percentiles = c(16, 84)
CUTEXACT= Fisher when >=50% of cells have expected < 5 Fisher when any expected < 5 (cutexact = "any") cutexact = 50
ASD= %stddiff: Gaussian and non-Gaussian variables apart, printed as a percentage gtsummary’s generic standardized mean difference, a proportion compute with hvtiRpropensity::ps_stddiff() and add it with hv_tbl_compare()
ORD1= n (%), Kruskal-Wallis folds into categorical, chi-square none

overall = TRUE matches the macro’s TOTALCOL=1. Both manuscript layouts show the Overall column when the table carries it. continuous_stat = "both" shows mean +/- SD and median (percentiles) side by side, in two columns per group, for choosing one when the manuscript is drafted. To show mean +/- SD for some variables and medians for others while every continuous test stays nonparametric, list the former in mean_sd.

A ported call

The %summarytable call from tp.dc.stddiff.summarytable.sas:

%summarytable(data=built,
              class=grp_res,
              con3=&nongau.,
              cat1=&binary.,
              cat2=&catg.,
              pp=15 85,
              totalcol=0,
              pvalues=1,
              list=
    /* Demography */
       female AGE BSA race_gp
    /* Symptoms */
       surgstat nyha_pr
);

and its equivalent. Note that pp= and list= map straight across, totalcol=0 becomes overall = FALSE, and pvalues=1 becomes compare = "pvalue":

tbl <- hv_tbl_summary(
  built,
  by = "grp_res",
  groups = list(
    Demography = c("female", "AGE", "BSA", "race_gp"),
    Symptoms   = c("surgstat", "nyha_pr")
  ),
  continuous  = c("AGE", "BSA"),
  binary      = c("female", "surgstat"),
  categorical = c("race_gp", "nyha_pr"),
  compare     = "pvalue",
  percentiles = c(15, 85),
  overall     = FALSE
)

Parameter map

%summarytable Here Notes
DATA= data
CLASS= by
LIST= groups a named list; the section comments become the names
CON1= continuous and normal both: normal alone leaves the variable unclassified. Mean +/- SD with one-way ANOVA, as the macro did
CON2= continuous statistic changes - median (min, max) becomes median (percentiles)
CON3= continuous direct equivalent; this is the behavior continuous reproduces
CAT1= binary
CAT2= categorical
ORD1= categorical test changes - Kruskal-Wallis becomes chi-square
PP= percentiles
PVALUES= compare = "pvalue"
ASD= compare = "smd" / "both", or hv_tbl_compare() number changes - see Defaults that differ
CUTEXACT= cutexact default differs - see Defaults that differ
TOTALCOL= overall defaults match (overall = TRUE); totalcol=0 becomes overall = FALSE
NCOL= automatic per-group Ns always shown, and the Overall column’s N with the default overall = TRUE (NCOL=3)
TBLTITLE= caption required by style = "manuscript", optional for the report
ADDFN= footnotes on the save function
PRINTFN= omit footnotes (default) On the standard hv_tbl_summary() -> hv_man_table() path, the default dagger follows continuous_stat and percentiles automatically. Use footnotes = NULL for PRINTFN=0.
RTFFILE=, PDFFILE=, XMLFILE= file output is always .docx

When continuous_stat = "mean" or "both", mean±SD cells follow Eugene H. Blackstone’s (EHB’s) paired rounding rule: the mean is rounded to the SD’s first-significant-digit place and the SD one place finer. An SD beginning with 1 gives both one additional place, and exact ties round to even. The rule does not define median/percentile precision; those cells follow the house notation: brackets, 2 significant digits, and a value of 10 or more to the units place (102 [63, 170]). P values print without a leading zero (.013, <.001).

DESCLABEL=1 suffixed each row label with its statistic. A table that mixes mean and median here marks cells instead: a section sign on the medians when means are the default, a pilcrow on the means otherwise, each with its own note.

The save stage is a sibling pair, and the two do not take the same arguments: footnotes has a different shape in each. Both take caption; the manuscript style requires it.

Not supported: WEIGHT=, PROPENMT=, MWOUTCOMES=, SUBSET= (subset the data frame first), SORTBY= (order comes from groups), COLPCT=0 (row percentages), MISSCOL=, ADHOC=, ODDSRATIOS=, CUTEXACT=, PAGE= (pages are US Letter), CWIDTH1-3=, PAPERSIZE=.

Why my p-value moved

Three causes, in order of how often they explain it.

  1. A CON1= variable not listed in normal. The macro ran one-way ANOVA on it. A variable in continuous alone is tested non-parametrically.
  2. An ORD1= variable. The macro ran Kruskal-Wallis; folded into categorical, it gets chi-square. No trend test is run.
  3. A sparse categorical variable. Both switch to Fisher’s exact, at different thresholds - CUTEXACT=50 needs half the cells to have expected counts below 5, gtsummary needs one.

Percentiles and QNTLDEF

SAS’s house QNTLDEF=5 is not R’s default quantile definition. It corresponds to type = 2:

stats::quantile(c(1, 2, 3, 4), 0.25, type = 2, names = FALSE)  # SAS house
#> [1] 1.5
stats::quantile(c(1, 2, 3, 4), 0.25, type = 7, names = FALSE)  # R default
#> [1] 1.75

Medians agree at every n; Q1, Q3 and pNN diverge silently. gtsummary’s {pXX} statistics already use type = 2, so hv_tbl_summary() matches SAS without intervention. Any stats::quantile() call you write by hand in ported code does not - pin type = 2 explicitly.

The percentiles actually used are recorded on the table object as attr(tbl, "hv_stat_label"), and the describe layout’s second header row reads it, so the sub-header names the statistic the cells hold. Before 1.0.5 the default was a literal mean-and-SD string, and a median table rendered without an explicit stat_label = claimed mean/SD over median columns.

tbl <- hv_tbl_summary(
  gtsummary::trial, by = "trt",
  groups = list(Demography = c("age", "marker")),
  continuous = c("age", "marker"),
  percentiles = c(16, 84)
)
attr(tbl, "hv_stat_label")
#> [1] "No. (%) or Median [16th, 84th percentiles]"

Reading the SAS data in

haven::read_sas() returns haven_labelled columns over doubles, because every SAS numeric is an 8-byte float. A 0/1 flag therefore arrives looking like a continuous variable, and type guessers classify it as one.

This is why binary and categorical are stated rather than inferred: name every variable in exactly one of continuous, binary or categorical, and a misclassification is an error rather than a wrong table.

hv_tbl_summary(
  gtsummary::trial, groups = list(D = "age"),
  continuous = "age", binary = "age"
)
#> Error:
#> ! `age` appears in more than one of `continuous`, `binary`, and `categorical`. Every variable must be classified exactly once. Overlapping: age (continuous, binary).

proc corr

hv_correlation_table() ports a proc corr job, not a %summarytable one:

proc sort data=built; by a1c_grp; run;
proc corr data=built nosimple spearman pearson fisher(biasadj=no alpha=.32);
  var rdw_pr mcv_pr alb_pr;
  with hbga1cpr;
  by a1c_grp;
run;
hv_correlation_table(built, vars = c("rdw_pr", "mcv_pr", "alb_pr"),
                     with = "hbga1cpr", by = "a1c_grp")

alpha=.32 is a 68% interval, and it is this function’s default. The interval itself is Fisher’s z without bias adjustment, matching SAS’s biasadj=no. The overall table, the same SAS job run without BY, is a second call without by; the two results have different columns, since only the stratified one carries the by column, so add it by hand (e.g. overall$a1c_grp <- "Overall") before rbind()-ing the two together. The scatter-plot matrix, SAS’s plots=matrix, is hvtiPlotR::hv_correlation_matrix(); this function only produces the table.