content: /*
   *    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 2 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, write to the Free Software
   *    Foundation, Inc., 675 Mass Ave, Cambridge, MA 02139, USA.
   */
  
  /*
   *    CostBenefitAnalysis.java
   *    Copyright (C) 2009 University of Waikato, Hamilton, New Zealand
   *
   */
  
  package weka.gui.beans;
  
  import java.awt.BorderLayout;
  import java.awt.Color;
  import java.awt.Dimension;
  import java.awt.FlowLayout;
  import java.awt.GraphicsEnvironment;
  import java.awt.GridLayout;
  import java.awt.Graphics;
  import java.awt.event.ActionEvent;
  import java.awt.event.ActionListener;
  import java.awt.event.FocusEvent;
  import java.awt.event.FocusListener;
  import java.beans.EventSetDescriptor;
  import java.beans.PropertyVetoException;
  import java.beans.VetoableChangeListener;
  import java.beans.beancontext.BeanContext;
  import java.beans.beancontext.BeanContextChild;
  import java.beans.beancontext.BeanContextChildSupport;
  import java.io.Serializable;
  import java.util.ArrayList;
  import java.util.Enumeration;
  import java.util.EventObject;
  import java.util.List;
  import java.util.Vector;
  
  import javax.swing.BorderFactory;
  import javax.swing.ButtonGroup;
  import javax.swing.JButton;
  import javax.swing.JFrame;
  import javax.swing.JLabel;
  import javax.swing.JPanel;
  import javax.swing.JRadioButton;
  import javax.swing.JSlider;
  import javax.swing.JTextField;
  import javax.swing.SwingConstants;
  import javax.swing.event.ChangeEvent;
  import javax.swing.event.ChangeListener;
  
  import weka.classifiers.evaluation.ThresholdCurve;
  import weka.core.Attribute;
  import weka.core.FastVector;
  import weka.core.Instance;
  import weka.core.DenseInstance;
  import weka.core.Instances;
  import weka.core.Utils;
  import weka.gui.Logger;
  import weka.gui.visualize.VisualizePanel;
  import weka.gui.visualize.Plot2D;
  import weka.gui.visualize.PlotData2D;
  
  
  /**
   * Bean that aids in analyzing cost/benefit tradeoffs.
   * 
   * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
   * @version $Revision: 7657 $
   */
  public class CostBenefitAnalysis extends JPanel 
    implements BeanCommon, ThresholdDataListener, Visible, UserRequestAcceptor,
    Serializable, BeanContextChild, HeadlessEventCollector {
    
    /** For serialization */
    private static final long serialVersionUID = 8647471654613320469L;
  
    protected BeanVisual m_visual = new BeanVisual("CostBenefitAnalysis", 
        BeanVisual.ICON_PATH+"ModelPerformanceChart.gif",
        BeanVisual.ICON_PATH
        +"ModelPerformanceChart_animated.gif");
    
    protected transient JFrame m_popupFrame;
  
    protected boolean m_framePoppedUp = false;
    
    private transient AnalysisPanel m_analysisPanel;
    
    /**
     * True if this bean's appearance is the design mode appearance
     */
    protected boolean m_design;
  
    /**
     * BeanContex that this bean might be contained within
     */
    protected transient BeanContext m_beanContext = null;
    
    /**
     * BeanContextChild support
     */
    protected BeanContextChildSupport m_bcSupport = 
      new BeanContextChildSupport(this);
    
    /**
     * The object sending us data (we allow only one connection at any one time)
     */
    protected Object m_listenee;
    
    protected List<EventObject> m_headlessEvents;
    
    /**
     * Inner class for displaying the plots and all control widgets.
     * 
     * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
     */
    protected static class AnalysisPanel extends JPanel {
      
      /** For serialization */
      private static final long serialVersionUID = 5364871945448769003L;
  
      /** Displays the performance graphs(s) */
      protected VisualizePanel m_performancePanel = new VisualizePanel();
      
      /** Displays the cost/benefit (profit/loss) graph */
      protected VisualizePanel m_costBenefitPanel = new VisualizePanel();
      
      /** 
       * The class attribute from the data that was used to generate
       * the threshold curve
       */
      protected Attribute m_classAttribute;
      
      /** Data for the threshold curve */
      protected PlotData2D m_masterPlot;
      
      /** Data for the cost/benefit curve */
      protected PlotData2D m_costBenefit;
      
      /** The size of the points being plotted */
      protected int[] m_shapeSizes;
      
      /** The index of the previous plotted point that was highlighted */
      protected int m_previousShapeIndex = -1;
          
      /** The slider for adjusting the threshold */
      protected JSlider m_thresholdSlider = new JSlider(0,100,0);
      
      protected JRadioButton m_percPop = new JRadioButton("% of Population");
      protected JRadioButton m_percOfTarget = new JRadioButton("% of Target (recall)");
      protected JRadioButton m_threshold = new JRadioButton("Score Threshold");
      
      protected JLabel m_percPopLab = new JLabel();
      protected JLabel m_percOfTargetLab = new JLabel();
      protected JLabel m_thresholdLab = new JLabel();
      
      // Confusion matrix stuff
      protected JLabel m_conf_predictedA = new JLabel("Predicted (a)", SwingConstants.RIGHT);
      protected JLabel m_conf_predictedB = new JLabel("Predicted (b)", SwingConstants.RIGHT);
      protected JLabel m_conf_actualA = new JLabel(" Actual (a):");
      protected JLabel m_conf_actualB = new JLabel(" Actual (b):");
      protected ConfusionCell m_conf_aa = new ConfusionCell();
      protected ConfusionCell m_conf_ab = new ConfusionCell();
      protected ConfusionCell m_conf_ba = new ConfusionCell();
      protected ConfusionCell m_conf_bb = new ConfusionCell();
      
      // Cost matrix stuff
      protected JLabel m_cost_predictedA = new JLabel("Predicted (a)", SwingConstants.RIGHT);
      protected JLabel m_cost_predictedB = new JLabel("Predicted (b)", SwingConstants.RIGHT);
      protected JLabel m_cost_actualA = new JLabel(" Actual (a)");
      protected JLabel m_cost_actualB = new JLabel(" Actual (b)");
      protected JTextField m_cost_aa = new JTextField("0.0", 5);
      protected JTextField m_cost_ab = new JTextField("1.0", 5);
      protected JTextField m_cost_ba = new JTextField("1.0", 5);
      protected JTextField m_cost_bb = new JTextField("0.0" ,5);
      protected JButton m_maximizeCB = new JButton("Maximize Cost/Benefit");
      protected JButton m_minimizeCB = new JButton("Minimize Cost/Benefit");
      protected JRadioButton m_costR = new JRadioButton("Cost");
      protected JRadioButton m_benefitR = new JRadioButton("Benefit");
      protected JLabel m_costBenefitL = new JLabel("Cost: ", SwingConstants.RIGHT);
      protected JLabel m_costBenefitV = new JLabel("0");
      protected JLabel m_randomV = new JLabel("0");
      protected JLabel m_gainV = new JLabel("0");
      
      protected int m_originalPopSize;
      
      /** Population text field */
      protected JTextField m_totalPopField = new JTextField(6);
      protected int m_totalPopPrevious;
      
      /** Classification accuracy */
      protected JLabel m_classificationAccV = new JLabel("-");
      
      // Only update curve & stats if values in cost matrix have changed
      protected double m_tpPrevious;
      protected double m_fpPrevious;
      protected double m_tnPrevious;
      protected double m_fnPrevious;
      
      /**
       * Inner class for handling a single cell in the confusion matrix.
       * Displays the value, value as a percentage of total population and
       * graphical depiction of percentage.
       * 
       * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
       */
      protected static class ConfusionCell extends JPanel {
  
        /** For serialization */
        private static final long serialVersionUID = 6148640235434494767L;
        
        private JLabel m_conf_cell = new JLabel("-", SwingConstants.RIGHT);
        JLabel m_conf_perc = new JLabel("-", SwingConstants.RIGHT);
        
        private JPanel m_percentageP;
        
        protected double m_percentage = 0;
        
        public ConfusionCell() {
          setLayout(new BorderLayout());
          setBorder(BorderFactory.createEtchedBorder());
          
          add(m_conf_cell, BorderLayout.NORTH);
          
          m_percentageP = new JPanel() {
            public void paintComponent(Graphics gx) {
              super.paintComponent(gx);
              
              if (m_percentage > 0) {
                gx.setColor(Color.BLUE);
                int height = this.getHeight();
                double width = this.getWidth();
                int barWidth = (int)(m_percentage * width); 
                gx.fillRect(0, 0, barWidth, height);
              }
            }
          };
          
          Dimension d = new Dimension(30,5);
          m_percentageP.setMinimumSize(d);
          m_percentageP.setPreferredSize(d);
          JPanel percHolder = new JPanel();
          percHolder.setLayout(new BorderLayout());
          percHolder.add(m_percentageP, BorderLayout.CENTER);
          percHolder.add(m_conf_perc, BorderLayout.EAST);
          
          add(percHolder, BorderLayout.SOUTH);
        }
        
        /**
         * Set the value of a cell.
         * 
         * @param cellValue the value of the cell
         * @param max the max (for setting value as a percentage)
         * @param scaleFactor scale the value by this amount
         * @param precision precision for the percentage value
         */
        public void setCellValue(double cellValue, double max, double scaleFactor, int precision) {
          if (!Utils.isMissingValue(cellValue)) {
            m_percentage = cellValue / max;
          } else {
            m_percentage = 0;
          }
          
          m_conf_cell.setText(Utils.doubleToString((cellValue * scaleFactor), 0));
          m_conf_perc.setText(Utils.doubleToString(m_percentage * 100.0, precision) + "%");
          
          // refresh the percentage bar
          m_percentageP.repaint();
        }
      }
      
      public AnalysisPanel() {
        setLayout(new BorderLayout());
        m_performancePanel.setShowAttBars(false);
        m_performancePanel.setShowClassPanel(false);
        m_costBenefitPanel.setShowAttBars(false);
        m_costBenefitPanel.setShowClassPanel(false);
        
        Dimension size = new Dimension(500, 400);
        m_performancePanel.setPreferredSize(size);
        m_performancePanel.setMinimumSize(size);
        
        size = new Dimension(500, 400);
        m_costBenefitPanel.setMinimumSize(size);
        m_costBenefitPanel.setPreferredSize(size);
        
        m_thresholdSlider.addChangeListener(new ChangeListener() {
          public void stateChanged(ChangeEvent e) {
            updateInfoForSliderValue((double)m_thresholdSlider.getValue() / 100.0);
          }
        });
        
        JPanel plotHolder = new JPanel();
        plotHolder.setLayout(new GridLayout(1,2));      
        plotHolder.add(m_performancePanel);
        plotHolder.add(m_costBenefitPanel);
        add(plotHolder, BorderLayout.CENTER);
        
        JPanel lowerPanel = new JPanel();
        lowerPanel.setLayout(new BorderLayout());
        
        ButtonGroup bGroup = new ButtonGroup();
        bGroup.add(m_percPop);
        bGroup.add(m_percOfTarget);
        bGroup.add(m_threshold);
        
        ButtonGroup bGroup2 = new ButtonGroup();
        bGroup2.add(m_costR);
        bGroup2.add(m_benefitR);
        ActionListener rl = new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (m_costR.isSelected()) {
              m_costBenefitL.setText("Cost: ");
            } else {
              m_costBenefitL.setText("Benefit: ");
            }
  
            double gain = Double.parseDouble(m_gainV.getText());
            gain = -gain;
            m_gainV.setText(Utils.doubleToString(gain, 2));
          }
        };
        m_costR.addActionListener(rl);
        m_benefitR.addActionListener(rl);
        m_costR.setSelected(true);
        
        m_percPop.setSelected(true);
        JPanel threshPanel = new JPanel();
        threshPanel.setLayout(new BorderLayout());
        JPanel radioHolder = new JPanel();
        radioHolder.setLayout(new FlowLayout());
        radioHolder.add(m_percPop);
        radioHolder.add(m_percOfTarget);
        radioHolder.add(m_threshold);
        threshPanel.add(radioHolder, BorderLayout.NORTH);
        threshPanel.add(m_thresholdSlider, BorderLayout.SOUTH);
        
        JPanel threshInfoPanel = new JPanel();
        threshInfoPanel.setLayout(new GridLayout(3,2));
        threshInfoPanel.add(new JLabel("% of Population: ", SwingConstants.RIGHT));
        threshInfoPanel.add(m_percPopLab);
        threshInfoPanel.add(new JLabel("% of Target: ", SwingConstants.RIGHT));
        threshInfoPanel.add(m_percOfTargetLab);
        threshInfoPanel.add(new JLabel("Score Threshold: ", SwingConstants.RIGHT));
        threshInfoPanel.add(m_thresholdLab);
        
        JPanel threshHolder = new JPanel();
        threshHolder.setBorder(BorderFactory.createTitledBorder("Threshold"));
        threshHolder.setLayout(new BorderLayout());
        threshHolder.add(threshPanel, BorderLayout.CENTER);
        threshHolder.add(threshInfoPanel, BorderLayout.EAST);
        
        lowerPanel.add(threshHolder, BorderLayout.NORTH);
        
        // holder for the two matrixes
        JPanel matrixHolder = new JPanel();
        matrixHolder.setLayout(new GridLayout(1,2));
        
        // confusion matrix
        JPanel confusionPanel = new JPanel();
        confusionPanel.setLayout(new GridLayout(3,3));
        confusionPanel.add(m_conf_predictedA);
        confusionPanel.add(m_conf_predictedB);
        confusionPanel.add(new JLabel()); // dummy
        confusionPanel.add(m_conf_aa);
        confusionPanel.add(m_conf_ab);
        confusionPanel.add(m_conf_actualA);
        confusionPanel.add(m_conf_ba);
        confusionPanel.add(m_conf_bb);
        confusionPanel.add(m_conf_actualB);
        JPanel tempHolderCA = new JPanel();
        tempHolderCA.setLayout(new BorderLayout());
        tempHolderCA.setBorder(BorderFactory.createTitledBorder("Confusion Matrix"));
        tempHolderCA.add(confusionPanel, BorderLayout.CENTER);
        
        JPanel accHolder = new JPanel();
        accHolder.setLayout(new FlowLayout(FlowLayout.LEFT));
        accHolder.add(new JLabel("Classification Accuracy: "));
        accHolder.add(m_classificationAccV);
        tempHolderCA.add(accHolder, BorderLayout.SOUTH);
        
        matrixHolder.add(tempHolderCA);
        
        // cost matrix
        JPanel costPanel = new JPanel();
        costPanel.setBorder(BorderFactory.createTitledBorder("Cost Matrix"));
        costPanel.setLayout(new BorderLayout());
        
        JPanel cmHolder = new JPanel();
        cmHolder.setLayout(new GridLayout(3, 3));
        cmHolder.add(m_cost_predictedA);      
        cmHolder.add(m_cost_predictedB);
        cmHolder.add(new JLabel()); // dummy
        cmHolder.add(m_cost_aa);
        cmHolder.add(m_cost_ab);
        cmHolder.add(m_cost_actualA);
        cmHolder.add(m_cost_ba);
        cmHolder.add(m_cost_bb);
        cmHolder.add(m_cost_actualB);
        costPanel.add(cmHolder, BorderLayout.CENTER);
        
        FocusListener fl = new FocusListener() {
          public void focusGained(FocusEvent e) {
            
          }
          
          public void focusLost(FocusEvent e) {
            if (constructCostBenefitData()) {
              try {
                m_costBenefitPanel.setMasterPlot(m_costBenefit);
                m_costBenefitPanel.validate(); m_costBenefitPanel.repaint();
              } catch (Exception ex) {
                ex.printStackTrace();
              }
              updateCostBenefit();
            }
          }
        };
        
        ActionListener al = new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            if (constructCostBenefitData()) {
              try {
                m_costBenefitPanel.setMasterPlot(m_costBenefit);
                m_costBenefitPanel.validate(); m_costBenefitPanel.repaint();
              } catch (Exception ex) {
                ex.printStackTrace();
              }
              updateCostBenefit();
            }
          }
        };
              
        m_cost_aa.addFocusListener(fl);
        m_cost_aa.addActionListener(al);
        m_cost_ab.addFocusListener(fl);
        m_cost_ab.addActionListener(al);
        m_cost_ba.addFocusListener(fl);
        m_cost_ba.addActionListener(al);
        m_cost_bb.addFocusListener(fl);
        m_cost_bb.addActionListener(al);
        
        m_totalPopField.addFocusListener(fl);
        m_totalPopField.addActionListener(al);
        
        JPanel cbHolder = new JPanel();
        cbHolder.setLayout(new BorderLayout());
        JPanel tempP = new JPanel();
        tempP.setLayout(new GridLayout(3, 2));
        tempP.add(m_costBenefitL);
        tempP.add(m_costBenefitV);
        tempP.add(new JLabel("Random: ", SwingConstants.RIGHT));
        tempP.add(m_randomV);
        tempP.add(new JLabel("Gain: ", SwingConstants.RIGHT));
        tempP.add(m_gainV);
        cbHolder.add(tempP, BorderLayout.NORTH);
        JPanel butHolder = new JPanel();
        butHolder.setLayout(new GridLayout(2, 1));
        butHolder.add(m_maximizeCB);
        butHolder.add(m_minimizeCB);
        m_maximizeCB.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            findMaxMinCB(true);
          }
        });
        
        m_minimizeCB.addActionListener(new ActionListener() {
          public void actionPerformed(ActionEvent e) {
            findMaxMinCB(false);
          }
        });
        
        cbHolder.add(butHolder, BorderLayout.SOUTH);
        costPanel.add(cbHolder, BorderLayout.EAST);
        
        JPanel popCBR = new JPanel();
        popCBR.setLayout(new GridLayout(1, 2));
        JPanel popHolder = new JPanel();
        popHolder.setLayout(new FlowLayout(FlowLayout.LEFT));
        popHolder.add(new JLabel("Total Population: "));
        popHolder.add(m_totalPopField);
        
        JPanel radioHolder2 = new JPanel();
        radioHolder2.setLayout(new FlowLayout(FlowLayout.RIGHT));
        radioHolder2.add(m_costR);
        radioHolder2.add(m_benefitR);
        popCBR.add(popHolder);
        popCBR.add(radioHolder2);
        
        costPanel.add(popCBR, BorderLayout.SOUTH);
        
        matrixHolder.add(costPanel);
        
        
        lowerPanel.add(matrixHolder, BorderLayout.SOUTH);
        
  
  
  //      popAccHolder.add(popHolder);
        
        //popAccHolder.add(accHolder);
        
        /*JPanel lowerPanel2 = new JPanel();
        lowerPanel2.setLayout(new BorderLayout());
        lowerPanel2.add(lowerPanel, BorderLayout.NORTH);
        lowerPanel2.add(popAccHolder, BorderLayout.SOUTH); */
        
        add(lowerPanel, BorderLayout.SOUTH);
        
      }
      
      private void findMaxMinCB(boolean max) {
        double maxMin = (max) 
        ? Double.NEGATIVE_INFINITY 
            : Double.POSITIVE_INFINITY;
        
        Instances cBCurve = m_costBenefit.getPlotInstances();
        int maxMinIndex = 0;
        
        for (int i = 0; i < cBCurve.numInstances(); i++) {
          Instance current = cBCurve.instance(i);
          if (max) {
            if (current.value(1) > maxMin) {
              maxMin = current.value(1);
              maxMinIndex = i;
            }
          } else {
            if (current.value(1) < maxMin) {
              maxMin = current.value(1);
              maxMinIndex = i;
            }
          }
        }
        
        
        // set the slider to the correct position
        int indexOfSampleSize = 
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.SAMPLE_SIZE_NAME).index();
        int indexOfPercOfTarget = 
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.RECALL_NAME).index();
        int indexOfThreshold =
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.THRESHOLD_NAME).index();
        int indexOfMetric;
        
        if (m_percPop.isSelected()) {
          indexOfMetric = indexOfSampleSize;           
        } else if (m_percOfTarget.isSelected()) {
          indexOfMetric = indexOfPercOfTarget;
        } else {
          indexOfMetric = indexOfThreshold;
        }
        
        double valueOfMetric = m_masterPlot.getPlotInstances().instance(maxMinIndex).value(indexOfMetric);
        valueOfMetric *= 100.0;
        
        // set the approximate location of the slider
        m_thresholdSlider.setValue((int)valueOfMetric);
        
        // make sure the actual values relate to the true min/max rather
        // than being off due to slider location error.
        updateInfoGivenIndex(maxMinIndex);
      }
      
      private void updateCostBenefit() {
        double value = (double)m_thresholdSlider.getValue() / 100.0;
        Instances plotInstances = m_masterPlot.getPlotInstances();
        int indexOfSampleSize = 
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.SAMPLE_SIZE_NAME).index();
        int indexOfPercOfTarget = 
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.RECALL_NAME).index();
        int indexOfThreshold =
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.THRESHOLD_NAME).index();
        int indexOfMetric;
        
        if (m_percPop.isSelected()) {
          indexOfMetric = indexOfSampleSize;           
        } else if (m_percOfTarget.isSelected()) {
          indexOfMetric = indexOfPercOfTarget;
        } else {
          indexOfMetric = indexOfThreshold;
        }
        
        int index = findIndexForValue(value, plotInstances, indexOfMetric);
        updateCBRandomGainInfo(index);
      }
      
      private void updateCBRandomGainInfo(int index) {
        double requestedPopSize = m_originalPopSize;
        try {
          requestedPopSize = Double.parseDouble(m_totalPopField.getText());
        } catch (NumberFormatException e) {}
        double scaleFactor = requestedPopSize / m_originalPopSize;
        
        double CB = m_costBenefit.
          getPlotInstances().instance(index).value(1);
        m_costBenefitV.setText(Utils.doubleToString(CB,2));
        
        double totalRandomCB = 0.0;
        Instance first = m_masterPlot.getPlotInstances().instance(0);
        double totalPos = first.value(m_masterPlot.getPlotInstances().
            attribute(ThresholdCurve.TRUE_POS_NAME).index()) * scaleFactor;
        double totalNeg = first.value(m_masterPlot.getPlotInstances().
            attribute(ThresholdCurve.FALSE_POS_NAME)) * scaleFactor;
  
        double posInSample = (totalPos * (Double.parseDouble(m_percPopLab.getText()) / 100.0));
        double negInSample = (totalNeg * (Double.parseDouble(m_percPopLab.getText()) / 100.0));
        double posOutSample = totalPos - posInSample;
        double negOutSample = totalNeg - negInSample;
        
        double tpCost = 0.0;
        try {
          tpCost = Double.parseDouble(m_cost_aa.getText());
        } catch (NumberFormatException n) {}
        double fpCost = 0.0;
        try {
          fpCost = Double.parseDouble(m_cost_ba.getText());
        } catch (NumberFormatException n) {}
        double tnCost = 0.0;
        try {
          tnCost = Double.parseDouble(m_cost_bb.getText());
        } catch (NumberFormatException n) {}
        double fnCost = 0.0;
        try {
          fnCost = Double.parseDouble(m_cost_ab.getText());
        } catch (NumberFormatException n) {}
              
        totalRandomCB += posInSample * tpCost;
        totalRandomCB += negInSample * fpCost;
        totalRandomCB += posOutSample * fnCost;
        totalRandomCB += negOutSample * tnCost;
        
        m_randomV.setText(Utils.doubleToString(totalRandomCB, 2));
        double gain = (m_costR.isSelected()) 
        ? totalRandomCB - CB
            : CB - totalRandomCB;
        m_gainV.setText(Utils.doubleToString(gain, 2));
        
        // update classification rate
        Instance currentInst = m_masterPlot.getPlotInstances().instance(index);
        double tp = currentInst.value(m_masterPlot.getPlotInstances().
            attribute(ThresholdCurve.TRUE_POS_NAME).index());
        double tn = currentInst.value(m_masterPlot.getPlotInstances().
            attribute(ThresholdCurve.TRUE_NEG_NAME).index());
        m_classificationAccV.
          setText(Utils.doubleToString((tp + tn) / (totalPos + totalNeg) * 100.0, 4) + "%");      
      }
      
      private void updateInfoGivenIndex(int index) {
        Instances plotInstances = m_masterPlot.getPlotInstances();
        int indexOfSampleSize = 
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.SAMPLE_SIZE_NAME).index();
        int indexOfPercOfTarget = 
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.RECALL_NAME).index();
        int indexOfThreshold =
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.THRESHOLD_NAME).index();
        
        // update labels
        m_percPopLab.setText(Utils.
            doubleToString(100.0 * plotInstances.instance(index).value(indexOfSampleSize), 4));
        m_percOfTargetLab.setText(Utils.doubleToString(
            100.0 * plotInstances.instance(index).value(indexOfPercOfTarget), 4));
        m_thresholdLab.setText(Utils.doubleToString(plotInstances.instance(index).value(indexOfThreshold), 4));
        /*if (m_percPop.isSelected()) {
          m_percPopLab.setText(Utils.doubleToString(100.0 * value, 4));
        } else if (m_percOfTarget.isSelected()) {
          m_percOfTargetLab.setText(Utils.doubleToString(100.0 * value, 4));
        } else {
          m_thresholdLab.setText(Utils.doubleToString(value, 4));
        }*/
        
        // Update the highlighted point on the graphs */
        if (m_previousShapeIndex >= 0) {
          m_shapeSizes[m_previousShapeIndex] = 1;
        }
       
        m_shapeSizes[index] = 10;
        m_previousShapeIndex = index;
        
        // Update the confusion matrix
  //      double totalInstances = 
        int tp = plotInstances.attribute(ThresholdCurve.TRUE_POS_NAME).index();
        int fp = plotInstances.attribute(ThresholdCurve.FALSE_POS_NAME).index();
        int tn = plotInstances.attribute(ThresholdCurve.TRUE_NEG_NAME).index();
        int fn = plotInstances.attribute(ThresholdCurve.FALSE_NEG_NAME).index();
        Instance temp = plotInstances.instance(index);
        double totalInstances = temp.value(tp) + temp.value(fp) + temp.value(tn) + temp.value(fn);
        // get the value out of the total pop field (if possible)
        double requestedPopSize = totalInstances;
        try {
          requestedPopSize = Double.parseDouble(m_totalPopField.getText());
        } catch (NumberFormatException e) {}
        
        m_conf_aa.setCellValue(temp.value(tp), totalInstances, 
            requestedPopSize / totalInstances, 2);
        m_conf_ab.setCellValue(temp.value(fn), totalInstances, 
            requestedPopSize / totalInstances, 2);
        m_conf_ba.setCellValue(temp.value(fp), totalInstances, 
            requestedPopSize / totalInstances, 2);
        m_conf_bb.setCellValue(temp.value(tn), totalInstances, 
              requestedPopSize / totalInstances, 2);
        
        updateCBRandomGainInfo(index);
        
        repaint();
      }
      
      private void updateInfoForSliderValue(double value) {
        int indexOfSampleSize = 
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.SAMPLE_SIZE_NAME).index();
        int indexOfPercOfTarget = 
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.RECALL_NAME).index();
        int indexOfThreshold =
          m_masterPlot.getPlotInstances().attribute(ThresholdCurve.THRESHOLD_NAME).index();
        int indexOfMetric;
        
        if (m_percPop.isSelected()) {
          indexOfMetric = indexOfSampleSize;           
        } else if (m_percOfTarget.isSelected()) {
          indexOfMetric = indexOfPercOfTarget;
        } else {
          indexOfMetric = indexOfThreshold;
        }
        
        Instances plotInstances = m_masterPlot.getPlotInstances();
        int index = findIndexForValue(value, plotInstances, indexOfMetric);
        updateInfoGivenIndex(index);
      }
      
      private int findIndexForValue(double value, Instances plotInstances, int indexOfMetric) {
        // binary search
        // threshold curve is sorted ascending in the threshold (thus
        // descending for recall and pop size)
        int index = -1;
        int lower = 0;
        int upper = plotInstances.numInstances() - 1;
        int mid = (upper - lower) / 2;
        boolean done = false;
        while (!done) {
          if (upper - lower <= 1) {
            
            // choose the one closest to the value
            double comp1 = plotInstances.instance(upper).value(indexOfMetric);
            double comp2 = plotInstances.instance(lower).value(indexOfMetric);
            if (Math.abs(comp1 - value) < Math.abs(comp2 - value)) {
              index = upper;
            } else {
              index = lower;
            }
            
            break;
          }
          double comparisonVal = plotInstances.instance(mid).value(indexOfMetric);
          if (value > comparisonVal) {
            if (m_threshold.isSelected()) {
              lower = mid;
              mid += (upper - lower) / 2;
            } else {
              upper = mid;
              mid -= (upper - lower) / 2;
            }
          } else if (value < comparisonVal) {
            if (m_threshold.isSelected()) {
              upper = mid;
              mid -= (upper - lower) / 2;
            } else {
              lower = mid;
              mid += (upper - lower) / 2;
            }
          } else {
            index = mid;
            done = true;
          }
        }
        
        // now check for ties in the appropriate direction
        if (!m_threshold.isSelected()) {
          while (index + 1 < plotInstances.numInstances()) {
            if (plotInstances.instance(index + 1).value(indexOfMetric) == 
              plotInstances.instance(index).value(indexOfMetric)) {
              index++;
            } else {
              break;
            }
          }
        } else {
          while (index - 1 >= 0) {
            if (plotInstances.instance(index - 1).value(indexOfMetric) == 
              plotInstances.instance(index).value(indexOfMetric)) {
              index--;
            } else {
              break;
            } 
          }
        }
        return index;
      }
      
      /**
       * Set the threshold data for the panel to use.
       * 
       * @param data PlotData2D object encapsulating the threshold data.
       * @param classAtt the class attribute from the original data used to generate
       * the threshold data.
       * @throws Exception if something goes wrong.
       */
      public synchronized void setDataSet(PlotData2D data, Attribute classAtt) throws Exception {      
        // make a copy of the PlotData2D object
        m_masterPlot = new PlotData2D(data.getPlotInstances());
        boolean[] connectPoints = new boolean[m_masterPlot.getPlotInstances().numInstances()];
        for (int i = 1; i < connectPoints.length; i++) {
          connectPoints[i] = true;
        }
        m_masterPlot.setConnectPoints(connectPoints);
  
        m_masterPlot.m_alwaysDisplayPointsOfThisSize = 10;
        setClassForConfusionMatrix(classAtt);
        m_performancePanel.setMasterPlot(m_masterPlot);
        m_performancePanel.validate(); m_performancePanel.repaint();
  
        m_shapeSizes = new int[m_masterPlot.getPlotInstances().numInstances()];
        for (int i = 0; i < m_shapeSizes.length; i++) {
          m_shapeSizes[i] = 1;
        }
        m_masterPlot.setShapeSize(m_shapeSizes);
        constructCostBenefitData();
        m_costBenefitPanel.setMasterPlot(m_costBenefit);
        m_costBenefitPanel.validate(); m_costBenefitPanel.repaint();
  
        m_totalPopPrevious = 0;
        m_fpPrevious = 0;
        m_tpPrevious = 0;
        m_tnPrevious = 0;
        m_fnPrevious = 0;
        m_previousShapeIndex = -1;
  
        // set the total population size
        Instance first = m_masterPlot.getPlotInstances().instance(0);
        double totalPos = first.value(m_masterPlot.getPlotInstances().
            attribute(ThresholdCurve.TRUE_POS_NAME).index());
        double totalNeg = first.value(m_masterPlot.getPlotInstances().
            attribute(ThresholdCurve.FALSE_POS_NAME));
        m_originalPopSize = (int)(totalPos + totalNeg);
        m_totalPopField.setText("" + m_originalPopSize);
  
        m_performancePanel.setYIndex(5);
        m_performancePanel.setXIndex(10);
        m_costBenefitPanel.setXIndex(0);
        m_costBenefitPanel.setYIndex(1);
        //      System.err.println(m_masterPlot.getPlotInstances());
        updateInfoForSliderValue((double)m_thresholdSlider.getValue() / 100.0);
      }
      
      private void setClassForConfusionMatrix(Attribute classAtt) {
        m_classAttribute = classAtt;
        m_conf_actualA.setText(" Actual (a): " + classAtt.value(0));
        m_conf_actualA.setToolTipText(classAtt.value(0));
        String negClasses = "";
        for (int i = 1; i < classAtt.numValues(); i++) {
          negClasses += classAtt.value(i);
          if (i < classAtt.numValues() - 1) {
            negClasses += ",";
          }
        }
        m_conf_actualB.setText(" Actual (b): " + negClasses);
        m_conf_actualB.setToolTipText(negClasses);
      }
      
      private boolean constructCostBenefitData() {
        double tpCost = 0.0;
        try {
          tpCost = Double.parseDouble(m_cost_aa.getText());
        } catch (NumberFormatException n) {}
        double fpCost = 0.0;
        try {
          fpCost = Double.parseDouble(m_cost_ba.getText());
        } catch (NumberFormatException n) {}
        double tnCost = 0.0;
        try {
          tnCost = Double.parseDouble(m_cost_bb.getText());
        } catch (NumberFormatException n) {}
        double fnCost = 0.0;
        try {
          fnCost = Double.parseDouble(m_cost_ab.getText());
        } catch (NumberFormatException n) {}
        
        double requestedPopSize = m_originalPopSize;
        try {
          requestedPopSize = Double.parseDouble(m_totalPopField.getText());
        } catch (NumberFormatException e) {}
        
        double scaleFactor = 1.0;
        if (m_originalPopSize != 0) {
          scaleFactor = requestedPopSize / m_originalPopSize;
        }
        
        if (tpCost == m_tpPrevious && fpCost == m_fpPrevious &&
            tnCost == m_tnPrevious && fnCost == m_fnPrevious &&
            requestedPopSize == m_totalPopPrevious) {
          return false;
        }
        
        // First construct some Instances for the curve
        FastVector fv = new FastVector();
        fv.addElement(new Attribute("Sample Size"));
        fv.addElement(new Attribute("Cost/Benefit"));
        Instances costBenefitI = new Instances("Cost/Benefit Curve", fv, 100);
        
        // process the performance data to make this curve
        Instances performanceI = m_masterPlot.getPlotInstances();
        
        for (int i = 0; i < performanceI.numInstances(); i++) {
          Instance current = performanceI.instance(i);
          
          double[] vals = new double[2];
          vals[0] = current.value(10); // sample size
          vals[1] = (current.value(0) * tpCost
              + current.value(1) * fnCost
              + current.value(2) * fpCost
              + current.value(3) * tnCost) * scaleFactor;
          Instance newInst = new DenseInstance(1.0, vals);
          costBenefitI.add(newInst);
        }
        
        costBenefitI.compactify();
        
        // now set up the plot data
        m_costBenefit = new PlotData2D(costBenefitI);
        m_costBenefit.m_alwaysDisplayPointsOfThisSize = 10;
        m_costBenefit.setPlotName("Cost/benefit curve");
        boolean[] connectPoints = new boolean[costBenefitI.numInstances()];
        
        for (int i = 0; i < connectPoints.length; i++) {
          connectPoints[i] = true;
        }
        try {
          m_costBenefit.setConnectPoints(connectPoints);
          m_costBenefit.setShapeSize(m_shapeSizes);
        } catch (Exception ex) {
          // ignore
        }
        
        m_tpPrevious = tpCost;
        m_fpPrevious = fpCost;
        m_tnPrevious = tnCost;
        m_fnPrevious = fnCost;
        
        return true;
      }
    }
    
    /**
     * Constructor.
     */
    public CostBenefitAnalysis() {
      java.awt.GraphicsEnvironment ge = 
        java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment();
      if (!ge.isHeadless()) {
        appearanceFinal();
      } else {
        m_headlessEvents = new ArrayList<EventObject>();
      }
    }
    
    /**
     * Global info for this bean
     *
     * @return a <code>String</code> value
     */
    public String globalInfo() {
      return "Visualize performance charts (such as ROC).";
    }
  
    /**
     * Accept a threshold data event and set up the visualization.
     * @param e a threshold data event
     */
    public void acceptDataSet(ThresholdDataEvent e) {
      if (!GraphicsEnvironment.isHeadless()) {  
        try {
          setCurveData(e.getDataSet(), e.getClassAttribute());
        } catch (Exception ex) {
          System.err.println("[CostBenefitAnalysis] Problem setting up visualization.");
          ex.printStackTrace();
        }
      } else {
        m_headlessEvents = new ArrayList<EventObject>();
        m_headlessEvents.add(e);
      }
    }
    
    /**
     * Set the threshold curve data to use.
     * 
     * @param curveData a PlotData2D object set up with the curve data.
     * @param origClassAtt the class attribute from the original data used to
     * generate the curve.
     * @throws Exception if somthing goes wrong during the setup process.
     */
    public void setCurveData(PlotData2D curveData, Attribute origClassAtt) 
      throws Exception {
  
      if (m_analysisPanel == null) {
        m_analysisPanel = new AnalysisPanel();
      }
      m_analysisPanel.setDataSet(curveData, origClassAtt);
    }
  
    public BeanVisual getVisual() {
      return m_visual;
    }
  
    public void setVisual(BeanVisual newVisual) {
      m_visual = newVisual;
    }
  
    public void useDefaultVisual() {
      m_visual.loadIcons(BeanVisual.ICON_PATH+"DefaultDataVisualizer.gif",
          BeanVisual.ICON_PATH+"DefaultDataVisualizer_animated.gif");
    }
  
    public Enumeration enumerateRequests() {
      Vector newVector = new Vector(0);
      if (m_analysisPanel != null) {
        if (m_analysisPanel.m_masterPlot != null) {
          newVector.addElement("Show analysis");
        }
      }
      return newVector.elements();
    }
  
    public void performRequest(String request) {
      if (request.compareTo("Show analysis") == 0) {
        try {
          // popup visualize panel
          if (!m_framePoppedUp) {
            m_framePoppedUp = true;
  
            final javax.swing.JFrame jf = 
              new javax.swing.JFrame("Cost/Benefit Analysis");
            jf.setSize(1000,600);
            jf.getContentPane().setLayout(new BorderLayout());
            jf.getContentPane().add(m_analysisPanel, BorderLayout.CENTER);
            jf.addWindowListener(new java.awt.event.WindowAdapter() {
                public void windowClosing(java.awt.event.WindowEvent e) {
                  jf.dispose();
                  m_framePoppedUp = false;
                }
              });
            jf.setVisible(true);
            m_popupFrame = jf;
          } else {
            m_popupFrame.toFront();
          }
        } catch (Exception ex) {
          ex.printStackTrace();
          m_framePoppedUp = false;
        }
      } else {
        throw new IllegalArgumentException(request
            + " not supported (Cost/Benefit Analysis");
      }
    }
  
    public void addVetoableChangeListener(String name, VetoableChangeListener vcl) {
      m_bcSupport.addVetoableChangeListener(name, vcl);
    }
  
    public BeanContext getBeanContext() {
      return m_beanContext;
    }
  
    public void removeVetoableChangeListener(String name,
        VetoableChangeListener vcl) {
      m_bcSupport.removeVetoableChangeListener(name, vcl);
    }
    
    protected void appearanceFinal() {
      removeAll();
      setLayout(new BorderLayout());
      setUpFinal();
    }
    
    protected void setUpFinal() {
      if (m_analysisPanel == null) {
        m_analysisPanel = new AnalysisPanel();
      }
      add(m_analysisPanel, BorderLayout.CENTER);
    }
    
    protected void appearanceDesign() {
      removeAll();
      useDefaultVisual();
      setLayout(new BorderLayout());
      add(m_visual, BorderLayout.CENTER);
    }
  
    public void setBeanContext(BeanContext bc) throws PropertyVetoException {
      m_beanContext = bc;
      m_design = m_beanContext.isDesignTime();
      if (m_design) {
        appearanceDesign();
      } else {
        java.awt.GraphicsEnvironment ge = 
          java.awt.GraphicsEnvironment.getLocalGraphicsEnvironment(); 
        if (!ge.isHeadless()) {
          appearanceFinal();
        }
      }
    }
    
    /**
     * Returns true if, at this time, 
     * the object will accept a connection via the named event
     *
     * @param eventName the name of the event in question
     * @return true if the object will accept a connection
     */
    public boolean connectionAllowed(String eventName) {
      return (m_listenee == null);
    }
  
    /**
     * Notify this object that it has been registered as a listener with
     * a source for recieving events described by the named event
     * This object is responsible for recording this fact.
     *
     * @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;
      }
    }
    
    /**
     * 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());
    }
  
    /**
     * Notify this object that it has been deregistered as a listener with
     * a source for named event. This object is responsible
     * for recording this fact.
     *
     * @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;
      }
      
    }
  
    /**
     * 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
     * (i.e. perhaps a worker thread is performing some calculation).
     * 
     * @return true if the bean is busy.
     */
    public boolean isBusy() {
      return false;
    }
  
    /**
     * Set a custom (descriptive) name for this bean
     * 
     * @param name the name to use
     */
    public void setCustomName(String name) {
      m_visual.setText(name);
    }
  
    /**
     * Set a logger
     *
     * @param logger a <code>weka.gui.Logger</code> value
     */
    public void setLog(Logger logger) {
      // we don't need to do any logging    
    }
  
    /**
     * Stop any processing that the bean might be doing.
     */
    public void stop() {
      // nothing to do here
    }
      
    public static void main(String[] args) {
      try {
        Instances train = new Instances(new java.io.BufferedReader(new java.io.FileReader(args[0])));
        train.setClassIndex(train.numAttributes() - 1);
        weka.classifiers.evaluation.ThresholdCurve tc = 
          new weka.classifiers.evaluation.ThresholdCurve();
        weka.classifiers.evaluation.EvaluationUtils eu = 
          new weka.classifiers.evaluation.EvaluationUtils();
        //weka.classifiers.Classifier classifier = new weka.classifiers.functions.Logistic();
        weka.classifiers.Classifier classifier = new weka.classifiers.bayes.NaiveBayes();
        FastVector predictions = new FastVector();
        eu.setSeed(1);
        predictions.appendElements(eu.getCVPredictions(classifier, train, 10));
        Instances result = tc.getCurve(predictions, 0);
        PlotData2D pd = new PlotData2D(result);
        pd.m_alwaysDisplayPointsOfThisSize = 10;
  
        boolean[] connectPoints = new boolean[result.numInstances()];
        for (int i = 1; i < connectPoints.length; i++) {
          connectPoints[i] = true;
        }
        pd.setConnectPoints(connectPoints);
        final javax.swing.JFrame jf = 
          new javax.swing.JFrame("CostBenefitTest");
        jf.setSize(1000,600);
        //jf.pack();
        jf.getContentPane().setLayout(new BorderLayout());
        final CostBenefitAnalysis.AnalysisPanel analysisPanel = 
          new CostBenefitAnalysis.AnalysisPanel();
        
        jf.getContentPane().add(analysisPanel, BorderLayout.CENTER);
        jf.addWindowListener(new java.awt.event.WindowAdapter() {
          public void windowClosing(java.awt.event.WindowEvent e) {
            jf.dispose();
            System.exit(0);
          }
        });
        
        jf.setVisible(true);
        
        analysisPanel.setDataSet(pd, train.classAttribute());
        
      } catch (Exception ex) {
        ex.printStackTrace();
      }
   
    }
  
    /**
     * Get the list of events processed in headless mode. May return
     * null or an empty list if not running in headless mode or no
     * events were processed
     * 
     * @return a list of EventObjects or null.
     */
    public List<EventObject> retrieveHeadlessEvents() {
      return m_headlessEvents;
    }
  
    /**
     * Process a list of events that have been collected earlier. Has
     * no affect if the component is running in headless mode.
     * 
     * @param headless a list of EventObjects to process.
     */
    public void processHeadlessEvents(List<EventObject> headless) {
      // only process if we're not headless
      if (!GraphicsEnvironment.isHeadless()) {
        for (EventObject e : headless) {
          if (e instanceof ThresholdDataEvent) {
            acceptDataSet((ThresholdDataEvent)e);
          }
        }
      }    
    }
  }
  