fluxprune: keeping the points that matter

fluxprune keeps a subset of a series' points, read as straight lines between them: the first and last points (to stitch blocks), the minimum and maximum, then whichever points lower the time-weighted RMSE of the lines most, until a point no longer buys enough. prune(times, values) works with its defaults; three settings change what it keeps:

The first two sections show them on each signal; the rest is how it works and how it was chosen.

Data: 19 synthetic signals × 5 days of 1 s scans, cut into 1 h storage blocks of 3,600 points (tslab/prune/signals.py; the studies of ranking and stopping rules use the first 12). Relative errors are relative to each block's range (max − min). Regenerate with uv run python make_prune_report.py in experimental/.

Choosing θ and max_points

error_per_point (θ) is an exchange rate: prune adds points in order of how much they lower the time-weighted RMSE and stops when a point no longer buys θ of it, the RMSE relative to the block's range and read above the block's own white noise (the noise floor). Choose it first, as the RMSE you would accept at some number of points divided by that number: θ = Y / X reads "Y at X points". X is not a target: a block that needs fewer stops sooner, and a harder one goes on at a worse error. The default is 0.1% of the range at a tenth of the input's points, θ = 0.001 / (n / 10): 2.8·10⁻⁶ for an hour of 1 s scans. max_points is a hard cap on top; it drops the rmse pass's least significant points. Three settings:

