Skip to content

ergodic 0.3.0

Latest

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_probabilities() (pair
    states) with edge_errors(), probability(feature) with a Monte Carlo error,
    log_target(graph), best_graph and top(k). It is exact when it can be: every DAG
    enumerated up to 4 variables (5 for a score), and for edge probabilities without a
    path-level order constraint a subset recursion over sink sets (about 3^n steps, up
    to max_exact=15) that gives the exact normalizer, exact edge marginals, exact draws
    and the exact best graph. Otherwise multi-chain structure MCMC (single-pair add,
    delete and reverse moves; each chain a four-rung parallel-tempering ladder; chains
    from the greedy decode and random DAGs) with split
    R-hat and bulk effective sample sizes, Rao-Blackwellized edge marginals for edge
    probabilities, and a PosteriorConvergenceWarning.

  • evaluate_graph(candidate, probabilities_or_samples) reads a DAG, CPDAG, PAG, MAG or
    ADMG against that posterior (GraphEvaluation): the log-ratio to the best graph, the
    credible level (the mass ranked strictly above, ties half; a Bayesian tail
    probability, not a p-value), one GraphClaim per statement with its probability and
    Monte Carlo error, the class probability of a CPDAG, the expected SHD against the
    between-draw baseline (with the former's Monte Carlo error), and P(acyclic) under raw
    independent marginals. When every draw agrees, a Monte Carlo error is the rule-of-three
    bound 3 / draws (bound on FeatureProbability, GraphClaim and
    AdjustmentGroup) rather than 0, and under the exact
    recursion the candidate's own share of its credible level is exact. A bidirected
    edge or latent node is listed as not assessable.

  • Effects over a posterior over DAGs (DEC-082 decision 6). adjustment_probability(samples, treatment, outcome, adjustment) is the posterior probability that a set satisfies the
    adjustment criterion. average_effect(samples, data, treatment, outcome) groups the
    DAGs by the adjustment set identification picks, estimates once per set (the family
    default, seeded; strategy=, learner=, rule="minimal") and returns an
    AveragedEffect: the AdjustmentGroups (set, mass with its Monte Carlo error, DAG
    count, estimate, standard error), the mixture mean with its Monte Carlo error from the
    masses, and its variance by the law of
    total variance, and the zero-effect (no directed path), unidentified (a variable the
    data lacks makes every set invalid) and truncated (beyond max_sets) masses, the last
    two left out of the mixture and reported.

  • The BGe score (score="bge"): the linear-Gaussian marginal likelihood of Geiger and
    Heckerman with the Kuipers, Moffa and Heckerman correction and an empirical-Bayes prior
    mean (the sample mean; BiDAG uses zero, which agrees on centred data), decomposable and
    score-equivalent, for graph_posterior and the score-based searches.
    Score.marginal_likelihood says whether a score is one (BDeu, BGe).

Changed

  • A decode with knowledge, hard-only knowledge included, now audits it: one extra
    knowledge-free decode, which can cost more than the constrained one, and a
    KnowledgeConflictWarning on strong conflicts. Code that turns warnings into
    errors will see it; pass audit=False or filter the category.
  • The decoder aliases "expected_count" and "soft_f1" now resolve to the new
    expected_count_v2 and soft_f1_v2, which take their edge budget from the pair
    states with the hard knowledge applied (a forbidden direction holds no mass, a
    required one all of it), so a soft statement near 1 gives the hard result's budget.
    expected_count_v1 and soft_f1_v1 keep their 0.2 results; without knowledge the
    two versions agree.
  • Glasso.fit validates the knowledge first and raises on a contradiction (a
    confounded pair under an adjacency ban or outside a restrict_to support, for
    example) instead of dropping part of it.
  • The discovery searches (PC, FCI, RFCI, GES, BOSS, GRaSP, HillClimb,
    AStar, MMPC, DirectLiNGAM, Glasso, PCMCI) and bootstrap raise
    ValueError on soft statements instead of dropping them; pass knowledge.hard().
  • forbid_adjacency(a, b) also records the pair in a new nonadjacent field: an
    adjacency ban rules out a bidirected edge too, while forbid(a, b) with
    forbid(b, a) no longer does. So confound(a, b) with the two directed bans means
    "confounded, and neither causes the other": it is not a conflict, the decoders
    return a <-> b, FCI and RFCI keep the pair adjacent instead of pruning it, MMPC
    keeps it in its skeleton and glasso no longer zeroes it. forbid_adjacency(a, b)
    and the two directed bans no longer compare equal. The repr is unchanged. Every
    nonadjacent pair also forbids both directed edges, however the object is built.
  • A DomainKnowledge built directly with both directions of a pair in forbidden
    (the constructor or dataclasses.replace) no longer bans a bidirected edge on
    that pair; use forbid_adjacency or nonadjacent= for that.
  • DomainKnowledge.apply_to keeps a bidirected edge on a pair forbidden in both
    directions; only an adjacency ban removes every mark.
  • DomainKnowledge.conflicts() reports a confounded pair under an adjacency ban.
    violations() reads a node in graph.latent as a hidden cause: a latent node with
    two or more observed children joins each pair of them as a bidirected edge would
    (for confound, adjacency bans and a skeleton restriction), and edges at a latent
    node count against neither a root nor a sink.
  • ergodic.welt.discover(data, model, knowledge=k) without decoder= decodes with
    greedy at the model's native threshold (greedy's default rule when the model
    records none), as decoder="greedy" does, instead of raising. Confounded knowledge
    is no longer refused before the paid run; knowledge naming unknown variables or
    contradicting itself still is. Without knowledge and without a decoder nothing
    changes.
  • Pickles of DomainKnowledge from 0.2.x load, with no soft statements. A pair the
    pickle forbids in both directions becomes an adjacency ban (nonadjacent), its
    0.2 meaning, so it still rules out a bidirected edge.
  • The welt extra requires welt-client>=0.14, the release that serves Chronos-2
    forecasting (DEC-085): WeltForecaster submits through
    welt.Client.submit_forecast and waits with the SDK's Job.result.