Graph-free estimators
Contents
Install the public CausalScience distribution and use causalscience in Python.
This guide retains its 0.1.0a13 implementation baseline at committed source
bd14e4dc638982bf86fb0ba86625ab97bb2dda98.
It includes graph-free means introduced in a12 and paired ATE/pseudo-outcome CATE.
Use the maintained walkthrough for one complete
runnable block, or the individual examples below. Browser edition:
Graph-free API guide.
A graph-free call estimates a caller-selected functional of the observed
distribution; graph discovery and FMC/MC identification are not run.
The default backward learner is XGBoost; forward weights retain exact normalized minimum-L2 balance. All four entry points return point estimates with conditional fitted-score summaries. They do not automatically return causal confidence intervals.
action=1 estimates the interventional mean under that action, not automatically
an Average Treatment Effect (ATE). Use estimate_ate to pair actions 1 and 0 with checked source/evaluation units;
causal interpretation and uncertainty still require the stated identification
and learning conditions.
Back-door template
Declare treatment, outcome and the adjustment variables. In this example the
synthetic data-generating mechanism makes C a valid adjustment variable:
import numpy as np
import pandas as pd
from causalscience import estimate_bd, estimate_fd, estimate_uca
rng = np.random.default_rng(11)
c = rng.binomial(1, .5, 1200)
a = rng.binomial(1, .2 + .5*c)
y = rng.binomial(1, .1 + .3*c + .4*a)
bd_data = pd.DataFrame(dict(C=c, A=a, Y=y))
bd_schema = {v: "binary" for v in bd_data}
bd = estimate_bd(bd_data, treatment="A", outcome="Y", adjustment=["C"],
action=1, schema=bd_schema, num_boost_round=30)
assert bd["estimation_status"] == "ACCEPT"
print(bd["estimate"])
adjustment=None or [] uses no adjustment variables. This is a declared
back-door functional, not a check that the chosen set is valid. Consistency,
exchangeability given the adjustment set, overlap, source sampling and nuisance
learning conditions remain the caller’s responsibility.
Sequential back-door template
estimate_sbd declares an ordered treatment sequence. covariates[k] contains
the new covariate block measured before treatments[k]; earlier blocks and
fixed actions are included automatically. For the order C, A0, L, A1, Y, use
treatments=["A0", "A1"] and covariates=[["C"], ["L"]]. The intermediate
variable L may depend on the earlier action A0. Its distribution is averaged
conditional on that fixed action and the earlier covariates, rather than using
the marginal distribution of C, L.
from causalscience import estimate_sbd
rng = np.random.default_rng(17)
c = rng.binomial(1, .5, 1600)
a0 = rng.binomial(1, .2 + .4*c)
l = rng.binomial(1, .1 + .2*c + .5*a0)
a1 = rng.binomial(1, .1 + .1*c + .6*l + .1*a0)
y = rng.binomial(1, .05 + .15*c + .3*l + .1*a0 + .25*a1)
sbd_data = pd.DataFrame(dict(C=c, A0=a0, L=l, A1=a1, Y=y))
sbd = estimate_sbd(sbd_data, treatments=["A0", "A1"], outcome="Y",
covariates=[["C"], ["L"]], actions={"A0": 1, "A1": 1},
schema={v: "binary" for v in sbd_data}, num_boost_round=30)
assert sbd["estimation_status"] == "ACCEPT"
print(sbd["estimate"]) # Exact synthetic interventional mean is .685.
Each block retains a joint kernel, including dependent variables within a block.
Treatments and the outer covariate-block sequence must be ordered lists or tuples;
the action dictionary’s insertion order does not determine the sequence.
Supply one block per action; an empty block [] is allowed. Blocks, treatments
and outcome must be disjoint, and actions must contain exactly the treatment
names. A single action with covariates=[["C"]] reduces to the same functional
as estimate_bd(adjustment=["C"]). Results retain ordered treatments, new
covariates blocks and a snapshot of action_values.
The caller declares the temporal order, sequential exchangeability, consistency and sequential overlap. The API estimates the selected fixed-action regime mean; it does not discover the order, verify a sequential back-door criterion, or accept dynamic action policies. Schema, support, XGBoost/minimum-L2 and point-only inference contracts are shared with the other graph-free calls.
Conditional front-door template
mediators is required. covariates supplies baseline variables throughout the
conditional front-door expression, including the outer observed covariate
average. covariates=None or [] gives classic front-door. Multiple mediator or
covariate nodes retain joint kernels; the API does not assume they are independent.
rng = np.random.default_rng(11)
c = rng.binomial(1, .5, 1200)
u = rng.binomial(1, .5, 1200)
a = rng.binomial(1, .1 + .3*c + .4*u)
m = rng.binomial(1, .1 + .5*a + .25*c)
y = rng.binomial(1, .05 + .4*m + .25*c + .2*u)
fd_data = pd.DataFrame(dict(C=c, A=a, M=m, Y=y)) # U stays unobserved.
fd = estimate_fd(fd_data, treatment="A", mediators=["M"], outcome="Y",
covariates=["C"], action=1,
schema={v: "binary" for v in fd_data}, num_boost_round=30)
assert fd["estimation_status"] == "ACCEPT"
print(fd["estimate"]) # Exact synthetic interventional mean is .565.
The fixed action and the natural source action occupy different slots. Baseline covariates enter mediator, action and outcome conditionals and are averaged under their observed marginal distribution. Merely adding covariates to the final outcome regression would not implement this template. The caller must justify conditional front-door validity, support and learning assumptions.
Supply a general expression
Use the existing typed expression primitives; no graph or graph identifier is
needed. The following explicitly writes the same back-door functional as the
first example. An unbound outcome must be named with outcome:
from causalscience.indexed_graph import Slot, ObsKernel, Product, Sum
C, A, Y = map(Slot, ("C", "A", "Y"))
formula = Sum((C,), Product((
ObsKernel((("C", C),)),
ObsKernel((("Y", Y),), (("A", A), ("C", C))),
)))
uca = estimate_uca(bd_data, formula=formula, actions={"A": 1}, outcome="Y",
schema=bd_schema, num_boost_round=30)
assert uca["estimation_status"] == "ACCEPT"
np.testing.assert_array_equal(uca["estimate"], bd["estimate"])
formula may also be positional. Supported normalized source expressions use
ObsKernel, Product and Sum with typed Slot arguments. Unsupported schedules
return a reason instead of silently using a native estimator. Supplying a formula
is not proof that it equals a causal target.
Supply a self-contained typed program
An advanced user can bind supports, action literals, source stages and terminal function directly. For example, this one-stage program estimates an observed binary mean:
from causalscience.uca_estimation import SampledProgram, Kernel, Ref
Yref = Ref("observed-Y", "Y")
program = SampledProgram(
(Kernel((("Y", Yref),)),), lambda h: h[Yref], 1., {"Y": (0, 1)},
"caller-declared observational mean",
)
observed_mean = estimate_uca(bd_data[["Y"]], program, num_boost_round=30)
assert observed_mean["estimation_status"] == "ACCEPT"
The program is not mutated. XGBoost remains the default even for finite programs.
A program is already bound: separate actions, outcome, schema or domains
overrides raise ValueError. Explicit affine programs use the existing
model-specific API and are unsupported by these point-only facades. A caller’s
certificate string is recorded as user-supplied, never promoted to graph verification.
Schema, support and results
Declare each needed variable with schema={"A": "binary", "Y": "continuous"},
or metadata such as {"X": {"variable_type": "continuous", "dimension": 2,
"columns": ["X1", "X2"]}}. binary declares support (0, 1); continuous
uses the existing real-coordinate design. No variable, including treatment, is
automatically declared binary or continuous. With schema=None, explicit
domains for every needed scalar coordinate are required. For example,
domains={"C": (0, 1), "A": (0, 1), "Y": (0, 1)} replaces the binary schema.
Observed unique values or min/max are not inferred as population support.
Rows must have distinct columns and finite numeric values. Whole vector actions
must match declared dimensions. Treatment/outcome/mediator/covariate overlap,
duplicate roles and empty mediator lists are errors. Continuous fixed-point
actions remain unsupported in the default point-score route. Infeasible balance
or unsupported compilation returns estimation_status="UNSUPPORTED" and reason;
malformed inputs raise ValueError.
Successful results include status="ESTIMATED", estimation_status="ACCEPT",
estimate, score_results, and score diagnostics. Expression results retain
a snapshot of action_values and outcome; templates also retain treatment,
adjustment or mediator/covariate roles. They explicitly record:
graph_verification="not_run"andidentification_status="USER_ASSUMED";identification_basis="user_declared_template","user_supplied_expression"or"user_supplied_program";causal_interval=None,confidence_interval=None, and conditional fitted-score inference only.
There is no synthesized mc_certified=True. params, num_boost_round,
regression_factory and score_options configure the shared numerical engine;
these are learner/design choices, not identification certificates. Pass learner
parameters, seed and boosting rounds through their explicit arguments, not as
score_options duplicates; support determines the numerical backend. The existing
graph-verified route
remains available through fit_query_mean.
Paired ATE and pseudo-outcome CATE (candidate 0.1.0a13)
estimate_ate estimates one action regime minus a reference regime. For a single
binary treatment, actions 1 minus 0 define the Average Treatment Effect (ATE).
For SBD this is a contrast of two joint fixed-action regimes. Both calls reuse
the same data and seed, and actual source/training/evaluation identifiers must
match before score differences are formed. Their covariance is retained.
from causalscience import estimate_ate, estimate_cate
ate = estimate_ate(bd_data, estimator="bd", actions={"A": 1},
reference_actions={"A": 0}, treatment="A", outcome="Y", adjustment=["C"],
schema=bd_schema, num_boost_round=30)
assert ate["estimation_status"] == "ACCEPT"
print(ate["estimate"])
Use estimator="sbd" with ordered treatments and stage covariates, or
estimator="fd" with treatment, mediators and baseline covariates.
estimator="uca" accepts an unbound formula and outcome; action mappings bind
both regimes. Self-contained typed programs are unsupported by these effect
wrappers because they already bind their targets. Existing estimate_uca still
accepts them. Regime mappings must be nonempty and have identical action keys.
A Conditional Average Treatment Effect (CATE) is the action contrast conditional
on baseline modifiers. estimate_cate regresses paired full-score differences
(pseudo-outcomes) on these modifiers using XGBoost. Binary and continuous modifier
coordinates are supported. The following independent synthetic example has
CATE(0)=.2, CATE(1)=.4 and ATE=.3:
rng = np.random.default_rng(27)
z = rng.binomial(1, .5, 2400)
c = rng.binomial(1, .2 + .5*z)
a = rng.binomial(1, .1 + .3*c + .2*z)
y = rng.binomial(1, .05 + .15*c + .1*z + a*(.2 + .2*z))
effect_data = pd.DataFrame(dict(Z=z, C=c, A=a, Y=y))
cate = estimate_cate(effect_data, estimator="bd", modifiers=["Z"],
actions={"A": 1}, reference_actions={"A": 0}, treatment="A", outcome="Y",
adjustment=["Z", "C"], schema={v: "binary" for v in effect_data},
num_boost_round=30, cate_num_boost_round=30)
assert cate["estimation_status"] == "ACCEPT"
print(cate["model"].predict(pd.DataFrame({"Z": [0, 1]})))
Modifiers are whole baseline nodes, using the same schema column mappings as the
source model. BD requires them in adjustment, SBD in the first covariate block,
and FD in baseline covariates. Generic UCA requires an independent first joint
baseline kernel containing all modifiers, excluding action/outcome variables and
natural treatment copies; every later kernel must preserve the same baseline
slots in its source contexts. The actual compact statistical plan is checked,
including unchanged refs, single source and known first weight one. Unsupported
programs are declined rather than regressing an arbitrary global score.
CATE uses evaluation_design="shared" and source_design="compact"; conflicting
options are errors. The paired score rows are aligned by positional
evaluation_ids with the original modifier values. Nuisance training rows are
disjoint from all pseudo-outcome rows. The latter are split again into 70% CATE
training and 30% validation by default (cate_train_fraction controls this split).
cate_training_ids, cate_validation_ids and nuisance_training_ids retain the
actual source positions. model.predict(new_dataframe) returns a row-by-outcome
prediction array. cate_params and cate_num_boost_round control the second-stage
XGBoost model independently of the nuisance learners.
Conditional interpretation needs a valid caller-declared causal functional, positivity and source conditions, and conditional nuisance calibration (for example correct recursive regressions or appropriate true-weight identities). Finite-feature minimum-L2 balance does not certify that calibration. Second-stage regression also needs enough information and appropriate learning consistency; these are assumptions, not guarantees from a successful fit.
In a CATE result, inherited estimate, standard_error and
conditional_score_interval describe the marginal paired ATE fitted score.
Conditional predictions come from model.predict; no pointwise CATE confidence
interval is returned. validation_pseudo_mse evaluates prediction of noisy
held-out pseudo-outcomes, not error against the true CATE. Causal intervals remain
None for both effect wrappers. The whole-call API pilot
compares the four mean estimators separately from these effect-regression tests.