Below is a Ruby implementation of a random statistical event, based on a hash with the actual observed counts of outcomes.
I'd be interested in feedback in particular on what techniques I might use to avoid a loop-based accumulator in the RandomEvent#predict!
method. I'm also very curious as well about any other suggestions on refactoring, patterns and performance that might be applicable here.
The statistics material itself might be somewhat beyond the scope of a review but I'd appreciate any thoughts on appropriate naming and more effective (deterministic) ways to test this.
Spec
include Statistics
describe RandomEvent do
context 'when an event has only one outcome' do
it 'always happens' do
expect(RandomEvent.from_hash(always: 1).predict!).to eq(:always)
end
end
context 'when the event has multiple outcomes' do
let(:trials) { 10_000 }
subject(:event) do
RandomEvent.from_hash(heads: 51, tails: 49)
end
it 'should distribute them' do
coinflips = trials.times.map { event.predict! }
heads_variance = (coinflips.count(:heads) - trials/2).abs
tails_variance = (coinflips.count(:tails) - trials/2).abs
expected_variance = trials/10
expect(heads_variance).to be < expected_variance
expect(tails_variance).to be < expected_variance
end
end
end
Implementation
class RandomEvent
def initialize
@outcome_counts = {}
end
def add_outcome(outcome, count:)
@outcome_counts[outcome] = count
end
def normalized_outcome_probabilities
total_outcome_counts = @outcome_counts.values.reduce(&:+)
@outcome_counts.inject({}) do |hash,(outcome,count)|
hash[outcome] = count / total_outcome_counts.to_f
hash
end
end
def predict!
acc = 0.0
roll = rand
selected_outcome = nil
normalized_outcome_probabilities.each do |outcome, probability|
acc += probability
if acc > roll
selected_outcome = outcome
break
end
end
selected_outcome
end
def self.from_hash(outcome_counts_hash)
event = new
outcome_counts_hash.each do |outcome, count|
event.add_outcome(outcome, count: count)
end
event
end
end