    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    }