| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798 |
- """Robust statistics and trend helpers shared by both algorithms."""
- from __future__ import annotations
- import math
- from statistics import median
- from .constants import MIN_HISTORY_HOURS
- def number(value):
- if value is None:
- return None
- try:
- value = float(value)
- except (TypeError, ValueError):
- return None
- return value if math.isfinite(value) else None
- def finite(value):
- return number(value)
- def median_value(values):
- values = [value for value in values if value is not None]
- return median(values) if values else None
- def max_consecutive(values):
- longest = 0
- current = 0
- for value in values:
- if value:
- current += 1
- longest = max(longest, current)
- else:
- current = 0
- return longest
- def robust_baseline(values, minimum: int = MIN_HISTORY_HOURS):
- values = [value for value in values if value is not None]
- if len(values) < minimum:
- return None, None
- center = median(values)
- mad = median(abs(value - center) for value in values)
- # Keep a meaningful relative scale when a sensor is very stable.
- scale = max(1.4826 * mad, abs(center) * 0.05, 1e-6)
- return center, scale
- def linear_slope(values):
- pairs = [(index, value) for index, value in enumerate(values) if value is not None]
- if len(pairs) < 3:
- return None
- xbar = sum(x for x, _ in pairs) / len(pairs)
- ybar = sum(y for _, y in pairs) / len(pairs)
- denominator = sum((x - xbar) ** 2 for x, _ in pairs)
- if denominator == 0:
- return None
- return sum((x - xbar) * (y - ybar) for x, y in pairs) / denominator
- def consecutive_decline_hours(history: list[dict], current: dict) -> int:
- """Count contiguous valid hours that have not risen from the prior hour."""
- from datetime import timedelta
- count = 1
- newer = current
- for older in reversed(history):
- if newer["_dt"] - older["_dt"] != timedelta(hours=1):
- break
- if newer["oil_avg"] is None or older["oil_avg"] is None or newer["oil_avg"] > older["oil_avg"]:
- break
- count += 1
- newer = older
- return count
- def consecutive_flag_hours(history: list[dict], current: dict, field: str) -> int:
- from datetime import timedelta
- count = 1 if current.get(field) else 0
- if not count:
- return 0
- newer = current
- for older in reversed(history):
- if newer["_dt"] - older["_dt"] != timedelta(hours=1) or not older.get(field):
- break
- count += 1
- newer = older
- return count
- __all__ = [
- "number", "finite", "median_value", "max_consecutive", "robust_baseline",
- "linear_slope", "consecutive_decline_hours", "consecutive_flag_hours",
- ]
|