movingWeightedMeanBy()
BigDecimal objects mapped from a Stream<INPUT> via a mappingFunction and looking back windowSize elements, giving newer values more weight.Implementation Notes
This implementation is suitable for mapping an arbitrary Stream<INPUT> to BigDecimal via a mappingFunction; for a version that operates directly on a Stream<BigDecimal>, see movingWeightedMean().
A weighted moving mean works like movingMeanBy(), except that the values in the window do not count equally. With a windowSize, the oldest value in the window has a weight of 1, the next oldest has a weight of 2, and so on, up to the newest value, which has a weight of windowSize. The result is the sum of each value multiplied by its weight, divided by the sum of the weights. This is also known as the weighted moving average (WMA).
For example, if the window holds 10, 20, and 30 (oldest to newest), the weights are 1, 2, and 3, and the result is (10 * 1 + 20 * 2 + 30 * 3) / 6 = 140 / 6 = 23.333.... A plain mean of the same window is 20.
Because newer values count for more, the weighted mean reacts to changes faster than movingMeanBy(). Unlike the exponential moving average (see exponentialMovingAverageWithAlphaBy()), where old values fade but never fully disappear, a value in a weighted moving mean stops counting completely as soon as it leaves the window.
Until the window is full, the newest weights are used, as with custom weights below: with a windowSize of 3 and two values in the window, the weights are 2 and 3 (the newest two of 1, 2, 3), so the window 10, 20 gives (10 * 2 + 20 * 3) / 5 = 16, and the very first value gets a weight of 3 on its own.
By default, nulls are ignored and play no part in calculations, see treatNullAs() and treatNullAsZero() below for ways to change this behavior. The default MathContext
for all calculations is
MathContext.DECIMAL64, but this can be overridden (see withMathContext(), below). Each input is rounded to the MathContext once, when it arrives, and the weighted sum is otherwise calculated exactly. Only the final division is rounded.
Signatures
movingWeightedMeanBy(int windowSize, Function<INPUT, BigDecimal> mappingFunction)
windowSize- How many trailing elements to average over at any given point in the stream. Must be greater than 1. Weights run from 1 (oldest) towindowSize(newest).mappingFunction- A non-null function to map streamINPUTelements intoBigDecimalfor calculation
movingWeightedMeanBy(List<BigDecimal> weights, Function<INPUT, BigDecimal> mappingFunction)
weights- One weight per element in the window, ordered from oldest to newest. The window size is the number of weights, which must be at least 2. Weights may not be null or negative, and must not sum to zero. They do not need to sum to 1, because the result is always divided by the sum of the weights.mappingFunction- A non-null function to map streamINPUTelements intoBigDecimalfor calculation
Custom weights are ordered oldest to newest
The first weight applies to the oldest element in the window, and the last weight applies to the newest. If you have weights in “newest first” order, reverse them before passing them in. A reversed list is accepted without complaint and gives you the opposite weighting, so it is worth double-checking (see movingWeightedMean() for an example).
// Oldest -> newest: the newest value counts for half of the result
List.of(new BigDecimal("0.2"), new BigDecimal("0.3"), new BigDecimal("0.5"))
While the window is not yet full, the newest weights are used and the result is divided by their sum. With weights [1, 1, 2] and only two values seen so far, the weights [1, 2] are used. This is the same rule movingWeightedMeanBy(int windowSize, fn) follows, so movingWeightedMeanBy(3, fn) gives exactly the same results as movingWeightedMeanBy(List.of(1, 2, 3), fn), including while the window is filling.
Weights of zero are allowed. For example, [1, 1, 0] means “the average of the previous two values, excluding the current one”. While the window is filling, only the newest weights are used, and if those happen to sum to zero the mean is undefined (0 divided by 0), so nothing is emitted for that element. With [1, 1, 0], the first element emits nothing, because the only weight in use is the 0. A list whose weights all sum to zero is rejected when the method is called.
record NamedValue(String name, BigDecimal value) {}
Stream
.of(
new NamedValue("first", new BigDecimal("10")),
new NamedValue("second", new BigDecimal("20")),
new NamedValue("third", new BigDecimal("30")),
new NamedValue("fourth", new BigDecimal("40"))
)
.gather(Gatherers4j.movingWeightedMeanBy(
List.of(new BigDecimal("1"), new BigDecimal("1"), new BigDecimal("0")),
NamedValue::value
))
.toList();
// [10, 15, 25]
Additional Methods
| Method | Purpose |
|---|---|
excludePartialValues() | When calculating the weighted moving mean, and the full size of the window has not yet been reached, the gatherer should suppress emitting values until the lookback window is full. See example. |
treatNullAsZero() | When encountering a null value in a stream, treat it as BigDecimal.ZERO instead. See example. |
treatNullAs(BigDecimal replacement) | When encountering a null value in a stream, treat it as the given replacement value instead. See example. |
treatNullAsOne() | When encountering a null value in a stream, treat it as BigDecimal.ONE instead. |
withMathContext(MathContext mathContext) | Replace the MathContext used for all mathematical operations performed by this gatherer. See example. |
withOriginal() | Include the original input value from the stream in addition to the calculated value in a WithOriginal record. See example. |
Examples
Weighted moving mean of window size 3, mapped from an object
The weights are 1, 2, and 3. For the third element, (10 * 1 + 20 * 2 + 30 * 3) / 6 = 23.33....
record NamedValue(String name, BigDecimal value) {}
Stream
.of(
new NamedValue("first", new BigDecimal("10")),
new NamedValue("second", new BigDecimal("20")),
new NamedValue("third", new BigDecimal("30")),
new NamedValue("fourth", new BigDecimal("40"))
)
.gather(Gatherers4j.movingWeightedMeanBy(3, NamedValue::value))
.toList();
// [
// BigDecimal("10")
// BigDecimal("16")
// BigDecimal("23.33333333333333")
// BigDecimal("33.33333333333333")
// ]
Custom weights
Weights are ordered oldest to newest, so [1, 1, 2] counts the newest value twice as much as each of the other two. For the third element, (10 * 1 + 20 * 1 + 30 * 2) / 4 = 22.5.
record NamedValue(String name, BigDecimal value) {}
Stream
.of(
new NamedValue("first", new BigDecimal("10")),
new NamedValue("second", new BigDecimal("20")),
new NamedValue("third", new BigDecimal("30")),
new NamedValue("fourth", new BigDecimal("40"))
)
.gather(Gatherers4j.movingWeightedMeanBy(
List.of(new BigDecimal("1"), new BigDecimal("1"), new BigDecimal("2")),
NamedValue::value
))
.toList();
// [
// BigDecimal("10")
// BigDecimal("16.66666666666667")
// BigDecimal("22.5")
// BigDecimal("32.5")
// ]
Custom weights for a forecast
Here the newest value contributes 50%, the one before it 30%, and the oldest 20%: 100 * 0.2 + 120 * 0.3 + 150 * 0.5 = 131.
record NamedValue(String name, BigDecimal value) {}
Stream
.of(
new NamedValue("Mon", new BigDecimal("100")),
new NamedValue("Tue", new BigDecimal("120")),
new NamedValue("Wed", new BigDecimal("150")),
new NamedValue("Thu", new BigDecimal("140"))
)
.gather(Gatherers4j.movingWeightedMeanBy(
List.of(new BigDecimal("0.2"), new BigDecimal("0.3"), new BigDecimal("0.5")),
NamedValue::value
))
.toList();
// [
// BigDecimal("100")
// BigDecimal("112.5")
// BigDecimal("131")
// BigDecimal("139")
// ]
Excluding partial values
Showing that in-process weighted moving mean values are not emitted for each element until the lookback window has been filled.
record NamedValue(String name, BigDecimal value) {}
Stream
.of(
new NamedValue("first", new BigDecimal("10")),
new NamedValue("second", new BigDecimal("20")),
new NamedValue("third", new BigDecimal("30")),
new NamedValue("fourth", new BigDecimal("40"))
)
.gather(Gatherers4j.movingWeightedMeanBy(3, NamedValue::value).excludePartialValues())
.toList();
// [
// BigDecimal("23.33333333333333")
// BigDecimal("33.33333333333333")
// ]
Showing nulls are ignored by default
record NamedValue(String name, BigDecimal value) {}
Stream
.of(
new NamedValue("first", null),
new NamedValue("second", null),
new NamedValue("third", new BigDecimal("30")),
new NamedValue("fourth", new BigDecimal("60"))
)
.gather(Gatherers4j.movingWeightedMeanBy(3, NamedValue::value))
.toList();
// [
// BigDecimal("30")
// BigDecimal("48")
// ]
Treating null as zero
record NamedValue(String name, BigDecimal value) {}
Stream
.of(
new NamedValue("first", null),
new NamedValue("second", null),
new NamedValue("third", new BigDecimal("30")),
new NamedValue("fourth", new BigDecimal("60"))
)
.gather(Gatherers4j.movingWeightedMeanBy(3, NamedValue::value).treatNullAsZero())
.toList();
// [
// BigDecimal("0")
// BigDecimal("0")
// BigDecimal("15")
// BigDecimal("40")
// ]
Replacing null with another BigDecimal
record NamedValue(String name, BigDecimal value) {}
Stream
.of(
new NamedValue("first", null),
new NamedValue("second", null),
new NamedValue("third", new BigDecimal("30")),
new NamedValue("fourth", new BigDecimal("60"))
)
.gather(Gatherers4j.movingWeightedMeanBy(3, NamedValue::value).treatNullAs(new BigDecimal("3.5")))
.toList();
// [
// BigDecimal("3.5")
// BigDecimal("3.5")
// BigDecimal("16.75")
// BigDecimal("40.58333333333333")
// ]
Specifying a new MathContext
record NamedValue(String name, BigDecimal value) {}
Stream
.of(
new NamedValue("first", new BigDecimal("10")),
new NamedValue("second", new BigDecimal("20")),
new NamedValue("third", new BigDecimal("30")),
new NamedValue("fourth", new BigDecimal("40"))
)
.gather(Gatherers4j
.movingWeightedMeanBy(3, NamedValue::value)
.withMathContext(new MathContext(3, RoundingMode.DOWN))
)
.toList();
// [
// BigDecimal("10")
// BigDecimal("16")
// BigDecimal("23.3")
// BigDecimal("33.3")
// ]
Emitting a record containing the original and calculated values
record NamedValue(String name, BigDecimal value) {}
Stream
.of(
new NamedValue("first", new BigDecimal("10")),
new NamedValue("second", new BigDecimal("20")),
new NamedValue("third", new BigDecimal("30")),
new NamedValue("fourth", new BigDecimal("40"))
)
.gather(Gatherers4j.movingWeightedMeanBy(3, NamedValue::value).withOriginal())
.toList();
// [
// WithOriginal[original=NamedValue[name=first, value=10], calculated=10]
// WithOriginal[original=NamedValue[name=second, value=20], calculated=16]
// WithOriginal[original=NamedValue[name=third, value=30], calculated=23.33333333333333]
// WithOriginal[original=NamedValue[name=fourth, value=40], calculated=33.33333333333333]
// ]