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