Parcours
/
Julia
Julia
/
Exercices
/
Journal de bord du capitaine
Journal de bord du capitaine

Journal de bord du capitaine

Exercice d'apprentissage

Introduction

De nombreux programmes ont besoin de valeurs (apparemment) aléatoires pour simuler des événements du monde réel.

Voici quelques exemples courants et familiers :

  • Un lancer de pièce : une valeur aléatoire parmi ('H', 'T').
  • Le lancer d'un dé : un entier aléatoire de 1 à 6.
  • Le mélange d'un jeu de cartes : un ordre aléatoire d'une liste de cartes.

Générer de vraies valeurs aléatoires avec un ordinateur est un défi technique étonnamment difficile, c'est pourquoi on parle souvent de résultats « pseudo-aléatoires ».

Important : ce concept ne couvre pas les nombres aléatoires cryptographiquement sûrs, qui représentent un défi bien plus difficile.

Cependant, des bibliothèques bien conçues comme le module Random de la bibliothèque standard de Julia sont rapides, flexibles, et donnent des résultats largement suffisants pour la plupart des applications de modélisation, de simulation et de jeux.

Julia répartit les fonctionnalités aléatoires sur plusieurs emplacements :

  • Quelques fonctions de base, mais très polyvalentes, dans Base, qui sont toujours disponibles.
  • Un éventail plus large d'options dans le module Random.
  • Des fonctionnalités plus spécialisées dans des paquets qui doivent être installés avant utilisation (et qui ne sont pas disponibles dans Exercism).

Random fait partie de la bibliothèque standard et est probablement préinstallé, mais tu devras ajouter using Random en haut de ton programme pour importer son contenu dans l'espace de noms.

La fonction rand()

Ce que fait cette fonction dépend des arguments que tu lui passes. Les options sont nombreuses.

Sans argument, elle génère un nombre à virgule flottante entre 0 (inclus) et 1. Il s'agit d'une distribution uniform où toutes les valeurs ont la même probabilité, comme on le verra dans la section Travailler avec les distributions, plus bas.

Un seul argument entier génère un vecteur de cette longueur.

julia> rand()
0.10261774967264703

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

Pour obtenir une autre plage, il suffit de décaler et de mettre à l'échelle le résultat de façon appropriée.

L'exemple ci-dessous utilise la diffusion pour la soustraction, abordée dans le concept Opérations sur les vecteurs. Le .- applique simplement cette arithmétique à chaque élément du vecteur.

# 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

Avec un type comme seul argument, rand utilise typemin et typemax comme bornes. Ce n'est probablement pas ce que tu veux !

Pour des entiers aléatoires, on peut fournir une plage, et éventuellement le nombre de valeurs à générer.

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

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

Dans l'exemple rand(1:10, 5) ci-dessus, remarque que certaines valeurs se répètent (par coïncidence), parce que chaque tirage est indépendant. C'est ce qu'on appelle le « tirage avec remise », dont on reparle plus en détail ci-dessous.

Pour des valeurs à virgule flottante dans une plage, tu devras généralement préciser un pas. Sinon, le pas vaut 1,0 par défaut, ce qui est rarement utile.

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

Autrement, fournis un tableau ou un tuple, et rand renverra une entrée aléatoire :

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"

Tirage avec ou sans remise

Imaginons un sac contenant 3 boules rouges et 4 boules vertes, et qu'on tire une boule au hasard dans ce sac. Pour obtenir une deuxième boule, deux possibilités s'offrent à nous :

  1. Remettre la première boule dans le sac et bien secouer le tout avant d'en tirer une autre. Le nombre de boules est alors le même qu'avant (7), et le rapport entre rouges et vertes reste identique.
  2. Poser la première boule sur la table avant d'en tirer une seconde. Il ne reste alors que 6 boules dans le sac, et le rapport entre rouges et vertes dépend de la couleur de la première boule.

Le scénario 1 se fait avec remise, le scénario 2 sans remise, et ils donnent des résultats différents.

Pour simuler un tirage sans remise en Julia, il existe quelques options.

