Buckets¶
Stage 2 turns each feature's decision points into a partition of its domain. The point of this construction: within one bucket, every tree in the ensemble routes the value the same way. The buckets are exactly the resolution at which the model can distinguish feature values — a shift that stays inside a bucket is invisible to the model, and SWIFT will treat it that way.
from swift.bucketing import build_buckets, build_all_buckets
bucket_set = build_buckets(decision_points, feature_name) # one feature
bucket_sets = build_all_buckets(decision_points_dict) # all features
# Returns: BucketSet / dict[str, BucketSet]
The layout¶
For sorted thresholds \(t_1 < t_2 < \dots < t_m\), build_buckets produces \(m + 2\)
buckets:
- bucket 0 — a null bucket that catches missing values (NaN),
- bucket 1 — \((-\infty, t_1)\),
- buckets 2 … \(m\) — \([t_1, t_2), \dots, [t_{m-1}, t_m)\),
- bucket \(m{+}1\) — \([t_m, +\infty)\).
Intervals are half-open on the right: a value exactly equal to a threshold \(t_k\)
belongs to the bucket whose lower bound is \(t_k\). For example, if a feature has
decision points [0.5, 1.2, 3.0], the resulting bucket set is:
| Bucket | Interval |
|---|---|
| B0 | null bucket (NaN) |
| B1 | \((-\infty, 0.5)\) |
| B2 | \([0.5, 1.2)\) |
| B3 | \([1.2, 3.0)\) |
| B4 | \([3.0, +\infty)\) |
The null bucket¶
Bucket 0 always exists, for every feature, whether or not the reference data contains
missing values. NaN (and None) values map to it and to nothing else — the numeric
buckets never match a missing value. Because the null bucket later gets its own mean
SHAP level (Normalization), missingness is a first-class region of
the feature (missing values): a change in the missing-value rate is
visible to the drift test just like a
shift between numeric buckets.
The catch-all bucket¶
A feature with no decision points — the model never splits on it — gets a single catch-all bucket \((-\infty, +\infty)\) next to its null bucket, two buckets in total. After the SHAP transformation, every non-missing value of such a feature maps to the same number, both samples become identical in SHAP space, and the feature's SWIFT score is 0. This is intended behavior, not a bug: a feature the model ignores cannot raise a drift alarm.
The data model¶
Buckets are frozen dataclasses defined in swift.types:
class BucketType(Enum):
NULL = "null"
NUMERIC = "numeric"
CATEGORICAL = "categorical"
@dataclass(frozen=True)
class Bucket:
bucket_type: BucketType
index: int
lower: float = float("-inf")
upper: float = float("inf")
categories: Optional[frozenset] = None
mean_shap: Optional[float] = None
A BucketSet bundles one feature's buckets with its sorted decision_points array and
offers assign_bucket(value) (returns the containing bucket's index, ValueError if
none matches), get_mean_shap(bucket_index) (returns 0.0 with a warning if the bucket
has no mean SHAP assigned, KeyError for an unknown index), and the num_buckets
property.
mean_shap is None at this stage — Stage 3 fills it in.
Categorical buckets
BucketType.CATEGORICAL (a bucket holding a frozenset of category values) exists
in the data model, and the transformation has an element-wise fallback for it — but
the standard pipeline never constructs categorical buckets: build_buckets emits
only null and numeric buckets, and categorical splits are skipped at
extraction.
Next stage: each bucket gets a mean SHAP value in Normalization. Full signatures: API reference — Bucketing and Types.