Created
February 14, 2020 18:01
-
-
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.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/// 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