001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2008, by Object Refinery Limited 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     * [Java is a trademark or registered trademark of Sun Microsystems, Inc.
025     * in the United States and other countries.]
026     *
027     * ----------------------------
028     * BoxAndWhiskerCalculator.java
029     * ----------------------------
030     * (C) Copyright 2003-2008,  by Object Refinery Limited and Contributors.
031     *
032     * Original Author:  David Gilbert (for Object Refinery Limited);
033     * Contributor(s):   -;
034     *
035     * Changes
036     * -------
037     * 28-Aug-2003 : Version 1 (DG);
038     * 17-Nov-2003 : Fixed bug in calculations of outliers and median (DG);
039     * 10-Jan-2005 : Removed deprecated methods in preparation for 1.0.0
040     *               release (DG);
041     * ------------- JFREECHART 1.0.x ---------------------------------------------
042     * 15-Nov-2006 : Cleaned up handling of null arguments, and null or NaN items
043     *               in the list (DG);
044     *
045     */
046    
047    package org.jfree.data.statistics;
048    
049    import java.util.ArrayList;
050    import java.util.Collections;
051    import java.util.Iterator;
052    import java.util.List;
053    
054    /**
055     * A utility class that calculates the mean, median, quartiles Q1 and Q3, plus
056     * a list of outlier values...all from an arbitrary list of
057     * <code>Number</code> objects.
058     */
059    public abstract class BoxAndWhiskerCalculator {
060    
061        /**
062         * Calculates the statistics required for a {@link BoxAndWhiskerItem}
063         * from a list of <code>Number</code> objects.  Any items in the list
064         * that are <code>null</code>, not an instance of <code>Number</code>, or
065         * equivalent to <code>Double.NaN</code>, will be ignored.
066         *
067         * @param values  a list of numbers (a <code>null</code> list is not
068         *                permitted).
069         *
070         * @return A box-and-whisker item.
071         */
072        public static BoxAndWhiskerItem calculateBoxAndWhiskerStatistics(
073                                            List values) {
074            return calculateBoxAndWhiskerStatistics(values, true);
075        }
076    
077        /**
078         * Calculates the statistics required for a {@link BoxAndWhiskerItem}
079         * from a list of <code>Number</code> objects.  Any items in the list
080         * that are <code>null</code>, not an instance of <code>Number</code>, or
081         * equivalent to <code>Double.NaN</code>, will be ignored.
082         *
083         * @param values  a list of numbers (a <code>null</code> list is not
084         *                permitted).
085         * @param stripNullAndNaNItems  a flag that controls the handling of null
086         *     and NaN items.
087         *
088         * @return A box-and-whisker item.
089         *
090         * @since 1.0.3
091         */
092        public static BoxAndWhiskerItem calculateBoxAndWhiskerStatistics(
093                List values, boolean stripNullAndNaNItems) {
094    
095            if (values == null) {
096                throw new IllegalArgumentException("Null 'values' argument.");
097            }
098    
099            List vlist;
100            if (stripNullAndNaNItems) {
101                vlist = new ArrayList(values.size());
102                Iterator iterator = values.listIterator();
103                while (iterator.hasNext()) {
104                    Object obj = iterator.next();
105                    if (obj instanceof Number) {
106                        Number n = (Number) obj;
107                        double v = n.doubleValue();
108                        if (!Double.isNaN(v)) {
109                            vlist.add(n);
110                        }
111                    }
112                }
113            }
114            else {
115                vlist = values;
116            }
117            Collections.sort(vlist);
118    
119            double mean = Statistics.calculateMean(vlist, false);
120            double median = Statistics.calculateMedian(vlist, false);
121            double q1 = calculateQ1(vlist);
122            double q3 = calculateQ3(vlist);
123    
124            double interQuartileRange = q3 - q1;
125    
126            double upperOutlierThreshold = q3 + (interQuartileRange * 1.5);
127            double lowerOutlierThreshold = q1 - (interQuartileRange * 1.5);
128    
129            double upperFaroutThreshold = q3 + (interQuartileRange * 2.0);
130            double lowerFaroutThreshold = q1 - (interQuartileRange * 2.0);
131    
132            double minRegularValue = Double.POSITIVE_INFINITY;
133            double maxRegularValue = Double.NEGATIVE_INFINITY;
134            double minOutlier = Double.POSITIVE_INFINITY;
135            double maxOutlier = Double.NEGATIVE_INFINITY;
136            List outliers = new ArrayList();
137    
138            Iterator iterator = vlist.iterator();
139            while (iterator.hasNext()) {
140                Number number = (Number) iterator.next();
141                double value = number.doubleValue();
142                if (value > upperOutlierThreshold) {
143                    outliers.add(number);
144                    if (value > maxOutlier && value <= upperFaroutThreshold) {
145                        maxOutlier = value;
146                    }
147                }
148                else if (value < lowerOutlierThreshold) {
149                    outliers.add(number);
150                    if (value < minOutlier && value >= lowerFaroutThreshold) {
151                        minOutlier = value;
152                    }
153                }
154                else {
155                    minRegularValue = Math.min(minRegularValue, value);
156                    maxRegularValue = Math.max(maxRegularValue, value);
157                }
158                minOutlier = Math.min(minOutlier, minRegularValue);
159                maxOutlier = Math.max(maxOutlier, maxRegularValue);
160            }
161    
162            return new BoxAndWhiskerItem(new Double(mean), new Double(median),
163                    new Double(q1), new Double(q3), new Double(minRegularValue),
164                    new Double(maxRegularValue), new Double(minOutlier),
165                    new Double(maxOutlier), outliers);
166    
167        }
168    
169        /**
170         * Calculates the first quartile for a list of numbers in ascending order.
171         * If the items in the list are not in ascending order, the result is
172         * unspecified.  If the list contains items that are <code>null</code>, not
173         * an instance of <code>Number</code>, or equivalent to
174         * <code>Double.NaN</code>, the result is unspecified.
175         *
176         * @param values  the numbers in ascending order (<code>null</code> not
177         *     permitted).
178         *
179         * @return The first quartile.
180         */
181        public static double calculateQ1(List values) {
182            if (values == null) {
183                throw new IllegalArgumentException("Null 'values' argument.");
184            }
185    
186            double result = Double.NaN;
187            int count = values.size();
188            if (count > 0) {
189                if (count % 2 == 1) {
190                    if (count > 1) {
191                        result = Statistics.calculateMedian(values, 0, count / 2);
192                    }
193                    else {
194                        result = Statistics.calculateMedian(values, 0, 0);
195                    }
196                }
197                else {
198                    result = Statistics.calculateMedian(values, 0, count / 2 - 1);
199                }
200    
201            }
202            return result;
203        }
204    
205        /**
206         * Calculates the third quartile for a list of numbers in ascending order.
207         * If the items in the list are not in ascending order, the result is
208         * unspecified.  If the list contains items that are <code>null</code>, not
209         * an instance of <code>Number</code>, or equivalent to
210         * <code>Double.NaN</code>, the result is unspecified.
211         *
212         * @param values  the list of values (<code>null</code> not permitted).
213         *
214         * @return The third quartile.
215         */
216        public static double calculateQ3(List values) {
217            if (values == null) {
218                throw new IllegalArgumentException("Null 'values' argument.");
219            }
220            double result = Double.NaN;
221            int count = values.size();
222            if (count > 0) {
223                if (count % 2 == 1) {
224                    if (count > 1) {
225                        result = Statistics.calculateMedian(values, count / 2,
226                                count - 1);
227                    }
228                    else {
229                        result = Statistics.calculateMedian(values, 0, 0);
230                    }
231                }
232                else {
233                    result = Statistics.calculateMedian(values, count / 2,
234                            count - 1);
235                }
236            }
237            return result;
238        }
239    
240    }