VB.Net - OOP Statistical Functions
Overview
This is an OOP example that focuses on Statistical Functions, with input via a NumericUpDown Control, and displaying output in a PropertyGrid Control. The OOP part is simple. There's a Class for each of these...
- mean
- median
- mode
- range
- iqr
- sd
Each Class has just one familiar Public member - getValue, which returns either a single Decimal or an Array of Decimal. There is also a common Class which hosts methods used by more than one of the getValue Functions.
OOP Core Classes
The common Class
This contains methods used in more than one core method.
Public Class common
Public Shared Function ocToArray(items As ListBox.ObjectCollection) As Decimal()
Return items.Cast(Of Object).Select(Function(o) CDec(o.ToString)).ToArray
End Function
Public Shared Function median(ByVal numbers() As Decimal) As Decimal
Array.Sort(numbers)
If numbers.Length = 0 Then
Return 0
ElseIf numbers.Length = 1 Then
Return numbers(0)
End If
If numbers.Length Mod 2 = 1 Then 'odd count
Return numbers(CInt(Math.Floor(numbers.Length / 2)))
Else 'even count
Dim lower As Integer = CInt(numbers.Length / 2 - 1)
Dim higher As Integer = lower + 1
Return (numbers(lower) + numbers(higher)) / 2
End If
End Function
End Class
The mean Class
Mean is just the average of an Array of numbers, which is easily calculated in .Net
Public Class mean
Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As Decimal
Dim numbers() As Decimal = common.ocToArray(items)
Return numbers.Average
End Function
End Class
The median Class
Median is the middle value in a sorted Array of numbers
Public Class median
Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As Decimal
Dim numbers() As Decimal = common.ocToArray(items)
Return common.median(numbers)
End Function
End Class
The mode Class
Mode is the number with the highest occurence in an Array of numbers. This can be multi-modal which means more than one number
Public Class mode
Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As String
Dim numbers() As Decimal = common.ocToArray(items)
Dim map As New Dictionary(Of Decimal, Integer)
For Each f As Decimal In numbers
Dim c As Integer = 0
For x As Integer = 0 To numbers.Length - 1
If f = numbers(x) Then
c += 1
End If
Next x
If Not map.ContainsKey(f) Then
map.Add(f, c)
Else
map(f) = c
End If
Next
Dim highest As Integer = map.Values.Max
Dim counter As Integer = map.Where(Function(kvp) kvp.Value = highest).Count
Dim modeValues As List(Of Decimal) = map.Where(Function(kvp) kvp.Value = highest).Select(Function(kvp) kvp.Key).Distinct.ToList
If counter = items.Count Then
Return "...(All)"
Else
Dim result As String = ""
For Each f As Decimal In modeValues
result &= f.ToString() & ", "
Next f
Return result.Substring(0, result.Length - 2)
End If
End Function
End Class
The range Class
Range is the highest number minus the lowest number in an Array of numbers. In this example, min, max, and range are returned.
Public Class range
Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As Decimal()
Dim numbers() As Decimal = common.ocToArray(items)
Return New Decimal() {numbers(0), numbers(numbers.Length - 1), numbers(numbers.Length - 1) - numbers(0)}
End Function
End Class
The iqr Class
IQR is inter-quartile range, which is calculated like this.
Q1 (first quartile) is the median of the first half of a sorted Array of numbers.
Q3 (third quartile) is the median of the second half of a sorted Array of numbers.
IQR is Q3 - Q1.
In this example, Q1, Q3, and IQR are returned.
Public Class iqr
Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As Decimal()
Dim numbers() As Decimal = common.ocToArray(items)
Dim midPoint As Decimal = common.median(numbers)
Dim firstHalf As New List(Of Decimal)()
Dim secondHalf As New List(Of Decimal)()
For Each v As Decimal In numbers
If v < midPoint Then
firstHalf.Add(v)
ElseIf v > midPoint Then
secondHalf.Add(v)
Else
Dim c As Integer = 0
For x As Integer = 0 To numbers.Length - 1
If numbers(x) = midPoint Then
c += 1
End If
Next x
If c > 1 Then
Dim half As Integer = CInt(Math.Floor(c / 2.0))
Dim firstCount As Integer = 0
For x As Integer = 0 To firstHalf.Count - 1
If firstHalf(x) = v Then
firstCount += 1
End If
Next x
Dim secondCount As Integer = 0
For x As Integer = 0 To secondHalf.Count - 1
If secondHalf(x) = v Then
secondCount += 1
End If
Next x
If firstCount < half Then
firstHalf.Add(v)
ElseIf secondCount < half Then
secondHalf.Add(v)
End If
End If
End If
Next v
Dim Q1 As Decimal = common.median(firstHalf.ToArray)
Dim Q3 As Decimal = common.median(secondHalf.ToArray)
Return New Decimal() {Q1, Q3, Q3 - Q1}
End Function
End Class
The sd Class
Standard Deviation is used to quantify the amount of variation or dispersion of an Array of numbers.
In this example, sample and population StdDev is calculated.
Public Class sd
Public Shared Function getValue(ByVal items As ListBox.ObjectCollection) As Decimal()
Dim numbers() As Decimal = common.ocToArray(items)
Dim mean As Decimal = numbers.Average
Dim squaredDifference(numbers.Length - 1) As Decimal
For x As Integer = 0 To numbers.Length - 1
squaredDifference(x) = CDec(Math.Pow(numbers(x) - mean, 2))
Next x
Return New Decimal() {CDec(Math.Sqrt(minusOneAverage(squaredDifference))), CDec(Math.Sqrt(squaredDifference.Average))}
End Function
Private Shared Function minusOneAverage(ByVal a() As Decimal) As Decimal
If a.Length > 1 Then
Return a.Sum / (a.Length - 1)
Else
Return Nothing
End If
End Function
End Class
The Coordinating Class
In this example a PropertyGrid Control is used for output. To use a PropertyGrid Control, you bind a Class to the Control, which shows the Properties contained in the class. This can be a read/write arrangement, but in this Class, the Properties are all ReadOnly.
This is a coordinating class for the OOP core as well as the Datasource for the PropertyGrid, as there is a Public method (updateValues()) which, when invoked, gathers all of the Statistical Function values and assigns them to the ReadOnly Properties in this Class.
Imports System.ComponentModel
<TypeConverter(GetType(PropertySorter))>
Public Class datasource
Public Sub updateValues(listbox As ListBox)
Dim b As Boolean = listbox.Items.Count > 0
_mean = If(b, mean.getValue(listbox.Items).ToString, "")
_median = If(b, median.getValue(listbox.Items).ToString, "")
_mode = If(b, mode.getValue(listbox.Items).ToString, "")
If b Then
Dim values() As Decimal = range.getValue(listbox.Items)
_min = values(0).ToString
_max = values(1).ToString
_range = values(2).ToString
values = iqr.getValue(listbox.Items)
_q1 = values(0).ToString
_q3 = values(1).ToString
_iqr = values(2).ToString
values = sd.getValue(listbox.Items)
_sample = values(0).ToString
_population = values(1).ToString
Else
_min = ""
_max = ""
_range = ""
_q1 = ""
_q3 = ""
_iqr = ""
_sample = ""
_population = ""
End If
End Sub
Private _mean As String
<SortedCategory("General -", 0, 4), DisplayName("Mean:"), PropertyOrder(0)>
Public ReadOnly Property pmean As String
Get
Return _mean
End Get
End Property
Private _median As String
<SortedCategory("General -", 0, 4), DisplayName("Median:"), PropertyOrder(1)>
Public ReadOnly Property pmedian As String
Get
Return _median
End Get
End Property
Private _mode As String
<SortedCategory("General -", 0, 4), DisplayName("Mode:"), PropertyOrder(2)>
Public ReadOnly Property pmode As String
Get
Return _mode
End Get
End Property
Private _min As String
<SortedCategory("Range -", 1, 4), DisplayName("Min:"), PropertyOrder(3)>
Public ReadOnly Property pmin As String
Get
Return _min
End Get
End Property
Private _max As String
<SortedCategory("Range -", 1, 4), DisplayName("Max:"), PropertyOrder(4)>
Public ReadOnly Property pmax As String
Get
Return _max
End Get
End Property
Private _range As String
<SortedCategory("Range -", 1, 4), DisplayName("Range:"), PropertyOrder(5)>
Public ReadOnly Property prange As String
Get
Return _range
End Get
End Property
Private _q1 As String
<SortedCategory("IQR -", 2, 4), DisplayName("Q1:"), PropertyOrder(6)>
Public ReadOnly Property pq1 As String
Get
Return _q1
End Get
End Property
Private _q3 As String
<SortedCategory("IQR -", 2, 4), DisplayName("Q3:"), PropertyOrder(7)>
Public ReadOnly Property pq3 As String
Get
Return _q3
End Get
End Property
Private _iqr As String
<SortedCategory("IQR -", 2, 4), DisplayName("IQR:"), PropertyOrder(8)>
Public ReadOnly Property piqr As String
Get
Return _iqr
End Get
End Property
Private _sample As String
<SortedCategory("StdDev -", 3, 4), DisplayName("Sample:"), PropertyOrder(9)>
Public ReadOnly Property psample As String
Get
Return _sample
End Get
End Property
Private _population As String
<SortedCategory("StdDev -", 3, 4), DisplayName("Population:"), PropertyOrder(10)>
Public ReadOnly Property ppopulation As String
Get
Return _population
End Get
End Property
End Class
The GUI
The custom Attributes
There are two custom Attributes...
- SortedCategory
- PropertyOrder
The SortedCategoryAttribute
By default, the PropertyGrid sorts Categories alphabetically by name. The only way (in VB2017) to change that sort order is to trick the alphabetical sorter by prepending zero length unprintable characters to the Category display name. SubClassing the CategoryAttribute allows a custom CategoryAttribute to be used with three parameters instead of just the standard categoryKey. These parameters are: categoryKey (a string name), order (zero based) and categoryCount (which is the number of categories used in your class). The Protected GetLocalizedString Function prepends the correct number of zero length unprintable characters according to the input arguments you've provided, resulting in Categories displayed in the order you choose.
Imports System.ComponentModel
Public Class SortedCategoryAttribute
Inherits CategoryAttribute
Const trickString As String = Chr(31) & Chr(32)
Private order As Integer
Private categoryCount As Integer
Public Sub New(ByVal categoryKey As String, order As Integer, categoryCount As Integer)
MyBase.New(categoryKey)
Me.order = order
Me.categoryCount = categoryCount
End Sub
Protected Overrides Function GetLocalizedString(ByVal value As String) As String
Dim x As Integer = Me.categoryCount - Me.order
Dim s As String = ""
While x >= 1
s &= trickString
x -= 1
End While
Return s & value
End Function
End Class
The PropertyOrderAttribute
This is a simple Attribute holding an order value.
<AttributeUsage(AttributeTargets.Property)>
Public Class PropertyOrderAttribute
Inherits Attribute
'
' Simple attribute for a property to allow an order to be specified
'
Public ReadOnly Property Order() As Integer
Public Sub New(ByVal order As Integer)
Me.Order = order
End Sub
End Class
The TypeConverter
Including a PropertySorter means the properties within the categories will retain the order you specify, even when the sort alphabetically button is pressed, and also when returning to a categorised view. Examples of this type of sorter are freely and widely available on the internet.
Imports System.ComponentModel
Public Class PropertySorter
Inherits ExpandableObjectConverter
Public Overrides Function GetPropertiesSupported(ByVal context As ITypeDescriptorContext) As Boolean
Return True
End Function
Public Overrides Function GetProperties(ByVal context As ITypeDescriptorContext, ByVal value As Object, ByVal attributes() As Attribute) As PropertyDescriptorCollection
'
' This override returns a list of properties in order
'
Dim pdc As PropertyDescriptorCollection = TypeDescriptor.GetProperties(value, attributes)
Dim dProperties As New Dictionary(Of String, Integer)
For Each pd As PropertyDescriptor In pdc
Dim attribute As PropertyOrderAttribute = TryCast(pd.Attributes(GetType(PropertyOrderAttribute)), PropertyOrderAttribute)
dProperties.Add(pd.Name, If(Not attribute Is Nothing, attribute.Order, 0))
Next pd
'
' Perform the sorting using LINQ
'
Dim myList As List(Of KeyValuePair(Of String, Integer)) = dProperties.ToList()
myList.Sort(Function(x As KeyValuePair(Of String, Integer), y As KeyValuePair(Of String, Integer))
If x.Value = y.Value Then
Return x.Key.CompareTo(y.Key)
Else
Return x.Value.CompareTo(y.Value)
End If
Return 0
End Function)
'
' Pass in the ordered names for the PropertyDescriptorCollection to sort by
'
Return pdc.Sort(myList.Select(Function(kvp) kvp.Key).ToArray)
End Function
End Class
Conclusion
OOP coding ensures standards are adhered to, resulting in simpler, easily readable code. The .Net IDE offers a variety of Controls to utilize in your applications, which are either easily used or easily coerced to work in useful ways. Sorting a PropertyGrid is a case where a certain amount of persuading is necessary to alter the default sorter, but there are usually easily found solutions in these cases.
Download
More information and download here...