Rubrik: Variablen/Strings · Algorithmen/Mathematik | VB-Versionen: VB2008 | 20.02.09 |
Berechnung von Perzentilwerten einer Datenreihe Erweiterungsmethode für numerische System.Arrays zur Bestimmung der Perzentil-Werte einer Datenreihe. | ||
Autor: Manfred Bohn | Bewertung: | Views: 13.904 |
ohne Homepage | System: Win2k, WinXP, Win7, Win8, Win10, Win11 | kein Beispielprojekt |
Die Definition der Perzentile entnimmt man einem Statistik-Lehrbuch:
Das P-te Perzentil einer aufsteigend geordneten Folge von N Meßwerten ist der Wert mit der Rangnummer P*(N+1)/100 oder - wenn diese Rangnummer nicht ganzzahlig ist, der (linear) interpolierte Wert zwischen den in der Rangreihe benachbarten Werten, unterhalb dem P % aller Meßwerte liegen.
Der Wert des Perzentils 75 (=P75) gibt an, dass in der ausgewerteten Datenreihe 25% der Werte größer oder gleich diesem Wert sind.
Perzentil-Werte werden z.B. verwendet, um robuste (d.h. von einzelnen Ausreißer-Werten in den Daten wenig beeinflußbare) statistische Kennwerte zu bilden.
Einige Details zum Code:
Die generische Array-Erweiterungsmethode 'Percentil_Val' ermittelt zu einer Datenreihe (Parameter: Daten) den geforderten Perzentil-Wert (Parameter: Percentil). Der zu beachtende Streubereich der Datenausprägungen kann optional eingeschränkt werden (Parameter: MinValue, MaxValue).
Die Methode verarbeitet Arrays, deren Elemente 'Primitives' sind, sowie den präzisen Gleitkomma-Typ 'Decimal'. Bei IEEE-Daten (Single, Double) werden Sonderwerte gefiltert. Es müssen mindestens 10 plausible Daten vorliegen. Die Funktion gibt den Perzentil-Wert im Datentyp des Parameter-Arrays zurück.
Die Routine löst ggf. Ausnahmen aus und sollte innerhalb von Try/Catch-Blöcken verwendet werden.
Das Filtern und Sortieren der Daten erfolgt durch eine LINQ-Abfrage unter Verwendung der 'ToArray'-Erweiterung, die die erzeugte 'In-Memory-Query' in ein Array überträgt (Hilfsfunktion: 'FilterAndSort').
Einige Details zu VB:
Soweit Berechnungen (Interpolation) notwendig sind, werden sie unter Verwendung des Datentyps Double durchgeführt. Die erforderlichen Konvertierungen erledigen die HilfsRoutinen 'GetDouble' (Function) und 'GetEntry' (SUB) - unter Verwendung der ChangeType-Methode der System.Convert-Klasse. Beim nicht-primitiven Datentyp 'Decimal' ist das evt. mit einem (vernachlässigbaren) Genauigkeitsverlust der Interpolation verbunden.
Um bei den optionalen Double-Parametern erkennen zu können, ob der Benutzer eine Vorgabe gemacht hat, ist die Voreinstellung 'NaN' verwendet worden. Die Voreinstellung 'Nothing' übergibt nämlich eine 0. Null-fähige Parameter (z.B. Double?) können nicht optional sein.
Man beachte, dass die Math-Routinen 'Floor' und 'Ceiling' zwar Ganzzahlen bestimmen, aber diesen Wert dann als 'Double' zurückgeben.
Man beachte auch, dass bei der Array-Übergabe 'ByVal' zwar eine lokale Array-Variable gebildet wird, die aber auf die Elemente (stets Referenzen!) des externen Array zeigt. Auch Werttypen müssen deshalb bei Bedarf explizit lokal gemacht werden (hier: durch die LINQ-Abfrage).
Anwendungsbeispiele:
Public Sub Percentil_Demo() ' Integer-Array Dim int_arr(777) As Integer int_arr.Fill_Array() ' 95er Perzentil Dim Pz95 As Integer = int_arr.Percentil_Val(95) ' Single-Array mit einigen Sonderwerten Dim sng_arr(349) As Single sng_arr.Fill_Array() sng_arr(90) = Single.NaN sng_arr(100) = Single.NegativeInfinity sng_arr(55) = Single.PositiveInfinity sng_arr(6) = Single.MaxValue ' nur Werte zwischen -5000 und 0 werden einbezogen Dim Perz50 As Single = sng_arr.Percentil_Val(50, -5000, 0) ' Decimal-Array erstellen und füllen Dim decarr(2590) As Decimal decarr.Fill_Array() ' 2 Dezile Dim P10 As Decimal = decarr.Percentil_Val(10) Dim P90 As Decimal = decarr.Percentil_Val(90) ' Quartile Dim Q1 As Decimal = decarr.Percentil_Val(25) Dim Q2 As Decimal = decarr.Percentil_Val(50) Dim Q3 As Decimal = decarr.Percentil_Val(75) ' Aus den Perzentilen abgeleitete Kennwerte: ' Zentrale Tendenz der Daten Dim Median As Decimal = Q2 ' bei Berechnung des getrimmten Mittelwertes werden nur ' die mittleren Daten der sortierten Meßreihe verwendet Dim Getrimmter_Mittelwert As Decimal = _ decarr.FilterAndSort(P10, P90).Average ' Linq-Erweiterung ' Streuungsbereich der Daten Dim Quartilabstand As Decimal = Q3 - Q2 Dim DezilDifferenz As Decimal = P90 - P10 ' zum Vergleich: Dim Spannweite As Decimal = decarr.Max - decarr.Min ' Schiefe der Datenverteilung Dim Schiefe As Decimal = _ (P90 - 2 * Median + P10) / DezilDifferenz ' Exzess der Datenverteilung - erst sinnvoll ab ca. N=100 ' (relativ zur Normalverteilung) Dim Exzess As Decimal = _ CDec(0.263) - Quartilabstand / DezilDifferenz End Sub
Das Modul mit der Erweiterungsmethode und den Hilfsfunktionen:
Public Module modPerzentile ''' <summary> ''' Berechnung des Perzentils einer Verteilung ''' </summary> ''' <typeparam name="T">EntryType (Primitive)</typeparam> ''' <param name="Daten">Array mit Daten</param> ''' <param name="Percentil">Perzentil (von 1 bis 99)</param> ''' <param name="MinValue">optional: kleinster gültiger Wert</param> ''' <param name="MaxValue">optional: größter gültiger Wert</param> ''' <returns>Perzentil-Wert</returns> <Runtime.CompilerServices.Extension()> _ Public Function Percentil_Val(Of T As Structure) ( _ ByVal Daten() As T, ByVal Percentil As T, _ Optional ByVal MinValue As Double = Double.NaN, _ Optional ByVal MaxValue As Double = Double.NaN) As T ' Entry-Parameter in Double wandeln und prüfen Dim iPercentil As Double = GetDouble(Percentil) If iPercentil < 1 Or iPercentil > 99 Then Throw New ArgumentException _ ("Percentil-Parameter: nicht zwischen 1 und 99") End If ' Array filtern und sortieren Dim iDaten() As T = FilterAndSort(Daten, MinValue, MaxValue) ' Perzentil bestimmen: Anzahl plausibler Daten Dim N As Integer = iDaten.Length ' Perzentil-Rang nach Formel berechnen Dim RN As Double = iPercentil * (N + 1) / 100 ' Benachbarte reale Ränge ermitteln Dim RNU As Integer = CInt(Math.Max(Math.Floor(RN), 1)) Dim RNO As Integer = CInt(Math.Min(Math.Ceiling(RN), N)) ' Daten an der Position dieser Ränge lesen ' --> nullbasiertes Array !! Dim elem_rnu As T = iDaten(RNU - 1) Dim elem_rno As T = iDaten(RNO - 1) ' Double-Werte für Berechnungen und Vergleich Dim dbl_rnu As Double = GetDouble(elem_rnu) Dim dbl_rno As Double = GetDouble(elem_rno) ' Ist Interpolation erforderlich? If dbl_rnu < dbl_rno Then ' Lineare Interpolation auf Basis 'Double' Dim dbl_interpol As Double = dbl_rnu + _ (RN - RNU) / (RNO - RNU) * (dbl_rno - dbl_rnu) GetEntry(dbl_interpol, elem_rnu) End If ' Rückgabe des Perzentil-Wertes Return elem_rnu End Function
''' <summary>Filtert/Sortiert Daten in einem 1D-Array ''' (mindestens 10 plausible Daten)</summary> ''' <typeparam name="T">Typ: Primitive/Decimal</typeparam> ''' <param name="arr">Daten-Array</param> ''' <param name="MinValue">Untergrenze plausibler Werte</param> ''' <param name="MaxValue">Obergrenze plausibler Werte</param> ''' <returns>bearbeitetes Array</returns> <Runtime.CompilerServices.Extension()> _ Public Function FilterAndSort(Of T As Structure) ( _ ByVal arr() As T, _ Optional ByVal MinValue As Double = Double.NaN, _ Optional ByVal MaxValue As Double = Double.NaN) As T() ' Die generische Funktion checkt den Arraytyp, lokalisiert ' die Arrayelemente, entfernt bei IEEE-Typen ggf. die ' Sonderwerte, bearbeitet die optionalen Limits ' und sortiert die Rest-Daten im Array ' erst mal prüfen ... Check_Array(arr) ' optional vorgegebene Filter einrichten ' ggf. die IEEE-Sonderwerte ausfiltern (auch Single!) If Double.IsNaN(MinValue) Then MinValue = Double.MinValue If Double.IsNaN(MaxValue) Then MaxValue = Double.MaxValue ' Linq-Abfrage der Daten: Filtern & Sortieren Dim iarr(arr.Length - 1) As T iarr = (From elem As T In arr _ Let dbl As Double = GetDouble(elem) _ Where dbl >= MinValue And dbl <= MaxValue _ Order By elem Ascending _ Select elem).ToArray ' Bleiben genügend Daten übrig? If iarr.Length < 10 Then Throw New ArgumentException _ ("Filter: Weniger als 10 verwertbare Daten") End If ' Rückgabe des gefilterten und sortierten Array Return iarr End Function
Private Function GetDouble(Of T)(ByVal elem As T) As Double ' Ein Entry-Wert wir in einen Double-Wert gewandelt Return CType(Convert.ChangeType(elem, _ TypeCode.Double), Double) End Function
Private Sub GetEntry(Of T)(ByVal elem As Double, _ ByRef Entry As T) ' Ein Double-Wert wird in einen Entry-Wert gewandelt Entry = CType(Convert.ChangeType(elem, GetType(T)), T) End Sub
Private Sub Check_Array(Of T)(ByVal arr() As T) ' Routine überprüft Vorhandensein, Datentyp und ' Zahl der Array-Elemente Const dec_str As String = "System.Decimal" If arr Is Nothing Then Throw New ArgumentNullException _ ("Array-Parameter: Nothing") End If If Not GetType(T).IsPrimitive And _ Not GetType(T).ToString = dec_str Then Throw New ArgumentException _ ("Array-Elemente: keine Primitives bzw. Decimals") End If If arr.Length < 10 Then Throw New ArgumentException _ ("Array-Check: Weniger als 10 Elemente") End If End Sub
''' <summary>Hilfsfunktion: ''' Füllt ein num.Array mit Zufallszahlen (-/+10000)</summary> ''' <typeparam name="T">Datentyp</typeparam> ''' <param name="arr">zu füllendes Array</param> <System.Runtime.CompilerServices.Extension()> _ Public Sub Fill_Array(Of T)(ByRef arr As T()) For i As Integer = 0 To arr.GetUpperBound(0) arr(i) = CType(Convert.ChangeType(RandomNumber, GetType(T)), T) Next i End Sub
Private Function RandomNumber() As Double Dim r As Double = 10000.0# * Microsoft.VisualBasic.Rnd If Microsoft.VisualBasic.Rnd < 0.5 Then r *= -1 Return r End Function
End Module