sampling#
How a series was sampled and stored, estimated from the data alone: interval percentiles, change-of-value (COV) detection, dead band, heartbeat, quantisation step, compression factor (Thornhill et al. 2004) and how long values stood unchanged. Building automation archives often store only changes, which biases spread, spectra and oscillation indices; the profile shows which analyses a series supports. NumPy only.
import numpy as np
from pyedautils.sampling import sampling_profile, interval_histogram
p = sampling_profile(ts, values, long_hold=86_400_000) # ts: epoch ms
p.change_of_value, p.deadband, p.interval_min, p.compression
interval_histogram(ts, [1_000, 10_000, 60_000, 600_000, 3_600_000, 86_400_000])
API Reference#
How a time series was sampled and stored, estimated from the data alone.
Building automation archives store values either at a fixed interval or only on a change of value (COV: a new sample when the value moved by more than a dead band, sometimes with a periodic “heartbeat” of an unchanged value). Which of the two, and with which settings, decides which analyses are meaningful (e.g. oscillation indices need samples well below the oscillation period; a compressed archive biases spread and spectra, see Thornhill et al. 2004, doi:10.1016/j.jprocont.2003.06.003).
sampling_profile() estimates these settings from the timestamps and values.
Timestamps are numeric (e.g. int64 epoch milliseconds) or datetime64; durations
are returned in the same unit. NaN values are dropped.
- class pyedautils.sampling.SamplingProfile(n: int, span: float, interval_min: float | None, interval_p10: float | None, interval_median: float | None, interval_p90: float | None, interval_max: float | None, change_of_value: bool, repeat_share: float, deadband: float | None, resolution: float | None, heartbeat: float | None, compression: float | None, longest_hold: float, hold_share: float)[source]#
Bases:
NamedTupleResult of
sampling_profile(). Durations are in timestamp units; an estimate that the data does not support isNone.- n: int#
samples (without
NaN)
- span: float#
last - first timestamp
- interval_min: float | None#
1st percentile of the steps (minimum send interval)
- interval_p10: float | None#
Alias for field number 3
- interval_median: float | None#
Alias for field number 4
- interval_p90: float | None#
Alias for field number 5
- interval_max: float | None#
Alias for field number 6
- change_of_value: bool#
share of steps that repeat the previous value
- deadband: float | None#
smallest typical value change (change of value only)
- resolution: float | None#
every value change is a multiple of this
- heartbeat: float | None#
refresh interval of an unchanged value (COV)
- compression: float | None#
N / m, m = non-zero second differences (Thornhill)
- longest_hold: float#
longest time a value stood unchanged
share of the span in holds longer than
long_hold
- pyedautils.sampling.interval_histogram(ts: ndarray, edges: Sequence[float]) ndarray[source]#
Number of sampling intervals (positive time steps) per bin.
- Parameters:
ts – Sorted timestamps (numeric or
datetime64).edges – Increasing bin edges in timestamp units; steps below the first edge count in the first bin, steps above the last edge in the last bin.
- Returns:
len(edges) - 1counts.
- pyedautils.sampling.is_change_of_value(ts: ndarray, ratio: float = 10.0, share: float = 0.5, min_samples: int = 64, min_long: int = 8) bool[source]#
Change-of-value sampling: most of the time passes in steps far longer than the typical one (a regular series with a few outages does not qualify), or the steps follow no interval at all - few lie near a common length and they spread widely (a slow signal with a large dead band: long steps, but none of them regular).
- Parameters:
ts – Sorted timestamps (numeric or
datetime64).ratio – A step longer than
ratiox the median step is “long”.share – Long steps must cover more than this share of the time.
min_samples – Fewer (positive) steps are never change of value.
min_long – At least this many long steps.
- Returns:
Truefor change-of-value sampling.
Share of the (positive) steps near one of the
modesmost common step lengths (GRID_BINwide on a log scale): about 1 for sampling at one or a few intervals, small for change-of-value data.
- pyedautils.sampling.sampling_profile(ts: ndarray, values: ndarray, long_hold: float | None = None) SamplingProfile[source]#
Estimate how a series was sampled and stored.
Intervals: percentiles of the positive time steps;
interval_min(1st percentile) is the shortest interval the sender typically uses.heartbeat: the median interval between two samples with the same value, when such repeats are common and regular while changes come much faster (a change-of-value sender refreshing an unchanged value; periodic sampling of a mostly constant value repeats at its sampling interval and has no heartbeat).Change of value:
is_change_of_value()or a heartbeat. Thendeadbandis the 5th percentile of the non-zero value changes, i.e. what a value must move before a new sample is stored.resolution: a quantisation step, if (nearly) all value changes are whole multiples of it (e.g. 0.1 for values stored with one decimal).compression: N / m with m the number of non-zero second differences of the values (Thornhill et al. 2004): about 1 for uncompressed data, larger when an archive stores straight segments or holds. Change-of-value data is about 1 by construction; there, the dead band describes the compression.longest_hold,hold_share: how long values stood unchanged (from a sample to the next sample with a different value); the share counts holds longer thanlong_hold(None: no share, 0).
- Parameters:
ts – Sorted timestamps (numeric or
datetime64).values – Sample values, same length.
long_hold – Hold duration from which
hold_sharecounts.
- Returns: