Longitudinal TMLE#

What this solves#

Treatment is given more than once, and what happens between the doses matters. A patient’s lab value at month three both responds to the first dose and decides the second. Adjusting for that lab value blocks the confounding of the second dose and also blocks part of the first dose’s effect. Not adjusting for it leaves the second dose confounded. No single regression can do both.

This is time-varying confounding, and it is the reason a longitudinal question is not a point-treatment question with more columns. Longitudinal TMLE estimates the mean outcome under a treatment plan followed at every node, by iterating a regression backward through the nodes.

your situation

what this method buys

what it costs

repeated treatment with time-varying confounders

the mean outcome under a plan, identified by the g-formula and estimated as a plug-in

one regression per node per regimen, and positivity is now a statement about a cumulative product

units drop out over time

censoring enters the same cumulative product as treatment, and each node’s regression uses only its uncensored followers

a censoring model per node

the plan depends on the history

fixed, rowwise dynamic rules receive the history available at their node, and no static plan can express them

the fixed rule is part of the estimand. A rule learned from the same sample needs additional inference that this estimator does not provide

the plan draws the arm at random

a known policy \(q_t(a \mid H_t)\) at a node, with the integrated influence curve

the policy is part of the estimand and must be fixed before the fit

you want a survival curve

the same recursion, seeded at the horizon, reports cumulative risk at each horizon you name

each horizon is its own backward pass. The cost is quadratic in the node count

you want a survival curve that never decreases

survival_model="hazard", targeting="one_step" reads every horizon from one set of targeted hazards. isotonic=True projects any curve onto a non-decreasing one

the hazard model needs one categorical baseline treatment and no covariate after baseline. A projected level has a valid pointwise interval only where the true curve strictly increases

several causes of failure compete

cause-specific cumulative incidence, with the competing causes left alone

that is a total effect. Eliminating the competing event is a different question, and is refused by name

you want the effects summarised across regimens

a working model over regimen and horizon cells

see MSM projections

Reach for point-treatment TMLE when the exposure is measured once. Reach for IPSI when the plan multiplies each node’s odds of treatment. Reach for SequentialGComputation and SequentialIPW for the plug-in and weighting baselines of the same target. Their intervals hold only under correctly specified GLM models. Reach for ParametricGFormula for the forward-simulation form of the same target and for a fixed or rule-set continuous dose. Its interval holds only under correctly specified models.

Collaborative TMLE and DR-TMLE have no longitudinal derivation, and available_methods() says so before any model is fitted.

A worked applied analysis is in the sequential pricing tutorial. It runs two single regressions of the same data as controls. One omits the engagement and one holds it fixed, and both miss the policy effect. Time-to-event outcomes is the companion tutorial for the event-process extensions below.

The algorithm as implemented#

End-of-study regimen means#

Let \(L_t\) be the time-varying history, \(A_t\) the treatment, \(C_t\) the censoring indicator, and \(Y\) the final outcome. For a regimen \(g^*\), sequential regression works backward from \(Q_{T+1}=Y\):

\[ Q_t(h_t)=E\{Q_{t+1}(H_{t+1})\mid H_t=h_t,\ A_t\sim g_t^*,\ C_t=0\}. \]

The target is \(\psi_{g^*}=E\{Q_0(W)\}\). Its efficient influence function is a telescoping sum

\[ D(P)(O)=\sum_{t=0}^{T} H_t(P)(O)\{Q_{t+1}(O)-Q_t(O)\} + Q_0(W)-\psi_{g^*}, \]

where \(H_t\) is the cumulative product of the regimen-to-observed treatment and uncensoring density ratios through node \(t\).

Each regression is fitted on the units that followed the plan and stayed under observation through \(t\). It predicts for those that did so through \(t-1\), which are exactly the units the previous step is fitted on. That is what makes the recursion close, and a test asserts the two masks are the same set rather than leaving it to be read off a paragraph.

The untargeted substitution estimator does not generally solve the efficient influence-curve equation. LTMLE therefore gives each node a loss-weighted logistic intercept submodel,

\[ \operatorname{logit} Q_t(\epsilon) = \operatorname{logit} Q_t + \epsilon , \]

fitted with loss weight \(H_t\). Its score is the \(t\)-th term of the sum above, so solving all \(T\) of them makes the fit solve \(P_n D = 0\). The recursion carries the targeted prediction forward, not the initial one, so a residual left by one node is regressed away by the next instead of accumulating.

Bang and Robins (2005) supplies the sequential-regression foundation. Van der Laan and Gruber (2012) gives longitudinal TMLE for multiple intervention points. Chaffee and van der Laan (2012) covers fixed rowwise dynamic rules. See the longitudinal references. Implementation: longitudinal/sequential.py, longitudinal/regimen.py, and longitudinal/estimator.py.

Finite-sample choices that are part of the algorithm#

Four choices are algorithm rather than notation. Each one changes the finite-sample answer, and no exact law can see any of them, because every fluctuation coefficient is zero at the truth.

choice

what cleverly does

what the alternative would be

where the bound is applied

the raw treatment and censoring factors are multiplied first, and each cumulative prefix is then truncated

bounding each factor before multiplication, which is a different regularisation

where the loss weight sits

\(H_t\) is the loss weight of the logistic submodel

putting \(H_t\) in the submodel instead, which solves the same score along a different path

what the recursion carries

the targeted prediction

the initial prediction, which accumulates residuals

how a categorical node is read

one probability per declared level, selected by the raw label assigned on that row

a binary complement shortcut, which is wrong above two arms

Each node owns its own level set and dense encoding, so labels and level counts may differ over time. History designs contain the dense codes. Plans, settings, and errors retain your labels. Every mechanism training fold must contain every observed level. A missing level is refused before fitting, because probability-column alignment cannot identify an arm that is absent from that training law.

The split reads no treatment, so it cannot deliver that property. The fit checks the realized draw instead. preflight_mechanism_support runs once, over every node, before the first learner. It checks the first node at any level count under cross-fitting, and a later node from three levels up.

g_bounds defaults to the explicit fixed pair (0.01, 1.0), matching R ltmle. It is a heuristic convention. It is not an automatic procedure and not a derived rate. It does not depend on the row count, the effective sample size, the fitted probabilities, or the follow-up depth. A cumulative path probability shrinks with depth, so falling below 0.01 does not by itself prove a node-level positivity failure. Clipping can also replace every scored row and make the clever covariate constant. res.diagnostics.support().to_frame()["share_truncated"] reports the effect per regimen and node.

res.diagnostics.truncation_curve(bounds=...) repeats this finite-sample choice over an explicit grid. A scalar b resolves to the cumulative lower-only pair (b, 1). An explicit (lower, upper) pair keeps both limits. It never uses the point-treatment scalar convention (b, 1 - b).

Each grid point keeps the fitted treatment and censoring predictions fixed. It recomputes their bounded cumulative prefixes and reruns the complete backward recursion. It refits every bound-dependent outcome or pseudo-outcome regression and every targeting update. Reusing an earlier-node prediction would change the next regression’s response and is outside this contract.

The result is a descriptive ordinary LTMLE point estimate at each fixed bound. It changes the finite-sample estimator, not the causal estimand. It provides no standard error, interval, preferred bound, selection correction, or positivity verdict. The source audit records the algorithm provenance and the boundary of that claim.

This replay keeps the realized data, resolved plans, folds, weights, clusters, and parameter structure fixed. A clustered replay keeps the whole-cluster folds of its fit, and its variance stays cluster-summed.

A cross-fitted replay rebuilds the out-of-fold mechanism pair at each bound. The pooled fluctuation divides by that pair alone, and the untargeted fold regressions read no mechanism. The result must retain a replay recipe, and that recipe’s outcome and pseudo-outcome learners must be cloneable. Replay-only unavailability states the random_state rule each learner must satisfy, and lists every code that makes the operation unavailable. Truncation stability holds the frame schema and the score-cell counting rules.

Before evaluating the requested grid, replay at the fitted pair must reproduce every retained estimate, regimen fit, and MSM fit exactly. This preflight also runs when the grid omits that pair. A mismatch makes the operation unavailable instead of reporting a different fitted procedure.

Targeting options at each node#

LTMLE accepts the four targeting options of point-treatment TMLE: stopping=, step_bound=, step_search= and iterate=. Set them on LTMLE, or through Targeting on a longitudinal TMLEMethod. Each default runs the shipped node solver unchanged. The solver applies the options at each node of each regimen.

item

rule

units \(n_u\)

the distinct clusters of the panel when cluster= is set, else the rows. The solver receives every row, and a unit that does not follow the regimen adds zero. So the span and \(n_u\) are those of the panel

msm=

one stacked solve over the regimen cells. The unit codes are tiled, so a subject counts once across its cells

cross-fitting

the pooled targeting pass applies the options once over every fold

repeated outcomes

each horizon applies the options at its own nodes

long panels

the same node solver, so each node applies them

the one-column node

the node with one clever covariate. Under step_bound= its direction is \(\operatorname{sign}(s)\)

survival_model="hazard"

refuses iterate=, stopping= and step_bound=. See hazard-based survival targeting

fluctuation="linear"

refuses all three, as for a point fit

truncation replay

the replay recipe repeats the options, so a replay reruns the same node solve

A fit under a declared rule prints the pinned residual line in res.summary(). When a node misses its rule, the pinned failure line replaces the shipped warning. It names each node over its threshold once for each stacked solve. A LongitudinalResult has no score_check(), so the summary line is its report.

tests/unit/test_stopping_rule_longitudinal.py computes the threshold and the ratio of every node by hand with pandas. It covers a single fold, cross-fitting, survival, competing risks, every repeated-outcome composition, a long panel, clusters and stacked MSM nodes. The crafted-node witness uses confident-wrong offsets. The shipped test takes 3 iterations there and the rule takes 1, and the estimates differ by more than \(10^{-4}\). Three mutation controls fail the tests. They drop the cluster code, the span or the MSM tile. tests/unit/test_step_bound.py checks every node against a SciPy walk from the node’s own pseudo-outcome. tests/unit/test_best_iterate.py covers the best iterate per node.

Survival and competing risks#

Time-to-event outcomes works this section applied.

An outcome sequence represents an absorbing event process. Which population each node’s regression is fitted on is the whole of what changes, and it is the one thing here that is easy to get backwards. The recursion is seeded at the horizon with \(Q_{k+1}=0\) and carries back

\[ Z_t = Y_t + (1 - Y_t)\,Q^*_{t+1}, \]

fitted on the units at risk entering \(t\), which means event-free through \(t-1\). That is one node earlier than the censoring factor runs to. A unit that has the event at \(t\) is in node \(t\)’s regression, because it is the observation that the event happened. It is not in node \(t+1\)’s. So the identity the end-of-study recursion closed on generalises rather than holds:

at_risk(t + 1) == following(t) & event-free at t

For competing risks the pseudo-outcome carried back is

\[ Z_t = \mathbb{1}\{\text{cause } j \text{ at } t\} + \mathbb{1}\{\text{no event at } t\}\,Q^*_{t+1}, \]

a cause-specific numerator against an all-cause survival factor. A unit that left through a competing cause contributes a zero and carries nothing forward. It is no more available to have this cause’s event than one that already had it.

curve(scale="survival") reports the complement of a risk curve. It mirrors a level interval and it negates a contrast interval. A fit that declares two or more causes refuses this view, because one minus a cause-specific incidence is not all-cause survival. A fit that declares one cause reports the view, because the sum over the causes is that one incidence. incidence_total() sums the cause-specific influence curves and reports their joint standard error.

On a fit whose status supplies no inference, incidence_total() names its std_err column plugin_std_err, a diagnostic. On such a fit, curve() renames its three spread columns as to_frame() does, and it adds an inference column (inference status).

Two vocabularies describe a row of the curve. Each vocabulary gets its own column.

column

values

what it says

scale

level, difference

the word to_frame() uses for the same idea

view

risk, survival

the view the caller requested

estimand

a parameter name

the name of the quantity the row reports

parameter

a key of the result

the estimate the row derives from

On the survival view a level row reports the name survival_regimen[...]. The fit holds no estimate under that name. Read the parameter column to get the estimate behind any row.

What does not change is the positivity story. Being event-free is part of the history and not an intervened node, so it enters the indicator of the clever covariate and never its denominator. The cumulative product is still over the \(2T\) treatment and censoring factors. The causes share every nuisance fit and differ only in what is regressed, so \(J\) causes cost \(J\) backward passes and one mechanism.

Two reductions pin these as generalisations rather than as second estimators. A fit whose event can only happen at the last node reproduces the end-of-study fit bit for bit. A fit declaring a single cause reproduces a single-event survival fit bit for bit. Stitelman, De Gruttola and van der Laan (2012) is the survival implementation reference.

Repeated outcomes#

A repeated outcome is measured at every node and is not absorbing. A unit with \(Y_1 = 1\) can have \(Y_2 = 0\). Declare it with outcome=Repeated(["Y1", "Y2", "Y3"]). The node order is

\[ W,\; L_1, A_1, C_1, Y_1,\; L_2, A_2, C_2, Y_2,\; \ldots,\; L_T, A_T, C_T, Y_T. \]

The parameter is the curve of means \(E[Y_h^d]\), one for each plan \(d\) and horizon \(h\). A fit reports ey_regimen[d @ t=h] and ate_regimen[d vs ref @ t=h].

The construction is a reduction to the end-of-study fit. The container puts \(Y_s\) first in the covariate block of node \(s + 1\), before the covariate screen. Each later treatment, censoring and outcome regression can then read it. At horizon \(h\) the fit seeds the recursion with the observed \(Y_h\) and runs the end-of-study recursion over nodes \(1, \ldots, h\). That fit is the end-of-study fit of the panel truncated after \(h\), with \(Y_h\) as its outcome. The base for each horizon is the end-of-study derivation of Bang and Robins (2005) and van der Laan and Gruber (2012). Kim, Kennedy and Naimi (2021, arXiv v3 preprint) discuss the repeated-outcome setting. This implementation does not rely on that preprint.

item

value

history at node \(t\)

\(W, L_1, Y_1, L_2, \ldots, Y_{t-1}, L_t\) and the earlier treatments

seed at horizon \(h\)

the scaled \(Y_h\) on the rows uncensored through \(h\)

missing \(Y_t\) on a row uncensored through \(t\)

refused. Encode it as \(C_t = 0\)

outcome scale

one scale over the observed values of every node, so a continuous curve has one scale

family

resolved over the observed values of every node

influence curve at \((d, h)\)

the end-of-study influence curve of the truncated panel

joint band

the multiplier band over the stacked curves of every reported \((d, h)\)

working model, msm=

a design over \((d, h)\) cells. Each cell seeds its own \(Y_h\)

full-refit bootstrap

shown with the label experimental. No support row lists bootstrap for this outcome

The remainder at each horizon is the end-of-study product remainder. The stack over horizons adds no remainder term. The interval at \((d, h)\) is therefore the end-of-study interval of the truncated panel, under the same inference statuses.

composition

status

static, dynamic and known stochastic plans, modified treatment policies

fits. A rule may read \(Y_{t-1}\)

cross-fitting, clusters, weights, a held treatment, known mechanisms

fits

msm= over \((d, h)\) cells

fits

stopping=, step_bound=, step_search=, iterate=

fit. Each horizon applies them at its nodes (targeting options at each node)

IPSI

fits. Each horizon is the end-of-study fit of the truncated panel (below)

SDR

refused before any learner, until X32b

competing causes

not declarable. A cause mapping declares an absorbing outcome

rmst(), rmtl(), incidence_total(), ratio(view="survival")

refused. A mean of a repeated outcome is not a cumulative risk

curve()

scale="mean", the default for this outcome. "risk" and "survival" are refused

This construction does not cover a recurrent-event count process or truncation by death. If death is encoded as censoring, the parameter is the mean in a population where death is prevented.

estimator

status

source for horizon \(h\)

LTMLE

fits

Bang and Robins (2005); van der Laan and Gruber (2012)

IPSI

fits

Kim, Kennedy and Naimi (2021), Theorems 3.1, 4.1 and 5.1

SDR

refused until X32b

none yet

The allowlist REPEATED_OUTCOME_KINDS holds the kinds in this table that fit. tests/unit/test_repeated_outcome_subclasses.py fits every estimator subclass and requires either the reduction or the refusal.

A level of \(E[Y_h^d]\) is checked against the range of its own horizon. The outcome scale pools every node, so a level of \(Y_2\) outside the range of \(Y_2\) and inside the range of \(Y_1\) would pass the pooled range. The fit warns with ParameterSpaceWarning and names the range of that horizon. A declared q_bounds replaces the derived range (tests/unit/test_repeated_outcome_container.py, W16).

tests/unit/test_repeated_outcome.py checks the reduction at every horizon for 13 compositions of LTMLE, and for 11 of those 13 compositions of IPSI plus six incremental ones: the estimate, the influence curve, every node regression and the cumulative mechanism, at rtol=1e-12. The other two IPSI compositions refuse with the end-of-study message. tests/unit/test_influence_gateaux_repeated.py checks the influence curve against the complex-step Gateaux derivative of the g-formula on two exact laws in tests/discrete_law_repeated.py. One law is binary. The other has three values per node. Ten mutation controls fail these tests, among them the absorbing seed \(\max_{s \le h} Y_s\), a history without \(Y_{s}\), and one seed at \(Y_T\) for every horizon.

