I think NumPy can be faster than CPython for loops (I didn't test in PyPy).
I want to start from Joe Kington's code because this answer used NumPy.
%timeit f3(9999)
704 ms ± 2.33 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
by myself:
def f4(num):
x=np.ones(num-1)
y=np.arange(1,num)
return np.sum(np.true_divide(x,y))*np.sum(y)
155 µs ± 284 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In addition, High School Mathematics can simplify the problem to computer.
Problem= (1+2+...+(num-1)) * (1/1+1/2+...+1/(num-1))
1+2+...+(num-1)=np.sum(np.arange(1,num))=num*(num-1)/2
1/1+1/2+...+1/(num-1)=np.true_divide (1,y)=np.reciprocal(y.astype(np.float64))
Therefore,
def f5(num):
return np.sum(np.reciprocal(np.arange(1, num).astype(np.float64))) * num*(num-1)/2
%timeit f5(9999)
106 µs ± 615 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In addition, University Mathematics can simplify the problem to computer more.
1/1+1/2+...+1/(num-1)=np.log(num-1)+1/(2*num-2)+np.euler_gamma
(n>2)
np.euler_gamma: Euler-Mascheroni constant (0.57721566...)
Because of inaccuracy of Euler-Mascheroni constant in NumPy, You lose accuracy like
489223499.9991845 -> 489223500.0408554.
If You can ignore 0.0000000085% inaccuracy, You can save more time.
def f6(num):
return (np.log(num-1)+1/(2*num-2)+np.euler_gamma)* num*(num-1)/2
%timeit f6(9999)
4.82 µs ± 29.1 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
Benefit of NumPy becomes larger with larger input.
%timeit f3(99999)
56.7 s ± 590 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
%timeit f5(99999)
534 µs ± 86.5 µs per loop (mean ± std. dev. of 7 runs, 1000 loops each)
%timeit f5(99999999)
1.42 s ± 15.4 ms per loop (mean ± std. dev. of 7 runs, 1 loop each)
9.498947911958**416**e+16
%timeit f6(99999999)
4.88 µs ± 26.7 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
9.498947911958**506**e+16
%timeit f6(9999999999999999999)
17.9 µs ± 921 ns per loop (mean ± std. dev. of 7 runs, 10000 loops each)
In special case, You can use numba (Unfortunately not always).
from numba import jit
@jit
def f7(num):
return (np.log(num-1)+1/(2*num-2)+np.euler_gamma)* num*(num-1)/2
# same code with f6(num)
%timeit f6(999999999999999)
5.63 µs ± 29.6 ns per loop (mean ± std. dev. of 7 runs, 100000 loops each)
f7(123) # compile f7(num)
%timeit f7(999999999999999)
331 ns ± 1.9 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
%timeit f7(9999)
286 ns ± 3.09 ns per loop (mean ± std. dev. of 7 runs, 1000000 loops each)
So, I recommend to use NumPy, mathematics and numba together.