Free cookie consent management tool by TermsFeed Policy Generator

source: stable/HeuristicLab.Problems.DataAnalysis.Views/3.4/Controls/FactorPartialDependencePlot.cs @ 16189

Last change on this file since 16189 was 16162, checked in by gkronber, 6 years ago

#2888: merged r15839,r15845 from trunk to stable

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