다음을 통해 공유


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

Download here...

More information and download here...