Index: stable/HeuristicLab.Problems.DataAnalysis/3.4/Implementation/Classification/ClassificationPerformanceMeasures.cs
===================================================================
--- stable/HeuristicLab.Problems.DataAnalysis/3.4/Implementation/Classification/ClassificationPerformanceMeasures.cs	(revision 11872)
+++ stable/HeuristicLab.Problems.DataAnalysis/3.4/Implementation/Classification/ClassificationPerformanceMeasures.cs	(revision 11872)
@@ -0,0 +1,171 @@
+﻿#region License Information
+/* HeuristicLab
+ * Copyright (C) 2002-2014 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
+ *
+ * This file is part of HeuristicLab.
+ *
+ * HeuristicLab 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 3 of the License, or
+ * (at your option) any later version.
+ *
+ * HeuristicLab 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 HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
+ */
+#endregion
+
+using System;
+using HeuristicLab.Common;
+using HeuristicLab.Data;
+using HeuristicLab.Optimization;
+using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
+
+namespace HeuristicLab.Problems.DataAnalysis {
+  [StorableClass]
+  public class ClassificationPerformanceMeasuresResultCollection : ResultCollection {
+    #region result names
+    protected const string ClassificationPositiveClassNameResultName = "Classification positive class";
+    protected const string TrainingTruePositiveRateResultName = "True positive rate (training)";
+    protected const string TrainingTrueNegativeRateResultName = "True negative rate (training)";
+    protected const string TrainingPositivePredictiveValueResultName = "Positive predictive value (training)";
+    protected const string TrainingNegativePredictiveValueResultName = "Negative predictive value (training)";
+    protected const string TrainingFalsePositiveRateResultName = "False positive rate (training)";
+    protected const string TrainingFalseDiscoveryRateResultName = "False discovery rate (training)";
+    protected const string TestTruePositiveRateResultName = "True positive rate (test)";
+    protected const string TestTrueNegativeRateResultName = "True negative rate (test)";
+    protected const string TestPositivePredictiveValueResultName = "Positive predictive value (test)";
+    protected const string TestNegativePredictiveValueResultName = "Negative predictive value (test)";
+    protected const string TestFalsePositiveRateResultName = "False positive rate (test)";
+    protected const string TestFalseDiscoveryRateResultName = "False discovery rate (test)";
+    #endregion
+
+    public ClassificationPerformanceMeasuresResultCollection()
+      : base() {
+      AddMeasures();
+    }
+    [StorableConstructor]
+    protected ClassificationPerformanceMeasuresResultCollection(bool deserializing)
+      : base(deserializing) {
+    }
+
+    protected ClassificationPerformanceMeasuresResultCollection(ClassificationPerformanceMeasuresResultCollection original, Cloner cloner)
+      : base(original, cloner) { }
+    public override IDeepCloneable Clone(Cloner cloner) {
+      return new ClassificationPerformanceMeasuresResultCollection(this, cloner);
+    }
+
+    #region result properties
+    public string ClassificationPositiveClassName {
+      get { return ((StringValue)this[ClassificationPositiveClassNameResultName].Value).Value; }
+      set { ((StringValue)this[ClassificationPositiveClassNameResultName].Value).Value = value; }
+    }
+    public double TrainingTruePositiveRate {
+      get { return ((DoubleValue)this[TrainingTruePositiveRateResultName].Value).Value; }
+      set { ((DoubleValue)this[TrainingTruePositiveRateResultName].Value).Value = value; }
+    }
+    public double TrainingTrueNegativeRate {
+      get { return ((DoubleValue)this[TrainingTrueNegativeRateResultName].Value).Value; }
+      set { ((DoubleValue)this[TrainingTrueNegativeRateResultName].Value).Value = value; }
+    }
+    public double TrainingPositivePredictiveValue {
+      get { return ((DoubleValue)this[TrainingPositivePredictiveValueResultName].Value).Value; }
+      set { ((DoubleValue)this[TrainingPositivePredictiveValueResultName].Value).Value = value; }
+    }
+    public double TrainingNegativePredictiveValue {
+      get { return ((DoubleValue)this[TrainingNegativePredictiveValueResultName].Value).Value; }
+      set { ((DoubleValue)this[TrainingNegativePredictiveValueResultName].Value).Value = value; }
+    }
+    public double TrainingFalsePositiveRate {
+      get { return ((DoubleValue)this[TrainingFalsePositiveRateResultName].Value).Value; }
+      set { ((DoubleValue)this[TrainingFalsePositiveRateResultName].Value).Value = value; }
+    }
+    public double TrainingFalseDiscoveryRate {
+      get { return ((DoubleValue)this[TrainingFalseDiscoveryRateResultName].Value).Value; }
+      set { ((DoubleValue)this[TrainingFalseDiscoveryRateResultName].Value).Value = value; }
+    }
+    public double TestTruePositiveRate {
+      get { return ((DoubleValue)this[TestTruePositiveRateResultName].Value).Value; }
+      set { ((DoubleValue)this[TestTruePositiveRateResultName].Value).Value = value; }
+    }
+    public double TestTrueNegativeRate {
+      get { return ((DoubleValue)this[TestTrueNegativeRateResultName].Value).Value; }
+      set { ((DoubleValue)this[TestTrueNegativeRateResultName].Value).Value = value; }
+    }
+    public double TestPositivePredictiveValue {
+      get { return ((DoubleValue)this[TestPositivePredictiveValueResultName].Value).Value; }
+      set { ((DoubleValue)this[TestPositivePredictiveValueResultName].Value).Value = value; }
+    }
+    public double TestNegativePredictiveValue {
+      get { return ((DoubleValue)this[TestNegativePredictiveValueResultName].Value).Value; }
+      set { ((DoubleValue)this[TestNegativePredictiveValueResultName].Value).Value = value; }
+    }
+    public double TestFalsePositiveRate {
+      get { return ((DoubleValue)this[TestFalsePositiveRateResultName].Value).Value; }
+      set { ((DoubleValue)this[TestFalsePositiveRateResultName].Value).Value = value; }
+    }
+    public double TestFalseDiscoveryRate {
+      get { return ((DoubleValue)this[TestFalseDiscoveryRateResultName].Value).Value; }
+      set { ((DoubleValue)this[TestFalseDiscoveryRateResultName].Value).Value = value; }
+    }
+    #endregion
+
+    protected void AddMeasures() {
+      Add(new Result(ClassificationPositiveClassNameResultName, "The positive class which is used for the performance measure calculations.", new StringValue()));
+      Add(new Result(TrainingTruePositiveRateResultName, "Sensitivity/True positive rate of the model on the training partition\n(TP/(TP+FN)).", new PercentValue()));
+      Add(new Result(TrainingTrueNegativeRateResultName, "Specificity/True negative rate of the model on the training partition\n(TN/(FP+TN)).", new PercentValue()));
+      Add(new Result(TrainingPositivePredictiveValueResultName, "Precision/Positive predictive value of the model on the training partition\n(TP/(TP+FP)).", new PercentValue()));
+      Add(new Result(TrainingNegativePredictiveValueResultName, "Negative predictive value of the model on the training partition\n(TN/(TN+FN)).", new PercentValue()));
+      Add(new Result(TrainingFalsePositiveRateResultName, "The false positive rate is the complement of the true negative rate of the model on the training partition.", new PercentValue()));
+      Add(new Result(TrainingFalseDiscoveryRateResultName, "The false discovery rate is the complement of the positive predictive value of the model on the training partition.", new PercentValue()));
+      Add(new Result(TestTruePositiveRateResultName, "Sensitivity/True positive rate of the model on the test partition\n(TP/(TP+FN)).", new PercentValue()));
+      Add(new Result(TestTrueNegativeRateResultName, "Specificity/True negative rate of the model on the test partition\n(TN/(FP+TN)).", new PercentValue()));
+      Add(new Result(TestPositivePredictiveValueResultName, "Precision/Positive predictive value of the model on the test partition\n(TP/(TP+FP)).", new PercentValue()));
+      Add(new Result(TestNegativePredictiveValueResultName, "Negative predictive value of the model on the test partition\n(TN/(TN+FN)).", new PercentValue()));
+      Add(new Result(TestFalsePositiveRateResultName, "The false positive rate is the complement of the true negative rate of the model on the test partition.", new PercentValue()));
+      Add(new Result(TestFalseDiscoveryRateResultName, "The false discovery rate is the complement of the positive predictive value of the model on the test partition.", new PercentValue()));
+      TrainingTruePositiveRate = double.NaN;
+      TrainingTrueNegativeRate = double.NaN;
+      TrainingPositivePredictiveValue = double.NaN;
+      TrainingNegativePredictiveValue = double.NaN;
+      TrainingFalsePositiveRate = double.NaN;
+      TrainingFalseDiscoveryRate = double.NaN;
+      TestTruePositiveRate = double.NaN;
+      TestTrueNegativeRate = double.NaN;
+      TestPositivePredictiveValue = double.NaN;
+      TestNegativePredictiveValue = double.NaN;
+      TestFalsePositiveRate = double.NaN;
+      TestFalseDiscoveryRate = double.NaN;
+    }
+
+    public void SetTrainingResults(ClassificationPerformanceMeasuresCalculator trainingPerformanceCalculator) {
+      if (!string.IsNullOrWhiteSpace(ClassificationPositiveClassName)
+              && !ClassificationPositiveClassName.Equals(trainingPerformanceCalculator.PositiveClassName))
+        throw new ArgumentException("Classification positive class of the training data doesn't match with the data of test partition.");
+      ClassificationPositiveClassName = trainingPerformanceCalculator.PositiveClassName;
+      TrainingTruePositiveRate = trainingPerformanceCalculator.TruePositiveRate;
+      TrainingTrueNegativeRate = trainingPerformanceCalculator.TrueNegativeRate;
+      TrainingPositivePredictiveValue = trainingPerformanceCalculator.PositivePredictiveValue;
+      TrainingNegativePredictiveValue = trainingPerformanceCalculator.NegativePredictiveValue;
+      TrainingFalsePositiveRate = trainingPerformanceCalculator.FalsePositiveRate;
+      TrainingFalseDiscoveryRate = trainingPerformanceCalculator.FalseDiscoveryRate;
+    }
+
+    public void SetTestResults(ClassificationPerformanceMeasuresCalculator testPerformanceCalculator) {
+      if (!string.IsNullOrWhiteSpace(ClassificationPositiveClassName)
+                && !ClassificationPositiveClassName.Equals(testPerformanceCalculator.PositiveClassName))
+        throw new ArgumentException("Classification positive class of the test data doesn't match with the data of training partition.");
+      ClassificationPositiveClassName = testPerformanceCalculator.PositiveClassName;
+      TestTruePositiveRate = testPerformanceCalculator.TruePositiveRate;
+      TestTrueNegativeRate = testPerformanceCalculator.TrueNegativeRate;
+      TestPositivePredictiveValue = testPerformanceCalculator.PositivePredictiveValue;
+      TestNegativePredictiveValue = testPerformanceCalculator.NegativePredictiveValue;
+      TestFalsePositiveRate = testPerformanceCalculator.FalsePositiveRate;
+      TestFalseDiscoveryRate = testPerformanceCalculator.FalseDiscoveryRate;
+    }
+  }
+}
Index: stable/HeuristicLab.Problems.DataAnalysis/3.4/Implementation/Classification/ClassificationProblemData.cs
===================================================================
--- stable/HeuristicLab.Problems.DataAnalysis/3.4/Implementation/Classification/ClassificationProblemData.cs	(revision 11170)
+++ stable/HeuristicLab.Problems.DataAnalysis/3.4/Implementation/Classification/ClassificationProblemData.cs	(revision 11872)
@@ -36,4 +36,5 @@
     protected const string ClassNamesParameterName = "ClassNames";
     protected const string ClassificationPenaltiesParameterName = "ClassificationPenalties";
