001/* 002 * Copyright © 2025 CUI-OpenSource-Software (info@cuioss.de) 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 */ 016package de.cuioss.benchmarking.common.report; 017 018import de.cuioss.benchmarking.common.constants.BenchmarkConstants; 019import lombok.Getter; 020 021import java.util.Collection; 022import java.util.List; 023import java.util.Objects; 024 025/** 026 * Provides pure statistical computation utilities for benchmark metrics. 027 * <p> 028 * This class is responsible for all statistical calculations including: 029 * <ul> 030 * <li>Basic statistics (min, max, mean, median)</li> 031 * <li>Moving averages</li> 032 * <li>Percentage changes and trends</li> 033 * <li>Standard deviation and variance</li> 034 * </ul> 035 * <p> 036 * Use this class when you need to perform mathematical/statistical operations on benchmark data. 037 * For metric-specific calculations (performance scores, grades), use {@link MetricsComputer}. 038 * For time-series analysis and trend detection, use {@link TrendDataProcessor}. 039 * 040 * @see MetricsComputer for benchmark-specific metric calculations 041 * @see TrendDataProcessor for time-series and trend analysis 042 */ 043public final class StatisticsCalculator { 044 045 public static final String COLLECTION_CANNOT_BE_NULL = "Values collection cannot be null"; 046 047 private StatisticsCalculator() { 048 // Utility class with static methods only 049 } 050 051 /** 052 * Calculates the arithmetic mean (average) of a collection of values. 053 * 054 * @param values collection of numeric values 055 * @return the mean value, or 0.0 if the collection is empty 056 * @throws NullPointerException if values is null 057 */ 058 public static double calculateMean(Collection<Double> values) { 059 Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL); 060 061 if (values.isEmpty()) { 062 return 0.0; 063 } 064 065 return values.stream() 066 .mapToDouble(Double::doubleValue) 067 .average() 068 .orElse(0.0); 069 } 070 071 /** 072 * Finds the minimum value in a collection. 073 * 074 * @param values collection of numeric values 075 * @return the minimum value, or Double.MAX_VALUE if the collection is empty 076 * @throws NullPointerException if values is null 077 */ 078 public static double findMin(Collection<Double> values) { 079 Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL); 080 081 return values.stream() 082 .mapToDouble(Double::doubleValue) 083 .min() 084 .orElse(Double.MAX_VALUE); 085 } 086 087 /** 088 * Finds the maximum value in a collection. 089 * 090 * @param values collection of numeric values 091 * @return the maximum value, or Double.MIN_VALUE if the collection is empty 092 * @throws NullPointerException if values is null 093 */ 094 public static double findMax(Collection<Double> values) { 095 Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL); 096 097 return values.stream() 098 .mapToDouble(Double::doubleValue) 099 .max() 100 .orElse(Double.MIN_VALUE); 101 } 102 103 /** 104 * Calculates the median value of a collection. 105 * 106 * @param values collection of numeric values 107 * @return the median value, or 0.0 if the collection is empty 108 * @throws NullPointerException if values is null 109 */ 110 public static double calculateMedian(Collection<Double> values) { 111 Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL); 112 113 if (values.isEmpty()) { 114 return 0.0; 115 } 116 117 List<Double> sorted = values.stream() 118 .sorted() 119 .toList(); 120 121 int size = sorted.size(); 122 if (size % 2 == 0) { 123 return (sorted.get(size / 2 - 1) + sorted.get(size / 2)) / 2.0; 124 } else { 125 return sorted.get(size / 2); 126 } 127 } 128 129 /** 130 * Calculates the percentage change between two values. 131 * <p> 132 * Formula: ((newValue - oldValue) / oldValue) * 100 133 * 134 * @param oldValue the original value 135 * @param newValue the new value 136 * @return the percentage change 137 */ 138 public static double calculatePercentageChange(double oldValue, double newValue) { 139 if (oldValue == 0) { 140 return newValue == 0 ? 0.0 : 100.0; 141 } 142 return ((newValue - oldValue) / oldValue) * 100; 143 } 144 145 /** 146 * Calculates a simple moving average for the most recent N values. 147 * 148 * @param values list of values (assumed to be in chronological order) 149 * @param windowSize the number of most recent values to include 150 * @return the moving average 151 * @throws NullPointerException if values is null 152 * @throws IllegalArgumentException if windowSize is less than 1 153 */ 154 public static double calculateMovingAverage(List<Double> values, int windowSize) { 155 Objects.requireNonNull(values, "Values list cannot be null"); 156 if (windowSize < 1) { 157 throw new IllegalArgumentException("Window size must be at least 1"); 158 } 159 160 if (values.isEmpty()) { 161 return 0.0; 162 } 163 164 int actualWindow = Math.min(windowSize, values.size()); 165 List<Double> window = values.size() <= actualWindow 166 ? values 167 : values.subList(values.size() - actualWindow, values.size()); 168 169 return calculateMean(window); 170 } 171 172 /** 173 * Calculates the standard deviation of a collection of values. 174 * 175 * @param values collection of numeric values 176 * @return the standard deviation, or 0.0 if the collection has less than 2 elements 177 * @throws NullPointerException if values is null 178 */ 179 public static double calculateStandardDeviation(Collection<Double> values) { 180 Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL); 181 182 if (values.size() < 2) { 183 return 0.0; 184 } 185 186 double mean = calculateMean(values); 187 double variance = values.stream() 188 .mapToDouble(value -> Math.pow(value - mean, 2)) 189 .average() 190 .orElse(0.0); 191 192 return Math.sqrt(variance); 193 } 194 195 /** 196 * Calculates an Exponentially Weighted Moving Average (EWMA) baseline from historical values. 197 * <p> 198 * EWMA gives more weight to recent values while still considering historical context, 199 * making it ideal for performance monitoring and trend detection. The decay factor (lambda) 200 * determines how quickly older values lose influence. 201 * <p> 202 * Formula: EWMA = Σ(value_i × λ^i) / Σ(λ^i) 203 * where i=0 is the most recent value, i=1 is second most recent, etc. 204 * 205 * @param values list of historical values, ordered from newest to oldest 206 * @param lambda decay factor (typically 0.2-0.3 for performance monitoring). 207 * Smaller values = more weight on recent data. 208 * @return the weighted baseline value 209 * @throws NullPointerException if values is null 210 * @throws IllegalArgumentException if lambda is not in (0, 1] range 211 */ 212 public static double calculateEWMA(List<Double> values, double lambda) { 213 Objects.requireNonNull(values, "Values list cannot be null"); 214 if (lambda <= 0 || lambda > 1) { 215 throw new IllegalArgumentException("Lambda must be in range (0, 1], got: " + lambda); 216 } 217 218 if (values.isEmpty()) { 219 return 0.0; 220 } 221 222 if (values.size() == 1) { 223 return values.getFirst(); 224 } 225 226 double weightedSum = 0.0; 227 double weightSum = 0.0; 228 double currentWeight = 1.0; // Start with weight = λ^0 = 1.0 229 230 for (Double value : values) { 231 weightedSum += value * currentWeight; 232 weightSum += currentWeight; 233 currentWeight *= lambda; // Exponential decay: next weight = current × λ 234 } 235 236 return weightedSum / weightSum; 237 } 238 239 /** 240 * Determines the trend direction based on percentage change and a stability threshold. 241 * 242 * @param percentageChange the percentage change value 243 * @param stabilityThreshold the threshold (as a percentage) below which the trend is considered stable 244 * @return "up", "down", or "stable" 245 */ 246 public static String determineTrendDirection(double percentageChange, double stabilityThreshold) { 247 if (Math.abs(percentageChange) < stabilityThreshold) { 248 return BenchmarkConstants.Report.Badge.TrendDirection.STABLE; 249 } 250 return percentageChange > 0 ? BenchmarkConstants.Report.Badge.TrendDirection.UP : BenchmarkConstants.Report.Badge.TrendDirection.DOWN; 251 } 252 253 254 /** 255 * Statistical summary containing basic statistics for a dataset. 256 */ 257 @Getter 258 public static class Statistics { 259 private final double min; 260 private final double max; 261 private final double mean; 262 private final double median; 263 private final double stdDev; 264 private final int count; 265 266 public Statistics(double min, double max, double mean, double median, double stdDev, int count) { 267 this.min = min; 268 this.max = max; 269 this.mean = mean; 270 this.median = median; 271 this.stdDev = stdDev; 272 this.count = count; 273 } 274 } 275 276 /** 277 * Computes comprehensive statistics for a dataset. 278 * 279 * @param values collection of numeric values 280 * @return a Statistics object containing min, max, mean, median, standard deviation, and count 281 * @throws NullPointerException if values is null 282 */ 283 public static Statistics computeStatistics(Collection<Double> values) { 284 Objects.requireNonNull(values, COLLECTION_CANNOT_BE_NULL); 285 286 return new Statistics( 287 findMin(values), 288 findMax(values), 289 calculateMean(values), 290 calculateMedian(values), 291 calculateStandardDeviation(values), 292 values.size() 293 ); 294 } 295}