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

weka.gui.beans.TimeSeriesForecasting 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 .
 */


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

package weka.gui.beans;

import java.awt.BorderLayout;
import java.beans.EventSetDescriptor;
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.ObjectInputStream;
import java.io.ObjectOutputStream;
import java.io.OutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.GZIPInputStream;
import java.util.zip.GZIPOutputStream;

import javax.swing.JPanel;

import org.apache.commons.codec.binary.Base64;

import weka.classifiers.evaluation.NumericPrediction;
import weka.classifiers.timeseries.AbstractForecaster;
import weka.classifiers.timeseries.WekaForecaster;
import weka.core.SerializationHelper;
import weka.filters.supervised.attribute.TSLagMaker;
import weka.core.Attribute;
import weka.core.DenseInstance;
import weka.core.Environment;
import weka.core.EnvironmentHandler;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Utils;
import weka.gui.Logger;

/**
 * KnowledgeFlow component for producing a forecast using a time series
 * forecasting model.
 * 
 * @author Mark hall (mhall{[at]}pentaho{[dot]}com)
 * @version $Revision: 52593 $
 * 
 */
@KFStep(category = "Time Series", toolTipText = "Forecasting using a time series model")
public class TimeSeriesForecasting extends JPanel implements BeanCommon,
    Visible, EventConstraints, EnvironmentHandler, InstanceListener,
    DataSourceListener {

  /**
   * For serialization
   */
  private static final long serialVersionUID = -7826178727365267059L;

  /** The structure of the data used to train the forecaster */
  protected transient Instances m_header;

  /** The forecaster to use for forecasting */
  protected transient WekaForecaster m_forecaster;

  /**
   * The output instances structure - typically the same as the input structure.
   * Will have additional attributes for upper and lower confidence intervals if
   * the forecaster produces them
   */
  protected transient Instances m_outgoingStructure;

  /**
   * The filename to load from - takes precendence over an encoded forecaster if
   * not null and not equal to "-NONE-"
   */
  protected String m_fileName = "-NONE-";

  /**
   * The file name to save the updated forecaster to if the user has opted to
   * rebuild the forecasting model on the incoming data
   */
  protected String m_saveFileName = "";

  /**
   * Base 64 encoded forecasting model - this allows the model to be embeded in
   * the XML knowledge flow file format rather than loaded from a file at
   * execution time.
   */
  protected String m_encodedForecaster = "-NONE-";

  /**
   * Number of future time steps to forecast - will be ignored if overlay data
   * is being used since the number of instances containing overlay values will
   * dictate the number of forecasted values that can be produced
   */
  protected String m_numberOfStepsToForecast = "1";

  /** True if the forecaster should be rebuilt on incoming data */
  protected boolean m_rebuildForecaster = false;

  /**
   * The number of time units beyond the end of the training data used to train
   * the forecaster that the most recent incoming priming instance is. This is
   * used to adjust the artificial time stamp (if one is being used) to the
   * right value before a forecast is produced
   */
  protected String m_artificialTimeStartOffset = "0";

  /** Logging object */
  protected transient Logger m_log;

  /** Environment variables */
  protected transient Environment m_env;

  /** Upstream component sending us data */
  protected Object m_listenee = null;

  /** Incoming connection type */
  protected String m_incomingConnection = "";

  /** Components listening to our output */
  protected ArrayList m_instanceListeners = new ArrayList();

  private static enum Status {
    BUSY, IDLE;
  }

  protected Status m_forecastingStatus = Status.IDLE;

  /**
   * Global about information for this component.
   * 
   * @return global information for this component
   */
  public String globalInfo() {
    return "Encapsulates a time series forecasting model and uses it to"
        + " produce forecasts given incoming historical data. Forecaster "
        + "can optionally be rebuilt using the incoming data before a "
        + "forecast is generated.";
  }

  /** Visual representation */
  protected BeanVisual m_visual = new BeanVisual("TimeSeriesForecasting",
      BeanVisual.ICON_PATH + "DefaultClassifier.gif", BeanVisual.ICON_PATH
          + "DefaultClassifier_animated.gif");

  /**
   * Constructor
   */
  public TimeSeriesForecasting() {
    setLayout(new BorderLayout());
    useDefaultVisual();
    add(m_visual, BorderLayout.CENTER);
  }

  /**
   * Use the default images for a data source
   * 
   */
  public void useDefaultVisual() {
    m_visual.loadIcons(BeanVisual.ICON_PATH + "DefaultClassifier.gif",
        BeanVisual.ICON_PATH + "DefaultClassifier_animated.gif");
  }

  /**
   * Set the logging object to use
   * 
   * @param logger the logging object to use
   */
  public void setLog(Logger logger) {
    m_log = logger;
  }

  /**
   * Get the forecaster. Loads the forecaster from a file (if necessary).
   * 
   * @return the forecasting model
   * @throws Exception if there is a problem loading the forecaster
   */
  public WekaForecaster getForecaster() throws Exception {
    if (m_forecaster != null) {
      return m_forecaster;
    } else {

      // try and decode the base64 string (if set)
      List model = getForecaster(m_encodedForecaster);
      if (model != null) {
        m_forecaster = (WekaForecaster) model.get(0);
        m_header = (Instances) model.get(1);
        return m_forecaster;
      }
    }

    return null;
  }

  /**
   * Decodes and returns a forecasting model (list containing the forecaster and
   * Instances object containing the structure of the data used to train the
   * forecaster) from a base 64 string.
   * 
   * @param base64encoded a List containing forecaster and header
   *          encoded as a base 64 string
   * 
   * @return the decoded List containing forecaster and header
   * @throws Exception if there is a problem decoding
   */
  public static List getForecaster(String base64encoded)
      throws Exception {
    if (base64encoded != null && base64encoded.length() > 0
        && !base64encoded.equals("-NONE-")) {

      byte[] decoded = decodeFromBase64(base64encoded);
      ByteArrayInputStream bis = new ByteArrayInputStream(decoded);
      ObjectInputStream ois = SerializationHelper.getObjectInputStream(bis);

      List model = (List) ois.readObject();
      ois.close();
      return model;
    }

    return null;
  }

  /**
   * Set the base 64 encoded forecaster.
   * 
   * @param encodedForecaster a base 64 encoded List containing the
   *          forecaster and header
   */
  public void setEncodedForecaster(String encodedForecaster) {
    m_encodedForecaster = encodedForecaster;
  }

  /**
   * Gets the base 64 encoded forecaster
   * 
   * @return a base 64 string encoding a List that contains the
   *         forecasting model and the header
   */
  public String getEncodedForecaster() {
    return m_encodedForecaster;
  }

  /**
   * Set the filename to load from.
   * 
   * @param filename the filename to load from
   */
  public void setFilename(String filename) {
    m_fileName = filename;
  }

  /**
   * Get the filename to load from.
   * 
   * @return the filename to load from.
   */
  public String getFilename() {
    return m_fileName;
  }

  /**
   * Set the name of the file to save the forecasting model out to if the user
   * has opted to rebuild the forecaster using the incoming data.
   * 
   * @param fileName the file name to save to.
   */
  public void setSaveFilename(String fileName) {
    m_saveFileName = fileName;
  }

  /**
   * Get the name of the file to save the forecasting model to if the user has
   * opted to rebuild the forecaster using the incoming data.
   * 
   * @return the name of the file to save the forecaster to.
   */
  public String getSaveFilename() {
    return m_saveFileName;
  }

  /**
   * Set whether the forecaster should be rebuilt/re-estimated on the incoming
   * data.
   * 
   * @param rebuild true if the forecaster should be rebuilt using the incoming
   *          data
   */
  public void setRebuildForecaster(boolean rebuild) {
    m_rebuildForecaster = rebuild;
  }

  /**
   * Get whether the forecaster will be rebuilt/re-estimated on the incoming
   * data.
   * 
   * @return true if the forecaster is to be rebuilt on the incoming data
   */
  public boolean getRebuildForecaster() {
    return m_rebuildForecaster;
  }

  /**
   * Returns true if, at this time, the object will accept a connection
   * according to the supplied EventSetDescriptor
   * 
   * @param esd the EventSetDescriptor
   * @return true if the object will accept a connection
   */
  public boolean connectionAllowed(EventSetDescriptor esd) {
    return connectionAllowed(esd.getName());
  }

  /**
   * Returns true if, at this time, the object will accept a connection with
   * respect to the named event
   * 
   * @param eventName the event
   * @return true if the object will accept a connection
   */
  public boolean connectionAllowed(String eventName) {
    if (m_listenee != null) {
      return false;
    }
    return true;
  }

  /**
   * Set the number of time steps to forecast beyond the end of the incoming
   * priming data. This will be ignored if the forecaster is using overlay data
   * as the number of instances for which overlay data is present (and targets
   * are missing) in the incoming data will determine how many forecasted values
   * are produced.
   * 
   * @param n the number of steps to forecast.
   */
  public void setNumStepsToForecast(String n) {
    m_numberOfStepsToForecast = n;
  }

  /**
   * Get the number of time steps to forecast beyond the end of the incoming
   * priming data. This will be ignored if the forecaster is using overlay data
   * as the number of instances for which overlay data is present (and targets
   * are missing) in the incoming data will determine how many forecasted values
   * are produced.
   * 
   * @return the number of steps to forecast.
   */
  public String getNumStepsToForecast() {
    return m_numberOfStepsToForecast;
  }

  /**
   * Set the offset, from the value associated with the last training instance,
   * for the artificial time stamp. Has no effect if an artificial time stamp is
   * not in use by the forecaster. If in use, this needs to be set so that the
   * forecaster knows what time stamp value corresponds to the first requested
   * forecast (i.e. it should be equal to the number of recent historical
   * priming instances that occur after the last training instance in time).
   * 
   * @param art the offset from the last artificial time value in the training
   *          data for which the forecast is requested.
   */
  public void setArtificialTimeStartOffset(String art) {
    m_artificialTimeStartOffset = art;
  }

  /**
   * Get the offset, from the value associated with the last training instance,
   * for the artificial time stamp. Has no effect if an artificial time stamp is
   * not in use by the forecaster. If in use, this needs to be set so that the
   * forecaster knows what time stamp value corresponds to the first requested
   * forecast (i.e. it should be equal to the number of recent historical
   * priming instances that occur after the last training instance in time).
   * 
   * @return the offset from the last artificial time value in the training data
   *         for which the forecast is requested.
   */
  public String getArtificialTimeStartOffset() {
    return m_artificialTimeStartOffset;
  }

  /**
   * Notify this object that it has been registered as a listener with a source
   * with respect to the named event
   * 
   * @param eventName the event
   * @param source the source with which this object has been registered as a
   *          listener
   */
  public void connectionNotification(String eventName, Object source) {
    if (connectionAllowed(eventName)) {
      m_listenee = source;
      m_incomingConnection = eventName;
    }
  }

  /**
   * Notify this object that it has been deregistered as a listener with a
   * source with respect to the supplied event name
   * 
   * @param eventName the event
   * @param source the source with which this object has been registered as a
   *          listener
   */
  public void disconnectionNotification(String eventName, Object source) {
    if (m_listenee == source) {
      m_listenee = null;
      m_incomingConnection = "";
    }
  }

  /**
   * Get the custom (descriptive) name for this bean (if one has been set)
   * 
   * @return the custom name (or the default name)
   */
  public String getCustomName() {
    return m_visual.getText();
  }

  /**
   * Returns true if. at this time, the bean is busy with some task.
   * 
   * @return true if the bean is busy.
   */
  public boolean isBusy() {
    return (m_forecastingStatus == Status.BUSY);
  }

  /**
   * Set a custom (descriptive) name for this bean
   * 
   * @param name the name to use
   */
  public void setCustomName(String name) {
    m_visual.setText(name);
  }

  /**
   * Stop the component from executing. Also attempts to tell the immediate
   * upstream component to stop.
   */
  public void stop() {
    m_forecastingStatus = Status.IDLE;
    // tell upstream component to stop
    if (m_listenee != null) {
      if (m_listenee instanceof BeanCommon) {
        ((BeanCommon) m_listenee).stop();
      }
    }
  }

  /**
   * Set environment variables to use.
   * 
   * @param env the environment variables to use
   */
  public void setEnvironment(Environment env) {
    m_env = env;
  }

  /**
   * Gets the visual appearance of this wrapper bean
   */
  public BeanVisual getVisual() {
    return m_visual;
  }

  /**
   * Sets the visual appearance of this wrapper bean
   * 
   * @param newVisual a BeanVisual value
   */
  public void setVisual(BeanVisual newVisual) {
    m_visual = newVisual;
  }

  /**
   * Encode the model and header into a base 64 string. A List
   * containing first the model and then the header is encoded.
   * 
   * @param model the forecasting model to encode
   * @param header empty instances object containing just the structure of the
   *          data used to train the forecaster
   * @return a base 64 encoded String
   * @throws Exception if a problem occurs.
   */
  public static String encodeForecasterToBase64(WekaForecaster model,
      Instances header) throws Exception {

    if (model != null && header != null) {
      List modelAndHeader = new ArrayList();
      modelAndHeader.add(model);
      modelAndHeader.add(header);

      ByteArrayOutputStream bao = new ByteArrayOutputStream();
      BufferedOutputStream bos = new BufferedOutputStream(bao);
      ObjectOutputStream oo = new ObjectOutputStream(bos);
      oo.writeObject(modelAndHeader);
      oo.flush();

      byte[] modelBytes = bao.toByteArray();

      return encodeToBase64(modelBytes);
    } else {
      throw new Exception("[TimeSeriesForecasting] unable to encode model!");
    }

  }

  protected List loadModel(String filename) {

    List loaded = new ArrayList();
    try {
      if (!isEmpty(filename) && !filename.equals("-NONE-")) {

        // replace any environment variables
        if (m_env == null) {
          m_env = Environment.getSystemWide();
        }

        String filenameN = filename;
        try {
          filenameN = m_env.substitute(filename);
        } catch (Exception e) {
          // quietly ignore
        }

        InputStream is = new FileInputStream(filenameN);
        if (filenameN.toLowerCase().endsWith(".gz")) {
          is = new GZIPInputStream(is);
        }
        ObjectInputStream ois = SerializationHelper.getObjectInputStream(is);
        WekaForecaster forecaster = (WekaForecaster) ois.readObject();

        Instances header = (Instances) ois.readObject();
        is.close();

        loaded.add(forecaster);
        loaded.add(header);
        return loaded;
      } else {
        logMessage("Model is null or no filename specified to load from!");
        return null;
      }
    } catch (Exception ex) {
      ex.printStackTrace();
      logMessage(ex.getMessage());
      return null;
    }
  }

  /**
   * Utility method to check if a String is null or empty ("").
   * 
   * @param aString the String to check.
   * @return true if the supplied String is null or empty.
   */
  protected static boolean isEmpty(String aString) {
    if (aString == null || aString.length() == 0) {
      return true;
    }
    return false;
  }

  /**
   * Logs message to the log or System.out if the log object is not set.
   * 
   * @param message the message to log
   */
  protected void logMessage(String message) {
    message = "[TimeSeriesForecasting] " + statusMessagePrefix() + message;
    if (m_log != null) {
      m_log.logMessage(message);
    } else {
      System.out.println(message);
    }
  }

  /**
   * Logs an error message to the log or System.err if the log object is not set
   * 
   * @param message the message to log
   * @param ex the exception that generated the error (may be null)
   */
  protected void logError(String message, Exception ex) {
    String exceptionMessage = (ex != null) ? ex.getMessage() : "";
    message = "[TimeSeriesForecasting] " + statusMessagePrefix() + " "
        + message + exceptionMessage;
    if (m_log != null) {
      m_log.logMessage(message);
    } else {
      System.err.println(message);
    }
  }

  /**
   * Prints a message to the status area of the log or to System.out if the log
   * object is not set.
   * 
   * @param message the message to output
   */
  protected void statusMessage(String message) {
    message = statusMessagePrefix() + message;
    if (m_log != null) {
      m_log.statusMessage(message);
    } else {
      System.out.println(message);
    }
  }

  /**
   * Prints a fixed error message to the status area of the log
   */
  protected void statusError() {
    String message = statusMessagePrefix() + "ERROR: see log for details";
    if (m_log != null) {
      m_log.statusMessage(message);
    } else {
      System.err.println(message);
    }
  }

  /**
   * Prints a warning message to the status area of the log
   * 
   * @param message the message to log
   */
  protected void statusWarning(String message) {
    message = statusMessagePrefix() + "WARNING: " + message;
    if (m_log != null) {
      m_log.statusMessage(message);
    } else {
      System.out.println(message);
    }
  }

  protected static final String encodeToBase64(byte[] val) throws IOException {
    String string;
    if (val == null) {
      string = null;
    } else {
      ByteArrayOutputStream baos = new ByteArrayOutputStream();
      GZIPOutputStream gzos = new GZIPOutputStream(baos);
      BufferedOutputStream bos = new BufferedOutputStream(gzos);
      bos.write(val);
      bos.flush();
      bos.close();

      string = new String(Base64.encodeBase64(baos.toByteArray()));
    }
    return string;
  }

  protected static final byte[] decodeFromBase64(String string)
      throws Exception {

    byte[] bytes;
    if (string == null) {
      bytes = new byte[] {};
    } else {
      bytes = Base64.decodeBase64(string.getBytes());
    }
    if (bytes.length > 0) {
      ByteArrayInputStream bais = new ByteArrayInputStream(bytes);
      GZIPInputStream gzip = new GZIPInputStream(bais);
      BufferedInputStream bi = new BufferedInputStream(gzip);
      byte[] result = new byte[] {};

      byte[] extra = new byte[1000000];
      int nrExtra = bi.read(extra);
      while (nrExtra >= 0) {
        // add it to bytes...
        //
        int newSize = result.length + nrExtra;
        byte[] tmp = new byte[newSize];
        for (int i = 0; i < result.length; i++)
          tmp[i] = result[i];
        for (int i = 0; i < nrExtra; i++)
          tmp[result.length + i] = extra[i];

        // change the result
        result = tmp;
        nrExtra = bi.read(extra);
      }
      bytes = result;
      gzip.close();
    }

    return bytes;
  }

  /**
   * Returns true, if at the current time, the named event could be generated.
   * Assumes that the supplied event name is an event that could be generated by
   * this bean
   * 
   * @param eventName the name of the event in question
   * @return true if the named event could be generated at this point in time
   */
  public boolean eventGeneratable(String eventName) {
    if (eventName.equals("instance") || eventName.equals("dataset")) {
      if (m_listenee == null) {
        return false;
      }
    }
    return true;
  }

  /** holds overlay data (if present in the incoming data) */
  protected transient Instances m_overlayData;

  /**
   * holds the incoming data for either priming the forecaster or for rebuilding
   * the forecaster
   */
  protected transient Instances m_bufferedPrimeData;

  /** true if the forecaster is using overlay data */
  protected transient boolean m_isUsingOverlayData;

  /** the lag maker in use by the forecaster */
  protected transient TSLagMaker m_modelLagMaker;

  /** name of the time stamp attribute */
  protected transient String m_timeStampName = "";

  /** the fields that the forecaster is predicting */
  protected transient List m_fieldsToForecast;

  private void loadOrDecodeForecaster() {
    // filename takes precedence over encoded forecaster (if any)
    if (!isEmpty(m_fileName) && !m_fileName.equals("-NONE-")) {
      List loaded = loadModel(m_fileName);
      if (loaded == null) {
        statusError();
        logError("problem loading forecasting model.", null);
        stop();
        return;
      } else {
        m_forecaster = (WekaForecaster) loaded.get(0);
        m_header = (Instances) loaded.get(1);
      }
    } else if (m_encodedForecaster != null && m_encodedForecaster.length() > 0
        && !m_encodedForecaster.equals("-NONE-")) {
      try {
        getForecaster();
      } catch (Exception ex) {
        ex.printStackTrace();
        statusError();
        logError("a problem occurred while decoding the model. See the "
            + "log for details.", ex);
        stop();
        return;
      }
    } else {
      statusError();
      logError("unable to obtain a forecasting model to use.", null);
      stop();
      return;
    }
  }

  /**
   * Accept an incoming instance
   * 
   * @param e incoming InstanceEvent encapsulating the instance to process.
   */
  public void acceptInstance(InstanceEvent e) {

    int status = e.getStatus();
    if (status == InstanceEvent.FORMAT_AVAILABLE) {
      Instances dataset = e.getStructure();

      loadOrDecodeForecaster();

      // check the structure
      if (!m_header.equalHeaders(dataset)) {
        statusError();
        logError("incoming instances structure does not match the structure "
            + "of the data used to train the forecaster.", null);
        stop();
        return;
      }

      m_forecastingStatus = Status.BUSY;
      processInstance(null, true);

      return;
    }

    // get the instance, process it
    processInstance(e.getInstance(), false);

    if (status == InstanceEvent.BATCH_FINISHED) {
      processInstance(null, false); // finished

      // generate forecast
      generateForecast();
      m_forecastingStatus = Status.IDLE;
    }
  }

  protected void processInstance(Instance toProcess, boolean first) {

    if (first) {
      m_overlayData = null;
      m_bufferedPrimeData = null;
      statusMessage("configuring forecaster...");

      m_modelLagMaker = m_forecaster.getTSLagMaker();
      if (!m_modelLagMaker.isUsingAnArtificialTimeIndex()
          && m_modelLagMaker.getAdjustForTrends()) {
        m_timeStampName = m_modelLagMaker.getTimeStampField();
      }

      m_isUsingOverlayData = m_forecaster.isUsingOverlayData();

      if (!m_rebuildForecaster) {
        // m_isIncrementallyPrimeable = true;
        logMessage("forecaster will be primed " + "incrementally.");

        // first reset lag histories
        try {
          m_forecaster.primeForecaster(new Instances(m_header, 0));
        } catch (Exception ex) {
          ex.printStackTrace();
          statusError();
          logError("problem during initialization of the " + "priming data.",
              ex);
          stop();
          return;
        }
      } else {
        logMessage("forecaster will be "
            + "rebuilt/re-estimated on incoming data");
      }

      if (m_isUsingOverlayData) {
        logMessage("forecaster is using "
            + "overlay data. We expect to see overlay attribute "
            + "values for the forecasting period.");
        m_overlayData = new Instances(m_header, 0);
      }

      if (m_rebuildForecaster) {
        m_bufferedPrimeData = new Instances(m_header, 0);
      }

      m_fieldsToForecast = AbstractForecaster.stringToList(m_forecaster
          .getFieldsToForecast());

      // notify instance listeners of the structure (same as incoming
      // but may have confidence bounds attributes added)
      m_outgoingStructure = new Instances(m_header);
      if (m_forecaster.isProducingConfidenceIntervals()) {
        ArrayList atts = new ArrayList();
        for (int i = 0; i < m_header.numAttributes(); i++) {
          atts.add((Attribute) m_header.attribute(i).copy());
        }
        for (String f : m_fieldsToForecast) {
          Attribute lb = new Attribute(f + "_lowerBound");
          Attribute ub = new Attribute(f + "_upperBound");
          atts.add(lb);
          atts.add(ub);
        }
        m_outgoingStructure = new Instances(m_header.relationName() + "_"
            + "plus_forecast", atts, 0);
      }
      InstanceEvent ie = new InstanceEvent(this, m_outgoingStructure);
      // notify all listeners
      notifyInstanceListeners(ie);

    } else if (toProcess == null) {
      // no more input. rebuild forecaster if necessary
      if (m_rebuildForecaster && m_bufferedPrimeData.numInstances() > 0) {

        // push out the historical data first
        for (int i = 0; i < m_bufferedPrimeData.numInstances(); i++) {
          InstanceEvent ie = new InstanceEvent(this,
              m_bufferedPrimeData.instance(i), InstanceEvent.INSTANCE_AVAILABLE);
          // notify listeners here
          notifyInstanceListeners(ie);
        }

        // rebuild the forecaster
        try {
          statusMessage("rebuilding the forecasting model...");
          logMessage("rebuilding the forecasting model.");
          m_forecaster.buildForecaster(m_bufferedPrimeData);

          statusMessage("priming the forecasting model...");
          logMessage("priming the forecasting model.");
          // prime the forecaster
          m_forecaster.primeForecaster(m_bufferedPrimeData);
        } catch (Exception e) {
          e.printStackTrace();
          statusError();
          logError("a problem occurred when rebuilding the forecaster", e);
          stop();
        }
      }

      if (m_rebuildForecaster && !isEmpty(m_saveFileName)) {
        // save the forecaster
        String saveName = m_saveFileName;
        if (m_env == null) {
          m_env = Environment.getSystemWide();
        }

        try {
          saveName = m_env.substitute(saveName);
        } catch (Exception ex) {
          // quietly ignore
        }

        statusMessage("Saving rebuilt forecasting model...");
        logMessage("Saving rebuild forecasting model to \"" + saveName + "\"");
        try {
          OutputStream os = new FileOutputStream(saveName);
          if (saveName.toLowerCase().endsWith(".gz")) {
            os = new GZIPOutputStream(os);
          }
          ObjectOutputStream oos = new ObjectOutputStream(
              new BufferedOutputStream(os));
          oos.writeObject(m_forecaster);
          oos.writeObject(m_header);
          oos.close();
        } catch (IOException e) {
          statusError();
          logError("a problem occurred when trying to save rebuilt model.", e);
          stop();
        }
      }

    } else {
      // if we are expecting overlay data, then check this instance to see if
      // all
      // target values predicted by the forecaster are missing. If so, then this
      // *might* indicate the start of the overlay data. We will start buffering
      // instances into the overlay buffer. If we get an instace with all
      // non-missing targets
      // at some future point then we will flush the overlay buffer either into
      // the
      // forecaster as priming instances (if forecaster is incrementally
      // primeable)
      // or into the buffered prime/training data if forecaster is not
      // incrementally
      // primeable or we are rebuilding/re-estimating the model

      if (m_isUsingOverlayData) {
        boolean allMissing = true;
        for (String field : m_fieldsToForecast) {
          if (!toProcess.isMissing(m_header.attribute(field))) {
            allMissing = false;
            break;
          }
        }

        if (allMissing) {
          // add it to the overlay buffer
          m_overlayData.add(toProcess);
          statusMessage("buffering overlay instance");
        } else {
          // check the overlay buffer - if it's not empty then flush it
          // into either the forecaster directly (if incrementally primeable)
          // or into the priming buffer
          if (m_overlayData.numInstances() > 0) {
            // first buffer this one (will get flushed anyway)
            m_overlayData.add(toProcess);
            logMessage("encountered a supposed "
                + "overlay instance with non-missing target values - "
                + "converting buffered overlay data into "
                + (m_rebuildForecaster ? "training" : "priming") + " data...");
            statusMessage("flushing overlay buffer.");

            for (int i = 0; i < m_overlayData.numInstances(); i++) {
              if (!m_rebuildForecaster) {
                try {
                  m_forecaster.primeForecasterIncremental(m_overlayData
                      .instance(i));

                  // output this instance immediately (make sure that we include
                  // any attributes for confidence intervals - these will be
                  // necessarily missing for historical instances)
                  Instance outgoing = convertToOutputFormat(m_overlayData
                      .instance(i));
                  InstanceEvent ie = new InstanceEvent(this, outgoing,
                      InstanceEvent.INSTANCE_AVAILABLE);
                  // notify listeners here
                  notifyInstanceListeners(ie);
                } catch (Exception e) {
                  e.printStackTrace();
                  statusError();
                  logError("problem occurred during priming.", e);
                  stop();
                  return;
                }

              } else {
                // transfer to the priming buffer
                m_bufferedPrimeData.add(m_overlayData.instance(i));
              }
            }

            // clear out the overlay data
            m_overlayData = new Instances(m_header, 0);
          } else {
            // not all missing and overlay buffer is empty then it's a priming
            // instance

            // either buffer it or send it directly to the forecaster (if
            // incrementally
            // primeable
            if (!m_rebuildForecaster) {
              try {
                m_forecaster.primeForecasterIncremental(toProcess);

                // output this instance immediately (make sure that we include
                // any attributes for confidence intervals - these will be
                // necessarily missing for historical instances)
                Instance outgoing = convertToOutputFormat(toProcess);
                InstanceEvent ie = new InstanceEvent(this, outgoing,
                    InstanceEvent.INSTANCE_AVAILABLE);
                // tell listeners
                notifyInstanceListeners(ie);
              } catch (Exception ex) {
                ex.printStackTrace();
                statusError();
                logError("problem occurred during priming.", ex);
                stop();
                return;
              }
            } else {
              // buffer
              m_bufferedPrimeData.add(toProcess);
            }
          }

        }
      } else {
        // not using overlay data
        if (!m_rebuildForecaster) {
          try {
            m_forecaster.primeForecasterIncremental(toProcess);

            // output this instance immediately (make sure that we include
            // any attributes for confidence intervals - these will be
            // necessarily missing for historical instances)
            Instance outgoing = convertToOutputFormat(toProcess);
            InstanceEvent ie = new InstanceEvent(this, outgoing,
                InstanceEvent.INSTANCE_AVAILABLE);
            notifyInstanceListeners(ie);
          } catch (Exception ex) {
            ex.printStackTrace();
            statusError();
            logError("problem occurred during priming.", ex);
            stop();
            return;
          }
        } else {
          // buffer
          m_bufferedPrimeData.add(toProcess);
        }
      }
    }
  }

  /**
   * Accept an incoming data set
   * 
   * @param dse incoming DataSetEvent encapsulating the instances to process
   */
  public void acceptDataSet(DataSetEvent dse) {

    loadOrDecodeForecaster();

    Instances data = dse.getDataSet();
    if (!m_header.equalHeaders(data)) {
      statusError();
      logError(
          "Incoming instance structure does not match what the forecaster "
              + "was trained with", null);
      return;
    }

    if (dse.isStructureOnly()) {
      return;
    }

    m_forecastingStatus = Status.BUSY;
    processInstance(null, true);

    for (int i = 0; i < data.numInstances(); i++) {
      processInstance(data.instance(i), false);
    }

    processInstance(null, false); // finished

    // generate forecast
    generateForecast();

    m_forecastingStatus = Status.IDLE;
  }

  private void generateForecast() {

    // doesn't matter if we're not using a time stamp
    double lastTimeFromPrime = -1;

    if (m_modelLagMaker.getAdjustForTrends()
        && m_modelLagMaker.getTimeStampField() != null
        && m_modelLagMaker.getTimeStampField().length() > 0
        && !m_modelLagMaker.isUsingAnArtificialTimeIndex()) {
      try {
        lastTimeFromPrime = m_modelLagMaker.getCurrentTimeStampValue();
      } catch (Exception ex) {
        statusError();
        logError("a problem occurred while establishing the current "
            + "time stamp value", ex);
        stop();
        return;
      }
    } else if (m_modelLagMaker.getAdjustForTrends()
        && m_modelLagMaker.isUsingAnArtificialTimeIndex()) {

      // If an artificial time stamp is in use then we need to set the
      // initial value to whatever offset from training that the user has
      // indicated to be the first forecasted point.
      try {
        String artOff = m_artificialTimeStartOffset;
        if (m_env != null) {
          artOff = m_env.substitute(artOff);
        }
        double artificialStartValue = m_modelLagMaker
            .getArtificialTimeStartValue();
        artificialStartValue += Integer.parseInt(artOff);
        m_modelLagMaker.setArtificialTimeStartValue(artificialStartValue);
      } catch (Exception ex) {
        ex.printStackTrace();
        statusError();
        logError("unable to set the value of the artificial time stamp.", ex);
        stop();
        return;
      }
    }

    boolean overlay = (m_overlayData != null
        && m_overlayData.numInstances() > 0 && m_isUsingOverlayData);

    String numS = m_numberOfStepsToForecast;
    if (m_env != null) {
      try {
        numS = m_env.substitute(numS);
      } catch (Exception e) {
        // quietly ignore
        // e.printStackTrace();
      }
    }

    int numSteps = (overlay) ? m_overlayData.numInstances() : Integer
        .parseInt(numS);

    List> forecast = null;

    // TODO adapt the log to PrintStream for the forecasting methods
    try {
      if (overlay) {
        forecast = m_forecaster.forecast(numSteps, m_overlayData);
      } else {
        forecast = m_forecaster.forecast(numSteps);
      }
    } catch (Exception ex) {
      ex.printStackTrace();
      statusError();
      logError("unable to generate a forecast.", ex);
      stop();
      return;
    }

    // now convert the forecast into instances. If we have overlay
    // data then we can just fill in the forecasted values (and
    // potentially add for confidence intervals)
    double time = lastTimeFromPrime;
    int timeStampIndex = -1;
    if (m_timeStampName.length() > 0) {
      Attribute timeStampAtt = m_outgoingStructure.attribute(m_timeStampName);
      if (timeStampAtt == null) {
        statusError();
        logError("couldn't find time stamp: " + m_timeStampName
            + "in the input data", null);
        stop();
        return;
      }
      timeStampIndex = timeStampAtt.index();
    }

    statusMessage("Generating forecast...");
    logMessage("Generating forecast.");
    for (int i = 0; i < numSteps; i++) {
      Instance outputI = null;
      double[] outVals = new double[m_outgoingStructure.numAttributes()];
      for (int j = 0; j < outVals.length; j++) {
        if (overlay) {
          outVals[j] = m_overlayData.instance(i).value(j);
        } else {
          outVals[j] = Utils.missingValue();
        }
      }
      List predsForStep = forecast.get(i);

      if (timeStampIndex != -1) {
        // set time value
        time = m_modelLagMaker.advanceSuppliedTimeValue(time);
        outVals[timeStampIndex] = time;
      }

      for (int j = 0; j < m_fieldsToForecast.size(); j++) {
        String target = m_fieldsToForecast.get(j);
        int targetI = m_outgoingStructure.attribute(target).index();
        NumericPrediction predForTargetAtStep = predsForStep.get(j);
        double y = predForTargetAtStep.predicted();
        double yHigh = y;
        double yLow = y;
        double[][] conf = predForTargetAtStep.predictionIntervals();
        if (!Utils.isMissingValue(y)) {
          outVals[targetI] = y;
        }

        // any confidence bounds?
        if (conf.length > 0) {
          yLow = conf[0][0];
          yHigh = conf[0][1];
          int indexOfLow = m_outgoingStructure
              .attribute(target + "_lowerBound").index();
          int indexOfHigh = m_outgoingStructure.attribute(
              target + "_upperBound").index();
          outVals[indexOfLow] = yLow;
          outVals[indexOfHigh] = yHigh;
        }
      }
      outputI = new DenseInstance(1.0, outVals);
      outputI.setDataset(m_outgoingStructure);

      // notify listeners of output instance
      InstanceEvent ie = new InstanceEvent(this, outputI,
          InstanceEvent.INSTANCE_AVAILABLE);

      if (i == (numSteps - 1)) {
        ie.setStatus(InstanceEvent.BATCH_FINISHED);
      }
      notifyInstanceListeners(ie);
    }
    statusMessage("Finished.");
    logMessage("Finished. Generated " + numSteps + " forecasted values.");
  }

  private String statusMessagePrefix() {
    return getCustomName()
        + "$"
        + hashCode()
        + "|"
        + ((Utils.joinOptions(m_forecaster.getOptions()).length() > 0) ? Utils
            .joinOptions(m_forecaster.getOptions()) + "|" : "");
  }

  private Instance convertToOutputFormat(Instance incoming) {

    Instance output = (Instance) incoming.copy();
    if (m_forecaster.isProducingConfidenceIntervals()) {
      double[] values = new double[incoming.numAttributes()
          + (m_fieldsToForecast.size() * 2)];
      for (int i = 0; i < incoming.numAttributes(); i++) {
        values[i] = incoming.value(i);
      }

      // set all bounds to missing (initially)
      for (int i = incoming.numAttributes(); i < incoming.numAttributes()
          + (m_fieldsToForecast.size() * 2); i++) {
        values[i] = Utils.missingValue();
      }

      output = new DenseInstance(1.0, values);
    }
    output.setDataset(m_outgoingStructure);

    return output;
  }

  private void notifyInstanceListeners(InstanceEvent e) {
    ArrayList l = null;
    synchronized (this) {
      l = (ArrayList) m_instanceListeners.clone();
    }
    if (l.size() > 0) {
      for (int i = 0; i < l.size(); i++) {
        l.get(i).acceptInstance(e);
      }
    }
  }

  /**
   * Add an listener to be notified of outgoing InstanceEvents
   * 
   * @param l the listener to register to receive outging instance events
   */
  public synchronized void addInstanceListener(InstanceListener l) {
    m_instanceListeners.add(l);
  }

  /**
   * Deregister and remove a listener of InstanceEvents
   * 
   * @param l the listener to remove.
   */
  public synchronized void removeInstanceListener(InstanceListener l) {
    m_instanceListeners.remove(l);
  }
}