+    protected const string PositiveClassParameterName = "PositiveClass";
     protected const int MaximumNumberOfClasses = 100;
     protected const int InspectedRowsToDetermineTargets = 2000;
@@ -213,4 +214,7 @@
       get { return (IFixedValueParameter<StringMatrix>)Parameters[ClassNamesParameterName]; }
     }
+    public IConstrainedValueParameter<StringValue> PositiveClassParameter {
+      get { return (IConstrainedValueParameter<StringValue>)Parameters[PositiveClassParameterName]; }
+    }
     public IFixedValueParameter<DoubleMatrix> ClassificationPenaltiesParameter {
       get { return (IFixedValueParameter<DoubleMatrix>)Parameters[ClassificationPenaltiesParameterName]; }
@@ -262,4 +266,13 @@
       get { return ClassNamesCache; }
     }
+
+    public string PositiveClass {
+      get { return PositiveClassParameter.Value.Value; }
+      set {
+        var matchingValue = PositiveClassParameter.ValidValues.SingleOrDefault(x => x.Value == value);
+        if (matchingValue == null) throw new ArgumentException(string.Format("{0} cannot be set as positive class.", value));
+        PositiveClassParameter.Value = matchingValue;
+      }
+    }
     #endregion
 
@@ -270,4 +283,13 @@
     private void AfterDeserialization() {
       RegisterParameterEvents();
+      // BackwardsCompatibility3.4
+      #region Backwards compatible code, remove with 3.5
+      if (!Parameters.ContainsKey(PositiveClassParameterName)) {
+        var validValues = new ItemSet<StringValue>(ClassNames.Select(s => new StringValue(s).AsReadOnly()));
+        Parameters.Add(new ConstrainedValueParameter<StringValue>(PositiveClassParameterName,
+          "The positive class which is used for quality measure calculation (e.g., specifity, sensitivity,...)", validValues, validValues.First()));
+      }
+      #endregion
+
     }
 
