Repository navigation
Releases: ergodic-ai/ergodic
Release list
ergodic 0.3.0
To be released together with welt-client 0.14 (API-164): decoding with domain knowledge
(API-151), graph evaluation (API-152) and the Welt Chronos-2 forecaster (API-163).
Added
-
Soft domain knowledge.
require,forbid,forbid_adjacency,forbid_between,
order,with_tiers,root,sinkandrestrict_totakeconfidence=. Left
out (or 1) a statement is the hard constraint it always was. A value in[0.5, 1)
makes it soft: it lands in the newDomainKnowledge.softfield as a
KnowledgeStatement(checked on construction), and 0.5 changes nothing. Values
below 0.5, above 1, NaN or a bool raiseValueError(state the opposite instead).
The confidence applies to each pair a statement covers.confoundstays hard
only.&pools soft statements as a set, so a statement made twice counts once. -
DomainKnowledge.log_odds(variables): the soft statements' summed log likelihood
ratios on each directed edge, against "no edge" on its pair.log_prior(graph)and
energy(graph), which used to raise, now score a graph under the soft statements
(minus infinity when it breaks a hard constraint). Soft orders are weighed pair by
pair and are not closed transitively.DomainKnowledge.statements()lists every
statement, read from the stored fields (hard) and the soft set, so equal knowledge
lists equal statements.DomainKnowledge.hard()drops the soft statements. -
decode(..., knowledge=)weighs soft statements: each pair's directed states are
multiplied by the statement ratios and renormalized before any decoder reads them,
so all six decoders run unchanged andexact_mapis exact on the combined score.
A soft weight on what a hard constraint settles is dropped. Greedy with
threshold=multiplies the input value's odds by the ratio on that direction.
diagnostics["knowledge_objective"]is the score on the weighted states;
objectivekeeps scoring the input. -
decodewithconfound(a, b)returns an ADMG: the directed part is decoded as
before anda <-> bis added, to the graph and every candidate. A pair that also
got a directed edge (a bow) carries its confounder as an explicit latent node
L__a__b.identify_effectreads both.diagnosticsaddgraph_kind,
confounded_pairsandlatent_confounders; the repr counts observed variables and
names the latent confounders. -
A knowledge audit on every decode with knowledge:
diagnostics["knowledge_audit"]
has one entry per statement (the model's probability that it holds, from direct
edges; the weakest of its pairs and the pairs below 0.1; whether it holds, an order
read through directed paths, in the decoded graph and in a knowledge-free decode
with the same settings and seed),knowledge_costthe score the knowledge cost,
andknowledge_conflictsthe statements with a pair the model gives under 0.1.
knowledge_violations_without_knowledgelists whatviolations()finds on the
knowledge-free graph,confoundpairs left out, so an order broken only through
the closure shows too.
n_hard_statementsandn_soft_statementscount them.decode(..., audit=False)
andDecoder(..., audit=False)skip it. -
ergodic.KnowledgeConflictWarning(aUserWarning), raised once by a decode whose
knowledge strongly conflicts with the model, pointing at your call. -
test_graph(candidate, data)(alsoergodic.discovery.test_graph) tests a
candidate graph against data (API-152, DEC-082). It lists one implied
independence per pair of observed variables without an edge, tests each with the
conditional-independence testdiscoverwould pick from the columns (ortest=),
and combines the p-values with Fisher's C on2kdegrees of freedom. A DAG uses
Shipley's basis set (the parents of both); a CPDAG uses the same set in its
consistent extension; an ADMG, a MAG, a DAG withlatent=orselection=nodes
and a PAG (through one member MAG) use m-separation, with the parents of both when
they separate and otherwise a minimal set of the pair's observed ancestors. Every
conditioning set is checked against the graph's separation routine, and a pair no
observed set separates is reported inuntestable. The result, a frozen
GraphTest, carries the statistic, p-value,k, the test,alpha, notes, and the
claims (ImpliedIndependence) ranked by p-value with Holm-adjusted p-values and
decisions;to_dict()andto_frame()export it. The combined p-value is the
level-alphadecision (rejected); the Holm decisions locate the failing claims.
Claims whose test has zero degrees of freedom go tovacuousand are left out of
k. An infinite value in an observed continuous column, or a non-finite p-value or
statistic from the test, raises instead of reading as a pass; missing rows are
dropped with a note. Only the default partial-correlation test is calibrated; the
result notes any other test, and says thatGCMwith a nearest-neighbour learner
and G squared with fewer than 1,000 rows over-reject true graphs. Claims whose
p-values tie (for example, underflowed to 0) rank by the larger|statistic|. The
p-value cannot separate Markov-equivalent graphs and a pass does not confirm the
graph; the new guide "Test a graph against data" says so. Size with the default
partial-correlation test on six-variable linear-Gaussian worlds atalpha = 0.05:
the registered run (200 worlds per sample size) rejected the true graph in 9.5% of
worlds with 200 rows, outside its [2%, 8%] band, and 4.5% with 1,000; a
pre-registered replication on 2,000 fresh worlds per size gave 4.65% [3.77%, 5.67%]
and 4.85% [3.95%, 5.88%], inside its [3.5%, 6.5%] band. Power against one missing
edge of weight at least 0.5 is close to 1. -
ergodic.welt.WeltForecaster(also importable asergodic.welt.Chronos) runs
Welt's hosted Chronos-2 as an ergodicForecastModel(API-163, DEC-087).fit
checks the columns and keeps the window locally; eachforecast(horizon, *, seed, future=None)is one paidPOST /v1/forecastsjob (DEC-085), sent with
welt.Client.submit_forecastand read with the SDK'sJob.result. Every target column
is one series of a panel by default (cross_learning=False), or the targets of one
series withmultivariate=True.past_covariates=andfuture_covariates=map to
Welt's covariate roles, categorical by the dataset's schema (cells sent as strings
or integers,Trueas1; other cells as finite numbers, a boolean as 0.0/1.0);
future covariates needfuture=(a dataframe orTimeSeriesData, only those
columns are read). A
HierarchicalTSDatais forecast at every node (an aggregate without a column from
its children's summed history, a node left out oftargets=from its children's
draws), andreconcile=reconciles insideforecast. The first forecast reads the
catalogue entry (free) and refuses, before paying, a model without theforecast
operation or a request outside every measuredforecast_envelopescorner;
max_context=sends only the most recent rows. Tables always go inline, as
welt-client sends them, and a request overwelt.forecast.MAX_REQUEST_BYTES
(the API's 10 MiB limit) is refused before sending. The first job's model and
configuration versions are required of every later job of the forecaster and its
copies (and checked against each result), which also share one client and one
max_forecastsbudget (10) of jobs Welt accepted. Each node's
WeltForecastDiagnosticsrecords the job id, versions, quantile levels, Welt's own
quantiles,context_lengthandrearranged_steps. A numeric time index, or a
datetime index on a calendar Welt has no unit for (B,C,BME,SMEand the
like, declared or inferred by pandas; a declared one is trusted as given), is sent as
consecutive days from 2000-01-01; any other irregular datetime index
(daily data with missing days, say) is refused with a hint to reindex and leave
the gaps as NaN.NATIVE_QUANTILE_LEVELSlists the 21 levels requested by default. -
ergodic.forecast.quantile_draws(levels, quantiles, *, draws, seed)turns
per-step quantiles into(S, H)draws by inverse-CDF sampling: rows sorted
(monotone rearrangement), linear between levels, exponential tails matched to the
density at the outer levels, each step sampled independently. The draws reproduce
the per-step marginals only, so sums over steps or series, and the intervals,
coverage and CRPS of abottom_upormintreconciliation, treat the errors as
independent. -
ergodic.forecast.CovariateForecastModel: aForecastModelthat names
future_covariatesand takes their values over the horizon as
forecast(..., future=...).backtesthands such a model, at each origin, those
columns over the held-out window and nothing else, and records them in
BacktestReport.future_covariates. It refuses, before any fit, a declared
covariate that is not a column, a string in place of a sequence, and one the model
names among itstargets; a model that forecasts a declared covariate without
naming it is refused on the first fold's result. Models without the attribute are called
exactly as before. -
New guide "Forecast with Welt's Chronos-2": forecast with a known promotion
calendar, backtest against baselines, and reconcile a hierarchy. -
A posterior over DAGs under a method (API-152, DEC-082).
graph_posterior(probabilities)
weighs each DAG by its pair log score (the pair states the decoders read) plus the
knowledge'slog_prior, over the DAGs the hard knowledge allows; order statements are
read ancestrally, asviolationsreads them.graph_posterior(data, score=...)uses a
decomposable score with a prior uniform over DAGs times the knowledge; BGe and BDeu are
marginal likelihoods, the BIC family is labelled approximate. The result,
GraphSamples, holds the distinct DAGs with weights, `edge_probabilit...
ergodic 0.2.1
Fixed
WeltLearnerfits a slot with no feature columns locally. An empty adjustment set
leaves nothing for a model to learn, so the nuisance is the training mean (or the
class frequencies for classification): no upload, no paid fit, no charge to
max_fits, andpredictor_id_isNone. In 0.2.0 it raised, so the "Discover in
Welt, estimate in ergodic" guide stopped whenever Welt's Arrow found no confounders
for the treatment, as it does on the guide's own data today. With no adjustment
variables, DML is then the cross-fitted regression slope of the outcome on the
treatment, identical to DML with ergodic'solslearner.ArcStatstreats two NaN gaps (START and END arcs) as equal and hashes them alike,
soprocess_map(log) == process_map(log)holds on Python 3.13 and 3.14, where
dataclass equality compares fields one by one.
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 handed `sample_posterior_predi...
ergodic 0.1.0
First public release of ergodic, the Causal AI library from ergodic.ai.
Pillars
- Graph & knowledge: one mixed-graph core (DAG, ADMG, MAG, CPDAG, PAG) with real validators (Meek, FCI closure, MAG/PAG round trips), plus composable domain knowledge.
- Identification: adjustment, front-door, instruments, the complete Shpitser-Pearl ID/IDC algorithm with typed estimands, generalized adjustment on equivalence classes, IDA, and mediation (NDE/NIE/CDE).
- Inference:
estimate_effectover thirteen estimators (AIPW, DML, DR/R/X/S/T-learners, ...), per-unit CATE with honest explainability (BLP, GATES, RATE, policy trees, causal forest), Bayesian learners, and the quasi-experimental designs (DiD incl. staggered, synthetic control, ITS, event studies). - Discovery: PC, FCI, RFCI, GES, BOSS, GRaSP, A*, LiNGAM, glasso, MMPC, PCMCI, Granger, with typed CI tests and scores, knowledge in four modes, and a moving-block bootstrap.
- Process intelligence: process maps, case tables that feed the causal bridge, Inductive Miner to sound workflow nets, token replay and alignments, decision-point analysis with positivity.
- SCM: Bayesian structural causal models on PyMC with
pm.dointerventions, twin-network counterfactuals, per-unit ITE, latent-district ADMG fitting, and an identification-gated honesty layer. - Forecast: Bayesian structural forecasting (
forecast/backtest) with temporal mechanisms, hierarchy reconciliation (bottom-up/top-down/middle-out/MinT/trust), window-graph drivers with staged posterior propagation, scenarios, and attribution.
Install: pip install ergodic (add [forecast], [scm], or [bayes] for the PyMC-backed pillars; jax+numpyro for the fast sampler).