One hour of eight signals (day 0, 09:00; the pyrometer's busiest hour, of which minutes 10-30 are shown); each panel gives the points kept in the hour, whether the cap bound, the RMSE over the hour's range and the largest error of any scan, in the signal's units.

One hour of eight signals with the points each setting keeps

All 19 signals (described below), 1 h blocks of all 5 days: mean points per hour (the most in one block) and the share of blocks where the cap bound; below, the mean RMSE over the block's range · the largest error of any block, in the signal's units. The default keeps 162 points per hour on average (median 77), and 22% of the blocks want more than 360: the noisy and fast-moving signals, which the cap holds at 360 for a larger RMSE. The coarse θ keeps 24 on average and lets the RMSE grow to several per cent where a signal is busy, which mostly costs the largest error: max_error bounds that.

signaldefaultdefault, max_points 360coarse
temperature452 (max 568)
0.9% · 0.236
360 (max 360) · capped 100%
1% · 0.255
33 (max 48)
3.2% · 0.797
flow419 (max 484)
5.7% · 5.16
360 (max 360) · capped 99%
6.1% · 5.17
76 (max 97)
9.4% · 9.84
tank-level61 (max 140)
0.035% · 0.12
61 (max 140)
0.035% · 0.12
5 (max 7)
0.077% · 1.52
valve6 (max 17)
3.4e-14% · 6.88e-13
6 (max 17)
3.4e-14% · 6.88e-13
5 (max 12)
0.079% · 7.4
ph-float32518 (max 567)
0.85% · 0.043
360 (max 360) · capped 100%
1% · 0.0544
36 (max 47)
3.4% · 0.167
motor-current22 (max 172)
8% · 6.53
22 (max 172)
8% · 6.53
12 (max 32)
8.1% · 6.71
totalizer5 (max 66)
0.001% · 0.124
5 (max 66)
0.001% · 0.124
2 (max 4)
0.0061% · 35.7
batch67 (max 160)
0.088% · 0.3
67 (max 160)
0.088% · 0.3
8 (max 11)
0.43% · 1.89
vibration78 (max 216)
5.5% · 0.189
78 (max 216)
5.5% · 0.189
19 (max 34)
5.9% · 0.654
compressor67 (max 194)
0.34% · 0.752
67 (max 194)
0.34% · 0.752
19 (max 23)
1.5% · 16.8
white-noise18 (max 171)
12% · 6.43
18 (max 171)
12% · 6.43
12 (max 47)
12% · 6.43
sine164 (max 164)
0.035% · 0.00163
164 (max 164)
0.035% · 0.00163
22 (max 22)
1.6% · 0.0829
furnace51 (max 76)
2.6% · 0.572
51 (max 76)
2.6% · 0.572
21 (max 30)
3.2% · 0.957
furnace-dropout45 (max 76)
2.1% · 0.702
45 (max 76)
2.1% · 0.702
18 (max 30)
2.6% · 6.62
grid-frequency584 (max 635)
1.8% · 0.00661
360 (max 360) · capped 100%
2.4% · 0.00967
60 (max 73)
6.1% · 0.0379
ambient-C83 (max 103)
2.3% · 0.3
83 (max 103)
2.3% · 0.3
27 (max 38)
3.4% · 0.414
ambient-K83 (max 102)
2.3% · 0.3
83 (max 102)
2.3% · 0.3
27 (max 38)
3.4% · 0.488
level-quiet107 (max 335)
9.1% · 0.0352
107 (max 335)
9.1% · 0.0352
20 (max 73)
9.4% · 0.119
pyrometer190 (max 364)
2.7% · 28
190 (max 360) · capped 1%
2.7% · 28
34 (max 57)
6.1% · 810
all162 (median 77) · capped 0%
3% (median 1.1%)
134 (median 77) · capped 22%
3.1% (median 1.2%)
24 (median 21) · capped 0%
4.3% (median 3.2%)

The pyrometer against its noise-free curve: points per hour in hours with spikes and in quiet ones, points on a spike per hour, how far the kept lines' top is below each spike's (°C, mean and worst), and the RMSE against the curve under the noise (°C). A time-weighted RMSE counts a narrow spike as cheap (a 600 °C spike 4 s wide adds about 30 °C to an hour's RMSE), so the coarse θ cuts its tops; the default keeps them.

settingpoints/h, busypoints/h, quieton spikes/hpeak missworstRMSE, busyRMSE, quiet
default236221451.771.60.60
default, max_points 360236221451.771.60.60
coarse411129239.580939.50.56

Adding max_error

max_error bounds the largest distance of any scan from the kept lines, in the signal's own units. It only adds points: after the rmse pass, every line that strays further is split (the max_error pass). It covers what an RMSE averages away: a narrow spike, a glitch, a quiet block whose range is its own dither. Choose it against the sensor's noise σ (Pruned.noise estimates it): lines through noisy scans can't stay closer than a few σ without keeping most of them. On white noise at θ = 0.0014, max_error = 1σ keeps 1681, 2σ keeps 381, 3σ keeps 34, 5σ keeps 12 points per hour of 3,600; below σ prune warns. With max_points as well, the cap goes to max_error first and the rmse pass gets the rest; if max_error can't be met within the cap it is relaxed to about the tightest bound the cap allows (max_error_met false). Four settings on top of the coarse θ, with a max_error per signal (columns σ, loose and fine below):

One hour of eight signals with the points each setting keeps

All signals, 1 h blocks of all days, as above; σ is the median noise estimate (0 where the times have no cadence or the signal no noise). A loose max_error adds points only where the coarse lines missed something: the pyrometer's spikes, the furnace's dropouts, the grid frequency's swings. A fine one, at about 2σ, is expensive wherever there is noise: hundreds of points per hour that mostly trace it (temperature 511, white noise 381; the pyrometer's quiet hours, below, go from about ten to over a hundred). The cap holds them at 360 by relaxing the bound, in the worst block to 1.05× the one asked (white-noise) to 1.42× (grid-frequency). At 3σ or more the same signals need far fewer.

signalσloosefinecoarsecoarse, loose max_errorcoarse, fine max_errorcoarse, fine max_error, max_points 360
temperature0.0420.50.133 (max 48)
3.2% · 0.797
34 (max 48)
3.2% · 0.5
511 (max 570)
1.1% · 0.1
358 (max 360) · capped 100%
1.3% · 0.12
flow15276 (max 97)
9.4% · 9.84
82 (max 102)
9.5% · 5
686 (max 740)
5.8% · 2
358 (max 360) · capped 100%
7.7% · 2.71
tank-level0.01810.15 (max 7)
0.077% · 1.52
5 (max 8)
0.075% · 0.997
7 (max 13)
0.052% · 0.1
7 (max 13)
0.052% · 0.1
valve020.55 (max 12)
0.079% · 7.4
5 (max 15)
0.07% · 1.98
6 (max 18)
9.6e-05% · 0.5
6 (max 18)
9.6e-05% · 0.5
ph-float320.00780.10.0236 (max 47)
3.4% · 0.167
38 (max 48)
3.4% · 0.1
513 (max 550)
1.1% · 0.02
359 (max 360) · capped 100%
1.3% · 0.0252
motor-current15212 (max 32)
8.1% · 6.71
12 (max 32)
8.1% · 5
381 (max 446)
7.9% · 2
355 (max 360) · capped 79%
8% · 2.11
totalizer1e-11512 (max 4)
0.0061% · 35.7
3 (max 4)
0.0032% · 5
3 (max 4)
0.0015% · 0.994
3 (max 4)
0.0015% · 0.994
batch0.04810.18 (max 11)
0.43% · 1.89
9 (max 13)
0.42% · 0.999
391 (max 458)
0.088% · 0.1
355 (max 360) · capped 88%
0.089% · 0.105
vibration0.0290.20.0619 (max 34)
5.9% · 0.654
19 (max 34)
5.9% · 0.199
369 (max 440)
5.6% · 0.06
351 (max 360) · capped 60%
5.7% · 0.0635
compressor0.120.2519 (max 23)
1.5% · 16.8
21 (max 27)
1.3% · 2
148 (max 188)
0.37% · 0.25
148 (max 188)
0.37% · 0.25
white-noise15212 (max 47)
12% · 6.43
12 (max 47)
12% · 4.99
381 (max 459)
12% · 2
355 (max 360) · capped 78%
12% · 2.1
sine1.2e-050.050.00522 (max 22)
1.6% · 0.0829
29 (max 31)
1.5% · 0.05
82 (max 84)
0.16% · 0.005
82 (max 84)
0.16% · 0.005
furnace0.1210.2521 (max 30)
3.2% · 0.957
21 (max 30)
3.2% · 0.957
213 (max 264)
2.6% · 0.25
213 (max 264)
2.6% · 0.25
furnace-dropout0.1210.2518 (max 30)
2.6% · 6.62
20 (max 30)
2.6% · 1
211 (max 264)
2.1% · 0.25
211 (max 264)
2.1% · 0.25
grid-frequency0.00120.010.00360 (max 73)
6.1% · 0.0379
82 (max 96)
5.9% · 0.01
640 (max 685)
2.1% · 0.003
355 (max 360) · capped 100%
3.1% · 0.00425
ambient-C0.0610.50.1527 (max 38)
3.4% · 0.414
27 (max 38)
3.4% · 0.414
105 (max 129)
2.7% · 0.15
105 (max 129)
2.7% · 0.15
ambient-K0.0610.50.1527 (max 38)
3.4% · 0.488
27 (max 38)
3.4% · 0.488
105 (max 131)
2.7% · 0.15
105 (max 131)
2.7% · 0.15
level-quiet0.00610.050.01520 (max 73)
9.4% · 0.119
20 (max 73)
9.4% · 0.0496
197 (max 275)
9.1% · 0.015
197 (max 275)
9.1% · 0.015
pyrometer2.150534 (max 57)
6.1% · 810
85 (max 159)
4.4% · 50
300 (max 486)
2.9% · 5
270 (max 360) · capped 47%
3% · 7
all24 (median 21) · capped 0%
4.3% (median 3.2%)
30 (median 22) · capped 0%
4.2% (median 3.1%)
282 (median 223) · capped 0%
3.1% (median 1.3%)
226 (median 223) · capped 40%
3.3% (median 1.6%)

The pyrometer, as above.

settingpoints/h, busypoints/h, quieton spikes/hpeak missworstRMSE, busyRMSE, quiet
coarse411129239.580939.50.56
coarse, loose max_error10511791.7719.80.56
coarse, fine max_error3501191851.771.51.22
coarse, fine max_error, max_points 3603111191741.771.61.22

The signals

The first eighteen are the corpus of the ranking and stopping-rule studies; the six after them are for what errors are relative to (small variations on a large offset), and the pyrometer for the noise floor.

signalwhat it is
temperatureDaily cycle + slow drift (τ 30 min) + noise σ 0.03 °C, 0.01 °C resolution
flowSetpoint steps (6/day) through a 60 s lag + process noise (σ 1.5, τ 5 s) + measurement noise σ 0.8, 0.1 resolution: noise above CompDev
tank-levelFill and drain ramps (0.01–0.05 %/s) between holds + noise σ 0.02 %, 0.01 % resolution
valvePosition commands in 5 % steps (30/day), 2 %/s travel, no noise, 0.1 % resolution
ph-float32pH 7 + slow drift (τ 10 min) + noise σ 0.004, stored as float32 (not decimal)
motor-currentLoad steps (24/day) through a 10 s lag + noise σ 1 A, 0.01 A resolution: CompDev far below the noise, so most scans are archived
totalizerFlow totalizer: integral of a stepped flow, 0.1 resolution (piecewise straight)
batchBatch reactor: heat ramp, soak, cool, ~2 h cycles + noise σ 0.05
vibrationVibration RMS: slow drift + rare bursts (seconds to minutes) + noise
compressorCompressor power: on/off cycles (5-15 min) with start-up overshoot + noise
white-noiseWhite noise only, σ 1
sineOne 20 min sine, nothing else (smooth: error falls as k⁻² with no floor)
furnaceFurnace temperature, °F: setpoints 2100-2130 with ramps, control cycling ±0.3-1.5 °F, 0.1 °F steps
furnace-dropoutThe furnace with 6 thermocouple dropouts a day (reads 0 for 2-20 s)
grid-frequencyGrid frequency, Hz: 60 ± ~10 mHz, 4 generator trips a day (−40 to −150 mHz), 1 mHz steps
ambient-COutdoor temperature, °C: −3 to 9 over the day, weather drift, 0.1 °C steps (crosses zero)
ambient-KThe same ambient temperature in kelvin (+273.15)
level-quietTank level, %: held at 62.4 (±1 step of 0.01 dither) but for 3 fill/draw cycles a day
pyrometerPyrometer at a furnace's exit conveyor, °C: a 45-55 floor with noise σ 2, and hot items passing every 1.5-5 min in production runs of 1-4 h with 0.5-3 h quiet between: spikes of 500-900 above the floor, a / (b + t²) with a half-width of 1.5-2.5 s, 0.1 °C steps

The noise floor

Lines through noisy samples leave about the noise's σ against them however many points are kept, until nearly all are, so the rmse pass on a quiet, noisy block keeps buying points that trace the noise: its range is a few σ, and relative to that the noise looks like signal. With the noise floor (the default) it stops on √(e² − σ²) instead of e, an estimate of the error against the signal under the noise. σ is estimated in one O(n) pass: each scan's distance from the chord of its two neighbours, divided by √(1 + w² + (1 − w)²) (w its position between them in time), median, × 1.4826 (fluxprune/_noise.py). Nothing is smoothed and the ranking is the same; only where it stops moves. Per signal, 1 h blocks of all days: the median σ over the range, and at θ = 0.0015 (the old default) and at the default (θ = 0.001 / (n / 10): 2.8e-6 for 3,600 scans) the points per hour without → with the floor and, below, the mean RMSE over the range (against the scans).

signalσ / rangeθ = 0.0015default
temperature1.1%32 → 32
3.2% → 3.3%
1462 → 452
0.38% → 0.9%
flow7.9%84 → 72
9.2% → 9.5%
2566 → 419
0.7% → 5.7%
tank-level0.033%5 → 5
0.082% → 0.082%
151 → 61
0.033% → 0.035%
valve0%4 → 4
0.06% → 0.06%
5 → 5
2.3e-14% → 2.3e-14%
ph-float320.97%35 → 35
3.5% → 3.5%
1439 → 518
0.38% → 0.85%
motor-current9.4%67 → 12
7.6% → 8.1%
2705 → 22
0.74% → 8%
totalizer2.2e-13%2 → 2
0.0054% → 0.0054%
5 → 5
0.00083% → 0.00089%
batch0.093%8 → 8
0.45% → 0.45%
327 → 67
0.074% → 0.088%
vibration8%47 → 18
5.6% → 6%
2289 → 78
0.62% → 5.5%
compressor0.41%19 → 18
1.5% → 1.5%
871 → 67
0.21% → 0.34%
white-noise14%95 → 11
11% → 12%
2941 → 18
0.81% → 12%
sine0.00059%22 → 22
1.6% → 1.6%
164 → 164
0.035% → 0.035%
furnace3.3%28 → 20
2.9% → 3.2%
2017 → 51
0.54% → 2.6%
furnace-dropout3%24 → 18
2.3% → 2.6%
1596 → 45
0.43% → 2.1%
grid-frequency2.2%58 → 57
6.2% → 6.2%
1905 → 584
0.51% → 1.8%
ambient-C2.6%30 → 26
3.3% → 3.5%
1706 → 83
0.45% → 2.3%
ambient-K2.6%30 → 26
3.2% → 3.5%
1704 → 83
0.44% → 2.3%
level-quiet15%77 → 19
9.3% → 9.4%
1869 → 107
0.5% → 9.1%
pyrometer0.25%51 → 33
6% → 6.2%
1152 → 190
0.3% → 2.7%

Archives

Every noise estimate assumes the times were chosen independently of the values; an archive's weren't (swinging door or a deadband keeps a point because of its value), and on a smooth tag its points pass tests of white noise as well as raw scans do, with σ read at about the CompDev. So the floor runs only on a cadence: at least 90% of the intervals one scan (fluxcode's own test, fluxcode._noise.cadence). The tags' swinging-door archives in 1 h blocks of all days: points per hour in, the share of blocks with a floor, and the default prune's points per hour · RMSE over the range against the 1 s scans, as is and with noise_floor=False, and the archive's own RMSE (the tank level and the valve archive 2-5 points per hour: nothing to prune).

tagarchived/hblocks with a floordefaultnoise_floor=Falsethe archive
temperature3250%197 · 1.8%197 · 1.8%1.7%
flow13620%1215 · 4.6%1215 · 4.6%4.5%
ph-float323400%213 · 1.9%213 · 1.9%1.8%
motor-current29570%2533 · 1.3%2533 · 1.3%0.71%

Against the signal under the noise instead: on white noise (σ = 1, 120 hours) the kept lines' RMSE from zero is 0.27σ on average with the floor and 1.00σ without; in the worst hour 0.48σ and 1.03σ (with few points left, the lines from the required minimum and maximum lean further).

Lower θ

Points per hour (day 0) without → with the floor as θ falls. Without it a lower θ buys noise, so the count follows the noise; with it the noise-dominated signals level off at what their content needs, and only signals with structure at finer scales keep more.

signalθ = 0.0015θ = 0.00015θ = 1.5e-05θ = 1.5e-06
temperature32 → 32136 → 123591 → 3651841 → 456
flow86 → 75455 → 2891584 → 4142866 → 422
motor-current74 → 11518 → 131791 → 142986 → 14
vibration48 → 17345 → 451343 → 642529 → 73
white-noise96 → 11657 → 152070 → 173142 → 17
sine22 → 2242 → 4291 → 91191 → 191
level-quiet72 → 22465 → 871360 → 1121866 → 142
pyrometer55 → 32251 → 71749 → 1321604 → 182
mean of the 1236 → 20197 → 60702 → 1221449 → 164

A pyrometer

The case that asked for it: a pyrometer at a furnace's exit conveyor reads its noisy floor most of the time and a narrow spike (a / (b + t²), about 10 s wide) as each hot item passes, 1,264 spikes in 5 days, with quiet hours between production runs (tslab/prune/signals.py, pyrometer). Against its noise-free curve: points per hour in hours with spikes and in quiet ones, points on a spike (3σ above the floor) per hour, how far the kept lines' top is below each spike's (°C, mean and worst), and the RMSE against the curve under the noise (°C) in busy and quiet hours. The floor takes the quiet hours from tracing noise to a handful of points; it changes nothing on the spikes, which a time-weighted RMSE counts as cheap: a 600 °C spike 4 s wide adds only about 30 °C to an hour's RMSE. A lower θ (the default) or a max_error of a few σ keeps them.

settingpoints/h, busypoints/h, quieton spikes/hpeak missworstRMSE, busyRMSE, quiet
θ = 0.0015, no floor399428253.180940.70.66
θ = 0.0015391128253.280940.70.56
default (θ = 2.8e-6 at 1 h)236221451.771.60.60
default, no floor66029331811.771.62.00
θ = 0.0015, max_error = 20162111201.778.20.56
A quiet and a busy hour of the pyrometer with the points kept

How it works, and how it was chosen

The studies behind the defaults: how good the ranking is, which stopping rule, what errors are relative to, how max_error is met, refit, archives as input, and speed. Some use rules and settings that are only experiments (tslab/prune/rules.py).

Ranking quality at a fixed count

Every method picks the same number of points per 1 h block (day 0, every 4th hour, so 6 blocks per signal); cells are the geometric mean of the block's RMSE over fluxprune's (refined). fluxprune and top-down keep the required points within k; swinging door's CompDev is searched for k points (the nearest count at or above k); LTTB and min/max buckets are the plotting downsamplers; Visvalingam-Whyatt (cartography's line simplification) is bottom-up by triangle area, and + refine adds fluxprune's two refinement passes to its points.

12 points per hour

methodtemperatureflowtank-levelvalveph-float32motor-currenttotalizerbatchvibrationcompressorwhite-noisesineall (RMSE)all (max error)
fluxprune1.001.001.00exact1.001.001.001.001.001.001.001.001.001.00
bottom-up (nested)1.361.331.16exact1.331.291.361.291.111.111.111.231.241.14
top-down1.240.981.23exact1.331.300.972.111.061.491.052.871.351.21
Visvalingam-Whyatt1.641.871.80exact1.532.221.631.461.741.442.231.111.671.39
Visvalingam-Whyatt + refine1.041.091.04exact1.081.270.980.961.081.061.210.991.071.08
swinging door1.781.601.37exact1.761.471.211.481.421.501.241.421.471.05
LTTB1.721.932.28∞1.682.601.582.772.422.792.381.182.061.53
min/max buckets1.832.3832.05∞1.883.516.6717.232.592.313.011.433.812.67
uniform1.631.2522.94∞1.611.915.477.581.361.751.471.682.562.35

36 points per hour

methodtemperatureflowtank-levelvalveph-float32motor-currenttotalizerbatchvibrationcompressorwhite-noisesineall (RMSE)all (max error)
fluxprune1.001.001.00exact1.001.001.001.001.001.001.001.001.001.00
bottom-up (nested)1.311.301.22exact1.321.221.291.221.151.141.231.211.241.24
top-down1.391.040.99exact1.451.030.951.111.0231.751.001.781.571.55
Visvalingam-Whyatt1.571.681.86exact1.591.981.541.821.632.251.911.651.761.59
Visvalingam-Whyatt + refine1.021.021.01exact1.031.040.981.081.001.371.011.031.051.14
swinging door1.681.501.28exact1.741.281.081.531.351.281.271.271.381.13
LTTB1.881.892.09∞1.802.231.462.372.3834.822.071.302.512.22
min/max buckets1.932.268.20∞1.892.823.078.302.5440.642.594.724.163.79
uniform1.681.394.20∞1.751.562.004.611.4036.501.401.552.542.90

108 points per hour

methodtemperatureflowtank-levelvalveph-float32motor-currenttotalizerbatchvibrationcompressorwhite-noisesineall (RMSE)all (max error)
fluxprune1.001.001.00exact1.001.001.001.001.001.001.001.001.001.00
bottom-up (nested)1.321.311.27exact1.311.251.201.261.181.231.261.131.251.18
top-down1.581.171.01exact1.461.010.931.011.0214.221.011.161.421.50
Visvalingam-Whyatt1.541.651.74exact1.541.751.441.701.561.721.751.301.601.47
Visvalingam-Whyatt + refine1.031.051.02exact1.031.020.991.011.011.041.011.111.031.08
swinging door1.631.431.27exact1.681.291.071.351.301.301.271.061.321.01
LTTB1.831.841.87∞1.801.881.381.912.1222.281.901.232.232.15
min/max buckets1.892.132.39∞1.972.471.362.412.3822.352.364.632.852.83
uniform1.781.511.72∞1.801.511.641.611.4224.831.491.282.022.32

Against the optimum

On the same whole blocks (3,600 points), the optimal choice of k points that include the required ones (dynamic programming, O(n²k): about 2 s a block, an oracle, not a method); each greedy method's RMSE over the optimum's, geometric mean.

methodk = 12k = 36k = 108
fluxprune1.05 (worst 1.25)1.04 (worst 1.38)1.06 (worst 1.40)
bottom-up (nested)1.30 (worst 1.63)1.29 (worst 1.60)1.32 (worst 1.53)
top-down1.41 (worst 4.08)1.64 (worst 66.18)1.50 (worst 74.09)

Error curves

What a rule sees: the relative RMSE of keeping k points (the ranking, before refinement), one hour of each signal. Smooth signals fall as k⁻² (sine), random walks as k⁻½ (temperature, pH), and white noise hardly at all until most points are kept; a step or ramp signal falls off a cliff and then sits on its noise. The dashed line is 0.5% for scale.

Error against points kept for one hour of each signal

How many points: the stopping rules

Each rule's one parameter is set so that all 1,440 blocks together keep 36 points per hour on average (1% of the scans), except knee, which can't be calibrated (its total is what its α gives). Cells are the mean points kept per hour and, below, the geometric mean RMSE of the blocks. The corpus columns are the mean points per block, the mean and median block RMSE, and the most points one block got.

rulewhat it doesparametertemperatureflowtank-levelvalveph-float32motor-currenttotalizerbatchvibrationcompressorwhite-noisesinepoints/hmeanmedianmax k
countK points per hourK = 3636
3%
36
9.7%
36
0.033%
36
2.3e-14%
36
3.4%
36
6.9%
36
0.0004%
36
0.085%
36
3.7%
36
0.36%
36
11%
36
0.61%
36.03.6%0.62%36
tolerancefewest points within RMSE εε = 0.1317
9.2%
92
9.8%
4
2.6%
4
4.7e-05%
8
9.5%
88
10%
2
0.009%
5
3.7%
7
8.7%
14
9.4%
194
11%
8
7.6%
36.07.5%8.9%432
balance√(K·k_ε), K = the budget per hour: the geometric mean of the twoε = 0.10518
4.4%
75
8.8%
12
0.035%
11
3.2e-14%
19
5%
71
6.8%
8
0.00051%
13
0.15%
23
4%
23
0.6%
141
11%
17
2.8%
36.03.9%2.9%165
marginalminimise e² + λkλ = 2.87e-0545
2.6%
216
6.9%
5
0.072%
4
2e-06%
52
2.8%
15
7%
2
0.00093%
8
0.43%
21
4.3%
21
0.71%
15
12%
27
1%
36.03.4%1.1%361
marginal (RMSE)minimise e + λk: the least mean RMSE for the totalλ = 0.00020371
2.1%
140
7.7%
6
0.05%
5
6.5e-14%
78
2.3%
14
7.1%
2
0.00069%
14
0.13%
21
3.9%
28
0.41%
13
12%
39
0.52%
36.03.3%0.54%277
amortizedfirst k with √(e² − σ²) / (points added) < θ / hours, σ the block's noise (the default)θ = 0.00045465
2.2%
161
7.3%
5
0.054%
4
7.5e-09%
71
2.4%
15
7.1%
2
0.00073%
11
0.22%
33
3.9%
21
0.69%
14
12%
29
0.95%
36.03.3%0.95%200
amortized, no floorfirst k with e / (points added) < θ / hoursθ = 0.0014533
3.1%
86
8.5%
5
0.064%
4
2.1e-06%
36
3.4%
69
6.8%
2
0.00093%
8
0.41%
49
4%
19
1.5%
99
11%
22
1.6%
36.03.6%1.7%112
tolerance+noisetolerance, ε raised to the noise σε = 0.096810
7%
223
7.6%
4
1.8%
4
2.2e-05%
12
7.1%
47
8.8%
2
0.0047%
5
2.7%
24
7%
15
6.2%
78
11%
8
7.4%
36.06%7%383
balance+noisebalance, ε raised to the noise σε = 0.03939
2.9%
112
8%
12
0.035%
12
2.2e-14%
43
3.1%
49
6.8%
9
0.00051%
14
0.13%
43
3.8%
25
0.49%
49
11%
25
1.2%
36.03.4%1.3%132
kneefirst k where 2k points don't improve e by 2^α (α = 0.25, not calibrated)α = 0.253061
1.3e-12%
8
12%
8
0.039%
5
2.3e-14%
2882
1.3e-13%
9
7.6%
3
0.00063%
19
0.094%
11
4.5%
28
0.44%
8
13%
3600
5.7e-18%
803.43.7%0.083%3,600

Sublinear?

Each rule's points per block against the tolerance rule's at the same budget (the tolerance rule follows complexity linearly by definition), every block of the corpus, and the slope of a log-log fit: 0 is a fixed count, 1 the tolerance, ½ the plain geometric mean. All slopes: count 0.00, tolerance 1.00, balance 0.61, marginal 0.47, marginal (RMSE) 0.36, amortized 0.37, amortized, no floor 0.70, tolerance+noise 0.82, balance+noise 0.38.

Points kept per block against what the tolerance rule keeps, per rule

Other budgets

The same calibration at other averages: mean / median block RMSE and, below, the most points one block got.

rule12/h36/h108/h324/h
count5.7% / 5.4%
max k 12
3.6% / 0.62%
max k 36
3.1% / 0.33%
max k 108
2.5% / 0.3%
max k 324
tolerance8.5% / 10%
max k 196
7.5% / 8.9%
max k 432
5.7% / 7%
max k 864
3% / 3.7%
max k 1,700
balance6.2% / 7%
max k 61
3.9% / 2.9%
max k 165
2.9% / 0.6%
max k 402
1.9% / 0.34%
max k 921
marginal4.3% / 1.9%
max k 47
3.4% / 1.1%
max k 361
2.9% / 1%
max k 1,066
1.9% / 0.57%
max k 1,773
marginal (RMSE)4.2% / 1.4%
max k 33
3.3% / 0.54%
max k 277
2.7% / 0.41%
max k 1,250
1.8% / 0.4%
max k 1,953
amortized4.7% / 3.9%
max k 38
3.3% / 0.95%
max k 200
2.8% / 0.39%
max k 473
2.8% / 0.37%
max k 1,654
amortized, no floor5.1% / 5.2%
max k 29
3.6% / 1.7%
max k 112
2.8% / 0.77%
max k 402
1.9% / 0.35%
max k 1,171
tolerance+noise8.4% / 10%
max k 161
6% / 7%
max k 383
3% / 1.1%
max k 531
2.7% / 0.36%
max k 1,698
balance+noise6.1% / 7%
max k 48
3.4% / 1.3%
max k 132
2.9% / 0.34%
max k 249
2.6% / 0.32%
max k 742
Mean and median RMSE and the largest block against the budget, per rule

Sampling rate and block length

A rule that adapts to the data, not to how it was stored, should keep about as many points of an hour sampled every 10 s as of the same hour sampled every second (ratio 1 when the signal is captured at both rates; noise can't be), and about as many in one 12 h block as in twelve 1 h blocks (count and balance scale K with the block's hours). Median over day 0's hours of k at 10 s over k at 1 s:

ruletemperatureflowtank-levelvalveph-float32motor-currenttotalizerbatchvibrationcompressorwhite-noisesine
count1.001.001.001.001.001.001.001.001.001.001.001.00
tolerance1.000.461.001.001.000.641.001.001.001.000.301.25
balance1.000.621.001.001.000.421.000.961.001.000.391.18
marginal0.940.781.001.000.9813.201.001.004.660.9817.170.89
marginal (RMSE)1.181.621.001.001.0723.851.001.009.300.9125.251.10
amortized0.690.141.001.000.661.341.001.000.850.961.411.00
amortized, no floor0.940.821.001.000.940.831.000.950.910.980.800.91
tolerance+noise1.000.101.001.001.000.381.001.001.001.000.221.22
balance+noise0.910.251.001.000.880.501.001.000.691.000.460.96
knee0.101.071.001.000.101.041.001.110.9711.211.000.10

k of a 12 h block over the sum of k of its twelve 1 h blocks (day 0, mean of the two halves):

ruletemperatureflowtank-levelvalveph-float32motor-currenttotalizerbatchvibrationcompressorwhite-noisesine
count1.001.001.001.001.001.001.001.001.001.001.001.00
tolerance0.090.000.290.230.150.010.120.420.070.790.000.78
balance0.310.060.560.540.470.060.360.680.140.910.750.93
marginal0.100.010.430.450.180.170.170.440.050.730.090.67
marginal (RMSE)0.070.010.400.460.120.190.230.380.120.640.090.62
amortized0.510.330.430.490.670.210.240.680.210.840.060.91
amortized, no floor0.470.260.440.510.630.280.250.590.080.830.830.92
tolerance+noise0.100.000.300.270.260.010.110.450.020.820.480.90
balance+noise0.360.220.630.640.540.150.350.680.110.920.560.97
knee1.270.130.420.550.000.240.250.370.450.020.090.00

Relative to what?

Errors so far are relative to each block's range, so a block's precision follows its own range whatever the units or the offset: an hour of furnace that moves 4 °F around 2,115 °F gets the same relative precision as an hour of a valve that moves 0-100%. The alternatives are the block's largest absolute value, the geometric mean of the two, and a known span per series (here each signal's range over all its days, as a historian's per-tag span would give, but which a store that knows nothing of the series lacks). The ranking doesn't depend on the scale, only the rules' thresholds do. Six more signals where it matters: furnace, furnace-dropout, grid-frequency, ambient-C, ambient-K, level-quiet.

Each rule and scale calibrated to 36 points per hour over all 2,160 blocks of the 18 signals (5 days each). Cells are the mean points kept per hour and, below, the mean RMSE relative to the signal's span: a measure that doesn't depend on the units, the offset or the block, so the scales can be compared (the furnace with dropouts is measured off its dropouts). The last row is the least mean RMSE over span possible for the total, which needs the span.

ruleerrors relative totemperatureflowtank-levelvalveph-float32motor-currenttotalizerbatchvibrationcompressorwhite-noisesinefurnacefurnace-dropoutgrid-frequencyambient-Cambient-Klevel-quietmeanworst signalmax k
tolerancerange7
2%
79
1.9%
4
3.3%
4
1.4%
8
5.1%
63
3.7%
2
0.05%
5
4.7%
7
1.3%
13
9.8%
127
8.6%
8
7.6%
10
1.2%
9
32%
22
3.6%
6
1.4%
6
1.4%
270
0.56%
5%32%469
tolerance√(range·max|x|)4
4%
4
5%
4
4.7%
3
2.1%
4
12%
4
7.5%
2
0.07%
4
7.3%
4
1.7%
13
11%
568
6.9%
8
7.6%
4
3.8%
4
46%
4
11%
5
2%
4
2.6%
4
2%
7.6%46%793
tolerancemax|x|4
4%
4
5.2%
3
5.8%
3
4.2%
4
12%
4
9.2%
2
0.072%
4
14%
4
2.8%
10
16%
573
6.8%
8
7.6%
4
3.8%
4
67%
4
11%
4
2.4%
4
2.6%
4
2.1%
9.7%67%813
tolerancespan (known)4
4%
4
4.5%
4
3.3%
4
1.8%
6
5.9%
5
6.1%
2
0.084%
5
3.1%
4
1.7%
16
4.9%
558
6.9%
10
6.5%
4
3.4%
5
3.4%
5
6.6%
4
2.6%
4
2.6%
4
0.91%
3.8%6.9%616
balancerange18
0.93%
74
1.5%
12
0.022%
11
0.00028%
19
2.6%
70
2.2%
8
1.3e-05%
13
0.13%
22
0.31%
23
0.6%
138
8.4%
17
2.8%
19
0.4%
18
0.56%
36
2.6%
16
0.6%
16
0.6%
115
0.027%
1.4%8.4%175
balance√(range·max|x|)14
1.1%
58
1.6%
12
0.022%
12
9.6e-15%
17
2.8%
78
2.2%
8
1.3e-05%
14
0.11%
21
0.3%
25
0.49%
276
7.9%
27
1.1%
12
0.71%
13
0.75%
12
3.9%
25
0.5%
12
0.73%
12
0.028%
1.3%7.9%285
balancemax|x|12
1.2%
23
1.7%
13
0.021%
12
9.7e-15%
14
3.1%
49
2.2%
8
1.3e-05%
15
0.1%
16
0.31%
26
0.46%
319
7.7%
34
0.7%
12
0.71%
13
0.73%
12
3.9%
46
0.43%
12
0.73%
12
0.028%
1.3%7.7%325
balancespan (known)16
1%
19
1.7%
12
0.022%
12
9.6e-15%
34
1.8%
82
2.2%
8
1.3e-05%
15
0.1%
13
0.32%
25
0.48%
269
7.9%
28
0.99%
15
0.47%
16
0.5%
48
2.2%
13
0.7%
13
0.69%
12
0.028%
1.2%7.9%272
amortizedrange55
0.49%
136
1.3%
5
0.036%
4
0.017%
60
1.3%
14
2.3%
2
4.9e-05%
10
0.21%
28
0.31%
21
0.76%
13
9.2%
26
1.1%
29
0.32%
25
1.1%
100
1.5%
41
0.4%
41
0.4%
37
0.032%
1.2%9.2%169
amortized√(range·max|x|)59
0.47%
176
1.2%
6
0.029%
5
0.0059%
76
1.2%
17
2.3%
2
2.5e-05%
13
0.11%
39
0.29%
27
0.43%
17
9.2%
51
0.33%
16
0.42%
14
0.95%
29
2.7%
64
0.34%
26
0.47%
10
0.031%
1.1%9.2%214
amortizedmax|x|50
0.51%
180
1.2%
7
0.025%
5
0.0015%
74
1.2%
19
2.3%
3
2.5e-05%
16
0.081%
45
0.28%
32
0.36%
18
9.2%
79
0.14%
9
1.2%
10
1.3%
8
4.7%
74
0.33%
14
0.66%
6
0.035%
1.3%9.2%224
amortizedspan (known)52
0.5%
109
1.4%
6
0.031%
5
0.0087%
99
1%
16
2.3%
2
0.00053%
13
0.11%
15
0.31%
27
0.43%
17
9.2%
45
0.43%
23
0.34%
23
0.39%
126
1.3%
32
0.43%
32
0.43%
6
0.034%
1%9.2%147
least mean RMSEspan (known)56
0.48%
27
1.7%
6
0.028%
5
1e-14%
113
0.95%
14
2.3%
2
9.7e-05%
17
0.077%
14
0.32%
32
0.36%
20
8.9%
68
0.18%
24
0.33%
25
0.37%
150
1.2%
34
0.42%
35
0.42%
6
0.033%
1%8.9%471
One hour of each offset signal with the points the amortized rule keeps when errors are relative to the range or to the largest |value|

Block length: k of a 12 h block over the sum of k of its twelve 1 h blocks (all days), with θ / hours for the amortized rule. A ratio below 1 is partly right: twelve blocks keep twelve sets of required points, and a longer block can spend its points where they matter most.

ruleerrors relative totemperatureflowtank-levelvalveph-float32motor-currenttotalizerbatchvibrationcompressorwhite-noisesinefurnacefurnace-dropoutgrid-frequencyambient-Cambient-Klevel-quietgeometric mean
amortizedrange0.420.360.440.560.690.190.250.650.200.840.060.900.490.060.560.340.340.030.30
amortized√(range·max|x|)0.620.600.400.550.810.150.260.700.310.840.040.960.660.240.660.570.520.110.40
amortizedmax|x|0.890.880.440.590.940.140.250.730.480.810.040.980.650.670.510.850.760.200.49
amortizedspan (known)0.940.960.420.560.970.170.260.730.730.840.050.940.850.870.980.900.900.200.55
least mean RMSEspan (known)0.180.070.380.520.190.230.260.440.240.630.050.360.490.500.060.230.220.160.23

max_error and max_points against swinging door

The rmse pass at θ = 0.0015 (the old default: coarse, so that the guards do the work) alone, then with max_error set to an engineer's value per signal (the historian tags' CompDev, otherwise about what matters), then with a safety cap of max_points = 360 per hour (10% of the scans) as well. The max_error pass splits each line between the rmse pass's points that strays further, by farthest reach (below). Swinging door at CompDev = max_error / 2 has the same guarantee: its doors pivot about the archived point, so at CompDev = max_error (last column) its line strays up to twice that. Cells: mean points per hour · the largest error of any block over max_error · mean RMSE over the signal's span; below, the share of blocks the cap bound (rmse_met or max_error_met false).