Known stochastic policies#

A plan node can hold a known policy density instead of a label or a rule. The unit then draws its arm at that node with probability \(q_t(a \mid H_t)\). Write the node as a Stochastic(q, name, density_kind="known") inside a DynamicRegimen:

import numpy as np
import pandas as pd

from cleverly.interventions import Stochastic
from cleverly.longitudinal import DynamicRegimen


LEVELS = ["high", "low", "standard"]  # each node's levels, sorted


def first(history):
    return np.tile([0.5, 0.25, 0.25], (len(history), 1))


def second(history):
    # Draw the first node's arm again with probability one half.
    drawn = pd.get_dummies(history["A1"]).reindex(columns=LEVELS, fill_value=0)
    drawn = drawn.to_numpy(dtype=float)
    return 0.5 * drawn + 0.25 * (1.0 - drawn)


mix = DynamicRegimen(
    "mix",
    (
        Stochastic(first, "first", density_kind="known"),
        Stochastic(second, "second", density_kind="known"),
    ),
)

The density function receives the policy frame: the history \([W, L_1, \ldots, L_t]\) and the earlier treatments as labels. It never receives \(A_t\), a censoring indicator, or a later column. It returns one column per level in treatment_levels[t - 1] order, or a dataframe whose columns are the levels. resolve_plans evaluates it once, before any learner. A density that is one-hot on every reachable row resolves as the rule it equals, and config.policy_point_mass_nodes names that node.

Write \(\pi_t(a \mid h)\) for the node’s intervention density. The table gives each part of the estimator at the two kinds of node.

part

label or rule node

policy node

\(\pi_t(a \mid h)\)

\(1\{a = d_t(h)\}\)

\(q_t(a \mid h)\)

rows that stay on the plan

the rows whose observed arm is the assigned arm

the rows whose observed arm has \(q_t > 0\)

node regression

covariate history, plus the arms of earlier policy nodes, on the rows that stay on the plan

one fit with the current arm and the arms of earlier policy nodes as columns, predicted at every level

value carried to node \(t - 1\)

\(Q^*_t(H_t)\)

\(\sum_j q_t(j \mid H_t)\, Q^*_t(j, H_t)\)

clever covariate

\(1 / \operatorname{bound}(\prod_{s \le t} g_s c_s)\)

\(\prod_{s \le t} \pi_s(A_s \mid H_s) / \operatorname{bound}(\prod_{s \le t} g_s c_s)\)

fluctuation

intercept, offset \(Q_t\) at the observed arm, loss weight \(w R_t\)

the same, and every level’s prediction moves by the same \(\epsilon_t\)

The influence curve is the integrated one:

\[ D^*(O) = \bar Q^*_1(H_1) - \Psi + \sum_{t=1}^{T} R_t\,\{Z_t - Q^*_t(A_t, H_t)\}, \]

with \(\bar Q^*_1\) the carried value at node 1 and \(R_t\) the clever covariate. The fluctuation is the ltmle::UpdateQ placement. It is also the lmtp placement in R/tmle.R, where the natural prediction is the offset and the shifted prediction takes the same coefficient.

The mechanism is evaluated at the observed history at a policy node. g_bounds bounds the cumulative denominator only, with its CalcCumG meaning. The numerator is at most one, so \(R_t \le 1/\text{lower}\). A side effect follows: when \(\prod q\) and \(\prod g c\) are both below the lower bound, the bound shrinks a moderate ratio. Take \(\prod q = \prod g c = 0.001\) and a lower bound of 0.01. The ratio is then 1 unbounded and 0.1 bounded. share_truncated counts denominator truncation, so a truncated cell need not carry a large ratio.

The base result is Díaz, Williams, Hoffman and Schenck (2023). The table gives each step of the extension and the condition it reads.

item

statement

base result

Section 2, journal page 849, admits a random regime \(d(a_t, h_t, \varepsilon_t)\) whose randomizer is drawn independently across units and of \(U\), with a law that does not depend on \(P\). The section places \(\varepsilon_t\) in \(L_t\). Theorem 3, page 853, then covers the estimator that records the randomizer

step

the integrated estimator above. Its curve is \(E[D_{aug} \mid O]\), the conditional expectation of the Theorem 3 curve given the data without the randomizer. Its remainder is the conditional expectation of the Theorem 3 remainder. The Eligibility rule lists this step as the chain rule with a known weight

conditions met by construction

no \(Q_t\) reads a randomizer; each ratio reads its own node only; the randomizers are independent across nodes; \(q_t\) reads \(H_t\) only

identification

sequential exchangeability, consistency, and positivity \(g_t c_t > 0\) wherever \(q_t > 0\). The policy history excludes \(A_t\), so Assumption 2 suffices and Assumption 3 is not needed

inherited conditions

the Theorem 3 rate condition, bounded ratios, and a policy density known and fixed before the fit (density_kind="known")

The same construction runs on every composition the fit supports. The table gives each one and its evidence.

composition

what changes

evidence

cross-fitting

each fold’s untargeted recursion carries the fold’s policy mean. The parent stitches the held-out per-level predictions, and the pooled fluctuation moves them

tests/unit/test_pooled_longitudinal_policy_targeting.py recomputes the stitch by hand. Three mutations fail it or the pooled score: a per-fold fluctuation, an observed-arm carry in the fold recursion, and per-level blocks moved to other rows

censoring, survival, competing risks

the pseudo-outcome is unchanged and composes the carried policy mean

exact-law curves on the survival and competing-risk laws in tests/unit/test_influence_gateaux_longitudinal_policy.py

observation weights

the weight enters every regression and the loss weight, never \(R_t\)

the weighted exact law in the same file

clusters

the split, the inner groups, the cluster-summed curve and the status are target-agnostic

tests/e2e/test_ltmle_multivalue.py

msm= over policy cells, in sample and cross-fitted

the stacked fluctuation moves each policy cell’s per-level predictions along that cell’s block of the design

tests/unit/test_longitudinal_policy_msm.py, at one and at five folds, under the identity and logit links, and over survival cells

contrasts, bands, longitudinal_truncation_curve

none; they read the stacked curves and the frozen plans

tests/unit/test_longitudinal_policy_plumbing.py replays a policy fit under truncation. The simultaneous_coverage cells of the registered study measure the band and its pointwise control

A level that the policy can draw at an at-risk row must also appear among the rows the node is fitted on. Otherwise its design column is all zero, and the regression extrapolates where no residual of the curve can see it. LTMLE.fit refuses that sample before any learner, per outer training fold.

A deterministic rule receives the history frame, which holds no earlier treatment. A rule that must read an arm drawn at an earlier policy node, such as “continue the arm drawn at the first node”, is a one-hot Stochastic node that reads the policy frame. It resolves as that rule.

Two neighbouring targets are outside this section. The table names where each one is covered.

target

why it is not a known policy

owner

a policy that reads the natural value \(A_t\), such as a modified treatment policy

its ratio reads the fitted mechanism, and identification needs Assumption 3

modified treatment policies at a node

a policy density that depends on \(P\), such as an incremental odds tilt

page 850 records that multiply robust estimation “is not generally possible” for such a regime; the curve needs a mechanism-derivative term

incremental interventions over time

tests/unit/test_influence_gateaux_longitudinal_policy.py holds the exact-law evidence. The point estimate and the curve equal the g-formula of tests/discrete_law_longitudinal_policy.py and its Gateaux derivative to \(10^{-10}\), on mixed plans in both orders and on a partial-support policy.

Mutations M1 to M7 each move the estimate or the curve away from the oracle by more than \(10^{-4}\). M8 and M9 each fail the targeting witness by more than \(10^{-4}\): the carried mean against its longhand, and the solved score. A misspecified initial fit gives every node a coefficient above 0.05 and pins the per-level update. A one-hot policy is its rule bit for bit. On an augmented law that records a uniform randomizer, the integrated curve is the mean of the recorded curve over the randomizer, and its variance is smaller.

The registered study stochastic-categorical-ltmle pairs the in-sample fit with R lmtp 1.5.4. lmtp takes one shifted value per unit, so tests/canonical/lmtp_policy_adapter.R realises each policy by copying every unit four times. Copy \(c\) takes its shifted arm from row \(c\) of the node’s allocation table, and every policy probability is a multiple of one quarter. The pre-declaration smoke run required the policy mean to pair within \(10^{-6}\).

Over the 2,000 committed replications its mean paired difference is \(4.9 \times 10^{-11}\), and the largest is \(6.3 \times 10^{-10}\). The low mean and the two contrasts are not exactness pairs, because lmtp pools label nodes over every arm. Their mean absolute paired difference is about \(9 \times 10^{-3}\), and the largest is 0.042. The study page gives every verdict.

Modified treatment policies at a node#

A node of a DynamicRegimen plan can be a modified treatment policy. That policy reads the natural value \(A_t\) and the history. The classes are those of the point-treatment section. Díaz, Williams, Hoffman and Schenck (2023), Theorem 1, identifies the mean under Assumption 3. Equation (3) gives the ratio at a continuous node, and the discrete formula gives it at a categorical node.

node kind

how to declare it

ratio

continuous dose

LTMLE.fit(..., continuous_treatment=["A1", "A2"])

a pooled-hazard density with density_bins bins, or ratio="classifier"

categorical

the default

the discrete formula over the fitted mechanism

vector of categorical columns

a list of columns as one entry of treatment=

the discrete formula over the joint levels

mixed vector, with categorical and continuous columns

a list of columns as one entry of treatment=, with a column of the list in continuous_treatment=

the joint ratio of a JointPolicy: a chain-rule density with ratio="density", or ratio="classifier". See vector nodes with a continuous component

At a continuous node the treatment factor of the cumulative product is the ratio itself. So g_bounds bounds the censoring and categorical factors only, and the fit stores each node’s ratio as node_ratio. A cross-fitted fit (n_folds above one) uses the pooled construction of the other plan kinds. msm= accepts modified treatment policy cells.

The bin count of the density route. A binned density at a fixed bin count is not consistent. A growing count is necessary, and the hazard learner must also be consistent. The bin edges are sample quantiles, so the tail bins are wide, and the binned ratio errs most there. The table gives the error that one draw of the longitudinal-mtp law measured with the exact bin probabilities, so the binning is the only error left.

bins

spread of the influence curve over the efficient one, n = 2,000

the same, n = 8,000

20

1.62

1.72

80

1.14

1.20

160

1.04

1.13

320

0.99

1.00

At a fixed count the excess spread stays as \(n\) grows. It falls as the count grows: at n = 2,000 it is 0.62 at 20 bins, 0.14 at 80 bins and 0.04 at 160 bins. So the default density_bins=None grows the count with the sample: max(20, ceil(2 n^(1/3))), the rate of Scott (1979). An explicit density_bins= is used as given. tests/unit/test_density_bins_consistency.py shows the tail error shrinking with \(n\) under the default, and a mutation that holds the count at 20 fails it. The same file checks the default count through fit_conditional_density and through a TMLE fit at n = 2,000.

The memory of the density route. The pooled hazard design holds about \(n (K + 1) / 2\) records of the covariates and \(K - 1\) bin columns, at 8 bytes each. At the default count it grows as \(n^{5/3}\). The table gives the design at one covariate column.

n

default bins

design

10,000

44

0.07 GiB

100,000

93

3.2 GiB

300,000

134

20 GiB

1,000,000

200

150 GiB

A fit counts the design before it allocates it. When the design exceeds the available memory, the fit raises MethodConfigurationError. The message gives the size and the largest bin count whose design fits in half of the memory. cleverly.learners.density.hazard_design_bytes gives the estimate. A learner copies its design, so the peak can be a multiple of the table.

Two consequences follow. When the outcome regression is wrong, the bin error enters the bias at first order, so double robustness through the density alone needs a ratio error of \(o(n^{-1/2})\), which a histogram does not give. When the outcome regression is right, a coarse count inflates the influence curve, and the intervals are conservative. When efficiency matters, use ratio="classifier" with a flexible classifier, or set density_bins= higher.

regimens= takes the policy as a plan node, and the policies= keyword stays refused by name: regimens={"+0.5": DynamicRegimen("+0.5", (Shift(0.5, cap=4.0),) * 2)}.

The sequential pricing tutorial fits a price rise and a price cut at three nodes under a declared band. It reads the support of larger rises and compares the fit with two single regressions.

Evidence: tests/unit/test_influence_gateaux_longitudinal_mtp.py checks the estimate and the curve against the g-formula of tests/discrete_law_longitudinal_mtp.py and its Gateaux derivative. tests/unit/test_longitudinal_mtp_targeting.py holds the mutation controls, and tests/unit/test_longitudinal_mtp_compositions.py covers survival, msm=, cross-fitting and the refusals. The registered study is longitudinal-mtp. Its mechanism-only cells read oracle bins that grow as \(n^{2/3}\), 320 at n = 2,000, for the reason above. So they test the targeting with a nearly exact ratio and not the default density.

Limits of the continuous-policy evidence#

The registered studies mtp-boundaries, mtp-pricing and mtp-tuning test the continuous policies of this section. Each row below states a limit that the studies publish.

limit

what it means for use

a continuous outcome needs Targeting(q_bounds=(lower, upper))

a longitudinal fit with a modified treatment policy refuses a continuous outcome with q_bounds=None. A revenue outcome is unbounded and positive, so declare its known support. The fit never clips an outcome

the learned-density ratio arms of mtp-pricing publish as reporting

the screen read SE ratios above 1.05: 1.052, 1.105 and 1.079 at tau 2, 3 and 5

the screened ratio learner is the quadratic logistic classifier

the study uses it at every tau, with ratio="classifier"

the cross-fold leak pair classifier_split is not registered

the pilot read a necessity power of 0.0002, so the pair cannot discriminate. The fast tier shows the effect of the leak only. In tests/unit/test_classifier_ratio_controls.py, test_t_c1_no_unit_trains_the_model_that_predicts_it shows no leak under unit folds. test_m_c1_folds_over_stacked_rows_leak shows a leak under row folds

Vector nodes with a continuous component#

A node can hold several treatment columns. A list of columns is one entry of treatment=, and continuous_treatment= names the columns that hold a dose. A node with a dose and a categorical column is a mixed node. Díaz, Williams, Hoffman and Schenck (2023), Section 2, let the treatment at a node be a vector \(A_t = (C_t, D_t)\). Their Theorem 1 gives the mean two readings for a vector node. Each reading needs its own exchangeability assumption at every node.

reading

the history at each node holds every common cause of

source

modified treatment policy effect

the vector, the outcome, and the later covariates and treatments

Theorem 1 (ii) and Assumption 3; Williams & Díaz (2025)

stochastic intervention (LMTP-SI)

the vector and the next covariates, which at the last node are the outcome

Theorem 1 (i) and Assumption 2

Identification.interpretations records both readings (tests/unit/test_mtp_interpretations.py).

LTMLE(regimens, ratio="classifier", ...).fit(
    frame,
    outcome="Y",
    treatment=[["flag1", "dose1"], ["flag2", "dose2"]],
    baseline=["W"],
    continuous_treatment=["dose1", "dose2"],
)

LTMLE and SDR fit a mixed node. Each regimen sets the node with a JointPolicy, which the point-treatment section defines. The policy frame at the node holds the history. Each map reads the natural values of the components as the table of that section states.

item

rule at a mixed node

ratio

the joint ratio \(g^d / g\) at the observed vector. It sits in the numerator of the cumulative product, as the ratio of a continuous node does

ratio="density"

the chain rule: the categorical mechanism, then a pooled-hazard density for each dose in order. A sum over the categorical preimages and Equation (3) for each dose give the induced density. g_bounds bounds the categorical factor in the running denominator

ratio="classifier"

the stacked classification of Section 5.4. The design holds the history, an indicator for each categorical component and the raw doses. The classifier cannot split the ratio, so g_bounds reaches no factor of the node

history of a later node

both components of an earlier node, with a dose as a dose column and a categorical component as its own drop-first indicators

a flag with a dose

the dose map of one mixed node reads the natural flag. Two consecutive nodes, a flag node and then a dose node, read the flag that the first node assigned. With an identity flag map the two readings coincide, and the fits are equal (W10)

What composes. Each row has a witness in tests/unit/test_longitudinal_mixed_node.py.

composition

status

censoring, whose retention reads both components of an earlier node

supported (W9)

survival outcomes and one-cause competing risks

supported. A one-cause fit equals the survival fit

weights, clusters with id=, cross-fitting

supported. A cluster stays whole, and the folds split units

msm= over joint-policy plans

supported. A saturated working model equals the separate fits

truncation replay, bootstrap, subset, dumps and loads

supported

SDR

supported on both routes, in sample and with folds that are each an exact copy of the law

a known randomizer on the JointPolicy or on one component

supported (W11, tests/unit/test_vector_component_randomizer.py)

RiskRatioTilt on a binary component

supported. It joins the product randomizer

a declared support for a dose, continuous_treatment={name: Support}

supported. The support is given the history, the categorical components and the earlier doses of the node. The fit checks each branch at the components it assigns (tests/unit/test_mtp_vector_support.py)