@@ -307,4 +329,5 @@
       Parameters.Add(new ConstrainedValueParameter<StringValue>(TargetVariableParameterName, new ItemSet<StringValue>(validTargetVariableValues), target));
       Parameters.Add(new FixedValueParameter<StringMatrix>(ClassNamesParameterName, ""));
+      Parameters.Add(new ConstrainedValueParameter<StringValue>(PositiveClassParameterName, "The positive class which is used for quality measure calculation (e.g., specifity, sensitivity,...)"));
       Parameters.Add(new FixedValueParameter<DoubleMatrix>(ClassificationPenaltiesParameterName, ""));
 
@@ -340,4 +363,9 @@
       ClassNamesParameter.Value.RowNames = ClassValues.Select(s => "ClassValue: " + s);
 
+      PositiveClassParameter.ValidValues.Clear();
+      foreach (var className in ClassNames) {
+        PositiveClassParameter.ValidValues.Add(new StringValue(className).AsReadOnly());
+      }
+
       ((IStringConvertibleMatrix)ClassificationPenaltiesParameter.Value).Rows = Classes;
       ((IStringConvertibleMatrix)ClassificationPenaltiesParameter.Value).Columns = Classes;
@@ -411,7 +439,18 @@
     }
     private void Parameter_ValueChanged(object sender, EventArgs e) {
+      var oldPositiveClass = PositiveClass;
+      var oldClassNames = classNamesCache;
+      var index = oldClassNames.IndexOf(oldPositiveClass);
+
       classNamesCache = null;
       ClassificationPenaltiesParameter.Value.RowNames = ClassNames.Select(name => "Actual " + name);
       ClassificationPenaltiesParameter.Value.ColumnNames = ClassNames.Select(name => "Estimated " + name);
+
+      PositiveClassParameter.ValidValues.Clear();
+      foreach (var className in ClassNames) {
+        PositiveClassParameter.ValidValues.Add(new StringValue(className).AsReadOnly());
+      }
+      PositiveClassParameter.Value = PositiveClassParameter.ValidValues.ElementAt(index);
+
       OnChanged();
     }
@@ -435,5 +474,11 @@
       if (!newClassValues.SequenceEqual(ClassValues)) {
         errorMessage = errorMessage + string.Format("The class values differ in the provided classification problem data.");
-        return false;
+        returnValue = false;
+      }
+
+      var newPositivieClassName = classificationProblemData.PositiveClass;
+      if (newPositivieClassName != PositiveClass) {
+        errorMessage = errorMessage + string.Format("The positive class differs in the provided classification problem data.");
+        returnValue = false;
       }
 
@@ -452,4 +497,6 @@
         ClassNamesParameter.Value[i, 0] = classificationProblemData.ClassNames.ElementAt(i);
 
+      PositiveClass = classificationProblemData.PositiveClass;
+
       for (int i = 0; i < Classes; i++) {
         for (int j = 0; j < Classes; j++) {
Index: stable/HeuristicLab.Problems.DataAnalysis/3.4/Implementation/Classification/ClassificationSolutionBase.cs
===================================================================
--- stable/HeuristicLab.Problems.DataAnalysis/3.4/Implementation/Classification/ClassificationSolutionBase.cs	(revision 11170)
+++ stable/HeuristicLab.Problems.DataAnalysis/3.4/Implementation/Classification/ClassificationSolutionBase.cs	(revision 11872)
@@ -34,4 +34,5 @@
     private const string TrainingNormalizedGiniCoefficientResultName = "Normalized Gini Coefficient (training)";
     private const string TestNormalizedGiniCoefficientResultName = "Normalized Gini Coefficient (test)";
+    private const string ClassificationPerformanceMeasuresResultName = "Classification Performance Measures";
 
     public new IClassificationModel Model {
@@ -62,4 +63,8 @@
       protected set { ((DoubleValue)this[TestNormalizedGiniCoefficientResultName].Value).Value = value; }
     }
+    public ClassificationPerformanceMeasuresResultCollection ClassificationPerformanceMeasures {
+      get { return ((ClassificationPerformanceMeasuresResultCollection)this[ClassificationPerformanceMeasuresResultName].Value); }
+      protected set { (this[ClassificationPerformanceMeasuresResultName].Value) = value; }
+    }
     #endregion
 
@@ -75,4 +80,7 @@
       Add(new Result(TrainingNormalizedGiniCoefficientResultName, "Normalized Gini coefficient of the model on the training partition.", new DoubleValue()));
       Add(new Result(TestNormalizedGiniCoefficientResultName, "Normalized Gini coefficient of the model on the test partition.", new DoubleValue()));
+      Add(new Result(ClassificationPerformanceMeasuresResultName, @"Classification performance measures.\n
+                              In a multiclass classification all misclassifications of the negative class will be treated as true negatives except on positive class estimations.",
+                            new ClassificationPerformanceMeasuresResultCollection()));
     }
 
@@ -83,4 +91,10 @@
       if (!this.ContainsKey(TestNormalizedGiniCoefficientResultName))
         Add(new Result(TestNormalizedGiniCoefficientResultName, "Normalized Gini coefficient of the model on the test partition.", new DoubleValue()));
+      if (!this.ContainsKey(ClassificationPerformanceMeasuresResultName)) {
+        Add(new Result(ClassificationPerformanceMeasuresResultName, @"Classification performance measures.\n
+                              In a multiclass classification all misclassifications of the negative class will be treated as true negatives except on positive class estimations.",
+                              new ClassificationPerformanceMeasuresResultCollection()));
+        CalculateClassificationResults();
+      }
     }
 
