Track
/
Julia
Julia
/
Esercizi
/
Diario del capitano
Diario del capitano

Diario del capitano

Esercizio di apprendimento

Introduzione

Molti programmi hanno bisogno di valori (apparentemente) casuali per simulare eventi del mondo reale.

Tra gli esempi comuni e familiari ci sono:

  • Il lancio di una moneta: un valore casuale da ('H', 'T').
  • Il lancio di un dado: un numero intero casuale da 1 a 6.
  • Mescolare un mazzo di carte: un ordinamento casuale di un array di carte.

Generare valori davvero casuali con un computer è una sfida tecnica sorprendentemente difficile, perciò potresti vedere questi risultati indicati come «pseudocasuali».

Importante: questo concetto non copre i numeri casuali crittograficamente sicuri, che sono una sfida molto più difficile.

Tuttavia, librerie ben progettate come il modulo Random della libreria standard di Julia sono veloci, flessibili e producono risultati più che sufficienti per la maggior parte delle applicazioni di modellazione, simulazione e giochi.

Julia suddivide la funzionalità casuale in più punti:

  • Solo poche funzioni di base ma molto versatili in Base, sempre disponibili.
  • Una gamma più ampia di opzioni nel modulo Random.
  • Funzionalità più specializzate in pacchetti che devono essere installati prima dell'uso (e che non sono disponibili in Exercism).

Random fa parte della libreria standard ed è probabilmente già installato, ma dovrai aggiungere using Random all'inizio del programma per portarne il contenuto nel namespace.

La funzione rand()

Ciò che questa funzione fa dipende dagli argomenti che le passi. Le opzioni sono molte.

Senza argomenti, genera un numero in virgola mobile tra 0 (incluso) e 1. Si tratta di una distribuzione uniform in cui tutti i valori hanno la stessa probabilità, come vedremo nella sezione Lavorare con le distribuzioni, più avanti.

Un singolo argomento intero genera un array di quella lunghezza.

julia> rand()
0.10261774967264703

julia> rand(5)
5-element Vector{Float64}:
 0.24134501977563894
 0.5664193284851202
 0.9804412082089355
 0.6229551330613335
 0.47589221741904664

Per un intervallo diverso, basta traslare e riscalare il risultato in modo opportuno.

L'esempio seguente usa il broadcasting per la sottrazione, trattato nel concetto Operazioni sugli array. Il .- applica semplicemente questa aritmetica a ogni elemento dell'array.

# 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

Con un tipo come unico argomento, rand userà typemin e typemax come limiti. Probabilmente non è quello che vuoi!

Per numeri interi casuali, possiamo fornire un intervallo e, facoltativamente, quanti valori generare.

julia> rand(Int64)
-9159538335234594326 # not very useful

julia> rand(1:10, 5)
5-element Vector{Int64}:
 1
 1
 1
 4
 7

Nell'esempio rand(1:10, 5) qui sopra, nota che ci sono valori ripetuti (per coincidenza), perché ogni estrazione è indipendente. Questo è il «campionamento con reimmissione», di cui parleremo più in dettaglio qui sotto.

Per valori in virgola mobile in un intervallo, di solito dovrai specificare un passo. Altrimenti il passo sarà 1.0 per impostazione predefinita, cosa raramente utile.

julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
 3.19
 2.53
 3.14
 3.13

In alternativa, fornisci un array o una tupla e rand restituirà una voce casuale:

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"

Campionamento con o senza reimmissione

Immagina di avere un sacchetto con 3 palline rosse e 4 palline verdi, e di estrarre a caso una pallina dal sacchetto. Per ottenere una seconda pallina, ci sono due possibilità:

  1. Rimetti la prima pallina nel sacchetto e scuoti bene tutto prima di estrarne un'altra. Il numero di palline ora è lo stesso di prima (7), e anche il rapporto tra rosse e verdi resta lo stesso.
  2. Metti la prima pallina sul tavolo prima di estrarne una seconda. Ora nel sacchetto ci sono solo 6 palline, e il rapporto rosso:verde dipende dal colore della prima pallina.

Lo scenario 1 è con reimmissione, lo scenario 2 è senza, e danno risultati diversi.

Per simulare il campionamento senza reimmissione in Julia, ci sono un paio di opzioni.