Scope and refusals lists what a vector refuses and gives the reason for each refusal.

Evidence. tests/unit/test_longitudinal_mixed_node.py checks the estimate and the influence curve of every plan on the two-node mixed law of tests/discrete_law_longitudinal_mtp.py against the g-formula and its complex-step Gateaux derivative (W3). The law gives each node a flag and a dose on the grid \(\{0, 1, 2\}\). W4 sets the flag and gates the dose on the natural flag. Its law moves the estimand by more than \(10^{-2}\) between that reading and the reading of the assigned flag. W5 checks that the later history holds both components.

tests/unit/test_longitudinal_mixed_node_mutations.py holds the mutation controls, and each control moves a witness by more than \(10^{-4}\). They drop the flag or the dose from the classifier, read the assigned flag, drop the earlier flag from the history, read the kind off the first component only, and drop a component from the censoring or outcome design. tests/unit/test_vector_density_route.py repeats the plans on the density route and adds an ordered plan on a law with two doses per node. The registered study is vector-mtp.

One baseline treatment held over the nodes#

A point-treatment survival analysis has one treatment decision, at baseline, and an event process over \(K\) visits. Declare it with one column name: treatment="A" on LTMLE.fit, LongitudinalData.from_frame and LongitudinalTreatment. Node 1 is the decision. Each later node is an identity node.

item

rule

identity node

its plan value is the observed node-1 value, and its factor is exactly one in the ratio numerator and in the denominator. It fits no model, it is never a policy or modified treatment policy node, and it adds no block to any mechanism or outcome design

node count

a survival outcome gives one node per event column. An end-of-study outcome takes the count from censoring=, else from time_varying=, else it is one. Lengths that disagree raise DataError

regimens

each regimen resolves on node 1, once, on the node-1 history. A static plan that repeats one label over the nodes reads as that label. Any other plan of more than one node raises ValueError before any learner

plans at node 1

an arm, a known rule, a known stochastic policy, or a modified treatment policy (categorical or continuous), as for an ordinary node 1

identification

without \(L_t\), Benkeser, Carone and Gilbert (2018), Section 2.3. With \(L_t\), Díaz, Williams, Hoffman and Schenck (2023), Theorem 1, with the identity policy \(d_t(a_t, h_t) = a_t\) at \(t \ge 2\)

influence curve

the shipped telescoping curve, with \(H_t = 1\{A^d = A\}\,(q_1/g_1) \prod_{s \le t} 1\{C_s = 1\}/c_s\). For an arm or a rule, \(q_1/g_1 = 1/g_1(d \mid W)\), which is \(H_k\) of Benkeser et al. (2018), Section 4

end-of-study outcome

the same held design with the shipped end-of-study recursion. Without \(L_t\) it equals point TMLE with delta= and \(\Delta = C_1 \cdots C_K\), because with saturated nuisances the curve keeps only its last term

report

summary() prints treatment: one baseline decision 'A', held over K node(s). The nuisance report shows LONGITUDINAL_HELD_DECISION in place of a treatment row at each identity node. No support row lists a held baseline treatment with bootstrap, so the percentile interval is shown and carries the label experimental

The wide layout with one treatment column per node gives the same fit when the later columns copy node 1. With cell-mean learners at one fold the copied fit’s later factor is exactly one, so the two agree to the last bit. With a penalized treatment learner the copied fit’s later factor is not one, and the two fits differ.

lmtp with trt of length one evaluates every node’s regression at the shifted treatment. For a policy that is not idempotent, such as a stochastic draw or a - 1, that applies the policy once per node, which is a different estimand. Copied treatment columns with the identity policy after node 1 reproduce the held estimand in lmtp (point-treatment-survival-policies).

The evidence is in the table.

test module

what it checks

tests/unit/test_point_survival_law.py

the estimate against the g-formula of tests/discrete_law_point_survival.py, the curve against its Gateaux derivative, and the held fit against the copied-column fit

tests/unit/test_point_end_of_study_law.py

the same three identities on the end-of-study law, and the point-TMLE reduction

tests/unit/test_point_policy_mtp.py

a policy and a modified treatment policy at node 1. A mutation that makes the identity nodes policy nodes fails the Gateaux check

tests/unit/test_point_survival_mutations.py

the other witnesses and mutations

Time-to-event input#

LongitudinalData.from_time_to_event and the design TimeToEvent read one row per unit: a time, an event code (0 for censored), one baseline treatment and the baseline covariates. Node \(k\) covers the interval \((g_{k-1}, g_k]\) of a grid \(0 = g_0 < g_1 < \dots < g_K\). The rules follow the visit structure of Benkeser, Carone and Gilbert (2018), Section 2.1.

unit

nodes

an event of cause \(j\) at \(T \le g_K\)

the event at the node \(k\) with \(g_{k-1} < T \le g_k\)

censored at a grid time \(g_j < g_K\)

observed through node \(j\), then censored

an event or a censoring after \(g_K\), or censored at \(g_K\)

event-free and observed through node \(K\), the administrative end. This is the survtmle t0 convention (makeDataList.R)

An event and a censoring at one grid time are ordered event first, as survtmle, tmle3 and MOSS order them. grid=None needs integer times and uses \(g_k = k\) up to the largest event time. Each regimen and cause costs \(K(K+1)/2\) sequential regressions, so a default grid on day-level times can hold thousands of nodes. summary() prints the grid. On a gridded fit, horizons= and the tau of rmst take grid times, curve() reports the grid time in its time column, and to_frame() adds a time column. A parameter name keeps the node index. continuous_treatment=True declares the treatment a continuous dose. Node 1 then fits one conditional density, and a regimen is a modified treatment policy.

A censoring time strictly between two grid times is refused. Such a time cannot be ordered against the events of its interval, and both conventions are biased. Declare the grid at the visit times.

Take one interval \((0, 1]\), an exponential event time with rate 0.3 and an independent exponential censoring time with rate 0.2. The truth is \(P(T \le 1) = 0.259\). The rule that censors the unit inside the interval estimates 0.280. The rule that observes it through the interval estimates 0.236. test_both_off_grid_censoring_conventions_are_biased derives the three numbers in closed form.

Each refusal of the input is raised before any learner.

trigger

type

a time that is missing, not finite, or at or below zero

DataError

grid=None with a time that is not an integer

DataError

a grid that is not strictly increasing, positive and finite

ValueError

an event code that is missing, negative, not an integer, or not named by causes=; a cause with no event at or before \(g_K\)

DataError

a horizon or an RMST time off the grid, or past \(g_K\) on a declared grid

ValueError

time_varying= on TimeToEvent

CapabilityError, code api_redirection

a censoring time between two grid times

DataError

to_time_to_event() returns each unit’s grid time and event code. It is the inverse map on a law whose times sit on the grid. tests/unit/test_time_to_event_input.py checks one refusal per row and the round trip on two grids. It also checks the long fit against the wide held fit. Floor binning and the censor-first map are its mutations, and each one misses the truth.

Long-panel input#

LongitudinalData.from_long_panel and the design LongPanel read one row per unit and decision time. A row holds the covariates measured before its decision, the decision, and the censoring indicator of the node. The reader reshapes the rows into the wide arrays and calls LongitudinalData.from_frame, so the wide checks apply unchanged. The estimand, the recursion, and the influence curve are those of the wide layout. One unit is one observation, and cluster= maps to the cluster contract. The targeting options apply at each node, as they do on the wide layout.

item

rule

node

the \(t\)-th decision time, counted from 1. times= declares the grid in time order and cannot coarsen it. Without it the reader sorts the times, which must be numbers, dates or durations

generated column

"column@t" for the treatment, each time-varying column, the censoring column, each event column and each known probability column. PanelLayout keeps the original time labels

unit order

the sorted unit identifiers. Unsorted input gives the same container

history

the baseline columns and the time-varying columns of nodes 1 to \(t\). Every \(L_t\) block adds columns to the history design of every later node

leaving the study

censoring 0 on the last row of a unit that left, or the event row. A row after that row is refused

event and censoring

an event value on a row whose censoring is 0 is refused on both layouts, because the order at a node is \(A_t, C_t, Y_t\)

leading missing values

a time-varying column with no value at the first nodes starts at its first recorded node. PanelLayout.omitted records the nodes left out

report

summary() adds a long panel: line from PanelLayout.describe()

inverse

to_long_panel() returns one row per unit and node in the study. It normalizes the weights to mean one, leaves a screened-out covariate missing, and omits the known probability columns

A time-varying column is measured before the row’s decision, and the declaration states it. A rule at node \(t\) reads the columns of nodes 1 to \(t\) through each_node. The reader refuses what the data can show: a unit-level column that varies, a covariate that copies the outcome, and a time-varying column that stops being recorded. The scope and refusals page lists them. The registered evidence uses two decision nodes.

X39 ships the bootstrap options for three to five decisions as experimental. Its studies are pending.

tests/unit/test_long_panel.py checks the round trip on irregular censoring and unsorted input, one refusal per row of the table, and the equality of LTMLE and SDR fits on the adapted panel and on the equivalent wide frame. The LTMLE comparison also checks a nonzero targeting update, so the equality is not between two plug-in estimates.

A censoring node with no censoring#

A censoring node at which no eligible unit is censored has the fixed factor one and fits no learner. The eligible rows are the rows at risk before the node with positive weight. On the long layout node 1 always has this shape, because every censoring time is at least \(g_1\). A cross-fitted training fold with no censored unit predicts one, its empirical rate. The nuisance report shows LONGITUDINAL_NO_CENSORING, or LONGITUDINAL_NO_CENSORING_IN_FOLD beside the node’s row. survtmle sets G_dC = 1 at t = 1 and has a noCens branch for the same case. Before this rule a standard classifier raised on the constant target. tests/unit/test_no_censoring_node.py removes the rule and sees that error return.

A node with no event#

A node regression whose fitted rows all hold 0, or all hold 1, is that value, and no learner runs there. A grid node at which no follower of a regimen had the event has this shape, with hazard zero. Zero is the maximum-likelihood hazard, and survtmle reads such a node the same way. The fluctuation at the node is skipped, because the score is exactly zero at the initial fit. The rule applies to every outcome and pseudo-outcome node, on both layouts.

case

result

no follower had the event at every node through the horizon

risk zero and a zero influence curve. The parameter takes the constant_node_plugin status, so it reports no interval or p-value

a node of the recursion read a constant, at a horizon with events at other nodes

the point estimate stands. The node’s term of the influence curve is zero, so the parameter takes the constant_node_plugin status too

the followers of one outer training fold all hold 0, or all hold 1

that fold’s regression is the value. The pooled fluctuation then bounds its predictions as it does any prediction

msm= with a cell whose followers all hold 0, or all hold 1

CapabilityError. The pooled logistic fluctuation would move the value into its bounds

a pseudo-outcome that holds one value inside \((0, 1)\), such as an intercept-only regression carried back

not read by this rule. The learner fits it and the fluctuation moves it, so a fit that ran before the rule keeps its numbers

A node’s term of the influence curve is identically zero when the node reads a constant. The true outcome variance there can still be positive, so the plug-in standard error leaves that node’s variance out. A risk of zero would report a standard error of zero, and a contrast against it would carry the other arm’s variance only. Each parameter whose recursion reads such a node therefore takes the constant_node_plugin status (inference status). The other parameters of the fit keep theirs.

output

what it shows

the fit

one DataWarning with the count of such parameters, the first 10 names, and the reason. constant_node_parameters(result) lists them all

summary()

the estimate and a starred plug-in spread, with “not reported” for the interval and the p-value, and the reason under the table

curve() and to_frame()

the status in the inference column, NaN in the inferential spread columns, and the plug-in spread in the plugin_ columns

incidence_total()

each total takes the status of the incidences it sums, with the same columns as curve()

a contrast, rmst() and rmtl()

the status, when they read such a parameter

the simultaneous band

the other parameters only. A CausalStudy band narrowed to RegimeMean or RegimeContrast leaves them out too

assess()

the nuisance item adds the status note, with the count of such parameters

X54 part (i) holds an interval at such a node at a fixed law, and F31 keeps the rare-event regime. The nuisance report shows LONGITUDINAL_CONSTANT_TARGET in place of the node’s row, with the regimen, the cause and the horizon. A cross-fitted training fold that reads a constant while the sample does not adds LONGITUDINAL_CONSTANT_TARGET_IN_FOLD, which names the folds, and the node’s row stays.

tests/unit/test_event_free_node.py checks the zero risk, the status of each flagged parameter, the omission rows and the absent learner fits. One mutation removes the rule and sees the bounded fluctuation move the risk off zero. Another removes the status and sees the interval \([0, 0]\) return. tests/e2e/test_ltmle.py checks the fold-level omission, and its mutation removes the in-fold reading and sees the omission go.

Hazard-based survival targeting#

survival_model="hazard" models a survival or competing-risk curve through its discrete cause-specific hazards. The iterated-mean recursion targets each horizon in its own backward pass, so its curve can decrease. A hazard fit reads every horizon from targeted hazards. Its one-step algorithm reports a curve that never decreases. The implementation is longitudinal/hazard.py.

The contract is the table. The symbol \(r\) indexes a regimen, \(j\) and \(l\) index a cause, \(k\) is a node, and \(t\) is a reported horizon.

item

contract

design

one baseline treatment held over every node (treatment="A" or TimeToEvent). The treatment is categorical, the outcome is a survival or competing-risk outcome, and no covariate is measured after baseline. Every other design is refused before any learner (hazard and isotonic refusals)

regimens

a static arm, a known rule \(d(W)\), or a known stochastic policy \(q(a \mid W)\) at the baseline decision. A static arm is \(q = \delta_a\), and a rule is \(q = \delta_{d(W)}\). A modified treatment policy is refused

estimand

\(\psi_{r,j}(t) = E \sum_a q_r(a \mid W) F_j(t \mid a, W)\), the estimand of the held design. Contrasts, ratios, the survival view, rmst and rmtl derive from it as on an iterated-mean fit. No parameter name changes

identification

no unmeasured confounding of \(A\) given \(W\), censoring at random given \((A, W)\), and positivity. Moore and van der Laan (2009), Section 2. Cai and van der Laan (2020), Section 2

hazard

\(\lambda_l(k \mid a, W) = P(\text{cause } l \text{ at } k \mid \text{uncensored through } k,\ \text{event-free before } k,\ A = a,\ W)\). Then \(S(k) = \prod_{s \le k}(1 - \sum_l \lambda_l(s))\) and \(F_j(t) = \sum_{k \le t} S(k - 1)\,\lambda_j(k)\)

initial fit

the sequential pseudo-hazards of survtmle (hazards_estimate.R). Cause \(j\) at node \(k\) regresses its event on the rows at risk with no event of an earlier cause. Then \(\lambda_j = \rho_j (1 - \sum_{i < j} \lambda_i)\). outcome_learner fits each node and cause, and the fit predicts every treatment level for every unit

influence curve

\(D_{r,j,t} = \sum_{k \le t} \sum_l h^{r,j,t}_l(k, A, W)\,R_k\,(dN_l(k) - \lambda_l(k \mid A, W)) + \sum_a q_r(a \mid W) F_j(t \mid a, W) - \psi_{r,j}(t)\). \(R_k\) indicates a unit uncensored through \(k\) and event-free before \(k\)

clever covariate

\(h^{r,j,t}_l(k, a, W) = \frac{q_r(a \mid W)}{g(a \mid W)\,G_k(a, W)}\,1\{k \le t\}\bigl[1\{l = j\} - \frac{F_j(t \mid a, W) - F_j(k \mid a, W)}{S(k \mid a, W)}\bigr]\), with the retention \(G_k = \prod_{s \le k} c_s\). Moore and van der Laan (2009), (11) to (16), at one cause. Rytgaard and van der Laan (2024), Section 3.1

submodel

the multinomial logistic fluctuation \(\eta_l(\epsilon) = \eta_l + \sum_m \epsilon_m h^m_l\) of the softmax logits \(\eta_l = \log(\lambda_l / \lambda_0)\). \(\lambda_0\) is the probability of no event. At one cause the submodel is (10) of Moore and van der Laan (2009)

bound

the fit clips each logit into \([\operatorname{logit}(1 - \alpha), \operatorname{logit}(\alpha)]\) after every update, with alpha=. So \(\sum_l \lambda_l < 1\) holds, and no renormalization follows a clip

inference

the influence-curve variance and the multiplier band over the reported parameters, as on an iterated-mean fit. With id=, the cluster sums and the fit statuses of clusters apply

cross-fitting

with n_folds > 1, the hazards, \(g\) and \(G\) are out of fold. One pooled targeting then runs over every row

outcome_learner is the hazard learner on a hazard fit. pseudo_learner= is refused, because the fit regresses the event indicator at each node and fits no pseudo-outcome. Every arm is targeted together, so the score of every contrast is solved.

Two algorithms#

targeting= chooses the algorithm. Both stop on the relative score. That score is each target’s score divided by its scale \(\operatorname{mean}(\lvert w \rvert \sum_l \lvert h_l \rvert)\) over the rows at risk.

