Aggregation¶
The pipeline's per-feature outcomes — scores from Stage 4, p-values and decisions from Stage 5 — finally roll up into one model-level verdict. Aggregation answers the question an alerting system actually asks: is this model drifting, and where?
from swift.aggregation import aggregate_scores, compute_importance_weights
agg = aggregate_scores(scores, weights=None)
# Returns: AggregatedScores(swift_max, swift_mean, max_feature, swift_weighted)
weights = compute_importance_weights(shap_values, feature_names)
# Returns: dict[str, float] — non-negative weights summing to 1
Three aggregation strategies¶
Given per-feature scores \(\text{SWIFT}_j\):
swift_max— the score of the most-drifted feature, with its name inmax_feature. The right statistic for alerting: one badly drifted feature is a problem even if the average looks fine.swift_mean— the unweighted mean across features: overall drift pressure.swift_weighted— an importance-weighted sum \(\sum_j w_j \, \text{SWIFT}_j\), computed only when aweightsdict is passed (otherwiseNone).
The natural weights come from compute_importance_weights: normalized mean absolute
SHAP per feature on the reference data,
so drift in the model's workhorse features counts more than drift in marginal ones. (If all SHAP values are zero, the weights fall back to uniform.)
SWIFTMonitor.test() calls aggregate_scores without weights, so the SWIFTResult
it returns always has swift_weighted=None — compute weighted aggregates yourself from
monitor.shap_values_ and the score dict when you want them.
The result object¶
Everything from a test() call lands in a SWIFTResult (a frozen dataclass from
swift.types):
| Field / property | Meaning |
|---|---|
feature_results |
tuple of per-feature FeatureSWIFTResult records |
swift_max |
maximum SWIFT score across features |
swift_mean |
mean SWIFT score across features |
swift_weighted |
importance-weighted score (None unless computed with weights) |
alpha |
significance level used for testing |
correction_method |
the CorrectionMethod applied |
num_features |
number of features monitored |
num_drifted |
number of features flagged as drifted |
drifted_features |
names of the flagged features |
Each FeatureSWIFTResult carries the per-feature detail:
| Field | Meaning |
|---|---|
feature_name |
the feature |
swift_score |
Wasserstein distance on the SHAP-transformed distributions |
wasserstein_order |
WassersteinOrder.W1 or .W2 |
p_value |
permutation-test p-value (None if not computed) |
is_drifted |
drift flag after multiple testing correction |
num_buckets |
number of buckets used for the feature (incl. the null bucket) |
A typical read:
result = monitor.test(X_mon)
result.drifted_features # ('utilization', 'income')
result.num_drifted # 2
result.swift_max # 0.0834
for fr in result.feature_results:
status = "DRIFTED" if fr.is_drifted else "ok"
print(f"{fr.feature_name}: score={fr.swift_score:.4f}, p={fr.p_value:.4f} [{status}]")
plot_swift_scores consumes SWIFTResult objects directly — see
Visualization.
That closes the pipeline. Where to go from here: Configuration for tuning every parameter, or the API reference — Aggregation and Types for full signatures.