---
title: "Porting a %summarytable program to R"
author: "John Ehrlinger"
date: today
format: html
vignette: >
  %\VignetteIndexEntry{Porting a %summarytable program to R}
  %\VignetteEngine{quarto::html}
  %\VignetteEncoding{UTF-8}
---

```{r, include = FALSE}
knitr::opts_chunk$set(collapse = TRUE, comment = "#>")
```

```{r setup}
library(hvtiRtables)
```

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:

```{r, error = TRUE}
hv_tbl_summary(
  gtsummary::trial, by = "trt",
  groups = list(Demography = "age"), continuous = "age",
  tbltitle = "Table 1"
)
```

## 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`:

```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"`:

```{r eval = FALSE}
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`:

```{r}
stats::quantile(c(1, 2, 3, 4), 0.25, type = 2, names = FALSE)  # SAS house
stats::quantile(c(1, 2, 3, 4), 0.25, type = 7, names = FALSE)  # R default
```

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.

```{r}
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")
```

## 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.

```{r, error = TRUE}
hv_tbl_summary(
  gtsummary::trial, groups = list(D = "age"),
  continuous = "age", binary = "age"
)
```

## proc corr

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

```sas
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;
```

```{r eval = FALSE}
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.
