001    /* ===========================================================
002     * JFreeChart : a free chart library for the Java(tm) platform
003     * ===========================================================
004     *
005     * (C) Copyright 2000-2005, 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-2005,  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.1 2005/10/25 21:34:46 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     *
044     */
045    
046    package org.jfree.data.statistics;
047    
048    import java.util.ArrayList;
049    import java.util.Collections;
050    import java.util.Iterator;
051    import java.util.List;
052    
053    /**
054     * A utility class that calculates the mean, median, quartiles Q1 and Q3, plus
055     * a list of outlier values...all from an arbitrary list of 
056     * <code>Number</code> objects.
057     */
058    public abstract class BoxAndWhiskerCalculator {
059        
060        /**
061         * Calculates the statistics required for a {@link BoxAndWhiskerItem}.
062         * <P>
063         * Any items in the list that are not instances of the <code>Number</code> 
064         * class are ignored. Likewise, <code>null</code> values are ignored.
065         * 
066         * @param values  a list of numbers (a <code>null</code> list is not 
067         *                permitted).
068         * 
069         * @return Box-and-whisker statistics.
070         */
071        public static BoxAndWhiskerItem calculateBoxAndWhiskerStatistics(
072                                            List values) {
073            
074            Collections.sort(values);
075            
076            double mean = Statistics.calculateMean(values);
077            double median = Statistics.calculateMedian(values, false);
078            double q1 = calculateQ1(values);
079            double q3 = calculateQ3(values);
080            
081            double interQuartileRange = q3 - q1;
082            
083            double upperOutlierThreshold = q3 + (interQuartileRange * 1.5);
084            double lowerOutlierThreshold = q1 - (interQuartileRange * 1.5);
085            
086            double upperFaroutThreshold = q3 + (interQuartileRange * 2.0);
087            double lowerFaroutThreshold = q1 - (interQuartileRange * 2.0);
088    
089            double minRegularValue = Double.POSITIVE_INFINITY;
090            double maxRegularValue = Double.NEGATIVE_INFINITY;
091            double minOutlier = Double.POSITIVE_INFINITY;
092            double maxOutlier = Double.NEGATIVE_INFINITY;
093            List outliers = new ArrayList();
094            
095            Iterator iterator = values.iterator();
096            while (iterator.hasNext()) {
097                Object object = iterator.next();
098                if (object != null && object instanceof Number) {
099                    Number number = (Number) object;
100                    double value = number.doubleValue();
101                    if (value > upperOutlierThreshold) {
102                        outliers.add(number);
103                        if (value > maxOutlier && value <= upperFaroutThreshold) {
104                            maxOutlier = value;
105                        }
106                    }
107                    else if (value < lowerOutlierThreshold) {
108                        outliers.add(number);                    
109                        if (value < minOutlier && value >= lowerFaroutThreshold) {
110                            minOutlier = value;
111                        }
112                    }
113                    else {
114                        if (minRegularValue == Double.NaN) {
115                            minRegularValue = value;
116                        }
117                        else {
118                            minRegularValue = Math.min(minRegularValue, value);
119                        }
120                        if (maxRegularValue == Double.NaN) {
121                            maxRegularValue = value;
122                        }
123                        else {
124                            maxRegularValue = Math.max(maxRegularValue, value);
125                        }
126                    }
127                    
128                }
129            }
130            minOutlier = Math.min(minOutlier, minRegularValue);
131            maxOutlier = Math.max(maxOutlier, maxRegularValue);
132            
133            return new BoxAndWhiskerItem(
134                new Double(mean),
135                new Double(median),
136                new Double(q1),
137                new Double(q3),
138                new Double(minRegularValue),
139                new Double(maxRegularValue),
140                new Double(minOutlier),
141                new Double(maxOutlier),
142                outliers
143            );
144            
145        }
146    
147        /**
148         * Calculates the first quartile for a list of numbers in ascending order.
149         * 
150         * @param values  the numbers in ascending order.
151         * 
152         * @return The first quartile.
153         */
154        public static double calculateQ1(List values) {
155            double result = Double.NaN;
156            int count = values.size();
157            if (count > 0) {
158                if (count % 2 == 1) {
159                    if (count > 1) {
160                        result = Statistics.calculateMedian(values, 0, count / 2);
161                    }
162                    else {
163                        result = Statistics.calculateMedian(values, 0, 0);
164                    }
165                }
166                else {
167                    result = Statistics.calculateMedian(values, 0, count / 2 - 1);
168                }
169                
170            }
171            return result;
172        }
173        
174        /**
175         * Calculates the third quartile for a list of numbers in ascending order.
176         * 
177         * @param values  the list of values.
178         * 
179         * @return The third quartile.
180         */
181        public static double calculateQ3(List values) {
182            double result = Double.NaN;
183            int count = values.size();
184            if (count > 0) {
185                if (count % 2 == 1) {
186                    if (count > 1) {
187                        result = Statistics.calculateMedian(
188                            values, count / 2, count - 1
189                        );
190                    }
191                    else {
192                        result = Statistics.calculateMedian(values, 0, 0);
193                    }
194                }
195                else {
196                    result = Statistics.calculateMedian(
197                        values, count / 2, count - 1
198                    );
199                }
200                
201            }
202            return result;
203        }
204        
205    }