API Reference¶
Ordered the way the library is used: the two objects everything is built on (a causal graph and an
SCM), then the agents and environments that act in them, then the causal machinery those agents
call — intervention sets, identification, transport, discovery — and finally the decision, bounds
and certificate layer that scores a policy. The — Complete Reference sections at the end carry an
entry for every name in causalrl.__all__, grouped by causalrl.API_TIERS.
The full-lifecycle agent front doors are CausalMBRLAgent (fit → act on confounded logs) and
OnlineCausalMBRL (learn the SCM while acting); both are in the complete-reference sections below.
Graphs¶
causalrl.scm.graph.CausalGraph
¶
A causal graph: a DAG over observed variables plus bidirected edges that denote unobserved confounders (an ADMG).
Source code in src/causalrl/scm/graph.py
10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 | |
directed_edges
property
¶
The directed edges as (parent, child) pairs.
bidirected_edges
property
¶
The bidirected (latent-confounding) edges.
complete_parents
property
¶
Nodes asserted to have NO unobserved parent, mapped to why.
See :meth:has_complete_parents.
has_bidirected_edges()
¶
has_complete_parents(node)
¶
Whether node's observed parents are asserted to be all the parents there are.
This is the assertion that converts a bound into a point estimate, and it is the one thing a graph cannot learn from data: no observational test distinguishes "no unobserved parent" from "an unobserved parent I did not measure". It is licensed by design -- a randomised assignment, a feature flag, an experimenter's schedule, a rule-based pricing policy -- where the mechanism that sets the node is written down and contains nothing else.
The library already had :class:~causalrl.scm.fitters.PinnedMechanism, which asserts the
node's equation. That is a different and weaker claim: an equation can be supplied while
the world still feeds the node something you did not model. This asserts the absence.
Source code in src/causalrl/scm/graph.py
assert_complete_parents(*nodes, reason)
¶
A copy of this graph in which nodes have no unobserved parents, and why.
Bidirected edges incident to nodes are DROPPED, because they contradict the assertion:
a latent common cause of A and Y is an unobserved parent of A. This is what
upgrades an effect from bounded to identified, so it is deliberately a distinct call with a
mandatory reason rather than a constructor flag -- the assertion is a trust primitive,
and an incorrect one silently converts an honest interval into a false point estimate.
reason travels with the graph into every certificate built from it, so a reader can see
which claims rest on it.
Source code in src/causalrl/scm/graph.py
has_incident_bidirected_edges(node)
¶
c_components()
¶
Connected components of the bidirected graph (isolated nodes are singletons).
remove_incoming_edges(node)
¶
Return a copy with all directed edges into node removed (graph mutilation).
Source code in src/causalrl/scm/graph.py
ancestors(nodes)
¶
Ancestors of nodes via directed edges, INCLUDING the inputs (the inclusive An(·)
convention used by the identification/POMIS literature).
Source code in src/causalrl/scm/graph.py
descendants(nodes)
¶
Strict descendants of nodes via directed edges (excludes the inputs).
Source code in src/causalrl/scm/graph.py
induced_subgraph(nodes)
¶
Subgraph on nodes: keep directed/bidirected edges with both endpoints in nodes.
Source code in src/causalrl/scm/graph.py
do_mutilate(intervened)
¶
ADMG mutilation for do(intervened): drop incoming directed edges to each
intervened node AND every bidirected edge incident to an intervened node
(intervention severs latent confounding into the set). Distinct from
remove_incoming_edges, which keeps bidirected edges.
Source code in src/causalrl/scm/graph.py
latent_projection(keep)
¶
Latent projection onto keep: marginalize out every node not in keep, adding a
directed edge for each directed path through removed nodes and a bidirected edge for
each confounding path through removed nodes (the Tian-Pearl / Verma projection).
Directed Vi -> Vj when a directed path from Vi to Vj has all interior
nodes removed; bidirected Vi <-> Vj when a removed common cause (a marginalized
node, or the latent behind a bidirected edge) reaches both through removed interiors.
Removing a collider induces no confounding (its parents are never in its reached set).
Source code in src/causalrl/scm/graph.py
Structural Causal Models¶
causalrl.scm.scm.StructuralCausalModel
¶
Executable explicit-latent DAG SCM supporting L1/L2/L3 queries.
Bidirected-edge ADMGs are accepted by :class:CausalGraph for analytical graph
algorithms, but they are not executable SCMs: shared latent causes must be represented
as explicit parent nodes with their own mechanism and exogenous distribution.
Source code in src/causalrl/scm/scm.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 | |
do(interventions)
¶
Layer 2: return the mutilated SCM under do(interventions). Original is unchanged.
Preserves provenance/fit_report: the un-intervened mechanisms are the same
objects (shallow-copied dict), so a fitted model's L3 guard must still see them as
fitted after mutilation -- otherwise abduct on the result would silently stop
refusing point counterfactuals for the non-invertible nodes do never touched.
Source code in src/causalrl/scm/scm.py
non_invertible_nodes()
¶
Nodes whose fitted mechanism cannot recover its noise from (parents, value).
Mechanisms without an invertible attribute are treated as invertible: a hand-written
mechanism is an assertion by its author, so its coupling is given rather than inferred.
Source code in src/causalrl/scm/scm.py
abduct(evidence=None, *, known=None, n=20000, seed=None, atol=1e-06)
¶
Layer 3, step 1 — infer the exogenous posterior given evidence/known noise.
known pins supplied exogenous values exactly (the exact, continuous path: no
rejection). Remaining exogenous are sampled; if evidence is given they are
rejection-filtered so the factual evaluation matches evidence within atol.
Returns an :class:ExogenousPosterior; call its predict(do=...).
On a fitted SCM containing a non-invertible node this raises: L1 data identifies the
mechanisms but not the noise-to-value coupling, so a point counterfactual would report a
modelling choice as a result. Use :func:causalrl.counterfactual_interval instead. A
"mixed" model (some nodes pinned, some learned) is gated the same way: pinning one
equation does not identify the coupling at a node that was still learned from data.
Source code in src/causalrl/scm/scm.py
counterfactual(evidence, interventions, n, *, seed=None, atol=1e-06)
¶
Layer 3: abduction-action-prediction. Sugar over :meth:abduct + predict.
Source code in src/causalrl/scm/scm.py
see(n, *, seed=None)
¶
Assumption-Aware Agents¶
causalrl.agents.dovi.DOVI
¶
Bases: Agent
Deconfounded Optimistic Value Iteration (Wang, Yang, Wang, Bareinboim 2021), tabular.
Finite-horizon backward induction (LSVI form) whose optimism is capped by the Manski
upper causal bound on each (state, action)'s immediate reward, deconfounding the value
estimate. With horizon H::
r̃(s,a) = min( mean_online(s,a) + ucb_bonus(s,a), hi_Manski(s,a) )
Q_h(s,a) = r̃(s,a) + Σ_s' P̂(s'|s,a) · V_{h+1}(s')
V_h(s) = max_a Q_h(s,a), V_{H+1} ≡ 0, h = H, H-1, …, 1
P̂ is the empirical next-state distribution pooled from offline logs and online steps;
transitions that ended an episode (done) carry zero future value (the absorbing
terminal). At H = 1 the backup reduces exactly to v0.2's immediate Manski ceiling, so
the horizon-1 DTR result is preserved.
Bound validity: the backup is a certified optimistic bound on the return only when the
dynamics do not depend on the hidden confounder (true of SequentialDTREnv, whose
transitions are U-independent). On ConfoundedGridworld the behavior policy steers
around the hidden hazard, so P̂ is confounded and the backup is heuristic value
propagation, not a certified bound.
Source code in src/causalrl/agents/dovi.py
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 | |
is_certified
property
¶
Whether value propagation is within the documented causal guarantee.
causalrl.agents.scbandit.POMISThompsonSampling
¶
Bases: _ArmSubsetThompsonSampling
Thompson sampling over only the arms whose intervened-variable set is a POMIS.
manipulable (the variables you may intervene on) must be given explicitly; non-manipulable
variables are handled by the POMIS engine via latent projection (r40). When every non-reward
variable is manipulable this matches the unconstrained POMIS.
Source code in src/causalrl/agents/scbandit.py
Causal Gymnasium Wrapper¶
causalrl.envs.wrapper.CausalEnvWrapper
¶
Bases: Wrapper[Any, Any, Any, Any]
A Gymnasium wrapper that exposes the wrapped environment's causal structure.
Parameters¶
env:
Any gymnasium.Env. When the env carries a non-None .scm attribute and
reward_node is provided (and valid), the full causal interface is enabled.
When env.scm is None or reward_node is None, construction still
succeeds but the causal interface is disabled (pass-through mode).
reward_node:
The SCM variable name that represents the reward / return signal. Optional —
None disables the causal interface. If the env has a live SCM and
reward_node is supplied but not present in the graph, a ValueError is raised.
Attributes¶
reward_node:
The SCM node treated as the reward/return, or None when not set.
has_causal_interface:
True iff the wrapped env exposes a non-None SCM and reward_node is set
and present in the graph. All causal methods require this to be True.
reward_parents:
Direct SCM parents of reward_node in topological order. Requires
has_causal_interface.
scm:
The underlying :class:~causalrl.scm.scm.StructuralCausalModel (live reference to
the wrapped env's .scm), or None when not available.
active_interventions:
The currently stored intervention mapping, or None if no persistent
intervention is active.
Source code in src/causalrl/envs/wrapper.py
68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 | |
has_causal_interface
property
¶
True iff the causal interface is fully operational.
Requires the wrapped env to expose a non-None .scm and reward_node
to be set and present in the graph.
scm
property
¶
The underlying SCM (live reference to the wrapped env's .scm), or None.
reward_parents
property
¶
Direct SCM parents of reward_node in graph-topological order.
These are the variables that causally determine the immediate reward signal. Pass
their names as the factor_nodes argument of
:func:~causalrl.agents.factored_advantage.factored_advantage.
Raises¶
CausalInterfaceUnavailableError
When has_causal_interface is False.
active_interventions
property
¶
The currently stored intervention mapping, or None if none is active.
do(interventions)
¶
Return a new SCM mutilated by do(interventions).
The running environment's SCM is NOT modified. This is a pure causal-graph query suitable for off-policy reasoning or shaping.
Parameters¶
interventions:
Mapping {node_name: value} passed to
:meth:~causalrl.scm.scm.StructuralCausalModel.do.
Returns¶
StructuralCausalModel The mutilated SCM under the specified do-intervention.
Raises¶
CausalInterfaceUnavailableError
When has_causal_interface is False.
Source code in src/causalrl/envs/wrapper.py
intervene(node, value)
¶
Convenience wrapper: do({node: value}).
Parameters¶
node: The SCM variable to intervene on. value: The value to pin it to (scalar, sequence, or Tensor).
Returns¶
StructuralCausalModel
The mutilated SCM under do({node: value}).
Raises¶
CausalInterfaceUnavailableError
When has_causal_interface is False.
Source code in src/causalrl/envs/wrapper.py
set_intervention(interventions)
¶
Store a persistent intervention that affects subsequent reset and step.
After this call, every reset() and step() swaps the wrapped env's .scm
to the pre-computed mutilated SCM for the duration of the call, then restores the
original SCM in a finally block. Precomputed baselines stored on the env
(e.g. arm_values) are not recomputed.
Parameters¶
interventions:
Mapping {node_name: value} to pin persistently.
Raises¶
CausalInterfaceUnavailableError
When has_causal_interface is False.
Source code in src/causalrl/envs/wrapper.py
clear_intervention()
¶
Remove the persistent intervention; subsequent calls use the unintervened SCM.
reset(*, seed=None, options=None)
¶
Reset the wrapped environment, forwarding seed and options.
When a persistent intervention is active (set_intervention was called), the
wrapped env's .scm is temporarily replaced with the mutilated SCM for the
duration of this call, then restored unconditionally.
Source code in src/causalrl/envs/wrapper.py
step(action)
¶
Step the wrapped environment.
When a persistent intervention is active (set_intervention was called), the
wrapped env's .scm is temporarily replaced with the mutilated SCM for the
duration of this call, then restored unconditionally.
Source code in src/causalrl/envs/wrapper.py
Gymnasium Env Registration¶
causalrl.envs.registration.register_envs()
¶
Register causalrl demo environments in the Gymnasium registry.
Calling this function more than once in the same process is safe (idempotent).
After calling this function (or importing causalrl), you can use::
import gymnasium
import causalrl # triggers register_envs()
env = gymnasium.make("causalrl/StructuralCausalBandit-v0")
vec = gymnasium.make_vec("causalrl/StructuralCausalBandit-v0", num_envs=2)
Source code in src/causalrl/envs/registration.py
Causal Graph-Factored Advantage (CGFA-PPO)¶
The pure-NumPy rollout arithmetic (no framework dependency):
causalrl.agents.factored_advantage.factor_rewards(factor_trace)
¶
Per-factor rewards r^factor_{k,t} = phi_k(s_{t+1}) - phi_k(s_t).
This is the per-step quantity the CGFA environment wrapper publishes in
arXiv:2605.06066 §E.1: the change in each SCM factor across a transition. The factors
phi(s) are the SCM parents of the return node — for a
:class:~causalrl.envs.wrapper.CausalEnvWrapper those are wrapper.reward_parents.
Parameters¶
factor_trace:
Array of shape (T + 1, K): the factor values phi(s_0), …, phi(s_T) observed
along a rollout of T transitions.
Returns¶
NDArray[np.float64]
Shape (T, K) — the first difference along the time axis.
Raises¶
ValueError
If factor_trace is not 2-D or has fewer than two rows.
Examples¶
import numpy as np phi = np.array([[0.0, 1.0], [1.0, 1.0], [1.0, 4.0]]) # (T+1=3, K=2) factor_rewards(phi) array([[1., 0.], [0., 3.]])
References¶
- Cunha, Mian, French and Liu (2026), arXiv:2605.06066, §E.1.
Source code in src/causalrl/agents/factored_advantage.py
causalrl.agents.factored_advantage.factor_gae(rewards, values, *, gamma, lam=1.0, bootstrap_values=None, dones=None)
¶
Per-factor advantages and returns for the CGFA-PPO factor critic.
Runs the standard generalised-advantage recursion independently per factor::
delta_{k,t} = r^factor_{k,t} + gamma * V_k(s_{t+1}) * (1 - done_t) - V_k(s_t)
A_{k,t} = delta_{k,t} + gamma * lam * (1 - done_t) * A_{k,t+1}
G_{k,t} = A_{k,t} + V_k(s_t)
At lam=1.0 with no bootstrap this collapses exactly to the paper's written
equations: G_{k,t} = sum_i gamma^i r^factor_{k,t+i} (Eq. 8) and
A_{k,t} = G_{k,t} - V_k(s_t) (Eq. 10).
.. note:: Paper ambiguity. arXiv:2605.06066 writes Eq. 8 / Eq. 10 as a
Monte-Carlo return and its residual, but the surrounding prose (§5, §E.2) says the
per-factor advantage is computed "using the same generalised-advantage truncation as
the scalar critic", and Table 6 lists a single GAE lambda = 0.95 for the shared
backbone. Those two readings coincide only at lambda = 1. lam therefore
defaults to 1.0, which reproduces the paper's explicit equations; pass
lam=0.95 for the prose/Table-6 reading.
Parameters¶
rewards:
Per-factor rewards, shape (T, K) — typically :func:factor_rewards output.
values:
Per-factor critic estimates V_k(s_t), shape (T, K) — typically
:meth:~causalrl.agents.cgfa_critic.FactoredCritic.values.
gamma:
Discount factor, shared with the scalar return (paper default 0.995).
lam:
GAE lambda. 1.0 (default) reproduces Eq. 8 / Eq. 10 exactly.
bootstrap_values:
V_k(s_T) for the state after the last stored transition, shape (K,).
None (default) means zero — an episode that ended at T.
dones:
Boolean episode-termination flags, shape (T,). dones[t] true cuts the
bootstrap and the recursion at step t. None means no interior terminations.
Returns¶
tuple[NDArray[np.float64], NDArray[np.float64]]
(advantages, returns), both of shape (T, K).
Raises¶
ValueError
On any shape mismatch between rewards, values, bootstrap_values and
dones, or if rewards is not 2-D.
Examples¶
Two factors that accumulate at different rates, with a zero critic so the advantage is the raw discounted return (Eq. 8):
import numpy as np r = np.array([[1.0, 0.0], [0.0, 1.0]]) V = np.zeros((2, 2)) adv, ret = factor_gae(r, V, gamma=0.5) ret array([[1. , 0.5], [0. , 1. ]])
References¶
- Cunha, Mian, French and Liu (2026), arXiv:2605.06066, Eq. 8, Eq. 10, §E.2.
Source code in src/causalrl/agents/factored_advantage.py
284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 | |
causalrl.agents.factored_advantage.blend_advantages(scalar_advantages, factor_advantages, *, gate, weights=None)
¶
The CGFA-PPO state-conditional residual blend (arXiv:2605.06066, Eq. 11).
A_used(s_t, a_t) = (1 - g(s_t)) A^scalar(s_t, a_t) + g(s_t) * sum_k w_k A_k(s_t, a_t)
g -> 0 recovers vanilla PPO exactly; g -> 1 hands the policy update entirely to
the factor-aligned signal. In the paper g is a learned state-conditional MLP and
w = softmax(beta) are learned mixture logits initialised from the SCM's
logistic-regression coefficients on the return node; both are supplied by
:class:~causalrl.agents.cgfa_critic.FactoredCritic. Passing constants here is a valid
ablation (the paper's "CGFA without the gate" row).
Parameters¶
scalar_advantages:
The ordinary (scalar-critic) advantage, shape (T,).
factor_advantages:
Per-factor advantages A_{k,t}, shape (T, K) — :func:factor_gae output.
gate:
g(s_t) in (0, 1): shape (T,) or a scalar broadcast over the rollout.
weights:
Mixture weights w_k, shape (K,). None means uniform 1/K. The paper
uses softmax(beta), which sums to 1; this function does not renormalise, so an
unnormalised vector is passed through as given.
Returns¶
NDArray[np.float64]
Shape (T,) — the advantage to feed to the policy-gradient surrogate.
Raises¶
ValueError
On any shape mismatch, or if gate falls outside [0, 1].
Examples¶
import numpy as np a_scalar = np.array([1.0, 1.0]) a_factor = np.array([[4.0, 0.0], [4.0, 0.0]]) blend_advantages(a_scalar, a_factor, gate=0.0) # pure PPO array([1., 1.]) blend_advantages(a_scalar, a_factor, gate=1.0) # pure factored, uniform w array([2., 2.]) blend_advantages(a_scalar, a_factor, gate=0.5) # half and half array([1.5, 1.5])
References¶
- Cunha, Mian, French and Liu (2026), arXiv:2605.06066, Eq. 11, §E.3.
Source code in src/causalrl/agents/factored_advantage.py
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 | |
causalrl.agents.factored_advantage.factored_advantage(factor_values, baselines, *, config=None, aggregation='sum', weights=None)
¶
Combine per-factor value estimates against a common baseline into one advantage.
Given K causal parent factors of the return, and for each rollout step a vector of
per-factor value estimates V_1, …, V_K and a shared scalar baseline b, the
per-factor advantage is A_i = V_i - b and the combined advantage is their (weighted)
sum or mean.
When K = 1 (single factor) the output reduces exactly to the standard advantage
A = V - b, so this is a strict generalisation of the scalar advantage.
Relation to CGFA-PPO¶
This is a decomposition primitive, not Eq. 10 of arXiv:2605.06066. Eq. 10 is
A_{k,t} = G_{k,t} - V_k(s_t): the per-factor return minus the per-factor value,
i.e. a per-factor baseline of shape (T, K), which this signature cannot express (its
baselines is (T,)). Use :func:factor_gae for Eq. 10 and
:func:blend_advantages for the Eq. 11 residual blend; both are consumed by
:class:~causalrl.agents.cgfa_critic.FactoredCritic.
Parameters¶
factor_values:
Array of shape (T, K) where T is the number of rollout steps and K is
the number of causal factors (SCM parents of the return). Each column [:,i]
is the critic's value estimate for factor i.
baselines:
Array of shape (T,) — the shared scalar baseline for each step (typically the
current value-function estimate V(s_t)).
config:
A :class:FactoredAdvantageConfig that carries factor_nodes, aggregation,
and optional weights. When provided, aggregation and weights keyword
arguments are ignored (config takes precedence).
aggregation:
Used when config is None. "sum" (default) or "mean".
weights:
Used when config is None. Per-factor weights, shape (K,). None
means uniform unit weights.
Returns¶
NDArray[np.float64]
Shape (T,) — the combined causal-graph-factored advantage for each step.
Raises¶
ValueError
If factor_values is not 2-D, if baselines length does not match T, or
if weights shape does not match K.
Examples¶
Single-factor (reduces to standard advantage):
import numpy as np V = np.array([[2.0], [3.0], [1.0]]) # (T=3, K=1) b = np.array([1.5, 2.5, 0.5]) factored_advantage(V, b) array([0.5, 0.5, 0.5])
Two-factor sum (CGFA-PPO with two SCM parents of the return):
V2 = np.array([[2.0, 1.0], [3.0, 0.5]]) # (T=2, K=2) b2 = np.array([1.5, 2.0]) factored_advantage(V2, b2) # A_i = V_i - b; sum over i array([ 0. , -0.5])
References¶
- Cristiano da Costa Cunha, Ajmal Mian, Tim French, and Wei Liu (2026). "Causal Reinforcement Learning for Complex Card Games: A Magic: The Gathering Benchmark." arXiv:2605.06066.
Source code in src/causalrl/agents/factored_advantage.py
119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 | |
causalrl.agents.factored_advantage.FactoredAdvantageConfig
dataclass
¶
Configuration bundle for :func:factored_advantage.
Parameters¶
factor_nodes:
Ordered list of SCM parent-node names whose per-factor value estimates are passed to
:func:factored_advantage. The order must match the columns of the
factor_values array.
aggregation:
How to combine per-factor advantages into the final scalar advantage.
* ``"sum"`` (default): ``A = Σ_i A_i`` — the formulation in arXiv:2605.06066.
* ``"mean"``: ``A = mean_i A_i`` — normalises when the number of factors varies.
weights:
Optional per-factor weights w_i (length must match factor_nodes when
provided). The combined advantage becomes A = Σ_i w_i * A_i for
aggregation="sum" or the weighted mean for aggregation="mean". None
means uniform unit weights.
Source code in src/causalrl/agents/factored_advantage.py
weights_array
property
¶
The validated per-factor weight vector (uniform unit weights when unset).
The K-head critic that makes it an algorithm (needs the causalrl[torch] extra):
causalrl.agents.cgfa_critic.FactoredCritic
¶
The K-head causal-factor critic of CGFA-PPO (arXiv:2605.06066, §E.1).
One value head per SCM parent of the return, on a trunk shared with the scalar critic,
plus the learnable mixture logits and the state-conditional residual gate that turn those
K heads into a single advantage for the policy-gradient surrogate.
The heads only differentiate because they are trained against different targets: head
k regresses the per-factor return G_{k,t} accumulated from
r^factor_{k,t} = phi_k(s_{t+1}) - phi_k(s_t). Give every head the same target and
they collapse to the same function — which is exactly the degenerate "share one value
head" configuration this class exists to replace.
Parameters¶
obs_dim:
Dimensionality of the (flattened) observation fed to the trunk.
factor_nodes:
Ordered SCM node names of the return's causal parents — for a
:class:~causalrl.envs.wrapper.CausalEnvWrapper, wrapper.reward_parents. Their
order fixes the column order of every (T, K) array this class consumes or emits.
config:
A :class:CGFACriticConfig; None uses the paper's defaults.
mixture_init:
Initial beta, shape (K,). None gives zeros (a uniform mixture). The
paper seeds beta with the SCM's logistic-regression coefficients on the return
node, so that training starts from the structural prior.
seed:
Seed for torch's global RNG before parameter initialisation, for reproducibility.
Raises¶
ImportError
At construction, when PyTorch is not installed — naming the causalrl[torch]
extra.
ValueError
On a non-positive obs_dim, an empty factor_nodes, duplicate factor names, or
a mixture_init whose length does not match factor_nodes.
Examples¶
import numpy as np critic = FactoredCritic(obs_dim=3, factor_nodes=["X3", "U"], seed=0) obs = np.zeros((5, 3)) scalar, factors = critic.values(obs) factors.shape (5, 2) float(critic.mixture_weights().sum()) 1.0
References¶
- Cunha, Mian, French and Liu (2026), arXiv:2605.06066 — §5, Appendix E, Algorithm 1.
Source code in src/causalrl/agents/cgfa_critic.py
267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832 833 834 835 836 837 838 839 840 841 842 843 844 845 846 847 848 849 850 851 852 853 854 | |
factor_nodes
property
¶
The ordered SCM parent names, one per value head.
n_factors
property
¶
K — the number of causal factors, hence of value heads.
config
property
¶
The architecture / loss-coefficient bundle this critic was built with.
module
property
¶
The underlying torch.nn.Module — for state_dict, .to(device), saving.
parameters()
¶
Every learnable tensor: trunk, three heads, and the mixture logits beta.
Hand these to a joint optimiser alongside the actor's parameters when following Algorithm 1 line 22 literally (one Adam step on the whole of Eq. 13).
Source code in src/causalrl/agents/cgfa_critic.py
values(observations)
¶
(V(s_t), V_k(s_t)) for a batch of observations, shapes (T,) and (T, K).
Source code in src/causalrl/agents/cgfa_critic.py
gate(observations)
¶
The state-conditional residual gate g(s_t) in (0, 1), shape (T,) (§E.3).
Source code in src/causalrl/agents/cgfa_critic.py
mixture_weights()
¶
w = softmax(beta), shape (K,) — the learned per-factor mixture (§E.1).
Source code in src/causalrl/agents/cgfa_critic.py
advantages(observations, factor_rewards, scalar_advantages, *, gamma=0.995, lam=1.0, bootstrap_values=None, dones=None, normalize=True)
¶
Algorithm 1 lines 11-13: per-factor GAE, then the gated residual blend.
Runs the critic over the rollout, computes A_{k,t} and G_{k,t} against its own
factor heads (Eq. 8, Eq. 10), blends them into the caller's scalar advantage with the
learned gate and mixture weights (Eq. 11), and normalises the result.
Parameters¶
observations:
Rollout observations s_0 … s_{T-1}, shape (T, obs_dim).
factor_rewards:
r^factor_{k,t}, shape (T, K) —
:func:~causalrl.agents.factored_advantage.factor_rewards of the factor trace.
scalar_advantages:
The RL framework's own advantage A^scalar_t, shape (T,). Passing the
framework's GAE (rather than this class's scalar head) is the intended path: the
paper keeps the host PPO's critic and adds to it.
gamma:
Discount, shared with the scalar return (Table 6: 0.995).
lam:
Per-factor GAE lambda; see
:func:~causalrl.agents.factored_advantage.factor_gae for the paper ambiguity.
bootstrap_values:
V_k(s_T) for a truncated rollout, shape (K,); None means the episode
ended.
dones:
Episode-termination flags, shape (T,).
normalize:
Standardise A_used to zero mean / unit variance across the rollout, as
Algorithm 1 line 13 does. Set False to inspect the raw blend.
Returns¶
CGFAAdvantages
used feeds the surrogate; returns feeds :meth:update.
Source code in src/causalrl/agents/cgfa_critic.py
losses(observations, factor_returns, *, scalar_returns=None, scm_effects=None)
¶
The differentiable Eq. 13 components this class owns, as torch scalars.
L_ppo and L_ent are the RL framework's; pass its surrogate to
:meth:objective to assemble the whole of Eq. 13.
Parameters¶
observations:
Shape (T, obs_dim).
factor_returns:
G_{k,t}, shape (T, K) — :attr:CGFAAdvantages.returns.
scalar_returns:
Bootstrapped scalar returns R_t, shape (T,). None (default) zeroes
L_value, which is correct when the RL framework owns the scalar critic.
scm_effects:
eps_{k,t}, the SCM-predicted per-factor change, shape (T, K). None
zeroes L_cal — the paper's "CGFA without calibration" ablation.
Raises¶
ValueError
On any shape mismatch, or if factor_returns has a column count other than
:attr:n_factors.
Source code in src/causalrl/agents/cgfa_critic.py
551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 | |
blend(observations, scalar_advantages, factor_advantages)
¶
Eq. 11 as a differentiable torch tensor, shape (T,).
A_used = (1 - g(s_t)) A^scalar_t + g(s_t) sum_k w_k A_{k,t}, evaluated under the
current gate and mixture parameters so gradients reach g and beta.
.. note:: Paper ambiguity, and why this method exists. Algorithm 1 computes
A_used once at line 12 — outside the epoch loop — and stores it; a surrogate
built on that stored value gives g(s) and beta no gradient at all, so
with the Table 6 default c_e = 0 the only two parameters the paper calls
"learnable" would never move. Yet §E.3 speaks of the gate collapsing and logs its
evolution, and Algorithm 1 line 22 updates beta. The consistent reading is
that A_used is recomputed inside the minibatch loop; use this method to build
the surrogate, and both parameters train. Feeding
:attr:CGFAAdvantages.used (a detached NumPy array) to the surrogate instead
reproduces the literal, frozen-gate reading.
Parameters¶
observations:
Shape (T, obs_dim).
scalar_advantages:
A^scalar_t, shape (T,) — treated as a constant, as PPO always does.
factor_advantages:
A_{k,t}, shape (T, K) — likewise constant; the gradient path here is
through g and w, not through the factor critic.
Source code in src/causalrl/agents/cgfa_critic.py
objective(losses, *, policy_loss=None)
¶
Assemble Eq. 13 from :meth:losses output and the framework's surrogate.
L_CGFA = policy_loss + c_v L_value + c_f L_factor + c_c L_cal - c_e L_gate.
policy_loss is the caller's L_ppo - c_H L_ent (the clipped surrogate net of
its entropy bonus), ideally built on :meth:blend so the gate and mixture logits
receive gradient. Omit it to optimise only the critic-side terms — a deviation from
Algorithm 1 line 22, which steps actor and critic together; see the module docstring.
Source code in src/causalrl/agents/cgfa_critic.py
update(observations, factor_returns, *, scalar_returns=None, scm_effects=None, epochs=1, batch_size=None, seed=None)
¶
Algorithm 1 lines 14-22, restricted to the terms this class owns.
Each minibatch step evaluates :meth:losses, assembles :meth:objective, clips the
global gradient norm to :attr:CGFACriticConfig.max_grad_norm, and takes one Adam
step over the trunk, the three heads, and beta.
Parameters¶
observations, factor_returns, scalar_returns, scm_effects:
As in :meth:losses.
epochs:
Passes over the rollout (Table 6: 10).
batch_size:
Minibatch size (Table 6: 256). None uses the whole rollout.
seed:
Seed for the minibatch shuffle.
Returns¶
CGFAUpdateStats Mean losses over the steps taken, plus the Algorithm 1 line 25 diagnostics (per-factor calibration correlation, credit share, gate distribution) recomputed on the full batch after the update.
Raises¶
ValueError
If epochs is not positive, batch_size is not positive, or any array shape
disagrees.
Source code in src/causalrl/agents/cgfa_critic.py
703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744 745 746 747 748 749 750 751 752 753 754 755 756 757 758 759 760 761 762 763 764 765 766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 | |
causalrl.agents.cgfa_critic.CGFACriticConfig
dataclass
¶
Architecture and loss coefficients for :class:FactoredCritic (arXiv:2605.06066).
Defaults follow the paper's Table 6 where it states a value, and are flagged in the module docstring where it does not.
Parameters¶
hidden:
Widths of the shared critic trunk. Table 6 uses (512, 256) for a
3,077-dimensional observation; the default here is small enough for toy problems.
gate_hidden:
Hidden width of the state-conditional gate MLP (Table 6: 32).
gate_init:
Value of g(s) at initialisation, uniform over states (Table 6: 0.5). The gate
head's output layer is zero-initialised with the matching bias, so the gate starts
state-independent and differentiates as it trains.
learning_rate:
Adam learning rate for the critic parameters (Table 6 backbone: 3e-4).
value_coef:
c_v — weight on the scalar value loss. Only active when scalar_returns are
supplied to :meth:FactoredCritic.update; a caller whose RL framework owns the
scalar critic leaves them out and this term is exactly zero.
factor_coef:
c_f — weight on the per-factor MSE, Eq. 9 (Table 6: 0.5).
calibration_coef:
c_c — weight on the intervention-calibration loss, Eq. 12 (Table 6: 0.1).
gate_entropy_coef:
c_e — weight on the Bernoulli gate-entropy bonus (Table 6 default: 0.0, swept in
the ablation). Eq. 13 subtracts it, so a positive value rewards an undecided gate.
calibration_eps:
delta — the stability constant in Eq. 12's denominator.
min_std:
Lower clamp on the per-factor standard deviations in Eq. 12 (§E.4 requires a clamp
but gives no value).
max_grad_norm:
Global gradient-norm clip, Algorithm 1 line 22 (Table 6: 0.5).
Source code in src/causalrl/agents/cgfa_critic.py
causalrl.agents.cgfa_critic.CGFAAdvantages
dataclass
¶
One rollout's worth of CGFA advantage arithmetic (Algorithm 1 lines 11-13).
Attributes¶
used:
A_used, shape (T,) — the advantage to hand to the policy-gradient surrogate.
scalar:
The scalar-critic advantage that was blended in, shape (T,).
factor:
Per-factor advantages A_{k,t}, shape (T, K) (Eq. 10).
returns:
Per-factor returns G_{k,t}, shape (T, K) (Eq. 8) — the regression targets
:meth:FactoredCritic.update expects.
gate:
g(s_t), shape (T,).
weights:
w = softmax(beta), shape (K,).
Source code in src/causalrl/agents/cgfa_critic.py
causalrl.agents.cgfa_critic.CGFALosses
dataclass
¶
The differentiable Eq. 13 terms :class:FactoredCritic owns, as torch scalars.
Attributes¶
value:
L_value — the scalar critic's MSE (Algorithm 1 line 17). Exactly zero when the
caller's RL framework owns the scalar critic and supplies no scalar_returns.
factor:
L_factor — the per-factor MSE of Eq. 9, averaged over the K heads.
calibration:
L_cal — the negative mean Pearson correlation of Eq. 12. Exactly zero when no
SCM-predicted effects are supplied (the paper's "without calibration" ablation).
gate_entropy:
L_gate — the mean Bernoulli entropy of g(s_t) (Algorithm 1 line 20).
Source code in src/causalrl/agents/cgfa_critic.py
causalrl.agents.cgfa_critic.CGFAUpdateStats
dataclass
¶
Loss values and the per-factor diagnostics of Algorithm 1 line 25.
Attributes¶
total:
Mean of the optimised objective over the update's minibatch steps.
value, factor, calibration, gate_entropy:
Mean of each Eq. 13 component, before its coefficient.
mixture_weights:
softmax(beta) after the update, shape (K,).
factor_correlation:
Per-factor Pearson correlation corr(A_{k,t}, eps_{k,t}) after the update, shape
(K,); all-nan when no SCM effects were supplied. This is the paper's
headline calibration diagnostic: it says whether each learned factor advantage moves
in the same direction as the SCM's predicted intervention effect.
credit_share:
mean_t |w_k A_{k,t}| / sum_j |w_j A_{j,t}|, shape (K,) — which causal factors
dominate the policy update.
gate_mean:
Mean g(s_t) after the update. Near 0 means the gate has collapsed to vanilla
PPO; near 1 means the update rides entirely on the factor-aligned advantage.
Source code in src/causalrl/agents/cgfa_critic.py
Intervention Sets¶
causalrl.identification.intervention_sets.pomis(graph, reward, manipulable=None)
¶
All POMISs for reward: a deduplicated, canonically sorted list of frozensets.
frozenset() (the observational regime) appears when it is possibly optimal. When
manipulable is given, only those variables may be intervened on: by r40's Theorem 4
this is the unconstrained POMIS of the latent projection onto manipulable | {reward}.
Source code in src/causalrl/identification/intervention_sets.py
causalrl.identification.intervention_sets.minimal_intervention_sets(graph, reward, manipulable=None)
¶
All MISs for reward: a deduplicated, canonically sorted list of frozensets.
When manipulable is given, the result is filtered to sets that avoid the
non-manipulable variables (r40: a constrained MIS is just the filtered unconstrained MIS).
Source code in src/causalrl/identification/intervention_sets.py
causalrl.identification.intervention_sets.requires_experiment(graph, treatment, outcome)
¶
Whether learning P(outcome | do(treatment)) requires experimentation (Task 2, "when").
Returns True exactly when the effect is not identifiable from observational (L1) data,
so an online experiment (an L2 intervention) is necessary, and False when offline data
already suffices. This is the "when to intervene" companion to POMIS's "where": :func:pomis
narrows which intervention sets could be optimal, while this decides whether you must
intervene at all. Delegates to the complete ID algorithm
(:func:causalrl.identification.id_algorithm.is_identifiable_effect).
Source code in src/causalrl/identification/intervention_sets.py
causalrl.identification.intervention_sets.AdmissibleInterventions
¶
POMIS for a graph, recomputed for the variables each context actually permits.
:func:pomis answers "where could it be optimal to intervene" once, at design time. A live
agent needs the same answer per decision, because feasibility moves with the state: a lever
can be unavailable this step and available the next. This holds the graph fixed and takes the
manipulable set as the varying input.
Restricting the manipulable set is not a filter of the unconstrained POMIS. It is the
unconstrained POMIS of the latent projection onto the manipulable variables (Lee &
Bareinboim, AAAI 2019, Theorem 4 — the r40 result :func:pomis already implements). Projecting
away a variable turns paths through it into bidirected edges, which can change the territory
and border and so introduce possibly-optimal sets that the unconstrained enumeration never
listed. Dropping the sets that mention an infeasible variable would therefore lose optimality,
not merely prune; that is why :meth:sets re-enters :func:pomis rather than filtering a
cached list.
Because re-entering is not free and the manipulable set usually changes rarely, results are
memoised on it. cache_size bounds the memo; the oldest entry is evicted when it is
exceeded, which suits the common pattern of a feasible set that oscillates among a handful of
configurations.
Source code in src/causalrl/identification/intervention_sets.py
143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 | |
reward
property
¶
The reward variable these intervention sets target.
sets(manipulable)
¶
The POMISs available when exactly manipulable may be intervened on.
The reward itself is never manipulable and is ignored if present, matching :func:pomis.
Source code in src/causalrl/identification/intervention_sets.py
arms(space)
¶
Every admissible intervention worth considering in space, deduplicated.
The two halves of the decision, composed: :meth:sets says which variables could be worth
setting given what space permits, and
:meth:~causalrl.intervention.InterventionSpace.assignments turns each of those sets into
the concrete assignments an agent chooses between. The observational regime appears as the
empty intervention whenever the empty set is possibly optimal.
Order is deterministic — sets in :func:pomis's canonical order (by size, then name), and
assignments in the order the space enumerates them — so a tie broken by position is stable
across runs rather than dependent on set iteration order.
Source code in src/causalrl/identification/intervention_sets.py
Benchmark Reports¶
causalrl.eval.benchmark.BenchmarkEstimate
dataclass
¶
A per-seed benchmark measurement with simple descriptive uncertainty.
Source code in src/causalrl/eval/benchmark.py
causalrl.eval.benchmark.run_confounded_chain_benchmark(*, seeds=(0, 1, 2, 3, 4), n_steps=8000, tail_window=2000, n_mc=2000)
¶
Report POMIS, brute-force, and fixed-set behavior on the confounded-chain demo.
Source code in src/causalrl/eval/benchmark.py
causalrl.eval.benchmark.run_frontdoor_benchmark(*, seeds=(0, 1, 2, 3, 4), n_steps=30000, tail_window=10000, n_mc=20000)
¶
Report manipulability-aware and naive-filter behavior on the front-door demo.
Source code in src/causalrl/eval/benchmark.py
Counterfactual Decision-Making (L3 / ETT)¶
causalrl.identification.counterfactual.counterfactual_expectation(scm, *, outcome, intervention, evidence, n=20000, seed=None)
¶
Return E[ outcome_{do(intervention)} | evidence ] (a Layer-3 counterfactual mean).
Wraps :meth:StructuralCausalModel.counterfactual (abduction-action-prediction) and averages
the outcome over the retained, evidence-consistent units. With empty evidence this reduces
to the interventional mean E[outcome_{do(intervention)}].
Source code in src/causalrl/identification/counterfactual.py
causalrl.identification.counterfactual.effect_of_treatment_on_treated(scm, *, treatment, outcome, treated, control, n=20000, seed=None)
¶
Effect of Treatment on the Treated: E[Y_{treated} - Y_{control} | treatment = treated].
The treatment effect among the subpopulation that actually received treated (Pearl,
Causality §8.2.1). Under confounding this differs from the average treatment effect. Both
potential outcomes use the same seed (common random numbers), so they are evaluated on the
same abducted units and the difference is matched and low-variance.
Source code in src/causalrl/identification/counterfactual.py
causalrl.agents.counterfactual.CounterfactualOptimalPolicy
¶
Bases: BatchAgent
Plays argmax_a E[Y_{do(action_node=a)} | intent] from a known SCM.
The Layer-3 oracle: it precomputes the Regret Decision Criterion table once at construction and
then acts greedily on the observed intent. update is a no-op — the model is known, so there
is nothing to learn online. The computed table is exposed as decision_table for inspection.
Source code in src/causalrl/agents/counterfactual.py
Transportability¶
causalrl.identification.transport.SelectionDiagram
dataclass
¶
A causal graph plus the variables whose mechanism differs between source and target.
Each selection variable carries an implicit selection node S -> variable (Bareinboim &
Pearl). selection_variables must be a subset of graph.nodes.
Source code in src/causalrl/identification/transport.py
causalrl.identification.transport.transport_formula(diagram, *, treatment, outcome, max_adjustment_size=3)
¶
Return the transport formula for P*(outcome | do(treatment)), or None if it is not
provably transportable within the supported class (direct / S-admissible adjustment).
Source code in src/causalrl/identification/transport.py
causalrl.identification.transport.is_transportable(diagram, *, treatment, outcome)
¶
Whether the target effect is provably transportable (see :func:transport_formula).
Source code in src/causalrl/identification/transport.py
causalrl.identification.transport.transported_effect(formula, *, treatment, treated_value, outcome, source, target, n=40000, seed=None)
¶
Compute E*[outcome | do(treatment=treated_value)] via the transport formula.
direct: the source interventional mean transfers unchanged. adjustment: reweight the
source conditionals E[outcome | treatment, z] by the target covariate marginal P*(z)
(discrete z assumed; the demo uses binary covariates). Strata absent from the source sample
are skipped.
Source code in src/causalrl/identification/transport.py
causalrl.identification.transport.is_backdoor_admissible(graph, treatment, outcome, z)
¶
Back-door criterion: z has no descendant of treatment and blocks every back-door path
(treatment ⊥ outcome | z in the graph with treatment's outgoing edges removed).
Source code in src/causalrl/identification/transport.py
General Transportability (sID)¶
causalrl.identification.transport.transport_estimand(diagram, *, treatment, outcome)
¶
The general (sID) transport estimand for P*(outcome | do(treatment)) over diagram.
A :class:SelectionDiagram adapter over
:func:causalrl.identification.id_algorithm.identify_transport: each target c-factor is taken
from the source if its mechanism is invariant, else identified from the target. Raises
:class:~causalrl.exceptions.NotIdentifiableError when not transportable. Generalizes the
direct / S-admissible-adjustment :func:transport_formula (which returns a readable closed form
for those two cases).
Source code in src/causalrl/identification/transport.py
causalrl.identification.id_algorithm.identify_transport(graph, treatment, outcome, selection)
¶
Return an :class:Estimand for the target effect P*(outcome | do(treatment)) across a
selection diagram, or raise :class:NotIdentifiableError.
selection lists the variables whose mechanism differs between source and target (each
carries an implicit selection node). The target effect is decomposed into c-factors; a c-factor
with no selection-marked variable is invariant and taken from the source distribution, while
one that contains a selection-marked variable is identified from the target's observational
distribution. With an empty selection this reduces to source identification (the ID
algorithm).
This is the sound c-factor-routing transportability algorithm; it subsumes direct and S-admissible-adjustment transportability. It is not the complete sID (a c-factor identifiable only by combining source and target is reported as non-transportable rather than guessed).
Faithful to J. Pearl, E. Bareinboim, Transportability of Causal and Statistical Relations, AAAI 2011 / External Validity, Statistical Science 2014, via Tian's c-factor decomposition.
Source code in src/causalrl/identification/id_algorithm.py
causalrl.identification.id_algorithm.is_transportable_effect(graph, treatment, outcome, selection)
¶
Whether the target effect P*(outcome | do(treatment)) is transportable (see
:func:identify_transport).
Source code in src/causalrl/identification/id_algorithm.py
causalrl.identification.id_algorithm.estimate_transported_effect(graph, treatment, outcome, selection, source_data, target_data, *, do)
¶
Estimate the target effect P*(outcome | do(treatment = do)) from source + target data.
Identifies the transport estimand (:func:identify_transport), then evaluates it with source
c-factors read from source_data and target c-factors from target_data (both discrete
integer columns over graph.nodes). Returns the outcome distribution keyed in
sorted(outcome) order.
Source code in src/causalrl/identification/id_algorithm.py
Multiple Domains And Experiments (mz / meta)¶
causalrl.identification.id_algorithm.Domain
dataclass
¶
A source domain relative to the target: which mechanisms differ, and what data it offers.
selection lists the variables whose mechanism differs from the target (each carries an
implicit selection node S -> v); a c-factor transfers from this domain only if it touches no
selection-marked variable. experiments lists intervention targets available here (each a set
of variables that can be randomized). The target domain is always implicitly available
observationally as a fallback, so it need not be listed.
Source code in src/causalrl/identification/id_algorithm.py
causalrl.identification.id_algorithm.identify_transport_general(target_graph, treatment, outcome, domains)
¶
Return an :class:Estimand for the target effect P*(outcome | do(treatment)) combining
one or more source domains with the target's own data, or raise
:class:NotIdentifiableError.
Generalizes :func:identify_transport to multiple domains (meta-transportability) and to
surrogate experiments (mz-transportability): each target c-factor is taken from any domain whose
mechanism for it is invariant and that can identify it from its observational or experimental
data, with the target as the fallback. With a single observational source it coincides with
:func:identify_transport; with no selection and no experiments it reduces to the ID algorithm.
Faithful to E. Bareinboim & J. Pearl, A General Algorithm for Deciding Transportability of Experimental Results (Journal of Causal Inference 2013) and Meta-Transportability of Causal Effects: A Formal Approach (AISTATS 2013), unified via the surrogate-experiment view of S. Lee, J. Correa & E. Bareinboim (UAI 2019). No external code is ported.
Source code in src/causalrl/identification/id_algorithm.py
causalrl.identification.id_algorithm.is_transportable_general(target_graph, treatment, outcome, domains)
¶
Whether P*(outcome | do(treatment)) is transportable from domains (see
:func:identify_transport_general).
Source code in src/causalrl/identification/id_algorithm.py
causalrl.identification.id_algorithm.estimate_transport_general(target_graph, treatment, outcome, domains, *, domain_data, experiment_data=None, do)
¶
Estimate the target effect P*(outcome | do(treatment = do)) across domains.
domain_data maps each domain name (including "target") to an observational dataset over
target_graph.nodes; experiment_data maps (domain, frozenset(target)) to a randomized
experimental dataset. Identifies the estimand via :func:identify_transport_general, evaluates
it against the supplied data, and returns the outcome distribution keyed in sorted(outcome)
order.
Source code in src/causalrl/identification/id_algorithm.py
General Identification (ID Algorithm)¶
causalrl.identification.id_algorithm.identify_effect(graph, treatment, outcome, *, return_certificate=None)
¶
Return a do-free :class:Estimand for P(outcome | do(treatment)), or raise.
Runs the ID algorithm. Raises :class:NotIdentifiableError (with the witnessing hedge attached
as .witness) when the effect is not non-parametrically identifiable, and
:class:CausalGraphError for malformed inputs (unknown nodes, empty outcome, or a treatment and
outcome that overlap).
return_certificate (2.0 default): returns a :class:Certificate; pass False for the
legacy :class:Estimand, or True (equivalently, identify_effect_certified) to be
explicit.
Source code in src/causalrl/identification/id_algorithm.py
causalrl.identification.id_algorithm.is_identifiable_effect(graph, treatment, outcome)
¶
Whether P(outcome | do(treatment)) is identifiable from observational data.
Source code in src/causalrl/identification/id_algorithm.py
causalrl.identification.id_algorithm.estimate_effect(graph, treatment, outcome, data, *, do)
¶
Estimate P(outcome | do(treatment = do)) from observational data via the ID estimand.
Identifies the effect (raising if it cannot), then evaluates the resulting estimand on the
empirical joint of data (discrete integer columns over graph.nodes) at the intervention
do. Returns the outcome distribution as {assignment: probability} with assignments keyed
in sorted(outcome) order.
Source code in src/causalrl/identification/id_algorithm.py
causalrl.identification.id_algorithm.Estimand
¶
A do-free expression for an interventional distribution over the observational law P(V).
Render it with :meth:render; evaluate it on data with :func:estimate_effect.
Source code in src/causalrl/identification/id_algorithm.py
From Surrogate Experiments (gID)¶
causalrl.identification.id_algorithm.identify_effect_with_experiments(graph, treatment, outcome, experiments)
¶
Return an :class:Estimand for P(outcome | do(treatment)) using surrogate experiments.
This is general identification (gID): it runs the ID recursion but, where observational data
hits a hedge, it tries to obtain the needed c-factor from one of the available experiments
(each a set of variables you can intervene on; observational data is always available too).
Raises :class:NotIdentifiableError if no combination of observational data and experiments
identifies the effect.
Faithful to S. Lee, J. Correa, E. Bareinboim, General Identifiability with Arbitrary Surrogate Experiments, UAI 2019, building on Tian & Pearl's c-factor identification. No code is ported.
Source code in src/causalrl/identification/id_algorithm.py
causalrl.identification.id_algorithm.is_gid_identifiable(graph, treatment, outcome, experiments)
¶
Whether P(outcome | do(treatment)) is identifiable from data plus those experiments.
Source code in src/causalrl/identification/id_algorithm.py
causalrl.identification.id_algorithm.estimate_effect_with_experiments(graph, treatment, outcome, data, experiments_data, *, do)
¶
Estimate P(outcome | do(treatment = do)) from observational data plus experiments.
experiments_data maps each available intervention target (a frozenset of variables) to a
dataset drawn from do(target) over graph.nodes. Identifies the effect via gID
(:func:identify_effect_with_experiments), then evaluates the estimand against the empirical
observational joint and each experiment's empirical c-factor. Returns the outcome distribution
keyed in sorted(outcome) order.
Source code in src/causalrl/identification/id_algorithm.py
Causal Discovery¶
causalrl.discovery.discover(data, variables, *, threshold=0.01, max_conditioning_size=3)
¶
Discover the CPDAG over variables from data via the PC algorithm.
threshold is the conditional-mutual-information cutoff below which two variables are judged
independent; max_conditioning_size caps the separating-set search.
Source code in src/causalrl/discovery.py
causalrl.discovery.discover_interventional(observational, interventions, variables, *, threshold=0.01, shift_threshold=0.05, max_conditioning_size=3, min_interventional_samples=20)
¶
Discover the interventional essential graph from observational and experimental data.
Runs the observational PC algorithm (:func:discover), then orients the edges incident to each
intervention target by the invariance principle: under a perfect intervention do(T) a
child of T shifts its marginal, while a parent (a non-descendant) stays invariant. Each
incident edge T - B is oriented T -> B if B shifts and B -> T if not; Meek's
rules R1-R3 then propagate the new orientations, so the result refines the observational CPDAG
toward the true DAG as more targets are experimented on.
interventions maps each intervened target T to a dataset drawn from do(T) (a perfect
intervention covering every variable in variables). shift_threshold is the
total-variation cutoff above which an endpoint's marginal is judged to have shifted.
min_interventional_samples is the smallest per-target sample this will orient from; below it
the call raises rather than guessing. The shift test is an empirical total variation, so a tiny
sample clears any threshold by chance and an empty one clears it unconditionally — orienting
every incident edge from no evidence at all. Lower it only if you accept orientations backed by
that little data.
Source code in src/causalrl/discovery.py
causalrl.discovery.discover_latent(data, variables, *, threshold=0.01, max_conditioning_size=3)
¶
Discover a PAG over variables from data via the FCI algorithm (allows latents).
Unlike :func:discover, FCI does not assume causal sufficiency: it learns the PC skeleton, then
refines it with the Possible-D-SEP step (sound under latent confounders), re-orients unshielded
colliders, and applies the complete orientation rules R1-R10 (Zhang 2008 — sound and complete
for latent confounders and selection bias). The result is a :class:PAG: a <-> b
witnesses a latent confounder; a circle endpoint is undetermined by the equivalence class.
threshold and max_conditioning_size mirror :func:discover.
Source code in src/causalrl/discovery.py
causalrl.discovery.CPDAG
dataclass
¶
A completed partially directed acyclic graph (a Markov equivalence class).
Source code in src/causalrl/discovery.py
to_causal_graph()
¶
Convert to a :class:CausalGraph; raises if any edge is still unoriented.
Source code in src/causalrl/discovery.py
causalrl.discovery.PAG
dataclass
¶
A partial ancestral graph: the complete Markov-equivalence class of MAGs (the FCI output).
marks[(a, b)] is the mark on the b end of edge a—b — a circle o (undetermined by
the equivalence class), an arrowhead >, or a tail -. An edge exists iff both (a, b)
and (b, a) are present. a -> b is tail-at-a / arrow-at-b; a <-> b
(arrowheads at both ends) witnesses a latent confounder; a o-o b is fully unoriented.
Source code in src/causalrl/discovery.py
is_directed(a, b)
¶
is_bidirected(a, b)
¶
Whether a <-> b (arrowheads at both ends — a latent confounder).
edges()
¶
(a, b, mark_at_a, mark_at_b) for each edge, with a < b.
Source code in src/causalrl/discovery.py
causalrl.discovery.conditional_mutual_information(data, x, y, z)
¶
Empirical I(X; Y | Z) in nats (discrete columns; 0 iff X ⊥ Y | Z).
Counted with :func:numpy.unique over integer-encoded cells rather than a Python loop over
rows. This is the inner loop of every discovery routine here -- PC, FCI and the interventional
and invariance variants all bottom out in it, once per (pair, conditioning set) triple -- so its
constant factor sets how large a problem discover can take at all. The counts are sparse
(only cells that occur are materialised), so the memory cost follows the data rather than the
product of the variables' level counts.
Source code in src/causalrl/discovery.py
Causal Imitation Learning¶
causalrl.imitation.is_imitable(graph, *, action, outcome, observable)
¶
Whether the expert is imitable: an observed back-door-admissible set exists.
Source code in src/causalrl/imitation.py
causalrl.imitation.imitation_backdoor_set(graph, *, action, outcome, observable, max_size=3)
¶
Smallest observed back-door-admissible set for action -> outcome, or None.
A set Z ⊆ observable \ {action, outcome} is admissible when it has no descendant of
action and blocks every back-door path (action ⊥ outcome | Z with action's outgoing
edges removed). Cloning P(action | Z) then reproduces the expert's outcome distribution.
Source code in src/causalrl/imitation.py
causalrl.imitation.CausalImitator
¶
Bases: Agent
Clones P(A | Z) for a back-door-admissible observed set Z (the adjustment set).
Conditioning the cloned policy on Z reproduces the confounding the expert responded to, so
deployment matches the expert's reward. Unseen Z values fall back to the marginal P(A).
Source code in src/causalrl/imitation.py
causalrl.imitation.BehavioralCloning
¶
Bases: Agent
Naive imitator: clones the marginal action distribution P(A), ignoring covariates.
Source code in src/causalrl/imitation.py
Causal Curriculum Learning¶
causalrl.curriculum.causal_curriculum(graph, goal=None)
¶
A curriculum (skill order) respecting the causal structure: a topological order in which
every parent (prerequisite) precedes its children. With goal, restrict to the goal and its
ancestors — the skills the goal depends on — still in topological order.
Source code in src/causalrl/curriculum.py
causalrl.curriculum.curriculum_q_learning(tasks, *, episodes_per_task, alpha=0.5, epsilon=0.1, seed=None)
¶
Learn the target policy by Q-learning through a curriculum of subtasks, easiest first.
tasks is ordered from the simplest subtask to the target (the last element); all tasks share
the same state and action spaces. The Q-table is carried forward between stages (warm-start
transfer), so value learned on the easy subtasks bootstraps the harder ones. This is the causal
curriculum applied to RL: order subgoals by prerequisite structure (see
:func:causal_curriculum), then train in that order to reach a target policy that flat learning
on the sparse target alone struggles to find. Returns the greedy policy on the target task.
Faithful to Y. Bengio, J. Louradour, R. Collobert, J. Weston, Curriculum Learning, ICML 2009;
the Q-learning update matches :func:causalrl.shaping.q_learning. No external code is ported.
Source code in src/causalrl/curriculum.py
causalrl.curriculum.is_valid_curriculum(graph, order)
¶
Whether order respects prerequisites: each skill appears after every parent of it that is
also in order.
Source code in src/causalrl/curriculum.py
causalrl.curriculum.PrerequisiteLearner
¶
Causally-gated skill acquisition: walking the curriculum left to right, a skill is mastered iff all of its parents (prerequisites) are already mastered. Deterministic and order-faithful, so the effect of the ordering is exactly readable from the mastered set.
Source code in src/causalrl/curriculum.py
train(curriculum)
¶
Process the curriculum once and return the set of mastered skills.
Source code in src/causalrl/curriculum.py
Causal Reward Shaping¶
causalrl.shaping.apply_potential_shaping(mdp, potential)
¶
Return a new MDP with rewards[s, a] += gamma * Phi(s') - Phi(s) (policy-invariant).
Source code in src/causalrl/shaping.py
causalrl.shaping.causal_potential(mdp)
¶
The ideal (causal) shaping potential: V* with terminal states pinned to 0 (the
condition under which potential shaping is policy-invariant for episodic tasks).
Source code in src/causalrl/shaping.py
causalrl.shaping.value_iteration(mdp, *, tol=1e-09, max_iter=1000)
¶
Return (V*, greedy optimal policy) for the deterministic MDP.
Source code in src/causalrl/shaping.py
causalrl.shaping.q_learning(mdp, *, episodes, potential=None, alpha=0.5, epsilon=0.1, max_steps=None, seed=None)
¶
Tabular epsilon-greedy Q-learning from state 0. With potential given, the reward is
shaped online by gamma * Phi(s') - Phi(s). Returns the greedy policy.
Source code in src/causalrl/shaping.py
causalrl.shaping.TabularMDP
dataclass
¶
A finite deterministic MDP: transitions/rewards are keyed by (state, action).
Source code in src/causalrl/shaping.py
Causal Game Theory¶
causalrl.games.CausalGame
dataclass
¶
A finite game as a causal influence diagram.
utilities[agent] maps each joint action profile (a tuple in agents order) to agent's
payoff; graph is the (M)ACID with a decision and utility node per agent.
Source code in src/causalrl/games.py
causalrl.games.pure_nash_equilibria(game)
¶
All pure-strategy Nash equilibria, by enumeration over the finite joint action space.
Source code in src/causalrl/games.py
causalrl.games.mixed_nash_equilibria(game)
¶
All mixed-strategy Nash equilibria (pure and properly mixed), by support enumeration.
Two-player games are solved exactly over rational arithmetic (:class:fractions.Fraction),
so symmetric games yield exact mixes (matching pennies gives 0.5/0.5). Games with three
or more agents are solved by support enumeration with a numerical (Newton) solve of the
multilinear indifference system; every returned profile is then verified to be an ε-Nash
equilibrium (no agent can gain more than 1e-6 by deviating to a pure action). Each
equilibrium maps an agent to {action: probability} with off-support actions at zero.
Assumes a non-degenerate game; degenerate games may admit a continuum of equilibria, of which
only support-extreme points are enumerated, and the numerical solver may miss a support whose
system is ill-conditioned. Raises :class:CausalGraphError for fewer than two agents (use
:func:pure_nash_equilibria for pure equilibria of any game).
Faithful to the support-enumeration method (R. Porter, E. Nudelman, Y. Shoham, Simple Search Methods for Finding a Nash Equilibrium, Games and Economic Behavior 2008; B. von Stengel, Computing Equilibria for Two-Person Games, Handbook of Game Theory 2002). No code is ported.
Source code in src/causalrl/games.py
causalrl.games.best_response(game, agent, profile)
¶
The actions maximizing agent's payoff given the other agents' actions in profile.
Source code in src/causalrl/games.py
causalrl.games.is_nash_equilibrium(game, profile)
¶
Whether every agent's action in profile is a best response to the others.
Exceptions¶
causalrl.exceptions.CausalRLError
¶
causalrl.exceptions.CausalInterfaceUnavailableError
¶
Bases: CausalRLError
The causal interface is not available on this wrapper.
Raised when a method that requires a live SCM and a named reward node is called
on a :class:~causalrl.envs.wrapper.CausalEnvWrapper that was constructed without
them (e.g. wrapping a :class:~causalrl.envs.base.ConfoundedMDP that carries
scm=None, or without passing a reward_node).
Source code in src/causalrl/exceptions.py
causalrl.exceptions.NotIdentifiableError
¶
Bases: CausalRLError
A causal query is not identifiable from the available data.
Source code in src/causalrl/exceptions.py
causalrl.exceptions.CausalGraphError
¶
Bases: CausalRLError
Invalid graph operation (unknown node, cycle, malformed edge).
causalrl.exceptions.RealizabilityError
¶
Bases: CausalRLError
A counterfactual query cannot be realized from the given evidence.
causalrl.exceptions.UnverifiedAssumptionError
¶
Bases: CausalRLError
A method's claimed guarantee requires an assumption the caller has not declared.
Partial-Identification And OPE Bounds¶
causalrl.identification.bounds.manski_bounds(data, *, treatment, outcome, action, outcome_range=(0.0, 1.0))
¶
Sharp no-assumptions bounds on E[outcome | do(treatment = action)] (Manski 1990).
From observational data (integer treatment column, numeric outcome in
outcome_range): the units that took action contribute their observed mean, while
the rest are bounded only by the outcome range. With p = P(treatment = action) and
m = E[outcome | treatment = action] the bounds are
[m*p + y_min*(1-p), m*p + y_max*(1-p)] — sharp, collapsing to a point when every unit took
action. The observational counterpart of :func:~causalrl.ope.bounds.causal_q_bounds.
Source code in src/causalrl/identification/bounds.py
causalrl.ope.bounds.ipw_sensitivity_bounds(outcomes, propensities, *, gamma, return_certificate=None)
¶
Marginal-sensitivity-model bounds on the treated counterfactual mean E[Y(1)].
outcomes and propensities are the treated units' outcomes Y_i and nominal
propensities e(Z_i) = P(treated | Z_i) (what an unconfounded model fits). Under Tan's
marginal sensitivity model the true inverse-propensity weight lies within an odds-ratio factor
gamma >= 1 of the nominal, giving a_i in [1 + (1/g)(1/e_i - 1), 1 + g(1/e_i - 1)]; the
bounds are the extreme stabilized (Hájek) weighted means over that range. At gamma = 1 the
interval collapses to the IPW point estimate; it widens monotonically with gamma and
contains E[Y(1)] whenever the true confounding odds ratio is at most gamma.
Faithful to Z. Tan, A Distributional Approach for Causal Inference Using Propensity Scores (JASA 2006) and Q. Zhao, D. Small, B. Bhattacharya, Sensitivity Analysis for Inverse Probability Weighting Estimators via the Percentile Bootstrap (JRSS-B 2019). No code is ported.
return_certificate (2.0 default): returns a BOUNDED :class:Certificate; pass False
for the legacy :class:Interval, or True (equivalently, the *_certified variant) to be
explicit.
Source code in src/causalrl/ope/bounds.py
causalrl.ope.bounds.causal_q_bounds(dataset, state, action, *, require_identified=False)
¶
Manski natural bounds on E[return | do(action), state] from confounded logs.
For a return in [0, 1] with empirical mean m = E[R|s,a] and propensity p = P(a|s):
lower = m * p, upper = m * p + (1 - p).
A never-logged action (p = 0) yields the vacuous [0, 1] — not identifiable from the
logs alone. With require_identified=True, a vacuous bound raises NotIdentifiableError
carrying (state, action) as the witness.
Source code in src/causalrl/ope/bounds.py
causalrl.ope.bounds.msm_policy_value_bounds(outcomes, logging_propensities, target_propensities, *, gamma, return_certificate=None)
¶
Marginal-sensitivity-model bounds on an off-policy value V(pi_t) = E[(pi_t/e0) Y].
Self-normalised (Hájek) off-policy value of a target policy pi_t estimated from logs of a
logging policy with nominal propensities e0(a|x) = P(a | x) (a valid probability in
(0, 1]). outcomes are the logged rewards Y_i; target_propensities are
pi_t(a_i | x_i) at the logged action. Under Tan's marginal sensitivity model the true
logging propensity deviates from nominal by an odds-ratio at most gamma >= 1, so the true
inverse weight 1/ẽ0 lies in [1 + odds/gamma, 1 + odds*gamma] with odds = (1-e0)/e0;
each unit's contribution weight is pi_t(a_i|x_i) * (1/ẽ0). The bounds are the extreme
stabilised weighted means of Y over those per-unit weight ranges.
Reduces to :func:ipw_sensitivity_bounds when pi_t is constant across the logged actions
(the treated / uniform-target mean — the constant cancels in the self-normalised ratio), and
collapses to the self-normalised IPS point at gamma = 1. The off-policy generalisation of
Tan's MSM in the spirit of N. Kallus & A. Zhou, Confounding-Robust Policy Evaluation in
Infinite-Horizon Reinforcement Learning (NeurIPS 2020). The caller supplies pi_t and the
nominal e0; no code is ported.
return_certificate (2.0 default): returns a BOUNDED :class:Certificate; pass False
for the legacy :class:Interval, or True (equivalently, the *_certified variant) to be
explicit.
Source code in src/causalrl/ope/bounds.py
causalrl.ope.bounds.msm_contribution_bounds(outcomes, logging_propensities, target_propensities_on, target_propensities_off, *, gamma)
¶
Marginal-sensitivity-model bounds on a contribution V(pi_on) - V(pi_off).
The off-policy value DIFFERENCE between two target rules, estimated from confounded logs
under Tan's marginal sensitivity model — e.g. a per-agent credit or per-factor contribution
E[Y_{do(F=1)}] - E[Y_{do(F=0)}]. Each arm is bounded by :func:msm_policy_value_bounds
(target_propensities_on = pi_on(a_i | x_i) at the logged action, ..._off likewise,
shared nominal e0) and the contribution interval is the difference
[ on.lower - off.upper , on.upper - off.lower ].
Always valid (it contains the true difference for any targets); sharp when the two
target supports are disjoint — e.g. the deterministic one-hot arms 1{F=1} / 1{F=0}
that partition the logged units, so the two arms' weight perturbations are independent — and
conservative otherwise. Collapses to the difference of the two self-normalised IPS points at
gamma = 1 and widens monotonically with gamma.
Source code in src/causalrl/ope/bounds.py
causalrl.ope.bounds.msm_per_step_bounds(rewards_by_step, propensities_by_step, *, gamma)
¶
Per-step marginal-sensitivity-model bounds on a cumulative (summed) reward.
Each element of rewards_by_step / propensities_by_step is one time step's
per-unit rewards r_t and nominal propensities e_t; the cumulative-reward MSM
bound is the sum over steps of the per-step :func:ipw_sensitivity_bounds. This is the
additive (per-step) cumulative-reward MSM: each step is bounded independently under the
sensitivity model and the bounds add, which is tight for sparse / per-step rewards.
Reusable kernel of the per-step cumulative-reward MSM used for confounded multi-step OPE (Bennett & Kallus, Efficient and Sharp OPE in Robust MDPs, NeurIPS 2024). The experiment supplies the per-step nominal propensities; no code is ported.
Source code in src/causalrl/ope/bounds.py
causalrl.ope.bounds.msm_stratified_bounds(values, propensities, strata, target_weights, *, gamma)
¶
Stratified marginal-sensitivity-model bounds: Σ_s w_s · MSM_s.
Compute the MSM bound within each stratum (units sharing a strata label) and combine
them with target_weights (the target stratum marginal w_s, e.g. a uniform initial
state distribution). Strata absent from the data contribute nothing. Because conditioning
removes between-stratum weight variation, the stratified bound is never wider than the
pooled :func:ipw_sensitivity_bounds and never under-covers (THEORY.md, Prop 1).
The reusable kernel of the stratified cumulative-reward MSM; the experiment supplies the stratum labels (e.g. initial state) and nominal propensities.
Source code in src/causalrl/ope/bounds.py
causalrl.conformal.core.conformal_action_value(dataset, target_actions=None, *, alpha=0.1)
¶
Calibrated band for the return of one decision taken under a target policy.
The off-policy caller of the weighted path. At logged transition i the likelihood ratio
dP_target/dP_behavior is pi_target(a_i | s_i) / pi_behavior(a_i | s_i), computed here
from the log's :meth:~causalrl.data.logged.LoggedDecisions.logging_propensities and
target_actions[i] — the action the policy takes at that transition's state, one per
logged transition, exactly the argument :func:causalrl.certify_policy takes. Passing
target_actions=None scores the logging policy itself, whose logged returns need no
reweighting; that is the reference the lower-bound gate compares against.
Returns a kind=EMPIRICAL certificate whose ci is the band and whose value is
None. The claim is coverage of a single fresh return, not a confidence interval for
V(pi) = E[return]: each end is a one-sided conformal bound at alpha / 2, so
ci.lower is a distribution-free lower confidence bound on the next return under the target
policy at level 1 - alpha/2, and the two-sided band covers at 1 - alpha. Too few
effectively-weighted samples for the level returns an infinite end rather than a false one.
Valid under (i) weighted exchangeability of the logged returns; (ii) no unmeasured confounding
of the logged action, without which the ratio above is not dP_target/dP_behavior; and
(iii) positivity — every action the policy reaches must have been played by the logs in that
state, else the band is vacuous and carries a positivity :class:~causalrl.certify.Hedge; a
log that cannot check this carries the hedge too, rather than passing quietly. The
state distribution is taken to be unchanged by the policy: the one-step / terminal-return
regime :func:causalrl.certify_policy documents.
Source code in src/causalrl/conformal/core.py
228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 | |
Decision Certificates¶
The decision stack — certify whether a confounded / off-policy decision ("is action 1 better than
action 0?") is robust to hidden confounding, cheapest layer first. certify_decision is the
one-call front door over the layers below.
causalrl.identification.decision.certify_decision(rewards=None, actions=None, *, confounder_bins=None, mi_cap=None, propensities=None, gamma_max=10.0, estimate=None)
¶
Certify whether a binary decision from confounded logs is robust to hidden confounding.
rewards are the logged rewards Y_i; actions is the binary arm indicator (1 = the
arm under test, 0 = baseline). The naive decision is the sign of the logged contrast
E[Y | F=1] - E[Y | F=0]. Supply at least one evidence source — they compose:
confounder_bins(a measured hidden variableZ) ormi_cap(a structural cap on the information channelMI(I; Z)) runs the sign-robustness layer (:func:pivotality_certificate): certifies iff no hidden confounder consistent with that information can flip the contrast's sign. One-sided — failure to certify is not evidence of a flip.propensities(the nominal logging propensitiese0(a_i | x_i)at the logged action) runs the MSM tipping layer: the smallest Tan odds-ratioGammaat which the off-policy (IPS) value contrast band first includes zero (:func:tipping_gammaover the sharp one-hot :func:msm_contribution_bounds).tipping_gamma is Nonemeans the decision is robust to confounding at least as strong asgamma_max.
With informative propensities the MSM layer concerns the inverse-propensity-weighted off-policy contrast, which coincides with the raw logged contrast only under uniform logging.
Alternatively pass a pre-built estimate (a :class:PolicyValueContrast, e.g. from an
interop adapter) instead of raw rewards / actions; the two forms are mutually
exclusive and certify_decision(estimate=c) is exactly certify_estimate(c).
Source code in src/causalrl/identification/decision.py
causalrl.identification.decision.DecisionCertificate
¶
Bases: NamedTuple
Result of :func:certify_decision.
certified is the headline: is the naive (logged) decision robust to hidden confounding,
by the strongest layer that ran? When the structural/measured layer ran it carries its
one-sided guarantee (no confounder consistent with the supplied information can flip the
sign); otherwise it reports whether the MSM layer found the decision robust up to gamma_max.
When the finite-sample gate also ran (:func:causalrl.certify_policy with alpha),
certified requires it too and conformal_lcb reports the bound it gated on.
The component fields and summary make the exact guarantee explicit.
Source code in src/causalrl/identification/decision.py
recommendation
property
¶
Verdict-based action, resolving the naive-vs-adjusted footgun.
"act" iff the naive :attr:decision is certified robust to hidden confounding by the
strongest layer that ran; otherwise "abstain". Read THIS, not :attr:decision, to
decide whether to act — :attr:decision is the (possibly confounded) hypothesis under
test, not the recommendation.
causalrl.identification.bounds.pivotality_certificate(outcomes, treated, confounder_bins=None, *, mi_cap=None)
¶
One-sided sign-robustness certificate for a naive contrast under hidden confounding.
certified=True means: no hidden variable consistent with the supplied information can
flip the sign of E[Y|F=1] - E[Y|F=0]. Two modes:
confounder_binsgiven (a measured hidden variable, e.g. post-hoc showdown/oracle data): certify iff the TV-form :func:confounding_bias_boundis strictly below|naive|;mi_measuredreports the plug-in channel.mi_capgiven (a structural cap onMI(I;Z)from the environment's information rules — the data-processing route): certify iffmi_cap < mi_flipcomputed with the outcome-span relaxation (noZneeded anywhere).
The cheapest layer of the decision stack — certificate, then MSM band
(:func:~causalrl.ope.bounds.msm_contribution_bounds), then abstention
(:func:tipping_gamma). One-sided:
failure to certify is NOT evidence of a flip. Verified against measured ground truth in
three game-log regimes in experiments/games/theory/verify_pivotality.py.
Source code in src/causalrl/identification/bounds.py
causalrl.identification.bounds.confounding_bias_bound(outcomes, treated, confounder_bins, *, form='tv')
¶
Upper bound on the omitted-variable bias |naive - Z-adjusted| from logged rows.
Omitted-variable bias in total-variation form:
``|bias| <= M1 * TV(P_{Z|F=1}, P_Z) + M0 * TV(P_{Z|F=0}, P_Z)``
with M_f the span over Z-bins of E[Y | F=f, Z] (each signed measure integrates
to zero, so the midrange trick gives the span, not the sup; sharp — attained with equality
by an explicit two-point family at every parameter value). form="mi" applies Pinsker
with the KL budget p*KL1 + (1-p)*KL0 = MI(F;Z) split optimally across arms (sharp
small-MI constant):
``|bias| <= sqrt( MI/2 * (M1^2/p + M0^2/(1-p)) )`` (capped at the trivial ``M1+M0``),
the (looser-than-TV) form whose budget the environment's information structure can cap.
Every stratum must contain both arms (positivity); restrict to an overlap population first —
a logger that conditions hard on Z destroys overlap, which is a finding, not a nuisance.
Source code in src/causalrl/identification/bounds.py
causalrl.identification.bounds.mi_flip_threshold(naive, span_treated, span_control, p_treated)
¶
Channel capacity (nats) below which NO hidden confounder can flip the naive sign.
The decision-pivotality threshold (sharp form)
``MI_flip = 2 naive^2 / (M1^2/p + M0^2/(1-p))``:
if the mutual information between the logged binary action and a hidden variable Z is
below this value, the omitted-Z bias is strictly smaller than |naive| (omitted-
variable bias in TV form + Pinsker with the KL budget split optimally across arms), so the
Z-adjusted contrast has the same sign as the naive one. This constant is SHARP: a
binary-channel family attains the corresponding bias bound with ratio -> 1 in the small-MI
limit (THEORY_pivotality.md, tightness section); the additive form
2(|naive|/(M1/sqrt(p)+M0/sqrt(1-p)))^2 is its (looser) Cauchy-Schwarz relaxation.
Combined with the data-processing inequality MI(F;Z) <= MI(I;Z) — the logging actor's
policy reads only its information set I — the information structure of the
environment caps the reachable confounding budget.
span_treated / span_control are the spans of E[Y | F=f, Z=z] over z (the
regression spans M1 / M0); with Z unmeasured, use the outcome span for both
(valid, looser).
Source code in src/causalrl/identification/bounds.py
causalrl.identification.bounds.tipping_gamma(bound, *, reference=0.0, gamma_max=10.0, tol=0.001)
¶
Sensitivity tipping point: the smallest gamma >= 1 at which the partial-ID interval
bound(gamma) first contains reference.
This is the causal-sensitivity reporting companion to the MSM bound kernels: it answers
"how strong would unmeasured confounding have to be (on the MSM/Rosenbaum odds-ratio scale)
to overturn the conclusion that the estimand lies strictly on one side of reference?".
A larger tipping gamma ⇒ a more robust conclusion — the odds-ratio-scale analog of the
E-value (VanderWeele & Ding, Ann. Intern. Med. 2017) and of Rosenbaum's Gamma.
bound maps a sensitivity level gamma to an :class:Interval; it must collapse to a
point at gamma = 1 and widen monotonically with gamma (as every MSM kernel here does —
e.g. lambda g: msm_contribution_bounds(y, e0, on, off, gamma=g)). Returns 1.0 if the
point already sits on reference, and None if the interval never reaches reference
by gamma_max (the conclusion is robust to confounding at least that strong).
Source code in src/causalrl/identification/bounds.py
Unified Certificates (§5.2)¶
causalrl.certify.certificate.Certificate
dataclass
¶
A serializable certified claim (plan §5.2).
Source code in src/causalrl/certify/certificate.py
138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 | |
causalrl.certify.adapters.as_certificate(obj)
¶
Convert a shipped certificate to a unified :class:Certificate (additive view).
Accepts DecisionCertificate, PivotalityCertificate, TransportRegretCertificate, or a
Certificate (returned unchanged). Raises :class:TypeError for anything else.
Source code in src/causalrl/certify/adapters.py
Identification-Aware Estimation (Phase 1)¶
causalrl.estimate.compiler.certify_effect(graph, treatment, outcome, data, *, method='dml', alpha=0.05, n_folds=5, seed=0, outcome_model=None, propensity_model=None, overlap_eps=0.01, clip=0.001)
¶
Certify the back-door ATE E[Y|do(X=1)] - E[Y|do(X=0)] of a binary treatment.
Returns a kind=IDENTIFIED :class:Certificate with the point estimate (value), its
confidence interval (ci), the adjustment-set witness, and provenance — or a hedged
certificate (value=None, hedge set) when the effect is not identifiable, needs an
unsupported ID method, or when estimated positivity falls below overlap_eps (I3). data
maps variable names to arrays (see :func:~causalrl.estimate.estimators.estimate_ate).
Source code in src/causalrl/estimate/compiler.py
112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 | |
causalrl.bounds.continuous.msm_sensitivity_bounds(outcomes, *, gamma, propensities=None, treatment=None, covariates=None, propensity_model=None)
¶
MSM bounds on E[Y(1)] with estimated logging propensities (Dorn-Guo/Kallus line).
Two modes (note outcomes differs between them):
propensities=given -> treated units' outcomes + nominal propensities; delegates toipw_sensitivity_boundsunchanged (the known-propensity case it reduces to exactly).treatment=+covariates=given ->outcomes/treatment/covariatesare the full per-logged-unit arrays; fitse_hat = P(T=1 | covariates)withpropensity_model(default logistic) and applies the same sharp box kernel to the treated subset.
Source code in src/causalrl/bounds/continuous.py
causalrl.transport.estimate.certify_transported_effect(diagram, source_data, target_data, *, treatment, outcome, treated_value=1.0, max_adjustment_size=3)
¶
Certify the transported mean E*[outcome | do(treatment=treated_value)] from data.
Uses the shipped :func:~causalrl.identification.transport.transport_formula to decide
transportability, then estimates the closed form from source_data / target_data (numpy,
discrete adjustment covariates). Returns a kind=IDENTIFIED certificate with the point value
and the transport witness, or a hedged certificate when the effect is not transportable (I3).
Source code in src/causalrl/transport/estimate.py
Multi-Agent Core (Phase 2)¶
causalrl.magames.population.Population
dataclass
¶
N named agents instantiated from :class:AgentType templates with explicit sharing.
Source code in src/causalrl/magames/population.py
max_kind
property
¶
The strongest Kind a certificate about this population may claim (per topology).
to_game()
¶
Materialise the normal-form :class:~causalrl.games.CausalGame (small N).
Per-agent graphs stay factored in the templates; this fully materialises the joint payoff tables and a MAID with an edge from every decision to every utility (a general normal-form game: each payoff depends on all actions).
Source code in src/causalrl/magames/population.py
causalrl.magames.empirical.EmpiricalGame
dataclass
¶
A finite game estimated by replication: per-profile payoff means, standard errors and n.
All three mappings are keyed by a profile of strategy labels in agent order. Build one with
:meth:from_samples rather than by hand — it is the constructor that computes the error.
Source code in src/causalrl/magames/empirical.py
56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 | |
from_samples(strategies, samples, *, agents=None)
classmethod
¶
Average a complete round-robin into a game, keeping each cell's standard error.
samples maps a profile of strategy labels to the replications observed for it, each
replication being one payoff per agent. Every profile in the product of strategies must
be present with at least two replications: a cell with one sample has no error bar, and an
object whose contract is to carry the error bar must not invent one.
Source code in src/causalrl/magames/empirical.py
payoff_error(*, alpha=0.05, functional_terms=1)
¶
The cellwise error bound this table licenses, at level alpha.
A normal interval on each cell's mean, Bonferroni-corrected over every (profile, agent)
cell so the bound holds simultaneously — which is what the certificate needs, since its
linear programs touch all of them at once. The reported utility is the widest such
half-width, because the relaxation it feeds is a single scalar over all constraints.
functional_terms is how many estimated payoffs the functional being certified sums: 1
for one agent's payoff (the usual case), len(agents) for welfare, 0 for a functional
that does not read the payoffs at all.
Source code in src/causalrl/magames/empirical.py
to_game()
¶
Materialise the normal form over integer actions, using the estimated means as payoffs.
Strategy i is action i. The returned game says nothing about its own error — pass
:meth:payoff_error alongside it to :func:~causalrl.magames.cce.certify_cce_do.
Source code in src/causalrl/magames/empirical.py
causalrl.magames.cce.PayoffError
dataclass
¶
How far a measured payoff table may be from the game it estimates.
utility bounds |u_hat - u_true| for every agent and profile; functional bounds the
same for the values of the functional being certified (0 when the functional does not read the
estimated payoffs — an indicator of a profile, say — and k times utility when it sums
k of them). alpha records the level at which the two bounds hold when they came from
standard errors rather than from a hard envelope; it is reported, not used in the arithmetic.
Passing one to :func:certify_cce_do is what stops an estimated game being certified as if it
were exact: see that function for where each of the two numbers enters.
Source code in src/causalrl/magames/cce.py
exact
property
¶
Whether this error is no error at all — the table is exact and licenses everything.
causalrl.magames.learning.run_no_regret(population, rounds, *, do=None, algorithm='regret_matching', learning_rate=None, explore=0.0, seed=0, trace_points=8)
¶
Play rounds rounds of no-regret learning and return the realized empirical joint.
Every agent not pinned by do gets its own learner and, each round, the counterfactual payoff
vector of what its actions would have paid against the profile the rest of the population
actually played (full-information feedback, available because the game's payoffs are known).
Agents pinned by do play their forced action and do not learn — the same restriction
:func:~causalrl.magames.cce.cce_polytope applies, so the returned run is feasible for the
intervened polytope by construction.
algorithm selects :class:~causalrl.agents.no_regret.RegretMatching (parameter-free) or
:class:~causalrl.agents.no_regret.MultiplicativeWeights (Hedge; learning_rate defaults to
the theory rate for this horizon and the agent's own payoff range). explore mixes in uniform
play, trace_points sets how many log-spaced horizons the regret trace records.
The canonical use is the finite-time certificate, which assumes nothing about the limit::
run = run_no_regret(population, 20_000, do={"A2": 0})
cert = certify_cce_do(game, functional, do={"A2": 0}, no_regret=False, epsilon=run.regret)
Source code in src/causalrl/magames/learning.py
149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 | |
causalrl.magames.learning.NoRegretRun
dataclass
¶
Realized play of a no-regret population: the empirical joint plus its regret trace.
weights is aligned with profiles, which is the profile order of
:func:~causalrl.magames.cce.cce_polytope for the same do — so both weights and
:attr:empirical_joint drop straight into :func:~causalrl.magames.cce.cce_regret and
:func:~causalrl.magames.cce.certify_cce_do. regret_trace pairs a horizon with the measured
regret of the time average up to it; it is the evidence that the population is learning.
Source code in src/causalrl/magames/learning.py
empirical_joint
property
¶
The realized joint distribution as {profile: probability} over played profiles.
regret
property
¶
The measured realized regret at the full horizon — pass it as epsilon.
marginal(agent)
¶
What one agent played: the realized joint summed over everyone else.
Keyed by the agent's own actions, including any it never played (weight 0.0), so the values
always sum to 1. An agent pinned by do did not learn, so its marginal is the
intervention itself.
Source code in src/causalrl/magames/learning.py
boundary_mass(agent)
¶
Realized mass on the extreme actions of agent's action set.
Meaningful only when the actions are an ordered grid — a discretisation of a quantity, not a set of labelled strategies — in which case this is the truncation diagnostic: mass pressed against the smallest or largest action means the play may be capped by where the grid was stopped rather than settled where the game puts it, and the honest response is to widen the grid and re-run. An agent with a single available action scores 1.0.
Source code in src/causalrl/magames/learning.py
causalrl.agents.no_regret.NoRegretLearner
¶
Bases: Agent
Base class for a learner over n_actions whose external regret grows sublinearly.
explore mixes the algorithm's distribution with the uniform one ((1 - explore) * p +
explore / K). Leave it at 0 under full-information feedback (:meth:observe); set it
positive under bandit feedback (:meth:update), where it bounds the inverse-propensity weights.
:meth:act samples the current mixed strategy. A repeated finite game has no state to condition
on, so observation is ignored here; it is present because it is part of the
:class:~causalrl.agents.base.Agent interface, and subclasses that do condition on it should
say so.
Source code in src/causalrl/agents/no_regret.py
distribution()
¶
The mixed strategy actually played: the algorithm's, mixed with uniform explore.
Source code in src/causalrl/agents/no_regret.py
observe(payoffs)
¶
Learn from the counterfactual payoff vector: payoffs[k] is what action k paid.
Source code in src/causalrl/agents/no_regret.py
act(observation=None)
¶
Sample an action from :meth:distribution (observation is not conditioned on).
update(observation, action, reward)
¶
Learn from bandit feedback: one realized reward for the action actually played.
The single observation is expanded into an unbiased estimate of the full payoff vector by
inverse-propensity weighting (reward / p[action] on the played coordinate, 0
elsewhere) before the ordinary full-information update. With explore > 0 this is EXP3
for :class:MultiplicativeWeights; observation is not conditioned on.
Source code in src/causalrl/agents/no_regret.py
causalrl.magames.equilibrium.certify_equilibrium(game, profile, *, do=None, topology=LearnerTopology.SINGLE_LEARNER, tol=1e-09, require_kind=None)
¶
Certify profile is a robust (tol-)Nash equilibrium of game, optionally under do.
do fixes the given agents' actions; the remaining free agents must each best-respond
within tol for the profile to be certified. kind is capped by topology (I2). If
require_kind is stronger than the topology licenses (e.g. IDENTIFIED under
INDEPENDENT_LEARNERS), raise :class:KindNotLicensedError — the requested guarantee is not
available, so no certificate is returned (acceptance d).
Source code in src/causalrl/magames/equilibrium.py
causalrl.magames.views.linear_gaussian_population_env(ego='ego', coplayer='co', **kwargs)
¶
Construct a :class:LinearGaussianPopulationEnv (see it for the DGP coefficients).
Source code in src/causalrl/magames/views.py
causalrl.magames.views.LinearGaussianPopulationEnv
dataclass
¶
The environment one ego faces inside a fixed population: a synthetic DGP, not a learner.
An observed context Z confounds the ego's logging action and drives a co-player's action;
the reward is linear in the ego action, the co-player action and Z, with Gaussian noise. The
ego's causal action effect E[Y | do(ego=1)] - E[Y | do(ego=0)] = ego_effect (the co-player
and context are unaffected by the ego's action) is back-door identified by adjusting for Z.
Nothing here updates from experience: the fields below are fixed structural coefficients.
Source code in src/causalrl/magames/views.py
causalrl.agents.no_regret.RegretMatching
¶
Bases: NoRegretLearner
Hart & Mas-Colell (2000) regret matching on external regrets: parameter-free, no-regret.
The cumulative regret of action k is the total extra payoff it would have earned had it
replaced the strategy actually played; the next round is drawn proportionally to the positive
part of that vector (uniformly while no regret is positive).
Source code in src/causalrl/agents/no_regret.py
causalrl.agents.no_regret.MultiplicativeWeights
¶
Bases: NoRegretLearner
Hedge / multiplicative weights (Freund & Schapire 1997): p[k] ~ exp(eta * cumulative[k]).
learning_rate is eta. Left at None it is the theory rate
sqrt(8 log K / T) / payoff_range: with horizon=T given, the fixed rate tuned to that
horizon; without one, the anytime rate that uses the number of rounds seen so far instead
(both from N. Cesa-Bianchi, G. Lugosi, Prediction, Learning, and Games, CUP 2006, §2.3).
payoff_range is the width of the interval the payoffs live in, which is what the rate must
be scaled by; pass the game's own range when it is not 1.
Source code in src/causalrl/agents/no_regret.py
Scale & Streaming (Phase 3)¶
causalrl.ope.ipw.stream_policy_value(source, *, weight='weight', reward='reward', batch_size=100000, alpha=0.05, min_ess_fraction=0.1, policy=None)
¶
Certify a policy's self-normalised importance-sampling value, streamed over a columnar log.
Each decision contributes an importance weight (the weight value cell,
rho = π_target/π_behaviour) and a reward cell; the estimate is the Hájek ratio
V = Σ(rho·r) / Σ rho with an influence-function CI. Returns a kind=IDENTIFIED
:class:Certificate (value + ci) under logged-propensity positivity, else a hedge
when the Kish effective sample size falls below min_ess_fraction of the decision count — the
streaming positivity guard (I3). source is a :class:TrajectoryLog or a Parquet path,
streamed in row batches of batch_size without materialising it.
Source code in src/causalrl/ope/ipw.py
causalrl.bounds.streaming.stream_msm_bounds(source, *, gamma, outcome='reward', propensity='propensity', treatment=None, batch_size=100000)
¶
Certify MSM bounds on E[Y(1)] from a streamed log under confounding budget gamma.
Each decision supplies an outcome cell and a nominal propensity cell; when treatment
is given, only decisions with that indicator cell > 0.5 (the treated units) enter the bound.
Returns a kind=BOUNDED :class:Certificate whose value is the Tan interval: it holds
E[Y(1)] whenever the true confounding odds ratio is at most gamma, collapses to the IPW
point at gamma = 1, and widens monotonically. source streams in row batches of
batch_size without materialising the log (only the two needed columns accumulate).
Source code in src/causalrl/bounds/streaming.py
causalrl.backends.streaming.StreamingMoments
¶
Running count / mean / (sample) variance over streamed batches.
Combines batches with Chan et al.'s parallel merge — carrying count, mean and M2
(the running sum of squared deviations) — which is numerically stable and order-independent, so
update-ing batches or merge-ing partial accumulators yields the same moments (up to
floating point) as a single pass over the concatenated data.
Source code in src/causalrl/backends/streaming.py
variance
property
¶
Sample variance (ddof=1); nan for fewer than two observations.
se
property
¶
Standard error of the mean; nan for fewer than two observations.
update(values)
¶
Absorb a batch of observations; returns self for chaining.
Source code in src/causalrl/backends/streaming.py
merge(other)
¶
Fold another accumulator into this one (associative); returns self.
ci(z)
¶
Wald confidence interval mean ± z·se (the caller supplies the z multiplier).
causalrl.backends.streaming.WeightedStreamingRatio
¶
Streaming self-normalised (Hájek) weighted mean Σ w y / Σ w with a one-pass SE.
Accumulates the six sufficient statistics n, Σw, Σwy, Σw², Σw²y, Σw²y², from which the
self-normalised estimate V = Σwy/Σw and its standard error follow in a single pass: the
Hájek influence contribution is ψ_i = w_i (y_i - V) / w̄ (mean zero by construction), so
SE = √(Σ w_i² (y_i - V)²) / Σ w — expandable from the stored moments without a second pass.
With importance weights w_i = π_target(a_i|s_i)/π_behavior(a_i|s_i) and y_i the reward,
V is the self-normalised IS off-policy value; :meth:effective_sample_size is the standard
(Σw)²/Σw² overlap diagnostic used to hedge when positivity is too weak (invariant I3).
Source code in src/causalrl/backends/streaming.py
101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 | |
effective_sample_size
property
¶
Kish effective sample size (Σw)² / Σw² (0 when no weight has accumulated).
se
property
¶
Influence-function standard error of :attr:value; nan below two observations.
update(weights, values)
¶
Absorb a batch of (weight, value) pairs; returns self for chaining.
Source code in src/causalrl/backends/streaming.py
merge(other)
¶
Fold another accumulator into this one (associative); returns self.
Source code in src/causalrl/backends/streaming.py
ci(z)
¶
Wald confidence interval value ± z·se (the caller supplies the z multiplier).
Interop & Scale (Phase 4)¶
causalrl.interop.sbi_numpyro.regimes_from_posterior(samples, *, name_prefix='posterior', selection=(), max_regimes=None, seed=None)
¶
Turn posterior samples over environment parameters into one :class:Regime per sample.
samples is either a mapping param_name -> samples or a (matrix, names) pair with
matrix of shape (n_samples, n_params). Each regime carries that sample's values
and marks selection as the mechanism variables that differ across the population/config.
max_regimes subsamples without replacement (deterministic given seed) to keep a large
posterior tractable.
Source code in src/causalrl/interop/sbi_numpyro.py
causalrl.interop.sbi_numpyro.across_regimes(regimes, fn)
¶
Quantify a functional across calibrated regimes as its [min, max] :class:Interval.
fn maps each regime (its parameter configuration) to a scalar — e.g. a transported effect or
a partial-identification bound evaluated under that configuration. The returned interval is the
range over the posterior ensemble: a worst-case envelope across calibrated configurations.
Source code in src/causalrl/interop/sbi_numpyro.py
causalrl.interop.columnar_sim.ColumnarSimulator
¶
Reference :class:~causalrl.protocols.CausalEnvProtocol adapter over row-emitting callables.
sample_fn(n, seed) returns an iterable of row mappings (the TrajectoryLog schema); the
adapter wraps them into a :class:TrajectoryLog. do_fn(interventions, n, seed) is optional:
without it, :meth:do raises :class:CausalInterfaceUnavailableError (honest: the simulator
exposes only observational sampling). ledger optionally supplies a white-box
:class:NoiseLedger for counterfactuals.
Source code in src/causalrl/interop/columnar_sim.py
causalrl.interop.columnar_sim.simulator_from_callables(sample_fn, do_fn=None, *, metadata=None, ledger=None)
¶
Build a :class:ColumnarSimulator from a sample callable (and optional do/ledger).
Source code in src/causalrl/interop/columnar_sim.py
causalrl.ope.certify.certify_policy(dataset, target_actions, *, gamma_max=10.0, alpha=None)
¶
Certify whether a learned policy's value improvement over the behaviour policy is robust to hidden confounding.
target_actions[i] is the action the learned policy would take at the i-th logged
decision's state (e.g. from a d3rlpy policy's greedy prediction, using the same observation
encoding it was trained on). dataset is any
:class:~causalrl.data.logged.LoggedDecisions: the tabular
:class:~causalrl.ConfoundedTrajectoryDataset whose actions are arm indices, or a
:class:~causalrl.data.logged.FeatureDecisionLog whose states are feature vectors and whose
actions are :data:~causalrl.Intervention assignments -- so an
:class:~causalrl.agents.interventional.InterventionalAgent can be certified directly, with no
arm codebook in between. The action type is inferred from the log, so a mismatched pair is a
type error rather than a silent miscount.
The contrast is the off-policy value V(pi) - V(behaviour) under
Tan's marginal sensitivity model: the nominal logging propensities are the dataset's empirical
behavior_propensity, pi's per-unit target probability is 1 where it matches the
logged action (greedy), and the behaviour arm's target probability is the logging propensity, so
its self-normalised value is the logged empirical return. Returns the same
:class:~causalrl.DecisionCertificate as :func:causalrl.certify_decision.
Passing alpha additionally runs the finite-sample lower-confidence-bound gate for safe
policy improvement: :func:causalrl.conformal_action_value calibrates a distribution-free
lower bound on a fresh return under pi and under the logging policy from the same logs, and
certified then requires the confounding verdict and lcb(pi) >= lcb(behaviour). Too
few effectively-weighted samples for the level leave lcb(pi) at -inf, which refuses
rather than passes — no evidence is not evidence of safety. The gate is on the return of a
single decision, not on V(pi): a policy with the better mean but a worse downside fails it.
Honest scope: the MSM sensitivity is on the logging propensities; the behaviour value is the
logged empirical return (on-policy for the behaviour policy, hence unconfounded). This is the
one-step / terminal-return contrast; the per-step cumulative-reward extension uses
:func:causalrl.msm_per_step_bounds.
Source code in src/causalrl/ope/certify.py
causalrl.scale.d3rlpy.certify_fqe(estimated_value, *, policy=None, method='FQE')
¶
Wrap a fitted-Q-evaluation off-policy value estimate in an honest EMPIRICAL certificate.
Fitted-Q evaluation (d3rlpy's FQE) is a model-based off-policy value estimate with no
identification guarantee under hidden confounding, so the certificate is kind=EMPIRICAL
(I2/I3) — it reports the estimate and its provenance, never a causal point claim. Upgrade to an
identified certify_effect (§7.2) or bound it with :func:causalrl.certify_policy when the
structure licenses it. estimated_value is the scalar FQE value (e.g. the initial-state value
a trained FQE predicts for the evaluated policy).
Source code in src/causalrl/scale/d3rlpy.py
causalrl.scale.d3rlpy.policy_actions(policy, observations)
¶
Greedy actions a trained (duck-typed) policy takes on observations — a do() handle.
Calls the policy's predict (d3rlpy's algorithm interface); returns integer actions ready
to pass as target_actions to :func:causalrl.certify_policy, or as a policy intervention.
Never imports d3rlpy; any object with a predict(observations) method works.
Source code in src/causalrl/scale/d3rlpy.py
Set-Valued Interventions¶
causalrl.intervention.InterventionSpace
dataclass
¶
Which variables may be intervened on right now, and the values each may take.
Hashable and composable, following :class:causalrl.Regime's idiom: the domains are stored as
a normalised tuple of pairs so that two spaces built from equal mappings compare equal
regardless of insertion order. Build one with :meth:create.
A space is a context-dependent object. The intended pattern is to construct a fresh one per decision from the current state -- dropping the levers this state does not permit and narrowing the values the others may take -- and hand it to the agent for that decision. What stays fixed across decisions is the graph; what varies is this.
Source code in src/causalrl/intervention.py
151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 | |
variables
property
¶
The manipulable variables — what :func:causalrl.pomis takes as manipulable.
create(domains)
classmethod
¶
Build a space from {variable: allowed values}.
A variable mapped to an empty domain is refused rather than silently retained: it would be a variable that is nominally manipulable but has nothing it can be set to, which makes every intervention set containing it unsatisfiable while still counting as admissible. Drop the variable instead -- that is what "not manipulable here" means.
Source code in src/causalrl/intervention.py
domain(variable)
¶
The :data:InterventionDomain of variable.
Raises KeyError if it is not manipulable here.
Source code in src/causalrl/intervention.py
values(variable)
¶
The admissible values of a DISCRETE variable.
Raises KeyError if variable is not manipulable, and TypeError if its domain is
continuous -- an interval has no value list, and returning a sampled or gridded stand-in
would quietly turn an exact domain into an approximation of one.
Source code in src/causalrl/intervention.py
permits(intervention)
¶
Whether every variable in intervention is manipulable and set to an allowed value.
Source code in src/causalrl/intervention.py
project(intervention)
¶
intervention moved into this space: each value clipped or snapped to its domain.
The counterpart of :meth:permits for a search that proposes freely and needs its
proposals made admissible -- clipping a dose to its safe range rather than discarding the
candidate. Raises KeyError naming any variable that is not manipulable here, since no
amount of clipping makes an unavailable lever available.
Source code in src/causalrl/intervention.py
sample(variables, rng)
¶
One admissible assignment of variables, drawn uniformly from each domain.
The continuous counterpart of :meth:assignments: where a finite space can be enumerated,
an interval must be searched, and a search needs proposals.
Source code in src/causalrl/intervention.py
restrict(variables)
¶
The sub-space over variables, ignoring any that are not manipulable here.
Source code in src/causalrl/intervention.py
__and__(other)
¶
Intersect two spaces: variables in both, each with the values both allow.
A variable whose domains do not overlap drops out entirely, since retaining it with an
empty domain is exactly what :meth:create refuses.
Source code in src/causalrl/intervention.py
assignments(variables)
¶
Every assignment of variables admissible here — the arms of an intervention set.
This is the step from "which variables to intervene on" (what POMIS answers) to "which interventions to choose between" (what an agent needs). The empty set yields exactly one assignment, the empty intervention, which is the observational regime rather than nothing.
Raises KeyError naming any variable that is not manipulable here, and ValueError
when the product exceeds :data:_MAX_ASSIGNMENTS.
Source code in src/causalrl/intervention.py
causalrl.intervention.canonical(intervention)
¶
A hashable, order-independent key for an intervention.
Intervention is a Mapping and therefore unhashable, which makes the obvious things --
deduplicating arms, using an intervention as a dict key, comparing two of them for identity --
awkward at every call site. This is the one canonical form, sorted by variable name so
{"A": 1, "B": 0} and {"B": 0, "A": 1} produce the same key.
Source code in src/causalrl/intervention.py
causalrl.agents.interventional.InterventionalAgent
¶
Bases: ABC
An agent whose action is an intervention — an assignment, not an arm index.
Implementations must return an intervention that space permits. The base class does not
enforce that (a subclass may have a cheaper way to stay in bounds than re-checking), but
:meth:check_permitted is provided for the ones that want it.
deadline is advisory and cooperative: nothing here interrupts a running computation, so an
implementation that ignores it will simply overrun. An agent that does honour it should keep
a usable incumbent answer at all times and return it once
:meth:~causalrl.deadline.Deadline.expired reports the budget is gone. None means no
budget, which is the default so that offline and test call sites stay free of timing concerns.
Source code in src/causalrl/agents/interventional.py
act(observation, *, space, deadline=None)
abstractmethod
¶
Choose an intervention admissible in space, ideally within deadline.
Source code in src/causalrl/agents/interventional.py
update(observation, intervention, reward)
abstractmethod
¶
check_permitted(intervention, space)
staticmethod
¶
Return intervention if space permits it, else raise ValueError saying why.
A helper rather than an enforced invariant: an agent that constructs its candidates from
space in the first place cannot violate it and should not pay to re-check.
Source code in src/causalrl/agents/interventional.py
causalrl.agents.interventional.ScalarAgentAdapter
¶
Bases: InterventionalAgent
Present an arm-indexed :class:causalrl.Agent as an :class:InterventionalAgent.
arms[i] is the intervention the wrapped agent's action i denotes, so the adapter is
just a codebook in both directions: :meth:act maps the chosen index to its intervention, and
:meth:update maps an intervention back to its index before forwarding the reward.
The wrapped agent's arm list is fixed, so space can only be checked, not honoured. A
scalar agent has no way to be told that arm 3 is unavailable this turn -- its policy is defined
over all its arms. :meth:act therefore verifies that the arm it returns is admissible and
raises if it is not, rather than silently emitting an inadmissible action or quietly
substituting a different one. Where the arm list genuinely varies per decision, implement
:class:InterventionalAgent directly instead of adapting a scalar agent.
deadline is accepted and ignored for the same reason: :meth:causalrl.Agent.act takes no
budget, so there is nothing to pass it to.
Source code in src/causalrl/agents/interventional.py
arms
property
¶
The codebook: arms[i] is the intervention denoted by the wrapped agent's action.
act(observation, *, space, deadline=None)
¶
Map the wrapped agent's chosen index to its intervention, checked against space.
Source code in src/causalrl/agents/interventional.py
update(observation, intervention, reward)
¶
Forward reward to the wrapped agent under the index that intervention encodes.
Source code in src/causalrl/agents/interventional.py
causalrl.deadline.Deadline
dataclass
¶
A monotonic instant by which a decision must be returned.
Construct with :meth:after (a duration from now) rather than the raw constructor, which
takes an absolute :func:time.monotonic reading and is mainly useful in tests where the clock
is supplied explicitly.
Source code in src/causalrl/deadline.py
at
instance-attribute
¶
Absolute :func:time.monotonic value at which the budget expires.
after(seconds)
classmethod
¶
A deadline seconds from now.
A non-positive duration is allowed and yields an already-expired deadline: callers that compute a remaining budget by subtraction should get "no time left", not an exception.
Source code in src/causalrl/deadline.py
remaining()
¶
expired()
¶
fraction_of(share)
¶
share of the remaining budget, in seconds — for splitting time across sub-steps.
share must lie in [0, 1]: it is a fraction of what is left, not a multiplier, and
a value above 1 would hand out a slice larger than the remaining budget under a name that
says otherwise.
Source code in src/causalrl/deadline.py
Known Mechanisms¶
causalrl.scm.fitters.PinnedMechanism
¶
Deploy a known structural equation at this node instead of fitting one.
Real systems are rarely all-known or all-unknown: a documented rule, a tariff, a physical law
or a contractual formula sits next to behaviour nobody has a closed form for. This fitter is
the "known" half. Pass it to fit_scm(..., families={"V": PinnedMechanism(g)}) and node
V deploys V = g(parents) + U exactly as given, while every other node is learned from
the data as usual.
mean is the deterministic part g, taking the same dict[str, Tensor] of parent
columns every mechanism in this module receives and returning one column. The noise enters
additively, so the mechanism is invertible and counterfactuals at this node stay identified —
which is part of the point: pinning a mechanism can restore an identification that a fitted
node would have lost.
noise is the exogenous distribution. Leaving it None estimates it by resampling the
residuals V - g(parents) on the training rows, so the shape of the equation is pinned
while the spread around it still comes from data; supply a distribution to pin that too.
A pinned node is still scored, and that score is the assumption's test. fit_scm reports
:attr:~causalrl.scm.fit.NodeFit.holdout_score for this node like any other — but here it is
not a measure of how well a flexible family fitted, it is a measure of whether the equation you
asserted actually describes held-out data. A pinned node with a poor holdout score is telling
you the assertion is wrong, and nothing else in the pipeline will.
Source code in src/causalrl/scm/fitters.py
526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578 579 580 581 582 583 584 585 586 587 588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606 | |
Continuous States¶
causalrl.state.StateEncoder
¶
Bases: Protocol
Maps an observation to a fixed-length feature vector.
dim is the length of every vector :meth:encode returns; downstream learners size their
design matrices from it, so it must not vary between calls.
Source code in src/causalrl/state.py
causalrl.state.OneHotEncoder
¶
Encode a discrete state index as an indicator vector — the tabular case, contained.
This is the bridge between the two worlds. Feature-space learning with these features and an exactly-solving linear learner reproduces the tabular backup, because the indicator basis spans every function on a finite state set. That makes tabular behaviour a special case of the fitted path rather than a separate code path, and it is what the faithfulness tests check.
key names the observation entry holding the state index, matching the "state" key the
shipped tabular agents already read.
Source code in src/causalrl/state.py
causalrl.state.IdentityEncoder
¶
Read named continuous entries out of the observation, in the order given.
The minimal continuous encoder: no basis expansion, so a linear learner over these features
fits a linear value function. keys fixes the column order, which must stay stable for the
features to mean the same thing across calls.
Source code in src/causalrl/state.py
causalrl.state.RBFEncoder
¶
Gaussian radial basis features over an inner encoder's output.
The nonlinear counterpart to :class:IdentityEncoder, and the same basis
:class:causalrl.FunctionApproxBackdoorAgent already uses for a continuous confounder — the
point being that a continuous state and a continuous confounder want the same machinery,
which is precisely why the tabular state interface was the odd one out.
centers has one row per basis function, each of the inner encoder's dimension. A constant
column is prepended so a learner can represent an offset without a separate intercept term.
Source code in src/causalrl/state.py
causalrl.state.FeatureTransition
dataclass
¶
A logged transition whose endpoints are feature vectors rather than state indices.
The feature-space counterpart of :class:causalrl.data.dataset.Transition, and the argument
type of the fitted backup. done marks an absorbing terminal, whose successor carries no
future value — the same convention the tabular agents use, kept so that one-hot features
reproduce their behaviour exactly.
Source code in src/causalrl/state.py
causalrl.state.encode_batch(encoder, observations)
¶
Stack encoder.encode over observations into an (n, dim) design matrix.
Returns a correctly-shaped empty array for an empty sequence, so a caller assembling a design matrix from a possibly-empty slice does not have to special-case it.
Source code in src/causalrl/state.py
causalrl.agents.fitted.FittedQIteration
¶
Bases: BatchAgent
Finite-horizon fitted Q iteration over encoded state features.
Backward induction, identical in shape to :class:causalrl.DOVI's but with the table replaced
by one fitted regressor per (step, action)::
V_{H+1}(s) = 0
Q_h(·, a) = fit( features(s) -> r + [not done] · V_{h+1}(s') ) over rows with A = a
Q_h = min(Q_h, reward_max · (H - h + 1)) # the global optimism cap
V_h(s) = max_a Q_h(s, a)
encoder decides what a state is. With :class:~causalrl.state.OneHotEncoder and a
least-squares regressor the fitted backup reproduces the tabular one, because the indicator
basis spans every function on a finite state set — the tabular agent is a special case of this
one, not a separate algorithm.
An action never taken at some step has no data to fit, so its Q is set to the cap. That keeps the backup optimistic in the unexplored direction, matching how the tabular agent treats an unvisited cell.
reward_max must be a genuine per-step upper bound on the reward; the cap is only as valid
as that number. Rewards are not otherwise assumed bounded below.
Source code in src/causalrl/agents/fitted.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 | |
is_certified
property
¶
Always False: a fitted backup carries no per-cell causal bound.
Named to match :attr:causalrl.DOVI.is_certified so the two are directly comparable. The
tabular agent can answer True because its optimism is capped by a Manski bound computed
per (state, action); this one caps optimism globally by horizon and reward range, which
is valid but is not an identification guarantee.
cap(step)
¶
The global optimism cap at step step: reward_max * (horizon - step + 1).
The largest return achievable from step onward when each of the remaining steps pays at
most reward_max. Valid with no assumption on the function class, and much weaker than a
per-cell bound.
Source code in src/causalrl/agents/fitted.py
observe(transition)
¶
Add one feature-typed transition to the buffer and invalidate the current plan.
buffered_transitions()
¶
The transitions observed so far and not yet discarded by a fit(transitions=...).
The online buffer is otherwise write-only: a caller driving this agent through
:meth:observe_step had no way to see what it had actually collected, which makes
"did my driver wire the hook up correctly" unanswerable without reaching into a private
attribute.
Source code in src/causalrl/agents/fitted.py
observe_transition(state, action, next_state, done)
¶
Refused: this agent's states are feature vectors, not indices.
The tabular hook is overridden rather than left as the inherited no-op, which would accept
the call and silently discard the transition. Encode the endpoints and call
:meth:observe with a :class:~causalrl.state.FeatureTransition instead.
Source code in src/causalrl/agents/fitted.py
observe_step(observation, action, reward, next_observation, done)
¶
Encode both endpoints with this agent's encoder and buffer the transition.
The feature-space answer to the tabular hook above: a driver hands over raw observations
and the agent encodes them, so :func:causalrl.eval.harness.run_episodes can drive this
agent without knowing anything about its state representation.
Source code in src/causalrl/agents/fitted.py
fit(transitions=None)
¶
Run the backward induction over transitions (or the observed buffer) and plan.
Passing transitions replaces the buffer, so a caller can refit from a fresh batch
without carrying stale online data. Raises ValueError on an empty dataset — there is no
such thing as a fitted value function with nothing to fit it on.
Source code in src/causalrl/agents/fitted.py
q_values(observation, step=1)
¶
Capped Q values for every action at observation, as a (n_actions,) array.
Source code in src/causalrl/agents/fitted.py
value(observation, step=1)
¶
max_a Q_step(observation, a) — the fitted value at this observation.
act(observation)
¶
Greedy action at the observation's step, ties broken uniformly at random.
The step is read from observation["t"] (0-indexed, as the shipped environments emit it)
and clamped to the horizon, matching :meth:causalrl.DOVI.act.
Source code in src/causalrl/agents/fitted.py
certificate()
¶
An EMPIRICAL certificate recording exactly which guarantee was given up.
The hedge carries downgraded_from="bounded": the tabular path is partially identified
by a per-cell Manski bound, and replacing the table with a function class forfeits that in
exchange for a global horizon cap. Reporting this as BOUNDED would claim an
identification status the fitted backup does not have.
Source code in src/causalrl/agents/fitted.py
Function-Valued Causal Bounds¶
causalrl.bounds.functional.FunctionalManskiBounds
¶
Cross-fitted pointwise Manski bounds on E[R | do(a), X = x].
outcome_model and propensity_model are factories returning a fresh
:class:~causalrl.estimate.nuisance.Regressor / :class:~causalrl.estimate.nuisance.Classifier
— the same duck-typed protocols the DML estimators use, so any scikit-learn estimator works and
the defaults keep the core dependency-free. The propensity is fitted one-vs-rest and normalised
across actions.
reward_range must genuinely contain the reward; it is what the unlogged fraction is bounded
by, and the bound is only valid if it holds. It defaults to (0, 1), matching
:func:causalrl.causal_q_bounds.
Cross-fitting produces two different predictors, deliberately kept apart:
:attr:in_sample are out-of-fold values for the training rows (each from a model that never saw
that row), and :meth:bounds evaluates new points by averaging the fold models. Use the former
to summarise the data you fitted on and the latter to bound anywhere else.
Source code in src/causalrl/bounds/functional.py
88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 | |
in_sample
property
¶
Out-of-fold (lower, upper) for the training rows, each (n, n_actions).
fit(features, actions, rewards)
¶
Cross-fit the outcome and propensity models and record the out-of-fold bounds.
Source code in src/causalrl/bounds/functional.py
bounds(features)
¶
(lower, upper) at arbitrary feature rows, from the averaged fold models.
Each is (n_rows, n_actions). Evaluating the training rows through this path is
not the same as :attr:in_sample: the average includes the fold model that saw them, so
the interval is optimistically tight there.
Source code in src/causalrl/bounds/functional.py
diagnostic()
¶
Overlap summary from the out-of-fold propensities — see :class:OverlapDiagnostic.
Source code in src/causalrl/bounds/functional.py
causalrl.bounds.functional.OverlapDiagnostic
dataclass
¶
How much support the fitted propensity actually found, per action.
min_propensity and mean_propensity are taken over the estimated e(x, a) at the
training rows for the action actually taken. vacuous_fraction is the share of (row,
action) pairs whose estimated propensity falls below :data:_VACUOUS_PROPENSITY, i.e. where
the interval is close to the whole reward range.
A high vacuous_fraction is not a failure — it is the bound correctly reporting that the logs
do not pin the effect down. The case to be suspicious of is the opposite: a very high
mean_propensity from a flexible model, which yields tight intervals that are only as
trustworthy as the propensity fit.
Source code in src/causalrl/bounds/functional.py
causalrl.agents.bounded_fitted.BoundedFittedQIteration
¶
Bases: BatchAgent
Finite-horizon envelope propagation over encoded features, under Manski reward bounds.
encoder decides what a state is; bounds_model supplies the immediate-reward interval at
each feature point (default :class:~causalrl.bounds.functional.FunctionalManskiBounds with the
dependency-free nuisances); regressor estimates the continuation expectation.
As with :class:~causalrl.agents.fitted.FittedQIteration, the tabular case is contained rather
than replaced: with :class:~causalrl.state.OneHotEncoder the reward bounds reduce to
:func:causalrl.causal_q_bounds per cell, so the envelope reduces to the interval DOVI's
ceiling caps.
transition_assumption mirrors :class:causalrl.DOVI. At "unknown" with a horizon above
1, :meth:fit raises :class:~causalrl.exceptions.UnverifiedAssumptionError unless
allow_heuristic=True — propagating an interval through a confounded successor distribution
produces a number that is not a bound, and running it silently would be the one failure mode
this class exists to prevent.
Source code in src/causalrl/agents/bounded_fitted.py
58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 | |
is_certified
property
¶
Whether the propagated interval is a bound rather than heuristic value propagation.
True at horizon 1 (nothing is propagated, so only the reward bound is used) or when the
caller asserts unconfounded transitions. Mirrors :attr:causalrl.DOVI.is_certified.
Note this is a statement about the transition assumption only. Even when it is True,
the interval still rests on the outcome and propensity models being correctly specified —
an assumption the tabular bound does not need, and one every certificate here records.
fit(transitions=None)
¶
Fit the reward bounds and propagate the envelope back from the horizon.
Passing transitions replaces the observed buffer, matching
:meth:causalrl.FittedQIteration.fit; omitting it refits from whatever
:meth:observe_step has accumulated.
Source code in src/causalrl/agents/bounded_fitted.py
envelope(observation, step=1)
¶
(lower, upper) arrays over actions at observation and step.
Source code in src/causalrl/agents/bounded_fitted.py
interval(observation, action, step=1)
¶
The bound on Q_step(observation, action) as an :class:Interval.
Source code in src/causalrl/agents/bounded_fitted.py
non_dominated(observation, step=1)
¶
Actions whose upper bound reaches the best lower bound — the survivors.
The feature-space counterpart of
:func:causalrl.agents.primitives.non_dominated_actions. An action whose whole interval
lies below another's floor cannot be optimal at this state and can be dropped before any
exploration. As in the tabular case, natural Manski bounds are wide and this is often a
no-op returning every action, which is correct rather than a defect.
Source code in src/causalrl/agents/bounded_fitted.py
act(observation)
¶
Optimism in the face of confounding: the action with the highest upper bound.
Source code in src/causalrl/agents/bounded_fitted.py
observe_transition(state, action, next_state, done)
¶
Refused: states here are feature vectors, not indices.
Source code in src/causalrl/agents/bounded_fitted.py
observe_step(observation, action, reward, next_observation, done)
¶
Encode both endpoints and buffer the transition.
See :meth:causalrl.FittedQIteration.observe_step.
Source code in src/causalrl/agents/bounded_fitted.py
buffered_transitions()
¶
The transitions observed so far and not yet discarded by a fit(transitions=...).
The online buffer is otherwise write-only: a caller driving this agent through
:meth:observe_step had no way to see what it had actually collected, which makes
"did my driver wire the hook up correctly" unanswerable without reaching into a private
attribute.
Source code in src/causalrl/agents/bounded_fitted.py
certificate()
¶
A BOUNDED certificate when the transition assumption holds, else EMPIRICAL.
Either way the assumptions are recorded explicitly, including the two the tabular path does not need — that the outcome regression and the propensity model are correctly specified. The overlap diagnostic rides along on the propensity assumption, since it is the one checkable thing about it: where estimated propensities are near zero the interval is wide and honest, and where a flexible model has driven them near one the interval is tight and only as good as that model.
Source code in src/causalrl/agents/bounded_fitted.py
298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 | |
Core — Complete Reference¶
causalrl.agents.causal_mbrl.CausalMBRLAgent
¶
Route an offline confounded decision problem to the right causal planner, behind fit/act.
Routing (first match wins):
============================ ============================================================
You provide Strategy (planner)
============================ ============================================================
horizon=... "sequential" — deconfounded value iteration (DOVI)
transport=(...) "transport" — deconfound + transport by the target P
continuous_confounder "function_approx" — ridge-RBF back-door on a continuous Z
covariates=[...] "g_formula" — multivariate standardization (real datasets)
graph=None "discovery" — skeleton discovery + temporal tiers, adjust
otherwise (graph given) "backdoor" — back-door adjust an observed confounder
============================ ============================================================
Then fit on offline data and act on an observation. explain() reports the chosen
strategy and the identification basis (adjustment set / transportability / confounder) — the
trust artifact for the identifiable regime this agent targets. It beats correlational offline
RL only there (confounded / offline / transfer); it is not a clean-benchmark performer.
Source code in src/causalrl/agents/causal_mbrl.py
27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 | |
planner
property
¶
The underlying specialized planner, for full control or inspection.
adjustment_set
property
¶
The back-door adjustment set the planner used, when the strategy exposes one.
fit(data, *, target_covariates=None)
¶
Fit the routed planner on offline data and return self.
data is columnar {treatment, outcome, *covariates} for every strategy except
"sequential", which takes a ConfoundedTrajectoryDataset of trajectory logs.
target_covariates (target-domain draws of the transport variables) is required for the
"transport" strategy.
Source code in src/causalrl/agents/causal_mbrl.py
act(observation)
¶
Return the planner's action for observation.
Supply the variables the planner conditions on by name — the adjustment set for
"backdoor"/"discovery", those plus the transport variables for "transport", the
continuous confounder for "function_approx", the covariates for "g_formula" (e.g.
{"Z": 1} or {"age": 42, "smoke": 1}) — and the decision is contextual. The
"sequential" planner takes {"state": s} instead. An observation carrying none of
them falls back to the marginal argmax_a E[Y | do(a)], which is the right answer only
when there is genuinely no context to condition on.
Source code in src/causalrl/agents/causal_mbrl.py
update(observation, action, reward)
¶
The fitted outcome model is fixed after fit; the policy varies with the observation,
not with online experience.
explain()
¶
Human-readable provenance: the strategy and the identification basis it relied on.
Source code in src/causalrl/agents/causal_mbrl.py
causalrl.agents.bandits.CausalThompsonSampling
¶
Bases: Agent
Thompson sampling with one Beta posterior per (intuition, arm) cell.
Conditioning on the observed confounder proxy intuition de-confounds the choice,
letting the agent learn the lucky arm for each intuition value.
Sampling uses a per-instance numpy.random.Generator seeded from seed (see
:class:NaiveThompsonSampling for the reproducibility contract).
Source code in src/causalrl/agents/bandits.py
causalrl.scm.fit.fit_scm(data, *, graph, families=None, holdout=0.2, seed=0)
¶
Fit every mechanism of graph from data; return a learned SCM.
Each node is fitted by families[node] when given, else by dtype: integer-valued columns
with at most 20 levels get :class:TabularCPT, everything else :class:ANMFit. The result is
an ordinary :class:StructuralCausalModel marked provenance="fitted".
Mechanisms may be pinned instead of learned. Passing
:class:~causalrl.scm.fitters.PinnedMechanism as a node's family deploys the equation you
supply at that node while the rest of the graph is still fitted from data — the ordinary
case where a documented rule sits next to behaviour that has no closed form. A model with both
kinds of node carries provenance="mixed"; one whose every node is pinned carries
provenance="specified", since nothing about its equations came from the data. Pinned nodes
are listed by :attr:FitReport.pinned_nodes and still receive a holdout_score, which for
them tests the asserted equation rather than measuring a fit.
The deployed mechanisms are fitted on a fraction of the data. data is split once, by a
seed-controlled permutation shared by every node: 1 - holdout of the rows train the
mechanisms the returned SCM actually carries, and the remaining holdout fraction is scored
but never trained on. Nothing is refitted on the full sample afterwards, so at the default
holdout=0.2 every number this model reports — every do(), every contrast — comes from
an 80%-data fit. Pass a smaller holdout to trade the out-of-sample score for training rows.
holdout must lie in (0, 1). 0.0 is refused rather than silently reporting an
in-sample number under :attr:NodeFit.holdout_score, whose contract is data the fit never
saw; >= 1.0 would leave a one-row training set.
Raises :class:NotIdentifiableError on a graph with bidirected edges: under latent confounding
a node's mechanism is not recoverable by regression on its observed parents, so this function
has no single SCM to return. :func:causalrl.fit_scm_bounded takes that graph instead, fitting
the nodes whose own parents are unconfounded and bounding the rest.
Source code in src/causalrl/scm/fit.py
120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 | |
Identification, Transport & Discovery — Complete Reference¶
causalrl.identification.counterfactual_bounds.CounterfactualBound
¶
Bases: NamedTuple
Bounds on a counterfactual expectation, with whether they are tight.
A separate type from :class:~causalrl.identification.bounds.Interval, whose documented
tuple-unpacking contract (lo, hi = interval) a third field would break. tight is
False only for bounds this library knows it cannot close; phase-1
counterfactual_interval refuses such queries rather than returning a loose answer, so it
always reports True.
Source code in src/causalrl/identification/counterfactual_bounds.py
interval
property
¶
The bound as a plain :class:Interval, for the partial-identification surface.
causalrl.identification.bounds.Interval
¶
Bases: NamedTuple
A partial-identification interval [lower, upper].
Tuple-compatible: lo, hi = interval and interval[0] work, so existing
tuple-consuming callers are unaffected; .lower / .upper read more clearly.
Source code in src/causalrl/identification/bounds.py
causalrl.identification.bounds.PivotalityCertificate
¶
Bases: NamedTuple
Result of :func:pivotality_certificate (one-sided sign-robustness certificate).
Source code in src/causalrl/identification/bounds.py
causalrl.identification.estimate.PolicyValueContrast
dataclass
¶
An off-policy value contrast V(pi_on) - V(pi_off) in the terms certify_estimate needs.
Supply MSM evidence (logging_propensities + target_on + target_off) and/or
structural evidence (treated + confounder_bins or mi_cap); at least one layer
must be runnable. See :func:causalrl.certify_estimate.
Source code in src/causalrl/identification/estimate.py
has_msm
property
¶
Whether the MSM sensitivity layer can run (logging propensities supplied).
has_pivotality
property
¶
Whether the structural sign-robustness layer can run (binary arms + a channel bound).
from_binary(outcomes, treated, *, propensities=None, confounder_bins=None, mi_cap=None)
classmethod
¶
The raw-logs case: one-hot arms pi_on = 1{treated}, pi_off = 1{control}.
Exactly the contrast :func:causalrl.certify_decision builds internally. The MSM layer runs
only when propensities are given; the pivotality layer runs when confounder_bins or
mi_cap is given.
Source code in src/causalrl/identification/estimate.py
causalrl.identification.transport.TransportFormula
dataclass
¶
How to compute P*(y | do(x)) from source and target data.
Source code in src/causalrl/identification/transport.py
causalrl.identification.transport_regret.TransportRegretCertificate
dataclass
¶
The transport-regret certificate for P*(value | do(action)) and a specific policy.
Attributes¶
transportable:
:func:is_transportable_effect on the selection diagram.
formula:
The readable :class:TransportFormula for the transportable part, or None.
non_transportable_witness:
The selection-marked variable(s) the policy must not trust off-distribution: the
selection-marked variables appearing in the transport formula's adjustment set (the
mechanism one would have to re-weight by the target marginal to transport), or the full
selection set if the effect is outright non-identifiable. Empty for a direct formula
with empty adjustment — the certified zero-regret case.
reweight_required:
Whether transporting the queried effect requires re-weighting by a selection-marked
variable (non_transportable_witness non-empty). Can be True even when
transportable is True: a policy that merely reads the witness without
re-weighting still incurs the bounded regret.
decision_dependence:
mu — the fraction of decision units whose executed behaviour depends on the
selection-marked mechanism, measured by the do()-sweep (:func:decision_flip_rate).
A property of the specific policy being certified, supplied by the caller.
value_range:
(V_min, V_max) of the policy value, used to scale the bound.
regret_bound:
Interval(0, (V_max - V_min) * mu) — the computable transfer-regret upper bound.
Collapses to Interval(0, 0) for a behaviour-invariant policy (mu = 0).
Source code in src/causalrl/identification/transport_regret.py
is_vacuous(*, frac=0.9)
¶
Whether the bound is vacuous: its width >= frac of the trivial value span.
Source code in src/causalrl/identification/transport_regret.py
causalrl.identification.criteria.backdoor_adjustment_set(graph, treatment, outcome)
¶
Return the observed-parent back-door adjustment set for treatment.
SCOPE / PRECONDITION: this returns parents(treatment), which is a valid parent
adjustment set when the treatment is not incident to latent confounding. It does not
implement front-door identification or the general ID/sID/gID algorithms. Graphs outside
that supported contract raise rather than returning an invalid adjustment set.
Source code in src/causalrl/identification/criteria.py
causalrl.identification.decision.certify_estimate(estimate, *, gamma_max=10.0, labels=('pi_on', 'pi_off'))
¶
Certify whether an off-policy value contrast's sign is robust to hidden confounding.
Runs the MSM tipping layer over the general V(pi_on) - V(pi_off) contribution bound when
estimate carries logging propensities, and the structural pivotality layer when it carries a
binary-arm reduction (treated + confounder_bins / mi_cap). Returns the same
:class:DecisionCertificate as :func:certify_decision; labels names the two policies in
the verdict (labels[0] when the contrast is positive).
Honest scope: the MSM sensitivity is on the logging propensities (sharp when the two target supports are disjoint, valid-but-conservative otherwise); the pivotality layer is defined only for the binary-arm contrast. No sensitivity claim is made for arbitrary outcome-model estimators.
Source code in src/causalrl/identification/decision.py
causalrl.transport.estimate.certify_transported_policy_value(diagram, source_data, target_data, *, stages, outcome, target_actions, alpha=0.05, outcome_model=None, policy='pi')
¶
Certify a deterministic policy's transported finite-horizon value (hedge-first; §7.5).
stages[t] is {"history": (names...), "treatment": name} for stage t;
target_actions gives a constant target action per stage. Sequential/policy-value transport
is research-grade, so this is identified only in one subcase — when the selection difference
is confined to the baseline distribution H_1 (a population shift, downstream mechanisms
shared): the source sequential g-computation is reweighted to the target baseline marginal
(discrete baseline strata, as in :func:certify_transported_effect). Any selection node on a
time-varying covariate, treatment, or the outcome yields a hedge (I3), with
transport_regret_certificate the shipped floor. Sequential ignorability is recorded as a
non-checkable assumption.
Source code in src/causalrl/transport/estimate.py
252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 | |
causalrl.identification.bounds.confounding_bias_per_step_bounds(outcomes, treated_by_step, confounder_bins, *, form='tv')
¶
Per-step channel-cap bounds for sequential per-factor credits (THEORY Theorem-seq).
One row per unit (e.g. per episode); treated_by_step[t] is step t's binary action
factor for every unit, confounder_bins the shared episode-level hidden variable. Each
step's omitted-Z credit bias |naive_t - adjusted_t| is bounded by
:func:confounding_bias_bound at that step's arm split; a cumulative additive credit's
bias is bounded by the sum. This is the average-budget (information) sibling of the
uniform-odds :func:msm_per_step_bounds: there each step gets an MSM gamma budget,
here each step gets the channel budget MI(F_t;Z) <= MI(I_t;Z) of ITS OWN information
set — in games, later information sets typically leak more about Z, so later credits
are systematically less protected (the growing-channel prediction).
Source code in src/causalrl/identification/bounds.py
causalrl.identification.counterfactual_bounds.counterfactual_interval(scm, *, evidence, interventions, target, n=20000, seed=None)
¶
Bound E[target_{do(interventions)} | evidence] on a fitted SCM.
A fitted discrete mechanism pins P(V | parents) but not the coupling from noise to value,
and the two are indistinguishable in L1 and L2 data. So the counterfactual is an interval over
admissible couplings, not a point. Its extremes solve a box-and-sum problem: the factual and
counterfactual columns of the coupling share known marginals, which bounds each cell, and the
conditioning event fixes their sum.
Invertible mechanisms contribute no width — their noise is recoverable — so an all-invertible
SCM returns a point, matching :meth:StructuralCausalModel.abduct exactly. So does an
intervention that pins target itself, or one that leaves target's parents at their
factual values: neither leaves any coupling to choose.
What tight=True claims. The bound is exact given the fitted conditionals: no
relaxation is applied to them, and the extremes are solved in closed form rather than
approximated. Those conditionals are not exact, though — they are Monte-Carlo estimates,
n draws from target's exogenous distribution pushed through its fitted mechanism at
each parent configuration, under seed (None gives a fixed default stream, not a
random one). So the endpoints carry that estimate's sampling error, and a level whose fitted
probability is far below 1/n can be missing from the sampled support altogether, which
narrows the reported interval below the identified one. Raise n to shrink both effects;
tight describes the estimator, not the number of correct digits.
Source code in src/causalrl/identification/counterfactual_bounds.py
47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
causalrl.identification.transport_regret.decision_abstain_mask(greedy, units, *, values=(0, 1))
¶
Abstention mask: fire where the policy's immediate greedy decision is not invariant to intervening on the selection-marked witness.
Fires on unit iff greedy(unit, w0) != greedy(unit, w1). This is the per-unit
decision-flip rule — by construction it recovers the truly-confused set when "confused" is
defined as the same single-decision flip (the mechanistic guarantee the G1 gate's M4 pins).
A behaviour-invariant policy has an (almost) empty mask. Distinct from
:func:decision_flip_rate's trajectory-divergence mu that scales the regret bound.
Source code in src/causalrl/identification/transport_regret.py
causalrl.identification.transport_regret.decision_flip_rate(trace, units, *, values=(0, 1))
¶
mu — fraction of units whose executed trajectory diverges under the do()-sweep.
trace(unit, w) returns the policy's executed rollout trace (action or visited-state
sequence — any hashable summary of what the policy does) from unit under
do(W = w), with everything else held fixed. Two traces differing is exactly the event on
which V^pi(u; w0) != V^pi(u; w1) can be nonzero, so span * mu is a valid regret
bound (module docstring). Use this — not the single-decision flip rate — to scale the bound.
Source code in src/causalrl/identification/transport_regret.py
causalrl.identification.criteria.is_identifiable(graph, treatment, outcome)
¶
Whether P(outcome | do(treatment)) is identifiable from observational data.
Delegates to the sound and complete ID algorithm
(:func:causalrl.identification.id_algorithm.is_identifiable_effect), so it returns a definite
boolean for any ADMG — including front-door-style cases the earlier scoped heuristic could only
report as unknown. For the estimand itself, see
:func:~causalrl.identification.id_algorithm.identify_effect and
:func:~causalrl.identification.id_algorithm.estimate_effect.
Source code in src/causalrl/identification/criteria.py
causalrl.discovery.orient(cpdag, *, tiers=None)
¶
Orient a CPDAG's remaining undirected edges into a DAG, or refuse.
Resolution order per undirected edge: (1) tiers — an edge between different tiers points
from the earlier tier to the later one; (2) acyclicity — if one direction would close a cycle,
take the other; (3) refuse. Silently picking an orientation would commit to a choice the data
does not identify, so the third case raises and names :func:causalrl.fit_scm_mec, which fits
every member of the equivalence class instead. Exception: if tiers are provided and a
tier-forced edge would create a cycle (indicating a conflict between tiers and discovered
structure), raises immediately with a message naming the specific colliding edges.
Source code in src/causalrl/discovery.py
causalrl.identification.transport_regret.transport_regret_certificate(diagram, *, action, value, value_range, decision_dependence, max_adjustment_size=3)
¶
Build the transport-regret certificate for P*(value | do(action)) on diagram.
Composes the library transport decision with the decision-regret wrapper:
transportable/formulafrom the engine (:func:is_transportable_effect/ :func:transport_formula).non_transportable_witness= selection-marked variables the transport formula must re-weight (they appear in its adjustment set); the full selection set if the effect is non-identifiable; empty for a direct formula with empty adjustment (certified zero).regret_bound=Interval(0, (V_max - V_min) * mu)withmuthe supplieddecision_dependence(thedo()-sweep result, :func:decision_flip_rate— the trajectory-divergence rate, NOT the abstention statistic).
decision_dependence is supplied by the caller because it is a property of the specific
policy being certified; the certificate primitive itself stays a pure composition of
causal-engine outputs.
Source code in src/causalrl/identification/transport_regret.py
Structural Models & Data — Complete Reference¶
causalrl.scm.fitters.ANMFit
¶
Continuous node: additive noise model V = g(parents) + U for an arbitrary regressor.
estimator is a factory returning a fresh object with fit(X, y) / predict(X) — the
same duck-typed contract as GFormulaBackdoorAgent(outcome_model=...), so scikit-learn stays
optional. The default is a dependency-free ridge on RBF features. U is drawn from the
empirical residual distribution, and is recoverable from (parents, value) — invertible=True.
Source code in src/causalrl/scm/fitters.py
causalrl.scm.continuous.abduction.AmortizedGaussianAbduction
¶
Bases: Module
Amortized VI encoder q(u | parents, y) = N(mu, sigma) for a fixed mechanism.
Trained by ELBO (Gaussian reparameterisation; standard-normal prior) to reconstruct the observed
outcome through the mechanism. General — works for non-invertible mechanisms — so its
counterfactuals are kind=EMPIRICAL.
Source code in src/causalrl/scm/continuous/abduction.py
causalrl.scm.continuous.bayesian_fit.BayesianLinearFit
¶
Continuous node: Bayesian linear structural equation, posterior sampled by NUTS (NumPyro).
Where :class:~causalrl.scm.fitters.LinearGaussianFit solves a single OLS point estimate,
this samples the full posterior over (intercept, weights, sigma) and returns the
posterior-mean as a :class:~causalrl.scm.mechanisms.LinearGaussianMechanism -- so every
invariant the point-estimated continuous families satisfy still holds here: invertible=True,
a residual closure is attached, the round-trip identity holds exactly, and
evaluate_holdout scores it by R^2 like its siblings. The draws behind that mean are attached
as mechanism.posterior, a dict[str, np.ndarray] keyed "intercept", each parent name,
and "sigma" -- plain NumPy, not JAX, so a caller without JAX in scope can use them directly.
Materialising one SCM per draw is a natural follow-on; this fitter does not build it.
Model: intercept ~ Normal(0, 10), w_j ~ Normal(0, 10) per parent, sigma ~
HalfNormal(1), y ~ Normal(intercept + X @ w, sigma). Both X and y are standardised
internally so the weakly-informative priors stay sensible regardless of native scale on either
side -- leaving y unstandardised would put sigma's HalfNormal(1) prior on the
child's raw scale, silently shrinking both sigma and the weights toward zero whenever that
scale is far from 1. The posterior draws are unstandardised back onto the original data scale
before being exposed.
Source code in src/causalrl/scm/continuous/bayesian_fit.py
36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 | |
causalrl.protocols.CausalEnvProtocol
¶
Bases: Protocol
The contract a simulator implements so every estimator/bound can drive it (§5.1).
Source code in src/causalrl/protocols.py
causalrl.scm.continuous.mechanisms.ConditionalFlowMechanism
¶
Bases: Module
Conditional normalizing-flow mechanism, invertible in its scalar exogenous noise.
V_i = flow(noise; parents) is a composition of n_blocks conditional affine maps
u -> loc_k(parents) + scale_k(parents) * u (with scale_k = softplus(.) + min_scale > 0)
interleaved with a fixed invertible LeakyReLU (negative_slope in (0, 1)), ending on
an affine map. Each factor is strictly increasing in the scalar u, so the whole map is
strictly monotone and the noise is recovered in closed form (:meth:invert) — exact abduction
and exact continuous counterfactuals are therefore available, as for
:class:LocationScaleMechanism but with a strictly more expressive, non-affine transform.
References: Rezende & Mohamed, Variational Inference with Normalizing Flows (2015); Papamakarios et al., Normalizing Flows for Probabilistic Modeling and Inference (JMLR 2021). Formula-level implementation; no third-party code is ported.
Source code in src/causalrl/scm/continuous/mechanisms.py
invert(parent_values, observed)
¶
Recover the exogenous noise exactly by undoing each factor in reverse order.
Source code in src/causalrl/scm/continuous/mechanisms.py
causalrl.data.dataset.ConfoundedTrajectoryDataset
¶
Immutable offline log plus empirical behavior statistics per (state, action).
Source code in src/causalrl/data/dataset.py
32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 | |
behavior_propensity(state, action)
¶
Empirical P(action | state). 0.0 if the state was never visited.
mean_reward(state, action)
¶
Empirical E[return | state, action]. 0.0 if (state, action) never logged.
outcomes()
¶
logging_propensities()
¶
Empirical pi_behavior(a_i | s_i) for the action logged at each transition.
matches(target_actions)
¶
Whether the target policy would have taken the logged action, per transition.
Source code in src/causalrl/data/dataset.py
target_propensities(target_actions)
¶
Empirical pi_behavior(a | s_i) for the action the target policy takes.
Never None: a tabular log counts every cell, so the propensity of an action nobody
took in a state is known to be exactly zero.
Source code in src/causalrl/data/dataset.py
positivity(target_actions)
¶
(state, action) cells the target policy reaches that the logs never played.
Always checkable: a tabular log counts every cell, so an action nobody took in a state
is known to have zero behaviour propensity rather than merely unobserved -- the
distinction :class:~causalrl.data.logged.PositivityReport exists to preserve.
Source code in src/causalrl/data/dataset.py
causalrl.data.logged.FeatureDecisionLog
¶
Bases: Generic[ActionT]
A :class:LoggedDecisions over feature-space states and any action type.
Build one from an agent's own logs when its states are vectors or its actions are
:data:~causalrl.Intervention assignments. Actions are compared through :func:action_key,
so a policy that returns {"dose": 2.0} matches a logged {"dose": 2.0} regardless of
mapping identity or key order.
behavior_propensity is optional and exists solely to make positivity checkable: given
pi_behavior(a | s) it can ask whether the target policy's action had support at each logged
state. Without it the log is still fully usable -- every weight the certificate layer forms
uses the logged action's own propensity, which is always present -- but
:meth:positivity reports that it could not check rather than that it found nothing.
Source code in src/causalrl/data/logged.py
162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 | |
decisions
property
¶
The logged decisions, in order.
causalrl.data.logged.LoggedDecision
dataclass
¶
Bases: Generic[ActionT]
One logged decision in feature space, with the propensity that produced it.
The feature-space counterpart of :class:~causalrl.data.dataset.Transition for the
certification path rather than the planning path: it carries no successor state, because
nothing in off-policy certification of a terminal return looks at one. propensity is
supplied rather than counted -- in feature space there are no cells to count.
Source code in src/causalrl/data/logged.py
causalrl.data.logged.LoggedDecisions
¶
Bases: Protocol[ActionT_contra]
The off-policy certification interface: outcomes, logging propensities, target matches.
Deliberately smaller than any concrete log. Nothing here mentions a state representation, because none of the certificate machinery needs one -- the marginal sensitivity model reweights units, and a unit is identified by its position in the log.
Source code in src/causalrl/data/logged.py
__len__()
¶
outcomes()
¶
logging_propensities()
¶
matches(target_actions)
¶
target_propensities(target_actions)
¶
pi_behavior(pi_target(s_i) | s_i) per decision, or None if unknowable here.
Note this is the behaviour propensity of the action the target policy would take, which
is a strictly harder thing to know than :meth:logging_propensities: it asks about an
action that may never have been played. A tabular log counts cells and can answer; a
feature-space log without a behaviour model returns None. The conformal band needs it
to bound the likelihood ratio at a fresh test point, and None correctly forces that
bound to infinity rather than letting an unknown pass as a small number.
Source code in src/causalrl/data/logged.py
positivity(target_actions)
¶
Where the target policy leaves the logs' support, if that is knowable at all.
Must agree with :meth:target_propensities: checkable exactly when that returns a
sequence, and non-empty gaps exactly where its entries are non-positive. The two are
separate methods because one supplies a bound and the other a human-readable diagnostic,
and collapsing them would cost the cell labels that make a positivity failure actionable.
Source code in src/causalrl/data/logged.py
causalrl.data.logged.PositivityReport
dataclass
¶
Whether a target policy strays outside the logs' support -- and whether that is knowable.
checkable is the load-bearing field. A tabular log counts every (state, action) cell,
so it can answer the question and checkable is True; a feature-space log without a
behaviour-propensity model cannot answer it at all, and reports False with empty gaps
rather than an empty gaps that reads as a clean bill of health. Consumers must not treat
the two the same: the first licenses a checkable positivity assumption, the second a hedge.
Source code in src/causalrl/data/logged.py
violated
property
¶
Whether a gap was actually found. False when the check could not be run.
causalrl.protocols.DictNoiseLedger
¶
A white-box NoiseLedger backed by registered exogenous draws (§5.5).
The black-box posterior path (amortized abduction) arrives in Phase 1; it raises here.
Source code in src/causalrl/protocols.py
causalrl.scm.scm.ExogenousPosterior
¶
Retained exogenous values from abduction, ready for prediction under interventions.
Holds the source SCM and a dict of exogenous tensors (the abducted units). Call
:meth:predict to evaluate the model — optionally mutilated by do — on them. Abduct
once, predict under many interventions (the efficient counterfactual pattern).
Source code in src/causalrl/scm/scm.py
predict(*, do=None)
¶
Evaluate the (optionally do-mutilated) model on the retained exogenous units.
Source code in src/causalrl/scm/scm.py
causalrl.scm.fit.FitReport
¶
Bases: NamedTuple
Per-node provenance for a fitted SCM -- see :class:NodeFit for what each field reports.
Source code in src/causalrl/scm/fit.py
pinned_nodes
property
¶
Nodes whose equation was supplied rather than learned, in report order.
causalrl.scm.mechanisms.FunctionalMechanism
¶
Wrap an arbitrary callable f(parent_values, noise) -> Tensor.
Source code in src/causalrl/scm/mechanisms.py
causalrl.scm.fitters.LinearGaussianFit
¶
Continuous node: closed-form OLS mean with Gaussian residual noise.
The noise enters additively and is therefore recoverable from (parents, value), so
counterfactuals at this node are identified — invertible=True.
Source code in src/causalrl/scm/fitters.py
causalrl.scm.mechanisms.LinearGaussianMechanism
¶
V_i = sum_j w_j * parent_j + bias + noise.
Source code in src/causalrl/scm/mechanisms.py
causalrl.scm.continuous.mechanisms.LocationScaleMechanism
¶
Bases: Module
Invertible location-scale mechanism V_i = loc(parents) + scale(parents) * noise.
scale = softplus(raw_scale(parents)) + min_scale > 0, so the map is invertible in noise:
noise = (V_i - loc) / scale. Exact abduction is therefore available via
:func:causalrl.scm.continuous.abduction.abduct_location_scale.
Source code in src/causalrl/scm/continuous/mechanisms.py
invert(parent_values, observed)
¶
Recover the exogenous noise exactly: (observed - loc) / scale.
Source code in src/causalrl/scm/continuous/mechanisms.py
causalrl.scm.continuous.mechanisms.MLPMechanism
¶
Bases: Module
Additive-noise neural mechanism V_i = MLP(parents) + noise.
Source code in src/causalrl/scm/continuous/mechanisms.py
causalrl.scm.mechanisms.Mechanism
¶
Bases: Protocol
A structural equation V_i = f_i(parents, noise).
Source code in src/causalrl/scm/mechanisms.py
causalrl.scm.continuous.nuts.NUTSNoisePosterior
¶
A NoisePosterior backed by NUTS draws of a mechanism's per-unit exogenous noise.
mean / std summarise the posterior per unit; sample(n) returns n posterior draws
(each a full per-unit noise vector), tiling if more are requested than were drawn. Pure NumPy —
it holds no JAX state, so it composes with the torch/NumPy stack unchanged.
Source code in src/causalrl/scm/continuous/nuts.py
causalrl.scm.fitters.NeuralFit
¶
Continuous node: V = net(parents) + U, an MLP mean with Gaussian residual noise.
Fires :class:causalrl.scm.mechanisms.NeuralMechanism — the neural causal model primitive.
The noise head is additive, so the mechanism stays invertible and counterfactuals at this node
remain identified; a general (non-additive) net would not.
Source code in src/causalrl/scm/fitters.py
causalrl.scm.mechanisms.NeuralMechanism
¶
Bases: Module
V_i = net([parents, noise]). Makes the SCM a neural causal model (NCM).
Source code in src/causalrl/scm/mechanisms.py
causalrl.scm.fit.NodeFit
¶
Bases: NamedTuple
What was fitted at one node, and what that licenses.
holdout_score evaluates the DEPLOYED (train-fitted) mechanism against data the fit never
saw -- mean log-likelihood for a discrete (non-invertible) mechanism, R^2 for a continuous
(invertible, additive-noise) one -- via :func:causalrl.scm.fitters.evaluate_holdout. It sits
on the same scale as each family's in-sample score, but is a genuine out-of-sample number:
an overfit deployed mechanism scores worse here even though its in-sample score looks fine.
pinned marks a node whose equation was SUPPLIED rather than learned (see
:class:causalrl.scm.fitters.PinnedMechanism). It changes how holdout_score should be
read: for a learned node the score measures how well a family fitted, and for a pinned one it
measures whether the equation the caller asserted survives contact with held-out data.
Source code in src/causalrl/scm/fit.py
causalrl.protocols.NoiseLedger
¶
Bases: Protocol
Explicit noise handling for Level-3 queries (§5.5, I6).
Either registered white-box draws (draws) or a posterior obtained by abduction
(posterior, Phase 1). No counterfactual API may silently assume access to noise.
Source code in src/causalrl/protocols.py
causalrl.protocols.NoisePosterior
¶
Bases: Protocol
A posterior over exogenous noise (the Phase-1 abduction output).
Source code in src/causalrl/protocols.py
causalrl.scm.fitters.PoissonGLMFit
¶
Count node: a Poisson GLM with a log link, fitted by IRLS.
log E[V | parents] = intercept + w · parents. Linear in the parents where a conditional
probability table is exponential in them, which is what makes lag-unrolled count data (6-12
lagged parents) tractable at all — and it keeps the point-process structure a table discards.
The mechanism is V = F⁻¹(U | parents) with U ~ Uniform(0, 1), the same canonical
construction :class:TabularCPT uses. A Poisson draw is not recoverable from
(parents, count), so invertible=False: counterfactuals at this node are genuinely
unidentified and the L3 guard correctly refuses them. log_prob is attached so
:func:evaluate_holdout scores this family by mean held-out log-likelihood rather than R².
Implements the standard IRLS/Fisher-scoring fit for a Poisson GLM (McCullagh & Nelder, Generalized Linear Models, 2nd ed., §2.5); no external code is ported.
Source code in src/causalrl/scm/fitters.py
646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 | |
causalrl.regime.Regime
dataclass
¶
A labeled data-generating configuration (§5.3).
Source code in src/causalrl/regime.py
__or__(other)
¶
Merge two regimes; raise on a shared parameter set to conflicting values.
Source code in src/causalrl/regime.py
to_domain(*, experiments=frozenset())
¶
Project to a transport :class:Domain (selection-marked variables carry over).
causalrl.protocols.SCMCausalEnv
¶
Conform a StructuralCausalModel (or any see/do model) to CausalEnvProtocol.
Duck-typed and torch-free: sample calls scm.see(n, seed=...); do calls
scm.do(interventions).see(n, ...), packing each into a TrajectoryLog (one entity
per unit, single step). The white-box noise_ledger (wrapping ExogenousPosterior)
arrives in Phase 1; it returns None here.
Source code in src/causalrl/protocols.py
causalrl.scm.fitters.TabularCPT
¶
Discrete node: a Laplace-smoothed conditional probability table, sampled by inverse CDF.
The mechanism is V = F^-1(U | parents) with U ~ Uniform(0, 1). That construction is
one of many couplings reproducing the same P(V | parents), and the data cannot
distinguish them — hence invertible=False, which makes counterfactuals at this node an
interval rather than a point.
Every parent is discretised by its distinct observed values, and the table holds one row
per parent configuration — the product of those cardinalities. So this family fits a discrete
node with discrete parents. A continuous parent contributes one level per observed value and
the product explodes: the commonest real-data shape, a binary treatment with two continuous
confounders at n=800, already asks for 409,600 rows. fit therefore raises ValueError
above :data:_MAX_CPT_ROWS rather than exhausting memory, or (where it fits) silently
becoming a nearest-neighbour memoriser of the training rows. Pass a continuous family for that
node instead — fit_scm(..., families={"A": ANMFit()}).
alpha is the Laplace pseudo-count and must be positive: at alpha=0 an unobserved parent
configuration would give an all-zero count row and every draw there would silently be the
smallest level.
Source code in src/causalrl/scm/fitters.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 | |
causalrl.data.trajectory.TrajectoryLog
¶
A columnar, long-format trajectory log (§5.4).
Source code in src/causalrl/data/trajectory.py
81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 | |
values_by_name(name)
¶
scan(batch_size)
¶
Iterate the log in row batches (streaming access; metadata carried on each batch).
Source code in src/causalrl/data/trajectory.py
pivot()
¶
Long -> wide: sorted (entity_id, episode_id, t) keys and one array per variable name.
Cells missing a value for a key are None (dense-case helper; last write wins).
Source code in src/causalrl/data/trajectory.py
fingerprint()
¶
A stable content hash over the columns and metadata (I8 data-fingerprint).
Source code in src/causalrl/data/trajectory.py
from_confounded_dataset(ds)
classmethod
¶
Lossless bridge in: one (entity_id=0, episode_id, t) row-group per transition.
Source code in src/causalrl/data/trajectory.py
to_confounded_dataset()
¶
Lossless bridge out. Requires n_states/n_actions in metadata.
Source code in src/causalrl/data/trajectory.py
sorted_by_key()
¶
Return a copy with rows sorted by (entity_id, episode_id, t) (stable within a key).
Key-contiguous ordering lets the streaming estimators join a decision's cells with an O(1) carry-over buffer instead of holding the whole log (plan §9).
Source code in src/causalrl/data/trajectory.py
iter_parquet_batches(path, batch_size)
classmethod
¶
Stream a Parquet log in row batches without materialising it (plan §9; [data] extra).
Yields one :class:TrajectoryLog per Arrow record batch; log-level metadata is not carried
on the per-batch logs (the streaming estimators consume named value columns, not metadata).
Source code in src/causalrl/data/trajectory.py
causalrl.data.dataset.Transition
dataclass
¶
A single logged transition. reward is the terminal return (0 except on done).
Source code in src/causalrl/data/dataset.py
causalrl.scm.continuous.abduction.abduct_invertible(mechanism, parent_values, observed, *, name='U')
¶
Exactly recover any invertible mechanism's exogenous noise from an observed outcome.
Works for the location-scale and conditional-flow families alike (anything implementing
:class:InvertibleMechanism); the recovered noise is a point mass, so counterfactuals built on
it may claim kind=IDENTIFIED.
Source code in src/causalrl/scm/continuous/abduction.py
causalrl.scm.continuous.abduction.abduct_location_scale(mechanism, parent_values, observed, *, name='U')
¶
Exactly recover a location-scale mechanism's noise (see :func:abduct_invertible).
Source code in src/causalrl/scm/continuous/abduction.py
causalrl.scm.continuous.nuts.abduct_nuts(forward, parent_values, observed, *, noise_scale=0.1, num_warmup=500, num_samples=1000, seed=0, name='U')
¶
Draw the posterior over a mechanism's exogenous noise by NUTS (NumPyro).
Model: a standard-normal prior u_i ~ N(0, 1) per unit and a Gaussian likelihood
observed_i ~ N(forward(parents, u)_i, noise_scale). forward must be written in
jax.numpy (elementwise over units). Returns a pure-NumPy :class:NUTSNoisePosterior.
Requires the [numpyro] extra.
Source code in src/causalrl/scm/continuous/nuts.py
causalrl.scm.unrolled.build_unrolled_scm(transition, horizon, *, state0_dist, latents, actions=None, process_noise_dist=None, state_prefix='state')
¶
Build a time-unrolled :class:StructuralCausalModel over horizon steps.
Parameters¶
transition:
Callable (state_t, action_t, latents, noise_t) -> state_{t+1}. Receives the current
state tensor (n, state_dim), the step's action constant, a {name: tensor} mapping
of the shared latent values, and the step's per-step exogenous tensor. Deterministic
transitions simply ignore noise_t.
horizon:
Number of transition steps T. The model has states {prefix}_0 .. {prefix}_T.
state0_dist:
Exogenous distribution for the initial state {prefix}_0. (For the deterministic
abduction pattern the actual start is pinned via abduct(known=...), but the node
still needs a declared exogenous distribution.)
latents:
Shared latent node(s), e.g. {"F": Bernoulli(0.5)}. Each is a parent of every
transition step and is therefore interveneable via do / pinnable via abduct.
actions:
Optional per-step action constants; actions[t] is passed to the transition that
produces {prefix}_{t+1}. Must have length horizon when given. None passes
None as every action.
process_noise_dist:
Optional per-step exogenous distribution attached to each produced state node. Defaults
to a point mass at zero (fully deterministic dynamics).
state_prefix:
Node-name prefix for the state chain (default "state").
Returns¶
StructuralCausalModel
With nodes {prefix}_0 .. {prefix}_T and one node per latent.
Source code in src/causalrl/scm/unrolled.py
51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 | |
causalrl.conformal.core.certify_conformal_interval(prediction, cal_y, cal_pred, *, alpha=0.1, weights=None, test_weight=None)
¶
A kind=EMPIRICAL certificate for a split-conformal prediction interval.
value is left None (the claim is coverage of an individual outcome, not a point
estimate); the interval is in ci and alpha is the miscoverage. The
(weighted-)exchangeability requirement is recorded as a non-checkable :class:Assumption.
The query is "see" and is not configurable: residuals of a fitted prediction carry no
intervention, and weights only move the observational law to a shifted one, so neither
an interventional nor a counterfactual label would be earned. For the off-policy claim, use
:func:conformal_action_value, which computes the propensity ratio and records the causal
assumptions that license it.
Source code in src/causalrl/conformal/core.py
causalrl.scm.continuous.abduction.certify_counterfactual(claim, ppc, *, exact, alpha=None)
¶
Wrap a counterfactual query in a certificate.
kind is IDENTIFIED for the exact-inversion path and EMPIRICAL for amortized VI (I2); the PPC
is recorded as a checkable assumption.
Source code in src/causalrl/scm/continuous/abduction.py
causalrl.scm.continuous.nuts.certify_nuts_counterfactual(claim, ppc, *, alpha=None)
¶
Wrap a NUTS-abducted counterfactual in an EMPIRICAL certificate (sampling evidence; I2).
The posterior-predictive check is recorded as a checkable assumption.
Source code in src/causalrl/scm/continuous/nuts.py
causalrl.conformal.core.conformal_quantile(scores, alpha=0.1, *, weights=None, test_weight=None)
¶
Conformal quantile of nonconformity scores at miscoverage alpha.
Returns the smallest score whose (weight-)normalised rank reaches 1 - alpha once the test
point's +inf mass is included, so prediction ± result (or the CQR adjustment) is
marginally valid at level 1 - alpha under (weighted) exchangeability. Returns inf (a
valid, infinite interval) when the calibration set is too small for the requested level.
weights are calibration likelihood ratios dP_test/dP_cal; test_weight defaults to
their mean.
Source code in src/causalrl/conformal/core.py
causalrl.conformal.core.cqr_interval(pred_lo, pred_hi, cal_y, cal_lo, cal_hi, alpha=0.1, *, weights=None, test_weight=None)
¶
Conformalized quantile regression interval (Romano, Patterson & Candes 2019).
cal_lo/cal_hi are the calibration lower/upper quantile predictions; the conformity score
is max(cal_lo - y, y - cal_hi) and the returned interval widens the test quantile band
[pred_lo, pred_hi] by the conformal quantile, giving adaptive finite-sample coverage.
Source code in src/causalrl/conformal/core.py
causalrl.scm.fit.fit_scm_mec(data, *, cpdag, max_members=32, **kwargs)
¶
Fit one SCM per DAG in cpdag's Markov equivalence class.
The returned list is the SCM belief: observational data cannot choose among these, so every
member fits it equally well. Sub-project 3 (active interventional discovery) shrinks this set
by intervening. Raises ValueError above max_members naming the true class size —
equivalence classes are exponential and silently truncating one would misreport the belief.
A second ValueError fires before enumerating when the CPDAG has too many undirected
edges: finding the class means trying all 2 ** k orientations of them, and that work is
exponential in k no matter how small the class turns out to be, so max_members cannot
bound it (see :data:_MAX_MEC_ENUMERATION). The pre-check can only name 2 ** k, an upper
bound on the class size rather than the size itself, and so over-refuses: the honest reading
is "this search is too big to run", not "this class is too big to fit".
Source code in src/causalrl/scm/fit.py
causalrl.data.dataset.generate_logs(env, n_episodes, seed)
¶
Roll out an env's confounded behavior_policy to build an offline dataset.
The env must expose n_states, n_actions, reset(seed=...), step(action),
and behavior_policy(observation). Rewards are recorded as the per-step reward; for
the finite-horizon envs here the terminal step carries the return.
Source code in src/causalrl/data/dataset.py
causalrl.conformal.core.split_conformal_interval(prediction, cal_y, cal_pred, alpha=0.1, *, weights=None, test_weight=None)
¶
Symmetric split-conformal interval prediction ± q from absolute calibration residuals.
Source code in src/causalrl/conformal/core.py
Agents, Environments & Interventions — Complete Reference¶
causalrl.agents.base.Agent
¶
Bases: ABC
Minimal agent interface: choose an action, then learn from the reward.
Implement this directly for an online learner, one whose policy moves in response to each
reward it sees. If learning happens in a batch step instead, subclass :class:BatchAgent.
Source code in src/causalrl/agents/base.py
observe_transition(state, action, next_state, done)
¶
Optional model-learning hook: observe a (s, a, s', done) transition.
Default no-op. Model-based agents (e.g. DOVI's value iteration) override this to build an empirical transition model; reward-only agents ignore it.
Source code in src/causalrl/agents/base.py
observe_step(observation, action, reward, next_observation, done)
¶
Observe one transition in the environment's own observation space.
This is the hook a driver should call. :meth:observe_transition takes state indices,
which forces whoever drives the loop to discretise before the agent sees anything -- and an
agent whose states are feature vectors then has no way to receive the transition at all.
Here the agent is handed the raw observations and decides for itself how to represent them.
The default is the tabular reading, so every existing agent and driver keeps its exact
behaviour: pull observation["state"] from each endpoint and forward to
:meth:observe_transition. A feature-space agent overrides this to encode instead.
Source code in src/causalrl/agents/base.py
causalrl.magames.AgentType
dataclass
¶
A template shared by a class of agents: an action set and a payoff over the joint profile.
payoff(own_action, others_actions, params) receives the other agents' actions in population
order (self excluded) and the instance's params; sharing one AgentType across instances
is the parameter sharing.
Source code in src/causalrl/magames/population.py
causalrl.agents.mbrl.BackdoorAdjustedAgent
¶
Bases: BatchAgent
Active deconfounded optimizer: pick the action with the highest back-door-adjusted value
E[Y | do(A=a)] = Σ_z P(z) · E[Y | A=a, Z=z], with the adjustment set read from the graph via
:func:~causalrl.backdoor_adjustment_set.
Unlike the certify-gated agent (whose ceiling is the behavior policy), this recovers the
interventional optimum from confounded logs, given an observed admissible adjustment set. It is
fitted on columnar data {treatment, outcome, *adjustment} (equal-length arrays).
:meth:fit keeps the per-stratum conditional means, so :meth:act is a policy over the
adjustment set — argmax_a Ehat[Y | do(a), Z=z] — not the one action that wins on average.
Source code in src/causalrl/agents/mbrl.py
fit(data)
¶
Estimate each action's back-door-adjusted value, cache the per-stratum action values
:meth:act conditions on, and record the marginal argmax.
Source code in src/causalrl/agents/mbrl.py
act(observation)
¶
Contextual policy argmax_a Ehat[Y | do(A=a), Z=z] at the adjustment values in
observation (supplied by name, e.g. {"Z": 1}).
Conditioning on the back-door set is what makes this a policy rather than a single action:
the arm that wins after averaging over Z need not win inside a stratum. With none of
the adjustment variables supplied — or an empty adjustment set, or a stratum never logged —
the marginal decision argmax_a E[Y | do(a)] is returned, which is the right call when
there is no context to condition on. A partial adjustment set raises.
Source code in src/causalrl/agents/mbrl.py
causalrl.agents.scbandit.BruteForceInterventionTS
¶
Bases: _ArmSubsetThompsonSampling
Thompson sampling over every arm (no pruning) — the unguided baseline.
Source code in src/causalrl/agents/scbandit.py
causalrl.magames.CCEPolytope
dataclass
¶
The CCE polytope of a (possibly intervened) finite game, in distribution-over-profiles form.
profiles are the joint actions (in agents order) consistent with the intervention;
deviation_gains[k, j] is the payoff gain of constraint_labels[k] = (agent, action)
switching unilaterally to action when the realized profile is profiles[j]. A
distribution mu over profiles is an epsilon-CCE iff deviation_gains @ mu is
componentwise at most epsilon.
Source code in src/causalrl/magames/cce.py
causalrl.envs.base.CausalEnv
¶
Bases: Env
Base class for environments backed by a StructuralCausalModel.
Subclasses define how SCM samples map to (observation, reward) and which node is the
action. The SCM is available as self.scm for see/do/counterfactual queries.
Source code in src/causalrl/envs/base.py
causalrl.agents.mbrl.CertifiedPolicyAgent
¶
Bases: BatchAgent
Ship the best deterministic policy whose improvement over behavior certifies robust to hidden confounding; abstain to the empirical behavior policy otherwise.
Setting alpha adds the finite-sample layer: a candidate must also clear
:func:causalrl.certify_policy's conformal lower-confidence-bound gate, i.e. its calibrated
worst-case return (:func:causalrl.conformal_action_value, weights = the propensity ratio
pi/pi_behavior) must be at least the behavior policy's. This is the safe-policy-improvement
reading of the agent: a candidate that improves the mean but degrades the downside, or that
has too little effective support to calibrate a bound at all, is refused. None (the
default) runs the confounding layer alone.
Source code in src/causalrl/agents/mbrl.py
causalrl.envs.suite.confounded_context.ConfoundedContextualBandit
¶
Bases: CausalEnv
A 2-context, 2-action bandit with a hidden confounder U on a backdoor A <- U -> Y.
Source code in src/causalrl/envs/suite/confounded_context.py
behavior_policy(observation)
¶
Confounded logging policy: play a = U with prob gamma, else uniform.
Source code in src/causalrl/envs/suite/confounded_context.py
true_policy_value(policy)
¶
Exact interventional value of a deterministic policy (marginalizing U).
Source code in src/causalrl/envs/suite/confounded_context.py
causalrl.envs.suite.gridworld.ConfoundedGridworld
¶
Bases: ConfoundedMDP
A size x size gridworld with a hidden confounder that biases the logging policy.
State = row * size + col (0-indexed), plus a terminal state = sizesize. Start at top-left (state 0), goal at bottom-right. Actions: 0=up, 1=down, 2=left, 3=right. Reaching the goal yields terminal return 1.0; hitting the per-episode 'hazard' cell (chosen by the hidden confounder U) yields 0.0 and ends the episode. Horizon = 2size. The behavior policy, knowing U, avoids the hazard — so the logs are confounded.
Source code in src/causalrl/envs/suite/gridworld.py
behavior_policy(observation)
¶
Knows the hazard U: biases toward down/right but avoids stepping onto the hazard.
Source code in src/causalrl/envs/suite/gridworld.py
causalrl.envs.base.ConfoundedMDP
¶
Bases: CausalEnv
Finite-horizon tabular MDP whose logging policy depends on an unobserved confounder.
Subclasses set n_states, n_actions, horizon, draw a fresh UC per episode in
reset, and implement step dynamics. behavior_policy is the UC-dependent logging
policy used to generate offline data; it may read the per-episode UC via self.
scm is set to None here because confounded MDPs are defined by their dynamics and
behavior policy rather than an explicit SCM; the attribute is kept for interface
compatibility with CausalEnv.
Source code in src/causalrl/envs/base.py
behavior_policy(observation)
abstractmethod
¶
causalrl.envs.suite.continuous_confounded.ContinuousConfoundedBandit
¶
One-decision bandit with a continuous observed confounder Z and a nonlinear arm-1 reward.
Z is the back-door confounder; the reward is continuous (Gaussian observation noise), so the
outcome model must be a function approximator rather than a per-stratum mean.
Source code in src/causalrl/envs/suite/continuous_confounded.py
sample(n, *, seed=None)
¶
Draw n confounded observations {"Z", "A", "Y"} (Z/Y continuous).
Source code in src/causalrl/envs/suite/continuous_confounded.py
true_action_value(action, *, n_mc=200000)
¶
Exact E[Y | do(A=action)] = E_z[E[Y | action, z]] over Z ~ Uniform(0, 1).
Source code in src/causalrl/envs/suite/continuous_confounded.py
causalrl.envs.suite.counterfactual_bandit.CounterfactualBanditEnv
¶
Bases: CausalEnv
One-step three-arm bandit. Observe intent intuition = I in {0,1,2}, play arm
X in {0,1,2}, receive reward Y (0.8 if X == U else 0.15). The
counterfactual-optimal policy plays X = I.
Source code in src/causalrl/envs/suite/counterfactual_bandit.py
causalrl.envs.suite.dtr.DTREnv
¶
Bases: ConfoundedMDP
Confounded (single-stage) dynamic treatment regime.
A context subtype Z ~ Bernoulli(0.5) is OBSERVED by the agent (the state). An
unobserved confounder U ~ Bernoulli(0.5) is hidden from the agent but drives the
logging policy's treatment choice. The agent picks a treatment a in {0, 1} and
receives a Bernoulli reward whose success probability depends on (Z, a, U).
state = 0 / 1 : observed subtype Z state = 2 : terminal Horizon 1 (one treatment decision per episode).
The reward table is chosen so that: - the do-optimal policy is a = Z (matched treatment), with average value 0.75; - the confounder U inflates the apparent reward of treatment 1, so a logging policy that follows U over-selects treatment 1. In the resulting confounded logs, treatment 1 looks best for BOTH subtypes, so a naive offline learner picks a = 1 everywhere and is WRONG for subtype Z = 0 (true value 0.55 vs the optimal 0.70) — average value 0.675.
This is the canonical causal-RL lesson: naively trusting confounded logs yields a confidently biased, suboptimal policy.
Source code in src/causalrl/envs/suite/dtr.py
optimal_value
property
¶
Average value of the do-optimal policy a*(Z) = argmax_a do_value(Z, a).
do_value(z, a)
¶
The interventional value E[R | do(a), Z=z], averaged over U ~ Bernoulli(0.5).
behavior_policy(observation)
¶
Confounded logging: the clinician observes the hidden comorbidity U and prescribes it with probability 0.85 (else the opposite), independent of the subtype Z. This ties the logged action to U, which also drives the reward — the source of confounding.
Source code in src/causalrl/envs/suite/dtr.py
causalrl.agents.deep_deconfounded.DeepDeconfoundedQ
¶
Bases: Agent
DQN-style agent whose bootstrap targets are clamped into the Manski causal bounds.
Function approximation can extrapolate Q outside the causally-valid envelope; clamping each (state, action) target into [lower, upper] keeps the learned values consistent with what the confounded offline data can support, while online experience tightens them. A small one-step bootstrap is used (sufficient for the toy demo); a full offline-RL backbone (d3rlpy) is the designated reuse path at real scale.
Source code in src/causalrl/agents/deep_deconfounded.py
causalrl.agents.mbrl.DiscoveryBackdoorAgent
¶
Bases: BatchAgent
Learns the structure: discovers the causal skeleton from data, orients it with the known
temporal tier order (covariates precede treatment precede outcome — standard in DTR/medicine),
takes the treatment's earlier-tier neighbours as the back-door set, then adjusts. The M1 upgrade
of the handed-the-graph :class:BackdoorAdjustedAgent. Fitted with :meth:discover_and_fit.
The discovered adjustment set is also the policy's context: :meth:act returns
argmax_a Ehat[Y | do(a), Z=z] over the variables discovery selected.
Source code in src/causalrl/agents/mbrl.py
discover_and_fit(data, *, tiers, threshold=0.01)
¶
Discover the skeleton, orient it by the temporal tiers (ordered groups, earliest
first), take the treatment's earlier-tier neighbours as the back-door set, and adjust.
Temporal tiering is standard domain knowledge in DTR / medicine and makes adjustment-set recovery reliable (pure interventional edge-orientation was not, on this graph).
Source code in src/causalrl/agents/mbrl.py
act(observation)
¶
Contextual policy argmax_a Ehat[Y | do(A=a), Z=z] over the discovered adjustment
set, at the values in observation (supplied by name, e.g. {"Z": 1}).
With none of the discovered variables supplied — or an empty discovered set, or a stratum
never logged — the marginal decision argmax_a E[Y | do(a)] is returned; a partial
context raises.
Source code in src/causalrl/agents/mbrl.py
causalrl.agents.scbandit.FixedSetThompsonSampling
¶
Bases: _ArmSubsetThompsonSampling
Thompson sampling restricted to a single fixed intervention set (the naive baseline).
Source code in src/causalrl/agents/scbandit.py
causalrl.agents.mbrl.FunctionApproxBackdoorAgent
¶
Bases: BatchAgent
Function-approximation back-door: fit qhat(a, z) by ridge regression on RBF features of a
CONTINUOUS confounder, then back-door-adjust by Monte-Carlo integrating qhat(a, .) over the
observed confounder sample, and ship the argmax. The M3 upgrade that carries the deconfounding
recipe past the tabular regime to a learned continuous estimator.
The single continuous confounder is read from graph via
:func:~causalrl.backdoor_adjustment_set. Fit on columnar
{confounder, treatment, outcome} via :meth:fit.
The fitted qhat(a, .) is kept, so :meth:act evaluates it at the observed z and returns
argmax_a qhat(a, z) — the same model the back-door integral averages, read per unit instead
of summed away. A thin reward bump can make an arm interventionally worse on average and still
optimal inside the bump; only a contextual policy can play it there.
Source code in src/causalrl/agents/mbrl.py
467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514 515 516 517 518 519 520 521 522 523 524 525 526 527 528 529 530 531 532 533 534 535 536 537 538 539 540 541 542 543 544 545 546 547 548 549 550 551 552 553 554 555 556 557 558 559 560 | |
fit(data)
¶
Fit the per-action RBF-ridge outcome model, cache it for :meth:act, and select the
back-door-adjusted (marginal) argmax.
Source code in src/causalrl/agents/mbrl.py
act(observation)
¶
Contextual policy argmax_a qhat(a, z) at the confounder value in observation
(supplied by name, e.g. {"Z": 0.85}).
This is the fitted outcome model read at one point rather than integrated over the sample,
so the arm an interventional average rejects can still be played where it wins. Without
the confounder in observation (or before :meth:fit) the marginal decision
argmax_a E[Y | do(a)] is returned.
Source code in src/causalrl/agents/mbrl.py
causalrl.agents.mbrl.GFormulaBackdoorAgent
¶
Bases: BatchAgent
Multivariate g-formula (standardization) back-door for many, mixed-type covariates.
Fits a per-action outcome model Ehat_a[Y | X] over ALL covariates X (a T-learner), then
standardizes E[Y | do(a)] = mean_i Ehat_a(X_i) and ships the argmax. This is the strategy
for the realistic confounding of real datasets, where per-stratum adjustment degenerates (each
stratum has ~1 row) and a single-confounder RBF is too narrow. The default outcome model is a
numpy ridge over [1, standardized X] (dependency-free); pass outcome_model — a factory
returning a fresh sklearn-style estimator (fit/predict) — for a flexible offline model.
:meth:fit keeps the per-action models, so :meth:act is the policy the T-learner already
implies: argmax_a Ehat_a(x), whose sign for a binary treatment is exactly the sign of
:meth:cate. That is the same CATE-to-policy conversion
:func:~causalrl.interop.econml.policy_from_econml_cate performs for a third-party estimator.
Source code in src/causalrl/agents/mbrl.py
607 608 609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628 629 630 631 632 633 634 635 636 637 638 639 640 641 642 643 644 645 646 647 648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677 678 679 680 681 682 683 684 685 686 687 688 689 690 691 692 693 694 695 696 697 698 699 700 701 702 703 704 705 706 707 708 709 710 711 712 713 714 715 716 | |
contrast
property
¶
E[Y|do(1)] - E[Y|do(0)] (the ATE) for a binary treatment.
fit(data)
¶
Fit the per-action outcome models, keep them for the contextual :meth:act, standardize
to E[Y|do(a)], and select the marginal argmax.
Source code in src/causalrl/agents/mbrl.py
cate(data)
¶
Per-unit CATE Ehat_1(X_i) - Ehat_0(X_i) for each row of data (the T-learner
individual-effect estimate; binary treatment). data carries the covariates plus the
observed treatment/outcome the per-action models are fit on.
Source code in src/causalrl/agents/mbrl.py
act(observation)
¶
Contextual policy argmax_a Ehat_a(x) at the covariates in observation (supplied
by name, e.g. {"age": 42, "smoke": 1}).
For a binary treatment this is exactly the sign of :meth:cate at that row — the per-unit
decision the T-learner already estimates, shipped instead of discarded. With none of
:attr:covariates in observation (or before :meth:fit) the marginal decision
argmax_a E[Y | do(a)] is returned; a partial covariate vector raises rather than
silently answering a different query.
Source code in src/causalrl/agents/mbrl.py
causalrl.intervention.Intervention = Mapping[str, Any]
module-attribute
¶
An assignment {variable: value} — the argument StructuralCausalModel.do accepts.
The empty mapping is the observational regime (do() on nothing), matching the frozenset()
that :func:causalrl.pomis emits when not intervening is possibly optimal.
causalrl.magames.KindNotLicensedError
¶
causalrl.magames.LearnerTopology
¶
Bases: Enum
How a population learns; caps the strongest Kind a certificate about it may claim.
Source code in src/causalrl/magames/population.py
causalrl.envs.suite.mabuc.MABUCEnv
¶
Bases: CausalEnv
One-step bandit. Each episode: observe intuition I, choose arm X in {0,1}, receive reward Y. The optimal policy plays X = I (the lucky arm).
Source code in src/causalrl/envs/suite/mabuc.py
causalrl.agents.baselines.NaiveOffline
¶
Bases: BatchAgent
Fits E[R|s,a] from the logs as if unconfounded and acts greedily. Provably biased under confounding — the cautionary baseline.
Source code in src/causalrl/agents/baselines.py
causalrl.agents.scbandit.NaivePOMISThompsonSampling
¶
Bases: _ArmSubsetThompsonSampling
Naive baseline that ignores manipulability: it computes the UNCONSTRAINED POMIS and keeps only the arms that happen to exist. When the optimal lever lies outside the unconstrained POMIS — as when a non-manipulable variable dominates — it cannot reach the optimum (r40 Prop. 1: filtering the unconstrained POMIS is insufficient).
Source code in src/causalrl/agents/scbandit.py
causalrl.agents.bandits.NaiveThompsonSampling
¶
Bases: Agent
Beta-Bernoulli Thompson sampling, one posterior per arm. Ignores intuition, so it cannot distinguish the arms (their interventional means are equal).
Sampling uses a per-instance numpy.random.Generator seeded from seed, so two
agents with different seeds draw independent action sequences and a fixed seed is
reproducible regardless of global RNG state.
Source code in src/causalrl/agents/bandits.py
causalrl.agents.online_causal_mbrl.OnlineCausalMBRL
¶
An agent that learns its SCM from its own interventions plus an off-policy log.
variables are the columns every buffered row must carry; treatment is the node the
agent intervenes on, outcome the node it is scored by, and actions the values it may
set the treatment to. Data arrives through :meth:ingest (bulk, off-policy) or :meth:observe
(one transition, on-policy), and :meth:refit turns the buffers into the belief:
.. code-block:: text
discover_interventional(observational, interventional, variables) -> CPDAG (the I-MEC)
fit_scm_mec(all rows, cpdag=..., max_members=...) -> one fitted SCM per member
:meth:act turns the belief into an action from actions -- policy decides how the
members' verdicts are combined and n_rollout how many samples each verdict costs -- and
:meth:probe returns the intervention target the members disagree about most. The loop those
two are meant for branches on :meth:structure_uncertain::
target = agent.probe() if agent.structure_uncertain() else None
action = agent.act() if target is None else None
refit_every is the cadence that loop should call :meth:refit at -- rerunning PC every
step is wasteful and makes the belief flicker on sampling noise -- and is not self-triggering.
max_members is handed to :func:~causalrl.scm.fit.fit_scm_mec, whose refusal above the cap
propagates out of :meth:refit deliberately: an equivalence class too large to enumerate is
information the caller needs, and a silently truncated belief is a misreported one.
Source code in src/causalrl/agents/online_causal_mbrl.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 462 463 464 465 466 467 468 469 470 471 472 473 474 475 476 477 478 479 480 481 482 483 484 485 486 487 488 489 490 491 492 493 494 | |
cpdag
property
¶
The I-essential graph from the last :meth:refit; None before the first.
steps
property
¶
Transitions buffered so far, observational and interventional together.
ingest(data, *, source='observational', target=None)
¶
Append a bulk log to the observational buffer, or to target's do-buffer.
source="interventional" requires target (the node that was set) and
source="observational" forbids it. Every variable must be present with equal lengths:
:func:~causalrl.discovery.discover_interventional compares the marginal of every
endpoint between regimes, so a do-sample holding only the reward cannot orient anything.
Source code in src/causalrl/agents/online_causal_mbrl.py
observe(row, *, intervention=None)
¶
Append one transition -- to the observational buffer, or to its target's do-buffer.
With intervention, the row joins the buffer for that target and the do-value is what is
recorded for it: under a perfect intervention the target was set, so row may omit it.
Exactly one target is supported per row, because
:func:~causalrl.discovery.discover_interventional orients per single target.
Source code in src/causalrl/agents/online_causal_mbrl.py
refit()
¶
Recompute the I-MEC from the buffers and fit one SCM per member; return the belief.
Two roles for the same rows, and they are not interchangeable. Orientation comes from
the do-data: :func:~causalrl.discovery.discover_interventional runs PC on the
observational sample for the skeleton and then orients the edges incident to each target by
the invariance principle, which only the interventional marginals can decide.
Mechanisms are fitted from every row the agent holds, observational and interventional
concatenated, because a mechanism is invariant across regimes (Peters et al. 2016) and the
do-rows are extra samples of every node that was not intervened on. The intervened node's
own mechanism is fitted from rows where it was overridden, which biases its marginal --
harmless here, since planning replaces that mechanism with do(treatment=a) anyway.
A target holding fewer than min_interventional_samples rows is dropped rather than
passed on. The invariance test compares empirical marginals, so a small do-sample clears
any shift threshold by chance and an empty one clears it unconditionally — orienting every
edge incident to that target from evidence the agent does not have.
:func:~causalrl.discovery.discover_interventional refuses such a sample outright; the
agent filters first so that an early round, when a target has been probed once or twice, is
an ordinary under-informed refit rather than a raise.
Raises ValueError if no observational rows have been buffered (PC needs them for the
base CPDAG), and lets :func:~causalrl.scm.fit.fit_scm_mec's over-size refusal propagate.
Source code in src/causalrl/agents/online_causal_mbrl.py
act()
¶
Choose an action from actions by planning inside the belief.
Every policy scores an action the same way -- member.do({treatment: action}), then
see(n_rollout) and the mean of outcome -- and they differ only in how the members'
verdicts are combined:
"thompson"draws one member and returns its argmax. Thompson sampling over structure (Ortega & Braun,arXiv:1303.4431 <https://arxiv.org/abs/1303.4431>_): the spread of the belief supplies the exploration, so the policy sharpens exactly as the I-MEC collapses and needs no separate schedule."average"takes the mean value across members -- marginalising the structure out."robust"takes the maximin: score each action by its worst member, then take the best of those. The one to reach for when a wrong action is expensive, since it is the only one of the three that never bets on a particular member being the true DAG.
Deterministic given seed. The thompson draw comes from an RNG derived from seed,
the transition count and the number of draws already taken, so a fixed seed replays the
whole decision sequence, successive calls draw successive members rather than repeating
one, and two seeds diverge. Rollouts are seeded per member, which also hands every action
of a member the same exogenous draws (common random numbers), so the gap between two
actions is not partly the gap between two samples. Ties go to the earliest action in
actions.
This costs rollouts: n_rollout samples per action per member scored, so
len(actions) * n_rollout under "thompson" and
len(actions) * belief_size() * n_rollout under "average" and "robust", every
call. That is what planning in a model costs instead of reading a value table.
Raises ValueError before the first :meth:refit, when there is no belief to plan in.
Source code in src/causalrl/agents/online_causal_mbrl.py
probe()
¶
Return the intervention target the belief's members disagree about most.
A target, not an action: the answer to "what experiment should I run next", which is
the other half of the loop :meth:act serves. Each candidate is scored by the mean
pairwise total variation between the members' outcome distributions under do(candidate)
-- predictive disagreement standing in for expected information gain, as in Scherrer et
al. (arXiv:2109.02429 <https://arxiv.org/abs/2109.02429>_) and Zhang et al. (Nature
Machine Intelligence 2023). It concentrates experiments on the edges the I-MEC still leaves
unoriented, because those are the only ones whose do() distribution the members can
disagree about; a target every member already predicts identically scores zero and is
excluded, and if every candidate scores zero this raises rather than returning an
arbitrary one.
Each candidate is intervened at one representative value: the largest it takes anywhere in the agent's buffers. That keeps the do-value inside the observed support, so no fitted mechanism is asked to extrapolate, while sitting as far as the data allows from the bottom of the range -- a do-value near the middle of a variable's spread perturbs its descendants least and makes members that disagree look alike. One value cannot certify agreement, though: a zero score means "no disagreement detected at this value", not "these members agree everywhere".
This costs rollouts: n_rollout samples per candidate per member, i.e.
len(variables) * belief_size() * n_rollout every call. All members share one exogenous
stream per candidate (common random numbers), because the score is a difference between
members and independent draws would put a finite-sample floor under it.
Raises ValueError before the first :meth:refit, on a single-member belief (nothing to
disambiguate -- :meth:structure_uncertain is the guard to branch on), and when no
candidate scores any disagreement at all.
Source code in src/causalrl/agents/online_causal_mbrl.py
belief()
¶
belief_size()
¶
structure_uncertain()
¶
Whether more than one I-MEC member survives -- a decision may need the certificate layer.
Raises ValueError before the first :meth:refit rather than answering. With an empty
belief neither answer is honest: there is nothing to be uncertain between yet, and
nothing that says the structure is known either. This is the guard the documented loop
branches on -- :meth:probe while it is True, :meth:act once it is False -- so
reporting False from an empty belief would quietly route a caller who never fitted
anything into the exploit branch.
Source code in src/causalrl/agents/online_causal_mbrl.py
history()
¶
An immutable snapshot of the belief trajectory, one :class:RefitRecord per refit.
causalrl.agents.baselines.OnlineOnlyUCB
¶
Bases: Agent
UCB1 per state, ignoring any offline data. The 'learn from scratch online' baseline.
Source code in src/causalrl/agents/baselines.py
causalrl.envs.suite.seq_dtr.SequentialDTREnv
¶
Bases: ConfoundedMDP
Multi-stage confounded dynamic treatment regime — genuinely confounded AND sequential.
Per episode a persistent hidden comorbidity U ~ Bernoulli(0.5) is drawn. At each of
H stages the agent observes a subtype Z (the state, encoded stage*2 + Z so the
stage is baked into the index; terminal = 2H), picks a treatment a in {0,1} and
receives reward ~ Bernoulli(p(Z, a, U)). The subtype then transitions deterministically
by Z' = a — a function of the chosen action and observed state only, NOT of U — so
the transition dynamics are unconfounded and DOVI's value-iteration bound stays certified.
The reward table is tuned so the immediately-greedy action differs from the lookahead-
optimal one (a genuine foresight gap), and U adds +0.2 to every cell when U = 1
while the logging clinician plays a = U (prob 0.85): so naive-offline over-prescribes
treatment 1 in the logs AND, being myopic, also misorders the sequential trade-off.
Source code in src/causalrl/envs/suite/seq_dtr.py
optimal_value
property
¶
Expected return of the do-optimal policy from a random initial subtype Z.
dv(z, a)
¶
Per-stage interventional value E[R | do(a), Z=z], averaged over U ~ Bernoulli(0.5).
do_value(z, a, stage)
¶
Interventional value of taking a at stage in subtype z, then optimal.
Source code in src/causalrl/envs/suite/seq_dtr.py
behavior_policy(observation)
¶
Confounded logging: the clinician follows the hidden comorbidity U (plays a = U with prob 0.85), tying the logged action to U, which also drives the reward.
Source code in src/causalrl/envs/suite/seq_dtr.py
causalrl.envs.suite.seq_mabuc.SequentialMABUCEnv
¶
Bases: ConfoundedMDP
Horizon-H contextual bandit with an unobserved confounder (a genuine MABUC).
Each step is an independent confounded contextual-bandit decision: an observed context
C ~ Bernoulli(0.5) (the state, stage*2 + C; terminal = 2H) and a HIDDEN
U ~ Bernoulli(0.5) are drawn, then reward ~ Bernoulli(p(C, a, U)). The logging
policy follows the hidden U (plays a = U with prob 0.85), so conditional on the
observed context the logged action is tied to U — which also drives the reward.
Naive-offline therefore over-prescribes treatment 1 in every context (its apparent reward
is inflated by U), exactly as in DTREnv, but here repeated across the horizon.
There are no state transitions: this is the bandit member of the suite. The
sequential-credit-assignment lesson lives in SequentialDTREnv.
Source code in src/causalrl/envs/suite/seq_mabuc.py
causalrl.envs.suite.simpson_bandit.SimpsonBandit
¶
Single-decision bandit with an observed confounder Z on the back-door path A <- Z -> Y.
Source code in src/causalrl/envs/suite/simpson_bandit.py
sample(n, *, seed=None)
¶
Draw n confounded observations as columnar arrays {"Z", "A", "Y"}.
sample_do(do, n, *, seed=None)
¶
Draw n samples under the mutilated model do ({} = observational).
Supports interventions on Z and/or A; unset variables follow their mechanism, so
interventional causal discovery can orient the graph by the invariance principle.
Source code in src/causalrl/envs/suite/simpson_bandit.py
true_action_value(action)
¶
Exact interventional value E[Y | do(A = action)] = Σ_z P(z) · P(Y=1 | action, z).
causalrl.envs.suite.scbandit.StructuralCausalBanditEnv
¶
Bases: CausalEnv
One-step bandit whose arms are interventions on an SCM. step returns a single
stochastic reward draw under do(arm) (not its expected value); arm_values and
optimal_value — Monte-Carlo estimates computed once at construction — are the ground
truth for regret. graph is the abstracted ADMG given to agents; arms and
manipulable support POMIS arm pruning.
Source code in src/causalrl/envs/suite/scbandit.py
causalrl.agents.mbrl.TransportBackdoorAgent
¶
Bases: BatchAgent
Deconfound + transport: back-door-adjust for the confounders (source weighting) and reweight
the selection variables by the target distribution, then ship the argmax of the transported
interventional value E_target[Y | do(A=a)]. The M2 upgrade of :class:BackdoorAdjustedAgent
that carries a policy across a covariate shift where a correlational agent cannot.
The back-door set comes from graph via :func:~causalrl.backdoor_adjustment_set (minus the
declared transport variables); the estimand's transportability is confirmed up front with
:func:~causalrl.is_transportable_effect (dogfooding the identification layer). Fitted on
columnar source logs plus unlabeled target draws of the transport variables via :meth:fit.
:meth:act is a policy over adjustment + transport: the transported marginal can say
"never play arm 1" while arm 1 still wins in a particular cell.
Source code in src/causalrl/agents/mbrl.py
307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 | |
fit(source, *, target_covariates)
¶
Estimate each action's transported interventional value, cache the per-cell action
values :meth:act conditions on, and record the marginal argmax.
source is columnar {treatment, outcome, *adjustment, *transport};
target_covariates supplies unlabeled target draws of the transport variables.
Source code in src/causalrl/agents/mbrl.py
act(observation)
¶
Contextual policy argmax_a E_target[Y | do(A=a), Z=z, W=w] at the adjustment and
transport values in observation (supplied by name, e.g. {"Z": 1, "W": 0}).
The target reweighting that :meth:fit applies is a statement about P(W), not about the
outcome mechanism: under the S-admissibility this agent checks up front,
E_target[Y | do(a), z, w] = E_source[Y | a, z, w], so the per-unit decision carries
across the shift with no reweighting at all — only the marginal needs one. With none of the
conditioning variables supplied, or a cell never logged in the source, the transported
marginal decision is returned; a partial context raises.
Source code in src/causalrl/agents/mbrl.py
causalrl.envs.suite.transport_bandit.TransportableConfoundedBandit
¶
One-decision bandit with an orthogonal confounder Z and shift variable W.
Graph Z -> A, Z -> Y, W -> Y, A -> Y. Z is the back-door confounder; W is the
selection variable whose distribution differs between source and target.
Source code in src/causalrl/envs/suite/transport_bandit.py
sample(n, *, domain='source', seed=None)
¶
Draw n confounded observations {"Z", "W", "A", "Y"} from domain.
domain is "source" (log-collection) or "target" (deployment); they differ only
in P(W=1).
Source code in src/causalrl/envs/suite/transport_bandit.py
true_action_value(action, *, domain='target')
¶
Exact E[Y | do(A=action)] = Σ_{z,w} P(z) P(w) E[Y | action, z, w] in domain.
Source code in src/causalrl/envs/suite/transport_bandit.py
causalrl.agents.offline_online.UCDTR
¶
Bases: Agent
Causal offline-to-online learner (Zhang & Bareinboim 2019).
Offline: prune each state's action set to the non-dominated actions implied by the Manski causal bounds (a safety mechanism; with natural bounds it is a no-op). Online: run UCB1 over only the surviving actions. Unlike NaiveOffline, it never trusts the confounded offline point estimates, so it corrects to the true optimum online.
Source code in src/causalrl/agents/offline_online.py
causalrl.magames.cce_bounds(game, functional, *, do=None, epsilon=0.0)
¶
Min/max of functional's expectation over the epsilon-CCE polytope under do.
functional maps a joint profile ({agent: action}) to a real value; epsilon is a
uniform or per-agent relaxation of the no-deviation constraints — pass a measured realized
regret (:func:cce_regret) for the finite-time form.
Source code in src/causalrl/magames/cce.py
causalrl.magames.cce_polytope(game, *, do=None)
¶
Deviation-gain representation of the CCE polytope of game under intervention do.
do pins the given agents' actions: profiles are restricted to the consistent ones and only
the free agents contribute no-deviation constraints (the intervened game).
Source code in src/causalrl/magames/cce.py
causalrl.magames.cce_regret(game, weights, *, do=None)
¶
Maximal deviation gain of a joint distribution over profiles: the measured realized regret.
weights is either a mapping from joint-action tuples (in game.agents order) to
probabilities or a sequence aligned with cce_polytope(game, do=do).profiles. The result is
the smallest epsilon for which weights is epsilon-CCE-feasible (0 for an exact CCE);
it is what a no-regret population drives to 0 and what :func:certify_cce_do accepts as the
measured epsilon.
Source code in src/causalrl/magames/cce.py
causalrl.magames.certify_cce_do(game, functional, *, do=None, no_regret=True, epsilon=None, payoff_error=None, tol=1e-09, seed=0)
¶
Certify what functional's time average can be for a learning population under do.
no_regret asserts the population's time-averaged play approaches the exact CCE set (the
asymptotic route); epsilon instead supplies a measured realized regret at the horizon
actually run (the finite-time route, which needs no assumption). See the module docstring for
the resulting IDENTIFIED/BOUNDED/EMPIRICAL ladder.
payoff_error says the game's payoff table was measured rather than given — from a
round-robin of simulated matches, say (:class:~causalrl.magames.empirical.EmpiricalGame
produces one from its own standard errors). It never narrows a claim's arithmetic and it never
widens value, which stays the partial-identification region of the table as handed over;
it fills ci with the interval that also survives the table's error at level alpha,
judges vacuity on that, and withholds IDENTIFIED — a functional constant over an
estimated polytope is constant in the estimate, which is not the same claim.
Source code in src/causalrl/magames/cce.py
278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430 431 432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457 458 459 460 461 | |
causalrl.magames.topology_max_kind(topology)
¶
The strongest epistemic :class:~causalrl.certify.certificate.Kind topology licenses.
Estimation, Bounds & Certificates — Complete Reference¶
causalrl.certify.Assumption
dataclass
¶
A consumed assumption, optionally with a falsification/diagnostic result.
Source code in src/causalrl/certify/certificate.py
causalrl.estimate.estimators.EffectEstimate
dataclass
¶
A point estimate of an ATE with an (asymptotic) confidence interval and overlap diagnostics.
overlap records min_propensity / max_propensity of the estimated propensity score
(nan for the propensity-free plugin estimator); a certifier downgrades to a hedge when
positivity is destroyed (I3). std_error/ci are influence-function based for aipw and
dml (the estimators with a valid asymptotic distribution); for plugin they are a coarse
outcome-spread approximation and for ipw a self-normalised-weight approximation.
Source code in src/causalrl/estimate/estimators.py
causalrl.certify.EstimandSpec
dataclass
¶
What a certificate is about.
Named EstimandSpec to avoid colliding with the shipped identification
:class:causalrl.identification.id_algorithm.Estimand. target is the functional
(mean/quantile/tail); only mean is used before Phase 1.
Source code in src/causalrl/certify/certificate.py
causalrl.certify.Hedge
dataclass
¶
Why a claim was refused or weakened (subsumes the shipped act/abstain verdict; I3).
Source code in src/causalrl/certify/certificate.py
causalrl.certify.Kind
¶
Bases: Enum
Epistemic status of a certificate's claim (I2). Never conflate the three.
Source code in src/causalrl/certify/certificate.py
causalrl.certify.Provenance
dataclass
¶
Reproducibility record (I8): library version, seeds, data/graph fingerprints, timestamp.
Source code in src/causalrl/certify/certificate.py
create(*, seeds=(), data_fingerprint=None, graph_hash=None)
staticmethod
¶
Fill library_version and a UTC timestamp automatically.
Source code in src/causalrl/certify/certificate.py
causalrl.ope.sequential.SequentialValueEstimate
dataclass
¶
A finite-horizon policy-value estimate with an (influence-function) CI and per-stage overlap.
overlap_by_step[t] records min_propensity / max_propensity of the stage-t
estimated propensity (nan for the propensity-free gcomp estimator); a certifier
downgrades to a hedge when any stage's positivity is destroyed (I3). std_error / ci are
influence-function based for dr (cross-fitted) and a coarse pseudo-outcome-spread
approximation for gcomp.
Source code in src/causalrl/ope/sequential.py
causalrl.certify.Witness
dataclass
¶
Why an identified/bounded claim holds: adjustment set, transport formula, ID trace, ...
Source code in src/causalrl/certify/certificate.py
causalrl.bounds.continuous.certify_sensitivity_bounds(outcomes, *, gamma, propensities=None, treatment=None, covariates=None, propensity_model=None)
¶
A kind=BOUNDED certificate wrapping :func:msm_sensitivity_bounds.
Source code in src/causalrl/bounds/continuous.py
causalrl.ope.sequential.certify_sequential_value(histories, actions, target_actions, reward, *, method='dr', alpha=0.05, n_folds=5, seed=0, outcome_model=None, propensity_model=None, overlap_eps=0.01, clip=0.001, policy='pi')
¶
Certify a deterministic policy's finite-horizon value under sequential ignorability.
Returns a kind=IDENTIFIED :class:Certificate with the point value (value), its
influence-function confidence interval (ci), a per-stage sequential-ignorability
witness, and provenance — or a hedged certificate when any stage's estimated positivity falls
below overlap_eps (I3). Sequential ignorability is a non-checkable assumption the caller
asserts (identification from a graph is out of scope); it is recorded explicitly, never assumed
silently.
Source code in src/causalrl/ope/sequential.py
327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391 392 393 394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409 410 411 412 413 414 415 416 417 | |
causalrl.eval.metrics.cumulative_regret(rewards, optimal_per_step)
¶
Total regret = sum over steps of (optimal expected reward - realized reward).
causalrl.estimate.estimators.estimate_ate(data, treatment, outcome, adjustment, *, method='dml', alpha=0.05, n_folds=5, seed=0, outcome_model=None, propensity_model=None, clip=0.001)
¶
Estimate the back-door ATE of binary treatment on outcome given adjustment.
data maps variable name -> 1-D array (covariates may be name -> (n,) or (n, k)).
method selects the estimator (plugin/ipw/aipw/dml). outcome_model and
propensity_model are zero-argument factories returning fresh sklearn-style learners
(defaults: ridge outcome, logistic propensity). Raises ValueError for a non-binary treatment
or a missing adjustment variable.
Source code in src/causalrl/estimate/estimators.py
causalrl.ope.sequential.estimate_sequential_value(histories, actions, target_actions, reward, *, method='dr', alpha=0.05, n_folds=5, seed=0, outcome_model=None, propensity_model=None, clip=0.001)
¶
Estimate a deterministic target policy's finite-horizon value under sequential ignorability.
histories[t] is the stage-t adjustment history H_t (shape (n,) or (n, d_t));
actions[t] / target_actions[t] are binary (n,) observed / target actions.
method is "gcomp" (g-computation) or "dr" (cross-fitted sequentially doubly-robust).
Nuisances are pluggable sklearn-style factories (defaults: ridge outcome, logistic propensity).
Source code in src/causalrl/ope/sequential.py
causalrl.eval.metrics.finite_horizon_regret(returns, optimal_return)
¶
Total regret over episodes = sum of (optimal episode return - realized return).
causalrl.certify.identify_effect_certified(graph, treatment, outcome)
¶
identify_effect as an IDENTIFIED certificate; raises NotIdentifiableError if not.
The witness carries the do-free identification formula. Identification is symbolic, so value
is None (there is no numeric estimate to report — estimation is Phase 1).
Source code in src/causalrl/certify/routines.py
causalrl.certify.ipw_sensitivity_bounds_certified(outcomes, propensities, *, gamma)
¶
ipw_sensitivity_bounds as a BOUNDED certificate (Tan MSM on E[Y(1)]).
Source code in src/causalrl/certify/routines.py
causalrl.ope.ipw.ipw_value(actions, rewards, behavior_probs, target_probs)
¶
Inverse-propensity-weighted off-policy value estimate.
Samples with zero behavior propensity (b == 0) contribute 0 to the sum but are
still counted in the n denominator, which biases the estimate toward 0 — pass only
samples with positive behavior propensity for an unbiased estimate.
Source code in src/causalrl/ope/ipw.py
causalrl.certify.msm_policy_value_bounds_certified(outcomes, logging_propensities, target_propensities, *, gamma)
¶
msm_policy_value_bounds as a BOUNDED certificate (off-policy value under Tan MSM).
Source code in src/causalrl/certify/routines.py
causalrl.eval.benchmark.report_to_dict(report)
¶
Convert a report into JSON-serializable dictionaries.
causalrl.eval.harness.run_episodes(agent, env, n_episodes, seed)
¶
Run an agent on a finite-horizon env for n_episodes, returning per-episode returns.
The agent acts each step, is updated with the step reward, and is shown the transition via
observe_step -- the representation-neutral hook, so an agent whose states are feature
vectors can be driven by this harness too; the tabular default forwards to
observe_transition unchanged. The per-episode return is the sum of step rewards
(terminal-reward envs put it all on the last step).
Source code in src/causalrl/eval/harness.py
causalrl.eval.mbrl_probe.run_m0_kill_gate(*, seeds=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), n=5000)
¶
Back-door-adjusted vs naive-marginal on the Simpson bandit; report interventional values.
Keys: causal (back-door-adjusted agent), naive (marginal E[Y|A] agent), optimal
(oracle). GO iff causal.mean > naive.mean.
Source code in src/causalrl/eval/mbrl_probe.py
causalrl.eval.mbrl_probe.run_m1_discovery_gate(*, seeds=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), n=5000)
¶
M1: an agent that learns the structure (interventional discovery) then adjusts, vs naive.
Keys: discovery (discover -> back-door-adjust), naive (marginal), optimal (oracle).
GO iff discovery.mean > naive.mean (the recovered adjustment set is checked in the tests).
Source code in src/causalrl/eval/mbrl_probe.py
causalrl.eval.mbrl_probe.run_m1b_dtr_gate(*, seeds=(0, 1, 2, 3, 4), horizon=2, n_episodes=8000)
¶
M1b: deconfounded sequential planning vs naive on the confounded medicine DTR.
On SequentialDTREnv (a hidden comorbidity U, a confounded clinician who plays a = U, and
a foresight gap), the deconfounded value-iteration agent DOVI (an existing library agent) is
trained on confounded logs and rolled out; NaiveOffline is the confounded baseline. Keys:
causal (DOVI late-window mean return), naive, optimal. GO iff causal > naive.
Source code in src/causalrl/eval/mbrl_probe.py
causalrl.eval.mbrl_probe.run_m2_phase_diagram(*, gammas=(0.0, 0.25, 0.5, 0.75, 1.0), shifts=(0.0, 0.25, 0.5, 0.75, 1.0), seeds=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), n=6000, mono_tol=0.02)
¶
M2: 2-D phase diagram of the causal-minus-naive post-shift gap over gamma x shift.
Per cell, a :class:~causalrl.agents.mbrl.TransportBackdoorAgent (deconfound + transport) and
a NaiveOffline marginal agent are fit on SOURCE logs and evaluated at the TARGET; the gap is
causal_target - naive_target. Returns per-cell :class:BenchmarkEstimates keyed by
(gamma, shift) plus the mean-gap grid and whether it is monotone nondecreasing (within
mono_tol) in each axis -- the "confounding bites where theory predicts" signature.
Source code in src/causalrl/eval/mbrl_probe.py
causalrl.eval.mbrl_probe.run_m3_function_approx_gate(*, seeds=(0, 1, 2, 3, 4, 5, 6, 7, 8, 9), gamma=1.0, n=6000)
¶
M3: a continuous-confounder function-approximation agent vs naive on a nonlinear reward.
On ContinuousConfoundedBandit (continuous Z ~ Uniform, a nonlinear arm-1 bump, and
behavior that over-samples arm 1 near the bump), a FunctionApproxBackdoorAgent (ridge
on RBF features + back-door integration) recovers the true low value of arm 1 and keeps the safe
arm 0; the NaiveOffline marginal is fooled into arm 1. Keys: causal, naive,
optimal. GO iff causal.mean > naive.mean.
Source code in src/causalrl/eval/mbrl_probe.py
causalrl.ope.sequential.sequential_ice_values(histories, actions, target_actions, reward, *, outcome_model=None)
¶
Per-unit iterated-conditional-expectation values q_1(H_1^i) under the target policy.
The g-computation backbone shared by :func:estimate_sequential_value (gcomp) and the
identified sequential-transport subcase: the per-unit conditional policy value given the
baseline history, before averaging — so a downstream caller may re-average it over a
different (e.g. transported) baseline distribution.
Source code in src/causalrl/ope/sequential.py
Interop, Scale & Errors — Complete Reference¶
causalrl.interop.sbi_numpyro.PosteriorRegimeSampler
¶
A reusable sampler over posterior regimes (a calibrated-configuration ensemble).
Holds the posterior matrix once; :meth:regimes materialises all regimes, :meth:sample draws
a bootstrap subset, and :meth:mean_regime returns the single posterior-mean configuration.
Source code in src/causalrl/interop/sbi_numpyro.py
regimes()
¶
sample(n, *, seed=None)
¶
Draw n regimes uniformly with replacement (a bootstrap over the posterior).
Source code in src/causalrl/interop/sbi_numpyro.py
mean_regime(*, name='posterior-mean')
¶
The single regime at the posterior mean of every parameter.
Source code in src/causalrl/interop/sbi_numpyro.py
causalrl.interop.columnar_sim.check_conformance(sim, *, n=16, seed=0)
¶
Validate that sim emits a well-formed TrajectoryLog; return diagnostics or raise.
Calls sim.sample(n) and checks it returns a :class:TrajectoryLog, then probes do (a
do-nothing intervention) to record whether interventions are supported. Returns the observed row
count, the distinct value names, and whether do / a noise ledger are available — a quick
conformance report for a new simulator integration.
Source code in src/causalrl/interop/columnar_sim.py
causalrl.interop.causal_gym.from_causal_gym(env, behavior_policy, *, n_episodes=1000, max_steps=100, seed=0, n_states=None, n_actions=None, state_fn=None)
¶
Roll env out under behavior_policy into a ConfoundedTrajectoryDataset.
behavior_policy(observation) -> action chooses the discrete action; state_fn maps
an observation to a discrete state index (defaults to an int observation or obs['state']).
n_states / n_actions are taken from the env's attributes when present, else from these
arguments, else inferred from the logged data. Gymnasium
reset(seed=...) -> (obs, info) and step(action) -> (obs, reward, terminated, truncated,
info) are assumed (a legacy 4-tuple step is also tolerated).
Source code in src/causalrl/interop/causal_gym.py
causalrl.interop.pettingzoo.pettingzoo_to_trajectory_log(env, policies, *, n_episodes=1, max_steps=100, seed=0)
¶
Roll env out under policies and log every agent-step to a TrajectoryLog.
policies[agent](observation) -> action drives each active agent. Each agent-step logs the
observation columns, the chosen action, and the reward, keyed by entity_id (the
agent's index), episode_id, and t. Raises KeyError if an active agent has no policy.
Source code in src/causalrl/interop/pettingzoo.py
causalrl.scm.bounded_fit.fit_scm_bounded(data, *, graph, value_ranges=None, families=None, holdout=0.2, seed=0)
¶
Fit graph's identified mechanisms and bound the confounded ones.
The confounded-graph counterpart of :func:causalrl.fit_scm, which refuses this input. A node
is bounded exactly when it shares a bidirected edge with one of its own parents; every other
node is fitted point-wise by the same machinery, with the same families overrides and the
same held-out scoring.
value_ranges gives (low, high) for each bounded node and is REQUIRED for them: a Manski
bound is built out of the range the unobserved fraction could occupy, so a missing range is a
missing bound rather than a default. Ranges for identified nodes are ignored.
Bounded nodes' confounded parents must be discrete with few levels -- their joint configuration indexes the arms of the bound.
Source code in src/causalrl/scm/bounded_fit.py
237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 | |
causalrl.scm.bounded_fit.BoundedSCMFit
¶
A fit over a confounded graph: point mechanisms where identified, intervals elsewhere.
Query it with :meth:interval. An identified node returns a degenerate interval (the point
estimate, twice over) so that a caller can treat every node uniformly without first asking
which kind it is -- and a caller that does care can ask :meth:is_identified.
Source code in src/causalrl/scm/bounded_fit.py
83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 | |
nodes
property
¶
Per-node results, in the graph's topological order.
bounded_nodes
property
¶
Nodes whose mechanism is not identified, and so is reported as an interval.
identified_nodes
property
¶
Nodes whose mechanism the data identify point-wise.
point_model
property
¶
The fitted SCM, whose mechanisms are trustworthy at :attr:identified_nodes ONLY.
Exposed because an identified node deserves the whole library -- do/see,
transport, counterfactuals -- rather than the single scalar :meth:interval returns. Its
mechanisms at :attr:bounded_nodes are the regression the confounding invalidates, and are
not to be used; they exist because one fitting path is fitted for every node at once.
is_identified(node)
¶
interval(node, assignment, *, n_samples=4096)
¶
Bound E[node | do(parents = assignment)].
assignment must give a value for every parent of node. An identified node returns
a degenerate interval, estimated from :attr:point_model with n_samples draws; a
confounded one returns the cross-fitted Manski bound, which is as wide as the logs'
overlap at that arm requires and is vacuous where they carry no information at all.
Source code in src/causalrl/scm/bounded_fit.py
certificate()
¶
A BOUNDED certificate naming which nodes cost the model its point identification.
Source code in src/causalrl/scm/bounded_fit.py
causalrl.scm.bounded_fit.BoundedNodeFit
dataclass
¶
What one node's mechanism turned out to be: a point fit, or an interval.
identified is the field to branch on. When True the node was fitted exactly as
:func:causalrl.fit_scm would have, and confounded_parents is empty. When False the
node has at least one parent it shares a latent cause with, and every query about it comes back
as an :class:~causalrl.Interval whose width is what that confounding costs.
Source code in src/causalrl/scm/bounded_fit.py
causalrl.transport.localize.localize_mechanism_shift(data_by_regime, *, graph, alpha=0.05)
¶
Test each node's mechanism for invariance across regimes; emit the selection set.
data_by_regime maps a regime name to its columns -- at least two regimes, each carrying
every node of graph. Columns must be integer-coded (the CI test is the discrete one); bin
continuous variables first, and be aware that the test then asks about the binned mechanism.
Returns a :class:ShiftReport whose :attr:~ShiftReport.selection plugs straight into
:func:~causalrl.identify_transport.
Source code in src/causalrl/transport/localize.py
causalrl.transport.localize.ShiftReport
¶
Which mechanisms moved between regimes, and the selection set that follows.
Source code in src/causalrl/transport/localize.py
shifts
property
¶
Every node's verdict, in the graph's topological order -- not only the rejections.
selection
property
¶
The shifted nodes, ready to hand to :func:~causalrl.identify_transport.
invariant
property
¶
Nodes whose mechanism the data give no reason to think moved.
certificate()
¶
An EMPIRICAL certificate: this is a hypothesis test, not an identification result.
Failing to reject invariance is not proof of it, and the certificate says so rather than
reporting the invariant set as established. Anything built on top -- a transport formula
keyed on :attr:selection -- inherits that.
Source code in src/causalrl/transport/localize.py
causalrl.transport.localize.MechanismShift
dataclass
¶
The invariance verdict at one node.
statistic is the G-test statistic 2 n I(V; R | Pa(V)) in nats-times-samples, which is
asymptotically chi-squared under invariance; p_value is its upper tail. shifted is the
verdict at the caller's alpha, and is what puts the node in the selection set.
Source code in src/causalrl/transport/localize.py
causalrl.agents.anytime.Anytime
¶
Bases: Protocol
An agent with a usable answer at every instant, refinable while time remains.
The two halves are inseparable: :meth:incumbent without :meth:refine is a fixed answer, and
:meth:refine without :meth:incumbent is a computation you cannot interrupt. Together they
are what lets a decision be handed a wall-clock budget instead of being run to convergence.
Source code in src/causalrl/agents/anytime.py
incumbent()
¶
refine(deadline=None)
¶
Improve the incumbent within deadline; return False once converged.
False means further calls cannot help -- the caller should stop rather than spin. A
return of True says only that more work remains, not that the last call used its whole
budget.
Source code in src/causalrl/agents/anytime.py
causalrl.agents.anytime.AnytimeInterventionSearch
¶
Bases: InterventionalAgent
Deadline-honouring random-shooting search over an :class:~causalrl.InterventionSpace.
Round 0 samples the whole space; each later round resamples around the incumbent inside a
window that shrinks by shrink, so the answer improves monotonically and is usable after any
number of rounds. value_fn(observation, intervention) scores a candidate -- typically a
fitted outcome model, a BoundedSCMFit interval's midpoint, or an SCM's do expectation.
The deadline is checked between candidates, not only between rounds, so a budget that expires
mid-round still returns the incumbent rather than overrunning to a round boundary. Nothing here
interrupts value_fn itself: a single call that overruns the budget will overrun it, which
is the cooperative contract :class:causalrl.Deadline documents.
Source code in src/causalrl/agents/anytime.py
127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 | |
last_search
property
¶
How the most recent :meth:act terminated. Its certificate carries the hedge.
incumbent()
¶
act(observation, *, space, deadline=None)
¶
Search space for the highest-value intervention within deadline.
Source code in src/causalrl/agents/anytime.py
refine(deadline=None)
¶
Not separately drivable: this agent's search runs inside :meth:act.
Present because :class:Anytime requires it. It reports convergence (False) whenever
the last :meth:act ran to completion, so a caller driving the protocol generically stops
instead of spinning on a search that has nowhere left to go.
Source code in src/causalrl/agents/anytime.py
update(observation, intervention, reward)
¶
No-op: this agent's policy is its value_fn, which it does not fit.
Learning the value model is a separate concern with its own entry points (fit_scm,
FittedQIteration, fit_scm_bounded); this class is the search over one.
Source code in src/causalrl/agents/anytime.py
causalrl.agents.anytime.SearchReport
dataclass
¶
How much of the search actually ran, and whether the budget stopped it.
exhausted is the load-bearing field: True means the search finished the work it
intended, False means a deadline cut it off and the answer is the best of a subset.
Source code in src/causalrl/agents/anytime.py
certificate()
¶
An EMPIRICAL certificate, hedged when the budget truncated the search.
Always EMPIRICAL: a search over sampled candidates is sample evidence about which
intervention looks best, and carries no identification guarantee whatever its budget. What
the deadline changes is completeness, and that is what the hedge reports.
Source code in src/causalrl/agents/anytime.py
causalrl.intervention.Continuous
dataclass
¶
A closed real interval [low, high] of admissible values.
The action-side counterpart of :mod:causalrl.state's feature vectors. The estimation core
never needed a discretisation -- cross-fitted DML, additive-noise mechanisms, the continuous
bounds and the RBF-encoded function approximators all take real-valued inputs -- so a
continuous confounder was always expressible while a continuous treatment was not. A dose, a
budget, a duration or any other quantity chosen on a scale is the ordinary case.
Deliberately NOT named Interval: :class:causalrl.Interval is a bound on an estimand's
value, and reusing the name for an action domain would make every docstring mentioning one
ambiguous about which it meant.
Source code in src/causalrl/intervention.py
causalrl.intervention.Discrete
dataclass
¶
A finite set of admissible values -- the only domain kind before continuous ones existed.
Source code in src/causalrl/intervention.py
project(value)
¶
The admissible value closest to value; value itself when already admissible.
Closeness is numeric where the values are numeric and identity otherwise, so a
non-admissible categorical projects to the first admissible value rather than raising --
:meth:InterventionSpace.project is the "make this admissible" path, and a caller who
wants a refusal should ask :meth:InterventionSpace.permits instead.
Source code in src/causalrl/intervention.py
causalrl.intervention.InterventionDomain = Discrete | Continuous
module-attribute
¶
What a manipulable variable may be set to: a finite set, or a real interval.
Named in full because :class:causalrl.Domain is already taken by the transport domain -- a
source or target population in a selection diagram. The two are unrelated, and one of them had to
say which it was.
causalrl.ope.sequential_test.confidence_sequence(values, *, alpha=0.05, value_range, rho=None)
¶
A band on E[value] valid at every sample size simultaneously.
value_range must genuinely contain every observation: it sets the sub-Gaussian variance
proxy (high - low) / 2, and a range that is too narrow silently voids the guarantee rather
than degrading it. rho tunes where the boundary is tightest and defaults to a value
optimised near the sample size actually supplied; validity does not depend on it.
Source code in src/causalrl/ope/sequential_test.py
causalrl.ope.sequential_test.ConfidenceSequence
dataclass
¶
A time-uniform band on a mean, and the record of what it took to get there.
interval is deliberately NOT reported through Certificate.ci's usual fixed-sample
reading. It is a different object with a stronger guarantee -- valid at all sample sizes at
once -- and conflating the two would let a reader assume the wrong one.
Source code in src/causalrl/ope/sequential_test.py
excludes_zero
property
¶
Whether the band has separated from zero -- a valid stopping condition, at any time.
causalrl.ope.sequential_test.sequential_policy_comparison(dataset, target_actions, *, alpha=0.05, reward_range, weight_cap=100.0)
¶
Compare a target policy to the logging policy with a band valid under repeated looking.
The per-decision score is the clipped importance-weighted contrast
(w_i - 1) * r_i with w_i = 1[a_i = pi(s_i)] / e0(a_i | s_i) capped at weight_cap,
whose mean is the value improvement V(pi) - V(behaviour). Clipping is what makes the score
bounded and hence sub-Gaussian; it also biases the estimate toward the behaviour policy, which
is the conservative direction for a "should I ship this" question and is recorded as an
assumption rather than left implicit.
Read :attr:SequentialVerdict.stop as the stopping rule: it may be consulted after every new
batch of logs without spending any error budget.
Source code in src/causalrl/ope/sequential_test.py
causalrl.ope.sequential_test.SequentialVerdict
dataclass
¶
The result of an anytime-valid policy comparison, and whether it is safe to stop.
Source code in src/causalrl/ope/sequential_test.py
causalrl.scm.fidelity.certify_fitted_query(model, data, *, intervention, outcome, tolerance=2.0, atol=1e-08, n_samples=256, seed=0)
¶
Answer E[outcome | do(intervention)] and gate it on the model's fidelity in that regime.
Runs the query, then tests the outcome's mechanism against the rows of data where the
intervened variables already take the queried values. A mechanism that mispredicts those rows
by more than tolerance observed standard deviations gets a model-fidelity
:class:~causalrl.certify.Hedge and a kind downgraded to EMPIRICAL; a regime with no
matching rows at all gets a no-factual-support hedge, because a query there is pure
extrapolation and nothing in the data speaks to it either way.
The returned certificate always carries the model's answer. Gating changes what the answer is claimed to be, not whether it is reported -- a caller who wants the number anyway can still read it, and one who is deciding on it can read the hedge.
Source code in src/causalrl/scm/fidelity.py
102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 | |
causalrl.scm.fidelity.FidelityReport
dataclass
¶
How well the outcome's fitted mechanism predicts the regime being queried.
standardised_error is the mean absolute prediction error over the probed rows, divided by
the observed spread of the outcome in that regime. Standardising is what makes one tolerance
meaningful across outcomes measured in different units; when the observed spread is zero it
falls back to the absolute error, since "off by 3" against a constant is still off by 3.
Source code in src/causalrl/scm/fidelity.py
trustworthy
property
¶
Whether the mechanism predicts this regime well enough to point-answer a query in it.
causalrl.data.builder.TrajectoryLogBuilder
¶
Accumulate rows for a :class:TrajectoryLog across many calls instead of just one.
The counterpart to :meth:TrajectoryLog.from_rows for a producer that does not have all its
rows up front: push them in as they happen and call :meth:freeze whenever a
:class:TrajectoryLog is needed -- as many times as needed, including more than once per
builder. :meth:push is the primitive; :meth:episode is a context manager built on top of it
that fixes episode_id for every push inside it.
Source code in src/causalrl/data/builder.py
100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 | |
metadata
property
¶
A copy of the log-level metadata :meth:freeze will attach to the next snapshot.
__init__(metadata=None)
¶
Start an empty builder. metadata is attached to every log :meth:freeze returns.
Source code in src/causalrl/data/builder.py
__len__()
¶
push(*, episode_id, kind, name, value, t, entity_id=0, regime='observed', observed=True)
¶
Append one row.
Prefer :meth:episode when stepping through one episode's rows in order -- it saves
repeating episode_id at every call. Call this directly when a caller already tracks
its own episode id per row (replaying an existing sequence of row dicts into a builder,
for instance) and a with block would only add ceremony.
entity_id defaults to 0, the single-trajectory case this class exists for (one
controller, one robot, one running experiment); regime/observed default exactly as
:meth:TrajectoryLog.from_rows defaults them, so the two stay interchangeable. Values are
stored as given -- push does no coercion or validation of its own -- and only converted to
the schema's NumPy dtypes inside :meth:freeze, the same place from_rows does its own
conversion, so a malformed value is reported the same way it would be for any other caller
of :class:TrajectoryLog, not through a second, builder-specific error path.
Source code in src/causalrl/data/builder.py
episode(episode_id)
¶
Open a push scope that stamps every row inside it with episode_id.
Yields an :class:EpisodeWriter bound to episode_id; call :meth:~EpisodeWriter.push
on it instead of repeating the id at every call. The handle stops accepting pushes the
moment this block exits, including when the body raises: the finally that closes it
runs either way, so an exception partway through an episode still leaves the builder
holding exactly the rows pushed before the failure, ready for more pushes or a
:meth:freeze -- the exception itself propagates to the caller unchanged, nothing here
swallows it.
Source code in src/causalrl/data/builder.py
freeze()
¶
Materialise everything pushed so far into a genuine :class:TrajectoryLog.
Repeatable, not a one-shot terminal operation: call it as often as you like, including
after pushing more rows following an earlier call. That is the deliberate choice between
the two honest options for what "appending after freeze" should mean -- refuse it
outright, or allow it and let freeze be called again -- because a live producer's entire
reason for existing is that it keeps running after someone has inspected a snapshot, and
the streaming certificate this class exists to feed
(:func:~causalrl.ope.ipw.stream_policy_value) is exactly the kind of thing wanted
mid-run, not only once at the very end. Refusing further pushes after the first freeze
would force a caller to either stop the producer just to get a certificate, or maintain a
fresh builder per snapshot and stitch the results back together -- both defeat the point
of having an incremental builder at all.
Safe to call repeatedly precisely because it is not free of a copy: :class:TrajectoryLog
converts every column list into a new NumPy array in its constructor, so a log returned by
an earlier freeze() shares no storage with this builder and cannot be changed by rows
pushed after it was returned -- each snapshot is a real, independent log, not a view.