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.
   */
  
  /*
   *    GeneralRegression.java
   *    Copyright (C) 2008 University of Waikato, Hamilton, New Zealand
   *
   */
  
  package weka.classifiers.pmml.consumer;
  
  import java.io.Serializable;
  import java.util.ArrayList;
  import org.w3c.dom.Element;
  import org.w3c.dom.Node;
  import org.w3c.dom.NodeList;
  
  import weka.core.Attribute;
  import weka.core.Instance;
  import weka.core.Instances;
  import weka.core.RevisionUtils;
  import weka.core.Utils;
  import weka.core.pmml.*;
  
  /**
   * Class implementing import of PMML General Regression model. Can be
   * used as a Weka classifier for prediction (buildClassifier()
   * raises an Exception).
   *
   * @author Mark Hall (mhall{[at]}pentaho{[dot]}com)
   * @version $Revision: 5987 $
   */
  public class GeneralRegression extends PMMLClassifier
    implements Serializable {
  
    /**
     * For serialization
     */
    private static final long serialVersionUID = 2583880411828388959L;
  
    /**
     * Enumerated type for the model type.
     */
    enum ModelType {
  
      // same type of model
      REGRESSION ("regression"), 
        GENERALLINEAR ("generalLinear"), 
        MULTINOMIALLOGISTIC ("multinomialLogistic"),
        ORDINALMULTINOMIAL ("ordinalMultinomial"), 
        GENERALIZEDLINEAR ("generalizedLinear");
  
      private final String m_stringVal;
      ModelType(String name) {
        m_stringVal = name;
      }
      
      public String toString() {
        return m_stringVal;
      }
    }
    
    // the model type
    protected ModelType m_modelType = ModelType.REGRESSION;
  
    // the model name (if defined)
    protected String m_modelName;
      
    // the algorithm name (if defined)
    protected String m_algorithmName;
  
    // the function type (regression or classification)
    protected int m_functionType = Regression.RegressionTable.REGRESSION;
  
    /**
     * Enumerated type for the cumulative link function
     * (ordinal multinomial model type only).
     */
    enum CumulativeLinkFunction {
      NONE ("none") {
        double eval(double value, double offset) {
          return Double.NaN; // no evaluation defined in this case!
        }
      },
      LOGIT ("logit") {
        double eval(double value, double offset) {
          return 1.0 / (1.0 + Math.exp(-(value + offset)));
        }
      },
      PROBIT ("probit") {
        double eval(double value, double offset) {
          return weka.core.matrix.Maths.pnorm(value + offset); 
        }
      },
      CLOGLOG ("cloglog") {
        double eval(double value, double offset) {
          return 1.0 - Math.exp(-Math.exp(value + offset));
        }
      },
      LOGLOG ("loglog") {
        double eval(double value, double offset) {
          return Math.exp(-Math.exp(-(value + offset))); 
        }
      },
      CAUCHIT ("cauchit") {
        double eval(double value, double offset) {
          return 0.5 + (1.0 / Math.PI) * Math.atan(value + offset);
        }
      };
  
      /**
       * Evaluation function.
       * 
       * @param value the raw response value
       * @param offset the offset to add to the raw value 
       * @return the result of the link function
       */
      abstract double eval(double value, double offset);
      
      private final String m_stringVal;
      
      /**
       * Constructor
       * 
       * @param name textual name for this enum
       */
      CumulativeLinkFunction(String name) {
        m_stringVal = name;
      }
      
      /* (non-Javadoc)
       * @see java.lang.Enum#toString()
       */
      public String toString() {
        return m_stringVal;
      }
    }
    
    // cumulative link function (ordinal multinomial only)
    protected CumulativeLinkFunction m_cumulativeLinkFunction 
      = CumulativeLinkFunction.NONE;
  
  
    /**
     * Enumerated type for the link function (general linear and
     * generalized linear model types only).
     */
    enum LinkFunction {
      NONE ("none") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return Double.NaN; // no evaluation defined in this case!
        }
      },
      CLOGLOG ("cloglog") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return (1.0 - Math.exp(-Math.exp(value + offset))) * trials;
        }
      },
      IDENTITY ("identity") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return (value + offset) * trials;
        }
      },
      LOG ("log") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return Math.exp(value + offset) * trials;
        }
      },
      LOGC ("logc") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return (1.0 - Math.exp(value + offset)) * trials;
        }
      },
      LOGIT ("logit") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return (1.0 / (1.0 + Math.exp(-(value + offset)))) * trials;
        }
      },
      LOGLOG ("loglog") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return Math.exp(-Math.exp(-(value + offset))) * trials;
        }
      },
      NEGBIN ("negbin") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return (1.0 / (distParam * (Math.exp(-(value + offset)) - 1.0))) * trials;
        }
      },
      ODDSPOWER ("oddspower") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return (linkParam < 0.0 || linkParam > 0.0)
          ? (1.0 / (1.0 + Math.pow(1.0 + linkParam * (value + offset), (-1.0 / linkParam)))) * trials
          : (1.0 / (1.0 + Math.exp(-(value + offset)))) * trials;
        }
      },
      POWER ("power") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return (linkParam < 0.0 || linkParam > 0.0)
          ? Math.pow(value + offset, (1.0 / linkParam)) * trials
              : Math.exp(value + offset) * trials;
        }
      },
      PROBIT ("probit") {
        double eval(double value, double offset, double trials,
                    double distParam, double linkParam) {
          return weka.core.matrix.Maths.pnorm(value + offset) * trials;
        }
      };
  
      /**
       * Evaluation function.
       * 
       * @param value the raw response value
       * @param offset the offset to add to the raw value
       * @param trials the trials value to multiply the result by
       * @param distParam the distribution parameter (negbin only)
       * @param linkParam the link parameter (power and oddspower only) 
       * @return the result of the link function
       */
      abstract double eval(double value, double offset, double trials, 
                           double distParam, double linkParam);
      
      private final String m_stringVal;
      
      /**
       * Constructor.
       * 
       * @param name the textual name of this link function
       */
      LinkFunction(String name) {
        m_stringVal = name;
      }
  
      /* (non-Javadoc)
       * @see java.lang.Enum#toString()
       */
      public String toString() {
        return m_stringVal;
      }
    }
    
    // link function (generalLinear model type only)
    protected LinkFunction m_linkFunction = LinkFunction.NONE;
    protected double m_linkParameter = Double.NaN;
    protected String m_trialsVariable;
    protected double m_trialsValue = Double.NaN;
  
    /**
     * Enumerated type for the distribution (general linear
     * and generalized linear model types only).
     */
    enum Distribution {
      NONE ("none"),
      NORMAL ("normal"),
      BINOMIAL ("binomial"),
      GAMMA ("gamma"),
      INVGAUSSIAN ("igauss"),
      NEGBINOMIAL ("negbin"),
      POISSON ("poisson");
  
      private final String m_stringVal;
      Distribution(String name) {
        m_stringVal = name;
      }
  
      /* (non-Javadoc)
       * @see java.lang.Enum#toString()
       */
      public String toString() {
        return m_stringVal;
      }
    }
    
    // generalLinear and generalizedLinear model type only
    protected Distribution m_distribution = Distribution.NORMAL;
  
    // ancillary parameter value for the negative binomial distribution
    protected double m_distParameter = Double.NaN;
  
    // if present, this variable is used during scoring generalizedLinear/generalLinear or
    // ordinalMultinomial models
    protected String m_offsetVariable;
  
    // if present, this variable is used during scoring generalizedLinear/generalLinear or
    // ordinalMultinomial models. It works like a user-specified intercept.
    // At most, only one of offsetVariable or offsetValue may be specified.
    protected double m_offsetValue = Double.NaN;
  
    /**
     * Small inner class to hold the name of a parameter plus
     * its optional descriptive label
     */
    static class Parameter implements Serializable {
      // ESCA-JAVA0096:
      /** For serialization */
      // CHECK ME WITH serialver
      private static final long serialVersionUID = 6502780192411755341L;
  
      protected String m_name = null;
      protected String m_label = null;
    }
  
    // List of model parameters
    protected ArrayList<Parameter> m_parameterList = new ArrayList<Parameter>();
  
    /**
     * Small inner class to hold the name of a factor or covariate,
     * plus the index of the attribute it corresponds to in the
     * mining schema.
     */
    static class Predictor implements Serializable {
      /** For serialization */
      // CHECK ME WITH serialver
      private static final long serialVersionUID = 6502780192411755341L;
  
      protected String m_name = null;
      protected int m_miningSchemaIndex = -1;
      
      public String toString() {
        return m_name;
      }
    }
    
    // FactorList
    protected ArrayList<Predictor> m_factorList = new ArrayList<Predictor>();
  
    // CovariateList
    protected ArrayList<Predictor> m_covariateList = new ArrayList<Predictor>();
  
    /**
     * Small inner class to hold details on a predictor-to-parameter
     * correlation.
     */
    static class PPCell implements Serializable {
      /** For serialization */
      // CHECK ME WITH serialver
      private static final long serialVersionUID = 6502780192411755341L;
      
      protected String m_predictorName = null;
      protected String m_parameterName = null;
  
      // either the exponent of a numeric attribute or the index of
      // a discrete value
      protected double m_value = 0;
  
      // optional. The default is for all target categories to
      // share the same PPMatrix.
      // TO-DO: implement multiple PPMatrixes 
      protected String m_targetCategory = null;
      
    }
    
    // PPMatrix (predictor-to-parameter matrix)
    // rows = parameters, columns = predictors (attributes)
    protected PPCell[][] m_ppMatrix;
  
    /**
     * Small inner class to hold a single entry in the 
     * ParamMatrix (parameter matrix).
     */
    static class PCell implements Serializable {
      
      /** For serialization */
      // CHECK ME WITH serialver
      private static final long serialVersionUID = 6502780192411755341L;
  
      // may be null for numeric target. May also be null if this coefficent
      // applies to all target categories.
      protected String m_targetCategory = null;
      protected String m_parameterName = null;
      // coefficient
      protected double m_beta = 0.0;
      // optional degrees of freedom
      protected int m_df = -1;
    }
    
    // ParamMatrix. rows = target categories (only one if target is numeric),
    // columns = parameters (in order that they occur in the parameter list).
    protected PCell[][] m_paramMatrix;
  
    /**
     * Constructs a GeneralRegression classifier.
     * 
     * @param model the Element that holds the model definition
     * @param dataDictionary the data dictionary as a set of Instances
     * @param miningSchema the mining schema
     * @throws Exception if there is a problem constructing the general regression
     * object from the PMML.
     */
    public GeneralRegression(Element model, Instances dataDictionary,
                             MiningSchema miningSchema) throws Exception {
  
      super(dataDictionary, miningSchema);
   
      // get the model type
      String mType = model.getAttribute("modelType");
      boolean found = false;
      for (ModelType m : ModelType.values()) {
        if (m.toString().equals(mType)) {
          m_modelType = m;
          found = true;
          break;
        }      
      }
      if (!found) {
        throw new Exception("[GeneralRegression] unknown model type: " + mType);
      }
  
      if (m_modelType == ModelType.ORDINALMULTINOMIAL) {
        // get the cumulative link function
        String cLink = model.getAttribute("cumulativeLink");
        found = false;
        for (CumulativeLinkFunction c : CumulativeLinkFunction.values()) {
          if (c.toString().equals(cLink)) {
            m_cumulativeLinkFunction = c;
            found = true;
            break;
          }
        }
        if (!found) {
          throw new Exception("[GeneralRegression] cumulative link function " + cLink);
        }
      } else if (m_modelType == ModelType.GENERALIZEDLINEAR || 
                  m_modelType == ModelType.GENERALLINEAR) {
        // get the link function
        String link = model.getAttribute("linkFunction");
        found = false;
        for (LinkFunction l : LinkFunction.values()) {
          if (l.toString().equals(link)) {
            m_linkFunction = l;
            found = true;
            break;
          }
        }
        if (!found) {
          throw new Exception("[GeneralRegression] unknown link function " + link);
        }
  
        // get the link parameter
        String linkP = model.getAttribute("linkParameter");
        if (linkP != null && linkP.length() > 0) {
          try {
            m_linkParameter = Double.parseDouble(linkP);
          } catch (IllegalArgumentException ex) {
            throw new Exception("[GeneralRegression] unable to parse the link parameter");
          }
        }
  
        // get the trials variable
        String trials = model.getAttribute("trialsVariable");
        if (trials != null && trials.length() > 0) {
          m_trialsVariable = trials;
        }
  
        // get the trials value
        String trialsV = model.getAttribute("trialsValue");
        if (trialsV != null && trialsV.length() > 0) {
          try {
            m_trialsValue = Double.parseDouble(trialsV);
          } catch (IllegalArgumentException ex) {
            throw new Exception("[GeneralRegression] unable to parse the trials value"); 
          }
        }
      }
    
      String mName = model.getAttribute("modelName");
      if (mName != null && mName.length() > 0) {
        m_modelName = mName;
      }
  
      String fName = model.getAttribute("functionName");
      if (fName.equals("classification")) {
        m_functionType = Regression.RegressionTable.CLASSIFICATION;
      }
  
      String algName = model.getAttribute("algorithmName");
      if (algName != null && algName.length() > 0) {
        m_algorithmName = algName;
      }
  
      String distribution = model.getAttribute("distribution");
      if (distribution != null && distribution.length() > 0) {
        found = false;
        for (Distribution d : Distribution.values()) {
          if (d.toString().equals(distribution)) {
            m_distribution = d;
            found = true;
            break;
          }
        }
        if (!found) {
          throw new Exception("[GeneralRegression] unknown distribution type " + distribution);
        }
      }
  
      String distP = model.getAttribute("distParameter");
      if (distP != null && distP.length() > 0) {
        try {
          m_distParameter = Double.parseDouble(distP);
        } catch (IllegalArgumentException ex) {
          throw new Exception("[GeneralRegression] unable to parse the distribution parameter");
        }
      }
  
      String offsetV = model.getAttribute("offsetVariable");
      if (offsetV != null && offsetV.length() > 0) {
         m_offsetVariable = offsetV;
      }
  
      String offsetVal = model.getAttribute("offsetValue");
      if (offsetVal != null && offsetVal.length() > 0) {
        try {
          m_offsetValue = Double.parseDouble(offsetVal);
        } catch (IllegalArgumentException ex) {
          throw new Exception("[GeneralRegression] unable to parse the offset value");
        }
      }
  
      // get the parameter list
      readParameterList(model);
      
      // get the factors and covariates
      readFactorsAndCovariates(model, "FactorList");
      readFactorsAndCovariates(model, "CovariateList");
  
      // read the PPMatrix
      readPPMatrix(model);
  
      // read the parameter estimates
      readParamMatrix(model);
    }
  
    /**
     * Read the list of parameters.
     *
     * @param model the Element that contains the model
     * @throws Exception if there is some problem with extracting the
     * parameters.
     */
    protected void readParameterList(Element model) throws Exception {
      NodeList paramL = model.getElementsByTagName("ParameterList");
  
      // should be just one parameter list
      if (paramL.getLength() == 1) {
        Node paramN = paramL.item(0);
        if (paramN.getNodeType() == Node.ELEMENT_NODE) {
          NodeList parameterList = ((Element)paramN).getElementsByTagName("Parameter");
          for (int i = 0; i < parameterList.getLength(); i++) {
            Node parameter = parameterList.item(i);
            if (parameter.getNodeType() == Node.ELEMENT_NODE) {
              Parameter p = new Parameter();
              p.m_name = ((Element)parameter).getAttribute("name");
              String label = ((Element)parameter).getAttribute("label");
              if (label != null && label.length() > 0) {
                p.m_label = label;
              }
              m_parameterList.add(p);
            }
          }
        }
      } else {
        throw new Exception("[GeneralRegression] more than one parameter list!");
      }
    }
  
    /**
     * Read the lists of factors and covariates.
     *
     * @param model the Element that contains the model
     * @param factorOrCovariate holds the String "FactorList" or
     * "CovariateList"
     * @throws Exception if there is a factor or covariate listed
     * that isn't in the mining schema
     */
    protected void readFactorsAndCovariates(Element model, 
                                            String factorOrCovariate) 
      throws Exception {
      Instances miningSchemaI = m_miningSchema.getFieldsAsInstances();
  
      NodeList factorL = model.getElementsByTagName(factorOrCovariate);
      if (factorL.getLength() == 1) { // should be 0 or 1 FactorList element
        Node factor = factorL.item(0);
        if (factor.getNodeType() == Node.ELEMENT_NODE) {
          NodeList predL = ((Element)factor).getElementsByTagName("Predictor");
          for (int i = 0; i < predL.getLength(); i++) {
            Node pred = predL.item(i);
            if (pred.getNodeType() == Node.ELEMENT_NODE) {
              Predictor p = new Predictor();
              p.m_name = ((Element)pred).getAttribute("name");
              // find the index of this predictor in the mining schema
              boolean found = false;
              for (int j = 0; j < miningSchemaI.numAttributes(); j++) {
                if (miningSchemaI.attribute(j).name().equals(p.m_name)) {
                  found = true;
                  p.m_miningSchemaIndex = j;
                  break;
                }
              }
              if (found) {
                if (factorOrCovariate.equals("FactorList")) {
                  m_factorList.add(p);
                } else {
                  m_covariateList.add(p);
                }
              } else {
                throw new Exception("[GeneralRegression] reading factors and covariates - "
                                    + "unable to find predictor " +
                                    p.m_name + " in the mining schema");
              }
            }
          }
        }
      } else if (factorL.getLength() > 1){
        throw new Exception("[GeneralRegression] more than one " + factorOrCovariate
                            + "! ");
      }
    }
  
    /**
     * Read the PPMatrix from the xml. Does not handle multiple PPMatrixes yet.
     *
     * @param model the Element that contains the model
     * @throws Exception if there is a problem parsing cell values.
     */
    protected void readPPMatrix(Element model) throws Exception {
      Instances miningSchemaI = m_miningSchema.getFieldsAsInstances();
      
      NodeList matrixL = model.getElementsByTagName("PPMatrix");
  
      // should be exactly one PPMatrix
      if (matrixL.getLength() == 1) {
        // allocate space for the matrix
        // column that corresponds to the class will be empty (and will be missed out
        // when printing the model).
        m_ppMatrix = new PPCell[m_parameterList.size()][miningSchemaI.numAttributes()];
  
        Node ppM = matrixL.item(0);
        if (ppM.getNodeType() == Node.ELEMENT_NODE) {
          NodeList cellL = ((Element)ppM).getElementsByTagName("PPCell");
          for (int i = 0; i < cellL.getLength(); i++) {
            Node cell = cellL.item(i);
            if (cell.getNodeType() == Node.ELEMENT_NODE) {
              String predictorName = ((Element)cell).getAttribute("predictorName");
              String parameterName = ((Element)cell).getAttribute("parameterName");
              String value = ((Element)cell).getAttribute("value");
              double expOrIndex = -1;
              int predictorIndex = -1;
              int parameterIndex = -1;
              for (int j = 0; j < m_parameterList.size(); j++) {
                if (m_parameterList.get(j).m_name.equals(parameterName)) {
                  parameterIndex = j;
                  break;
                }
              }
              if (parameterIndex == -1) {
                throw new Exception("[GeneralRegression] unable to find parameter name "
                                    + parameterName + " in parameter list");
              }
  
              Predictor p = getCovariate(predictorName);
              if (p != null) {
                try {
                  expOrIndex = Double.parseDouble(value);
                  predictorIndex = p.m_miningSchemaIndex;
                } catch (IllegalArgumentException ex) {
                  throw new Exception("[GeneralRegression] unable to parse PPCell value: "
                                      + value);
                }
              } else {
                // try as a factor
                p = getFactor(predictorName);
                if (p != null) {
                  // An example pmml file from DMG seems to suggest that it
                  // is possible for a continuous variable in the mining schema
                  // to be treated as a factor, so we have to check for this
                  if (miningSchemaI.attribute(p.m_miningSchemaIndex).isNumeric()) {
                    // parse this value as a double. It will be treated as a value
                    // to match rather than an exponent since we are dealing with
                    // a factor here
                    try {
                      expOrIndex = Double.parseDouble(value);
                    } catch (IllegalArgumentException ex) {
                      throw new Exception("[GeneralRegresion] unable to parse PPCell value: "
                                          + value);
                    }
                  } else {
                    // it is a nominal attribute in the mining schema so find
                    // the index that correponds to this value
                    Attribute att = miningSchemaI.attribute(p.m_miningSchemaIndex); 
                    expOrIndex = att.indexOfValue(value);
                    if (expOrIndex == -1) {
                      throw new Exception("[GeneralRegression] unable to find PPCell value "
                                          + value + " in mining schema attribute "
                                          + att.name());
                    }
                  }
                } else {
                  throw new Exception("[GeneralRegression] cant find predictor "
                                      + predictorName + "in either the factors list "
                                      + "or the covariates list");
                }
                predictorIndex = p.m_miningSchemaIndex;
              }
  
              // fill in cell value
              PPCell ppc = new PPCell();
              ppc.m_predictorName = predictorName; ppc.m_parameterName = parameterName;
              ppc.m_value = expOrIndex;
  
              // TO-DO: ppc.m_targetCategory (when handling for multiple PPMatrixes is implemented)
              m_ppMatrix[parameterIndex][predictorIndex] = ppc;
            }
          }
        }
      } else {
        throw new Exception("[GeneralRegression] more than one PPMatrix!");
      }
    }
  
    private Predictor getCovariate(String predictorName) {
      for (int i = 0; i < m_covariateList.size(); i++) {
        if (predictorName.equals(m_covariateList.get(i).m_name)) {
          return m_covariateList.get(i);
        }
      }
      return null;
    }
  
    private Predictor getFactor(String predictorName) {
      for (int i = 0; i < m_factorList.size(); i++) {
        if (predictorName.equals(m_factorList.get(i).m_name)) {
          return m_factorList.get(i);
        }
      }
      return null;
    }
  
    /**
     * Read the parameter matrix from the xml.
     * 
     * @param model Element that holds the model
     * @throws Exception if a problem is encountered during extraction of
     * the parameter matrix
     */
    private void readParamMatrix(Element model) throws Exception {
  
      Instances miningSchemaI = m_miningSchema.getFieldsAsInstances();
      Attribute classAtt = miningSchemaI.classAttribute();
      // used when function type is classification but class attribute is numeric
      // in the mining schema. We will assume that there is a Target specified in
      // the pmml that defines the legal values for this class.
      ArrayList<String> targetVals = null;
  
      NodeList matrixL = model.getElementsByTagName("ParamMatrix");
      if (matrixL.getLength() != 1) {
        throw new Exception("[GeneralRegression] more than one ParamMatrix!");
      }
      Element matrix = (Element)matrixL.item(0);
  
  
      // check for the case where the class in the mining schema is numeric,
      // but this attribute is treated as discrete
      if (m_functionType == Regression.RegressionTable.CLASSIFICATION &&
          classAtt.isNumeric()) {
        // try and convert the class attribute to nominal. For this to succeed
        // there has to be a Target element defined in the PMML.
        if (!m_miningSchema.hasTargetMetaData()) {
          throw new Exception("[GeneralRegression] function type is classification and "
                              + "class attribute in mining schema is numeric, however, "
                              + "there is no Target element "
                              + "specifying legal discrete values for the target!");
  
        }
  
        if (m_miningSchema.getTargetMetaData().getOptype() 
            != TargetMetaInfo.Optype.CATEGORICAL) {
          throw new Exception("[GeneralRegression] function type is classification and "
                              + "class attribute in mining schema is numeric, however "
                              + "Target element in PMML does not have optype categorical!");
        }
  
        // OK now get legal values
        targetVals = m_miningSchema.getTargetMetaData().getValues();
        if (targetVals.size() == 0) {
          throw new Exception("[GeneralRegression] function type is classification and "
                              + "class attribute in mining schema is numeric, however "
                              + "Target element in PMML does not have any discrete values "
                              + "defined!");
        }
  
        // Finally, convert the class in the mining schema to nominal
        m_miningSchema.convertNumericAttToNominal(miningSchemaI.classIndex(), targetVals);
      }
      
      // allocate space for the matrix 
      m_paramMatrix = 
          new PCell[(classAtt.isNumeric())
                    ? 1
                    : classAtt.numValues()][m_parameterList.size()];
  
      NodeList pcellL = matrix.getElementsByTagName("PCell");
      for (int i = 0; i < pcellL.getLength(); i++) {
        // indicates that that this beta applies to all target categories
        // or target is numeric
        int targetCategoryIndex = -1;
        int parameterIndex = -1;
        Node pcell = pcellL.item(i);
        if (pcell.getNodeType() == Node.ELEMENT_NODE) {
          String paramName = ((Element)pcell).getAttribute("parameterName");
          String targetCatName = ((Element)pcell).getAttribute("targetCategory");
          String coefficient = ((Element)pcell).getAttribute("beta");
          String df = ((Element)pcell).getAttribute("df");
  
          for (int j = 0; j < m_parameterList.size(); j++) {
            if (m_parameterList.get(j).m_name.equals(paramName)) {
              parameterIndex = j;
              // use the label if defined
              if (m_parameterList.get(j).m_label != null) {
                paramName = m_parameterList.get(j).m_label;
              }
              break;
            }
          }
          if (parameterIndex == -1) {
            throw new Exception("[GeneralRegression] unable to find parameter name "
                                + paramName + " in parameter list");
          }
  
          if (targetCatName != null && targetCatName.length() > 0) {
            if (classAtt.isNominal() || classAtt.isString()) {
              targetCategoryIndex = classAtt.indexOfValue(targetCatName);
            } else {
              throw new Exception("[GeneralRegression] found a PCell with a named "
                                  + "target category: " + targetCatName
                                  + " but class attribute is numeric in "
                                  + "mining schema");
            }
          }
  
          PCell p = new PCell();
          if (targetCategoryIndex != -1) {
            p.m_targetCategory = targetCatName;
          }
          p.m_parameterName = paramName;
          try {
            p.m_beta = Double.parseDouble(coefficient);
          } catch (IllegalArgumentException ex) {
            throw new Exception("[GeneralRegression] unable to parse beta value "
                                + coefficient + " as a double from PCell");
          }
          if (df != null && df.length() > 0) {
            try {
              p.m_df = Integer.parseInt(df);
            } catch (IllegalArgumentException ex) {
              throw new Exception("[GeneralRegression] unable to parse df value "
                                + df + " as an int from PCell");
            }
          }
          
          if (targetCategoryIndex != -1) {
            m_paramMatrix[targetCategoryIndex][parameterIndex] = p;
          } else {
            // this PCell to all target categories (covers numeric class, in
            // which case there will be only one row in the matrix anyway)
            for (int j = 0; j < m_paramMatrix.length; j++) {
              m_paramMatrix[j][parameterIndex] = p;
            }
          }
        }
      }
    }
  
    /**
     * Return a textual description of this general regression.
     * 
     * @return a description of this general regression
     */
    public String toString() {
      StringBuffer temp = new StringBuffer();
      temp.append("PMML version " + getPMMLVersion());
      if (!getCreatorApplication().equals("?")) {
        temp.append("\nApplication: " + getCreatorApplication());
      }
      temp.append("\nPMML Model: " + m_modelType);
      temp.append("\n\n");
      temp.append(m_miningSchema);
  
      if (m_factorList.size() > 0) {
        temp.append("Factors:\n");
        for (Predictor p : m_factorList) {
          temp.append("\t" + p + "\n");
        }
      }
      temp.append("\n");
      if (m_covariateList.size() > 0) {
        temp.append("Covariates:\n");
        for (Predictor p : m_covariateList) {
          temp.append("\t" + p + "\n");
        }
      }
      temp.append("\n");
      
      printPPMatrix(temp);
      temp.append("\n");
      printParameterMatrix(temp);
      
      // do the link function stuff
      temp.append("\n");
      
      if (m_linkFunction != LinkFunction.NONE) {
        temp.append("Link function: " + m_linkFunction);
        if (m_offsetVariable != null) {
          temp.append("\n\tOffset variable " + m_offsetVariable);
        } else if (!Double.isNaN(m_offsetValue)) {
          temp.append("\n\tOffset value " + m_offsetValue);
        }
        
        if (m_trialsVariable != null) {
          temp.append("\n\tTrials variable " + m_trialsVariable);
        } else if (!Double.isNaN(m_trialsValue)) {
          temp.append("\n\tTrials value " + m_trialsValue);
        }
        
        if (m_distribution != Distribution.NONE) {
          temp.append("\nDistribution: " + m_distribution);
        }
        
        if (m_linkFunction == LinkFunction.NEGBIN &&
            m_distribution == Distribution.NEGBINOMIAL &&
            !Double.isNaN(m_distParameter)) {
          temp.append("\n\tDistribution parameter " + m_distParameter);
        }
        
        if (m_linkFunction == LinkFunction.POWER ||
            m_linkFunction == LinkFunction.ODDSPOWER) {
          if (!Double.isNaN(m_linkParameter)) {
            temp.append("\n\nLink parameter " + m_linkParameter);
          }
        }
      }
      
      if (m_cumulativeLinkFunction != CumulativeLinkFunction.NONE) {
        temp.append("Cumulative link function: " + m_cumulativeLinkFunction);
        
        if (m_offsetVariable != null) {
          temp.append("\n\tOffset variable " + m_offsetVariable);
        } else if (!Double.isNaN(m_offsetValue)) {
          temp.append("\n\tOffset value " + m_offsetValue);
        }
      }
      temp.append("\n");
      
      return temp.toString();
    }
    
    /**
     * Format and print the PPMatrix to the supplied StringBuffer.
     * 
     * @param buff the StringBuffer to append to
     */
    protected void printPPMatrix(StringBuffer buff) {
      Instances miningSchemaI = m_miningSchema.getFieldsAsInstances();
      int maxAttWidth = 0;
      for (int i = 0; i < miningSchemaI.numAttributes(); i++) {
        Attribute a = miningSchemaI.attribute(i);
        if (a.name().length() > maxAttWidth) {
          maxAttWidth = a.name().length();
        }
      }
  
      // check the width of the values
      for (int i = 0; i < m_parameterList.size(); i++) {
        for (int j = 0; j < miningSchemaI.numAttributes(); j++) {
          if (m_ppMatrix[i][j] != null) {
            double width = Math.log(Math.abs(m_ppMatrix[i][j].m_value)) /
              Math.log(10.0);
            if (width < 0) {
              width = 1;
            }
            // decimal + # decimal places + 1
            width += 2.0;
            if ((int)width > maxAttWidth) {
              maxAttWidth = (int)width;
            }
            if (miningSchemaI.attribute(j).isNominal() || 
                miningSchemaI.attribute(j).isString()) {
              // check the width of this value
              String val = miningSchemaI.attribute(j).value((int)m_ppMatrix[i][j].m_value) + " ";
              if (val.length() > maxAttWidth) {
                maxAttWidth = val.length();
              }
            }
          }
        }
      }
  
      // get the max parameter width
      int maxParamWidth = "Parameter  ".length();
      for (Parameter p : m_parameterList) {
        String temp = (p.m_label != null)
          ? p.m_label + " "
          : p.m_name + " ";
  
        if (temp.length() > maxParamWidth) {
          maxParamWidth = temp.length();
        }
      }
  
      buff.append("Predictor-to-Parameter matrix:\n");
      buff.append(PMMLUtils.pad("Predictor", " ", (maxParamWidth + (maxAttWidth * 2 + 2))
                                - "Predictor".length(), true));
      buff.append("\n" + PMMLUtils.pad("Parameter", " ", maxParamWidth - "Parameter".length(), false));
      // attribute names
      for (int i = 0; i < miningSchemaI.numAttributes(); i++) {
        if (i != miningSchemaI.classIndex()) {
          String attName = miningSchemaI.attribute(i).name();
          buff.append(PMMLUtils.pad(attName, " ", maxAttWidth + 1 - attName.length(), true));
        }
      }
      buff.append("\n");
  
      for (int i = 0; i < m_parameterList.size(); i++) {
        Parameter param = m_parameterList.get(i);
        String paramS = (param.m_label != null)
          ? param.m_label
          : param.m_name;
        buff.append(PMMLUtils.pad(paramS, " ", 
                                  maxParamWidth - paramS.length(), false));
        for (int j = 0; j < miningSchemaI.numAttributes(); j++) {
          if (j != miningSchemaI.classIndex()) {
            PPCell p = m_ppMatrix[i][j];
            String val = " ";
            if (p != null) {
              if (miningSchemaI.attribute(j).isNominal() ||
                  miningSchemaI.attribute(j).isString()) {
                val = miningSchemaI.attribute(j).value((int)p.m_value);
              } else {
                val = "" + Utils.doubleToString(p.m_value, maxAttWidth, 4).trim();
              }
            }
            buff.append(PMMLUtils.pad(val, " ", maxAttWidth + 1 - val.length(), true));
          }
        }
        buff.append("\n");
      }
    }
  
    /**
     * Format and print the parameter matrix to the supplied StringBuffer.
     * 
     * @param buff the StringBuffer to append to
     */
    protected void printParameterMatrix(StringBuffer buff) {
      Instances miningSchemaI = m_miningSchema.getFieldsAsInstances();
  
      // get the maximum class value width (nominal)
      int maxClassWidth = miningSchemaI.classAttribute().name().length();
      if (miningSchemaI.classAttribute().isNominal()
          || miningSchemaI.classAttribute().isString()) {
        for (int i = 0; i < miningSchemaI.classAttribute().numValues(); i++) {
          if (miningSchemaI.classAttribute().value(i).length() > maxClassWidth) {
            maxClassWidth = miningSchemaI.classAttribute().value(i).length();
          }
        }
      }
  
      // get the maximum parameter name/label width
      int maxParamWidth = 0;
      for (int i = 0; i < m_parameterList.size(); i++) {
        Parameter p = m_parameterList.get(i);
        String val = (p.m_label != null)
          ? p.m_label + " "
          : p.m_name + " ";
        if (val.length() > maxParamWidth) {
          maxParamWidth = val.length();
        }
      }
  
      // get the max beta value width
      int maxBetaWidth = "Coeff.".length();
      for (int i = 0; i < m_paramMatrix.length; i++) {
        for (int j = 0; j < m_parameterList.size(); j++) {
          PCell p = m_paramMatrix[i][j];
          if (p != null) {
            double width = Math.log(Math.abs(p.m_beta)) / Math.log(10);
            if (width < 0) {
              width = 1;
            }
            // decimal + # decimal places + 1
            width += 7.0;
            if ((int)width > maxBetaWidth) {
              maxBetaWidth = (int)width;
            }
          }
        }
      }
  
      buff.append("Parameter estimates:\n");
      buff.append(PMMLUtils.pad(miningSchemaI.classAttribute().name(), " ", 
                                maxClassWidth + maxParamWidth + 2 - 
                                miningSchemaI.classAttribute().name().length(), false));
      buff.append(PMMLUtils.pad("Coeff.", " ", maxBetaWidth + 1 - "Coeff.".length(), true));
      buff.append(PMMLUtils.pad("df", " ", maxBetaWidth - "df".length(), true));
      buff.append("\n");
      for (int i = 0; i < m_paramMatrix.length; i++) {
        // scan for non-null entry for this class value
        boolean ok = false;
        for (int j = 0; j < m_parameterList.size(); j++) {
          if (m_paramMatrix[i][j] != null) {
            ok = true;
          }
        }
        if (!ok) {
          continue;
        }
        // first the class value (if nominal)
        String cVal = (miningSchemaI.classAttribute().isNominal() || 
            miningSchemaI.classAttribute().isString())
          ? miningSchemaI.classAttribute().value(i)
          : " ";
        buff.append(PMMLUtils.pad(cVal, " ", maxClassWidth - cVal.length(), false));     
        buff.append("\n");
        for (int j = 0; j < m_parameterList.size(); j++) {
          PCell p = m_paramMatrix[i][j];
          if (p != null) {
            String label = p.m_parameterName;
            buff.append(PMMLUtils.pad(label, " ", maxClassWidth + maxParamWidth + 2 -
                                      label.length(), true));
            String betaS = Utils.doubleToString(p.m_beta, maxBetaWidth, 4).trim();
            buff.append(PMMLUtils.pad(betaS, " ", maxBetaWidth + 1 - betaS.length(), true));
            String dfS = Utils.doubleToString(p.m_df, maxBetaWidth, 4).trim();
            buff.append(PMMLUtils.pad(dfS, " ", maxBetaWidth - dfS.length(), true));
            buff.append("\n");
          }
        }
      }
    }
    
    /**
     * Construct the incoming parameter vector based on the values
     * in the incoming test instance.
     * 
     * @param incomingInst the values of the incoming test instance
     * @return the populated parameter vector ready to be multiplied against
     * the vector of coefficients.
     * @throws Exception if there is some problem whilst constructing the
     * parameter vector
     */
    private double[] incomingParamVector(double[] incomingInst) throws Exception {
      Instances miningSchemaI = m_miningSchema.getFieldsAsInstances();
      double[] incomingPV = new double[m_parameterList.size()];
      
      for (int i = 0; i < m_parameterList.size(); i++) {
        //
        // default is that this row represents the intercept.
        // this will be the case if there are all null entries in this row
        incomingPV[i] = 1.0;
  
        // loop over the attributes (predictors)
        for (int j = 0; j < miningSchemaI.numAttributes(); j++) {        
          PPCell cellEntry = m_ppMatrix[i][j];
          Predictor p = null;
          if (cellEntry != null) {
            if ((p = getFactor(cellEntry.m_predictorName)) != null) {
              if ((int)incomingInst[p.m_miningSchemaIndex] == (int)cellEntry.m_value) {
                incomingPV[i] *= 1.0; // we have a match
              } else {
                incomingPV[i] *= 0.0;
              }
            } else if ((p = getCovariate(cellEntry.m_predictorName)) != null) {
                incomingPV[i] *= Math.pow(incomingInst[p.m_miningSchemaIndex], cellEntry.m_value);
            } else {
              throw new Exception("[GeneralRegression] can't find predictor "
                  + cellEntry.m_predictorName + " in either the list of factors or covariates");
            }
          }
        }
      }
      
      return incomingPV;
    }
  
    /**                                                                                                             
     * Classifies the given test instance. The instance has to belong to a                                          
     * dataset when it's being classified.                                                          
     *                                                                                                              
     * @param inst the instance to be classified                                                                
     * @return the predicted most likely class for the instance or                                                  
     * Utils.missingValue() if no prediction is made                                                             
     * @exception Exception if an error occurred during the prediction                                              
     */
    public double[] distributionForInstance(Instance inst) throws Exception {
      if (!m_initialized) {
        mapToMiningSchema(inst.dataset());
      }
      double[] preds = null;
      if (m_miningSchema.getFieldsAsInstances().classAttribute().isNumeric()) {
        preds = new double[1];
      } else {
        preds = new double[m_miningSchema.getFieldsAsInstances().classAttribute().numValues()];
      }
      
      // create an array of doubles that holds values from the incoming
      // instance; in order of the fields in the mining schema. We will
      // also handle missing values and outliers here.
      double[] incoming = m_fieldsMap.instanceToSchema(inst, m_miningSchema);
      
      // In this implementation we will default to information in the Target element (default
      // value for numeric prediction and prior probabilities for classification). If there is
      // no Target element defined, then an Exception is thrown.
  
      boolean hasMissing = false;
      for (int i = 0; i < incoming.length; i++) {
        if (i != m_miningSchema.getFieldsAsInstances().classIndex() && 
            Double.isNaN(incoming[i])) {
          hasMissing = true;
          break;
        }
      }
      
      if (hasMissing) {
        if (!m_miningSchema.hasTargetMetaData()) {
          String message = "[GeneralRegression] WARNING: Instance to predict has missing value(s) but "
            + "there is no missing value handling meta data and no "
            + "prior probabilities/default value to fall back to. No "
            + "prediction will be made (" 
            + ((m_miningSchema.getFieldsAsInstances().classAttribute().isNominal()
                || m_miningSchema.getFieldsAsInstances().classAttribute().isString())
                ? "zero probabilities output)."
                : "NaN output).");
          if (m_log == null) {
            System.err.println(message);
          } else {
            m_log.logMessage(message);
          }
          
          if (m_miningSchema.getFieldsAsInstances().classAttribute().isNumeric()) {
            preds[0] = Utils.missingValue();
          }
          return preds;
        } else {
          // use prior probablilities/default value
          TargetMetaInfo targetData = m_miningSchema.getTargetMetaData();
          if (m_miningSchema.getFieldsAsInstances().classAttribute().isNumeric()) {
            preds[0] = targetData.getDefaultValue();
          } else {
            Instances miningSchemaI = m_miningSchema.getFieldsAsInstances();
            for (int i = 0; i < miningSchemaI.classAttribute().numValues(); i++) {
              preds[i] = targetData.getPriorProbability(miningSchemaI.classAttribute().value(i));
            }
          }
          return preds;
        }
      } else {
        // construct input parameter vector here
        double[] inputParamVector = incomingParamVector(incoming);
        computeResponses(incoming, inputParamVector, preds);
      }
      
      return preds;
    }
    
    /**
     * Compute the responses for the function given the parameter values corresponding
     * to the current incoming instance.
     * 
     * @param incomingInst raw incoming instance values (after missing value
     * replacement and outlier treatment)
     * @param incomingParamVector incoming instance values mapped to parameters
     * @param responses will contain the responses computed by the function
     * @throws Exception if something goes wrong
     */
    private void computeResponses(double[] incomingInst, 
                                  double[] incomingParamVector,
                                  double[] responses) throws Exception {
      for (int i = 0; i < responses.length; i++) {
        for (int j = 0; j < m_parameterList.size(); j++) {
          // a row of the parameter matrix should have all non-null entries
          // except for the last class (in the case of classification) which
          // should have just an intercept of 0. Need to handle the case where
          // no intercept has been defined in the pmml file for the last class
          PCell p = m_paramMatrix[i][j];
          if (p == null) {
            responses[i] += 0.0 * incomingParamVector[j];
          } else {
            responses[i] += incomingParamVector[j] * p.m_beta;
          }
        }
      }
      
      switch(m_modelType) {
      case MULTINOMIALLOGISTIC:
        computeProbabilitiesMultinomialLogistic(responses);
        break;
      case REGRESSION:
        // nothing to be done
        break;
      case GENERALLINEAR:
      case GENERALIZEDLINEAR:
        if (m_linkFunction != LinkFunction.NONE) {
          computeResponseGeneralizedLinear(incomingInst, responses);
        } else {
          throw new Exception("[GeneralRegression] no link function specified!");
        }
        break;
      case ORDINALMULTINOMIAL:
        if (m_cumulativeLinkFunction != CumulativeLinkFunction.NONE) {
          computeResponseOrdinalMultinomial(incomingInst, responses);
        } else {
          throw new Exception("[GeneralRegression] no cumulative link function specified!");
        }
        break;
        default:
          throw new Exception("[GeneralRegression] unknown model type");
      }
    }
    
    /**
     * Computes probabilities for the multinomial logistic model type.
     * 
     * @param responses will hold the responses computed by the function.
     */
    private static void computeProbabilitiesMultinomialLogistic(double[] responses) {
      double[] r = responses.clone();
      for (int j = 0; j < r.length; j++) {
        double sum = 0;
        boolean overflow = false;
        for (int k = 0; k < r.length; k++) {
          if (r[k] - r[j] > 700) {
            overflow = true;
            break;
          }
          sum += Math.exp(r[k] - r[j]);
        }
        if (overflow) {
          responses[j] = 0.0;
        } else {
          responses[j] = 1.0 / sum;
        }
      }
    }
    
    /**
     * Computes responses for the general linear and generalized linear model
     * types.
     * 
     * @param incomingInst the raw incoming instance values (after missing value
     * replacement and outlier treatment etc).
     * @param responses will hold the responses computed by the function
     * @throws Exception if a problem occurs. 
     */
    private void computeResponseGeneralizedLinear(double[] incomingInst, 
                                                  double[] responses) 
      throws Exception {
      double[] r = responses.clone();
      
      double offset = 0;
      if (m_offsetVariable != null) {
        Attribute offsetAtt = 
          m_miningSchema.getFieldsAsInstances().attribute(m_offsetVariable);
        if (offsetAtt == null) {
          throw new Exception("[GeneralRegression] unable to find offset variable "
              + m_offsetVariable + " in the mining schema!");
        }
        offset = incomingInst[offsetAtt.index()];
      } else if (!Double.isNaN(m_offsetValue)) {
        offset = m_offsetValue;
      }
      
      double trials = 1;
      if (m_trialsVariable != null) {
        Attribute trialsAtt = m_miningSchema.getFieldsAsInstances().attribute(m_trialsVariable);
        if (trialsAtt == null) {
          throw new Exception("[GeneralRegression] unable to find trials variable "
              + m_trialsVariable + " in the mining schema!");
        }
        trials = incomingInst[trialsAtt.index()];
      } else if (!Double.isNaN(m_trialsValue)) {
        trials = m_trialsValue;
      }
      
      double distParam = 0;
      if (m_linkFunction == LinkFunction.NEGBIN && 
          m_distribution == Distribution.NEGBINOMIAL) {
        if (Double.isNaN(m_distParameter)) {
          throw new Exception("[GeneralRegression] no distribution parameter defined!");
        }
        distParam = m_distParameter;
      }
      
      double linkParam = 0;
      if (m_linkFunction == LinkFunction.POWER || 
          m_linkFunction == LinkFunction.ODDSPOWER) {
        if (Double.isNaN(m_linkParameter)) {
          throw new Exception("[GeneralRegression] no link parameter defined!");
        }
        linkParam = m_linkParameter;
      }
     
      for (int i = 0; i < r.length; i++) {
        responses[i] = m_linkFunction.eval(r[i], offset, trials, distParam, linkParam);
      }
    }
      
    /**
     * Computes responses for the ordinal multinomial model type.
     * 
     * @param incomingInst the raw incoming instance values (after missing value
     * replacement and outlier treatment etc).
     * @param responses will hold the responses computed by the function
     * @throws Exception if a problem occurs. 
     */
    private void computeResponseOrdinalMultinomial(double[] incomingInst, 
                                                    double[] responses) throws Exception {
      
      double[] r = responses.clone();
      
      double offset = 0;
      if (m_offsetVariable != null) {
        Attribute offsetAtt = 
          m_miningSchema.getFieldsAsInstances().attribute(m_offsetVariable);
        if (offsetAtt == null) {
          throw new Exception("[GeneralRegression] unable to find offset variable "
              + m_offsetVariable + " in the mining schema!");
        }
        offset = incomingInst[offsetAtt.index()];
      } else if (!Double.isNaN(m_offsetValue)) {
        offset = m_offsetValue;
      }
      
      for (int i = 0; i < r.length; i++) {
        if (i == 0) {
          responses[i] = m_cumulativeLinkFunction.eval(r[i], offset);
     
        } else if (i == (r.length - 1)) {
          responses[i] = 1.0 - responses[i - 1];
        } else {
          responses[i] = m_cumulativeLinkFunction.eval(r[i], offset) - responses[i - 1];
        }
      }
    }
  
    /* (non-Javadoc)
     * @see weka.core.RevisionHandler#getRevision()
     */
    public String getRevision() {
      return RevisionUtils.extract("$Revision: 5987 $");
    }
  }
  