Targeting and cross-fitting, as implemented#
The theorem fixes the estimating equations. It does not fix the order they are solved in, the number of rounds, the fold layout, or where the truncation bounds sit. This page records those choices and the evidence for each. The theorem-backed contract carries what the theorem itself states.
The alternation#
The paper’s recursive algorithm uses H_1(g) = a/g, H_2(g_1,g_2) = a·g_2/g_1 and
H_3(Q_r,g) = Q_r/g. It runs in six steps.
Initialise.
Fluctuate
Q̄alongH_1.Estimate
g_{r,1}andg_{r,2}at the once-updated outcome regression.Fluctuate along
H_2.Estimate
Q_rat the twice-updated one, then fluctuategalongH_3.Iterate until the three empirical means are approximately zero. The outcome fluctuations are fitted using only rows with
A = a, which is this package’s per-arm indicator design.
Two things the algorithm has no counterpart for and this implementation adds: a closing pass at frozen final reductions, so the reported scores are the scores of the state returned; and the truncation of the previous section.
The alternation is not guaranteed to converge. Equation (10)’s covariate becomes small on exactly the fits anybody wants, so its inner solve can be singular or stop at working precision. The archived 96-fit sweep had 87 tolerance exits, 8 stalls, and 1 cap. It had cross-fitting disabled and therefore did not cover the shipped ten-fold default.
A fixed-seed glm check produced the table below. The
counter’s historical name is ill_conditioned, but it counts every equation-(10) inner failure,
including a tolerance-limited full-rank solve; res.validate() therefore describes these as
numerically difficult rather than asserting that every Hessian was singular. The three ten-fold
rows were measured when the default draw balanced the treatment. The shipped draw is unstratified,
so those three rows are dated and the off rows are not.
n |
primary folds |
exit |
numerically difficult rounds |
final score check |
|---|---|---|---|---|
200 |
off |
cap |
0 of 50 |
passed |
200 |
10 |
stall |
3 of 46 |
passed |
1000 |
off |
tolerance |
0 of 44 |
passed |
1000 |
10 |
tolerance |
2 of 16 |
passed |
3000 |
off |
cap |
0 of 50 |
passed |
3000 |
10 |
tolerance |
0 of 8 |
passed |
The affected 10-fold n=200 solves had full-rank two-column designs and absolute scores between
4e-13 and 9e-13; the statistical score check passed because those residuals were negligible.
No argument here proves the iterates approach a common zero of the three equations, which is why
the diagnostics surface both convergence and numerical difficulty.
Every one of those verdicts is read on two rulers, and it has to be. Equations (9) and (10)
carry covariates that vanish where their own nuisance is right. A score relative to the
covariate’s own magnitude is then a ratio of two small numbers. It cannot reach a tight bar
however well the equation is solved. Each equation therefore counts as solved when it clears
either the relative tolerance or an absolute bar of 1e-3 / n.
The loop’s exit test always used both. Two verdicts read off the result did not, and the
registered study is what surfaced it. Of 24 recorded solver failures, 9 had an absolute worst
score below both that bar and the one score_check applies to the reported fit. converged was
also false on fits that exited cleanly at tolerance. failure and converged now use the same
pair.
Correcting them is bit for bit: over 360 estimates on the paper law at n = 3000 the largest
change was 9.7e-17, because both were always statements about the diagnostics rather than about
the fit.
Two neighbouring things deliberately keep the relative bar alone.
closing_capped records how the closing stage ended, not whether the score it left matters. It
is routinely true on a fit that solved everything, because equation (9)’s covariate reads the
mechanism it tilts. Read it beside failure, which answers the other question on both rulers.
The closing stage itself exits on the relative bar because of what it is for. It exists so the
mean of the curve the fit reports is zero, it costs arithmetic rather than refits, and running
every step is therefore its cheap outcome. Letting it stop on the absolute bar moved the reported
curve’s mean from 5.8e-7 to 1.5e-6 at n = 600, which
tests/unit/test_drtmle_fit.py::TestTheCurveReadsWhatTheAlternationLeft caught.
The update order#
update_order="drtmle" (default) or "benkeser". A diagnostic keyword rather than a tuning
one. The value "drtmle" takes the round order of the canonical R package: equation (9), refit only
g_{r,1} and g_{r,2}, equations (10) and (8), then refit only Q_r.
Before the first round, both orders prime Qbar with one equation-(8) step. R drtmle does not
take that step. Step 2 of the algorithm in Benkeser et al. (2017), Section 3.2, fluctuates the
outcome regression before it fits any reduced regression, and the prime follows it.
"benkeser" follows the published
six-step recursion. The working paper’s step 7 states its termination as the three empirical means being
approximately zero, so its six-step order is one route to a fixed point rather than something
Theorem 1 assumes about the returned collection. The theorem’s hypotheses are conditions on the
returned collection, not on the route. "benkeser" implements that order beside R drtmle’s,
sharing the stopping rule, stall test and closing pass, deliberately: what is in question is the
route, and a comparison in which two things differ answers nothing.
Two cautions apply. Compare the scores and the estimates, not the fluctuation coefficients. The
submodels a round passes through differ, so an epsilon from one is not an epsilon from the
other. And compare at the same nuisances: same data, same random_state.
How far the two routes agree depends on which nuisance is wrong. An earlier reading of one
draw at n = 600 recorded that they agree on ψ and differ on σ²_n, and generalised from it.
The registered study measures the same comparison on the paper law at n = 3000 over 120 paired
draws in each nuisance regime, and the generalisation does not hold. The
canonical DR-TMLE study was regenerated under the
package’s drawn unstratified ten-fold split, and the table below is that measurement. The
fold and outcome-scale rules give the split.
regime |
route difference in |
worst draw |
|---|---|---|
both nuisances correct |
7.4% |
34% |
outcome regression correct |
5.8% |
29% |
treatment mechanism correct |
45.0% |
196% |
Both routes solve all three equations to about 1e-11 on those draws, so this is not one route
failing to converge. The three equations do not pin down a single answer when the outcome
regression is misspecified. Step 7 states its termination as the three empirical means being
approximately zero, and on this law more than one state satisfies that: the route decides which
one is reached. For scale, the disagreement between this package and R drtmle in the same cell
is 51.7% of a sampling standard deviation, so cleverly against cleverly and cleverly against
R are the same size of difference and have the same cause.
The reported σ²_n differs for the reason it always did, and that part stands: the exit condition
constrains the three empirical means, while the reported variance is the second moment of a
curve built from the reductions, which the two routes leave at different vintages by construction.
The median |Δσ|/σ is 0.02% and 0.25% where the outcome regression is right, and 3.02% where it
is not. Both routes ran at the registered max_outer = 100.
Read update_order as a diagnostic, and read a large route difference as a statement about the
law rather than about the code. The
canonical DR-TMLE study carries the
measurement.
RM19 measured the prime against R on 2,000 fresh draws of the binary treatment_correct law at
n = 3000. A transcription of the R loop took the prime and three other named choices each from R or
from this package. Averaged over the settings of the other three factors, the prime moved the mean ate by
-0.000534. Its interval at the level 0.998 runs from -0.001298 to 0.000231. The whole
difference between this package and R reads no increment at the declared resolution.
What the localization design found gives each
reading.
How many rounds#
max_outer (default 50) caps the alternation. It is not max_iter, which caps the Newton
steps inside one fluctuation, and the two were confused in this project’s own published record:
the registered DR-TMLE manifest listed max_iter: 100 as its alternation setting while the loop
ran at a hard-coded 50 that no caller could reach. The value that applied is now on
result.repeats[0].fluctuations["mean"].reduction.max_outer.
Raising the cap changes only the fits that reached it. Measured on the paper law at n = 3000
over 120 draws per nuisance regime, under the package’s drawn unstratified ten-fold split, going
from 50 to 100 left all 336 tolerance exits bit for bit and moved the 24 fits that had hit the cap
by up to 1.7e-3, which is 8% of one sampling standard deviation. Act on a
"cap" exit rather than noting it: it says the draw had not settled, and the estimate reported
is the one the loop stopped at. Read it beside score_check, which says whether the scores the
fit left are small enough to matter.
The exits under a declared stopping rule#
Targeting(stopping="scaled_var") and "sample_size" change how the loop judges the outcome
equation. The alternation tests the outcome equation by the rule ratio, the largest score over its
threshold, and it keeps tol for equations (9) and (10).
Stopping rules defines the
ratio. The default stopping="relative" keeps each exit as it was.
A ratio is valid only at the pair of fit and score where it was computed. The "benkeser" order
moves the regression and the mechanism after the outcome solve, and so does each round of the
missing-outcome reduction. So these paths restate the score at the pair they judge.
site |
outcome test under a declared rule |
|---|---|
the outer exit and the stall rule, both orders |
the ratio restated at the pair after the last move. The outcome entry of the stall score is |
the closing judgements, |
the ratio of the record each one judges, computed at that record’s pair. Equation (10) is judged by the ratio of its own columns, because the closing joint solve stops by the rule over them. Equation (9) keeps |
the missing-outcome reduction |
the ratio restated from the stacked standard and extra scores. The outcome record holds that stacked ratio, and the reduction record holds the extra block’s part of it. The drift blocks keep |
|
the restatement is a no-op, so the restated ratio equals the solver’s |
the closing pass at frozen reductions |
no outer outcome test. The outcome solves apply the rule themselves |
tests/unit/test_stopping_rule_engines.py recomputes the ratio by hand at every round and compares
it with the ratio the exit read. Under update_order="benkeser", seed 5 and a step of 0.01, the
restated ratio reaches 1.75 while the solver’s own ratio is 0.966. An exit that read the solver’s
ratio would judge the rule met there. A mutation control that reads it fails the audit.
score_check() under a declared rule judges the (reduced) row by the reduction record’s rule
ratio, and it reports the D*_Q and D*_Y correction rows as diagnostics. The mechanism rows keep
the default bar. TestRuleGovernedRows in tests/unit/test_stopping_rule_report.py checks a
one-step fit in each order and a missing-outcome fit, with a mutation control that restores the
default bar. score_check() reports no rule_bound_se for a DR-TMLE fit, because the corrected
curve carries terms that the outcome columns do not.
The result summary also carries the pinned line for treatment_adjustment=, described in
treatment-mechanism covariates.
The canonical cvFolds mapping#
R drtmle’s cvFolds path maps to cross_fit=True, reduced_crossfit="pooled", targeting_scheme="pooled", cv_evaluation=False, stratify_folds="none". cvFolds draws an
unstratified V-fold split, and "none" is the only policy a cross-fitted cleverly fit accepts. The
evidence manifest states the map, the
test that pins it, and which settings the source audit refuses.
Reduced-regression cross-fitting#
reduced_crossfit="pooled" (default) reuses the primary split as it stands; "nested" takes
fold k’s training designs and targets from primary models fitted with fold k left out as
well. A diagnostic keyword rather than a tuning one, for a specific reason: the argument for
the cheap construction needs one quantity to vanish, and that quantity is the difference between
these two.
Cross-fitting is not in the theorem. The canonical mapping above is source evidence for the implemented computation, and the 2023 article’s statement that cross-validated nuisance estimates weaken the entropy conditions is supporting evidence that cross-validated DR-TMLE is intended; neither is the missing proof. What is specifically unaddressed is the pooled construction, in which an observation influences other rows’ generated regressors and then returns to its own reduced-regression prediction through those rows. Generic cross-fitting results do not cover that because the conditional independence they turn on is exactly what the generated design breaks.
Cross-fitting does not repair a one-wrong fit whose mechanism breaks the level sets of g_0. The
probe used a wrong outcome regression and a gradient-boosting mechanism on law L. The ten-fold
default gave an ATE bias of +1.53, +2.01 and +1.51 SD at n = 1,000, 4,000 and 16,000. That bias is
in the estimator’s population limit at the fitted mechanism, so cross-fitting does not remove it.
A data-adaptive mechanism gives the measurement.
The argument supplied here. Writing ĥ_k for what fold k’s reduced regression contributes and
h̃_k for the nested version:
(P_{n,k} − P_0) ĥ_k = (P_{n,k} − P_0) h̃_k [A]
+ (P_{n,k} − P_0) Δ_k ∘ ĝ^(−k) [B], Δ_k = Q̂_r^(−k) − Q̃_r^(−k)
[A] is the ordinary cross-fitting argument: conditional on the fold-k complement both maps
are fixed, so the term has conditional mean zero and is o_p(n^(−1/2)) under the L_2 convergence
Theorem 1 already assumes. [B] is the whole of the open question, because Δ_k is precisely
the object that depends on fold k; no cross-fitting lemma reaches it. What does is asymptotic
equicontinuity, needing two conditions:
(E) the reduction learner’s fitted functions of one or two scalars lie, with probability tending to one, in a class whose bracketing entropy is bounded uniformly in the underlying measure.
The structural fact that makes (E) available is that the reductions have fixed dimension:
one scalar for the default construction and two for the bivariate reduced probability, however
many covariates the fit adjusted for. Composition with a fixed map transports brackets exactly,
so the entropy requirement falls entirely on a class of functions of at most two variables and
not at all on the primary nuisances’ complexity. The measure-free phrasing is load-bearing, since
the pushforward is a random measure and a bound holding at P_0 says nothing about it. A
fixed-basis linear or logistic model is a bounded fixed-dimensional sieve; histogram
boosting is a fixed bounded-variation ball when its iteration and leaf limits are fixed. A
CV-selected round count would take boosting out of the class. A random forest is outside.
(S)
‖Δ_k‖_{L_2} = o_p(1): the reduction fit isL_2-continuous in the design and target columns it is handed.
(S) is the open condition. It is free for a fixed-basis linear smoother and not free for
anything that selects structure from the data: a split point, a bandwidth, a CV-chosen
candidate. An arbitrarily small design perturbation can move any of those selections discretely.
Boosting is entropy-safe and design-continuity-unsafe. reduced_crossfit="nested" is what computes
Δ_k; a measured dispatch put its consequence on ψ at or below what a redrawn fold split moves
in every cell, which is supported, not shown, since a consequence can hold by cancellation.
Two things not to read into “nested”. It costs K times the primary nuisance fitting, and was
measured at 1.3x to 17x a pooled fit’s wall clock over four draws, reaching the outer cap on
two. What dominates that cost is not the extra fitting: nested reductions are noisier, so equation
(10)’s near-singular solve takes more rounds. And neither construction makes the targeted
collection fold-independent: epsilon is solved on all n rows, since
targeting_scheme="fold" is refused, so a nested fit is nested in the nuisance models and pooled
in the tilt.
Where the truncations are#
On the univariate construction, g_{r,2}’s bound is fixed at fit time, because the array is
a regression of a quotient by the mechanism. It is the only bound in the package chosen at fit
time rather than at targeting time. Two consequences a reader will otherwise trip on.
DiagnosticsFacade.truncation_curvemoves the clever covariate’s denominator and does not move these arrays. That part of the curve is flat by construction.ReducedSet.g_boundsis on record, so a reader of such a curve can find out that the sweep never reached them.gr1is stored untruncated and bounded at read time throughReducedSet.bounded_gr1, column by column and not complemented across arms.
One further condition sits beside (E) and is a rate rather than an entropy bound. g_{r,2}’s
target is (1_a − ĝ)/ĝ at the bounded mechanism, so its envelope is 1/lo − 1, and equation
(10)’s covariate divides by g_{r,1} truncated at the same bound, so that envelope is O(1/lo²).
Under g_bounds="auto", lo → 0 and the envelope grows with n. That pulls against the
bound-sequence row of the scope section, where exactly that shrinkage
is what makes the truncation asymptotically inactive. Both are open.
The bound-inactive scope#
Truncation is not in the theorem’s algorithm. The theorem’s D_g is evaluated at the same
g* its score is solved at, and that g* is not truncated anywhere. One mechanism, produced by
an unconstrained expit fluctuation, appears identically in equation (9)’s covariate, in equation
(9)’s residual, and in D_A. Boundedness is an assumption about g_0, not an operation on
ĝ. So the theorem supports neither this package’s original hybrid (bounded
denominator, raw residual) nor R’s post-fit clip.
The package instead uses a constrained estimating equation. It puts clip inside F and
solves that for a root, so the final score is the declared estimator’s score
(fluctuation/mechanism.py::solve_bounded_mechanism). Where nothing clips, that solver returns
the unconstrained solve untouched, so such a fit is the estimator Theorem 1 is stated for, not
an approximation to it.
The guarantee is therefore scoped rather than assumed through:
a fit whose truncations are inactive is inside the theorem-backed contract;
a bound-active fit is empirically supported and reported as outside it. It is not a failing fit: on
weak_overlap_dgpevery identity holds at1e-17and every score is negligible while a third of the(row, arm)pairs clip at the initial mechanism.
Three truncations must be inactive, not one. A randomized missing-outcome fit has five truncations because it divides by two separately bounded mechanisms. The last truncation has no corresponding theorem assumption:
truncation |
witness on the fit |
in Theorem 1’s assumptions |
|---|---|---|
|
|
an assumption on |
|
|
an assumption on |
|
|
the same one |
|
|
the same one |
|
|
none. It is a regression of an arm indicator on |
The two observation rows are not implied by the treatment ones, which is why they are
separate columns rather than a wider reading of the existing ones. A randomized trial’s
treatment mechanism is flat by design and cannot clip, while its observation mechanism is a
fitted probability that can sit at its floor on a large share of rows. A check reading only the
treatment witnesses therefore reports contract = "theorem" on a fit that is squarely bound-active.
tests/unit/test_drtmle_missing.py::TestTheContractSeesTheObservationTruncations is the pair of
fits that says so: a well-behaved trial reading "theorem", and one whose π̂ is pinned on a
fifth of its rows reading "bound-active" with only the two π entries named.
The asymptotic half of the inactive-bound claim needs three conditions, and only the first is Theorem 1’s.
g_0 ∈ [δ, 1−δ]. This one is the theorem’s.A bound sequence eventually below
δ.g_bounds="auto"supplies it as5/(√n·log n) → 0. A fixed bound aboveess inf g_0does not supply it at all.ĝconsistent in sup norm. This is stronger than theL_2conditions the theorem assumes, and it is unverified.
CorrectionCheck.contract reports "theorem", "bound-active" or "none". It is a scope
label and not a verdict. CorrectionCheck.passed deliberately does not read it, because
folding it in would report a perfectly well-solved fit as broken.