The initial answer from @Daniel put me on the right track. However, the new level must be given a name, and editing its label afterwards is a good idea.
Here's the code in full. The new lines are the two that use the forcats
function-
# Define the data:-
df <- data.frame(pat = c(1:90))
df$by <- ifelse(df$pat < 30, "A", "B")
df$lvl <- ifelse(df$pat < 30, NA, ifelse(df$pat < 60, "Red", "Blue"))
# Convert the NA level to something that will predictably appear at the
# bottom of an alphabetical list, then edit its label to something 'legible'
df$lvl_f <- forcats::fct_na_value_to_level(df$lvl, level = 'zz_Missing')
df$lvl_f <- forcats::fct_recode(df$lvl_f, Missing = "zz_Missing")
# Output in GTSummary
df |> select(by, lvl_f) |>
tbl_summary(by = by,
percent = "column",
type = list(all_continuous() ~ "continuous2"),
missing = "ifany",
missing_text = "Missing",
missing_stat = "{N_miss} ({p_miss})"
) |>
modify_post_fmt_fun(
fmt_fun = ~ifelse(. == "0 (NA%)", "0", .),
columns = all_categorical()
) |>
add_overall(last = TRUE)
And here's the updated output, with corrected percentage. Thanks @Daniel (for the answer, and a great package in GTSummary!).