Viele Programme brauchen (scheinbar) zufällige Werte, um Ereignisse aus der realen Welt zu simulieren.
Häufige und bekannte Beispiele sind:
('H', 'T').Wirklich zufällige Werte mit einem Computer zu erzeugen, ist eine überraschend schwierige technische Herausforderung, deshalb werden solche Ergebnisse oft als „pseudozufällig" bezeichnet.
Wichtig: Dieses Konzept behandelt nicht kryptografisch sichere Zufallszahlen, die eine viel schwierigere Herausforderung darstellen.
Gut entworfene Bibliotheken wie das Modul Random in der Julia-Standardbibliothek sind jedoch schnell, flexibel und liefern Ergebnisse, die für die meisten Anwendungen in Modellierung, Simulation und Spielen mehr als gut genug sind.
Julia verteilt die Zufallsfunktionalität auf mehrere Orte:
Base, die immer verfügbar sind.Random.Random ist Teil der Standardbibliothek und wahrscheinlich vorinstalliert, aber du musst using Random oben in dein Programm schreiben, um seine Inhalte in den Namensraum zu holen.
rand()
Was diese Funktion tut, hängt von den Argumenten ab, die du ihr gibst. Es gibt viele Möglichkeiten.
Ohne Argumente erzeugt sie eine Gleitkommazahl zwischen 0 (einschließlich) und 1.
Das ist eine uniform Verteilung, bei der alle Werte gleich wahrscheinlich sind, wie im Abschnitt „Arbeiten mit Verteilungen" weiter unten beschrieben.
Ein einzelnes Ganzzahl-Argument erzeugt einen Vektor dieser Länge.
julia> rand()
0.10261774967264703
julia> rand(5)
5-element Vector{Float64}:
0.24134501977563894
0.5664193284851202
0.9804412082089355
0.6229551330613335
0.47589221741904664
Für einen anderen Bereich verschiebst und skalierst du das Ergebnis einfach passend.
Das folgende Beispiel verwendet Broadcasting für die Subtraktion, was im Konzept Vector Operations behandelt wird.
Der Operator .- wendet diese Arithmetik einfach auf jedes Vektorelement an.
# numbers between -1.0 and +1.0
julia> (rand(5) .- 0.5) * 2
5-element Vector{Float64}:
-0.5303906759076336
0.9635682226775855
-0.048823697086981754
0.465842804648374
0.9880834344780736
Wenn du nur einen Typ als Argument übergibst, verwendet rand typemin und typemax als Grenzen.
Das ist wahrscheinlich nicht das, was du willst!
Für zufällige Ganzzahlen können wir einen Bereich angeben und optional dazu, wie viele Werte erzeugt werden sollen.
julia> rand(Int64)
-9159538335234594326 # not very useful
julia> rand(1:10, 5)
5-element Vector{Int64}:
1
1
1
4
7
Im obigen Beispiel rand(1:10, 5) fällt auf, dass es (zufällig) wiederholte Werte gibt, weil jede Auswahl unabhängig ist.
Das ist das „Ziehen mit Zurücklegen", worauf ich weiter unten genauer eingehe.
Für Gleitkommawerte in einem Bereich musst du normalerweise eine Schrittweite angeben. Andernfalls ist die Schrittweite standardmäßig 1.0, was selten nützlich ist.
julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
3.19
2.53
3.14
3.13
Alternativ übergibst du ein Array oder Tupel, und rand gibt einen zufälligen Eintrag zurück:
julia> rand([4, 9, 16, 25])
16
# coin flip
julia> rand(['H', 'T'])
'H': ASCII/Unicode U+0048 (category Lu: Letter, uppercase)
# mixed types in tuple
julia> rand( (1, 3.2, "name"), 2 )
2-element Vector{Any}:
1
"name"
Stell dir vor, wir haben einen Beutel mit 3 roten und 4 grünen Kugeln und ziehen zufällig eine Kugel aus dem Beutel. Für eine zweite Kugel gibt es zwei Möglichkeiten:
Szenario 1 ist mit Zurücklegen, Szenario 2 ohne, und sie liefern unterschiedliche Ergebnisse.
Um das Ziehen ohne Zurücklegen in Julia zu simulieren, gibt es ein paar Möglichkeiten.
Am einfachsten (und innerhalb von Exercism die einzige Möglichkeit) verwendest du Random.shuffle(), um die Einträge in eine zufällige Reihenfolge zu bringen, und nimmst dann die ersten n Elemente.
Das ist für kleine Probleme in Ordnung, skaliert aber möglicherweise nicht gut auf große Sammlungen: shuffle muss das gesamte Array erzeugen, selbst wenn du nur einen kleinen Teil davon willst.
Um das Ziehen mit Zurücklegen „richtig" zu machen, installierst du das Paket StatsBase.jl.
Das stellt die Funktion sample() mit einer ganzen Reihe von Optionen bereit.
Wir können berechtigterweise hoffen, dass ähnliche Funktionalität in einer zukünftigen Version zu Random hinzugefügt wird, um sie zu einem Teil der Standardbibliothek zu machen (die Codebeispiele in diesem Dokument wurden mit Julia 1.11 getestet).
Bisher haben wir uns auf Fälle konzentriert, in denen alle Ergebnisse gleich wahrscheinlich sind.
Zum Beispiel liefert rand(1:100) mit gleicher Wahrscheinlichkeit jede Ganzzahl von 1 bis 100.
Viele Situationen aus der realen Welt sind weit weniger einfach als diese.
Deshalb haben Statistiker eine große Vielfalt von distributions entwickelt, um „reale" Ergebnisse mathematisch zu beschreiben.
Die oben beschriebene Funktion rand() wird verwendet, wenn alle Wahrscheinlichkeiten gleich sind.
Man nennt das eine [uniform][uniform-distribution] Verteilung.
Sie wird auch „Normalverteilung" oder „Glockenkurve" genannt und ist eine sehr verbreitete Art, Ungenauigkeiten bei gemessenen Werten zu beschreiben.
Angenommen, die Fabrik, in der du arbeitest, hat gerade 10.000 Schrauben gekauft, die eigentlich identisch sein sollten.
Du willst den Fabrikroboter so einrichten, dass er sie handhabt, also wiegst du eine Stichprobe von 100 Stück und stellst fest, dass sie ein durchschnittliches (oder mean) Gewicht von 4,731 g haben.
Das bedeutet höchst unwahrscheinlich, dass sie alle genau 4,731 g wiegen.
Vielleicht stellst du fest, dass die Werte von 4,627 bis 4,794 g reichen, aber um 4,731 g herum liegen.
Das ist die Gaussian distribution, bei der die Wahrscheinlichkeiten am Mittelwert ihren Höhepunkt haben und zu beiden Seiten symmetrisch abflachen (daher „glockenförmig").
Um das in Software zu simulieren, brauchen wir eine Möglichkeit, die Breite der Kurve anzugeben (typischerweise liegen teure Schrauben enger um den Mittelwert als billige!).
Üblicherweise geschieht das mit der standard deviation: kleine Werte für eine scharfe, schmale Kurve, große für eine flache, breite Kurve.
Mathematiker lieben griechische Buchstaben, also verwenden wir μ („mu") für den Mittelwert und σ („sigma") für die Standardabweichung.
Wenn du also liest, dass „95 % der Werte innerhalb von 2σ um μ liegen" oder „das Higgs-Boson wurde mit 5-Sigma-Konfidenz nachgewiesen", dann beziehen sich solche Aussagen auf die Standardabweichung.
Mehr dazu gibt es im Konzept Statistics.
randn()
Kurz für „random normal", ähnelt sie der Gleitkomma-Variante von rand(), nur dass die Werte als Gauß-Verteilung mit Mittelwert 0 und Standardabweichung 1 verteilt sind.
Auch hier willst du die Rohausgabe von randn vielleicht für die Standardabweichung skalieren und für den Mittelwert verschieben.
Das folgende Beispiel rechnet auf Mittelwert 30 und Standardabweichung 5 um.
julia> raw = randn(5)
5-element Vector{Float64}:
3.0762588867281475
1.5101100620253902
-0.5914858221637778
0.684175554069735
-0.8416433926114673
julia> raw * 5 .+ 30
5-element Vector{Float64}:
45.38129443364074
37.55055031012695
27.04257088918111
33.420877770348675
25.791783036942665
An der Ausgabe ist schwer zu erkennen, dass die Rohausgabe näher bei null liegt als bei einer uniformen Verteilung. Wenn du Zweifel hast, erzeuge 1000 oder mehr und zeichne sie auf, damit es deutlicher wird.
Random
Dieses Modul enthält die nächste Stufe an Funktionalität, die aus Base ausgelassen wurde, um die Größe von Julias Standardkonfiguration möglichst gering zu halten.
Random ergänzt rand und randn in Base um mutierende Versionen, rand! und randn!.
Eine nützliche Ergänzung ist randstring, die einen String gegebener Länge erzeugt.
Standardmäßig verwendet sie Groß- und Kleinbuchstaben sowie die Ziffern 0 bis 9, aber andere Auswahlen lassen sich festlegen.
julia> using Random
julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"
Außerdem gibt es eine Funktion bitrand, um ein zufälliges BitArray gegebener Länge zu erzeugen.
julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
1
1
0
0
1
Um Einträge in einem Vector zufällig zu mischen, gibt es shuffle; außerdem shuffle!, um den Eingabevektor direkt zu verändern.
julia> v = ['A', '1', '2', 'J', 'Q', 'K'];
julia> shuffle(v)
6-element Vector{Char}:
'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
'1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
'2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
# shuffles are random:
julia> shuffle(v)
6-element Vector{Char}:
'2': ASCII/Unicode U+0032 (category Nd: Number, decimal digit)
'K': ASCII/Unicode U+004B (category Lu: Letter, uppercase)
'A': ASCII/Unicode U+0041 (category Lu: Letter, uppercase)
'Q': ASCII/Unicode U+0051 (category Lu: Letter, uppercase)
'J': ASCII/Unicode U+004A (category Lu: Letter, uppercase)
'1': ASCII/Unicode U+0031 (category Nd: Number, decimal digit)
Manchmal ist es nützlich, stattdessen die gemischten Indizes zu haben.
Dafür verwendest du randperm(n), wobei n die Länge der Sequenz ist.
julia> randperm(6)
6-element Vector{Int64}:
6
2
4
1
3
5
Tatsächlich liefert das obige Beispiel dieselben Ergebnisse wie shuffle(1:6).
Verwandte Funktionen sind randsubseq, um Einträge mit fester Wahrscheinlichkeit herauszuziehen, und randcycle für zyklische Permutationen.
Diese erfordern etwas Spezialwissen, also schau in die Dokumentation, wenn sie dich interessieren.
Mehrere Algorithmen für Zufallszahlengeneratoren (RNG) sind standardmäßig in Random eingebaut, und jeder mit den passenden mathematischen Fähigkeiten kann weitere hinzufügen.
Solche Dinge gehen weit über den Rahmen dieses Dokuments hinaus!
Ein häufigerer Grund, mit RNGs zu arbeiten, ist die Angabe eines seed, was bewirkt, dass die Folge der „zufälligen" Ausgaben von einem Lauf zum nächsten reproduzierbar ist.
Solche Reproduzierbarkeit ist in Produktionscode nicht angebracht, kann aber beim Testen und Debuggen helfen.
Außerhalb von Exercism gibt es viele installierbare Pakete rund um Zufall, Wahrscheinlichkeit und Statistik.
Weitere Informationen findest du im Konzept Statistics.
StatsBase.jl
Die meisten Funktionen aus StatsBase sind ziemlich technisch und für dieses Dokument nicht relevant.
Die Ausnahme ist StatsBase.sample, das eine vollständige Umsetzung des Ziehens mit oder ohne Zurücklegen bietet (siehe einen weiter oben stehenden Abschnitt).
Es gibt auch Funktionen für gewichtetes (nicht uniformes) Ziehen.
Distributions.jl
Die Verteilungen uniform und normal (oder Gauß) wurden oben beschrieben.
Das Modul Random enthält außerdem randexp, um aus der Exponentialverteilung zu ziehen, die mit der (sehr verbreiteten) Poisson-Verteilung verwandt ist.
Für eine viel größere Auswahl an Möglichkeiten gibt es das Paket Distributions.jl für alle mit einem passenden statistischen Hintergrund.