@Alain T. already answers the specific question about graphs.
My answer will address the generic question: "What's the fastest way of finding a random index in a Python list, a large number of times?", meaning generating random subsample of N elements from a given list or NumPy array.
We will evaluate the following methods:
## Returns Python list
random_subset = random.sample(data, N)
## Returns numpy.ndarray
random_selection = np.random.choice(data, N)
## Obtain a single random idx using random.randrange, N times, used to index data
random_selection = [data[idx] for idx in (random.randrange(0, len(data)) for _ in range(N))]
## Obtain a single random idx using using np.random.randint, N times, used to index data
random_selection = [data[idx] for idx in (np.random.randint(0, len(data)) for _ in range(N))]
## Create an NumPy array of `N` random indexes using np.random.randint, used to index data
random_selection = [data[idx] for idx in np.random.randint(0, len(data), N)]
Assume we have 1MM data values (the exact values do not matter).
data_nparray = np.random.random(int(1e6))
data_list = list(data_nparray)
As it turns out, when evaluating the fastest way to generate a series of random elements, the answer changes if data stored as a list or a NumPy array.
Random selection from a Python list or tuple

- The plot above shows the increase time (microseconds) when randomly selecting elements from a Python list.
- For <50 elements,
random.sample is the fastest (i.e. random.sample(data, N))
- For >= 50 elements, it is fastest to create an array of
N random indexes using np.random.randint (i.e. [data[idx] for idx in np.random.randint(0, len(data), N)])
np.random.choice is extremely slow here, as it converts the data into a NumPy array each time. However, for >100,000 selections, it becomes the fastest option.
- You can probably get away with using
random.sample throughout.
- The same results were found to hold for Python tuples as well.
Random selection from a NumPy array

