""" Create one plot for each possible parameter combination from a :class:`~pySPACE.resources.dataset_defs.performance_result.PerformanceResultSummary`
This module contains implementations for analyzing data contained in a csv file (e.g. the result
of a Weka Classification Operation).
An *AnalysisProcess* consists of evaluating the effect of several parameter
on a set of metrics. For each numeric parameter, each pair of numeric parameters
and each nominal parameter, one plot is created for each metric.
Furthermore, for each value of each parameter, the rows of the data where
the specific parameter takes on the specific value are selected and the same
analysis is done for this subset recursively.
This is useful for large experiments where several parameters are differed.
For instance, if one wants to analyze how the performance is for certain
settings of certain parameters, on can get all plots in the respective
subdirectories. For instance, if one is interested only in the performance
of one classifier, on can go into the subdirectory of the respective classifier.
.. note:: This operation should not be used any longer, since it produces to many files.
If you want to draw all interesting pictures,
use comp_analysis instead. If you want to have only few pictures,
use the :mod:`~pySPACE.run.gui.performance_results_analysis` gui.
"""
import sys
if sys.version_info[0] == 2 and sys.version_info[1] < 6:
import processing
else:
import multiprocessing as processing
import pylab
import numpy
import os
import itertools
import matplotlib.font_manager
from collections import defaultdict
from pySPACE.tools.filesystem import create_directory
from pySPACE.resources.dataset_defs.base import BaseDataset
import pySPACE
from pySPACE.missions.operations.base import Operation, Process
[docs]class AnalysisOperation(Operation):
""" Operation to analyze and plot performance result data
An *AnalysisOperation* loads the data from a csv-file (typically the result
of a Weka Classification Operation) and evaluates the effect of various
parameters on several metrics.
"""
[docs] def __init__(self, processes, operation_spec, result_directory,
number_processes, create_process=None):
super(AnalysisOperation, self).__init__(processes, operation_spec,
result_directory)
self.operation_spec = operation_spec
self.create_process = create_process
self.number_processes = number_processes
@classmethod
[docs] def create(cls, operation_spec, result_directory, debug=False, input_paths=[]):
""" A factory method that creates an Analysis operation based on the
information given in the operation specification operation_spec
"""
assert(operation_spec["type"] == "analysis")
input_path = operation_spec["input_path"]
summary = BaseDataset.load(os.path.join(pySPACE.configuration.storage,
input_path))
data_dict = summary.data
# Determine the parameters that should be analyzed
parameters = operation_spec["parameters"]
# Determine the metrics that should be plotted
metrics = operation_spec["metrics"]
# Determine how many processes will be created
number_parameter_values = [len(set(data_dict[param])) for param in parameters]
number_processes = cls._numberOfProcesses(0, number_parameter_values)+1
if debug == True:
# To better debug creation of processes we don't limit the queue
# and create all processes before executing them
processes = processing.Queue()
cls._createProcesses(processes, result_directory, data_dict, parameters,
metrics, True)
return cls( processes, operation_spec, result_directory, number_processes)
else:
# Create all plot processes by calling a recursive helper method in
# another thread so that already created processes can be executed
# although creation of processes is not finished yet. Therefore a queue
# is used which size is limited to guarantee that not to much objects
# are created (since this costs memory). However, the actual number
# of 100 is arbitrary and might be changed according to the system at hand.
processes = processing.Queue(100)
create_process = processing.Process(target=cls._createProcesses,
args=( processes, result_directory, data_dict,
parameters, metrics, True))
create_process.start()
# create and return the operation object
return cls( processes, operation_spec, result_directory, number_processes, create_process)
@classmethod
[docs] def _numberOfProcesses(cls, number_of_processes, number_of_parameter_values):
""" Recursive function to determine the number of processes that
will be created for the given *number_of_parameter_values*
"""
if len(number_of_parameter_values) < 3:
number_of_processes += sum(number_of_parameter_values)
return number_of_processes
else:
for i in range(len(number_of_parameter_values)):
number_of_processes += number_of_parameter_values[i] * \
cls._numberOfProcesses(0,
[number_of_parameter_values[j] for j in \
range(len(number_of_parameter_values)) if j != i]) + \
number_of_parameter_values[i]
return number_of_processes
@classmethod
[docs] def _createProcesses(cls, processes, result_dir, data_dict, parameters,
metrics, top_level):
""" Recursive function that is used to create the analysis processes
Each process creates one plot for each numeric parameter, each pair of
numeric parameters, and each nominal parameter based on the data
contained in the *data_dict*. The results are stored in *result_dir*.
The method calls itself recursively for each value of each parameter.
"""
# Create the analysis process for the given parameters and the
# given data
process = AnalysisProcess(result_dir, data_dict, parameters, metrics)
processes.put(process)
# If we have less than two parameters it does not make sense to
# split further
if len(parameters) < 2:
if top_level == True:
# If we have only one parameter to visualize,
# we don't need to create any further processes,
# and we have to finish the creating process.
processes.put(False)
return
# For each parameter
for proj_parameter in parameters:
# We split the data based on the values of this parameter
remaining_parameters = [parameter for parameter in parameters
if parameter != proj_parameter]
# For each value the respective projection parameter can take on
for value in set(data_dict[proj_parameter]):
# Project the result dict onto the rows where the respective
# parameter takes on the given value
projected_dict = defaultdict(list)
entries_added = False
for i in range(len(data_dict[parameter])):
if data_dict[proj_parameter][i] == value:
entries_added = True
for column_key in data_dict.keys():
if column_key == proj_parameter: continue
projected_dict[column_key].append(data_dict[column_key][i])
# If the projected_dict is empty we continue
if not entries_added:
continue
# Create result_dir and do the recursive call for the
# projected data
# Parameter is seperated via #
proj_result_dir = result_dir + os.sep + "%s#%s" % (proj_parameter,
value)
create_directory(proj_result_dir)
cls._createProcesses(processes, proj_result_dir, projected_dict,
remaining_parameters, metrics, False)
if top_level == True:
# print "last process created"
# give executing process the sign that creation is now finished
processes.put(False)
[docs] def consolidate(self):
pass
[docs]class AnalysisProcess(Process):
""" Process for analyzing and plotting data
An *AnalysisProcess* consists of evaluating the effect of several
*parameters* on a set of *metrics*. For each numeric parameter,
each pair of numeric parameters and each nominal parameter,
one plot is created for each metric.
**Expected arguments**
:result_dir: The directory in which the actual results are stored
:data_dict: A dictionary containing all the data. The dictionary
contains a mapping from an attribute (e.g. accuracy)
to a list of values taken by an attribute. An entry is the
entirety of all i-th values over all dict-values
:parameters: The parameters which have been varied during the
experiment and whose effect on the *metrics* should be
investigated. These must be keys of the *data_dict*.
:metrics: The metrics the should be evaluated. Must be keys of the
*data_dict*.
"""
[docs] def __init__(self, result_dir, data_dict, parameters, metrics):
super(AnalysisProcess, self).__init__()
self.result_dir = result_dir
self.data_dict = data_dict
self.parameters = parameters
# Usually the value of a metric for a certain situation is just a scalar
# value. However, for certain metrics the value can be a sequence
# (typically the change of some measure over time). These cases must be
# indicated externally by the the usage of ("metric_name", "sequence")
# instead of just "mteric_name".
self.metrics = [(metric, "scalar") if isinstance(metric, basestring)
else metric for metric in metrics]
[docs] def __call__(self):
"""
Executes this process on the respective modality
"""
# Restore configuration
pySPACE.configuration = self.configuration
############## Prepare benchmarking ##############
super(AnalysisProcess, self).pre_benchmarking()
# Split parameters into nominal and numeric parameters
nominal_parameters = []
numeric_parameters = []
for parameter in self.parameters:
try:
# Try to create a float of the first value of the parameter
float(self.data_dict[parameter][0])
# No exception thus a numeric attribute
numeric_parameters.append(parameter)
except ValueError:
# This is not a numeric parameter, treat it as nominal
nominal_parameters.append(parameter)
except KeyError:
#"This exception should inform the user about wrong parameters in his YAML file.", said Jan-Hendrik-Metzen.
import warnings
warnings.warn('The parameter ... is not contained in the performance results...')
except IndexError:
#This exception informs the user about wrong parameters in his YAML file.
import warnings
warnings.warn('The parameter "' + parameter + '" could not be found.')
#TODO: Better exception treatment! The Program should ignore unknown
# parameters and go on after giving information on the wrong parameter.
# For all performance measures
for metric in self.metrics:
if metric[1] == 'scalar':
self._scalar_metric(metric[0], numeric_parameters,
nominal_parameters)
else:
self._sequence_metric(metric[0], numeric_parameters,
nominal_parameters, **metric[2])
############## Clean up after benchmarking ##############
super(AnalysisProcess, self).post_benchmarking()
[docs] def _scalar_metric(self, metric, numeric_parameters, nominal_parameters):
""" Creates the plots for a scalar metric """
# For all numeric parameters
for index, parameter1 in enumerate(numeric_parameters):
self._plot_numeric(self.data_dict, self.result_dir,
x_key = parameter1,
y_key = metric,
one_figure = False,
show_errors = True)
# For all combinations of two numeric parameters
for parameter2 in numeric_parameters[index+1:]:
axis_keys = [parameter1, parameter2]
self._plot_numeric_vs_numeric(self.data_dict,
self.result_dir,
axis_keys = axis_keys,
value_key = metric)
# For all combinations of a numeric and a nominal parameter
for parameter2 in nominal_parameters:
axis_keys = [parameter1, parameter2]
self._plot_numeric_vs_nominal(self.data_dict,
self.result_dir,
numeric_key = parameter1,
nominal_key = parameter2,
value_key = metric)
# For all nominal parameters:
for index, parameter1 in enumerate(nominal_parameters):
self._plot_nominal(self.data_dict, self.result_dir,
x_key = parameter1,
y_key = metric)
[docs] def _sequence_metric(self, metric, numeric_parameters, nominal_parameters,
mwa_window_length):
""" Creates the plots for a sequence metric """
# TODO: Do not distinguish nominal and numeric parameters for the moment
parameters = list(numeric_parameters)
parameters.extend(nominal_parameters)
metric_values = map(eval, self.data_dict[metric])
# Sometimes, the number of values are not identical, so we cut all to
# the same minimal length
num_values = min(map(len, metric_values))
metric_values = map(lambda l: l[0:num_values], metric_values)
# Moving window average of the metric values
mwa_metric_values = []
for sequence in metric_values:
mwa_metric_values.append([])
for index in range(len(sequence)):
# Chop window such that does not go beyond the range of
# available values
window_width = min(index, len(sequence) - index - 1,
mwa_window_length/2)
subrange = (index - window_width, index + window_width)
mwa_metric_values[-1].append(numpy.mean(sequence[subrange[0]:subrange[1]]))
# For each parameter
for parameter in parameters:
# Split the data according to the values the parameter takes on
curves = defaultdict(list)
for row in range(len(self.data_dict[parameter])):
curves[self.data_dict[parameter][row]].append(mwa_metric_values[row])
# Plot the mean curve over all runs for this parameter setting
for parameter_value, curve in curves.iteritems():
# Create a simple plot
pylab.plot(range(len(metric_values[0])), numpy.mean(curve, 0),
label = parameter_value)
# # Create an errorbar plot
# pylab.errorbar(x = range(len(metric_values[0])),
# y = numpy.mean(curve, 0),
# yerr = numpy.std(curve, 0),
# elinewidth = 1, capsize = 5,
# label = parameter_value)
pylab.legend(loc = 0)
pylab.xlabel("Step")
pylab.ylabel(metric)
pylab.savefig("%s.pdf" % os.path.join(self.result_dir,
"_".join([metric, parameter])))
pylab.gca().clear()
pylab.close("all")
[docs] def _plot_numeric(self, data, result_dir, x_key, y_key, conditions = [],
one_figure = False, show_errors = False):
""" Creates a plot of the y_keys for the given numeric parameter x_key.
A method that allows to create a plot that visualizes the effect
of differing one variable onto a second one (e.g. the effect of
differing the number of features onto the accuracy).
**Expected arguments**
:data: A dictionary, that contains a mapping from an attribute
(e.g. accuracy) to a list of values taken by an attribute.
An entry is the entirety of all i-th values over all dict-values
:result_dir: The directory in which the plots will be saved.
:x_key: The key of the dictionary whose values should be used as
values for the x-axis (the independent variables)
:y_key: The key of the dictionary whose values should be used as
values for the y-axis, i.e. the dependent variables
:conditions: A list of functions that need to be fulfilled in order to
use one entry in the plot. Each function has to take two
arguments: The data dictionary containing all entries and
the index of the entry that should be checked. Each condition
must return a boolean value.
:one_figure: If true, all curves are plotted in the same figure.
Otherwise, for each value of curve_key, a new figure
is generated (currently ignored)
:show_errors: If true, error bars are plotted
"""
pylab.xlabel(x_key)
curves = defaultdict(lambda : defaultdict(list))
for i in range(len(data[x_key])):
# Check is this particular entry should be used
if not all(condition(data, i) for condition in conditions):
continue
# Get the value of the independent variable for this entry
x_value = float(data[x_key][i])
# Attach the corresponding value to the respective partition
y_value = float(data[y_key][i])
curves[y_key][x_value].append(y_value)
for y_key, curve in curves.iteritems():
curve_x = []
curve_y = []
for x_value, y_values in sorted(curve.iteritems()):
curve_x.append(x_value)
curve_y.append(y_values)
# create the actual plot
if show_errors:
# Create an errorbar plot
pylab.errorbar(x = curve_x,
y = map(numpy.mean, curve_y),
yerr = map(numpy.std, curve_y),
elinewidth = 1,
capsize = 5,
label = y_key)
else:
# Create a simple plot
pylab.plot(curve_x,
map(numpy.mean, curve_y),
label = y_key)
pylab.legend(loc = 0)
pylab.ylabel(y_key)
pylab.savefig("%s.pdf" % os.path.join(result_dir,
"_".join([y_key, x_key])))
pylab.gca().clear()
pylab.close("all")
[docs] def _plot_numeric_vs_numeric(self, data, result_dir, axis_keys, value_key):
""" Contour plot of the value_keys for the two numeric parameters axis_keys.
A method that allows to create a contour plot that visualizes the effect
of differing two variables on a third one (e.g. the effect of differing
the lower and upper cutoff frequency of a bandpass filter onto
the accuracy).
**Expected arguments**
:data: A dictionary that contains a mapping from an attribute
(e.g. accuracy) to a list of values taken by an attribute.
An entry is the entirety of all i-th values over all dict-values
:result_dir: The directory in which the plots will be saved.
:axis_keys: The two keys of the dictionary that are assumed to have
an effect on a third variable (the dependent variable)
:value_key: The dependent variables whose values determine the
color of the contour plot
"""
assert(len(axis_keys) == 2)
# Determine a sorted list of the values taken on by the axis keys:
x_values = set([float(value) for value in data[axis_keys[0]]])
x_values = sorted(list(x_values))
y_values = set([float(value) for value in data[axis_keys[1]]])
y_values = sorted(list(y_values))
#Done
# We cannot create a contour plot if one dimension is only 1d
if len(x_values) == 1 or len(y_values) == 1:
return
# Create a meshgrid of them
X, Y = pylab.meshgrid(x_values, y_values)
# Determine the average value taken on by the dependent variable
# for each combination of the the two source variables
Z = numpy.zeros((len(x_values),len(y_values)))
counter = numpy.zeros((len(x_values),len(y_values)))
for i in range(len(data[axis_keys[0]])):
x_value = float(data[axis_keys[0]][i])
y_value = float(data[axis_keys[1]][i])
value = float(data[value_key][i])
Z[x_values.index(x_value), y_values.index(y_value)] += value
counter[x_values.index(x_value), y_values.index(y_value)] += 1
Z = Z / counter
# Create the plot for this specific dependent variable
pylab.figure()
cf = pylab.contourf(X, Y, Z.T, N = 20)
pylab.colorbar(cf)
pylab.xlabel(axis_keys[0])
pylab.ylabel(axis_keys[1])
pylab.xlim(min(x_values), max(x_values))
pylab.ylim(min(y_values), max(y_values))
pylab.title(value_key)
pylab.savefig("%s%s%s_%s_vs_%s.pdf" % (result_dir, os.sep, value_key,
axis_keys[0], axis_keys[1]))
pylab.gca().clear()
pylab.close("all")
[docs] def _plot_numeric_vs_nominal(self, data, result_dir, numeric_key,
nominal_key, value_key):
""" Plot for comparison of several different values of a nominal parameter
A method that allows to create a plot that visualizes the effect of
varying one numeric parameter onto the performance for several
different values of a nominal parameter.
**Expected arguments**
:data: A dictionary that contains a mapping from an attribute
(e.g. accuracy) to a list of values taken by an attribute.
An entry is the entirety of all i-th values over all dict-values
:result_dir: The directory in which the plots will be saved.
:numeric_key: The numeric parameter whose effect (together with the
nominal parameter) onto the dependent variable should
be investigated.
:nominal_key: The nominal parameter whose effect (together with the
numeric parameter) onto the dependent variable should
be investigated.
:value_key: The dependent variables whose values determine the
color of the contour plot
"""
# Determine a mapping from the value of the nominal value to a mapping
# from the value of the numeric value to the achieved performance:
# nominal -> (numeric -> performance)
curves = defaultdict(lambda: defaultdict(list))
for i in range(len(data[nominal_key])):
curve_key = data[nominal_key][i]
parameter_value = float(data[numeric_key][i])
if value_key[0] is not "#":
performance_value = float(data[value_key][i])
else: # A weighted cost function
weight1, value_key1, weight2, value_key2 = value_key[1:].split("#")
performance_value = float(weight1) * float(data[value_key1][i]) \
+ float(weight2) * float(data[value_key2][i])
curves[curve_key][parameter_value].append(performance_value)
linecycler = itertools.cycle( ['-']*7 + ['--']*7 +
['-.']*7 + [':']*7 ).next
# Iterate over all values of the nominal parameter and create one curve
# in the plot showing the mapping from numeric parameter to performance
# for this particular value of the nominal parameter
for curve_key, curve in curves.iteritems():
x_values = []
y_values = []
for x_value, y_value in sorted(curve.iteritems()):
x_values.append(x_value)
# Plot the mean of all values of the performance for this
# particular combination of nominal and numeric parameter
y_values.append(pylab.mean(y_value))
pylab.plot(x_values, y_values, label = curve_key, linestyle=linecycler())
pylab.gca().set_xlabel(numeric_key.replace("_", " "))
if value_key[0] is not "#":
pylab.gca().set_ylabel(value_key.replace("_", " "))
else:
pylab.gca().set_ylabel("%s*%s+%s*%s" % tuple(value_key[1:].split("#")))
if len(curves) > 6:
prop = matplotlib.font_manager.FontProperties(size='small')
pylab.legend(prop=prop, loc=0, ncol=2)
else:
pylab.legend(loc=0)
pylab.savefig("%s%s%s_%s_vs_%s.pdf" % (result_dir, os.sep, value_key,
nominal_key, numeric_key))
pylab.gca().clear()
pylab.close("all")
[docs] def _plot_nominal(self, data, result_dir, x_key, y_key):
""" Creates a boxplot of the y_keys for the given nominal parameter x_key.
A method that allows to create a plot that visualizes the effect
of differing one nominal variable onto a second one (e.g. the effect of
differing the classifier onto the accuracy).
**Expected arguments**
:data: A dictionary, that contains a mapping from an attribute
(e.g. accuracy) to a list of values taken by an attribute.
An entry is the entirety of all i-th values over all dict-values
:result_dir: The director in which the plots will be saved.
:x_key: The key of the dictionary whose values should be used as
values for the x-axis (the independent variables)
:y_key: The key of the dictionary whose values should be used as
values for the y-axis, i.e. the dependent variable
"""
# Create the plot for this specific dependent variable
values = defaultdict(list)
for i in range(len(data[x_key])):
parameter_value = data[x_key][i]
if y_key[0] is not "#":
performance_value = float(data[y_key][i])
else: # A weighted cost function
weight1, y_key1, weight2, y_key2 = y_key[1:].split("#")
performance_value = float(weight1) * float(data[y_key1][i]) \
+ float(weight2) * float(data[y_key2][i])
values[parameter_value].append(performance_value)
values = sorted(values.items())
# values = [("Standard_vs_Target", values["Standard_vs_Target"]),
# ("MissedTarget_vs_Target", values["MissedTarget_vs_Target"])]
pylab.subplots_adjust(bottom = 0.3, # the bottom of the subplots of the figure
)
pylab.boxplot(map(lambda x: x[1], values))
pylab.gca().set_xticklabels(map(lambda x: x[0], values))
pylab.setp(pylab.gca().get_xticklabels(), rotation=-90)
pylab.setp(pylab.gca().get_xticklabels(), size='x-small')
pylab.gca().set_xlabel(x_key.replace("_", " "))
if y_key[0] is not "#":
pylab.gca().set_ylabel(y_key.replace("_", " "))
else:
pylab.gca().set_ylabel("%s*%s+%s*%s" % tuple(y_key[1:].split("#")))
pylab.savefig("%s%s%s_%s.pdf" % (result_dir, os.sep, y_key, x_key))
pylab.gca().clear()
pylab.close("all")