Repository navigation
Expand file tree
/
Copy pathExponentialMovingAverage.java
More file actions
233 lines (215 loc) · 8.26 KB
/
Copy pathExponentialMovingAverage.java
File metadata and controls
233 lines (215 loc) · 8.26 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
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
package com.thealgorithms.streaming;
/**
* Exponentially weighted moving average (EWMA) and, alongside it, the exponentially weighted
* variance of the same stream.
*
* <p>A plain moving average has to remember the whole window. An exponentially weighted one does
* not: every sample simply decays, so a single number carries the entire history.
*
* <pre>
* mean <- mean + alpha * (x - mean)
* variance <- (1 - alpha) * (variance + alpha * (x - mean_before)^2)
* </pre>
*
* <p>The smoothing factor {@code alpha} in {@code (0, 1]} sets how fast the past is forgotten:
* {@code alpha == 1} keeps only the latest sample, while a small {@code alpha} produces a smooth but
* sluggish estimate. It is usually easier to specify the responsiveness in terms of a window, which
* the factory methods do for you:
*
* <table border="1">
* <caption>Ways to choose alpha</caption>
* <tr><th>Factory</th><th>alpha</th><th>Meaning</th></tr>
* <tr><td>{@link #ofAlpha(double)}</td><td>as given</td><td>direct control</td></tr>
* <tr><td>{@link #ofSpan(double)}</td><td>{@code 2 / (span + 1)}</td><td>comparable to a simple moving average of {@code span} samples</td></tr>
* <tr><td>{@link #ofHalfLife(double)}</td><td>{@code 1 - exp(-ln2 / halfLife)}</td><td>a sample loses half of its weight after {@code halfLife} steps</td></tr>
* </table>
*
* <p>The average is seeded with the first sample, which avoids the warm-up bias that a zero seed
* would introduce. Pass an explicit seed to {@link #ofAlpha(double, double)} when the resting level
* of the signal is known in advance, as a control chart does.
*
* <p>Both the update and every query run in O(1) time and O(1) memory. This class is not
* thread-safe.
*
* @see EwmaChangeDetector
* @see <a href="https://en.wikipedia.org/wiki/Moving_average#Exponential_moving_average">Exponential moving average</a>
*/
public final class ExponentialMovingAverage {
private final double alpha;
private final double seed;
private final boolean seeded;
private double mean;
private double variance;
private long count;
private boolean initialized;
private ExponentialMovingAverage(double alpha, double seed, boolean seeded) {
if (!(alpha > 0.0) || alpha > 1.0) {
throw new IllegalArgumentException("The smoothing factor alpha must lie in (0, 1], but was " + alpha);
}
if (seeded && !Double.isFinite(seed)) {
throw new IllegalArgumentException("The seed must be finite, but was " + seed);
}
this.alpha = alpha;
this.seed = seed;
this.seeded = seeded;
reset();
}
/**
* Creates an average that is seeded with its first sample.
*
* @param alpha smoothing factor in {@code (0, 1]}
* @return a new average
* @throws IllegalArgumentException if {@code alpha} is outside {@code (0, 1]}
*/
public static ExponentialMovingAverage ofAlpha(double alpha) {
return new ExponentialMovingAverage(alpha, 0.0, false);
}
/**
* Creates an average that starts from a known level instead of waiting for the first sample.
*
* @param alpha smoothing factor in {@code (0, 1]}
* @param seed the initial value of the average
* @return a new average
* @throws IllegalArgumentException if {@code alpha} is outside {@code (0, 1]} or {@code seed} is not finite
*/
public static ExponentialMovingAverage ofAlpha(double alpha, double seed) {
return new ExponentialMovingAverage(alpha, seed, true);
}
/**
* Creates an average whose responsiveness matches a simple moving average of {@code span}
* samples, that is {@code alpha = 2 / (span + 1)}.
*
* @param span the equivalent window length, greater than or equal to one
* @return a new average
* @throws IllegalArgumentException if {@code span} is smaller than one or not finite
*/
public static ExponentialMovingAverage ofSpan(double span) {
if (!(span >= 1.0) || !Double.isFinite(span)) {
throw new IllegalArgumentException("The span must be finite and at least 1, but was " + span);
}
return ofAlpha(2.0 / (span + 1.0));
}
/**
* Creates an average in which a sample loses half of its weight after {@code halfLife} updates,
* that is {@code alpha = 1 - exp(-ln2 / halfLife)}.
*
* @param halfLife number of updates after which a weight is halved, strictly positive
* @return a new average
* @throws IllegalArgumentException if {@code halfLife} is not strictly positive or not finite
*/
public static ExponentialMovingAverage ofHalfLife(double halfLife) {
if (!(halfLife > 0.0) || !Double.isFinite(halfLife)) {
throw new IllegalArgumentException("The half-life must be finite and positive, but was " + halfLife);
}
return ofAlpha(-Math.expm1(-Math.log(2.0) / halfLife));
}
/**
* Incorporates one sample.
*
* @param value the sample to add
* @return the updated average
* @throws IllegalArgumentException if {@code value} is NaN or infinite
*/
public double add(double value) {
if (!Double.isFinite(value)) {
throw new IllegalArgumentException("Samples must be finite, but was " + value);
}
count++;
if (!initialized) {
initialized = true;
mean = value;
variance = 0.0;
return mean;
}
double deviation = value - mean;
mean += alpha * deviation;
variance = (1.0 - alpha) * (variance + alpha * deviation * deviation);
return mean;
}
/**
* Incorporates every given sample, in order.
*
* @param values the samples to add
* @return the average after the last sample
* @throws IllegalArgumentException if any value is NaN or infinite
* @throws IllegalStateException if {@code values} is empty and the average is not initialized yet
* @throws NullPointerException if {@code values} is {@code null}
*/
public double addAll(double... values) {
for (double value : values) {
add(value);
}
return value();
}
/**
* Returns the current average.
*
* @return the exponentially weighted mean
* @throws IllegalStateException if the average has neither a seed nor a sample yet
*/
public double value() {
if (!initialized) {
throw new IllegalStateException("The average has not seen any sample yet");
}
return mean;
}
/**
* Returns the exponentially weighted variance of the stream, the natural companion of
* {@link #value()} when the spread matters as much as the level.
*
* @return the weighted variance, {@code 0} until the second sample arrives
* @throws IllegalStateException if the average has neither a seed nor a sample yet
*/
public double variance() {
if (!initialized) {
throw new IllegalStateException("The average has not seen any sample yet");
}
return variance;
}
/**
* Returns the square root of {@link #variance()}.
*
* @return the weighted standard deviation
* @throws IllegalStateException if the average has neither a seed nor a sample yet
*/
public double standardDeviation() {
return Math.sqrt(variance());
}
/**
* Returns the smoothing factor in use.
*
* @return alpha
*/
public double alpha() {
return alpha;
}
/**
* Returns the number of samples added since the last reset.
*
* @return the sample count, which excludes the seed
*/
public long count() {
return count;
}
/**
* Tells whether {@link #value()} may be queried.
*
* @return {@code true} once a seed or at least one sample is available
*/
public boolean isInitialized() {
return initialized;
}
/**
* Restores the state the average had right after construction.
*/
public void reset() {
count = 0;
variance = 0.0;
mean = seeded ? seed : 0.0;
initialized = seeded;
}
@Override
public String toString() {
return "ExponentialMovingAverage{alpha=" + alpha + ", value=" + (initialized ? mean : Double.NaN) + ", count=" + count + '}';
}
}