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 * MovingAverage.java 029 * ------------------ 030 * (C) Copyright 2003-2022, by David Gilbert. 031 * 032 * Original Author: David Gilbert; 033 * Contributor(s): Benoit Xhenseval; 034 * 035 */ 036 037package org.jfree.data.time; 038 039import org.jfree.chart.internal.Args; 040import org.jfree.data.xy.XYDataset; 041import org.jfree.data.xy.XYSeries; 042import org.jfree.data.xy.XYSeriesCollection; 043 044/** 045 * A utility class for calculating moving averages of time series data. 046 */ 047public class MovingAverage { 048 049 /** 050 * Creates a new {@link TimeSeriesCollection} containing a moving average 051 * series for each series in the source collection. 052 * 053 * @param source the source collection. 054 * @param suffix the suffix added to each source series name to create the 055 * corresponding moving average series name. 056 * @param periodCount the number of periods in the moving average 057 * calculation. 058 * @param skip the number of initial periods to skip. 059 * 060 * @return A collection of moving average time series. 061 */ 062 public static TimeSeriesCollection createMovingAverage( 063 TimeSeriesCollection source, String suffix, int periodCount, 064 int skip) { 065 066 Args.nullNotPermitted(source, "source"); 067 if (periodCount < 1) { 068 throw new IllegalArgumentException("periodCount must be greater " 069 + "than or equal to 1."); 070 } 071 072 TimeSeriesCollection result = new TimeSeriesCollection(); 073 for (int i = 0; i < source.getSeriesCount(); i++) { 074 TimeSeries sourceSeries = source.getSeries(i); 075 TimeSeries maSeries = createMovingAverage(sourceSeries, 076 sourceSeries.getKey() + suffix, periodCount, skip); 077 result.addSeries(maSeries); 078 } 079 return result; 080 081 } 082 083 /** 084 * Creates a new {@link TimeSeries} containing moving average values for 085 * the given series. If the series is empty (contains zero items), the 086 * result is an empty series. 087 * 088 * @param source the source series. 089 * @param name the series key ({@code null} not permitted). 090 * @param periodCount the number of periods used in the average 091 * calculation. 092 * @param skip the number of initial periods to skip. 093 * 094 * @param <S> the type for the series keys. 095 * 096 * @return The moving average series. 097 */ 098 public static <S extends Comparable<S>> TimeSeries<S> createMovingAverage( 099 TimeSeries<S> source, S name, int periodCount, int skip) { 100 101 Args.nullNotPermitted(source, "source"); 102 if (periodCount < 1) { 103 throw new IllegalArgumentException("periodCount must be greater " 104 + "than or equal to 1."); 105 } 106 107 TimeSeries<S> result = new TimeSeries<>(name); 108 if (source.getItemCount() > 0) { 109 110 // if the initial averaging period is to be excluded, then 111 // calculate the index of the 112 // first data item to have an average calculated... 113 long firstSerial = source.getTimePeriod(0).getSerialIndex() + skip; 114 115 for (int i = source.getItemCount() - 1; i >= 0; i--) { 116 117 // get the current data item... 118 RegularTimePeriod period = source.getTimePeriod(i); 119 long serial = period.getSerialIndex(); 120 121 if (serial >= firstSerial) { 122 // work out the average for the earlier values... 123 int n = 0; 124 double sum = 0.0; 125 long serialLimit = period.getSerialIndex() - periodCount; 126 int offset = 0; 127 boolean finished = false; 128 129 while ((offset < periodCount) && (!finished)) { 130 if ((i - offset) >= 0) { 131 TimeSeriesDataItem item = source.getRawDataItem( 132 i - offset); 133 RegularTimePeriod p = item.getPeriod(); 134 Number v = item.getValue(); 135 long currentIndex = p.getSerialIndex(); 136 if (currentIndex > serialLimit) { 137 if (v != null) { 138 sum = sum + v.doubleValue(); 139 n = n + 1; 140 } 141 } 142 else { 143 finished = true; 144 } 145 } 146 offset = offset + 1; 147 } 148 if (n > 0) { 149 result.add(period, sum / n); 150 } 151 else { 152 result.add(period, null); 153 } 154 } 155 156 } 157 } 158 return result; 159 } 160 161 /** 162 * Creates a new {@link TimeSeries} containing moving average values for 163 * the given series, calculated by number of points (irrespective of the 164 * 'age' of those points). If the series is empty (contains zero items), 165 * the result is an empty series. 166 * <p> 167 * Developed by Benoit Xhenseval (www.ObjectLab.co.uk). 168 * 169 * @param source the source series. 170 * @param name the series key ({@code null} not permitted). 171 * @param pointCount the number of POINTS used in the average calculation 172 * (not periods!) 173 * 174 * @param <S> the type for the series keys. 175 * 176 * @return The moving average series. 177 */ 178 public static <S extends Comparable<S>> TimeSeries<S> createPointMovingAverage( 179 TimeSeries<S> source, S name, int pointCount) { 180 181 Args.nullNotPermitted(source, "source"); 182 if (pointCount < 2) { 183 throw new IllegalArgumentException("periodCount must be greater " 184 + "than or equal to 2."); 185 } 186 187 TimeSeries<S> result = new TimeSeries<>(name); 188 double rollingSumForPeriod = 0.0; 189 for (int i = 0; i < source.getItemCount(); i++) { 190 // get the current data item... 191 TimeSeriesDataItem current = source.getRawDataItem(i); 192 RegularTimePeriod period = current.getPeriod(); 193 // FIXME: what if value is null on next line? 194 rollingSumForPeriod += current.getValue().doubleValue(); 195 196 if (i > pointCount - 1) { 197 // remove the point i-periodCount out of the rolling sum. 198 TimeSeriesDataItem startOfMovingAvg = source.getRawDataItem( 199 i - pointCount); 200 rollingSumForPeriod -= startOfMovingAvg.getValue() 201 .doubleValue(); 202 result.add(period, rollingSumForPeriod / pointCount); 203 } 204 else if (i == pointCount - 1) { 205 result.add(period, rollingSumForPeriod / pointCount); 206 } 207 } 208 return result; 209 } 210 211 /** 212 * Creates a new {@link XYDataset} containing the moving averages of each 213 * series in the {@code source} dataset. 214 * 215 * @param source the source dataset. 216 * @param suffix the string to append to source series names to create 217 * target series names. 218 * @param period the averaging period. 219 * @param skip the length of the initial skip period. 220 * 221 * @return The dataset. 222 */ 223 public static XYDataset createMovingAverage(XYDataset source, String suffix, 224 long period, long skip) { 225 226 return createMovingAverage(source, suffix, (double) period, 227 (double) skip); 228 229 } 230 231 232 /** 233 * Creates a new {@link XYDataset} containing the moving averages of each 234 * series in the {@code source} dataset. 235 * 236 * @param source the source dataset. 237 * @param suffix the string to append to source series names to create 238 * target series names. 239 * @param period the averaging period. 240 * @param skip the length of the initial skip period. 241 * 242 * @return The dataset. 243 */ 244 public static XYDataset createMovingAverage(XYDataset source, 245 String suffix, double period, double skip) { 246 247 Args.nullNotPermitted(source, "source"); 248 XYSeriesCollection result = new XYSeriesCollection(); 249 for (int i = 0; i < source.getSeriesCount(); i++) { 250 XYSeries s = createMovingAverage(source, i, source.getSeriesKey(i) 251 + suffix, period, skip); 252 result.addSeries(s); 253 } 254 return result; 255 } 256 257 /** 258 * Creates a new {@link XYSeries} containing the moving averages of one 259 * series in the {@code source} dataset. 260 * 261 * @param source the source dataset. 262 * @param series the series index (zero based). 263 * @param name the name for the new series. 264 * @param period the averaging period. 265 * @param skip the length of the initial skip period. 266 * 267 * @return The dataset. 268 */ 269 public static XYSeries createMovingAverage(XYDataset source, 270 int series, String name, double period, double skip) { 271 272 Args.nullNotPermitted(source, "source"); 273 if (period < Double.MIN_VALUE) { 274 throw new IllegalArgumentException("period must be positive."); 275 } 276 if (skip < 0.0) { 277 throw new IllegalArgumentException("skip must be >= 0.0."); 278 } 279 280 XYSeries result = new XYSeries(name); 281 282 if (source.getItemCount(series) > 0) { 283 284 // if the initial averaging period is to be excluded, then 285 // calculate the lowest x-value to have an average calculated... 286 double first = source.getXValue(series, 0) + skip; 287 288 for (int i = source.getItemCount(series) - 1; i >= 0; i--) { 289 290 // get the current data item... 291 double x = source.getXValue(series, i); 292 293 if (x >= first) { 294 // work out the average for the earlier values... 295 int n = 0; 296 double sum = 0.0; 297 double limit = x - period; 298 int offset = 0; 299 boolean finished = false; 300 301 while (!finished) { 302 if ((i - offset) >= 0) { 303 double xx = source.getXValue(series, i - offset); 304 Number yy = source.getY(series, i - offset); 305 if (xx > limit) { 306 if (yy != null) { 307 sum = sum + yy.doubleValue(); 308 n = n + 1; 309 } 310 } 311 else { 312 finished = true; 313 } 314 } 315 else { 316 finished = true; 317 } 318 offset = offset + 1; 319 } 320 if (n > 0) { 321 result.add(x, sum / n); 322 } 323 else { 324 result.add(x, null); 325 } 326 } 327 328 } 329 } 330 331 return result; 332 333 } 334 335}