Generar números aleatorios con una distribución dada

Mira esta pregunta:

Swift probabilidad de número aleatorio seleccionado?

La respuesta superior sugiere usar una instrucción switch, que hace el trabajo. Sin embargo, si tengo una gran cantidad de casos para considerar, el código parece muy poco elegante; Tengo una statement de cambio gigante con un código muy similar en cada caso repetido una y otra vez.

¿Hay alguna manera más clara y más clara de elegir un número aleatorio con una cierta probabilidad cuando tienes una gran cantidad de probabilidades para considerar? (como ~ 30)

Esta es una implementación de Swift fuertemente influenciada por las diversas respuestas para Generar números aleatorios con una distribución dada (numérica) :

func randomNumber(#probabilities: [Double]) -> Int { // Sum of all probabilities (so that we don't have to require that the sum is 1.0): let sum = reduce(probabilities, 0, +) // Random number in the range 0.0 < = rnd < sum : let rnd = sum * Double(arc4random_uniform(UInt32.max)) / Double(UInt32.max) // Find the first interval of accumulated probabilities into which `rnd` falls: var accum = 0.0 for (i, p) in enumerate(probabilities) { accum += p if rnd < accum { return i } } // This point might be reached due to floating point inaccuracies: return (probabilities.count - 1) } 

Ejemplos:

 let x = randomNumber(probabilities: [0.2, 0.3, 0.5]) 

devuelve 0 con probabilidad 0.2, 1 con probabilidad 0.3, y 2 con probabilidad 0.5.

 let x = randomNumber(probabilities: [1.0, 2.0]) 

devuelve 0 con probabilidad 1/3 y 1 con probabilidad 2/3.


Actualización para Swift 2 / Xcode 7:

 func randomNumber(probabilities probabilities: [Double]) -> Int { // Sum of all probabilities (so that we don't have to require that the sum is 1.0): let sum = probabilities.reduce(0, combine: +) // Random number in the range 0.0 < = rnd < sum : let rnd = sum * Double(arc4random_uniform(UInt32.max)) / Double(UInt32.max) // Find the first interval of accumulated probabilities into which `rnd` falls: var accum = 0.0 for (i, p) in probabilities.enumerate() { accum += p if rnd < accum { return i } } // This point might be reached due to floating point inaccuracies: return (probabilities.count - 1) } 

Actualización para Swift 3 / Xcode 8:

 func randomNumber(probabilities: [Double]) -> Int { // Sum of all probabilities (so that we don't have to require that the sum is 1.0): let sum = probabilities.reduce(0, +) // Random number in the range 0.0 < = rnd < sum : let rnd = sum * Double(arc4random_uniform(UInt32.max)) / Double(UInt32.max) // Find the first interval of accumulated probabilities into which `rnd` falls: var accum = 0.0 for (i, p) in probabilities.enumerated() { accum += p if rnd < accum { return i } } // This point might be reached due to floating point inaccuracies: return (probabilities.count - 1) } 

Actualización para Swift 4.2 / Xcode 10:

 func randomNumber(probabilities: [Double]) -> Int { // Sum of all probabilities (so that we don't have to require that the sum is 1.0): let sum = probabilities.reduce(0, +) // Random number in the range 0.0 < = rnd < sum : let rnd = Double.random(in: 0.0 ..< sum) // Find the first interval of accumulated probabilities into which `rnd` falls: var accum = 0.0 for (i, p) in probabilities.enumerated() { accum += p if rnd < accum { return i } } // This point might be reached due to floating point inaccuracies: return (probabilities.count - 1) } 

¿Hay alguna manera más clara y más clara de elegir un número aleatorio con una cierta probabilidad cuando tienes una gran cantidad de probabilidades para considerar?

Por supuesto. Escribe una función que genere un número basado en una tabla de probabilidades. Eso es esencialmente a lo que la statement de cambio que ha señalado es: una tabla definida en el código. Podría hacer lo mismo con los datos usando una tabla que se define como una lista de probabilidades y resultados:

 resultado de probabilidad
 ----------- -------
    0.4 1
    0.2 2
    0.1 3
    0,15 4
    0.15 5

Ahora puedes elegir un número entre 0 y 1 al azar. Comenzando desde la parte superior de la lista, sume las probabilidades hasta que haya excedido el número que eligió y use el resultado correspondiente. Por ejemplo, supongamos que el número que elige es 0.6527637. Comience en la parte superior: 0.4 es más pequeño, así que continúe. 0.6 (0.4 + 0.2) es más pequeño, así que continúa. 0.7 (0.6 + 0.1) es más grande, entonces detente. El resultado es 3.

He mantenido la tabla breve aquí por el bien de la claridad, pero puede hacerla todo el tiempo que quiera, y puede definirla en un archivo de datos para que no tenga que volver a comstackr cuando la lista cambie.

Tenga en cuenta que no hay nada particularmente específico para Swift sobre este método: podría hacer lo mismo en C o Swift o Lisp.

Puede hacerlo con funciones exponenciales o cuadráticas: tenga x sea su número aleatorio, tome y como el nuevo número aleatorio. Entonces, solo tienes que sacudir la ecuación hasta que se adapte a tu caso de uso. Digamos que tuve (x ^ 2) / 10 + (x / 300). Ingrese su número aleatorio, (como una forma de coma flotante), y luego obtenga la palabra con Int () cuando salga. Entonces, si mi generador de números aleatorios va de 0 a 9, tengo un 40% de probabilidades de obtener 0 y un 30% de probabilidades de obtener 1 – 3, un 20% de probabilidades de obtener 4 – 6 y un 10% de posibilidades de un 8. Básicamente estás tratando de simular algún tipo de distribución normal.

Aquí hay una idea de cómo se vería en Swift:

 func giveY (x: UInt32) -> Int { let xD = Double(x) return Int(xD * xD / 10 + xD / 300) } let ans = giveY (arc4random_uniform(10)) 

EDITAR:

No estaba muy claro arriba, lo que quise decir es que podrías reemplazar la statement de cambio con alguna función que devolvería un conjunto de números con una distribución de probabilidad que podrías deducir con la regresión usando wolfram o algo así. Entonces, para la pregunta a la que se vinculó, podría hacer algo como esto:

 import Foundation func returnLevelChange() -> Double { return 0.06 * exp(0.4 * Double(arc4random_uniform(10))) - 0.1 } newItemLevel = oldItemLevel * returnLevelChange() 

Entonces esa función devuelve un doble en algún lugar entre -0.05 y 2.1. Esa sería su figura de “x% peor / mejor que el nivel de elemento actual”. Pero, dado que es una función exponencial, no devolverá una distribución uniforme de números. El arc4random_uniform (10) devuelve un int de 0 a 9, y cada uno de ellos da como resultado un doble como este:

 0: -0.04 1: -0.01 2: 0.03 3: 0.1 4: 0.2 5: 0.34 6: 0.56 7: 0.89 8: 1.37 9: 2.1 

Como cada una de esas entradas del arc4random_uniform tiene la misma posibilidad de aparecer, obtienes probabilidades como esta:

 40% chance of -0.04 to 0.1 (~ -5% - 10%) 30% chance of 0.2 to 0.56 (~ 20% - 55%) 20% chance of 0.89 to 1.37 (~ 90% - 140%) 10% chance of 2.1 (~ 200%) 

Lo cual es algo similar a las probabilidades que tuvo otra persona. Ahora, para su función, es mucho más difícil, y las otras respuestas son casi definitivamente más aplicables y elegantes. PERO aún podrías hacerlo.

Organice cada una de las letras en orden de su probabilidad, de mayor a menor. Luego, obtenga sus sums acumulativas, comenzando con 0, sin la última. (entonces las probabilidades de 50%, 30%, 20% se convierten en 0, 0.5, 0.8). Luego, los multiplica hasta que sean enteros con una precisión razonable (0, 5, 8). Luego, grábalos: tus probabilidades acumuladas son tus x, las cosas que deseas seleccionar con una probabilidad dada (tus letras) son tus y. (Obviamente, no puede trazar letras reales en el eje y, por lo que simplemente trazar sus índices en algún conjunto). Entonces, tratarías de encontrar alguna regresión allí, y que esa sea tu función. Por ejemplo, probando esos números, obtuve

 e^0.14x - 1 

y esto:

 let letters: [Character] = ["a", "b", "c"] func randLetter() -> Character { return letters[Int(exp(0.14 * Double(arc4random_uniform(10))) - 1)] } 

devuelve “a” el 50% del tiempo, “b” el 30% del tiempo y “c” el 20% del tiempo. Obviamente, es bastante engorroso para más letras, y llevaría un tiempo calcular la regresión correcta, y si desea cambiar las ponderaciones, tendrá que hacerlo manualmente. PERO, si encontrara una buena ecuación que se ajustara a sus valores, la función real solo sería un par de líneas largas y rápidas.

Esto parece una buena oportunidad para un enchufe desvergonzado para mi pequeña biblioteca, swiftstats: https://github.com/r0fls/swiftstats

Por ejemplo, esto generaría 3 variables aleatorias de una distribución normal con media 0 y varianza 1:

 import SwiftStats let n = SwiftStats.Distributions.Normal(0, 1.0) print(n.random()) 

Las distribuciones compatibles incluyen: normal, exponencial, binomial, etc.

También es compatible con el ajuste de datos de muestra a una distribución determinada, utilizando el Estimador de máxima verosimilitud para la distribución.

Ver el archivo léame del proyecto para más información.