001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-2022, by David Gilbert and Contributors.
006 *
007 * Project Info:  http://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-2022, by David Gilbert and Contributors.
031 *
032 * Original Author:  David Gilbert;
033 * Contributor(s):   -;
034 *
035 */
036
037package org.jfree.data.statistics;
038
039import org.jfree.chart.internal.Args;
040
041import java.util.ArrayList;
042import java.util.Collections;
043import java.util.List;
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    /**
053     * Calculates the statistics required for a {@link BoxAndWhiskerItem}
054     * from a list of {@code Number} objects.  Any items in the list
055     * that are {@code null}, not an instance of {@code Number}, or
056     * equivalent to {@code Double.NaN}, will be ignored.
057     *
058     * @param values  a list of numbers (a {@code null} list is not
059     *                permitted).
060     *
061     * @return A box-and-whisker item.
062     */
063    public static BoxAndWhiskerItem calculateBoxAndWhiskerStatistics(
064            List<? extends Number> values) {
065        return calculateBoxAndWhiskerStatistics(values, true);
066    }
067
068    /**
069     * Calculates the statistics required for a {@link BoxAndWhiskerItem}
070     * from a list of {@code Number} objects.  Any items in the list
071     * that are {@code null}, not an instance of {@code Number}, or
072     * equivalent to {@code Double.NaN}, will be ignored.
073     *
074     * @param values  a list of numbers (a {@code null} list is not
075     *                permitted).
076     * @param stripNullAndNaNItems  a flag that controls the handling of null
077     *     and NaN items.
078     *
079     * @return A box-and-whisker item.
080     *
081     * @since 1.0.3
082     */
083    public static BoxAndWhiskerItem calculateBoxAndWhiskerStatistics(
084            List<? extends Number> values, boolean stripNullAndNaNItems) {
085
086        Args.nullNotPermitted(values, "values");
087
088        List vlist;
089        if (stripNullAndNaNItems) {
090            vlist = new ArrayList(values.size());
091            for (Object obj : values) {
092                if (obj instanceof Number) {
093                    Number n = (Number) obj;
094                    double v = n.doubleValue();
095                    if (!Double.isNaN(v)) {
096                        vlist.add(n);
097                    }
098                }
099            }
100        }
101        else {
102            vlist = values;
103        }
104        Collections.sort(vlist);
105
106        double mean = Statistics.calculateMean(vlist, false);
107        double median = Statistics.calculateMedian(vlist, false);
108        double q1 = calculateQ1(vlist);
109        double q3 = calculateQ3(vlist);
110
111        double interQuartileRange = q3 - q1;
112
113        double upperOutlierThreshold = q3 + (interQuartileRange * 1.5);
114        double lowerOutlierThreshold = q1 - (interQuartileRange * 1.5);
115
116        double upperFaroutThreshold = q3 + (interQuartileRange * 2.0);
117        double lowerFaroutThreshold = q1 - (interQuartileRange * 2.0);
118
119        double minRegularValue = Double.POSITIVE_INFINITY;
120        double maxRegularValue = Double.NEGATIVE_INFINITY;
121        double minOutlier = Double.POSITIVE_INFINITY;
122        double maxOutlier = Double.NEGATIVE_INFINITY;
123        List<Number> outliers = new ArrayList<>();
124
125        for (Object o : vlist) {
126            Number number = (Number) o;
127            double value = number.doubleValue();
128            if (value > upperOutlierThreshold) {
129                outliers.add(number);
130                if (value > maxOutlier && value <= upperFaroutThreshold) {
131                    maxOutlier = value;
132                }
133            }
134            else if (value < lowerOutlierThreshold) {
135                outliers.add(number);
136                if (value < minOutlier && value >= lowerFaroutThreshold) {
137                    minOutlier = value;
138                }
139            }
140            else {
141                minRegularValue = Math.min(minRegularValue, value);
142                maxRegularValue = Math.max(maxRegularValue, value);
143            }
144            minOutlier = Math.min(minOutlier, minRegularValue);
145            maxOutlier = Math.max(maxOutlier, maxRegularValue);
146        }
147
148        return new BoxAndWhiskerItem(mean, median, q1, q3, minRegularValue,
149                maxRegularValue, minOutlier, maxOutlier, outliers);
150
151    }
152
153    /**
154     * Calculates the first quartile for a list of numbers in ascending order.
155     * If the items in the list are not in ascending order, the result is
156     * unspecified.  If the list contains items that are {@code null}, not
157     * an instance of {@code Number}, or equivalent to
158     * {@code Double.NaN}, the result is unspecified.
159     *
160     * @param values  the numbers in ascending order ({@code null} not
161     *     permitted).
162     *
163     * @return The first quartile.
164     */
165    public static double calculateQ1(List values) {
166        Args.nullNotPermitted(values, "values");
167
168        double result = Double.NaN;
169        int count = values.size();
170        if (count > 0) {
171            if (count % 2 == 1) {
172                if (count > 1) {
173                    result = Statistics.calculateMedian(values, 0, count / 2);
174                }
175                else {
176                    result = Statistics.calculateMedian(values, 0, 0);
177                }
178            }
179            else {
180                result = Statistics.calculateMedian(values, 0, count / 2 - 1);
181            }
182
183        }
184        return result;
185    }
186
187    /**
188     * Calculates the third quartile for a list of numbers in ascending order.
189     * If the items in the list are not in ascending order, the result is
190     * unspecified.  If the list contains items that are {@code null}, not
191     * an instance of {@code Number}, or equivalent to
192     * {@code Double.NaN}, the result is unspecified.
193     *
194     * @param values  the list of values ({@code null} not permitted).
195     *
196     * @return The third quartile.
197     */
198    public static double calculateQ3(List values) {
199        Args.nullNotPermitted(values, "values");
200        double result = Double.NaN;
201        int count = values.size();
202        if (count > 0) {
203            if (count % 2 == 1) {
204                if (count > 1) {
205                    result = Statistics.calculateMedian(values, count / 2,
206                            count - 1);
207                }
208                else {
209                    result = Statistics.calculateMedian(values, 0, 0);
210                }
211            }
212            else {
213                result = Statistics.calculateMedian(values, count / 2,
214                        count - 1);
215            }
216        }
217        return result;
218    }
219
220}