- The plot above shows the increase time (microseconds) when randomly selecting elements from a NumPy array
random.sample does not work with NumPy arrays, only lists. Converting 1MM elements from a NumPy array to a list itself takes ~23,000 microseconds on average, so we will skip random.sample.
- For <10 elements, it is fastest to select
N random indexes one-by-one using random.randrange (i.e. [data[idx] for idx in (random.randrange(0, len(data)) for _ in range(N))])
- For 10 to 50 elements, it is fastest to create an array of
N random indexes using np.random.randint (i.e. [data[idx] for idx in np.random.randint(0, len(data), N)])
- For >50 elements, it is fastest to use
np.random.choice (i.e. np.random.choice(data, N)) which also returns a NumPy array, not a list.
- You can probably get away with using
random.randrange for <25 elements, then switching to np.random.choice.
Code to reproduce
import time
import random
import numpy as np
import pandas as pd
from IPython.display import display
def time_fn(fn, num_runs=7, num_loops=1_000):
run_times = []
for num_run_i in range(num_runs):
start = time.time()
for _ in range(num_loops):
fn()
end = time.time()
run_times.append((end-start)/num_loops)
return run_times
def create_times_df_row(data, N, fn, fn_name, num_runs=7, num_loops=1_000):
try:
run_times = time_fn(fn, num_runs=num_runs, num_loops=num_loops)
return [
{
'N': N,
'function': fn_name,
'run_time_avg': np.mean(run_times),
'run_time_std': np.std(run_times),
'num_runs': num_runs,
'num_loops': num_loops,
'input_type': str(type(data)),
'return_type': str(type(fn())),
}
]
except Exception as e:
print(e)
return []
data_nparray = np.random.random(int(1e4))
data_list = list(data_nparray)
all_times_df = []
for N in list(range(1, 10)) + list(range(10, 25, 5)) + list(range(25, 100, 25)) + list(range(100, 1000+1, 100)):
times_df = []
for data in [data_nparray, data_list]:
num_loops = 1_000
if N >= 100:
num_loops = 100
if N >= 1_000:
num_loops = 10
times_df.extend(create_times_df_row(
data=data,
N=N,
fn=lambda: np.random.choice(data, N),
fn_name='np.random.choice',
num_loops=num_loops if isinstance(data, np.ndarray) else 10,
))
times_df.extend(create_times_df_row(
data=data,
N=N,
fn=lambda: random.sample(data, N),
fn_name='random.sample',
num_loops=num_loops,
))
times_df.extend(create_times_df_row(
data=data,
N=N,
fn=lambda: [data[idx] for idx in (random.randrange(0, len(data)) for _ in range(N))],
fn_name='random.randrange',
num_loops=num_loops,
))
times_df.extend(create_times_df_row(
data=data,
N=N,
fn=lambda: [data[idx] for idx in (np.random.randint(0, len(data)) for _ in range(N))],
fn_name='np.random.randint (single idx)',
num_loops=num_loops,
))
times_df.extend(create_times_df_row(
data=data,
N=N,
fn=lambda: [data[idx] for idx in np.random.randint(0, len(data), N)],
fn_name='np.random.randint (idx array)',
num_loops=num_loops,
))
print(N, end=' ')
times_df = pd.DataFrame(times_df)
display(times_df)
all_times_df.append(times_df)
all_times_df = pd.concat(all_times_df).reset_index(drop=True)
from __future__ import division
import numpy as np
import matplotlib
import matplotlib.pyplot as plt
import time
from typing import *
import pandas as pd
import seaborn as sns
import matplotlib.pyplot as plt
# Assignment Constants
RANDOM_STATE = 10
FIGSIZE = (12.0, 9.0)
#### Use the following line before plt.plot(....) to increase the plot size ####
plt.figure(figsize=FIGSIZE)
### Adjust some plot display options:
sns.set(rc={
'figure.figsize':FIGSIZE,
'axes.facecolor':'white',
'figure.facecolor':'white',
'grid.color': 'gainsboro',
})
sns.set_context("talk")
def time_to_ms(row):
row['run_time_avg'] = 1000*row['run_time_avg']
row['run_time_std'] = 1000*row['run_time_std']
return row
def time_to_us(row):
row['run_time_avg'] = 1e6*row['run_time_avg']
row['run_time_std'] = 1e6*row['run_time_std']
return row
cmap = dict(zip(all_times_df['function'].unique(), sns.color_palette('tab10')))
xticks = [1, 2, 3, 5, 7, 10, 15, 25, 50, 100, 200, 500, 1000,]
## Plot for Python list
line_plot = sns.lineplot(
data=all_times_df.query(f'''input_type == "<class 'list'>"''').apply(time_to_us, axis=1),
x='N', y='run_time_avg', hue='function', linewidth=3, palette=cmap,
)
scatter_plot = sns.scatterplot(
data=all_times_df.query(f'''input_type == "<class 'list'>"''').apply(time_to_us, axis=1),
x='N', y='run_time_avg', hue='function', linewidth=3, palette=cmap,
legend=False,
)
line_plot.set_xscale("log")
line_plot.set_yscale("log")
plt.xticks(xticks)
line_plot.get_xaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter())
plt.xlabel("Size")
plt.ylabel("Time (µs)")
line_plot.set_title("Random selection from a Python list")
plt.legend(bbox_to_anchor=(0.5,0.5))
plt.ylim(7e-1, 2e5)
display(line_plot)
## Plot for Numpy array
line_plot = sns.lineplot(
data=all_times_df.query(f'''input_type == "<class 'numpy.ndarray'>"''').apply(time_to_us, axis=1),
x='N', y='run_time_avg', hue='function', linewidth=3, palette=cmap,
# legend=False,
)
sns.scatterplot(
data=all_times_df.query(f'''input_type == "<class 'numpy.ndarray'>"''').apply(time_to_us, axis=1),
x='N', y='run_time_avg', hue='function', linewidth=3, palette=cmap,
legend=False,
)
line_plot.set_xscale("log")
line_plot.set_yscale("log")
plt.xticks(xticks)
line_plot.get_xaxis().set_major_formatter(matplotlib.ticker.ScalarFormatter())
plt.xlabel("Size")
plt.ylabel("Time (µs)")
line_plot.set_title("Random selection from a NumPy array")
plt.legend(bbox_to_anchor=(1, 1))
plt.ylim(7e-1, 2e5)
display(line_plot)