# 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: ```python 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](../technical-reference/cv-tmle.md#fold-and-outcome-scale-rules) states. See [methods and learners](methods-learners.md). ## Arm-indexed questions | question | typed estimand | reported quantity | | --- | --- | --- | | Average treatment effect | `ATE` | mean under an arm minus mean under the reference arm | | Effect among treated / controls | `ATT`, `ATC` | contrast in the observed treatment-defined population | | Counterfactual level | `CounterfactualMean` | mean outcome under one or all arms | | Relative effect | `RiskRatio`, `OddsRatio` | arm-to-reference ratio on risk or odds scale | | Natural course | `NaturalCourseMean` | mean observed outcome | | Population impact | `PopulationAttributableRisk`, `PopulationAttributableFraction` | natural-course mean compared with a reference intervention | ```python 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: ```python 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: ```python 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](../technical-reference/point-treatment-tmle.md#par-and-paf-with-missing-outcomes) gives the stack, its conditions, and the compositions it refuses. [Missing-outcome natural-course contracts](../technical-reference/scope-and-refusals.md#missing-outcome-natural-course-contracts) lists every refused composition for both configurations. See the [missing-outcome construction and evidence](../technical-reference/point-treatment-tmle.md#missing-outcomes-and-controlled-direct-effects). `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](../technical-reference/scope-and-refusals.md#missing-outcome-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`. ```python 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](../technical-reference/scope-and-refusals.md#wrong-by-construction) 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](#incremental-propensity-score-interventions). 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](../roadmap.md#x57-joint-intervention-axes-and-tilt-compositions) 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](../technical-reference/point-treatment-tmle.md#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 | `under-covers at the exceptional law`, in the [boundary study](../technical-reference/method-evidence/learned-rule-cvtmle-boundary.md) | | a weak effect, $0.15 W_1$ on the logit scale | 0.8948, with a 99% interval of 0.8842 to 0.9048 | `under-covers at the weak_blip law`, in the same study | | 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](../technical-reference/method-evidence/learned-rule-cvtmle.md) | ```python 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 | | --- | --- | | `learned.estimates` | one parameter, `ey_learned_rule[learned rule]` | | `learned.extra["learned_rule"]` | the fold sizes, the fold estimates, the treated share of each fold, and quantiles of the estimated blip | | `learned.nuisance.regimes.values[:, 1, 0]` | the rule of each row, `1.0` for treat | | `learned.summary()` | 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](../roadmap.md#x11-learned-policy-follow-ups) and [F27](../roadmap.md#f27-learned-policy-value-outside-the-published-conditions) 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. ```python 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 | | --- | --- | | `Scale(factor, cap=...)` | a dose multiplied by a factor, held at its own value above the cap | | `Piecewise(pieces)` | a `Shift` or a `Scale` on each interval of the dose | | `ModifiedPolicy(name, pieces=..., policy_kind="known")` | your own map, with its inverse and its derivative on each piece | | `ModifiedPolicy(name, apply=..., policy_kind="known")` | a map of the levels of a categorical treatment | | `RiskRatioTilt(delta)` | the `ipsi` tilt of `lmtp` on a 0/1 treatment, reported as `ey_rr_tilt`. Fit it with `TMLE(policies=...)`. The typed study requests refuse it | | `JointPolicy(name, components={column: policy}, policy_kind="known")` | a vector treatment of several columns. It sets one policy for each component, as the [vector treatments](../technical-reference/point-treatment-tmle.md#vector-treatments) section states | ```python 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](../technical-reference/longitudinal-tmle.md#modified-treatment-policies-at-a-node) 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 | | --- | --- | | `Incremental(2.0)` | two levels only: the odds of the second sorted level times 2 | | `Incremental({"high": 2.0})` | the odds of `"high"` against every other level times 2 | | `Incremental({"high": 2.0, "low": 0.5})` | the factors of `"high"` and `"low"`, and 1 for the other levels | 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](../technical-reference/method-evidence/categorical-incremental.md) when it matches that study's rows. Any other such fit is labelled `experimental`. ```python 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](../technical-reference/msm-projections.md#variations) 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](../technical-reference/msm-projections.md#variations) lists this refusal. [X10](../roadmap.md#x10-continuous-dose-msm-with-a-second-mechanism) 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](../technical-reference/index.md) for assumptions and citations for every family.