/**
* Copyright (C) 2001-2017 by RapidMiner and the contributors
*
* Complete list of developers available at our web site:
*
* http://rapidminer.com
*
* This program is free software: you can redistribute it and/or modify it under the terms of the
* GNU Affero General Public License as published by the Free Software Foundation, either version 3
* 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
* Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License along with this program.
* If not, see http://www.gnu.org/licenses/.
*/
package com.rapidminer.operator.learner.tree;
import java.util.Arrays;
import java.util.Comparator;
import java.util.Iterator;
import java.util.List;
import com.rapidminer.example.Attribute;
import com.rapidminer.example.Example;
import com.rapidminer.example.ExampleSet;
import com.rapidminer.example.set.ExampleSetUtilities;
import com.rapidminer.operator.Model;
import com.rapidminer.operator.OperatorCapability;
import com.rapidminer.operator.OperatorDescription;
import com.rapidminer.operator.OperatorException;
import com.rapidminer.operator.learner.AbstractLearner;
import com.rapidminer.operator.learner.PredictionModel;
import com.rapidminer.operator.learner.SimplePredictionModel;
import com.rapidminer.parameter.ParameterType;
import com.rapidminer.parameter.ParameterTypeCategory;
import com.rapidminer.parameter.UndefinedParameterError;
import com.rapidminer.tools.Tools;
/**
* A DecisionStump clone that allows to specify different utility functions. It is quick for nominal
* attributes, but does not yet apply pruning for continuous attributes. Currently it can only
* handle boolean class labels.
*
* @author Martin Scholz
*
* @deprecated This learner is not used anymore.
*/
@Deprecated
public class MultiCriterionDecisionStumps extends AbstractLearner {
private static final String ACC = "accuracy"; // TP + TN = p + N - n ~ p - n
private static final String ENTROPY = "entropy";
private static final String SQRT_PN = "sqrt(TP*FP) + sqrt(FN*TN)"; // sqrt(pn) +
// sqrt((P-p)(N-n))
private static final String GINI = "gini index"; // sqrt(pn) + sqrt((P-p)(N-n))
private static final String CHI_SQUARE = "chi square test";
private static final String[] UTILITY_FUNCTION_LIST = new String[] { ENTROPY, ACC, SQRT_PN, GINI, CHI_SQUARE };
private static final String PARAMETER_UTILITY_FUNCTION = "utility_function";
public static class DecisionStumpModel extends SimplePredictionModel {
private static final long serialVersionUID = -261158567126510415L;
private final Attribute testAttribute;
private final double testValue;
private final boolean prediction;
private boolean includeNaNs;
private final boolean numerical;
// nominal attribute: test is "equals"
// numerical attribute: test is "<="
// if true, then the provided prediction is made
public DecisionStumpModel(Attribute attribute, double testValue, ExampleSet exampleSet, boolean prediction,
boolean includeNaNs) {
super(exampleSet, ExampleSetUtilities.SetsCompareOption.USE_INTERSECTION,
ExampleSetUtilities.TypesCompareOption.ALLOW_SAME_PARENTS);
this.prediction = prediction;
this.includeNaNs = includeNaNs;
this.testAttribute = attribute;
this.testValue = testValue;
if (testAttribute == null || !testAttribute.isNominal()) {
this.numerical = true;
} else {
this.numerical = false;
}
}
@Override
public double predict(Example example) {
boolean evaluatesToTrue;
if (this.testAttribute == null) {
evaluatesToTrue = true;
} else {
double exampleValue = example.getValue(testAttribute);
if (Double.isNaN(exampleValue)) {
evaluatesToTrue = includeNaNs;
} else if (this.numerical) {
evaluatesToTrue = example.getValue(testAttribute) <= testValue;
} else {
evaluatesToTrue = example.getValue(testAttribute) == testValue;
}
}
if (evaluatesToTrue == prediction) {
return this.getLabel().getMapping().getPositiveIndex();
} else {
return this.getLabel().getMapping().getNegativeIndex();
}
}
@Override
protected boolean supportsConfidences(Attribute label) {
return false;
}
/** @return a <code>String</code> representation of this rule model. */
@Override
public String toString() {
String posIndexS = getLabel().getMapping().getPositiveString();
String negIndexS = getLabel().getMapping().getNegativeString();
StringBuffer result = new StringBuffer(super.toString());
result.append(Tools.getLineSeparator() + " (" + this.getLabel().getName() + "=");
result.append((prediction ? posIndexS : negIndexS) + ") <-- ");
result.append(testAttribute != null ? testAttribute.getName()
+ (numerical ? " <= " + testValue : " = " + testAttribute.getMapping().mapIndex((int) testValue)) : "");
result.append(Tools.getLineSeparator() + " unknown: predict '" + (includeNaNs ? posIndexS : negIndexS) + "'.");
return result.toString();
}
}
private int posIndex;
private double globalP;
private double globalN;
private Model bestModel;
private double bestScore;
private String utilityFunction;
public MultiCriterionDecisionStumps(OperatorDescription description) {
super(description);
}
@Override
public Class<? extends PredictionModel> getModelClass() {
return DecisionStumpModel.class;
}
@Override
public boolean supportsCapability(OperatorCapability lc) {
if (lc == com.rapidminer.operator.OperatorCapability.NUMERICAL_ATTRIBUTES) {
return true;
}
if (lc == com.rapidminer.operator.OperatorCapability.POLYNOMINAL_ATTRIBUTES) {
return true;
}
if (lc == com.rapidminer.operator.OperatorCapability.BINOMINAL_ATTRIBUTES) {
return true;
}
if (lc == com.rapidminer.operator.OperatorCapability.BINOMINAL_LABEL) {
return true;
}
if (lc == com.rapidminer.operator.OperatorCapability.WEIGHTED_EXAMPLES) {
return true;
}
return false;
}
protected void initHighscore() {
this.bestModel = null;
this.bestScore = Double.NEGATIVE_INFINITY;
}
/** @return the best decision stump found */
protected Model getBestModel() {
return this.bestModel;
}
private void setBestModel(DecisionStumpModel model, double score) {
this.bestModel = model;
this.bestScore = score;
}
@Override
public Model learn(ExampleSet exampleSet) throws OperatorException {
this.utilityFunction = UTILITY_FUNCTION_LIST[this.getParameterAsInt(PARAMETER_UTILITY_FUNCTION)];
this.initHighscore();
this.posIndex = exampleSet.getAttributes().getLabel().getMapping().getPositiveIndex();
double[] globalCounts = this.computePriors(exampleSet);
this.globalP = globalCounts[0];
this.globalN = globalCounts[1];
{ // init with better on eof the default models
boolean defaultModelPrecition = this.getScore(globalCounts, true) >= this.getScore(globalCounts, false);
this.setBestModel(new DecisionStumpModel(null, 0, exampleSet, defaultModelPrecition, true),
this.getScore(globalCounts, defaultModelPrecition));
}
this.evaluateNominalAttributes(exampleSet);
this.evaluateNumericalAttributes(exampleSet);
return this.getBestModel();
}
private void evaluateNumericalAttributes(ExampleSet exampleSet) throws OperatorException {
int numAttr = exampleSet.getAttributes().size();
int[] mapAttribToIndex = new int[numAttr];
Attribute[] mapIndexToAttrib = new Attribute[numAttr];
int index = 0;
{
int i = 0;
for (Attribute attribute : exampleSet.getAttributes()) {
if (!attribute.isNominal()) {
mapIndexToAttrib[index] = attribute;
mapAttribToIndex[i] = index++;
} else {
mapAttribToIndex[i] = -1;
}
i++;
}
}
if (index == 0) {
return;
}
boolean hasWeight = exampleSet.getAttributes().getWeight() != null;
double[][] weightedLabel = new double[exampleSet.size()][2];
double[][][] values = new double[index][exampleSet.size()][];
Iterator<Example> reader = exampleSet.iterator();
int exampleNum = 0;
double[] weightedPriors = new double[2];
while (reader.hasNext()) {
Example example = reader.next();
int label = example.getLabel() == posIndex ? 0 : 1;
double weight = hasWeight ? example.getWeight() : 1.0d;
weightedPriors[label] += weight;
weightedLabel[exampleNum] = new double[] { label, weight };
for (int i = 0; i < index; i++) {
double attribValue = example.getValue(mapIndexToAttrib[i]);
values[i][exampleNum] = new double[] { attribValue, exampleNum };
}
exampleNum++;
}
final boolean predictNaN = weightedPriors[0] >= weightedPriors[1];
Comparator<double[]> cmp = new Comparator<double[]>() {
@Override
public int compare(double[] arg0, double[] arg1) {
return Double.compare(arg0[0], arg1[0]);
}
};
for (int i = 0; i < index; i++) {
final Attribute currentAttribute = mapIndexToAttrib[i];
final double[][] currentAttributeValues = values[i];
Arrays.sort(currentAttributeValues, cmp);
final double counts[] = new double[exampleSet.getAttributes().getLabel().getMapping().size()];
double lastValue = Double.NEGATIVE_INFINITY;
double lastScore = Double.NEGATIVE_INFINITY;
boolean betterPrediction = false;
for (int j = 0; j < currentAttributeValues.length; j++) {
final double curAttribValue = currentAttributeValues[j][0];
if (Double.isNaN(curAttribValue) || curAttribValue == Double.POSITIVE_INFINITY) {
break;
}
final int curExampleNumber = (int) currentAttributeValues[j][1];
final int curLabel = (int) weightedLabel[curExampleNumber][0];
final double curWeight = weightedLabel[curExampleNumber][1];
if (curAttribValue != lastValue && lastScore > this.bestScore) {
double testValue = (curAttribValue + lastValue) / 2.0d;
boolean includeNaNs = predictNaN == betterPrediction;
DecisionStumpModel dsm = new DecisionStumpModel(currentAttribute, testValue, exampleSet,
betterPrediction, includeNaNs);
this.setBestModel(dsm, lastScore);
}
counts[curLabel] += curWeight;
double scorePos = this.getScore(counts, true);
double scoreNeg = this.getScore(counts, false);
lastScore = Math.max(scorePos, scoreNeg);
betterPrediction = scorePos >= scoreNeg;
lastValue = curAttribValue;
}
}
}
private void evaluateNominalAttributes(ExampleSet exampleSet) throws OperatorException {
int numAttr = exampleSet.getAttributes().size();
int[] mapAttribToIndex = new int[numAttr];
Attribute[] mapIndexToAttrib = new Attribute[numAttr];
int index = 0;
{
int i = 0;
for (Attribute attribute : exampleSet.getAttributes()) {
if (attribute.isNominal()) {
mapIndexToAttrib[index] = attribute;
mapAttribToIndex[i] = index++;
} else {
mapAttribToIndex[i] = -1;
}
i++;
}
}
if (index == 0) {
return;
}
double[][][] counter = new double[index][][];
double[][] countNaNs = new double[index][exampleSet.getAttributes().getLabel().getMapping().size()];
for (int i = 0; i < index; i++) {
int numValues = mapIndexToAttrib[i].getMapping().size();
counter[i] = new double[numValues][exampleSet.getAttributes().getLabel().getMapping().size()];
}
Attribute weightAttr = exampleSet.getAttributes().getWeight();
Iterator<Example> reader = exampleSet.iterator();
while (reader.hasNext()) {
Example example = reader.next();
double weight = weightAttr == null ? 1.0d : example.getWeight();
int label = example.getLabel() == posIndex ? 0 : 1;
for (int i = 0; i < index; i++) {
double attributeValue = example.getValue(mapIndexToAttrib[i]);
if (Double.isNaN(attributeValue)) {
countNaNs[i][label] += weight;
} else {
counter[i][(int) attributeValue][label] += weight;
}
}
}
for (int i = 0; i < index; i++) {
double[][] attributeMatrix = counter[i];
for (int j = 0; j < attributeMatrix.length; j++) {
ScoreNaNInfo snp = this.getScore(attributeMatrix[j], countNaNs[i]);
if (snp.score > this.bestScore) {
Attribute attribute = mapIndexToAttrib[i];
double testValue = j;
this.setBestModel(new DecisionStumpModel(attribute, testValue, exampleSet, snp.predicted,
snp.includeNaNs), snp.score);
}
}
}
}
// Helper class.
private static class ScoreNaNInfo {
public double score;
public boolean includeNaNs;
public boolean predicted;
ScoreNaNInfo(double score, boolean includeNaNs, boolean predicted) {
this.score = score;
this.includeNaNs = includeNaNs;
this.predicted = predicted;
}
public ScoreNaNInfo max(ScoreNaNInfo other) {
if (this.score >= other.score) {
return this;
} else {
return other;
}
}
}
// Evaluate all four combinations: with and without including NaNs, predicting pos or neg class
private ScoreNaNInfo getScore(double[] counts, double[] countNaNs) throws UndefinedParameterError {
ScoreNaNInfo snp, snp2;
// exclude NaNs, predict true
double score = this.getScore(counts, true);
snp = new ScoreNaNInfo(score, false, true);
// exclude NaNs, predict false
score = this.getScore(counts, false);
snp2 = new ScoreNaNInfo(score, false, false);
snp = snp.max(snp2);
if (countNaNs[0] > 0 || countNaNs[1] > 0) {
counts[0] += countNaNs[0];
counts[1] += countNaNs[1];
// include NaNs, predict true
score = this.getScore(counts, true);
snp2 = new ScoreNaNInfo(score, true, true);
snp = snp.max(snp2);
// include NaNs, predict false
score = this.getScore(counts, false);
snp2 = new ScoreNaNInfo(score, true, false);
snp = snp.max(snp2);
}
return snp;
}
/**
* Computes the score for the specified utility function, the provided counts and class.
*/
protected double getScore(double[] counts, boolean predictPositives) {
double p = counts[0];
double n = counts[1];
double score;
if (this.utilityFunction.equals(ACC)) {
score = predictPositives ? p - n : n - p;
} else if (this.utilityFunction.equals(ENTROPY)) {
if (p - n >= 0 ^ predictPositives) {
return Double.NEGATIVE_INFINITY;
}
double cov = p + n;
double uncov = globalP + globalN - cov;
double scoreCovered = cov == 0 ? 0 : entropyLog2(p / cov) + entropyLog2(n / cov);
double scoreUncovered = uncov == 0 ? 0 : entropyLog2((globalP - p) / uncov) + entropyLog2((globalN - n) / uncov);
score = (cov * scoreCovered + uncov * scoreUncovered) / (cov + uncov);
score = -score; // maximization problem assumed
} else if (this.utilityFunction.equals(SQRT_PN)) {
if (p - n >= 0 ^ predictPositives) {
return Double.NEGATIVE_INFINITY;
}
score = Math.sqrt(p * n) + Math.sqrt((globalP - p) * (globalN - n));
score = -score; // maximization problem assumed
} else if (this.utilityFunction.equals(GINI)) {
if (p - n >= 0 ^ predictPositives) {
return Double.NEGATIVE_INFINITY;
}
double cov = p + n;
double uncov = globalP + globalN - cov;
double scoreCovered = cov == 0 ? 0 : p / cov * (n / cov);
double scoreUncovered = uncov == 0 ? 0 : (globalP - p) / uncov * ((globalN - n) / uncov);
score = (cov * scoreCovered + uncov * scoreUncovered) / (cov + uncov);
score = -score; // maximization problem assumed
} else if (this.utilityFunction.equals(CHI_SQUARE)) {
double q = globalP - p;
double r = globalN - n;
double cov = p + n;
double uncov = q + r;
double total = cov + uncov;
double c11, c12, c21, c22;
c11 = cov * globalP / total;
c12 = cov * globalN / total;
c21 = uncov * globalP / total;
c22 = uncov * globalN / total;
if (cov > 0 && uncov > 0) {
score = (p - c11) * (p - c11) / c11 + (n - c12) * (n - c12) / c12 + (q - c21) * (q - c21) / c21 + (r - c22)
* (r - c22) / c22;
} else {
score = 0;
}
} else {
score = Double.NaN;
logWarning("Found unknown utility function: " + this.utilityFunction);
}
return score;
}
// more intuitive than log_e, although it should make no difference
private double entropyLog2(double p) {
if (Double.isNaN(p) || p == 0) { // NaN may e.g. occur when coverage is 0
return 0;
} else {
return -p * Math.log(p) / Math.log(2.0d);
}
}
/**
* @param exampleSet
* the exampleSet to get the weighted priors for
* @return a double[2] object, first parameter is p, second is n.
*/
protected double[] computePriors(ExampleSet exampleSet) {
Attribute weightAttr = exampleSet.getAttributes().getWeight();
double p = 0;
double n = 0;
Iterator<Example> reader = exampleSet.iterator();
while (reader.hasNext()) {
Example example = reader.next();
double weight = weightAttr == null ? 1 : example.getValue(weightAttr);
if (example.getLabel() == posIndex) {
p += weight;
} else {
n += weight;
}
}
return new double[] { p, n };
}
/**
* Adds the parameter &utility function".
*/
@Override
public List<ParameterType> getParameterTypes() {
List<ParameterType> types = super.getParameterTypes();
types.add(new ParameterTypeCategory(PARAMETER_UTILITY_FUNCTION, "The function to be optimized by the rule.",
UTILITY_FUNCTION_LIST, 0));
return types;
}
}