Skip to contents

ps_trunc() bounds extreme propensity scores to fixed limits, replacing out-of-range values with the boundary value (a form of winsorizing). The result is a vector or matrix of the same length and dimensions as .propensity, with no observations removed. This contrasts with ps_trim(), which sets extreme values to NA (effectively removing those observations from analysis).

Usage

ps_trunc(
  .propensity,
  method = c("ps", "pctl", "cr"),
  lower = NULL,
  upper = NULL,
  .exposure = NULL,
  .focal_level = NULL,
  .reference_level = NULL,
  ...,
  .treated = NULL,
  .untreated = NULL,
  ps = lifecycle::deprecated()
)

Arguments

.propensity

A numeric vector of propensity scores in (0, 1) (binary exposures), or a matrix/data.frame where each column contains propensity scores for one level of a categorical exposure. A data frame truncated for a binary exposure is reduced to a single column: the second column of a two column data frame, which is the probability of the second level in the layout model predictions come in, and the first column otherwise. The column taken is announced; options(propensity.quiet = TRUE) silences the announcement. A matrix is held to the same open interval as a vector, so a score of exactly 0 or 1 in any cell is refused and a separated multinomial fit cannot be repaired by truncating it; see Propensity scores at 0 and 1 in wt_ate().

method

One of "ps", "pctl", or "cr":

  • "ps" (default): Truncate directly on propensity score values. Values outside [lower, upper] are set to the nearest bound. For categorical exposures, applies symmetric truncation using lower as the threshold (delta) and renormalizes rows to sum to 1.

  • "pctl": Truncate at quantiles of the propensity score distribution. The lower and upper arguments specify quantile probabilities. For categorical exposures, quantiles are computed across all columns.

  • "cr": Truncate to the common range of propensity scores across exposure groups (binary exposures only). Bounds are [min(.propensity[focal]), max(.propensity[reference])]. Requires .exposure. When those bounds cross, so that the two distributions do not overlap at all, there is no common range to bound the scores to and the call errors with class propensity_no_overlap_error. That differs deliberately from ps_trim(), which trims every observed unit in the same situation, a truthful record of an empty overlap region.

For categorical exposures, only "ps" and "pctl" are supported.

lower, upper

Bounds for truncation. Interpretation depends on method:

  • method = "ps": Propensity score values (defaults: 0.1 and 0.9). For categorical exposures, lower is the truncation threshold delta (default: 0.01) and upper is ignored. That default deliberately differs from the 0.1 threshold ps_trim() uses for categorical exposures: truncation keeps every unit and only pins the most extreme scores back to the threshold, so its default is a gentle winsorization, whereas trimming discards the units it selects and follows common-support trimming practice. With k exposure levels, a threshold of 1/k or larger cannot be met by every column of a row that sums to one, and is an error.

  • method = "pctl": Quantile probabilities (defaults: 0.05 and 0.95; categorical defaults: 0.01 and 0.99).

  • method = "cr": Ignored; bounds are determined by the data.

.exposure

An exposure vector. Required for method "cr" (binary exposure vector) and for categorical exposures (factor or character vector) with any method.

.focal_level

The value of .exposure representing the focal (treated) group, used by "cr". Every binary coding honors it: 0/1 numeric, logical, two-level factor, and two-level character exposures are all coded with the named level as focal, and a level the exposure never takes is an error. With no level named, a binary exposure defaults to its higher level, which is 1 for a 0/1 exposure, TRUE for a logical one, and the second of the two levels a factor or character exposure takes. Levels a factor declares but never takes are not candidates. Naming any other level reverses the coding, so .propensity must then hold the probability of the named level.

.reference_level

The value of .exposure representing the reference (control) group. Naming it makes the exposure's other level focal, with the same consequence for .propensity, and a level the exposure never takes is an error. Automatically detected if not supplied.

...

Additional arguments passed to methods.

.treated

[Deprecated] Use .focal_level instead.

.untreated

[Deprecated] Use .reference_level instead.

ps

[Deprecated] Use .propensity instead. A call that names ps must name the arguments after it as well, since a positional argument binds to .propensity.

Value

A ps_trunc object (a numeric vector for binary exposures, or a matrix for categorical exposures). Use ps_trunc_meta() to inspect metadata including method, lower_bound, upper_bound, truncated_idx (positions of modified values), and n_obs (the number of observations those positions describe).

Details

Unlike ps_trim(), truncation preserves all observations. No NA values are introduced; out-of-range scores are replaced with the boundary value.

For binary exposures, each propensity score \(e_i\) is bounded:

  • If \(e_i < l\), set \(e_i = l\) (the lower bound).

  • If \(e_i > u\), set \(e_i = u\) (the upper bound).

For categorical exposures, values below the threshold are set to the threshold and each row is renormalized to sum to 1.

Arithmetic behavior: Arithmetic operations on ps_trunc objects return plain numeric vectors. Once propensity scores are transformed (e.g., into weights), the result is no longer a propensity score.

Combining behavior: Combining ps_trunc objects with c() requires matching truncation parameters. Mismatched parameters produce a warning and return a plain numeric vector.

Missing values

A propensity score that arrives missing is not one this function pinned to a bound, so it takes no part in the truncation record: it never joins truncated_idx, and is_unit_truncated() reports FALSE for it. The value stays missing in the result. For a matrix of categorical propensity scores, a row with a missing cell comes back missing throughout, because renormalizing a row divides by its sum and that sum is unknown.

The methods that read their bounds off the data read them off the data they have. "pctl" and "cr" work their bounds out from the complete scores, and the categorical "pctl" bounds come from the cells of the complete rows, so the bounds are the ones the same call would produce with the missing observations dropped.

A missing exposure is a different matter, and "cr" refuses one with an error of class propensity_missing_value_error. Its bounds come from the exposure groups, and a unit that belongs to neither leaves them undefined. Remove or impute the missing exposure values first.

The truncation record

