112

When reading how to use std::rand, I found this code on cppreference.com

int x = 7;
while(x > 6) 
    x = 1 + std::rand()/((RAND_MAX + 1u)/6);  // Note: 1+rand()%6 is biased

What is wrong with the expression on the right? Tried it and it works perfectly.

Andrew T.
  • 4,701
  • 8
  • 43
  • 62
yO_
  • 1,135
  • 2
  • 12
  • 18
  • The code is fine. My reading of the comment says don't use mod (%), saying you want the integer result of division, not the remainder. – KeithSmith Apr 17 '18 at 13:11
  • 24
    Note that it's *even better* to use `std::uniform_int_distribution` for dice – Caleth Apr 17 '18 at 13:16
  • 1
    @Caleth Yes, it was just to understand why this code was 'wrong' .. – yO_ Apr 17 '18 at 13:18
  • 17
    Changed "is wrong" to "is biased" – Cubbi Apr 17 '18 at 14:20
  • See also: [How to generate a random integer number from within a range](https://stackoverflow.com/questions/2509679/how-to-generate-a-random-integer-number-from-within-a-range). – dbush Apr 17 '18 at 15:29
  • some bits are usually less random than others. – mathreadler Apr 17 '18 at 17:46
  • This video may be helpful where the presenter really did a why-is-this-code-wrong at the beginning. (Ignore me if anyone else has already posted it.) https://channel9.msdn.com/Events/GoingNative/2013/rand-Considered-Harmful – pjhades Apr 17 '18 at 18:13
  • 3
    `rand()` is so bad in typical implementations, you might as well use the [xkcd RNG](https://xkcd.com/221/). So it's wrong because it uses `rand()`. – CodesInChaos Apr 17 '18 at 18:29
  • 3
    I wrote this thing (well, not the comment - that's @Cubbi) and what I had in mind at the time was what [Pete Becker's answer](https://stackoverflow.com/a/49880109/2756719) explained. (FYI, this is basically the same algorithm as libstdc++'s `uniform_int_distribution`.) – T.C. Apr 17 '18 at 23:52
  • 1
    When you say it works perfectly, do you mean it you see no problems with the sequence of results? Have you tried any sort of analysis? I once used `rand()%2` and the results were strictly alternating `0` and `1`! – PJTraill Apr 18 '18 at 09:08
  • @PJTraill The original comment was that this code was "wrong". So I thought it was generating a bug! Intrigated why, I tried and it gave a few results that seemed normal. But yes, it wasn't a statistical analysis. – yO_ Apr 18 '18 at 10:09
  • After the editing this question is now very similar to [this one](https://stackoverflow.com/questions/10984974/why-do-people-say-there-is-modulo-bias-when-using-a-random-number-generator), whose accepted answer has more detail than this question's accepted answer. – user7761803 Apr 18 '18 at 13:10
  • 1
    Amusingly, the "correct" code in the question still exhibits non-uniformity, as explained in the video linked to by @PJ.Hades. – user7761803 Apr 18 '18 at 13:22
  • https://channel9.msdn.com/Events/GoingNative/2013/rand-Considered-Harmful Here's a talk from a guy who explains really well why rand() should be avoided. – kevin Apr 18 '18 at 14:19
  • @user7761803 Nope, it does not. – T.C. Apr 18 '18 at 23:06
  • 1
    How do you define “works perfectly”? Did you do any tests to see if dice rolls have the expected statistical distribution? – dan04 Apr 18 '18 at 23:41
  • 1
    @dan04: Not only individual results, but also relationships between successive results need checking to uncover things like alternating even & odd. – PJTraill Apr 19 '18 at 07:13

5 Answers5

141

There are two issues with rand() % 6 (the 1+ doesn't affect either problem).

First, as several answers have pointed out, if the low bits of rand() aren't appropriately uniform, the result of the remainder operator is also not uniform.

Second, if the number of distinct values produced by rand() is not a multiple of 6, then the remainder will produce more low values than high values. That's true even if rand() returns perfectly distributed values.

As an extreme example, pretend that rand() produces uniformly distributed values in the range [0..6]. If you look at the remainders for those values, when rand() returns a value in the range [0..5], the remainder produces uniformly distributed results in the range [0..5]. When rand() returns 6, rand() % 6 returns 0, just as if rand() had returned 0. So you get a distribution with twice as many 0's as any other value.

The second is the real problem with rand() % 6.

The way to avoid that problem is to discard values that would produce non-uniform duplicates. You calculate the largest multiple of 6 that's less than or equal to RAND_MAX, and whenever rand() returns a value that's greater than or equal to that multiple you reject it and call `rand() again, as many times a needed.

So:

int max = 6 * ((RAND_MAX + 1u) / 6)
int value = rand();
while (value >= max)
    value = rand();

That's a different implementation of the code in question, intended to more clearly show what's going on.

T.C.
  • 133,968
  • 17
  • 288
  • 421
Pete Becker
  • 74,985
  • 8
  • 76
  • 165
  • 2
    I have promised at least one regular on this site to produce a paper on this but I *think* that sampling and rejection *can* throw high moments off; e.g. overinflate the variance. – Bathsheba Apr 17 '18 at 14:09
  • 31
    I did a graph of how much bias this technique introduces if rand_max is 32768, which it is in some implementations. https://ericlippert.com/2013/12/16/how-much-bias-is-introduced-by-the-remainder-technique/ – Eric Lippert Apr 17 '18 at 14:14
  • 1
    @EricLippert -- 32767 is the minimum allowed value, and I suspect it's quite common. But that's not particularly important. Yes, the effect is small for small target ranges like `[0..5]`, but for a generic implementation it's not inherently small, and the usual technique for implementing range restrictions is to discard out-of-range values. – Pete Becker Apr 17 '18 at 14:19
  • 2
    @Bathsheba: it's true that some rejection functions could cause this, but this simple rejection will transform a uniform IID into a different uniform IID distribution. No bits carry over, so independent, all samples use same rejection so identical, and trivial to show uniformity. And the higher moments of a uniform integral random variable are fully defined by its range. – MSalters Apr 17 '18 at 16:14
  • 4
    @MSalters: Your first sentence is correct for a *true* generator, not necessarily true for a pseudo generator. When I retire, I'm going to write a paper on this. – Bathsheba Apr 17 '18 at 16:20
  • @Bathsheba: The big problem with PRNG's is of course that they are practically by definition not IID. Outcomes share a common dependency, the hidden state. – MSalters Apr 17 '18 at 16:29
  • I'm trying to understand this in relationship to other (less strict) languages, namely PHP. So I'm wanting to take a random number from my full random number range and then divide it by my largest possible int and divide that by 6, is that right? I tested with `$random_seed = random_int(0 , PHP_INT_MAX); $random = 1 + $random_seed/(PHP_INT_MAX/6); $other_random = 1 + $random_seed % 6;` expecting similar results. Is that a misunderstanding? And is `1 + $random_seed/(PHP_INT_MAX/6)` the equivalent to the `1 + std::rand()/((RAND_MAX + 1u)/6)`? – Anthony Apr 17 '18 at 23:28
  • 2
    @Anthony Think in terms of dice. You want a random number between 1 and 3 and you only have a standard 6-sided die. You can get that by just subtracting 3 if you roll a 4-6. But let's say instead you want a number between 1 and 5. If you subtract 5 when you roll a 6, then you'll end up with twice as many 1s as any other number. That's basically what the cppreference code is doing. The correct thing to do is to reroll the 6s. That's what Pete's doing here: divide the die up so there are the same number of ways to roll each number, and reroll any numbers that didn't fit into the even divisions – Ray Apr 18 '18 at 17:00
  • AIUI all bits of the input affect the output when the divisor is an odd number, and only the lowest bit (i.e. x%2) will have this problem for 6. But of course the lowest bit is the *worst* on bad implementations – Random832 Apr 18 '18 at 17:21
  • I'm not suggesting that this is a better technique, but if you took the approach of: `(rand()%6+rand()%6+rand()%6+rand()%6+rand()%6+rand()%6)%6` wouldn't that make it a result which has no bias? Provided that it was uniform of course. – Ryan Beesley Apr 18 '18 at 20:22
  • 1
    @RyanBeesley -- I don't have an exact cite, but I vaguely recall that Donald Knuth investigated combining multiple (flawed) pseudo-random number generators, and found that the result was worse. – Pete Becker Apr 18 '18 at 20:27
  • @PeteBecker, interesting. I would think that since 6*rand() would be a multiple of 6, ignoring the potential overflow, adding 6 rand() values and then modulo would address that concern. There are other problems with rand() in that it alternates odd/even, so maybe this is a false sense. That fact alone would probably invalidate my thinking. – Ryan Beesley Apr 18 '18 at 20:50
  • 2
    @RyanBeesley -- `rand()` is not required to alternate odd/even, and good implementations don't. – Pete Becker Apr 18 '18 at 21:21
  • @RyanBeesley -- adding `rand()%6` six times is not the same as `6*rand()`. Each call to `rand()` can return a different value... – Pete Becker Apr 18 '18 at 21:22
  • 1
    @RyanBeesley That solution doesn't work. Suppose `roll()` generates a random integer from `[1-x]`. Then rolling `y` times generates`x^y` equally likely outcomes where an outcome is the ordered multiset containing the result of all rolls (EG. for `x = 9, y = 3`, some outcomes are `[1,1,1],[1,8,5],[8,5,1],[9,9,9],[1,1,5],[1,5,1]`). Those outcomes can only be distributed evenly between 6 final values when the size of the set of all outcomes, `x^y`, is a multiple of 6. That only happens when `x` is a multiple of 6 and doesn't depend on `y` (the number of rolls). – Paul Apr 18 '18 at 22:58
  • @RyanBeesley The reasoning that applies to 6 also generalises to all square-free numbers, but a non square-free number such as 4 is a bit different because `x^y` can be a multiple of 4 even when `x` isn't, so the number of rolls might actually impact it. If you wanted to generate a random number in `[1-4]` from a random function that returns `[1-x]` you could do so whenever `x` is a multiple of 4, but also when `x` is a multiple of 2 and `y >= 2`. EG. `x = 6, y = 2`. – Paul Apr 18 '18 at 22:58
  • 1
    @RyanBeesley You still can't generate a number in `[1-4]` uniformly at random by doing `(roll()%4 + roll()%4)%4`, but the fact that 4 divides `6^2 = 36` means there is an algorithm that guarantees a uniform number in ``[1-4]` after two dice rolls. Here is one example (in JavaScript, and assuming `roll()` returns an integer in `[1-6]` uniformly at random): `1 + Math.floor((roll()-1)/3) + 2*Math.floor((roll()-1)/3)`. – Paul Apr 18 '18 at 22:58
  • @RyanBeesley In English, roll two dice (`die1` and `die2`). If both show 3 or less, let the result be **1**. If neither shows 3 or less, let the result be **4**. If `die1` shows 3 or less and `die2` doesn't ,let the result be **3**. If `die2` shows 3 or less and `die1` doesn't, let the result be **2**. Four equally likely outcomes from rolling two six-sided dice – Paul Apr 18 '18 at 22:59
  • I guess once the condition `value >= max` get false then `value` should be divided by `((RAND_MAX + 1u) / 6)` in order to get values between `[0, 6)`. – haccks Jun 19 '18 at 07:25
19

There are hidden depths here:

  1. The use of the small u in RAND_MAX + 1u. RAND_MAX is defined to be an int type, and is often the largest possible int. The behaviour of RAND_MAX + 1 would be undefined in such instances as you'd be overflowing a signed type. Writing 1u forces type conversion of RAND_MAX to unsigned, so obviating the overflow.

  2. The use of % 6 can (but on every implementation of std::rand I've seen doesn't) introduce any additional statistical bias above and beyond the alternative presented. Such instances where % 6 is hazardous are cases where the number generator has correlation plains in the low order bits, such as a rather famous IBM implementation (in C) of rand in, I think, the 1970s which flipped the high and low bits as "a final flourish". A further consideration is that 6 is very small cf. RAND_MAX, so there will be a minimal effect if RAND_MAX is not a multiple of 6, which it probably isn't.

In conclusion, these days, due to its tractability, I'd use % 6. It's not likely to introduce any statistical anomalies beyond those introduced by the generator itself. If you are still in doubt, test your generator to see if it has the appropriate statistical properties for your use case.

Jonathan Leffler
  • 730,956
  • 141
  • 904
  • 1,278
Bathsheba
  • 231,907
  • 34
  • 361
  • 483
  • 13
    `% 6` produces a biased result whenever the number of distinct values generated by `rand()` is not a multiple of 6. Pigeon-hole principle. Granted, the bias is small when `RAND_MAX` is much larger than 6, but it's there. And for larger target ranges the effect is, of course, larger. – Pete Becker Apr 17 '18 at 14:00
  • 2
    @PeteBecker: Indeed, I should make that clear. But note that you also get pigeon-holing as you sample range approaches RAND_MAX, due to integer division truncation effects. – Bathsheba Apr 17 '18 at 14:01
  • 2
    @Bathsheba doesn‘t that truncation effect lead to a result larger than 6 and thus in a repeated execution of the whole operation? – Gerhardh Apr 17 '18 at 14:40
  • 1
    @Gerhardh: Correct. In fact, it leads _exactly_ to the result `x==7`. bsically, you divide the range `[0, RAND_MAX]` in 7 subranges, 6 of the same size and one smaller subrange at the end. Outcomes from the last subrange are discarded. It's fairly obvious that you can't have two smaller sub-ranges at the end this way. – MSalters Apr 17 '18 at 16:07
  • @MSalters: Indeed. But do note that the other way still suffers due to truncation. My hypothesis is that folk plump for the latter since the statistical pitfalls are harder to comprehend! – Bathsheba Apr 17 '18 at 16:09
  • "Bad" RNG are more common than you would expect. For instance, students learn that a random walk on Z^2 is recurrent, but using uniform_int_distribution(0,1) and default_random_engine from libstdc++ produces a strong bias where whatever the seed, the walk escapes, always in the same quadrant... Changing the engine is enough to get something closer to what mathematics say. – Marc Glisse Apr 18 '18 at 13:59
13

This example code illustrates that std::rand is a case of legacy cargo cult balderdash that should make your eyebrows raise every time you see it.

There are several issues here:

The contract people usually assume—even the poor hapless souls who don't know any better and won't think of it in precisely these terms—is that rand samples from the uniform distribution on the integers in 0, 1, 2, …, RAND_MAX, and each call yields an independent sample.

The first problem is that the assumed contract, independent uniform random samples in each call, is not actually what the documentation says—and in practice, implementations historically failed to provide even the barest simulacrum of independence. For example, C99 §7.20.2.1 ‘The rand function’ says, without elaboration:

The rand function computes a sequence of pseudo-random integers in the range 0 to RAND_MAX.

This is a meaningless sentence, because pseudorandomness is a property of a function (or family of functions), not of an integer, but that doesn't stop even ISO bureaucrats from abusing the language. After all, the only readers who would be upset by it know better than to read the documentation for rand for fear of their brain cells decaying.

A typical historical implementation in C works like this:

static unsigned int seed = 1;

static void
srand(unsigned int s)
{
    seed = s;
}

static unsigned int
rand(void)
{
    seed = (seed*1103515245 + 12345) % ((unsigned long)RAND_MAX + 1);
    return (int)seed;
}

This has the unfortunate property that even though a single sample may be uniformly distributed under a uniform random seed (which depends on the specific value of RAND_MAX), it alternates between even and odd integers in consecutive calls—after

int a = rand();
int b = rand();

the expression (a & 1) ^ (b & 1) yields 1 with 100% probability, which is not the case for independent random samples on any distribution supported on even and odd integers. Thus, a cargo cult emerged that one should discard the low-order bits to chase the elusive beast of ‘better randomness’. (Spoiler alert: This is not a technical term. This is a sign that whosever prose you are reading either doesn't know what they're talking about, or thinks you are clueless and must be condescended to.)

The second problem is that even if each call did sample independently from a uniform random distribution on 0, 1, 2, …, RAND_MAX, the outcome of rand() % 6 would not be uniformly distributed in 0, 1, 2, 3, 4, 5 like a die roll, unless RAND_MAX is congruent to -1 modulo 6. Simple counterexample: If RAND_MAX = 6, then from rand(), all outcomes have equal probability 1/7, but from rand() % 6, the outcome 0 has probability 2/7 while all other outcomes have probability 1/7.

The right way to do this is with rejection sampling: repeatedly draw an independent uniform random sample s from 0, 1, 2, …, RAND_MAX, and reject (for example) the outcomes 0, 1, 2, …, ((RAND_MAX + 1) % 6) - 1—if you get one of those, start over; otherwise, yield s % 6.

unsigned int s;
while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6)
    continue;
return s % 6;

This way, the set of outcomes from rand() that we accept is evenly divisible by 6, and each possible outcome from s % 6 is obtained by the same number of accepted outcomes from rand(), so if rand() is uniformly distributed then so is s. There is no bound on the number of trials, but the expected number is less than 2, and the probability of success grows exponentially with the number of trials.

The choice of which outcomes of rand() you reject is immaterial, provided that you map an equal number of them to each integer below 6. The code at cppreference.com makes a different choice, because of the first problem above—that nothing is guaranteed about the distribution or independence of outputs of rand(), and in practice the low-order bits exhibited patterns that don't ‘look random enough’ (never mind that the next output is a deterministic function of the previous one).

Exercise for the reader: Prove that the code at cppreference.com yields a uniform distribution on die rolls if rand() yields a uniform distribution on 0, 1, 2, …, RAND_MAX.

Exercise for the reader: Why might you prefer one or the other subsets to reject? What computation is needed for each trial in the two cases?

A third problem is that the seed space is so small that even if the seed is uniformly distributed, an adversary armed with knowledge of your program and one outcome but not the seed can readily predict the seed and subsequent outcomes, which makes them seem not so random after all. So don't even think about using this for cryptography.

You can go the fancy overengineered route and C++11's std::uniform_int_distribution class with an appropriate random device and your favorite random engine like the ever-popular Mersenne twister std::mt19937 to play at dice with your four-year-old cousin, but even that is not going to be fit for generating cryptographic key material—and the Mersenne twister is a terrible space hog too with a multi-kilobyte state wreaking havoc on your CPU's cache with an obscene setup time, so it is bad even for, e.g., parallel Monte Carlo simulations with reproducible trees of subcomputations; its popularity likely arises mainly from its catchy name. But you can use it for toy dice rolling like this example!

Another approach is to use a simple cryptographic pseudorandom number generator with a small state, such as a simple fast key erasure PRNG, or just a stream cipher such as AES-CTR or ChaCha20 if you are confident (e.g., in a Monte Carlo simulation for research in the natural sciences) that there are no adverse consequences to predicting past outcomes if the state is ever compromised.

  • 4
    "an obscene setup time" You shouldn't really be using more than one random number generator (per thread) anyway, so setup time will be amortized unless your program doesn't run very long. – JAB Apr 17 '18 at 15:56
  • 1
    "because pseudorandomness is a property of a function (or family of functions), not of an integer" - you're missing the point that ISO C says the values are taken from a distribution (namely one from 0 to `RAND_MAX`. Unfortunately, it fails to further narrow this to uniformly distributed, but your specific critique here I don't agree with. – MSalters Apr 17 '18 at 16:17
  • 2
    Downvote BTW for not understanding that the loop in the question is doing the exact same rejection sampling, of exactly the same `(RAND_MAX + 1 )% 6` values. It doesn't matter _how_ your subdivide the possible outcomes. You can reject them from anywhere in the range `[0, RAND_MAX)`, as long as the size of the accepted range is a multiple of 6. Hell, you can flat out reject any outcome `x>6`, and you won't need the `%6` anymore. – MSalters Apr 17 '18 at 16:25
  • 13
    I’m not quite happy with this answer. Rants can be good but you’re taking it into the wrong direction. For example, you complain that “better randomness” isn’t a technical term and that it’s meaningless. This is half true. Yes, it’s not a technical term, but it’s a perfectly meaningful shorthand in context. To insinuate that users of such a term are either ignorant or malicious is, itself, one of these things. “Good randomness” may be very hard to define precisely but it’s easy enough to grasp when a function produces results with better or worse randomness properties. – Konrad Rudolph Apr 17 '18 at 16:29
  • 3
    I liked this answer. It is a bit of rant, but it's got lots of good background information. Bear in mind, the REAL experts only ever use hardware random generators, the problem is that hard. – Tiger4Hire Apr 17 '18 at 16:49
  • 11
    For me it's the reverse. While it does contain good information, it's too much of a rant to come across as anything but opinion. Usefulness aside. – Mr Lister Apr 17 '18 at 17:16
  • +1 for educational value. -1 for non-brevity in regards to OPs actual question. +1 for being well written even so. – Stian Apr 18 '18 at 09:10
  • `while ((s = rand()) < ((unsigned long)RAND_MAX + 1) % 6) continue;` is this really correct? If I get it right it means, discard any s that is smaller than something between 0 or 5 depending on `RAND_MAX` – Kami Kaze Apr 18 '18 at 09:26
  • @KamiKaze: yes, it's right. It ensures that `s` is between `a` and `RAND_MAX`, where `RAND_MAX - a` is an exact multiple of 6. It's easier to write than discarding values from the high end, but has the same effect. – Toby Speight Apr 19 '18 at 08:40
  • I'd probably do the rejection sampling as `do { s = rand(); } while (s < (RAND_MAX - 5) % 6);` - that avoids the cast and the `break`. Just looks clearer to me. – Toby Speight Apr 19 '18 at 08:44
3

One can think of a random number generator as working on a stream of binary digits. The generator turns the stream into numbers by slicing it up into chunks. If the std:rand function is working with a RAND_MAX of 32767, then it is using 15 bits in each slice.

When one takes the modules of a number between 0 and 32767 inclusive one finds that 5462 '0's and '1's but only 5461 '2's, '3's, '4's, and '5's. Hence the result is biased. The larger the RAND_MAX value is, the less bias there will be, but it is inescapable.

What is not biased is a number in the range [0..(2^n)-1]. You can generate a (theoretically) better number in the range 0..5 by extracting 3 bits, converting them to an integer in the range 0..7 and rejecting 6 and 7.

One hopes that every bit in the bit stream has an equal chance of being a '0' or a '1' irrespective of where it is in the stream or the values of other bits. This is exceptionally difficult in practice. The many different implementations of software PRNGs offer different compromises between speed and quality. A linear congruential generator such as std::rand offers fastest speed for lowest quality. A cryptographic generator offers highest quality for lowest speed.

Simon G.
  • 6,587
  • 25
  • 30
2

I'm not an experienced C++ user by any means, but was interested to see if the other answers regarding std::rand()/((RAND_MAX + 1u)/6) being less biased than 1+std::rand()%6 actually holds true. So I wrote a test program to tabulate the results for both methods (I haven't written C++ in ages, please check it). A link for running the code is found here. It's also reproduced as follows:

// Example program
#include <cstdlib>
#include <iostream>
#include <ctime>
#include <string>

int main()
{
    std::srand(std::time(nullptr)); // use current time as seed for random generator

    // Roll the die 6000000 times using the supposedly unbiased method and keep track of the results

    int results[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::rand()/((RAND_MAX + 1u)/6);  // Note: 1+rand()%6 is biased

        results[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results[n] << ' ';
    }

    std::cout << "\n";


    // Roll the die 6000000 times using the supposedly biased method and keep track of the results

    int results_bias[6] = {0,0,0,0,0,0};

    // roll a 6-sided die 20 times
    for (int n=0; n != 6000000; ++n) {
        int x = 7;
        while(x > 6) 
            x = 1 + std::rand()%6;

        results_bias[x-1]++;
    }

    for (int n=0; n !=6; n++) {
        std::cout << results_bias[n] << ' ';
    }
}

I then took the output of this and used the chisq.test function in R to run a Chi-square test to see if the results are significantly different than expected. This stackexchange question goes into more detail of using the chi-square test to test die fairness: How can I test whether a die is fair?. Here are the results for a few runs:

> ?chisq.test
> unbias <- c(100150, 99658, 100319, 99342, 100418, 100113)
> bias <- c(100049, 100040, 100091, 99966, 100188, 99666 )

> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 8.6168, df = 5, p-value = 0.1254

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 1.6034, df = 5, p-value = 0.9008

> unbias <- c(998630, 1001188, 998932, 1001048, 1000968, 999234 )
> bias <- c(1000071, 1000910, 999078, 1000080, 998786, 1001075   )
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.051, df = 5, p-value = 0.2169

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 4.319, df = 5, p-value = 0.5045

> unbias <- c(998630, 999010, 1000736, 999142, 1000631, 1001851)
> bias <- c(999803, 998651, 1000639, 1000735, 1000064,1000108)
> chisq.test(unbias)

Chi-squared test for given probabilities

data:  unbias
X-squared = 7.9592, df = 5, p-value = 0.1585

> chisq.test(bias)

Chi-squared test for given probabilities

data:  bias
X-squared = 2.8229, df = 5, p-value = 0.7273

In the three runs that I did, the p-value for both methods was always greater than typical alpha values used to test significance (0.05). This means that we wouldn't consider either of them to be biased. Interestingly, the supposedly unbiased method has consistently lower p-values, which indicates that it might actually be more biased. The caveat being that I only did 3 runs.

UPDATE: While I was writing my answer, Konrad Rudolph posted an answer that takes the same approach, but gets a very different result. I don't have the reputation to comment on his answer, so I'm going to address it here. First, the main thing is that the code he uses uses the same seed for the random number generator every time it's run. If you change the seed, you actually get a variety of results. Second, if you don't change the seed, but change the number of trials, you also get a variety of results. Try increasing or decreasing by an order of magnitude to see what I mean. Third, there is some integer truncation or rounding going on where the expected values aren't quite accurate. It probably isn't enough to make a difference, but it's there.

Basically, in summary, he just happened to get the right seed and number of trials that he might be getting a false result.

anjama
  • 400
  • 2
  • 9
  • Your implementation contains a fatal flaw due to a misunderstanding on your part: the quoted passage is **not** comparing `rand()%6` with `rand()/(1+RAND_MAX)/6`. Rather, it is comparing straightforward taking of the remainder with *rejection sampling* (see other answers for an explanation). Consequently, your second code is wrong (the `while` loop does nothing). Your statistical testing also has issues (you can’t just run repeats of your test for robustness, you didn’t perform correction, …). – Konrad Rudolph Apr 17 '18 at 16:43
  • 1
    @KonradRudolph I don't have the rep to comment on your answer, so I added it as an update to mine. Your's also has a fatal flaw in that it happens to use a set seed and number of trials every run that gives a false result. If you had run repeats with different seeds, you might have caught that. But yes, you are correct the while loop does nothing, but it also doesn't change the results of that particular code block – anjama Apr 17 '18 at 16:58
  • I did run repeats, actually. The seed intentionally isn’t set since setting a random seed with `std::srand` (and no use of ``) [is quite hard to do in a standards conforming way](http://www.eternallyconfuzzled.com/tuts/algorithms/jsw_tut_rand.aspx) and I didn’t want its complicatedness to detract from the remaining code. It’s also irrelevant for the calculation: repeating the same sequence in a simulation is entirely acceptable. Of course different seeds *will* yield different results, and some will be non-significant. That’s entirely expected based on how the p-value is defined. – Konrad Rudolph Apr 17 '18 at 17:03
  • 1
    Rats, I made a mistake in my repeats; and you’re right, 95th quantile of the repeat runs is quite close to p=0.05 — i.e. exactly what we’d expect under then null. In sum, my standard library implementation of `std::rand` yields remarkably good coin toss simulations for a d6, across the range of random seeds. – Konrad Rudolph Apr 17 '18 at 17:19
  • 1
    The statistical _significance_ is only one part of the story. You have a null hypothesis (uniformly distributed) and an alternative hypothesis (modulo bias)—actually, a family of alternative hypotheses, indexed by the choice of `RAND_MAX`, which determines the _effect size_ of the modulo bias. The statistical significance is the probability under the null hypothesis that you falsely reject it. What's the _statistical power_ — the probability under an alternative hypothesis that your test _correctly_ rejects the null hypothesis? Would you detect `rand() % 6` this way when RAND_MAX = 2^31 - 1? – Squeamish Ossifrage Apr 17 '18 at 22:10
  • “20 times” but the loop goes to 6 with a unch of zeros? – JDługosz Apr 18 '18 at 11:07
  • How about adding a case for C++11’s random library, and see how the statistics show? – JDługosz Apr 18 '18 at 11:13
  • You're only testing for the aggregate results, not for, as in @Squeamish Ossifrage answer, the high correlation between successive rolls. One way to check for this is to save series of rolls (like 4 rolls in a row), and check if they are evenly distributed. In his example, some sequences would even be completely absent, showing serious bias. Making rolling 20 6s in a row impossible is not a huge problem, but making 4 6s in a row impossible will look seriously biased to users that roll a lot of dice. – meneldal Apr 19 '18 at 03:46