Stage 187: α acceleration signal
Browse files- core/config.py +11 -4
- core/drift.py +8 -1
- core/pipeline.py +34 -2
- core/signals.py +75 -0
- verticals/pharma/config.yaml +9 -0
- verticals/retail/config.yaml +10 -0
core/config.py
CHANGED
|
@@ -38,16 +38,21 @@ class MetricConfig:
|
|
| 38 |
|
| 39 |
``direction`` — which way is bad (drives the directional Δ).
|
| 40 |
``weight`` — relative weight inside the Δ aggregate.
|
| 41 |
-
``feeds_stability``
|
| 42 |
-
``feeds_anomaly``
|
| 43 |
-
``
|
| 44 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 45 |
"""
|
| 46 |
name: str
|
| 47 |
direction: str
|
| 48 |
weight: float = 1.0
|
| 49 |
feeds_stability: bool = False
|
| 50 |
feeds_anomaly: bool = True
|
|
|
|
| 51 |
latency_target: Optional[float] = None
|
| 52 |
coherence_target: Optional[float] = None
|
| 53 |
|
|
@@ -69,6 +74,7 @@ class MetricConfig:
|
|
| 69 |
weight=float(d.get("weight", 1.0)),
|
| 70 |
feeds_stability=bool(d.get("feeds_stability", False)),
|
| 71 |
feeds_anomaly=bool(d.get("feeds_anomaly", True)),
|
|
|
|
| 72 |
latency_target=_opt_float(d.get("latency_target")),
|
| 73 |
coherence_target=_opt_float(d.get("coherence_target")),
|
| 74 |
)
|
|
@@ -79,6 +85,7 @@ class MetricConfig:
|
|
| 79 |
"weight": self.weight,
|
| 80 |
"feeds_stability": self.feeds_stability,
|
| 81 |
"feeds_anomaly": self.feeds_anomaly,
|
|
|
|
| 82 |
"latency_target": self.latency_target,
|
| 83 |
"coherence_target": self.coherence_target,
|
| 84 |
}
|
|
|
|
| 38 |
|
| 39 |
``direction`` — which way is bad (drives the directional Δ).
|
| 40 |
``weight`` — relative weight inside the Δ aggregate.
|
| 41 |
+
``feeds_stability`` — included in the ψ (stability) computation.
|
| 42 |
+
``feeds_anomaly`` — included in the ξ (anomaly) computation.
|
| 43 |
+
``feeds_acceleration``— included in the α (acceleration / second
|
| 44 |
+
derivative) computation. Off by default
|
| 45 |
+
so legacy configs are unaffected by
|
| 46 |
+
Stage 187.
|
| 47 |
+
``latency_target`` — if set, the metric contributes to γ against this SLA.
|
| 48 |
+
``coherence_target`` — if set, distance from this target contributes to κ.
|
| 49 |
"""
|
| 50 |
name: str
|
| 51 |
direction: str
|
| 52 |
weight: float = 1.0
|
| 53 |
feeds_stability: bool = False
|
| 54 |
feeds_anomaly: bool = True
|
| 55 |
+
feeds_acceleration: bool = False
|
| 56 |
latency_target: Optional[float] = None
|
| 57 |
coherence_target: Optional[float] = None
|
| 58 |
|
|
|
|
| 74 |
weight=float(d.get("weight", 1.0)),
|
| 75 |
feeds_stability=bool(d.get("feeds_stability", False)),
|
| 76 |
feeds_anomaly=bool(d.get("feeds_anomaly", True)),
|
| 77 |
+
feeds_acceleration=bool(d.get("feeds_acceleration", False)),
|
| 78 |
latency_target=_opt_float(d.get("latency_target")),
|
| 79 |
coherence_target=_opt_float(d.get("coherence_target")),
|
| 80 |
)
|
|
|
|
| 85 |
"weight": self.weight,
|
| 86 |
"feeds_stability": self.feeds_stability,
|
| 87 |
"feeds_anomaly": self.feeds_anomaly,
|
| 88 |
+
"feeds_acceleration": self.feeds_acceleration,
|
| 89 |
"latency_target": self.latency_target,
|
| 90 |
"coherence_target": self.coherence_target,
|
| 91 |
}
|
core/drift.py
CHANGED
|
@@ -25,6 +25,11 @@ DEFAULT_DRIFT_WEIGHTS = {
|
|
| 25 |
"xi": 0.25,
|
| 26 |
"gamma": 0.15,
|
| 27 |
"kappa": 0.15,
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 28 |
}
|
| 29 |
DEFAULT_SEVERITY_THRESHOLDS = {
|
| 30 |
"critical": 0.75,
|
|
@@ -47,12 +52,14 @@ def drift_score(signals: Dict[str, float], weights: Dict[str, float] = None) ->
|
|
| 47 |
xi = signals.get("xi", 0.0)
|
| 48 |
gamma = signals.get("gamma", 0.0)
|
| 49 |
kappa = signals.get("kappa", 1.0)
|
|
|
|
| 50 |
return (
|
| 51 |
w.get("delta", DEFAULT_DRIFT_WEIGHTS["delta"]) * delta +
|
| 52 |
w.get("psi", DEFAULT_DRIFT_WEIGHTS["psi"]) * (1.0 - psi) +
|
| 53 |
w.get("xi", DEFAULT_DRIFT_WEIGHTS["xi"]) * xi +
|
| 54 |
w.get("gamma", DEFAULT_DRIFT_WEIGHTS["gamma"]) * gamma +
|
| 55 |
-
w.get("kappa", DEFAULT_DRIFT_WEIGHTS["kappa"]) * (1.0 - kappa)
|
|
|
|
| 56 |
)
|
| 57 |
|
| 58 |
|
|
|
|
| 25 |
"xi": 0.25,
|
| 26 |
"gamma": 0.15,
|
| 27 |
"kappa": 0.15,
|
| 28 |
+
# Stage 187 — α (acceleration / second derivative). Default
|
| 29 |
+
# weight 0 so the signal is fully opt-in via entity_type
|
| 30 |
+
# config — adding the signal must not silently shift drift
|
| 31 |
+
# scores on any pre-existing tenant.
|
| 32 |
+
"alpha": 0.0,
|
| 33 |
}
|
| 34 |
DEFAULT_SEVERITY_THRESHOLDS = {
|
| 35 |
"critical": 0.75,
|
|
|
|
| 52 |
xi = signals.get("xi", 0.0)
|
| 53 |
gamma = signals.get("gamma", 0.0)
|
| 54 |
kappa = signals.get("kappa", 1.0)
|
| 55 |
+
alpha = signals.get("alpha", 0.0) # Stage 187
|
| 56 |
return (
|
| 57 |
w.get("delta", DEFAULT_DRIFT_WEIGHTS["delta"]) * delta +
|
| 58 |
w.get("psi", DEFAULT_DRIFT_WEIGHTS["psi"]) * (1.0 - psi) +
|
| 59 |
w.get("xi", DEFAULT_DRIFT_WEIGHTS["xi"]) * xi +
|
| 60 |
w.get("gamma", DEFAULT_DRIFT_WEIGHTS["gamma"]) * gamma +
|
| 61 |
+
w.get("kappa", DEFAULT_DRIFT_WEIGHTS["kappa"]) * (1.0 - kappa) +
|
| 62 |
+
w.get("alpha", DEFAULT_DRIFT_WEIGHTS["alpha"]) * alpha
|
| 63 |
)
|
| 64 |
|
| 65 |
|
core/pipeline.py
CHANGED
|
@@ -36,6 +36,7 @@ from .config import HIGHER_IS_WORSE, EntityTypeConfig
|
|
| 36 |
from .decisions import DecisionItem, recommendation_for_issue
|
| 37 |
from .drift import DriftIssue, drift_score, severity_from_score
|
| 38 |
from .signals import (
|
|
|
|
| 39 |
anomaly_xi,
|
| 40 |
health_omega,
|
| 41 |
latency_gamma,
|
|
@@ -124,6 +125,10 @@ def _entity_states(entity_id: str, obs: List[Observation],
|
|
| 124 |
psi_candidates: List[float] = []
|
| 125 |
gamma_candidates: List[float] = []
|
| 126 |
coherence_violations: List[float] = []
|
|
|
|
|
|
|
|
|
|
|
|
|
| 127 |
|
| 128 |
for m in config.metrics:
|
| 129 |
series = history[m.name]
|
|
@@ -145,6 +150,11 @@ def _entity_states(entity_id: str, obs: List[Observation],
|
|
| 145 |
xi_candidates.append(anomaly_xi(value, hist))
|
| 146 |
if m.feeds_stability and len(recent) >= 2:
|
| 147 |
psi_candidates.append(stability_psi(recent))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 148 |
if m.latency_target is not None:
|
| 149 |
gamma_candidates.append(latency_gamma(value, m.latency_target))
|
| 150 |
if m.coherence_target is not None:
|
|
@@ -159,10 +169,15 @@ def _entity_states(entity_id: str, obs: List[Observation],
|
|
| 159 |
gamma = max(gamma_candidates) if gamma_candidates else 0.0
|
| 160 |
kappa = (1.0 - safe_mean(coherence_violations)) if coherence_violations else 1.0
|
| 161 |
kappa = max(0.0, min(1.0, kappa))
|
|
|
|
|
|
|
|
|
|
|
|
|
| 162 |
omega = health_omega(delta, psi, xi, gamma, kappa)
|
| 163 |
|
| 164 |
signals = {"delta": delta, "psi": psi, "xi": xi,
|
| 165 |
-
"gamma": gamma, "kappa": kappa, "
|
|
|
|
| 166 |
score = drift_score(signals, weights=config.signal_weights)
|
| 167 |
|
| 168 |
# confidence grounded in how much history we have (Stage 1 improvement)
|
|
@@ -215,6 +230,22 @@ def _evidence_for_state(state: Dict, config: EntityTypeConfig) -> List[Dict]:
|
|
| 215 |
"truth_type": "soft_truth",
|
| 216 |
"confidence": state["confidence"],
|
| 217 |
})
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 218 |
return evidence
|
| 219 |
|
| 220 |
|
|
@@ -267,7 +298,8 @@ def run_pipeline(observations: List[Observation],
|
|
| 267 |
f"health Ω={state['signals']['omega']}. Signals — "
|
| 268 |
f"Δ={state['signals']['delta']}, ψ={state['signals']['psi']}, "
|
| 269 |
f"ξ={state['signals']['xi']}, γ={state['signals']['gamma']}, "
|
| 270 |
-
f"κ={state['signals']['kappa']}
|
|
|
|
| 271 |
),
|
| 272 |
evidence=evidence,
|
| 273 |
)
|
|
|
|
| 36 |
from .decisions import DecisionItem, recommendation_for_issue
|
| 37 |
from .drift import DriftIssue, drift_score, severity_from_score
|
| 38 |
from .signals import (
|
| 39 |
+
acceleration_alpha,
|
| 40 |
anomaly_xi,
|
| 41 |
health_omega,
|
| 42 |
latency_gamma,
|
|
|
|
| 125 |
psi_candidates: List[float] = []
|
| 126 |
gamma_candidates: List[float] = []
|
| 127 |
coherence_violations: List[float] = []
|
| 128 |
+
# Stage 187 — α (acceleration) candidates. Stays empty for
|
| 129 |
+
# legacy configs (no metric has feeds_acceleration=True), so
|
| 130 |
+
# alpha is 0.0 and nothing about existing behaviour shifts.
|
| 131 |
+
alpha_candidates: List[float] = []
|
| 132 |
|
| 133 |
for m in config.metrics:
|
| 134 |
series = history[m.name]
|
|
|
|
| 150 |
xi_candidates.append(anomaly_xi(value, hist))
|
| 151 |
if m.feeds_stability and len(recent) >= 2:
|
| 152 |
psi_candidates.append(stability_psi(recent))
|
| 153 |
+
if getattr(m, "feeds_acceleration", False):
|
| 154 |
+
baseline_window = hist[:cut]
|
| 155 |
+
if len(baseline_window) >= 3 and len(recent) >= 3:
|
| 156 |
+
alpha_candidates.append(acceleration_alpha(
|
| 157 |
+
baseline_window, recent, m.direction))
|
| 158 |
if m.latency_target is not None:
|
| 159 |
gamma_candidates.append(latency_gamma(value, m.latency_target))
|
| 160 |
if m.coherence_target is not None:
|
|
|
|
| 169 |
gamma = max(gamma_candidates) if gamma_candidates else 0.0
|
| 170 |
kappa = (1.0 - safe_mean(coherence_violations)) if coherence_violations else 1.0
|
| 171 |
kappa = max(0.0, min(1.0, kappa))
|
| 172 |
+
# Stage 187 — α (acceleration). Max across all opted-in
|
| 173 |
+
# metrics: a single accelerating metric is enough to fire,
|
| 174 |
+
# mirroring how ξ (anomaly) and γ (latency) aggregate.
|
| 175 |
+
alpha = max(alpha_candidates) if alpha_candidates else 0.0
|
| 176 |
omega = health_omega(delta, psi, xi, gamma, kappa)
|
| 177 |
|
| 178 |
signals = {"delta": delta, "psi": psi, "xi": xi,
|
| 179 |
+
"gamma": gamma, "kappa": kappa, "alpha": alpha,
|
| 180 |
+
"omega": omega}
|
| 181 |
score = drift_score(signals, weights=config.signal_weights)
|
| 182 |
|
| 183 |
# confidence grounded in how much history we have (Stage 1 improvement)
|
|
|
|
| 230 |
"truth_type": "soft_truth",
|
| 231 |
"confidence": state["confidence"],
|
| 232 |
})
|
| 233 |
+
# Stage 187 — α (acceleration). Only surfaces when at least one
|
| 234 |
+
# metric is opted in AND it's actually accelerating. The 0.5
|
| 235 |
+
# threshold means the recent slope is ≥ 2× baseline slope before
|
| 236 |
+
# it shows up as evidence — that's a meaningful "death-spiral"
|
| 237 |
+
# signal, not noise on a near-flat baseline.
|
| 238 |
+
if sig.get("alpha", 0.0) >= 0.5:
|
| 239 |
+
evidence.append({
|
| 240 |
+
"label": "acceleration",
|
| 241 |
+
"summary": (
|
| 242 |
+
f"Acceleration α={sig['alpha']} — the rate of "
|
| 243 |
+
"degradation is itself increasing (recent slope > "
|
| 244 |
+
"baseline slope in the adverse direction)."
|
| 245 |
+
),
|
| 246 |
+
"truth_type": "hard_truth",
|
| 247 |
+
"confidence": state["confidence"],
|
| 248 |
+
})
|
| 249 |
return evidence
|
| 250 |
|
| 251 |
|
|
|
|
| 298 |
f"health Ω={state['signals']['omega']}. Signals — "
|
| 299 |
f"Δ={state['signals']['delta']}, ψ={state['signals']['psi']}, "
|
| 300 |
f"ξ={state['signals']['xi']}, γ={state['signals']['gamma']}, "
|
| 301 |
+
f"κ={state['signals']['kappa']}, "
|
| 302 |
+
f"α={state['signals'].get('alpha', 0.0)}."
|
| 303 |
),
|
| 304 |
evidence=evidence,
|
| 305 |
)
|
core/signals.py
CHANGED
|
@@ -81,3 +81,78 @@ def health_omega(delta: float, psi: float, xi: float, gamma: float, kappa: float
|
|
| 81 |
"coherence": 1.0,
|
| 82 |
}
|
| 83 |
return weighted_geometric_mean(values, weights)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 81 |
"coherence": 1.0,
|
| 82 |
}
|
| 83 |
return weighted_geometric_mean(values, weights)
|
| 84 |
+
|
| 85 |
+
|
| 86 |
+
# --- α (alpha) — trend acceleration -----------------------------------
|
| 87 |
+
#
|
| 88 |
+
# Stage 187. The other 5 signals catch: "X% worse than baseline" (δ),
|
| 89 |
+
# "today spiked" (ξ), "missed SLA" (γ), "stability dropped" (ψ),
|
| 90 |
+
# "coherence dropped" (κ). None of them catch the second derivative —
|
| 91 |
+
# "the RATE of degradation is itself accelerating." A customer dropping
|
| 92 |
+
# 2%/wk is bad; one dropping 2 → 5 → 9%/wk is on a death spiral and
|
| 93 |
+
# needs a different intervention than the steady decliner.
|
| 94 |
+
#
|
| 95 |
+
# Implementation:
|
| 96 |
+
# 1. Fit a least-squares linear slope to the baseline window.
|
| 97 |
+
# 2. Fit a least-squares linear slope to the recent window.
|
| 98 |
+
# 3. If recent slope is adverse AND its magnitude exceeds the
|
| 99 |
+
# baseline slope, output the magnitude ratio normalised into
|
| 100 |
+
# [0, 1] via 1 - 1/(1+excess).
|
| 101 |
+
#
|
| 102 |
+
# Returns 0.0 (no acceleration) when:
|
| 103 |
+
# - either window has < 3 points (slope undefined),
|
| 104 |
+
# - the recent slope direction is NOT adverse,
|
| 105 |
+
# - the recent slope magnitude doesn't exceed baseline.
|
| 106 |
+
|
| 107 |
+
|
| 108 |
+
def _linear_slope(values: List[float]) -> float:
|
| 109 |
+
"""Least-squares slope on uniformly-spaced x. Returns 0 if the
|
| 110 |
+
series is too short or has zero spread on x."""
|
| 111 |
+
n = len(values)
|
| 112 |
+
if n < 2:
|
| 113 |
+
return 0.0
|
| 114 |
+
xs = list(range(n))
|
| 115 |
+
x_mean = (n - 1) / 2.0
|
| 116 |
+
y_mean = sum(values) / n
|
| 117 |
+
num = sum((xs[i] - x_mean) * (values[i] - y_mean) for i in range(n))
|
| 118 |
+
den = sum((xs[i] - x_mean) ** 2 for i in range(n))
|
| 119 |
+
if den < 1e-12:
|
| 120 |
+
return 0.0
|
| 121 |
+
return num / den
|
| 122 |
+
|
| 123 |
+
|
| 124 |
+
def acceleration_alpha(baseline: List[float],
|
| 125 |
+
recent: List[float],
|
| 126 |
+
direction: str) -> float:
|
| 127 |
+
"""The α signal. ``direction`` is "higher_is_worse" or
|
| 128 |
+
"lower_is_worse" — fires only when the recent slope is in the
|
| 129 |
+
adverse direction AND steeper than the baseline slope.
|
| 130 |
+
|
| 131 |
+
Output ∈ [0, 1]:
|
| 132 |
+
excess = max(0, |recent| - |baseline|) / max(|baseline|, eps)
|
| 133 |
+
alpha = 1 - 1/(1 + excess)
|
| 134 |
+
excess=0 → 0; excess=1 (recent is 2× baseline) → 0.5;
|
| 135 |
+
excess→∞ → 1. Saturates softly so one-off outliers don't max out.
|
| 136 |
+
"""
|
| 137 |
+
if len(baseline) < 3 or len(recent) < 3:
|
| 138 |
+
return 0.0
|
| 139 |
+
b_clean = [v for v in baseline if v is not None]
|
| 140 |
+
r_clean = [v for v in recent if v is not None]
|
| 141 |
+
if len(b_clean) < 3 or len(r_clean) < 3:
|
| 142 |
+
return 0.0
|
| 143 |
+
s_base = _linear_slope(b_clean)
|
| 144 |
+
s_rec = _linear_slope(r_clean)
|
| 145 |
+
if direction == "higher_is_worse":
|
| 146 |
+
adverse_recent = s_rec > 0
|
| 147 |
+
elif direction == "lower_is_worse":
|
| 148 |
+
adverse_recent = s_rec < 0
|
| 149 |
+
else:
|
| 150 |
+
return 0.0
|
| 151 |
+
if not adverse_recent:
|
| 152 |
+
return 0.0
|
| 153 |
+
abs_base = abs(s_base)
|
| 154 |
+
abs_rec = abs(s_rec)
|
| 155 |
+
if abs_rec <= abs_base:
|
| 156 |
+
return 0.0
|
| 157 |
+
excess = (abs_rec - abs_base) / max(abs_base, 1e-6)
|
| 158 |
+
return 1.0 - 1.0 / (1.0 + excess)
|
verticals/pharma/config.yaml
CHANGED
|
@@ -18,13 +18,22 @@ entity_types:
|
|
| 18 |
baseline_window: 14
|
| 19 |
baseline_lag: 7
|
| 20 |
recent_window: 14
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 21 |
metrics:
|
| 22 |
# δ — drop in prescription fill (the pharmacy's revenue + duty
|
| 23 |
# of care signal).
|
|
|
|
|
|
|
|
|
|
| 24 |
prescription_fill_count:
|
| 25 |
direction: lower_is_worse
|
| 26 |
weight: 0.25
|
| 27 |
feeds_anomaly: true
|
|
|
|
| 28 |
# ψ — spike in rejected scripts (insurance / formulary issues
|
| 29 |
# or a system fault).
|
| 30 |
rejection_rate:
|
|
|
|
| 18 |
baseline_window: 14
|
| 19 |
baseline_lag: 7
|
| 20 |
recent_window: 14
|
| 21 |
+
# Stage 187 — α (acceleration) weight 0.10 for pharmacies. A
|
| 22 |
+
# pharmacy losing prescription fills faster every week is at
|
| 23 |
+
# acute risk; weighted 10% of the drift score.
|
| 24 |
+
signal_weights:
|
| 25 |
+
alpha: 0.10
|
| 26 |
metrics:
|
| 27 |
# δ — drop in prescription fill (the pharmacy's revenue + duty
|
| 28 |
# of care signal).
|
| 29 |
+
# α — Stage 187: prescription fill accelerating downward is a
|
| 30 |
+
# death-spiral signal — customers are leaving for a competitor
|
| 31 |
+
# pharmacy faster every week, not just slowly drifting.
|
| 32 |
prescription_fill_count:
|
| 33 |
direction: lower_is_worse
|
| 34 |
weight: 0.25
|
| 35 |
feeds_anomaly: true
|
| 36 |
+
feeds_acceleration: true
|
| 37 |
# ψ — spike in rejected scripts (insurance / formulary issues
|
| 38 |
# or a system fault).
|
| 39 |
rejection_rate:
|
verticals/retail/config.yaml
CHANGED
|
@@ -21,6 +21,12 @@ entity_types:
|
|
| 21 |
baseline_window: 14
|
| 22 |
baseline_lag: 7
|
| 23 |
recent_window: 14
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
metrics:
|
| 25 |
# δ — sales vs baseline. The headline retail signal.
|
| 26 |
sales:
|
|
@@ -47,11 +53,15 @@ entity_types:
|
|
| 47 |
feeds_stability: true
|
| 48 |
latency_target: 4.0 # SLA: 4 min avg checkout
|
| 49 |
# κ — conversion volatility (footfall → buyer ratio).
|
|
|
|
|
|
|
|
|
|
| 50 |
conversion:
|
| 51 |
direction: lower_is_worse
|
| 52 |
weight: 0.20
|
| 53 |
feeds_anomaly: true
|
| 54 |
feeds_stability: true
|
|
|
|
| 55 |
coherence_target: 0.18 # expected conversion 18%
|
| 56 |
|
| 57 |
distribution:
|
|
|
|
| 21 |
baseline_window: 14
|
| 22 |
baseline_lag: 7
|
| 23 |
recent_window: 14
|
| 24 |
+
# Stage 187 — α (acceleration) weight 0.10 for retail stores.
|
| 25 |
+
# Conversion accelerating downward is a real death-spiral signal
|
| 26 |
+
# in this vertical, so the 6th signal contributes ~10% of the
|
| 27 |
+
# drift score. Default 0.0 stays untouched on legacy verticals.
|
| 28 |
+
signal_weights:
|
| 29 |
+
alpha: 0.10
|
| 30 |
metrics:
|
| 31 |
# δ — sales vs baseline. The headline retail signal.
|
| 32 |
sales:
|
|
|
|
| 53 |
feeds_stability: true
|
| 54 |
latency_target: 4.0 # SLA: 4 min avg checkout
|
| 55 |
# κ — conversion volatility (footfall → buyer ratio).
|
| 56 |
+
# α — Stage 187: conversion accelerating downward = acute
|
| 57 |
+
# death-spiral signal. A chain dropping conversion 1pp/wk
|
| 58 |
+
# is bad; one dropping 1 → 3 → 6pp/wk needs intervention now.
|
| 59 |
conversion:
|
| 60 |
direction: lower_is_worse
|
| 61 |
weight: 0.20
|
| 62 |
feeds_anomaly: true
|
| 63 |
feeds_stability: true
|
| 64 |
+
feeds_acceleration: true
|
| 65 |
coherence_target: 0.18 # expected conversion 18%
|
| 66 |
|
| 67 |
distribution:
|