Tracks
/
Julia
Julia
/
Temario
/
Aleatoriedad
Al

Aleatoriedad en Julia

2 ejercicios

Acerca de Aleatoriedad

Muchos programas necesitan valores (aparentemente) aleatorios para simular eventos del mundo real.

Algunos ejemplos comunes y conocidos son:

  • El lanzamiento de una moneda: un valor aleatorio de ('H', 'T').
  • La tirada de un dado: un número entero aleatorio del 1 al 6.
  • Mezclar una baraja de cartas: un orden aleatorio de una lista de cartas.

Generar valores verdaderamente aleatorios con una computadora es un desafío técnico sorprendentemente difícil, por lo que quizás veas que estos resultados se describen como «pseudoaleatorios».

Importante: este Concepto no cubre los números aleatorios criptográficamente seguros, que son un desafío mucho más difícil.

Sin embargo, las bibliotecas bien diseñadas, como el módulo Random de la biblioteca estándar de Julia, son rápidas, flexibles y dan resultados que son más que suficientes para la mayoría de las aplicaciones de modelado, simulación y juegos.

Julia divide la funcionalidad aleatoria en varios lugares:

  • Solo unas pocas funciones básicas pero muy versátiles en Base, que siempre están disponibles.
  • Una gama más amplia de opciones en el módulo Random.
  • Funcionalidad más especializada en paquetes que deben instalarse antes de usarse (y que no están disponibles en Exercism).

Random es parte de la biblioteca estándar y probablemente ya esté preinstalado, pero necesitarás agregar using Random al inicio de tu programa para traer su contenido al espacio de nombres.

La función rand()

Lo que hace esta función depende de los argumentos que le pases. Hay muchas opciones.

Sin argumentos, genera un número de punto flotante entre 0 (inclusive) y 1. Esta es una distribución uniform, en la que todos los valores tienen la misma probabilidad, como se explica en la sección Trabajar con distribuciones, más abajo.

Un solo argumento entero genera un vector de esa longitud.

julia> rand()
0.10261774967264703

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

Para obtener un rango diferente, solo desplaza y escala el resultado de forma adecuada.

El ejemplo de abajo usa broadcasting para la resta, algo que se cubre en el Concepto Operaciones con vectores. El .- simplemente aplica esta aritmética a cada elemento del vector.

# 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 como único argumento, rand usará typemin y typemax como límites. ¡Esto probablemente no sea lo que quieres!

Para obtener números enteros aleatorios, podemos proporcionar un rango y, opcionalmente, cuántos valores generar.

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

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

En el ejemplo rand(1:10, 5) de arriba, fíjate que hay valores repetidos (de forma casual), porque cada elección es independiente. Esto es «muestreo con reemplazo», que se explica con más detalle abajo.

Para valores de punto flotante dentro de un rango, normalmente tendrás que especificar un tamaño de paso. De lo contrario, el paso será 1.0 de forma predeterminada, lo cual rara vez es útil.

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

Como alternativa, proporciona un array o una tupla y rand devolverá una entrada aleatoria:

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"

Muestreo con o sin reemplazo

Imagina que tenemos una bolsa con 3 bolas rojas y 4 bolas verdes, y sacamos una bola al azar de la bolsa. Para sacar una segunda bola, hay dos posibilidades:

  1. Devolver la primera bola a la bolsa y agitarla bien antes de sacar otra. La cantidad de bolas ahora es la misma que antes (7), y la proporción de rojas a verdes también es la misma.
  2. Dejar la primera bola sobre la mesa antes de sacar una segunda. Ahora solo quedan 6 bolas en la bolsa, y la proporción de rojas a verdes depende del color de la primera bola.

El escenario 1 es con reemplazo, el escenario 2 es sin reemplazo, y dan resultados diferentes.

Para simular un muestreo sin reemplazo en Julia, hay un par de opciones.

La más simple (y dentro de Exercism, la única opción): usa Random.shuffle() para poner las entradas en orden aleatorio y luego toma los primeros n elementos. Esto está bien para problemas pequeños, pero puede no escalar bien a colecciones grandes: shuffle necesita generar el array completo, incluso si solo quieres una pequeña fracción de él.

Para hacer un muestreo con reemplazo «como es debido», instala el paquete StatsBase.jl. Ese paquete proporciona la función sample() con una gama completa de opciones.

Es razonable esperar que se agregue una funcionalidad similar a Random en una versión futura, para que forme parte de la biblioteca estándar (los ejemplos de código de este documento se probaron con Julia 1.11).

Trabajar con distribuciones

Hasta ahora nos hemos centrado en casos donde todos los resultados tienen la misma probabilidad. Por ejemplo, rand(1:100) tiene la misma probabilidad de dar cualquier número entero del 1 al 100.

Muchas situaciones del mundo real son mucho menos simples que esto. Por eso, los estadísticos han creado una amplia variedad de distributions para describir matemáticamente resultados del «mundo real».

Distribuciones uniformes

La función rand() descrita arriba se usa cuando todas las probabilidades son iguales. Esto se llama distribución [uniform][uniform-distribution].

