CausalState produces a lot of diagnostic output. This
vignette walks through a stable, objective workflow for using
it. The core idea is that several checks and tuning decisions should
happen before the final estimation, and a smaller set
of post-estimation checks confirm the fit behaved sensibly.
The recommended flow:
weight_diagnostics() and policy_change_summary
to judge whether the intervention is supported and to pick
trim.sdr(),
itmle(), or qreg() under the
natural-course policy; use
branch_cal_summary() to tune the SuperLearner libraries for
the g- and Q-branches. For itmle() this is also where you
tune the targeting library sl_tmle.sdr() / itmle() under the
actual policy of interest.recursion_diag, target_cal (iTMLE only), and
ic_df. These describe what the estimator did with
a final fit; they are informative but not part of the tuning loop.We use sim_bin() throughout for illustration.
sim_cont() (continuous treatment) and
sim_multi() (any mix of binary + continuous treatments)
follow the same shape.
Fit the density ratios before touching any outcome model. This step answers two independent questions:
Rt_cum >> N) will destabilise both the SDR and iTMLE
corrections regardless of how well the Q-models fit.policy_change_summary will show it as extreme shift
magnitudes.sl_lib <- c("SL.mean", "SL.glm")
policy_bin <- function(D_block, t, a_names) {
out <- D_block[, ..a_names, drop = FALSE]
out[[a_names[1]]] <- pmax(
D_block[[a_names[1]]], as.integer(D_block[["L1"]] > 1.0)
)
out
}
wr <- density_ratio(
df = df,
a_names = "A",
tmax = 5L,
baseline = c("age", "sex"),
tv_names = c("L1", "L2"),
sl_g = sl_lib,
k = 1L,
inner_v = 3L,
v = 3L,
seed = 1L,
id = "id",
time = "time",
policy_spec_fun = policy_bin
)weight_diagnostics()The most important columns:
cum_mean — mean of the cumulative
density-ratio product at each time step. This is what actually drives
the size of the EIF correction in sdr() /
itmle(). If cum_mean blows up (say above 5) or
the ratio between successive time steps grows, cumulative weights are
accumulating fast — the correction will be variance-dominated and the
estimator will be noisy.Rt_mean — mean of the
instantaneous density ratio at each step (restricted to at-risk
subjects). Tells you where in time the shift is biting hardest. A single
spike says the intervention is extreme at that time; a gentle slope
across time is easier to handle.Secondary columns, useful for tuning and plausibility checks:
Rt_ess,
cum_ess — Kish effective sample size. Low
values flag that most of the mass is concentrated in a few subjects. Not
a hard rule but useful when comparing candidate sl_g
libraries or candidate policies.Rt_max,
cum_max — worst-case weight. Useful for
spotting positivity violations. If cum_max is orders of
magnitude larger than cum_mean, the intervention is
unrealistic somewhere in the covariate space.policy_change_summaryShows the fraction of subjects whose treatment actually changed under the policy at each time-step, plus the mean/quantiles of the observed-vs-shifted difference among those who changed. A policy that changes 100% of subjects by large amounts will have extreme weights by construction — the diagnostics above are then just confirming a design choice rather than a modelling problem.
weight_diagnostics() accepts trim (default
1, i.e. no trimming) so you can see how a candidate trim
reshapes the weight distribution before committing:
weight_diagnostics(wr, trim = 0.99) # cap Rt_t at the 99th percentile
weight_diagnostics(wr, trim = 0.95)sdr() and itmle() take the same
trim argument. Trimming reduces variance at the cost of a
small (usually negligible) bias in the weighted correction — the
tradeoff is worth it when a small number of extreme weights are
dominating the influence curve.
Once weights look reasonable, fit the estimator under the natural course — i.e. with a policy that returns the observed treatment unchanged. This diagnostic run has two properties that make it uniquely useful for tuning:
branch_cal table is populated with per-fold,
per-time calibration slopes and target-vs-pred means for each of the
four branches (g_remain, g_death,
q_rem, q_exit).Under the actual intervention policy, calibration is unmeasurable in the usual sense — the counterfactual outcome is never observed. That is why you should tune here, under the natural course, and then lock the SL library choice before the intervention run.
policy_nat <- function(D_block, t, a_names) {
D_block[, ..a_names, drop = FALSE]
}
wr_nat <- density_ratio(
df = df, a_names = "A", tmax = 5L,
baseline = c("age", "sex"), tv_names = c("L1", "L2"),
sl_g = sl_lib, k = 1L, inner_v = 3L, v = 3L, seed = 1L,
id = "id", time = "time",
policy_spec_fun = policy_nat
)
res_nat <- sdr(
df = df, weight_object = wr_nat, tmax = 5L,
id = "id", time = "time", alive = "alive", in_state = "in_state", y = "Y",
baseline = c("age", "sex"), tv_names = c("L1", "L2"), a_names = "A",
sl_remain = sl_lib, sl_death = sl_lib,
sl_recursive = sl_lib, sl_y = sl_lib,
k = 1L, inner_v = 3L, parallel = FALSE, seed = 1L,
policy_spec_fun = policy_nat
)branch_cal_summary()Focus on:
tgt_vl — empirical mean of the target
on the validation folds.pred_vl — mean of the model prediction
on the same folds. Under the natural course, these should track closely
per branch.cal_slope_vl — slope from regressing
the target on the model prediction (on the logit scale for binomial
branches). A slope of 1.0 is perfect calibration;
< 1 indicates over-fitting (the model spread its
predictions too widely); > 1 indicates under-fitting
(predictions too flat).A slope of 0.7-1.3 and tgt_vl ≈ pred_vl per
branch is a reasonable target. When a branch is systematically off,
revise the corresponding sl_* library — usually by adding
smoother learners (SL.glm, SL.glmnet) if
calibration slope is < 1, or richer learners
(SL.xgboost, spline learners) if slope is
> 1 and the target is poorly matched.
qreg()sdr() computes both g- and Q-branches, and both are
reflected in branch_cal. If you want to isolate the Q-side
without the g-branch noise (or without needing weights),
qreg() runs the pure Q-recursion under the natural
course:
res_qreg <- qreg(
df = df, tmax = 5L,
id = "id", time = "time", alive = "alive", in_state = "in_state", y = "Y",
baseline = c("age", "sex"), tv_names = c("L1", "L2"), a_names = "A",
sl_remain = sl_lib, sl_death = sl_lib,
sl_recursive = sl_lib, sl_y = sl_lib,
k = 1L, inner_v = 3L, parallel = FALSE, seed = 1L,
policy_spec_fun = policy_nat
)
branch_cal_summary(res_qreg)itmle() only)itmle() adds a fluctuation/targeting step on top of the
Q-mixture. The targeting model has its own SuperLearner library
(sl_tmle / tgt_lib). A natural-course
itmle() run lets you tune this library too:
tgt_lib <- c("SL.tmle_empty", "SL.tmle_intercept", "SL.tmle_glm")
res_itmle_nat <- itmle(
df = df, weight_object = wr_nat, tmax = 5L,
id = "id", time = "time", alive = "alive", in_state = "in_state", y = "Y",
baseline = c("age", "sex"), tv_names = c("L1", "L2"), a_names = "A",
sl_remain = sl_lib, sl_death = sl_lib,
sl_recursive = sl_lib, sl_y = sl_lib, sl_tmle = tgt_lib,
k = 1L, inner_v = 3L, v_target_itmle = 3L, v_sl_inner_itmle = 3L,
parallel = FALSE, seed = 1L, policy_spec_fun = policy_nat
)
res_itmle_nat$diagnostics$target_sltarget_sl shows which targeting wrappers were selected
per (fold, t). If a wrapper never gets picked, drop it from
tgt_lib. If the fit looks unstable, prefer
covariate-adaptive wrappers (SL.tmle_glm,
SL.tmle_glmnet_*) over intercept-only ones.
Only after the weight and calibration diagnostics are satisfactory should you run the estimator under the actual policy of interest. Keep the SL library choice locked from the tuning step.
res <- sdr(
df = df, weight_object = wr, tmax = 5L,
id = "id", time = "time", alive = "alive", in_state = "in_state", y = "Y",
baseline = c("age", "sex"), tv_names = c("L1", "L2"), a_names = "A",
sl_remain = sl_lib, sl_death = sl_lib,
sl_recursive = sl_lib, sl_y = sl_lib,
k = 1L, inner_v = 3L, parallel = FALSE, seed = 1L,
policy_spec_fun = policy_bin, trim = 0.99
)
resThe remaining diagnostics describe what the estimator did with the final fit. They cannot easily be used to tune before running the estimator because they depend on the joint behaviour of the weights, the Q-models, and the specific intervention. Treat them as sanity checks rather than a feedback loop.
diagnostics$recursion_diagOne row per fold × time-step, ordered from t = tmax down
to 1. The columns are grouped by concept.
Sample counts and fit-status flags
n_train_at_risk, n_valid_at_risk —
subjects available for the recursion at each t.n_train_death, n_train_dc,
n_train_remain — outcome counts used to fit that
time-step’s models.used_const_rem / _dex /
_qexit / _qrem — TRUE when the
corresponding SL fit had too few observations and a constant was
substituted. A cluster of TRUE values at later
t typically indicates late-follow-up subject scarcity.Predicted branch probabilities and means (natural vs shifted, training side)
p_rem_{nat,shf}_{mean,sd} — g_remain
predictions (probability of staying in-state).p_dex_{nat,shf}_{mean,sd} — g_death_exit
predictions (probability of death given exit).q_rem_{nat,shf}_{mean,sd} — Q_rem
predictions (expected outcome for remainers).q_exit_{nat,shf}_{mean,sd} — mean of the exit-branch
mixture p_dex · q_death + (1 - p_dex) · q_dc.Differences between _nat and _shf columns
reflect how much the intervention shifts the branch predictions. Very
small differences on a policy that meaningfully changes treatment
suggest the models are insensitive to the treatment covariates — worth
investigating.
Full mixture Q on training data
Q_{nat,shf}_pre_{mean,sd,min,max} — mixture Q at time
t, before the EIF update (SDR) or targeting (iTMLE).Y_target_{mean,sd,min,max} — the current regression
target passed into the Q_rem model at this step. For SDR
this is the running pseudo-outcome pre-update; for iTMLE it is the
targeted Q from the outer step s+1.pseudo_post_{mean,sd,min,max} — pseudo-outcome after
the EIF update (SDR only). Should stay close to Y_target
in-mean; large divergence in the tails signals weight extremes.EIF update / targeting magnitudes
delta_{sd, q95_abs, max_abs} (SDR) — spread and tail of
the EIF correction (pseudo_post - Y_target). Big
delta_max_abs relative to delta_sd indicates a
small number of subjects are driving the correction — usually because of
extreme weights.delta_{nat,shf}_target_{sd, q95_abs, max_abs} (iTMLE) —
same idea but for the targeting update rather than the EIF update,
computed separately under natural and shifted branches.n_post_below_0, n_post_above_1 — how many
EIF-corrected pseudo-outcomes fell outside [0, 1].
Meaningful only for binomial or bounded-Gaussian outcomes; a couple of
violations per fold is normal, a large number signals that the
correction is frequently pushing predictions out of the plausible
range.n_{nat,shf}_post_{below_0,above_1} (iTMLE) — same, but
for the post-targeting Q rather than the EIF-corrected
pseudo-outcome.Model residuals
resid_{sd, q95_abs, max_abs} —
Y_target - Q_{shf}. This is how far off the fitted Q was
from the target it was regressing against, evaluated on the shifted
branch. Large residuals suggest the recursive Q model is misspecified
for the current pseudo-outcome — a signal to revisit
sl_recursive (or sl_rec_early if the miss is
concentrated at early time-points).Validation-side sanity
Q_{nat,shf}_vl_mean (SDR) or
Q_{nat,shf}_vl_{pre,post}_mean (iTMLE) — validation-fold Q
means. Comparing to their training counterparts is a quick check for
cross-fitting stability. Large training-vs-validation gaps signal
over-fitting somewhere in the Q-recursion.delta_{nat,shf}_vl_target_q95_abs (iTMLE) —
validation-side targeting update magnitude, useful for the same
overfit/underfit check.diagnostics$target_cal (iTMLE only)One row per (fold, outer iteration, inner iteration) of the Luedtke targeting loop. Tracks:
Useful for spotting a targeting loop that is not converging (EIF magnitude not shrinking, or coefficients oscillating). If this happens under the actual policy, it usually points to weight-driven instability in the fluctuation step.
diagnostics$sl_summaryPer-fold, per-time, per-component SuperLearner weight table. Consistently zero-weight learners can be pruned from the library on the next run — this is a legitimate feedback loop back into the tuning step.
density_ratio() → weight_diagnostics() +
policy_change_summary → pick trim, revise
policy or sl_g if weights or shift look unreasonable.sdr() (or itmle(), or
qreg()) → branch_cal_summary() → revise
sl_remain / sl_death /
sl_recursive / sl_y (and for iTMLE,
sl_tmle) until each branch calibrates.sdr() / itmle() under the
intervention policy.recursion_diag (and target_cal for
iTMLE) for sanity; use sl_summary to prune dead learners
for the next run; use ic_df via contrast() for
downstream comparisons.