001/* =========================================================== 002 * JFreeChart : a free chart library for the Java(tm) platform 003 * =========================================================== 004 * 005 * (C) Copyright 2000-2022, by David Gilbert 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-2022, by David Gilbert and Contributors. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): -; 034 * 035 */ 036 037package org.jfree.data.statistics; 038 039import org.jfree.chart.internal.Args; 040 041import java.util.ArrayList; 042import java.util.Collections; 043import java.util.List; 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<? extends Number> 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<? extends Number> values, boolean stripNullAndNaNItems) { 085 086 Args.nullNotPermitted(values, "values"); 087 088 List vlist; 089 if (stripNullAndNaNItems) { 090 vlist = new ArrayList(values.size()); 091 for (Object obj : values) { 092 if (obj instanceof Number) { 093 Number n = (Number) obj; 094 double v = n.doubleValue(); 095 if (!Double.isNaN(v)) { 096 vlist.add(n); 097 } 098 } 099 } 100 } 101 else { 102 vlist = values; 103 } 104 Collections.sort(vlist); 105 106 double mean = Statistics.calculateMean(vlist, false); 107 double median = Statistics.calculateMedian(vlist, false); 108 double q1 = calculateQ1(vlist); 109 double q3 = calculateQ3(vlist); 110 111 double interQuartileRange = q3 - q1; 112 113 double upperOutlierThreshold = q3 + (interQuartileRange * 1.5); 114 double lowerOutlierThreshold = q1 - (interQuartileRange * 1.5); 115 116 double upperFaroutThreshold = q3 + (interQuartileRange * 2.0); 117 double lowerFaroutThreshold = q1 - (interQuartileRange * 2.0); 118 119 double minRegularValue = Double.POSITIVE_INFINITY; 120 double maxRegularValue = Double.NEGATIVE_INFINITY; 121 double minOutlier = Double.POSITIVE_INFINITY; 122 double maxOutlier = Double.NEGATIVE_INFINITY; 123 List<Number> outliers = new ArrayList<>(); 124 125 for (Object o : vlist) { 126 Number number = (Number) o; 127 double value = number.doubleValue(); 128 if (value > upperOutlierThreshold) { 129 outliers.add(number); 130 if (value > maxOutlier && value <= upperFaroutThreshold) { 131 maxOutlier = value; 132 } 133 } 134 else if (value < lowerOutlierThreshold) { 135 outliers.add(number); 136 if (value < minOutlier && value >= lowerFaroutThreshold) { 137 minOutlier = value; 138 } 139 } 140 else { 141 minRegularValue = Math.min(minRegularValue, value); 142 maxRegularValue = Math.max(maxRegularValue, value); 143 } 144 minOutlier = Math.min(minOutlier, minRegularValue); 145 maxOutlier = Math.max(maxOutlier, maxRegularValue); 146 } 147 148 return new BoxAndWhiskerItem(mean, median, q1, q3, minRegularValue, 149 maxRegularValue, minOutlier, maxOutlier, outliers); 150 151 } 152 153 /** 154 * Calculates the first quartile for a list of numbers in ascending order. 155 * If the items in the list are not in ascending order, the result is 156 * unspecified. If the list contains items that are {@code null}, not 157 * an instance of {@code Number}, or equivalent to 158 * {@code Double.NaN}, the result is unspecified. 159 * 160 * @param values the numbers in ascending order ({@code null} not 161 * permitted). 162 * 163 * @return The first quartile. 164 */ 165 public static double calculateQ1(List values) { 166 Args.nullNotPermitted(values, "values"); 167 168 double result = Double.NaN; 169 int count = values.size(); 170 if (count > 0) { 171 if (count % 2 == 1) { 172 if (count > 1) { 173 result = Statistics.calculateMedian(values, 0, count / 2); 174 } 175 else { 176 result = Statistics.calculateMedian(values, 0, 0); 177 } 178 } 179 else { 180 result = Statistics.calculateMedian(values, 0, count / 2 - 1); 181 } 182 183 } 184 return result; 185 } 186 187 /** 188 * Calculates the third quartile for a list of numbers in ascending order. 189 * If the items in the list are not in ascending order, the result is 190 * unspecified. If the list contains items that are {@code null}, not 191 * an instance of {@code Number}, or equivalent to 192 * {@code Double.NaN}, the result is unspecified. 193 * 194 * @param values the list of values ({@code null} not permitted). 195 * 196 * @return The third quartile. 197 */ 198 public static double calculateQ3(List values) { 199 Args.nullNotPermitted(values, "values"); 200 double result = Double.NaN; 201 int count = values.size(); 202 if (count > 0) { 203 if (count % 2 == 1) { 204 if (count > 1) { 205 result = Statistics.calculateMedian(values, count / 2, 206 count - 1); 207 } 208 else { 209 result = Statistics.calculateMedian(values, 0, 0); 210 } 211 } 212 else { 213 result = Statistics.calculateMedian(values, count / 2, 214 count - 1); 215 } 216 } 217 return result; 218 } 219 220}