cleverly.estimators.TMLEResult#
- class cleverly.estimators.TMLEResult(estimates: dict[str, ~cleverly.inference.influence.ParameterEstimate], repeats: tuple[~cleverly.estimators._nuisance.RepeatFit, ...], data: ~cleverly.data.causal_data.CausalData, config: ~cleverly.estimators.base.TMLEConfig, estimator: ~typing.Any = None, provenance: ~cleverly.provenance.Provenance | None = None, simultaneous: ~cleverly.inference.multiplier.SimultaneousBands | None = None, bootstrap: ~cleverly.inference.bootstrap.BootstrapResult | None = None, intermediate_value: float | None = None, extra: dict[str, ~typing.Any] = <factory>, identified_effect: ~typing.Any = None, method: ~typing.Any = None, parameter_keys: dict[str, ~typing.Any] = <factory>, fitted_method: str = 'unknown', solved_corrections: bool = False)[source]#
Store a fitted point-treatment TMLE result.
Behaves like a mapping from estimand name to
ParameterEstimate, and carries the nuisance fits so that sensitivity and validation analyses can be run without refitting.- Parameters:
- estimates
dictofstrtoParameterEstimate Estimates keyed by stable parameter alias.
- repeats
tupleofRepeatFit One
RepeatFitper draw of the cross-fitting split – a one-element tuple for an ordinary fit,Rof them underrepeats=R. This is where the nuisances and the fluctuations actually live;nuisanceandfluctuationsread through to the first entry, which is what keeps every analysis written against a single fit working unchanged. Anything that must account for all the draws – and every analysis that produces a number, as against one that describes a mechanism, must – iterates this.- data
CausalData Validated study data and dataframe backend.
- config
TMLEConfig Normalized implementation configuration.
- estimator
AnyorNone Fitted estimator that produced this result.
- provenance
ProvenanceorNone Runtime, dependency, and data fingerprints.
- simultaneous
SimultaneousBandsorNone Joint confidence bands for multi-parameter fits.
- bootstrap
BootstrapResultorNone Optional refit-bootstrap draws and failure counts.
- intermediate_value
floatorNone The level of the intermediate variable this fit targets, or
Nonefor an ordinary point-treatment fit. It is part of the estimand, not a setting: every estimate here is a controlled direct effect holdingZat this level, and the same data yields a different parameter at the other one. Seecleverly.estimators.direct_effect.- extra
dict Method-specific fitted artifacts.
- identified_effect
IdentifiedEffectorNone Causal question and identifying assumptions.
- method
EstimationMethodorNone Typed public method configuration.
- parameter_keys
dictofstrtoParameterKey Structured identities for reported aliases.
- fitted_method
str Method identity stamped by the estimator; unknown for unstamped artifacts.
- solved_correctionsbool
Whether the fit participated in the guarded correction system.
- estimates
- Attributes:
- assessment_cache
dict Saved diagnostic and sensitivity outputs. Not a constructor argument, so every constructed result owns its own cache. A result derived with
dataclasses.replace()therefore starts empty and cannot serve the original result’s verdicts. Persistence restores the mapping without the constructor.
- assessment_cache
See also
cleverly.ParameterEstimateOne entry of
estimates.cleverly.assessment.DiagnosticsFacadeReached as
result.diagnostics.cleverly.assessment.SensitivityFacadeReached as
result.sensitivity.cleverly.longitudinal.LongitudinalResultThe same contract for a sequential fit.
Examples
>>> from sklearn.linear_model import LinearRegression, LogisticRegression >>> from cleverly import ATE, CausalStudy, PointTreatment >>> from cleverly.datasets import make_linear_ate >>> frame, _ = make_linear_ate(n=200, seed=0) >>> study = CausalStudy( ... frame, ... design=PointTreatment( ... outcome="Y", treatment="A", adjustment=("W1", "W2", "W3", "W4") ... ), ... ) >>> result = study.identify(ATE()).estimate( ... outcome_learner=LinearRegression(), ... treatment_learner=LogisticRegression(max_iter=1000), ... cross_fit=False, ... random_state=0, ... )
The result is a mapping from estimand alias to
ParameterEstimate:>>> sorted(result.estimates) ['ate'] >>> low, high = result["ate"].ci >>> low < high True
The interval bounds are ordered. The default validation battery reads the fitted artifacts without refitting:
>>> result["ate"].std_error > 0 True >>> len(result.validate().items) > 0 True
Methods#
- TMLEResult.assess(*, include_refits: bool = False, include_retargets: bool = False, arguments: Mapping[str, Mapping[str, Any]] | None = None, random_state: int | None = None) AssessmentReport[source]#
Run the applicable post-fit assessment battery.
- Parameters:
- Returns:
cleverly.AssessmentReportValidation, diagnostics, and sensitivity in one report.
See also
TMLEResult.validateRun the stored-artifact validation checks.
cleverly.assessment.DiagnosticsFacade.run_allRun diagnostics alone.
Examples
>>> from sklearn.linear_model import LinearRegression, LogisticRegression >>> from cleverly import ATE, CausalStudy, PointTreatment >>> from cleverly.datasets import make_linear_ate >>> frame, _ = make_linear_ate(n=80, seed=1) >>> result = CausalStudy( ... frame, ... design=PointTreatment( ... outcome="Y", treatment="A", adjustment=("W1", "W2", "W3", "W4") ... ), ... ).identify(ATE()).estimate( ... outcome_learner=LinearRegression(), ... treatment_learner=LogisticRegression(max_iter=1000), ... cross_fit=False, ... random_state=0, ... ) >>> result.assess().validation.passed True
- TMLEResult.coefficients(scale: str = 'link') Any[source]#
A working model’s coefficients, on the link scale or exponentiated.
scale="link"reports what the fit estimated: \(\hat\beta\), with a Wald interval on the scale the model is linear on.scale="ratio"reports \(e^{\hat\beta}\) with the interval exponentiated from that same scale, which is how the applied literature reports a log- or logit-link marginal structural model and the reason those links exist here.What the exponential means depends on the link, and the two are not interchangeable. Under
link="log"a coefficient exponentiates to a risk (or rate) ratio; underlink="logit"to an odds ratio. Reporting one as the other is a real error rather than a wording preference, so thescalecolumn of the returned frame names which it is, row by row. The intercept is a third thing again: \(e^{\beta_0}\) is a baseline mean (log) or a baseline odds (logit), not a ratio of anything, and its p-value tests \(\beta_0 = 0\) rather than any absence of effect. It is reported rather than dropped – it is a coefficient of the declared model like the others – and labelled"baseline".Refused on an identity-link fit, where \(\beta\) is a risk difference and \(e^\beta\) is not a quantity; and on a fit with no working model at all, whose parameters are counterfactual means rather than coefficients.
Nothing is re-estimated: the interval and the p-value come from the influence curve the fit already reported, read on the scale
scalenames. This is a view, in the sensecleverly.longitudinal.LTMLEResult.curve()is one.- Parameters:
- scale{“link”, “ratio”}
Coefficient scale to report.
- Returns:
dataframeOne row per working-model coefficient in the input backend.
- TMLEResult.contrast(function: Callable[[ndarray[tuple[Any, ...], dtype[float64]]], float], names: Sequence[str], *, name: str | None = None, scale: Literal['level', 'difference', 'ratio', 'fraction'] = 'difference', gradient: Callable[[ndarray[tuple[Any, ...], dtype[float64]]], ndarray[tuple[Any, ...], dtype[float64]]] | None = None, transform: Transform | None = None) ParameterEstimate[source]#
A smooth function of several estimands, with correct inference.
Applies the delta method to the joint influence curve, so the correlation between the estimands is handled rather than ignored: \(D_\phi = \nabla\phi(\hat\psi)^\top D\). For example,
result.contrast(lambda p: p[0] - p[1], ["ey1", "ey0"])compares two means already stored onresult.Pass
gradientwhen the function’s derivative is known in closed form. The default is a central difference, which is accurate to about1e-10relative – fine for reporting, but not for a test asserting agreement at1e-12.The result is an ordinary
ParameterEstimate, so it carries its own influence curve and can itself be fed back into a contrast. It inherits the inference status of the estimates it reads, so a contrast of non-inferential estimates refuses an interval exactly as they do.scale="ratio"reports a positive contrast with its interval on the log scale.transform=computes the interval and the test on the scale of a declared monotone map and maps the interval back with the inverse, asci(contrast = list(f, f_inv, h, fh_grad))in Rdrtmle1.1.2 does.- Parameters:
- function
callable() Smooth scalar function of the selected estimates.
- namessequence
ofstr Aliases passed to
functionin order.- name
strorNone Alias for the derived estimate.
- scale
str Reported scale of the derived estimate. With
transform=it must be"level"or"difference".- gradient
callable()orNone Analytic gradient.
Noneuses central differences.- transform
TransformorNone The map the interval and the test are computed on.
Nonecomputes them on the reported scale, or on the log scale forscale="ratio".
- function
- Returns:
ParameterEstimateDerived estimate with influence-curve inference.
- TMLEResult.covariance(names: Sequence[str] | None = None) ndarray[tuple[Any, ...], dtype[float64]][source]#
Return joint covariance for selected estimates.
The estimands are not independent – they are functionals of one targeted distribution and share most of their influence curve – so a contrast built from two of them needs this rather than the two variances. Computed from the influence curves at the right independent unit, so a clustered fit gets the cluster-level covariance.
Each estimate declares its
covariance_rule, and every selection size applies that one rule. The ordinary rule is the sample covariance of the curves. On a fold-evaluated CV-TMLE fit, the diagonal therefore differs from the stored cross-validated variance. The stacked cross-fitted natural-course mean declares the raw second moment, which is also its stored variance. A selection that mixes rules, or a raw second-moment selection on a clustered result, raisesValueError.On a fit whose
inference_statussupplies no inference, the matrix is the plug-in covariance of the reported curves. It is a diagnostic and it licenses no interval, for the reason that status records.
- TMLEResult.influence_frame() Any[source]#
One column per estimand of per-observation influence-curve values.
On a fit whose
inference_statussupplies no inference, no result shows the column is the estimator’s influence curve, so this frame emits curves and not inference.- Returns:
dataframeOne column per estimand of per-observation influence-curve values.
- TMLEResult.ratio(numerator: str, denominator: str, *, kind: Literal['rr', 'or'] = 'rr', name: str | None = None) ParameterEstimate[source]#
The risk ratio or odds ratio of two level estimates of this fit.
The interval is computed on the log scale and exponentiated, as
lmtp_contrast(type = "rr")andtype = "or"in Rlmtp1.5.4 compute it. The p-value tests a ratio of one.ParameterEstimate.wald_test()tests any positive null. The ratio inherits the inference status of its inputs.- Parameters:
- numerator
str The name of the level in the numerator, such as
"ey1".- denominator
str The name of the level in the denominator, such as
"ey0".- kind{“rr”, “or”}, default=”rr”
A ratio of the levels, or a ratio of their odds.
"or"needs a binary outcome.- name
strorNone, default=None The name of the derived estimate.
Nonegives the registered name when the inputs are an arm and the reference arm, such as"rr"forratio("ey[1]", "ey[0]")on a two-armed fit. Otherwise it gives"rr[<numerator> vs <denominator>]".
- numerator
- Returns:
ParameterEstimateThe ratio, on the
"ratio"scale.
- Raises:
CapabilityErrorWhen
kind="or"and the outcome is not binary.ValueErrorWhen an input is not a level, or a level is outside the domain of the log.
See also
cleverly.inference.ParameterEstimate.wald_testTest the ratio at a null.
Examples
>>> from sklearn.linear_model import LogisticRegression >>> from cleverly import CausalStudy, CounterfactualMean, PointTreatment >>> from cleverly.datasets import make_binary_outcome >>> frame, _ = make_binary_outcome(n=200, seed=0) >>> study = CausalStudy( ... frame, ... design=PointTreatment(outcome="Y", treatment="A", adjustment=("W1", "W2")), ... ) >>> result = study.identify(CounterfactualMean()).estimate( ... outcome_learner=LogisticRegression(max_iter=1000), ... treatment_learner=LogisticRegression(max_iter=1000), ... cross_fit=False, ... random_state=0, ... ) >>> rr = result.ratio("ey[1]", "ey[0]") >>> rr.name, rr.scale, rr.psi > 0 ('rr', 'ratio', True)
- TMLEResult.repeat_spread() dict[str, float][source]#
Split spread of the estimate across the cross-fitting draws, per estimand.
A diagnostic, and emphatically not a standard error. It measures how much the arbitrary fold assignment moved the answer: the
Rdraws differ in nothing but the split, so this is the size of the fold noise a single fit carries silently.Reported on the inference scale, which is the scale
std_erroris on: the original outcome scale for a level or a difference, and the log scale for a ratio. Aratioestimand therefore contributes \(\mathrm{sd}(\log\hat\psi_r)\). Read againststd_error: a spread that is an appreciable fraction of the standard error means the split mattered, and one near zero means the nuisance fits were stable enough that repeating bought little. That is a way to read the number and not a threshold on it. No rule in this package assigns the ratio a status, andRepeatSpreadRowreports it without one. A spread taken onpsiitself would not be comparable with the standard error of anrror anor, and would be wrong by roughly the estimate’s own magnitude.A draw whose
psiis not finite, or whose ratio-scalepsiis not positive, yieldsnanfor that estimand rather than-infor an exception. There is no spread on the inference scale to report there, and the caller needs the other estimands.What it must not be used for is inference. It says nothing about the sampling variability of the estimand, so it is not an alternative to the reported standard error. The median variance rule already includes between-draw displacement.
Raises when there is only one draw, since the standard deviation of one number is not a diagnostic but an artefact.
- TMLEResult.save(path: Any) Any[source]#
Write this result to a trusted joblib artifact; see
cleverly.load().Loading joblib data can execute arbitrary Python code. Only load files from a trusted source in a compatible Python and dependency environment. The artifact records
cleverly.__version__. A load by a different version warns withVersionMismatchWarningand does not migrate the result.- Parameters:
- pathpath-like
Destination file.
- Returns:
PathThe destination path, as given.
- TMLEResult.summary(*, protocol: Literal['full', 'fingerprint'] = 'full') str[source]#
A printable report of the fit.
- Parameters:
- protocol{“full”, “fingerprint”}
"full"prints every field of the study protocol record."fingerprint"prints only its schema and fingerprint, for a reader who has the record already.
- Returns:
strA printable report: the estimates, the settings, then the diagnostics.
- TMLEResult.to_frame() Any[source]#
Tidy results, one row per estimand, in the caller’s dataframe backend.
An ordinary fit emits
std_err,ci_lower,ci_upperandp_value. A fit whoseinference_statussupplies no inference emits aninferencecolumn naming that status, andplugin_std_err,plugin_interval_lowerandplugin_interval_upperin their place. Those three are a diagnostic and they are not a confidence interval. Exactly one ofstd_errandinferenceis present in any frame, so a consumer that has to tell the two apart has a total test. A fit that divided by a declared known treatment mechanism adds atreatment_mechanismcolumn that reads"known".- Returns:
dataframeOne row per estimand, in the backend the data arrived in.
- TMLEResult.validate() ValidationReport[source]#
Run the inexpensive method-appropriate checks without refitting.
- Returns:
ValidationReportOne item per check the fitted method declares, read off stored artifacts.
See also
cleverly.ValidationReportThe returned validation statuses.
cleverly.assessment.DiagnosticsFacade.run_allRun the broader diagnostic set.
Examples
>>> from sklearn.linear_model import LinearRegression, LogisticRegression >>> from cleverly import ATE, CausalStudy, PointTreatment >>> from cleverly.datasets import make_linear_ate >>> frame, _ = make_linear_ate(n=80, seed=1) >>> study = CausalStudy( ... frame, ... design=PointTreatment( ... outcome="Y", treatment="A", adjustment=("W1", "W2", "W3", "W4") ... ), ... ) >>> result = study.identify(ATE()).estimate( ... outcome_learner=LinearRegression(), ... treatment_learner=LogisticRegression(max_iter=1000), ... cross_fit=False, ... random_state=0, ... ) >>> report = result.validate() >>> len(report.items) > 0 True
Attributes#
- TMLEResult.assessment_family: ClassVar[str] = 'point'#
Which family of assessment declarations applies to this result. A class constant rather than a property: the family is a property of the result type, and nothing a fit can do changes it.
cleverly.assessmentreads it to pick the capability table. The method identity isfitted_method, which is the only name for it, because two names for one value drift.
- TMLEResult.ate#
The average treatment effect, the most commonly wanted estimand.
- TMLEResult.bootstrap: BootstrapResult | None = None#
- TMLEResult.ctmle_selection#
The collaborative selection artifact a C-TMLE fit retained, if any.
The sibling of
cv_targetingfor thectmlekey, and typed for the same reason: a reader who reaches intoextragetsAnyand has to re-assert the type at every call site.Nonecovers both an ordinary fit and a collaborative result whose artifact is absent, which the reports distinguish byfitted_method.The two classes are imported inside the property because
cleverly.estimators.ctmleimports this module, so the dependency only runs in the direction the package already has.
- TMLEResult.cv_targeting#
Fold-level detail for fold evaluation or fold-specific targeting.
Carries both the stacked and original fold-evaluated reports whichever one
result[name]was configured to show, so the two can always be compared.
- TMLEResult.diagnostics#
Unified support, nuisance, score, and refutation diagnostics.
- TMLEResult.estimate#
The sole parameter estimate, with an explicit refusal for multi-parameter fits.
- TMLEResult.fluctuations#
The first draw’s solved fluctuations, keyed by target group.
Reads through to
repeatsfor the reasonnuisancedoes, and carries the same warning: on a repeated fit this is one draw’sepsilonand one draw’s targetedQbar.
- TMLEResult.identified_effect: Any = None#
Fits made through
CausalStudyrecord this effect. Direct estimator calls leave it unset.
- TMLEResult.inference_status#
The inference status every reported estimate declares.
One status per fit: the estimator stamps it on every estimate it produces, and
inference_status()refuses a mix withValueError."influence_curve"when the fit reports no estimate, because such a fit refuses nothing.
- TMLEResult.influence_curves#
Return influence curves by parameter alias.
- TMLEResult.method: Any = None#
The normalized typed method configuration used by
IdentifiedEffect.estimate.
- TMLEResult.n#
Return the number of observations.
- TMLEResult.n_repeats#
How many draws of the cross-fitting split this fit combined.
- TMLEResult.nuisance#
The first draw’s nuisance predictions.
The attribute every sensitivity analysis and validation diagnostic was written against, kept pointing at one object so that a fit with no repeats behaves exactly as it always has. On a repeated fit this is draw zero of
Rand nothing else: it is the right thing to describe a fitted mechanism with, and the wrong thing to compute a reported number from. Userepeatsfor the latter.
- TMLEResult.nuisances#
Every draw’s nuisance predictions, in fit order.
- TMLEResult.provenance: Provenance | None = None#
- TMLEResult.replayability#
Which post-fit operations this in-memory or loaded result can replay.
- TMLEResult.score_verdict#
This fit’s own answer to whether its interval is licensed.
The same object
result.diagnostics.score_equations()returns, at the default tolerance, held here becausesummary()reports it and because a caller should not have to know which subsystem owns the question. Derived, never stored: it is recomputed from the fluctuations a result carries, so a fit reloaded from disk answers with its own records rather than with a flag written at fit time that nothing could check afterwards. Whole-result persistence retains those records directly instead of rebuilding a partial result graph.__getstate__()is what makes that true across a save, because the memo this property writes is otherwise part of the artifact.Free: it reads cached arrays and refits nothing.
- TMLEResult.sensitivity#
Capability-aware sensitivity analyses for this fitted method.
- TMLEResult.simultaneous: SimultaneousBands | None = None#
- TMLEResult.split_plan#
Return immutable assignments for every fitted repeat.
Bound to the rows that produced it, through the same data fingerprint
provenancerecords. A fold label is a position, so a plan is only meaningful for the rows it was realised on: reuse it on reordered rows, or on rows a refutation replaced, and every label points at a different unit while the row count still agrees.validate()refuses that, and a caller who means to reuse the labels on other rows says so withunbound().The plan carries each repeat’s
FoldOriginwhenrandom_partition()drew every repeat, and a fit accepts only such a plan. A stratified or in-sample fit returns a plan whoseprovenanceisNone: it records the split the fit ran, and no fit accepts it back.
- TMLEResult.estimates: dict[str, ParameterEstimate]#
- TMLEResult.data: CausalData#
- TMLEResult.config: TMLEConfig#
- TMLEResult.parameter_keys: dict[str, Any]#
Alias-to-structured-key mapping. Routing must read this rather than parse aliases.
- TMLEResult.assessment_cache: dict[str, Any]#
Persistent assessment results, keyed by operation plus normalized arguments. Filling this mapping never changes the fitted parameter or its summary.
init=Falsebecause the key records the operation and its arguments and nothing about the result that answered them.dataclasses.replaceused to pass the original’s mapping straight through, so a result derived byattach_bootstrap– which changesestimates, and therefore every sensitivity answer – served the original’s cached verdicts under a different method stamp. A constructed result now owns an empty cache, and joblib persistence restores the saved mapping with the instance state rather than through__init__.