signalmax_errorrmse passmax_errormax_error, max_points 360swinging door, CompDev = max_error / 2swinging door, CompDev = max_error
temperature0.132 · 8 · 0.66%509 · 1 · 0.22%358 · 1.2 · 0.26%
capped 100%
1136 · 0.95 · 0.15%342 · 1.9 · 0.31%
flow172 · 9.8 · 1.5%1742 · 1 · 0.37%358 · 2.7 · 1.2%
capped 100%
2201 · 0.93 · 0.24%1363 · 1.9 · 0.6%
tank-level0.25 · 9 · 0.053%5 · 1 · 0.037%5 · 1 · 0.037%4 · 0.99 · 0.07%4 · 2 · 0.18%
valve0.54 · 20 · 0.029%5 · 1 · 5.7e-05%5 · 1 · 5.7e-05%5 · 1.4e-12 · 1e-14%4 · 2 · 0.018%
ph-float320.0235 · 8.3 · 1.8%512 · 1 · 0.55%359 · 1.3 · 0.67%
capped 100%
1054 · 0.95 · 0.38%357 · 1.9 · 0.82%
motor-current0.212 · 34 · 2.3%3150 · 1 · 0.091%357 · 11 · 2.3%
capped 100%
3261 · 0.88 · 0.06%2958 · 1.8 · 0.16%
totalizer12 · 36 · 0.00022%2 · 0.99 · 2.6e-05%2 · 0.99 · 2.6e-05%2 · 0.98 · 6.1e-05%2 · 2 · 0.00018%
batch0.28 · 9.5 · 0.36%23 · 1 · 0.094%23 · 1 · 0.094%228 · 0.98 · 0.075%13 · 2 · 0.15%
vibration0.118 · 6.5 · 0.35%36 · 1 · 0.41%36 · 1 · 0.41%422 · 0.99 · 0.29%15 · 2 · 0.44%
compressor0.518 · 34 · 1.5%28 · 1 · 0.54%28 · 1 · 0.54%87 · 0.97 · 0.42%23 · 1.6 · 0.61%
white-noise111 · 6.4 · 9.2%1681 · 1 · 3.8%357 · 2.1 · 9.1%
capped 100%
2223 · 0.94 · 2.4%1333 · 1.9 · 6%
sine0.0122 · 8.3 · 1.6%61 · 1 · 0.33%61 · 1 · 0.33%62 · 0.83 · 0.26%44 · 1.6 · 0.52%
furnace120 · 0.96 · 0.37%20 · 0.96 · 0.37%20 · 0.96 · 0.37%15 · 0.93 · 0.84%9 · 1.9 · 2.1%
furnace-dropout118 · 6.6 · 1.2%20 · 1 · 0.6%20 · 1 · 0.6%16 · 0.93 · 0.86%9 · 1.9 · 2.1%
grid-frequency0.00557 · 7.6 · 2%282 · 1 · 1.2%282 · 1 · 1.2%597 · 0.97 · 0.85%178 · 1.9 · 1.8%
ambient-C0.226 · 2.1 · 0.48%49 · 1 · 0.45%49 · 1 · 0.45%148 · 0.99 · 0.37%26 · 1.9 · 0.73%
ambient-K0.226 · 2.4 · 0.48%48 · 1 · 0.45%48 · 1 · 0.45%111 · 0.99 · 0.39%27 · 1.9 · 0.72%
level-quiet0.119 · 1.2 · 0.035%19 · 0.98 · 0.035%19 · 0.98 · 0.035%2 · 0.99 · 0.082%2 · 2 · 0.15%
all23 · 36 · 1.3%455 · 1 · 0.53%133 · 11 · 0.99%643 · 0.99 · 0.43%373 · 2 · 0.97%

