Skip to content

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.