Introduction
I needed to have a simple linear trend calculation for X values either as double
or datetime
. So basically the Y value for a single item is always a double
but the type of the X varies. Beyond that, I needed basic statistics calculated from the values. These included:
- Slope
- Y-intercept
- Correlation coefficient
- R-squared value
Excerpt from the user interface
As an additional step I decided to have my first look at F# so now one implementation of the calculation is done using F#. I have to admit that it isn't yet F#ish and looks more like C# but on the other hand that may help readers to see equivalences (and differences).
Formulas used in calculations
Since the requirement was to do the calculation in a similar way as it would be done in Excel, I used the same variations for formulas as Excel uses. This also made it simple to check the correctness of the calculations. So the formulas are:
Line
$\large y=mx+b$
where
m
is slope x
is the horizontal axis value b
is the Y-intercept
Slope calculation
$\large s=\frac{\sum \left ( x - \bar{x} \right ) \left ( y - \bar{y} \right )}{\sum \left ( x - \bar{x} \right )^{2}}$
where
x
and y
are individual values accented x
and y
are averages for the corresponding values
The correlation coefficient
$\large c=\frac{\sum \left ( x - \bar{x} \right ) \left ( y - \bar{y} \right )}{\sqrt{\sum \left ( x - \bar{x} \right )^{2} \left ( y - \bar{y} \right )^{2}}}$
where again
x
and y
are individual values accented x
and y
are averages for the corresponding values
R-squared value
$\large r^{2}=1 - \frac{\sum \left ( y - \hat{y} \right )^{2}}{\sum y^{2} - \frac{\left ( \sum y \right )^{2}}{n}}$
where
y
is individual values accented y
(with a hat) is the corresponding calculated trend value n
is the count of values.
Classes for value items
The first thing is to create the classes for the actual value items, both double
and datetime
. Basically the classes are simple, just properties X
and Y
. But things get a bit more complicated since the type of the X
varies. Instead of using an object
property I wanted to have separate classes for the different item types and to be able use double
and datetime
types instead of object. This approach quickly lead to using an abstract base class with generics.
However, using generics for X
introduces a new problem, how to use the same calculation for two different data types. Since I didn’t have any specific requirements concerning the calculation, I decided to convert the X
values always to double. In order to use this value in calculation an extra property ConvertedX
is defined.
The classes look like following
Abstract
(MustInherit)
base class for value items
namespace TrendCalculus {
public abstract class ValueItem<TX> : IValueItem {
private double _y;
public event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
public abstract TX X { get; set; }
public abstract double ConvertedX { get; set; }
public double Y {
get {
return this._y;
}
set {
if (this._y != value) {
this._y = value;
this.NotifyPropertyChanged("Y");
}
}
}
protected void NotifyPropertyChanged(string propertyName) {
System.ComponentModel.PropertyChangedEventHandler handler = this.PropertyChanged;
if (handler != null) {
handler(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName));
}
}
public abstract object CreateCopy();
public abstract object NewTrendItem();
}
}
Public MustInherit Class ValueItem(Of TX)
Implements IValueItem
Private _y As Double
Public Event PropertyChanged As System.ComponentModel.PropertyChangedEventHandler
Implements IValueItem.PropertyChanged
Public MustOverride Property X As TX
Public MustOverride Property ConvertedX As Double Implements IValueItem.ConvertedX
Public Property Y As Double Implements IValueItem.Y
Get
Return Me._y
End Get
Set
If (Me._y <> Value) Then
Me._y = Value
Me.NotifyPropertyChanged("Y")
End If
End Set
End Property
Protected Sub NotifyPropertyChanged(propertyName As String)
RaiseEvent PropertyChanged(Me, New System.ComponentModel.PropertyChangedEventArgs(propertyName))
End Sub
Public MustOverride Function CreateCopy() As Object Implements IValueItem.CreateCopy
Public MustOverride Function NewTrendItem() As Object Implements IValueItem.NewTrendItem
End Class
namespace TrendCalculusFSharp
[<AbstractClass>]
type public ValueItem<'TX>() =
let mutable yValue : double = 0.0
let mutable convertedXValue : double = 0.0
let propertyChanged = new Event<System.ComponentModel.PropertyChangedEventHandler,
System.ComponentModel.PropertyChangedEventArgs>()
interface IValueItem with
[<CLIEvent>]
member this.PropertyChanged :
Control.IEvent<System. ComponentModel.PropertyChangedEventHandler,
System.ComponentModel.PropertyChangedEventArgs>
= propertyChanged.Publish
member this.ConvertedX
with get() = this.ConvertedX
and set(value) = this.ConvertedX <- value
member this.CreateCopy() = this.CreateCopy()
member this.NewTrendItem() = this.NewTrendItem()
member this.Y
with get() = this.Y
and set(value) = this.Y <- value
member this.Y
with get() = yValue
and set(value) =
if yValue <> value then
yValue <- value
this.NotifyPropertyChanged("Y")
abstract member NewTrendItem : unit -> obj
default __.NewTrendItem() = null
abstract member CreateCopy : unit -> obj
default __.CreateCopy() = null
abstract X : 'TX with get, set
abstract ConvertedX : double with get, set
default __.ConvertedX
with get() = convertedXValue
and set(value) = convertedXValue <-value
member this.NotifyPropertyChanged(propertyName) =
propertyChanged.Trigger(this, new System.ComponentModel.PropertyChangedEventArgs(propertyName))
Value item class for double
values
namespace TrendCalculus {
public class NumberItem : ValueItem<double> {
private double _x;
public override double X {
get {
return this._x;
}
set {
if (this._x != value) {
this._x = value;
this.NotifyPropertyChanged("X");
}
}
}
public override double ConvertedX {
get {
return this.X;
}
set {
if (this.X != value) {
this.X = value;
}
}
}
public override object NewTrendItem() {
return new NumberItem();
}
public override object CreateCopy() {
return new NumberItem() {
X = this.X,
Y = this.Y
};
}
}
}
Public Class NumberItem
Inherits ValueItem(Of Double)
Private _x As Double
Public Overrides Property X As Double
Get
Return Me._x
End Get
Set(value As Double)
If (Me._x <> value) Then
Me._x = value
Me.NotifyPropertyChanged("X")
End If
End Set
End Property
Public Overrides Property ConvertedX As Double
Get
Return Me.X
End Get
Set(value As Double)
If (Me.X <> value) Then
Me.X = value
End If
End Set
End Property
Public Overrides Function NewTrendItem() As Object
Return New NumberItem()
End Function
Public Overrides Function CreateCopy() As Object
Dim newItem As NumberItem = New NumberItem()
newItem.X = Me.X
newItem.Y = Me.Y
Return newItem
End Function
End Class
namespace TrendCalculusFSharp
type NumberItem() =
inherit ValueItem<double>()
let mutable xValue : double = 0.0
override this.X
with get() = xValue
and set(value) =
if xValue <> value then
xValue <- value
this.NotifyPropertyChanged("X")
override this.ConvertedX
with get() = this.X
and set(value) =
if this.X <> value then
this.X <- value
override this.NewTrendItem() =
new NumberItem() :> obj
override this.CreateCopy() =
let copy = new NumberItem()
copy.X <- this.X
copy.Y <- this.Y
copy :> obj
Value item class for datetime
values
namespace TrendCalculus {
public class DateItem : ValueItem<System.DateTime> {
private System.DateTime _x;
public override System.DateTime X {
get {
return this._x;
}
set {
if (this._x != value) {
this._x = value;
this.NotifyPropertyChanged("X");
}
}
}
public override double ConvertedX {
get {
double returnValue = 0;
if (this.X != null) {
returnValue = this.X.ToOADate();
}
return returnValue;
}
set {
System.DateTime converted = System.DateTime.FromOADate(value);
if (this.X != converted) {
this.X = converted;
}
}
}
public override object NewTrendItem() {
return new DateItem();
}
public override object CreateCopy() {
return new DateItem() {
X = this.X,
Y = this.Y
};
}
}
}
Public Class DateItem
Inherits ValueItem(Of System.DateTime)
Private _x As System.DateTime
Public Overrides Property X As System.DateTime
Get
Return Me._x
End Get
Set(value As System.DateTime)
If (Me._x <> value) Then
Me._x = value
Me.NotifyPropertyChanged("X")
End If
End Set
End Property
Public Overrides Property ConvertedX As Double
Get
Dim returnValue As Double = 0
returnValue = Me.X.ToOADate()
Return returnValue
End Get
Set(value As Double)
Dim converted As System.DateTime = System.DateTime.FromOADate(value)
If (Me.X <> converted) Then
Me.X = converted
End If
End Set
End Property
Public Overrides Function NewTrendItem() As Object
Return New DateItem()
End Function
Public Overrides Function CreateCopy() As Object
Dim newItem As DateItem = New DateItem()
newItem.X = Me.X
newItem.Y = Me.Y
Return newItem
End Function
End Class
namespace TrendCalculusFSharp
type DateItem() =
inherit ValueItem<System.DateTime>()
let mutable xValue : System.DateTime = System.DateTime.MinValue
override this.X
with get() = xValue
and set(value) =
if xValue <> value then
xValue <- value
this.NotifyPropertyChanged("X")
override this.ConvertedX
with get() = this.X.ToOADate()
and set(value) =
let converted : System.DateTime = System.DateTime.FromOADate(value)
if this.X <> converted then
this.X <- converted
override this.NewTrendItem() =
new NumberItem() :> obj
override this.CreateCopy() =
let copy = new DateItem()
copy.X <- this.X
copy.Y <- this.Y
copy :> obj
As you might notice the abstract class implements IValueItem
interface. This interface is used for collections of data items. The interface helps the collection handling since it defines all the necessary methods and properties and eliminates the need to know the actual data type for X
, which would be needed if the abstract class definition would be used. So the interface looks like this
namespace TrendCalculus {
public interface IValueItem {
event System.ComponentModel.PropertyChangedEventHandler PropertyChanged;
double ConvertedX { get; set; }
double Y { get; set; }
object CreateCopy();
object NewTrendItem();
}
}
Public Interface IValueItem
Event PropertyChanged As System.ComponentModel.PropertyChangedEventHandler
Property ConvertedX As Double
Property Y As Double
Function CreateCopy() As Object
Function NewTrendItem() As Object
End Interface
namespace TrendCalculusFSharp
type public IValueItem =
interface
[<CLIEvent>]
abstract member PropertyChanged :
Control.IEvent<System.ComponentModel.PropertyChangedEventHandler,
System.ComponentModel.PropertyChangedEventArgs>
abstract ConvertedX : double with get, set
abstract Y : double with get, set
abstract member CreateCopy : unit -> obj
abstract member NewTrendItem : unit -> obj
end
List of values
The next thing is to create a list for the value items. Of course a simple list could do, but to make things more easy to use I wanted to have a collection which would satisfy following requirements
- Changes in the collection are automatically detected by WPF
- Only items implementing
IValueItem
could be added to collection - Any change in the collection would case a data change notification. This would include adding or removing items but also changes in the property values of the items.
Because of these I inherited a new class from ObservableCollection
as follows
namespace TrendCalculus {
public class ValueList<TValueItem> : System.Collections.ObjectModel.ObservableCollection<TValueItem>
where TValueItem : IValueItem {
public event System.EventHandler DataChanged;
public ValueListTypes ListType { get; private set; }
private ValueList() {
this.CollectionChanged += ValueList_CollectionChanged;
}
internal ValueList(ValueListTypes listType) : this() {
this.ListType = listType;
}
private void ValueList_CollectionChanged(object sender,
System.Collections.Specialized.NotifyCollectionChangedEventArgs e) {
if (e.OldItems != null) {
foreach (IValueItem item in e.OldItems) {
item.PropertyChanged -= item_PropertyChanged;
}
}
if (e.NewItems != null) {
foreach (IValueItem item in e.NewItems) {
item.PropertyChanged += item_PropertyChanged;
}
}
this.NotifyDataChanged(this);
}
private void item_PropertyChanged(object sender,
System.ComponentModel.PropertyChangedEventArgs e) {
this.NotifyDataChanged(sender);
}
private void NotifyDataChanged(object sender) {
System.EventHandler handler = this.DataChanged;
if (handler != null) {
handler(sender, new System.EventArgs());
}
}
}
}
Public Class ValueList(Of TValueItem As IValueItem)
Inherits System.Collections.ObjectModel.ObservableCollection(Of TValueItem)
Public Event DataChanged As System.EventHandler
Public ListType As ValueListTypes
Private Sub New()
AddHandler Me.CollectionChanged, AddressOf ValueList_CollectionChanged
End Sub
Friend Sub New(listType As ValueListTypes)
Me.New()
Me.ListType = listType
End Sub
Private Sub ValueList_CollectionChanged(sender As Object,
e As System.Collections.Specialized.NotifyCollectionChangedEventArgs)
If (Not e.OldItems Is Nothing) Then
For Each Item As IValueItem In e.OldItems
RemoveHandler Item.PropertyChanged, AddressOf item_PropertyChanged
Next
End If
If (Not e.NewItems Is Nothing) Then
For Each item As IValueItem In e.NewItems
AddHandler item.PropertyChanged, AddressOf item_PropertyChanged
Next
End If
Me.NotifyDataChanged(Me)
End Sub
Private Sub item_PropertyChanged(sender As Object,
e As System.ComponentModel.PropertyChangedEventArgs)
Me.NotifyDataChanged(sender)
End Sub
Private Sub NotifyDataChanged(sender As Object)
RaiseEvent DataChanged(sender, New System.EventArgs())
End Sub
End Class
namespace TrendCalculusFSharp
type ValueList<'TValueItem when 'TValueItem :> IValueItem>(listType : ValueListTypes) as this =
inherit System.Collections.ObjectModel.ObservableCollection<'TValueItem>()
let dataChanged = new Control.Event<obj>()
let collectionChangedHandler = new System.Collections.Specialized.NotifyCollectionChangedEventHandler(this.ValueList_CollectionChanged)
let propertyChangedHandler = new System.ComponentModel.PropertyChangedEventHandler(this.item_PropertyChanged)
do this.CollectionChanged.AddHandler(collectionChangedHandler)
[<CLIEvent>]
member this.DataChanged = dataChanged.Publish
member this.ListType : ValueListTypes = listType
member this.ValueList_CollectionChanged (sender : obj) (e : System.Collections.Specialized.NotifyCollectionChangedEventArgs) =
if e.OldItems <> null then
let items = seq { for item in e.OldItems -> (item :?> IValueItem)}
for valueitem in items do
valueitem.PropertyChanged.RemoveHandler(propertyChangedHandler)
if e.NewItems <> null then
let items = seq { for item in e.NewItems -> (item :?> IValueItem)}
for valueitem in items do
valueitem.PropertyChanged.AddHandler(propertyChangedHandler)
this.NotifyDataChanged(this)
member private this.item_PropertyChanged(sender : obj) ( e : System.ComponentModel.PropertyChangedEventArgs) =
this.NotifyDataChanged(sender)
member private this.NotifyDataChanged(sender : obj) =
dataChanged.Trigger(sender)
As you see the constructor wires the CollectionChanged
event so any modification to the collection will be noticed. When the collection is changed the PropertyChanged
event for all the items is wired so that if any changes occur in the properties of individual value items, the collection is notified. Both event handlers raise DataChanged
event if any change occur.
The calculation
The calculation is done by the LinearTrend
class. The usage is that first the DataItems
collection is filled with proper value items and when done, Calculate
method is called. The calculation fills the following properties
Calculated
, the value is true after Calculate
has been called. However, the class keeps track of changes in the data item collection by listening DataChanged
event so if the source data changes in any way, this property is set to false Slope
contains the calculated slope Intercept
contains the value for Y when Y axis is crossed Correl
contains the correlation coefficient R2
contains the r-squared value DataItems
contains the source data TrendItems
contains the calculated trend value for each unique X value in the source data StartPoint
returns the calculated trend value for the first X value EndPoint
returns the calculated trend value for the last X value
So the coding part of the calculation looks like this
public LinearTrend() {
this.DataItems = new ValueList<TValueItem>(ValueListTypes.DataItems);
this.TrendItems = new ValueList<TValueItem>(ValueListTypes.TrendItems);
this.Calculated = false;
this.DataItems.DataChanged += DataItems_DataChanged;
}
private void DataItems_DataChanged(object sender, System.EventArgs e) {
if (this.Calculated) {
this.Calculated = false;
this.Slope = null;
this.Intercept = null;
this.Correl = null;
this.TrendItems.Clear();
}
}
public bool Calculate() {
double slopeNumerator;
double slopeDenominator;
double correlDenominator;
double r2Numerator;
double r2Denominator;
double averageX;
double averageY;
TValueItem trendItem;
if (this.DataItems.Count == 0) {
return false;
}
averageX = this.DataItems.Average(item => item.ConvertedX);
averageY = this.DataItems.Average(item => item.Y);
slopeNumerator = this.DataItems.Sum(item => (item.ConvertedX - averageX)
* (item.Y - averageY));
slopeDenominator = this.DataItems.Sum(item => System.Math.Pow(item.ConvertedX - averageX, 2));
this.Slope = slopeNumerator / slopeDenominator;
this.Intercept = averageY - this.Slope * averageX;
correlDenominator = System.Math.Sqrt(
this.DataItems.Sum(item => System.Math.Pow(item.ConvertedX - averageX, 2))
* this.DataItems.Sum(item => System.Math.Pow(item.Y - averageY, 2)));
this.Correl = slopeNumerator / correlDenominator;
foreach (TValueItem item in this.DataItems.OrderBy(dataItem => dataItem.ConvertedX)) {
if (this.TrendItems.Where(existingItem
=> existingItem.ConvertedX == item.ConvertedX).FirstOrDefault() == null) {
trendItem = (TValueItem)item.NewTrendItem();
trendItem.ConvertedX = item.ConvertedX;
trendItem.Y = this.Slope.Value * item.ConvertedX + this.Intercept.Value;
this.TrendItems.Add(trendItem);
}
}
r2Numerator = this.DataItems.Sum(
dataItem => System.Math.Pow(dataItem.Y
- this.TrendItems.Where(
calcItem => calcItem.ConvertedX == dataItem.ConvertedX).First().Y, 2));
r2Denominator = this.DataItems.Sum(dataItem => System.Math.Pow(dataItem.Y, 2))
- (System.Math.Pow(this.DataItems.Sum(dataItem => dataItem.Y), 2) / this.DataItems.Count);
this.R2 = 1 - (r2Numerator / r2Denominator);
this.Calculated = true;
return true;
}
Public Sub New()
Me.DataItems = New ValueList(Of TValueItem)(ValueListTypes.DataItems)
Me.TrendItems = New ValueList(Of TValueItem)(ValueListTypes.TrendItems)
Me.Calculated = False
AddHandler Me.DataItems.DataChanged, AddressOf DataItems_DataChanged
End Sub
Private Sub DataItems_DataChanged(sender As Object, e As System.EventArgs)
If (Me.Calculated) Then
Me._Calculated = False
Me._Slope = Nothing
Me._Intercept = Nothing
Me._Correl = Nothing
Me.TrendItems.Clear()
End If
End Sub
Public Function Calculate() As Boolean
Dim slopeNumerator As Double
Dim slopeDenominator As Double
Dim correlDenominator As Double
Dim r2Numerator As Double
Dim r2Denominator As Double
Dim averageX As Double
Dim averageY As Double
Dim trendItem As TValueItem
If (Me.DataItems.Count = 0) Then
Return False
End If
averageX = Me.DataItems.Average(Function(item) item.ConvertedX)
averageY = Me.DataItems.Average(Function(item) item.Y)
slopeNumerator = Me.DataItems.Sum(Function(item) (item.ConvertedX - averageX) * _
(item.Y - averageY))
slopeDenominator = Me.DataItems.Sum(Function(item) _
System.Math.Pow(item.ConvertedX - averageX, 2))
Me._Slope = slopeNumerator / slopeDenominator
Me._Intercept = averageY - Me.Slope * averageX
correlDenominator = System.Math.Sqrt(Me.DataItems.Sum( Function(item) _
System.Math.Pow(item.ConvertedX - averageX, 2)) * Me.DataItems.Sum(Function(item) _
System.Math.Pow(item.Y - averageY, 2)))
Me._Correl = slopeNumerator / correlDenominator
For Each item As TValueItem In Me.DataItems.OrderBy(Function(dataItem) dataItem.ConvertedX)
If (Me.TrendItems.Where(Function(existingItem) existingItem.ConvertedX = _
item.ConvertedX).FirstOrDefault() Is Nothing) Then
trendItem = CType(item.NewTrendItem(), TValueItem)
trendItem.ConvertedX = item.ConvertedX
trendItem.Y = Me.Slope.Value * item.ConvertedX + Me.Intercept.Value
Me.TrendItems.Add(trendItem)
End If
Next
r2Numerator = Me.DataItems.Sum(
Function(dataItem) System.Math.Pow(dataItem.Y _
- Me.TrendItems.Where(
Function(calcItem) calcItem.ConvertedX = dataItem.ConvertedX).First().Y, 2))
r2Denominator = Me.DataItems.Sum(Function(dataItem) System.Math.Pow(dataItem.Y, 2)) _
- (System.Math.Pow(Me.DataItems.Sum(Function(dataItem) dataItem.Y), 2) / Me.DataItems.Count)
Me._R2 = 1 - (r2Numerator / r2Denominator)
Me._Calculated = True
Return True
End Function
namespace TrendCalculusFSharp
open System.Linq
type LinearTrend<'TValueItem when 'TValueItem :> IValueItem>() as this =
let mutable calculatedValue : bool = false
let mutable slopeValue : System.Nullable<double> = System.Nullable<double>()
let mutable interceptValue : System.Nullable<double> = System.Nullable<double>()
let mutable correlValue : System.Nullable<double> = System.Nullable<double>()
let mutable r2Value : System.Nullable<double> = System.Nullable<double>()
let dataItemsValue : ValueList<'TValueItem> = ValueList<'TValueItem>(ValueListTypes.DataItems)
let trendItemsValue : ValueList<'TValueItem> = ValueList<'TValueItem>(ValueListTypes.TrendItems)
let dataChangedHandler = this.DataItems_DataChanged
do this.DataItems.DataChanged.Add(dataChangedHandler)
member this.Calculated = calculatedValue
member this.Slope : System.Nullable<double> = slopeValue
member this.Intercept : System.Nullable<double> = interceptValue
member this.Correl : System.Nullable<double> = correlValue
member this.R2 : System.Nullable<double> = r2Value
member this.DataItems : ValueList<'TValueItem> = dataItemsValue
member this.TrendItems : ValueList<'TValueItem> = trendItemsValue
member this.StartPoint
with get() =
match this.Calculated with
| false -> Unchecked.defaultof<'TValueItem>
| true -> this.TrendItems.OrderBy(fun item -> item.ConvertedX).FirstOrDefault()
member this.EndPoint
with get() =
match this.Calculated with
| false -> Unchecked.defaultof<'TValueItem>
| true -> this.TrendItems.OrderByDescending(fun item -> item.ConvertedX).FirstOrDefault()
member private this.DataItems_DataChanged(sender : obj) =
if this.Calculated = true then
calculatedValue <- false
slopeValue <- System.Nullable<double>()
interceptValue <- System.Nullable<double>()
correlValue <- System.Nullable<double>()
this.TrendItems.Clear()
member this.AverageX : double =
Seq.averageBy(fun item -> (item :> IValueItem).ConvertedX) (this.DataItems)
member this.AverageY : double =
Seq.averageBy(fun item -> (item :> IValueItem).Y) (this.DataItems)
member this.Calculate() : bool =
let mutable slopeNumerator : double = 0.0
let mutable correlDenominator : double = 0.0
let mutable r2Numerator : double = 0.0
let mutable r2Denominator : double = 0.0
calculatedValue <- false
if this.DataItems.Count <> 0 then
slopeNumerator <-
Seq.sumBy(fun item-> ((item :> IValueItem).ConvertedX - this.AverageX) * ((item :> IValueItem).Y - this.AverageY)) (this.DataItems)
correlDenominator <-
sqrt (
Seq.sumBy(fun item-> pown ((item :> IValueItem).ConvertedX - this.AverageX) 2) (this.DataItems)
*
Seq.sumBy(fun item-> pown ((item :> IValueItem).Y - this.AverageY) 2) (this.DataItems)
)
slopeValue <- System.Nullable(
slopeNumerator
/
Seq.sumBy(fun item-> pown ((item :> IValueItem).ConvertedX - this.AverageX) 2) (this.DataItems)
)
interceptValue <- System.Nullable(
this.AverageY - this.Slope.Value * this.AverageX
)
correlValue <- System.Nullable(
slopeNumerator / correlDenominator
)
for item in this.DataItems.OrderBy(fun dataItem -> dataItem.ConvertedX) do
if this.TrendItems.Where(fun existingItem -> existingItem.ConvertedX = item.ConvertedX).Count() = 0 then
let trendItem : 'TValueItem = item.NewTrendItem() :?> 'TValueItem
trendItem.ConvertedX <- item.ConvertedX
trendItem.Y <- this.Slope.Value * item.ConvertedX + this.Intercept.Value
this.TrendItems.Add(trendItem);
r2Numerator <-
this.DataItems.Sum(fun dataItem ->
pown (dataItem.Y - this.TrendItems.Where(fun calcItem ->
calcItem.ConvertedX = dataItem.ConvertedX).First().Y) 2)
r2Denominator <-
this.DataItems.Sum(fun dataItem -> pown dataItem.Y 2)
- ((pown (this.DataItems.Sum(fun dataItem -> dataItem.Y)) 2) / (float this.DataItems.Count))
r2Value <- System.Nullable(1.0 - (r2Numerator / r2Denominator))
calculatedValue <- true
calculatedValue
As you can see I have used LINQ in calculations. It would have been possible to condense the calculation even more, but in order to help debugging I calculated numerators and denominators separately. But as a side-note, using LINQ here simplifies the code a lot.
The test application
Now in order to test the functionality let’s create a small test application. The application should be able to generate both double
and datetime
values as test material and also to show the results of the calculation. The window looks like this with double
values
And an example with datetime
values
The code is quite simple. The "Generate values" -button creates the test data with Random
object and when the test material is created one can press the "Calculate" -button to show the results
namespace TrendTest {
public partial class TestWindow : System.Windows.Window {
TrendCalculus.LinearTrend<TrendCalculus.IValueItem> linearTrend
= new TrendCalculus.LinearTrend<TrendCalculus.IValueItem>();
public TestWindow() {
InitializeComponent();
this.UseDouble.IsChecked = true;
this.Values.ItemsSource = linearTrend.DataItems;
this.TrendItems.ItemsSource = this.linearTrend.TrendItems;
}
private void GenerateValues_Click(object sender, System.Windows.RoutedEventArgs e) {
System.Random random = new System.Random();
linearTrend.DataItems.Clear();
for (int counter = 0; counter < 10; counter++) {
if (this.UseDouble.IsChecked.Value) {
linearTrend.DataItems.Add(new TrendCalculus.NumberItem() {
X = System.Math.Round(random.NextDouble() * 100),
Y = System.Math.Round(random.NextDouble() * 100)
});
} else {
linearTrend.DataItems.Add(new TrendCalculus.DateItem() {
X = System.DateTime.Now.AddDays(System.Math.Round(random.NextDouble() * -100)).Date,
Y = System.Math.Round(random.NextDouble() * 100)
});
}
}
}
private void Calculate_Click(object sender, System.Windows.RoutedEventArgs e) {
if (this.linearTrend.Calculate()) {
this.TrendItems.ItemsSource = this.linearTrend.TrendItems;
this.Slope.Text = this.linearTrend.Slope.ToString();
this.Intercept.Text = this.linearTrend.Intercept.ToString();
this.Correl.Text = this.linearTrend.Correl.ToString();
this.R2.Text = this.linearTrend.R2.ToString();
this.StartX.Text = this.linearTrend.StartPoint.ConvertedX.ToString();
this.StartY.Text = this.linearTrend.StartPoint.Y.ToString();
this.EndX.Text = this.linearTrend.EndPoint.ConvertedX.ToString();
this.EndY.Text = this.linearTrend.EndPoint.Y.ToString();
}
}
private void UseDouble_Checked(object sender, System.Windows.RoutedEventArgs e) {
this.linearTrend.DataItems.Clear();
}
private void UseDatetime_Checked(object sender, System.Windows.RoutedEventArgs e) {
this.linearTrend.DataItems.Clear();
}
private void DataItemsToClipboard_Click(object sender, System.Windows.RoutedEventArgs e) {
System.Text.StringBuilder clipboardData = new System.Text.StringBuilder();
clipboardData.AppendFormat("{0}\t{1}\t{2}", "Actual X", "Converted X", "Y").AppendLine();
foreach (TrendCalculus.IValueItem item in linearTrend.DataItems) {
if (item is TrendCalculus.DateItem) {
clipboardData.AppendFormat("{0}\t{1}\t{2}",
((TrendCalculus.DateItem)item).X.ToShortDateString(), item.ConvertedX, item.Y);
} else {
clipboardData.AppendFormat("{0}\t{1}\t{2}",
((TrendCalculus.NumberItem)item).X.ToString(), item.ConvertedX, item.Y);
}
clipboardData.AppendLine();
}
System.Windows.Clipboard.SetText(clipboardData.ToString());
}
}
}
Class TestWindow
Dim linearTrend As TrendCalculusVB.LinearTrend(Of TrendCalculusVB.IValueItem) = _
New TrendCalculusVB.LinearTrend(Of TrendCalculusVB.IValueItem)()
Private Sub Window_Loaded(sender As Object, e As RoutedEventArgs)
Me.UseDouble.IsChecked = True
Me.Values.ItemsSource = Me.linearTrend.DataItems
Me.TrendItems.ItemsSource = Me.linearTrend.TrendItems
End Sub
Private Sub GenerateValues_Click(sender As Object, e As System.Windows.RoutedEventArgs)
Dim random As System.Random = New System.Random()
linearTrend.DataItems.Clear()
For counter As Int32 = 0 To 9
If (Me.UseDouble.IsChecked.Value) Then
linearTrend.DataItems.Add(New TrendCalculusVB.NumberItem() With {
.X = System.Math.Round(random.NextDouble() * 100),
.Y = System.Math.Round(random.NextDouble() * 100)
})
Else
linearTrend.DataItems.Add(New TrendCalculusVB.DateItem() With {
.X = System.DateTime.Now.AddDays(System.Math.Round(random.NextDouble() * -100)).Date,
.Y = System.Math.Round(random.NextDouble() * 100)
})
End If
Next counter
End Sub
Private Sub Calculate_Click(sender As Object, e As System.Windows.RoutedEventArgs)
If (Me.linearTrend.Calculate()) Then
Me.TrendItems.ItemsSource = Me.linearTrend.TrendItems
Me.Slope.Text = Me.linearTrend.Slope.ToString()
Me.Intercept.Text = Me.linearTrend.Intercept.ToString()
Me.Correl.Text = Me.linearTrend.Correl.ToString()
Me.R2.Text = Me.linearTrend.R2.ToString()
Me.StartX.Text = Me.linearTrend.StartPoint.ConvertedX.ToString()
Me.StartY.Text = Me.linearTrend.StartPoint.Y.ToString()
Me.EndX.Text = Me.linearTrend.EndPoint.ConvertedX.ToString()
Me.EndY.Text = Me.linearTrend.EndPoint.Y.ToString()
End If
End Sub
Private Sub UseDouble_Checked(sender As Object, e As System.Windows.RoutedEventArgs)
Me.linearTrend.DataItems.Clear()
End Sub
Private Sub UseDatetime_Checked(sender As Object, e As System.Windows.RoutedEventArgs)
Me.linearTrend.DataItems.Clear()
End Sub
Private Sub DataItemsToClipboard_Click(sender As Object, e As System.Windows.RoutedEventArgs)
Dim clipboardData As System.Text.StringBuilder = New System.Text.StringBuilder()
clipboardData.AppendFormat("{0}{1}{2}{3}{4}", "Actual X", vbTab, "Converted X", _
vbTab, "Y").AppendLine()
For Each item As TrendCalculusVB.IValueItem In linearTrend.DataItems
If (TypeOf (item) Is TrendCalculusVB.DateItem) Then
clipboardData.AppendFormat("{0}{1}{2}{3}{4}", (CType(item, _
TrendCalculusVB.DateItem)).X.ToShortDateString(), vbTab, item.ConvertedX, vbTab, item.Y)
Else
clipboardData.AppendFormat("{0}{1}{2}{3}{4}", (CType(item, _
TrendCalculusVB.NumberItem)).X.ToString(), vbTab, item.ConvertedX, vbTab, item.Y)
End If
clipboardData.AppendLine()
Next
System.Windows.Clipboard.SetText(clipboardData.ToString())
End Sub
End Class
In order to easily test the calculations a "Copy to clipboard" -button is included that copies the source data to clipboard with tabulators as delimiters so that the data can easily be pasted into Excel.
Remarks
As this was the first trial on F# I understand that changing the mindset and learning to produce good F# is going to be a rocky road. However, having used procedural and OO languages for ages F# seems like a fresh breath so far :)
References
The references concerning the corresponding Excel functions can be found at:
History
- 22nd May, 2016: Article created
- 28th May, 2016: VB.Net version added
- 6th June, 2016: F# version added
- 15th August, 2017: Replaced pictures of formulas with LaTeX equations