Skip to content

Bug fixes from code review; closes #4, #8; see #25 - #26

Open
tgerke wants to merge 18 commits into
stopsack:mainfrom
tgerke:review-2026-09
Open

tgerke wants to merge 18 commits into
stopsack:mainfrom
tgerke:review-2026-09

Conversation

@tgerke

@tgerke tgerke commented Sep 18, 2026

Copy link
Copy Markdown
Contributor

Fixes for everything in #25, one commit per finding. Each fix has a test that failed before the change. The suite went from 19 test_that() blocks and 128 expectations on main to 35 and 197, with logbin and addreg installed so nothing is skipped. One exception to "one commit per finding": #8 and the swallowed-error fix share a commit, because they touch the same lines.

R CMD check --as-cran on the built tarball: 0 errors, 0 warnings, 1 note. The note is CRAN incoming feasibility: the development version number, and the PubMed links in the vignettes (HTTP 203, "please use DOIs"). main gives the same URL complaint, there as a warning because 0.4.3 is already on CRAN.

Five things that need your call

  1. Continuous exposures, RR scale (7d294af). margstd_boot now matches margstd_delta: the log ratio of average risks. That is what the categorical branch does and what most "auto" users already get. It changes margstd_boot results for continuous exposures. If you would rather go the other way, the default changes instead.
  2. Outcome must be 0/1 or logical (890352b). You agreed to this on Edits round 1 #12. It is the rule rifttable::check_outcome() already applies. The cost: a two-level factor outcome used to work under "auto" and now needs recoding.
  3. Native pipe and R (>= 4.1.0) (2efee0a). You objected to this on Edits round 1 #12 because of users on old R. Since then broom, dplyr, purrr, and tidyr all moved to R (>= 4.1), so 3.5.0 has not been installable for a while. The commit is mechanical and sits on its own. Revert it and everything else still applies.
  4. roxygen2 8.1.0 (647fb91). My installed version rewrote RoxygenNote as Config/roxygen2/version and regrouped importFrom(). If you are on 7.3.x, your next document() flips it back.
  5. CI (565fcb9). main.yml is replaced by the r-lib v2 check-standard and test-coverage workflows, generated by usethis and unmodified. The weekly cron is not carried over. Say if you want it back.

What I left alone

The summary.* functions copied from stats::summary.glm(), apart from one line for #4. logbin and addreg stay in Suggests. No new dependencies. New code is base R.

Reverse dependencies

I ran the test suites of rifttable 0.7.2 and gtregression 1.1.0 against this branch and against CRAN 0.4.3. Both pass with either version: 167 expectations in rifttable, 2,208 in gtregression, no failures or skips. R CMD check on both source packages with this branch first on the library path: 0 errors, 0 warnings, 0 notes. I confirmed inside the test process that the branch build was the one loaded.

Weights (#14) are not implemented. They now give an error instead of being ignored.

Closes #4. Closes #8.

…ons only

With missing values in a covariate, coefficients were silently NA. With missing values in the exposure, rows not used for model fitting entered the standardization population, so estimates differed from margstd_delta and from the theta used for BCa intervals.
With missing data, predicted probabilities (complete cases) were shorter than the resampled data, and rbinom() recycled them across misaligned rows. Confidence intervals could exclude the point estimate.
boot::boot.ci(type = "bca") switches to jackknife influence values if R < n and calls the statistic again without the '...' passed to boot::boot(). With the default of 1000 repeats, any data set with more than 1000 rows failed. The statistic is now a closure. Also drops an unused model fit.
summary(), tidy(), and confint() failed for margstd_boot models if the formula contained transformations such as factor(x), I(x / 10), or log(x), because refitting used object$model. Now uses the original data, restricted to observations used for model fitting.
Logical exposures: the reference level was the value in the first row. If that was TRUE, margstd_delta failed (subscript out of bounds), approach = "auto" silently fell back to margstd_boot, and the coefficient changed sign. FALSE is now always the reference, as in glm().

Categorical exposures with 'at': margstd_delta failed if the level requested as reference was the value in the first row without being the reference level of the model. Logical 'at' values are now passed to relevel() as character.
Ordered factors have polynomial contrasts, so the model matrix has no column per level, and margstd_delta failed (subscript out of bounds). approach = "auto" silently fell back to margstd_boot. The model is now refitted with the exposure as an unordered factor; estimates and standard errors are identical to those for an unordered exposure.
…l risks

Changes results. For continuous exposures, margstd_boot averaged individual-level log risk ratios, while margstd_delta (and both approaches for categorical exposures) return the log ratio of average risks. approach = "auto" switched between the two estimands depending on whether the formula had an interaction term. margstd_boot now matches margstd_delta. Risk differences were and are identical.

Two hard-coded test values for margstd_boot had pinned the old estimand; they now equal the margstd_delta values.
… hidden by "auto"

Directly requested models (glm, glm_startp, glm_startd, robpoisson): errors that indicate non-convergence are explained, with the original message retained. Other errors, e.g., from data or formula, are passed on unchanged.

approach = "auto" and "legacy": the final error now includes the errors of the failed marginal standardization attempts. Previously, a misspelled 'variable' only gave 'No model besides the logistic model converged'.
)

