Skip to content

bagging

Generic bootstrap bagging — the default epistemic-variance source.

No mainstream GBM exposes usable per-feature epistemic variance natively (see docs/DECISIONS.md), so TRIAD refits K bootstrap replicas and reads variance from their disagreement: per-bin shape variance in exact mode, per-instance attribution variance in approximate mode.

fit_bagged

fit_bagged(
    fit_fn: Callable[[DataFrame, ndarray, int], ModelT],
    X: DataFrame,
    y: ndarray,
    *,
    n_bags: int = 8,
    seed: int = 0
) -> list[ModelT]

Fit n_bags replicas of a model on bootstrap resamples of (X, y).

fit_fn(X_boot, y_boot, bag_seed) receives a distinct seed per bag. Bootstraps that collapse to a single class (binary targets with rare positives) are redrawn.

Source code in src/triadxai/bagging.py
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
def fit_bagged(
    fit_fn: Callable[[pd.DataFrame, np.ndarray, int], ModelT],
    X: pd.DataFrame,
    y: np.ndarray,
    *,
    n_bags: int = 8,
    seed: int = 0,
) -> list[ModelT]:
    """Fit ``n_bags`` replicas of a model on bootstrap resamples of (X, y).

    ``fit_fn(X_boot, y_boot, bag_seed)`` receives a distinct seed per bag.
    Bootstraps that collapse to a single class (binary targets with rare
    positives) are redrawn.
    """
    y = np.asarray(y)
    n = len(X)
    if len(y) != n:
        raise ValueError(f"X and y length mismatch: {n} vs {len(y)}")
    classes = np.unique(y)
    check_classes = len(classes) <= 20  # classification-like target
    if check_classes and len(classes) < 2:
        raise ValueError("y contains a single class; cannot fit bagged models")
    rng = np.random.default_rng(seed)
    models: list[ModelT] = []
    for bag in range(n_bags):
        for _ in range(_MAX_RESAMPLE_ATTEMPTS):
            idx = rng.integers(0, n, size=n)
            if not check_classes or len(np.unique(y[idx])) == len(classes):
                break
        else:
            raise ValueError(
                f"could not draw a bootstrap containing all classes in "
                f"{_MAX_RESAMPLE_ATTEMPTS} attempts (bag {bag})"
            )
        X_boot = X.iloc[idx].reset_index(drop=True) if hasattr(X, "iloc") else X[idx]
        bag_seed = seed * 10_000 + bag + 1
        models.append(fit_fn(X_boot, y[idx], bag_seed))
    return models

phi_variance

phi_variance(
    predict_contrib_fns: Sequence[
        Callable[[ndarray], ndarray]
    ],
    X: ndarray,
) -> np.ndarray

Per-instance, per-feature variance of attributions across replicas.

Each callable maps X to an (n_samples, n_features) attribution matrix; the result is the elementwise sample variance (ddof=1) across replicas.

Source code in src/triadxai/bagging.py
65
66
67
68
69
70
71
72
73
74
75
76
77
def phi_variance(
    predict_contrib_fns: Sequence[Callable[[np.ndarray], np.ndarray]],
    X: np.ndarray,
) -> np.ndarray:
    """Per-instance, per-feature variance of attributions across replicas.

    Each callable maps X to an (n_samples, n_features) attribution matrix;
    the result is the elementwise sample variance (ddof=1) across replicas.
    """
    if len(predict_contrib_fns) < 2:
        raise ValueError("need at least 2 replicas to estimate variance")
    stack = np.stack([fn(X) for fn in predict_contrib_fns], axis=0)
    return stack.var(axis=0, ddof=1)