The hazard walk takes none of the point-treatment targeting rules. iterate=, stopping= and step_bound= are refused beside survival_model="hazard", in this order, as one implementation_gap that the roadmap tracks as X33. The walk imports only relative_score and TargetingFailure from the targeting modules. tests/unit/test_hazard_x22_refusal.py checks the refusal text and the order. It also shows that the refusal comes before any data or learner. A tripwire in the same file replaces every targeting solver, and a default hazard fit still runs.

item

per horizon, targeting="iterative" (default)

one step, targeting="one_step"

source

Moore and van der Laan (2009), Section 4.1. Benkeser, Carone and Gilbert (2018), Appendix. survtmle method = "hazard"

Cai and van der Laan (2020), Section 5, Algorithm 2. Rytgaard and van der Laan (2024), Sections 3.3 and 3.4

targets

every regimen and cause at one horizon

every regimen, cause and reported horizon together

one update

a finite-difference Newton step on the target scores along the current clever covariates. This step departs from the source. When the step does not reduce the scores, the fit falls back to the maximum-likelihood fluctuation with \(h\) held fixed (Moore and van der Laan 2009, (13))

a step of length step_size along \(\Sigma^{-1} s / \lVert s \rVert_\Sigma\), with \(s = P_n D^{(1)}\). A step that raises \(\lVert s \rVert_\Sigma\) is halved

after each update

the fit rebuilds \(h\)

the fit rebuilds \(h\)

metric

none

\(\Sigma = \operatorname{diag}(\max(P_n D_m^2, 10^{-12}))\) at the initial fit, fixed for the whole walk (Rytgaard and van der Laan 2024, Remark 2)

stop

every relative score at most tol, or max_iter outer iterations

every relative score at most tol, 20,000 accepted steps, or a step below \(10^{-14}\)

next horizon

restarts from the initial hazards, as survtmle refits for each t0

not applicable. Every horizon reads the one set of hazards

ignored settings

step_size is accepted and unused, as on point TMLE

max_iter

A fit that stops short sets converged to false and raises ConvergenceWarning. Its record names the failure class. The accepted \(\Sigma\)-norm trace of a walk never rises. The log-likelihood along the walk falls by at most \(10^{-8}\) per step, because a finite step carries a second-order term. The test test_e11_the_walk_trace checks both traces and the fixed \(\Sigma\).

output

what it shows

summary()

hazard targeting: one step over N parameter(s), S step(s), largest relative score x, converged, or the per-horizon form with the horizon count and the largest iteration count

result.fits[...].hazard

the targeting record: the targets, the initial and final scores, the relative-score trace, and the fixed \(\Sigma\) of a walk. A one-step fit shares one record between every reported parameter

diagnostics.score_equations()

one solver row per regimen, cause and horizon, at the horizon node

diagnostics.nuisance_models()

one hazard row per node and cause, which scores the pseudo-hazard \(\rho_j\) the learner fitted

truncation_curve()

a rerun of the targeting at each bound from the stored initial hazards. It refits no learner

n_bootstrap=

shown with the label experimental. No support row lists bootstrap for a hazard fit

Monotonicity#

fit

what holds

evidence

one step

each level curve \(\psi_{r,j}(t)\) is non-decreasing in \(t\) in floating point, with no tolerance, for every regimen and cause

test_w1_the_one_step_curve_never_decreases and test_w1_each_cause_and_their_sum_never_decrease in tests/unit/test_survival_monotone.py

one step or per horizon

at one horizon, the cause-specific incidences sum to one minus the all-cause survival, to a relative tolerance of \(10^{-12}\)

test_e6_the_causes_and_survival_add_to_one

per horizon

no claim across horizons, because each horizon reads its own targeted hazards. On the W1 witness the curve decreases by a rounding error

test_w1_the_per_horizon_curve_decreases_in_floating_point

iterated mean

no claim. On the W1 witness the curve of arm 1 decreases by about \(10^{-3}\) between horizons 2 and 3

test_w1_the_iterated_mean_curve_decreases asserts a decrease larger than \(10^{-6}\)

a contrast

no claim

none

The one-step guarantee has a floating-point condition. Each unit’s \(F_j(t)\) is a running sum of non-negative terms, and a policy mixture of such sums is a sum of non-negative products. The fit then reduces each horizon’s column in one order that does not depend on the values. Rounding to nearest is monotone, so a weighted mean of non-decreasing columns in that order is non-decreasing. The W1 witness is make_point_survival(n=300, seed=3, layout="wide", hazard_scale={3: 0.1}) with logistic learners that draw no random number.

Conditions the interval inherits#

condition

statement

source

identification

the conditions of the contract table

Moore and van der Laan (2009), Cai and van der Laan (2020), and Rytgaard and van der Laan (2024), each in Section 2

positivity

\(g(a \mid W)\,G_k(a, W) > \delta\) wherever \(q_r(a \mid W) > 0\), and \(S(k \mid a, W) > 0\) for \(k < t\). g_bounds bounds the cumulative product as on an iterated-mean fit, and alpha bounds the softmax logits

Rytgaard and van der Laan (2024), Assumption 1

remainder

the second-order remainder is \(o_P(n^{-1/2})\). Its terms are products of \(\hat g - g_0\) or \(\hat G - G_0\) with \(\hat\lambda - \lambda_0\). Donsker classes or cross-fitting control the empirical-process term

Moore and van der Laan (2009), Section 4.1.2 and Appendix E. Cai and van der Laan (2020), Section 4.3

grid

the reported horizons are a finite grid fixed before the sample, so the band is over a stack of fixed dimension

the refining-grid Theorem 1 of Rytgaard and van der Laan (2024), Section 4.2, is not claimed

solver

every relative score is at most tol. Otherwise converged is false, and the fit warns

the shipped solver rule

cross-fitting

the out-of-fold nuisances and one pooled targeting solve every component score

Zheng and van der Laan (2011), Section 2, applied to each algorithm’s endpoint. The cross-fitted hazard study measures the one-step walk and one per-horizon cell at five folds

No registered study measures the coverage of a hazard fit yet. The evidence today is the exact-law and mutation tests below.

A hazard cell with no event#

A hazard cell is one node, one treatment level and one cause. Its followers are the rows at risk at the node, with positive weight, that received the level. The fit holds a cell’s hazard at exactly zero for every unit when its followers exist and none has an event of that cause. Zero is the maximum-likelihood value, and no fluctuation moves it.

The hold replaces the learner’s prediction for every unit in the cell, whatever the learner. A zero that holds only within a stratum of \(W\) is not a held cell. The fit clips its logit to the floor \(\operatorname{logit}(1 - \alpha)\), and the fluctuation then moves it. That floor is a hazard of about \(5 \times 10^{-4}\) at the default \(\alpha\), so it biases a fit whose true per-node hazards lie below it.

A cell whose followers all have an event is full. The fit holds the all-cause hazard of a full cell at exactly one, its maximum-likelihood value. The survival after a full cell is zero, and each ratio over that survival is zero there.

The hold is necessary. Without it, the fit clips the logit at its floor. The joint score equations then need that hazard below the floor, so the one-step walk cannot solve them. The mutation test_m_a_clipped_cell_stalls_the_walk removes the hold and sees the walk fail to converge. The full-cell module has a mutation of the same name, and it clips the full cell instead.

Each parameter whose regimen reads a flagged cell at a node up to its horizon takes the constant_node_plugin status. A regimen reads a level when it puts mass on that level for some unit. RegimenFit.hazard_constant_nodes lists the flagged nodes, and constant_nodes(fit) reports them. The status then works as a node with no event states.

The empty-cell witness is make_point_survival(n=400, seed=36, layout="wide", hazard_scale={3: 0.05}). Arm 1 has followers at risk at node 3, and none has the event. Its risks at horizons 3 to 5 take the status, and its risks at horizons 1 and 2 keep their intervals (tests/unit/test_hazard_empty_cells.py). In the full-cell witness, every arm-1 follower at node 2 has the event. Its risk at horizon 2 is 1 and takes the status (tests/unit/test_hazard_full_cells.py).

A node and cause whose training rows hold no event of that cause fits no learner. The nuisance report shows LONGITUDINAL_CONSTANT_TARGET in place of its hazard row.

Evidence#

test module

what it checks

tests/unit/test_hazard_survival_law.py

on the exact law of tests/discrete_law_point_survival.py, both algorithms: the saturated estimate equals the g-formula, the influence curve equals the Gateaux derivative, and targeting repairs a wrong hazard fit with the exact mechanism. The maximum-likelihood fallback alone also repairs it. Also every score solved, one set of hazards per one-step fit, the competing-risk identity, policies, weights, the replay, and the walk trace

tests/unit/test_hazard_survival_mutations.py

M1 to M9, M11, M12 and M14: each deliberate mutation breaks the identity it should break. M14 reads the metric from the coefficients of each step. M4 (a sign flip under per-horizon targeting), M12 (the cause order) and M15 (the softmax clip) are positive controls

tests/unit/test_survival_monotone.py

the W1 witness of the monotonicity table

tests/unit/test_hazard_empty_cells.py

the hold, its mutation, and the status

tests/unit/test_hazard_full_cells.py

the hold of a full cell at one, its mutation, the status, and per-horizon convergence

tests/unit/test_hazard_refusals.py

each refusal H1 to H11, by type and message, before any learner

tests/e2e/test_hazard_survival.py

the TimeToEvent study path with three arms, two causes, a policy, weights, clusters and two folds. Also the replay, the bootstrap diagnostic, the summary, the nuisance report, and save and load

The registered study point survival hazard validates the per-horizon algorithm on repeated samples of two exact laws. Its survtmle comparator runs are pending.

The registered study point survival curve validates the one-step walk on repeated samples of the survival law and on further exact laws in its property cells. Its tmle3 comparator runs are pending.

The registered study point survival curve crossfit validates the one-step walk at five folds on the same exact laws. It has no comparator.

The isotonic projection#

isotonic=True projects each survival or competing-risk curve onto a non-decreasing curve over the reported horizons. It applies to an LTMLE fit with either survival model. Westling, van der Laan and Carone (2020), Section 2.1, give the rule. On a grid in one dimension the projection is the pool-adjacent-violators algorithm (PAVA).

item

rule

levels

the fit projects the reported levels of each regimen and cause, in horizon order, by unit-weight PAVA (scipy.optimize.isotonic_regression). A curve that does not decrease is returned bit for bit

curve band limits

the fit builds the band at the unprojected levels. It then projects each curve’s lower limits and upper limits by the same PAVA (Westling et al. 2020, Theorem 1 (iv) and (v))

contrasts

each contrast is the difference or ratio of projected levels. Its band is kept as built

what does not move

every influence curve, every standard error, and the critical value

a parameter with no interval

a constant_node_plugin parameter is left out of the band, and so out of the band projection

record

result.isotonic_moves lists (regimen, cause, horizon, before, after) for each moved level. summary() prints isotonic projection moved k level(s)

identity

a one-step hazard fit, a fit with one reported horizon, and a curve that does not decrease

refused

an end-of-study outcome (H3), msm= (H5), and a point-treatment functional (H10)

bootstrap

shown with the label experimental. No support row lists bootstrap for a projected fit

R lmtp 1.5.4 lmtp_survival projects over every grid time with isotone::gpava and unit weights. cleverly projects over the reported horizons only. The two agree when the reported horizons are every grid time. test_e9_the_projection_is_pava_and_lmtps_transcription transcribes the lmtp arithmetic over all grid times and compares it with the fit.

The projection changes what the reported numbers mean. The table states each claim and its condition.

claim

condition

source

the projected curve band covers the truth whenever the unprojected band covers it

any \(n\)

Westling et al. (2020), Theorem 1 (iv). test_e9b_the_projected_band_keeps_coverage_and_is_no_wider

the projected curve band is no wider on average or at its maximum

any \(n\)

Theorem 1 (v). The same test

the projected levels have a sup error over the reported horizons no larger than the unprojected levels

any \(n\)

Theorem 1 (i)

a pointwise interval at a projected level, with the unprojected standard error, is valid

only when the true curve is strictly increasing at the reported horizons. On a fixed finite grid the projection is then the identity with probability tending to one

Theorem 2, and the sentence after Corollary 1

the influence curve of a projected level is the unprojected one

a reporting convention, not an asymptotic statement. At a flat region the limit law of a projected level is not Gaussian

Theorem 2 and Corollary 1 cover the strictly increasing case only

Two mutation controls guard the rule. The test test_m13_a_band_centred_on_the_projected_estimate_fails_e9b centres a band on the projected estimate with the unprojected half-widths, and that band loses coverage. The test test_m10_an_influence_curve_moved_with_its_level_fails_e9 moves the influence curve with the level, and the identity check fails. No registered study measures a projected fit yet.

Known node mechanisms#

A sequentially randomized trial, such as a SMART, fixes its treatment probabilities by design. A study can also know its retention probabilities. Declare either with treatment_probabilities= and censoring_probabilities= in LTMLE.fit or in LongitudinalData.from_frame. In the study API, declare them with LongitudinalTreatment(treatment_probabilities=..., censoring_probabilities=...). A time-to-event input declares its baseline mechanism as a level to column mapping, in LongitudinalData.from_time_to_event(treatment_probabilities=...) or in TimeToEvent(treatment_probabilities=...). A declared node fits no learner, so the fold-support check skips it.

keyword

form

reading

treatment_probabilities

{"A1": {0: "p1_0", 1: "p1_1"}, ...}

each node’s levels mapped to frame columns of \(P(A_t=a\mid\text{observed past})\). Nodes left out are estimated. This form is required when the nodes have different level sets

treatment_probabilities

(n, T) array

\(P(A_t=\text{levels}_t[1]\mid\text{observed past})\), at binary nodes only

treatment_probabilities

(n, T, K) array

the full matrix, when every node shares \(K\) levels

censoring_probabilities

{"C1": "r1", ...} or (n, T) array

\(P(C_t=1\mid\text{observed past})\)

A value on a row that is not at risk at its node is never read, and it may be missing. A missing value on an at-risk row is refused with the row count. A node may give an arm probability one, as a SMART does for the units it does not re-randomize. A declared zero for the arm an at-risk unit took is refused, because the data contradict it. A declared retention of zero for a unit that stayed is refused for the same reason.

A declared zero can also leave a regimen unidentified. The fit refuses such a regimen before any learner. The check reads the rows that followed the plan through the previous node and are at risk at this one.

node

refused when, on a row at risk under the plan

label or rule

the declared probability of the assigned arm is zero

known stochastic policy

the policy puts mass on an arm whose declared probability is zero

modified treatment policy

the policy sends a unit to a level whose declared probability is zero

declared censoring

the declared retention of a unit that took the plan’s arm is zero

No unit with such a history can follow the regimen, so its mean there would extrapolate the outcome regression. The message names the regimen, the node and the row count. tests/unit/test_known_node_mechanisms.py checks a SMART whose responders keep their first arm with certainty: the regimen that switches them is refused, and always is fitted.

A node’s factor is the declared probability of the arm each row is assigned. The clever covariate is nonzero only where the plan’s history equals the observed one. The declaration describes the observed history, so the fit reads it only where the two agree. That is the meaning of ltmle’s numeric gform. On a held design the one decision is the one declared node.

With every treatment and censoring factor known, the remainder is zero and the curve is \(D^*(\bar Q_\infty,g_0)\) for any outcome regression. Theorem 2 of van der Laan and Gruber (2012) states the double robustness. Their Section 4, page 19, states the curve at \(g_n=g_0\). Petersen et al. (2014), Appendix B, Corollary 1, give the identity \(-P_0D^*(Q,g_0)=\Psi(Q)-\psi_0\).

With the treatment declared and the censoring estimated, the censoring factor must be consistent. That is the common SMART analysis. With a wrong outcome regression, the interval is conservative for a correctly specified parametric censoring model. It is not established for a flexible censoring learner.

Bounds. The cumulative bound applies to the running product of the declared factors on every row that follows a plan. A product outside g_bounds moves the estimate with no variance reason, so the fit refuses before any learner. The message names g_bounds and the node where the product first leaves the bounds. Products fall fast: 0.5 at each node reaches 0.01 near the seventh node. Pass a lower bound below the smallest declared product, such as g_bounds=(1e-8, 1.0).

truncation_curve() runs the same check on each pair of its grid before any replay. A pair that the fit refuses is refused with a sentence that quotes the fit’s condition, and with the invalid_declaration code. A curve row therefore never truncates a declared product. With every factor declared, the curve over admitted pairs is flat. With the censoring estimated, an admitted pair can still truncate the estimated factor, and the curve reports that movement. tests/unit/test_known_node_mechanisms.py pins the refusal, the flat curve and the estimated-censoring case.

composition

result

static, dynamic and known stochastic plans, msm=, cross-fitting, clusters, survival and competing risks, held and time-to-event designs, n_bootstrap=

supported. A bootstrap replicate carries the declared values of its own rows

a modified treatment policy at a categorical declared node

supported. The ratio numerator \(g^d_t(A_t\mid H_t)\) reads the declared matrix by the discrete formula. A declared node is the degenerate estimate \(g_n=g_0\), so the remainder, a product of the ratio error and the outcome error, is zero

a continuous node

refused. A known density is not supported

