View Javadoc
1   package org.djutils.stats.summarizers.quantileaccumulator;
2   
3   import org.djutils.exceptions.Throw;
4   import org.djutils.stats.summarizers.Tally;
5   
6   /**
7    * FixedBinsAccumulator.java. <br>
8    * This accumulator is created with a caller prescribes set of bins. All bins have the same width.
9    * <br>
10   * Copyright (c) 2021-2023 Delft University of Technology, Jaffalaan 5, 2628 BX Delft, the Netherlands. All rights reserved. See
11   * for project information <a href="https://djutils.org" target="_blank"> https://djutils.org</a>. The DJUTILS project is
12   * distributed under a three-clause BSD-style license, which can be found at
13   * <a href="https://djutils.org/docs/license.html" target="_blank"> https://djutils.org/docs/license.html</a>. <br>
14   * @author <a href="https://www.tudelft.nl/averbraeck">Alexander Verbraeck</a>
15   * @author <a href="https://www.tudelft.nl/pknoppers">Peter Knoppers</a>
16   */
17  public class FixedBinsAccumulator implements QuantileAccumulator
18  {
19      /** Center value of minimum bin. */
20      private final double minimumBinCenter;
21  
22      /** Width of each bin. */
23      private final double binWidth;
24  
25      /** Storage for the accumulated values. */
26      private long[] accumulator;
27  
28      /** Cumulative counts. */
29      private long[] cumulatives = null;
30  
31      /** Total number of registered values. */
32      private long totalCount = 0;
33  
34      /** Count number of registered items that fall below the range. */
35      private long belowCount = 0;
36  
37      /** Count number of registered items that fall above the range. */
38      private long aboveCount = 0;
39  
40      /**
41       * Construct a new FullStorageAccumulator.
42       * @param minimumBinCenter double; center value of bin for minimum value of range (minimum value in range is
43       *            <code>minimumBinCenter - binWidth / 2</code>, maximum value in range is
44       *            <code>minimumBinCenter + binWidth * (binCount - 0.5)</code>)
45       * @param binWidth double; width of each bin
46       * @param binCount int; number of bins
47       */
48      public FixedBinsAccumulator(final double minimumBinCenter, final double binWidth, final int binCount)
49      {
50          Throw.when(!Double.isFinite(minimumBinCenter), IllegalArgumentException.class, "minimumBinCenter must be finite");
51          Throw.when(!Double.isFinite(binWidth), IllegalArgumentException.class, "binWidth must be finite");
52          Throw.when(binWidth <= 0, IllegalArgumentException.class, "binWidth must be positive");
53          Throw.when(binCount < 1, IllegalArgumentException.class, "binCount must be > 0");
54          this.minimumBinCenter = minimumBinCenter;
55          this.binWidth = binWidth;
56          this.belowCount = 0;
57          this.aboveCount = 0;
58          this.accumulator = new long[binCount];
59          this.cumulatives = null;
60          this.totalCount = 0;
61      }
62  
63      /**
64       * Retrieve the bin width.
65       * @return double; the bin width
66       */
67      public double getBinWidth()
68      {
69          return this.binWidth;
70      }
71  
72      /**
73       * Retrieve the bin count.
74       * @return int; the bin count
75       */
76      public int getBinCount()
77      {
78          return this.accumulator.length;
79      }
80  
81      /**
82       * Retrieve the total number of registered values.
83       * @return long; the total number of registered values
84       */
85      public long getN()
86      {
87          return this.totalCount;
88      }
89  
90      /**
91       * Retrieve the number of registered values that were below the range of this FixedBinsAccumulator.
92       * @return long; the number of registered values that were below the range of this FixedBinsAccumulator
93       */
94      public long getBelowCount()
95      {
96          return this.belowCount;
97      }
98  
99      /**
100      * Retrieve the number of registered values that were above the range of this FixedBinsAccumulator.
101      * @return long; the number of registered values that were above the range of this FixedBinsAccumulator
102      */
103     public long getAboveCount()
104     {
105         return this.aboveCount;
106     }
107 
108     /**
109      * Return the center of a particular bin.
110      * @param bin int the bin number
111      * @return double; the center of requested bin
112      */
113     public double getBinCenter(final int bin)
114     {
115         Throw.when(bin < 0 || bin >= this.accumulator.length, IllegalArgumentException.class,
116                 "bin must be in range 0..$1; got $2", this.accumulator.length - 1, bin);
117         return this.minimumBinCenter + bin * this.binWidth;
118     }
119 
120     /** {@inheritDoc} */
121     @Override
122     public double register(final double value)
123     {
124         Throw.when(Double.isNaN(value), IllegalArgumentException.class, "accumulator can not accumlate NaN value");
125         this.cumulatives = null;
126         double floatBin = (value - this.minimumBinCenter) / binWidth;
127         int bin = (int) Math.rint(floatBin);
128         if (bin < 0)
129         {
130             this.belowCount++;
131         }
132         else if (bin >= this.accumulator.length)
133         {
134             this.aboveCount++;
135         }
136         else
137         {
138             this.accumulator[bin]++;
139         }
140         this.totalCount++;
141         return value;
142     }
143     
144     /**
145      * Compute the cumulative values if not already available.
146      */
147     private void ensureCumulatives()
148     {
149         if (null == this.cumulatives)
150         {
151             long count = 0;
152             this.cumulatives = new long[this.accumulator.length];
153             for (int bin = 0; bin < this.accumulator.length; bin++)
154             {
155                 count += this.accumulator[bin];
156                 this.cumulatives[bin] = count;
157             }
158         }
159     }
160 
161     /** {@inheritDoc} */
162     @Override
163     public double getQuantile(final Tally tally, final double probability)
164     {
165         Throw.when(!Double.isFinite(probability) || probability < 0.0 || probability > 1.0, IllegalArgumentException.class,
166                 "probability must be a value between 0 and 1");
167         ensureCumulatives();
168         // TODO do something clever with belowCount and aboveCount (could involve the tally as well)
169         long usableCount = this.totalCount - this.belowCount - this.aboveCount;
170         if (usableCount == 0)
171         {
172             return Double.NaN;
173         }
174         double value = (usableCount) * probability;
175         // TODO use bisection to home in
176         for (int bin = 0; bin < this.cumulatives.length; bin++)
177         {
178             if (this.cumulatives[bin] >= value)
179             {
180                 return bin * this.binWidth + this.minimumBinCenter;
181             }
182         }
183         return 0; // cannot happen
184     }
185 
186     /** {@inheritDoc} */
187     @Override
188     public double getCumulativeProbability(final Tally tally, final double quantile) throws IllegalArgumentException
189     {
190         Throw.when(Double.isNaN(quantile), IllegalArgumentException.class, "quantile may not be NaN");
191         // TODO do something clever with belowCount and aboveCount (could involve the tally as well)
192         if (this.totalCount == 0)
193         {
194             return Double.NaN;
195         }
196         double floatBin = (quantile - this.minimumBinCenter) / binWidth;
197         int bin = (int) Math.rint(floatBin);
198         if (bin < 0)
199         {
200             return 0.0;
201         }
202         if (bin >= this.accumulator.length)
203         {
204             return 1.0;
205         }
206         ensureCumulatives();
207         return 1.0 * this.cumulatives[bin] / this.totalCount + (floatBin - bin - 0.5) * this.accumulator[bin] / this.totalCount;
208     }
209 
210     /** {@inheritDoc} */
211     @Override
212     public void initialize()
213     {
214         this.belowCount = 0;
215         this.aboveCount = 0;
216         this.accumulator = new long[this.accumulator.length];
217         this.cumulatives = null;
218         this.totalCount = 0;
219     }
220 
221     /** {@inheritDoc} */
222     @Override
223     public String toString()
224     {
225         return "FixedBinsAccumulator [minimumBinCenter=" + minimumBinCenter + ", binWidth=" + binWidth + ", totalCount="
226                 + totalCount + ", belowCount=" + belowCount + ", aboveCount=" + aboveCount + "]";
227     }
228 
229 }