Workflow#
cleverly makes the order of a causal analysis visible. The estimator cannot rescue an undefined
question or an implausible identification argument, so those decisions precede learner selection.
1. Formulate the causal question#
State the population, treatment strategy, outcome, time horizon, and contrast. Choose the target family. It can be an arm contrast, a regime mean, a treatment policy, an incremental intervention, an MSM projection, or a longitudinal quantity. A label such as “ATE” is insufficient when the reference arm or target population is ambiguous.
Store the study context in a StudyProtocol when the analysis needs a durable record:
from cleverly import StudyProtocol
protocol = StudyProtocol(
target_population="Adults discharged home from participating hospitals",
eligibility=("Adult at discharge", "Discharged alive", "Discharged home"),
time_zero="Hospital discharge, before navigation assignment",
treatment_strategies=("Offer navigation", "Provide usual discharge support"),
treatment_versions=("Bedside plan and two scheduled contacts", "No navigation offer"),
outcome="Thirty-day patient-reported transition score",
horizon="30 days after discharge",
intercurrent_event_handling=("Use the score regardless of readmission",),
interference_unit="Patient",
assumption_rationale=(
"Baseline adjustment covers the measured common causes of assignment and outcome",
"Reserved navigator capacity supports the no-interference argument",
),
)
This record combines selected vocabulary from the target-trial components, ICH E9(R1), and the literature on treatment versions. It is not a complete target-trial protocol or proof of ICH estimand compliance.
2. Declare the observed-data design#
Use PointTreatment for one treatment node and LongitudinalTreatment for an ordered treatment
history. Column roles are part of the design rather than learner arguments.
from sklearn.linear_model import LinearRegression, LogisticRegression
from cleverly import CausalStudy, PointTreatment
study = CausalStudy(
data,
design=PointTreatment(
outcome="outcome",
treatment="treatment",
adjustment=("age", "baseline_score", "site"),
weights="sampling_weight",
cluster="household",
),
protocol=protocol,
)
An empty adjustment set is an identification claim. Set randomized=True when randomization, not
omission, justifies it. randomized=True does not give the fit the allocation probabilities: the
treatment mechanism is still estimated. A trial that records its allocation probabilities declares
them with treatment_probabilities=, and every method then divides by them
(known treatment mechanism).
Each public object keeps one responsibility:
object |
owns |
does not own |
|---|---|---|
|
the population, eligibility, timing, strategies, versions, outcome, follow-up, intercurrent-event handling, interference unit, and assumption rationale |
the estimand contrast or analysis configuration |
study design |
observed column roles and treatment-time structure |
the scientific rationale for those choices |
typed estimand |
the mathematical contrast, intervention, and target-specific metadata |
observed column roles or learner choices |
identification |
the observed-data functional, assumptions, nuisances, and remainder condition |
the evidence that assumptions hold in this study |
typed method |
learners, cross-fitting, targeting, inference, and runtime settings |
the causal question or identification argument |
CausalStudy stamps the optional protocol onto the identified effect and each fitted result. A
summary states causal study protocol: absent when the study has no protocol record. A summary
prints the whole record by default, and summary(protocol="fingerprint") prints only its
fingerprint line.
3. Choose a typed estimand#
from cleverly import ATE
question = ATE(reference=0)
Typed estimands carry their required design and intervention information. They prevent a continuous-dose policy from being interpreted as an arm contrast and keep longitudinal regimen, horizon, and cause metadata intact.
4. Inspect identification#
effect = study.identify(question)
print(effect.functional)
print(effect.identification.assumptions)
print(effect.identification.required_nuisances)
print(effect.available_methods())
The identified effect is the boundary between scientific and computational choices. If a
composition is well posed but unsupported, cleverly raises a capability error with the missing
derivation or implementation rather than estimating a convenient different parameter.
5. Configure estimation#
Start with the ordinary "tmle" preset. Use an immutable method object when the analysis needs an
auditable configuration:
from cleverly import CrossFitting, Inference, ModelSpec, Runtime, TMLEMethod
method = TMLEMethod(
models=ModelSpec(
outcome_learner=LinearRegression(), treatment_learner=LogisticRegression(max_iter=1000)
),
cross_fitting=CrossFitting(n_folds=5, learner_folds=3, repeats=1),
inference=Inference(alpha=0.05, simultaneous=False),
runtime=Runtime(random_state=17, n_jobs=1),
)
result = effect.estimate(method=method)
Common keyword shortcuts normalize into the same objects. Unknown or inapplicable options raise
MethodConfigurationError before an engine is constructed.
The example above fits a binary outcome. A cross-fitted fit of a continuous outcome also needs
Targeting(q_bounds=(lower, upper)), holding the outcome’s known support. Without it the fit
would read its scale off every observed outcome, held-out rows included, and cleverly refuses it
before any learner runs. The narrated transition score is bounded by its own maximum, so declare
that range. An unbounded outcome has no such range, so fit it in sample with
CrossFitting(enabled=False) instead.
Fold and outcome-scale rules
gives the messages.
6. Assess estimation quality#
Inspect support, nuisance performance, targeting scores, and sensitivity in the context of the identified estimand. A solved score is evidence about the targeting step, not proof that nuisance models are consistent or that identification assumptions hold.
print(result.diagnostics.support().summary())
print(result.diagnostics.nuisance_models().summary())
print(result.diagnostics.score_equations().summary())
print(result.sensitivity.run_all().summary())
7. Report the result and its limits#
Report the parameter definition, population, identification assumptions, learner and cross-fitting
configuration, estimate and interval, support diagnostics, sensitivity analysis, and package
version or commit. This page’s fit is weighted and cross-fitted by household, and the
households differ in size. Below 40 households the interval uses a Student t reference. Below 10
households the fit reports no interval, and result.inference_status names the status. Then
report the point estimate and the reason of the status in place of the interval
(clusters).
Save structured results when an audit trail matters:
result.save("analysis.joblib")
The joblib artifact stores the complete fitted result, including estimator configuration and
nuisance-model objects. Load only trusted artifacts in a compatible Python environment.
cleverly.load() warns with VersionMismatchWarning when another cleverly version saved the
artifact. It then loads the artifact as saved, with no migration. See
results, inference, and assessment for replayability rules.