The nuisance report shows LONGITUDINAL_KNOWN_MECHANISM in place of each declared node’s row. LongitudinalConfig.known_factors names the declared treatment and censoring columns, and the summary prints them on one line. tests/unit/test_known_node_mechanisms.py checks a two-node SMART exact law: a wrong outcome regression with every factor declared returns the truth, with and without declared censoring. It also checks four compositions: a modified treatment policy, a held survival design, competing risks and a time-to-event container. In each one, a declared factor and a learner that returns it give one fit. The registered known-node-mechanisms study pairs the fit with R ltmle at a numeric gform. The sequential offers tutorial declares a three-level offer at three nodes beside an estimated consent model.

Functionals of a fitted result#

Each method below reads the influence curves of the reported estimates. Each one is an exact delta-method or linear-functional computation, so it needs no new theory. Each derived estimate takes the inference status, the covariance rule and the cluster unit of its inputs.

method

value

influence curve

ratio(a, b)

\(\psi_a / \psi_b\), interval on the log scale

\(IC_a/\psi_a - IC_b/\psi_b\)

ratio(a, b, kind="or")

the ratio of the odds, interval on the log scale

\(IC_a/(\psi_a(1-\psi_a)) - IC_b/(\psi_b(1-\psi_b))\)

ratio(a, b, view="survival")

the ratio of \(1 - \psi_a\) and \(1 - \psi_b\)

the same curves after \(\psi \mapsto 1 - \psi\) and \(IC \mapsto -IC\)

rmst(d, tau)

\(\tau - \sum_{t=1}^{\tau-1} F_d(t)\)

\(-\sum_{t=1}^{\tau-1} IC_{F_d(t)}\)

rmtl(d, tau, cause)

\(\sum_{t=1}^{\tau-1} F_{d,j}(t)\)

\(\sum_{t=1}^{\tau-1} IC_{F_{d,j}(t)}\)

The ratio curves are those of lmtp_contrast(type = "rr") and type = "or" in lmtp 1.5.4 (R/contrasts.R, lines 19 to 57). A ratio of two ey_regimen, risk_regimen or cif_regimen levels at one cause and horizon takes the name rr_regimen[a vs b @ t=h]. The survival view takes survival_rr_regimen[...]. The view is refused on an end-of-study fit, and on a fit that declares two or more causes, for the reason curve() gives. An odds ratio of end-of-study means needs a binary outcome. concrete reports a relative risk of cumulative incidence with a linear-scale interval and no arm covariance. This package uses the joint curve and the log scale, as lmtp does.

Let \(T\) be the node of the first event, with \(T = K + 1\) for a unit that is event-free through node \(K\). Then \(\mathrm{RMST}_d(\tau) = E[\min(T^d, \tau)]\) for \(2 \le \tau \le K + 1\). The restricted mean time lost to cause \(j\) is \(E[(\tau - \min(T^d, \tau))\,\mathbb 1\{J = j\}]\). Royston and Parmar (2013) define the RMST. Andersen, Hansen and Klein (2004) define the time lost to one cause.

The unit is the node index. For nodes \(\Delta\) apart, the RMST in calendar time is \(\Delta \times \mathrm{RMST}\).

On a fit with a time grid, \(\tau\) is a grid time \(g_m\) with \(2 \le m \le K\), and

\[ \mathrm{RMST}_d(g_m) = \sum_{k \le m} (g_k - g_{k-1})\,S_d(g_{k-1}) = g_m - \sum_{k=2}^{m} (g_k - g_{k-1})\,F_d(g_{k-1}), \]

with the curve \(-\sum_{k=2}^{m} (g_k - g_{k-1})\,IC_{F_d(g_{k-1})}\). rmtl takes the same spacings with a plus sign. This is \(E[\min(\tilde T^d, g_m)]\), with \(\tilde T\) the grid ceiling of the event time. It equals \(\int_0^{g_m} S_d(u)\,du\) only when the events sit on the grid, and for a continuous event time it is larger. The name carries the node index \(m\). On the unit grid \(g_k = k\) the time \(\tau = K + 1\) is also accepted, and rmst equals the wide fit’s rmst (test_on_the_unit_grid_rmst_is_the_wide_rmst). On a competing-risk fit, \(F_d\) is the sum of the cause-specific incidences, so rmst needs every declared cause. rmst refuses a fit that omits a horizon below \(\tau\), an end-of-study fit, and a working-model fit.

tests/unit/test_contrast_conveniences.py checks each row on the exact laws. A four-node law in tests/studies/survival_grid_law.py states the RMST truth by enumeration over the event times. It sums no risk curve.

The full-refit bootstrap#

LTMLE(n_bootstrap=B) refits the whole estimator on B resamples. The contract is the table.

question

contract

the unit

one row of the wide table: the whole trajectory of a unit, with its censoring and event nodes. A censored row and an event row resample as whole rows

a cluster

bootstrap_resampling="auto" resamples whole clusters when id= is declared. Each drawn occurrence gets its own cluster code. "cluster" without id= is refused before any learner

weights

the weights resample with their rows and are renormalized to mean one. They are not derived again

what a replicate refits

everything: the outer split, the treatment and censoring mechanisms, every backward regression and fluctuation for every regimen, cause and horizon, and the working-model projection. The replicate uses the same regimens, reference, horizons, bounds and learner specifications, with simultaneous=False

the outer split

a replicate draws its split from the fit’s own random_state, as the point-treatment bootstrap does. Two copies of one unit can fall in different folds

what a replicate returns

the point estimate and the influence-curve standard error of every reported parameter. A refit that returns no standard error raises TypeError

the fast path

an unclustered, unweighted, in-sample LTMLE or SDR fit of one end-of-study horizon uses FrequencyReplicate when every treatment and censoring factor is declared, every treatment node is categorical, and the outcome and pseudo-outcome learners declare frequency_exact. The replicate fits the distinct rows with the resample counts as weights. It reads the row index that run_bootstrap draws for the same seed. tests/unit/test_bootstrap_fast_path.py checks each draw and standard error against the materialized refit to \(10^{-10}\). Any other fit refits the materialized resample

the interval

bootstrap_interval="percentile" (default) takes np.quantile of the draws. "bca" reads bias-corrected and accelerated limits as order statistics, with the acceleration from each estimate’s own influence curve. With id= the bootstrap resamples clusters, so the acceleration reads the cluster sums of the curve. A derived estimate reads its own curve on the scale of its draws, with a ratio’s log and a decreasing transform= undone. A draw equal to the estimate counts for half in \(z_0\). "bca" without n_bootstrap raises ValueError

the band

with simultaneous=True and two or more estimates that supply inference, result.bootstrap_band holds the studentized sup-t band. Its critical value is the \(\lceil (1-\alpha)(B+1) \rceil\)-th order statistic of \(\max_j \lvert\psi^*_{bj}-\hat\psi_j\rvert / SE^*_{bj}\). The multiplier band in result.simultaneous does not change

a failed replicate

it is dropped and counted in n_failed, and it is not drawn again. A resample that leaves a regimen with no followers fails

status

a replicate carries no status. The fit’s status names the bootstrap columns. Under few_cluster_plugin, and on a fit with a Student \(t\) reference below 40 clusters, the summary prints bootstrap sd and percentile range

replay

truncation_curve() does not rerun the bootstrap. Its check at the fitted bound compares the estimates without their bootstrap summaries

random draws

run_bootstrap spawns the stream from random_state. The stream draws nothing the fit uses, so every analytic field of the fit is unchanged. tests/unit/test_ltmle_bootstrap.py pins a committed canonical-ltmle row at n_bootstrap=0 and at n_bootstrap=2

parallel layers

the replicates run in parallel over n_jobs, and each replicate fit runs with one worker

licensing scope

The bootstrap kind of a support row decides the label. A fit is covered when it has an end-of-study or single-cause survival outcome, is fitted in sample, uses static regimens, one treatment decision per node, a binary outcome, no weights and no working model. Any other fit, such as a held baseline treatment, matches no such row. The rows G01 and G11.nodes4 list it, and the full-refit bootstrap study measured them. A cross-fitted fit, a clustered fit and a clustered cross-fitted fit have no such row, so their percentile interval carries the label experimental. Owner X20-bootstrap holds the red cells. A fit with fewer than 20 clusters prints bootstrap sd and a percentile range, whatever its status. The study measures correctly specified cell-mean nuisances on finite binary laws, at \(n = 1000\), and at 1,500 rows in 60 clusters for the cluster bootstrap. No result covers a data-adaptive nuisance. Cai and van der Laan (2020) is the warning for that case

BCa and band scope

A BCa interval needs the bootstrap_bca kind, and the bootstrap band needs the bootstrap_band kind. No support row lists either kind, so both carry the label experimental on every fit. The X39 code ships in that state. The long-horizon Python studies are pending as a follow-up, and they are to add the rows. The R comparators (ltmle, lmtp) are deferred to X59. SparseInfluenceWarning fires on an unclustered Wald fit when the relative standard deviation of the variance estimate exceeds 0.125. A fit with a percentile bootstrap warns too, because its first interval is the Wald one, and a fit with bootstrap_interval="bca" does not. result.diagnostics.influence() is a direct call and is not part of run_all(). With id= it reports the cluster-level measures. The X39 work set that threshold after it read the pilots

derived estimates

ratio, rmst, rmtl and contrast apply the same function to each replicate’s estimates, and attach the percentile interval of those values. The derived interval is licensed only when every input’s interval is

Variations#

option

what it does

regimens=

static plans, fixed rowwise dynamic rules, categorical arms, or known stochastic policies. A plan is a sequence of arms, or one arm meaning that arm at every node. A plan with a callable node must be a DynamicRegimen declared rule_kind="known". LTMLE.fit refuses an undeclared or "estimated" rule, and a callable written inline in a mapping, before any learner. Sample-adaptive thresholds and learned rules need additional inference and are outside this contract. The scope page gives the threshold witness

reference=

which regimen the contrasts are taken against. It is part of the estimand rather than a display setting

horizons=

which time points a survival fit reports cumulative risk at. None reports the whole curve. Name the horizons you will report: the cost is \(T(T+1)/2\) regressions per regimen rather than \(T\)

survival_model=

"iterated_mean" (default) runs one backward recursion per horizon. "hazard" fits and targets the cause-specific hazards, as hazard-based survival targeting states. The outcome learner is then the hazard learner

targeting=, step_size=

the hazard targeting algorithm: "iterative" (default) per horizon, or "one_step" over the whole curve. step_size is the first step of the one-step walk. "one_step" requires survival_model="hazard"

isotonic=

projects each curve’s levels and band limits onto a non-decreasing curve, as the isotonic projection states

msm=

a working model over the regimen and horizon cells, in sample or cross-fitted. See MSM projections

four learner slots

outcome_learner, pseudo_learner, treatment_learner, censoring_learner. The pseudo learner fits the intermediate regressions. Under LTMLE their outcome is a bounded prediction. Under SDR it is a transform that can leave \([0, 1]\), so the pseudo learner must fit a real-valued target

n_folds=, learner_folds=

one outer split serves every node and regimen. The split is unstratified: random_partition draws it from the row count and the seed, and it balances no treatment node. Each fold fits the mechanism and an untargeted backward regression sequence on its training rows. One pooled fluctuation per node then targets the out-of-fold predictions, as cross-fitting the recursion states. The mechanism keeps the out-of-fold probabilities only, so its memory does not grow with \(K\)

g_bounds=, q_bounds=, alpha=

cumulative truncation, outcome scaling, and the logistic shrink. Above one fold, a continuous outcome must declare q_bounds

alpha_sig=, simultaneous=, n_multiplier=, multiplier_kind=

interval level, and the simultaneous bands across the reported regimens

n_bootstrap=, bootstrap_resampling=

the full-refit bootstrap. The bootstrap contract states what each replicate resamples and refits

Cross-fitting the recursion#

Above one fold, a per-regimen fit follows Section 5.2, Steps 1 to 4, of Díaz, Williams, Hoffman and Schenck (2023). Those steps are on journal pages 852 and 853. arXiv:2006.01366 v4 uses the same section and step numbers. The functions _fit_regimen_crossfit and _pooled_targeting in longitudinal/sequential.py implement the steps.

part of the fit

rows

what the fit does

Section 5.2

fold regressions

the training rows of fold \(k\)

fit the treatment and censoring mechanism, and run an untargeted backward regression sequence. Node \(t\) regresses the fold’s own untargeted prediction from node \(t + 1\)

the cross-fitted nuisance estimates that Step 1 takes as its start

out-of-fold estimate

the held-out rows of fold \(k\)

predict each node’s regression and each mechanism factor. The \(K\) folds give one out-of-fold prediction per row and node

Step 1

loss weight

every follower of node \(t\)

the observation weight times the inverse of the out-of-fold cumulative mechanism

Step 2

pooled fluctuation

every follower of node \(t\), from \(t = T\) back to node 1

fit one logistic fluctuation. Its offset is the out-of-fold prediction, and its outcome is the pooled targeted prediction from node \(t + 1\)

Step 3, which fits “using all the data points in the sample”

estimate

every row

average the targeted node-1 prediction

Step 4

Each node’s fluctuation solves its score over every follower. The fit therefore solves the estimated efficient score equation, as a single-fold fit does. res.diagnostics.score_equations() reports one solver row per node on both fits.

Theorem 3, on journal page 853, gives weak convergence at the nonparametric efficiency bound for this construction. Its proof in the supplement, Section 6 of arXiv v4, conditions on each fold’s initial nuisance fit being fixed given its training data. The pooled coefficients depend on the full sample and are handled as a low-dimensional fluctuation class. The untargeted fold regressions meet the conditional requirement. Carrying a pooled coefficient back into a fold’s initial regression would not, so the package does not carry it back.

tests/unit/test_pooled_longitudinal_targeting.py recomputes each row of the table by hand. It also carries four mutation controls. One fits the coefficient on one fold’s training rows. The others drop the loss weight, read a refit of one fold’s mechanism model, or carry targeted values back into the folds.

n_folds=1 keeps the canonical single-fold recursion. Each node regresses the targeted prediction from node \(t + 1\), and each fluctuation solves over the rows its regression was fitted on.

The package also computes specializations and compositions that Section 5.2 does not state. The Eligibility rule admits a natural extension of a published result. The table identifies a direct specialized source where one exists and otherwise gives the base, step, and established argument.

composition

base result

the step

the established argument

evidence

cumulative risk at a horizon

Díaz, Hoffman, Hejazi and Williams (2024), corrected in 2025, with one cause

\(Y\) is the indicator of an event by the horizon. The survival pseudo-outcome is \(Z_t = Y_t + (1 - Y_t)\,\bar Q^*_{t+1}\)

Appendix E gives the cross-fitted pooled TMLE directly; a single event type is its one-cause reduction

survival-curve study

cause-specific cumulative incidence

Díaz, Hoffman, Hejazi and Williams (2024), corrected in 2025

\(Y\) is the indicator of an event of one cause by the horizon. A competing event ends follow-up, and the regression is zero after it

Proposition 1 identifies the target and Appendix E gives the out-of-fold nuisances, all-row per-node fluctuation, pooled backward carry, and score argument

competing-risk study

known observation weights

Theorem 3 under iid sampling

the target is a weighted mean of the node-1 regression, and every fluctuation carries the weight in its loss

the chain rule for influence functions, applied to a ratio of two means under iid draws of the observation and its known, bounded weight

weighted study, which fails two property cells and three paired comparisons that conclude underpowered, and publishes them under a reporting policy

categorical treatments and deterministic dynamic rules

Theorem 3 for a fixed modified treatment policy \(d(a_t, h_t)\) that does not depend on \(P\)

a fixed rowwise rule assigns one level from that unit’s history

Section 2 lets a fixed policy depend on the unit’s history, and Section 4, journal page 850, gives the intervention density for a discrete exposure. A sample-adaptive threshold or learned rule is outside this result

categorical study, and the fixed dynamic rule in the end-of-study study

several horizons, causes, regimens, and their contrasts

Theorem 3 for each parameter

each parameter has its own recursion on one shared split, and the report stacks their influence curves

a fixed-dimension stack by Cramér–Wold, then linearity or the delta method for each contrast

the survival-curve and competing-risk studies

ratios of regimen levels, RMST and RMTL

the stacked curves of the row above

a log ratio is a smooth function of two levels. RMST and RMTL are fixed linear combinations of the risks below the horizon

the delta method, and linearity

the ratio and RMST cells of the full-refit bootstrap study

the full-refit bootstrap

the estimator of each row above

refit the whole estimator on each resample

none for this estimator. The percentile interval is labelled supported only when a support row lists bootstrap and its cells are green, with correctly specified cell-mean nuisances on finite binary laws

the bootstrap cells of the full-refit bootstrap study

Theorem 3 also requires every mechanism ratio and targeted sequential regression to be consistent, the sum over nodes of their error products to be \(o_P(n^{-1/2})\), and the density ratios to stay bounded. On a continuous node a binned density meets the mechanism-ratio rate only if its bin count grows with \(n\) (see the bin count paragraph under modified treatment policies). Those are conditions on the targeted regressions. Appendix E of the competing-risk paper shows that a sufficient condition stated in terms of the initial recursive learners can contain cross-time products between a mechanism error at node \(t\) and regression errors at later nodes. The weighted row additionally needs weights that are known and bounded.

