001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2007, 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-2007,  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    }