code: ///////////////////////////////////////////////////////////////////////////////
  // For information as to what this class does, see the Javadoc, below.       //
  // Copyright (C) 1998, 1999, 2000, 2001, 2002, 2003, 2004, 2005, 2006,       //
  // 2007, 2008, 2009, 2010, 2014, 2015 by Peter Spirtes, Richard Scheines, Joseph   //
  // Ramsey, and Clark Glymour.                                                //
  //                                                                           //
  // 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., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA //
  ///////////////////////////////////////////////////////////////////////////////
  
  package edu.cmu.tetrad.search;
  
  import edu.cmu.tetrad.bayes.*;
  import edu.cmu.tetrad.data.DataSet;
  import edu.cmu.tetrad.data.DataUtils;
  import edu.cmu.tetrad.data.DiscreteVariable;
  import edu.cmu.tetrad.graph.Node;
  import edu.cmu.tetrad.util.TetradLogger;
  import edu.cmu.tetrad.util.TetradSerializable;
  
  import java.io.IOException;
  import java.io.ObjectInputStream;
  import java.util.Arrays;
  import java.util.LinkedList;
  import java.util.List;
  
  /**
   * This class contains a method classify which uses an instantiated Bayes net (BayesIm) provided in the constructor. For
   * each case (record) in  the DataSet it uses the values of all variables but the target variable to update the
   * distributions of all the variables.  It then computes an estimated value for the target variable by selecting the
   * value with the greatest probability in the updated distribution.  The method returns a crosstabulation table in the
   * form of a two-dimensional integer array in which coefs of observed versus estimated values of the target variable
   * are stored. </p> Note that the variables must be the same in the dataset and the Bayes net.
   *
   * @author Frank Wimberly based on a specification by Clark Glymour
   */
  public final class BayesUpdaterClassifier
          implements DiscreteClassifier, TetradSerializable {
      static final long serialVersionUID = 23L;
  
      /**
       * The BayesIm instance used to create an updater.  Supplied as an argument
       * to the constructor.
       *
       * @serial
       */
      private BayesIm bayesIm;
  
      /**
       * The dataset to be classified.
       *
       * @serial
       */
      private DataSet testData;
  
      /**
       * The percentage of correct estimates of the target variable.  This will be
       * set to a meaningful value upon completion of the crossTabulate method.
       *
       * @serial
       */
      private double percentCorrect;
  
      /**
       * The target variable (inferred from its name).
       *
       * @serial
       */
      private DiscreteVariable targetVariable;
  
      /**
       * True iff a missing value case was found on the last run through the
       * data.
       *
       * @serial
       */
      private boolean missingValueCaseFound;
  
      /**
       * The cutoff used for binary classifications.
       *
       * @serial
       */
      private double binaryCutoff = 0.5;
  
      /*
       * The variables in the dataset to be classified.  These should be
       * the same variables as in the training dataset according to the
       * "equals" method of DiscreteVariable.
       * @serial
       */
      private List<Node> bayesImVars;
  
      /**
       * @serial
       */
      private int[] classifications;
  
      /**
       * @serial
       */
      private double[][] marginals;
  
      /**
       * @serial
       */
      private int numCases = -1;
  
      /**
       * @serial
       */
      private int totalUsableCases;
  
      //===========================CONSTRUCTORS==========================//
  
      /*
       * The constructor sets the values of the private member variables.
       */
  
      public BayesUpdaterClassifier(BayesIm bayesIm,
                                    DataSet testData) {
          if (bayesIm == null) {
              throw new IllegalArgumentException("BayesIm must not be null.");
          }
  
          if (testData == null) {
              throw new IllegalArgumentException("DataSet must not be null.");
          }
  
          this.bayesIm = bayesIm;
          this.testData = testData;
          this.percentCorrect = Double.NaN;
          this.bayesImVars = new LinkedList<>(bayesIm.getVariables());
      }
  
      /**
       * Generates a simple exemplar of this class to test serialization.
       */
      public static BayesUpdaterClassifier serializableInstance() {
          return new BayesUpdaterClassifier(MlBayesIm.serializableInstance(),
                  DataUtils.discreteSerializableInstance());
      }
  
      //==========================PUBLIC METHODS========================//
  
      public void setTarget(String target, int targetCategory) {
  
          //Find the target variable using its name.
          DiscreteVariable targetVariable = null;
  
          for (int j = 0; j < getBayesImVars().size(); j++) {
              DiscreteVariable dv = (DiscreteVariable) getBayesImVars().get(j);
  
              if (dv.getName().equals(target)) {
                  targetVariable = dv;
                  break;
              }
          }
  
          if (targetVariable == null) {
              throw new IllegalArgumentException(
                      "Not an available target: " + target);
          }
  
          this.targetVariable = targetVariable;
      }
  
      /**
       * Computes and returns the crosstabulation of observed versus estimated
       * values of the target variable as described above.
       */
      public int[] classify() {
          if (targetVariable == null) {
              throw new NullPointerException("Target not set.");
          }
  
          //Create an updater for the instantiated Bayes net.
          BayesUpdater bayesUpdater = new RowSummingExactUpdater(getBayesIm());
  
          //Get the raw data from the dataset to be classified, the number
          //of variables and the number of cases.
          int nvars = getBayesImVars().size();
          int ncases = testData.getNumRows();
  
          int[] varIndices = new int[nvars];
          List<Node> dataVars = testData.getVariables();
  
          for (int i = 0; i < nvars; i++) {
              DiscreteVariable variable =
                      (DiscreteVariable) getBayesImVars().get(i);
  
  
              if (variable == targetVariable) {
                  continue;
              }
  
              varIndices[i] = dataVars.indexOf(variable);
  
              if (varIndices[i] == -1) {
                  throw new IllegalArgumentException(
                          "Can't find the (non-target) variable " + variable +
                                  " in the data. Either it's not there, or else its " +
                                  "categories are in a different order.");
              }
          }
  
          DataSet selectedData = testData.subsetColumns(varIndices);
  
          this.numCases = ncases;
  
          int[] estimatedValues = new int[ncases];
          int numTargetCategories = targetVariable.getNumCategories();
          double[][] probOfClassifiedValues =
                  new double[numTargetCategories][ncases];
          Arrays.fill(estimatedValues, -1);
  
          //For each case in the dataset to be classified compute the estimated
          //value of the target variable and increment the appropriate element
          //of the crosstabulation array. Compute the estimated value of the
          // target variable by using the observed values of the other variables
          // and Bayesian updating.
          for (int i = 0; i < ncases; i++) {
  
              //Create an Evidence instance for the instantiated Bayes net
              //which will allow that updating.
              Evidence evidence = Evidence.tautology(getBayesIm());
  
              //Let the target variable range over all its values.
              int itarget = evidence.getNodeIndex(targetVariable.getName());
              evidence.getProposition().setVariable(itarget, true);
  
              this.missingValueCaseFound = false;
  
              //Restrict all other variables to their observed values in
              //this case.
              for (int j = 0; j < getBayesImVars().size(); j++) {
                  if (j == getBayesImVars().indexOf(targetVariable)) {
                      continue;
                  }
  
                  int observedValue = selectedData.getInt(i, j);
  
                  if (observedValue == DiscreteVariable.MISSING_VALUE) {
                      this.missingValueCaseFound = true;
                      continue;
                  }
  
                  String jName = getBayesImVars().get(j).getName();
                  int jIndex = evidence.getNodeIndex(jName);
                  evidence.getProposition().setCategory(jIndex, observedValue);
              }
  
              //Update using those values.
              bayesUpdater.setEvidence(evidence);
  
              //for each possible value of target compute its probability in
              //the updated Bayes net.  Select the value with the highest
              //probability as the estimated value.
              Node targetNode = getBayesIm().getNode(targetVariable.getName());
              int indexTargetBN = getBayesIm().getNodeIndex(targetNode);
  
              //Straw man values--to be replaced.
              int estimatedValue = -1;
  
  //            if (numTargetCategories == 2) {
  //                for (int j = 0; j < numTargetCategories; j++) {
  //                    double marginal =
  //                            bayesUpdater.getMarginal(indexTargetBN, j);
  //                    probOfClassifiedValues[j][i] = marginal;
  //                    probOfClassifiedValues[1 - j][i] = 1.0 - marginal;
  //
  //                    if (targetCategory == j) {
  //                        if (marginal > binaryCutoff) {
  //                            estimatedValue = j;
  //                        } else {
  //                            estimatedValue = 1 - j;
  //                        }
  //
  //                        break;
  //                    }
  //                }
  //            } else
              {
                  double highestProb = -0.1;
  
                  for (int j = 0; j < numTargetCategories; j++) {
                      double marginal =
                              bayesUpdater.getMarginal(indexTargetBN, j);
                      probOfClassifiedValues[j][i] = marginal;
  
                      if (marginal >= highestProb) {
                          highestProb = marginal;
                          estimatedValue = j;
                      }
                  }
              }
  
              //Sometimes the marginal cannot be computed because certain
              //combinations of values of the variables do not occur in the
              //training dataset.  If that happens skip the case.
              if (estimatedValue < 0) {
                  TetradLogger.getInstance().log("details", "Case " + i + " does not return valid marginal.");
  
                  for (int m = 0; m < nvars; m++) {
                      //System.out.print(getBayesImVars()
                      //        .get(m).getNode());
                      TetradLogger.getInstance().log("details", "  " + selectedData.getDouble(i, m));
                  }
  
                  estimatedValues[i] = DiscreteVariable.MISSING_VALUE;
                  continue;
              }
  
              estimatedValues[i] = estimatedValue;
          }
  
          this.classifications = estimatedValues;
          this.marginals = probOfClassifiedValues;
  
          return estimatedValues;
      }
  
      /**
       * Computes the "confusion matrix" of coefs of the number of cases
       * associated with each combination of estimated and observed values in the
       * test dataset.  Each row, column i,j corresponds to the ith and jth
       * categories of the target variable.
       *
       * @return an int[][] array containing the coefs, or null if the target
       * variable is not in the test data.
       */
      public int[][] crossTabulation() {
          int[] estimatedValues = classify();
  
          // Retrieve the column for the test variable from the test data; these
          // will be the observed values.
          Node variable = testData.getVariable(targetVariable.getName());
          int varIndex = testData.getVariables().indexOf(variable);
  
          if (variable == null) {
              return null;
          }
  
          int ncases = testData.getNumRows();
  
          // Create a cross-tabulation table to store the coefs of observed
          // versus estimated occurrences of each value of the target variable.
          int nvalues = targetVariable.getNumCategories();
          int[][] crosstabs = new int[nvalues][nvalues];
          for (int i = 0; i < nvalues; i++) {
              for (int j = 0; j < nvalues; j++) {
                  crosstabs[i][j] = 0;
              }
          }
  
          //Will count the number of cases where the target variable
          //is correctly classified.
          int numberCorrect = 0;
          int ntot = 0;
  
          for (int i = 0; i < ncases; i++) {
              int estimatedValue = estimatedValues[i];
              int observedValue = testData.getInt(i, varIndex);
  
              if (estimatedValue < 0) {
                  continue;
              }
              if (observedValue < 0) {
                  continue;
              }
              ntot++;
              crosstabs[observedValue][estimatedValue]++;
              if (observedValue == estimatedValue) {
                  numberCorrect++;
              }
          }
  
          this.percentCorrect =
                  100.0 * ((double) numberCorrect) / ((double) ncases);
          this.totalUsableCases = ntot;
  
          return crosstabs;
      }
  
      /**
       * @return the percentage of cases in which the target variable is correctly
       * classified.
       */
      public double getPercentCorrect() {
          if (Double.isNaN(percentCorrect)) {
              crossTabulation();
          }
          return percentCorrect;
      }
  
      /**
       * @return the DiscreteVariable which is the target variable.
       */
      public DiscreteVariable getTargetVariable() {
          return targetVariable;
      }
  
      public BayesIm getBayesIm() {
          return bayesIm;
      }
  
      public DataSet getTestData() {
          return testData;
      }
  
      public int[] getClassifications() {
          return classifications;
      }
  
      public double[][] getMarginals() {
          return marginals;
      }
  
      public int getNumCases() {
          return numCases;
      }
  
      public int getTotalUsableCases() {
          return totalUsableCases;
      }
  
      public boolean isMissingValueCaseFound() {
          return missingValueCaseFound;
      }
  
      public double getBinaryCutoff() {
          return binaryCutoff;
      }
  
      public void setBinaryCutoff(double binaryCutoff) {
          if (binaryCutoff < 0.0 || binaryCutoff > 1.0) {
              throw new IllegalArgumentException();
          }
  
          this.binaryCutoff = binaryCutoff;
      }
  
      public List<Node> getBayesImVars() {
          return bayesImVars;
      }
  
      /**
       * Adds semantic checks to the default deserialization method. This method
       * must have the standard signature for a readObject method, and the body of
       * the method must begin with "s.defaultReadObject();". Other than that, any
       * semantic checks can be specified and do not need to stay the same from
       * version to version. A readObject method of this form may be added to any
       * class, even if Tetrad sessions were previously saved out using a version
       * of the class that didn't include it. (That's what the
       * "s.defaultReadObject();" is for. See J. Bloch, Effective Java, for help.
       *
       * @throws java.io.IOException
       * @throws ClassNotFoundException
       */
      private void readObject(ObjectInputStream s)
              throws IOException, ClassNotFoundException {
          s.defaultReadObject();
  
          if (bayesIm == null) {
              throw new NullPointerException();
          }
  
          if (testData == null) {
              throw new NullPointerException();
          }
  
          if (getBayesImVars() == null) {
              //Assumes all implementations of BayesIM create a non-null variables list.
              throw new NullPointerException();
          }
  
          if (binaryCutoff < 0.0 || binaryCutoff > 1.0) {
              throw new IllegalStateException();
          }
      }
  }
  
  
  
  