4

I have the following numpy array:

a = np.array([1,4,2])

I wish to create a new array by dividing this equally by 5 between each element in the a array to get:

b = [1., 1.75, 2.5, 3.25, 4., 3.5, 3., 2.5, 2.]

How can I do this efficiently in python?

naseefo
  • 720
  • 1
  • 9
  • 30

4 Answers4

6

You are looking for a linear interpolation for a 1-d array, which can be done using NumPy.interp.

s = 4       # number of intervals between two numbers
l = (a.size - 1) * s + 1          # total length after interpolation
np.interp(np.arange(l), np.arange(l, step=s), a)        # interpolate

# array([1.  , 1.75, 2.5 , 3.25, 4.  , 3.5 , 3.  , 2.5 , 2.  ])
Psidom
  • 209,562
  • 33
  • 339
  • 356
  • 1
    This works fine! Thank you. I am looking for few more answers as the code involves handling a huge data within a for-loop. I am doing timeit for a few of the codes. Thanks for the solution. – naseefo Dec 28 '18 at 15:29
  • 1
    my guess is that this is going to be the best solution as it uses a compiled interpolation function while my and the other answers use python loops – overfull hbox Dec 28 '18 at 15:43
1

Other option using arange:

import numpy as np
a = np.array([1,4,2])

res = np.array([float(a[-1])])
  for x, y in zip(a, a[1:]):
  res = np.insert(res, -1, np.array(np.arange(x,y,(y-x)/4)))

print(res)
#=> [1.   1.75 2.5  3.25 4.   3.5  3.   2.5  2.  ]
iGian
  • 11,023
  • 3
  • 21
  • 36
1

We could use vectorized linspace : create_ranges -

# https://stackoverflow.com/a/40624614/ @Divakar
def create_ranges(start, stop, N, endpoint=True):
    if endpoint==1:
        divisor = N-1
    else:
        divisor = N
    steps = (1.0/divisor) * (stop - start)
    return steps[:,None]*np.arange(N) + start[:,None]

def ranges_based(a,N):
    ranges2D = create_ranges(a[:-1],a[1:],N-1,endpoint=False)
    return np.concatenate((ranges2D.ravel(),[a[-1]]))

Sample run -

In [151]: a
Out[151]: array([1, 4, 2])

In [152]: ranges_based(a,N=5)
Out[152]: array([1.  , 1.75, 2.5 , 3.25, 4.  , 3.5 , 3.  , 2.5 , 2.  ])

Benchmarking for vectorized solutions

# @Psidom's soln
def interp_based(a,N=5):
    s = N-1
    l = (a.size - 1) * s + 1    # total length after interpolation
    return np.interp(np.arange(l), np.arange(l, step=s), a)   

Timings on large arrays with 5 interval -

In [199]: np.random.seed(0)

In [200]: a = np.random.randint(0,10,(10000))

In [201]: %timeit interp_based(a,N=5)
     ...: %timeit ranges_based(a,N=5)
1000 loops, best of 3: 318 µs per loop
1000 loops, best of 3: 227 µs per loop

In [202]: np.random.seed(0)

In [203]: a = np.random.randint(0,10,(100000))

In [204]: %timeit interp_based(a,N=5)
     ...: %timeit ranges_based(a,N=5)
100 loops, best of 3: 3.39 ms per loop
100 loops, best of 3: 2.77 ms per loop

Timings on large arrays with bigger 50 interval -

In [205]: np.random.seed(0)

In [206]: a = np.random.randint(0,10,(10000))

In [207]: %timeit interp_based(a,N=50)
     ...: %timeit ranges_based(a,N=50)
100 loops, best of 3: 3.65 ms per loop
100 loops, best of 3: 2.14 ms per loop

In [208]: np.random.seed(0)

In [209]: a = np.random.randint(0,10,(100000))

In [210]: %timeit interp_based(a,N=50)
     ...: %timeit ranges_based(a,N=50)
10 loops, best of 3: 43.4 ms per loop
10 loops, best of 3: 31.1 ms per loop

With bigger interval lengths, it seems the performance boost with create_ranges is getting bigger too.

Further improvement

We could optimize further by doing a concatenation at the start and then slicing out at the end, thus avoiding the concatenation there, like so -

def ranges_based_v2(a,N):
    start = a
    stop = np.concatenate((a[1:],[0]))
    return create_ranges(start, stop, N-1, endpoint=False).ravel()[:-N+2]

Timings on larger array with 5 and 50 interval lengths -

In [243]: np.random.seed(0)

In [244]: a = np.random.randint(0,10,(100000))

In [245]: %timeit interp_based(a,N=5)
     ...: %timeit ranges_based(a,N=5)
     ...: %timeit ranges_based_v2(a,N=5)
100 loops, best of 3: 3.38 ms per loop
100 loops, best of 3: 2.71 ms per loop
100 loops, best of 3: 2.49 ms per loop