La più semplice (e, su Exercism, l'unica opzione) è usare Random.shuffle() per mettere le voci in ordine casuale, poi usare i primi n elementi. Va bene per problemi piccoli, ma potrebbe non scalare bene su collezioni grandi: shuffle deve generare l'array completo, anche se ne vuoi solo una piccola parte.

Per fare il campionamento con reimmissione «come si deve», installa il pacchetto StatsBase.jl. Esso fornisce la funzione sample() con una gamma completa di opzioni.

Possiamo ragionevolmente sperare che funzionalità simili vengano aggiunte a Random in una versione futura, per farla entrare a far parte della libreria standard (gli esempi di codice in questo documento sono stati testati con Julia 1.11).

Lavorare con le distribuzioni

Finora ci siamo concentrati sui casi in cui tutti i risultati hanno la stessa probabilità. Per esempio, rand(1:100) ha la stessa probabilità di restituire qualsiasi intero da 1 a 100.

Molte situazioni del mondo reale sono molto meno semplici di così. Di conseguenza, gli statistici hanno creato un'ampia varietà di distributions per descrivere matematicamente i risultati del «mondo reale».

Distribuzioni uniformi

La funzione rand() descritta sopra si usa quando tutte le probabilità sono uguali. Questa si chiama distribuzione uniform.

Distribuzione gaussiana

Chiamata anche distribuzione «normale» o curva «a campana», è un modo molto comune per descrivere l'imprecisione nei valori misurati.

Per esempio, supponi che la fabbrica in cui lavori abbia appena comprato 10.000 bulloni che dovrebbero essere identici. Vuoi configurare il robot della fabbrica per maneggiarli, quindi pesi un campione di 100 e scopri che hanno un peso medio (o mean) di 4,731 g. È estremamente improbabile che questo significhi che pesino tutti esattamente 4,731 g. Magari scopri che i valori vanno da 4,627 a 4,794 g ma si concentrano intorno a 4,731 g.

Questa è la Gaussian distribution, in cui le probabilità raggiungono il massimo in corrispondenza della media e diminuiscono simmetricamente su entrambi i lati (da qui la forma «a campana»). Per simularla nel software, ci serve un modo per specificare l'ampiezza della curva (in genere, i bulloni costosi si concentrano più strettamente intorno alla media di quelli economici!).

Per convenzione, questo si fa con la standard deviation: valori piccoli per una curva stretta e appuntita, valori grandi per una curva bassa e larga. I matematici adorano le lettere greche, quindi usiamo μ («mu») per rappresentare la media e σ («sigma») per rappresentare la deviazione standard. Così, se leggi che «il 95% dei valori è entro 2σ da μ» o che «il bosone di Higgs è stato rilevato con una confidenza di 5 sigma», questi commenti hanno a che fare con la deviazione standard.

Ci sarà altro da dire su questo nel concetto Statistics.

La funzione randn()

Abbreviazione di «random normal», è simile alla variante in virgola mobile di rand(), tranne per il fatto che i valori sono distribuiti come una gaussiana con media 0 e deviazione standard 1.

Anche qui, potresti voler riscalare l'output grezzo di randn per la deviazione standard e spostarlo per la media. L'esempio seguente converte a media 30 e deviazione standard 5.

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

È difficile capire, guardando l'output, che l'output grezzo si concentra più vicino a zero rispetto a una distribuzione uniforme. Se ne dubiti, generane 1000 o più e rappresentali in un grafico per renderlo più evidente.

Il modulo Random

Questo modulo contiene il livello successivo di funzionalità, omesso da Base per contribuire a ridurre al minimo le dimensioni della configurazione predefinita di Julia.

Random integra rand e randn di Base con versioni mutanti, rand! e randn!.

Un'aggiunta utile è randstring, che genera una stringa di lunghezza data. Per impostazione predefinita, usa lettere maiuscole e minuscole più le cifre da 0 a 9, ma si possono specificare altre scelte.

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

Inoltre, c'è una funzione bitrand per generare un BitArray casuale di lunghezza specificata.

julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
 1
 1
 0
 0
 1

Mescolamenti e permutazioni

Per mescolare casualmente le voci di un Vector abbiamo shuffle; e anche shuffle! per modificare l'array di input sul posto.

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)

