Estimands and interventions#
Choose the object that names the scientific quantity, not the estimator you expect to use.
Every fit on this page shares one parametric configuration, so the estimand is the only thing changing between them:
from sklearn.linear_model import LinearRegression, LogisticRegression
from cleverly import CrossFitting, ModelSpec, TMLEMethod
quick = TMLEMethod(
models=ModelSpec(
outcome_learner=LinearRegression(), treatment_learner=LogisticRegression(max_iter=1000)
),
cross_fitting=CrossFitting(n_folds=5, learner_folds=3),
)
Passing no method instead inherits the default Super Learner library over ten unstratified outer
folds, which is the right default for an analysis and slow enough to be surprising in an example.
This study’s outcome is binary. A cross-fitted fit of a continuous outcome also needs a declared
Targeting(q_bounds=(lower, upper)), which
fold and outcome-scale rules
states. See
methods and learners.
Arm-indexed questions#
question |
typed estimand |
reported quantity |
|---|---|---|
Average treatment effect |
|
mean under an arm minus mean under the reference arm |
Effect among treated / controls |
|
contrast in the observed treatment-defined population |
Counterfactual level |
|
mean outcome under one or all arms |
Relative effect |
|
arm-to-reference ratio on risk or odds scale |
Natural course |
|
mean observed outcome |
Population impact |
|
natural-course mean compared with a reference intervention |
from cleverly import ATE, CounterfactualMean, RiskRatio
ate = study.estimate(ATE(reference=0), method=quick, random_state=3)
levels = study.estimate(CounterfactualMean(), method=quick, random_state=3)
relative = study.estimate(RiskRatio(reference=0), method=quick, random_state=3)
Multi-valued treatments preserve original arm labels in ParameterKey.value and
ParameterKey.reference.
NaturalCourseMean() can also target the mean before outcome nonresponse when the study declares
missingness=. That path uses the outcome regression and the response mechanism. It uses no
treatment model.
cleverly supports two scalar TMLE configurations for this target. An ordinary fit uses
CrossFitting(enabled=False). It accepts a binary outcome or a continuous outcome with fixed
q_bounds. A stacked CV-TMLE fit accepts a binary outcome and uses this cross-fitting declaration:
stacked_mar = TMLEMethod(
models=ModelSpec(
outcome_learner=LogisticRegression(max_iter=1000),
missingness_learner=LogisticRegression(max_iter=1000),
),
cross_fitting=CrossFitting(
n_folds=10,
repeats=1,
stratify_by="none",
targeting_scheme="pooled",
fold_evaluation=False,
),
)
Pass stacked_mar when you estimate NaturalCourseMean() from a study with missingness=. The
fit generates one near-balanced outer partition. Both configurations require iterative unweighted
logistic targeting. The treatment can have two or more arms. The ordinary fit also accepts fixed
weights and clusters. The stacked fit requires unweighted iid rows.
PopulationAttributableRisk and PopulationAttributableFraction accept missingness= too. The
fit stacks the natural-course mean and the reference-arm mean on the same rows, so it also uses a
treatment model:
import numpy as np
from cleverly import CausalStudy, PointTreatment, PopulationAttributableRisk
rng = np.random.default_rng(7)
observed = frame.assign(Delta=(rng.random(len(frame)) < 0.85).astype(int))
observed.loc[observed["Delta"] == 0, "Y"] = np.nan
mar_study = CausalStudy(
observed,
design=PointTreatment(
outcome="Y", treatment="A", adjustment=("W1", "W2", "W3"), missingness="Delta"
),
)
in_sample_mar = TMLEMethod(
models=ModelSpec(
outcome_learner=LogisticRegression(max_iter=1000),
treatment_learner=LogisticRegression(max_iter=1000),
missingness_learner=LogisticRegression(max_iter=1000),
),
cross_fitting=CrossFitting(enabled=False),
)
attributable = mar_study.estimate(PopulationAttributableRisk(reference=0), method=in_sample_mar)
The PAR and PAF contract gives the stack, its conditions, and the compositions it refuses.
Missing-outcome natural-course contracts lists every refused composition for both configurations. See the missing-outcome construction and evidence.
ATE, CounterfactualMean, RiskRatio, and OddsRatio also accept missingness=. Under
cross-fitting with missing outcomes, only TMLEMethod fits these targets.
CollaborativeTMLEMethod and DRTMLEMethod are refused. ATT and ATC are refused under
cross-fitting with missing outcomes. Two further requirements are not special to this surface.
Every cross-fitted fit draws CrossFitting(stratify_by="none") folds, which is the default, and
every cross-fitted continuous outcome needs Targeting(q_bounds=(lower, upper)) equal to its known
support.
The arm-indexed contract
lists every refusal. Without a known finite support, fit in sample with
CrossFitting(enabled=False). The in-sample fit keeps whichever fold policy you declare, because
one fold balances nothing.
Known regimes#
Use Static, Rule, or Stochastic interventions with RegimeMean or RegimeContrast.
import numpy as np
from cleverly import RegimeContrast
from cleverly.interventions import Rule, Static, Stochastic
plans = (
Static(1, name="treat all"),
Static(0, name="treat none"),
Rule(
lambda data: (data["W1"] > 0).astype(float),
name="treat if W1 positive",
rule_kind="known",
),
Stochastic(
lambda data: np.column_stack([np.full(len(data), 0.4), np.full(len(data), 0.6)]),
name="assign with probability 0.6",
density_kind="known",
),
)
result = study.estimate(RegimeContrast(plans, reference="treat none"), method=quick, random_state=3)
Write a Rule as a fixed rowwise function of the covariates, and declare rule_kind="known".
Choose the rule independently of the analysis sample. Rule raises CapabilityError for a rule
with no declaration and for a rule declared "estimated". For a threshold at a sample mean, the
fixed-rule curve omits the derivative through that mean. Code cannot inspect a closure, so the
fit accepts a false "known" declaration. The scope page
gives the size of that error on one exact law.
Stochastic takes the assignment density, not a scalar probability. The density is a function
of the covariate frame. It returns one column per arm in data.treatment_levels order, and its
rows sum to one. For this API, choose it independently of the analysis sample, and declare
density_kind="known".
Stochastic raises CapabilityError for a density with no declaration and for a density declared
"estimated". For a population-law target whose \(g^\star\) depends on \(P\), the regime curve
omits the derivative of that dependence. A realized learned density is a different target whose
inference needs conditions this API does not check. For the population odds tilt of the treatment
mechanism, use an incremental intervention.
On a point-treatment design, each typed estimand holds one intervention kind.
CausalStudy.identify raises CapabilityError when a set holds another kind, such as an
Incremental in RegimeContrast. The message names the typed estimands for that item.
identify raises DataError when the set is a mapping, an iterator, a string, or a single item.
Write one item as a one-item tuple.
X57 tracks a fit with two kinds.
Learned treatment rules#
Use LearnedRuleValue to estimate the value of a treatment rule that the fit learns from the data.
The fit learns one rule inside each training fold and reports the average of the values of those
rules. The interval can under-cover when the effect is zero or near zero for a share of units.
Learned rules states the
conditions.
Two registered studies measure the 95% interval over 6,000 replications at n = 2,000. The table gives its coverage at each law.
law |
coverage |
reading |
|---|---|---|
no effect for any unit |
0.8938, with a 99% interval of 0.8832 to 0.9038 |
|
a weak effect, \(0.15 W_1\) on the logit scale |
0.8948, with a 99% interval of 0.8842 to 0.9048 |
|
two laws that meet the published conditions, with the declared logistic outcome learner |
0.9465 and 0.9413 |
every verdict passes, in the gated study |
from sklearn.linear_model import LogisticRegression
from sklearn.pipeline import make_pipeline
from sklearn.preprocessing import PolynomialFeatures
from cleverly import CrossFitting, LearnedRuleValue, ModelSpec, TMLEMethod
fold_evaluated = TMLEMethod(
models=ModelSpec(
outcome_learner=make_pipeline(
PolynomialFeatures(2, interaction_only=True, include_bias=False),
LogisticRegression(max_iter=1000),
),
treatment_learner=LogisticRegression(max_iter=1000),
),
cross_fitting=CrossFitting(n_folds=5, fold_evaluation=True),
)
learned = study.estimate(LearnedRuleValue(), method=fold_evaluated, random_state=3)
record = learned.extra["learned_rule"]
shares = record.treated_shares
The rule of fold \(v\) treats a unit when the outcome regression fitted on the other folds predicts a higher outcome under treatment. A tie assigns control. The outcome learner therefore sets the rule, so give it the treatment-by-covariate terms that the rule can depend on.
The reported value is a data-adaptive target. It depends on the realized split and on the fitted rules. It is not the value of one rule fitted on all rows, and it is not the value of the optimal rule. The table gives what the result holds.
where |
content |
|---|---|
|
one parameter, |
|
the fold sizes, the fold estimates, the treated share of each fold, and quantiles of the estimated blip |
|
the rule of each row, |
|
a line that states the target |
The fit needs the fold-evaluated CV-TMLE, CrossFitting(enabled=True, fold_evaluation=True). The
default CrossFitting() refuses before any learner and names that remedy. The fit also needs a
binary treatment, complete outcomes, iid rows without weights, and no strata or intermediate
variable. CausalStudy.identify refuses each of those designs.
A refit relearns the rules, so refute reads unavailable on this fit. The package does not
implement a sensitivity analysis for this target, so every sensitivity analysis reads
unavailable too. The support report and truncation_curve run.
X11 and
F27 hold the refused
requests.
Modified treatment policies#
For a continuous dose, Shift maps each observed dose to a policy dose. cap= is part of the
policy and its support argument; it is not estimated from the sample maximum. q_bounds follows
the same rule for the outcome. Declare it from known support, and never from the observed range.
from cleverly import ModifiedTreatmentPolicyEffect
from cleverly.interventions import Shift
policies = (Shift(0.0, cap=None), Shift(0.25, cap=4.0), Shift(0.5, cap=4.0))
result = dose_study.estimate(
ModifiedTreatmentPolicyEffect(policies),
method=quick,
density_bins=40,
random_state=5,
)
A shifted dose above the cap holds the row at its own dose. The example cap of 4.0 lies inside
the observed doses. The fit issues a PositivityWarning when a declared cap lies above the largest
observed dose and the policy assigns a dose above that maximum.
Other maps of the dose use the same policies= axis. The table names each class.
class |
use it for |
|---|---|
|
a dose multiplied by a factor, held at its own value above the cap |
|
a |
|
your own map, with its inverse and its derivative on each piece |
|
a map of the levels of a categorical treatment |
|
the |
|
a vector treatment of several columns. It sets one policy for each component, as the vector treatments section states |
from cleverly.interventions import ModifiedPolicy, Piece, Scale
halve_below_3 = ModifiedPolicy(
"halve below 3",
pieces=(
Piece(
-float("inf"),
3.0,
lambda a, h: (a + 3.0) / 2.0,
lambda b, h: 2.0 * b - 3.0,
lambda b, h: 2.0,
),
Piece(3.0, float("inf")),
),
policy_kind="known",
)
policies = (Shift(0.0, cap=None), Scale(1.25, cap=5.5, name="x1.25"), halve_below_3)
A Piece without a map leaves its doses unchanged. The fit checks that every observed dose lies in
exactly one piece. It also checks that each declared inverse inverts its map, and that each declared
derivative matches the map. In a longitudinal fit, a policy is a node of
a DynamicRegimen plan. The
longitudinal reference
gives the details.
Incremental propensity-score interventions#
Incremental(delta) multiplies the observed treatment odds by delta. It avoids a conventional
positivity requirement, but the target depends on the true treatment mechanism and therefore does
not have ordinary two-sided double robustness.
A treatment with three or more levels takes one factor for each level. Declare the factors as a mapping from the level label to the factor. A level that the mapping does not name keeps the factor 1. The odds of level \(a\) against level \(b\) are multiplied by the ratio of their factors.
declaration |
meaning |
|---|---|
|
two levels only: the odds of the second sorted level times 2 |
|
the odds of |
|
the factors of |
A scalar on a treatment with other than two levels is refused before any learner, and so is a key
that is not a level. Keys are the labels that the data reports, and a position is not a label.
A vector treatment whose columns are all categorical is one treatment over its observed tuples.
Its keys are the tuples. In a longitudinal fit, one Incremental applies to every node, and its
keys are read against the levels of each node in turn.
A fit with three or more levels carries the label of the registered study
categorical incremental policies
when it matches that study’s rows. Any other such fit is labelled experimental.
from cleverly import IncrementalEffect
from cleverly.interventions import Incremental
tilts = (Incremental(1.0), Incremental(2.0), Incremental(0.5))
result = study.estimate(IncrementalEffect(tilts), method=quick, random_state=3)
Marginal structural models#
MSMProjection defines a projection of counterfactual means onto a declared working model. A
coefficient remains a projection parameter when the working model is misspecified. A written
design= function needs design_kind="known". A design that MSM.linear builds needs no
declaration.
MSM projections gives the refusals.
On a continuous dose, MSMProjection needs every outcome observed. CausalStudy.identify
raises CapabilityError when an outcome is missing. The refused table of
MSM projections lists this refusal.
X10 tracks that composition.
Controlled direct effects#
ControlledDirectEffect(intermediate=z) compares treatment arms while setting the declared
intermediate to z. Fits at two intermediate levels are two controlled direct effects, not an
automatic direct / indirect decomposition.
See the technical implementation matrix for assumptions and citations for every family.