X_train_clean = X_train.drop(columns=["post_hoc_flag_count"]) detector = AdversarialDriftDetector(auc_threshold=0.65) detector.fit(X_train_clean) X_production = X_train_clean.sample(1000, random_state=1).reset_index(drop=True) # each column's own range stays plausible on its own - it's the relationship # between them that flips, which is exactly what a per-feature check misses shift_mask = X_production["num_transactions_30d"] > X_production["num_transactions_30d"].median() X_production.loc[shift_mask, "avg_transaction_value"] *= 0.6 X_production.loc[~shift_mask, "num_transactions_30d"] = ( X_production.loc[~shift_mask, "num_transactions_30d"] * 1.8 ).astype(int) result = detector.check(X_production) print(result["auc"], result["drift_detected"]) print(result["top_drift_features"])