A volte è utile avere invece gli indici mescolati. Per questo, usa randperm(n), dove n è la lunghezza della sequenza.

julia> randperm(6)
6-element Vector{Int64}:
 6
 2
 4
 1
 3
 5

In effetti, l'esempio sopra dà gli stessi risultati di shuffle(1:6).

Istruzioni

Mary è una grande fan della serie TV Star Trek: The Next Generation. Spesso gioca a giochi di ruolo con carta e penna, in cui lei e i suoi amici fingono di essere l'equipaggio della Starship Enterprise. Il personaggio di Mary è il Capitano Picard, il che significa che deve tenere il diario del capitano. Ama la parte creativa del gioco, ma non le piace generare dati casuali sul momento.

Aiuta Mary creando dei generatori casuali per i dati che compaiono di frequente nel diario del capitano.

1. Genera un pianeta casuale

La Starship Enterprise incontra molti pianeti nei suoi viaggi. I pianeti nell'universo di Star Trek sono divisi in categorie in base alle loro proprietà. Per esempio, la Terra è un pianeta di classe M. Tutte le possibili classi planetarie sono: D, H, J, K, L, M, N, R, T e Y.

Implementa la funzione random_planet. Dovrebbe restituire una delle classi planetarie in modo casuale.

julia> random_planet_class()
'K'

2. Genera un numero di registrazione casuale di un'astronave

L'Enterprise (numero di registrazione NCC-1701) non è l'unica astronave in circolazione! Quando si incontra con un'altra astronave, Mary deve registrare il numero di registrazione di quell'astronave.

I numeri di registrazione iniziano con il prefisso "NCC-" e poi usano un numero da 1000 a 9999 (inclusi).

Implementa la funzione random_ship_registry_number che restituisce un numero di registrazione casuale di un'astronave.

julia> random_ship_registry_number()
"NCC-1947"

3. Genera una data stellare casuale

A cosa serve un diario se non include le date?

Una data stellare è un numero in virgola mobile. Le avventure della Starship Enterprise della prima stagione di The Next Generation si svolgono tra le date stellari 41000.0 e 42000.0. Il «4» sta per il XXIV secolo, l'«1» per la prima stagione.

Implementa la funzione random_stardate che restituisce un numero in virgola mobile tra 41000.0 (incluso) e 42000.0.

julia> random_stardate()
41458.15721310934

4. Genera una data stellare arrotondata

Ci sono state lamentele per il fatto che le date stellari hanno troppi punti decimali, così Mary decide di arrotondare i diari a una cifra decimale.

Anche se si potrebbe fare arrotondando i valori di random_stardate(), ti rendi conto che esiste un approccio molto più semplice.

Implementa la funzione random_stardate_v2 che restituisce un numero in virgola mobile tra 41000.0 e 42000.0 (inclusi), con una sola cifra decimale.

julia> random_stardate_v2()
41732.6

5. Scegli alcune astronavi casuali da una lista

Una versione del gioco prevede l'interazione con altre astronavi: Mary ha bisogno di una selezione.

C'è un elenco completo di possibili astronavi, ma in ogni partita ne servono solo alcune, e vanno scelte in modo casuale.

Implementa la funzione pick_starships(starships, number_needed), che prende in input un vettore di numeri di registrazione unici di astronavi e restituisce un vettore di lunghezza number_needed. I valori del risultato vanno presi da starships, senza duplicati.

julia> pick_starships(["NCC-5011", "NCC-1228", "NCC-7039", "NCC-3978", "NCC-1476"], 2)
2-element Vector{String}:
 "NCC-1228"
 "NCC-5011"

5a. Facoltativamente, pensa ad altri tipi di argomento.

Abbiamo vincolato l'argomento starships a essere un vettore. Come implementeresti pick_starships() in modo che funzioni con altri iterabili, come tuple o insiemi?

Non ci sono test per questa parte.

Modifica tramite GitHub Il link si apre in una nuova finestra o scheda
Julia Exercism

Vuoi iniziare Diario del capitano?

Iscriviti a Exercism per imparare e padroneggiare Julia con 35 concetti128 esercizi e il mentoring di persone reali, tutto gratis.