001/* ===========================================================
002 * JFreeChart : a free chart library for the Java(tm) platform
003 * ===========================================================
004 *
005 * (C) Copyright 2000-present, 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-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    /**
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 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    public static BoxAndWhiskerItem calculateBoxAndWhiskerStatistics(
082            List values, boolean stripNullAndNaNItems) {
083
084        Args.nullNotPermitted(values, "values");
085
086        List vlist;
087        if (stripNullAndNaNItems) {
088            vlist = new ArrayList(values.size());
089            Iterator iterator = values.listIterator();
090            while (iterator.hasNext()) {
091                Object obj = iterator.next();
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 outliers = new ArrayList();
124
125        Iterator iterator = vlist.iterator();
126        while (iterator.hasNext()) {
127            Number number = (Number) iterator.next();
128            double value = number.doubleValue();
129            if (value > upperOutlierThreshold) {
130                outliers.add(number);
131                if (value > maxOutlier && value <= upperFaroutThreshold) {
132                    maxOutlier = value;
133                }
134            }
135            else if (value < lowerOutlierThreshold) {
136                outliers.add(number);
137                if (value < minOutlier && value >= lowerFaroutThreshold) {
138                    minOutlier = value;
139                }
140            }
141            else {
142                minRegularValue = Math.min(minRegularValue, value);
143                maxRegularValue = Math.max(maxRegularValue, value);
144            }
145            minOutlier = Math.min(minOutlier, minRegularValue);
146            maxOutlier = Math.max(maxOutlier, maxRegularValue);
147        }
148
149        return new BoxAndWhiskerItem(mean, median, q1, q3, minRegularValue,
150                maxRegularValue, minOutlier, maxOutlier, outliers);
151
152    }
153
154    /**
155     * Calculates the first quartile for a list of numbers in ascending order.
156     * If the items in the list are not in ascending order, the result is
157     * unspecified.  If the list contains items that are {@code null}, not
158     * an instance of {@code Number}, or equivalent to
159     * {@code Double.NaN}, the result is unspecified.
160     *
161     * @param values  the numbers in ascending order ({@code null} not
162     *     permitted).
163     *
164     * @return The first quartile.
165     */
166    public static double calculateQ1(List values) {
167        Args.nullNotPermitted(values, "values");
168
169        double result = Double.NaN;
170        int count = values.size();
171        if (count > 0) {
172            if (count % 2 == 1) {
173                if (count > 1) {
174                    result = Statistics.calculateMedian(values, 0, count / 2);
175                }
176                else {
177                    result = Statistics.calculateMedian(values, 0, 0);
178                }
179            }
180            else {
181                result = Statistics.calculateMedian(values, 0, count / 2 - 1);
182            }
183
184        }
185        return result;
186    }
187
188    /**
189     * Calculates the third quartile for a list of numbers in ascending order.
190     * If the items in the list are not in ascending order, the result is
191     * unspecified.  If the list contains items that are {@code null}, not
192     * an instance of {@code Number}, or equivalent to
193     * {@code Double.NaN}, the result is unspecified.
194     *
195     * @param values  the list of values ({@code null} not permitted).
196     *
197     * @return The third quartile.
198     */
199    public static double calculateQ3(List values) {
200        Args.nullNotPermitted(values, "values");
201        double result = Double.NaN;
202        int count = values.size();
203        if (count > 0) {
204            if (count % 2 == 1) {
205                if (count > 1) {
206                    result = Statistics.calculateMedian(values, count / 2,
207                            count - 1);
208                }
209                else {
210                    result = Statistics.calculateMedian(values, 0, 0);
211                }
212            }
213            else {
214                result = Statistics.calculateMedian(values, count / 2,
215                        count - 1);
216            }
217        }
218        return result;
219    }
220
221}