원클릭으로
drift-detection
Run and interpret DATA drift (PSI) AND CONCEPT drift (sliced performance) for an ML service
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
메뉴
Run and interpret DATA drift (PSI) AND CONCEPT drift (sliced performance) for an ML service
Codex 또는 Claude로 설치 이 Prompt를 복사해 Codex, Claude 또는 다른 어시스턴트에 붙여 넣으면 Skill 페이지를 검토하고 설치를 진행할 수 있습니다.
SOC 직업 분류 기준
Scaffold and run batch scoring jobs (CronJob + Parquet output) that reuse the service's model + feature-engineering code without opening the live API
Root-cause a performance alert using sliced metrics + ground-truth
Review cloud costs against budget and identify optimization opportunities
Debug ML inference issues — latency spikes, wrong predictions, event loop blocking
Deploy ML service to EKS with Kustomize overlays and IRSA
Deploy ML service to GKE with Kustomize overlays and Workload Identity
| name | drift-detection |
| description | Run and interpret DATA drift (PSI) AND CONCEPT drift (sliced performance) for an ML service |
| allowed-tools | ["Read","Grep","Glob","Bash(python:*)","Bash(kubectl:*)","Bash(curl:*)"] |
| when_to_use | Use when checking data OR concept drift, interpreting PSI/AUC metrics, configuring drift thresholds, diagnosing sliced alerts, or deciding whether retraining is needed. Examples: 'check drift for bankchurn', 'PSI alert fired', 'AUC dropped', 'country=ES showing performance regression', 'interpret the performance report' |
| argument-hint | <service-name> |
| arguments | ["service-name"] |
| authorization_mode | {"run_psi_check":"AUTO","run_sliced_metrics":"AUTO","interpret_thresholds":"AUTO","trigger_retrain":"CONSULT","silence_alert":"STOP","escalation_triggers":[{"psi_over_2x_threshold":"STOP"},{"sliced_auc_drop_over_5pp":"CONSULT"},{"heartbeat_missing":"CONSULT"}]} |
Two complementary layers (ADR-006):
Always investigate data drift FIRST (cheaper, faster). Escalate to concept drift analysis when (a) PSI alert fires and you need to confirm impact, or (b) a performance alert fires directly (AUC below threshold).
| PSI Value | Status | Action | Exit Code |
|---|---|---|---|
| < 0.10 | Stable | No action | 0 |
| 0.10 – 0.20 | Warning | Monitor, increase check frequency | 1 |
| > 0.20 | Alert | Trigger retraining | 2 |
ALWAYS use quantile-based bins (not uniform):
breakpoints = np.percentile(reference, np.linspace(0, 100, bins + 1))
Uniform bins can produce empty bins at extremes → PSI dominated by epsilon noise.
| Feature Type | Problem with PSI | Alternative |
|---|---|---|
| Time series (seasonal) | PSI flags every seasonal change as "drift" | Year-over-Year comparison (same period last year) |
| Text/NLP features | PSI not meaningful for text | OOV (Out-of-Vocabulary) rate: warning > 20%, alert > 35% |
| Low-cardinality categorical | Quantile bins don't work with 3-5 categories | Categorical PSI variant: bins = unique categories |
| Boolean features | Only 2 bins → unstable PSI | Simple proportion test (chi-squared) |
exit 0 → all features stableexit 1 → warning-level drift (monitor)exit 2 → alert-level drift (retraining needed, GitHub Issue created)python src/{service}/monitoring/drift_detection.py \
--reference data/reference/{service}_reference.csv \
--current data/production/{service}_latest.csv \
--output drift_report.json
For each feature, review:
{
"feature_name": "age",
"psi": 0.15,
"status": "warning",
"reference_mean": 45.2,
"current_mean": 48.1,
"bins_detail": [...]
}
| Pattern | Likely Cause | Action |
|---|---|---|
| Single feature PSI high | Upstream data change | Investigate ETL pipeline |
| All features PSI high | Data source change | Full retraining |
| Temporal features PSI high | Seasonal pattern | Use YoY comparison |
| Categorical OOV rate up | New categories in production | Update schema + retrain |
Each feature needs per-feature thresholds with domain reasoning:
THRESHOLDS = {
"stable_feature": {"warning": 0.10, "alert": 0.20},
"volatile_feature": {"warning": 0.15, "alert": 0.30}, # Higher natural variance
"categorical_feature": {"warning": 0.10, "alert": 0.20},
}
For temporal data (e.g., time series, seasonal patterns):
from prometheus_client import CollectorRegistry, Gauge, push_to_gateway
registry = CollectorRegistry()
psi_gauge = Gauge('service_psi_score', 'PSI per feature', ['feature'], registry=registry)
for feature, psi in results.items():
psi_gauge.labels(feature=feature).set(psi)
push_to_gateway('pushgateway:9091', job='drift-detection', registry=registry)
# Check CronJob status
kubectl get cronjob drift-detection -n {namespace}
# Check last job
kubectl get jobs -l app=drift-detection -n {namespace} --sort-by=.metadata.creationTimestamp
# Check heartbeat
curl -s 'http://prometheus:9090/api/v1/query?query=drift_detection_last_run_timestamp'
PSI tells you features CHANGED, not that performance DEGRADED. For degradation you need ground truth (ADR-006). The sliced performance monitor does this:
python -m src.{service}.monitoring.performance_monitor \
--predictions data/predictions_log \
--labels data/labels_log \
--slices configs/slices.yaml \
--window 24h \
--baseline models/baseline_metrics.json \
--output reports/performance.json --push-metrics
Interpret reports/performance.json:
status: ok → no actionstatus: warning → investigate within 4h (P2)status: alert → retraining candidate (P1/P2)status: insufficient_data → ground-truth pipeline issue — investigate
{service}_performance_last_run_timestampjoined_count < predictions_count → label arrival lag — expected if labels
delayed; persistent gap is a ground-truth bugWhen an alert says SlicedAUCBelowAlert country=ES, the RCA pattern is:
cat reports/performance.json | jq '.slices.by_country.ES'jq '.features' drift_report.json | grep -A2 <feature>
sample_size >= min_samples_per_slice must hold; otherwise
the reading is noise and you should wait for more data, NOT retrain.Slices are defined in configs/slices.yaml. NEVER add high-cardinality columns
(user_id, transaction_id) — they blow up Prometheus labels and make grouping
meaningless. Stick to bounded categoricals (country, channel, segment) or
numeric bins.
If data drift AND concept drift agree (PSI up + AUC down for same slice):
gh workflow run retrain-{service}.yml \
-f reason="Concept drift: AUC={auc} for slice {slice}={value}; PSI={psi} on {feature}"
The retrain workflow now runs Champion/Challenger offline BEFORE promotion (ADR-008). Do NOT expect retraining to automatically replace the champion — statistical superiority must be proven.
Chain to /retrain workflow for the full retraining process.
The skill is complete when ALL of the following hold:
configs/slices.yaml (ADR-007)monitor, retrain, or investigate — never silentdrift_detection_last_run_timestamp pushed to Prometheus Pushgateway
(heartbeat — gap absence is itself an alert per ADR-009)ops/last_drift_report.json updated so risk_context.py can read it
on the next agent decisionpsi_over_2x_threshold fired: agent emitted [AGENT MODE: STOP]
and did NOT proceed to retrain or alert silencingops/audit.jsonl with operation=drift_check,
summary of severities, and any chain (retrain / RCA / incident)concept-drift-analysis (deeper, single-slice)model-retrain (consequence path when PSI + AUC agree)