Generate random numbers with a given distribution

前端 未结 4 466
悲&欢浪女
悲&欢浪女 2020-11-28 13:39

Check out this question:

Swift probability of random number being selected?

The top answer suggests to use a switch statement, which does the job. However, i

相关标签:
4条回答
  • 2020-11-28 14:27

    Is there a nicer, cleaner way to pick a random number with a certain probability when you have a large number of probabilities to consider?

    Sure. Write a function that generates a number based on a table of probabilities. That's essentially what the switch statement you've pointed to is: a table defined in code. You could do the same thing with data using a table that's defined as a list of probabilities and outcomes:

    probability    outcome
    -----------    -------
       0.4            1
       0.2            2
       0.1            3
       0.15           4
       0.15           5
    

    Now you can pick a number between 0 and 1 at random. Starting from the top of the list, add up probabilities until you've exceeded the number you picked, and use the corresponding outcome. For example, let's say the number you pick is 0.6527637. Start at the top: 0.4 is smaller, so keep going. 0.6 (0.4 + 0.2) is smaller, so keep going. 0.7 (0.6 + 0.1) is larger, so stop. The outcome is 3.

    I've kept the table short here for the sake of clarity, but you can make it as long as you like, and you can define it in a data file so that you don't have to recompile when the list changes.

    Note that there's nothing particularly specific to Swift about this method -- you could do the same thing in C or Swift or Lisp.

    0 讨论(0)
  • 2020-11-28 14:32

    This seems like a good opportunity for a shameless plug to my small library, swiftstats: https://github.com/r0fls/swiftstats

    For example, this would generate 3 random variables from a normal distribution with mean 0 and variance 1:

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

    Supported distributions include: normal, exponential, binomial, etc...

    It also supports fitting sample data to a given distribution, using the Maximum Likelihood Estimator for the distribution.

    See the project readme for more info.

    0 讨论(0)
  • 2020-11-28 14:34

    This is a Swift implementation strongly influenced by the various answers to Generate random numbers with a given (numerical) distribution.

    For Swift 4.2/Xcode 10 and later (explanations inline):

    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)
    }
    

    Examples:

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

    returns 0 with probability 0.2, 1 with probability 0.3, and 2 with probability 0.5.

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

    return 0 with probability 1/3 and 1 with probability 2/3.


    For 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)
    }
    

    For 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)
    }
    
    0 讨论(0)
  • 2020-11-28 14:36

    You could do it with exponential or quadratic functions - have x be your random number, take y as the new random number. Then, you just have to jiggle the equation until it fits your use case. Say I had (x^2)/10 + (x/300). Put your random number in, (as some floating-point form), and then get the floor with Int() when it comes out. So, if my random number generator goes from 0 to 9, I have a 40% chance of getting 0, and a 30% chance of getting 1 - 3, a 20% chance of getting 4 - 6, and a 10% chance of an 8. You're basically trying to fake some kind of normal distribution.

    Here's an idea of what it would look like in Swift:

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

    EDIT:

    I wasn't very clear above - what I meant was you could replace the switch statement with some function that would return a set of numbers with a probability distribution that you could figure out with regression using wolfram or something. So, for the question you linked to, you could do something like this:

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

    So that function returns a double somewhere between -0.05 and 2.1. That would be your "x% worse/better than current item level" figure. But, since it's an exponential function, it won't return an even spread of numbers. The arc4random_uniform(10) returns an int from 0 - 9, and each of those would result in a double like this:

    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
    

    Since each of those ints from the arc4random_uniform has an equal chance of showing up, you get probabilities like this:

    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%)
    

    Which is something similar to the probabilities that other person had. Now, for your function, it's much more difficult, and the other answers are almost definitely more applicable and elegant. BUT you could still do it.

    Arrange each of the letters in order of their probability - from largest to smallest. Then, get their cumulative sums, starting with 0, without the last. (so probabilities of 50%, 30%, 20% becomes 0, 0.5, 0.8). Then you multiply them up until they're integers with reasonable accuracy (0, 5, 8). Then, plot them - your cumulative probabilities are your x's, the things you want to select with a given probability (your letters) are your y's. (you obviously can't plot actual letters on the y axis, so you'd just plot their indices in some array). Then, you'd try find some regression there, and have that be your function. For instance, trying those numbers, I got

    e^0.14x - 1
    

    and this:

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

    returns "a" 50% of the time, "b" 30% of the time, and "c" 20% of the time. Obviously pretty cumbersome for more letters, and it would take a while to figure out the right regression, and if you wanted to change the weightings you're have to do it manually. BUT if you did find a nice equation that did fit your values, the actual function would only be a couple lines long, and fast.

    0 讨论(0)
提交回复
热议问题