All Downloads are FREE. Search and download functionalities are using the official Maven repository.

weka.classifiers.timeseries.core.Utils Maven / Gradle / Ivy

Go to download

Provides a time series forecasting environment for Weka. Includes a wrapper for Weka regression schemes that automates the process of creating lagged variables and date-derived periodic variables and provides the ability to do closed-loop forecasting. New evaluation routines are provided by a special evaluation module and graphing of predictions/forecasts are provided via the JFreeChart library. Includes both command-line and GUI user interfaces. Sample time series data can be found in ${WEKA_HOME}/packages/timeseriesForecasting/sample-data.

There is a newer version: 1.1.27
Show newest version
/*
 *   This program is free software: you can redistribute it and/or modify
 *   it under the terms of the GNU General Public License as published by
 *   the Free Software Foundation, either version 3 of the License, or
 *   (at your option) any later version.
 *
 *   This program is distributed in the hope that it will be useful,
 *   but WITHOUT ANY WARRANTY; without even the implied warranty of
 *   MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *   GNU General Public License for more details.
 *
 *   You should have received a copy of the GNU General Public License
 *   along with this program.  If not, see .
 */


/*
 *    Utils.java
 *    Copyright (C) 2010-2016 University of Waikato, Hamilton, New Zealand
 */

package weka.classifiers.timeseries.core;

import java.text.SimpleDateFormat;
import java.util.Calendar;
import java.util.Date;
import java.util.GregorianCalendar;
import java.util.List;

import weka.filters.supervised.attribute.TSLagMaker;
import weka.filters.supervised.attribute.TSLagMaker.Periodicity;
import weka.filters.supervised.attribute.TSLagMaker.PeriodicityHandler;
import weka.core.Attribute;
import weka.core.DenseInstance;
import weka.core.Instance;
import weka.core.Instances;

/**
 * Static utility routines.
 * 
 * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision: 52593 $
 * 
 */
public class Utils {

  protected static Instance makeInstance(double timeToAdvance,
      PeriodicityHandler periodicityHandler, int numAtts, int timeIndex) {

    double incrTime = advanceSuppliedTimeValue(timeToAdvance,
        periodicityHandler, false);
    double[] newVals = new double[numAtts];
    for (int i = 0; i < newVals.length; i++) {
      newVals[i] = weka.core.Utils.missingValue();
    }
    newVals[timeIndex] = incrTime;
    Instance newInst = new DenseInstance(1.0, newVals);

    /*
     * if (missingReport != null) { if (periodicityHandler.isDateBased()) {
     * String timeFormat = "yyyy-MM-dd'T'HH:mm:ss"; SimpleDateFormat sdf = new
     * SimpleDateFormat(); sdf.applyPattern(timeFormat); Date d = new
     * Date((long) incrTime); String result = sdf.format(d); //
     * System.err.println("Creating a missing row " + result);
     * missingReport.add(result); } else { missingReport.add("" + incrTime); } }
     */

    return newInst;
  }

  protected static void addToMissingReport(double incrTime,
      PeriodicityHandler periodicityHandler, List missingReport) {
    if (missingReport != null) {
      if (periodicityHandler.isDateBased()) {
        String timeFormat = "yyyy-MM-dd'T'HH:mm:ss";
        SimpleDateFormat sdf = new SimpleDateFormat();
        sdf.applyPattern(timeFormat);
        Date d = new Date((long) incrTime);
        String result = sdf.format(d);
        // System.err.println("Creating a missing row " + result);
        missingReport.add(result);
      } else {
        missingReport.add("" + incrTime);
      }
    }
  }

