Repository navigation
ergodic 0.2.0
To be released together with welt-client 0.13.
Added
- A versioned graph format.
MixedGraph.to_dict()writes
"format": "ergodic.graph/1", andergodic.GRAPH_FORMAT(also
ergodic.graph.GRAPH_FORMAT) names the format this version reads.
MixedGraph.from_dict()reads a dict without the field as version 1, so older
output and hand-written payloads (kind,nodes, and edges of
{u, v, u_mark, v_mark}) still load, and it raisesValueErrornaming the
supported format on any other value. The rest of theto_dict()output is
unchanged. Round trips are tested for every graph kind, latent and selection
nodes included. ergodic.welt, an optional Welt integration installed with
pip install "ergodic[welt]"(welt-client>=0.13, Python 3.11 to 3.13; the
rest of ergodic never imports it, enforced by a test).ergodic.welt.discover
anddiscover(data, method="welt:<model>")run a Welt causal discovery model
and return aDiscoveryResultwith the native DAG and Welt provenance in
diagnostics(ids, model and configuration versions, the edge-score
dataframe). A model known to return non-DAG graphs (cdfm,avici, or any the
catalogue marks so) raisesWeltGraphNotDAGErrorbefore anything is uploaded or
paid for, and a result Welt does not declare a DAG raises it with the result
attached; nothing is projected.WeltLearnerputs any Welt classification or
regression model in a nuisance slot, refusessample_weight, and shares one
client and amax_fitsbudget (default 10) with its clones.
upload_datasetandload_research_datasetmove tabular data between ergodic
and Welt.bootstraprefuseswelt:methods (each resample would be a paid
job).ergodic.EdgeProbabilities(alsoergodic.discovery.EdgeProbabilities): an
immutable matrix of directed-edge probabilities over named variables, source rows
and target columns, with a zero diagonal and a read-only copy of the values.kind
is"independent"(per-edge marginals, as AVICI and CDFM return),"pair_state"
(three exclusive states per pair,P[i, j] + P[j, i] <= 1enforced) or
"frequency"(bootstrap shares);calibrateddefaults toFalseandprovenance
is a read-only mapping.to_pair_states()conditions independent marginals on "not
both directions",existence()gives skeleton probabilities,threshold(t)a
diagnosticmixedgraph that may be cyclic,to_frame()/from_frame()a
dataframe round trip, andfrom_bootstrap()reads aBootstrapResult. It is not
a graph posterior.ergodic.decode(probabilities, method="greedy", *, threshold=None, knowledge=None, samples=1, seed=None, max_nodes=20): explicit, versioned decoders from edge
probabilities to a DAG, ported from Ergodic's research library:greedy_v1
(default; optional strictthreshold),exact_map_v1(A*, capped atmax_nodes),
expected_count_v1,soft_f1_v1, and the seeded samplerspair_sampler_v1and
perturb_and_map_v1(candidate DAGs plus a medoid consensus; a seed is required).
It returns aDecodeResult(aDiscoveryResult,method="decode:<id>") labelled
decodedwith the decoder, parameters, seed, rejected cycles, objective and source
provenance, and the note "not a graph posterior; orientation not certified".
DomainKnowledgeconstrains decoding (forbidden edges, tiers, roots, sinks,
restrict_to, required edges first); order statements and tiers are enforced as
one time order consistent with every order statement and every edge, which is
stricter thanDomainKnowledge.violationsfor separateorder()pairs.
confoundis refused.
ergodic.discovery.Decoderholds a decoder and its settings.ergodic.welt.discover(..., decoder=...)and
discover(data, method="welt:<model>", decoder=...): opt-in local decoding. The
run then acceptsaviciandcdfm, reads the native scores as
EdgeProbabilities(kind="independent")with the Welt provenance, and returns a
DecodeResultwhose graph is the decoded DAG, withedge_probabilities,
welt_native_edgesand the decode labels indiagnostics. Greedy defaults to the
model's native threshold (0.5 for AVICI, the recorded adaptive threshold for CDFM).
With a decoder,knowledge=constrains the local decoding; it and every decoder
setting,max_nodesagainst the variable count included, are checked before
anything is uploaded. Any failure after the paid run raisesWeltDecodeError
with the result and its scores attached. Withoutdecoder=nothing changes:
non-DAG models are still refused before paying.- A new documentation site at https://docs.ergodic.dev, one page per task. Each guide
runs one complete program on bundled data and shows its recorded output; the
docs check reruns every program and compares, and a program that needs a Welt
key is tested against a fake Welt service. The site is versioned, searchable,
and publishes each page as Markdown and anllms.txtindex. - Three learn notebooks: structural causal models (interventions,
counterfactuals and sensitivity), hidden regimes (recover, certify and
calibrate), and causal forests with policy trees. - Every public function, class and member has a docstring, and the reference
build fails when one is missing. PolicyTree.evaluate(held_out)scores a learned rule on doubly robust scores
from units it was not learned on and returns aPolicyValue(value,
std_error,interval(),treat_all_value,regret()andregret_std_error),
the honest out-of-sample value; the tree's ownvalueis measured on the scores
it was chosen to maximize, so it is optimistic and its docstrings now say so.SCM(cores=)andPyMCLearner(cores=)(default 1) cap the processes PyMC may
use for the chains; every SCM fit (singleton, latent district, sensitivity
refit, process-pool worker) honours it. PyMC otherwise ran up to four chains in
parallel processes. The draws do not depend on it.MediationEstimate.mediated_interaction,total - nde - nie.LatentResult.rhat_max_unaligned, the raw split R-hat (see Changed).ergodic.inference.learners.ConvergenceWarning, whichLogisticRegressionemits
when it stops atmax_iterbefore converging. It is ergodic's own class, so a
filter on scikit-learn'sConvergenceWarningdoes not catch it.MixedGraph.from_networkx(graph, kind=...), like the module-level function.SCMResult.kind="predictive"for posterior-predictive draws, and
CausalForest.cate()recordsdiagnostics["ij_zero_se_share"](see Fixed).
Changed
DiscoveryResult.diagnosticsis typedMapping[str, Any]so a Welt run can
carry its provenance. Learner cloning honors a learner's own
__sklearn_clone__override; scikit-learn estimators keep theget_params
path.- A weighted nuisance slot (the R-learner's
final) refuses, before any fit, a
learner that declaressupports_sample_weight = False, asWeltLearnerdoes.
discover(..., method="welt:<model>")refusesalphaand local-only keyword
arguments before any request. - Identification's refusal of a
mixedgraph now says how to fix it, reading
the graph's marks. A fully directed acyclic graph gets two readings with their
conditions:graph.as_kind("dag")for a search that assumes no hidden
confounding (PC or GES with knowledge orienting every edge), and
graph.as_kind("mag")for an FCI or RFCI result, whose directed edges mean
ancestry and can hide confounding. A directed-plus-bidirected graph gets the
same pair with"admg". A partially directed graph lists the options (orient
the rest with more knowledge, take one member withconsistent_extension, or
rediscover without the orienting knowledge); a graph that fits the CPDAG, MAG,
or PAG rules names that face; a graph that fits none names seeded domain
knowledge as the likely cause. Which graphs identification accepts is
unchanged, andestimate_effectandcateraise the same message. - A bound conditional-independence test or score handed data it was not bound
to (for exampleFCI(test=GCM(data)).fit(data)) still raises, and the message
now names the algorithm and both correct usages: fit the algorithm with no
data, or build the test or score unbound and pass the data tofitor
discover. ADSeparationOraclehanded data says it takes none. The
discoverdocstring now says it binds an unbound test. - Install hints in error messages and module docs quote their extras
(pip install 'ergodic[scm]'), which zsh would otherwise read as a glob. LatentResult.rhat_maxis now the split R-hat after label alignment, over the
sign-corrected delta draws grouped by chain, and the R-hat warning reads it. It
was the raw R-hat, so chains sitting in mirror modes of a good fit reported 1.83
(the spread arm of the golden worlds; aligned, 1.01); that number is now
rhat_max_unaligned.MixedGraphneighbourhood lookups (parents,children,neighbors,
spouses,ancestors,descendants,markov_blanket) andHierarchylookups
(children_of,parent_of,is_leaf,leaf_descendants) raiseValueErroron
a name that is not a node instead of answering with an empty set (or, for
is_leaf,True), as identification and separation already did.
get_edge/has_edgestill answerNone/Falsefor an absent edge, and
HierarchicalTSData.aggregateon an unknown node raises thisValueErrorrather
than aKeyError. KNN used beforefitraisesRuntimeError, andset_params
on the native KNN, OLS and logistic learners refuses unknown names, as sklearn
does.- The documentation moved to https://docs.ergodic.dev (Graphite builder); the MkDocs
site is retired.
Fixed
-
SCM.sample,SCM.interventionalandSCM.ateread the fitted posterior on
every supported stack. With PyMC 5 and arviz 0.x (the
versions Python 3.11 resolves), 0.1.0 handedsample_posterior_predictivea
posterior it could not read, so every SCM parameter was silently drawn from its
prior. The merged posterior is now a plainxarray.Dataset, which PyMC 5 and 6
read the same way. If you usedergodic.scmon Python 3.11 (or with arviz below
1.0), rerun those results. -
estimate_mediation: the total effect is now computed directly as
E[Y(1, M(1))] - E[Y(0, M(0))]. It used to be the pure NDE plus the pure NIE,
which drops the mediated interaction whenever the treatment interacts with the
mediator (always possible, since the outcome model is saturated in treatment and
mediator). Onload_berkeley(gender, department, admission) the total was
-0.072 with a proportion mediated of 1.99; it is now the -0.142 raw gap and the
proportion 1.01.ndeandnieare unchanged (the pure effects), so under an
interaction they no longer sum to the total;proportion_mediatedstays
nie / total, Pearl's share explained by mediation, and the docs now name the
other convention,1 - nde / total(VanderWeele's total NIE over TE, as CMAverse
reports; 1.50 on Berkeley). TheDesign.estimandno longer repeats theNDE =/NIE =
prefix, and a treatment with more than two levels is told to recode to two
levels instead of being pointed attreated=(which needs two levels too). -
CATEResult.ate()no longer treats per-unit analytic standard errors as
independent (sqrt(sum se^2) / n); the units share one fitted model, so that
was aboutsqrt(n)too narrow (0.0009 against a sampling spread of 0.038 on a
4,000-row world). It now reports the AIPW average off the result's cached
cross-fit scores (standard errorstd(scores) / sqrt(n), 0.039 there, the
same asestimate_effect(strategy="aipw")), the result's own standard error
for a homogeneous result such asDML.cate(), and otherwise the mean as a
point with no standard error. For the DR-learner's linear final stage the point
is unchanged; for the causal forest it is the score average, which can differ
slightly from the plain mean of the per-unit effects. -
PolicyTree.valueis now the policy's mean outcome gain over treating no one,
mean[pi(X) * Gamma], in outcome units, as the docstrings described. It was
mean[(2 pi(X) - 1) * Gamma], soregret()and the gap totreat_all_value
came out twice the outcome gain (example 11 priced its rule at ~$324k on a
200k book; the true figure is ~$162k, value 0.82 against an oracle 0.80).
treat_none_valueis now 0 andtreat_all_value(the ATE) is unchanged;
std_errorandinterval()are on the new scale. The learned rule is the same
(the two objectives share their argmax). -
Identification on a DAG or ADMG with
selection=nodes now raises, as the
class faces already did. It used to ignore the selection mark, so a selected
colliderX -> S <- Yidentified as plainP(Y | X)although selecting onS
makes the two dependent in the sample. This coversidentify_effect,
identify_distribution,identify_mediation, the adjustment, front-door and
instrument helpers, and soestimate_effectandcate. -
AStarraisesValueErrornaming the violations when its searched DAG cannot
satisfy the domain knowledge (for example a confounded pair), instead of an
AssertionError(whichpython -Owould also have skipped). -
Error messages and docstrings state limits plainly instead of internal planning
words ("reserved seam", "not built yet", "phase 1", "MVP", "post-MVP", "when the
causal layer lands", "E1", "v1", "lands in D2", "in this phase", "Phase-2",
"Track-D", "deferred"): the neural and foundation CATE method names,ITEResult.explain,
multi-unit synthetic control, a continuous mediator, per-member ID on a PAG,
count and categorical forecast columns, scenario pins on endogenous nodes, the
SCM's custom-mechanism andengine="dml"refusals, soft knowledge, and the
ergodic.latentrefusals. The neural and foundation names no longer suggest
installingergodic[neural]/ergodic[foundation], which do not provide those
learners in this version. A test reads
every string constant in the package and refuses those words. -
load_birthweight: thetargetsaid -225 g with a tolerance of 90 g, the
description and the case study said about -203 g, and neither figure traced to
a published estimate on this extract. The target is now -231 g (tolerance
30 g, naive -275.3 g), the AIPW estimate on this same 4,642-row Cattaneo (2010)
extract in the Stata treatment-effects manual, and the description, the case
study, and the example quote it with that source. Theergodic.datasets
docstrings no longer call every bundled file public-domain or permissively
licensed; they say which files derive from public-domain US government data
and which are authors' replication data without an explicit licence, as the
NOTICE does. -
Reprs:
CATEResult,ITEResult,DiDEffect,SyntheticControlEffect, and
ITSEffectprint the short one-line summary again (with their own class name)
instead of a field-by-field dump of every array, andDiscoveryResultprints
the graph kind ascpdagrather thanGraphKind.CPDAG. -
SyntheticControlEffect.kindis"placebo"instead of"analytic": there is
no standard error, and the interval is a placebo band. -
HillClimbbreaks ties between score-equivalent moves by its documented
enumeration order instead of by floating-point rounding. Addingx -> yand
y -> x(or reversing a covered edge) score the same, but the computed deltas
differ in the last bits depending on the BLAS build, so the climb took
machine-dependent paths; on Apple Accelerate the default run on a five-node
world stopped at a lower-scoring graph (BIC -127.0 against the truth's -123.2)
with an extra edge. Moves within a relative 1e-9 are now ties, and a tie keeps
the first move. -
The best-linear-projection docstrings no longer say the intercept is the ATE:
the covariates enter uncentred, so the intercept is the projected effect at
covariates all zero (the ATE only when they are centred). Outputs unchanged. -
KNNRegressorandKNNClassifierhonoursample_weight(a weighted mean
over the neighbours). They accepted it and dropped it, so the R-learner's
weighted final stage ran unweighted with a KNN learner.PyMCLearnernow
declaressupports_sample_weight = False, so the R-learner refuses it before
any fit, and itsfitraises on weights instead of ignoring them. -
SCMrefuses a graph that is not a DAG or an ADMG. A MAG passed the
acyclic-directed-plus-bidirected check and was fitted as an ADMG, reading
ancestral edges as direct mechanisms; it now raises with the alternatives
(pick a member, or re-face a graph you read as one world withas_kind). -
SCM.interventional,sample, andmarginalreturn posterior-predictive
draws of the outcome, not a posterior ofE[Y | do(X)]: the mean estimates the
expectation, butstd_errorandinterval()are the outcome's spread (sd 1.4
in the guide's example, where the effect's own posterior sd is 0.023). The docstrings
and the guide said otherwise, and the repr printed a "95% CrI". These results
now carrykind="predictive"and print "95% predictive interval";ateand
counterfactualkeepkind="posterior"and the credible interval. -
Process trees splice a nested sequence into its parent, so
seq(a, seq(b, c))isseq(a, b, c).discover_model(noise=...)could return
an unflattenedseq(..., seq(xor(TAU, 'standard'), xor(TAU, 'expedite')), ...)
that did not compare equal to the same model written by hand, although the
tree docstring promised a canonical form. Nestedxorandparnodes are
kept as built (decision_pointsreads eachxoras its own decision). -
CausalForest.cate()records the share of per-unit standard errors clipped to
zero (diagnostics["ij_zero_se_share"]) and warns when it exceeds 5%. With too
few trees for the rows, the infinitesimal-jackknife Monte-Carlo correction
exceeds the raw variance and the band has zero width; on a 4,000-row world 400
trees gave 25% zero-width bands and 68% coverage of a nominal 90%, 1,600 trees
1% and 87%. The estimator is unchanged; its docstring states these
measurements, names the variance correctly (the infinitesimal jackknife with the
Wager-Hastie-Efron Monte-Carlo correction, not a bootstrap of little bags) and
recommends at least as many trees as rows. -
The heterogeneity layer (
explain(): BLP, CLAN;policy_tree;
PolicyTree.evaluate) named the doubly robust design by variable while a
categorical covariate with three or more levels has one column per non-first
level, so names and columns drifted apart: a split reported onZwas really an
indicator of the categorical, and BLP returned four coefficients for three names.
The scores now name every column ("C==level"for such a categorical; a
continuous or two-level covariate keeps its name),policy_tree(result, "C")
selects the categorical's whole block,DRScoresrefuses a design whose width
does not match its names, andevaluate()refuses held-out scores that encode
the categorical with other levels. The forest'svariable_importance()uses the
same column names.