Add hourly granularity validation warnings for insufficient baseline data with period-based availability checks and refactor health check settings card layout to vertical stacking of baseline/recent day inputs
Browse files
panel_app/kpi_health_check_panel.py
CHANGED
|
@@ -1435,6 +1435,47 @@ def _validate_inputs() -> tuple[list[str], list[str]]:
|
|
| 1435 |
elif thr < 0:
|
| 1436 |
errors.append("Relative change threshold (%) must be >= 0")
|
| 1437 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1438 |
ar = analysis_range.value
|
| 1439 |
if ar and isinstance(ar, (list, tuple)) and len(ar) == 2 and ar[0] and ar[1]:
|
| 1440 |
try:
|
|
@@ -4012,7 +4053,8 @@ sidebar = pn.Column(
|
|
| 4012 |
pn.Card(
|
| 4013 |
analysis_range,
|
| 4014 |
granularity_select,
|
| 4015 |
-
|
|
|
|
| 4016 |
rel_threshold_pct,
|
| 4017 |
min_consecutive_days,
|
| 4018 |
title="Health check settings",
|
|
|
|
| 1435 |
elif thr < 0:
|
| 1436 |
errors.append("Relative change threshold (%) must be >= 0")
|
| 1437 |
|
| 1438 |
+
g = str(granularity_select.value or "Daily").strip().lower()
|
| 1439 |
+
is_hourly = g.startswith("hour") or g.startswith("h")
|
| 1440 |
+
if is_hourly and (current_daily_by_rat or {}) and rd is not None and bd is not None:
|
| 1441 |
+
try:
|
| 1442 |
+
recent_periods = int(rd) * 24
|
| 1443 |
+
baseline_periods = int(bd) * 24
|
| 1444 |
+
except Exception: # noqa: BLE001
|
| 1445 |
+
recent_periods = 0
|
| 1446 |
+
baseline_periods = 0
|
| 1447 |
+
|
| 1448 |
+
if recent_periods > 0:
|
| 1449 |
+
for rat_name, df0 in (current_daily_by_rat or {}).items():
|
| 1450 |
+
if df0 is None or df0.empty or "period_start" not in df0.columns:
|
| 1451 |
+
continue
|
| 1452 |
+
df1 = _filtered_daily(df0)
|
| 1453 |
+
if df1 is None or df1.empty or "period_start" not in df1.columns:
|
| 1454 |
+
continue
|
| 1455 |
+
t = pd.to_datetime(df1["period_start"], errors="coerce").dropna()
|
| 1456 |
+
if t.empty:
|
| 1457 |
+
continue
|
| 1458 |
+
end_dt = pd.Timestamp(t.max()).floor("h")
|
| 1459 |
+
min_dt = pd.Timestamp(t.min()).floor("h")
|
| 1460 |
+
baseline_end = end_dt - timedelta(hours=int(recent_periods))
|
| 1461 |
+
if min_dt > baseline_end:
|
| 1462 |
+
warnings.append(
|
| 1463 |
+
f"{rat_name}: Hourly baseline is empty (no data before the recent window). "
|
| 1464 |
+
f"Export more hourly history or reduce Recent window (days)."
|
| 1465 |
+
)
|
| 1466 |
+
elif baseline_periods > 0:
|
| 1467 |
+
try:
|
| 1468 |
+
available_baseline_hours = int(
|
| 1469 |
+
((baseline_end - min_dt) / timedelta(hours=1)) + 1
|
| 1470 |
+
)
|
| 1471 |
+
except Exception: # noqa: BLE001
|
| 1472 |
+
available_baseline_hours = 0
|
| 1473 |
+
if 0 < available_baseline_hours < int(baseline_periods):
|
| 1474 |
+
warnings.append(
|
| 1475 |
+
f"{rat_name}: Hourly baseline window is truncated "
|
| 1476 |
+
f"({available_baseline_hours}h available < {int(baseline_periods)}h requested)."
|
| 1477 |
+
)
|
| 1478 |
+
|
| 1479 |
ar = analysis_range.value
|
| 1480 |
if ar and isinstance(ar, (list, tuple)) and len(ar) == 2 and ar[0] and ar[1]:
|
| 1481 |
try:
|
|
|
|
| 4053 |
pn.Card(
|
| 4054 |
analysis_range,
|
| 4055 |
granularity_select,
|
| 4056 |
+
baseline_days,
|
| 4057 |
+
recent_days,
|
| 4058 |
rel_threshold_pct,
|
| 4059 |
min_consecutive_days,
|
| 4060 |
title="Health check settings",
|