Generierung von Zufallszahlen gemäß einer vorgegebenen diskreten Verteilung Die folgende Fallstudie eignet sich sehr gut zur Einarbeitung in die objektorientierte Programmierung. Es wird zunächst eine Klasse beschrieben, welche diskrete Zufallszahlen generiert. Danach wird gezeigt, wie man diesen Zufallszahlengenerator von einem Formular aus benutzen kann. Zufallszahlen werden häufig für Simulationsanwendungen gebraucht. Beispiel: Simulation eines Geldautomaten zur Optimierung der Füllung und der Auszahlungsstrategie. Man führt hierzu nacheinander eine größere Zahl fiktiver Abhebungen an dem gedachten Automaten durch und protokolliert, wie sich diese Abhebungen auf den Geldbestand und die Zahlungsfähigkeit des Automaten auswirken. Die Abhebungsbeträge werden mit Hilfe eines Zufallszahlengenerators gefunden. Natürlich sollen die generierten Abhebungsbeträge der Verteilung der tatsächlichen Abhebungen entsprechen. Belaufen sich z.b. in der Realität 50% der Abhebungen auf einen Betrag von 300, so sollen auch annähernd 50% der generierten Abhebungen diesem Betrag entsprechen. Man muss also dem Generator eine Häufigkeits- bzw. Wahrscheinlichkeitsverteilung der möglichen Beträge vorgeben können. Die ersten beiden Spalten der unten stehenden Tabelle zeigen, wie eine solche Verteilungsvorgabe aussehen kann. Die erst Spalte enthält die möglichen Werte, die zweite deren relative Häufigkeit bzw. Wahrscheinlichkeit in Prozent. Diese ersten beiden Spalten repräsentieren die sogenannte Wahrscheinlichkeitsfunktion. Die dritte Spalte enthält die kumulierten Wahrscheinlichkeiten, repräsentiert also mit der ersten Spalte zusammen die Verteilungsfunktion ( Summenkurve ). Die Spalten vier und fünf sind lediglich Umformungen der Spalten zwei und drei. Die Prozentangaben sind darin durch Anteilsangaben im Bereich [0, 1] ersetzt. Dies ist für die weitere Argumentation bequemer. Das folgende Bild zeigt die Graphen der Wahrscheinlichkeits- (links) und der Verteilungsfunktion (rechts).
Wie können wir Zufallszahlen generieren, die der dargestellten Verteilung entsprechen? In VBA haben wir, wie in den meisten Programmierumgebungen, nur einen Standard-Zufallszahlengenerator zur Verfügung, welcher gleichverteilte kontinuierliche Zufallszahlen im Bereich [0, 1[ erzeugt. Der diesbezügliche Befehl heißt Rnd (für random). Mit den folgenden Anweisungen können wir einer Variablen eine solche Standard-Zufallszahl zuweisen: Dim r As Double r = Rnd Das folgende Bild veranschaulicht das Prinzip, mit dem wir gleichverteilte kontinuierliche Zufallszahlen im Bereich [0, 1[ in diskrete Zufallszahlen mit der gewünschten Verteilung verwandeln können. Wir teilen hierfür den Wertebereich [0, 1[ der Standard-Zufallszahl in Teilbereiche auf, welche den Wahrscheinlichkeiten der Werte der gewünschten Verteilung entsprechen. Dabei beginnen wir mit dem kleinsten Wert. Dieser ist hier 50, seine Wahrscheinlichkeit beträgt 0,1 bzw. 10%. Die übrigen Werte werden in derselben Weise angefügt. Das so erzeugte Raster ist das Vehikel zur Umwandlung in die diskreten Werte. Generieren wir eine größere Menge von Standard-Zufallszahlen, so werden sich rund 10 % davon zwischen 0 und 0,1 befinden, also im Bereich des Werts 50, weitere 20% im Bereich des Werts 100, weitere 10% im Bereich des Werts 200 und so fort. Wenn wir also jeder Standard-Zufallszahl den diskreten Wert zuordnen, der dem Bereich entspricht, in den sie fällt, so erhalten wir damit Zufallszahlen der gewünschten diskreten Verteilung. Hierzu ein Beispiel: Eine mit Rnd erzeugte Standard-Zufallszahl sei 0.34592. Da dieser Wert sich im Bereich [0.3, 0.4[ des diskreten Werts 200 befindet, leiten wir davon die Zahl 200 ab. Die Berechnung dieses Umformungsraster macht keinen großen Aufwand, denn es ergibt sich aus den Ordinatenwerten der oben dargestellten Verteilungsfunktion. Diese bilden die Grenzen der Teilbereiche, die wir als Basis für die Umwandlung brauchen. Zur Verdeutlichung ist im folgenden Bild noch einmal die Verteilungsfunktion abgebildet, nun aber mit eingezeichneten Bereichsgrenzen.
Die Klasse DiscrDistGenerator (Zufallsgenerator) Die Klasse beginnt mit der Deklaration einer Instanzenvariablen s. Sie soll, in tabellarischer Form, die Verteilungsfunktion aufnehmen, welche der Generierung der Zufallszahlen zugrunde gelegt wird (s. oben). Es folgen dann drei Methoden; davon sind die ersten beiden die wichtigsten. Die Methode setdist nimmt ein Array entgegen, das die Wahrscheinlichkeitsfunktion der gewünschten Verteilung, also die möglichen Werte und ihre Wahrscheinlichkeiten, in tabellarischer Form enthält. Die Wahrscheinlichkeiten sind darin in % ausgedrückt. In setdist wird aus dieser Wahrscheinlichkeitsfunktion die Verteilungsfunktion abgeleitet und in der Instanzenvariablen s abgelegt. Methode rdist erzeugt einen einzigen Zufallswert gemäß der in s enthaltenen Verteilung auf der Basis einer gleichverteilten Standardzufallszahl runi, welche als Parameter übergeben wird. Wie oben ausgeführt, muss hierfür der Teilbereich innerhalb des Gesamtbereichs [0, 1[ identifiziert werden, in den runi fällt. Man könnte runi auch direkt innerhalb der Methode mit dem Befehl Rnd erzeugen, jedoch eröffnet die gewählte Lösung mit der Parameterübergabe die Möglichkeit, beliebige Zufallsgeneratoren zur Erzeugung von runi einzusetzen. Die Methode nvalues dient lediglich der Bequemlichkeit der Benutzer der Klasse. Sie erzeugt ein Array mit einer vorgegebenen Anzahl von Zufallszahlen. Hierfür wird die Methode rdist zur Erzeugung der einzelnen Zahlen aufgerufen. Option Explicit Private s() As Double 'Summenkurve (Verteilungsfunktion) 'aus der Verteilung v die Summenkurve s ableiten Public Sub setdist(byref v() As Double) Dim i As Integer ReDim s(1 To UBound(v, 1), 1 To 2) s(1, 1) = v(1, 1) s(1, 2) = v(1, 2) / 100 For i = 2 To UBound(s, 1) s(i, 1) = v(i, 1)
s(i, 2) = s(i - 1, 2) + v(i, 2) / 100 End Sub 'aus der gleichverteilten Zufallszahl runi einen Zufallswert ableiten Public Function rdist(byval runi As Double) As Double Dim i As Integer rdist = s(1, 1) For i = 2 To UBound(s, 1) If runi >= s(i - 1, 2) And runi < s(i, 2) Then rdist = s(i, 1) Exit For End If End Function 'ein Array mit n Zufallswerten generieren Public Function nvalues(byval n As Integer) As Double() Dim v() As Double ReDim v(1 To n, 1 To 1) Dim i As Integer For i = 1 To n v(i, 1) = rdist(rnd) nvalues = v End Function Eine Beispielanwendung, die den Zufallszahlengenerator verwendet In der folgenden Anwendung wird ein kleines Formular benutzt, um eine bestimmte, vom Benutzer vorzugebende Anzahl von Zufallszahlen zu erzeugen und auf einem Arbeitsblatt auszugeben (s. folgendes Bild). Die Wahrscheinlichkeitsfunktion der gewünschten Verteilung, bestehend aus den möglichen Werten und ihren in Prozent ausgedrückten Wahrscheinlichkeiten, muss vorher in tabellarischer Form eingegeben worden sein. Das Formular sollte mit Hilfe einer Prozedur in nichtmodaler Form gestartet werden. Nach dem Öffnen des Formulars gibt der Benutzer den Bereich des Arbeitsblatts ein, auf dem sich die Wahrscheinlichkeitsfunktion befindet. Zum Einlesen wird ein RefEdit-Steuerelement benutzt. Wurde bereits vor dem Starten der Maske dieser Bereich markiert, so ist das Steuerelement nach dem Start bereits mit diesem Bereich gefüllt. Die Anzahl der Zufallszahlen wird vom Benutzer mit Hilfe eines Auswahlfelds (ComboBox) festgelegt. Zur Auswahl stehen die Werte 10, 50, 100 und 1000. Danach wird die Erzeugung der Zahlen durch Anklicken der Schaltfläche <Zufallswerte generieren> gestartet. Fehler bei der Eingabe können lediglich beim RefEdit-Steuerelement vorkommen. Falls dieses Feld keine verwertbare Bereichsangabe enthält, wird eine entsprechende Fehlermeldung in einer MsgBox ausgegeben. Der Benutzer hat danach die Möglichkeit, seine Eingaben zu korrigieren.
Wir betrachten nun die Formularklasse ZZGeneratorForm. Die Instanzenvariable gen der oben beschriebenen Klasse DiscrDistGenerator wird innerhalb der Methode UserForm_Initialize mit einem Objekt dieser Klasse besetzt und steht danach der Methode GenerierenBtn_Click zur Verfügung. Diese leistet die Hauptarbeit, indem sie zunächst den im RefEdit-Steuerelement genannten Bereich liest und mit Hilfe der Hilfsfunktion RangeToDoubleMatrix in ein Array v umwandelt. Dieses Array wird dann dem Generator-Objekt übergeben, so dass dieses die in v enthaltene Verteilungsfunktion bei der Erzeugung der Zufallszahlen berücksichtigen kann. Anschließend werden die Zufallszahlen mit Hilfe der Methode nvalues vom Generator abgerufen und in das Arbeitsblatt eingestellt. Option Explicit Private gen As DiscrDistGenerator 'Aktionen beim Öffnen des Formulars Private Sub UserForm_Initialize() Set gen = New DiscrDistGenerator Me.AnzahlCBx.AddItem ("10") Me.AnzahlCBx.AddItem ("50") Me.AnzahlCBx.AddItem ("100") Me.AnzahlCBx.AddItem ("1000") Me.AnzahlCBx.Value = "50" Me.DistrREd.Text = ActiveWindow.RangeSelection.Address End Sub 'Aktionen nach Anklicken des Buttons <Generieren> Private Sub GenerierenBtn_Click() Dim rng As Range 'für die Verteilung Dim v() As Double 'für die Verteilung Dim z() As Double Dim i As Integer, n As Integer
'dem Generator die Verteilung übergeben On Error GoTo errorhandler Set rng = Range(Me.DistrREd.Text) v = RangeToDoubleMatrix(rng) gen.setdist v 'die Zufallswerte abrufen und ausgeben n = CInt(Me.AnzahlCBx.Text) z = gen.nvalues(n) Worksheets("Tabelle1").Range("E:E").Clear Worksheets("Tabelle1").Range("E2:E" & (n + 1)) = z Exit Sub errorhandler: MsgBox "Bitte den Bereich mit der Verteilung eingeben" Me.DistrREd.SetFocus End Sub 'Umwandlung eines Range in ein 2-dimensionales Array Public Function RangeToDoubleMatrix(ByVal rng As Range) As Double() Dim m() As Double Dim i As Integer, j As Integer ReDim m(1 To rng.cells.rows.count, 1 To rng.cells.columns.count) For i = 1 To UBound(m, 1) For j = 1 To UBound(m, 2) m(i, j) = CDbl(rng.Cells(i, j).value) Next j RangeToDoubleMatrix = m End Function Beachten Sie die Validierung der Bereichseingabe in das Steuerelement DistrREd. Eingaben dieser Art sind sehr schwer zu überprüfen. Man kann sich in solchen Fällen behelfen, indem man die Anweisung On Error GoTo vor die fehlerträchtigen Anweisungen setzt. Tritt dann tatsächlich ein Fehler auf, so wird die Marke angesprungen, welche nach dem GoTo genannt ist, hier also errorhandler. Dort erfolgt dann die Fehlerbehandlung.