@@ -88,6 +102,12 @@
       double[] estimatedTrainingClassValues = EstimatedTrainingClassValues.ToArray(); // cache values
       double[] originalTrainingClassValues = ProblemData.Dataset.GetDoubleValues(ProblemData.TargetVariable, ProblemData.TrainingIndices).ToArray();
+
       double[] estimatedTestClassValues = EstimatedTestClassValues.ToArray(); // cache values
       double[] originalTestClassValues = ProblemData.Dataset.GetDoubleValues(ProblemData.TargetVariable, ProblemData.TestIndices).ToArray();
+
+      var positiveClassName = ProblemData.PositiveClass;
+      double positiveClassValue = ProblemData.GetClassValue(positiveClassName);
+      ClassificationPerformanceMeasuresCalculator trainingPerformanceCalculator = new ClassificationPerformanceMeasuresCalculator(positiveClassName, positiveClassValue);
+      ClassificationPerformanceMeasuresCalculator testPerformanceCalculator = new ClassificationPerformanceMeasuresCalculator(positiveClassName, positiveClassValue);
 
       OnlineCalculatorError errorState;
@@ -107,4 +127,12 @@
       TrainingNormalizedGiniCoefficient = trainingNormalizedGini;
       TestNormalizedGiniCoefficient = testNormalizedGini;
+
+      trainingPerformanceCalculator.Calculate(originalTrainingClassValues, estimatedTrainingClassValues);
+      if (trainingPerformanceCalculator.ErrorState == OnlineCalculatorError.None)
+        ClassificationPerformanceMeasures.SetTrainingResults(trainingPerformanceCalculator);
+
+      testPerformanceCalculator.Calculate(originalTestClassValues, estimatedTestClassValues);
+      if (testPerformanceCalculator.ErrorState == OnlineCalculatorError.None)
+        ClassificationPerformanceMeasures.SetTestResults(testPerformanceCalculator);
     }
 