The split balances nothing. It used to balance the first treatment node, and the reviewed longitudinal theorem defines a random near-balanced row partition instead. The audit in fold and outcome-scale rules found no source for the first-node strata, so the package now draws the partition from the seed alone. Each fold must also carry enough events for each declared cause, which scope and refusals states with its remedy.

One design is refused above one fold. A continuous outcome with q_bounds=None is refused, because the sample outcome range would take the scale from held-out rows. The message names the in-sample fit, which is CrossFitting(enabled=False), or n_folds=1 on the engine. A fit with id= cross-fits with whole-cluster folds, as clusters states.

Longitudinal msm= cross-fits the same way. Each fold runs the untargeted recursion of every regimen and horizon cell. One stacked fluctuation per node then pools the update over every follower of every cell. MSM projections states the construction and its evidence. The score report has one solver row per node and term.

LTMLE refuses each point-treatment keyword in _REFUSED in longitudinal/estimator.py by name, with its own reason. Some keywords redirect to the argument that does the work. interventions= redirects to regimens=. incremental= redirects to IPSI, which fits the odds tilt over time. The table gives the refusals that are statements about the question, the construction, or coverage.

refused

code

what it would need

eliminating the competing events

different_estimand

what is reported is the cause-specific cumulative incidence with the competing causes left alone, so a competing event is part of the history. Removing it makes it an intervened node: a further factor per node in the denominator, and its own no-unmeasured-confounding and positivity assumptions

intermediate=

different_estimand

a controlled direct effect fixes a mediator at one time point. Over a sequence, with mediators that are themselves time-varying, that is a different identification rather than a further column

a truncation_curve() pair that truncates a declared cumulative product

invalid_declaration

the fit refuses the same pair as g_bounds, because truncating a known product moves the estimate with no variance reason. A curve row at that pair would show a movement that no admitted fit has. Remove the pair from the grid. A pair (lower, 1) with lower below the smallest declared product is admitted

mechanism=True on truncation_curve()

invalid_declaration

a longitudinal fit holds one cumulative treatment-and-censoring bound, and it fits no separate observation mechanism to sweep. The option names a point-treatment axis, so the call raises CapabilityError rather than sweeping the cumulative bound under another name

a callable node that is not declared "known", or that is declared "estimated"

invalid_declaration when undeclared, implementation_gap when "estimated"

the fit treats each rule as fixed. A rule learned from the analysis sample needs a learned-policy estimand and its own inference. LTMLE.fit raises CapabilityError before any learner. Declare a rule fixed before the fit with DynamicRegimen(label, plan, rule_kind="known")

a callable written inline in a regimens= mapping

invalid_declaration

an inline callable carries no declaration, so the fit refuses it before any learner. Write the plan as a DynamicRegimen declared rule_kind="known", with (rule,) * T for one rule at every node

an outcome missing for a reason other than censoring

api_redirection

left as it is, the probability of observing it is silently taken to be one. Encode it as a final censoring column, so it is estimated and enters the cumulative product

longitudinal sensitivity-bound estimation

implementation_gap

a sample estimator and sampling theory for its bound functionals. F16 holds the stop

a continuous outcome with q_bounds=None above one fold

invalid_declaration

with q_bounds=None the scale comes from every observed outcome, held-out rows included, and no shipped result covers that scale

Two plan shapes raise DataError before any learner. Each one is a statement about the input, so the table of codes above does not list it.

plan

what the refusal asks for

an iterator in regimens=, such as a generator

a tuple, or a DynamicRegimen, which reads its plan once and stores it as a tuple. The fit does not read the iterator: a fit reads regimens= at each call, and an iterator is empty after its first read

a DynamicRegimen plan that is one label or one callable

one entry per node. Write one rule for every node as (rule,) * T

See scope and refusals for what each code means.

Clusters#

LTMLE.fit(id=...) treats the cluster as the unit. The cluster is the fold unit and the variance unit, in sample and under cross-fitting. The table gives each part of a clustered fit.

part

rule

outer split

random_partition draws whole clusters from the labels and random_state. LTMLE.fit checks the drawn split with check_integrity before any learner. A split cluster raises DataError

fold count

the requested n_folds, capped at the cluster count with a UserWarning

inner Super Learner folds

grouped on the same labels, at every mechanism and node regression

targeting

the pooled fluctuation over every follower of each node, as on an unclustered fit

curve and variance

the row curve summed within each cluster. The variance is \(J\,\widehat{\mathrm{var}}(S_c)/n^2\), with \(S_c = \sum_{i \in c} D_i\) and \(J\) clusters

status, reference and bands

the rules of clusters for a longitudinal fit. Below 40 positive-mass clusters the reference is Student \(t\) with \(J - 2\) degrees of freedom. Below 20 the fit takes "few_cluster_plugin", in sample and cross-fitted, because 20 is the smallest count the registered studies measure. Simultaneous bands need 40 or more

bootstrap

no support row lists bootstrap for a clustered cross-fitted fit, so the percentile interval is shown with the label experimental. Each replicate redraws a grouped split over its resampled clusters, so two copies of one cluster can land in different folds

The base result is Díaz, Williams, Hoffman and Schenck (2023), Section 5.2, journal page 852, and Theorem 3, page 853. Theorem 3 takes \(n\) iid units and a random partition of them.

Take the unit to be the cluster \(O_c = (O_{c1}, \dots, O_{cN_c})\), with \(\varphi_c = \sum_i \varphi(O_{ci})\). The row estimating equation is then \(\sum_c \{\varphi_c - N_c \hat\theta\} = 0\). That is a cluster-level equation for \(E\varphi_c / E N_c\). A partition of clusters is a random partition of the iid units, so each training fold is independent of its validation fold. The remainder rates then read \(o_P(J^{-1/2})\), and the curve is \((\varphi_c - N_c\theta)/E(N)\).

The targeting pools every follower, so the curve is centred at the pooled estimate. The variance therefore takes ddof=1 over all \(J\) cluster sums, and the fold count does not enter the degrees of freedom. A fold-targeted or fold-evaluated LTMLE would need the centred rule of the fold-evaluated point-treatment fit. The package ships neither.

The result inherits five conditions: independent clusters, no interference between units, a bounded cluster size, the Theorem 3 rates and bounded density ratios in the cluster count, and the unequal-size and few-cluster rules of clusters. Wang, Park, Small and Li (2024), Remark 3, caution against complex learners at about 20 clusters. The registered evidence uses parametric nuisances.

The cluster handling does not depend on the target. The split, the inner groups, the cluster-summed curve and the status are the same for every target, a known policy included. A modified treatment policy node is included too. IPSI uses the same handling for an odds tilt.

Two side effects follow from the cross-fitted default. A fit with fewer than 10 clusters warns that it reduces n_folds to the cluster count, and a fit with fewer than 20 reports no interval. At few clusters a training fold can lack a first-node level. The fit then raises LongitudinalError after the draw, and the message names the in-sample fit. A training fold whose followers all hold one outcome value reads that value, as a node with no event states.

tests/unit/test_clustered_cross_fitted_ltmle.py holds the fast evidence. Each witness has a mutation control that fails it.

claim

witness

every cluster lands in one fold

the fit’s split equals the grouped draw. A draw without cluster=, or an overriding _folds, raises DataError before any learner

the inner folds are grouped

a recording learner receives the codes of exactly its training rows at every nuisance fit

a held-out cluster is unseen

a learner that memorizes a site cannot see its own cluster. Under row-level folds, the flip of a cluster mate moves the prediction

the curve is cluster-summed

the variance of every reported and derived estimate equals the cluster-sum variance of its curve to rel=1e-12. At 60 clusters of 10 to 70 rows it exceeds the row variance by a factor above 3

the size term is carried

the variance reads \(A_c - N_c\hat\psi\), which differs from \(A_c\) by more than 10% at unequal sizes

every target kind

end of study (static, dynamic, categorical), survival, competing risks with incidence_total(), weights with a zero-mass cluster, ratios, RMST and RMTL, at 40 and 21 clusters

the floor

20 clusters keep \(t_{18}\) and 19 take "few_cluster_plugin", cross-fitted and in sample. A fit that reads the point-treatment floor of 10 fails it

Two registered studies measure the fit, and both publish under reporting. The clustered cross-fitted study pairs the fit with R lmtp 1.5.4 at 100 clusters of 40 rows. Its four cluster-robust pairs and five paired comparisons pass. Its band covers 0.933, below the 0.92 lower bound of its interval band, and the band-finite-sample owner holds that cell. The few-cluster study measures the \(t\) reference at 20 and 30 clusters, and all 8 scored cells pass.

limit

reason

no competing-risk study

make_longitudinal_competing has no cluster option. The fast tier pins the exact clustered identity for every incidence and for incidence_total()

no informative-size cell

the size enters no longitudinal study law. clustered-unequal-cvtmle measures the row-weighted target for point treatment

no bootstrap licence

the cluster_cross_fit kind has no study cell

the lmtp pair at equal sizes only

ife aggregates cluster means, which equal cluster sums at equal sizes only

4 to 19 clusters

the fit takes "few_cluster_plugin" and reports no interval. No cross-fitted study measures that range: a probe of 2,000 draws at 10 clusters found 3 and 8 draws that admit no fit, and the harness refuses a cell that loses a replication. X54 part (b) holds it

The sequentially doubly robust estimator#

SDR estimates the same parameters as LTMLE with the sequentially doubly robust estimator of Díaz, Williams, Hoffman and Schenck (2023), Section 5.3. It fits no fluctuation. Each node’s regression fits a transformed outcome, and the estimate is the mean of the first transform. The estimator is consistent when, at each node, either the mechanism ratio or the regression of the transform is consistent (Lemma 4). That property is sequential double robustness.

LTMLE does not have it. In tests/unit/test_sdr_double_robustness.py, node 1’s mechanism and node 2’s regression are both wrong. SDR is exact on that law, and LTMLE is not.

from sklearn.linear_model import LinearRegression, LogisticRegression

from cleverly.datasets import make_longitudinal
from cleverly.longitudinal import SDR

frame, _ = make_longitudinal(n=300, seed=0)
result = SDR(
    {"always": 1, "never": 0},
    reference="never",
    outcome_learner=LogisticRegression(),
    pseudo_learner=LinearRegression(),
    treatment_learner=LogisticRegression(),
    censoring_learner=LogisticRegression(),
    n_folds=1,
    simultaneous=False,
).fit(
    frame,
    outcome="Y",
    treatment=["A1", "A2"],
    baseline=["W1", "W2"],
    time_varying=[[], ["L2"]],
    censoring=["C1", "C2"],
)
print(result.summary())

sdr() is the one-call form, and CausalStudy(...).estimate(method="sdr") routes a longitudinal functional to the same engine. A point-treatment functional refuses method="sdr".

The transform#

Write \(\pi_t\) for the intervention density at the observed arm. It is 1 at a label or rule node, \(q_t\) at a known stochastic policy node, and the policy numerator at a modified treatment policy node. Write \(p_t = g_t c_t\) for the treatment and censoring mechanism at the observed arm on the plan. Write \(\check m_t\) for the node regression. The transformed outcome is Equation (4) of the paper:

\[ \varphi_t = v_t + \sum_{s=t}^{h} W_{t,s} \bigl(Z_s - \check m_s(A_s, H_s)\bigr), \qquad W_{t,s} = \frac{\prod_{k=t}^{s} \pi_k}{\operatorname{bound}\bigl(\prod_{k=t}^{s} p_k\bigr)}. \]

term

definition

\(v_t\)

the value node \(t\) carries: the regression at the plan’s arm, or the policy-weighted mean of the per-arm regressions at a policy node

\(Z_s\)

\(v_{s+1}\) composed with the event at node \(s\), as the shipped pseudo-outcome composes it. \(v_{h+1}\) is the scaled outcome, or 0 at a survival horizon

\(W_{t,s}\)

the weight of the path from node \(t\) to node \(s\), on the rows that follow the plan through \(s\), and 0 elsewhere

node-\(t\) regression target

\(\varphi_{t+1}\) composed with the node’s event for \(t < h\), and the outcome at the horizon

estimate

the weighted mean of \(\varphi_1\)

influence curve

\(w(\varphi_1 - \hat\psi)\). The standard error is the empirical standard deviation of this curve, the sentence below Theorem 4

_recursion in longitudinal/sdr.py implements the table. tests/unit/test_sdr_construction.py compares it with an independent numpy recursion to 1e-12, in sample and at five folds.

Contract#

item

contract

base result

Díaz, Williams, Hoffman and Schenck (2023), Journal of the American Statistical Association 118(542). Equation (4) defines \(\varphi_t\). Theorem 2 gives the efficient influence function \(\varphi_1 - \theta\). Section 5.3, journal page 854, gives the estimator, Lemma 3, Lemma 4 and Theorem 4

rate condition

\(\sum_t \lVert \hat r_t - r_t \rVert\, \lVert \check m_t - \tilde m_t \rVert = o_P(n^{-1/2})\), Theorem 4. Here \(\tilde m_t\) is the regression of the estimated transform, not of the true one

efficiency

the variance reaches the bound only when every nuisance is consistent, and when \(\lVert \varphi_1(\hat\eta) - \varphi_1(\eta) \rVert = o_P(1)\)

exact or known ratios

the remainder is 0 for any outcome learner. With the fold-measurable construction below and a GLM regression that converges to a fixed limit, the estimate is asymptotically normal with variance \(\operatorname{Var} \varphi_1(r, \bar m)\)

bounded ratios

\(P\{r_t < c\} = P\{\hat r_t < c\} = 1\) for some \(c\). g_bounds gives the second

consistency without a rate

Lemma 4: at each node, the ratio is consistent or \(\lVert \check m_t - \tilde m_t \rVert \to 0\). Lemma 4 gives no interval

in sample

n_folds=1 rests on Donsker-type conditions (Luedtke et al. 2017, condition DC), not on Theorem 4, as for LTMLE

no interval claimed

multiply robust intervals. Page 854 states that neither SDR nor TMLE gives them

Cross-fitting#

Section 5.3, Step 2 composes a training row’s transform from the folds that hold that row out. lmtp reads a training row’s ratio from the out-of-fold mechanism. Neither transform is fixed given the fold’s training rows. The proof of Theorem 3 (supplement Section 6) conditions on that fixed fit. Section 5.4 states that cross-fitting keeps the independences that the proofs of Theorems 3 and 4 need. SDR therefore ships the training-fold recursion.

part of the fit

rows

what the fit does

fold mechanism

the training rows of fold \(k\)

fit the treatment and censoring mechanism, and read it at every row

fold recursion

the training followers of fold \(k\)

run the recursion of the transform table, with the fold’s own regressions and its own mechanism in each training row’s transform

held-out transform

the held-out rows of fold \(k\)

compute \(\varphi_1\) from fold \(k\)’s regressions and mechanism

estimate

every row

average the stitched \(\varphi_1\)

With ratios that do not depend on the fold, such as a known mechanism, the construction equals lmtp::cf_sdr. Rotnitzky, Robins and Babino (2017), Section 5.2.1, leave the rate of an in-fold transform’s regression open. Theorem 4 assumes that rate, as the LTMLE contract assumes its own. tests/unit/test_sdr_construction.py carries two mutation controls. The stitched Step 2 moves the estimate by more than 1e-4, and so does a training row that reads the out-of-fold mechanism.

The interval under heavy cumulative density ratios#

The SDR interval runs short of nominal when the cumulative density ratios are heavy. The study cell p4_sdr of mtp-boundaries reads a standard-error ratio of 0.9408 at n = 4,000, with a 99% interval of 0.9060 to 0.9775, and a coverage of 0.9357. The LTMLE cell of the same plan reads a standard-error ratio of 0.9572. The cell uses a pseudo-outcome learner that projects onto [0, 1].

Treat this interval as experimental under the measured-shortfall standard. The study publishes the cell as reporting, and the red-cell owner X40-sdr-heavy-ratios records the measured numbers. The fit support key does not read the plan, so a fit carries no label of its own for this cell.

Choices that differ from LTMLE#

choice

SDR

reason

fluctuation

none. alpha=, max_iter=, tol=, stopping=, step_bound=, step_search= and iterate= raise MethodConfigurationError with code invalid_declaration

the transform replaces the targeting step, so no stopping rule, step bound or iterate exists

regression clip

none. The default pseudo_learner is SuperLearner(task="regression", clip=None)

a transform can leave \([0, 1]\), and lmtp predicts it unbounded. A clipping pseudo learner is accepted and moves the estimate away from lmtp_sdr (tests/unit/test_sdr_bounds_and_clipping.py)

g_bounds=

bounds each partial path probability \(\prod_{k=t}^{s} p_k\)

at \(t = 1\) this is the cumulative product that LTMLE divides by, equal to rounding. Where a bound binds, the bounded weight is a misspecified ratio on that path, and no SDR property is claimed there. diagnostics.support() reports the binding share per node, over the paths that the estimate reads. A cross-fitted fit counts the held-out rows of each fold

a level outside the outcome range

reported as computed, with ParameterSpaceWarning

an average of transforms can leave \([0, 1]\), or \([q_\text{lo}, q_\text{hi}]\) on a continuous outcome (page 854)

