Missing Data, Trimming, and Winsorization in RWE
Practical handling of missing data (under MCAR/MAR/MNAR mechanisms) and of extreme propensity-score weights or outcome/cost outliers via trimming (excluding observations outside a propensity or weight threshold) and Winsorization (capping values at chosen percentiles), each of which changes the estimand and the target population it identifies.
On this page
When researchers weight a study to balance who got each treatment, one or two patients can accidentally end up with enormous weights — meaning that single person drives almost the entire result. Trimming drops those extreme patients from the analysis entirely (changing who the answer applies to), while winsorizing caps their weight at a chosen ceiling (keeping them in but limiting their pull). Both choices stabilize the estimate, but both also change what the study is actually measuring, so the rule must be written into the analysis plan before anyone looks at the data.
This entry covers two operations that practitioners routinely apply after covariate adjustment but that quietly alter what is being estimated: handling missing data and taming extreme inverse-probability weights or outlier values through trimming and Winsorization. They are bundled because they share one deep property — both are responses to regions of the data where the estimand is weakly or non-identified (limited overlap, structural absence of records), and both trade variance for a shift in the target population if applied carelessly.
Core conceptual distinction
Three mechanisms govern missingness and they are not testable from observed data alone MCAR (missing completely at random): missingness is independent of observed and unobserved values — essentially never true in claims or EHR. MAR (missing at random): missingness depends only on observed data, so it can be corrected by multiple imputation or inverse-probability-of-observation weighting conditional on the observed covariates
MNAR (missing not at random): missingness depends on the unobserved value itself — the dominant reality in RWE (a lab is missing because the clinician judged it unnecessary; future claims are absent because the patient died or disenrolled) For trimming and Winsorization, the distinction is between trimming (removing units whose propensity score or weight falls outside a threshold, i.e., dropping the unit entirely) and Winsorization/truncation (retaining the unit but replacing its extreme weight or value with a capped value)
Trimming targets positivity/overlap; it deliberately redefines the population to the region of common support, so the estimand becomes a trimmed ATE/ATT on that subpopulation, not the original-cohort ATE. Winsorization keeps every unit but biases the weighted estimator toward the bulk, reducing variance at the cost of residual confounding from the down-weighted tails The decisive point regulators raise: trimming and Winsorization change the estimand, not merely the estimator, and that change must be stated in the protocol/SAP, not buried in a footnote.
Pros, cons, and trade-offs
(specific and comparative).
- Trimming/Winsorization of weights vs. doing nothing (raw IPTW): Raw IPTW with a handful of weights >50 is high-variance, non-robust, and effectively non-positive — one patient can swing the ATE. Trimming/Winsorization stabilizes the estimate dramatically. Cost: bias relative to the full-cohort estimand and sensitivity to the (arbitrary) rule. Prefer when a small tail dominates the effective sample size and the question tolerates a restricted population.
- Symmetric PS trimming (Stürmer) vs. data-driven Crump trimming: Stürmer's fixed-percentile trimming of the PS tails is transparent and easy to pre-specify but arbitrary; Crump's optimal-overlap rule chooses the threshold to minimize asymptotic variance subject to retaining support, which is more principled but less interpretable and harder to communicate to a non- statistician reviewer. Prefer Crump (or overlap weights) when positivity is the core threat and you can defend the machinery; prefer fixed symmetric trimming for routine pharmacoepi with mild non-overlap.
- Trimming/Winsorization vs. overlap (ATO) weights: Overlap weights (Li, Morgan, Zaslavsky) achieve exact balance and bounded weights by construction, sidestepping the need for an ad hoc cap — but they change the estimand to the overlap population (patients in clinical equipoise). Prefer overlap weights when the equipoise population is itself the policy- relevant target; prefer explicit trimming when stakeholders require the ATE/ATT on a nameable population.
- Winsorization of cost outliers vs. two-part / GLM gamma models: Capping a $2M CAR-T claim at the 99th percentile is crude; a gamma or two-part model accommodates the skew without discarding magnitude. Prefer the model when the tail is real signal (oncology HCRU); prefer Winsorization only as a transparent, pre-specified sensitivity layer.
- Complete-case vs. multiple imputation for missing covariates: Complete-case analysis is unbiased only under MCAR (and sometimes under MAR conditional on the outcome model) and wastes data; MI under MAR is principled but invalid if the mechanism is MNAR. Neither recovers structurally absent post-death/post-disenrollment data — that requires re-defining the estimand (e.g., a composite or a while-alive contrast).
When NOT to use — and when it is actively misleading or dangerous
(decision rules).
- Trimming to "fix" a non-overlap problem that is really confounding-by-indication. If one drug is reserved for renally- impaired patients, the separated PS distribution is a design failure; trimming the non-overlapping tail silently changes the population to one where the comparison may no longer be the question of interest, and reviewers will read the residual estimand as the same one promised in the objectives. Diagnose with the PS-overlap plot and clinical review first.
- Winsorizing real outcome signal. In oncology cost studies, the high-cost tail (CAR-T, prolonged ICU) is the phenomenon; capping it can reverse a cost-effectiveness conclusion. Capping outcomes is dangerous whenever the tail is the decision-driver, not noise.
- Imputing structurally missing data. Standard MI on observed covariates cannot recover information that is missing because the patient died or disenrolled — that is MNAR by construction. Imputing post-death cost as if it were MAR fabricates person-time and biases PPPM/PMPM. Handle with a while-alive estimand, competing-risks framing, or explicit MNAR sensitivity (pattern-mixture / delta-adjustment), never naive MI.
- Imputing time-varying confounders without respecting time order. Imputing a confounder using post-treatment values can open a collider path and induce bias worse than the missingness it cured.
- Reporting a single trimmed result with no sensitivity layer. A point estimate that flips under 1% vs. 5% Winsorization is not a finding; it is an artifact. Presenting only the favorable cut is the classic HTA/regulatory red flag.
Data-source operational depth
(claims vs. EHR vs. registry vs. linked).
- Claims (FFS vs. MA): "No claim observed" conflates true absence with unobserved care. MA-only person-time lacks the fee-for-service encounter and pharmacy claims that FFS Parts A/B/D generate, so a patient who looks event-free or treatment-naïve may simply be invisible — restrict to fully-observable (A/B/D or commercial medical+pharmacy) person-time rather than imputing the gap. Disenrollment and death are MNAR by design: future records are structurally absent, so censor at the enrollment-span end and never carry costs/outcomes past it. Differential competing risks by exposure in elderly claims (a frailer arm dies sooner, producing systematically shorter, more "complete-looking" follow-up) interact with both weight tails and missingness — the frail tail often carries the largest weights and the heaviest truncated cost.
- EHR: Visit-driven capture makes completeness a function of engagement — sicker/more-engaged patients have richer records, so missing labs are typically MNAR. Use missingness indicators or model the visit/observation process (IPCW) rather than assuming MAR. Immortal time in procedure studies can masquerade as missingness: a window with no records before a procedure may reflect care delivered out-of-system, not absence of disease — confirm with linkage before treating it as a clean lookback.
- Registry: Lower missingness on adjudicated key endpoints, but utilization, costs, and PROs are often incomplete; use the registry as a validation substrate for claims-based imputation or trimming rules rather than as the costing source.
- Linked claims–EHR–vital records: The ideal substrate — EHR severity to enrich the PS (reducing extreme weights at the source) plus claims completeness and a death index to terminate person-time correctly — but linkage selects the linkable subset and creates date discrepancies (order vs. fill vs. service) that must be reconciled before any windowing.
Worked claims example
Question: 12-month all-cause cost (PPPM) of an injectable specialty drug vs. an active comparator among adults with continuous commercial + Medicare FFS A/B/D enrollment (1) Cohort: new initiators, 365-day continuous, FFS-observable enrollment lookback (exclude MA-only person-time so "no prior fill" is real, not missing) (2) Confounding: high-dimensional PS, then stabilized IPTW. Inspect the weight distribution — suppose `max(weight)=84` and the top 0.5% of units hold 22% of total weight (effective sample size collapsed from 9,800 to ~4,100)
(3) Pre-specified primary rule: symmetric PS trimming at the 1st/99th percentiles of the treated-arm PS (Stürmer), recomputing weights on the trimmed cohort; report that 1.9% of units and 2.4% of person-time are dropped, and that the estimand is now the trimmed-population ATT. (4) Outcome handling: per-member-per-month cost is right-censored at death and disenrollment (no post-exit imputation — MNAR by design); the cost distribution is Winsorized at the 99th percentile as a sensitivity layer only, with the gamma two-part model as primary
(5) Sensitivity grid: {no trim, 1/99 trim, Crump optimal, overlap weights} × {no Winsor, 99th- pct Winsor} reported side by side, with ESS and the share of trimmed person-time for each cell The conclusion is reported as robust only if the sign and rough magnitude of the cost difference survive the grid; cells that diverge are flagged as the locus of residual positivity/MNAR risk rather than averaged away.
Interpreting the output
Consider the six-patient IPTW cost comparison: the raw weighted cost difference between treated and comparator arms is approximately $32,740. After symmetric PS trimming at the 1st/99th percentiles (dropping the highest-weight units), the trimmed difference is approximately $10,000. Winsorizing costs at the 99th percentile as a sensitivity layer yields a difference of approximately $29,640.
(1) Formal statistical interpretation. Trimming and Winsorization both change the estimand, not just the estimator Trimming removes the highest-weight units from the cohort entirely; the resulting estimate applies to a trimmed target population — the ATT among patients with PS within the 1st–99th percentile range — not the full treated population Winsorization caps extreme cost values rather than removing patients; the estimand remains the ATT for all treated patients, but extreme costs are replaced by the cap value
Neither approach is bias-free: both sacrifice some external validity for robustness to extreme observations The wide divergence between the trimmed ($10,000) and winsorized ($29,640) estimates signals that a small number of high-cost, high-weight patients drive a large share of the raw difference ($32,740), and that the conclusion is sensitive to how those observations are handled.
(2) Practical interpretation for a decision-maker. The "true" cost difference lies somewhere in the range defined by the sensitivity grid, not at any single number. The primary estimate from the pre-specified method (here, PS trimming) should be the headline, with the winsorized and untrimmed estimates reported as sensitivity. Divergence across cells flags positivity and influence concerns that warrant further investigation before a formulary or coverage decision.
Decision diagram
flowchart TD
W[Estimated PS + stabilized IPTW] --> D{Inspect weights:<br/>max weight, ESS, tail share}
D -->|stable, ESS adequate| KEEP[Use as-is; report distribution]
D -->|few extreme weights| POS{Source of the tail?}
POS -->|limited overlap / positivity| TRIM[Trim PS tails Sturmer/Crump<br/>OR switch to overlap weights<br/>RECOMPUTE weights]
POS -->|heavy-tailed outcome / cost| WIN[Winsorize outcome at percentile<br/>OR gamma / two-part model]
POS -->|non-overlap = confounding by indication| STOP[Design problem:<br/>re-examine comparator, do not silently trim]
TRIM --> EST[Trimmed-population estimand<br/>state new target population]
WIN --> EST
EST --> SENS[Sensitivity grid across rules<br/>report ESS + pct person-time affected]
style STOP fill:#ffcccc
style SENS fill:#cce5ffflowchart TD
OBS[Observation incomplete?] --> M{Mechanism}
M -->|MCAR: independent of all data| CC[Complete-case ~ unbiased<br/>but inefficient]
M -->|MAR: depends on OBSERVED data| MI[Multiple imputation<br/>or IPCW conditional on observed]
M -->|MNAR: depends on UNOBSERVED value| SENS[Pattern-mixture / delta-adjustment<br/>re-define estimand]
M -->|Structural: death / disenrollment| STR[Censor person-time;<br/>while-alive or composite estimand]
style SENS fill:#ffe5cc
style STR fill:#ffccccWorked example
Scenario
A pharmacoepidemiology team is estimating the average difference in one-year total cost between patients on a specialty drug (treated, T=1) and an active comparator (T=0). They fit a logistic model to get a propensity score for each patient, then compute stabilized inverse-probability weights. The marginal probability of being treated is 3/6 = 0.50, so the stabilized weight for each treated patient is 0.50 divided by their propensity score, and for each comparator patient is 0.50 divided by (1 minus their propensity score). Inspecting the weights reveals two patients — one in each arm — with a propensity score near the boundary of their group, producing a weight of 10.0. Those two patients together hold most of the total weight in the analysis. The team pre-specified a rule: compare trimming (drop patients with weight above 5.0) versus winsorizing (cap weights above 5.0 at 5.0), and report both alongside the raw result.
Dataset
Analysis-ready table: one row per patient with propensity score, stabilized weight, and one-year total cost in thousands of dollars.
| patient_id | arm | propensity_score | stabilized_weight | total_cost_k |
|---|---|---|---|---|
| P01 | treated | 0.8 | 0.625 | 18 |
| P02 | treated | 0.4 | 1.25 | 22 |
| P03 | treated | 0.05 | 10.0 | 45 |
| P04 | comparator | 0.2 | 0.625 | 12 |
| P05 | comparator | 0.6 | 1.25 | 10 |
| P06 | comparator | 0.95 | 10.0 | 8 |
Steps
Result
Raw (no adjustment): $32,740 — dominated by P03, a single treated patient whose low propensity score (0.05) gave them a weight of 10.0. Trimmed (drop weights above 5.0, cap at 1st/99th in practice): $10,000, on the four-patient overlap cohort only — a population defined by having a propensity score that plausibly appears in both arms. Winsorized (cap weights at 5.0): $29,640, retaining all six patients but limiting any single patient's influence. The three estimates span a $22,740 range, illustrating why the rule must be pre-specified: the analyst who looks at the numbers first can always choose the most favorable one. The team reports all three as the sensitivity grid required by their SAP, with the trimmed result as primary because it targets a clearly definable overlap population and the two extreme patients were clinically implausible comparisons.
Trade-offs
Runnable example
Operationalize stabilized IPTW with diagnostics, then apply (a) symmetric PS trimming and (b) percentile Winsorization, reporting effective sample size for each rule. Required input (one row per person, already cleaned): df : person_id, treat (1/0 study vs comparator), ps (estimated propensity, 0<ps<1), y (outcome,...
import numpy as np
import pandas as pd
def stabilized_iptw(df: pd.DataFrame) -> pd.Series:
"""Stabilized inverse-probability-of-treatment weights (Austin & Stuart 2015)."""
p_treat = df["treat"].mean()
w = np.where(df["treat"] == 1, p_treat / df["ps"], (1 - p_treat) / (1 - df["ps"]))
return pd.Series(w, index=df.index)
def effective_sample_size(w: pd.Series) -> float:
"""Kish ESS: collapses as a few weights dominate."""
return (w.sum() ** 2) / (w ** 2).sum()
def symmetric_ps_trim(df: pd.DataFrame, lo: float = 0.01, hi: float = 0.99) -> pd.DataFrame:
"""Stürmer-style trim on percentiles of the TREATED-arm PS, then keep the overlap region."""
ps_t = df.loc[df["treat"] == 1, "ps"]
lo_cut, hi_cut = ps_t.quantile(lo), ps_t.quantile(hi)
return df[(df["ps"] >= lo_cut) & (df["ps"] <= hi_cut)].copy()
def winsorize(s: pd.Series, lo: float = 0.01, hi: float = 0.99) -> pd.Series:
return s.clip(lower=s.quantile(lo), upper=s.quantile(hi))
def weighted_mean_diff(df: pd.DataFrame, w: pd.Series) -> float:
"""Weighted ATE-style contrast of y between arms (e.g., cost difference)."""
t = df["treat"] == 1
m1 = np.average(df.loc[t, "y"], weights=w[t])
m0 = np.average(df.loc[~t, "y"], weights=w[~t])
return m1 - m0
# --- Sensitivity grid: no-trim vs symmetric trim, crossed with no-Winsor vs 99th-pct Winsor ---
rows = []
for trim_name, d in [("no_trim", df), ("trim_1_99", symmetric_ps_trim(df))]:
w = stabilized_iptw(d) # recompute weights AFTER trimming
for win_name, yy in [("no_winsor", d["y"]), ("winsor_99", winsorize(d["y"]))]:
dd = d.assign(y=yy.values)
rows.append({
"trim": trim_name, "winsor": win_name,
"n": len(dd), "ess": round(effective_sample_size(w), 1),
"pct_trimmed": round(100 * (1 - len(dd) / len(df)), 2),
"max_weight": round(float(w.max()), 1),
"effect": round(weighted_mean_diff(dd, w), 2),
})
print(pd.DataFrame(rows))Same sensitivity engine in R: stabilized IPTW with effective sample size, symmetric PS trimming (recompute weights after trimming), and percentile Winsorization, reported as a grid. Input analysis-ready data frame: df : person_id, treat (1/0), ps (0<ps<1), y (outcome)
library(data.table)
stabilized_iptw <- function(d) {
p <- mean(d$treat)
ifelse(d$treat == 1L, p / d$ps, (1 - p) / (1 - d$ps))
}
ess <- function(w) sum(w)^2 / sum(w^2) # Kish effective sample size
symmetric_ps_trim <- function(d, lo = 0.01, hi = 0.99) {
q <- quantile(d$ps[d$treat == 1L], probs = c(lo, hi))
d[d$ps >= q[1] & d$ps <= q[2]]
}
winsorize <- function(x, lo = 0.01, hi = 0.99) {
q <- quantile(x, probs = c(lo, hi)); pmin(pmax(x, q[1]), q[2])
}
wmean_diff <- function(d, w) {
t <- d$treat == 1L
weighted.mean(d$y[t], w[t]) - weighted.mean(d$y[!t], w[!t])
}
build_grid <- function(df) {
setDT(df)
out <- list()
for (tn in c("no_trim", "trim_1_99")) {
d <- if (tn == "no_trim") copy(df) else symmetric_ps_trim(df)
w <- stabilized_iptw(d) # recompute AFTER trimming
for (wn in c("no_winsor", "winsor_99")) {
yy <- if (wn == "no_winsor") d$y else winsorize(d$y)
dd <- copy(d)[, y := yy]
out[[paste(tn, wn)]] <- data.table(
trim = tn, winsor = wn, n = nrow(dd),
ess = round(ess(w), 1),
pct_trimmed = round(100 * (1 - nrow(dd) / nrow(df)), 2),
max_weight = round(max(w), 1),
effect = round(wmean_diff(dd, w), 2)
)
}
}
rbindlist(out)
}
print(build_grid(df))SAS implementation: estimate the PS with PROC PSMATCH (or PROC LOGISTIC), build stabilized IPTW, apply symmetric PS trimming and percentile Winsorization, and fit the weighted outcome model with PROC GENMOD.
/* 1. Propensity score from baseline covariates (logit link). */
proc logistic data=work.analytic descending noprint;
class <categorical covariates>;
model treat = <baseline covariates>;
output out=ps_out pred=ps; /* ps = P(treat=1 | covariates) */
run;
/* 2. Stabilized IPTW (Austin & Stuart 2015). */
proc sql noprint;
select mean(treat) into :ptreat trimmed from work.analytic;
quit;
data wts;
set ps_out;
if treat=1 then sw = &ptreat / ps;
else sw = (1-&ptreat) / (1-ps);
run;
/* 3. Symmetric PS trim on the 1st/99th percentiles of the TREATED-arm PS. */
proc univariate data=wts(where=(treat=1)) noprint;
var ps;
output out=cuts pctlpts=1 99 pctlpre=ps_;
run;
data trimmed;
if _n_=1 then set cuts; /* ps_1, ps_99 */
set wts;
if ps_1 <= ps <= ps_99; /* keep overlap region; weights recomputed below */
run;
/* 4. Recompute stabilized weights on the trimmed cohort. */
proc sql noprint;
select mean(treat) into :ptrim trimmed from trimmed;
quit;
data trimmed;
set trimmed;
if treat=1 then sw = &ptrim / ps; else sw = (1-&ptrim) / (1-ps);
run;
/* 5. Winsorize the outcome at the 99th percentile (sensitivity layer). */
proc univariate data=trimmed noprint;
var y; output out=ycut pctlpts=99 pctlpre=y_;
run;
data trimmed_w;
if _n_=1 then set ycut; /* y_99 */
set trimmed;
y_win = min(y, y_99);
run;
/* 6. Weighted outcome model (gamma-log GLM for skewed cost; swap dist=/link= as needed). */
proc genmod data=trimmed_w;
class person_id;
weight sw;
model y_win = treat / dist=gamma link=log;
repeated subject=person_id / type=ind; /* robust SEs for the weighted estimator */
estimate 'treat vs comparator (log scale)' treat 1 / exp;
run;Citations
- [1]Stürmer T, Rothman KJ, Avorn J, Glynn RJ. Treatment effects in the presence of unmeasured confounding: dealing with observations in the tails of the propensity score distribution--a simulation study. American Journal of Epidemiology. 2010;172(7):843-854.
- [2]Crump RK, Hotz VJ, Imbens GW, Mitnik OA. Dealing with limited overlap in estimation of average treatment effects. Biometrika. 2009;96(1):187-199.