The max_error pass

How max_error is met. Each line between the rmse pass's points that strays further than max_error is split from its left end. Every scan after an anchor bounds the slope of a line from the anchor that passes within max_error of it, from below and above. Swinging door's rule ends the line at the first scan its chord can't reach. Farthest reach, fluxprune's, scans on while the bounds still overlap (some line could still pass every scan so far), notes the last scan whose own chord satisfies them, and when they cross makes that scan the next anchor: with noise, a scan after one the chord can't reach is often reachable again. Fewest points is the exact minimum (dynamic programming over every feasible chord). Cells: points per hour · RMSE over the span; the rmse pass's points the same in each column; below, the max_error pass's time per 1 h block.

signalswinging door's rulefarthest reach (fluxprune)fewest points (exact)
temperature766 · 0.21%509 · 0.22%424 · 0.22%
flow1926 · 0.34%1742 · 0.37%1693 · 0.37%
tank-level5 · 0.037%5 · 0.037%5 · 0.037%
valve6 · 8.2e-05%6 · 8.2e-05%6 · 0.0034%
ph-float32728 · 0.53%512 · 0.55%436 · 0.56%
motor-current3162 · 0.09%3150 · 0.091%3149 · 0.091%
totalizer3 · 2.9e-05%3 · 2.9e-05%3 · 2.8e-05%
batch73 · 0.088%23 · 0.094%20 · 0.091%
vibration176 · 0.35%36 · 0.41%31 · 0.41%
compressor33 · 0.5%28 · 0.54%28 · 0.54%
white-noise1914 · 3.5%1681 · 3.8%1609 · 3.9%
sine62 · 0.33%61 · 0.33%60 · 0.33%
furnace20 · 0.37%20 · 0.37%20 · 0.37%
furnace-dropout20 · 0.58%20 · 0.6%19 · 0.6%
grid-frequency379 · 1.1%282 · 1.2%245 · 1.2%
ambient-C69 · 0.42%49 · 0.45%45 · 0.44%
ambient-K65 · 0.43%48 · 0.45%44 · 0.45%
level-quiet19 · 0.035%19 · 0.035%19 · 0.035%
all536 · 0.51%
10 µs per block, at most 24 µs
466 · 0.54%
13 µs per block, at most 45 µs
447 · 0.55%
489 µs per block, at most 8,378 µs