summary() for margstd_boot and margstd_delta returned df = c(1, df.residual, number of parameters of the logistic model). print.summary.glm() reports the difference between the third and first element as coefficients "not defined because of singularities". summary() now also states that coefficients are for the exposure only and names the reference level.

Vignette: explain this output, note that breastcancer is part of the package, update the stale example of the first summary line.
With an outcome coded 1/2, approach = "robpoisson" fitted a Poisson model on these values and returned wrong estimates, with only a message about implausible predicted probabilities. Factor outcomes failed with unrelated errors for robpoisson and duplicate. Both now give one clear error for all approaches.

Behavior change: a two-level factor outcome used to work with approach = "auto" and now needs recoding. Same requirement as in rifttable::check_outcome(). Follows up on the review of stopsack#12.
Arguments in '...' only reach glm(), logbin(), and addreg(). approach = "robpoisson" and "duplicate" dropped them without notice, so that a model requested with weights or a subset was fitted unweighted on all data. Other approaches failed with unrelated errors. All approaches now stop with one clear error. Weights themselves remain a feature request (stopsack#14).
No change to roxygen comments. roxygen2 8.x records its version in Config/roxygen2/version instead of RoxygenNote, groups importFrom() directives, and lists the maintainer among the authors in the package help page.
…osed

A single missing outcome value made all estimates NA, and a stratum without unexposed observations made the estimate for the affected exposure level NA. Observations with missing values are now excluded, as in regression models, and strata without unexposed have zero weight.

New tests compare stratified estimates and standard errors for RR and RD to a direct implementation of the Greenland-Robins formulas. The existing estimators were correct.
…), docs

- tidy.risks(): the error handler for approach = "all" called tibble(), which is not imported; now tibble::tibble()
- riskdiff(approach = "all"): model 3 is named glm_startp, as for riskratio(), not glm_start
- is.risks() had a help page but was not exported
- print.summary.risks(): remove an assignment that was overwritten right away
- Help page of riskratio(): typos, glm_start is now glm_startp, which approaches use '...'
- Vignette on models: default approach as of 0.4.0, models of approach = "all" by name instead of a stale count, what happens if approach = "glm" does not converge
Mechanical replacement of %>% by |> in R/ (65 uses) and vignettes (5). No use relied on magrittr-only features. Drops importFrom(dplyr, "%>%").

R >= 4.1.0 does not raise the effective requirement: the imports broom, dplyr, purrr, and tidyr already depend on R >= 4.1. This commit is independent of the bug fixes on this branch and can be reverted on its own.
The re-knit output shows the summary() changes for stopsack#4. It also no longer has the deviance residuals that R >= 4.3 stopped printing.
Generated with usethis::use_github_action(), unmodified. They replace main.yml, which used actions/checkout@v2 and r-lib/actions at HEAD. Checks now also cover R-devel and the previous R release on Linux.

Not carried over: the weekly cron job. README badges point to the new workflows.
This was referenced Sep 18, 2026

This branch has not been deployed

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Convergence errors could be described Undefined coefs

1 participant