  /**
   * Check to see if there are any instances (time steps) that are missing
   * entirely from the data (and are not in the skip list). We make the
   * assumption that the data won't contain both missing date values *and*
   * entirely missing rows as this is a kind of chicken and egg situation with
   * regards to detecting missing rows. E.g. if we have missing date values
   * bracketing one or more missing rows then we can't do the date comparisons
   * necessary for detecting whether the intermediate rows are missing.
   * Similarly the missing date handling routine assumes that there aren't
   * missing rows so that it can advance the previous date value by one unit in
   * order to compute the value for the current missing date value.
   * 
   * @param toInsert the instances to check
   * @param timeStampAtt the name of time stamp att
   * @param periodicityHandler the periodicity handler
   * @param m_skipEntries the list of data "holes" that are expected (i.e. don't
   *          actually count as time stamp increments)
   * @param missingReport will hold time stamps of any instances we insert
   * @return the (potentially) modified instances
   */
  public static Instances insertMissing(Instances toInsert,
      Attribute timeStampAtt, TSLagMaker.PeriodicityHandler periodicityHandler,
      String m_skipEntries, List missingReport) {

    if (m_skipEntries != null && m_skipEntries.length() > 0) {
      try {
        periodicityHandler.setSkipList(m_skipEntries, "yyyy-MM-dd'T'HH:mm:ss");
      } catch (Exception ex) {
        ex.printStackTrace();
      }
    }

    int timeIndex = timeStampAtt.index();

    /*
     * check to see if there are any instances (time steps) that are missing
     * entirely from the data (and are not in the skip list). We make the
     * assumption that the data won't contain both missing date values *and*
     * entirely missing rows as this is a kind of chicken and egg situation with
     * regards to detecting missing rows. E.g. if we have missing date values
     * bracketing one or more missing rows then we can't do the date comparisons
     * necessary for detecting whether the intermediate rows are missing.
     * Similarly the missing date handling routine assumes that there aren't
     * missing rows so that it can advance the previous date value by one unit
     * in order to compute the value for the current missing date value.
     */
    Instance prevInst = null;
    int current = 0;
    while (true) {
      if (current == toInsert.numInstances()) {
        break;
      }

      if (prevInst != null && !toInsert.instance(current).isMissing(timeIndex)) {
        if (periodicityHandler.getPeriodicity() != Periodicity.MONTHLY
            && periodicityHandler.getPeriodicity() != Periodicity.QUARTERLY) {
          double delta = periodicityHandler.getPeriodicity().deltaTime();

          double diff = toInsert.instance(current).value(timeIndex)
              - prevInst.value(timeIndex);

          /*
           * if the difference is more than 1 delta then insert a new row
           */
          if (diff > 1.5 * delta) {
            if (!periodicityHandler.isDateBased()
                || !periodicityHandler.dateInSkipList(new Date((long) prevInst
                    .value(timeIndex)))) {
              Instance newInst = makeInstance(prevInst.value(timeIndex),
                  periodicityHandler, toInsert.numAttributes(), timeIndex);

              Date candidate = new Date((long) newInst.value(timeIndex));
              if (!periodicityHandler.dateInSkipList(candidate)
                  && candidate.getTime() < toInsert.instance(current).value(
                      timeIndex)) {
                newInst.setDataset(toInsert);
                toInsert.add(current, newInst);
                addToMissingReport(newInst.value(timeIndex),
                    periodicityHandler, missingReport);
              }
            }
          }
        } else {
          Date d = new Date((long) toInsert.instance(current).value(timeIndex));
          Calendar c = new GregorianCalendar();
          c.setTime(d);
          int currentMonth = c.get(Calendar.MONTH);

          d = new Date((long) prevInst.value(timeIndex));
          c.setTime(d);
          int prevMonth = c.get(Calendar.MONTH);

          double diff = (currentMonth + 1)
              - ((prevMonth == Calendar.DECEMBER) ? 0 : prevMonth + 1);

          if (periodicityHandler.getPeriodicity() == Periodicity.MONTHLY) {
            /*
             * if the difference is more than 1 month then insert a new row
             */
            if (diff > 1.5) {
              if (!periodicityHandler.isDateBased()
                  || !periodicityHandler.dateInSkipList(new Date(
                      (long) prevInst.value(timeIndex)))) {
                Instance newInst = makeInstance(prevInst.value(timeIndex),
                    periodicityHandler, toInsert.numAttributes(), timeIndex);
                Date candidate = new Date((long) newInst.value(timeIndex));
                if (!periodicityHandler.dateInSkipList(candidate)
                    && candidate.getTime() < toInsert.instance(current).value(
                        timeIndex)) {
                  newInst.setDataset(toInsert);
                  toInsert.add(current, newInst);
                  addToMissingReport(newInst.value(timeIndex),
                      periodicityHandler, missingReport);
                }
              }
            }
          } else if (periodicityHandler.getPeriodicity() == Periodicity.QUARTERLY) {
            /*
             * if the difference is more than 1 quarter (3 months) then insert a
             * new row
             */
            if (diff > 4.5) {
              if (!periodicityHandler.isDateBased()
                  || !periodicityHandler.dateInSkipList(new Date(
                      (long) prevInst.value(timeIndex)))) {
                Instance newInst = makeInstance(prevInst.value(timeIndex),
                    periodicityHandler, toInsert.numAttributes(), timeIndex);
                Date candidate = new Date((long) newInst.value(timeIndex));
                if (!periodicityHandler.dateInSkipList(candidate)
                    && candidate.getTime() < toInsert.instance(current).value(
                        timeIndex)) {
                  toInsert.add(current, newInst);
                  addToMissingReport(newInst.value(timeIndex),
                      periodicityHandler, missingReport);
                }
              }
            }
          }
        }
      }

      if (!toInsert.instance(current).isMissing(timeIndex)) {
        prevInst = toInsert.instance(current);
      }

      current++;
    }

    return null;
  }

