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