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

StudyProtocol

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.