Muitos programas precisam de valores aleatórios (aparentemente) para simular acontecimentos do mundo real.
Exemplos comuns e familiares incluem:
('H', 'T').Gerar valores verdadeiramente aleatórios com um computador é um desafio técnico surpreendentemente difícil, por isso podes ver estes resultados designados por «pseudoaleatórios».
Importante: Este Conceito não aborda números aleatórios criptograficamente seguros, que são um desafio muito mais difícil.
No entanto, bibliotecas bem concebidas como o módulo Random da biblioteca padrão do Julia são rápidas, flexíveis e dão resultados suficientemente bons para a maioria das aplicações em modelação, simulação e jogos.
O Julia divide a funcionalidade aleatória por vários locais:
Base, que estão sempre disponíveis.Random.O Random faz parte da biblioteca padrão e é provável que já esteja pré-instalado, mas vais precisar de acrescentar using Random no início do teu programa para trazer o seu conteúdo para o namespace.
rand()
O que esta função faz depende dos argumentos que lhe deres. Há muitas opções.
Sem argumentos, gera um número de vírgula flutuante entre 0 (inclusive) e 1.
Esta é uma distribuição uniform em que todos os valores são igualmente prováveis, como se explica na secção Trabalhar com distribuições, abaixo.
Um único argumento inteiro gera um vetor com esse comprimento.
julia> rand()
0.10261774967264703
julia> rand(5)
5-element Vector{Float64}:
0.24134501977563894
0.5664193284851202
0.9804412082089355
0.6229551330613335
0.47589221741904664
Para obter um intervalo diferente, basta deslocar e escalar o resultado de forma adequada.
O exemplo abaixo usa broadcasting para a subtração, abordado no Conceito Operações com Vetores.
O .- limita-se a aplicar esta aritmética a cada elemento do vetor.
# 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
Com um tipo como único argumento, rand usa typemin e typemax como limites.
Isto provavelmente não é o que queres!
Para números inteiros aleatórios, podemos fornecer um intervalo e, opcionalmente, quantos valores gerar.
julia> rand(Int64)
-9159538335234594326 # not very useful
julia> rand(1:10, 5)
5-element Vector{Int64}:
1
1
1
4
7
No exemplo rand(1:10, 5) acima, repara que há valores repetidos (por coincidência), porque cada escolha é independente.
Isto é «amostragem com reposição», explicada com mais detalhe abaixo.
Para valores de vírgula flutuante dentro de um intervalo, normalmente vais precisar de especificar o tamanho do passo. Caso contrário, o passo assume por defeito o valor 1.0, o que raramente é útil.
julia> rand(2.4:0.01:3.2, 4)
4-element Vector{Float64}:
3.19
2.53
3.14
3.13
Em alternativa, fornece um array ou um tuplo, e rand devolve uma entrada aleatória:
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"
Imagina que temos um saco com 3 bolas vermelhas e 4 bolas verdes, e que tiramos uma bola do saco ao acaso. Para tirar uma segunda bola, há duas possibilidades:
O cenário 1 é com reposição, o cenário 2 é sem reposição, e dão resultados diferentes.
Para simular amostragem sem reposição no Julia, há algumas opções.
A mais simples (e, no Exercism, a única opção) é usar Random.shuffle() para pôr as entradas por ordem aleatória e depois usar os primeiros n elementos.
Isto é adequado para problemas pequenos, mas pode não escalar bem para coleções grandes: shuffle precisa de gerar o array completo, mesmo que só queiras uma pequena fração dele.
Para fazeres amostragem com reposição de forma adequada, instala o pacote StatsBase.jl.
Este fornece a função sample() com uma gama completa de opções.
Podemos razoavelmente esperar que funcionalidade semelhante seja adicionada ao Random numa versão futura, para passar a fazer parte da biblioteca padrão (os exemplos de código neste documento foram testados com o Julia 1.11).
Até agora, concentrámo-nos nos casos em que todos os resultados são igualmente prováveis.
Por exemplo, rand(1:100) tem a mesma probabilidade de dar qualquer número inteiro de 1 a 100.
Muitas situações do mundo real são bem menos simples do que isto.
Por esse motivo, os estatísticos criaram uma grande variedade de distributions para descrever resultados «do mundo real» matematicamente.
A função rand() descrita acima usa-se quando todas as probabilidades são iguais.
Chama-se a isto uma distribuição uniform.
Também chamada distribuição «normal» ou curva «em forma de sino», é uma forma muito comum de descrever a imprecisão em valores medidos.
Por exemplo, supõe que a fábrica onde trabalhas acabou de comprar 10 000 parafusos que deviam ser idênticos.
Queres preparar o robô da fábrica para os manusear, por isso pesas uma amostra de 100 e verificas que têm um peso médio (ou mean) de 4,731 g.
É extremamente improvável que isto signifique que todos pesam exatamente 4,731 g.
Talvez descubras que os valores variam entre 4,627 e 4,794 g, mas se agrupam em torno de 4,731 g.
Esta é a Gaussian distribution, na qual as probabilidades atingem o máximo na média e diminuem simetricamente para ambos os lados (daí a forma de «sino»).
Para simular isto em software, precisamos de alguma forma de especificar a largura da curva (tipicamente, os parafusos caros agrupam-se mais estreitamente em torno da média do que os baratos!).
Por convenção, isto faz-se com o standard deviation: valores pequenos para uma curva estreita e acentuada, grandes para uma curva baixa e larga.
Os matemáticos adoram letras gregas, por isso usamos μ («mu») para representar a média e σ («sigma») para representar o desvio padrão.
Assim, se leres que «95% dos valores estão dentro de 2σ de μ» ou que «o bosão de Higgs foi detetado com 5 sigma de confiança», estes comentários estão relacionados com o desvio padrão.
Haverá mais a dizer sobre isto no Conceito Statistics.
randn()
Abreviatura de «random normal», é semelhante à variante de vírgula flutuante de rand(), exceto que os valores se distribuem segundo uma gaussiana com média 0 e desvio padrão 1.
Mais uma vez, podes querer escalar a saída em bruto de randn para o desvio padrão e deslocá-la para a média.
O exemplo abaixo converte para média 30 e desvio padrão 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
É difícil perceber, olhando para a saída, que a saída em bruto se agrupa mais perto de zero do que no caso de uma distribuição uniforme. Se tiveres dúvidas, gera 1000 ou mais e representa-os num gráfico para tornar isso mais óbvio.
Random
Este módulo contém o nível seguinte de funcionalidade, omitido de Base para ajudar a minimizar o tamanho da configuração padrão do Julia.
O Random complementa rand e randn em Base com versões que modificam os valores no local, rand! e randn!.
Uma adição útil é randstring, que gera uma string com um determinado comprimento.
Por defeito, usa letras maiúsculas e minúsculas mais os algarismos 0 a 9, mas podem especificar-se outras opções.
julia> using Random
julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"
Além disso, há uma função bitrand para gerar um BitArray aleatório com um determinado comprimento.
julia> bitrand(5)
julia> bitrand(5)
5-element BitVector:
1
1
0
0
1
Para baralhar aleatoriamente as entradas de um Vector, temos shuffle; e também shuffle! para modificar o vetor de entrada no local.
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)
Por vezes, é útil ter antes os índices baralhados.
Para isso, usa randperm(n), em que n é o comprimento da sequência.
julia> randperm(6)
6-element Vector{Int64}:
6
2
4
1
3
5
Na prática, o exemplo acima dá os mesmos resultados que shuffle(1:6).
A Mary é uma grande fã da série de televisão Star Trek: The Next Generation. Joga muitas vezes jogos de interpretação de papéis com lápis e papel, nos quais ela e os amigos fazem de conta que são a tripulação da Starship Enterprise. A personagem da Mary é o Capitão Picard, o que significa que tem de manter o diário de bordo do capitão. Adora a parte criativa do jogo, mas não gosta de gerar dados aleatórios na hora.
Ajuda a Mary a criar geradores aleatórios para os dados que aparecem habitualmente no diário de bordo do capitão.
A Starship Enterprise encontra muitos planetas nas suas viagens. No universo de Star Trek, os planetas estão divididos em categorias com base nas suas propriedades. Por exemplo, a Terra é um planeta de classe M. Todas as classes planetárias possíveis são: D, H, J, K, L, M, N, R, T e Y.
Implementa a função random_planet. Deve devolver uma das classes planetárias, aleatoriamente.
julia> random_planet_class()
'K'
A Enterprise (número de registo NCC-1701) não é a única nave estelar a voar por aí! Quando se encontra com outra nave estelar, a Mary precisa de registar o número de registo dessa nave.
Os números de registo começam com o prefixo "NCC-" e usam depois um número de 1000 a 9999 (inclusive).
Implementa a função random_ship_registry_number, que devolve um número de registo de nave estelar aleatório.
julia> random_ship_registry_number()
"NCC-1947"
Para que serve um diário se não incluir datas?
Uma data estelar é um número de vírgula flutuante. As aventuras da Starship Enterprise na primeira temporada de The Next Generation decorrem entre as datas estelares 41000.0 e 42000.0. O "4" representa o século XXIV e o "1" representa a primeira temporada.
Implementa a função random_stardate, que devolve um número de vírgula flutuante entre 41000.0 (inclusive) e 42000.0.
julia> random_stardate()
41458.15721310934
Tem havido queixas de que as datas estelares têm demasiadas casas decimais, por isso a Mary decide arredondar os registos para uma casa decimal.
Embora isto pudesse ser feito arredondando valores de random_stardate(), percebes que há uma abordagem muito mais simples.
Implementa a função random_stardate_v2, que devolve um número de vírgula flutuante entre 41000.0 e 42000.0 (inclusive), com uma única casa decimal.
julia> random_stardate_v2()
41732.6
Uma versão do jogo implica interagir com outras naves estelares, e a Mary precisa de uma seleção.
Há uma lista completa de naves estelares possíveis, mas só algumas são necessárias em cada jogo, e devem ser escolhidas aleatoriamente.
Implementa a função pick_starships(starships, number_needed), que recebe um vetor de números de registo de naves estelares únicos e devolve um vetor de comprimento number_needed. Os valores do resultado devem ser retirados de starships, sem duplicados.
julia> pick_starships(["NCC-5011", "NCC-1228", "NCC-7039", "NCC-3978", "NCC-1476"], 2)
2-element Vector{String}:
"NCC-1228"
"NCC-5011"
Restringimos o argumento starships a um vetor.
Como implementarias pick_starships() para funcionar com outros iteráveis, como tuplas ou conjuntos?
Não há testes para esta parte.
Inscreve-te no Exercism para aprenderes e dominares Julia com 35 conceitos128 exercícios, e mentoria humana real, tudo grátis.