source: branches/DataAnalysis/HeuristicLab.Problems.DataAnalysis.Regression/3.3/Symbolic/Analyzers/FixedValidationBestScaledSymbolicRegressionSolutionAnalyzer.cs @ 4297

Last change on this file since 4297 was 4297, checked in by gkronber, 12 years ago

Added output parameter for validation quality to validation analyzer, added input parameter for validation quality to overfitting analyzer, and fixed bugs in pruning operator. #1142

File size: 23.9 KB
Line 
1#region License Information
2/* HeuristicLab
3 * Copyright (C) 2002-2010 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.Collections.Generic;
23using System.Linq;
24using HeuristicLab.Analysis;
25using HeuristicLab.Core;
26using HeuristicLab.Data;
27using HeuristicLab.Encodings.SymbolicExpressionTreeEncoding;
28using HeuristicLab.Operators;
29using HeuristicLab.Optimization;
30using HeuristicLab.Parameters;
31using HeuristicLab.Persistence.Default.CompositeSerializers.Storable;
32using HeuristicLab.Problems.DataAnalysis.Evaluators;
33using HeuristicLab.Problems.DataAnalysis.Symbolic;
34using System;
35
36namespace HeuristicLab.Problems.DataAnalysis.Regression.Symbolic.Analyzers {
37  /// <summary>
38  /// An operator that analyzes the validation best scaled symbolic regression solution.
39  /// </summary>
40  [Item("FixedValidationBestScaledSymbolicRegressionSolutionAnalyzer", "An operator that analyzes the validation best scaled symbolic regression solution.")]
41  [StorableClass]
42  public sealed class FixedValidationBestScaledSymbolicRegressionSolutionAnalyzer : SingleSuccessorOperator, ISymbolicRegressionAnalyzer {
43    private const string RandomParameterName = "Random";
44    private const string SymbolicExpressionTreeParameterName = "SymbolicExpressionTree";
45    private const string SymbolicExpressionTreeInterpreterParameterName = "SymbolicExpressionTreeInterpreter";
46    private const string ProblemDataParameterName = "ProblemData";
47    private const string ValidationSamplesStartParameterName = "SamplesStart";
48    private const string ValidationSamplesEndParameterName = "SamplesEnd";
49    // private const string QualityParameterName = "Quality";
50    private const string UpperEstimationLimitParameterName = "UpperEstimationLimit";
51    private const string LowerEstimationLimitParameterName = "LowerEstimationLimit";
52    private const string EvaluatorParameterName = "Evaluator";
53    private const string MaximizationParameterName = "Maximization";
54    private const string BestSolutionParameterName = "Best solution (validation)";
55    private const string BestSolutionQualityParameterName = "Best solution quality (validation)";
56    private const string CurrentBestValidationQualityParameterName = "Current best validation quality";
57    private const string BestSolutionQualityValuesParameterName = "Validation Quality";
58    private const string ResultsParameterName = "Results";
59    private const string VariableFrequenciesParameterName = "VariableFrequencies";
60    private const string BestKnownQualityParameterName = "BestKnownQuality";
61    private const string GenerationsParameterName = "Generations";
62    private const string RelativeNumberOfEvaluatedSamplesParameterName = "RelativeNumberOfEvaluatedSamples";
63
64    private const string TrainingMeanSquaredErrorQualityParameterName = "Mean squared error (training)";
65    private const string MinTrainingMeanSquaredErrorQualityParameterName = "Min mean squared error (training)";
66    private const string MaxTrainingMeanSquaredErrorQualityParameterName = "Max mean squared error (training)";
67    private const string AverageTrainingMeanSquaredErrorQualityParameterName = "Average mean squared error (training)";
68    private const string BestTrainingMeanSquaredErrorQualityParameterName = "Best mean squared error (training)";
69
70    private const string TrainingAverageRelativeErrorQualityParameterName = "Average relative error (training)";
71    private const string MinTrainingAverageRelativeErrorQualityParameterName = "Min average relative error (training)";
72    private const string MaxTrainingAverageRelativeErrorQualityParameterName = "Max average relative error (training)";
73    private const string AverageTrainingAverageRelativeErrorQualityParameterName = "Average average relative error (training)";
74    private const string BestTrainingAverageRelativeErrorQualityParameterName = "Best average relative error (training)";
75
76    private const string TrainingRSquaredQualityParameterName = "R² (training)";
77    private const string MinTrainingRSquaredQualityParameterName = "Min R² (training)";
78    private const string MaxTrainingRSquaredQualityParameterName = "Max R² (training)";
79    private const string AverageTrainingRSquaredQualityParameterName = "Average R² (training)";
80    private const string BestTrainingRSquaredQualityParameterName = "Best R² (training)";
81
82    private const string TestMeanSquaredErrorQualityParameterName = "Mean squared error (test)";
83    private const string MinTestMeanSquaredErrorQualityParameterName = "Min mean squared error (test)";
84    private const string MaxTestMeanSquaredErrorQualityParameterName = "Max mean squared error (test)";
85    private const string AverageTestMeanSquaredErrorQualityParameterName = "Average mean squared error (test)";
86    private const string BestTestMeanSquaredErrorQualityParameterName = "Best mean squared error (test)";
87
88    private const string TestAverageRelativeErrorQualityParameterName = "Average relative error (test)";
89    private const string MinTestAverageRelativeErrorQualityParameterName = "Min average relative error (test)";
90    private const string MaxTestAverageRelativeErrorQualityParameterName = "Max average relative error (test)";
91    private const string AverageTestAverageRelativeErrorQualityParameterName = "Average average relative error (test)";
92    private const string BestTestAverageRelativeErrorQualityParameterName = "Best average relative error (test)";
93
94    private const string TestRSquaredQualityParameterName = "R² (test)";
95    private const string MinTestRSquaredQualityParameterName = "Min R² (test)";
96    private const string MaxTestRSquaredQualityParameterName = "Max R² (test)";
97    private const string AverageTestRSquaredQualityParameterName = "Average R² (test)";
98    private const string BestTestRSquaredQualityParameterName = "Best R² (test)";
99
100    private const string RSquaredValuesParameterName = "R²";
101    private const string MeanSquaredErrorValuesParameterName = "Mean squared error";
102    private const string RelativeErrorValuesParameterName = "Average relative error";
103
104    #region parameter properties
105    public ILookupParameter<IRandom> RandomParameter {
106      get { return (ILookupParameter<IRandom>)Parameters[RandomParameterName]; }
107    }
108    public ScopeTreeLookupParameter<SymbolicExpressionTree> SymbolicExpressionTreeParameter {
109      get { return (ScopeTreeLookupParameter<SymbolicExpressionTree>)Parameters[SymbolicExpressionTreeParameterName]; }
110    }
111    public IValueLookupParameter<ISymbolicExpressionTreeInterpreter> SymbolicExpressionTreeInterpreterParameter {
112      get { return (IValueLookupParameter<ISymbolicExpressionTreeInterpreter>)Parameters[SymbolicExpressionTreeInterpreterParameterName]; }
113    }
114    public ILookupParameter<ISymbolicRegressionEvaluator> EvaluatorParameter {
115      get { return (ILookupParameter<ISymbolicRegressionEvaluator>)Parameters[EvaluatorParameterName]; }
116    }
117    public ILookupParameter<BoolValue> MaximizationParameter {
118      get { return (ILookupParameter<BoolValue>)Parameters[MaximizationParameterName]; }
119    }
120    public IValueLookupParameter<DataAnalysisProblemData> ProblemDataParameter {
121      get { return (IValueLookupParameter<DataAnalysisProblemData>)Parameters[ProblemDataParameterName]; }
122    }
123    public IValueLookupParameter<IntValue> ValidationSamplesStartParameter {
124      get { return (IValueLookupParameter<IntValue>)Parameters[ValidationSamplesStartParameterName]; }
125    }
126    public IValueLookupParameter<IntValue> ValidationSamplesEndParameter {
127      get { return (IValueLookupParameter<IntValue>)Parameters[ValidationSamplesEndParameterName]; }
128    }
129    public IValueParameter<PercentValue> RelativeNumberOfEvaluatedSamplesParameter {
130      get { return (IValueParameter<PercentValue>)Parameters[RelativeNumberOfEvaluatedSamplesParameterName]; }
131    }
132
133    public IValueLookupParameter<DoubleValue> UpperEstimationLimitParameter {
134      get { return (IValueLookupParameter<DoubleValue>)Parameters[UpperEstimationLimitParameterName]; }
135    }
136    public IValueLookupParameter<DoubleValue> LowerEstimationLimitParameter {
137      get { return (IValueLookupParameter<DoubleValue>)Parameters[LowerEstimationLimitParameterName]; }
138    }
139    public ILookupParameter<SymbolicRegressionSolution> BestSolutionParameter {
140      get { return (ILookupParameter<SymbolicRegressionSolution>)Parameters[BestSolutionParameterName]; }
141    }
142    public ILookupParameter<SymbolicRegressionSolution> BestTrainingSolutionParameter {
143      get { return (ILookupParameter<SymbolicRegressionSolution>)Parameters["BestTrainingSolution"]; }
144    }
145    public ScopeTreeLookupParameter<DoubleValue> QualityParameter {
146      get { return (ScopeTreeLookupParameter<DoubleValue>)Parameters["Quality"]; }
147    }
148    public ScopeTreeLookupParameter<DoubleValue> ValidationQualityParameter {
149      get { return (ScopeTreeLookupParameter<DoubleValue>)Parameters["ValidationQuality"]; }
150    }
151
152    public ILookupParameter<IntValue> GenerationsParameter {
153      get { return (ILookupParameter<IntValue>)Parameters[GenerationsParameterName]; }
154    }
155    public ILookupParameter<DoubleValue> BestSolutionQualityParameter {
156      get { return (ILookupParameter<DoubleValue>)Parameters[BestSolutionQualityParameterName]; }
157    }
158    public ILookupParameter<DataTable> BestSolutionQualityValuesParameter {
159      get { return (ILookupParameter<DataTable>)Parameters[BestSolutionQualityValuesParameterName]; }
160    }
161    public ILookupParameter<ResultCollection> ResultsParameter {
162      get { return (ILookupParameter<ResultCollection>)Parameters[ResultsParameterName]; }
163    }
164    public ILookupParameter<DoubleValue> BestKnownQualityParameter {
165      get { return (ILookupParameter<DoubleValue>)Parameters[BestKnownQualityParameterName]; }
166    }
167    public ILookupParameter<DoubleValue> CurrentBestValidationQualityParameter {
168      get { return (ILookupParameter<DoubleValue>)Parameters[CurrentBestValidationQualityParameterName]; }
169    }
170
171    public ILookupParameter<DataTable> VariableFrequenciesParameter {
172      get { return (ILookupParameter<DataTable>)Parameters[VariableFrequenciesParameterName]; }
173    }
174
175    #endregion
176    #region properties
177    public IRandom Random {
178      get { return RandomParameter.ActualValue; }
179    }
180    public ItemArray<SymbolicExpressionTree> SymbolicExpressionTree {
181      get { return SymbolicExpressionTreeParameter.ActualValue; }
182    }
183    public ISymbolicExpressionTreeInterpreter SymbolicExpressionTreeInterpreter {
184      get { return SymbolicExpressionTreeInterpreterParameter.ActualValue; }
185    }
186    public ISymbolicRegressionEvaluator Evaluator {
187      get { return EvaluatorParameter.ActualValue; }
188    }
189    public BoolValue Maximization {
190      get { return MaximizationParameter.ActualValue; }
191    }
192    public DataAnalysisProblemData ProblemData {
193      get { return ProblemDataParameter.ActualValue; }
194    }
195    public IntValue ValidiationSamplesStart {
196      get { return ValidationSamplesStartParameter.ActualValue; }
197    }
198    public IntValue ValidationSamplesEnd {
199      get { return ValidationSamplesEndParameter.ActualValue; }
200    }
201    public PercentValue RelativeNumberOfEvaluatedSamples {
202      get { return RelativeNumberOfEvaluatedSamplesParameter.Value; }
203    }
204
205    public DoubleValue UpperEstimationLimit {
206      get { return UpperEstimationLimitParameter.ActualValue; }
207    }
208    public DoubleValue LowerEstimationLimit {
209      get { return LowerEstimationLimitParameter.ActualValue; }
210    }
211    public ResultCollection Results {
212      get { return ResultsParameter.ActualValue; }
213    }
214    public DataTable VariableFrequencies {
215      get { return VariableFrequenciesParameter.ActualValue; }
216    }
217    public IntValue Generations {
218      get { return GenerationsParameter.ActualValue; }
219    }
220    public DoubleValue BestSolutionQuality {
221      get { return BestSolutionQualityParameter.ActualValue; }
222    }
223
224    #endregion
225
226    public FixedValidationBestScaledSymbolicRegressionSolutionAnalyzer()
227      : base() {
228      Parameters.Add(new LookupParameter<IRandom>(RandomParameterName, "The random generator to use."));
229      Parameters.Add(new LookupParameter<ISymbolicRegressionEvaluator>(EvaluatorParameterName, "The evaluator which should be used to evaluate the solution on the validation set."));
230      Parameters.Add(new ScopeTreeLookupParameter<SymbolicExpressionTree>(SymbolicExpressionTreeParameterName, "The symbolic expression trees to analyze."));
231      Parameters.Add(new LookupParameter<BoolValue>(MaximizationParameterName, "The direction of optimization."));
232      Parameters.Add(new ValueLookupParameter<ISymbolicExpressionTreeInterpreter>(SymbolicExpressionTreeInterpreterParameterName, "The interpreter that should be used for the analysis of symbolic expression trees."));
233      Parameters.Add(new ValueLookupParameter<DataAnalysisProblemData>(ProblemDataParameterName, "The problem data for which the symbolic expression tree is a solution."));
234      Parameters.Add(new ValueLookupParameter<IntValue>(ValidationSamplesStartParameterName, "The first index of the validation partition of the data set."));
235      Parameters.Add(new ValueLookupParameter<IntValue>(ValidationSamplesEndParameterName, "The last index of the validation partition of the data set."));
236      Parameters.Add(new ValueParameter<PercentValue>(RelativeNumberOfEvaluatedSamplesParameterName, "The relative number of samples of the dataset partition, which should be randomly chosen for evaluation between the start and end index.", new PercentValue(1)));
237      Parameters.Add(new ValueLookupParameter<DoubleValue>(UpperEstimationLimitParameterName, "The upper estimation limit that was set for the evaluation of the symbolic expression trees."));
238      Parameters.Add(new ValueLookupParameter<DoubleValue>(LowerEstimationLimitParameterName, "The lower estimation limit that was set for the evaluation of the symbolic expression trees."));
239      Parameters.Add(new LookupParameter<SymbolicRegressionSolution>(BestSolutionParameterName, "The best symbolic regression solution."));
240      Parameters.Add(new LookupParameter<SymbolicRegressionSolution>("BestTrainingSolution"));
241      Parameters.Add(new ScopeTreeLookupParameter<DoubleValue>("Quality"));
242      Parameters.Add(new ScopeTreeLookupParameter<DoubleValue>("ValidationQuality"));
243      Parameters.Add(new LookupParameter<IntValue>(GenerationsParameterName, "The number of generations calculated so far."));
244      Parameters.Add(new LookupParameter<DoubleValue>(BestSolutionQualityParameterName, "The quality of the best symbolic regression solution."));
245      Parameters.Add(new LookupParameter<ResultCollection>(ResultsParameterName, "The result collection where the best symbolic regression solution should be stored."));
246      Parameters.Add(new LookupParameter<DoubleValue>(BestKnownQualityParameterName, "The best known (validation) quality achieved on the data set."));
247      Parameters.Add(new LookupParameter<DoubleValue>(CurrentBestValidationQualityParameterName, "The quality of the best solution (on the validation set) of the current generation."));
248      Parameters.Add(new LookupParameter<DataTable>(BestSolutionQualityValuesParameterName));
249      Parameters.Add(new LookupParameter<DataTable>(VariableFrequenciesParameterName, "The variable frequencies table to use for the calculation of variable impacts"));
250    }
251
252    [StorableConstructor]
253    private FixedValidationBestScaledSymbolicRegressionSolutionAnalyzer(bool deserializing) : base(deserializing) { }
254
255    [StorableHook(HookType.AfterDeserialization)]
256    private void AfterDeserialization() {
257      #region compatibility remove before releasing 3.3.1
258      if (!Parameters.ContainsKey(EvaluatorParameterName)) {
259        Parameters.Add(new LookupParameter<ISymbolicRegressionEvaluator>(EvaluatorParameterName, "The evaluator which should be used to evaluate the solution on the validation set."));
260      }
261      if (!Parameters.ContainsKey(MaximizationParameterName)) {
262        Parameters.Add(new LookupParameter<BoolValue>(MaximizationParameterName, "The direction of optimization."));
263      }
264      if (!Parameters.ContainsKey(BestSolutionQualityValuesParameterName)) {
265        Parameters.Add(new LookupParameter<DataTable>(BestSolutionQualityValuesParameterName));
266      }
267      if (!Parameters.ContainsKey("BestTrainingSolution")) {
268        Parameters.Add(new LookupParameter<SymbolicRegressionSolution>("BestTrainingSolution"));
269      }
270      if (!Parameters.ContainsKey("Quality")) {
271        Parameters.Add(new ScopeTreeLookupParameter<DoubleValue>("Quality"));
272      }
273      if (!Parameters.ContainsKey("ValidationQuality")) {
274        Parameters.Add(new ScopeTreeLookupParameter<DoubleValue>("ValidationQuality"));
275      }
276      #endregion
277    }
278
279    public override IOperation Apply() {
280      ItemArray<SymbolicExpressionTree> trees = SymbolicExpressionTree;
281      ItemArray<DoubleValue> qualities = QualityParameter.ActualValue;
282
283      string targetVariable = ProblemData.TargetVariable.Value;
284
285      // select a random subset of rows in the validation set
286      int validationStart = ValidiationSamplesStart.Value;
287      int validationEnd = ValidationSamplesEnd.Value;
288      int seed = Random.Next();
289      int count = (int)((validationEnd - validationStart) * RelativeNumberOfEvaluatedSamples.Value);
290      if (count == 0) count = 1;
291      IEnumerable<int> rows = RandomEnumerable.SampleRandomNumbers(seed, validationStart, validationEnd, count);
292
293      double upperEstimationLimit = UpperEstimationLimit != null ? UpperEstimationLimit.Value : double.PositiveInfinity;
294      double lowerEstimationLimit = LowerEstimationLimit != null ? LowerEstimationLimit.Value : double.NegativeInfinity;
295
296      double bestQuality = Maximization.Value ? double.NegativeInfinity : double.PositiveInfinity;
297      SymbolicExpressionTree bestTree = null;
298      SymbolicExpressionTree bestTrainingTree = trees[0];
299      double bestTrainingQuality = qualities[0].Value;
300      ItemArray<DoubleValue> validationQualites = new ItemArray<DoubleValue>(qualities.Length);
301      for (int i = 0; i < trees.Length; i++) {
302        SymbolicExpressionTree tree = trees[i];
303        double quality = Evaluator.Evaluate(SymbolicExpressionTreeInterpreter, tree,
304          lowerEstimationLimit, upperEstimationLimit,
305          ProblemData.Dataset, targetVariable,
306         rows);
307        validationQualites[i] = new DoubleValue(quality);
308        if ((Maximization.Value && quality > bestQuality) ||
309            (!Maximization.Value && quality < bestQuality)) {
310          bestQuality = quality;
311          bestTree = tree;
312        }
313        if ((Maximization.Value && qualities[i].Value > bestTrainingQuality) ||
314            (!Maximization.Value && qualities[i].Value < bestTrainingQuality)) {
315          bestTrainingQuality = qualities[i].Value;
316          bestTrainingTree = tree;
317        }
318      }
319      ValidationQualityParameter.ActualValue = validationQualites;
320
321      var scaledBestTrainingTree = GetScaledTree(bestTrainingTree);
322
323      SymbolicRegressionSolution bestTrainingSolution = new SymbolicRegressionSolution(ProblemData,
324        new SymbolicRegressionModel(SymbolicExpressionTreeInterpreter, scaledBestTrainingTree),
325        lowerEstimationLimit, upperEstimationLimit);
326      bestTrainingSolution.Name = "Best solution (training)";
327      bestTrainingSolution.Description = "The solution of the population with the highest fitness";
328
329      // if the best validation tree is better than the current best solution => update
330      bool newBest =
331        BestSolutionQuality == null ||
332        (Maximization.Value && bestQuality > BestSolutionQuality.Value) ||
333        (!Maximization.Value && bestQuality < BestSolutionQuality.Value);
334      if (newBest) {
335        var scaledTree = GetScaledTree(bestTree);
336        var model = new SymbolicRegressionModel((ISymbolicExpressionTreeInterpreter)SymbolicExpressionTreeInterpreter.Clone(),
337          scaledTree);
338        var solution = new SymbolicRegressionSolution(ProblemData, model, lowerEstimationLimit, upperEstimationLimit);
339        solution.Name = BestSolutionParameterName;
340        solution.Description = "Best solution on validation partition found over the whole run.";
341
342        BestSolutionParameter.ActualValue = solution;
343        BestSolutionQualityParameter.ActualValue = new DoubleValue(bestQuality);
344
345        BestSymbolicRegressionSolutionAnalyzer.UpdateBestSolutionResults(solution, ProblemData, Results, Generations, VariableFrequencies);
346      }
347
348      CurrentBestValidationQualityParameter.ActualValue = new DoubleValue(bestQuality);
349
350      if (!Results.ContainsKey(BestSolutionQualityValuesParameterName)) {
351        Results.Add(new Result(BestSolutionQualityValuesParameterName, new DataTable(BestSolutionQualityValuesParameterName, BestSolutionQualityValuesParameterName)));
352        Results.Add(new Result(BestSolutionQualityParameterName, new DoubleValue()));
353        Results.Add(new Result(CurrentBestValidationQualityParameterName, new DoubleValue()));
354        Results.Add(new Result("Best solution (training)", bestTrainingSolution));
355      }
356      Results[BestSolutionQualityParameterName].Value = new DoubleValue(BestSolutionQualityParameter.ActualValue.Value);
357      Results[CurrentBestValidationQualityParameterName].Value = new DoubleValue(bestQuality);
358      Results["Best solution (training)"].Value = bestTrainingSolution;
359
360      DataTable validationValues = (DataTable)Results[BestSolutionQualityValuesParameterName].Value;
361      AddValue(validationValues, BestSolutionQualityParameter.ActualValue.Value, BestSolutionQualityParameterName, BestSolutionQualityParameterName);
362      AddValue(validationValues, bestQuality, CurrentBestValidationQualityParameterName, CurrentBestValidationQualityParameterName);
363
364      BestSolutionQualityValuesParameter.ActualValue = validationValues;
365
366      return base.Apply();
367    }
368
369    private SymbolicExpressionTree GetScaledTree(SymbolicExpressionTree tree) {
370      // calculate scaling parameters and only for the best tree using the full training set
371      double alpha, beta;
372      int trainingStart = ProblemData.TrainingSamplesStart.Value;
373      int trainingEnd = ProblemData.TrainingSamplesEnd.Value;
374      IEnumerable<int> trainingRows = Enumerable.Range(trainingStart, trainingEnd - trainingStart);
375      IEnumerable<double> originalValues = ProblemData.Dataset.GetEnumeratedVariableValues(ProblemData.TargetVariable.Value, trainingRows);
376      IEnumerable<double> estimatedValues = SymbolicExpressionTreeInterpreter.GetSymbolicExpressionTreeValues(tree, ProblemData.Dataset, trainingRows);
377
378      SymbolicRegressionScaledMeanSquaredErrorEvaluator.CalculateScalingParameters(originalValues, estimatedValues, out beta, out alpha);
379
380      // scale tree for solution
381      return SymbolicRegressionSolutionLinearScaler.Scale(tree, alpha, beta);
382    }
383
384    [StorableHook(HookType.AfterDeserialization)]
385    private void Initialize() { }
386
387    private static void AddValue(DataTable table, double data, string name, string description) {
388      DataRow row;
389      table.Rows.TryGetValue(name, out row);
390      if (row == null) {
391        row = new DataRow(name, description);
392        row.Values.Add(data);
393        table.Rows.Add(row);
394      } else {
395        row.Values.Add(data);
396      }
397    }
398  }
399}
Note: See TracBrowser for help on using the repository browser.