Skip to content

Commit 03b0fb8

Browse files
authored
Merge branch 'master' into crapxxi-branch
2 parents 835933a + a0c3eb5 commit 03b0fb8

2 files changed

Lines changed: 566 additions & 0 deletions

File tree

Lines changed: 354 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,354 @@
1+
package com.thealgorithms.streaming;
2+
3+
/**
4+
* <b>ADWIN</b>, adaptive windowing, after Bifet and Gavalda: an average over a window whose length is
5+
* not a parameter but a result.
6+
*
7+
* <p>Every windowed estimator forces the same bad choice. A long window is accurate while nothing
8+
* changes and hopelessly slow once something does; a short one reacts immediately and is noisy the
9+
* rest of the time. ADWIN refuses the choice: it keeps a window of recent values and, after every
10+
* sample, looks for a way to split it into an old part and a recent part whose means are too far
11+
* apart to be explained by chance. When it finds one, the old part is dropped. The window therefore
12+
* grows on its own while the stream is stationary and collapses as soon as the stream moves, and the
13+
* length it settles at is an estimate of how long the current regime has been running.
14+
*
15+
* <p>"Too far apart" is a variance sensitive Hoeffding bound. For a cut into sub-windows of
16+
* {@code n0} and {@code n1} elements, with {@code v} the variance of the whole window:
17+
*
18+
* <pre>
19+
* m = 1 / (n0 - minLength + 1) + 1 / (n1 - minLength + 1)
20+
* d = ln( 2 * ln(width) / delta )
21+
* epsilon = sqrt(2 * m * v * d) + 2/3 * d * m
22+
* cut when |mean0 - mean1| &gt; epsilon
23+
* </pre>
24+
*
25+
* <p>The {@code delta} parameter is a confidence level: the probability of cutting a window that
26+
* never changed is bounded by it, which is the guarantee that makes the window length trustworthy.
27+
* Smaller values make the detector more conservative and slower.
28+
*
29+
* <p>Keeping every sample would cost O(n) memory, so the window is stored as an exponential
30+
* histogram: buckets of 1, 2, 4, 8 ... elements, at most {@code MAX_BUCKETS} of each size, each
31+
* holding the sum and the variance of the elements it covers. That is O(log n) buckets for a window
32+
* of n elements, and cuts are only tried at bucket boundaries, which is what keeps a sample O(log n)
33+
* instead of O(n) while costing only a bounded loss of resolution.
34+
*
35+
* <h2>Usage</h2>
36+
*
37+
* <pre>{@code
38+
* Adwin window = new Adwin(0.002);
39+
* for (double sample : stream) {
40+
* if (window.accept(sample)) {
41+
* alert(window.estimate(), window.width());
42+
* }
43+
* }
44+
* }</pre>
45+
*
46+
* <p>This class is not thread-safe.
47+
*
48+
* @see CusumDetector
49+
* @see <a href="https://en.wikipedia.org/wiki/Concept_drift">Concept drift</a>, the problem ADWIN was written for; the algorithm is due to A. Bifet and R. Gavalda, Learning from Time-Changing Data with Adaptive Windowing, SDM 2007
50+
*/
51+
public final class Adwin {
52+
53+
/** Confidence level used when none is given. */
54+
public static final double DEFAULT_DELTA = 0.002;
55+
56+
/** How many buckets of the same size the histogram holds before merging the two oldest. */
57+
public static final int MAX_BUCKETS = 5;
58+
59+
private static final int MIN_SUBWINDOW = 5;
60+
private static final long MIN_WIDTH_FOR_DETECTION = 2L * MIN_SUBWINDOW;
61+
62+
private final double delta;
63+
64+
private Row newest;
65+
private Row oldest;
66+
67+
private long width;
68+
private double total;
69+
private double variance;
70+
private int bucketCount;
71+
private long count;
72+
private long changeCount;
73+
74+
/**
75+
* Creates a window with the customary confidence level of {@code 0.002}.
76+
*/
77+
public Adwin() {
78+
this(DEFAULT_DELTA);
79+
}
80+
81+
/**
82+
* Creates a window.
83+
*
84+
* @param delta the confidence level, in {@code (0, 1)}; smaller values cut less eagerly
85+
* @throws IllegalArgumentException if {@code delta} is outside {@code (0, 1)}
86+
*/
87+
public Adwin(double delta) {
88+
if (!(delta > 0.0) || !(delta < 1.0)) {
89+
throw new IllegalArgumentException("The delta must lie in (0, 1), but was " + delta);
90+
}
91+
this.delta = delta;
92+
start();
93+
}
94+
95+
/**
96+
* Feeds one sample into the window.
97+
*
98+
* @param value the incoming sample
99+
* @return {@code true} if the window was cut, that is if the stream changed
100+
* @throws IllegalArgumentException if {@code value} is NaN or infinite
101+
*/
102+
public boolean accept(double value) {
103+
if (!Double.isFinite(value)) {
104+
throw new IllegalArgumentException("Samples must be finite, but was " + value);
105+
}
106+
count++;
107+
insert(value);
108+
return detectChange();
109+
}
110+
111+
/**
112+
* Runs the window over a whole signal.
113+
*
114+
* @param signal the samples to inspect
115+
* @return a new array of the same length saying for every sample whether it cut the window
116+
* @throws IllegalArgumentException if any sample is NaN or infinite
117+
* @throws NullPointerException if {@code signal} is {@code null}
118+
*/
119+
public boolean[] scan(double[] signal) {
120+
boolean[] cuts = new boolean[signal.length];
121+
for (int i = 0; i < signal.length; i++) {
122+
cuts[i] = accept(signal[i]);
123+
}
124+
return cuts;
125+
}
126+
127+
/**
128+
* Returns the current estimate of the level of the stream.
129+
*
130+
* @return the mean of the window, {@code 0} while the window is empty
131+
*/
132+
public double estimate() {
133+
return width == 0 ? 0.0 : total / width;
134+
}
135+
136+
/**
137+
* Returns the length of the window, which is how many recent samples the estimate rests on.
138+
*
139+
* @return the window width
140+
*/
141+
public long width() {
142+
return width;
143+
}
144+
145+
/**
146+
* Returns the variance of the samples inside the window.
147+
*
148+
* @return the window variance, {@code 0} while the window holds fewer than two samples
149+
*/
150+
public double variance() {
151+
return width < 2 ? 0.0 : variance / width;
152+
}
153+
154+
/**
155+
* Returns how many buckets the histogram holds, which grows like the logarithm of the width.
156+
*
157+
* @return the bucket count
158+
*/
159+
public int bucketCount() {
160+
return bucketCount;
161+
}
162+
163+
/**
164+
* Returns how many samples have been inspected since the last reset.
165+
*
166+
* @return the sample count
167+
*/
168+
public long count() {
169+
return count;
170+
}
171+
172+
/**
173+
* Returns how many times the window has been cut since the last reset.
174+
*
175+
* @return the number of detected changes
176+
*/
177+
public long changeCount() {
178+
return changeCount;
179+
}
180+
181+
/**
182+
* Returns the configured confidence level.
183+
*
184+
* @return the delta given at construction time
185+
*/
186+
public double delta() {
187+
return delta;
188+
}
189+
190+
/**
191+
* Empties the window.
192+
*/
193+
public void reset() {
194+
start();
195+
count = 0;
196+
changeCount = 0;
197+
}
198+
199+
@Override
200+
public String toString() {
201+
return "Adwin{width=" + width + ", estimate=" + estimate() + ", buckets=" + bucketCount + ", changes=" + changeCount + "}";
202+
}
203+
204+
private void start() {
205+
newest = new Row(0);
206+
oldest = newest;
207+
width = 0;
208+
total = 0.0;
209+
variance = 0.0;
210+
bucketCount = 0;
211+
}
212+
213+
private void insert(double value) {
214+
width++;
215+
newest.add(value, 0.0);
216+
bucketCount++;
217+
if (width > 1) {
218+
double deviation = value - total / (width - 1);
219+
variance += (width - 1) * deviation * deviation / width;
220+
}
221+
total += value;
222+
compress();
223+
}
224+
225+
/**
226+
* Merges the two oldest buckets of every row that has run out of room into one bucket of the next
227+
* row, which is what keeps the number of buckets logarithmic in the width.
228+
*/
229+
private void compress() {
230+
Row row = newest;
231+
while (row != null && row.size > MAX_BUCKETS) {
232+
if (row.older == null) {
233+
row.older = new Row(row.level + 1);
234+
row.older.newer = row;
235+
oldest = row.older;
236+
}
237+
long size = 1L << row.level;
238+
double firstMean = row.totals[0] / size;
239+
double secondMean = row.totals[1] / size;
240+
double merged = size * size * (firstMean - secondMean) * (firstMean - secondMean) / (size + size);
241+
row.older.add(row.totals[0] + row.totals[1], row.variances[0] + row.variances[1] + merged);
242+
row.removeOldest(2);
243+
bucketCount--;
244+
row = row.older;
245+
}
246+
}
247+
248+
/**
249+
* Tries every cut the histogram allows, from the oldest boundary inwards, and drops the oldest
250+
* bucket whenever a cut is significant. Repeats until no cut is left.
251+
*
252+
* @return whether anything was dropped
253+
*/
254+
private boolean detectChange() {
255+
boolean changed = false;
256+
boolean searching = true;
257+
258+
while (searching && width >= MIN_WIDTH_FOR_DETECTION) {
259+
searching = false;
260+
long oldWidth = 0;
261+
double oldTotal = 0.0;
262+
263+
outer:
264+
for (Row row = oldest; row != null; row = row.newer) {
265+
for (int bucket = 0; bucket < row.size; bucket++) {
266+
if (row.newer == null && bucket == row.size - 1) {
267+
break outer;
268+
}
269+
oldWidth += 1L << row.level;
270+
oldTotal += row.totals[bucket];
271+
long recentWidth = width - oldWidth;
272+
double recentTotal = total - oldTotal;
273+
if (recentWidth < MIN_SUBWINDOW) {
274+
break outer;
275+
}
276+
if (oldWidth >= MIN_SUBWINDOW && isSignificant(oldWidth, recentWidth, oldTotal / oldWidth - recentTotal / recentWidth)) {
277+
changed = true;
278+
searching = true;
279+
changeCount++;
280+
dropOldestBucket();
281+
break outer;
282+
}
283+
}
284+
}
285+
}
286+
return changed;
287+
}
288+
289+
private boolean isSignificant(long oldWidth, long recentWidth, double difference) {
290+
double harmonic = 1.0 / (oldWidth - MIN_SUBWINDOW + 1) + 1.0 / (recentWidth - MIN_SUBWINDOW + 1);
291+
double confidence = Math.log(2.0 * Math.log(width) / delta);
292+
double windowVariance = variance / width;
293+
double epsilon = Math.sqrt(2.0 * harmonic * windowVariance * confidence) + 2.0 / 3.0 * confidence * harmonic;
294+
return Math.abs(difference) > epsilon;
295+
}
296+
297+
private void dropOldestBucket() {
298+
Row row = oldest;
299+
long size = 1L << row.level;
300+
double bucketTotal = row.totals[0];
301+
302+
width -= size;
303+
total -= bucketTotal;
304+
if (width > 0) {
305+
double bucketMean = bucketTotal / size;
306+
double difference = bucketMean - total / width;
307+
variance -= row.variances[0] + size * width * difference * difference / (size + width);
308+
} else {
309+
variance = 0.0;
310+
}
311+
if (variance < 0.0) {
312+
variance = 0.0;
313+
}
314+
315+
row.removeOldest(1);
316+
bucketCount--;
317+
if (row.size == 0 && row.newer != null) {
318+
oldest = row.newer;
319+
oldest.older = null;
320+
}
321+
}
322+
323+
/**
324+
* One row of the exponential histogram: up to {@code MAX_BUCKETS + 1} buckets that each cover
325+
* {@code 2^level} samples, the oldest at index zero.
326+
*/
327+
private static final class Row {
328+
329+
private final int level;
330+
private final double[] totals = new double[MAX_BUCKETS + 1];
331+
private final double[] variances = new double[MAX_BUCKETS + 1];
332+
private int size;
333+
private Row older;
334+
private Row newer;
335+
336+
Row(int level) {
337+
this.level = level;
338+
}
339+
340+
void add(double total, double variance) {
341+
totals[size] = total;
342+
variances[size] = variance;
343+
size++;
344+
}
345+
346+
void removeOldest(int buckets) {
347+
for (int i = buckets; i < size; i++) {
348+
totals[i - buckets] = totals[i];
349+
variances[i - buckets] = variances[i];
350+
}
351+
size -= buckets;
352+
}
353+
}
354+
}

0 commit comments

Comments
 (0)