arraystats#
Fast, NumPy-only statistics for large time series (millions of samples):
descriptive statistics with coverage (describe()),
sampling interval, gap and stuck-value detection, and outlier detection
(IQR, z-score, Hampel filter). No pandas required; designed for interactive
tools where every millisecond counts. The pandas-based functions in
pyedautils.data_quality use these as their core.
import numpy as np
from pyedautils.arraystats import describe, outliers_hampel
ts = np.arange(525_600, dtype=np.int64) * 60_000 # one year, 1-min, epoch ms
values = 20 + 5 * np.sin(ts / 8.64e7 * 2 * np.pi)
stats = describe(values, ts) # n, coverage, min, max, mean, median, ...
mask = outliers_hampel(values, window=30) # boolean outlier mask
API Reference#
Fast statistics for (large) time series on plain NumPy arrays.
All functions operate on plain NumPy arrays and avoid Python loops over data
points, so they are suitable for interactive tools with millions of samples
per series. Timestamps are numeric (e.g. int64 epoch milliseconds) or
datetime64; intervals and durations are returned in the same unit as the
timestamps (datetime64 input is treated as its underlying int64 ticks).
Missing values are represented as NaN and ignored by all statistics.
- class pyedautils.arraystats.Description[source]#
Bases:
TypedDictResult of
describe(). Statistics arenanifn == 0.- n: int#
- n_missing: int#
- coverage: float#
- min: float#
- max: float#
- mean: float#
- median: float#
- std: float#
- p05: float#
- p95: float#
- ts_min: int | None#
- ts_max: int | None#
- ts_first: int | None#
- ts_last: int | None#
- pyedautils.arraystats.describe(values: ndarray, ts: ndarray | None = None, expected_interval: float | None = None, start: float | None = None, end: float | None = None, hold: str | None = None, max_hold: None | float | ndarray = None, prior: float | None = None) Description[source]#
Descriptive statistics of a series, ignoring
NaNvalues.- Parameters:
values – Sample values.
ts – Timestamps belonging to
values(same length, sorted). Needed forcoverageand thets_*fields.expected_interval – Expected sampling interval in timestamp units. Inferred via
infer_interval()whenNone.start – Start of the analysed range (inclusive) for
coverage. Defaults to the first timestamp.end – End of the analysed range (exclusive) for
coverage. Defaults to the last timestamp plus one interval.hold – One of
HOLD_MODES: mean, std, median and the percentiles are then weighted by the time each sample stands for (hold_weights()), i.e. they describe the signal over time, not the samples.None: plain sample statistics.max_hold – Cap on a hold, see
hold_weights().prior – For
hold="step": the value holding atstartfrom before the range (the sample before the first one). It weighs in fromstartto the first timestamp but is not counted inn,min,max.
- Returns:
Descriptionwith n, missing count, coverage (0..1,nanwithout timestamps), min, max, mean, median, std (population), 5th/95th percentile and the timestamps of min, max, first and last valid sample.
- pyedautils.arraystats.HOLD_MODES = ('step', 'step-back', 'linear')#
stepholds until the next sample (setpoints, states, change-of-value data),step-backholds until its own timestamp (counter readings),linearis a continuous signal.- Type:
How a value is read between two samples
- pyedautils.arraystats.hold_weights(ts: ndarray, hold: str, start: float | None = None, end: float | None = None, expected_interval: float | None = None, max_hold: None | float | ndarray = None) ndarray[source]#
The time each sample stands for, as a weight per sample.
- Parameters:
ts – Sorted timestamps.
hold –
step: a sample holds until the next one, the last one untilend(else oneexpected_interval).step-back: a sample holds from the previous one, the first one fromstart.linear: a sample stands for half the interval to each neighbour (trapezoid rule).start – Bounds of the analysed range for the first / last hold.
end – Bounds of the analysed range for the first / last hold.
expected_interval – Fallback for the open end; inferred when
None.max_hold – Cap on a hold (a number or one cap per sample,
inf= none): a value does not hold across an outage.
- Returns:
Float weights, same length as
ts(zeros for a single sample without bounds).
- pyedautils.arraystats.weighted_stats(values: ndarray, weights: ndarray) dict[source]#
Mean, population std, median and the 5th / 95th percentile of
valuesweighted byweights(e.g.hold_weights()): the statistics of the signal over time rather than of the samples. Non-finite values and non-positive weights are ignored; allnanwithout any weight.Percentiles are the inverse of the weighted cumulative distribution (the smallest value the signal is at or below for the share
pof the weight): exact for a step signal, within one sample ofnumpy.percentilefor equal weights.
- pyedautils.arraystats.held_at(ts: ndarray, values: ndarray, at: ndarray, max_hold: None | float | ndarray = None) ndarray[source]#
The value a step series holds at each time in
at: the last sample at or before it,nanbefore the first sample or once the hold is capped bymax_hold(a number or one cap per sample). Non-finite samples do not hold.
- pyedautils.arraystats.step_bucket_mean(ts: ndarray, values: ndarray, edges: ndarray, max_hold: None | float | ndarray = None) ndarray[source]#
Time-weighted mean of a step series (each sample holds until the next one) in the buckets
[edges[i], edges[i + 1]): the integral of the held value over the time the bucket is covered, divided by that time;nanwhere nothing holds.max_holdcaps a hold (a number or one cap per sample), the last sample holds until the last edge at most.
- pyedautils.arraystats.bucket_reduce(index: ndarray, values: ndarray, n: int, fn: str) ndarray[source]#
One value per bucket
0 .. n-1from samples with their bucketindex(sorted):mean,min,max,firstorlastof the finite samples in the bucket,nanfor a bucket without one. Samples outside0 .. n-1are ignored.
- pyedautils.arraystats.step_bucket_reduce(ts: ndarray, values: ndarray, edges: ndarray, fn: str, max_hold: None | float | ndarray = None) ndarray[source]#
One value per bucket
[edges[i], edges[i + 1])of a step series (each sample holds until the next one, formax_holdat most):meanis time-weighted (step_bucket_mean());min/maxtake the samples in the bucket and the value entering it;firstis the entering value, else the first sample;lastthe last sample, else the value held at the bucket’s end. A bucket isnanonly when nothing holds in it.
- pyedautils.arraystats.infer_interval(ts: ndarray) float | None[source]#
Estimate the sampling interval as the median positive time difference.
- Parameters:
ts – Sorted timestamps (numeric or
datetime64).- Returns:
The median positive interval in timestamp units, or
Noneif there are fewer than two distinct timestamps.
- pyedautils.arraystats.gap_indices(ts: ndarray, expected_interval: float | None = None, factor: float = 2.0, min_gap: float | None = None, adaptive: bool = False, block: int = 16) ndarray[source]#
Indices
iwhere a gap lies betweents[i]andts[i + 1].A gap is a time difference larger than
max(factor * expected_interval, min_gap); the interval is inferred viainfer_interval()whenNone. Withadaptive=True(and noexpected_interval) the expected interval is the local one (local_interval()), so a change of the sampling rate is not a gap. Returns an int64 array (empty if no gaps).
- pyedautils.arraystats.detect_gaps(ts: ndarray, expected_interval: float | None = None, factor: float = 2.0, min_gap: float | None = None, adaptive: bool = False, block: int = 16) tuple[ndarray, ndarray][source]#
Find gaps between consecutive timestamps.
A gap is a time difference larger than
max(factor * expected_interval, min_gap).- Parameters:
ts – Sorted timestamps (numeric or
datetime64).expected_interval – Expected sampling interval in timestamp units; inferred via
infer_interval()whenNone.factor – Multiplier on the expected interval. Default 2.0.
min_gap – Minimum gap length in timestamp units. Default 0.
adaptive – Compare against the local interval (
local_interval()) instead of one interval for the whole series. Default False.block – Block size of the local interval. Default 16.
- Returns:
arrays with the last timestamp before and the first timestamp after each gap (same dtype as
ts).- Return type:
(gap_start, gap_end)
- pyedautils.arraystats.local_interval(ts: ndarray, block: int = 16) ndarray[source]#
Expected sampling interval before each step, robust to rate changes.
The positive time differences are split into blocks of
blocksteps; the expected interval of a step is the largest median of its own block and the two neighbouring blocks. A switch from 10 to 60 minutes (e.g. a LoRaWAN downlink) therefore does not make the first 60-minute steps look like gaps, while a long outage between regular samples stands out. Fewer than2 * blocksteps fall back to the global median. Vectorised, O(n).Limitation: if more than half of the packets of a block are lost, the local interval reads too long and those losses are not flagged.
- Parameters:
ts – Sorted timestamps (numeric or
datetime64).block – Steps per block. Default 16.
- Returns:
float64 array of length
len(ts) - 1;nanfor duplicate timestamps.- Raises:
ValueError – if
tsis not sorted.
- class pyedautils.arraystats.Gaps(index: np.ndarray, start: np.ndarray, end: np.ndarray, expected: np.ndarray)[source]#
Bases:
NamedTupleGaps of a series:
indexi means betweents[i]andts[i + 1](len(ts) - 1for a trailing gap up toend).- index: ndarray#
Alias for field number 0
- start: ndarray#
Alias for field number 1
- end: ndarray#
Alias for field number 2
- expected: ndarray#
Alias for field number 3
- property duration: ndarray#
- pyedautils.arraystats.find_gaps(ts: ndarray, factor: float = 3.0, min_gap: float | None = None, block: int = 16, expected_interval: float | None = None, end: float | None = None) Gaps[source]#
Gaps longer than
max(factor * expected interval, min_gap).The expected interval is the local one (
local_interval()) unlessexpected_intervalis given. Withend(e.g. “now”), the time after the last sample counts as a trailing gap if it is that long.- Parameters:
ts – Sorted timestamps (numeric or
datetime64).factor – Multiplier on the expected interval. Default 3.0.
min_gap – Minimum gap length in timestamp units. Default 0.
block – Block size of
local_interval(). Default 16.expected_interval – Fixed expected interval (disables the local estimate).
end – Timestamp up to which data is expected (ticks).
- Returns:
Gaps(start and end in ticks).
- class pyedautils.arraystats.IntervalSegments(start: np.ndarray, end: np.ndarray, interval: np.ndarray, n: np.ndarray)[source]#
Bases:
NamedTuplePeriods with one sampling interval (e.g. 10 min, then 60 min).
- start: ndarray#
Alias for field number 0
- end: ndarray#
Alias for field number 1
- interval: ndarray#
Alias for field number 2
- n: ndarray#
Alias for field number 3
- pyedautils.arraystats.interval_segments(ts: ndarray, block: int = 16, tolerance: float = 0.25, min_blocks: int = 3) IntervalSegments[source]#
Split a series into periods with a stable sampling interval.
Block medians (see
local_interval()) are put into logarithmic classes of relative widthtolerance; runs of the same class form segments, and runs shorter thanmin_blocksblocks (flicker, outages) join the previous segment. The interval of a segment is the median of its positive time differences.- Parameters:
ts – Sorted timestamps (numeric or
datetime64).block – Steps per block. Default 16.
tolerance – Relative difference treated as the same interval. Default 0.25.
min_blocks – Minimum length of a segment in blocks. Default 3.
- Returns:
IntervalSegments(empty for fewer than two distinct timestamps).
- pyedautils.arraystats.interval_overlap(starts: ndarray, ends: ndarray, edges: ndarray) ndarray[source]#
Total length of sorted, non-overlapping intervals
[starts, ends)inside each bucket[edges[k], edges[k + 1]). Vectorised, O((n + k) log n).
- class pyedautils.arraystats.BucketCoverage(observed: np.ndarray, missing: np.ndarray, samples: np.ndarray, n_gaps: np.ndarray)[source]#
Bases:
NamedTuplePer bucket: observed time, missing time (in gaps), samples and gap count. All fields are additive, e.g. over the series of a building.
- observed: ndarray#
Alias for field number 0
- missing: ndarray#
Alias for field number 1
- samples: ndarray#
Alias for field number 2
- n_gaps: ndarray#
Alias for field number 3
- property coverage: ndarray#
Share of observed time without gap (
nanwhere nothing was observed).
- pyedautils.arraystats.bucket_coverage(edges: ndarray, gaps: Gaps, first: float, obs_end: float, ts: ndarray | None = None, exclude: ndarray | Sequence[Sequence[float]] | None = None) BucketCoverage[source]#
Time-based coverage per bucket
[edges[k], edges[k + 1]).The observed period is
[first, obs_end)(e.g. first sample to now). Of each gap,[start + expected, end)is missing: the sample before a gap still covers one interval.- Parameters:
edges – Sorted bucket edges (ticks).
gaps – Result of
find_gaps().first – Start of the observed period.
obs_end – End of the observed period.
ts – Timestamps for the sample counts (optional).
exclude – Ranges
[[start, end], ...]that are neither observed nor missing (e.g. excluded test data). Optional.
- Returns:
- pyedautils.arraystats.detect_stuck(values: ndarray, ts: ndarray, min_repeats: int = 20, min_duration: float = 0) tuple[ndarray, ndarray, ndarray, ndarray][source]#
Find runs of identical consecutive values (run-length encoding).
NaNvalues are dropped before the runs are formed.- Parameters:
values – Sample values.
ts – Sorted timestamps (numeric or
datetime64), same length.min_repeats – Minimum number of identical consecutive samples.
min_duration – Minimum run duration (last - first timestamp) in timestamp units.
- Returns:
(run_start, run_end, run_value, n_repeats)arrays.
- pyedautils.arraystats.outliers_iqr(values: ndarray, multiplier: float = 1.5) ndarray[source]#
Flag values outside
[Q1 - k*IQR, Q3 + k*IQR](Tukey fences).- Returns:
Boolean mask,
FalseforNaNvalues.
- pyedautils.arraystats.outliers_zscore(values: ndarray, threshold: float = 3.0) ndarray[source]#
Flag values whose absolute z-score exceeds
threshold.- Returns:
Boolean mask,
FalseforNaNvalues or zero variance.
- pyedautils.arraystats.outliers_hampel(values: ndarray, window: int = 15, n_sigma: float = 3.0) ndarray[source]#
Hampel filter: flag values deviating from the rolling median.
A value is an outlier if
|x - median| > n_sigma * 1.4826 * MADwithin a centred window of2 * window + 1samples. Robust against level changes, unlike global IQR/z-score methods.NaNvalues are never flagged; for the window medians they are replaced by the previous valid value (or the first valid value at the start).- Returns:
Boolean mask.