Changelog
Source:NEWS.md
tplyr2 0.2.0
CRAN release: 2026-08-08
First CRAN release.
tplyr2 builds clinical summary tables from a declarative specification. A tplyr_spec() describes what to compute — count, descriptive, shift, and custom analysis layers, with population-based denominators, formatting, sorting, and traceability — and tplyr_build() executes it against data. The spec is pure configuration, so it can be written, serialized to JSON or YAML, reviewed, and re-run independently of any dataset.
tplyr2 is a ground-up successor to the Tplyr package rather than a new version of it. The two have different APIs and are intended to coexist while users migrate; vignette("migration") maps one onto the other.
Versions before 0.2.0 were development releases available only from GitHub. The sections below record what changed since 0.1.0, and are relevant to anyone who installed the package that way.
Breaking changes since 0.1.0
Several settings that were previously accepted and silently ignored now error. Each of these produced a plausible-looking but wrong table, which is the wrong default for a clinical reporting package.
- Unknown
...overrides passed totplyr_build()error instead of being ignored (#73).tplyr_build(spec, adsl, wher = "SAFFL == 'Y'")used to build on unfiltered data; it now errors and lists the valid override names. - Unknown option names passed to
tplyr2_options()error (#74). A misspelledIBMroundingused to set a dead option and leave the whole output package on banker’s rounding. -
result_order_varnaming a statistic the layer does not compute errors instead of falling back to"n", andordering_colsmatching none of the observed column levels errors instead of zeroing every sort key (#78). A partially unmatchedordering_colswarns and sorts on the levels that matched. -
denoms_bymust name the layer’s own grouping variables — the column variables,byvariables, and (for count and shift layers) the target variable (#77). An unrecognized name used to shrink the join key set silently, either multiplying table rows or attaching another group’s denominator. - Unrecognized
missing_countkeys error (#80). -
tplyr_stats_data()now returns the grouping columns plus the requested statistic, as documented, rather than the entire layer frame (#79). Usetplyr_numeric_data()for every statistic. - Count and shift layers now render a percentage with no usable denominator as blank rather than
0(#76), matching what desc layers already did. A genuine zero count against a real denominator still renders0.0%.
New features
- New single-proportion confidence-interval statistic for count layers (#44). Four
f_strkeywords —ci_lower/ci_upper(fromn/total) anddistinct_ci_lower/distinct_ci_upper(fromdistinct_n/distinct_total) — are computed per column-by-target-level cell on the percentage scale, so an incidence CI drops straight into a count-layer format string, e.g.f_str("xx (xx.x%) [xx.x, xx.x]", "distinct_n", "distinct_pct", "distinct_ci_lower", "distinct_ci_upper"). Two newlayer_settings()controls choose the method and coverage:ci_method("clopper_pearson"default / exact, matching SASPROC FREQ EXACTandstats::binom.test(); plus"wilson","wald","agresti_coull","jeffreys") andci_level(default0.95). The bounds are computed lazily (only when a format references a CI keyword) and appear on Total/Missing rows just likepct. The underlying vectorized helper,proportion_ci(), is also exported. -
apply_formats()gainsna,width, andpadarguments (#41).nais a string substituted for cells whose format-group inputs are all NA, used instead of the blank-width fill (na = ""yields a truly empty cell,nchar0;na = "NE"renders"NE"), lettingapply_formats()replace hand-rolled fixed-width formatters for externally row-bound statistics.widthpads each formatted token to a fixed total width (pad = "right"/"left"); when thenasubstitution applies, it wins and the cell is not padded. The defaults (NULL) preserve existing behavior. - New
as_display()helper returning a display-ready frame — therowlabel*,res*, andrdiff*columns only, with the internalord*(androw_id) columns dropped, ready to hand to a table-rendering package (#36). Passlabels = TRUEto rename the result columns to their column-group header labels. - New
n_recordsdescriptive statistic keyword forgroup_desc()— the number of records assessed (non-missing + missing), for tables that report annof records/subjects assessed rather than the non-missing analysis count (#34). The existingn(non-missing) keyword is unchanged. - New
denom_rowsetting for shift layers — emits the per-baseline-group denominator (theshift_denom = "column"denominator) as an integernrow above the shift-to rows, instead of forcing callers to recompute it (#35). The label defaults to"n"(denom_row_label). - New
assoc_test()for count and shift layers — an omnibus association-test column (#37). It runs a caller-supplied function once perbygroup over the raw source-data subset for that group (allcolslevels), so a Fisher’s exact or CMH test can tabulate across the treatment columns, and lands the formatted result as a single trailingpval1column (one value per by-group, on the group’s first row). Attach vialayer_settings(assoc_test = ...). -
assoc_test()gains a pairwise / per-level mode for count layers (#40). Supplyingcomparisons(with an optionalreference, defaulting to the firstcolslevel) compares the reference arm to each named arm and emits onepvalcolumn per comparison, each with a value on every target-level row (likerisk_diff’srdiffcolumns) — the standard AE-by-SOC/PT layout. In this mode the caller-suppliedfnreceives a 2x2 incidence matrixmatrix(c(n_ref, n_cmp, N_ref - n_ref, N_cmp - n_cmp), nrow = 2)(distinct counts/denominators whendistinct_byis set) and returns a scalar p-value, so any test —fisher.test, and beyond — can be used.labelmay be a per-comparison vector; the default is"<reference> vs <comparison>". -
assoc_test()’sfnmay now return a character string, passed through to thepvalcell verbatim (#47);formatapplies only whenfnreturns a numeric. This completes the arbitrary-fndesign — the function that computes the test can also supply the finished display, so conditional p-value conventions (a significance flag like0.031*, a>.99/<.0001ceiling/floor, an"NE"/"N/A"sentinel, trailing-space alignment) all live in thefn. Works in both omnibus and pairwise modes;NA(numeric or character) still renders a blank. Existing numeric-returning fns are unaffected. - Pairwise
assoc_test()now works on nested count layers (#49), where it was previously a no-op. It emits apvalcolumn per comparison with a value on every row of every level — each inner (e.g. preferred-term) row and each outer (e.g. system-organ-class subtotal) row — building each row’s 2x2 from that row’s own distinct counts and thepop_datadenominators (the outer row uses the level’s “any event in that group” subject count). This is the canonical AE-incidence-by-SOC/PT Fisher layout. A newtotal_rowargument toassoc_test()(defaultTRUE) also lands a p-value on the layer’s total (“any event anywhere”) row; settotal_row = FALSEto leave it blank. Missing rows are always blank. Combined with the character-return display (#47), thefncan supply the exact* / >.99 / trailing-space / blankcell text on every nested row. A zero-event arm is handled correctly: its 2x2 denominator is taken frompop_data(subjects at risk), so a sparse or empty reference arm still yields a valid0-vs-ktest on every row instead of blanking the column. -
assoc_test()now works ongroup_desclayers in omnibus mode (#51), giving a continuous-variable comparison across arms — ANOVA, Kruskal-Wallis, a t-test — a native home. Same contract as count/shift:fnreceives the by-group’s raw source subset (allcolslevels) and returns a scalar p (formatted byformat) or a verbatim character string (#47), placed on the by-group’s first statistic row (NA→ blank). A demographics table can now produce its comparison p-values — continuous and categorical characteristics sharing onepvalcolumn — entirely through tplyr2 instead of a hand-rolledaov/kruskal.testside pipeline. Pairwise/per-level mode remains count-layer only; supplyingcomparisonson a desc layer is now a clear error rather than silently ignored. -
assoc_test()’sfnmay now return multiple values rendered into one cell (#60). Whenformatreferences more than one variable,fnreturns a numeric vector of matching length, mapped positionally onto the format — so a procedure that emits a tuple (an odds ratio with its confidence interval, an estimate with a p-value) lands as a single formatted cell, e.g.f_str("xx.xx (xx.xx, xx.xx)", "or", "lo", "hi"). A scalar return with a one-variable format is unchanged; an all-NAreturn or an arity mismatch renders a blank; the character-return passthrough (#47) still wins for a finished display string. - New
shift_denomsetting for shift layers (#18).shift_denom = "column"computes percentages column-wise — out of each shift column group (the “from”/baseline group) within the treatment arm — the standard “% within the from group” shift display, and the header(N=)labels then reflect those per-column-group denominators. The default"total"keeps the arm-total denominator. - New
pct_ltandpct_gtcount-layer settings for the regulatory “less-than / greater-than” percent conventions (#14). A cell with a nonzero count whose percent would display belowpct_ltrenders as"<"+ the threshold (e.g.1 ( <1%)instead of1 ( 0%)); a percent below 100 that would display abovepct_gtrenders as">"+ the threshold (e.g.>99). The comparison is against the rounded display value, so a percent that rounds up to the threshold keeps its number. - New
zero_count_displaycount-layer setting (#14) controlling how cells with a zero count render:"full"(default, unchanged),"count_only"(just the count field, e.g." 0"), or"blank"(empty string). - New
stat_columnslayer setting for count layers (#10). Passing a named list off_str()objects produces one result column per statistic per column group — for example, a distinct-subject “n (%)” column beside a raw event-count “E” column under each treatment arm. Column label attributes follow the pattern"<column group> (N=n) | <stat name>"so renderers can span the group over its stat sub-columns. Works with nested count layers, by variables, total/missing rows, risk difference, cell metadata, JSON/YAML serialization, and ARD conversion. -
tplyr_metaobjects gain an optionalstatisticfield recording which statistic a cell displays (populated forstat_columnslayers). -
missing_countgainsdenom_exclude(#80), which was previously accepted in config and never implemented. Withdenom_exclude = TRUEthe rows folded into the Missing row (NAplus anything inmissing_values) leave the layer’s denominator, so percentages are of the non-missing population. Every keymissing_countaccepts is now documented. - Failures in user-supplied code no longer discard their message (#75). Custom summaries and
assoc_testfunctions still cannot abort a build and still renderNAas a blank cell, but the reasons are now collected and reported as one warning per build, deduplicated and naming the summary or test and the group affected. Previously a partial failure — real numbers everywhere and one blank cell where the expression errored — was indistinguishable from data legitimately missing. Anassoc_testfunction whose return does not match its format’s variable count is reported as the caller bug it is. - Missing and zero denominators are no longer silent (#76). A count with
n > 0against anNAor zero denominator warns, naming the layer and the affected groups, andtplyr_build()warns whenpop_datahas no rows for a column level present in the analysis data.
Bug fixes
A layer whose
whereclause left a column group with no rows emitted fewer result columns than its sibling layers, and those columns were then aligned positionally — putting that layer’s values under the wrong treatment arm. Intplyr_adae, for example, awhere = AESEV == "SEVERE"layer alongside an unfiltered one reported Xanomeline Low Dose’s severe events under the Placebo label. The column-variable level set is now captured from the table’s full data and pinned for every layer, so an empty column group completes with zeros in its own position.A
total_group()combined with acustom_group()on the same column variable double-counted the pooled subjects: the total duplicated the custom group’s copies as well as the originals, so a 254-subject study reportedTotal (N=422)and a sex count of 233 where 143 was correct. Duplicated rows now record which column variable they were created for, so a total group skips copies made on its own variable while still spanning copies made for a different one.A shift layer or a
stats_as_columnsdesc layer combined with a standard layer produced a table whoserescolumns meant different things in different row blocks, keeping only the first layer’s column labels. Those combinations are now rejected byvalidate_spec()with a message pointing at separate specs. (This replaces a silently mislabeled table, so a spec that “worked” before may now error — it was not producing a correct table.)tplyr_meta_subset()treated an empty filter set as “nothing matches” and returned zero rows. A cell can legitimately have no filters — atotal_group()column crossed with a total row, or with a desc statistic in a layer that has nobyvariable — and those cells describe the whole dataset. It now returns all rows.Cell metadata dropped the
byfilter when abylevel was an empty string, and aborted the entire metadata build with “missing value where TRUE/FALSE needed” when abylevel wasNA. Both are real levels: an empty string now filters on""andNAfilters withis.na(). A nested layer’s structurally absent inner label still contributes no filter, as before.Cell metadata compared
byvalues against the trimmed row label, so abyvariable whose values carry leading or trailing whitespace (common in SAS-derived character data) produced filters matching zero rows. Filters now use the untrimmed value.generate_row_ids()silently produced duplicate IDs when row labels had been blanked byapply_row_masks()or when a target level collided with atotal_row_label, so metadata lookups resolved to the wrong cell. It now warns.A missing-subjects row built without
distinct_bycarried filters resolving to the subjects that do appear — the exact complement of what the cell counts. Row-level missing-subjects counting is a population-minus-target difference that no filter set can express, so no metadata is emitted for it and the build warns.Cell metadata for a
stats_as_columnsdesc layer resolved to nothing. That layout labels its result columns"<arm> (N=n) | <statistic>", the same grammar count-layerstat_columnsuses, but the trailing statistic segment was stripped only for count layers — so every filter readTRT == "A | n"and matched zero rows.tplyr_meta_subset()now returns the correct source rows.A count layer’s total row rendered a blank instead of
0for any column group with no rows in the analysis data, while the category rows above it correctly showed0(#66).nis counted from the raw analysis rows, so an empty column group never appears there; the denominator join now brings it in from the completed category counts and zero-fills it. Nothing errored or warned, so the value silently vanished from a delivered table.A count layer’s total row counted
nby summing the category rows whiledistinct_ncounted from the raw data, so the two disagreed in the same cell andtotal_row_count_missingshad no effect onn. Category rows exclude NA target values (data completion drops them) and any level folded into the Missing row, so the sum silently omitted them.nis now counted from the raw rows with the same missing handling asdistinct_n, makingn,distinct_n, and the cell’s metadata agree. A total row over data with missing target values will change: with the defaulttotal_row_count_missings = TRUEit now includes them, as documented.tplyr_build(metadata = TRUE)now warns when astats_as_columnsdesc layer has nobyvariable. That layout names its result columns after the statistics rather thanres1,res2, …, and cell metadata is keyed onrescolumns, so none was produced — previously without any indication.missing_count’smissing_valuesno longer double-counts. Values named there are folded into the Missing row, but they also kept their own category row, so the same records were counted twice and the column summed past 100%. They are now removed from the category rows, matching Tplyr v1’sset_missing_count()and the exclusiontplyr_meta()already assumed. On a nested count layer, naming an outer-level value removes its inner rows along with it. This changes the numbers in any table that usedmissing_values— previously those tables were wrong.f_str(empty = )now honors its unnamed form. Onlyc(.overall = "...")was implemented; an unnamedempty = "NA"was silently ignored. It now fills each NA format group in place, right-justified to that group’s field width, sof_str("xx (xxx)", "n", "pct", empty = "NA")renders"NA ( NA)"and a partially missing cell keeps its alignment. This restores v1 parity;.overallis unchanged and still replaces the whole cell only when every group is NA.Shift layers now compute the single-proportion confidence-interval keywords (
ci_lower,ci_upper,distinct_ci_lower,distinct_ci_upper). They were accepted by validation but never computed, so a shift format referencing one rendered an empty field with no warning. Bounds follow whichever denominatorshift_denomselects.as_display()no longer discards the result columns of astats_as_columnsdesc layer built without abyvariable. That layout names its columns after the statistics rather thanres1,res2, …, and the whitelist dropped them, returning row labels alone. It now removes the internalord_layer_*/row_idhelpers and keeps everything else.stats_as_columnswith nobyvariable now orders its columns by format-string order rather than alphabetically by statistic label.collect_precision()now warns whenprecision_datadoes not cover everyprecision_bygroup present in the data (those cells render blank), and whenprecision_dataomits theprecision_bycolumns entirely (its widths are applied to every group). Both were silent.f_str()now warns when a format group requests parenthesis hugging (X/A) but has no literal text in front of it — there is nothing to hug, and the number is left-justified with trailing spaces instead.Count-layer row ordering now honors the sort settings it advertised (#57).
order_count_method = "bycount"actually sorts by descending count (it previously fell back to the default order);ordering_colsselects which column’s count drives that sort, andresult_order_varwhich statistic;outer_sort_position = "desc"reverses a nested layer’s outer level. Any explicitorder_count_method(and the default) now also keepsby-groups blocked instead of interleaving them, and the default respects the target variable’s factor levels (previously it ordered the target alphabetically even when it was a factor). The target sort key is threaded through the cast so all methods compose correctly with by-groups and special (total/missing) rows.order_count_method = "bycount"now also reaches the inner level of a nested count layer (#64), sorting (e.g.) preferred terms by descending count within each system organ class – the AE-by-SOC/PT convention – withresult_order_var/ordering_colshonored. Previously it only affected single-level layers. The outer level keeps its own order (controlled byouter_sort_position), so the useful “outer alphabetical, inner by descending count” layout comes for free.risk_diffon a nested count layer now errors instead of silently emitting an all-blank column (#58). Risk difference is computed only on single-level count layers; on a nested SOC/PT layer the setting previously produced an emptyrdiffcolumn with no warning. The error points to pairwiseassoc_test(), which does compute a per-level comparison on nested layers.group_shift(denom_row = TRUE)no longer renders the literal string"NA"for a baseline (shift-column) group that is absent within abygroup (#55); an absent group’s denominator is zero, so the cell now reads0(consistent withzero_count_displayon the shift-to rows). A newdenom_row_formatsetting also lets the denominator row carry its ownf_strwidth independent of then_countscells (e.g.denom_row_format = f_str("xx", "n")for a plain narrow integer) instead of inheriting their padding.Omnibus
assoc_test()no longer letstotal_group()/custom_group()duplicate rows leak into thefn’s.data(#53). Those rows are a display construct for the count columns; including them double-counted every subject and silently returned a wrong p-value (no error, no warning). The synthetic rows — and their now-unused factor levels (e.g. a phantom"Total"level that madechisq.test()returnNaN) — are dropped beforefnruns, so it sees only the real observations.Omnibus
assoc_test()now places its p-value on the layer’s first display row, not the arbitrary pre-sort (dcast) row (#54). The value was written before theord*reorder (e.g.order_count_method = "byfactor"), so it could strand on the wrong category (landing on65-80instead of<65, etc.); placement is now derived from the ordering columns, per by-group.group_count()missing_countnow always emits the Missing row when set, zero-filling every column/by group that has no missing values, so the row reads0 ( 0%)throughout instead of being dropped (when the total missing count is zero) or leaving empty cells (when only some columns have missings) (#33). Matches classic Tplyrset_missing_count().group_shift(shift_denom = "column")with abyvariable now scopes the column (from-group) denominator within each by-group instead of pooling it across them (#28). A shift-by-visit table now gets per-visit percentages. With abyvariable the header(N=)reflects the arm total (the per-column-group denominator varies by by-group, so no single header N can represent it); the no-bybehavior (from-group N in the header) is unchanged.group_shift()now honors thezero_count_display,pct_lt, andpct_gtlayer settings, applying them the same waygroup_count()does (#31). Previously a shift layer ignored them (e.g. a zero cell always rendered as0 ( 0%)even withzero_count_display = "count_only").Descriptive statistics that round to negative zero now display as
0.0instead of-0.0, matching base Rformat()(#29).Result and risk-difference columns are now ordered by their numeric suffix when layers are combined and when metadata is built. Previously tables with more than 9 result columns sorted them lexicographically (
res10beforeres2), scrambling column order.Count layers now order their
res*columns by thecolsvariable’s factor levels, matchinggroup_desc()(#13). Previously count layers ordered result columns alphabetically by thecolsvalue, so a spec mixing count and desc layers (or any renderer assumingres1is the firstcolslevel) could get inconsistent column order. Shift layers likewise order their column dimension by the shift variable’s factor levels.group_count()now orders itsby-group rows by thebyvariable’s factor levels (then a VARN companion, then alphabetically) instead of always alphabetically (#24). Previously a factorbysuch as visits came out mis-ordered (e.g.Week 12beforeWeek 2), matchinggroup_shift()andgroup_desc().Fixed
group_count()total/missing rows with abyvariable (#24): each by-group’sTotal(orMissing) row is now labelled with its by-group value instead of a blank, and special rows now sort after the normal rows within each group (previously e.g.Totalwas interleaved alphabetically among the target values, and with abyvariable the row label was dropped entirely).group_count(order_count_method = "byfactor")now orders category rows by the target variable’s factor levels instead of alphabetically (#16). The target column is coerced to character while counts are built, so the level order is now recovered from the source data (compute_var_order()). Nested count layers likewise order their outer and inner categories by factor levels (previously they fell back to the dcast’s alphabetical row order).group_desc(stats_as_columns = TRUE)combined with abyvariable no longer drops the by-groups and returns only the last group’s statistics (#20). It now keeps the by-groups as rows and produces one result column per treatment x statistic (labelled"<arm> | <stat>"). Behavior without abyvariable (treatment groups as rows, statistics as columns) is unchanged.group_desc()now orders itsby-group rows by thebyvariable’s factor levels (then a VARN companion, then alphabetically) instead of always alphabetically (#20). Previously a factorbysuch as visits came out mis-ordered (e.g.Week 12beforeWeek 2); this applies to both the standard stats-as-rows output andstats_as_columns = TRUE.Risk difference and pairwise
assoc_testcolumns came out entirely blank whenbyled with a string label (#72). Both merge functions assumed thebydata variables occupied the firstrowlabelcolumns, so withby = c("Age Group", "SEX")the join keyed the constant-label column againstSEXvalues and matched nothing. They now share one helper that offsets past the label columns.A
whereclause longer than about 60 characters could not be read back from a spec file (#70).rlang::expr_deparse()wraps at that width, and the resulting multi-element array was not somethingparse_expr()could accept — so any realistic multi-condition ADaM filter broke the spec file in both formats. Files written by the old code still read.precision_capwas silently dropped on a spec-file round trip in both formats (#69). Both writers dropped the names of the named numeric vector, andapply_precision_cap()dispatches on those names, so a round-tripped spec rendered different numbers than the original with no error or warning.apply_precision_cap()now also warns when given a cap carrying neither anintnor adecname.Multi-element character settings (
denoms_by,keep_levels,precision_by, and friends) deserialized as lists from JSON and broke the build (#68); YAML was unaffected only because it auto-simplifies. Adenoms_bylist made data.table’sby=error out. One field-type table now restores every plain setting’s vector type, and a test asserts that nolayer_settings()parameter is missing from it.A
denom_whereexpression read back from a spec file was evaluated as a call rather than stored, erroring on the first variable name it contained.tplyr_from_ard()re-defaulted desc-layer format strings instead of sharingget_desc_formats()with the build path (#71), reconstructing a 1-row table at a different width than the 6-row build. Format-string rows also keptdcast’s alphabetical order rather than their declared order, which affected ARD-reconstructed desc layers and analyze layers alike.Unknown keys in a spec file are no longer dropped silently (#81). Hand-editing spec files is supported, and a typo’d key such as
total_rowsbuilt a table without the requested behavior and said nothing. Unknown layer settings and unknown top-level spec keys now warn, naming the layer.A
pop_datathat renames the column variable —pop_data(c(TRTA = "TRT01P"))— skippedtotal_group()andcustom_group()on the population side, because the rename ran after those were applied. The Total column had no population rows, so every nonzero count in it displayed0.0%.compute_risk_diff()computed the plain difference inside the sametryCatch()as the confidence interval, so aprop.test()failure blanked the difference along with the CI even though the difference needs no test (#76). It also now pre-validates that counts do not exceed their denominators instead of lettingprop.test()’s error be swallowed into an all-NArow.
Documentation
-
The formatting vignettes have been reorganized. The two previous articles, “General String Formatting” and “Advanced Descriptive Statistics Formatting”, are replaced by three organized around what the user is trying to do rather than by layer type:
-
vignette("format_strings")— the format string grammar, where format strings attach per layer type, the complete statistic keyword reference for count/shift/desc layers, rounding, missing-value handling, and standaloneapply_formats(). -
vignette("precision_alignment")— auto-precision (a/A,+N,precision_on,precision_by,precision_cap,precision_data) and parenthesis hugging (X/A). -
vignette("display_conventions")— the display rules imposed by shells and SAPs:pct_lt/pct_gt,zero_count_display,stat_columnsandstats_as_columns,keep_levels,missing_count, shift denominators, and indenting/wrapping nested terms.
emptyat similar depth while disagreeing about what hugging does, and their four statistic keyword lists contradicted each other and the source. The following were previously documented in no vignette at all and are now covered:pct_lt,pct_gt,zero_count_display,keep_levels,missing_count,total_row_count_missings,stats_as_columns,shift_denom,denom_row,denom_row_label,denom_row_format, the desc-layertotal/pctkeywords, andapply_formats()’sna/width/pad/lt/gtarguments. -
- Corrected the description of parenthesis hugging. tplyr2 moves a hugged group’s leading spaces to just inside the trailing literal’s last character (
12 (34.5% )); Tplyr v1 moved them to the left of the opening delimiter (12 (34.5%)). Two vignettes described v1’s behavior. The difference is now stated explicitly for anyone reconciling output against v1. - Corrected the description of
shift_denom = "column": it denominates by the result column group (arm × post-baseline category), so each result column sums to 100%.vignette("shift")now shows all three shift denominators, including how to get row-wise percentages withdenoms_by. - Documented that auto-precision (
a/A) resolves against the data only ingroup_desc()layers; elsewhere it degrades to a fixed width equal to the number of characters written. - Documented several other scope limits that were previously unstated: the four confidence-interval keywords are count-layer only (a shift layer accepts them and renders them empty); the desc-layer
totalkeyword is a record count, sopctis a share of the arm only on one-row-per-subject data;keep_levelsfilters after the denominators are computed, so the kept percentages do not re-base;precision_datavalidates onlymax_int/max_dec, rendering a blank cell for any group it fails to cover;pct_lt/pct_gtandzero_count_displaytarget the first matching format group; andstr_indent_wrap()charges an existing indent againstwidthtwice. - Documented that literal text in a format string cannot contain
x,X,a, orA— those characters are always parsed as format groups, so a template like"xx days"silently gains a second group. -
vignette("sort")now coversorder_count_method = "bycount"on nested count layers, including that it sorts the inner level only and thatouter_sort_positionreverses the outer order rather than ranking it by count. -
vignette("post_processing")now coversas_display(),collapse_row_labels(nest = TRUE), and theapply_formats()na/widtharguments, and points at the declarativepct_lt/zero_count_displaysettings beforeapply_conditional_format(). -
vignette("riskdiff")now states thatrisk_differrors on nested count layers and points to pairwiseassoc_test(). - Fixed the IBM-rounding example in
vignette("options"), which showed two identical tables under captions promising a difference. - Fixed a broken
vignette("serialize")cross-reference invignette("ard"), and expanded the vignette indexes in the README andvignette("tplyr2"), which listed 10 and 8 of the 19 articles respectively. -
print()on anf_strobject no longer runs its fields together on one line. - New vignette “Comparative Statistics and Binding External Results” (
vignette("binding-statistics")) — how to attach cross-arm comparisons (assoc_test(),risk_diff, single-proportion CIs) and how to bind externally-computed model results (MMRM/ANCOVA/Cox/logistic p-values, LS-means, CIs) onto an assembled table viaapply_formats()andas_display(), plus wheregroup_analyze()fits. - Clarified in
?layer_settingsand the denominators vignette thatdenoms_byreplaces (does not augment) the defaultcols-based denominator grouping, so you must include thecolsvariable(s) to get per-column denominators (#19).