Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
64 commits
Select commit Hold shift + click to select a range
b864446
add failing tests for refusing a bare continuous conditional mean
malcolmbarrett Sep 16, 2026
dc18dec
refuse a bare continuous conditional mean in the modified score routes
malcolmbarrett Sep 16, 2026
72fb640
add failing tests for modified scores in the data-frame weight route
malcolmbarrett Sep 16, 2026
c99d241
keep modified score records in the data-frame weight route and refuse…
malcolmbarrett Sep 16, 2026
8ed16e9
add failing tests for trimming a dose model on the density scale
malcolmbarrett Sep 16, 2026
34a2cd5
trim a dose model on the scale of its conditional density
malcolmbarrett Sep 16, 2026
1afcd7f
add failing tests for the dose-aware refit and weights from a density…
malcolmbarrett Sep 17, 2026
7ec5dae
refit a dose model on the kept rows and build weights from its trim r…
malcolmbarrett Sep 17, 2026
007e674
add failing tests for the psw truncation record
malcolmbarrett Sep 17, 2026
4cfa348
add a truncation record to propensity score weights
malcolmbarrett Sep 17, 2026
16aeb04
add failing tests for wt_trunc()
malcolmbarrett Sep 17, 2026
a234747
add wt_trunc() to bound extreme weights
malcolmbarrett Sep 17, 2026
afa76a5
add failing tests for ipw() refusing truncated weights
malcolmbarrett Sep 17, 2026
3baa216
refuse truncated weights in ipw()
malcolmbarrett Sep 17, 2026
281f5f5
add failing tests for the truncation sensitivity grid
malcolmbarrett Sep 17, 2026
b1dd098
add wt_trunc_sensitivity() to tabulate weights over a grid of bounds
malcolmbarrett Sep 17, 2026
6b01ec6
add failing tests for ps_trunc(method = "adaptive") on binary scores
malcolmbarrett Sep 17, 2026
e0f7a17
add an adaptive bound to ps_trunc() for binary scores
malcolmbarrett Sep 17, 2026
0d8322d
add a failing test for ps_refit() on a two-level multinom fit
malcolmbarrett Sep 17, 2026
b5c3873
refit a two-level multinom through its probability prediction
malcolmbarrett Sep 17, 2026
4861442
add failing tests for trimming a binomial gam
malcolmbarrett Sep 17, 2026
e37106e
drop the dim attribute of a gam's fitted values
malcolmbarrett Sep 17, 2026
cdc1014
add failing tests for ps_refit() with a subset expression
malcolmbarrett Sep 17, 2026
b6906cc
evaluate expressions passed to ps_refit() against the retained data
malcolmbarrett Sep 17, 2026
503807a
refuse a refit whose model does not match the shape of the trimmed sc…
malcolmbarrett Sep 17, 2026
3099185
add failing tests for refitting a trim with a first-level focal level
malcolmbarrett Sep 17, 2026
6507fc9
keep the focal level when refitting a trimmed score
malcolmbarrett Sep 17, 2026
286d299
add failing tests for the ps_trunc(method = "ps") default bounds
malcolmbarrett Sep 17, 2026
dcfb36a
mirror a single supplied bound in ps_trunc(method = "ps")
malcolmbarrett Sep 17, 2026
53afe8f
add failing tests for combining a single modified vector
malcolmbarrett Sep 17, 2026
b85cb92
keep the records of a single modified vector through c()
malcolmbarrett Sep 17, 2026
f5244f6
tidy the trim parameter comparison and the refit data hint
malcolmbarrett Sep 17, 2026
ff5b557
keep records through c() of a single modified psw only
malcolmbarrett Sep 17, 2026
2e84eaa
validate a lone ps_trunc bound and tighten the refit data hint
malcolmbarrett Sep 17, 2026
796704c
add failing tests for modified scores combined under an explicit prot…
malcolmbarrett Sep 17, 2026
af9ac5b
drop a score record restored against an explicit prototype
malcolmbarrett Sep 17, 2026
ad4623b
add failing tests for keeping the calibration record on weights
malcolmbarrett Sep 17, 2026
304d183
keep the calibration record through subsetting and combining
malcolmbarrett Sep 17, 2026
5c513c0
add failing tests for grouped mutate and unique on modified scores
malcolmbarrett Sep 17, 2026
79db8fd
drop score records silently and keep them consistent through unique()
malcolmbarrett Sep 17, 2026
e628130
add failing tests for unique() on modified score matrices
malcolmbarrett Sep 17, 2026
4c6109c
keep unique() on score matrices row-wise
malcolmbarrett Sep 17, 2026
499e451
document wt_trunc(), density-scale trimming, and when to use each
malcolmbarrett Sep 17, 2026
91b4c4a
test unique() on one-row and empty score matrices
malcolmbarrett Sep 17, 2026
c3aeca4
add failing tests for reordering weights with position records
malcolmbarrett Sep 17, 2026
e99a23c
reindex position records when weights are reordered
malcolmbarrett Sep 17, 2026
0e2965d
speed up record reindexing, dose trimming and wt_trunc()
malcolmbarrett Sep 17, 2026
a0a9fe5
keep the non-positional part of a weight record when its positions ar…
malcolmbarrett Sep 17, 2026
69b7e69
drop weight record positions when other data is cast onto them
malcolmbarrett Sep 17, 2026
caa0f6f
add failing tests for slicing modified scores at the same length
malcolmbarrett Sep 17, 2026
0928aed
drop score positions when a slice cannot place them
malcolmbarrett Sep 17, 2026
36cdddf
add failing tests for reordering a per-observation stabilization score
malcolmbarrett Sep 17, 2026
500246e
reorder the per-observation stabilization score with the weights
malcolmbarrett Sep 17, 2026
1d58b56
add failing tests for record settings through weight combines
malcolmbarrett Sep 17, 2026
726ee5c
compare and keep record settings when weights are combined
malcolmbarrett Sep 17, 2026
1bd2760
tidy score handling on casts, assignment and is.na<-
malcolmbarrett Sep 17, 2026
672467b
add failing tests for converting modified score matrices to data frames
malcolmbarrett Sep 17, 2026
b16c9ea
keep the record when a modified score matrix becomes a data frame
malcolmbarrett Sep 17, 2026
db1867a
add a failing test for refusing a refit of truncated scores
malcolmbarrett Sep 17, 2026
15a2b91
explain why ps_refit() refuses truncated scores
malcolmbarrett Sep 17, 2026
d28fe58
add failing tests for assigning weights with different records
malcolmbarrett Sep 17, 2026
dd53028
refuse assigning weights whose records differ
malcolmbarrett Sep 17, 2026
858a9e8
refuse refitting one column of a trimmed score matrix
malcolmbarrett Sep 17, 2026
a05c334
explain refused record casts and document assignment of missing values
malcolmbarrett Sep 17, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions NAMESPACE
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,22 @@ S3method("[",ps_trim)
S3method("[",ps_trim_matrix)
S3method("[",ps_trunc)
S3method("[",ps_trunc_matrix)
S3method("[",psw)
S3method("is.na<-",ps_trim)
S3method("is.na<-",ps_trunc)
S3method("is.na<-",psw)
S3method(Summary,ps_calib)
S3method(Summary,ps_trim)
S3method(Summary,ps_trunc)
S3method(anyDuplicated,ps_trim)
S3method(anyDuplicated,ps_trunc)
S3method(as.data.frame,ipw_diagnostic_se)
S3method(as.data.frame,ps_trim_matrix)
S3method(as.data.frame,ps_trunc_matrix)
S3method(as.double,ps_calib)
S3method(c,ps_trim)
S3method(c,ps_trunc)
S3method(c,psw)
S3method(causalgenerics::ipw,default)
S3method(causalgenerics::ipw,glm)
S3method(causalgenerics::ipw,joint_wt_models)
Expand Down Expand Up @@ -74,6 +83,10 @@ S3method(is_unit_truncated,default)
S3method(is_unit_truncated,ps_trunc)
S3method(is_unit_truncated,ps_trunc_matrix)
S3method(is_unit_truncated,psw)
S3method(is_unit_wt_truncated,default)
S3method(is_unit_wt_truncated,psw)
S3method(is_wt_truncated,default)
S3method(is_wt_truncated,psw)
S3method(max,ps_calib)
S3method(max,ps_trim)
S3method(max,ps_trunc)
Expand All @@ -88,6 +101,8 @@ S3method(model_fits_levels,multinom)
S3method(model_levels,multinom)
S3method(na.omit,ps_trim)
S3method(obj_print_footer,psw)
S3method(predict_binary_ps,default)
S3method(predict_binary_ps,multinom)
S3method(print,ipw_diagnostic_se)
S3method(print,propensity_density)
S3method(print,propensity_density_meta)
Expand All @@ -107,12 +122,14 @@ S3method(ps_tilt,numeric)
S3method(ps_trim,data.frame)
S3method(ps_trim,default)
S3method(ps_trim,glm)
S3method(ps_trim,lm)
S3method(ps_trim,matrix)
S3method(ps_trim,multinom)
S3method(ps_trim,ps_trim)
S3method(ps_trunc,data.frame)
S3method(ps_trunc,default)
S3method(ps_trunc,glm)
S3method(ps_trunc,lm)
S3method(ps_trunc,matrix)
S3method(ps_trunc,multinom)
S3method(ps_trunc,ps_trunc)
Expand All @@ -127,6 +144,8 @@ S3method(sort,ps_trim)
S3method(sort,ps_trunc)
S3method(summary,ps_trim)
S3method(summary,ps_trunc)
S3method(tibble::as_tibble,ps_trim_matrix)
S3method(tibble::as_tibble,ps_trunc_matrix)
S3method(unique,ps_trim)
S3method(unique,ps_trunc)
S3method(vec_arith,ps_calib)
Expand Down Expand Up @@ -291,6 +310,12 @@ S3method(wt_entropy,numeric)
S3method(wt_entropy,ps_calib)
S3method(wt_entropy,ps_trim)
S3method(wt_entropy,ps_trunc)
S3method(wt_trunc,default)
S3method(wt_trunc,numeric)
S3method(wt_trunc,ps_calib)
S3method(wt_trunc,ps_trim)
S3method(wt_trunc,ps_trunc)
S3method(wt_trunc,psw)
export("estimand<-")
export(as_conditional)
export(as_marginal)
Expand All @@ -317,6 +342,8 @@ export(is_refit)
export(is_stabilized)
export(is_unit_trimmed)
export(is_unit_truncated)
export(is_unit_wt_truncated)
export(is_wt_truncated)
export(joint_wt_meta)
export(joint_wt_models)
export(new_psw)
Expand All @@ -341,6 +368,8 @@ export(wt_atu)
export(wt_cens)
export(wt_entropy)
export(wt_joint)
export(wt_trunc)
export(wt_trunc_sensitivity)
import(vctrs)
importFrom(causalgenerics,
"estimand<-",
Expand Down
277 changes: 274 additions & 3 deletions NEWS.md

Large diffs are not rendered by default.

11 changes: 10 additions & 1 deletion R/categorical-models.R
Original file line number Diff line number Diff line change
Expand Up @@ -166,7 +166,7 @@ model_levels.multinom <- function(model) {
# that leave nothing to read as the probability of the exposure, which is what a
# binary exposure needs.
#
# `remedy` goes unread here. What is wrong with a `multinom` of more than two
# `remedy` and `problem` go unread here. What is wrong with a `multinom` of more than two
# levels is the number of levels rather than the family, so the remedy is the
# one written below whichever argument the model arrived in. A `multinom`
# supplied as a numerator model never reaches this method at all:
Expand All @@ -178,6 +178,7 @@ check_binary_model_family.multinom <- function(
model,
arg = ".propensity",
remedy = NULL,
problem = NULL,
call = rlang::caller_env()
) {
n_levels <- length(model$lev)
Expand Down Expand Up @@ -211,6 +212,14 @@ extract_binary_ps.multinom <- function(model, call = rlang::caller_env()) {
as.numeric(stats::fitted(model))
}

# `predict()` on a `multinom` offers no response type. Its probabilities for a
# two-level fit are the one column `fitted()` reports, the probability of the
# second level.
#' @export
predict_binary_ps.multinom <- function(model, newdata) {
as.numeric(stats::predict(model, newdata = newdata, type = "probs"))
}

# A `multinom` keeps no model frame, so `model.frame()` rebuilds one by
# re-evaluating the fitting call in the environment the formula came from. That
# environment no longer holds the arguments of a fit made inside a function, and
Expand Down
56 changes: 37 additions & 19 deletions R/continuous-models.R
Original file line number Diff line number Diff line change
Expand Up @@ -36,8 +36,15 @@ extract_continuous_ps.lm <- function(model, call = rlang::caller_env()) {
# route says which, so the fit is refused here rather than downstream, where a
# matrix with a row for each unit is only ever described by a length that the
# caller never wrote.
#
# `problem` and `remedy` say what the caller needed the single mean for and what
# to do instead, since trimming a dose model cannot take the means on their own.
check_continuous_model_response <- function(
model,
problem = "Weights for a continuous exposure need a model of one conditional
mean for each unit.",
remedy = "Fit the exposure on its own, or pass the conditional means of this
exposure to {.arg .propensity} as a numeric vector.",
call = rlang::caller_env()
) {
fitted_values <- stats::fitted(model)
Expand All @@ -50,13 +57,11 @@ check_continuous_model_response <- function(

abort(
c(
"Weights for a continuous exposure need a model of one conditional mean
for each unit.",
problem,
x = "{.arg .propensity} is {.cls {class(model)[[1]]}}, a fit of
{dims[[2]]} response{?s}, whose fitted values are {dims[[1]]} by
{dims[[2]]}.",
i = "Fit the exposure on its own, or pass the conditional means of this
exposure to {.arg .propensity} as a numeric vector."
i = remedy
),
error_class = "propensity_ps_shape_error",
call = call
Expand Down Expand Up @@ -101,12 +106,16 @@ extract_continuous_ps.gam <- function(model, call = rlang::caller_env()) {
# `arg` is the argument the model arrived in, and `remedy` the advice that goes
# with it: a propensity score model's fitted means can be passed directly
# instead, and a numerator model a caller stabilizes on is a model or nothing.
# `problem` states what the caller needed the model for, since a refit of a
# trimmed dose model needs the same model for a different purpose.
check_continuous_model_family <- function(
model,
arg = ".propensity",
remedy = "Fit the propensity score model with {.fun gaussian}, {.fun lm},
{.fun mgcv::gam}, or {.fun MASS::rlm}, or pass fitted conditional
means to {.arg .propensity} directly.",
problem = "Weights for a continuous exposure need a model of its conditional
mean with a single spread.",
call = rlang::caller_env()
) {
family <- model[["family"]]
Expand Down Expand Up @@ -137,8 +146,7 @@ check_continuous_model_family <- function(

abort(
c(
"Weights for a continuous exposure need a model of its conditional mean
with a single spread.",
problem,
x = fit_with,
i = remedy
),
Expand All @@ -154,14 +162,19 @@ check_continuous_model_family <- function(
# own.
#
# `arg` names the argument the model arrived in and `remedy` says what to do
# about it, because the same question is asked of two models: the propensity
# score model of a binary exposure, and the numerator model that estimates the
# probability the weights are stabilized on.
# about it, because the same question is asked of more than one model: the
# propensity score model of a binary exposure, the numerator model that
# estimates the probability the weights are stabilized on, and the model a
# modified score is read from or refit with. `problem` states what the caller
# needed a probability for, which differs between building weights, bounding
# scores, and refitting them.
check_binary_model_family <- function(
model,
arg = ".propensity",
remedy = "Fit the propensity score model with {.fun binomial}, or pass
fitted probabilities to {.arg .propensity} directly.",
problem = "Weights for a binary exposure need a model of the probability of
the exposure.",
call = rlang::caller_env()
) {
UseMethod("check_binary_model_family")
Expand All @@ -178,23 +191,20 @@ check_binary_model_family.default <- function(
arg = ".propensity",
remedy = "Fit the propensity score model with {.fun binomial}, or pass
fitted probabilities to {.arg .propensity} directly.",
problem = "Weights for a binary exposure need a model of the probability of
the exposure.",
call = rlang::caller_env()
) {
family <- model[["family"]]

# A family object is a list. An element that is not one is not a family, and
# `$` on an atomic vector raises an error of base R's rather than one of this
# package's, so what the element is gets read before anything is asked of it.
#
# `isTRUE()` because a family object that carries no name answers the test
# with nothing at all, which `&&` can read as neither true nor false. A
# family like that is not one of the two, and is refused below by what it is
# missing.
# A family object that carries no name is not one of the binomial two, and is
# refused below by what it is missing.
family_object <- is.list(family)
binomial_family <- family_object &&
isTRUE(family$family %in% c("binomial", "quasibinomial"))

if (binomial_family) {
if (is_binomial_family(family)) {
return(invisible(NULL))
}

Expand All @@ -214,8 +224,7 @@ check_binary_model_family.default <- function(

abort(
c(
"Weights for a binary exposure need a model of the probability of the
exposure.",
problem,
x = fitted_by,
i = remedy
),
Expand All @@ -224,6 +233,15 @@ check_binary_model_family.default <- function(
)
}

# Whether a family fits the probability of a binary exposure. A family object is
# a list, and an element that is not one is not a family. `isTRUE()` because a
# family object that carries no name answers the test with nothing at all, which
# `&&` can read as neither true nor false.
is_binomial_family <- function(family) {
is.list(family) &&
isTRUE(family$family %in% c("binomial", "quasibinomial"))
}

# Whether a family object names itself. Every family built by `binomial()` and
# its neighbors holds its name in `$family`, and a message about one is written
# from that string; an object that carries a link and no name has nothing to be
Expand Down
10 changes: 7 additions & 3 deletions R/density.R
Original file line number Diff line number Diff line change
Expand Up @@ -856,7 +856,7 @@ t_sigma_mle <- function(residuals, df, call = rlang::caller_env()) {
c(
"The scale of a {.val t} density cannot be estimated by maximum
likelihood from these residuals.",
x = "{n_zero} of the {n} residuals of the propensity score model
x = "{n_zero} of the {n} residuals of the model of the exposure
{?is/are} exactly zero, and with {df} degrees of freedom that
leaves the likelihood no maximum at a positive scale.",
i = "Use {.code sigma_method = \"rms\"}, or supply a spread with
Expand Down Expand Up @@ -902,7 +902,7 @@ check_sigma_method <- function(.sigma, density, call = rlang::caller_env()) {
"{.code sigma_method = \"mle\"} cannot be used with {.arg .sigma}.",
x = "{.code sigma_method = \"mle\"} estimates the scale of the
{.val {family}} conditional density from the residuals of the
propensity score model, and {.arg .sigma} is a spread of your own
model of the exposure, and {.arg .sigma} is a spread of your own
that replaces it.",
i = "A {.val {family}} density is spread by its own estimator unless you
ask for another, so {.code sigma_method = \"mle\"} is what it
Expand Down Expand Up @@ -984,8 +984,12 @@ continuous_density_ratio <- function(

# The marginal density is the same family read at the exposure's own center
# and spread, so it is standardized by those rather than by the conditional
# spread. A kernel numerator is therefore fit on its own values.
# spread. A kernel numerator is therefore fit on its own values, and only on
# those of the units the conditional density reads: a unit with no fitted
# mean, such as one a trim set aside, is outside the population the moments
# were read over and carries no weight, so it has no part in the fit either.
z_a <- (exposure - mu_a) / sigma_a
z_a[is.na(z)] <- NA_real_
f_num <- density_eval_present(density, z_a, call = call) / sigma_a

f_num / f_den
Expand Down
7 changes: 4 additions & 3 deletions R/ipw-continuous-models.R
Original file line number Diff line number Diff line change
Expand Up @@ -911,9 +911,10 @@ check_ipw_stabilization_score <- function(
c(
headline,
x = paste("They weight {n} observation{?s} and", held),
i = "A per-observation score is one value for each unit, so it does not \\
survive the rows being restricted: subsetting the weights drops it, and \\
a model frame that drops incomplete rows leaves it at the length the \\
i = "A per-observation score is one value for each unit. Subsetting the \\
weights with {.code [} keeps it aligned, but a slice that cannot place \\
it, such as {.fn vctrs::vec_slice} or {.fn dplyr::arrange}, drops it, \\
and a model frame that drops incomplete rows leaves it at the length the \\
weights were built at.",
i = "Rebuild the weights on the rows being analyzed, or stabilize on a \\
single {.arg stabilization_score}, which scales every weight and \\
Expand Down
Loading
Loading