Refit: values that aren't samples

prune(refit=True) keeps the same times but replaces the values with those whose lines fit the input best in time-weighted L2 (a tridiagonal system over the kept points, O(n)), the first, last, minimum and maximum points (and the points around gaps) keeping their samples. Per signal, over 1 h blocks of all days with any error left: the geometric mean of the RMSE and the largest error with refit over without, at the rmse pass's points; and with max_error = the signal's guard value, the share of blocks where it still holds after the refit (it isn't checked again).

signalRMSE, refit / sampleslargest error, refit / samplesmax_error still holds
temperature0.991.000%
flow0.990.990%
tank-level0.981.0099.2%
valve0.991.00100%
ph-float320.990.990%
motor-current0.991.0087.5%
totalizer0.960.98100%
batch0.760.901.67%
vibration0.970.980%
compressor0.580.8315%
white-noise1.001.000%
sine0.540.82100%
furnace0.990.99100%
furnace-dropout0.950.9880.8%
grid-frequency0.990.990%
ambient-C0.970.980%
ambient-K0.970.990%
level-quiet0.980.97100%
all0.910.9641.3%

Swinging-door archives as input

The same rules with the same parameters on what each tag's swinging-door historian archived (tslab/common/historian.py: exception + swinging-door filters at the tag's CompDev), in 1 h blocks: points per hour in and kept (share of the archive), and below, the geometric-mean RMSE against the archive's own straight lines (relative to the block's range).

