forked from wolph/python-progressbar
-
Notifications
You must be signed in to change notification settings - Fork 0
Expand file tree
/
Copy pathalgorithms.py
More file actions
116 lines (92 loc) · 4.02 KB
/
Copy pathalgorithms.py
File metadata and controls
116 lines (92 loc) · 4.02 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
"""Smoothing algorithms backing `SmoothingETA` and similar widgets.
Both concrete implementations below seed their running state with the
*first* observed value rather than `0`, so an EMA/DEMA-backed ETA doesn't
start out biased toward zero before enough samples have arrived. Both
`update` methods also accept an `elapsed` argument that they currently
ignore -- it's part of the `SmoothingAlgorithm` contract (for algorithms
that might weight by time rather than call count) but neither
implementation here uses it.
"""
from __future__ import annotations
import abc
import typing
from datetime import timedelta
class SmoothingAlgorithm(abc.ABC):
"""Contract for a stateful value smoother fed one sample at a time."""
@abc.abstractmethod
def __init__(self, **kwargs: typing.Any):
"""Configure the algorithm.
Args:
**kwargs: Algorithm-specific parameters (e.g. `alpha`).
"""
raise NotImplementedError
@abc.abstractmethod
def update(self, new_value: float, elapsed: timedelta) -> float:
"""Updates the algorithm with a new value and returns the smoothed
value.
"""
raise NotImplementedError
class ExponentialMovingAverage(SmoothingAlgorithm):
"""
The Exponential Moving Average (EMA) is an exponentially weighted moving
average that reduces the lag that's typically associated with a simple
moving average. It's more responsive to recent changes in data.
"""
def __init__(self, alpha: float = 0.5) -> None:
"""Set the smoothing factor.
Args:
alpha: Weight given to the newest observation on each
`update()` (0-1); higher tracks recent values more
closely, lower smooths harder.
"""
self.alpha: float = alpha
self.value: float | None = None
def update(self, new_value: float, elapsed: timedelta) -> float:
"""Fold `new_value` into the running average.
Args:
new_value: Latest observed value.
elapsed: Accepted for `SmoothingAlgorithm` compatibility but
not used by this implementation -- the average is weighted
by call count, not by wall-clock time.
Returns:
The updated EMA.
"""
if self.value is None:
# Seed with the first observation instead of biasing towards 0
self.value = new_value
else:
self.value = self.alpha * new_value + (1 - self.alpha) * self.value
return self.value
class DoubleExponentialMovingAverage(SmoothingAlgorithm):
"""
The Double Exponential Moving Average (DEMA) is essentially an EMA of an
EMA, which reduces the lag that's typically associated with a simple EMA.
It's more responsive to recent changes in data.
"""
def __init__(self, alpha: float = 0.5) -> None:
"""Set the smoothing factor.
Args:
alpha: Weight given to the newest observation in each of the
two nested EMAs (0-1); higher tracks recent values more
closely, lower smooths harder.
"""
self.alpha: float = alpha
self.ema1: float | None = None
self.ema2: float | None = None
def update(self, new_value: float, elapsed: timedelta) -> float:
"""Fold `new_value` into both nested EMAs.
Args:
new_value: Latest observed value.
elapsed: Accepted for `SmoothingAlgorithm` compatibility but
not used by this implementation -- both EMAs are weighted
by call count, not by wall-clock time.
Returns:
The DEMA estimate, `2 * ema1 - ema2`.
"""
if self.ema1 is None or self.ema2 is None:
# Seed with the first observation instead of biasing towards 0
self.ema1 = self.ema2 = new_value
else:
self.ema1 = self.alpha * new_value + (1 - self.alpha) * self.ema1
self.ema2 = self.alpha * self.ema1 + (1 - self.alpha) * self.ema2
return 2 * self.ema1 - self.ema2