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-2024 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     @Override
121     public double register(final double value)
122     {
123         Throw.when(Double.isNaN(value), IllegalArgumentException.class, "accumulator can not accumlate NaN value");
124         this.cumulatives = null;
125         double floatBin = (value - this.minimumBinCenter) / binWidth;
126         int bin = (int) Math.rint(floatBin);
127         if (bin < 0)
128         {
129             this.belowCount++;
130         }
131         else if (bin >= this.accumulator.length)
132         {
133             this.aboveCount++;
134         }
135         else
136         {
137             this.accumulator[bin]++;
138         }
139         this.totalCount++;
140         return value;
141     }
142     
143     /**
144      * Compute the cumulative values if not already available.
145      */
146     private void ensureCumulatives()
147     {
148         if (null == this.cumulatives)
149         {
150             long count = 0;
151             this.cumulatives = new long[this.accumulator.length];
152             for (int bin = 0; bin < this.accumulator.length; bin++)
153             {
154                 count += this.accumulator[bin];
155                 this.cumulatives[bin] = count;
156             }
157         }
158     }
159 
160     @Override
161     public double getQuantile(final Tally tally, final double probability)
162     {
163         Throw.when(!Double.isFinite(probability) || probability < 0.0 || probability > 1.0, IllegalArgumentException.class,
164                 "probability must be a value between 0 and 1");
165         ensureCumulatives();
166         // TODO do something clever with belowCount and aboveCount (could involve the tally as well)
167         long usableCount = this.totalCount - this.belowCount - this.aboveCount;
168         if (usableCount == 0)
169         {
170             return Double.NaN;
171         }
172         double value = (usableCount) * probability;
173         // TODO use bisection to home in
174         for (int bin = 0; bin < this.cumulatives.length; bin++)
175         {
176             if (this.cumulatives[bin] >= value)
177             {
178                 return bin * this.binWidth + this.minimumBinCenter;
179             }
180         }
181         return 0; // cannot happen
182     }
183 
184     @Override
185     public double getCumulativeProbability(final Tally tally, final double quantile) throws IllegalArgumentException
186     {
187         Throw.when(Double.isNaN(quantile), IllegalArgumentException.class, "quantile may not be NaN");
188         // TODO do something clever with belowCount and aboveCount (could involve the tally as well)
189         if (this.totalCount == 0)
190         {
191             return Double.NaN;
192         }
193         double floatBin = (quantile - this.minimumBinCenter) / binWidth;
194         int bin = (int) Math.rint(floatBin);
195         if (bin < 0)
196         {
197             return 0.0;
198         }
199         if (bin >= this.accumulator.length)
200         {
201             return 1.0;
202         }
203         ensureCumulatives();
204         return 1.0 * this.cumulatives[bin] / this.totalCount + (floatBin - bin - 0.5) * this.accumulator[bin] / this.totalCount;
205     }
206 
207     @Override
208     public void initialize()
209     {
210         this.belowCount = 0;
211         this.aboveCount = 0;
212         this.accumulator = new long[this.accumulator.length];
213         this.cumulatives = null;
214         this.totalCount = 0;
215     }
216 
217     @Override
218     public String toString()
219     {
220         return "FixedBinsAccumulator [minimumBinCenter=" + minimumBinCenter + ", binWidth=" + binWidth + ", totalCount="
221                 + totalCount + ", belowCount=" + belowCount + ", aboveCount=" + aboveCount + "]";
222     }
223 
224 }