Skip to content

Add add_hierarchical_zero_rows() for unobserved hierarchical levels - #609

Open
Melkiades wants to merge 5 commits into
pharmaverse:mainfrom
Melkiades:602_hierarchical_zero_rows
Open

Add add_hierarchical_zero_rows() for unobserved hierarchical levels#609
Melkiades wants to merge 5 commits into
pharmaverse:mainfrom
Melkiades:602_hierarchical_zero_rows

Conversation

@Melkiades

Copy link
Copy Markdown
Contributor

Adds add_hierarchical_zero_rows(), an ARD-level helper that appends zero-count rows for unobserved hierarchical levels using a mapping of the expected level universe.

A single mapping argument (named list or two-column data.frame) covers:

  • an unobserved top-level category (e.g. an SOC with no events),
  • the children of a missing parent,
  • an unobserved child under an observed parent.

Preserves the by structure and carries denominators over so percentages stay correct (p = 0, no NaN).

closes #602

Stacked hierarchical ARDs include only observed levels because the
tabulation routes through the strata (observed-only) branch, so predefined
categories such as SMQ/CQ baskets, SOCs, and preferred terms disappear
instead of showing a zero count.

add_hierarchical_zero_rows() appends zero-count rows for unobserved levels
using a mapping of the expected level universe. A single mapping argument
(named list or data.frame) covers both an unobserved top-level category and
an unobserved child of an observed parent, and preserves the by structure
and denominators so percentages remain correct.
Comment thread R/add_hierarchical_zero_rows.R Outdated
#' @param variables ([`tidy-select`][dplyr::dplyr_tidy_select])\cr
#' the hierarchical variables used to create `x`, in the same order. The first
#' variable is the top level; the second, when present, is the nested child.
#' @param mapping (named `list` or `data.frame`)\cr

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I was thinking a bit more about this. Would the named list require positional matching to the variables added in ard_hierarcihcal(variables, include)? If so, that is probably not the best, and providing a data frame is more robust.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we have a way to handle a situation like:

ard <- ard_heirarchical(variables = c(AESOC, AEPT), include = AEPT)

add_zero_rows(ard, ....) # how do we know to only add the AEPT zeros and not the AESOC?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Both of these were from before I reworked the API, and I think the new version resolves them.

It's now driven by variables instead of the mapping, you name which levels or level to complete, and it reads the expected levels straight from the stored factor levels(). So your include = AEPT case works directly:

ard <- ard_stack_hierarchical(adae, variables = c(AESOC, AEPT), include = AEPT, ...)

# completes only PT — AESOC is left untouched
add_hierarchical_unobserved_levels(ard, variables = AEPT) # TODO: find a good name ahah

Nothing is positional: variables is tidy-selected by name against the ARD, and the (now optional) mapping list is keyed by parent-level value, not variable order. mapping is only needed for the one thing factor levels can't express: children under a parent that itself never occurs.

So for the common cases there's no data-frame requirement. factor levels cover them. If needed you can add a data.frame too. Do you still want the data.frame form as the primary documented input, or is factor-levels-first fine with you?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I like more the mapping list as it looks intuitive to me ^^

@ddsjoberg

Copy link
Copy Markdown
Collaborator

Preserves the by structure and carries denominators over so percentages stay correct (p = 0, no NaN).

What do you mean by percentages stay correct? 0 / 0 != 0, but it is undefined! You already have functinality in crane to turn a 0 (NaN%) into 0

Read the expected level universe from the factor levels the ARD already
carries, so top-level and nested completion need only `variables`. `mapping`
becomes optional and additive, used only for children of an unobserved parent
or a bespoke universe that factor levels cannot express.
Comment thread R/add_hierarchical_zero_rows.R Outdated
Comment thread R/add_hierarchical_zero_rows.R Outdated
Comment thread R/add_hierarchical_zero_rows.R Outdated
Comment thread R/add_hierarchical_zero_rows.R Outdated
Rename the function (and its file/tests/docs) to
add_hierarchical_unobserved_levels(), matching the intent of adding
unobserved factor levels rather than raw zero-rows.

Drop the user-facing `statistic` argument; the count-style stats are zeroed
internally while denominators carry over, so callers no longer manage that
detail. Rewrite the documentation to lead with the single-variable case and
cross-link gtsummary::tbl_hierarchical() so users can discover it.
@Melkiades
Melkiades marked this pull request as ready for review August 25, 2026 13:34
A never-observed level has no one at risk, so its proportion is 0 / 0 --
undefined, not zero. Set only the counts (n, n_cum) to zero and leave
p/p_cum as NaN, letting the display layer recode them rather than asserting
zero in the ARD.
@Melkiades

Copy link
Copy Markdown
Contributor Author

Preserves the by structure and carries denominators over so percentages stay correct (p = 0, no NaN).

What do you mean by percentages stay correct? 0 / 0 != 0, but it is undefined! You already have functinality in crane to turn a 0 (NaN%) into 0

I was mapping it to 0 which is wrong. now for an added unobserved level the proportion is 0 / 0, which is undefined ,so I leave p/p_cum as NaN rather than asserting 0 in the ARD. The "show as 0" is a display choice and gets recoded downstream, so the ARD stays correct!!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

2 participants