Randomizing array indices
October 12, 2026 at 9:15 PM by Dr. Drang
I mentioned in my last post that I’m working my way through Practical Deep Learning by Ronald Kneusel. The math so far has been straightforward, and the code is written in Python, which I find easy to follow.
“Easy to follow” does not mean “correct” or “to my liking.” Some of the code in the book simply doesn’t work, and I’ve had to make changes here and there to get it to run. It’s possible that some of the APIs the book relies on have changed their calling conventions, but I also found a pretty obvious off-by-one error. And I’m only three chapters in.
This post, though, is about the “not to my liking” aspect of the code. I feel a little uncomfortable doing this, but I’ve been reading and writing data analysis code in Python for 15–20 years, so I’m going to put my discomfort to the side.
One of the examples used in the book is the well-known iris measurement dataset. There are 150 records in the dataset; here are the first ten:
| Index | Sepal length |
Sepal width |
Petal length |
Petal width |
Class |
|---|---|---|---|---|---|
| 0 | 5.1 | 3.5 | 1.4 | 0.2 | Iris-setosa |
| 1 | 4.9 | 3.0 | 1.4 | 0.2 | Iris-setosa |
| 2 | 4.7 | 3.2 | 1.3 | 0.2 | Iris-setosa |
| 3 | 4.6 | 3.1 | 1.5 | 0.2 | Iris-setosa |
| 4 | 5.0 | 3.6 | 1.4 | 0.2 | Iris-setosa |
| 5 | 5.4 | 3.9 | 1.7 | 0.4 | Iris-setosa |
| 6 | 4.6 | 3.4 | 1.4 | 0.3 | Iris-setosa |
| 7 | 5.0 | 3.4 | 1.5 | 0.2 | Iris-setosa |
| 8 | 4.4 | 2.9 | 1.4 | 0.2 | Iris-setosa |
| 9 | 4.9 | 3.1 | 1.5 | 0.1 | Iris-setosa |
There are three species of iris in the dataset: setosa, versicolor, and virginica. The records, which are easy to find, are sorted according to class, and it’s necessary to randomize the records for certain operations.
Let’s say the measurements are in a 2D (150×4) NumPy array called x, and the classes, which have been reduced to the integers 0, 1, and 2 for setosa, versicolor, and virginica, respectively, are in a 1D NumPy array called y. Here they are in an interactive Python session:
>>> x
array([[5.1, 3.5, 1.4, 0.2],
[4.9, 3. , 1.4, 0.2],
[4.7, 3.2, 1.3, 0.2],
[4.6, 3.1, 1.5, 0.2],
[5. , 3.6, 1.4, 0.2],
[140 other rows],
[6.7, 3. , 5.2, 2.3],
[6.3, 2.5, 5. , 1.9],
[6.5, 3. , 5.2, 2. ],
[6.2, 3.4, 5.4, 2.3],
[5.9, 3. , 5.1, 1.8]])
>>> y
array([0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1,
1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2,
2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2, 2])
Kneusel uses NumPy’s integer array indexing to randomize y and the rows of x using the same rearrangement in each. Here’s what he does:
>>> idx = np.argsort(np.random.random(x.shape[0]))
>>> x = x[idx, :]
>>> y = y[idx]
What this does is generate an array of 150 (x.shape[0] is 150) random numbers between 0 and 1. It then returns an array of the indices of the sorted version of that array. So idx is an array of the integers from 0 through 149 in random order. The following two lines rearrange x and y in that same random order.
I don’t know about you, but I see this as a weird way to go about randomization. I realize you can’t just shuffle x and y individually, as that would break the connection between an iris’s measurements and its class. But why go through two steps when you can create the idx array directly?
>>> idx = np.random.permutation(x.shape[0])
Maybe Kneusel’s method is faster, even though it seems like it should be slower. This won’t matter for the small iris dataset, but he may be preparing his readers for huge datasets to come.
I tested the two methods using IPython’s %timeit magic command. Here’s what I found, with 1,000,000 indices instead of 150:
>>> %timeit np.argsort(np.random.random(1_000_000))
63.8 ms ± 434 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
>>> %timeit np.random.permutation(1_000_000)
5.98 ms ± 17.8 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
So the simple code is both more direct and about an order of magnitude faster. I’ll be looking at the code in the book with a more critical eye as I proceed—assuming I don’t bail out and look for another machine learning reference.
You may have noticed that the code above uses NumPy’s older random number system. The current approach would be to create a Generator instance and use that as the basis for later function calls. Would this have any effect on the timing results? Let’s see.
>>> rng = np.random.default_rng()
>>> %timeit np.argsort(rng.random(1_000_000))
65 ms ± 188 μs per loop (mean ± std. dev. of 7 runs, 10 loops each)
>>> %timeit rng.permutation(1_000_000)
4.91 ms ± 10.9 μs per loop (mean ± std. dev. of 7 runs, 100 loops each)
No, the runtimes are about the same.