42

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)

kenorb
  • 155,785
  • 88
  • 678
  • 743
Steven Lu
  • 41,389
  • 58
  • 210
  • 364
  • 1
    I wrote a [`mergeiter` function](http://stackoverflow.com/a/14465236) for a previous answer; I add indices with `enumerate()`. – Martijn Pieters Jun 05 '13 at 16:50
  • @MartijnPieters Being pretty green with Python I had some trouble grokking that `mergeiter` function of yours at first. But after looking over these other answers, yours is clearly the right type of approach. And yet it's the only one that isn't posted as an answer... – Steven Lu Jun 06 '13 at 02:18
  • For the question in title, it's [python - Getting the index of the returned max or min item using max()/min() on a list - Stack Overflow](https://stackoverflow.com/questions/2474015/getting-the-index-of-the-returned-max-or-min-item-using-max-min-on-a-list) – user202729 Feb 24 '21 at 07:36

10 Answers10

62
 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.

trophallaxis
  • 115
  • 1
  • 8
MatthieuBizien
  • 1,647
  • 1
  • 10
  • 19
  • Great, suppose I want to drill down some more (like, say i wanna sort by some part of the first indexed), do I need to use a lambda for that? And please consider my "useless stuff" as justification for why I want to make it all so damn complicated :) – Steven Lu Jun 05 '13 at 17:47
  • I'll answer my question in the last comment, i think the way to dig deeper is with a lambda. – Steven Lu Jun 07 '13 at 03:53
  • 7
    `max(enumerate(items), key=lambda x: x[1])`, to save you an import. – stillanoob Aug 11 '17 at 17:41
  • This is always worse than the other solution, see [benchmark](https://stackoverflow.com/a/11825864/5267751). The lambda is probably even slower. – user202729 Feb 24 '21 at 07:37
59

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]
1''
  • 26,823
  • 32
  • 143
  • 200
  • 2
    Only non-loop solution that can give you both max and argmax in a single sweep! – gaborous Aug 27 '16 at 13:29
  • here's a related approach, based on inverting the tuple lexicographic ordering - `max(enumerate(l), key=lambda t: list(reversed(t)))`. Unlike the original, when the max appears multiple times, this would give you the last index (which is more useful in a few cases) – yoniLavi Aug 31 '18 at 09:16
21

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.

Eric Leschinski
  • 146,994
  • 96
  • 417
  • 335
stupidsven
  • 227
  • 2
  • 2
  • 11
    It will also iterate the list twice, so don't do this. – mrexodia Jan 15 '17 at 17:38
  • 1
    @mrexodia What's wrong with iterating over the list twice? Just the inefficiency? This implementation has the potential to be much faster than the ones based on `enumerate`, because they allocate a pair on the heap for each element. This solution is also shorter and easier to understand than the others, so arguably more Pythonic. But it may be slower if the items are expensive to compare, and it doesn't work on general iterables. – benrg Sep 01 '20 at 00:53
4

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)]
Mike Müller
  • 82,630
  • 20
  • 166
  • 161
  • I think sorting is entirely unnecessary here even though it is probably not gonna make the runtime horrific or anything. Only a min operation is needed – Steven Lu Jun 06 '13 at 02:05
  • Python's sorting is very efficient (http://en.wikipedia.org/wiki/Timsort). Merge sort is also used. If I understand you right, you would like to apply `min` to all indices. Take out the smallest and repeat until the list is consumed? I run a few test that indicated that sorting is faster for larger lists and if the items are already in a somewhat sorted order. – Mike Müller Jun 06 '13 at 04:03
  • Hm. you might be right. There are after all more than one min operation. – Steven Lu Jun 06 '13 at 04:11
  • Updated my solution after I saw yours and better understood what you are after. Now it is only two lines. Try to time it against your solution for large data sets. – Mike Müller Jun 06 '13 at 04:36
  • Oh I like this! This code looks to my intuition to be more suitable for optimization by a VM. I don't doubt it will be more performant. Thanks – Steven Lu Jun 06 '13 at 04:39
  • This is better than my answer because it does not suffer from `min` attempting to index into empty lists. Likely improvement on the order O(n log n) vs mine which is O(n^2). Could be improved by avoiding the use of `enumerate`. – Steven Lu Jun 11 '13 at 04:05
4

Yet another way to get the argmax is:

def argmax(lst):
    return max(range(len(lst)), key=lst.__getitem__)
yoniLavi
  • 2,624
  • 1
  • 24
  • 30
1

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]
Ryan Saxe
  • 17,123
  • 23
  • 80
  • 128
  • i was trying to avoid re-lookups which is what that `a.index(min(a))` does. index is a search... – Steven Lu Jun 05 '13 at 22:52
  • Oh, alright. That list comprehension is a quick sorter for nearly any array! a list, list of lists...it basically works on anything, I just needed to add the `[1]`. Although it's hard to wrap my head around how you plan on sorting things if you don't want to look them up... – Ryan Saxe Jun 05 '13 at 23:06
  • Well yeah it is hard to wrap my head around it too, which is why i posted the problem, there's gotta be a way to do it though. To make it computationally efficient we just gotta track the state of how far we've moved down each sub array. That can be done by popping them I think, but I just dunno how to put it all together. – Steven Lu Jun 06 '13 at 01:06
  • based on your explanation, I think my code suits it fine. Because what my code does if you read the comment, is it tracks which list was in which tuple! the reason why there is `a.index` is because you need to do that in order to use `a.pop`. Am i misunderstanding you? – Ryan Saxe Jun 06 '13 at 01:31
  • Yeah but all it needs it to know which tuple out of the items has the least value as the first index in its first item (those being the lists-of-two). so it can go pop it. Then the next time around it will repeat that. It does not need to call `index` with the lists-of-two. – Steven Lu Jun 06 '13 at 01:53
  • `pop` takes an integer (the index) for the placement of what you want to pop. So without using `a.index` you will get the following error: `TypeError: an integer is required` – Ryan Saxe Jun 06 '13 at 02:05
  • Well. Yeah. I don't think it makes sense to be building that `a` list in order to pop stuff out of it. There's no point finding the max over the entire set. It should be finding the max between the next item on each tuple. See my self-answer for the direction I'm looking to take it in. Hopefully it makes it clearer. – Steven Lu Jun 06 '13 at 02:14
1

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 
Yoni Kremer
  • 11
  • 1
  • 4
0

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
0

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.

Steven Lu
  • 41,389
  • 58
  • 210
  • 364
  • this might be a bit of an exhausting deal, but why not put it in a `try` and `except` and then you just figure out which list is empty in the `except` and then you have the rest! – Ryan Saxe Jun 06 '13 at 02:20
  • Yeah bringing in exceptions for this feels too heavy-handed considering i'm even avoiding sorting and assembling more lists since i consider those to be unnecessary. It does look like exceptions can play nice evem if i intend to keep this compatible with generators as input lists. So that's nice. I think i got a solution for it though. Will edit answer once i test it – Steven Lu Jun 06 '13 at 02:23
0

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__)
Mi chael
  • 1
  • 1
  • As it’s currently written, your answer is unclear. Please [edit] to add additional details that will help others understand how this addresses the question asked. You can find more information on how to write good answers [in the help center](/help/how-to-answer). – Community Feb 01 '23 at 09:36