Source for org.jfree.data.time.MovingAverage

   1: /* ===========================================================
   2:  * JFreeChart : a free chart library for the Java(tm) platform
   3:  * ===========================================================
   4:  *
   5:  * (C) Copyright 2000-2007, by Object Refinery Limited and Contributors.
   6:  *
   7:  * Project Info:  http://www.jfree.org/jfreechart/index.html
   8:  *
   9:  * This library is free software; you can redistribute it and/or modify it 
  10:  * under the terms of the GNU Lesser General Public License as published by 
  11:  * the Free Software Foundation; either version 2.1 of the License, or 
  12:  * (at your option) any later version.
  13:  *
  14:  * This library is distributed in the hope that it will be useful, but 
  15:  * WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY 
  16:  * or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Lesser General Public 
  17:  * License for more details.
  18:  *
  19:  * You should have received a copy of the GNU Lesser General Public
  20:  * License along with this library; if not, write to the Free Software
  21:  * Foundation, Inc., 51 Franklin Street, Fifth Floor, Boston, MA  02110-1301, 
  22:  * USA.  
  23:  *
  24:  * [Java is a trademark or registered trademark of Sun Microsystems, Inc. 
  25:  * in the United States and other countries.]
  26:  *
  27:  * ------------------
  28:  * MovingAverage.java
  29:  * ------------------
  30:  * (C) Copyright 2003-2007, by Object Refinery Limited.
  31:  *
  32:  * Original Author:  David Gilbert (for Object Refinery Limited);
  33:  * Contributor(s):   Benoit Xhenseval;
  34:  *
  35:  * Changes
  36:  * -------
  37:  * 28-Jan-2003 : Version 1 (DG);
  38:  * 10-Mar-2003 : Added createPointMovingAverage() method contributed by Benoit 
  39:  *               Xhenseval (DG);
  40:  * 01-Aug-2003 : Added new method for TimeSeriesCollection, and fixed bug in 
  41:  *               XYDataset method (DG);
  42:  * 15-Jul-2004 : Switched getX() with getXValue() and getY() with 
  43:  *               getYValue() (DG);
  44:  * 11-Jan-2005 : Removed deprecated code in preparation for the 1.0.0 
  45:  *               release (DG);
  46:  *
  47:  */
  48: 
  49: package org.jfree.data.time;
  50: 
  51: import org.jfree.data.xy.XYDataset;
  52: import org.jfree.data.xy.XYSeries;
  53: import org.jfree.data.xy.XYSeriesCollection;
  54: 
  55: /**
  56:  * A utility class for calculating moving averages of time series data.
  57:  */
  58: public class MovingAverage {
  59: 
  60:     /**
  61:      * Creates a new {@link TimeSeriesCollection} containing a moving average 
  62:      * series for each series in the source collection.
  63:      * 
  64:      * @param source  the source collection.
  65:      * @param suffix  the suffix added to each source series name to create the
  66:      *                corresponding moving average series name.
  67:      * @param periodCount  the number of periods in the moving average 
  68:      *                     calculation.
  69:      * @param skip  the number of initial periods to skip.
  70:      * 
  71:      * @return A collection of moving average time series.
  72:      */
  73:     public static TimeSeriesCollection createMovingAverage(
  74:         TimeSeriesCollection source, String suffix, int periodCount,
  75:         int skip) {
  76:     
  77:         // check arguments
  78:         if (source == null) {
  79:             throw new IllegalArgumentException(
  80:                 "MovingAverage.createMovingAverage() : null source."
  81:             );
  82:         }
  83: 
  84:         if (periodCount < 1) {
  85:             throw new IllegalArgumentException(
  86:                 "periodCount must be greater than or equal to 1."
  87:             );
  88:         }
  89: 
  90:         TimeSeriesCollection result = new TimeSeriesCollection();
  91:         
  92:         for (int i = 0; i < source.getSeriesCount(); i++) {
  93:             TimeSeries sourceSeries = source.getSeries(i);
  94:             TimeSeries maSeries = createMovingAverage(
  95:                 sourceSeries, sourceSeries.getKey() + suffix, periodCount, skip
  96:             );
  97:             result.addSeries(maSeries);       
  98:         }
  99:         
 100:         return result;
 101:         
 102:     }
 103:     
 104:     /**
 105:      * Creates a new {@link TimeSeries} containing moving average values for 
 106:      * the given series.  If the series is empty (contains zero items), the 
 107:      * result is an empty series.
 108:      *
 109:      * @param source  the source series.
 110:      * @param name  the name of the new series.
 111:      * @param periodCount  the number of periods used in the average 
 112:      *                     calculation.
 113:      * @param skip  the number of initial periods to skip.
 114:      *
 115:      * @return The moving average series.
 116:      */
 117:     public static TimeSeries createMovingAverage(TimeSeries source,
 118:                                                  String name,
 119:                                                  int periodCount,
 120:                                                  int skip) {
 121: 
 122:         // check arguments
 123:         if (source == null) {
 124:             throw new IllegalArgumentException("Null source.");
 125:         }
 126: 
 127:         if (periodCount < 1) {
 128:             throw new IllegalArgumentException(
 129:                 "periodCount must be greater than or equal to 1."
 130:             );
 131: 
 132:         }
 133: 
 134:         TimeSeries result = new TimeSeries(name, source.getTimePeriodClass());
 135: 
 136:         if (source.getItemCount() > 0) {
 137: 
 138:             // if the initial averaging period is to be excluded, then 
 139:             // calculate the index of the
 140:             // first data item to have an average calculated...
 141:             long firstSerial 
 142:                 = source.getDataItem(0).getPeriod().getSerialIndex() + skip;
 143: 
 144:             for (int i = source.getItemCount() - 1; i >= 0; i--) {
 145: 
 146:                 // get the current data item...
 147:                 TimeSeriesDataItem current = source.getDataItem(i);
 148:                 RegularTimePeriod period = current.getPeriod();
 149:                 long serial = period.getSerialIndex();
 150: 
 151:                 if (serial >= firstSerial) {
 152:                     // work out the average for the earlier values...
 153:                     int n = 0;
 154:                     double sum = 0.0;
 155:                     long serialLimit = period.getSerialIndex() - periodCount;
 156:                     int offset = 0;
 157:                     boolean finished = false;
 158: 
 159:                     while ((offset < periodCount) && (!finished)) {
 160:                         if ((i - offset) >= 0) {
 161:                             TimeSeriesDataItem item 
 162:                                 = source.getDataItem(i - offset);
 163:                             RegularTimePeriod p = item.getPeriod();
 164:                             Number v = item.getValue();
 165:                             long currentIndex = p.getSerialIndex();
 166:                             if (currentIndex > serialLimit) {
 167:                                 if (v != null) {
 168:                                     sum = sum + v.doubleValue();
 169:                                     n = n + 1;
 170:                                 }
 171:                             }
 172:                             else {
 173:                                 finished = true;
 174:                             }
 175:                         }
 176:                         offset = offset + 1;
 177:                     }
 178:                     if (n > 0) {
 179:                         result.add(period, sum / n);
 180:                     }
 181:                     else {
 182:                         result.add(period, null);
 183:                     }
 184:                 }
 185: 
 186:             }
 187:         }
 188: 
 189:         return result;
 190: 
 191:     }
 192: 
 193:     /**
 194:      * Creates a new {@link TimeSeries} containing moving average values for 
 195:      * the given series, calculated by number of points (irrespective of the 
 196:      * 'age' of those points).  If the series is empty (contains zero items), 
 197:      * the result is an empty series.
 198:      * <p>
 199:      * Developed by Benoit Xhenseval (www.ObjectLab.co.uk).
 200:      *
 201:      * @param source  the source series.
 202:      * @param name  the name of the new series.
 203:      * @param pointCount  the number of POINTS used in the average calculation 
 204:      *                    (not periods!)
 205:      *
 206:      * @return The moving average series.
 207:      */
 208:     public static TimeSeries createPointMovingAverage(TimeSeries source,
 209:                                                       String name, 
 210:                                                       int pointCount) {
 211: 
 212:         // check arguments
 213:         if (source == null) {
 214:             throw new IllegalArgumentException("Null 'source'.");
 215:         }
 216: 
 217:         if (pointCount < 2) {
 218:             throw new IllegalArgumentException(
 219:                 "periodCount must be greater than or equal to 2."
 220:             );
 221:         }
 222: 
 223:         TimeSeries result = new TimeSeries(name, source.getTimePeriodClass());
 224:         double rollingSumForPeriod = 0.0;
 225:         for (int i = 0; i < source.getItemCount(); i++) {
 226:             // get the current data item...
 227:             TimeSeriesDataItem current = source.getDataItem(i);
 228:             RegularTimePeriod period = current.getPeriod();
 229:             rollingSumForPeriod += current.getValue().doubleValue();
 230: 
 231:             if (i > pointCount - 1) {
 232:                 // remove the point i-periodCount out of the rolling sum.
 233:                 TimeSeriesDataItem startOfMovingAvg 
 234:                     = source.getDataItem(i - pointCount);
 235:                 rollingSumForPeriod 
 236:                     -= startOfMovingAvg.getValue().doubleValue();
 237:                 result.add(period, rollingSumForPeriod / pointCount);
 238:             }
 239:             else if (i == pointCount - 1) {
 240:                 result.add(period, rollingSumForPeriod / pointCount);
 241:             }
 242:         }
 243:         return result;
 244:     }
 245: 
 246:     /**
 247:      * Creates a new {@link XYDataset} containing the moving averages of each 
 248:      * series in the <code>source</code> dataset.
 249:      *
 250:      * @param source  the source dataset.
 251:      * @param suffix  the string to append to source series names to create 
 252:      *                target series names.
 253:      * @param period  the averaging period.
 254:      * @param skip  the length of the initial skip period.
 255:      *
 256:      * @return The dataset.
 257:      */
 258:     public static XYDataset createMovingAverage(XYDataset source, String suffix,
 259:                                                 long period, final long skip) {
 260: 
 261:         return createMovingAverage(
 262:             source, suffix, (double) period, (double) skip
 263:         );
 264:         
 265:     }
 266: 
 267: 
 268:     /**
 269:      * Creates a new {@link XYDataset} containing the moving averages of each 
 270:      * series in the <code>source</code> dataset.
 271:      *
 272:      * @param source  the source dataset.
 273:      * @param suffix  the string to append to source series names to create 
 274:      *                target series names.
 275:      * @param period  the averaging period.
 276:      * @param skip  the length of the initial skip period.
 277:      *
 278:      * @return The dataset.
 279:      */
 280:     public static XYDataset createMovingAverage(XYDataset source, String suffix,
 281:                                                 double period, double skip) {
 282: 
 283:         // check arguments
 284:         if (source == null) {
 285:             throw new IllegalArgumentException("Null source (XYDataset).");
 286:         }
 287:         
 288:         XYSeriesCollection result = new XYSeriesCollection();
 289: 
 290:         for (int i = 0; i < source.getSeriesCount(); i++) {
 291:             XYSeries s = createMovingAverage(
 292:                 source, i, source.getSeriesKey(i) + suffix, period, skip
 293:             );
 294:             result.addSeries(s);
 295:         }
 296: 
 297:         return result;
 298: 
 299:     }
 300: 
 301:     /**
 302:      * Creates a new {@link XYSeries} containing the moving averages of one 
 303:      * series in the <code>source</code> dataset.
 304:      *
 305:      * @param source  the source dataset.
 306:      * @param series  the series index (zero based).
 307:      * @param name  the name for the new series.
 308:      * @param period  the averaging period.
 309:      * @param skip  the length of the initial skip period.
 310:      *
 311:      * @return The dataset.
 312:      */
 313:     public static XYSeries createMovingAverage(XYDataset source, 
 314:                                                int series, String name,
 315:                                                double period, double skip) {
 316: 
 317:                                                
 318:         // check arguments
 319:         if (source == null) {
 320:             throw new IllegalArgumentException("Null source (XYDataset).");
 321:         }
 322: 
 323:         if (period < Double.MIN_VALUE) {
 324:             throw new IllegalArgumentException("period must be positive.");
 325: 
 326:         }
 327: 
 328:         if (skip < 0.0) {
 329:             throw new IllegalArgumentException("skip must be >= 0.0.");
 330: 
 331:         }
 332: 
 333:         XYSeries result = new XYSeries(name);
 334: 
 335:         if (source.getItemCount(series) > 0) {
 336: 
 337:             // if the initial averaging period is to be excluded, then 
 338:             // calculate the lowest x-value to have an average calculated...
 339:             double first = source.getXValue(series, 0) + skip;
 340: 
 341:             for (int i = source.getItemCount(series) - 1; i >= 0; i--) {
 342: 
 343:                 // get the current data item...
 344:                 double x = source.getXValue(series, i);
 345: 
 346:                 if (x >= first) {
 347:                     // work out the average for the earlier values...
 348:                     int n = 0;
 349:                     double sum = 0.0;
 350:                     double limit = x - period;
 351:                     int offset = 0;
 352:                     boolean finished = false;
 353: 
 354:                     while (!finished) {
 355:                         if ((i - offset) >= 0) {
 356:                             double xx = source.getXValue(series, i - offset);
 357:                             Number yy = source.getY(series, i - offset);
 358:                             if (xx > limit) {
 359:                                 if (yy != null) {
 360:                                     sum = sum + yy.doubleValue();
 361:                                     n = n + 1;
 362:                                 }
 363:                             }
 364:                             else {
 365:                                 finished = true;
 366:                             }
 367:                         }
 368:                         else {
 369:                             finished = true;
 370:                         }
 371:                         offset = offset + 1;
 372:                     }
 373:                     if (n > 0) {
 374:                         result.add(x, sum / n);
 375:                     }
 376:                     else {
 377:                         result.add(x, null);
 378:                     }
 379:                 }
 380: 
 381:             }
 382:         }
 383: 
 384:         return result;
 385: 
 386:     }
 387: 
 388: }