Steven Lu
Steven Lu

Reputation: 43427

Finding the index of the value which is the min or max in Python

I've got a structure of the form:

>>> items
[([[0, 1], [2, 20]], 'zz', ''), ([[1, 3], [5, 29], [50, 500]], 'a', 'b')]

The first item in each tuple is a list of ranges, and I want to make a generator that provides me the ranges in ascending order based on the starting index.

Since the range-lists are already sorted by their starting index this operation is simple: it is just a sorted merge. I'm hoping to do it with good computational efficiency, so I'm thinking that one good way to implicitly track the state of my merge is to simply pop the front off of the list of the tuple which has the smallest starting index in its range list.

I can use min() to obtain [0, 1] which is the first one I want, but how do I get the index of it?

I have this:

[ min (items[i][0]) for i in range(len(items)) ]

which gives me the first item in each list, which I can then min() over somehow, but it fails once any of the lists becomes empty, and also it's not clear how to get the index to use pop() with without looking it back up in the list.

To summarize: Want to build generator that returns for me:

([0,1], 'zz', '')
([1,3], 'a', 'b')
([2,20], 'zz', '')
([5,29], 'a', 'b')
([50,500], 'a', 'b')

Or even more efficiently, I only need this data:

[0, 1, 0, 1, 1]

(the indices of the tuples i want to take the front item of)

Upvotes: 48

Views: 103806

Answers (10)

Mi chael
Mi chael

Reputation: 1

Yet another option:

max(zip(lst, range(len(lst))))[0]

This seems to be the fastest, together with

max(range(len(lst)), key=lst.__getitem__)

Upvotes: -1

Yoni Kremer
Yoni Kremer

Reputation: 9

The simplest and most efficient way (O(n))

arg_max, maximum = max(list(enumerate(nums)), key=lambda x: x[1])  # Returns both the maximum element and it's index 

Upvotes: 0

stupidsven
stupidsven

Reputation: 315

The index of the max of a list:

def argmax(lst):
  return lst.index(max(lst))

If there are duplicate max values in lst, this will return the index of the first maximum value found.

Upvotes: 29

yoniLavi
yoniLavi

Reputation: 2752

Yet another way to get the argmax is:

def argmax(lst):
    return max(range(len(lst)), key=lst.__getitem__)

Upvotes: 4

MatthieuBizien
MatthieuBizien

Reputation: 1725

 from operator import itemgetter
 index, element = max(enumerate(items), key=itemgetter(1))

Return the index of the biggest element in items and the element itself.

Upvotes: 68

1''
1''

Reputation: 27095

This method finds the index of the maximum element of any iterable and does not require any external imports:

def argmax(iterable):
    return max(enumerate(iterable), key=lambda x: x[1])[0]

Upvotes: 62

Mike Müller
Mike Müller

Reputation: 85432

This works:

by_index = ([sub_index, list_index] for list_index, list_item in
             enumerate(items) for sub_index in list_item[0])
[item[1] for item in sorted(by_index)]

Gives:

[0, 1, 0, 1, 1]

In detail. The generator:

by_index = ([sub_index, list_index] for list_index, list_item in
             enumerate(items) for sub_index in list_item[0])
list(by_index)    
[[[0, 1], 0], [[2, 20], 0], [[1, 3], 1], [[5, 29], 1], [[50, 500], 1]]

So the only thing needed is sorting and getting only the desired index:

[item[1] for item in sorted(by_index)]

Upvotes: 5

Steven Lu
Steven Lu

Reputation: 43427

I'm not sure what happened but I think everyone's a bit off the mark. I'll blame it on doing a bad job explaining the problem I'm trying to solve. Anyway, here's how much I've gotten:

items[min(range(len(items)), key = lambda x: items[x][0][0])][0].pop(0)

This takes me most of the way there but what's left to deal with is treating the situation where one list has been exhausted. Once that's taken care of it should be trivial to make this a generator as I can just put it in a loop and yield inside it, and also hopefully without too much more work this can be adapted into performing an efficient sort-merge over generators.

>>> items[min(range(len(items)), key = lambda x: items[x][0][0])][0].pop(0)
[0, 1]
>>> items[min(range(len(items)), key = lambda x: items[x][0][0])][0].pop(0)
[1, 3]
>>> items[min(range(len(items)), key = lambda x: items[x][0][0])][0].pop(0)
[2, 20]
>>> items[min(range(len(items)), key = lambda x: items[x][0][0])][0].pop(0)
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "<stdin>", line 1, in <lambda>
IndexError: list index out of range

Update:

Assembling the proper subset of still-valid-items to min over is the ticket.

def next_value_in_sections(sections):                 
    while 1:                                          
        idxs = []                                     
        for i, x in enumerate(sections):              
            if x[0]:                                  
                idxs.append(i)                        
        print idxs                                    
        if not idxs:                                  
            break                                     
        j = min(idxs, key=lambda x: sections[x][0][0])
        yield (sections[j][0].pop(0), j)              

items = [([[0, 1], [2, 20]], 'zz', ''),               
         ([[1, 3], [5, 29], [50, 500]], 'a', 'b')]    
x = next_value_in_sections(items)                     
for i in x:                                           
    print i                                           

Executed:

$ python test.py  
[0, 1]
([0, 1], 0)
[0, 1]
([1, 3], 1)
[0, 1]
([2, 20], 0)
[1]
([5, 29], 1)
[1]
([50, 500], 1)
[]

I'll note this still can be improved, the idxs list is being rebuilt each iteration. It does not need to be, but doing that does not improve asymptotic bound... Of course, one has to wonder if we really care about performance, whether the use of the lambda is a good idea either, though I really don't see a way around that without taking apart min, which is simply a descent into madness.

Upvotes: 0

Ryan Saxe
Ryan Saxe

Reputation: 17829

so this is a real quick and easy way to get that efficient version you are looking for:

a = []
count = 0
for i in items:
    for x in i[0]:
        #place a list with the index next to it in list a for sorting
        a.append((x,count))
#continually grabs the smallest list and returns the index it was in
sort = [a.pop(a.index(min(a)))[1] for i in range(len(a))]

Here is it with your items to show that it works:

>>> items = [([[0, 1], [2, 20]], 'zz', ''), ([[1, 3], [5, 29], [50, 500]], 'a', 'b')]
>>> a = []
>>> count = 0
>>> for i in items:
...     for x in i[0]:
...             a.append((x,count))
...     count += 1
... 
>>> sort = [a.pop(a.index(min(a)))[1] for i in range(len(a))]
>>> sort
[0, 1, 0, 1, 1]

Upvotes: 0

Phil Reinhold
Phil Reinhold

Reputation: 31

It's easy if you don't try to use the fact that the internal range lists are sorted

sorted(sum([ [(rng,) + i[1:] for rng in i[0]] for i in items ], []), lambda i: i[0][0])

It sounds like you want a function that returns the index of the smallest value though

def min_idx(l, key=lambda x: x):
    min_i, min_key = None, float('inf')
    for i, v in enumerate(l):
        key_v = key(v)
        if key_v < min_key:
            mini_i = i
            min_key = key_v
    return min_i

def merge_items(items):
    res = []
    while True:
        i = min_idx(items, key=lambda i: i[0][0][0])
        item = items[i]
        res.append((item[0][0],) + item[1:])
    return res

Upvotes: 0

Related Questions