3

In a numpy array the indexes of all values closest to a given constant are needed. The background is digital signal processing. The array holds the magnitude function of a filter (np.abs(np.fft.rfft(h))) and certain frequencies (=indexes) are searched where the magnitude is e.g. 0.5 or in another case 0. Most the time the value in question is not included exactly in the sequence. The index of the closed value should be found in this.

So far I came up with the following method where I look at the change in sign of the difference between the sequence and the constant. However, this only works for sequences which are monotonically increasing or decresasing at the points in question. It also is off by 1 sometimes.

def findvalue(seq, value):
    diffseq = seq - value
    signseq = np.sign(diffseq)
    signseq[signseq == 0] = 1
    return np.where(np.diff(signseq))[0]

I'm wondering if there is a better solution for this. It is only for 1D real float arrays and the requirement of the computation efficiency is not so high in my case.

As a numerical example the following code should return [8, 41]. I replaced the filter magnitude response with a half-wave for simplicity here.

f=np.sin(np.linspace(0, np.pi))
findvalue(f, 0.5)

Similar questions I found are as following but they do only return the first or second index:
Find the second closest index to value
Find nearest value in numpy array

Martin Scharrer
  • 1,424
  • 1
  • 18
  • 35

4 Answers4

1

The following function will return a fractional index showing approximately when the value is crossed:

def FindValueIndex(seq, val):
    r = np.where(np.diff(np.sign(seq - val)) != 0)
    idx = r + (val - seq[r]) / (seq[r + np.ones_like(r)] - seq[r])
    idx = np.append(idx, np.where(seq == val))
    idx = np.sort(idx)
    return idx

The logic: Find where sign of seq - val is changing. Take the value one index below and above the transition and interpolate.Add to this index where the value is actually equals the value.

If you want an integer index just use np.round. You can also choose np.floor or np.ceil to round the index to your preference.

def FindValueIndex(seq, val):
    r = np.where(np.diff(np.sign(seq - val)) != 0)
    idx = r + (val - seq[r]) / (seq[r + np.ones_like(r)] - seq[r])
    idx = np.append(idx, np.where(seq == val))
    idx = np.sort(idx)
    return np.round(idx)
Aguy
  • 7,851
  • 5
  • 31
  • 58
  • Thanks, Interpolation is something I thought about as well to improve the result. One question: Why do you write `r + np.ones_like(r)` instead of simply `r + 1`? – Martin Scharrer Feb 21 '18 at 10:53
  • That's because np.where returns a tuple. Alternatively I could have probably done `r = np.where(np.diff(np.sign(seq - val)) != 0)[0]` and then `r + 1` would work. – Aguy Feb 21 '18 at 11:08
  • Thanks, based on your input I ended up with the following method. I changed `sort` to `unique` to remove duplicates and made the rounding optional. `def argvalue(seq, val, intidx=True): r = np.where(np.diff(np.sign(seq - val)) != 0) idx = r + (val - seq[r]) / (seq[r + np.ones_like(r)] - seq[r]) idx = np.append(idx, np.where(seq == val)) if intidx: idx = np.round(idx).astype(int) idx = np.unique(idx) return idx` – Martin Scharrer Feb 27 '18 at 06:34
1
def findvalue(seq, value):
    diffseq = seq - value
    signseq = np.sign(diffseq)
    zero_crossings = signseq[0:-2] != signseq[1:-1]
    indices = np.where(zero_crossings)[0]
    for i, v in enumerate(indices):
        if abs(seq[v + 1] - value) < abs(seq[v] - value):
            indices[i] = v + 1
    return indices

Some more explanation

def print_vec(v):
    for i, f in enumerate(v):
        print("[{}]{:.2f} ".format(i,f), end='')
    print('')

def findvalue_loud(seq, value):
    diffseq = seq - value
    signseq = np.sign(diffseq)
    print_vec(signseq)
    zero_crossings = signseq[0:-2] != signseq[1:-1]
    print(zero_crossings)

    indices = np.where(zero_crossings)[0]
    # indices contains the index in the original vector
    # just before the seq crosses the value [8 40]
    # this may be good enough for you
    print(indices)

    for i, v in enumerate(indices):
        if abs(seq[v + 1] - value) < abs(seq[v] - value):
            indices[i] = v + 1
    # now indices contains the closest [8 41]
    print(indices)
    return indices
1

I think you have two choices here. One is to make some assumptions on the shape and look for the zero-crossings of the difference between the seq and your val (as @ColonelFazackerley did in their answer). The other one is to state up to which relative tolerance you want to consider the value close enough.

In the latter case you can use numpy.isclose:

import numpy as np

def findvalue(seq, val, rtol=0.05):    # value that works for your example
    return np.where(np.isclose(seq, val, rtol=rtol))[0]

Example:

x = np.sin(np.linspace(0, np.pi))
print(findvalue(x, 0.5))
# array([ 8, 41])

This has the disadvantage that it depends on the value of rtol. Set it too large (0.1 for this example) and you get multiple values close to the crossing, set it too low and you don't get any.

Community
  • 1
  • 1
Graipher
  • 6,891
  • 27
  • 47
0

This is probably far from being the best way to do this (I'm still learning numpy), but I hope it helps you to find one.

min_distance = np.abs(your_array - your_constant).min()
# These two tuples contain number closest to your constant from each side.
np.where(bar == val - min_distance)  # Closest, < your_constant
np.where(bar == val + min_distance)  # Closest, > your_constant
0xc0de
  • 8,028
  • 5
  • 49
  • 75