package yi.ai.classifiers.j48p;

import java.util.*;
import weka.core.*;
import weka.classifiers.j48.*;

/**
 * Class for selecting a Local feature based split for a given dataset.
 *
 * The difference is that for a global feature, if there is a split on it, then
 * the classification of instances/cases is decided on this feature.
 *
 * On the other side, for local features, even a prediction may fail on one
 * local feature, it may be true for another local feature belonged to a same
 * instance/case.
 *
 * Here is the deal, when a split rule is considered, say R>128, we must exam
 * all the local features for an instance, if any of them satisfy the rule
 * in other words, there is a region has the required feature, then we say the
 * instance satisfy the rule.
 *
 * One thing to remember, for a rule, say R>128, we should not only consider
 * whether there is a local feature satisfy it, but also consider the opposite
 * rule R<=128 because there is situations are not complementary any more
 *
 * Title:        Learning local features
 * Description:
 * Copyright:    Copyright (c) 2002
 * Company:      University of Washington
 * @author Yi Li
 * @version 1.0
 */

public class LocalC45ModelSelection extends ModelSelection {

  /** Minimum number of instances in interval. */
  private int m_minNoObj;

  /** The FULL training dataset. */
  private Instances m_allData;

  /**
   * Initializes the local feature based split selection method with the given
   * parameters.
   *
   * @param m_minNoObj minimum number of instances that have to occur in at
   * least two subsets induced by split
   * @param allData FULL training dataset (necessary for selection of split
   * points).
   */
  public LocalC45ModelSelection(int m_minNoObj,Instances allData){
    m_minNoObj = m_minNoObj;
    m_allData = allData;
  }


  /**
   * Sets reference to training data to null.
   */
  public void cleanup() {

    m_allData = null;
  }

  /**
   * Selects local feature based split for the given dataset.
   */
  public final ClassifierSplitModel selectModel(Instances data){
    double minResult;
    double currentResult;
    LocalC45Split [] currentModel;
    LocalC45Split bestModel = null;
    NoSplit noSplitModel = null;
    double averageInfoGain = 0;
    int validModels = 0;
    boolean multiVal = true;
    Distribution checkDistribution;
    Attribute attribute;
    double sumOfWeights;
    int i;

    try{

      // Check if all Instances belong to one class or if not
      // enough Instances to split.
      checkDistribution = new Distribution(data);
      noSplitModel = new NoSplit(checkDistribution);
      if (Utils.sm(checkDistribution.total(),2*m_minNoObj) ||
          Utils.eq(checkDistribution.total(),
                   checkDistribution.perClass(checkDistribution.maxClass())))
        return noSplitModel;

      // Check if all attributes are nominal and have a
      // lot of values.
      if (m_allData != null) {
        Enumeration enum = data.enumerateAttributes();
        while (enum.hasMoreElements()) {
          attribute = (Attribute) enum.nextElement();
          if ((attribute.isNumeric()) ||
              (Utils.sm((double)attribute.numValues(),
                        (0.3*(double)m_allData.numInstances())))){
            multiVal = false;
            break;
          }
        }
      }

      // for case-based classifier, every attribute split can generate two
      // situation:
      // for example, if the split point is 100, then we can classify a case
      // based on:
      // - if it has a local instance satisfying < 100, then the case satisfys
      //   < 100;
      // - if it has a local instance satisfying > 100, then the case satisfys
      //   > 100;
      // so the number of split situation is doubled.
      currentModel = new LocalC45Split[data.numAttributes()*2];
      sumOfWeights = data.sumOfWeights();

      // For each attribute.
      for (i = 0; i < data.numAttributes(); i++){

        // Apart from class attribute.
        if (i != (data).classIndex() && i != (data).caseIndex()){

          // Get models for current attribute.
          // There are two split situations on one attribute.  The first one
          // corresponds the ascending sorting of the attribute values; and the
          // second one corresponds the descending sorting
          currentModel[2*i] = new LocalC45Split(i,m_minNoObj,sumOfWeights);
          currentModel[2*i].buildClassifier(data);
          currentModel[2*i+1] = new LocalC45Split(i,m_minNoObj,sumOfWeights,true);
          currentModel[2*i+1].buildClassifier(data);
        }else {
          currentModel[2*i] = null;
          currentModel[2*i+1] = null;
        }
      }

      for (i=0; i<currentModel.length; i++) {
          if (currentModel[i] == null) continue;
          // Check if useful split for current attribute
          // exists and check for enumerated attributes with
          // a lot of values.
          if (currentModel[i].checkModel())
            if (m_allData != null) {
              if ((data.attribute(i/2).isNumeric()) ||
                  (multiVal || Utils.sm((double)data.attribute(i/2).numValues(),
                                        (0.3*(double)m_allData.numInstances())))){
                averageInfoGain = averageInfoGain+currentModel[i].infoGain();
                validModels++;
              }
            } else {
              averageInfoGain = averageInfoGain+currentModel[i].infoGain();
              validModels++;
            }
      }

      // Check if any useful split was found.
      if (validModels == 0)
        return noSplitModel;
      averageInfoGain = averageInfoGain/(double)validModels;

      // Find "best" attribute to split on.
      minResult = 0;
      for (i=0;i<currentModel.length;i++){
        if ((i/2 != (data).classIndex()) && (i/2 != (data).caseIndex()) &&
            (currentModel[i].checkModel()))

          // Use 1E-3 here to get a closer approximation to the original
          // implementation.
          if ((currentModel[i].infoGain() >= (averageInfoGain-1E-3)) &&
              Utils.gr(currentModel[i].gainRatio(),minResult)){
            bestModel = currentModel[i];
            minResult = currentModel[i].gainRatio();
          }
      }

      // Check if useful split was found.
      if (Utils.eq(minResult,0))
        return noSplitModel;

      /* Let 's just ignore this for now, 'cause all our instance have no
         missing attributes
      // Add all Instances with unknown values for the corresponding
      // attribute to the distribution for the model, so that
      // the complete distribution is stored with the model.
      bestModel.distribution().
          addInstWithUnknown(data,bestModel.attIndex());
      */

      /* it is wierd
      // Set the split point analogue to C45 if attribute numeric.
      if (m_allData != null)
        bestModel.setSplitPoint(m_allData);
      */
      return bestModel;
    }catch(Exception e){
      e.printStackTrace();
    }
    return null;
  }

  /**
   * Selects C4.5-type split for the given dataset.
   */
  public final ClassifierSplitModel selectModel(Instances train, Instances test)
  {
    return selectModel(train);
  }

}