  /**
   * Replace missing target values by interpolation. Also replaces missing date
   * values (if a date time stamp has been specified and if possible).
   * 
   * @param toReplace the instances to replace missing target values and time
   *          stamp values
   * @param targets a list of target attributes to check for missing values
   * @param timeStampName the name of the time stamp attribute (or null if there
   *          is no time stamp)
   * @param dateOnly if true, only replace missing date values and not missing
   *          target values (useful for hold-out test sets)
   * @param userHint user-specified hint as to the data periodicity
   * @param skipEntries any skip entries (may be null)
   * @param missingReport a varargs parameter that, if provided, is expected to
   *          be up to two lists of Integers and one list of Strings. The first
   *          list will be populated with the instance numbers (duplicates are
   *          possible) of instances that have missing targets replaced. The
   *          second list will be populated with the instance numbers of
   *          instances that have missing time stamp values replaced. The third
   *          list will be populated with the time stamps for any new instances
   *          that are inserted into the data (i.e. if we detect that there are
   *          "holes" in the data that aren't covered by the skip list entries.
   * 
   * @return the instances with missing targets and (possibly) missing time
   *         stamp values replaced.
   */
  public static Instances replaceMissing(Instances toReplace,
      List targets, String timeStampName, boolean dateOnly,
      Periodicity userHint, String skipEntries, Object... missingReport) {

    Instances result = toReplace;

    Attribute timeStampAtt = null;
    TSLagMaker.PeriodicityHandler detected = null;

    List missingTargetList = null;
    List missingTimeStampList = null;
    List missingTimeStampRows = null;
    if (missingReport.length > 0) {
      missingTargetList = (List) missingReport[0];

      if (missingReport.length == 2) {
        missingTimeStampList = (List) missingReport[1];
      }

      if (missingReport.length == 3) {
        missingTimeStampRows = (List) missingReport[2];
      }
    }

    if (timeStampName != null && timeStampName.length() > 0) {
      timeStampAtt = toReplace.attribute(timeStampName);

      // must be a non-artificial time stamp
      if (timeStampAtt != null) {
        detected = TSLagMaker
            .determinePeriodicity(result, timeStampName, userHint);

        // check insertMissing (if periodicity is not UNKNOWN)
        /*
         * If we do this first, then we can interpolate the missing target
         * values that will be created for the rows that get inserted
         */
        if (detected.getPeriodicity() != Periodicity.UNKNOWN) {
          insertMissing(toReplace, timeStampAtt, detected, skipEntries,
              missingTimeStampRows);
        }
      }
    }

    // do a quick check to see if we need to replace any missing values
    boolean ok = true;
    for (int i = 0; i < toReplace.numInstances(); i++) {
      if (toReplace.instance(i).hasMissingValue()) {
        // now check against targets and possibly date
        if (!dateOnly) {
          for (String target : targets) {
            int attIndex = toReplace.attribute(target).index();
            if (toReplace.instance(i).isMissing(attIndex)) {
              ok = false;
              break;
            }
          }
          if (!ok) {
            break; // outer loop
          }
        }

        // check date if necessary
        if (timeStampAtt != null) {
          if (toReplace.instance(i).isMissing(timeStampAtt)) {
            ok = false;
            break;
          }
        }
      }
    }

    if (ok) {
      // nothing to do
      return result;
    }

    // process the target(s) first
    if (!dateOnly) {
      for (String target : targets) {
        if (result.attribute(target) != null) {
          int attIndex = result.attribute(target).index();
          double lastNonMissing = weka.core.Utils.missingValue();

          // We won't handle missing target values at the start or end
          // as experiments with using simple linear regression to fill
          // the missing values that are created by default by the lagging
          // process showed inferior performance compared to just letting
          // Weka take care of it via mean/mode replacement

          for (int i = 0; i < result.numInstances(); i++) {
            Instance current = result.instance(i);
            if (current.isMissing(attIndex)) {
              if (!weka.core.Utils.isMissingValue(lastNonMissing)) {
                // Search forward to the next non missing value (if any)
                double futureNonMissing = weka.core.Utils.missingValue();

                double x2 = 2; // number of x steps (lastNonMissing is at 0 on x
                               // axis)
                for (int j = i + 1; j < result.numInstances(); j++) {
                  if (!result.instance(j).isMissing(attIndex)) {
                    futureNonMissing = result.instance(j).value(attIndex);
                    break;
                  }
                  x2++;
                }

                if (!weka.core.Utils.isMissingValue(futureNonMissing)) {
                  // Now do the linear interpolation
                  double offset = lastNonMissing;
                  double slope = (futureNonMissing - lastNonMissing) / x2;

                  // fill in the missing values
                  for (int j = i; j < i + x2; j++) {
                    if (result.instance(j).isMissing(attIndex)) {
                      double interpolated = (((j - i) + 1) * slope) + offset;

                      result.instance(j).setValue(attIndex, interpolated);
                      if (missingTargetList != null) {
                        missingTargetList.add(new Integer(j + 1));
                      }
                    }
                  }
                }
              } else {
                // won't do anything with start/end missing values
              }
            } else {
              lastNonMissing = current.value(attIndex);
            }
          }
        }
      }
    }

    // now check for missing date values (if necessary)
    if (timeStampAtt != null) {

      int attIndex = timeStampAtt.index(); // result.attribute(timeStampName).index();

      double firstNonMissing = result.instance(0).value(attIndex);
      double previousNonMissing = firstNonMissing;
      int firstNonMissingIndex = -1;
      boolean leadingMissingDates = weka.core.Utils
          .isMissingValue(firstNonMissing);

      for (int i = 0; i < result.numInstances(); i++) {
        Instance current = result.instance(i);

        if (current.isMissing(attIndex)) {
          if (!weka.core.Utils.isMissingValue(previousNonMissing)) {
            double newV = advanceSuppliedTimeValue(previousNonMissing, detected);
            current.setValue(attIndex, newV);
            // previousNonMissing = newV;
            if (missingTimeStampList != null) {
              missingTimeStampList.add(new Integer(i + 1));
            }
          }
        } else if (firstNonMissingIndex == -1) {
          firstNonMissingIndex = i;
          firstNonMissing = current.value(attIndex);
        }
        previousNonMissing = current.value(attIndex);
      }

      if (leadingMissingDates) {
        if (firstNonMissingIndex > 0) {
          for (int i = firstNonMissingIndex - 1; i >= 0; i--) {
            Instance current = result.instance(i);
            double newV = decrementSuppliedTimeValue(firstNonMissing, detected);
            current.setValue(attIndex, newV);
            if (missingTimeStampList != null) {
              missingTimeStampList.add(new Integer(i + 1));
            }
            firstNonMissing = newV;
          }
        }
      }
    }

    return result;
  }

