source: trunk/sources/HeuristicLab.Problems.DataAnalysis.Views/3.4/Controls/FactorPartialDependencePlot.cs @ 15222

Last change on this file since 15222 was 15222, checked in by mkommend, 2 years ago

#2807: Removed obsolete comments from partial dependence plots.

File size: 21.4 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2016 Heuristic and Evolutionary Algorithms Laboratory (HEAL)
4 *
5 * This file is part of HeuristicLab.
6 *
7 * HeuristicLab is free software: you can redistribute it and/or modify
8 * it under the terms of the GNU General Public License as published by
9 * the Free Software Foundation, either version 3 of the License, or
10 * (at your option) any later version.
11 *
12 * HeuristicLab is distributed in the hope that it will be useful,
13 * but WITHOUT ANY WARRANTY; without even the implied warranty of
14 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
15 * GNU General Public License for more details.
16 *
17 * You should have received a copy of the GNU General Public License
18 * along with HeuristicLab. If not, see <http://www.gnu.org/licenses/>.
19 */
20#endregion
21
22using System;
23using System.Collections;
24using System.Collections.Generic;
25using System.Drawing;
26using System.Linq;
27using System.Threading;
28using System.Threading.Tasks;
29using System.Windows.Forms;
30using System.Windows.Forms.DataVisualization.Charting;
31using HeuristicLab.Common;
32using HeuristicLab.MainForm.WindowsForms;
33using HeuristicLab.Visualization.ChartControlsExtensions;
34
35namespace HeuristicLab.Problems.DataAnalysis.Views {
36  public partial class FactorPartialDependencePlot : UserControl, IPartialDependencePlot {
37    private ModifiableDataset sharedFixedVariables; // used for synchronising variable values between charts
38    private ModifiableDataset internalDataset; // holds the x values for each point drawn
39
40    private CancellationTokenSource cancelCurrentRecalculateSource;
41
42    private readonly List<IRegressionSolution> solutions;
43    private readonly Dictionary<IRegressionSolution, Series> seriesCache;
44    private readonly Dictionary<IRegressionSolution, Series> ciSeriesCache;
45
46    #region Properties
47    public string XAxisTitle {
48      get { return chart.ChartAreas[0].AxisX.Title; }
49      set { chart.ChartAreas[0].AxisX.Title = value; }
50    }
51
52    public string YAxisTitle {
53      get { return chart.ChartAreas[0].AxisY.Title; }
54      set { chart.ChartAreas[0].AxisY.Title = value; }
55    }
56
57    public bool ShowLegend {
58      get { return chart.Legends[0].Enabled; }
59      set { chart.Legends[0].Enabled = value; }
60    }
61    public bool ShowCursor {
62      get { return chart.Annotations[0].Visible; }
63      set {
64        chart.Annotations[0].Visible = value;
65        if (!value) chart.Titles[0].Text = string.Empty;
66      }
67    }
68
69    private int yAxisTicks = 5;
70    public int YAxisTicks {
71      get { return yAxisTicks; }
72      set {
73        if (value != yAxisTicks) {
74          yAxisTicks = value;
75          SetupAxis(chart, chart.ChartAreas[0].AxisY, yMin, yMax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
76          RecalculateInternalDataset();
77        }
78      }
79    }
80    private double? fixedYAxisMin;
81    public double? FixedYAxisMin {
82      get { return fixedYAxisMin; }
83      set {
84        if ((value.HasValue && fixedYAxisMin.HasValue && !value.Value.IsAlmost(fixedYAxisMin.Value)) || (value.HasValue != fixedYAxisMin.HasValue)) {
85          fixedYAxisMin = value;
86          SetupAxis(chart, chart.ChartAreas[0].AxisY, yMin, yMax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
87        }
88      }
89    }
90    private double? fixedYAxisMax;
91    public double? FixedYAxisMax {
92      get { return fixedYAxisMax; }
93      set {
94        if ((value.HasValue && fixedYAxisMax.HasValue && !value.Value.IsAlmost(fixedYAxisMax.Value)) || (value.HasValue != fixedYAxisMax.HasValue)) {
95          fixedYAxisMax = value;
96          SetupAxis(chart, chart.ChartAreas[0].AxisY, yMin, yMax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
97        }
98      }
99    }
100
101    private string freeVariable;
102    public string FreeVariable {
103      get { return freeVariable; }
104      set {
105        if (value == freeVariable) return;
106        if (solutions.Any(s => !s.ProblemData.Dataset.StringVariables.Contains(value))) {
107          throw new ArgumentException("Variable does not exist in the ProblemData of the Solutions.");
108        }
109        freeVariable = value;
110        RecalculateInternalDataset();
111      }
112    }
113
114    private double yMin;
115    public double YMin {
116      get { return yMin; }
117    }
118    private double yMax;
119    public double YMax {
120      get { return yMax; }
121    }
122
123    public bool IsZoomed {
124      get { return chart.ChartAreas[0].AxisX.ScaleView.IsZoomed; }
125    }
126
127    internal ElementPosition InnerPlotPosition {
128      get { return chart.ChartAreas[0].InnerPlotPosition; }
129    }
130    #endregion
131
132    private List<string> variableValues;
133
134    public event EventHandler ChartPostPaint;
135
136    public FactorPartialDependencePlot() {
137      InitializeComponent();
138
139      solutions = new List<IRegressionSolution>();
140      seriesCache = new Dictionary<IRegressionSolution, Series>();
141      ciSeriesCache = new Dictionary<IRegressionSolution, Series>();
142
143      // Configure axis
144      chart.CustomizeAllChartAreas();
145      chart.ChartAreas[0].CursorX.IsUserSelectionEnabled = true;
146      chart.ChartAreas[0].AxisX.ScaleView.Zoomable = true;
147      chart.ChartAreas[0].CursorX.Interval = 0;
148
149      chart.ChartAreas[0].CursorY.IsUserSelectionEnabled = true;
150      chart.ChartAreas[0].AxisY.ScaleView.Zoomable = true;
151      chart.ChartAreas[0].CursorY.Interval = 0;
152
153      Disposed += Control_Disposed;
154    }
155
156    private void Control_Disposed(object sender, EventArgs e) {
157      if (cancelCurrentRecalculateSource != null)
158        cancelCurrentRecalculateSource.Cancel();
159    }
160
161    public void Configure(IEnumerable<IRegressionSolution> solutions, ModifiableDataset sharedFixedVariables, string freeVariable, IList<string> variableValues, bool initializeAxisRanges = true) {
162      if (!SolutionsCompatible(solutions))
163        throw new ArgumentException("Solutions are not compatible with the problem data.");
164      this.freeVariable = freeVariable;
165      this.variableValues = new List<string>(variableValues);
166
167      this.solutions.Clear();
168      this.solutions.AddRange(solutions);
169
170      // add an event such that whenever a value is changed in the shared dataset,
171      // this change is reflected in the internal dataset (where the value becomes a whole column)
172      if (this.sharedFixedVariables != null)
173        this.sharedFixedVariables.ItemChanged -= sharedFixedVariables_ItemChanged;
174      this.sharedFixedVariables = sharedFixedVariables;
175      this.sharedFixedVariables.ItemChanged += sharedFixedVariables_ItemChanged;
176
177      RecalculateInternalDataset();
178
179      chart.Series.Clear();
180      seriesCache.Clear();
181      ciSeriesCache.Clear();
182      foreach (var solution in this.solutions) {
183        var series = CreateSeries(solution);
184        seriesCache.Add(solution, series.Item1);
185        if (series.Item2 != null)
186          ciSeriesCache.Add(solution, series.Item2);
187      }
188
189      InitSeriesData();
190      OrderAndColorSeries();
191
192    }
193
194    public async Task RecalculateAsync(bool updateOnFinish = true, bool resetYAxis = true) {
195      if (IsDisposed
196        || sharedFixedVariables == null || !solutions.Any() || string.IsNullOrEmpty(freeVariable)
197        || !variableValues.Any())
198        return;
199
200      calculationPendingTimer.Start();
201
202      // cancel previous recalculate call
203      if (cancelCurrentRecalculateSource != null)
204        cancelCurrentRecalculateSource.Cancel();
205      cancelCurrentRecalculateSource = new CancellationTokenSource();
206      var cancellationToken = cancelCurrentRecalculateSource.Token;
207
208      // Update series
209      try {
210        var limits = await UpdateAllSeriesDataAsync(cancellationToken);
211        chart.Invalidate();
212
213        yMin = limits.Lower;
214        yMax = limits.Upper;
215        // Set y-axis
216        if (resetYAxis)
217          SetupAxis(chart, chart.ChartAreas[0].AxisY, yMin, yMax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
218
219        calculationPendingTimer.Stop();
220        calculationPendingLabel.Visible = false;
221        if (updateOnFinish)
222          Update();
223      }
224      catch (OperationCanceledException) { }
225      catch (AggregateException ae) {
226        if (!ae.InnerExceptions.Any(e => e is OperationCanceledException))
227          throw;
228      }
229    }
230
231    public void UpdateTitlePosition() {
232      var title = chart.Titles[0];
233      var plotArea = InnerPlotPosition;
234
235      title.Visible = plotArea.Width != 0;
236
237      title.Position.X = plotArea.X + (plotArea.Width / 2);
238    }
239
240    private static void SetupAxis(EnhancedChart chart, Axis axis, double minValue, double maxValue, int ticks, double? fixedAxisMin, double? fixedAxisMax) {
241      //guard if only one distinct value is present
242      if (minValue.IsAlmost(maxValue)) {
243        minValue = minValue - 0.5;
244        maxValue = minValue + 0.5;
245      }
246
247      double axisMin, axisMax, axisInterval;
248      ChartUtil.CalculateAxisInterval(minValue, maxValue, ticks, out axisMin, out axisMax, out axisInterval);
249      axis.Minimum = fixedAxisMin ?? axisMin;
250      axis.Maximum = fixedAxisMax ?? axisMax;
251      axis.Interval = (axis.Maximum - axis.Minimum) / ticks;
252
253      chart.ChartAreas[0].RecalculateAxesScale();
254    }
255
256
257    private void RecalculateInternalDataset() {
258      if (sharedFixedVariables == null)
259        return;
260
261      var factorValues = new List<string>(variableValues);
262
263      var variables = sharedFixedVariables.VariableNames.ToList();
264      var values = new List<IList>();
265      foreach (var varName in variables) {
266        if (varName == FreeVariable) {
267          values.Add(factorValues);
268        } else if (sharedFixedVariables.VariableHasType<double>(varName)) {
269          values.Add(Enumerable.Repeat(sharedFixedVariables.GetDoubleValue(varName, 0), factorValues.Count).ToList());
270        } else if (sharedFixedVariables.VariableHasType<string>(varName)) {
271          values.Add(Enumerable.Repeat(sharedFixedVariables.GetStringValue(varName, 0), factorValues.Count).ToList());
272        }
273      }
274
275      internalDataset = new ModifiableDataset(variables, values);
276    }
277
278    private Tuple<Series, Series> CreateSeries(IRegressionSolution solution) {
279      var series = new Series {
280        ChartType = SeriesChartType.Column,
281        Name = solution.ProblemData.TargetVariable + " " + solutions.IndexOf(solution),
282        XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.String
283      };
284      series.LegendText = series.Name;
285
286      Series confidenceIntervalSeries = null;
287      confidenceIntervalSeries = new Series {
288        ChartType = SeriesChartType.BoxPlot,
289        XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.String,
290        Color = Color.Black,
291        YValuesPerPoint = 5,
292        Name = "95% Conf. Interval " + series.Name,
293        IsVisibleInLegend = false
294      };
295      return Tuple.Create(series, confidenceIntervalSeries);
296    }
297
298    private void OrderAndColorSeries() {
299      chart.SuspendRepaint();
300
301      chart.Series.Clear();
302      // Add mean series for applying palette colors
303      foreach (var solution in solutions) {
304        chart.Series.Add(seriesCache[solution]);
305      }
306
307      chart.Palette = ChartColorPalette.BrightPastel;
308      chart.ApplyPaletteColors();
309      chart.Palette = ChartColorPalette.None;
310
311      // Add confidence interval series after its coresponding series for correct z index
312      foreach (var solution in solutions) {
313        Series ciSeries;
314        if (ciSeriesCache.TryGetValue(solution, out ciSeries)) {
315          int idx = chart.Series.IndexOf(seriesCache[solution]);
316          chart.Series.Insert(idx + 1, ciSeries);
317        }
318      }
319
320      chart.ResumeRepaint(true);
321    }
322
323    private async Task<DoubleLimit> UpdateAllSeriesDataAsync(CancellationToken cancellationToken) {
324      var updateTasks = solutions.Select(solution => UpdateSeriesDataAsync(solution, cancellationToken));
325
326      double min = double.MaxValue, max = double.MinValue;
327      foreach (var update in updateTasks) {
328        var limit = await update;
329        if (limit.Lower < min) min = limit.Lower;
330        if (limit.Upper > max) max = limit.Upper;
331      }
332
333      return new DoubleLimit(min, max);
334    }
335
336    private Task<DoubleLimit> UpdateSeriesDataAsync(IRegressionSolution solution, CancellationToken cancellationToken) {
337      return Task.Run(() => {
338        var yvalues = solution.Model.GetEstimatedValues(internalDataset, Enumerable.Range(0, internalDataset.Rows)).ToList();
339
340        double min = double.MaxValue, max = double.MinValue;
341
342        var series = seriesCache[solution];
343        for (int i = 0; i < variableValues.Count; i++) {
344          series.Points[i].SetValueXY(variableValues[i], yvalues[i]);
345          if (yvalues[i] < min) min = yvalues[i];
346          if (yvalues[i] > max) max = yvalues[i];
347        }
348
349        cancellationToken.ThrowIfCancellationRequested();
350
351        var confidenceBoundSolution = solution as IConfidenceRegressionSolution;
352        if (confidenceBoundSolution != null) {
353          var confidenceIntervalSeries = ciSeriesCache[solution];
354          var variances = confidenceBoundSolution.Model.GetEstimatedVariances(internalDataset, Enumerable.Range(0, internalDataset.Rows)).ToList();
355          for (int i = 0; i < variableValues.Count; i++) {
356            var lower = yvalues[i] - 1.96 * Math.Sqrt(variances[i]);
357            var upper = yvalues[i] + 1.96 * Math.Sqrt(variances[i]);
358            confidenceIntervalSeries.Points[i].SetValueXY(variableValues[i], lower, upper, yvalues[i], yvalues[i], yvalues[i]);
359            if (lower < min) min = lower;
360            if (upper > max) max = upper;
361          }
362        }
363
364        cancellationToken.ThrowIfCancellationRequested();
365        return new DoubleLimit(min, max);
366      }, cancellationToken);
367    }
368
369    private void InitSeriesData() {
370      if (internalDataset == null)
371        return;
372
373      foreach (var solution in solutions)
374        InitSeriesData(solution, variableValues);
375    }
376
377    private void InitSeriesData(IRegressionSolution solution, IList<string> values) {
378
379      var series = seriesCache[solution];
380      series.Points.SuspendUpdates();
381      series.Points.Clear();
382      for (int i = 0; i < values.Count; i++) {
383        series.Points.AddXY(values[i], 0.0);
384        series.Points.Last().ToolTip = values[i];
385      }
386
387      UpdateAllSeriesStyles(variableValues.IndexOf(sharedFixedVariables.GetStringValue(FreeVariable, 0)));
388      series.Points.ResumeUpdates();
389
390      Series confidenceIntervalSeries;
391      if (ciSeriesCache.TryGetValue(solution, out confidenceIntervalSeries)) {
392        confidenceIntervalSeries.Points.SuspendUpdates();
393        confidenceIntervalSeries.Points.Clear();
394        for (int i = 0; i < values.Count; i++)
395          confidenceIntervalSeries.Points.AddXY(values[i], 0.0, 0.0, 0.0, 0.0, 0.0);
396        confidenceIntervalSeries.Points.ResumeUpdates();
397      }
398    }
399
400    public async Task AddSolutionAsync(IRegressionSolution solution) {
401      if (!SolutionsCompatible(solutions.Concat(new[] { solution })))
402        throw new ArgumentException("The solution is not compatible with the problem data.");
403      if (solutions.Contains(solution))
404        return;
405
406      solutions.Add(solution);
407
408      var series = CreateSeries(solution);
409      seriesCache.Add(solution, series.Item1);
410      if (series.Item2 != null)
411        ciSeriesCache.Add(solution, series.Item2);
412
413      InitSeriesData(solution, variableValues);
414      OrderAndColorSeries();
415
416      await RecalculateAsync();
417      var args = new EventArgs<IRegressionSolution>(solution);
418      OnSolutionAdded(this, args);
419    }
420
421    public async Task RemoveSolutionAsync(IRegressionSolution solution) {
422      if (!solutions.Remove(solution))
423        return;
424
425      seriesCache.Remove(solution);
426      ciSeriesCache.Remove(solution);
427
428      await RecalculateAsync();
429      var args = new EventArgs<IRegressionSolution>(solution);
430      OnSolutionRemoved(this, args);
431    }
432
433    private static bool SolutionsCompatible(IEnumerable<IRegressionSolution> solutions) {
434      var refSolution = solutions.First();
435      var refSolVars = refSolution.ProblemData.Dataset.VariableNames;
436      foreach (var solution in solutions.Skip(1)) {
437        var variables1 = solution.ProblemData.Dataset.VariableNames;
438        if (!variables1.All(refSolVars.Contains))
439          return false;
440
441        foreach (var factorVar in variables1.Where(solution.ProblemData.Dataset.VariableHasType<string>)) {
442          var distinctVals = refSolution.ProblemData.Dataset.GetStringValues(factorVar).Distinct();
443          if (solution.ProblemData.Dataset.GetStringValues(factorVar).Any(val => !distinctVals.Contains(val))) return false;
444        }
445      }
446      return true;
447    }
448
449    #region Events
450    public event EventHandler<EventArgs<IRegressionSolution>> SolutionAdded;
451    public void OnSolutionAdded(object sender, EventArgs<IRegressionSolution> args) {
452      var added = SolutionAdded;
453      if (added == null) return;
454      added(sender, args);
455    }
456
457    public event EventHandler<EventArgs<IRegressionSolution>> SolutionRemoved;
458    public void OnSolutionRemoved(object sender, EventArgs<IRegressionSolution> args) {
459      var removed = SolutionRemoved;
460      if (removed == null) return;
461      removed(sender, args);
462    }
463
464    public event EventHandler VariableValueChanged;
465    public void OnVariableValueChanged(object sender, EventArgs args) {
466      var changed = VariableValueChanged;
467      if (changed == null) return;
468      changed(sender, args);
469    }
470
471    public event EventHandler ZoomChanged;
472    public void OnZoomChanged(object sender, EventArgs args) {
473      var changed = ZoomChanged;
474      if (changed == null) return;
475      changed(sender, args);
476    }
477
478    private void sharedFixedVariables_ItemChanged(object o, EventArgs<int, int> e) {
479      if (o != sharedFixedVariables) return;
480      var variables = sharedFixedVariables.VariableNames.ToList();
481      var rowIndex = e.Value;
482      var columnIndex = e.Value2;
483
484      var variableName = variables[columnIndex];
485      if (variableName == FreeVariable) return;
486      if (internalDataset.VariableHasType<double>(variableName)) {
487        var v = sharedFixedVariables.GetDoubleValue(variableName, rowIndex);
488        var values = new List<double>(Enumerable.Repeat(v, internalDataset.Rows));
489        internalDataset.ReplaceVariable(variableName, values);
490      } else if (internalDataset.VariableHasType<string>(variableName)) {
491        var v = sharedFixedVariables.GetStringValue(variableName, rowIndex);
492        var values = new List<String>(Enumerable.Repeat(v, internalDataset.Rows));
493        internalDataset.ReplaceVariable(variableName, values);
494      } else {
495        // unsupported type
496        throw new NotSupportedException();
497      }
498    }
499
500    private async void chart_DragDrop(object sender, DragEventArgs e) {
501      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
502      if (data != null) {
503        var solution = data as IRegressionSolution;
504        if (!solutions.Contains(solution))
505          await AddSolutionAsync(solution);
506      }
507    }
508    private void chart_DragEnter(object sender, DragEventArgs e) {
509      if (!e.Data.GetDataPresent(HeuristicLab.Common.Constants.DragDropDataFormat)) return;
510      e.Effect = DragDropEffects.None;
511
512      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
513      var regressionSolution = data as IRegressionSolution;
514      if (regressionSolution != null) {
515        e.Effect = DragDropEffects.Copy;
516      }
517    }
518
519    private void calculationPendingTimer_Tick(object sender, EventArgs e) {
520      calculationPendingLabel.Visible = true;
521      Update();
522    }
523
524    private void chart_SelectionRangeChanged(object sender, CursorEventArgs e) {
525      OnZoomChanged(this, EventArgs.Empty);
526    }
527
528    private void chart_Resize(object sender, EventArgs e) {
529      UpdateTitlePosition();
530    }
531
532    private void chart_PostPaint(object sender, ChartPaintEventArgs e) {
533      if (ChartPostPaint != null)
534        ChartPostPaint(this, EventArgs.Empty);
535    }
536    #endregion
537
538    private void chart_MouseClick(object sender, MouseEventArgs e) {
539      var hitTestResult = chart.HitTest(e.X, e.Y, ChartElementType.DataPoint);
540      if (hitTestResult != null && hitTestResult.ChartElementType == ChartElementType.DataPoint) {
541        var series = hitTestResult.Series;
542        var dataPoint = (DataPoint)hitTestResult.Object;
543        var idx = series.Points.IndexOf(dataPoint);
544        UpdateSelectedValue(variableValues[idx]);
545
546        UpdateAllSeriesStyles(idx);
547      }
548    }
549
550    private void UpdateAllSeriesStyles(int selectedValueIndex) {
551      if (ShowCursor) {
552        chart.Titles[0].Text = FreeVariable + " : " + variableValues[selectedValueIndex];
553        chart.Update();
554      }
555      foreach (var s in seriesCache.Values) {
556        if (s.ChartType == SeriesChartType.Column)
557          for (int i = 0; i < s.Points.Count; i++) {
558            if (i != selectedValueIndex) {
559              s.Points[i].BorderDashStyle = ChartDashStyle.NotSet;
560            } else {
561              s.Points[i].BorderDashStyle = ChartDashStyle.Dash;
562              s.Points[i].BorderColor = Color.Red;
563            }
564          }
565      }
566    }
567
568    private void UpdateSelectedValue(string variableValue) {
569      sharedFixedVariables.SetVariableValue(variableValue, FreeVariable, 0);
570      OnVariableValueChanged(this, EventArgs.Empty);
571    }
572  }
573}
574
Note: See TracBrowser for help on using the repository browser.