#region License Information
/* HeuristicLab
* Copyright (C) 2002-2018 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 .
*/
#endregion
using System;
using System.Collections;
using System.Collections.Generic;
using System.Drawing;
using System.Globalization;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using HeuristicLab.Common;
using HeuristicLab.MainForm;
using HeuristicLab.Visualization.ChartControlsExtensions;
namespace HeuristicLab.Problems.DataAnalysis.Views {
[View("Partial Dependence Plots")]
[Content(typeof(IRegressionSolution))]
public partial class RegressionSolutionPartialDependencePlotView : DataAnalysisSolutionEvaluationView {
private readonly Dictionary partialDependencePlots;
private readonly Dictionary densityCharts;
private readonly Dictionary groupingPanels;
private ModifiableDataset sharedFixedVariables;
private const int Points = 200;
private int MaxColumns = 4;
private IEnumerable VisibleVariables {
get {
foreach (ListViewItem item in variableListView.CheckedItems)
yield return item.Text;
}
}
private IEnumerable VisiblePartialDependencePlots {
get { return VisibleVariables.Select(v => partialDependencePlots[v]); }
}
private IEnumerable VisibleDensityCharts {
get { return VisibleVariables.Select(v => densityCharts[v]); }
}
private IEnumerable VisibleChartsPanels {
get { return VisibleVariables.Select(v => groupingPanels[v]); }
}
public RegressionSolutionPartialDependencePlotView() {
InitializeComponent();
partialDependencePlots = new Dictionary();
densityCharts = new Dictionary();
groupingPanels = new Dictionary();
limitView.Content = new DoubleLimit(0, 1);
limitView.Content.ValueChanged += limit_ValueChanged;
densityComboBox.SelectedIndex = 1; // select Training
// Avoid additional horizontal scrollbar
var vertScrollWidth = SystemInformation.VerticalScrollBarWidth;
scrollPanel.Padding = new Padding(0, 0, vertScrollWidth, 0);
scrollPanel.AutoScroll = true;
}
public new IRegressionSolution Content {
get { return (IRegressionSolution)base.Content; }
set { base.Content = value; }
}
protected override void RegisterContentEvents() {
base.RegisterContentEvents();
Content.ModelChanged += solution_ModelChanged;
}
protected override void DeregisterContentEvents() {
Content.ModelChanged -= solution_ModelChanged;
base.DeregisterContentEvents();
}
protected override void OnContentChanged() {
base.OnContentChanged();
if (Content == null) return;
var problemData = Content.ProblemData;
if (sharedFixedVariables != null) {
sharedFixedVariables.ItemChanged -= SharedFixedVariables_ItemChanged;
sharedFixedVariables.Reset -= SharedFixedVariables_Reset;
}
// Init Y-axis range
double min = double.MaxValue, max = double.MinValue;
var trainingTarget = problemData.Dataset.GetDoubleValues(problemData.TargetVariable, problemData.TrainingIndices);
foreach (var t in trainingTarget) {
if (t < min) min = t;
if (t > max) max = t;
}
double range = max - min;
const double scale = 1.0 / 3.0;
double axisMin, axisMax, axisInterval;
ChartUtil.CalculateAxisInterval(min - scale * range, max + scale * range, 5, out axisMin, out axisMax, out axisInterval);
automaticYAxisCheckBox.Checked = false;
limitView.ReadOnly = false;
limitView.Content.Lower = axisMin;
limitView.Content.Upper = axisMax;
// create dataset of problemData input variables and model input variables
// necessary workaround to have the variables in the occuring order
var inputvariables =
new HashSet(Content.ProblemData.AllowedInputVariables.Union(Content.Model.VariablesUsedForPrediction));
var allowedInputVariables =
Content.ProblemData.Dataset.VariableNames.Where(v => inputvariables.Contains(v)).ToList();
var doubleVariables = allowedInputVariables.Where(problemData.Dataset.VariableHasType);
var doubleVariableValues = (IEnumerable)doubleVariables.Select(x => new List {
problemData.Dataset.GetDoubleValue(x, 0)
});
var factorVariables = allowedInputVariables.Where(problemData.Dataset.VariableHasType);
var factorVariableValues = (IEnumerable)factorVariables.Select(x => new List {
problemData.Dataset.GetStringValue(x, 0)
});
sharedFixedVariables = new ModifiableDataset(doubleVariables.Concat(factorVariables), doubleVariableValues.Concat(factorVariableValues));
variableValuesModeComboBox.SelectedItem = "Median"; // triggers UpdateVariableValue and changes shardFixedVariables
// create controls
partialDependencePlots.Clear();
densityCharts.Clear();
groupingPanels.Clear();
foreach (var variableName in doubleVariables) {
var plot = CreatePartialDependencePlot(variableName, sharedFixedVariables);
partialDependencePlots.Add(variableName, plot);
var densityChart = new DensityChart() {
Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right,
Margin = Padding.Empty,
Height = 12,
Visible = false,
Top = (int)(plot.Height * 0.1),
};
densityCharts.Add(variableName, densityChart);
plot.ZoomChanged += (o, e) => {
var pdp = (PartialDependencePlot)o;
var density = densityCharts[pdp.FreeVariable];
density.Visible = densityComboBox.SelectedIndex != 0 && !pdp.IsZoomed;
if (density.Visible)
UpdateDensityChart(density, pdp.FreeVariable);
};
plot.SizeChanged += (o, e) => {
var pdp = (PartialDependencePlot)o;
var density = densityCharts[pdp.FreeVariable];
density.Top = (int)(pdp.Height * 0.1);
};
// Initially, the inner plot areas are not initialized for hidden charts (scrollpanel, ...)
// This event handler listens for the paint event once (where everything is already initialized) to do some manual layouting.
plot.ChartPostPaint += OnPartialDependencePlotPostPaint;
var panel = new Panel() {
Dock = DockStyle.Fill,
Margin = Padding.Empty,
BackColor = Color.White
};
panel.Controls.Add(densityChart);
panel.Controls.Add(plot);
groupingPanels.Add(variableName, panel);
}
foreach (var variableName in factorVariables) {
var plot = CreateFactorPartialDependencePlot(variableName, sharedFixedVariables);
partialDependencePlots.Add(variableName, plot);
var densityChart = new DensityChart() {
Anchor = AnchorStyles.Left | AnchorStyles.Top | AnchorStyles.Right,
Margin = Padding.Empty,
Height = 12,
Visible = false,
Top = (int)(plot.Height * 0.1),
};
densityCharts.Add(variableName, densityChart);
plot.ZoomChanged += (o, e) => {
var pdp = (FactorPartialDependencePlot)o;
var density = densityCharts[pdp.FreeVariable];
density.Visible = densityComboBox.SelectedIndex != 0 && !pdp.IsZoomed;
if (density.Visible)
UpdateDensityChart(density, pdp.FreeVariable);
};
plot.SizeChanged += (o, e) => {
var pdp = (FactorPartialDependencePlot)o;
var density = densityCharts[pdp.FreeVariable];
density.Top = (int)(pdp.Height * 0.1);
};
// Initially, the inner plot areas are not initialized for hidden charts (scrollpanel, ...)
// This event handler listens for the paint event once (where everything is already initialized) to do some manual layouting.
plot.ChartPostPaint += OnFactorPartialDependencePlotPostPaint;
var panel = new Panel() {
Dock = DockStyle.Fill,
Margin = Padding.Empty,
BackColor = Color.White
};
panel.Controls.Add(densityChart);
panel.Controls.Add(plot);
groupingPanels.Add(variableName, panel);
}
// update variable list
variableListView.ItemChecked -= variableListView_ItemChecked;
variableListView.Items.Clear();
foreach (var variable in allowedInputVariables)
variableListView.Items.Add(key: variable, text: variable, imageIndex: 0);
foreach (var variable in Content.Model.VariablesUsedForPrediction)
variableListView.Items[variable].Checked = true;
variableListView.ItemChecked += variableListView_ItemChecked;
sharedFixedVariables.ItemChanged += SharedFixedVariables_ItemChanged;
sharedFixedVariables.Reset += SharedFixedVariables_Reset;
rowNrNumericUpDown.Maximum = Content.ProblemData.Dataset.Rows - 1;
RecalculateAndRelayoutCharts();
}
private void SharedFixedVariables_ItemChanged(object sender, EventArgs e) {
SharedFixedVariablesChanged();
}
private void SharedFixedVariables_Reset(object sender, EventArgs e) {
SharedFixedVariablesChanged();
}
private void SharedFixedVariablesChanged() {
if (!setVariableValues) // set mode to "nothing" if change was not initiated from a "mode change"
variableValuesModeComboBox.SelectedIndex = -1;
double yValue = Content.Model.GetEstimatedValues(sharedFixedVariables, new[] { 0 }).Single();
string title = Content.ProblemData.TargetVariable + ": " + yValue.ToString("G5", CultureInfo.CurrentCulture);
foreach (var chart in partialDependencePlots.Values) {
if (!string.IsNullOrEmpty(chart.YAxisTitle)) { // only show title for first column in grid
chart.YAxisTitle = title;
}
}
}
private void OnPartialDependencePlotPostPaint(object o, EventArgs e) {
var plot = (PartialDependencePlot)o;
var density = densityCharts[plot.FreeVariable];
density.Width = plot.Width;
var gcPlotPosition = plot.InnerPlotPosition;
density.Left = (int)(gcPlotPosition.X / 100.0 * plot.Width);
density.Width = (int)(gcPlotPosition.Width / 100.0 * plot.Width);
plot.UpdateTitlePosition();
// removed after succesful layouting due to performance reasons
if (gcPlotPosition.Width != 0)
plot.ChartPostPaint -= OnPartialDependencePlotPostPaint;
}
private void OnFactorPartialDependencePlotPostPaint(object o, EventArgs e) {
var plot = (FactorPartialDependencePlot)o;
var density = densityCharts[plot.FreeVariable];
density.Width = plot.Width;
var gcPlotPosition = plot.InnerPlotPosition;
density.Left = (int)(gcPlotPosition.X / 100.0 * plot.Width);
density.Width = (int)(gcPlotPosition.Width / 100.0 * plot.Width);
plot.UpdateTitlePosition();
// removed after succesful layouting due to performance reasons
if (gcPlotPosition.Width != 0)
plot.ChartPostPaint -= OnFactorPartialDependencePlotPostPaint;
}
private async void RecalculateAndRelayoutCharts() {
foreach (var variable in VisibleVariables) {
var plot = partialDependencePlots[variable];
await plot.RecalculateAsync(false, false);
}
partialDependencePlotTableLayout.SuspendLayout();
SetupYAxis();
ReOrderControls();
SetStyles();
partialDependencePlotTableLayout.ResumeLayout();
partialDependencePlotTableLayout.Refresh();
foreach (var variable in VisibleVariables) {
DensityChart densityChart;
if (densityCharts.TryGetValue(variable, out densityChart)) {
UpdateDensityChart(densityChart, variable);
}
}
}
private PartialDependencePlot CreatePartialDependencePlot(string variableName, ModifiableDataset sharedFixedVariables) {
var plot = new PartialDependencePlot {
Dock = DockStyle.Fill,
Margin = Padding.Empty,
ShowLegend = false,
ShowCursor = true,
ShowConfigButton = false,
YAxisTicks = 5,
};
plot.VariableValueChanged += async (o, e) => {
var recalculations = VisiblePartialDependencePlots
.Except(new[] { (IPartialDependencePlot)o })
.Select(async chart => {
await chart.RecalculateAsync(updateOnFinish: false, resetYAxis: false);
}).ToList();
await Task.WhenAll(recalculations);
if (recalculations.All(t => t.IsCompleted))
SetupYAxis();
};
plot.Configure(new[] { Content }, sharedFixedVariables, variableName, Points);
plot.SolutionAdded += partialDependencePlot_SolutionAdded;
plot.SolutionRemoved += partialDependencePlot_SolutionRemoved;
return plot;
}
private FactorPartialDependencePlot CreateFactorPartialDependencePlot(string variableName, ModifiableDataset sharedFixedVariables) {
var plot = new FactorPartialDependencePlot {
Dock = DockStyle.Fill,
Margin = Padding.Empty,
ShowLegend = false,
ShowCursor = true,
YAxisTicks = 5,
};
plot.VariableValueChanged += async (o, e) => {
var recalculations = VisiblePartialDependencePlots
.Except(new[] { (FactorPartialDependencePlot)o })
.Select(async chart => {
await chart.RecalculateAsync(updateOnFinish: false, resetYAxis: false);
}).ToList();
await Task.WhenAll(recalculations);
if (recalculations.All(t => t.IsCompleted))
SetupYAxis();
};
var variableValues = Content.ProblemData.Dataset.GetStringValues(variableName).Distinct().OrderBy(n => n).ToList();
plot.Configure(new[] { Content }, sharedFixedVariables, variableName, variableValues);
plot.SolutionAdded += partialDependencePlot_SolutionAdded;
plot.SolutionRemoved += partialDependencePlot_SolutionRemoved;
return plot;
}
private void SetupYAxis() {
double axisMin, axisMax;
if (automaticYAxisCheckBox.Checked) {
double min = double.MaxValue, max = double.MinValue;
foreach (var chart in VisiblePartialDependencePlots) {
if (chart.YMin < min) min = chart.YMin;
if (chart.YMax > max) max = chart.YMax;
}
double axisInterval;
ChartUtil.CalculateAxisInterval(min, max, 5, out axisMin, out axisMax, out axisInterval);
} else {
axisMin = limitView.Content.Lower;
axisMax = limitView.Content.Upper;
}
foreach (var chart in VisiblePartialDependencePlots) {
chart.FixedYAxisMin = axisMin;
chart.FixedYAxisMax = axisMax;
}
}
// reorder chart controls so that they always appear in the same order as in the list view
// the table layout containing the controls should be suspended before calling this method
private void ReOrderControls() {
var tl = partialDependencePlotTableLayout;
tl.Controls.Clear();
int row = 0, column = 0;
double yValue = Content.Model.GetEstimatedValues(sharedFixedVariables, new[] { 0 }).Single();
string title = Content.ProblemData.TargetVariable + ": " + yValue.ToString("G5", CultureInfo.CurrentCulture);
foreach (var v in VisibleVariables) {
var chartsPanel = groupingPanels[v];
tl.Controls.Add(chartsPanel, column, row);
var chart = partialDependencePlots[v];
chart.YAxisTitle = column == 0 ? title : string.Empty;
column++;
if (column == MaxColumns) {
row++;
column = 0;
}
}
}
private void SetStyles() {
var tl = partialDependencePlotTableLayout;
tl.RowStyles.Clear();
tl.ColumnStyles.Clear();
int numVariables = VisibleVariables.Count();
if (numVariables == 0)
return;
// set column styles
tl.ColumnCount = Math.Min(numVariables, MaxColumns);
for (int c = 0; c < tl.ColumnCount; c++)
tl.ColumnStyles.Add(new ColumnStyle(SizeType.Percent, 100.0f / tl.ColumnCount));
// set row styles
tl.RowCount = (int)Math.Ceiling((double)numVariables / tl.ColumnCount);
var columnWidth = tl.Width / tl.ColumnCount; // assume all columns have the same width
var rowHeight = (int)(0.8 * columnWidth);
for (int r = 0; r < tl.RowCount; r++)
tl.RowStyles.Add(new RowStyle(SizeType.Absolute, rowHeight));
}
private async void partialDependencePlot_SolutionAdded(object sender, EventArgs e) {
var solution = e.Value;
foreach (var chart in partialDependencePlots.Values) {
if (sender == chart) continue;
await chart.AddSolutionAsync(solution);
}
}
private async void partialDependencePlot_SolutionRemoved(object sender, EventArgs e) {
var solution = e.Value;
foreach (var chart in partialDependencePlots.Values) {
if (sender == chart) continue;
await chart.RemoveSolutionAsync(solution);
}
}
private async void variableListView_ItemChecked(object sender, ItemCheckedEventArgs e) {
var item = e.Item;
var variable = item.Text;
var plot = partialDependencePlots[variable];
var chartsPanel = groupingPanels[variable];
var tl = partialDependencePlotTableLayout;
tl.SuspendLayout();
if (item.Checked) {
tl.Controls.Add(chartsPanel);
await plot.RecalculateAsync(false, false);
} else {
tl.Controls.Remove(chartsPanel);
}
if (tl.Controls.Count > 0) {
SetupYAxis();
ReOrderControls();
SetStyles();
}
tl.ResumeLayout();
tl.Refresh();
densityComboBox_SelectedIndexChanged(this, EventArgs.Empty);
}
private void automaticYAxisCheckBox_CheckedChanged(object sender, EventArgs e) {
limitView.ReadOnly = automaticYAxisCheckBox.Checked;
SetupYAxis();
partialDependencePlotTableLayout.Refresh();
densityComboBox_SelectedIndexChanged(this, EventArgs.Empty); // necessary to realign the density plots
}
private void limit_ValueChanged(object sender, EventArgs e) {
if (automaticYAxisCheckBox.Checked)
return;
SetupYAxis();
partialDependencePlotTableLayout.Refresh();
densityComboBox_SelectedIndexChanged(this, EventArgs.Empty); // necessary to realign the density plots
}
private void densityComboBox_SelectedIndexChanged(object sender, EventArgs e) {
if (Content == null)
return;
int si = densityComboBox.SelectedIndex;
if (si == 0) {
foreach (var densityChart in densityCharts.Values)
densityChart.Visible = false;
} else {
var indices = GetDensityIndices(si).ToList();
foreach (var entry in densityCharts) {
var variableName = entry.Key;
var densityChart = entry.Value;
if (!VisibleVariables.Contains(variableName) || partialDependencePlots[variableName].IsZoomed)
continue;
UpdateDensityChart(densityChart, variableName, indices);
}
}
}
private IEnumerable GetDensityIndices(int selectedIndex) {
var problemData = Content.ProblemData;
return
selectedIndex == 1 ? problemData.TrainingIndices :
selectedIndex == 2 ? problemData.TestIndices :
problemData.AllIndices;
}
private void UpdateDensityChart(DensityChart densityChart, string variable, IList indices = null) {
if (densityComboBox.SelectedIndex == 0)
return;
if (indices == null) {
indices = GetDensityIndices(densityComboBox.SelectedIndex).ToList();
}
if (Content.ProblemData.Dataset.VariableHasType(variable)) {
var data = Content.ProblemData.Dataset.GetDoubleValues(variable, indices).ToList();
var plot = partialDependencePlots[variable] as PartialDependencePlot;
if (plot != null) {
var min = plot.FixedXAxisMin;
var max = plot.FixedXAxisMax;
var buckets = plot.DrawingSteps;
if (min.HasValue && max.HasValue) {
densityChart.UpdateChart(data, min.Value, max.Value, buckets);
densityChart.Width = plot.Width;
var gcPlotPosition = plot.InnerPlotPosition;
densityChart.Left = (int)(gcPlotPosition.X / 100.0 * plot.Width);
densityChart.Width = (int)(gcPlotPosition.Width / 100.0 * plot.Width);
densityChart.Visible = true;
}
plot.UpdateTitlePosition();
}
} else if (Content.ProblemData.Dataset.VariableHasType(variable)) {
var data = Content.ProblemData.Dataset.GetStringValues(variable).ToList();
var plot = partialDependencePlots[variable] as FactorPartialDependencePlot;
if (plot != null) {
densityChart.UpdateChart(data);
densityChart.Width = plot.Width;
var gcPlotPosition = plot.InnerPlotPosition;
densityChart.Left = (int)(gcPlotPosition.X / 100.0 * plot.Width);
densityChart.Width = (int)(gcPlotPosition.Width / 100.0 * plot.Width);
densityChart.Visible = true;
plot.UpdateTitlePosition();
}
}
}
private void columnsNumericUpDown_ValueChanged(object sender, EventArgs e) {
MaxColumns = (int)columnsNumericUpDown.Value;
int columns = Math.Min(VisibleVariables.Count(), MaxColumns);
if (columns > 0) {
var tl = partialDependencePlotTableLayout;
MaxColumns = columns;
tl.SuspendLayout();
ReOrderControls();
SetStyles();
tl.ResumeLayout();
tl.Refresh();
densityComboBox_SelectedIndexChanged(this, EventArgs.Empty);
}
}
private async void solution_ModelChanged(object sender, EventArgs e) {
foreach (var variable in VisibleVariables) {
var pdp = partialDependencePlots[variable];
var densityChart = densityCharts[variable];
// recalculate and refresh
await pdp.RecalculateAsync(false, false);
pdp.Refresh();
UpdateDensityChart(densityChart, variable);
}
}
// flag that the current change is not triggered by a manual change from within a single plot
private bool setVariableValues = false;
private void variableValuesComboBox_SelectedValueChanged(object sender, EventArgs e) {
if (variableValuesModeComboBox.SelectedIndex == -1)
return; // changed to "manual" due to manual change of a variable
setVariableValues = true;
UpdateVariableValues();
setVariableValues = false;
}
private void rowNrNumericUpDown_ValueChanged(object sender, EventArgs e) {
if ((string)variableValuesModeComboBox.SelectedItem != "Row") {
variableValuesModeComboBox.SelectedItem = "Row"; // triggers UpdateVariableValues
} else {
setVariableValues = true;
UpdateVariableValues();
setVariableValues = false;
}
}
private void UpdateVariableValues() {
string mode = (string)variableValuesModeComboBox.SelectedItem;
var dataset = Content.ProblemData.Dataset;
object[] newRow;
if (mode == "Row") {
int rowNumber = (int)rowNrNumericUpDown.Value;
newRow = sharedFixedVariables.VariableNames
.Select(variableName => {
if (dataset.DoubleVariables.Contains(variableName)) {
return dataset.GetDoubleValue(variableName, rowNumber);
} else if (dataset.StringVariables.Contains(variableName)) {
return dataset.GetStringValue(variableName, rowNumber);
} else {
throw new NotSupportedException("Only double and string(factor) columns are currently supported.");
}
}).ToArray();
} else {
newRow = sharedFixedVariables.VariableNames
.Select(variableName => {
if (dataset.DoubleVariables.Contains(variableName)) {
var values = dataset.GetDoubleValues(variableName);
return
mode == "Mean" ? values.Average() :
mode == "Median" ? values.Median() :
mode == "Most Common" ? MostCommon(values) :
throw new NotSupportedException();
} else if (dataset.StringVariables.Contains(variableName)) {
var values = dataset.GetStringValues(variableName);
return
mode == "Mean" ? MostCommon(values) :
mode == "Median" ? MostCommon(values) :
mode == "Most Common" ? MostCommon(values) :
throw new NotSupportedException();
} else {
throw new NotSupportedException("Only double and string(factor) columns are currently supported.");
}
}).ToArray();
}
sharedFixedVariables.ReplaceRow(0, newRow);
}
private static T MostCommon(IEnumerable values) {
return values.GroupBy(x => x).OrderByDescending(g => g.Count()).Select(g => g.Key).First();
}
// ToolTips cannot be shown longer than 5000ms, only by using ToolTip.Show manually
// See: https://stackoverflow.com/questions/8225807/c-sharp-tooltip-doesnt-display-long-enough
private void variableValuesModeComboBox_MouseHover(object sender, EventArgs e) {
string tooltipText = @"Sets each variable to a specific value:
Row - Selects the value based on a specified row of the dataset.
Mean - Sets the value to the arithmetic mean of the variable.
Median - Sets the value to the median of the variable.
Most Common - Sets the value to the most common value of the variable (first if multiple).
Note: For categorical values, the most common value is used when selecting Mean, Median or Most Common.";
toolTip.Show(tooltipText, variableValuesModeComboBox, 30000);
toolTip.Active = true;
}
private void variableValuesModeComboBox_MouseLeave(object sender, EventArgs e) {
toolTip.Active = false;
}
}
}