001/* ======================================================
002 * JFreeChart : a chart library for the Java(tm) platform
003 * ======================================================
004 *
005 * (C) Copyright 2000-present, by David Gilbert and Contributors.
006 *
007 * Project Info:  https://www.jfree.org/jfreechart/index.html
008 *
009 * This library is free software; you can redistribute it and/or modify it
010 * under the terms of the GNU Lesser General Public License as published by
011 * the Free Software Foundation; either version 2.1 of the License, or
012 * (at your option) any later version.
013 *
014 * This library is distributed in the hope that it will be useful, but
015 * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY
016 * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public
017 * License for more details.
018 *
019 * You should have received a copy of the GNU Lesser General Public
020 * License along with this library; if not, write to the Free Software
021 * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301,
022 * USA.
023 *
024 * [Oracle and Java are registered trademarks of Oracle and/or its affiliates. 
025 * Other names may be trademarks of their respective owners.]
026 *
027 * ----------------------------
028 * BoxAndWhiskerCalculator.java
029 * ----------------------------
030 * (C) Copyright 2003-present, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 *
035 */
036
037package org.jfree.data.statistics;
038
039import java.util.ArrayList;
040import java.util.Collections;
041import java.util.Iterator;
042import java.util.List;
043import org.jfree.chart.util.Args;
044
045/**
046 * A utility class that calculates the mean, median, quartiles Q1 and Q3, plus
047 * a list of outlier values...all from an arbitrary list of
048 * {@code Number} objects.
049 */
050public abstract class BoxAndWhiskerCalculator {
051
052    private BoxAndWhiskerCalculator() {
053        // no requirement to instantiate
054    }
055
056    /**
057     * Calculates the statistics required for a {@link BoxAndWhiskerItem}
058     * from a list of {@code Number} objects.  Any items in the list
059     * that are {@code null}, not an instance of {@code Number}, or
060     * equivalent to {@code Double.NaN}, will be ignored.
061     *
062     * @param values  a list of numbers (a {@code null} list is not
063     *                permitted).
064     *
065     * @return A box-and-whisker item.
066     */
067    public static BoxAndWhiskerItem calculateBoxAndWhiskerStatistics(
068                                        List values) {
069        return calculateBoxAndWhiskerStatistics(values, true);
070    }
071
072    /**
073     * Calculates the statistics required for a {@link BoxAndWhiskerItem}
074     * from a list of {@code Number} objects.  Any items in the list
075     * that are {@code null}, not an instance of {@code Number}, or
076     * equivalent to {@code Double.NaN}, will be ignored.
077     *
078     * @param values  a list of numbers (a {@code null} list is not
079     *                permitted).
080     * @param stripNullAndNaNItems  a flag that controls the handling of null
081     *     and NaN items.
082     *
083     * @return A box-and-whisker item.
084     */
085    public static BoxAndWhiskerItem calculateBoxAndWhiskerStatistics(
086            List values, boolean stripNullAndNaNItems) {
087
088        Args.nullNotPermitted(values, "values");
089
090        List vlist;
091        if (stripNullAndNaNItems) {
092            vlist = new ArrayList(values.size());
093            Iterator iterator = values.listIterator();
094            while (iterator.hasNext()) {
095                Object obj = iterator.next();
096                if (obj instanceof Number) {
097                    Number n = (Number) obj;
098                    double v = n.doubleValue();
099                    if (!Double.isNaN(v)) {
100                        vlist.add(n);
101                    }
102                }
103            }
104        }
105        else {
106            vlist = values;
107        }
108        Collections.sort(vlist);
109
110        double mean = Statistics.calculateMean(vlist, false);
111        double median = Statistics.calculateMedian(vlist, false);
112        double q1 = calculateQ1(vlist);
113        double q3 = calculateQ3(vlist);
114
115        double interQuartileRange = q3 - q1;
116
117        double upperOutlierThreshold = q3 + (interQuartileRange * 1.5);
118        double lowerOutlierThreshold = q1 - (interQuartileRange * 1.5);
119
120        double upperFaroutThreshold = q3 + (interQuartileRange * 2.0);
121        double lowerFaroutThreshold = q1 - (interQuartileRange * 2.0);
122
123        double minRegularValue = Double.POSITIVE_INFINITY;
124        double maxRegularValue = Double.NEGATIVE_INFINITY;
125        double minOutlier = Double.POSITIVE_INFINITY;
126        double maxOutlier = Double.NEGATIVE_INFINITY;
127        List outliers = new ArrayList();
128
129        Iterator iterator = vlist.iterator();
130        while (iterator.hasNext()) {
131            Number number = (Number) iterator.next();
132            double value = number.doubleValue();
133            if (value > upperOutlierThreshold) {
134                outliers.add(number);
135                if (value > maxOutlier && value <= upperFaroutThreshold) {
136                    maxOutlier = value;
137                }
138            }
139            else if (value < lowerOutlierThreshold) {
140                outliers.add(number);
141                if (value < minOutlier && value >= lowerFaroutThreshold) {
142                    minOutlier = value;
143                }
144            }
145            else {
146                minRegularValue = Math.min(minRegularValue, value);
147                maxRegularValue = Math.max(maxRegularValue, value);
148            }
149            minOutlier = Math.min(minOutlier, minRegularValue);
150            maxOutlier = Math.max(maxOutlier, maxRegularValue);
151        }
152
153        return new BoxAndWhiskerItem(mean, median, q1, q3, minRegularValue,
154                maxRegularValue, minOutlier, maxOutlier, outliers);
155
156    }
157
158    /**
159     * Calculates the first quartile for a list of numbers in ascending order.
160     * If the items in the list are not in ascending order, the result is
161     * unspecified.  If the list contains items that are {@code null}, not
162     * an instance of {@code Number}, or equivalent to
163     * {@code Double.NaN}, the result is unspecified.
164     *
165     * @param values  the numbers in ascending order ({@code null} not
166     *     permitted).
167     *
168     * @return The first quartile.
169     */
170    public static double calculateQ1(List values) {
171        Args.nullNotPermitted(values, "values");
172
173        double result = Double.NaN;
174        int count = values.size();
175        if (count > 0) {
176            if (count % 2 == 1) {
177                if (count > 1) {
178                    result = Statistics.calculateMedian(values, 0, count / 2);
179                }
180                else {
181                    result = Statistics.calculateMedian(values, 0, 0);
182                }
183            }
184            else {
185                result = Statistics.calculateMedian(values, 0, count / 2 - 1);
186            }
187
188        }
189        return result;
190    }
191
192    /**
193     * Calculates the third quartile for a list of numbers in ascending order.
194     * If the items in the list are not in ascending order, the result is
195     * unspecified.  If the list contains items that are {@code null}, not
196     * an instance of {@code Number}, or equivalent to
197     * {@code Double.NaN}, the result is unspecified.
198     *
199     * @param values  the list of values ({@code null} not permitted).
200     *
201     * @return The third quartile.
202     */
203    public static double calculateQ3(List values) {
204        Args.nullNotPermitted(values, "values");
205        double result = Double.NaN;
206        int count = values.size();
207        if (count > 0) {
208            if (count % 2 == 1) {
209                if (count > 1) {
210                    result = Statistics.calculateMedian(values, count / 2,
211                            count - 1);
212                }
213                else {
214                    result = Statistics.calculateMedian(values, 0, 0);
215                }
216            }
217            else {
218                result = Statistics.calculateMedian(values, count / 2,
219                        count - 1);
220            }
221        }
222        return result;
223    }
224
225}