ratio() of such a level

CapabilityError on the log scale for a level at or below 0, and on the odds scale outside \((0, 1)\)

the scale is undefined there. The difference scale stays available

msm=

the identity link always. A logit or log link when its projection converges, and CapabilityError otherwise

a transform mean outside the link’s range has no finite solution

full-refit bootstrap

runs, and reports bootstrap sd with no percentile inference

no registered study measures it

score equations

diagnostics.score_equations() is not_applicable

no node fits a fluctuation

Clusters, observation weights, survival, competing risks, known policies, modified treatment policies, contrasts, simultaneous bands and the truncation replay behave as they do for LTMLE. Each composition reuses the step that the LTMLE contract states. tests/unit/test_sdr_e2e.py runs each one end to end.

Incremental interventions over time#

An incremental intervention multiplies each unit’s odds of treatment by a fixed \(\delta\) at each node. With more than two levels at a node it multiplies the odds of each level by that level’s factor. It shifts the treatment mechanism and does not assign an arm. Every unit at risk stays in the plan, so the estimand needs no treatment positivity at a tilt node. A static plan such as “always treat” needs positivity wherever it puts mass.

IPSI in cleverly.longitudinal fits this estimand on a LongitudinalData panel. A node with two levels takes a scalar \(\delta\), which multiplies the odds of the larger of the node’s two sorted codes. A node with more levels takes one factor per level. IPSI implements the one-step estimator of Kennedy (2019), Algorithm 1. It adds the dropout weights of Kim, Kennedy and Naimi (2021), Appendix A, Algorithm 1. It is not a TMLE, and it fits no fluctuation.

your situation

what IPSI gives

what it costs

some histories almost never receive one arm

a plan mean with no treatment positivity at a tilt node

the treatment mechanism must be consistent at every tilt node, because the estimator is not doubly robust

you want a dose-response in the odds of treatment

one plan per \(\delta\), with contrasts and a simultaneous band over the declared plans

the band covers the declared plans, not a continuum of \(\delta\)

units drop out

the dropout weights of Kim et al. (2021)

a censoring model per node, and positivity of remaining under observation

the plan mixes a tilt with label, rule or known-policy nodes

one recursion that covers every node kind

positivity at each label and rule node

IPSI is Kennedy’s odds tilt. lmtp::ipsi is a risk-ratio tilt, which this package fits as RiskRatioTilt with LTMLE or SDR. The point-treatment odds tilt is TMLE(incremental=...). Implementation: longitudinal/ipsi.py holds the recursion, and the IPSI class is in longitudinal/estimator.py.

The incremental estimand#

Write \(\pi_t(a \mid H_t) = P(A_t = a \mid H_t)\) for the treatment mechanism at node \(t\) among the units at risk. A tilt node with levels \(a = 1, \ldots, K_t\) and factors \(\delta_{t,a} > 0\) draws the arm from the exponential tilt

\[ q_t(a \mid H_t) = \frac{\delta_{t,a}\, \pi_t(a \mid H_t)}{D_t(H_t)}, \qquad D_t(H_t) = \sum_b \delta_{t,b}\, \pi_t(b \mid H_t). \]

Multiplying every factor of a node by one constant changes nothing, so a node has \(K_t - 1\) free parameters. With two levels and \((\delta_{t,0}, \delta_{t,1}) = (1, \delta_t)\) the formula is Equation (1) of Kennedy (2019): \(q_t(1 \mid H_t) = \delta_t \pi_t / (\delta_t \pi_t + 1 - \pi_t)\).

The target \(\psi\) is the mean outcome when every node follows its plan. Theorem 1 of Kennedy (2019) identifies it under consistency and sequential exchangeability (Assumptions 1 and 2). It needs no positivity, for each \(\delta_{t,a}\) in a compact subset of \((0, \infty)\). The finite partition of the treatment into \(K_t\) levels changes no step of the proof.

Under dropout, Theorem 3.1 of Kim et al. (2021) adds missing at random given the history (Assumption A2-M). It also adds dropout positivity (Assumption A3). Then \(\psi\) is the plan mean with dropout prevented. At \(\delta = 1\) it is the natural course with dropout prevented, not the observed mean.

plan

how to declare it

one \(\delta\) at every node

IPSI([Incremental(0.5), Incremental(1.0), Incremental(2.0)]). Each Incremental is one plan, and its name is the label

one \(\delta_t\) per node

DynamicRegimen("ramp", (Incremental(1.5), Incremental(2.0)))

one factor per level at a node with three or more levels

Incremental({"high": 2.0, "low": 0.5}). A level that the mapping does not name keeps the factor 1

a tilt beside label, rule or known-policy nodes

DynamicRegimen("tilt then stop", (Incremental(2.0), 0))

a plan with no tilt node

a static or dynamic plan, so a tilt can be contrasted with it on one fit

One Incremental applies to every node of its plan. The fit resolves its keys against the levels of each node in turn. A key that is not a level of some node meets the key refusal at that node. A scalar declaration at a node with other than two levels meets the scalar refusal at that node. Both refusals run before any learner. A vector node whose components are all categorical is one categorical node over its observed tuples, and its keys are the tuples.

reference= must name one of the declared plans. List Incremental(1.0) to contrast each plan with the natural course. The parameter names are the longitudinal ones, such as ey_regimen[odds x2] and ate_regimen[odds x2 vs natural course].

A mean of a non-absorbing outcome \(Y_t\) under a tilt is the end-of-study estimand on the panel truncated after node \(t\). IPSI fits it with outcome=Repeated([...]) (below).

The tilt recursion and its influence function#

Each node regression \(m_t(H_t, A_t)\) fits the target \(Z_{t+1}\) on the rows at risk and under observation through \(t\). The node carries the tilted mean of its per-arm predictions, \(\bar m_t = \sum_a q_t(a \mid H_t)\, m_t(H_t, a)\). That is Remark 2 of Kennedy (2019). The recursion runs from the horizon \(h\) back to node 1.

quantity

definition

\(Z_{h+1}\)

the outcome, or the event indicator at a survival horizon

\(Z_{t+1}\), survival

\(Y_t + (1 - Y_t)\,\bar m_{t+1}\). A competing-risk fit uses one cause’s indicator and the all-cause survival factor

tilt factor \(r_t\)

\(\delta_{t,A_t} / D_t\). With two levels it is \((\delta_t A_t + 1 - A_t)/(\delta_t \pi_t + 1 - \pi_t)\). The fit never computes it as \(q_t / \pi_t\)

label, rule or policy factor

the plan’s density at the observed arm over the treatment mechanism

censoring factor

\(1\{C_t = 1\}/\omega_t\), with \(\omega_t\) the probability of remaining under observation

weight \(W_t\)

the product of every node factor through \(t\), and 0 off the rows that follow the plan through \(t\). \(W_0 = 1\)

The uncentred efficient influence function at the fitted nuisances is

\[ \varphi = \bar m_1(H_1) + \sum_{t=1}^{h} W_t \{Z_{t+1} - m_t(H_t, A_t)\} + \sum_{t \in \text{tilt}} W_{t-1}\, \frac{\delta_{t,A_t}}{D_t}\, \{m_t(H_t, A_t) - \bar m_t(H_t)\}. \]

The form follows Kennedy (2019), Theorem 2 and Appendix Lemmas 2 to 4. Kim et al. (2021), Theorem 4.1, adds the censoring factors. The last sum is the mechanism term. It exists because \(q_t\) reads \(\pi_t\). A known policy has no such term, and a label or rule node has none.

Only Lemma 4 changes with the number of levels. The chain rule through \(\pi_t\) gives the influence function of \(q_t(a \mid h)\) as \(\{1\{H_t = h\}/p(h)\}(\delta_{t,A_t}/D_t)(1\{A_t = a\} - q_t(a \mid h))\). The sum of this function against \(m_t(H_t, a)\) is the mechanism term above. With two levels the term is \(\delta_t \{m_t(H_t, 1) - m_t(H_t, 0)\}(A_t - \pi_t)/(\delta_t \pi_t + 1 - \pi_t)^2\), which is Kennedy’s Lemma 4. A node with two levels runs this binary arithmetic with \(\delta_t = \delta_{t,1}/\delta_{t,0}\), so its results are bit-identical to the results before the categorical change. The factor \(\delta_{t,A_t}/D_t\) and the term are unchanged when every \(\delta_{t,a}\) is multiplied by one constant.

The remainder is a product. Kennedy’s telescoped sum has one term per node. Each term carries \((\hat\pi_t - \pi_t)\) through \(\hat D_t - D_t = \sum_b \delta_{t,b}(\hat\pi_{t,b} - \pi_{t,b})\), as in the point-treatment remainder. No term is linear in one nuisance’s error, so the construction exposes no linear remainder. Under dropout the remainder also holds products of the form \(\lVert \hat\omega_t - \omega_t \rVert\, \lVert \hat m_t - m_t \rVert\) (Kim et al. 2021, Theorem 5.1). The mechanism term carries \(W_{t-1}\), which holds the censoring factors, so a fit that drops them from \(W_{t-1}\) changes the curve.

The rate condition has the form of Kennedy (2019), Theorem 3, with the norm taken over the levels of each node: \(\lVert \hat\pi_t - \pi_t \rVert\,(\lVert \hat m_s - m_s \rVert + \lVert \hat\pi_s - \pi_s \rVert) = o_P(n^{-1/2})\) for every \(s \ge t\). Under dropout, the Kim et al. (2021) Theorem 5.1 products must also be \(o_P(n^{-1/2})\). The point-treatment page gives the one-node form in its verdict row (ae).

The estimate is the weighted mean \(\hat\psi = \sum_i w_i \varphi_i / \sum_i w_i\). The influence curve is \(w_i(\varphi_i - \hat\psi)\), summed within a cluster under id=. The standard error is the empirical standard deviation of that curve, as in Kennedy (2019), Theorem 3.

tests/unit/test_categorical_ipsi_exact.py makes the same comparison on three-level nodes, on the law of tests/discrete_law_longitudinal_categorical_incremental.py. The cases are the censored law, the same law without dropout, a survival law and a competing-risk law. Each law has a tilt at one node, at both nodes with different factors, and beside a label node.

The module asserts at import that no term can vanish unnoticed. Each mechanism term holds at least a twentieth of the curve’s variance on the censored laws and a fiftieth on the survival and competing-risk laws. The complete-case functional differs from the parameter by more than \(0.01\).

tests/unit/test_categorical_ipsi_longhand.py matches the cross-fitted engine to an independent numpy longhand to \(10^{-12}\), with a non-saturated multinomial logistic mechanism. tests/unit/test_categorical_ipsi_mutations.py breaks one line at a time and requires a witness to fail by more than \(10^{-4}\) in a value. A mutation that raises an error does not count. tests/unit/test_categorical_ipsi_exact.py also checks that integer weights equal row duplication, that id= changes only the variance, that five exact copies as five folds give the exact cross-fitted fit, and that a bootstrap replicate equals the fit on the resampled rows. This is Python property evidence. No registered study covers a three-level node yet.

tests/unit/test_ipsi_exact_laws.py compares each estimate with the g-formula of tests/discrete_law_longitudinal_ipsi.py to \(10^{-12}\). It compares each curve with the complex-step Gateaux derivative to \(10^{-10}\). The oracle reads \(\pi_t\) off the perturbed law, so its derivative holds the mechanism term without being told. The cases are a censored two-node law, a three-node law with a tilt at every node, survival, competing risks and a no-positivity law. On two of these laws each mechanism term holds at least a tenth of the curve’s variance. tests/unit/test_ipsi_construction.py matches \(\varphi\) to transcriptions of npcausal ipsi.R and of Kim et al. (2021), Algorithm 1, to \(10^{-12}\).

Cross-fitting the tilt recursion#

Kennedy (2019), Algorithm 1, constructs every nuisance “using only \(D_0\)”, the training rows. A training row’s target is therefore built from the fold’s own predictions. IPSI ships this training-fold recursion.

part of the fit

rows

what the fit does

fold mechanism

the training rows of fold \(k\)

fit the treatment and censoring mechanisms, and read them at every row

fold recursion

the training rows of fold \(k\)

run the backward recursion, with the fold’s own regressions and mechanism in each training target

held-out curve

the held-out rows of fold \(k\)

compute \(\varphi\) from fold \(k\)’s regressions and mechanism

estimate

every row

the weighted mean of the stitched \(\varphi\)

Algorithm 1 averages the \(K\) fold means, and so does estimator (6) of Kim et al. (2021). The package pools every row, \(P_n \varphi(Z; \hat\eta_{-S})\), which is the form of Theorem 3 and of npcausal ipsi.R line 208. The two differ by \(\sum_k (n_k/n - 1/K) P_n^k \varphi\), which is 0 at equal fold sizes. Whole-cluster folds under id= can be unequal.

n_folds=1 fits every nuisance in sample. That is Algorithm 2 of Kennedy (2019), Appendix 8.3, which needs Donsker-type conditions. The default is n_folds=10. tests/unit/test_ipsi_construction.py compares the cross-fitted engine with an independent scikit-learn longhand to \(10^{-12}\). Two rejected constructions move the estimate by more than \(10^{-4}\): a stitched training target, and the average of fold means on unequal folds.

IPSI on a repeated outcome#

IPSI accepts outcome=Repeated(["Y1", "Y2", "Y3"]). The fit reports one mean per tilt and horizon, named ey_regimen[odds x2 @ t=2], and the contrasts ate_regimen[odds x2 vs natural course @ t=2]. Each horizon \(h\) is the end-of-study fit of the panel truncated after node \(h\), with \(Y_h\) as the outcome and each earlier \(Y_s\) as a covariate of node \(s + 1\). IncrementalMean(horizons=[1, 3]) and IncrementalEffect(horizons=[1, 3]) take the same horizons in a CausalStudy.

The parameter is \(\psi_h(\delta) = E[Y_h^{Q(\delta)}]\). The plan \(Q(\delta)\) draws \(A_t\) from \(q_t(1 \mid H_t)\) at every \(t \le h\) and prevents dropout. Theorem 3.1 of Kim et al. (2021) at \(t = h\) identifies it under consistency, exchangeability with dropout missing at random (Assumption A2-M), and dropout positivity (Assumption A3). A tilt needs no treatment positivity for \(\delta \in (0, \infty)\).

source (arXiv v3)

statement

code

witness

Theorem 3.1

\(\psi_t(\delta)\) is an integral of \(E[Y_t \mid H_t, A_t, R_{t+1} = 1]\) over the tilted arms

tilt_propensities, ipsi._recursion, seed_carried(horizon=h)

E4 compares the cell form with forward composition. E5 compares the fit with it

Theorem 4.1

the uncentered influence function, summed over \(s \le t\)

ipsi._weights, ipsi._recursion

E5 compares the fitted curve with the complex-step Gateaux derivative on an exact law

Theorem 5.1

a Gaussian process over \((\delta, t)\) under conditions 1 to 4

per-cell Wald interval, multiplier band over the stacked curves

E8

Kim’s \(X_t\) is \(L_t\) here, and \(R_{t+1}\) is \(C_t\). The arXiv v3 text has four typographical errors that this page and the code follow in their intended form: Algorithm 1, step 4b indexes \(\omega_s\) where it means \(\omega_t\). Condition 3 of Theorem 5.1 lacks two minus signs. The conclusion of Theorem 5.1 divides by \(n\) where it means \(\sqrt{n}\). The pointwise interval of Section 5.2 prints \(\hat\sigma^2\) where it means \(\hat\sigma\).

Single robustness. IPSI is consistent when \(\pi\) and \(\omega\) are consistent, whatever \(m\) is. It is not doubly robust. The tests below run on two exact laws with \(T = 2\) in tests/discrete_law_repeated.py. The law has a non-absorbing outcome, so \(Y_2 = 0\) can follow \(Y_1 = 1\).

witness

fit

result on the binary law at \(h = 2\)

E5

saturated \(m\), estimated \(\pi\) and \(\omega\)

the estimate equals the tilt functional, and the curve equals its Gateaux derivative, to \(10^{-11}\)

E6

\(m\) on \(W\) and the arm, estimated \(\pi\) and \(\omega\)

the estimate equals the truth. The plug-in of that \(m\) misses by +0.0471 (\(\delta = 0.5\)) and −0.0293 (\(\delta = 2\))

E6, known \(\pi\)

the same, with treatment_probabilities=

the mechanism term is zero, and the estimate equals the truth

E7

saturated \(m\), \(\pi\) and \(\omega\) on \(W\) alone

the estimate misses by +0.0008 (\(\delta = 0.5\)) and −0.0009 (\(\delta = 2\)). With \(\pi\) on \(W\) alone it misses by +0.0015 and −0.0003. With \(\omega\) on \(W\) alone it is exact

The E7 bias is small, and no sampled family can power it at the studied sizes. The exact law records it. tests/unit/test_repeated_law_values.py pins the same claim on the three-node law of the study: a saturated \(m\) with \(\pi\) and \(\omega\) on \(W\) alone misses by +0.0028 at \(\delta = 0.5\) and \(h = 3\).

