Decayed Value (aka. moving average)
A DecayedValue
can be approximately computed into a moving average. Please see an explanation of different averages here: The Decaying Average.
@johnynek mentioned that:
a DecayedValue is approximately like a moving average value with window size of the half-life. It is EXACTLY a sample of the Laplace transform of the signal of values. Therefore, if we normalize a decayed value with halflife/ln(2), which is the integral of exp(-t(ln(2))/halflife) from 0 to infinity. We get a moving average of the window size equal to halflife.
See the related issue: https://github.com/twitter/algebird/issues/235
Here is the code example for computing a DecayedValue
average:
import com.twitter.algebird._
// import com.twitter.algebird._
val data = {
val rnd = new scala.util.Random
(1 to 10).map { _ => rnd.nextInt(1000).toDouble }.toSeq
}
// data: scala.collection.immutable.Seq[Double] = Vector(540.0, 447.0, 4.0, 480.0, 155.0, 44.0, 582.0, 139.0, 270.0, 841.0)
val HalfLife = 5.0
// HalfLife: Double = 5.0
val normalization = HalfLife / math.log(2)
// normalization: Double = 7.213475204444817
implicit val dvMonoid = DecayedValue.monoidWithEpsilon(1e-3)
// dvMonoid: com.twitter.algebird.Monoid[com.twitter.algebird.DecayedValue] = DecayedValueMonoid(0.001)
data.zipWithIndex.scanLeft(Monoid.zero[DecayedValue]) { (previous, data) =>
val (value, time) = data
val decayed = Monoid.plus(previous, DecayedValue.build(value, time, HalfLife))
println("At %d: decayed=%f".format(time, (decayed.value / normalization)))
decayed
}
// At 0: decayed=74.859896
// At 1: decayed=127.136682
// At 2: decayed=111.233428
// At 3: decayed=163.376453
// At 4: decayed=163.715026
// At 5: decayed=148.621903
// At 6: decayed=210.065213
// At 7: decayed=202.141881
// At 8: decayed=213.404676
// At 9: decayed=302.366917
// res0: scala.collection.immutable.Seq[com.twitter.algebird.DecayedValue] = Vector(DecayedValue(0.0,-Infinity), DecayedValue(540.0,0.0), DecayedValue(917.097304179907,0.13862943611198905), DecayedValue(802.3795747511749,0.2772588722239781), DecayedValue(1178.51199077694,0.4158883083359671), DecayedValue(1180.9542774221018,0.5545177444479562), DecayedValue(1072.080411436778,0.6931471805599453), DecayedValue(1515.3002060750277,0.8317766166719343), DecayedValue(1458.1454479613483,0.9704060527839233), DecayedValue(1539.389341090431,1.1090354888959124), DecayedValue(2181.116258018324,1.2476649250079015))
Running the above code in comparison with a simple decayed average:
data.zipWithIndex.scanLeft(0.0) { (previous, data) =>
val (value, time) = data
val avg = (value + previous * (HalfLife - 1.0)) / HalfLife
println("At %d: windowed=%f".format(time, avg))
avg
}
// At 0: windowed=108.000000
// At 1: windowed=175.800000
// At 2: windowed=141.440000
// At 3: windowed=209.152000
// At 4: windowed=198.321600
// At 5: windowed=167.457280
// At 6: windowed=250.365824
// At 7: windowed=228.092659
// At 8: windowed=236.474127
// At 9: windowed=357.379302
// res1: scala.collection.immutable.Seq[Double] = Vector(0.0, 108.0, 175.8, 141.44, 209.152, 198.3216, 167.45728, 250.36582399999998, 228.09265919999999, 236.47412735999995, 357.379301888)
You’ll see that the averages are pretty close to each other.
DecayedValue FAQ
Is a DecayedValue average better than a moving average?
DecayedValue gives an exponential moving average. A standard windowed moving average of N
points requires O(N)
memory. An exponential moving average takes O(1)
memory. That’s the win. Usually one, or a few, exponential moving averages gives you what you need cheaper than the naive approach of keeping 100 or 1000 recent points.
Is a DecayedValue average better than a simple decayed average?
A simple decayed average looks like this:
val avg = (value + previousAvg * (HaflLife - 1.0)) / HalfLife
In a way, a DecayedValue
average is a simple decayed average with different scaling factor.
Documentation Help
We’d love your help fleshing out this documentation! You can edit this page in your browser by clicking this link. These links might be helpful: