Free cookie consent management tool by TermsFeed Policy Generator

source: branches/2972_PDPRowSelect/HeuristicLab.Problems.DataAnalysis.Views/3.4/Controls/FactorPartialDependencePlot.cs @ 16674

Last change on this file since 16674 was 16445, checked in by pfleck, 6 years ago

#2972 Implemented 1st version (UI is subject to change)

File size: 21.7 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2018 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 = false;
146      chart.ChartAreas[0].CursorY.IsUserSelectionEnabled = false;
147
148      chart.ChartAreas[0].Axes.ToList().ForEach(x => { x.ScaleView.Zoomable = false; });
149
150      Disposed += Control_Disposed;
151    }
152
153    private void Control_Disposed(object sender, EventArgs e) {
154      if (cancelCurrentRecalculateSource != null)
155        cancelCurrentRecalculateSource.Cancel();
156    }
157
158    public void Configure(IEnumerable<IRegressionSolution> solutions, ModifiableDataset sharedFixedVariables, string freeVariable, IList<string> variableValues, bool initializeAxisRanges = true) {
159      if (!SolutionsCompatible(solutions))
160        throw new ArgumentException("Solutions are not compatible with the problem data.");
161      this.freeVariable = freeVariable;
162      this.variableValues = new List<string>(variableValues);
163
164      this.solutions.Clear();
165      this.solutions.AddRange(solutions);
166
167      // add an event such that whenever a value is changed in the shared dataset,
168      // this change is reflected in the internal dataset (where the value becomes a whole column)
169      if (this.sharedFixedVariables != null) {
170        this.sharedFixedVariables.ItemChanged -= sharedFixedVariables_ItemChanged;
171        this.sharedFixedVariables.Reset -= sharedFixedVariables_Reset;
172      }
173
174      this.sharedFixedVariables = sharedFixedVariables;
175      this.sharedFixedVariables.ItemChanged += sharedFixedVariables_ItemChanged;
176      this.sharedFixedVariables.Reset += sharedFixedVariables_Reset;
177
178      RecalculateInternalDataset();
179
180      chart.Series.Clear();
181      seriesCache.Clear();
182      ciSeriesCache.Clear();
183      foreach (var solution in this.solutions) {
184        var series = CreateSeries(solution);
185        seriesCache.Add(solution, series.Item1);
186        if (series.Item2 != null)
187          ciSeriesCache.Add(solution, series.Item2);
188      }
189
190      InitSeriesData();
191      OrderAndColorSeries();
192
193    }
194
195    public async Task RecalculateAsync(bool updateOnFinish = true, bool resetYAxis = true) {
196      if (IsDisposed
197        || sharedFixedVariables == null || !solutions.Any() || string.IsNullOrEmpty(freeVariable)
198        || !variableValues.Any())
199        return;
200
201      calculationPendingTimer.Start();
202
203      // cancel previous recalculate call
204      if (cancelCurrentRecalculateSource != null)
205        cancelCurrentRecalculateSource.Cancel();
206      cancelCurrentRecalculateSource = new CancellationTokenSource();
207      var cancellationToken = cancelCurrentRecalculateSource.Token;
208
209      // Update series
210      try {
211        var limits = await UpdateAllSeriesDataAsync(cancellationToken);
212        chart.Invalidate();
213
214        yMin = limits.Lower;
215        yMax = limits.Upper;
216        // Set y-axis
217        if (resetYAxis)
218          SetupAxis(chart, chart.ChartAreas[0].AxisY, yMin, yMax, YAxisTicks, FixedYAxisMin, FixedYAxisMax);
219
220        calculationPendingTimer.Stop();
221        calculationPendingLabel.Visible = false;
222        if (updateOnFinish)
223          Update();
224      } catch (OperationCanceledException) { } catch (AggregateException ae) {
225        if (!ae.InnerExceptions.Any(e => e is OperationCanceledException))
226          throw;
227      }
228    }
229
230    public void UpdateTitlePosition() {
231      var title = chart.Titles[0];
232      var plotArea = InnerPlotPosition;
233
234      title.Visible = plotArea.Width != 0;
235
236      title.Position.X = plotArea.X + (plotArea.Width / 2);
237    }
238
239    private static void SetupAxis(EnhancedChart chart, Axis axis, double minValue, double maxValue, int ticks, double? fixedAxisMin, double? fixedAxisMax) {
240      //guard if only one distinct value is present
241      if (minValue.IsAlmost(maxValue)) {
242        minValue = minValue - 0.5;
243        maxValue = minValue + 0.5;
244      }
245
246      double axisMin, axisMax, axisInterval;
247      ChartUtil.CalculateAxisInterval(minValue, maxValue, ticks, out axisMin, out axisMax, out axisInterval);
248      axis.Minimum = fixedAxisMin ?? axisMin;
249      axis.Maximum = fixedAxisMax ?? axisMax;
250      axis.Interval = (axis.Maximum - axis.Minimum) / ticks;
251
252      chart.ChartAreas[0].RecalculateAxesScale();
253    }
254
255
256    private void RecalculateInternalDataset() {
257      if (sharedFixedVariables == null)
258        return;
259
260      var factorValues = new List<string>(variableValues);
261
262      var variables = sharedFixedVariables.VariableNames.ToList();
263      var values = new List<IList>();
264      foreach (var varName in variables) {
265        if (varName == FreeVariable) {
266          values.Add(factorValues);
267        } else if (sharedFixedVariables.VariableHasType<double>(varName)) {
268          values.Add(Enumerable.Repeat(sharedFixedVariables.GetDoubleValue(varName, 0), factorValues.Count).ToList());
269        } else if (sharedFixedVariables.VariableHasType<string>(varName)) {
270          values.Add(Enumerable.Repeat(sharedFixedVariables.GetStringValue(varName, 0), factorValues.Count).ToList());
271        }
272      }
273
274      internalDataset = new ModifiableDataset(variables, values);
275    }
276
277    private Tuple<Series, Series> CreateSeries(IRegressionSolution solution) {
278      var series = new Series {
279        ChartType = SeriesChartType.Column,
280        Name = solution.ProblemData.TargetVariable + " " + solutions.IndexOf(solution),
281        XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.String
282      };
283      series.LegendText = series.Name;
284
285      Series confidenceIntervalSeries = null;
286      confidenceIntervalSeries = new Series {
287        ChartType = SeriesChartType.BoxPlot,
288        XValueType = System.Windows.Forms.DataVisualization.Charting.ChartValueType.String,
289        Color = Color.Black,
290        YValuesPerPoint = 5,
291        Name = "95% Conf. Interval " + series.Name,
292        IsVisibleInLegend = false
293      };
294      return Tuple.Create(series, confidenceIntervalSeries);
295    }
296
297    private void OrderAndColorSeries() {
298      chart.SuspendRepaint();
299
300      chart.Series.Clear();
301      // Add mean series for applying palette colors
302      foreach (var solution in solutions) {
303        chart.Series.Add(seriesCache[solution]);
304      }
305
306      chart.Palette = ChartColorPalette.BrightPastel;
307      chart.ApplyPaletteColors();
308      chart.Palette = ChartColorPalette.None;
309
310      // Add confidence interval series after its coresponding series for correct z index
311      foreach (var solution in solutions) {
312        Series ciSeries;
313        if (ciSeriesCache.TryGetValue(solution, out ciSeries)) {
314          int idx = chart.Series.IndexOf(seriesCache[solution]);
315          chart.Series.Insert(idx + 1, ciSeries);
316        }
317      }
318
319      chart.ResumeRepaint(true);
320    }
321
322    private async Task<DoubleLimit> UpdateAllSeriesDataAsync(CancellationToken cancellationToken) {
323      var updateTasks = solutions.Select(solution => UpdateSeriesDataAsync(solution, cancellationToken));
324
325      double min = double.MaxValue, max = double.MinValue;
326      foreach (var update in updateTasks) {
327        var limit = await update;
328        if (limit.Lower < min) min = limit.Lower;
329        if (limit.Upper > max) max = limit.Upper;
330      }
331
332      return new DoubleLimit(min, max);
333    }
334
335    private Task<DoubleLimit> UpdateSeriesDataAsync(IRegressionSolution solution, CancellationToken cancellationToken) {
336      return Task.Run(() => {
337        var yvalues = solution.Model.GetEstimatedValues(internalDataset, Enumerable.Range(0, internalDataset.Rows)).ToList();
338
339        double min = double.MaxValue, max = double.MinValue;
340
341        var series = seriesCache[solution];
342        for (int i = 0; i < variableValues.Count; i++) {
343          series.Points[i].SetValueXY(variableValues[i], yvalues[i]);
344          if (yvalues[i] < min) min = yvalues[i];
345          if (yvalues[i] > max) max = yvalues[i];
346        }
347
348        cancellationToken.ThrowIfCancellationRequested();
349
350        var confidenceBoundSolution = solution as IConfidenceRegressionSolution;
351        if (confidenceBoundSolution != null) {
352          var confidenceIntervalSeries = ciSeriesCache[solution];
353          var variances = confidenceBoundSolution.Model.GetEstimatedVariances(internalDataset, Enumerable.Range(0, internalDataset.Rows)).ToList();
354          for (int i = 0; i < variableValues.Count; i++) {
355            var lower = yvalues[i] - 1.96 * Math.Sqrt(variances[i]);
356            var upper = yvalues[i] + 1.96 * Math.Sqrt(variances[i]);
357            confidenceIntervalSeries.Points[i].SetValueXY(variableValues[i], lower, upper, yvalues[i], yvalues[i], yvalues[i]);
358            if (lower < min) min = lower;
359            if (upper > max) max = upper;
360          }
361        }
362
363        cancellationToken.ThrowIfCancellationRequested();
364        return new DoubleLimit(min, max);
365      }, cancellationToken);
366    }
367
368    private void InitSeriesData() {
369      if (internalDataset == null)
370        return;
371
372      foreach (var solution in solutions)
373        InitSeriesData(solution, variableValues);
374    }
375
376    private void InitSeriesData(IRegressionSolution solution, IList<string> values) {
377
378      var series = seriesCache[solution];
379      series.Points.SuspendUpdates();
380      series.Points.Clear();
381      for (int i = 0; i < values.Count; i++) {
382        series.Points.AddXY(values[i], 0.0);
383        series.Points.Last().ToolTip = values[i];
384      }
385
386      UpdateAllSeriesStyles(variableValues.IndexOf(sharedFixedVariables.GetStringValue(FreeVariable, 0)));
387      series.Points.ResumeUpdates();
388
389      Series confidenceIntervalSeries;
390      if (ciSeriesCache.TryGetValue(solution, out confidenceIntervalSeries)) {
391        confidenceIntervalSeries.Points.SuspendUpdates();
392        confidenceIntervalSeries.Points.Clear();
393        for (int i = 0; i < values.Count; i++)
394          confidenceIntervalSeries.Points.AddXY(values[i], 0.0, 0.0, 0.0, 0.0, 0.0);
395        confidenceIntervalSeries.Points.ResumeUpdates();
396      }
397    }
398
399    public async Task AddSolutionAsync(IRegressionSolution solution) {
400      if (!SolutionsCompatible(solutions.Concat(new[] { solution })))
401        throw new ArgumentException("The solution is not compatible with the problem data.");
402      if (solutions.Contains(solution))
403        return;
404
405      solutions.Add(solution);
406
407      var series = CreateSeries(solution);
408      seriesCache.Add(solution, series.Item1);
409      if (series.Item2 != null)
410        ciSeriesCache.Add(solution, series.Item2);
411
412      InitSeriesData(solution, variableValues);
413      OrderAndColorSeries();
414
415      await RecalculateAsync();
416      var args = new EventArgs<IRegressionSolution>(solution);
417      OnSolutionAdded(this, args);
418    }
419
420    public async Task RemoveSolutionAsync(IRegressionSolution solution) {
421      if (!solutions.Remove(solution))
422        return;
423
424      seriesCache.Remove(solution);
425      ciSeriesCache.Remove(solution);
426
427      await RecalculateAsync();
428      var args = new EventArgs<IRegressionSolution>(solution);
429      OnSolutionRemoved(this, args);
430    }
431
432    private static bool SolutionsCompatible(IEnumerable<IRegressionSolution> solutions) {
433      var refSolution = solutions.First();
434      var refSolVars = refSolution.ProblemData.Dataset.VariableNames;
435      foreach (var solution in solutions.Skip(1)) {
436        var variables1 = solution.ProblemData.Dataset.VariableNames;
437        if (!variables1.All(refSolVars.Contains))
438          return false;
439
440        foreach (var factorVar in variables1.Where(solution.ProblemData.Dataset.VariableHasType<string>)) {
441          var distinctVals = refSolution.ProblemData.Dataset.GetStringValues(factorVar).Distinct();
442          if (solution.ProblemData.Dataset.GetStringValues(factorVar).Any(val => !distinctVals.Contains(val))) return false;
443        }
444      }
445      return true;
446    }
447
448    #region Events
449    public event EventHandler<EventArgs<IRegressionSolution>> SolutionAdded;
450    public void OnSolutionAdded(object sender, EventArgs<IRegressionSolution> args) {
451      var added = SolutionAdded;
452      if (added == null) return;
453      added(sender, args);
454    }
455
456    public event EventHandler<EventArgs<IRegressionSolution>> SolutionRemoved;
457    public void OnSolutionRemoved(object sender, EventArgs<IRegressionSolution> args) {
458      var removed = SolutionRemoved;
459      if (removed == null) return;
460      removed(sender, args);
461    }
462
463    public event EventHandler VariableValueChanged;
464    public void OnVariableValueChanged(object sender, EventArgs args) {
465      var changed = VariableValueChanged;
466      if (changed == null) return;
467      changed(sender, args);
468    }
469
470    public event EventHandler ZoomChanged;
471    public void OnZoomChanged(object sender, EventArgs args) {
472      var changed = ZoomChanged;
473      if (changed == null) return;
474      changed(sender, args);
475    }
476
477    private void sharedFixedVariables_ItemChanged(object o, EventArgs<int, int> e) {
478      if (o != sharedFixedVariables) return;
479      var variables = sharedFixedVariables.VariableNames.ToList();
480      var rowIndex = e.Value;
481      var columnIndex = e.Value2;
482
483      var variableName = variables[columnIndex];
484      if (variableName == FreeVariable) return;
485      if (internalDataset.VariableHasType<double>(variableName)) {
486        var v = sharedFixedVariables.GetDoubleValue(variableName, rowIndex);
487        var values = new List<double>(Enumerable.Repeat(v, internalDataset.Rows));
488        internalDataset.ReplaceVariable(variableName, values);
489      } else if (internalDataset.VariableHasType<string>(variableName)) {
490        var v = sharedFixedVariables.GetStringValue(variableName, rowIndex);
491        var values = new List<String>(Enumerable.Repeat(v, internalDataset.Rows));
492        internalDataset.ReplaceVariable(variableName, values);
493      } else {
494        // unsupported type
495        throw new NotSupportedException();
496      }
497    }
498
499    private void sharedFixedVariables_Reset(object sender, EventArgs e) {
500      var newValue = sharedFixedVariables.GetStringValue(FreeVariable, 0);
501      UpdateSelectedValue(newValue);
502
503      int idx = variableValues.IndexOf(newValue);
504      UpdateAllSeriesStyles(idx);
505    }
506
507    private async void chart_DragDrop(object sender, DragEventArgs e) {
508      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
509      if (data != null) {
510        var solution = data as IRegressionSolution;
511        if (!solutions.Contains(solution))
512          await AddSolutionAsync(solution);
513      }
514    }
515    private void chart_DragEnter(object sender, DragEventArgs e) {
516      if (!e.Data.GetDataPresent(HeuristicLab.Common.Constants.DragDropDataFormat)) return;
517      e.Effect = DragDropEffects.None;
518
519      var data = e.Data.GetData(HeuristicLab.Common.Constants.DragDropDataFormat);
520      var regressionSolution = data as IRegressionSolution;
521      if (regressionSolution != null) {
522        e.Effect = DragDropEffects.Copy;
523      }
524    }
525
526    private void calculationPendingTimer_Tick(object sender, EventArgs e) {
527      calculationPendingLabel.Visible = true;
528      Update();
529    }
530
531    private void chart_SelectionRangeChanged(object sender, CursorEventArgs e) {
532      OnZoomChanged(this, EventArgs.Empty);
533    }
534
535    private void chart_Resize(object sender, EventArgs e) {
536      UpdateTitlePosition();
537    }
538
539    private void chart_PostPaint(object sender, ChartPaintEventArgs e) {
540      if (ChartPostPaint != null)
541        ChartPostPaint(this, EventArgs.Empty);
542    }
543    #endregion
544
545    private void chart_MouseClick(object sender, MouseEventArgs e) {
546      var hitTestResult = chart.HitTest(e.X, e.Y, ChartElementType.DataPoint);
547      if (hitTestResult != null && hitTestResult.ChartElementType == ChartElementType.DataPoint) {
548        var series = hitTestResult.Series;
549        var dataPoint = (DataPoint)hitTestResult.Object;
550        var idx = series.Points.IndexOf(dataPoint);
551        UpdateSelectedValue(variableValues[idx]);
552
553        UpdateAllSeriesStyles(idx);
554      }
555    }
556
557    private void UpdateAllSeriesStyles(int selectedValueIndex) {
558      if (ShowCursor) {
559        chart.Titles[0].Text = FreeVariable + " : " + variableValues[selectedValueIndex];
560        chart.Update();
561      }
562      foreach (var s in seriesCache.Values) {
563        if (s.ChartType == SeriesChartType.Column)
564          for (int i = 0; i < s.Points.Count; i++) {
565            if (i != selectedValueIndex) {
566              s.Points[i].BorderDashStyle = ChartDashStyle.NotSet;
567            } else {
568              s.Points[i].BorderDashStyle = ChartDashStyle.Dash;
569              s.Points[i].BorderColor = Color.Red;
570            }
571          }
572      }
573    }
574
575    private void UpdateSelectedValue(string variableValue) {
576      sharedFixedVariables.SetVariableValue(variableValue, FreeVariable, 0);
577      OnVariableValueChanged(this, EventArgs.Empty);
578    }
579  }
580}
581
Note: See TracBrowser for help on using the repository browser.