    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    }