View Javadoc
1   package org.djutils.stats.summarizers.quantileaccumulator;
2   
3   import java.util.ArrayList;
4   import java.util.Collections;
5   import java.util.List;
6   
7   import org.djutils.exceptions.Throw;
8   import org.djutils.stats.summarizers.Tally;
9   
10  /**
11   * Quantile accumulator that stores all values and computes exact (within a few ULP) results. <br>
12   * @author <a href="https://www.tudelft.nl/staff/p.knoppers/">Peter Knoppers</a>
13   */
14  public class FullStorageAccumulator implements QuantileAccumulator
15  {
16      /** Storage for the accumulated values. */
17      private List<Double> accumulator = new ArrayList<>();
18  
19      /** Is the accumulator currently sorted? */
20      private boolean isSorted = true;
21  
22      /**
23       * Construct a new FullStorageAccumulator.
24       */
25      public FullStorageAccumulator()
26      {
27          // Nothing to do here
28      }
29  
30      /** {@inheritDoc} */
31      @Override
32      public double register(final double value)
33      {
34          Throw.when(Double.isNaN(value), IllegalArgumentException.class, "accumulator can not accumlate NaN value");
35          this.accumulator.add(value);
36          this.isSorted = false;
37          return value;
38      }
39  
40      /**
41       * Sort the values in the accumulator (if needed).
42       */
43      private void ensureSorted()
44      {
45          if (!this.isSorted)
46          {
47              Collections.sort(this.accumulator);
48              this.isSorted = true;
49          }
50      }
51  
52      /** {@inheritDoc} */
53      @Override
54      public double getQuantile(final Tally tally, final double probability)
55      {
56          Throw.whenNull(tally, "tally cannot be null");
57          Throw.when(probability < 0 || probability > 1, IllegalArgumentException.class,
58                  "Probability should be between 0.0 and 1.0 (inclusive); got {}", probability);
59          ensureSorted();
60          double doubleIndex = (this.accumulator.size() - 1) * probability;
61          int index = Math.min((int) Math.floor(doubleIndex), this.accumulator.size() - 1);
62          double v0 = this.accumulator.get(index);
63          if (index >= this.accumulator.size() - 1)
64          {
65              return v0;
66          }
67          double v1 = this.accumulator.get(index + 1);
68          return v1 * (doubleIndex - index) + v0 * (1.0 - (doubleIndex - index));
69      }
70  
71      /** {@inheritDoc} */
72      @Override
73      public double getCumulativeProbability(final Tally tally, final double quantile)
74      {
75          Throw.when(Double.isNaN(quantile), IllegalArgumentException.class, "quantile may not be NaN");
76          ensureSorted();
77          // @formatter:off
78          /*
79           * Make sure to handle all these cases correctly:
80           * 1: the accumulator list is empty (return NaN)
81           * 2: quantile is less than the first value in the list (return 0.0)
82           * 3: quantile is more than the last value in the list (return 1.0)
83           * 4: quantile equals exactly one element in the list (return (1 + 2 * rank) / size / 2)
84           * 5: quantile equals several (successive) elements in the list (return (1 + rankL + rankH) / size / 2)
85           * 6: quantile lies between two (successive) elements in the list (return (rankL + rankH) / size)
86           * 
87           * Although the basic idea of binary search is comparatively straightforward, the details can be surprisingly tricky.
88           * — Donald Knuth; Knuth 1998, §6.2.1 ("Searching an ordered table"), subsection "Binary search".
89           * Knuth was right (PK).
90           */
91          // @formatter:on
92          if (this.accumulator.size() == 0)
93          {
94              return Double.NaN; // case 1
95          }
96          int lowerBound = 0;
97          int upperBound = this.accumulator.size();
98  
99          while (lowerBound < upperBound)
100         {
101             int guess = (lowerBound + upperBound) / 2;
102             if (this.accumulator.get(guess) < quantile)
103             {
104                 lowerBound = guess + 1;
105             }
106             else 
107             {
108                 upperBound = guess;
109             }
110         }
111         int b = lowerBound;
112         upperBound = this.accumulator.size();
113         while (b < upperBound)
114         {
115             int guess = (b + upperBound) / 2;
116             double value = this.accumulator.get(guess);
117             if (value > quantile)
118             {
119                 upperBound = guess;
120             }
121             else 
122             {
123                 b = guess + 1;
124             }
125         }
126         upperBound--;
127         if (upperBound < 0)
128         {
129             return 0.0; // case 2
130         }
131         if (lowerBound >= this.accumulator.size())
132         {
133             return 1.0; // case 3
134         }
135         double adjust = 0;
136         if (upperBound >= lowerBound)
137         {
138             adjust = 1; // cases 4 and 5
139         }
140         if (upperBound < lowerBound)
141         {
142             adjust = 1; // case 6
143         }
144         return (adjust + upperBound + lowerBound) / this.accumulator.size() / 2; // cases 4, 5 and 6
145     }
146     
147     /** {@inheritDoc} */
148     @Override
149     public void initialize()
150     {
151         this.accumulator.clear();
152     }
153 
154     /** {@inheritDoc} */
155     @Override
156     public String toString()
157     {
158         return "FullStorageAccumulator [accumulator size=" + this.accumulator.size() + ", isSorted=" + this.isSorted + "]";
159     }
160 
161 }