A ps_trunc records which units were winsorized as positions among the observations it was written for, along with how many observations that was. Operations that hand this package the subscript re-index those positions onto the result: subsetting with [, sort(), unique(), and rep() all return a record written for what they return, and a subscript naming a position more than once reports that unit at every place it now holds.

Operations that change how many observations there are without supplying a subscript cannot re-index the record, and it is dropped rather than worked out from the values, since a score that arrived equal to a bound is indistinguishable from one this function pinned there. vctrs::vec_slice(), which is how filtering, joining, and grouped summaries in dplyr reach a column, is the usual route, and dropping the record there raises a warning of class propensity_trunc_record_warning. Combining with c() drops it without comment, because concatenation appends one set of observations to another and the prototype it builds the result from holds no positions to lose. The values, the class, and the method and its bounds are untouched either way.

A record can also outlive the observations it describes, because it travels by routes vctrs does not see: growing a ps_trunc by subassignment carries it across the length change. is_unit_truncated() therefore checks that the record covers the object it is given and raises an error of class propensity_missing_meta_error when it does not, rather than name truncated units at stale positions.

That check compares how many observations the record was written for against how many the object holds, which a reordering does not change. An operation that reorders the observations through vctrs, rather than through [, therefore keeps a record written for the order they used to be in: vctrs::vec_slice(x, 5:1) and dplyr::arrange() both return the values in a new order under positions still naming the old one, and is_unit_truncated() answers from those positions and names the wrong units. Subsetting with [ is handed the subscript and re-indexes, so reorder with [, or put the propensity scores in the order you want before truncating them.

Casting a numeric vector into a ps_trunc with vctrs::vec_cast() is a type operation and not a truncation. The result is described by the method and bounds of the target and records that none of the arriving values was pinned to a bound, so it can hold scores outside those bounds, including 0 and 1. Call ps_trunc() on the scores to truncate them.

References

Crump, R. K., Hotz, V. J., Imbens, G. W., & Mitnik, O. A. (2009). Dealing with limited overlap in estimation of average treatment effects. Biometrika, 96(1), 187–199.

Walker, A. M., Patrick, A. R., Lauer, M. S., et al. (2013). A tool for assessing the feasibility of comparative effectiveness research. Comparative Effectiveness Research, 3, 11–20.

See also

ps_trim() for removing (rather than bounding) extreme values, ps_refit() for refitting the propensity model after trimming, is_ps_truncated(), is_unit_truncated(), ps_trunc_meta()

Examples

set.seed(2)
n <- 200
x <- rnorm(n)
z <- rbinom(n, 1, plogis(1.2 * x))
fit <- glm(z ~ x, family = binomial)
ps <- predict(fit, type = "response")

# Truncate to [0.1, 0.9]
ps_t <- ps_trunc(ps, method = "ps", lower = 0.1, upper = 0.9)
ps_t
#> <ps_trunc{[0.1,0.9], method=ps}[200]>
#>         1         2         3         4         5         6         7         8 
#> 0.2913095 0.5707511 0.8590561 0.2418820 0.4993060 0.5567591 0.7011122 0.4561604 
#>         9        10        11        12        13        14        15        16 
#> 0.9000000 0.4834312 0.6312438 0.7594616 0.4153613 0.2603881 0.8827176 0.1000000 
#>        17        18        19        20        21        22        23        24 
#> 0.7384237 0.5307543 0.7655683 0.6349278 0.9000000 0.2283122 0.8592915 0.9000000 
#>        25        26        27        28        29        30        31        32 
#> 0.5224033 0.1000000 0.6461643 0.3628349 0.7199125 0.5983593 0.7081103 0.6059826 
#>        33        34        35        36        37        38        39        40 
#> 0.7776782 0.4442186 0.3189618 0.3630602 0.1432262 0.2900408 0.3722937 0.4543264 
#>        41        42        43        44        45        46        47        48 
#> 0.4177638 0.1148898 0.3038315 0.8956760 0.6813225 0.9000000 0.4385124 0.4964323 
#>        49        50        51        52        53        54        55        56 
#> 0.4711469 0.2285337 0.3046166 0.9000000 0.3714863 0.8128663 0.2587396 0.1141443 
#>        57        58        59        60        61        62        63        64 
#> 0.4338453 0.7502468 0.7892860 0.8697086 0.1351329 0.9000000 0.3365403 0.5636424 
#>        65        66        67        68        69        70        71        72 
#> 0.6533256 0.3088378 0.1105762 0.3927369 0.5438044 0.2916295 0.2858818 0.6089556 
#>        73        74        75        76        77        78        79        80 
#> 0.4826524 0.6355772 0.5065031 0.2890305 0.8174117 0.7154239 0.7732114 0.1906359 
#>        81        82        83        84        85        86        87        88 
#> 0.7622717 0.1472971 0.3788316 0.1970403 0.1000000 0.8871259 0.3486998 0.4440783 
#>        89        90        91        92        93        94        95        96 
#> 0.4168763 0.6233901 0.8606965 0.8708768 0.2314475 0.1994226 0.1740398 0.2183041 
#>        97        98        99       100       101       102       103       104 
#> 0.9000000 0.5231365 0.3036226 0.3616811 0.7773582 0.5907629 0.4361655 0.3253709 
#>       105       106       107       108       109       110       111       112 
#> 0.2991480 0.9000000 0.7510710 0.9000000 0.4078242 0.4264336 0.2629645 0.4532487 
#>       113       114       115       116       117       118       119       120 
#> 0.6448288 0.8262184 0.6674247 0.6408723 0.8053646 0.7907095 0.5498332 0.3173982 
#>       121       122       123       124       125       126       127       128 
#> 0.8071017 0.5584826 0.8744315 0.4053976 0.2594361 0.6609892 0.3447196 0.6851535 
#>       129       130       131       132       133       134       135       136 
#> 0.1434914 0.1410494 0.6969682 0.6090883 0.7368408 0.1087328 0.8022199 0.8001309 
#>       137       138       139       140       141       142       143       144 
#> 0.7692948 0.7186431 0.9000000 0.1834141 0.3662171 0.6292392 0.3118604 0.5441734 
#>       145       146       147       148       149       150       151       152 
#> 0.7097460 0.3486308 0.6894218 0.6639810 0.3119192 0.2693231 0.7582975 0.4751339 
#>       153       154       155       156       157       158       159       160 
#> 0.7043397 0.3032090 0.8131267 0.2020948 0.7139969 0.6429234 0.5927018 0.6918371 
#>       161       162       163       164       165       166       167       168 
#> 0.6263848 0.3524856 0.4486289 0.6165339 0.2074392 0.2942181 0.9000000 0.1003085 
#>       169       170       171       172       173       174       175       176 
#> 0.2210197 0.7611782 0.7800143 0.7302196 0.5364399 0.6072561 0.2895753 0.3489979 
#>       177       178       179       180       181       182       183       184 
#> 0.4500408 0.2829251 0.7262051 0.1573127 0.2623292 0.2166743 0.6247878 0.2416706 
#>       185       186       187       188       189       190       191       192 
#> 0.6625838 0.7959533 0.5278943 0.6555095 0.3485232 0.6526880 0.2148033 0.5002503 
#>       193       194       195       196       197       198       199       200 
#> 0.2017086 0.4490035 0.7798096 0.6994296 0.4022315 0.3161761 0.3003833 0.3261363 

# Truncate at the 1st and 99th percentiles
ps_trunc(ps, method = "pctl", lower = 0.01, upper = 0.99)
#> <ps_trunc{[0.0901,0.912], method=pctl}[200]>
#>          1          2          3          4          5          6          7 
#> 0.29130945 0.57075108 0.85905610 0.24188201 0.49930600 0.55675915 0.70111217 
#>          8          9         10         11         12         13         14 
#> 0.45616038 0.90360175 0.48343119 0.63124383 0.75946160 0.41536130 0.26038808 
#>         15         16         17         18         19         20         21 
#> 0.88271762 0.09006607 0.73842370 0.53075430 0.76556830 0.63492785 0.91202081 
#>         22         23         24         25         26         27         28 
#> 0.22831221 0.85929151 0.90074563 0.52240328 0.09006607 0.64616425 0.36283492 
#>         29         30         31         32         33         34         35 
#> 0.71991248 0.59835929 0.70811028 0.60598256 0.77767823 0.44421858 0.31896179 
#>         36         37         38         39         40         41         42 
#> 0.36306016 0.14322616 0.29004079 0.37229372 0.45432638 0.41776382 0.11488981 
#>         43         44         45         46         47         48         49 
#> 0.30383148 0.89567605 0.68132246 0.90420941 0.43851237 0.49643227 0.47114693 
#>         50         51         52         53         54         55         56 
#> 0.22853370 0.30461661 0.91106441 0.37148631 0.81286633 0.25873963 0.11414425 
#>         57         58         59         60         61         62         63 
#> 0.43384531 0.75024682 0.78928598 0.86970860 0.13513287 0.90793306 0.33654034 
#>         64         65         66         67         68         69         70 
#> 0.56364237 0.65332558 0.30883775 0.11057616 0.39273691 0.54380444 0.29162947 
#>         71         72         73         74         75         76         77 
#> 0.28588182 0.60895559 0.48265241 0.63557725 0.50650310 0.28903045 0.81741170 
#>         78         79         80         81         82         83         84 
#> 0.71542388 0.77321135 0.19063586 0.76227170 0.14729713 0.37883165 0.19704028 
#>         85         86         87         88         89         90         91 
#> 0.09015377 0.88712592 0.34869977 0.44407832 0.41687627 0.62339015 0.86069650 
#>         92         93         94         95         96         97         98 
#> 0.87087680 0.23144748 0.19942258 0.17403983 0.21830407 0.90120123 0.52313647 
#>         99        100        101        102        103        104        105 
#> 0.30362260 0.36168114 0.77735817 0.59076288 0.43616553 0.32537093 0.29914802 
#>        106        107        108        109        110        111        112 
#> 0.90944557 0.75107099 0.90586644 0.40782418 0.42643359 0.26296452 0.45324867 
#>        113        114        115        116        117        118        119 
#> 0.64482877 0.82621843 0.66742471 0.64087231 0.80536462 0.79070955 0.54983317 
#>        120        121        122        123        124        125        126 
#> 0.31739822 0.80710169 0.55848264 0.87443145 0.40539762 0.25943605 0.66098916 
#>        127        128        129        130        131        132        133 
#> 0.34471960 0.68515350 0.14349137 0.14104940 0.69696824 0.60908833 0.73684079 
#>        134        135        136        137        138        139        140 
#> 0.10873282 0.80221987 0.80013088 0.76929476 0.71864309 0.91202081 0.18341413 
#>        141        142        143        144        145        146        147 
#> 0.36621714 0.62923918 0.31186040 0.54417339 0.70974601 0.34863085 0.68942181 
#>        148        149        150        151        152        153        154 
#> 0.66398099 0.31191915 0.26932313 0.75829753 0.47513393 0.70433975 0.30320896 
#>        155        156        157        158        159        160        161 
#> 0.81312672 0.20209485 0.71399686 0.64292343 0.59270175 0.69183710 0.62638476 
#>        162        163        164        165        166        167        168 
#> 0.35248559 0.44862887 0.61653394 0.20743922 0.29421813 0.91200893 0.10030845 
#>        169        170        171        172        173        174        175 
#> 0.22101971 0.76117824 0.78001428 0.73021961 0.53643993 0.60725607 0.28957526 
#>        176        177        178        179        180        181        182 
#> 0.34899794 0.45004081 0.28292513 0.72620510 0.15731275 0.26232917 0.21667429 
#>        183        184        185        186        187        188        189 
#> 0.62478778 0.24167062 0.66258378 0.79595325 0.52789428 0.65550948 0.34852324 
#>        190        191        192        193        194        195        196 
#> 0.65268801 0.21480329 0.50025030 0.20170859 0.44900347 0.77980959 0.69942959 
#>        197        198        199        200 
#> 0.40223149 0.31617609 0.30038326 0.32613630 

# Use truncated scores to calculate weights
wt_ate(ps_t, .exposure = z)
#>  Treating `.exposure` as binary
#> <psw{estimand = ate; truncated}[200]>
#>   [1] 1.411053 2.329651 1.164068 1.319056 2.002780 2.256110 1.426305 1.838777
#>   [9] 1.111111 2.068547 2.711819 1.316722 1.710458 1.352060 1.132865 1.111111
#>  [17] 1.354236 1.884111 1.306219 2.739185 1.111111 1.295861 1.163749 1.111111
#>  [25] 2.093817 1.111111 1.547594 1.569452 1.389058 1.671237 1.412209 2.537959
#>  [33] 4.497985 1.799268 1.468346 2.754364 1.167169 3.447791 2.686051 1.832597
#>  [41] 2.393697 1.129803 3.291298 1.116475 1.467734 1.111111 2.280437 2.014373
#>  [49] 1.890884 1.296233 1.438056 1.111111 1.591055 1.230215 3.864889 1.128852
#>  [57] 1.766302 4.003953 4.745769 1.149810 7.400124 1.111111 1.507251 1.774175
#>  [65] 1.530630 3.237946 1.124323 2.546234 1.838896 1.411691 1.400328 1.642156
#>  [73] 1.932936 1.573373 1.974322 3.459843 1.223374 1.397773 1.293307 1.235538
#>  [81] 1.311868 1.172741 2.639695 1.245392 1.111111 1.127236 2.867797 1.798815
#>  [89] 2.398793 2.655268 1.161850 1.148268 1.301147 5.014477 1.210712 1.279270
#>  [97] 1.111111 2.097036 1.436003 1.566615 1.286408 1.692727 1.773570 1.482296
#> [105] 1.426835 1.111111 1.331432 1.111111 2.452037 2.345031 1.356787 1.828985
#> [113] 1.550799 1.210334 1.498296 1.560373 5.137812 1.264687 1.818733 3.150616
#> [121] 5.184079 1.790566 1.143600 2.466714 1.350322 1.512884 1.526064 3.176151
#> [129] 1.167531 7.089715 1.434786 2.558123 3.799981 1.121998 1.246541 1.249796
#> [137] 1.299892 1.391511 1.111111 5.452143 1.577827 2.697157 1.453194 2.193817
#> [145] 1.408955 1.535228 1.450491 2.976022 1.453318 3.713012 4.137318 1.905248
#> [153] 1.419769 1.435150 5.351220 1.253282 1.400566 1.555395 1.687189 3.245037
#> [161] 2.676550 1.544367 2.229014 1.621971 4.820689 1.416868 1.111111 1.111492
#> [169] 4.524483 1.313753 4.545750 1.369451 2.157218 2.546188 1.407609 2.865346
#> [177] 2.222021 1.394555 1.377021 1.186680 1.355618 1.276608 2.665158 1.318688
#> [185] 1.509243 1.256355 1.894319 1.525531 2.869249 2.879256 1.273566 2.001002
#> [193] 1.252675 1.814893 1.282364 3.327007 1.672888 1.462365 1.429354 1.483980

# Inspect which observations were truncated
is_unit_truncated(ps_t)
#>   [1] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE
#>  [13] FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE  TRUE
#>  [25] FALSE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#>  [37] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE
#>  [49] FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#>  [61] FALSE  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#>  [73] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#>  [85]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#>  [97]  TRUE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE  TRUE
#> [109] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> [121] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> [133] FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE FALSE FALSE FALSE FALSE
#> [145] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> [157] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE  TRUE FALSE
#> [169] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> [181] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE
#> [193] FALSE FALSE FALSE FALSE FALSE FALSE FALSE FALSE