Random distribution in Ruby
Unit test

The statistical reasoning in this unit test looks sloppy to me:


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 &lt; expected_variance
    expect(tails_variance).to be &lt; expected_variance
  end
end



It looks like you are flipping a slightly biased coin, but for some reason you expect heads and tails each to be 50%.  Then, for 10000 trials, you're requiring the count of heads and tails to be within the 40%–60% range — a very generous band.

The head count should follow a binomial distribution, namely \$B(n=10000, p=0.51)\$.  Applying Hoeffding's inequality

$$
\mathrm{Pr}(X \le k) \le e^{\frac{-2 (np - k)^2}{n}}
$$

for \$k = 4000\$ gives

$$
\mathrm{Pr}(X \le 4000) \le e^{-242} \approx 8 \times 10^{-106}
$$

The conclusion: for 10 coin flips, sure, the outcome can easily vary by 10%.  For 10000 coin flips, due to the Law of large numbers and the Central Limit Theorem, it basically never happens.  (It would be more likely that your computer is smashed by a meteor during the time it takes to run the test.)

Implementation

The convention is to use the ! suffix on methods that mutate the object.  Your predict! method doesn't really mutate the RandomEvent object, so I wouldn't name it with a !.1

Instead of doing the summation loop in predict!, it might be better to establish the cumulative thresholds in add_outcome, since that happens more rarely.



1 Functional programming purists would note that the method consumes randomness from the pseudorandom number generator when predict! calls rand, and thus it does have a side-effect.  By Ruby standards, though, I wouldn't consider it mutation.  Besides, your add_outcome is much more of a mutation than predict!.