Skip to content

Commit ff0842d

Browse files
authored
Merge branch 'master' into add-perceptron
2 parents 3fc82a7 + 1f08afb commit ff0842d

11 files changed

Lines changed: 1773 additions & 26 deletions

src/main/java/com/thealgorithms/streaming/CusumDetector.java

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -251,29 +251,4 @@ private static void requireFinite(double value, String name) {
251251
throw new IllegalArgumentException("The " + name + " must be finite, but was " + value);
252252
}
253253
}
254-
255-
/**
256-
* What the detector reports after looking at one sample: either the stream still behaves as
257-
* expected, or its level has shifted, in one direction or the other.
258-
*/
259-
public enum ShiftSignal {
260-
261-
/** No evidence of a change; the stream is in control. */
262-
NONE,
263-
264-
/** The level of the stream has moved above the target. */
265-
UPWARD,
266-
267-
/** The level of the stream has moved below the target. */
268-
DOWNWARD;
269-
270-
/**
271-
* Tells whether this signal reports a change.
272-
*
273-
* @return {@code true} for {@link #UPWARD} and {@link #DOWNWARD}
274-
*/
275-
public boolean isAlarm() {
276-
return this != NONE;
277-
}
278-
}
279254
}
Lines changed: 251 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,251 @@
1+
package com.thealgorithms.streaming;
2+
3+
/**
4+
* An <b>EWMA control chart</b>: change detection built on the exponentially weighted moving average.
5+
*
6+
* <p>The detector smooths the stream with
7+
* {@link ExponentialMovingAverage} seeded at the target level and compares the smoothed value with a
8+
* band around that target:
9+
*
10+
* <pre>
11+
* z &lt;- z + alpha * (x - target)
12+
* limit = width * sigma * sqrt( alpha / (2 - alpha) * (1 - (1 - alpha)^(2n)) )
13+
* </pre>
14+
*
15+
* <p>The band is the exact standard deviation of {@code z} under the null hypothesis, multiplied by
16+
* the desired width in sigmas. It starts narrow and widens towards its asymptote
17+
* {@code width * sigma * sqrt(alpha / (2 - alpha))}, which keeps the false alarm rate steady during
18+
* the warm-up instead of letting the first few samples trip the alarm. An alarm fires as soon as the
19+
* smoothed value leaves the band; the average is then reset to the target so that the detector
20+
* starts fresh on the next change rather than latching.
21+
*
22+
* <p>Where {@link CusumDetector} accumulates evidence without limit and so excels at small,
23+
* persistent shifts, an EWMA chart looks at a decaying window of the recent past: {@code alpha}
24+
* around {@code 0.1 - 0.3} is a good compromise, larger values reacting faster to big jumps and
25+
* smaller ones being more sensitive to slow drifts. A width of 3 sigmas is the customary setting.
26+
*
27+
* <h2>Usage</h2>
28+
*
29+
* <pre>{@code
30+
* EwmaChangeDetector detector = new EwmaChangeDetector(20.0, 0.5, 0.2, 3.0);
31+
* for (double sample : stream) {
32+
* if (detector.accept(sample).isAlarm()) {
33+
* alert(detector.statistic(), detector.controlLimit());
34+
* }
35+
* }
36+
* }</pre>
37+
*
38+
* <p>Each sample costs O(1) time and the detector keeps O(1) state. This class is not thread-safe.
39+
*
40+
* @see CusumDetector
41+
* @see ExponentialMovingAverage
42+
* @see <a href="https://en.wikipedia.org/wiki/EWMA_chart">EWMA chart</a>
43+
*/
44+
public final class EwmaChangeDetector {
45+
46+
/** Smoothing factor used when none is given. */
47+
public static final double DEFAULT_ALPHA = 0.2;
48+
49+
/** Half-width of the control band, in sigmas, used when none is given. */
50+
public static final double DEFAULT_WIDTH = 3.0;
51+
52+
private final double target;
53+
private final double standardDeviation;
54+
private final double alpha;
55+
private final double width;
56+
57+
private final ExponentialMovingAverage average;
58+
private long count;
59+
private long stepsSinceAlarm;
60+
private long alarmCount;
61+
private ShiftSignal lastSignal = ShiftSignal.NONE;
62+
63+
/**
64+
* Creates a detector with the customary smoothing factor of {@code 0.2} and a band of three
65+
* sigmas.
66+
*
67+
* @param target the level the stream is expected to sit at
68+
* @param standardDeviation the noise level of the stream, strictly positive
69+
* @throws IllegalArgumentException if {@code target} is not finite or {@code standardDeviation} is not strictly positive
70+
*/
71+
public EwmaChangeDetector(double target, double standardDeviation) {
72+
this(target, standardDeviation, DEFAULT_ALPHA, DEFAULT_WIDTH);
73+
}
74+
75+
/**
76+
* Creates a detector.
77+
*
78+
* @param target the level the stream is expected to sit at
79+
* @param standardDeviation the noise level of the stream, strictly positive
80+
* @param alpha smoothing factor in {@code (0, 1]}; larger values react faster but tolerate less noise
81+
* @param width half-width of the control band in sigmas, strictly positive
82+
* @throws IllegalArgumentException if any argument is not finite, if {@code standardDeviation} or
83+
* {@code width} is not strictly positive, or if {@code alpha} is outside {@code (0, 1]}
84+
*/
85+
public EwmaChangeDetector(double target, double standardDeviation, double alpha, double width) {
86+
if (!Double.isFinite(target)) {
87+
throw new IllegalArgumentException("The target must be finite, but was " + target);
88+
}
89+
if (!(standardDeviation > 0.0) || !Double.isFinite(standardDeviation)) {
90+
throw new IllegalArgumentException("The standard deviation must be finite and strictly positive, but was " + standardDeviation);
91+
}
92+
if (!(width > 0.0) || !Double.isFinite(width)) {
93+
throw new IllegalArgumentException("The width must be finite and strictly positive, but was " + width);
94+
}
95+
this.target = target;
96+
this.standardDeviation = standardDeviation;
97+
this.width = width;
98+
this.average = ExponentialMovingAverage.ofAlpha(alpha, target);
99+
this.alpha = this.average.alpha();
100+
}
101+
102+
/**
103+
* Feeds one sample into the detector.
104+
*
105+
* @param value the incoming sample
106+
* @return {@link ShiftSignal#NONE} while the smoothed value stays inside the control band,
107+
* otherwise the direction in which it left the band; the average is reset to the target on an alarm
108+
* @throws IllegalArgumentException if {@code value} is NaN or infinite
109+
*/
110+
public ShiftSignal accept(double value) {
111+
if (!Double.isFinite(value)) {
112+
throw new IllegalArgumentException("Samples must be finite, but was " + value);
113+
}
114+
count++;
115+
stepsSinceAlarm++;
116+
double smoothed = average.add(value);
117+
double deviation = smoothed - target;
118+
double limit = controlLimit();
119+
120+
if (deviation > limit) {
121+
lastSignal = ShiftSignal.UPWARD;
122+
} else if (deviation < -limit) {
123+
lastSignal = ShiftSignal.DOWNWARD;
124+
} else {
125+
lastSignal = ShiftSignal.NONE;
126+
}
127+
128+
if (lastSignal.isAlarm()) {
129+
alarmCount++;
130+
stepsSinceAlarm = 0;
131+
average.reset();
132+
}
133+
return lastSignal;
134+
}
135+
136+
/**
137+
* Runs the detector over a whole signal.
138+
*
139+
* @param signal the samples to inspect
140+
* @return a new array of the same length holding the verdict for every sample
141+
* @throws IllegalArgumentException if any sample is NaN or infinite
142+
* @throws NullPointerException if {@code signal} is {@code null}
143+
*/
144+
public ShiftSignal[] scan(double[] signal) {
145+
ShiftSignal[] signals = new ShiftSignal[signal.length];
146+
for (int i = 0; i < signal.length; i++) {
147+
signals[i] = accept(signal[i]);
148+
}
149+
return signals;
150+
}
151+
152+
/**
153+
* Returns the smoothed value the alarm decision is based on.
154+
*
155+
* @return the exponentially weighted average of the stream
156+
*/
157+
public double statistic() {
158+
return average.value();
159+
}
160+
161+
/**
162+
* Returns the current half-width of the control band, which widens during the warm-up and then
163+
* settles.
164+
*
165+
* @return the distance from the target at which an alarm fires
166+
*/
167+
public double controlLimit() {
168+
double asymptotic = alpha / (2.0 - alpha);
169+
double warmUp = -Math.expm1(2.0 * stepsSinceAlarm * Math.log1p(-alpha));
170+
return width * standardDeviation * Math.sqrt(asymptotic * warmUp);
171+
}
172+
173+
/**
174+
* Returns the verdict on the most recent sample.
175+
*
176+
* @return the last signal, {@link ShiftSignal#NONE} before the first sample
177+
*/
178+
public ShiftSignal lastSignal() {
179+
return lastSignal;
180+
}
181+
182+
/**
183+
* Returns how many samples have been inspected since the last reset.
184+
*
185+
* @return the sample count
186+
*/
187+
public long count() {
188+
return count;
189+
}
190+
191+
/**
192+
* Returns how many alarms have been raised since the last reset.
193+
*
194+
* @return the alarm count
195+
*/
196+
public long alarmCount() {
197+
return alarmCount;
198+
}
199+
200+
/**
201+
* Returns the expected level of the stream.
202+
*
203+
* @return the target given at construction time
204+
*/
205+
public double target() {
206+
return target;
207+
}
208+
209+
/**
210+
* Returns the assumed noise level.
211+
*
212+
* @return the standard deviation given at construction time
213+
*/
214+
public double standardDeviation() {
215+
return standardDeviation;
216+
}
217+
218+
/**
219+
* Returns the smoothing factor in use.
220+
*
221+
* @return alpha
222+
*/
223+
public double alpha() {
224+
return alpha;
225+
}
226+
227+
/**
228+
* Returns the configured band half-width.
229+
*
230+
* @return the width in sigmas given at construction time
231+
*/
232+
public double width() {
233+
return width;
234+
}
235+
236+
/**
237+
* Returns the average to the target and clears the counters.
238+
*/
239+
public void reset() {
240+
average.reset();
241+
count = 0;
242+
stepsSinceAlarm = 0;
243+
alarmCount = 0;
244+
lastSignal = ShiftSignal.NONE;
245+
}
246+
247+
@Override
248+
public String toString() {
249+
return "EwmaChangeDetector{target=" + target + ", statistic=" + statistic() + ", limit=" + controlLimit() + ", alarms=" + alarmCount + '}';
250+
}
251+
}

0 commit comments

Comments
 (0)