Returning members of an Enum with a given frequency

I have an enum, and I am trying to randomly return one of the members of the enum where each member can be given a frequencey of how often it is chosen (e.g. a member with a percentage of 0.05 would be returned 5% of the time).


I will post my best solution below, but I am hoping someone else has a better swift-2-ier way of doing it (e.g. filter/map/reduce, etc...), or a simpler solution.


Some notes:

  • ListableEnum is a protocol which adds the allMembers() function in an extension (it returns an array of all members)
  • init?(index:) is required by the ListableEnum to work its magic
  • random() is added to Double via extension and it returns a Double between 0 and 1 (inclusive)
  • I make an assumption that the frequencies add to 1.0


enum Test:Int,ListableEnum {
     case A,B,C

     init?(index:Int){self.init(rawValue:index)}

     var frequency:Double {
          switch self{
          case .A: return 0.1  //10%
          case .B: return 0.85 //85%
          case .C: return 0.05 //5%
          }
     }

     static var randomMember:Test{
          let allM = Test.allMembers()
          let rnd = Double.random()
          var total = 0
          for m in allM {
               total += m.frequency
               if total >= rnd {
                    return m
               }
          }
          return allM.last!
     }
}


Feel free to tell me I am being an ***** here (as long as it is accompanied by constructive feedback)

Replies

I'm having difficulties understanding what the fundamental requirements are, do you have to use enums for example, why not a Set (whose elements can be enumerated and conform to some protocol that requires them to have a frequency/probability)?


Can you describe what it is that you want to do in more general terms, with some context and without any specific implementation details?

I am experimenting with generating believable characters/clues for a little mystery game (mostly as an exercise for learning Swift 2).


I am a big fan of mysteries, and most games that have randomly generated mysteries have problems with making rare things too common, and also bad correlations (e.g. a 6'5" man with size 4 shoes). I would like my characters to have characteristics (e.g. eye color, blood type, etc...) that generally match real world numbers. It doesn't have to be perfect but the 2 main characteristics that I would like:

  • Rare things should only show up rarely (and common ones commonly)
  • Correlation between factors should be believable

I am currently handling the second by having a fixed order in which the characteristics are chosen (Gender then Height, etc...), and altering the frequencies of later stages based on the earlier results. I am open to better solutions for that as well. I am also taking into account familial relations when generating relatives... but that is another topic.

As an example, here is my frequency function for fingerprint patterns. Only about 5% of fingerprints are arches, so an arch pattern should only be chosen ~5% of the time.

var likelyhood:Double {
            switch self{
            case .RadialLoop,.UlnerLoop: return (0.60/2.0)
            case .PlainWhorl,.CentralPocketWhorl,.DoubleLoop,.AccidentalWhorl: return (0.35/4.0)
            case .PlainArch,.TentedArch: return (0.05/2.0)
            }
        }

Not sure if this is "better", but I feel it's a simpler way to get a % based result:


import Foundation
enum Test {
    case A
    case B
    case C
}
var tests:[Test] = Array<Test>(count: 10, repeatedValue: Test.A)
tests.extend(Array<Test>(count: 85, repeatedValue: Test.B))
tests.extend(Array<Test>(count: 5, repeatedValue: Test.C))
print(tests[random() % tests.count])


The idea is just that we fill a 100 element array with the appropriate amount of each item, and select one at random to return.

enum MyEnumeration : Double {
    // Cases are any values between 0.0 and 1.0, with 0.1 intervals
    // Sum need not be 1.0. It is 0.9 in this case
    case A = 0.2, B = 0.6, C = 0.1
   
    // Raw values per member
    static let memberValues = stride(from: 0.0, through: 1.0, by: 0.1).filter{MyEnumeration(rawValue: $0) != nil}
   
    // Sum of raw values
    static let memberSum = memberValues.reduce(0.0, combine: +)
   
    // Multiplier offset from a true 1.0 sum
    static let memberMultiplier = 1.0 / memberSum
   
    // Members -- may not be in expected order
    static let members = memberValues.flatMap{MyEnumeration(rawValue: $0)}
   
    // Expected frequency per member based on percent distribution
    static let expectedFrequencies = zip(members, memberValues.map{$0 * memberMultiplier}).map{($0, $1)}


    // Return a probability-weighted member
    static var randomMember : MyEnumeration {
        var values = ArraySlice(memberValues)
        var roll = memberSum * (Double(arc4random()) / Double(UINT32_MAX)) // weight by sum
        repeat {
            guard let first = values.first else {fatalError()}
            guard let firstItem = MyEnumeration(rawValue: first) else {fatalError()}
            if roll < first {return firstItem}
            roll -= first; values = dropFirst(values)
        } while !values.isEmpty
        guard let defaultEnumeration = MyEnumeration(rawValue: 0.0) else {fatalError()}
        return defaultEnumeration
    }
}


// Monte Carlo the results
var aCount = 0; var bCount = 0; var cCount = 0
let num = 1000
for _ in 0...num {
    switch MyEnumeration.randomMember {
    case .A: aCount++
    case .B: bCount++
    case .C: cCount++
    }
}


MyEnumeration.members // .C, .A, .B, not in expected order
print(MyEnumeration.expectedFrequencies)
print(zip(MyEnumeration.members, [cCount, aCount, bCount].map{Double($0) / Double(num)}).map{($0, $1)})