Skip to content

Releases: ergodic-ai/ergodic

ergodic 0.3.0

Choose a tag to compare

@fermisea fermisea released this 17 Oct 12:37
8e157c5

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, sink and restrict_to take confidence=. 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 new DomainKnowledge.soft field as a
    KnowledgeStatement (checked on construction), and 0.5 changes nothing. Values
    below 0.5, above 1, NaN or a bool raise ValueError (state the opposite instead).
    The confidence applies to each pair a statement covers. confound stays 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 and exact_map is 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;
    objective keeps scoring the input.

  • decode with confound(a, b) returns an ADMG: the directed part is decoded as
    before and a <-> b is 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_effect reads both. diagnostics add graph_kind,
    confounded_pairs and latent_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_cost the score the knowledge cost,
    and knowledge_conflicts the statements with a pair the model gives under 0.1.
    knowledge_violations_without_knowledge lists what violations() finds on the
    knowledge-free graph, confound pairs left out, so an order broken only through
    the closure shows too.
    n_hard_statements and n_soft_statements count them. decode(..., audit=False)
    and Decoder(..., audit=False) skip it.

  • ergodic.KnowledgeConflictWarning (a UserWarning), raised once by a decode whose
    knowledge strongly conflicts with the model, pointing at your call.

  • test_graph(candidate, data) (also ergodic.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 test discover would pick from the columns (or test=),
    and combines the p-values with Fisher's C on 2k degrees 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 with latent= or selection= 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 in untestable. 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() and to_frame() export it. The combined p-value is the
    level-alpha decision (rejected); the Holm decisions locate the failing claims.
    Claims whose test has zero degrees of freedom go to vacuous and 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 that GCM with 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 at alpha = 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 as ergodic.welt.Chronos) runs
    Welt's hosted Chronos-2 as an ergodic ForecastModel (API-163, DEC-087). fit
    checks the columns and keeps the window locally; each forecast(horizon, *, seed, future=None) is one paid POST /v1/forecasts job (DEC-085), sent with
    welt.Client.submit_forecast and read with the SDK's Job.result. Every target column
    is one series of a panel by default (cross_learning=False), or the targets of one
    series with multivariate=True. past_covariates= and future_covariates= map to
    Welt's covariate roles, categorical by the dataset's schema (cells sent as strings
    or integers, True as 1; other cells as finite numbers, a boolean as 0.0/1.0);
    future covariates need future= (a dataframe or TimeSeriesData, only those
    columns are read). A
    HierarchicalTSData is forecast at every node (an aggregate without a column from
    its children's summed history, a node left out of targets= from its children's
    draws), and reconcile= reconciles inside forecast. The first forecast reads the
    catalogue entry (free) and refuses, before paying, a model without the forecast
    operation or a request outside every measured forecast_envelopes corner;
    max_context= sends only the most recent rows. Tables always go inline, as
    welt-client sends them, and a request over welt.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_forecasts budget (10) of jobs Welt accepted. Each node's
    WeltForecastDiagnostics records the job id, versions, quantile levels, Welt's own
    quantiles, context_length and rearranged_steps. A numeric time index, or a
    datetime index on a calendar Welt has no unit for (B, C, BME, SME and 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_LEVELS lists 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 a bottom_up or mint reconciliation, treat the errors as
    independent.

  • ergodic.forecast.CovariateForecastModel: a ForecastModel that names
    future_covariates and takes their values over the horizon as
    forecast(..., future=...). backtest hands 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 its targets; 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's log_prior, over the DAGs the hard knowledge allows; order statements are
    read ancestrally, as violations reads 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...

Read more

ergodic 0.2.1

Choose a tag to compare

@fermisea fermisea released this 15 Oct 12:49
1f03639

Fixed

  • WeltLearner fits 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, and predictor_id_ is None. 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's ols learner.
  • ArcStats treats two NaN gaps (START and END arcs) as equal and hashes them alike,
    so process_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

Choose a tag to compare

@fermisea fermisea released this 15 Oct 11:30
deb4ffe

To be released together with welt-client 0.13.

Added

  • A versioned graph format. MixedGraph.to_dict() writes
    "format": "ergodic.graph/1", and ergodic.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 raises ValueError naming the
    supported format on any other value. The rest of the to_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
    and discover(data, method="welt:<model>") run a Welt causal discovery model
    and return a DiscoveryResult with 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) raises WeltGraphNotDAGError before anything is uploaded or
    paid for, and a result Welt does not declare a DAG raises it with the result
    attached; nothing is projected. WeltLearner puts any Welt classification or
    regression model in a nuisance slot, refuses sample_weight, and shares one
    client and a max_fits budget (default 10) with its clones.
    upload_dataset and load_research_dataset move tabular data between ergodic
    and Welt. bootstrap refuses welt: methods (each resample would be a paid
    job).
  • ergodic.EdgeProbabilities (also ergodic.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] <= 1 enforced) or
    "frequency" (bootstrap shares); calibrated defaults to False and provenance
    is a read-only mapping. to_pair_states() conditions independent marginals on "not
    both directions", existence() gives skeleton probabilities, threshold(t) a
    diagnostic mixed graph that may be cyclic, to_frame()/from_frame() a
    dataframe round trip, and from_bootstrap() reads a BootstrapResult. 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 strict threshold), exact_map_v1 (A*, capped at max_nodes),
    expected_count_v1, soft_f1_v1, and the seeded samplers pair_sampler_v1 and
    perturb_and_map_v1 (candidate DAGs plus a medoid consensus; a seed is required).
    It returns a DecodeResult (a DiscoveryResult, method="decode:<id>") labelled
    decoded with the decoder, parameters, seed, rejected cycles, objective and source
    provenance, and the note "not a graph posterior; orientation not certified".
    DomainKnowledge constrains 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 than DomainKnowledge.violations for separate order() pairs.
    confound is refused.
    ergodic.discovery.Decoder holds a decoder and its settings.
  • ergodic.welt.discover(..., decoder=...) and
    discover(data, method="welt:<model>", decoder=...): opt-in local decoding. The
    run then accepts avici and cdfm, reads the native scores as
    EdgeProbabilities(kind="independent") with the Welt provenance, and returns a
    DecodeResult whose graph is the decoded DAG, with edge_probabilities,
    welt_native_edges and the decode labels in diagnostics. 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_nodes against the variable count included, are checked before
    anything is uploaded. Any failure after the paid run raises WeltDecodeError
    with the result and its scores attached. Without decoder= 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 an llms.txt index.
  • 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 a PolicyValue (value,
    std_error, interval(), treat_all_value, regret() and regret_std_error),
    the honest out-of-sample value; the tree's own value is measured on the scores
    it was chosen to maximize, so it is optimistic and its docstrings now say so.
  • SCM(cores=) and PyMCLearner(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, which LogisticRegression emits
    when it stops at max_iter before converging. It is ergodic's own class, so a
    filter on scikit-learn's ConvergenceWarning does not catch it.
  • MixedGraph.from_networkx(graph, kind=...), like the module-level function.
  • SCMResult.kind="predictive" for posterior-predictive draws, and
    CausalForest.cate() records diagnostics["ij_zero_se_share"] (see Fixed).

Changed

  • DiscoveryResult.diagnostics is typed Mapping[str, Any] so a Welt run can
    carry its provenance. Learner cloning honors a learner's own
    __sklearn_clone__ override; scikit-learn estimators keep the get_params
    path.
  • A weighted nuisance slot (the R-learner's final) refuses, before any fit, a
    learner that declares supports_sample_weight = False, as WeltLearner does.
    discover(..., method="welt:<model>") refuses alpha and local-only keyword
    arguments before any request.
  • Identification's refusal of a mixed graph 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 with consistent_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, and estimate_effect and cate raise the same message.
  • A bound conditional-independence test or score handed data it was not bound
    to (for example FCI(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 to fit or
    discover. A DSeparationOracle handed data says it takes none. The
    discover docstring 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_max is 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.
  • MixedGraph neighbourhood lookups (parents, children, neighbors,
    spouses, ancestors, descendants, markov_blanket) and Hierarchy lookups
    (children_of, parent_of, is_leaf, leaf_descendants) raise ValueError on
    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_edge still answer None/False for an absent edge, and
    HierarchicalTSData.aggregate on an unknown node raises this ValueError rather
    than a KeyError. KNN used before fit raises RuntimeError, and set_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.interventional and SCM.ate read 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...
Read more

ergodic 0.1.0

Choose a tag to compare

@fermisea fermisea released this 17 Aug 23:34

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_effect over 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.do interventions, 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).