Skip to content

Conditional table sensitivity and discrete effects

sensitivity_from_table estimates the sensitivity of a fitted response surface. Sobol reports first-order variance indices; Morris reports elementary effects. Neither method in this wrapper assigns an index to discrete capacity q, categorical rules or profiles. A pooled analysis that drops those inputs can hide their effects or attribute them to correlated continuous settings.

Varying discrete/categorical factors are refused by default. Provide all factor definitions and either analyze a table where those factors are observed constants or use condition_on to select an actually observed stratum. Remaining varying discrete/categorical inputs are still refused. Every observed constant, including a continuous factor made constant by conditioning, is bound in prediction and excluded from screening. At least one continuous factor must vary in that stratum. Missing factor values and unobserved/invalid conditions fail explicitly.

Varying undeclared configuration keys are also refused: leaving q out of the factor list must not quietly hide it. Genuine config bookkeeping such as a seed can be explicitly listed in metadata_keys; these exclusions are reported in omitted_config_keys. This declaration asserts that the key is not an input whose effect is being studied. It cannot override a declared model factor. Put trial bookkeeping in ResultsTable.metadata where practical. Undeclared constant config fields are reported as omitted, without inventing a factor definition.

Complete synthetic workflow

This factorial has a known q × log(prior) interaction plus a continuous z effect. The replicate noise is deliberately shared across designs, so paired differences cancel it exactly. This is an analytic fixture, not uncertainty or coverage qualification for a stochastic simulator.

Install the optional trade-study[design,surrogate] dependencies.

import math
import numpy as np
from trade_study import Factor, FactorType, ResultsTable, sensitivity_from_table
from trade_study.paired import paired_difference

configs, scores, metadata = [], [], []
design = 0
for q in (2, 4):
    for prior in (1.0, 10.0, 100.0):
        for z in (0.0, 0.5, 1.0):
            for rep in range(4):
                configs.append({"q": q, "prior": prior, "z": z})
                scores.append([(q - 2) * math.log10(prior) / 2 + z + 0.01 * rep])
                metadata.append({"design_point": design, "rep": rep})
            design += 1
table = ResultsTable(configs, np.asarray(scores), ["loss"], metadata=metadata)
factors = [
    Factor("q", FactorType.DISCRETE, levels=[2, 4]),
    Factor("prior", FactorType.CONTINUOUS, bounds=(1.0, 100.0), log_scale=True),
    Factor("z", FactorType.CONTINUOUS, bounds=(0.0, 1.0)),
]
conditional = sensitivity_from_table(
    table, factors, condition_on={"q": 4}, cv_group_by="design", cv_folds=3,
    n_estimators=30, n_trajectories=32, seed=7, warn_below_r2=None,
)
assert conditional.screened_factors == ("prior", "z")
assert conditional.fixed_factors == {"q": 4}
assert conditional.source_rows == tuple(range(36, 72))
assert conditional.validation_scheme == "held-out-designs"

effects = {}
for prior in (1.0, 10.0, 100.0):
    comparison = paired_difference(
        table, {"q": 4, "prior": prior, "z": 0.5},
        {"q": 2, "prior": prior, "z": 0.5}, "loss", method="t",
    )
    effects[prior] = comparison.mean
assert np.allclose(list(effects.values()), [0.0, 1.0, 2.0])

The direct differences vary with prior setting, exposing the interaction. Each comparison fixes every other design factor and pairs genuine shared replicate identities. paired_rank can compare several capacities within such a fixed setting after selecting that subtable. Ranking an entire q × prior grid instead compares joint designs. Shared integer labels alone do not establish common-random-number or independent-replicate assumptions in a real application. Retain failures, unmatched/nonfinite pairs, the actual paired denominator and the chosen interval/multiplicity policy. The helper's finite-pair handling does not justify treating failed runs as missing at random.

Conditional Sobol indices at different q values are separately normalized by different conditional variances. Their difference is not an estimate of the effect of q or of its interaction. Use direct factorial/paired contrasts for those questions. Do not relabel q as continuous to bypass the refusal.

Validation and target distribution

cv_group_by="design" holds out complete identical declared configurations, keeping their repeated rows together. cv_group_by=["regime_factor", ...] holds out groups defined by declared configuration factors; this can test unseen regimes when those factors and the selected stratum support that interpretation. Both group options unset retain shuffled row validation. These answer different questions, and row validation can look strong when related designs/regimes generalize poorly.

Design-based grouping keeps repetitions of one configuration together. A shared simulated dataset reused across different configurations can still cross its folds. To hold out complete shared datasets/replicates, supply an explicit cv_groups vector aligned with every original table row, before conditioning. The wrapper validates the complete vector, then selects its source_rows.

shared = sensitivity_from_table(
    table, factors, condition_on={"q": 4},
    cv_groups=[meta["rep"] for meta in table.metadata], cv_folds=2,
    n_estimators=20, n_trajectories=32, seed=7, warn_below_r2=None,
)
assert shared.cv_group_source == "external"
assert shared.validation_scheme == "held-out-external-groups"
assert shared.validation_groups == [0, 1, 2, 3] * 9

cv_groups and cv_group_by are mutually exclusive. Explicit labels can come from a dataset ID, replicate ID or another caller-declared partition; no meaning is inferred from metadata_keys or automatic table-field discovery. Labels may be finite numbers, strings, bytes or tuples of those. Missing/nonfinite labels, unsupported objects, unordered/mapping vectors and length/shape mismatches are rejected, including in rows excluded by conditioning. Equal numeric labels such as 1, 1.0 and NumPy scalar equivalents share a group, also inside tuples.

The result retains external labels, stable integer group IDs and cv_group_source. Holding out datasets tests a different target from holding out designs. This route supplies one partition: grouping (design, replicate) pairs does not independently keep all designs and all replicates together. Crossed dependencies or other split policies need a separately justified validation design. Grouping does not validate its own scientific assumptions.

TableSensitivity preserves selected original source_rows, screened and fixed factors, requested conditions, excluded config keys, actual grouping names/ids, grouped surrogate_cv_r2/surrogate_cv_rmse, external source/labels where selected, and shuffled-row companions. With fewer than two surviving groups for an observable, grouped metrics remain NaN; row metrics never replace them. cv_folds_requested is a maximum, not a promise that every observable has that many usable folds. Nonfinite scores retain the existing per-observable surrogate filtering, so available groups can differ.

The recorded sampling_factors define independent uniform sampling over each declared linear coordinate or over its log coordinate for log_scale=True. Sobol/Morris queries can therefore use combinations never present in the saved table. support_ranges retains marginal observed ranges per fitted observable, and sampling beyond those ranges emits a warning once per observable. Marginal range coverage and good CV do not establish joint response-surface support, validity outside observed combinations, or an appropriate input distribution. Restrict the declared bounds or collect a suitable design when needed. A broad mixed-factor estimator, dependent-input sensitivity, noise decomposition and uncertainty for the estimated indices require separate qualification.

Existing continuous-only calls retain their default method and row CV, while returning explicit scope/validation information. Calls that previously silently dropped varying inputs now fail; supply an observed stratum or use direct contrasts. Existing frozen studies and environments need not be migrated.

Required CI also runs the scope, GP/RF and grouped-surrogate checks on a named older CPU stack: Python 3.11, NumPy 1.24.0, scikit-learn 1.3.2, SALib 1.5.0, SciPy 1.11.4 and pandas 2.0.3. Missing dependencies or skipped selected cases fail that job. Current supported Python jobs resolve their own optional dependencies; this compatibility point does not qualify every intervening version.