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.
-
A
CON1=variable not listed innormal. The macro ran one-way ANOVA on it. A variable incontinuousalone is tested non-parametrically. -
An
ORD1=variable. The macro ran Kruskal-Wallis; folded intocategorical, it gets chi-square. No trend test is run. -
A sparse categorical variable. Both switch to Fisher’s exact, at different thresholds -
CUTEXACT=50needs half the cells to have expected counts below 5,gtsummaryneeds one.
Percentiles and QNTLDEF
SAS’s house QNTLDEF=5 is not R’s default quantile definition. It corresponds to type = 2:
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.
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.