detectkit 0.50.0__tar.gz → 0.51.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {detectkit-0.50.0/detectkit.egg-info → detectkit-0.51.0}/PKG-INFO +1 -1
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/__init__.py +1 -1
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/grid_search.py +20 -6
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/rules/autotune.md +4 -3
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/rules/cli.md +1 -1
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/rules/detectors.md +23 -3
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/skills/dtk-tune/SKILL.md +4 -1
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/detectors/statistical/_windowed.py +55 -2
- detectkit-0.51.0/detectkit/tuning/assets/tune.js +187 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/tuning/payload.py +1 -0
- {detectkit-0.50.0 → detectkit-0.51.0/detectkit.egg-info}/PKG-INFO +1 -1
- detectkit-0.50.0/detectkit/tuning/assets/tune.js +0 -187
- {detectkit-0.50.0 → detectkit-0.51.0}/LICENSE +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/MANIFEST.in +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/README.md +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/channels/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/channels/base.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/channels/branding.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/channels/email.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/channels/factory.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/channels/mattermost.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/channels/slack.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/channels/telegram.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/channels/webhook.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/orchestrator/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/orchestrator/_base.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/orchestrator/_cooldown.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/orchestrator/_decision.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/orchestrator/_dispatch.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/orchestrator/_recovery.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/orchestrator/_replay.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/orchestrator/_types.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/alerting/orchestrator/orchestrator.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/_base.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/_types.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/autotuner.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/config_emitter.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/crossval.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/detector_select.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/distribution.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/labels.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/result.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/runner.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/scoring.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/seasonality_search.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/settings.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/autotune/window_select.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/_output.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/CLAUDE.section.md +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/rules/alerting.md +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/rules/metrics.md +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/rules/overview.md +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/rules/project.md +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/skills/dtk-autotune/SKILL.md +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/skills/dtk-feedback/SKILL.md +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/skills/dtk-new-metric/SKILL.md +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/assets/claude/skills/dtk-setup-project/SKILL.md +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/autotune.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/clean.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/init.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/init_claude.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/osi.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/run.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/test_alert.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/tune.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/ui.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/commands/unlock.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/cli/main.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/config/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/config/metric_config.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/config/metric_io.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/config/profile.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/config/project_config.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/config/validator.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/core/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/core/interval.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/core/models.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/_sql_manager.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/clickhouse_manager.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/_alert_states.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/_autotune_runs.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/_base.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/_datapoints.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/_detections.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/_maintenance.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/_metrics.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/_schema.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/_tasks.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/internal_tables/manager.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/manager.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/mysql_manager.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/postgres_manager.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/database/tables.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/detectors/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/detectors/base.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/detectors/factory.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/detectors/seasonality.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/detectors/statistical/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/detectors/statistical/iqr.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/detectors/statistical/mad.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/detectors/statistical/manual_bounds.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/detectors/statistical/zscore.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/loaders/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/loaders/metric_loader.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/loaders/query_template.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/orchestration/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/orchestration/error_dispatch.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/orchestration/task_manager/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/orchestration/task_manager/_alert_step.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/orchestration/task_manager/_base.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/orchestration/task_manager/_detect_step.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/orchestration/task_manager/_load_step.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/orchestration/task_manager/_types.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/orchestration/task_manager/manager.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/reporting/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/reporting/assets/report.js +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/reporting/builder.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/reporting/html_report.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/semantic/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/semantic/errors.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/semantic/exporter.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/semantic/importer.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/semantic/osi_model.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/semantic/query_gen.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/tuning/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/tuning/config_writer.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/tuning/html.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/tuning/server.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/ui/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/ui/assets/ui.js +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/ui/html.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/ui/jobs.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/ui/metric_files.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/ui/overview.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/ui/server.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/utils/__init__.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/utils/datetime_utils.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/utils/env_interpolation.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/utils/json_utils.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit/utils/stats.py +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit.egg-info/SOURCES.txt +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit.egg-info/dependency_links.txt +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit.egg-info/entry_points.txt +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit.egg-info/requires.txt +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/detectkit.egg-info/top_level.txt +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/pyproject.toml +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/requirements.txt +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/setup.cfg +0 -0
- {detectkit-0.50.0 → detectkit-0.51.0}/setup.py +0 -0
|
@@ -4,7 +4,7 @@ detectk - Anomaly Detection for Time-Series Metrics
|
|
|
4
4
|
A Python library for data analysts and engineers to monitor metrics with automatic anomaly detection.
|
|
5
5
|
"""
|
|
6
6
|
|
|
7
|
-
__version__ = "0.
|
|
7
|
+
__version__ = "0.51.0"
|
|
8
8
|
|
|
9
9
|
from detectkit.core.interval import Interval
|
|
10
10
|
from detectkit.core.models import ColumnDefinition, TableModel
|
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
"""Stage 3: bounded coordinate search over detector hyperparameters.
|
|
2
2
|
|
|
3
3
|
Not a Cartesian product — a greedy coordinate sweep per candidate type:
|
|
4
|
-
threshold → recency weighting → detrend (gated by a trend test) →
|
|
5
|
-
(with the trend-gated window tie-bias
|
|
6
|
-
re-sweep at the chosen window (the
|
|
7
|
-
is the cross-validated score
|
|
8
|
-
|
|
9
|
-
capped by
|
|
4
|
+
threshold → recency weighting → detrend (gated by a trend test) → stabilization
|
|
5
|
+
(anomaly-robust baseline) → window size (with the trend-gated window tie-bias
|
|
6
|
+
from window_select) → a final threshold re-sweep at the chosen window (the
|
|
7
|
+
threshold↔window coupling fix). The objective is the cross-validated score
|
|
8
|
+
(settings.metric, or the unsupervised objective when there are no labels).
|
|
9
|
+
Total evaluations stay in the low tens per type and are capped by
|
|
10
|
+
settings.max_candidates.
|
|
10
11
|
"""
|
|
11
12
|
|
|
12
13
|
from __future__ import annotations
|
|
@@ -149,6 +150,19 @@ def grid_search(
|
|
|
149
150
|
if ev is not None and ev.score > best.score + eps:
|
|
150
151
|
best, accepted["detrend"] = ev, detrend
|
|
151
152
|
|
|
153
|
+
# Axis 3b: stabilization (anomaly-robust baseline; adopt only when it
|
|
154
|
+
# clears the margin). Flagged points enter subsequent windows clamped
|
|
155
|
+
# to the bound they violated, so a sustained incident cannot inflate
|
|
156
|
+
# the band and mask its own tail — usually decisive on labeled data
|
|
157
|
+
# with long incidents, near-neutral on clean series. Swept before the
|
|
158
|
+
# window axis so select_window evaluates with the adopted baseline.
|
|
159
|
+
for stabilization in (None, "clamp"):
|
|
160
|
+
if stabilization == accepted.get("stabilization"):
|
|
161
|
+
continue
|
|
162
|
+
ev = tuner.safe_evaluate(detector_type, {**accepted, "stabilization": stabilization})
|
|
163
|
+
if ev is not None and ev.score > best.score + eps:
|
|
164
|
+
best, accepted["stabilization"] = ev, stabilization
|
|
165
|
+
|
|
152
166
|
# Axis 4: window size (large-window tie-bias, trend-gated in select_window).
|
|
153
167
|
window_best = select_window(tuner, detector_type, accepted, best, grid)
|
|
154
168
|
if window_best.score >= best.score - tuner.settings.window_tie_margin:
|
|
@@ -45,9 +45,10 @@ same windowed detectors and `detector_id` identity). The fastest path is the
|
|
|
45
45
|
detectors (`mad`/`zscore`/`iqr`) and cross-validation picks the winner — no
|
|
46
46
|
type is excluded by heuristic.
|
|
47
47
|
3. **Hyperparameters** — a bounded coordinate grid search over `threshold`,
|
|
48
|
-
recency weighting (and its **half-life** when adopted), detrending
|
|
49
|
-
`
|
|
50
|
-
|
|
48
|
+
recency weighting (and its **half-life** when adopted), detrending,
|
|
49
|
+
**stabilization** (`clamp`, adopted only when it clears the score margin)
|
|
50
|
+
and `window_size`, maximizing a cross-validated score, with a final
|
|
51
|
+
`threshold` re-sweep at the chosen window. Fold scores aggregate as
|
|
51
52
|
`mean − stability_lambda · downside_deviation` (downside-only, so a config that
|
|
52
53
|
scores *better* on recent folds isn't penalized; lower `stability_lambda` for a
|
|
53
54
|
regime-shift metric).
|
|
@@ -84,7 +84,7 @@ Full reference: `autotune.md`.
|
|
|
84
84
|
The **manual, interactive** sibling of `dtk autotune`. Opens a localhost browser
|
|
85
85
|
view of the metric's **real** persisted series and lets you turn the detector's
|
|
86
86
|
knobs (type — including **Manual bounds** with lower/upper sliders — threshold,
|
|
87
|
-
window, recency weighting + half-life, detrend, smoothing, **seasonality groups**,
|
|
87
|
+
window, recency weighting + half-life, detrend, stabilization, smoothing, **seasonality groups**,
|
|
88
88
|
**direction** (both/up/down), alert `consecutive_anomalies`) while the confidence
|
|
89
89
|
band and flagged anomalies **recompute live**. The whole screen is a **chart-first
|
|
90
90
|
cockpit**: ONE chart (the windshield) fills the view, the live metrics ride
|
|
@@ -66,6 +66,7 @@ expected interval for the current point.
|
|
|
66
66
|
window_weights: null # null (uniform) | exponential | linear
|
|
67
67
|
half_life: null # exponential half-life: int points or "3d"/"12h"
|
|
68
68
|
detrend: null # null | linear
|
|
69
|
+
stabilization: null # null (off) | clamp
|
|
69
70
|
# --- execution (NOT hashed) ---
|
|
70
71
|
start_time: "2024-01-01 00:00:00" # optional; when detection begins (default: loading_start_time)
|
|
71
72
|
batch_size: 500
|
|
@@ -161,6 +162,24 @@ Trade-off: a shorter `half_life` adapts faster but also "accepts" a real
|
|
|
161
162
|
sustained degradation as the new normal sooner. (`weight_decay` is a deprecated
|
|
162
163
|
alias for `half_life`; prefer `half_life`.)
|
|
163
164
|
|
|
165
|
+
## Stabilization — sustained incidents poisoning their own baseline
|
|
166
|
+
|
|
167
|
+
A sustained incident's anomalous points enter the trailing window and inflate
|
|
168
|
+
the spread / drag the center toward it, so the band widens mid-incident and the
|
|
169
|
+
detector stops flagging the tail ("the incident becomes the new normal") —
|
|
170
|
+
z-score (mean/std) is the most vulnerable; median/quartile-based mad/iqr resist
|
|
171
|
+
short incidents but still bend under a long one. `stabilization: clamp` fixes
|
|
172
|
+
this: once a point is flagged anomalous, subsequent trailing windows see it
|
|
173
|
+
**clamped to the confidence bound it violated** (winsorized) instead of the
|
|
174
|
+
raw value — only the statistics windows read the substituted history; scored
|
|
175
|
+
and persisted values are unchanged, and anomalies still render as anomalies.
|
|
176
|
+
Reach for it when alerting on metrics with occasional sustained incidents; it
|
|
177
|
+
composes with `window_weights`/`half_life` (where it matters most, since
|
|
178
|
+
recency weighting gives an ongoing incident's points more weight), `detrend`,
|
|
179
|
+
`smoothing`, `input_type` and seasonality groups, and is near-neutral on clean
|
|
180
|
+
series. Enabling it is a hashed change — it produces a new `detector_id` and
|
|
181
|
+
recomputes on the next run; existing configs that don't set it keep their ids.
|
|
182
|
+
|
|
164
183
|
## Feature compatibility
|
|
165
184
|
|
|
166
185
|
| Feature | mad | zscore | iqr | manual_bounds |
|
|
@@ -169,6 +188,7 @@ alias for `half_life`; prefer `half_life`.)
|
|
|
169
188
|
| `smoothing` | Yes | Yes | Yes | No |
|
|
170
189
|
| `window_weights` / `half_life` | Yes | Yes | Yes | No |
|
|
171
190
|
| `detrend` | Yes | Yes | Yes | No |
|
|
191
|
+
| `stabilization` | Yes | Yes | Yes | No |
|
|
172
192
|
| `seasonality_components` | Yes | Yes | Yes | No |
|
|
173
193
|
|
|
174
194
|
`manual_bounds` has no window, so window-based features don't apply.
|
|
@@ -177,9 +197,9 @@ alias for `half_life`; prefer `half_life`.)
|
|
|
177
197
|
|
|
178
198
|
Every parameter that affects results (threshold, window_size, min_samples,
|
|
179
199
|
seasonality_components, min_samples_per_group, input_type, smoothing*,
|
|
180
|
-
window_weights, half_life, detrend) is hashed into the
|
|
181
|
-
non-default values participate. Execution params
|
|
182
|
-
are **not** hashed.
|
|
200
|
+
window_weights, half_life, detrend, stabilization) is hashed into the
|
|
201
|
+
`detector_id` — only non-default values participate. Execution params
|
|
202
|
+
(`start_time`, `batch_size`) are **not** hashed.
|
|
183
203
|
|
|
184
204
|
Changing any hashed parameter creates a new `detector_id` and recomputes that
|
|
185
205
|
detector's detections from scratch on the next run; old rows stay under the
|
|
@@ -101,7 +101,10 @@ Walk the user to the mode that fits what they want to do:
|
|
|
101
101
|
**window size**, **recency weighting + half-life**, **detrend**, **smoothing**,
|
|
102
102
|
and **seasonality groups**; the band + anomalies + would-fire alerts recompute
|
|
103
103
|
on every change. The window-size / half-life readouts show the equivalent
|
|
104
|
-
wall-clock span.
|
|
104
|
+
wall-clock span. **Stabilization** (none / clamp) clamps a flagged point to
|
|
105
|
+
the bound it violated in later windows' statistics — try it when a sustained
|
|
106
|
+
incident visibly balloons the band mid-incident and the detector stops
|
|
107
|
+
flagging its tail. This is the core "turn the knobs yourself" loop.
|
|
105
108
|
|
|
106
109
|
- **Autotune** — *let the engine search, then refine.* Click **Run autotune** and
|
|
107
110
|
the **same engine `dtk autotune` uses** (seasonality → detector → grid → window,
|
|
@@ -48,6 +48,7 @@ _CHANGE_INPUT_TYPES = {"changes", "absolute_changes", "log_changes"}
|
|
|
48
48
|
_SMOOTHING_METHODS = {None, "ema", "sma"}
|
|
49
49
|
_WEIGHT_METHODS = {None, "exponential", "linear"}
|
|
50
50
|
_DETREND_METHODS = {None, "linear"}
|
|
51
|
+
_STABILIZATION_METHODS = {None, "clamp"}
|
|
51
52
|
|
|
52
53
|
|
|
53
54
|
class WindowedStatDetector(BaseDetector):
|
|
@@ -88,6 +89,18 @@ class WindowedStatDetector(BaseDetector):
|
|
|
88
89
|
compute statistics on values projected to the current point.
|
|
89
90
|
Recommended for metrics with a gradual trend so the slow
|
|
90
91
|
drift itself is not flagged as anomalous.
|
|
92
|
+
stabilization (str | None): None (default) or "clamp" — when a
|
|
93
|
+
point is flagged anomalous, subsequent trailing windows see
|
|
94
|
+
the confidence bound it violated (winsorized value) instead
|
|
95
|
+
of the observed value. Keeps a long incident from poisoning
|
|
96
|
+
the baseline (inflating spread / dragging the center toward
|
|
97
|
+
the incident), so the detector keeps flagging it instead of
|
|
98
|
+
adapting to it. Clamping to the bound (not the center) is
|
|
99
|
+
deliberate: substituting the band center feeds zero-deviation
|
|
100
|
+
points back into the spread statistics, which collapses the
|
|
101
|
+
band and cascades into false flags after a long incident.
|
|
102
|
+
The scored/persisted values are unchanged — only the
|
|
103
|
+
statistics windows read the substituted history.
|
|
91
104
|
|
|
92
105
|
All parameters that change detection output participate in the
|
|
93
106
|
detector ID hash (only non-default values are hashed).
|
|
@@ -124,6 +137,7 @@ class WindowedStatDetector(BaseDetector):
|
|
|
124
137
|
half_life: int | str | None = None,
|
|
125
138
|
weight_decay: float | None = None,
|
|
126
139
|
detrend: str | None = None,
|
|
140
|
+
stabilization: str | None = None,
|
|
127
141
|
):
|
|
128
142
|
super().__init__(
|
|
129
143
|
threshold=self.THRESHOLD_DEFAULT if threshold is None else threshold,
|
|
@@ -143,6 +157,7 @@ class WindowedStatDetector(BaseDetector):
|
|
|
143
157
|
half_life=half_life,
|
|
144
158
|
weight_decay=weight_decay,
|
|
145
159
|
detrend=detrend,
|
|
160
|
+
stabilization=stabilization,
|
|
146
161
|
)
|
|
147
162
|
# Whether the one-time "seasonality groups can't fill this window" check
|
|
148
163
|
# has run for this instance (so the warning fires at most once per run,
|
|
@@ -235,6 +250,10 @@ class WindowedStatDetector(BaseDetector):
|
|
|
235
250
|
if detrend not in _DETREND_METHODS:
|
|
236
251
|
raise ValueError(f"Unknown detrend method: {detrend}. Supported: linear")
|
|
237
252
|
|
|
253
|
+
stabilization = p.get("stabilization")
|
|
254
|
+
if stabilization not in _STABILIZATION_METHODS:
|
|
255
|
+
raise ValueError(f"Unknown stabilization method: {stabilization}. Supported: clamp")
|
|
256
|
+
|
|
238
257
|
# ------------------------------------------------------------------
|
|
239
258
|
# Identity
|
|
240
259
|
# ------------------------------------------------------------------
|
|
@@ -254,6 +273,7 @@ class WindowedStatDetector(BaseDetector):
|
|
|
254
273
|
"half_life": None,
|
|
255
274
|
"weight_decay": None,
|
|
256
275
|
"detrend": None,
|
|
276
|
+
"stabilization": None,
|
|
257
277
|
}
|
|
258
278
|
|
|
259
279
|
def _get_non_default_params(self) -> dict[str, Any]:
|
|
@@ -291,6 +311,15 @@ class WindowedStatDetector(BaseDetector):
|
|
|
291
311
|
if self.params.get("input_type", "values") in _CHANGE_INPUT_TYPES:
|
|
292
312
|
context += 1
|
|
293
313
|
|
|
314
|
+
if self.params.get("stabilization"):
|
|
315
|
+
# A point's substituted-history state depends on whether its own
|
|
316
|
+
# window points were flagged, which depends on THEIR windows. One
|
|
317
|
+
# extra window of warm-up lets every context point be scored on a
|
|
318
|
+
# full window, so incremental batches reproduce the same
|
|
319
|
+
# substitution history as a continuous run (residual differences
|
|
320
|
+
# decay geometrically past that).
|
|
321
|
+
context += int(self.params.get("window_size", 0) or 0)
|
|
322
|
+
|
|
294
323
|
return context
|
|
295
324
|
|
|
296
325
|
def _resolve_half_life_points(self, timestamps: np.ndarray) -> float:
|
|
@@ -463,12 +492,22 @@ class WindowedStatDetector(BaseDetector):
|
|
|
463
492
|
"min_samples_per_group", self.MIN_SAMPLES_PER_GROUP_DEFAULT
|
|
464
493
|
)
|
|
465
494
|
detrend = self.params.get("detrend")
|
|
495
|
+
stabilization = self.params.get("stabilization")
|
|
466
496
|
weighted = self.params.get("window_weights") is not None
|
|
467
497
|
|
|
468
498
|
# STEP 0: Preprocessing (smoothing first, then input_type)
|
|
469
499
|
smoothed_values = self._apply_smoothing(values)
|
|
470
500
|
processed_values = self._preprocess_input(smoothed_values)
|
|
471
501
|
|
|
502
|
+
# Stabilization (opt-in): statistics windows read from a working copy
|
|
503
|
+
# where every previously-flagged point is clamped to the confidence
|
|
504
|
+
# bound it violated, so an ongoing incident cannot inflate the spread
|
|
505
|
+
# or drag the center toward itself. The scored value and the persisted
|
|
506
|
+
# processed_value stay the raw observations; genuinely missing (NaN)
|
|
507
|
+
# points are never substituted.
|
|
508
|
+
work_values = processed_values.copy() if stabilization else processed_values
|
|
509
|
+
replaced = np.zeros(len(processed_values), dtype=bool) if stabilization else None
|
|
510
|
+
|
|
472
511
|
seasonality_dict = {}
|
|
473
512
|
if seasonality_components and len(seasonality_data) > 0 and seasonality_columns:
|
|
474
513
|
seasonality_dict = parse_seasonality_data(seasonality_data, seasonality_columns)
|
|
@@ -498,9 +537,11 @@ class WindowedStatDetector(BaseDetector):
|
|
|
498
537
|
)
|
|
499
538
|
continue
|
|
500
539
|
|
|
501
|
-
# Trailing window, current point excluded
|
|
540
|
+
# Trailing window, current point excluded. Both the global slice
|
|
541
|
+
# and the seasonality-group slices below read window_processed, so
|
|
542
|
+
# sourcing it from work_values covers every statistics consumer.
|
|
502
543
|
window_start = max(0, i - window_size)
|
|
503
|
-
window_processed =
|
|
544
|
+
window_processed = work_values[window_start:i]
|
|
504
545
|
valid_mask = ~np.isnan(window_processed)
|
|
505
546
|
window_valid = window_processed[valid_mask]
|
|
506
547
|
|
|
@@ -586,6 +627,14 @@ class WindowedStatDetector(BaseDetector):
|
|
|
586
627
|
current_processed > confidence_upper
|
|
587
628
|
)
|
|
588
629
|
|
|
630
|
+
# Stabilization write-back: later windows see this point clamped
|
|
631
|
+
# to the bound it violated, not the anomalous observation.
|
|
632
|
+
if stabilization == "clamp" and is_anomaly:
|
|
633
|
+
work_values[i] = (
|
|
634
|
+
confidence_lower if current_processed < confidence_lower else confidence_upper
|
|
635
|
+
)
|
|
636
|
+
replaced[i] = True # type: ignore[index]
|
|
637
|
+
|
|
589
638
|
# STEP 5: Metadata
|
|
590
639
|
metadata: dict[str, Any] = {}
|
|
591
640
|
for name, _ in self.STATS:
|
|
@@ -598,6 +647,10 @@ class WindowedStatDetector(BaseDetector):
|
|
|
598
647
|
metadata["ess"] = round(effective_sample_size(weights), 1)
|
|
599
648
|
if detrend == "linear":
|
|
600
649
|
metadata["trend_slope_per_point"] = float(slope)
|
|
650
|
+
if replaced is not None:
|
|
651
|
+
n_replaced = int(np.count_nonzero(replaced[window_start:i]))
|
|
652
|
+
if n_replaced:
|
|
653
|
+
metadata["stabilized_in_window"] = n_replaced
|
|
601
654
|
|
|
602
655
|
if self.params.get("smoothing") or self.params.get("input_type") != "values":
|
|
603
656
|
metadata["preprocessing"] = {
|