Skip to content

Instantly share code, notes, and snippets.

@wookiee
Created February 14, 2020 18:01
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save wookiee/821980db2af5d2f5796fc831d0767729 to your computer and use it in GitHub Desktop.
Save wookiee/821980db2af5d2f5796fc831d0767729 to your computer and use it in GitHub Desktop.
Swift function to pseudorandomly choose an item from an Array, allowing for individual weighting of their likelihood of choice.
/// Pseudorandomly choose a value from an array of values, where each is weighted relative to the others in the collection.
///
/// The `weight` component of each tuple is a positive integer signalling how likely a value is to be chosen vs. other values with different weights. For example, a value with a weight of `4` is four times as likely as a value with a weight of `1` to be chosen.
///
/// - Parameter values: an array of `(value:Thing,weight:Int)` tuples.
///
/// - Returns: a single chosen value.
///
func pickWeighted<Thing>(from weightedValues: [(value: Thing, weight: Int)]) -> Thing {
// compute the sum of the weights of all of the value/weight pairs
let totalWeight = weightedValues.map({$0.weight}).reduce(0,+)
// strategy: pick a random number up to the totalWeight, and then iterate the individual weights until we've exhausted the random value, effectively letting the spinner stop on that value.
// eg, given [1,3,5,1] a spinnerValue of 6 will land on the `5`.
var spinnerValue = Int.random(in: 1...totalWeight)
for weightedValue in weightedValues {
spinnerValue -= weightedValue.weight
if spinnerValue <= 0 {
return weightedValue.value
}
}
fatalError("It should not have been possible to run off the end of totalWeight without landing on a value.")
}
// Test it out!
// let's run it ten thousand times and see how often each value gets chosen.
let inputWeightedValues = [(value: "Hello", weight: 2), // should be around 10%
(value: "World", weight: 1), // should be around 5%
(value: "Nice", weight: 10), // should be around 50%
(value: "Day", weight: 1), // should be around 5%
(value: "Today", weight: 1), // should be around 5%
(value: "Huh", weight: 5)] // should be around 25%
var chosenValueCounts = [String:Int]()
let numberOfTests = 10_000
for _ in 1...numberOfTests {
let chosenValue = pickWeighted(from: inputWeightedValues)
chosenValueCounts[chosenValue, default: 0] += 1
}
var chosenValueFrequencies: [String:Double] = chosenValueCounts.mapValues {
(numberOfTimesChosen: Int) -> Double in
let frequency = Double(numberOfTimesChosen) / Double(numberOfTests)
return frequency
}
print(chosenValueFrequencies)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment