#region License Information
/* HeuristicLab
* Copyright (C) Heuristic and Evolutionary Algorithms Laboratory (HEAL)
*
* This file is part of HeuristicLab.
*
* HeuristicLab is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* HeuristicLab is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with HeuristicLab. If not, see .
*/
#endregion
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Windows.Forms;
using HeuristicLab.Collections;
using HeuristicLab.Common;
using HeuristicLab.Common.Resources;
using HeuristicLab.Core.Views;
using HeuristicLab.Data;
using HeuristicLab.MainForm;
using HeuristicLab.Optimization;
using HeuristicLab.Optimization.Views;
namespace HeuristicLab.Analysis.Statistics.Views {
[View("Statistical Tests", "HeuristicLab.Analysis.Statistics.Views.InfoResources.StatisticalTestsInfo.rtf")]
[Content(typeof(RunCollection), false)]
public sealed partial class StatisticalTestsView : ItemView, IConfigureableView {
private double significanceLevel = 0.05;
private const int requiredSampleSize = 5;
private double[][] data;
private bool suppressUpdates;
private bool initializing;
public double SignificanceLevel {
get { return significanceLevel; }
set {
if (!significanceLevel.IsAlmost(value)) {
significanceLevel = value;
ResetUI();
CalculateValues();
}
}
}
public new RunCollection Content {
get { return (RunCollection)base.Content; }
set { base.Content = value; }
}
public override bool ReadOnly {
get { return true; }
set { /*not needed because results are always readonly */}
}
public StatisticalTestsView() {
InitializeComponent();
}
public void ShowConfiguration() {
using (StatisticalTestsConfigurationDialog dlg = new StatisticalTestsConfigurationDialog(this)) {
dlg.ShowDialog(this);
}
}
protected override void OnContentChanged() {
base.OnContentChanged();
if (Content != null) {
UpdateUI();
} else {
ResetUI();
}
UpdateCaption();
}
private void UpdateUI() {
initializing = true;
UpdateResultComboBox();
UpdateGroupsComboBox();
RebuildDataTable();
FillCompComboBox();
ResetUI();
CalculateValues();
initializing = false;
}
private void UpdateCaption() {
Caption = Content != null ? Content.OptimizerName + " Statistical Tests" : ViewAttribute.GetViewName(GetType());
}
#region events
protected override void RegisterContentEvents() {
base.RegisterContentEvents();
Content.ColumnsChanged += Content_ColumnsChanged;
Content.RowsChanged += Content_RowsChanged;
Content.CollectionReset += Content_CollectionReset;
Content.UpdateOfRunsInProgressChanged += Content_UpdateOfRunsInProgressChanged;
}
protected override void DeregisterContentEvents() {
base.DeregisterContentEvents();
Content.ColumnsChanged -= Content_ColumnsChanged;
Content.RowsChanged -= Content_RowsChanged;
Content.CollectionReset -= Content_CollectionReset;
Content.UpdateOfRunsInProgressChanged -= Content_UpdateOfRunsInProgressChanged;
}
void Content_RowsChanged(object sender, EventArgs e) {
if (suppressUpdates) return;
if (InvokeRequired) Invoke((Action)Content_RowsChanged, sender, e);
else {
UpdateUI();
}
}
void Content_ColumnsChanged(object sender, EventArgs e) {
if (suppressUpdates) return;
if (InvokeRequired) Invoke((Action)Content_ColumnsChanged, sender, e);
else {
UpdateUI();
}
}
private void Content_CollectionReset(object sender, CollectionItemsChangedEventArgs e) {
if (suppressUpdates) return;
if (InvokeRequired) Invoke((Action>)Content_CollectionReset, sender, e);
else {
UpdateUI();
}
}
void Content_UpdateOfRunsInProgressChanged(object sender, EventArgs e) {
if (InvokeRequired) Invoke((Action)Content_UpdateOfRunsInProgressChanged, sender, e);
else {
suppressUpdates = Content.UpdateOfRunsInProgress;
if (!suppressUpdates) UpdateUI();
}
}
private void openBoxPlotToolStripMenuItem_Click(object sender, EventArgs e) {
RunCollectionBoxPlotView boxplotView = new RunCollectionBoxPlotView();
boxplotView.Content = Content;
boxplotView.SetXAxis(groupComboBox.SelectedItem.ToString());
boxplotView.SetYAxis(resultComboBox.SelectedItem.ToString());
boxplotView.Show();
}
private void groupCompComboBox_SelectedValueChanged(object sender, EventArgs e) {
if (initializing || suppressUpdates) return;
string curItem = (string)groupCompComboBox.SelectedItem;
CalculatePairwise(curItem);
}
private void resultComboBox_SelectedValueChanged(object sender, EventArgs e) {
if (initializing || suppressUpdates) return;
RebuildDataTable();
ResetUI();
CalculateValues();
}
private void groupComboBox_SelectedValueChanged(object sender, EventArgs e) {
if (initializing || suppressUpdates) return;
RebuildDataTable();
FillCompComboBox();
ResetUI();
CalculateValues();
}
#endregion
private void UpdateGroupsComboBox() {
string selectedItem = (string)groupComboBox.SelectedItem;
groupComboBox.Items.Clear();
var parameters = (from run in Content
where run.Visible
from param in run.Parameters
select param.Key).Distinct().ToArray();
foreach (var p in parameters) {
var variations = (from run in Content
where run.Visible && run.Parameters.ContainsKey(p) &&
(run.Parameters[p] is IntValue || run.Parameters[p] is DoubleValue ||
run.Parameters[p] is StringValue || run.Parameters[p] is BoolValue)
select ((dynamic)run.Parameters[p]).Value).Distinct();
if (variations.Count() > 1) {
groupComboBox.Items.Add(p);
}
}
if (groupComboBox.Items.Count > 0) {
//try to select something different than "Seed" or "Algorithm Name" as this makes no sense
//and takes a long time to group
List possibleIndizes = new List();
for (int i = 0; i < groupComboBox.Items.Count; i++) {
if (groupComboBox.Items[i].ToString() != "Seed"
&& groupComboBox.Items[i].ToString() != "Algorithm Name") {
possibleIndizes.Add(i);
}
}
if (selectedItem != null && groupComboBox.Items.Contains(selectedItem)) {
groupComboBox.SelectedItem = selectedItem;
} else if (possibleIndizes.Count > 0) {
groupComboBox.SelectedItem = groupComboBox.Items[possibleIndizes.First()];
}
}
}
private string[] GetColumnNames(IEnumerable runs) {
string parameterName = (string)groupComboBox.SelectedItem;
var r = runs.Where(x => x.Parameters.ContainsKey(parameterName));
return r.Select(x => ((dynamic)x.Parameters[parameterName]).Value).Distinct().Select(x => (string)x.ToString()).ToArray();
}
private void UpdateResultComboBox() {
string selectedItem = (string)resultComboBox.SelectedItem;
resultComboBox.Items.Clear();
var results = (from run in Content
where run.Visible
from result in run.Results
where result.Value is IntValue || result.Value is DoubleValue
select result.Key).Distinct().ToArray();
resultComboBox.Items.AddRange(results);
if (selectedItem != null && resultComboBox.Items.Contains(selectedItem)) {
resultComboBox.SelectedItem = selectedItem;
} else if (resultComboBox.Items.Count > 0) {
resultComboBox.SelectedItem = resultComboBox.Items[0];
}
}
private void FillCompComboBox() {
string selectedItem = (string)groupCompComboBox.SelectedItem;
string parameterName = (string)groupComboBox.SelectedItem;
if (parameterName != null) {
string resultName = (string)resultComboBox.SelectedItem;
if (resultName != null) {
var runs = Content.Where(x => x.Results.ContainsKey(resultName) && x.Visible);
var columnNames = GetColumnNames(runs).ToList();
groupCompComboBox.Items.Clear();
columnNames.ForEach(x => groupCompComboBox.Items.Add(x));
if (selectedItem != null && groupCompComboBox.Items.Contains(selectedItem)) {
groupCompComboBox.SelectedItem = selectedItem;
} else if (groupCompComboBox.Items.Count > 0) {
groupCompComboBox.SelectedItem = groupCompComboBox.Items[0];
}
}
}
}
private void RebuildDataTable() {
string parameterName = (string)groupComboBox.SelectedItem;
if (parameterName != null) {
string resultName = (string)resultComboBox.SelectedItem;
var runs = Content.Where(x => x.Results.ContainsKey(resultName) && x.Visible);
var columnNames = GetColumnNames(runs);
var groups = GetGroups(columnNames, runs);
data = new double[columnNames.Count()][];
if (!groups.Any() || !columnNames.Any()) {
return;
}
DoubleMatrix dt = new DoubleMatrix(groups.Select(x => x.Count()).Max(), columnNames.Count());
dt.ColumnNames = columnNames;
DataTable histogramDataTable = new DataTable(resultName);
for (int i = 0; i < columnNames.Count(); i++) {
int j = 0;
data[i] = new double[groups[i].Count()];
DataRow row = new DataRow(columnNames[i]);
row.VisualProperties.ChartType = DataRowVisualProperties.DataRowChartType.Histogram;
histogramDataTable.Rows.Add(row);
foreach (IRun run in groups[i]) {
dt[j, i] = (double)((dynamic)run.Results[resultName]).Value;
data[i][j] = dt[j, i];
row.Values.Add(dt[j, i]);
j++;
}
}
GenerateChart(histogramDataTable);
stringConvertibleMatrixView.Content = dt;
}
}
private void GenerateChart(DataTable histogramTable) {
histogramControl.ClearPoints();
foreach (var row in histogramTable.Rows) {
histogramControl.AddPoints(row.Name, row.Values, true);
}
}
private List> GetGroups(string[] columnNames, IEnumerable runs) {
List> runCols = new List>();
string parameterName = (string)groupComboBox.SelectedItem;
foreach (string cn in columnNames) {
var tmpRuns = runs.Where(x =>
x.Parameters.ContainsKey(parameterName) &&
(((string)((dynamic)x.Parameters[parameterName]).Value.ToString()) == cn));
runCols.Add(tmpRuns);
}
return runCols;
}
private void ResetUI() {
normalityLabel.Image = null;
normalityTextLabel.Text = string.Empty;
groupCompLabel.Image = null;
groupComTextLabel.Text = string.Empty;
pairwiseLabel.Image = null;
pairwiseTextLabel.Text = string.Empty;
pValTextBox.Text = string.Empty;
equalDistsTextBox.Text = string.Empty;
}
private bool VerifyDataLength(bool showMessage) {
if (data == null || data.Length < 2)
return false;
//alglib needs at least 5 samples for computation
if (data.Any(x => x.Length < requiredSampleSize)) {
if (showMessage)
MessageBox.Show(this, "You need at least " + requiredSampleSize
+ " samples per group for computing hypothesis tests.", "HeuristicLab", MessageBoxButtons.OK,
MessageBoxIcon.Error);
return false;
}
return true;
}
private void CalculateValues() {
if (!VerifyDataLength(true))
return;
if (data != null && data.All(x => x != null)) {
Progress.Show(this, "Calculating...", ProgressMode.Indeterminate);
string curItem = (string)groupCompComboBox.SelectedItem;
Task.Factory.StartNew(() => CalculateValuesAsync(curItem));
}
}
private void CalculateValuesAsync(string groupName) {
CalculateAllGroupsTest();
CalculateNormalityTest();
CalculatePairwiseTest(groupName);
Progress.Hide(this);
}
private void CalculatePairwise(string groupName) {
if (groupName == null) return;
if (!VerifyDataLength(false))
return;
Progress.ShowOnControl(pairwiseTestGroupBox, "Calculating...", ProgressMode.Indeterminate);
Task.Factory.StartNew(() => CalculatePairwiseAsync(groupName));
}
private void CalculatePairwiseAsync(string groupName) {
CalculatePairwiseTest(groupName);
Progress.HideFromControl(pairwiseTestGroupBox);
}
private void CalculateAllGroupsTest() {
double pval = KruskalWallisTest.Test(data);
DisplayAllGroupsTextResults(pval);
}
private void DisplayAllGroupsTextResults(double pval) {
if (InvokeRequired) {
Invoke((Action)DisplayAllGroupsTextResults, pval);
} else {
pValTextBox.Text = pval.ToString();
if (pval < significanceLevel) {
groupCompLabel.Image = VSImageLibrary.Default;
groupComTextLabel.Text = "There are groups with different distributions";
} else {
groupCompLabel.Image = VSImageLibrary.Warning;
groupComTextLabel.Text = "Groups have an equal distribution";
}
}
}
private void CalculateNormalityTest() {
double val;
List res = new List();
DoubleMatrix pValsMatrix = new DoubleMatrix(1, stringConvertibleMatrixView.Content.Columns);
pValsMatrix.ColumnNames = stringConvertibleMatrixView.Content.ColumnNames;
pValsMatrix.RowNames = new[] { "p-Value" };
for (int i = 0; i < data.Length; i++) {
alglib.jarqueberatest(data[i], data[i].Length, out val);
res.Add(val);
pValsMatrix[0, i] = val;
}
// p-value is below significance level and thus the null hypothesis (data is normally distributed) is rejected
if (res.Any(x => x < significanceLevel)) {
Invoke(new Action(() => {
normalityLabel.Image = VSImageLibrary.Warning;
normalityTextLabel.Text = "Some groups may not be normally distributed";
}));
} else {
Invoke(new Action(() => {
normalityLabel.Image = VSImageLibrary.Default;
normalityTextLabel.Text = "All sample data is normally distributed";
}));
}
Invoke(new Action(() => {
normalityStringConvertibleMatrixView.Content = pValsMatrix;
normalityStringConvertibleMatrixView.DataGridView.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
}));
}
private void ShowPairwiseResult(int nrOfEqualDistributions) {
double ratio = ((double)nrOfEqualDistributions) / (data.Length - 1) * 100.0;
equalDistsTextBox.Text = ratio + " %";
if (nrOfEqualDistributions == 0) {
Invoke(new Action(() => {
pairwiseLabel.Image = VSImageLibrary.Default;
pairwiseTextLabel.Text = "All groups have different distributions";
}));
} else {
Invoke(new Action(() => {
pairwiseLabel.Image = VSImageLibrary.Warning;
pairwiseTextLabel.Text = "Some groups have equal distributions";
}));
}
}
private void CalculatePairwiseTest(string groupName) {
var columnNames = stringConvertibleMatrixView.Content.ColumnNames.ToList();
int colIndex = columnNames.IndexOf(groupName);
columnNames = columnNames.Where(x => x != groupName).ToList();
double[][] newData = FilterDataForPairwiseTest(colIndex);
var rowNames = new[] { "p-Value of Mann-Whitney U", "Adjusted p-Value of Mann-Whitney U",
"p-Value of T-Test", "Adjusted p-Value of T-Test", "Cohen's d", "Hedges' g" };
DoubleMatrix pValsMatrix = new DoubleMatrix(rowNames.Length, columnNames.Count());
pValsMatrix.ColumnNames = columnNames;
pValsMatrix.RowNames = rowNames;
double mwuBothTails;
double tTestBothTails;
double[] mwuPValues = new double[newData.Length];
double[] tTestPValues = new double[newData.Length];
bool[] decision = null;
double[] adjustedMwuPValues = null;
double[] adjustedTtestPValues = null;
int cnt = 0;
for (int i = 0; i < newData.Length; i++) {
mwuBothTails = PairwiseTest.MannWhitneyUTest(data[colIndex], newData[i]);
tTestBothTails = PairwiseTest.TTest(data[colIndex], newData[i]);
mwuPValues[i] = mwuBothTails;
tTestPValues[i] = tTestBothTails;
if (mwuBothTails > significanceLevel) {
cnt++;
}
}
adjustedMwuPValues = BonferroniHolm.Calculate(significanceLevel, mwuPValues, out decision);
adjustedTtestPValues = BonferroniHolm.Calculate(significanceLevel, tTestPValues, out decision);
for (int i = 0; i < newData.Length; i++) {
pValsMatrix[0, i] = mwuPValues[i];
pValsMatrix[1, i] = adjustedMwuPValues[i];
pValsMatrix[2, i] = tTestPValues[i];
pValsMatrix[3, i] = adjustedTtestPValues[i];
pValsMatrix[4, i] = SampleSizeDetermination.CalculateCohensD(data[colIndex], newData[i]);
pValsMatrix[5, i] = SampleSizeDetermination.CalculateHedgesG(data[colIndex], newData[i]);
}
Invoke(new Action(() => {
pairwiseStringConvertibleMatrixView.Content = pValsMatrix;
pairwiseStringConvertibleMatrixView.DataGridView.AutoResizeColumns(DataGridViewAutoSizeColumnsMode.AllCells);
}));
ShowPairwiseResult(cnt);
}
private double[][] FilterDataForPairwiseTest(int columnToRemove) {
double[][] newData = new double[data.Length - 1][];
int i = 0;
int l = 0;
while (i < data.Length) {
if (i != columnToRemove) {
double[] row = new double[data[i].Length - 1];
newData[l] = row;
int j = 0, k = 0;
while (j < row.Length) {
if (i != columnToRemove) {
newData[l][j] = data[i][k];
j++;
k++;
} else {
k++;
}
}
i++;
l++;
} else {
i++;
}
}
return newData;
}
}
}