Missing data and denominators
Source:vignettes/missing-data-denominators.Rmd
missing-data-denominators.RmdThe policy in one minute
gtstats does not silently treat missing values as observations with a value. Each analysis retains or reports the observations contributing to its result. The usual default is to use observations with the values needed for that calculation and make the resulting denominator available for review.
There is no universal denominator. A categorical percentage needs observed values of that variable; a correlation needs complete pairs; a rate needs valid event-time records. The right denominator depends on the scientific question.
- Start with
describe_data()to see completeness for every variable. - Choose the percentage denominator deliberately for descriptive tables.
- Use
denominators_stats()before reporting results when exclusions matter. - Describe the analysis population when missing data are material.
Key terms
| Term | Meaning in gtstats |
|---|---|
n_total |
Rows in the relevant dataset or group before the statistic-specific exclusion |
n_nonmissing |
Rows with the values required for the variable |
n_missing |
Rows excluded because a required value is missing |
level |
Categorical level represented by a displayed cell, when relevant |
numerator |
Count of events or selected levels, where relevant |
denominator |
Eligible observations or total person-time used for the estimate |
rule |
Plain-language record of how the denominator was formed |
denominators_stats() returns a formatted table by
default, or a plain console tibble with format = "tibble".
For categorical summaries, it records the level as well as the group, so
row and overall percentages can be checked against the exact denominator
used in the displayed cell.
Start by describing missingness
describe_data() reports completeness from all rows in
the supplied dataset; it never drops rows to make the overview look
complete.
dat <- data.frame(
arm = factor(c("Control", "Control", "Treatment", "Treatment", "Treatment")),
age = c(45, NA, 51, 62, 57),
smoker = factor(c("No", "Yes", NA, "Yes", "No")),
follow_up = c(12, 10, 8, NA, 11)
)
to_flextable(describe_data(dat))Variable |
Type |
Complete |
Unique |
Overview |
Range / levels |
|---|---|---|---|---|---|
arm |
binary |
5/5 (100.0%) |
2 |
Treatment 3 (60.0%); Control 2 (40.0%) |
Control, Treatment |
age |
categorical |
4/5 (80.0%) |
4 |
45 1 (25.0%); 51 1 (25.0%); 57 1 (25.0%) |
45, 51, 62, 57 |
smoker |
binary |
4/5 (80.0%) |
2 |
No 2 (50.0%); Yes 2 (50.0%) |
No, Yes |
follow_up |
categorical |
4/5 (80.0%) |
4 |
10 1 (25.0%); 11 1 (25.0%); 12 1 (25.0%) |
12, 10, 8, 11 |
One row is shown per selected variable. | |||||
Potential data-quality findings and interpretation prompts are stored in `$issues`. | |||||
Ordered factors are identified as ordinal variables. | |||||
This reports the amount of missing data, not its cause. Clinical context, the data dictionary, and the study design are needed to decide whether a complete-case analysis is scientifically appropriate.
Descriptive tables: percentage denominators
For categorical variables, summary_table() makes the
denominator explicit. The default is
percent = "column".
| Setting | Percentage denominator | Typical use |
|---|---|---|
"column" |
Non-missing observations in each displayed group | Standard Table 1 |
"row" |
Non-missing observations for that level across groups | Distribution of a category across groups |
"overall" |
All non-missing observations of the variable | Share of the overall sample |
categorical = "n" |
No percentage | Counts only |
summary_table(
dat,
by = arm,
include = smoker,
percent = "column",
missing = "ifany"
) |> to_flextable()Characteristic |
Control |
Treatment |
|---|---|---|
smoker |
||
No |
1 (50.0%) |
1 (50.0%) |
Yes |
1 (50.0%) |
1 (50.0%) |
Missing |
0 (0.0%) |
1 (33.3%) |
Categorical data are n (%). | ||
to_flextable(summary_table(dat, by = arm, include = smoker, percent = "row"))Characteristic |
Control |
Treatment |
|---|---|---|
smoker |
||
No |
1 (50.0%) |
1 (50.0%) |
Yes |
1 (50.0%) |
1 (50.0%) |
Missing |
0 (0.0%) |
1 (33.3%) |
Categorical data are n (%). Percentages use row denominators. | ||
to_flextable(summary_table(dat, by = arm, include = smoker, percent = "overall"))Characteristic |
Control |
Treatment |
|---|---|---|
smoker |
||
No |
1 (25.0%) |
1 (25.0%) |
Yes |
1 (25.0%) |
1 (25.0%) |
Missing |
0 (0.0%) |
1 (33.3%) |
Categorical data are n (%). Percentages use overall denominators. | ||
to_flextable(summary_table(dat, by = arm, include = smoker, categorical = "n"))Characteristic |
Control |
Treatment |
|---|---|---|
smoker |
||
No |
1 |
1 |
Yes |
1 |
1 |
Missing |
0 |
1 |
Categorical data are counts. | ||
missing = "ifany" displays a Missing row only when it is
needed. Use "always" to show zero-missing rows or
"no" to suppress them. These three settings calculate
observed-category percentages from non-missing values; suppressing the
row does not change that denominator.
Use missing = "as_category" when missing values should
form part of the displayed categorical distribution. The Missing row is
then included in the percentage denominator, so the observed categories
plus Missing sum to 100%.
catheter_data <- data.frame(
catheter = factor(
c(rep("Yes", 32), rep(NA_character_, 68)),
levels = c("No", "Yes")
)
)
summary_table(
catheter_data,
include = catheter,
missing = "as_category"
) |> to_flextable()Characteristic |
Overall |
|---|---|
catheter |
|
No |
0 (0.0%) |
Yes |
32 (32.0%) |
Missing |
68 (68.0%) |
Categorical data are n (%). | |
This reports 32/100 (32%) as documented Yes and 68/100 (68%) as Missing. It does not assume that missing records are No. For continuous variables, missing values cannot become a numeric category and therefore remain a separate missingness row.
Continuous summaries use observed finite values of that variable. A displayed Missing row is calculated against all rows in the relevant group.
Use categorical = "n_over_N_percent" when the table
should show the non-missing denominator inside every cell, for example
30/59 (50.8%). For a continuous descriptive estimate with
precision but no p-value, use statistic = "mean_ci"; this
reports a t confidence interval for the observed mean using
conf.level.
Proportions and confidence intervals
proportion_stats() and add_proportion() use
non-missing observations of the selected variable as the denominator.
The numerator is the chosen level, and the confidence interval describes
that observed-data proportion.
smoking <- proportion_stats(dat, smoker, by = arm, level = "Yes")
to_flextable(smoking)Control |
Treatment |
|||
|---|---|---|---|---|
Event |
n (%) |
95% CI |
n (%) |
95% CI |
Yes |
1 (50.0%) |
9.5–90.5% |
1 (50.0%) |
9.5–90.5% |
Selected event: smoker = Yes. Estimates use Wilson score 95% confidence intervals. | ||||
This function estimates a proportion; it does not test differences between groups. | ||||
The denominator must define the population at risk, and observations should be independent. | ||||
denominators_stats(smoking)| Denominator audit | ||||||||
| Variable | Level | Group | Eligible observations | Used in analysis | Missing / excluded | Numerator | Denominator | Rule |
|---|---|---|---|---|---|---|---|---|
| smoker | NA | arm = Control | 2 | 2 | 0 | NA | 2 | Non-missing observations; event level = Yes |
| smoker | NA | arm = Treatment | 3 | 2 | 1 | NA | 2 | Non-missing observations; event level = Yes |
If unknown status should be included in a categorical descriptive
distribution, use missing = "as_category". Do not assume
that an unknown value is a non-event; recode it to No only when that
meaning is justified by the data definition.
An all-missing categorical variable remains visible in
summary_table() as an em dash, with a Missing row by
default. It is not silently dropped from the table.
Rates use complete event-time records
rate_stats() and add_rate() require finite,
non-negative event counts and person-time values. Rows missing either
value are excluded. The denominator is the sum of valid person-time, not
the number of patients. A group with zero accumulated time is retained
but its rate is reported as not estimable.
rate_dat <- data.frame(
arm = c("Control", "Control", "Treatment", "Treatment"),
events = c(1, 0, 2, 1),
person_years = c(1.2, NA, 0.8, 1.0)
)
rate <- rate_stats(rate_dat, event = events, time = person_years, by = arm)
denominators_stats(rate)| Denominator audit | ||||||||
| Variable | Level | Group | Eligible observations | Used in analysis | Missing / excluded | Numerator | Denominator | Rule |
|---|---|---|---|---|---|---|---|---|
| events | NA | Control | 2 | 1 | 1 | 1 | 1.2 | Accumulated person-time; rate per 1000 |
| events | NA | Treatment | 2 | 2 | 0 | 3 | 1.8 | Accumulated person-time; rate per 1000 |
The audit records both valid event-time records and the person-time denominator.
Cross-tabs, comparisons, and correlations
crosstabs() uses complete row-column pairs: a row is
excluded when either cross-tabulated variable is missing. Its margins,
percentages, test, and 2×2 measures therefore refer to that same
complete-pair population.
cross <- crosstabs(dat, row = arm, col = smoker)
denominators_stats(cross)| Denominator audit | ||||||||
| Variable | Level | Group | Eligible observations | Used in analysis | Missing / excluded | Numerator | Denominator | Rule |
|---|---|---|---|---|---|---|---|---|
| smoker | Yes | Treatment | 3 | 2 | 1 | 1 | 2 | Complete row/outcome observations; event level = Yes |
| smoker | Yes | Control | 2 | 2 | 0 | 1 | 2 | Complete row/outcome observations; event level = Yes |
For independent compare_groups(), an observation
requires both variable and group. For paired
analysis, an identifier must be observed once in each group with the
required outcome; the denominator becomes the number of complete,
aligned pairs.
comparison <- compare_groups(dat, variable = age, group = arm)
denominators_stats(comparison)| Denominator audit | ||||||||
| Variable | Level | Group | Eligible observations | Used in analysis | Missing / excluded | Numerator | Denominator | Rule |
|---|---|---|---|---|---|---|---|---|
| age | NA | arm = Control | 2 | 1 | 1 | NA | 1 | Non-missing outcome observations within group |
| age | NA | arm = Treatment | 3 | 3 | 0 | NA | 3 | Non-missing outcome observations within group |
correlation() similarly uses complete pairs: both
variables must be observed in the same row.
cor_dat <- data.frame(
age = c(34, 41, 45, 49, 53, 57, 62, 68),
follow_up = c(12, 11, NA, 9, 8, 7, 6, 5)
)
to_flextable(correlation(cor_dat, x = age, y = follow_up))Variables |
n |
Correlation (95% CI) |
p-value |
|---|---|---|---|
age and follow_up |
7 |
-1.00 (-1.00 to -0.98) |
<0.01 |
Pearson when both absolute sample skewness values are below 1; otherwise Spearman in auto mode. | |||
Inspect a scatterplot: Pearson requires an approximately linear relationship; Spearman requires a monotonic relationship. | |||
Correlation inference assumes independent observation pairs and no dominating influential observations. | |||
Analysis used 7 complete finite pairs; 1 pairs were excluded. | |||
Distribution assessment
assess_distribution() keeps data-quality information
visible. It reports n, Missing, and
Non-finite; skewness and Shapiro-Wilk use usable finite
values only. Missing or non-finite values are flagged in the guidance
rather than being interpreted as evidence of non-normality.
dist_dat <- data.frame(
arm = factor(rep(c("Control", "Treatment"), each = 8)),
age = c(34, 39, 44, 48, 52, 57, NA, 63,
36, 41, 46, 51, 56, 61, 66, Inf)
)
to_flextable(assess_distribution(dist_dat, vars = age, by = arm))Variable |
Group |
n |
Missing |
Non-finite |
Skewness |
Shape |
Suggested presentation |
Shapiro p |
|---|---|---|---|---|---|---|---|---|
age |
Control |
7 |
1 |
0 |
0.05 |
Little/no asymmetry |
Mean (SD) reasonable |
0.990 |
Treatment |
7 |
0 |
1 |
0.00 |
Little/no asymmetry |
0.949 |
||
Shape categories use absolute sample skewness: little/no asymmetry < 0.50; some asymmetry 0.50 to < 1.00; marked skew >= 1.00. They are descriptive guidance, not formal classifications. | ||||||||
Shapiro-Wilk is sensitive to sample size. Interpretation should consider skewness, graphical assessment, sample size and subject-matter knowledge. | ||||||||
Suggested summaries are intended for descriptive reporting only and should not be used alone to determine inferential statistical methods. | ||||||||
For grouped data, the suggested presentation applies to all groups of each variable. | ||||||||
For a grouped continuous variable, assess_variance()
also excludes missing and non-finite values from its SD and variance
calculations, while retaining their counts in the output.
to_flextable(assess_variance(dist_dat, vars = age, by = arm))Variable |
Control |
Treatment |
Observed SD ratio |
Observed variance ratio |
Levene p |
Interpretation |
|---|---|---|---|---|---|---|
age |
n = 7; SD = 10.12; variance = 102.48 (1 excluded) |
n = 7; SD = 10.80; variance = 116.67 (1 excluded) |
1.07 |
1.14 |
0.814 |
Descriptive spread shown; interpret it in the context of the study design. Missing or non-finite values are excluded from SD and variance. Some values were excluded. |
SD and variance ratios are the largest group value divided by the smallest group value. They describe observed spread; they are not pass/fail tests. | ||||||
For independent groups, Welch t-tests and Welch ANOVA do not require equal variances. `assess_variance()` does not select an inferential test and does not assess pairing or repeated-measures sphericity. | ||||||
The displayed Levene test is median-centred (Brown-Forsythe). It is supporting information only: its p-value neither proves equal variances nor selects an inferential test. | ||||||
Interpret spread alongside sample size, distributional shape, outliers, missingness, and the study design. | ||||||
Reporting checklist
- Does the displayed percentage use the denominator that answers the question?
- Are missing values shown, suppressed, or described elsewhere?
- Is the analysis population the same for every variable? If not, is this clear?
- For rates, is the denominator person-time rather than participants?
- For paired analysis, have you reported the number of complete pairs?
- For repeated-measures ANOVA, have you reviewed sphericity rather than using a cross-occasion variance test as a substitute?
- Have you reviewed
denominators_stats()where exclusions affect interpretation?
What gtstats does not do automatically
gtstats reports the observed-data calculation. It does not impute missing values, diagnose the missing-data mechanism, apply weighting, or decide whether complete-case analysis is scientifically valid. Prepare the appropriate analysis dataset first when those methods are required, then use gtstats to report the result transparently.