The Automatic-Modeling Contract¶
An explicit statement of what optimize(data) / get_estimator(data) may infer and what it may
not (worklist I6.1). The automatic path is a convenience for getting a defensible first model from raw
data; this page pins its promises so a caller knows exactly what it will and will not do.
Supported input containers¶
A sequence of observations — a
list(or any re-iterable sequence) of items. Each item may be a scalar, a fixed-lengthtuple(a record), or a numeric vector.Re-iterability is required. The detector reads the data more than once (to profile field types, then to fit and score). A one-shot iterator/generator is materialized to a list first rather than being silently half-consumed, so a generator input yields a determinate model, not a corrupted one.
Pre-encoded
enc_datais not a valid input for inference — automatic modeling needs the raw values to read their types.
Per-field schema inference¶
For each field (a scalar sequence is a one-field record), the detector picks a family from the values it sees. Measured behavior:
Field values |
Inferred family |
|---|---|
floats |
|
non-negative integer counts |
|
booleans |
|
low-cardinality strings/labels |
|
high-cardinality identifiers (≈ unique per row) |
|
numeric vectors (fixed length) |
|
fixed-length record (tuple) |
|
sequences / sets / graphs |
the corresponding structured family, when the values match its shape |
High-cardinality identifiers are ignored on purpose. A field whose values are (almost) all distinct
carries no distributional signal a density model can use; modeling it would overfit one parameter per row.
The detector routes such a field to an Ignored leaf rather than inventing a distribution for it.
Missing values¶
Missing entries are handled per the family’s missing-data contract (see Numerical Stability and Missing Data); caller-owned data is not rewritten in place to hide missing or non-finite values.
None, NaN, pd.NA, and pd.NaT are the absence sentinels: a field carrying any of them
fits an OptionalDistribution whose missingness rate is estimated alongside the base family.
A native +inf/-inf, by contrast, is a legitimate (if unusual) numeric value, not an
absence – but the automatic detector gives it the same generative-missingness treatment anyway
(its own fitted-rate OptionalDistribution(..., missing_value=+/-inf), one wrapper per sign
present) rather than rejecting it, so the auto-built model stays a proper normalized law instead of
refusing to fit. This is auto-inference’s own choice, not the family-level default: the same
family, given explicitly (bypassing estimator=None), follows the stricter contract above and
rejects a non-finite observation outside its support (GaussianDistribution requires support x in
(-inf,inf)). Because the two routes disagree, auto-inference’s reclassification is disclosed
rather than silent: get_estimator(data) (and therefore optimize(data) / fit(data) with
estimator=None) raises a UserWarning naming the affected field(s) whenever this happens.
Dependence between fields¶
The default record model is independent per field (a Composite). optimize(data) and fit(data)
— called with no estimator already supplied (estimator=None, the default, with automatic
structure="auto" search) — will upgrade to a dependence model for tuple-typed rows: a learned Bayesian
network, or (for all-continuous rows) a copula, only when it beats the independent baseline by BIC on the
same data; otherwise it keeps independence. It does not assume dependencies it cannot evidence.
get_estimator(data) and mixle.task.recommend.recommend_model(data) each already build a concrete
estimator before any fitting happens, so neither reaches the no-estimator search path directly: their own
only dependence-capturing route is a joint multivariate-Gaussian estimator, and only for fixed-length,
fully-observed, purely-numeric vector rows (never tuples) — narrower than the BN/copula upgrade above.
propose(data) (below) includes the no-estimator search itself as one of its candidates, so it reaches
the full BN/copula upgrade too, scored on held-out data exactly like every other candidate.
Selection and validation¶
get_estimator(data)returns a single inferred estimator (structure only; no held-out split).propose(data)builds a verified frontier: it fits each candidate on a train split and scores it on a held-out split (holdout=0.25by default), ranking by held-out mean log-density — the ranking is out-of-sample, not a guess. One candidate is the no-estimator structure search itself (see “Dependence between fields”), so the copula/Bayesian-network upgrade competes on held-out data alongside every other candidate rather than needing a separateoptimize(data)call. The frontier search is bounded bymax_candidates/timeout(worklist I6.5) — except atmax_candidates=0/timeout=0.0, which skip verification entirely and fall back to the raw heuristic recommendation, disclosed (not hidden) viaModel.notes/Model.frontier.
What it will not do¶
It will not claim universal correct family recovery. Genuinely nested/overlapping families (Exponential vs Gamma, LogNormal vs Inverse-Gaussian) are documented ambiguity regions — see the measured confusion matrix in
model_selection_benchmark_test(worklist I6.4).It will not model a high-cardinality identifier, fabricate a family without evidence, assume cross-field dependence it cannot justify by BIC, or silently consume a one-shot iterator.