  /**
   * Utility method to advance a supplied time value by one unit.
   * 
   * @param valueToAdvance the time value to advance
   * @param dateBasedPeriodicity the periodicity to use for data arithmetic
   * @return the advanced value or the original value if this lag maker is not
   *         adjusting for trends.
   * 
   */
  public static double advanceSuppliedTimeValue(double valueToAdvance,
      TSLagMaker.PeriodicityHandler dateBasedPeriodicity) {
    return advanceSuppliedTimeValue(valueToAdvance, dateBasedPeriodicity, false);
  }

  /**
   * Utility method to decrement a supplied time value by one unit.
   * 
   * @param valueToDecrement the time value to decrement
   * @param dateBasedPeriodicity the periodicity to use for data arithmetic
   * @return the advanced value or the original value if this lag maker is not
   *         adjusting for trends.
   * 
   */
  public static double decrementSuppliedTimeValue(double valueToDecrement,
      TSLagMaker.PeriodicityHandler dateBasedPeriodicity) {
    return advanceSuppliedTimeValue(valueToDecrement, dateBasedPeriodicity,
        true);
  }

  protected static double advanceSuppliedTimeValue(double valueToAdvance,
      TSLagMaker.PeriodicityHandler dateBasedPeriodicity, boolean decrement) {
    double result = valueToAdvance;
    int sign = (decrement) ? -1 : 1;

    // if (m_adjustForTrends) {
    // result = valueToAdvance + dateBasedPeriodicity.deltaTime();//
    // m_deltaTime;
    if (dateBasedPeriodicity.getPeriodicity() != Periodicity.UNKNOWN) {
      Date d = new Date((long) valueToAdvance);
      Calendar c = new GregorianCalendar();
      c.setTime(d);
      do {
        if (dateBasedPeriodicity.getPeriodicity() == Periodicity.YEARLY) {
          c.add(Calendar.YEAR, 1 * sign);
        } else if (dateBasedPeriodicity.getPeriodicity() == Periodicity.QUARTERLY) {
          c.add(Calendar.MONTH, 3 * sign);
        } else if (dateBasedPeriodicity.getPeriodicity() == Periodicity.MONTHLY) {
          c.add(Calendar.MONTH, 1 * sign);
        } else if (dateBasedPeriodicity.getPeriodicity() == Periodicity.WEEKLY) {
          c.add(Calendar.WEEK_OF_YEAR, 1 * sign);
        } else if (dateBasedPeriodicity.getPeriodicity() == Periodicity.DAILY) {
          c.add(Calendar.DAY_OF_YEAR, 1 * sign);
        } else if (dateBasedPeriodicity.getPeriodicity() == Periodicity.HOURLY) {
          c.add(Calendar.HOUR_OF_DAY, 1 * sign);
        }
        result = c.getTimeInMillis();

      } while (dateBasedPeriodicity.dateInSkipList(c.getTime()));
    } else {
      // just add the delta
      do {
        result += (dateBasedPeriodicity.deltaTime() * sign);
      } while (dateBasedPeriodicity.dateInSkipList(new Date((long) result)));
    }
    // }
    return result;
  }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy