---
title: "Continuous summary tables in R"
description: >
  Build continuous summary tables in R with table_continuous(),
  including grouped descriptives, group-comparison tests, effect sizes,
  and export to console, gt, tinytable, flextable, Excel, or Word.
output: rmarkdown::html_vignette
vignette: >
  %\VignetteIndexEntry{Continuous summary tables in R}
  %\VignetteEngine{knitr::rmarkdown}
  %\VignetteEncoding{UTF-8}
---

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

build_rich_tables <- identical(Sys.getenv("IN_PKGDOWN"), "true")

pkgdown_dark_gt <- function(tab) {
  tab |>
    gt::opt_css(
      css = paste(
        ".gt_table, .gt_heading, .gt_col_headings, .gt_col_heading,",
        ".gt_column_spanner_outer, .gt_column_spanner, .gt_title,",
        ".gt_subtitle, .gt_sourcenotes, .gt_sourcenote {",
        "  background-color: transparent !important;",
        "  color: currentColor !important;",
        "}",
        sep = "\n"
      )
    )
}
```

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

`table_continuous()` summarizes continuous variables either overall or
by a categorical grouping variable. It is designed for readable summary
tables in the console and for publication-ready outputs in rendered
documents. When `by` is supplied, it can also add group-comparison
tests, test statistics, and effect sizes.

## Basic usage

Use `select` to choose the continuous variables you want to summarize:

```{r basic}
table_continuous(
  sochealth,
  select = c(bmi, wellbeing_score, life_sat_health)
)
```

If you omit `select`, `table_continuous()` scans the data frame and
keeps numeric columns:

```{r default-select}
table_continuous(sochealth)
```

## Grouped summaries

Add `by` to summarize the same variables across categories. When
`by` is supplied, a Welch-test *p*-value column is added automatically:

```{r grouped}
table_continuous(
  sochealth,
  select = c(bmi, wellbeing_score, life_sat_health),
  by = education
)
```

This is the main pattern for reporting continuous variables across
groups such as education, sex, treatment arm, or survey wave. Pass
`p_value = FALSE` to suppress the test column and keep the output
strictly descriptive.

If you want the same outcomes reported in a linear-model workflow, with
heteroskedasticity-consistent standard errors or case weights, use
`table_continuous_lm()` instead:

```{r lm-companion}
table_continuous_lm(
  sochealth,
  select = c(bmi, wellbeing_score, life_sat_health),
  by = education,
  vcov = "HC3"
)
```

## Add test statistics and effect sizes

Grouped tables can also report the test statistic and an effect size
alongside the default *p*-value column:

```{r pvalue-effect}
table_continuous(
  sochealth,
  select = c(bmi, wellbeing_score, life_sat_health),
  by = education,
  statistic = TRUE,
  effect_size_ci = TRUE
)
```

Use `test = "student"` for equal-variance parametric tests or
`test = "nonparametric"` for rank-based comparisons:

```{r nonparametric}
table_continuous(
  sochealth,
  select = c(bmi, wellbeing_score),
  by = education,
  test = "nonparametric",
  statistic = TRUE,
  effect_size = TRUE
)
```

`effect_size = TRUE` auto-selects the canonical measure for the
chosen `test` and number of groups: Hedges' *g* (parametric, 2
groups), eta-squared (parametric, 3+ groups), rank-biserial *r*
(nonparametric, 2 groups), or epsilon-squared (nonparametric, 3+
groups). To pick a specific measure explicitly, pass its character
name:

```{r effect-size-explicit}
table_continuous(
  sochealth,
  select = wellbeing_score,
  by = sex,
  effect_size = "hedges_g",
  effect_size_ci = TRUE
)
```

Allowed values are `"none"` (default), `"auto"` (= legacy `TRUE`),
`"hedges_g"`, `"eta_sq"`, `"r_rb"`, and `"epsilon_sq"`. Incompatible
explicit choices (e.g. `"eta_sq"` with two groups, or `"hedges_g"`
with `test = "nonparametric"`) trigger an actionable error.

When you need the underlying columns for further processing, use
`output = "data.frame"`:

```{r raw-output}
table_continuous(
  sochealth,
  select = c(bmi, wellbeing_score),
  by = education,
  statistic = TRUE,
  effect_size = TRUE,
  output = "data.frame"
)
```

## Selecting variables

`select` supports tidyselect helpers:

```{r tidyselect}
table_continuous(
  sochealth,
  select = starts_with("life_sat"),
  by = sex
)
```

For more programmatic selection, set `regex = TRUE`:

```{r regex}
table_continuous(
  sochealth,
  select = "^life_sat",
  regex = TRUE,
  by = education,
  output = "data.frame"
)
```

Use `exclude` when you want a broad selection with one or two explicit
removals:

```{r exclude}
table_continuous(
  sochealth,
  select = c(bmi, wellbeing_score, life_sat_health, life_sat_work),
  exclude = "life_sat_work",
  by = sex
)
```

## Labels and output formats

Use `labels` to replace technical variable names with reporting labels:

```{r labels, eval = build_rich_tables}
pkgdown_dark_gt(
  table_continuous(
    sochealth,
    select = c(bmi, wellbeing_score, life_sat_health),
    by = education,
    labels = c(
      bmi = "Body mass index",
      wellbeing_score = "Well-being score",
      life_sat_health = "Satisfaction with health"
    ),
    output = "gt"
  )
)
```

`table_continuous()` supports the same reporting-oriented outputs as
`table_categorical()`:

| `output` value | Returned object |
|:--|:--|
| `"default"` | Styled ASCII console table |
| `"tinytable"` | Formatted tinytable |
| `"gt"` | Formatted gt table |
| `"flextable"` | Formatted flextable |
| `"excel"` | Written `.xlsx` file |
| `"clipboard"` | Copied text table |
| `"word"` | Written `.docx` file |

For example, `tinytable` works well in Quarto and R Markdown documents:

```{r tinytable, eval = build_rich_tables}
table_continuous(
  sochealth,
  select = c(bmi, wellbeing_score, life_sat_health),
  by = education,
  output = "tinytable"
)
```

## Export to Excel or Word

Use the same function to export the table directly:

```{r export, eval = FALSE}
table_continuous(
  sochealth,
  select = c(bmi, wellbeing_score, life_sat_health),
  by = education,
  output = "excel",
  excel_path = "table_continuous.xlsx"
)

table_continuous(
  sochealth,
  select = c(bmi, wellbeing_score, life_sat_health),
  by = education,
  output = "word",
  word_path = "table_continuous.docx"
)
```

## Display options

The printed ASCII table and every rendered output share the same
formatting vocabulary as `table_continuous_lm()` and
`table_categorical()`:

- `align = "decimal"` (default) aligns numeric columns on the
  decimal mark, matching SPSS / SAS / LaTeX `siunitx` conventions.
  `"center"`, `"right"`, and the legacy `"auto"` are the
  alternatives.
- `p_digits = 3` (default, the APA standard) drives both the
  displayed precision of the `p` column and the small-*p* threshold
  (`p_digits = 4` -> `.0451` and `<.0001`).
- `show_n = FALSE` and `ci = FALSE` drop the corresponding columns
  (and the CI spanner / borders) structurally from every output;
  the underlying `n` and `ci_lower` / `ci_upper` are always present
  in `output = "data.frame"` / `"long"` for downstream code.

```{r display-opts}
table_continuous(
  sochealth,
  select = wellbeing_score,
  by = sex,
  ci = FALSE,
  show_n = FALSE,
  p_digits = 4
)
```

## Tidying for downstream pipelines

`table_continuous()` returns an object that can be coerced to a plain
`data.frame` / `tbl_df` (stripping the spicy formatting attributes)
or piped into `broom::tidy()` / `broom::glance()` for use with
`gtsummary`, `modelsummary`, `parameters`, or any other tidyverse
stats workflow:

```{r tidy-glance}
out <- table_continuous(
  sochealth,
  select = c(bmi, wellbeing_score),
  by = sex
)

# Long descriptive rows: one per (variable x group) with broom-style
# columns (outcome, label, group, estimate = mean, std.error,
# conf.low / conf.high, n, min, max, sd).
broom::tidy(out)

# One row per outcome with the omnibus test + effect-size summary
# (test_type, statistic, df, df.residual, p.value, es_type, es_value,
# es_ci_lower / es_ci_upper, n_total).
broom::glance(out)

# Or just unbox to a plain data.frame (long-format underlying data)
head(as.data.frame(out))
```

## See also

- See `vignette("table-categorical", package = "spicy")` for grouped
  tables of categorical variables.
- See `vignette("table-continuous-lm", package = "spicy")` for
  model-based continuous summary tables with robust standard errors or
  case weights.
- See `vignette("summary-tables-reporting", package = "spicy")` for a
  cross-function reporting workflow using both summary-table helpers.