Dropout. The censoring factor \(1\{C_t = 1\}/\omega_t\) enters every weight \(W_t\) up to \(h\). With a consistent \(m\) the estimate does not read \(\omega\), but the curve does. Replacing \(1/\omega_t\) by one leaves the E5 estimate and moves its curve by at least 0.5 at every tilted cell, with \(\delta = 1\) included. With a wrong \(m\) it moves the estimate (E6): by +0.0413 (\(\delta = 0.5\)) and −0.0235 (\(\delta = 2\)) on the binary law at \(h = 2\).

The band. The default simultaneous=True band covers every declared \(\delta\) and horizon, and every contrast against the reference. It is the multiplier band over the stacked influence curves.

claim

condition

\(\sqrt{n}(\hat\theta - \theta)\) is jointly normal

Kim conditions 1 to 4 at every declared \((\delta, h)\). The dimension is fixed, so the Cramér–Wold device applies. This is Theorem 5.1 restricted to a finite grid

the multiplier critical value is consistent

the stacked curves estimate their covariance consistently

the horizons are dependent

the horizons share units and mechanisms, so the band uses the joint covariance. E8 requires a nonzero correlation between the horizons of one tilt and rebuilds the critical value from the stacked curves

no band over a continuum of \(\delta\)

the band covers the declared grid. It does not cover the points between the nodes of the grid

an inconsistent \(m\)

no interval claim. The product \((\hat\pi - \pi)(\hat m - m)\) is first order, so the curve is not the influence function. \(\psi(\delta)\) depends on \(\pi\), so the projection argument of a doubly robust estimator does not apply

The registered study measures the coverage of this curve on one three-node law. With \(n\) = 8,000 in sample, the coverage of the 15 reported parameters lies between 0.945 and 0.955, and the default band covers them jointly at 0.949. The interval at \((\delta, h)\) is an asymptotic statement under the conditions above. The exact-law witnesses E4 to E8 and the mutation controls in tests/unit/test_repeated_outcome_ipsi_mutations.py stand behind the point estimate and the curve.

The IPSI contract#

item

contract

base result, complete data

Kennedy (2019): Equation (1), Theorem 1, Theorem 2, Remark 2, Algorithm 1, Theorem 3 and Theorem 4

base result, dropout

Kim, Kennedy and Naimi (2021): Theorem 3.1, Theorem 4.1, estimator (6), Theorem 5.1 and Appendix A, Algorithm 1. Appendix F, Lemmas F.2 to F.5, proves the Lemma 2 to 4 statements that Kennedy (2019) leaves unproved

rate condition

\(\sup_\delta \bigl(\lVert \hat m_t - m_t \rVert + \lVert \hat\pi_t - \pi_t \rVert\bigr) \lVert \hat\pi_s - \pi_s \rVert = o_P(n^{-1/2})\) over the node pairs of Theorem 3, condition 4. Under dropout, Kim et al. (2021), Theorem 5.1, adds the \(\lVert \hat\omega_t - \omega_t \rVert\) terms

other conditions

each \(\delta_t\) in a compact subset of \((0, \infty)\); bounded \(m_t\) and \(\hat m_t\); consistency of \(\varphi(\hat\eta)\) and of the variance (Theorem 3, conditions 1 to 3)

positivity

none at a tilt node. Positivity at each label and rule node, and dropout positivity (Kim et al. 2021, Assumption A3)

not doubly robust

every product in the rate condition holds a mechanism error, so the mechanism must be consistent at every tilt node (Kennedy 2019, Section 3.3)

in sample

n_folds=1 rests on Donsker-type conditions (Algorithm 2), not on the sample-splitting argument of Theorem 3

tests/unit/test_ipsi_exact_laws.py witnesses the missing double robustness. With an exact mechanism, a constant regression leaves no error. With an exact regression and a constant mechanism, each tilt plan misses its truth by more than \(10^{-3}\). A static plan on the same fit stays exact to \(10^{-12}\). The Taylor remainder along a path is second order and nonzero.

IPSI compositions#

composition

behaviour

evidence

censoring

the censoring factor enters every \(W_t\). g_bounds bounds the cumulative censoring, label, rule and policy product

the censored exact laws; the Kim et al. (2021) transcription

survival horizons

one backward pass per horizon, with the survival target above

the survival exact law

competing risks

one cause at a time, with the all-cause survival factor

the competing exact law

observation weights

every regression and mechanism is weighted, and the estimate is the weighted mean of \(\varphi\)

the weighted exact law. A second test shows that the weights move \(\pi_t\) by more than 0.05

clusters, id=

whole-cluster folds and a cluster-summed curve. The few-cluster rules of LTMLE apply: below 20 clusters the fit reports no interval

tests/unit/test_ipsi_e2e.py at 19 and 20 clusters

contrasts and bands

a fixed-dimension stack of the plan and horizon curves. The multiplier band covers the declared plans and horizons

tests/unit/test_ipsi_e2e.py

msm=

each cell’s \(\varphi\) is projected onto the working model. The identity link is supported. A logit or log link is accepted only when its projection converges

the exact-law projection linear in \(\log\delta\); the logit refusal in tests/unit/test_ipsi_e2e.py

known treatment mechanism

treatment_probabilities= on fit declares \(\pi_t\) for every level of the node. The tilt is then a known policy, and the curve has no mechanism term (Kennedy 2019, Section 3.3)

the known-mechanism exact law; for three levels, tests/unit/test_categorical_ipsi_declaration.py

CausalStudy

IncrementalMean and IncrementalEffect on a LongitudinalTreatment design list ipsi as available. tmle and sdr list the reasons below

tests/unit/test_ipsi_e2e.py

LTMLE also fits a tilt when every tilt node has a declared treatment mechanism. It translates each tilt node to a known stochastic policy, which divides by the bounded mechanism. It reads the whole declared matrix, one column per level, so a node with three levels is translated too and equals the hand-built known Stochastic regimen to \(10^{-12}\). Where g_bounds moves that mechanism, LTMLE refuses the fit before any learner. IPSI keeps the cancelled tilt factor and needs no such bound.

IPSI choices that differ from LTMLE#

choice

IPSI

reason

fluctuation

none. alpha=, max_iter=, tol=, stopping=, step_bound=, step_search=, iterate=, targeting= and fluctuation= raise MethodConfigurationError with code invalid_declaration

the estimator averages the influence function at the fitted nuisances

g_bounds=

bounds the cumulative censoring, label, rule and policy product. The tilt product multiplies in after the bound

the tilt factor lies in \([\min_a \delta_{t,a} / \max_a \delta_{t,a},\ \max_a \delta_{t,a} / \min_a \delta_{t,a}]\) and divides by no mechanism. With two levels this is \([\min(\delta, 1/\delta), \max(\delta, 1/\delta)]\)

regression clip

none. The default pseudo_learner is a regression Super Learner with clip=None

npcausal predicts the targets unbounded

a level outside the outcome range

reported as computed, with ParameterSpaceWarning

an average of \(\varphi\) can leave \([0, 1]\), or the q_bounds range of a continuous outcome

ratio() of such a level

CapabilityError on the log scale at or below 0, and on the odds or logit scale outside \((0, 1)\)

the scale is undefined there. The difference scale stays available

full-refit bootstrap

runs, and reports bootstrap sd with no percentile inference

no registered study measures it

truncation_curve()

replays the bounded factors only

the tilt factor has no bound to sweep

score equations

diagnostics.score_equations() is not_applicable

no node fits a fluctuation

diagnostics

diagnostics.support() reports each tilt node’s factor range, the mechanism term’s share of the curve variance, and the share of rows where some level has \(\hat\pi_t\) at exactly 0

a tilt needs no positivity, so a degenerate \(\hat\pi_t\) is reported and not warned. With two levels the share counts \(\hat\pi_t\) at exactly 0 or 1

A survival risk curve from this estimator can be non-monotone across horizons. Each horizon is its own one-step average. The hazard model and the isotonic projection are LTMLE settings, and IPSIMethod refuses them (H12).

IPSI refusals#

refusal

message source

LTMLE with an estimated mechanism at a tilt node, or LTMLE(incremental=...)

LTMLE_TILT_REFUSAL in cleverly.methods

SDR with a tilt node

SDR_TILT_REFUSAL in cleverly.methods

a scalar Incremental at a node with other than two levels

the scalar refusal of Incremental.factors, which names the node and shows a mapping. Reason invalid_declaration

an Incremental key that is not a level of a node

the key refusal of Incremental.factors, which names the key, the node and its levels. Reason invalid_declaration

an IPSI plan with a modified treatment policy or a RiskRatioTilt node

tilt_mix_message in longitudinal/regimen.py

an odds tilt at a continuous node, on IPSI, LTMLE or SDR

tilt_continuous_message in longitudinal/regimen.py

a fluctuation setting on IPSI or IPSIMethod, including stopping=, step_bound=, step_search= and iterate=

IPSI_NO_FLUCTUATION in cleverly.methods

a point-treatment container, or IPSIMethod on a point functional

IPSI_POINT_REFUSAL in cleverly.methods

a design that holds one baseline decision over every node

IPSI names TMLE(incremental=[...]) on the baseline row

an odds tilt at a vector node with a continuous component

tilt_mixed_refusal in longitudinal/regimen.py. It runs before the node’s levels are read

Scope and refusals quotes the first, second and fourth messages. tests/unit/test_ipsi_e2e.py pins each refusal before any learner runs.

IPSI limitations#

limitation

detail

finite-sample bias at one fold

on the declared study laws, a one-fold fit carries a plug-in-order bias of about 0.2 standardized units at \(n\) = 8,000. The design probe reads -0.18 on law A and -0.19 on law C. Source: tests/diagnostics/x19_sizing/reading.csv. Kennedy (2019), Algorithm 2 (Appendix 8.3), gives the one-fold interval under Donsker conditions. Saturated cell means on a finite history set form a Donsker class. Kim et al. (2021), Theorem 5.1, states the dropout result with sample splitting only, and the same Donsker argument extends it to one fold. Both studies gate the one-fold fits at sizes where this bias is small

finite-sample bias at five folds

the cross-fitted contrasts carry a second-order bias of order \(1/n\). At \(n\) = 8,000 the design probe reads 0.25 to 0.27 standardized units on law A at five folds, 0.35 at two folds, and 0.28 to 0.31 on law C. A sizing rule sets each gated cell before its run. A primary or rate cell passes with a probability of at least 0.95 at the projected bias, and a calibration cell with at least 0.90. The primary studies run at \(n\) = 96,000 and 80,000 for this reason. Source: tests/unit/test_longitudinal_incremental_design.py

support label of a three-level node

the rows G43 of the registered study categorical incremental policies cover two nodes with three levels, a tilt regimen and a cross-fitted split. The study fitted saturated learners and five folds, and the rows match any learner and fold count. Any other fit with a node of other than two levels is experimental. The label reads .support on each estimate

a band over the continuum of \(\delta\)

not offered. Theorem 4 of Kennedy (2019) gives a band over a compact set of \(\delta\), and the API represents a finite grid only

the no-effect test

not offered. Kennedy (2019), Section 4.4, gives it, and npcausal does not return it

a TMLE for the tilt over time

not offered. The package ships Kennedy’s own estimator, which Theorem 3 covers with no extension

separate covariate sets for the mechanism and the regressions

not offered. Each nuisance reads the full history

Two registered studies measure the estimator. The complete-data study carries Python property evidence. Its npcausal 0.1.0 comparator runs at commit 56a5ac11 are pending. The dropout study calibrates dropout, survival, weights and clusters against exact truths, with no comparator. This page claims no result from either study.

The natural-course value is the sample mean of the outcome. A tilt of 1 sets every weight to 1, so the correction terms cancel. Each contrast with the natural course is therefore the tilted value minus that mean. Source: test_the_fitted_natural_course_is_the_sample_mean in tests/unit/test_longitudinal_incremental_design.py.

Validation issues special to this method#

The reported standard error is the plug-in influence-curve variance. It does not implement R ltmle’s default recursive variance.method="tmle", which takes the larger of that variance and a robust estimate. R itself warns that influence-curve-only inference can be substantially anti-conservative under positivity problems or rare outcomes, and its robust method has its own availability restrictions. Active truncation is a reason to qualify the interval. It is not evidence that the interval has absorbed truncation uncertainty.

The exact laws cannot see the finite-sample choices, because every fluctuation coefficient is zero there. That is why the canonical R fixture exists, and why it is deliberately narrow. It freezes R ltmle 1.3-0 with fixed numeric mechanism predictions, intercept-only outcome regressions, no cross-fitting, and bounds of (0.2, 0.99). One baseline stratum binds only at the second cumulative prefix, and the deepest fluctuation coefficient exceeds 0.4 in magnitude. A second, censoring-active variant exists so the uncensored and trained_on masks are not inferred from a fixture in which every censoring indicator equals one. The fixture compares the estimate, every row of the influence curve, the used cumulative probabilities, and the targeting coefficients.

Its interpretation is equally narrow, and is predetermined. Disagreement means the implementation choices differ and must be reconciled. Agreement is evidence for those choices only. The independently derived law and the Gateaux checks remain the acceptance evidence for the parameter and the influence curve.

A point estimate can stay green while six influence-curve comparisons go red. Evaluating the mechanism at a constant arm does exactly that, because with an exact initial fit the fluctuation coefficient is zero, the estimate is the plug-in, and no error in the mechanism can move it. That mutation is one of two the dynamic-rule oracle carries.

Tidying a t-1 to a t reads like a correction and is not. On the survival path it silently drops every failure from its own node’s regression, biases the risk downwards, and leaves every score at 1e-16 and every convergence flag green. It is a deliberate mutation, and it turns 26 of that module’s 30 tests red.

Writing the cause’s own survival factor is wrong by exactly the mass that left through the other causes. On the competing-risks path that mutation takes 21 of 130 tests, every one of them at the second horizon, because at the first horizon there is no survival factor to get wrong.

The exact law is blind to how an earlier arm is coded into the mechanism’s design. Its learners are saturated and partition by distinct design row, under which an ordinal code and a drop-first indicator tuple are a bijection. Only a separate non-monotone glm witness separates them, and it covers one node, one link, and one truth.

A dynamic rule needs a quadrature truth, and a wider rule is not a better one. An indicator puts a step function into the integrand, where a Gauss-Hermite rule converges algebraically rather than spectrally. The naive version moved by 1.7e-3 between 48 and 64 nodes, which is worse than the Monte Carlo it exists to avoid. The axis is therefore integrated as two Gauss-Legendre panels meeting at the jump, which makes the arm constant within a panel and the answer stable to 1e-13 under refinement.

where to read the evidence

what is there

ordinary end-of-study longitudinal TMLE

comparator-backed, against R ltmle 1.3-0, including a targeted-versus-unfluctuated pair that measures what the paired comparison cannot

cross-fitted end-of-study longitudinal TMLE

comparator-backed, against R lmtp 1.5.4 on one exact five-fold assignment, with the mechanism supplied to both, plus held-out recursion checks and independent theory properties

ordinary weighted end-of-study longitudinal TMLE

comparator-backed reporting evidence against weighted R ltmle on exact-size selected samples, with target-weight and learner-weight controls

cross-fitted weighted end-of-study longitudinal TMLE

comparator-backed reporting evidence against weighted R lmtp on identical five-fold assignments, with a dedicated weighted-learner adapter

ordinary survival-curve longitudinal TMLE

comparator-backed, against R ltmle 1.3-0 with survivalOutcome=TRUE, across two horizons and with survival-recursion controls

cross-fitted survival-curve longitudinal TMLE

comparator-backed, against R lmtp 1.5.4 across both horizons on one exact five-fold assignment, with held-out and survivor-only controls

ordinary categorical longitudinal TMLE

comparator-backed, against R lmtp 1.5.4 with exact categorical mechanisms, for three treatment levels at two nodes

cross-fitted categorical longitudinal TMLE

comparator-backed, against R lmtp 1.5.4 on the identical five-fold assignment, with a flexible-tree cross-fit control

ordinary competing-risk longitudinal TMLE

comparator-backed, against R lmtp 1.5.4 with the other cause in compete= and exact mechanisms supplied to both

cross-fitted competing-risk longitudinal TMLE

comparator-backed, against R lmtp 1.5.4 on the identical five-fold assignment, with a flexible-tree cross-fit pair

longitudinal estimands outside the target registry

the parameter and influence-curve oracle, the mutation witness, and the declared gaps, for each of the five longitudinal variants

clustered cross-fitted end-of-study longitudinal TMLE

comparator-backed, against R lmtp 1.5.4 with ife 0.2.3 at 100 clusters of 40 rows, on the realized whole-cluster folds, with four cluster-robust pairs and a cluster multiplier band

cross-fitted clustered longitudinal TMLE at few clusters

the \(t_{J-2}\) reference at 20 and 30 clusters of equal and unequal sizes, with an IID control

the implementation validation grid

the twelve registered longitudinal TMLE rows and each row’s declared limits

Competing-risk correctness also rests on the independent finite law, the Gateaux comparison, the all-cause-versus-cause-specific mutation, and the one-cause reduction. The two competing-risk rows add the R lmtp comparison. Both rows keep the competing event natural and supply the mechanisms. Neither row covers weights, clustering, or an eliminated competing event.