/* * Copyright (c) 2010 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. */ /* * ForecastingPanel.java * Copyright (C) 2010 Pentaho Corporation */ package weka.classifiers.timeseries.gui; import java.awt.BorderLayout; import java.awt.Dimension; import java.awt.Font; import java.awt.GridLayout; import java.awt.Point; import java.awt.event.ActionEvent; import java.awt.event.ActionListener; import java.awt.event.InputEvent; import java.awt.event.MouseAdapter; import java.awt.event.MouseEvent; import java.awt.event.WindowAdapter; import java.awt.event.WindowEvent; import java.io.File; import java.io.FileInputStream; import java.io.FileOutputStream; import java.io.ObjectInputStream; import java.io.ObjectOutputStream; import java.io.PrintStream; import java.text.SimpleDateFormat; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.List; import javax.swing.BorderFactory; import javax.swing.JButton; import javax.swing.JCheckBox; import javax.swing.JComponent; import javax.swing.JFileChooser; import javax.swing.JFrame; import javax.swing.JMenuItem; import javax.swing.JOptionPane; import javax.swing.JPanel; import javax.swing.JPopupMenu; import javax.swing.JScrollPane; import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JTextArea; import javax.swing.JViewport; import javax.swing.ListSelectionModel; import javax.swing.event.ChangeEvent; import javax.swing.event.ChangeListener; import javax.swing.event.ListSelectionEvent; import javax.swing.event.ListSelectionListener; import javax.swing.event.TableModelEvent; import javax.swing.event.TableModelListener; import javax.swing.table.TableModel; import weka.classifiers.timeseries.AbstractForecaster; import weka.classifiers.timeseries.TSForecaster; import weka.classifiers.timeseries.WekaForecaster; import weka.classifiers.timeseries.core.OverlayForecaster; import weka.classifiers.timeseries.core.TSLagMaker; import weka.classifiers.timeseries.core.TSLagUser; import weka.classifiers.timeseries.eval.TSEvaluation; import weka.classifiers.timeseries.eval.graph.GraphDriver; import weka.core.Attribute; import weka.core.Instance; import weka.core.Instances; import weka.core.Utils; import weka.gui.BrowserHelper; import weka.gui.LogPanel; import weka.gui.Logger; import weka.gui.ResultHistoryPanel; import weka.gui.TaskLogger; import weka.gui.WekaTaskMonitor; import weka.gui.beans.KnowledgeFlowApp; import weka.gui.beans.TimeSeriesForecasting; import weka.gui.beans.TimeSeriesForecastingKFPerspective; /** * Main GUI panel for the forecasting environment. * * @author Mark Hall (mhall{[at]}pentaho{[dot]}com) * @version $Revision: 49983 $ */ public class ForecastingPanel extends JPanel { /** For serialization */ private static final long serialVersionUID = -8415151090793037265L; /** The training instances to operate on */ protected Instances m_instances; /** Panel for logging */ protected Logger m_log;// = new LogPanel(new WekaTaskMonitor()); /** The simple configuration panel */ protected SimpleConfigPanel m_simpleConfigPanel; /** The advanced configuration panel */ protected AdvancedConfigPanel m_advancedConfigPanel; /** The current forecaster */ protected WekaForecaster m_forecaster = new WekaForecaster(); /** Tabbed pane to hold the simple and advanced config panels */ protected JTabbedPane m_configPane = new JTabbedPane(); /** The main textual output area */ protected JTextArea m_outText = new JTextArea(20, 50); /** A panel holding and controlling the results for viewing */ protected ResultHistoryPanel m_history = new ResultHistoryPanel(m_outText); /** Button to launch the forecaster */ protected JButton m_startBut = new JButton("Start"); /** Button to stop processing */ protected JButton m_stopBut = new JButton("Stop"); /** Button to visit help docs in browser */ protected JButton m_helpBut = new JButton("Help"); /** The split panel to divided the history/results area from the configuration */ protected JSplitPane m_splitP; protected Thread m_runThread; /** True if we are running in the KnowledgeFlow as a perspective */ protected boolean m_isRunningAsPerspective = false; /** The file chooser for selecting model files. */ protected JFileChooser m_fileChooser = new JFileChooser(new File(System.getProperty("user.dir"))); /** * For each dataset, perform a check (if a timestamp is specified) * just once to see if it is in ascending order */ protected boolean m_sortedCheck; /** * Tabbed pane that holds the main text output plus tabs for * any generated graphs */ JTabbedPane m_outputPane = new JTabbedPane(); /** * Inner class extending Thread. Executes a forecasting task */ protected class ForecastingThread extends Thread { protected boolean m_configAndBuild = true; protected WekaForecaster m_threadForecaster = null; protected String m_name; public ForecastingThread(WekaForecaster forecaster, String name) { m_threadForecaster = forecaster; m_name = name; } public void setConfigureAndBuild(boolean configAndBuild) { m_configAndBuild = configAndBuild; } public void run() { LogPrintStream logger = new LogPrintStream(); logger.println("Setting up..."); String name = m_name; StringBuffer outBuff = null; if (name == null) { name = (new SimpleDateFormat("HH:mm:ss - ")).format(new Date()); outBuff = new StringBuffer(); } String fname = ""; try { if (!m_sortedCheck) { sortCheck(); } // copy the current state of things Instances inst = new Instances(m_instances); TSEvaluation eval = new TSEvaluation(inst, m_advancedConfigPanel.getHoldoutSetSize()); if (m_configAndBuild) { // configure the WekaForecaster with the base // learner m_threadForecaster.setBaseForecaster(m_advancedConfigPanel.getBaseClassifier()); m_simpleConfigPanel.applyToForecaster(m_threadForecaster); m_advancedConfigPanel.applyToForecaster(m_threadForecaster); } m_simpleConfigPanel.applyToEvaluation(eval, m_threadForecaster); m_advancedConfigPanel.applyToEvaluation(eval, m_threadForecaster); eval.setForecastFuture(m_advancedConfigPanel.getOutputFuturePredictions() || m_advancedConfigPanel.getGraphFuturePredictions()); if (m_threadForecaster instanceof OverlayForecaster && ((OverlayForecaster)m_threadForecaster).isUsingOverlayData()) { if (!eval.getEvaluateOnTestData() && (m_advancedConfigPanel.m_outputFutureCheckBox.isSelected() || m_advancedConfigPanel.m_graphFutureCheckBox.isSelected())) { // warn the user that future forecast can't be produced for the training data dontShowMessageDialog("weka.classifiers.timeseries.gui.CantFutureForecastTraining", "Unable to generate a future forecast beyond the end of the training\n" + "data because there is no future overlay data available. Use a holdout\n" + "set for evaluation in order to simulate having \"future\" overlay\n" + "data available.\n\n", "ForecastingPanel"); } if (eval.getEvaluateOnTestData() && (m_advancedConfigPanel.m_outputFutureCheckBox.isSelected() || m_advancedConfigPanel.m_graphFutureCheckBox.isSelected())) { // warn the user that future forecast can't be produced for the test data dontShowMessageDialog("weka.classifiers.timeseries.gui.CantFutureForecastTesting", "Unable to generate a future forecast beyond the end of the test\n" + "data because there is no future overlay data available.\n\n", "ForecastingPanel"); } } fname = m_threadForecaster.getAlgorithmName(); if (m_name == null) { String algoName = fname.substring(0, fname.indexOf(' ')); if (algoName.startsWith("weka.classifiers.")) { name += algoName.substring("weka.classifiers.".length()); } else { name += algoName; } } String lagOptions = ""; if (m_threadForecaster instanceof TSLagUser) { TSLagMaker lagMaker = ((TSLagUser)m_threadForecaster).getTSLagMaker(); lagOptions = Utils.joinOptions(lagMaker.getOptions()); } if (lagOptions.length() > 0 && m_name == null) { name += " [" + lagOptions + "]"; } if (m_name == null) { outBuff.append("=== Run information ===\n\n"); } else { outBuff = m_history.getNamedBuffer(name); outBuff.append("\n=== Model re-evaluation===\n\n"); } outBuff.append("Scheme:\n\t" + fname).append("\n\n"); if (lagOptions.length() > 0) { outBuff.append("Lagged and derived variable options:\n\t"). append(lagOptions + "\n\n"); } outBuff.append("Relation: " + inst.relationName() + '\n'); outBuff.append("Instances: " + inst.numInstances() + '\n'); outBuff.append("Attributes: " + inst.numAttributes() + '\n'); if (inst.numAttributes() < 100) { for (int i = 0; i < inst.numAttributes(); i++) { outBuff.append(" " + inst.attribute(i).name() + '\n'); } } else { outBuff.append(" [list of attributes omitted]\n"); } if (m_configAndBuild) { m_history.addResult(name, outBuff); } m_history.setSingle(name); if (m_log != null) { m_log.logMessage("Started " + fname); if (m_configAndBuild) { logger.println("Training forecaster..."); } if (m_log instanceof TaskLogger) { ((TaskLogger)m_log).taskStarted(); } } Instances trainInst = eval.getTrainingData(); if (m_configAndBuild) { m_threadForecaster.buildForecaster(trainInst, logger); outBuff.append("\n" + m_threadForecaster.toString()); m_history.updateResult(name); } if (eval.getEvaluateOnTrainingData() || eval.getEvaluateOnTestData()) { logger.println("Evaluating..."); } // evaluate the forecaster eval.evaluateForecaster(m_threadForecaster, false, logger); // output any predictions if (m_advancedConfigPanel.getOutputPredictionsAtStep() > 0) { int step = m_advancedConfigPanel.getOutputPredictionsAtStep(); String targetName = m_advancedConfigPanel.getOutputPredictionsTarget(); String fieldsToForecast = m_threadForecaster.getFieldsToForecast(); if (!fieldsToForecast.contains(targetName)) { throw new Exception("Cannot output predictions for \"" + targetName + "\" because that field is not being predicted."); } if (eval.getTrainingData() != null && eval.getEvaluateOnTrainingData()) { String predString = eval.printPredictionsForTrainingData("=== Predictions " + "for training data: " + targetName + " (" + step + (step > 1 ? "-steps ahead)" : "-step ahead)") + " ===", targetName, step, eval.getPrimeWindowSize()); outBuff.append("\n").append(predString); } if (eval.getTestData() != null) { int instanceNumOffset = (eval.getTrainingData() != null && m_advancedConfigPanel.getHoldoutSetSize() > 0) ? eval.getTrainingData().numInstances() : 0; String predString = eval.printPredictionsForTestData("=== Predictions " + "for test data: " + targetName + " (" + step + (step > 1 ? "-steps ahead)" : "-step ahead)") + " ===", targetName, step, instanceNumOffset); outBuff.append("\n").append(predString); } m_history.updateResult(name); } // output any future predictions if (m_advancedConfigPanel.getOutputFuturePredictions()) { if (eval.getTrainingData() != null /*&& eval.getEvaluateOnTrainingData()*/) { outBuff.append("\n=== Future predictions from end of training data ===\n"); outBuff.append(eval.printFutureTrainingForecast(m_threadForecaster)); } if (eval.getTestData() != null && eval.getEvaluateOnTestData()) { outBuff.append("\n=== Future predictions from end of test data ===\n"); outBuff.append(eval.printFutureTestForecast(m_threadForecaster)); } m_history.updateResult(name); } // evaluation summary if (eval.getEvaluateOnTrainingData() || eval.getEvaluateOnTestData()) { outBuff.append("\n" + eval.toSummaryString()); m_history.updateResult(name); } // result object list List<Object> resultList = (m_configAndBuild) ? new ArrayList<Object>() : (List<Object>)m_history.getNamedObject(name); if (!m_configAndBuild) { // go through and remove any JPanels List<Object> newResultList = new ArrayList<Object>(); for (int z = 0; z < resultList.size(); z++) { if (resultList.get(z) instanceof TSForecaster || resultList.get(z) instanceof Instances) { newResultList.add(resultList.get(z)); } } resultList = newResultList; } // handle graphs List<JPanel> graphList = new ArrayList<JPanel>(); // graph predictions for targets at specific step if (m_advancedConfigPanel.getGraphPredictionsAtStep() > 0) { int stepNum = m_advancedConfigPanel.getGraphPredictionsAtStep(); List<String> targets = AbstractForecaster.stringToList(m_threadForecaster.getFieldsToForecast()); if (eval.getTrainingData() != null && eval.getEvaluateOnTrainingData()) { JPanel trainTargetsAtStep = eval.graphPredictionsForTargetsOnTraining(GraphDriver.getDefaultDriver(), m_threadForecaster, targets, stepNum, eval.getPrimeWindowSize()); trainTargetsAtStep.setToolTipText("Train pred. for targets"); graphList.add(trainTargetsAtStep); } if (eval.getTestData() != null && eval.getEvaluateOnTestData()) { int instanceOffset = (eval.getPrimeForTestDataWithTestData()) ? eval.getPrimeWindowSize() : 0; JPanel testTargetsAtStep = eval.graphPredictionsForTargetsOnTesting(GraphDriver.getDefaultDriver(), m_threadForecaster, targets, stepNum, instanceOffset); testTargetsAtStep.setToolTipText("Test pred. for targets"); graphList.add(testTargetsAtStep); } } // graph predictions for specific target at selected steps if (m_advancedConfigPanel.getGraphTargetForSteps()) { String fieldsToForecast = m_threadForecaster.getFieldsToForecast(); String selectedTarget = m_advancedConfigPanel.getGraphTargetForStepsTarget(); if (!fieldsToForecast.contains(selectedTarget)) { throw new Exception("Cannot graph predictions for \"" + selectedTarget + "\" because that field is not being predicted."); } List<Integer> stepList = m_advancedConfigPanel.getGraphTargetForStepsStepList(); if (eval.getTrainingData() != null && eval.getEvaluateOnTrainingData()) { JPanel trainStepsForTarget = eval.graphPredictionsForStepsOnTraining(GraphDriver.getDefaultDriver(), m_threadForecaster, selectedTarget, stepList, eval.getPrimeWindowSize()); trainStepsForTarget.setToolTipText("Train pred. at steps"); graphList.add(trainStepsForTarget); } if (eval.getTestData() != null && eval.getEvaluateOnTestData()) { int instanceOffset = (eval.getPrimeForTestDataWithTestData()) ? eval.getPrimeWindowSize() : 0; JPanel testStepsForTarget = eval.graphPredictionsForStepsOnTesting(GraphDriver.getDefaultDriver(), m_threadForecaster, selectedTarget, stepList, instanceOffset); testStepsForTarget.setToolTipText("Test pred. at steps"); graphList.add(testStepsForTarget); } } // graph future predictions if (m_advancedConfigPanel.getGraphFuturePredictions()) { if (eval.getTrainingData() != null /*&& eval.getEvaluateOnTrainingData()*/) { try { JPanel trainFuture = eval. graphFutureForecastOnTraining(GraphDriver.getDefaultDriver(), m_threadForecaster, AbstractForecaster. stringToList(m_threadForecaster.getFieldsToForecast())); trainFuture.setToolTipText("Train future pred."); graphList.add(trainFuture); } catch (Exception ex) { if (m_threadForecaster instanceof OverlayForecaster && ((OverlayForecaster)m_threadForecaster).isUsingOverlayData()) { if (m_log != null) { m_log.logMessage("Unable to graph future forecast for training " + "data because no future overlay data is available"); } } else { if (m_log != null) { m_log.logMessage("Unable to graph future forecast for " + "training data: " + ex.getMessage()); } } } } if (eval.getTestData() != null && eval.getEvaluateOnTestData()) { try { JPanel testFuture = eval. graphFutureForecastOnTesting(GraphDriver.getDefaultDriver(), m_threadForecaster, AbstractForecaster. stringToList(m_threadForecaster.getFieldsToForecast())); testFuture.setToolTipText("Test future pred."); graphList.add(testFuture); } catch (Exception ex) { if (m_threadForecaster instanceof OverlayForecaster && ((OverlayForecaster)m_threadForecaster).isUsingOverlayData()) { if (m_log != null) { m_log.logMessage("Unable to graph future forecast for test " + "data because no future overlay data is available"); } } else { if (m_log != null) { m_log.logMessage("Unable to graph future forecast for test " + "data: " + ex.getMessage()); } } } } } try { if (m_configAndBuild) { WekaForecaster copiedForecaster = (WekaForecaster)AbstractForecaster.makeCopy(m_threadForecaster); resultList.add(copiedForecaster); Instances trainHeader = new Instances(trainInst, 0); resultList.add(trainHeader); } } catch (Exception ex) { if (m_log != null) { logger.println("Problem copying model."); m_log.logMessage("Problem copying model: " + ex.getMessage()); } ex.printStackTrace(); } if (graphList.size() > 0) { resultList.add(graphList); } m_history.addObject(name, resultList); if (graphList.size() > 0) { updateMainTabs(name); } if (m_log != null) { m_log.logMessage("Finished " + fname); logger.println("OK"); } } catch (Exception ex) { ex.printStackTrace(); if (m_log != null) { m_log.logMessage(ex.getMessage()); logger.println("Problem evaluating forecaster"); } JOptionPane.showMessageDialog(ForecastingPanel.this, "Problem evaluating forecaster:\n" + ex.getMessage(), "Evaluate forecaster", JOptionPane.ERROR_MESSAGE); } finally { if (isInterrupted()) { if (m_log != null) { m_log.logMessage("Interrupted " + fname); logger.println("Interrupted"); } } synchronized (this) { m_startBut.setEnabled(true); m_stopBut.setEnabled(false); m_runThread = null; } if (m_log instanceof TaskLogger) { ((TaskLogger)m_log).taskFinished(); } } } } /** * Constructor. * * @param log the log to use (may be null for no log) * @param displayWelcome true if the welcome message is to be * displayed as the first log entry */ public ForecastingPanel(LogPanel log, boolean displayWelcome) { this(log, displayWelcome, true, false); } /** * Constructor * * @param log the log to use (may be null for no log) * @param displayLogWelcome true if the welcome message is to be * displayed as the first log entry * @param allowSeparateTestSet true if the separate test set button is * to be displayed */ public ForecastingPanel(LogPanel log, boolean displayLogWelcome, boolean allowSeparateTestSet, boolean kfPerspective) { m_isRunningAsPerspective = kfPerspective; m_simpleConfigPanel = new SimpleConfigPanel(this); m_advancedConfigPanel = new AdvancedConfigPanel(m_simpleConfigPanel, allowSeparateTestSet); setLayout(new BorderLayout()); m_log = log; if (m_log != null) { if (displayLogWelcome) { String date = (new SimpleDateFormat("EEEE, d MMMM yyyy")).format(new Date()); m_log.logMessage("Weka Forecaster"); /*m_logPanel.logMessage("(c) " + Copyright.getFromYear() + "-" + Copyright.getToYear() + " " + Copyright.getOwner() + ", " + Copyright.getAddress()); */ // m_logPanel.logMessage("web: " + Copyright.getURL()); m_log.logMessage("Started on " + date); m_log.statusMessage("Welcome to the Weka Forecaster"); } } m_simpleConfigPanel.setAdvancedConfig(m_advancedConfigPanel); m_configPane.addTab(m_simpleConfigPanel.getTabTitle(), null, m_simpleConfigPanel, m_simpleConfigPanel.getTabTitleToolTip()); m_configPane.addTab(m_advancedConfigPanel.getTabTitle(), null, m_advancedConfigPanel, m_advancedConfigPanel.getTabTitleToolTip()); m_outText.setEditable(false); m_outText.setFont(new Font("Monospaced", Font.PLAIN, 12)); m_outText.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); final JScrollPane js = new JScrollPane(m_outText); js.getViewport().addChangeListener(new ChangeListener() { private int lastHeight; public void stateChanged(ChangeEvent e) { JViewport vp = (JViewport)e.getSource(); int h = vp.getViewSize().height; if (h != lastHeight) { // i.e. an addition not just a user scrolling lastHeight = h; int x = h - vp.getExtentSize().height; vp.setViewPosition(new Point(0, x)); } } }); m_outputPane.setBorder(BorderFactory.createTitledBorder("Output/Visualization")); m_outputPane.addTab("Output", null, js, "Forecaster output"); m_history.setBorder(BorderFactory.createTitledBorder("Result list")); m_history.getList().getSelectionModel(). addListSelectionListener(new ListSelectionListener() { public void valueChanged(ListSelectionEvent e) { synchronized (ForecastingPanel.this) { if (!e.getValueIsAdjusting()) { ListSelectionModel lm = (ListSelectionModel)e.getSource(); for (int j = e.getFirstIndex(); j <= e.getLastIndex(); j++) { if (lm.isSelectedIndex(j)) { String name = m_history.getSelectedName(); updateMainTabs(name); } } } } } }); m_history.getList().addMouseListener(new MouseAdapter() { public void mouseClicked(MouseEvent e) { if (((e.getModifiers() & InputEvent.BUTTON1_MASK) != InputEvent.BUTTON1_MASK) || e.isAltDown()) { int index = m_history.getList().locationToIndex(e.getPoint()); if (index != -1) { String name = m_history.getNameAtIndex(index); resultPopup(name, e.getX(), e.getY()); } else { resultPopup(null, e.getX(), e.getY()); } } } }); m_history.setHandleRightClicks(false); // add(m_configPane, BorderLayout.NORTH); JPanel lowerPanel = new JPanel(); lowerPanel.setLayout(new BorderLayout()); JPanel butAndHistHolder = new JPanel(); butAndHistHolder.setLayout(new BorderLayout()); butAndHistHolder.add(m_history, BorderLayout.CENTER); JPanel butHolder = new JPanel(); butHolder.setLayout(new GridLayout(1,3)); butHolder.add(m_startBut); butHolder.add(m_stopBut); butHolder.add(m_helpBut); m_startBut.setToolTipText("Start the forecasting process"); m_stopBut.setToolTipText("Stop the running forecasting process"); butAndHistHolder.add(butHolder, BorderLayout.NORTH); m_startBut.setEnabled(false); m_stopBut.setEnabled(false); m_stopBut.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { stopForecaster(); } }); m_helpBut.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { BrowserHelper.openURL(ForecastingPanel.this, "http://wiki.pentaho.com/display/DATAMINING/" + "Time+Series+Analysis+and+Forecasting+with+Weka"); } }); m_helpBut.setToolTipText("Visit documentation for the time series " + "environment in your browser"); lowerPanel.add(butAndHistHolder, BorderLayout.WEST); lowerPanel.add(m_outputPane, BorderLayout.CENTER); m_splitP = new JSplitPane(JSplitPane.VERTICAL_SPLIT, m_configPane, lowerPanel); m_splitP.setOneTouchExpandable(true); // add(lowerPanel, BorderLayout.CENTER); add(m_splitP, BorderLayout.CENTER); if (m_log != null && m_log instanceof JComponent) { add((JComponent)m_log, BorderLayout.SOUTH); } double width = m_history.getPreferredSize().width; int height = m_history.getPreferredSize().height; width *= 0.75; m_history.setPreferredSize(new Dimension((int)width, height)); m_startBut.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { startForecaster(m_forecaster); } }); } /** * Enable the start button * * @param enable true if the start button is to be enabled */ protected void enableStartButton(boolean enable) { m_startBut.setEnabled(enable); } /** Map used to store the tabs containing graph output */ protected HashMap<String, JTabbedPane> m_framedOutputMap = new HashMap<String, JTabbedPane>(); /** * Opens the named result in a separate frame * * @param name the name of the result from the history list to use */ protected void openResultFrame(String name) { StringBuffer buffer = m_history.getNamedBuffer(name); JTabbedPane tabbedPane = m_framedOutputMap.get(name); if (buffer != null && tabbedPane == null) { JTextArea textA = new JTextArea(20, 50); textA.setEditable(false); textA.setFont(new Font("Monospaced", Font.PLAIN, 12)); textA.setBorder(BorderFactory.createEmptyBorder(5, 5, 5, 5)); textA.setText(buffer.toString()); tabbedPane = new JTabbedPane(); tabbedPane.addTab("Output", new JScrollPane(textA)); updateComponentTabs(name, tabbedPane); m_framedOutputMap.put(name, tabbedPane); final JFrame jf = new JFrame(name); jf.addWindowListener(new WindowAdapter() { public void windowClosing(WindowEvent e) { m_framedOutputMap.remove(jf.getTitle()); jf.dispose(); } }); jf.setLayout(new BorderLayout()); jf.add(tabbedPane, BorderLayout.CENTER); jf.pack(); jf.setSize(550, 400); jf.setVisible(true); } } /** * Pops up a contextual menu in the result history list * * @param name the selected entry in the list * @param x the x position for the popup * @param y the y position for the popup */ protected void resultPopup(final String name, int x, int y) { final String selectedName = name; JPopupMenu resultListMenu = new JPopupMenu(); JMenuItem showMainBuff = new JMenuItem("View in main window"); if (selectedName != null) { showMainBuff.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { m_history.setSingle(selectedName); updateMainTabs(selectedName); } }); } else { showMainBuff.setEnabled(false); } resultListMenu.add(showMainBuff); JMenuItem showSepBuff = new JMenuItem("View in separate window"); if (selectedName != null) { showSepBuff.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { openResultFrame(selectedName); } }); } else { showSepBuff.setEnabled(false); } resultListMenu.add(showSepBuff); JMenuItem deleteResultBuff = new JMenuItem("Delete result"); if (selectedName != null && m_runThread == null) { deleteResultBuff.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { m_history.removeResult(selectedName); } }); } else { deleteResultBuff.setEnabled(false); } resultListMenu.add(deleteResultBuff); resultListMenu.addSeparator(); List<Object> resultList = null; if (selectedName != null) { resultList = (List<Object>)m_history.getNamedObject(name); } WekaForecaster saveForecaster = null; Instances saveForecasterStructure = null; if (resultList != null) { for (Object o : resultList) { if (o instanceof WekaForecaster){ saveForecaster = (WekaForecaster)o; } else if (o instanceof Instances) { saveForecasterStructure = (Instances)o; } } } final WekaForecaster toSave = saveForecaster; final Instances structureToSave = saveForecasterStructure; JMenuItem saveForecasterMenuItem = new JMenuItem("Save forecasting model"); if (saveForecaster != null) { saveForecasterMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { saveForecaster(name, toSave, structureToSave); } }); } else { saveForecasterMenuItem.setEnabled(false); } resultListMenu.add(saveForecasterMenuItem); JMenuItem loadForecasterMenuItem = new JMenuItem("Load forecasting model"); resultListMenu.add(loadForecasterMenuItem); loadForecasterMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { loadForecaster(); } }); if (m_isRunningAsPerspective) { JMenuItem copyToKFClipboardMenuItem = new JMenuItem("Copy model to Knowledge Flow clipboard"); resultListMenu.add(copyToKFClipboardMenuItem); copyToKFClipboardMenuItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { try { KnowledgeFlowApp singleton = KnowledgeFlowApp.getSingleton(); String encoded = TimeSeriesForecasting.encodeForecasterToBase64(toSave, structureToSave); TimeSeriesForecasting component = new TimeSeriesForecasting(); component.setEncodedForecaster(encoded); TimeSeriesForecastingKFPerspective.setClipboard(component); } catch (Exception ex) { ex.printStackTrace(); } } }); } JMenuItem reevaluateModelItem = new JMenuItem("Re-evaluate model"); if (selectedName != null && m_runThread == null) { reevaluateModelItem.addActionListener(new ActionListener() { public void actionPerformed(ActionEvent e) { reevaluateForecaster(selectedName, toSave, structureToSave); } }); reevaluateModelItem. setEnabled((m_advancedConfigPanel.m_trainingCheckBox.isSelected() || m_advancedConfigPanel.m_holdoutCheckBox.isSelected()) && m_instances != null); } else { reevaluateModelItem.setEnabled(false); } resultListMenu.add(reevaluateModelItem); resultListMenu.show(m_history.getList(), x, y); } /** * Load a forecaster and add it to the history list */ protected void loadForecaster() { File sFile = null; int returnVal = m_fileChooser.showOpenDialog(this); if (returnVal == m_fileChooser.APPROVE_OPTION) { sFile = m_fileChooser.getSelectedFile(); if (m_log != null) { m_log.statusMessage("Loading forecaster..."); } Object f = null; Instances header = null; boolean loadOK = true; try { ObjectInputStream is = new ObjectInputStream(new FileInputStream(sFile)); f = is.readObject(); header = (Instances)is.readObject(); is.close(); } catch (Exception ex) { JOptionPane.showMessageDialog(null, ex, "Load failed", JOptionPane.ERROR_MESSAGE); loadOK = false; } if (!loadOK) { if (m_log != null) { m_log.logMessage("Loading from file " + sFile.getName() + "' failed."); m_log.statusMessage("OK"); } } else if (!(f instanceof WekaForecaster)) { JOptionPane.showMessageDialog(this, "Loaded model is not a weka forecaster!", "Weka forecasting", JOptionPane.ERROR_MESSAGE); loadOK = false; } if (loadOK) { String name = (new SimpleDateFormat("HH:mm:ss - ")).format(new Date()); StringBuffer outBuff = new StringBuffer(); WekaForecaster wf = (WekaForecaster)f; String lagOptions = ""; if (wf instanceof TSLagUser) { TSLagMaker lagMaker = ((TSLagUser)wf).getTSLagMaker(); lagOptions = Utils.joinOptions(lagMaker.getOptions()); } String fname = wf.getAlgorithmName(); String algoName = fname.substring(0, fname.indexOf(' ')); if (algoName.startsWith("weka.classifiers.")) { name += algoName.substring("weka.classifiers.".length()); } else { name += algoName; } name += " loaded from '" + sFile.getName() + "'"; outBuff.append("Scheme:\n\t" + fname).append("\n"); outBuff.append("loaded from '" + sFile.getName() + "'\n\n"); if (lagOptions.length() > 0) { outBuff.append("Lagged and derived variable options:\n\t"). append(lagOptions + "\n\n"); } outBuff.append(wf.toString()); m_history.addResult(name, outBuff); m_history.setSingle(name); List<Object> resultList = new ArrayList<Object>(); resultList.add(wf); resultList.add(header); m_history.addObject(name, resultList); updateMainTabs(name); } } } /** * Serialize a forecaster out to a file * * @param name the name of forecaster to save * @param forecaster the actual forecaster to save * @param structure the structure of the instances used to train the forecaster */ protected void saveForecaster(String name, TSForecaster forecaster, Instances structure) { File sFile = null; boolean saveOK = true; int returnVal = m_fileChooser.showSaveDialog(this); if (returnVal == JFileChooser.APPROVE_OPTION) { sFile = m_fileChooser.getSelectedFile(); if (!sFile.getName().toLowerCase().endsWith(".model")) { sFile = new File(sFile.getParent(), sFile.getName() + ".model"); } if (m_log != null) { m_log.statusMessage("Saving forecaster to file..."); } try { ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream(sFile)); oos.writeObject(forecaster); if (structure != null) { oos.writeObject(new Instances(structure, 0)); } oos.close(); } catch (Exception ex) { JOptionPane.showMessageDialog(null, ex, "Save Failed", JOptionPane.ERROR_MESSAGE); saveOK = false; } if (saveOK) { if (m_log != null) { m_log.logMessage("Saved model (" + name + " ) to file '" + sFile.getName() + "'"); m_log.statusMessage("OK"); } } } } /** * Updates the display with the results from the currently selected * entry in the result history list * * @param name the name of the entry from which to display results * @param outputPane the output pane to update */ protected synchronized void updateComponentTabs(String name, JTabbedPane outputPane) { // remove any tabs that are not the text output int numTabs = outputPane.getTabCount(); if (numTabs > 1) { for (int i = numTabs - 1; i > 0; i--) { outputPane.removeTabAt(i); } } // see if there are any graphs associated with this name List<Object> storedResults = (List<Object>)m_history.getNamedObject(name); List<JPanel> graphList = null; if (storedResults != null) { for (Object o : storedResults) { if (o instanceof List<?>) { graphList = (List<JPanel>)o; } } } if (graphList != null && graphList.size() > 0) { // add the graphs for (JPanel p : graphList) { outputPane.addTab(p.getToolTipText(), p); } } } /** * Updates the tabs in the main display. * * @param entryName the entry name of the currently displayed results. If * the selected result is the same as the current then no update is done. */ protected synchronized void updateMainTabs(String entryName) { String name = m_history.getSelectedName(); if (!name.equals(entryName)) { return; } updateComponentTabs(name, m_outputPane); } /** * Set the log to use * * @param log the log to use */ public void setLog(Logger log) { if (log instanceof JComponent) { if (m_log != null) { remove((JComponent)m_log); } m_log = log; add((JComponent)m_log, BorderLayout.SOUTH); } } /** * Set the training instances to use * * @param insts the training instances to use * @throws Exception if a problem occurs */ public void setInstances(Instances insts) throws Exception { // if this is the first set of instances seen then // install a listener on the simple config target panel's // table model so that we can enable/disable the start // button m_sortedCheck = false; boolean first = (m_simpleConfigPanel.m_targetPanel.getTableModel() == null); m_instances = new Instances(insts); m_simpleConfigPanel.setInstances(insts); m_advancedConfigPanel.setInstances(insts); if (first) { TableModel model = m_simpleConfigPanel.m_targetPanel.getTableModel(); model.addTableModelListener(new TableModelListener() { public void tableChanged(TableModelEvent e) { int[] selected = m_simpleConfigPanel.m_targetPanel.getSelectedAttributes(); if (selected != null && selected.length > 0) { m_startBut.setEnabled(true); } else { m_startBut.setEnabled(false); } m_advancedConfigPanel.updatePanel(); } }); } } /** * Check to see if the data is sorted in order of the date time * stamp (if present) */ protected void sortCheck() { if (m_instances == null) { return; } if (m_simpleConfigPanel.isUsingANativeTimeStamp()) { String timeStampF = m_simpleConfigPanel.getSelectedTimeStampField(); Attribute timeStampAtt = m_instances.attribute(timeStampF); if (timeStampAtt != null) { double lastNonMissing = Utils.missingValue(); boolean ok = true; boolean hasMissing = false; for (int i = 0; i < m_instances.numInstances(); i++) { Instance current = m_instances.instance(i); if (Utils.isMissingValue(lastNonMissing)) { if (!current.isMissing(timeStampAtt)) { lastNonMissing = current.value(timeStampAtt); } else { hasMissing = true; } } else { if (!current.isMissing(timeStampAtt)) { if (current.value(timeStampAtt) - lastNonMissing < 0) { ok = false; break; } lastNonMissing = current.value(timeStampAtt); } else { hasMissing = true; } } } if (!ok && !hasMissing) { // ask if we should sort int result = JOptionPane.showConfirmDialog(ForecastingPanel.this, "The data does not appear to be in sorted order of \"" + timeStampF + "\". Do you want to sort the data?", "Forecasting", JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { if (m_log != null) { m_log.statusMessage("Sorting data..."); } m_instances.sort(timeStampAtt); m_sortedCheck = true; } } if (!ok && hasMissing) { // we can't really proceed in this situation. We can't sort by the timestamp // because instances with missing values will be put at the end of the data. // The only thing we can do is to remove the instances with missing time // stamps but this is likely to screw up the periodicity and majorly impact // on results. int result = JOptionPane.showConfirmDialog(ForecastingPanel.this, "The data does not appear to be in sorted order of \"" + timeStampF + "\". \nFurthermore, there are rows with\n" + "missing timestamp values. We can remove these\n" + "rows and then sort the data but this is likely to\n" + "result in degraded performance. It is strongly\n" + "recommended that you fix these issues in the data\n" + "before continuing. Do you want the system to proceed\n" + "anyway by removing rows with missing timestamps and\n" + "then sorting the data?", "Forecasting", JOptionPane.YES_NO_OPTION); if (result == JOptionPane.YES_OPTION) { if (m_log != null) { m_log.statusMessage("Removing rows with missing time stamps and sorting data..."); } m_instances.deleteWithMissing(timeStampAtt); m_instances.sort(timeStampAtt); m_sortedCheck = true; } } } } } /** * Stop the currently running thread */ protected void stopForecaster() { if (m_runThread != null) { m_runThread.interrupt(); m_runThread.stop(); } } /** * Inner class defining a log based on PrintStream. This enables command line, * gui and central Weka logging to be unified. * * @author Mark Hall (mhall{[at]}pentaho{[dot]}com) */ class LogPrintStream extends PrintStream { public LogPrintStream() { super(System.out); } /** * Log to the status area. Logs to the log area if the string * begins with "WARNING" or "ERROR" * * @param string the string to display */ private void logStatusMessage(String string) { if (m_log != null) { m_log.statusMessage(string); if (string.contains("WARNING") || string.contains("ERROR")) { m_log.logMessage(string); } } } /** * Log to the status area * @param string the string to log */ public void println(String string) { // make sure that the global weka log picks it up System.out.println(string); logStatusMessage(string); } /** * Log to the status area * @param obj the object to log */ public void println(Object obj) { println(obj.toString()); } /** * Log to the status area * @param string the string to log */ public void print(String string){ // make sure that the global weka log picks it up System.out.print(string); logStatusMessage(string); } /** * Log to the status area * @param obj the object to log */ public void print(Object obj) { print(obj.toString()); } } /** * Reevaluate the supplied forecaster on the current data * * @param name the name of the result from the history list associated * with the forecaster to be reevaluated * * @param forecaster the forecaster to reevaluate * @param trainHeader the header of the data used to train the forecaster */ protected void reevaluateForecaster(final String name, final WekaForecaster forecaster, final Instances trainHeader) { if (!trainHeader.equalHeaders(m_instances)) { JOptionPane.showMessageDialog(null, "Data used to train this forecaster " + "is not compatible with the currently loaded data:\n\n" + trainHeader.equalHeadersMsg(m_instances), "Unable to reevaluate model", JOptionPane.ERROR_MESSAGE); } else { if (m_runThread == null) { synchronized (this) { m_startBut.setEnabled(false); m_stopBut.setEnabled(true); } m_runThread = new ForecastingThread(forecaster, name); ((ForecastingThread)m_runThread).setConfigureAndBuild(false); m_runThread.setPriority(Thread.MIN_PRIORITY); m_runThread.start(); } } } /** * Start the forecasting process for the supplied configured forecaster * * @param forecaster the forecaster to run */ protected void startForecaster(final WekaForecaster forecaster) { if (m_runThread == null) { synchronized (this) { m_startBut.setEnabled(false); m_stopBut.setEnabled(true); } m_runThread = new ForecastingThread(forecaster, null); m_runThread.setPriority(Thread.MIN_PRIORITY); m_runThread.start(); } } private void dontShowMessageDialog(String key, String message, String dialogTitle) { if (!Utils.getDontShowDialog(key)) { JCheckBox dontShow = new JCheckBox("Do not show this message again"); Object[] stuff = new Object[2]; stuff[0] = message + "\n"; stuff[1] = dontShow; JOptionPane.showMessageDialog(this, stuff, dialogTitle, JOptionPane.OK_OPTION); if (dontShow.isSelected()) { try { Utils.setDontShowDialog(key); } catch (Exception ex) { // quietly ignore } } } } /** * Tests the Weka Forecasting panel from the command line. * * @param args must contain the name of an arff file to load. */ public static void main(String[] args) { try { if (args.length == 0) { throw new Exception("supply the name of an arff file"); } Instances i = new Instances(new java.io.BufferedReader( new java.io.FileReader(args[0]))); ForecastingPanel scp = new ForecastingPanel(new LogPanel(new WekaTaskMonitor()), true, false, false); scp.setInstances(i); final javax.swing.JFrame jf = new javax.swing.JFrame("Weka Forecasting"); jf.getContentPane().setLayout(new BorderLayout()); jf.getContentPane().add(scp, BorderLayout.CENTER); jf.addWindowListener(new java.awt.event.WindowAdapter() { public void windowClosing(java.awt.event.WindowEvent e) { jf.dispose(); System.exit(0); } }); jf.pack(); jf.setVisible(true); } catch (Exception ex) { ex.printStackTrace(); System.err.println(ex.getMessage()); } } }