weka.gui.beans.TimeSeriesForecasting.java Source code

Java tutorial

Introduction

Here is the source code for weka.gui.beans.TimeSeriesForecasting.java

Source

/*
 * Copyright (c) 2011 Pentaho Corporation.  All rights reserved. 
 * This software was developed by Pentaho Corporation and is provided under the terms 
 * of the GNU Lesser General Public License, Version 2.1. You may not use 
 * this file except in compliance with the license. If you need a copy of the license, 
 * please go to http://www.gnu.org/licenses/lgpl-2.1.txt. The Original Code is Time Series 
 * Forecasting.  The Initial Developer is Pentaho Corporation.
 *
 * Software distributed under the GNU Lesser Public License is distributed on an "AS IS" 
 * basis, WITHOUT WARRANTY OF ANY KIND, either express or  implied. Please refer to 
 * the license for the specific language governing your rights and limitations.
 */

/*
 *    TimeSeriesForecasting.java
 *    Copyright (C) 2011 Pentaho Corporation
 *
 */

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.classifiers.timeseries.core.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: 49983 $
 * 
 */
@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<InstanceListener> m_instanceListeners = new ArrayList<InstanceListener>();

    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<Object> 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<Object> containing forecaster and header
     *          encoded as a base 64 string
     * 
     * @return the decoded List<Object> containing forecaster and header
     * @throws Exception if there is a problem decoding
     */
    public static List<Object> 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 = new ObjectInputStream(bis);

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

        return null;
    }

    /**
     * Set the base 64 encoded forecaster.
     * 
     * @param encodedForecaster a base 64 encoded List<Object> 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<Object> 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 arg0) {
        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 <code>BeanVisual</code> value
     */
    public void setVisual(BeanVisual newVisual) {
        m_visual = newVisual;
    }

    /**
     * Encode the model and header into a base 64 string. A List<Object>
     * 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<Object> modelAndHeader = new ArrayList<Object>();
            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<Object> loadModel(String filename) {

        List<Object> loaded = new ArrayList<Object>();
        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 = new ObjectInputStream(new BufferedInputStream(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<String> m_fieldsToForecast;

    private void loadOrDecodeForecaster() {
        // filename takes precedence over encoded forecaster (if any)
        if (!isEmpty(m_fileName) && !m_fileName.equals("-NONE-")) {
            List<Object> 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<Attribute> atts = new ArrayList<Attribute>();
                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<List<NumericPrediction>> 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<NumericPrediction> 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<InstanceListener> l = null;
        synchronized (this) {
            l = (ArrayList<InstanceListener>) 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);
    }
}