Le plus simple (et, dans Exercism, la seule option), utiliser Random.shuffle() pour mettre les entrées dans un ordre aléatoire, puis prendre les n premiers éléments. Cela convient pour de petits problèmes, mais passe mal à l'échelle sur de grandes collections : shuffle doit générer le tableau complet, même si tu n'en veux qu'une petite partie.

Pour faire un tirage avec remise « correctement », installe le paquet StatsBase.jl. Il fournit la fonction sample() avec tout un éventail d'options.

On peut raisonnablement espérer que des fonctionnalités similaires seront ajoutées à Random dans une future version, pour l'intégrer à la bibliothèque standard (les exemples de code de ce document ont été testés avec Julia 1.11).

Travailler avec les distributions

Jusqu'ici, on s'est concentré sur des cas où tous les résultats ont la même probabilité. Par exemple, rand(1:100) a autant de chances de donner n'importe quel entier de 1 à 100.

Beaucoup de situations réelles sont bien moins simples que cela. C'est pourquoi les statisticiens ont créé tout un éventail de distributions pour décrire mathématiquement des résultats du monde réel.

Distributions uniformes

La fonction rand() décrite plus haut s'utilise quand toutes les probabilités sont égales. On parle alors de distribution uniform.

Distribution gaussienne

Aussi appelée distribution « normale » ou courbe « en cloche », c'est une façon très courante de décrire l'imprécision de valeurs mesurées.

Par exemple, suppose que l'usine où tu travailles vient d'acheter 10 000 boulons censés être identiques. Tu veux régler le robot de l'usine pour qu'il les manipule, alors tu pèses un échantillon de 100 boulons et tu constates qu'ils ont un poids moyen (ou mean) de 4,731 g. Il est extrêmement peu probable que cela signifie qu'ils pèsent tous exactement 4,731 g. Peut-être constates-tu que les valeurs vont de 4,627 à 4,794 g, mais se regroupent autour de 4,731 g.

C'est la Gaussian distribution, pour laquelle les probabilités culminent à la moyenne et décroissent symétriquement de part et d'autre (d'où la forme « en cloche »). Pour la simuler dans un logiciel, il faut un moyen de préciser la largeur de la courbe (en général, des boulons chers se regroupent plus étroitement autour de la moyenne que des boulons bon marché !).

Par convention, on mesure cela avec le standard deviation : de petites valeurs pour une courbe piquée et étroite, de grandes valeurs pour une courbe basse et large. Les mathématiciens adorent les lettres grecques, alors on utilise μ (« mu ») pour représenter la moyenne et σ (« sigma ») pour représenter le standard deviation. Ainsi, si tu lis que « 95 % des valeurs se situent à moins de 2σ de μ » ou que « le boson de Higgs a été détecté avec un niveau de confiance de 5 sigma », ce genre de remarque se rapporte à l'écart type.

On en reparlera dans le concept Statistics.

La fonction randn()

Abréviation de « random normal », elle est semblable à la variante à virgule flottante de rand(), sauf que les valeurs suivent une distribution gaussienne de moyenne 0 et d'écart type 1.

Là encore, tu voudras peut-être multiplier la sortie brute de randn par l'écart type, et la décaler pour la moyenne. L'exemple ci-dessous convertit vers une moyenne de 30 et un écart type de 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

Il est difficile de voir, à la simple lecture de la sortie, que la sortie brute se regroupe plus près de zéro que pour une distribution uniforme. Si tu en doutes, génères-en 1000 ou plus et trace-les pour que ce soit plus évident.

Le module Random

Ce module contient le niveau de fonctionnalités suivant, omis de Base pour contribuer à réduire la taille de la configuration par défaut de Julia.

Random complète rand et randn de Base avec des versions mutantes, rand! et randn!.

Un ajout utile est randstring, qui génère une string de longueur donnée. Par défaut, elle utilise des lettres majuscules et minuscules ainsi que les chiffres de 0 à 9, mais d'autres choix peuvent être précisés.

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

En outre, il existe une fonction bitrand qui génère un BitArray aléatoire de longueur donnée.

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

Mélanges et permutations

Pour mélanger aléatoirement les entrées d'un Vector, on dispose de shuffle, ainsi que de shuffle! pour modifier le vecteur d'entrée sur place.

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)