In [246]: %timeit interp_based(a,N=50)
     ...: %timeit ranges_based(a,N=50)
     ...: %timeit ranges_based_v2(a,N=50)
10 loops, best of 3: 42.8 ms per loop
10 loops, best of 3: 30.1 ms per loop
10 loops, best of 3: 22.2 ms per loop

More with numexpr

We could leverage multi-core with numexpr -

# https://stackoverflow.com/a/40624614/ @Divakar
import numexpr as ne
def create_ranges_numexpr(start, stop, N, endpoint=True):
    if endpoint==1:
        divisor = N-1
    else:
        divisor = N
    s0 = start[:,None]
    s1 = stop[:,None]
    r = np.arange(N)
    return ne.evaluate('((1.0/divisor) * (s1 - s0))*r + s0')

def ranges_based_v3(a,N):
    start = a
    stop = np.concatenate((a[1:],[0]))
    return create_ranges_numexpr(start, stop, N-1, endpoint=False).ravel()[:-N+2]

Timings -

In [276]: np.random.seed(0)

In [277]: a = np.random.randint(0,10,(100000))

In [278]: %timeit interp_based(a,N=5)
     ...: %timeit ranges_based(a,N=5)
     ...: %timeit ranges_based_v2(a,N=5)
     ...: %timeit ranges_based_v3(a,N=5)
100 loops, best of 3: 3.39 ms per loop
100 loops, best of 3: 2.75 ms per loop
100 loops, best of 3: 2.49 ms per loop
1000 loops, best of 3: 1.17 ms per loop

In [279]: %timeit interp_based(a,N=50)
     ...: %timeit ranges_based(a,N=50)
     ...: %timeit ranges_based_v2(a,N=50)
     ...: %timeit ranges_based_v3(a,N=50)
10 loops, best of 3: 43.1 ms per loop
10 loops, best of 3: 31.3 ms per loop
10 loops, best of 3: 22.3 ms per loop
100 loops, best of 3: 11.4 ms per loop
Divakar
  • 218,885
  • 19
  • 262
  • 358
  • thanks for the solution. I see that you got the result 1.29 times faster than Psidolm's. I tried a real-case scenario for me- 7000 elements with 120 divisions repeated 50 times. ranges_based solution took 320ms (1.8 times faster than interp_based) while interp_based took 554ms. The arange solution took 356 seconds. I was wondering if this can be brought down further to 20-30ms by using Cython? – naseefo Dec 28 '18 at 17:18
  • @NaseefUmmer Not really knowledgeable about Cython, so don't have any inputs on it. Also, just added `ranges_based_v2`, which seems even better. So, do check that out too! – Divakar Dec 28 '18 at 17:21
  • @NaseefUmmer Also added `multi-core` based one for further improvements. – Divakar Dec 28 '18 at 17:51
  • You are right! Psidom's: 26ms; range_based_v1: 15ms (1.7x); range_based_v2: 10ms (2.5x) for 7000 elements with 120 division repeated 20 times. The time I mentioned earlier had to be divided by 20. And earlier too, it was repeated only 20 times. I think you have successfully made me reach my target :-) I wish I could upvote this ten times! – naseefo Dec 28 '18 at 17:52
  • Version 3 is coming down to 7 ms with Cython. Only a marginal difference through with Cython. I only did type additions for it. Anyway, thank you so much, Divakar for your time and effort. – naseefo Dec 28 '18 at 18:38
  • One additional observation: for less number of elements Psidolm's solution would be faster. Though it's not relevant to my current problem, but I found this interesting. – naseefo Dec 28 '18 at 18:43
0

You can first make an array of start stop points and then map linspace over this array.

v=np.vstack([a[:-1],a[1:]])
ls = np.apply_along_axis(lambda x: np.linspace(*x,5),1,v)

The last column contains duplicate endpoints (except the last row). We can get the "correct" elements using a mask.

mask = np.ones((len(a)-1,5),dtype='bool')
mask[:-1,-1] = 0

output = ls[mask]

Note that you could also select the rows using slicing and reshape.

output = np.zeros(5*(len(a)-1)-1)
output[:-1] = np.reshape(ls[:,:-1],-1)
output[-1] = a[-1]
overfull hbox
  • 747
  • 3
  • 10
  • It's not working. it is stacking three elements in each row. It's supposed to have only two end point, right? – naseefo Dec 28 '18 at 15:12
  • sorry, I hadn't completed the answer yet – overfull hbox Dec 28 '18 at 15:13
  • I am getting few errors with this implementation. With the first one error is IndexError: boolean index did not match indexed array along dimension 0; dimension is 2 but corresponding boolean dimension is 4. While for the second one error is ValueError: could not broadcast input array from shape (2) into shape (18) – naseefo Dec 28 '18 at 15:28
  • fixed I think. I miscopied some stuff from my computr – overfull hbox Dec 28 '18 at 15:41