Distribución gaussiana

También llamada distribución «normal» o «curva en forma de campana», es una forma muy común de describir la imprecisión en valores medidos.

Por ejemplo, supón que la fábrica donde trabajas acaba de comprar 10,000 pernos que deberían ser idénticos. Quieres configurar el robot de la fábrica para que los maneje, así que pesas una muestra de 100 y descubres que tienen un peso promedio (o mean) de 4.731g. Es muy poco probable que esto signifique que todas pesan exactamente 4.731g. Quizás descubras que los valores van de 4.627 a 4.794g, pero se agrupan alrededor de 4.731g.

Esta es la Gaussian distribution, en la que las probabilidades alcanzan su punto máximo en la media y disminuyen de forma simétrica a ambos lados (de ahí lo de «forma de campana»). Para simular esto en software, necesitamos alguna forma de especificar el ancho de la curva (normalmente, los pernos caros se agruparán más cerca de la media que los baratos).

Por convención, esto se hace con la standard deviation: valores pequeños para una curva marcada y estrecha, y valores grandes para una curva baja y ancha. A los matemáticos les encantan las letras griegas, así que usamos μ ('mu') para representar la media y σ ('sigma') para representar la desviación estándar. Por eso, si lees que «el 95% de los valores están dentro de 2σ de μ» o que «el bosón de Higgs se ha detectado con una confianza de 5 sigmas», ese tipo de comentarios se refieren a la desviación estándar.

Habrá más que decir sobre esto en el Concepto Statistics.

La función randn()

Es la abreviatura de «random normal», y es similar a la variante de punto flotante de rand(), salvo que los valores se distribuyen como una gaussiana con media 0 y desviación estándar 1.

De nuevo, quizás quieras escalar la salida bruta de randn para ajustar la desviación estándar, y desplazarla para ajustar la media. El ejemplo de abajo la convierte a una media de 30 y una desviación estándar 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

Es difícil notar, con solo mirar la salida, que la salida bruta se agrupa más cerca de cero que en una distribución uniforme. Si lo dudas, genera 1000 o más y grafícalos para que sea más evidente.

El módulo Random

Este módulo contiene el siguiente nivel de funcionalidad, que se omite de Base para ayudar a minimizar el tamaño de la configuración predeterminada de Julia.

Random complementa rand y randn de Base con versiones mutantes, rand! y randn!.

Una adición útil es randstring, que genera un string de una longitud dada. De forma predeterminada, usa letras mayúsculas y minúsculas, además de los dígitos del 0 al 9, pero se pueden especificar otras opciones.

julia> using Random

julia> randstring(20)
"BoJnIxrS33pJiWggXZQV"

Además, existe una función bitrand que genera un BitArray aleatorio de la longitud especificada.

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

Mezclas y permutaciones

Para mezclar aleatoriamente las entradas de un Vector tenemos shuffle; también shuffle! para mutar el vector de entrada en el lugar.

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 veces es útil tener los índices mezclados en su lugar. Para eso, usa randperm(n), donde n es la longitud de la secuencia.

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

En efecto, el ejemplo de arriba da los mismos resultados que shuffle(1:6).

Entre las funciones relacionadas están randsubseq, para extraer entradas con una probabilidad fija, y randcycle, para permutaciones cíclicas. Estas requieren conocimientos especializados, así que consulta la documentación si te interesan.

Semillas y algoritmos

Varios algoritmos de generación de números aleatorios (RNG) vienen integrados en Random de serie, y cualquiera con los conocimientos matemáticos adecuados puede agregar más. ¡Estas cosas están muy fuera del alcance de este documento!

Una razón más común para trabajar con los RNG es especificar una seed, que tiene el efecto de hacer que la secuencia de salidas «aleatorias» sea reproducible de una ejecución a la siguiente.

Esa reproducibilidad no es apropiada en el código de producción, pero puede ayudar con las pruebas y la depuración.

Otros paquetes

Fuera de Exercism, hay muchos paquetes instalables relacionados con la aleatoriedad, la probabilidad y la estadística. Para obtener más información, consulta el Concepto Statistics.

El paquete StatsBase.jl

La mayoría de las funciones de StatsBase son bastante técnicas y no son relevantes para este documento.

La excepción es StatsBase.sample, que proporciona una implementación completa del muestreo con o sin reemplazo (consulta una sección anterior, más arriba). También hay funciones para el muestreo ponderado (no uniforme).

El paquete Distributions.jl

Las distribuciones [uniform] y [normal] (o gaussiana) se describieron arriba.

El módulo Random también contiene randexp para muestrear de la distribución exponencial, que está relacionada con la distribución de Poisson (muy común).

Para una gama mucho más amplia de opciones, está el paquete Distributions.jl, para quienes tengan una base adecuada en estadística.

Editar en GitHub El enlace se abre en una ventana o pestaña nueva

Aprende Aleatoriedad

La práctica está bloqueada

Desbloquea 1 ejercicio más para practicar Aleatoriedad