Il est parfois utile d'obtenir plutôt les indices mélangés. Pour cela, utilise randperm(n), où n est la longueur de la séquence.

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

En pratique, l'exemple ci-dessus donne les mêmes résultats que shuffle(1:6).

Instructions

Mary est une grande fan de la série télévisée Star Trek: The Next Generation. Elle joue souvent à des jeux de rôle sur papier, où elle et ses amis jouent le rôle de l'équipage du Starship Enterprise. Le personnage de Mary est le capitaine Picard, ce qui signifie qu'elle doit tenir le journal de bord du capitaine. Elle adore la partie créative du jeu, mais n'aime pas générer des données aléatoires à la volée.

Aide Mary en créant des générateurs de données aléatoires pour les informations qui apparaissent couramment dans le journal du capitaine.

1. Génère une planète aléatoire

Le Starship Enterprise rencontre de nombreuses planètes au cours de ses voyages. Dans l'univers de Star Trek, les planètes sont réparties en catégories selon leurs propriétés. Par exemple, la Terre est une planète de classe M. Toutes les classes planétaires possibles sont : D, H, J, K, L, M, N, R, T et Y.

Implémente la fonction random_planet. Elle doit renvoyer l'une des classes planétaires au hasard.

julia> random_planet_class()
'K'

2. Génère un numéro d'immatriculation de vaisseau aléatoire

L'Enterprise (numéro d'immatriculation NCC-1701) n'est pas le seul vaisseau à voler dans les parages ! Quand il en croise un autre, Mary doit consigner le numéro d'immatriculation de ce vaisseau.

Les numéros d'immatriculation commencent par le préfixe « NCC- » suivi d'un nombre de 1000 à 9999 (bornes incluses).

Implémente la fonction random_ship_registry_number qui renvoie un numéro d'immatriculation de vaisseau aléatoire.

julia> random_ship_registry_number()
"NCC-1947"

3. Génère une date stellaire aléatoire

À quoi bon tenir un journal s'il ne contient pas de dates ?

Une date stellaire est un nombre à virgule flottante. Les aventures du Starship Enterprise de la première saison de The Next Generation se déroulent entre les dates stellaires 41000,0 et 42000,0. Le « 4 » correspond au 24e siècle, le « 1 » à la première saison.

Implémente la fonction random_stardate qui renvoie un nombre à virgule flottante compris entre 41000,0 et 42000,0 (bornes incluses).

julia> random_stardate()
41458.15721310934

4. Génère une date stellaire arrondie

On s'est plaint qu'il y avait trop de décimales dans les dates stellaires, alors Mary décide d'arrondir les journaux à une seule décimale.

Même si on pourrait y arriver en arrondissant les valeurs de random_stardate(), tu te rends compte qu'il existe une approche bien plus simple.

Implémente la fonction random_stardate_v2 qui renvoie un nombre à virgule flottante compris entre 41000,0 et 42000,0 (bornes incluses), avec une seule décimale.

julia> random_stardate_v2()
41732.6

5. Choisis quelques vaisseaux au hasard dans une liste.

Une version du jeu consiste à interagir avec d'autres vaisseaux, et Mary a besoin d'une sélection.

Il existe une liste complète de vaisseaux possibles, mais seuls quelques-uns sont nécessaires pour chaque partie, et ils doivent être choisis au hasard.

Implémente la fonction pick_starships(starships, number_needed), qui prend un vecteur de numéros d'immatriculation uniques de vaisseaux et renvoie un vecteur de longueur number_needed. Les valeurs du résultat doivent être tirées de starships, sans doublons.

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

5a. Réfléchis éventuellement à d'autres types d'arguments.

On a contraint l'argument starships à être un vecteur. Comment implémenterais-tu pick_starships() pour qu'elle fonctionne avec d'autres itérables, comme les tuples ou les ensembles ?

Il n'y a pas de tests pour cette partie.

Modifie via GitHub Le lien s'ouvre dans une nouvelle fenêtre ou un nouvel onglet
Julia Exercism

Prêt à commencer Journal de bord du capitaine ?

Inscris-toi sur Exercism pour apprendre et maîtriser Julia avec 35 concepts128 exercices, et un vrai mentorat humain, le tout gratuitement.