Repository: numba/numba

Issue:
np.random.choice doesn't adhere to np.random.seed inside of numba
np.random.choice does not seem to be following the np.random.seed the same way other np.random functions are, from within numba:

Example code:
```
choice_array = np.random.randint(300, size=1000).astype(np.int32)

# select two random values without replacement from an array:
def nprands(n_to_return):
    np.random.seed(1337)
    out = np.empty((n_to_return, 2), np.int32)
    for i in range(n_to_return):
        out[i] = np.random.choice(choice_array, 2, False)
    return out

nprands(5)

>>>
##  array([[286, 101],
##       [ 60, 126],
##       [225, 161],
##       [175,  17],
##       [125,  33]], dtype=int32)

@numba.njit
def numba_rands(n_to_return):
    np.random.seed(1337)    
    out = np.empty((n_to_return, 2), np.int32)
    for i in range(n_to_return):
        out[i] = np.random.choice(choice_array, 2, False)
    return out

numba_rands(5)

>>>
##  array([[233,  86],
##       [ 68, 243],
##       [257, 252],
##       [254, 182],
##       [  5,  64]], dtype=int32)





Discussion:
Thanks for the report. This is a bug, I think it's down to implementation differences between NumPy and Numba.

Resolving patch:
diff --git a/numba/cpython/randomimpl.py b/numba/cpython/randomimpl.py
--- a/numba/cpython/randomimpl.py
+++ b/numba/cpython/randomimpl.py
@@ -1413,14 +1413,15 @@ def choice_impl(a, size=None, replace=True):
                 if out.size > n:
                     raise ValueError("Cannot take a larger sample than "
                                      "population when 'replace=False'")
-                # Get a contiguous copy of the source so as to permute it
-                src = copy_source(a)
+                # Get a permuted copy of the source array
+                # we need this implementation in order to get the 
+                # np.random.choice inside numba to match the output
+                # of np.random.choice outside numba when np.random.seed
+                # is set to the same value
+                permuted_a = np.random.permutation(a)
                 fl = out.flat
                 for i in range(len(fl)):
-                    j = np.random.randint(i, n)
-                    fl[i] = src[j]
-                    # Move away selected element
-                    src[j] = src[i]
+                    fl[i] = permuted_a[i]
                 return out
 
     return choice_impl


Test patch:
diff --git a/numba/tests/test_random.py b/numba/tests/test_random.py
--- a/numba/tests/test_random.py
+++ b/numba/tests/test_random.py
@@ -1202,6 +1202,27 @@ def test_choice_array_3(self):
         pop = np.arange(50) * 2 + 100
         self._check_choice_3(pop, pop)
 
+    def test_choice_follows_seed(self):
+        # See issue #3888, np.random.choice must acknowledge the seed
+
+        @jit(nopython=True)
+        def numba_rands(n_to_return, choice_array):
+            np.random.seed(1337)
+            out = np.empty((n_to_return, 2), np.int32)
+            for i in range(n_to_return):
+                out[i] = np.random.choice(choice_array, 2, False)
+            return out
+
+        choice_array = np.random.randint(300, size=1000).astype(np.int32)
+        tmp_np = choice_array.copy()
+        expected = numba_rands.py_func(5, tmp_np)
+        tmp_nb = choice_array.copy()
+        got = numba_rands(5, tmp_nb)
+        np.testing.assert_allclose(expected, got)
+        # check no mutation
+        np.testing.assert_allclose(choice_array, tmp_np)
+        np.testing.assert_allclose(choice_array, tmp_nb)
+
 
 class TestRandomMultinomial(BaseTest):
     """