tagarchived/hcounttolerancebalancetolerance+noisebalance+noise
temperature32536 (11%)
2.8%
7 (2.1%)
9.3%
18 (5.4%)
4.4%
9 (2.9%)
7.1%
33 (10%)
3%
flow136236 (2.6%)
9.7%
80 (5.9%)
10%
71 (5.2%)
9%
15 (1.1%)
12%
24 (1.8%)
10%
tank-level22 (1e+02%)
1.5e-15%
2 (97%)
2.2e-09%
2 (98%)
3.6e-11%
2 (99%)
2.9e-12%
2 (1e+02%)
1.5e-15%
valve22 (1e+02%)
4.4e-16%
2 (88%)
0.0029%
2 (93%)
2e-05%
2 (90%)
2.3e-05%
2 (96%)
3e-08%
ph-float3234036 (11%)
3.3%
8 (2.3%)
9.6%
19 (5.6%)
5%
11 (3.3%)
7.2%
36 (11%)
3.3%
motor-current295736 (1.2%)
6.9%
86 (2.9%)
10%
71 (2.4%)
6.9%
7 (0.25%)
10%
17 (0.58%)
7%

Speed

Apple M3, one thread, best of 3, 1 s scans. rank is the O(n log n) pass; prune adds picking k (O(n)) and the two O(n) refinement passes, shown as the time it adds to rank; max_error (1% of the range) adds the max_error pass on top of that, and refit the least-squares values, both also against rank. bench/prune.py has the same per call for more signals.

