FixedBinsAccumulator.java
package org.djutils.stats.summarizers.quantileaccumulator;
import org.djutils.exceptions.Throw;
import org.djutils.stats.summarizers.Tally;
/**
* FixedBinsAccumulator.java. <br>
* This accumulator is created with a caller prescribes set of bins. All bins have the same width.
* <br>
* Copyright (c) 2021-2024 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
* for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
* distributed under a three-clause BSD-style license, which can be found at
* <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>. <br>
* @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
* @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
*/
public class FixedBinsAccumulator implements QuantileAccumulator
{
/** Center value of minimum bin. */
private final double minimumBinCenter;
/** Width of each bin. */
private final double binWidth;
/** Storage for the accumulated values. */
private long[] accumulator;
/** Cumulative counts. */
private long[] cumulatives = null;
/** Total number of registered values. */
private long totalCount = 0;
/** Count number of registered items that fall below the range. */
private long belowCount = 0;
/** Count number of registered items that fall above the range. */
private long aboveCount = 0;
/**
* Construct a new FullStorageAccumulator.
* @param minimumBinCenter double; center value of bin for minimum value of range (minimum value in range is
* <code>minimumBinCenter - binWidth / 2</code>, maximum value in range is
* <code>minimumBinCenter + binWidth * (binCount - 0.5)</code>)
* @param binWidth double; width of each bin
* @param binCount int; number of bins
*/
public FixedBinsAccumulator(final double minimumBinCenter, final double binWidth, final int binCount)
{
Throw.when(!Double.isFinite(minimumBinCenter), IllegalArgumentException.class, "minimumBinCenter must be finite");
Throw.when(!Double.isFinite(binWidth), IllegalArgumentException.class, "binWidth must be finite");
Throw.when(binWidth <= 0, IllegalArgumentException.class, "binWidth must be positive");
Throw.when(binCount < 1, IllegalArgumentException.class, "binCount must be > 0");
this.minimumBinCenter = minimumBinCenter;
this.binWidth = binWidth;
this.belowCount = 0;
this.aboveCount = 0;
this.accumulator = new long[binCount];
this.cumulatives = null;
this.totalCount = 0;
}
/**
* Retrieve the bin width.
* @return double; the bin width
*/
public double getBinWidth()
{
return this.binWidth;
}
/**
* Retrieve the bin count.
* @return int; the bin count
*/
public int getBinCount()
{
return this.accumulator.length;
}
/**
* Retrieve the total number of registered values.
* @return long; the total number of registered values
*/
public long getN()
{
return this.totalCount;
}
/**
* Retrieve the number of registered values that were below the range of this FixedBinsAccumulator.
* @return long; the number of registered values that were below the range of this FixedBinsAccumulator
*/
public long getBelowCount()
{
return this.belowCount;
}
/**
* Retrieve the number of registered values that were above the range of this FixedBinsAccumulator.
* @return long; the number of registered values that were above the range of this FixedBinsAccumulator
*/
public long getAboveCount()
{
return this.aboveCount;
}
/**
* Return the center of a particular bin.
* @param bin int the bin number
* @return double; the center of requested bin
*/
public double getBinCenter(final int bin)
{
Throw.when(bin < 0 || bin >= this.accumulator.length, IllegalArgumentException.class,
"bin must be in range 0..$1; got $2", this.accumulator.length - 1, bin);
return this.minimumBinCenter + bin * this.binWidth;
}
@Override
public double register(final double value)
{
Throw.when(Double.isNaN(value), IllegalArgumentException.class, "accumulator can not accumlate NaN value");
this.cumulatives = null;
double floatBin = (value - this.minimumBinCenter) / binWidth;
int bin = (int) Math.rint(floatBin);
if (bin < 0)
{
this.belowCount++;
}
else if (bin >= this.accumulator.length)
{
this.aboveCount++;
}
else
{
this.accumulator[bin]++;
}
this.totalCount++;
return value;
}
/**
* Compute the cumulative values if not already available.
*/
private void ensureCumulatives()
{
if (null == this.cumulatives)
{
long count = 0;
this.cumulatives = new long[this.accumulator.length];
for (int bin = 0; bin < this.accumulator.length; bin++)
{
count += this.accumulator[bin];
this.cumulatives[bin] = count;
}
}
}
@Override
public double getQuantile(final Tally tally, final double probability)
{
Throw.when(!Double.isFinite(probability) || probability < 0.0 || probability > 1.0, IllegalArgumentException.class,
"probability must be a value between 0 and 1");
ensureCumulatives();
// TODO do something clever with belowCount and aboveCount (could involve the tally as well)
long usableCount = this.totalCount - this.belowCount - this.aboveCount;
if (usableCount == 0)
{
return Double.NaN;
}
double value = (usableCount) * probability;
// TODO use bisection to home in
for (int bin = 0; bin < this.cumulatives.length; bin++)
{
if (this.cumulatives[bin] >= value)
{
return bin * this.binWidth + this.minimumBinCenter;
}
}
return 0; // cannot happen
}
@Override
public double getCumulativeProbability(final Tally tally, final double quantile) throws IllegalArgumentException
{
Throw.when(Double.isNaN(quantile), IllegalArgumentException.class, "quantile may not be NaN");
// TODO do something clever with belowCount and aboveCount (could involve the tally as well)
if (this.totalCount == 0)
{
return Double.NaN;
}
double floatBin = (quantile - this.minimumBinCenter) / binWidth;
int bin = (int) Math.rint(floatBin);
if (bin < 0)
{
return 0.0;
}
if (bin >= this.accumulator.length)
{
return 1.0;
}
ensureCumulatives();
return 1.0 * this.cumulatives[bin] / this.totalCount + (floatBin - bin - 0.5) * this.accumulator[bin] / this.totalCount;
}
@Override
public void initialize()
{
this.belowCount = 0;
this.aboveCount = 0;
this.accumulator = new long[this.accumulator.length];
this.cumulatives = null;
this.totalCount = 0;
}
@Override
public String toString()
{
return "FixedBinsAccumulator [minimumBinCenter=" + minimumBinCenter + ", binWidth=" + binWidth + ", totalCount="
+ totalCount + ", belowCount=" + belowCount + ", aboveCount=" + aboveCount + "]";
}
}