Find minimum element and its position in list with Nones
Asked Answered
R

10

5

I have a list of numbers and Nones like this:

l = [2., None, 3., 1., None, 2., None, 5.]

I want to get the minimum number and its index, while the Nones should simply be ignored. For the given example, the result would thus be:

(1., 3)

Of course, it is straightforward to write a function that does what I want but I would prefer some efficient built-in or at least high-level approach. I am particularly interested in solutions for Python 3, where the min-function does not accept None as an argument.

Rarity answered 12/6, 2018 at 17:52 Comment(4)
Does 1. == 1.0?Irma
@Irma Yes, it does.Rarity
Want to find the first index? Meaning, can 1. exist multiple times?Irma
Returning the first index would be fine for resolving ambiguities.Rarity
K
5
min((v,i) for i,v in enumerate(l) if v is not None)
(1.0, 3) # (value, index)
Knoxville answered 12/6, 2018 at 18:17 Comment(1)
This may well be the fastest.Balance
I
3

I'd probably two part it:

m = min(x for x in l if x is not None)
s = (m, l.index(m)) # this will grab the first index

If you want to make the list a single pass + one liner solution:

midx, mval = min(enumerate(x if x is not None else float('inf') for x in l), key=lambda i: i[1])

The enumerate() piece produces an iterable like so:

0 2.0
1 inf
2 3.0
3 1.0
4 inf
5 2.0
6 inf
7 5.0

Then the min() gets called and uses the enumerate() return with lambda to check the values in the i[1] index (e.g. 2.0, inf, ..., 5.0). Thus a final tuple is returned with only a single iteration using a generator from the original list to "filter and replace" the NoneType indices.

Irma answered 12/6, 2018 at 18:1 Comment(2)
@vaultah Not sure of another native solution. It total its O(N) + O(N) = O(N)Irma
As in my answer, I think min + enumerate should give a one-pass solution.Contrariwise
C
3

You can define a conversion function and use this with min:

lst = [2., None, 3., 1., None, 2., None, 5.]

def converter(x):
    return x[1] if x[1] is not None else float('inf')

res = min(enumerate(lst), key=converter)[::-1]

(1.0, 3)

If you're happy using a 3rd party library, the equivalent in NumPy:

arr = np.array(lst).astype(float)
arr[np.isnan(arr)] = np.inf

res = arr.min(), arr.argmin()

Or, more efficiently, you can use np.nanargmin:

arg = np.nanargmin(arr)
minval = arr[arg]

res = minval, arg
Contrariwise answered 12/6, 2018 at 18:4 Comment(4)
If you see my post, does the same without the need to add a function to the namespace.Irma
@pstatix, Sure, I upvoted yours, it works. But I think it's more readable to add a function to the namespace.Contrariwise
Fair enough, though personally, unless this list is massive, an O(N) solution as I originally put (and what yours and my updates are as well) is fine by breaking it into a very readable two liner. The fact that you iterate over the list twice makes no difference because it is O(N) regardless as discussed here.Irma
@pstatix, Yep I get your O(n) point. I think what vaultah and I mean is the one-pass method will also work with a generator (not list-specific), while this is not possible with the 2-pass (unless you copy your generator and exhaust it twice, which is expensive).Contrariwise
P
1
l = [2., None, 3., 1., None, 2., None, 5.]

idx = l.index(min(x for x in l if x is not None))

print(l[idx], idx) # get value, and idx

Output

1.0 3
Pack answered 12/6, 2018 at 17:58 Comment(1)
sorry i change it 1.0 3Pack
M
0

This is one approach.

Demo:

l = [2., None, 3., 1., None, 2., None, 5.]
l = [(v, i) for i, v in enumerate(l) if v is not None]
print( sorted(l, key=lambda x: x[0])[0] )

Output:

(1.0, 3)
Megalopolis answered 12/6, 2018 at 17:59 Comment(2)
Oh ok...sorry did not test thatMegalopolis
@vaultah l = [(v, i) for i, v in enumerate(l) if v is not None] ?Megalopolis
O
0

value = min(l, key=lambda x: float('inf') if x is None else x) index = l.index(value)

Maybe include a check that value isn't inf if that is a concern (ie. the case where there are no numbers in l)

This method avoids constructing a new array internally by simply changing the way min compares values.

Oast answered 12/6, 2018 at 18:0 Comment(0)
B
0

You can avoid using key like so:

>>> import operator as op
>>> import itertools as it
>>> min(it.filterfalse(op.methodcaller('__contains__', None), zip(l, it.count())))
(1.0, 3)
Balance answered 12/6, 2018 at 18:24 Comment(0)
C
0

The most complex was to replace in a list, i think :

        import numpy as np

        l = [2., None, 3., 1., None, 2., None, 5.]

        #### function to replace in a list 
        def replaced(sequence, old, new):
            return (new if x == old else x for x in sequence)

        l=list(replaced(l,None,np.nan))

        #### numpy specific function
        position = np.nanargmin(l)
        value = l[position]

        print(position, value)
Concelebrate answered 12/6, 2018 at 18:26 Comment(0)
I
0

Although I prefer accepted answer. A demo code where you can use NoneType :

lst = [2., None, 3., 1., None, 2., None, 5.]


    
def converter(x):
    NoneType = type(None)
    return x[1] if not isinstance(x[1], NoneType) else float('inf')
       
      
res = min(enumerate(lst), key=converter)[::-1]

print(res)

gives

(1.0, 3)
Incidence answered 26/6, 2021 at 14:6 Comment(0)
A
-1

why the results should be (1., 3), after 1, 2 is following

l = [2., None, 3., 1., None, 2., None, 5.]

bar = map(float, [e for e in l if isinstance(e, float)])
print (min(float(i) for i in bar))
Anglesite answered 12/6, 2018 at 18:5 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.