signalnrankper pointprune (rmse pass)prune, max_errorprune, refit
temperature3,6000.4 ms114 ns+0.1 ms (+28%)+0.2 ms (+44%)+0.1 ms (+33%)
temperature43,2007.0 ms161 ns+1.6 ms (+23%)+2.0 ms (+29%)+2.0 ms (+29%)
temperature86,40015.1 ms174 ns+3.3 ms (+22%)+4.0 ms (+26%)+4.1 ms (+28%)
temperature1,000,000323.8 ms324 ns+38.0 ms (+12%)+45.3 ms (+14%)+44.6 ms (+14%)
white-noise3,6000.4 ms111 ns+0.1 ms (+26%)+0.2 ms (+50%)+0.1 ms (+28%)
white-noise43,2007.3 ms169 ns+1.5 ms (+20%)+2.8 ms (+38%)+1.7 ms (+23%)
white-noise86,40015.5 ms179 ns+2.6 ms (+17%)+5.2 ms (+34%)+3.1 ms (+20%)
white-noise1,000,000328.8 ms329 ns+25.1 ms (+8%)+56.8 ms (+17%)+31.7 ms (+10%)

Findings

  1. Ranking: bottom-up, then refine. Removing the cheapest point repeatedly (exact time-weighted L2, a heap, O(n log n)) gives a nested ranking of every point and the whole error curve in one pass, but greedy merging is myopic: about 30% above the optimum. Two O(n) passes that slide each kept point to its best spot between its neighbours bring it to 4-6% above the optimum on whole 1 h blocks at 12, 36 and 108 points (worst 1.4×), for +10-30% of the ranking's time. Top-down splitting (Douglas-Peucker's order, and that of perceptually important points) is better than plain bottom-up on step-like signals and much worse on others (up to 74× the optimum on the compressor). Visvalingam-Whyatt (bottom-up by triangle area) is 1.6-1.8× fluxprune's RMSE, and 1.03-1.07× with the same refinement passes: the passes do most of the work, and exact L2 is still the better order to start from. SWAB (Keogh et al. 2001) is bottom-up in a sliding window, for streams; on whole blocks it is plain bottom-up. At the same number of points, swinging door's RMSE is 1.3-1.5× fluxprune's, LTTB's 2-2.5×, and min/max buckets' and even spacing's 2-4×: the plotting downsamplers keep shape for the eye, not the interpolation.
  2. The error curve tells the signal apart from the noise. Smooth signals fall as k⁻², random walks as k⁻½, noise stays flat until most points are kept. The curve at k = n/2 measures the white noise: it is 0.29σ for white noise at every block size (tslab/prune/rules.py, noise), regular or irregular times.
  3. Fixed count spends the same on a valve that two points describe exactly as on white noise, and fixed tolerance is the opposite: calibrated to the same total it hands noise blocks everything (white noise 194 points/h against 36 average, a block up to 432) and its ε ends at 13% of the range for all the others: the worst mean error of all rules. Neither alone is a rate control for mixed data.
  4. The geometric mean of the two (balance, √(K·k_ε)) is a real compromise (slope ~0.6 against the tolerance's counts) and bounded by √(K·n), but noise is still the most "complex" thing it sees: white noise gets 4× the average. Raising ε to the block's own noise level (balance+noise) fixes that and reaches nearly the least mean RMSE possible for the total at 36/h (3.4% vs 3.3%) with the smallest largest block of the adaptive rules (132); at large budgets it stops early on purpose (it won't trace noise), so its mean levels off. Its weakness is the noise estimate: correlated noise (flow: τ 5 s at 1 s scans) partly reads as signal, so the count depends on the sampling rate there (×0.25).
  5. The marginal rules (stop when a point gains less than λ) give the least mean error for the total by construction (e + λk) but depend on the sampling rate: a noise point's gain scales as 1/n, so the same λ keeps 13-25× more noise points at 10 s than at 1 s. The user's prediction holds: on noise they either give up at once or never.
  6. The amortized rule (stop at the first k where the error left is under θ per point added) is the best one-knob rule here: on its own error curve (no floor) within 4-21% of the least mean RMSE at every budget, largest block 2.4-3.7× the average, and the most stable under a change of sampling rate (×0.80-1.00 for every signal). It stops where the error curve meets the line θ·k: in effect error and points traded at a fixed rate, so it grows with complexity sublinearly (k ∝ c^⅓ for a smooth signal's k⁻² curve, ∝ σ for noise). It is error_per_point in the API.
  7. The noise floor (the default): the same rule on √(e² − σ²), σ the block's white noise from each scan's distance to its neighbours' chord (median, one O(n) pass), so it stops buying points that only trace the noise. At θ = 0.0015 it keeps a fraction of the points on noise-dominated blocks (white noise 95 → 11 per hour, motor current 67 → 12, the quiet level 77 → 19, vibration 47 → 18) and the same on smooth and quantized ones, for 0-10% more RMSE against the scans (mostly the noise the dropped points traced). Calibrated to 36/h it reaches the least mean RMSE possible for the total (3.3%, as marginal (RMSE)) with half the median (0.95% against 1.7%), where the rule without it is at 3.6%; at 324/h it levels off (2.8% against 1.9%), since it won't trace noise. A lower θ no longer buys noise: at θ = 1.5·10⁻⁶ white noise keeps 17 points per hour (3,142 without), while smooth signals keep refining. The pyrometer (narrow spikes on a noisy floor, quiet hours between production runs) shows both sides: at θ = 0.0015 its quiet hours go from 94 points to 11, closer to the curve under the noise (0.56 °C RMSE against 0.66); its spikes, which a time-weighted RMSE counts as cheap, are kept by a lower θ (the default: 236 points per busy hour and 22 per quiet one, every peak within 7 °C) or by max_error = 10σ (162, the same peaks), not by the floor. At the default θ the floor is what makes the low rate usable: without it the noise-dominated signals keep most of their scans (white noise 2,941 of 3,600 per hour, motor current 2,705, the pyrometer's quiet hours 2,933), with it 18, 22 and 22. Its costs: correlated noise reads partly as signal at the sampling rate (flow, τ 5 s: at 10 s scans the same noise reads as noise and keeps 0.14× as many points; temperature and pH 0.66-0.69×), so it is less stable under a change of sampling rate than the rule without it; and on pure noise the lines from the required minimum and maximum lean on the few points left (the worst of 120 hours of white noise 0.48σ from the zero under it, against 0.38σ without the floor; 0.26σ against 0.33σ on average). Every noise estimate assumes the times were chosen independently of the values, which an archive's weren't: on the tags' swinging-door archives the estimate read about the CompDev (temperature 3× its scans' noise) and passed tests of white noise (residuals near zero, their lag-1 correlation) as well as the scans, and the floor doubled the error against the scans (temperature 3.3% of the range against 1.9%). No test on the values tells them apart, so the floor runs only on a cadence (at least 90% of the intervals one scan, fluxcode's own test): the archives get none, and keep what they kept without it. Scoring the ranking against a smoothed copy of the scans was tried and dropped: a line through noisy samples is no closer to a smooth curve, so it keeps as many points on noise, needs a window, and shaves narrow peaks.
  8. Knee detection (stop when doubling the points stops paying) is unusable: periodic signals have plateaus at small k (12 h of a sine: 4 points) and random walks never stop (temperature: 3,061 of 3,600).
  9. Block length. Every rule but the fixed count keeps fewer points in one 12 h block than in twelve 1 h blocks, for two reasons. A parameter per block is spread over a longer block: with θ / hours for the amortized rule (as K · hours for a count) its geometric mean over the 18 signals is 0.30 (0.03-0.90, the low end the quiet level and the dropouts, whose range a single event sets, and white noise, which the noise floor holds at about the same count however long the block). And a longer block has a wider range: with a known span per series instead, the same rule reaches 0.55. The rest is as it should be: twelve blocks keep twelve sets of required points, and one block can place its points where they matter most.
  10. Relative to what? Of the scales that need no knowledge of the series, the block's own range is the best: with the amortized rule at 36/h, the mean error over each signal's span is 1.2%, against 1.0% with known spans and 1.0% the least possible. It gives small variations on a large offset the precision they need by itself: the furnace (4 °F an hour around 2,115) and the grid frequency keep 29 and 100 points per hour, where known spans give 23 and 126. The largest |value| depends on where zero is: the same ambient temperature keeps 74 points per hour in °C and 14 in kelvin, and it starves exactly the signals that have little range for their offset (furnace and grid frequency: 9 and 8 per hour, at 1.2% and 4.7% of their span, against 0.32% and 1.5%). √(range · max|x|) halves both effects but keeps them (64 against 26 points for °C and K, grid 2.7% of span), and it helps block length less than a known span (0.40 against 0.55). The range has two costs: a quiet block makes its own dither look like signal (the quiet level keeps 37 points per hour tracing one 0.01 step, against 6 with its span; 77 without the noise floor), and a glitch that sets the range takes its block's precision (furnace with dropouts: 1.1% of span against 0.32%). A known span fixes both, and so does an absolute max_error guard (next).
  11. Swinging-door archives as input prune further, depending on what the archive kept: a tag whose CompDev is below its noise (flow, motor current: 1,300-3,000 archived points per hour) drops to 0.6-6% of the archive at the same settings, a smooth tag (temperature, pH: ~330/h) to 2-11%, and an archive that already has two points per hour stays as it is.
  12. max_error only adds points: the max_error pass splits each line between the rmse pass's points that strays further than max_error from the scans by farthest reach: from each anchor, the line to the farthest scan it can reach while every scan between stays within max_error. Swinging door's rule (the line ends at the first scan the chord can't reach) keeps 1.1-5× as many points on the noisy signals (vibration 176 against 36 per hour, batch 73 against 23, temperature 766 against 509), the exact minimum 4% fewer overall at about 38× the time (13 against 490 µs per block, up to 8.4 ms). At the same guarantee as swinging door (CompDev = max_error / 2, since at CompDev = max_error its line strays up to 1.6-2× that), fluxprune keeps fewer points on 12 of the 18 signals (batch 23 against 228 per hour, vibration 36 against 422, temperature 509 against 1,136, grid frequency 282 against 597) and as many on two; more only where the rmse pass already wanted more than the guard needs (the quiet level's dither: 19 against 2). A max_error below a signal's noise keeps most of the scans, as swinging door does (motor current: 3,160 per hour): at max_error = σ about half the scans, at 0.6σ two thirds, at 2.5σ an eighth, so prune() warns below σ (the noise floor's estimate). max_points (here 360 per hour) caps that. The budget goes in a fixed order: the required points; then max_error, if it can be met within the cap, with the rmse pass taking what is left (its least significant points give way first, by a bisection on their number); and if it can't, max_error relaxed to about the tightest bound the cap allows (a bisection over the max_error pass), with no points from the rmse pass. The max_error pass always comes last, since adding a point can make the error worse. The cap bound only on the five noise-dominated signals; in their worst block the bound reached is 1.2× max_error for temperature, 1.3× for pH, 2.1× for white noise, 2.7× for flow and 11× for motor current, whose noise is five times max_error. Measured and not adopted: splitting the worst segment at its worst point when the cap binds (1.3-1.7× larger errors: it anchors on noise spikes); the max_error pass first and ranking again with its points fixed (the same RMSE at the same number of points, 3-5× slower); a secant search for the relaxed bound (9.4 max_error passes on average against 12.8, but up to 28 against 14); trying every number of the rmse pass's points instead of bisecting (0.4% lower RMSE on average).
  13. Refit (refit=True): least-squares values at the same kept times, the required points pinned to their samples, lower the RMSE by about a tenth overall, almost all of it on smooth or step-like signals (sine, compressor, batch: 0.5-0.8×) where a chord cuts a curve's corner; on noisy ones by 1-3%. It costs one O(n) tridiagonal solve. The values are then no longer readings, and it doesn't mix with max_error: a least-squares line through noise moves away from the scans that set the bound, so max_error still holds in only 41% of the blocks (none on the noisy signals, all on the smooth or quiet ones). It stays an option to measure on real data. Choosing the times and the values together (a free-knot fit, or Imai and Iri's 1986 fewest-segments algorithm for max_error with corners off the samples) could gain more and wasn't tried.

The API. prune(t, x) returns the kept times and values, the RMSE and largest error they leave, and rmse_met, max_error_met and max_points_met. The rmse pass is the default (error_per_point: an exchange rate, best chosen as the RMSE one would accept at a number of points divided by that number, θ = Y / X; the error relative to each block's range and read above its white noise, noise_floor=True). Its default is 0.1% at a tenth of the input's points, θ = 0.001 / (n / 10) (2.8·10⁻⁶ for 1 h of 1 s scans; ~4% of the scans on this corpus, the median block 2%): scaled by the length, it keeps about the same density in long blocks, but fewer points per hour at a slower sampling rate. Two optional guards bound it: max_points, a hard cap, and max_error, the largest error of any scan in value units, which also takes care of quiet blocks, glitches and the block length for a configured signal. θ = 0 with max_points keeps the best max_points; θ = ∞ with max_error runs the max_error pass alone. A run of non-finite values keeps its first point and the finite points around it (a forward hold), past max_points if need be. The other rules on this page (count, tolerance, balance, ε raised to the noise, marginal, knee) are experiments in tslab/prune/rules.py.