diff --git a/src/timing_profile.py b/src/timing_profile.py
index 502ddd868ddac96a4aaf7baae0aae765b3c0647d..b8c00d5a77d1c7ff4efd95432ec7ff1cf6022f78 100644
--- a/src/timing_profile.py
+++ b/src/timing_profile.py
@@ -1,3 +1,4 @@
+# %%
 import timeit
 from my_ext import array_sum, array_sum_nocopy, array_sum_pyobj  # noqa
 import numpy as np
@@ -5,25 +6,39 @@ import matplotlib.pyplot as plt
 from seaborn import despine
 
 
-# %%
-M = 500
-N = np.logspace(1, 5, 10).astype(int)
+# %% Utils
+def bs_mean_ci(x, alpha=0.95, n_bootstrap=1000):
+    """Bootstrap mean and confidence interval."""
+    n, _ = x.shape
+    mu = np.zeros(n)
+    ci = np.zeros((n, 2))
+    for k, xi in enumerate(x):
+        bs = np.random.choice(xi, size=(n_bootstrap,), replace=True)
+        mu[k] = bs.mean()
+        ci[k] = np.percentile(bs, [(1 - alpha) * 100 / 2, 100 - (1 - alpha) * 100 / 2])
+    return mu, ci
+
+
+# %% Perform timing experiments
+R = 20  # Number of repeated timing experiments
+M = 500  # Number of iterations in each timing measurement
+N = np.logspace(1, 5, 10).astype(int)  # Sizes to test
 
 res = {"np": [], "array_sum": [], "array_sum_nocopy": [], "array_sum_pyobj": []}
 for Ni in N:
     print(f"Ni = {Ni}")
     x = np.random.normal(0, 1, int(Ni))
-    ri = timeit.repeat("x.sum()", globals=globals(), number=M, repeat=5)
-    res["np"].append(np.median(ri) / M)
+    ri = np.array(timeit.repeat("x.sum()", globals=globals(), number=M, repeat=R)) / M
+    res["np"].append(ri)
 
-    ri = timeit.repeat("array_sum(x)", globals=globals(), number=M, repeat=5)
-    res["array_sum"].append(np.median(ri) / M)
+    ri = np.array(timeit.repeat("array_sum(x)", globals=globals(), number=M, repeat=R)) / M
+    res["array_sum"].append(ri)
 
-    ri = timeit.repeat("array_sum_nocopy(x)", globals=globals(), number=M, repeat=5)
-    res["array_sum_nocopy"].append(np.median(ri) / M)
+    ri = np.array(timeit.repeat("array_sum_nocopy(x)", globals=globals(), number=M, repeat=R)) / M
+    res["array_sum_nocopy"].append(ri)
 
-    ri = timeit.repeat("array_sum_pyobj(x)", globals=globals(), number=M, repeat=5)
-    res["array_sum_pyobj"].append(np.median(ri) / M)
+    ri = np.array(timeit.repeat("array_sum_pyobj(x)", globals=globals(), number=M, repeat=R)) / M
+    res["array_sum_pyobj"].append(ri)
 
 for k in res:
     res[k] = np.array(res[k])
@@ -31,10 +46,10 @@ for k in res:
 
 # %% Plot results
 fig, ax = plt.subplots(num=10, clear=True)
-ax.loglog(N, res["np"] * 1e9, label="np")
-ax.loglog(N, res["array_sum"] * 1e9, label="array_sum")
-ax.loglog(N, res["array_sum_nocopy"] * 1e9, label="array_sum_nocopy")
-ax.loglog(N, res["array_sum_pyobj"] * 1e9, label="array_sum_pyobj")
+for k in res:
+    mu, ci = bs_mean_ci(res[k])
+    ax.loglog(N, mu, label=k)
+    ax.fill_between(N, ci[:, 0], ci[:, 1], alpha=0.2)
 ax.legend(frameon=False)
 ax.set_xlabel("N")
 ax.set_ylabel("Time (ns)")