13

My question is very similar to these two links 1 and 2:

I have three different lists. I want to sort List1 based on List2 (in ascending order). However, I have repeats in List2. I then want to sort these repeats by List3 (in descending order). Confusing enough?

What I have:

List1 = ['a', 'b', 'c', 'd', 'e']
List2 = [4, 2, 3, 2, 4]
List3 = [0.1, 0.8, 0.3, 0.6, 0.4]

What I want:

new_List1 = ['b', 'd', 'c', 'e', 'a']

'b' comes before 'd' since 0.8 > 0.6. 'e' comes before 'a' since 0.4 > 0.1.

user202729
  • 3,358
  • 3
  • 25
  • 36
mcfly
  • 1,151
  • 4
  • 33
  • 55

3 Answers3

13

I think you should be able to do this by:

paired_sorted = sorted(zip(List2,List3,List1),key = lambda x: (x[0],-x[1]))
l2,l3,l1 = zip(*paired_sorted)

In action:

>>> List1 = ['a', 'b', 'c', 'd', 'e']
>>> List2 = [4, 2, 3, 2, 4]
>>> List3 = [0.1, 0.8, 0.3, 0.6, 0.4]
>>> paired_sorted = sorted(zip(List2,List3,List1),key = lambda x: (x[0],-x[1]))
>>> l2,l3,l1 = zip(*paired_sorted)
>>> print l1
('b', 'd', 'c', 'e', 'a')

Here's how it works. First we match corresponding elements from your lists using zip. We then sort those elements based on the items from List2 first and (negated) List3 second. Then we just need to pull off the List1 elements again using zip and argument unpacking -- Although you could do it easily with a list-comprehension if you wanted to make sure you had a list at the end of the day instead of a tuple.

This gets a little tougher if you can't easily negate the values in List3 -- e.g. if they're strings. You need to do the sorting in 2 passes:

paired = zip(List2,List3,List1)
rev_sorted = sorted(paired,reverse=True,key=lambda x: x[1])  #"minor" sort first
paired_sorted = sorted(rev_sorted,key=lambda x:x[0])         #"major" sort last
l2,l3,l1 = zip(*paired_sorted)

(you could use operator.itemgetter(1) in place of lambda x:x[1] in the above if you prefer). This works because python sorting is "stable". It doesn't re-order "equal" elements.

mgilson
  • 300,191
  • 65
  • 633
  • 696
5

This requires a decorate-sort-undecorate step:

decorated = zip(List1, List2, List3)
decorated.sort(key=lambda v: (v[1], -v[2]))
new_list1 = [v[0] for v in decorated]

or, combined into one line:

new_list1 = [v[0] for v in sorted(zip(List1, List2, List3), key=lambda v: (v[1], -v[2]))]

Output:

>>> List1 = ['a', 'b', 'c', 'd', 'e']
>>> List2 = [4, 2, 3, 2, 4]
>>> List3 = [0.1, 0.8, 0.3, 0.6, 0.4]
>>> new_list1 = [v[0] for v in sorted(zip(List1, List2, List3), key=lambda v: (v[1], -v[2]))]
>>> new_list1
['b', 'd', 'c', 'e', 'a']
Martijn Pieters
  • 1,048,767
  • 296
  • 4,058
  • 3,343
  • Bonus points if we can figure out how to make this work if negating the elements in List3 wouldn't cause it to be sorted in the opposite order :) e.g. -- if it contained strings – mgilson Dec 19 '12 at 17:27
  • @mgilson: I think you could do it in two passes and use `reverse=True`, no? – DSM Dec 19 '12 at 17:32
  • @DSM -- Yeah. I knew that's what it would take, but for some reason I couldn't figure out the logistics until I saw your comment. I've added that to my answer. – mgilson Dec 19 '12 at 17:37
3
>>> [v for i, v in sorted(enumerate(List1), key=lambda i_v: (List2[i_v[0]], -List3[i_v[0]]))]
['b', 'd', 'c', 'e', 'a']

This sorts the index/value pairs by using the indices to get the corresponding values from the other lists to use in the key function used for ordering by sorted(), and then extracts just the values using a list comprehension.

Here is a shorter alternative that sorts just the indices and then uses those indices to grab the values from List1:

>>> [List1[i] for i in sorted(range(len(List1)), key=lambda i: (List2[i], -List3[i]))]
['b', 'd', 'c', 'e', 'a']
Andrew Clark
  • 202,379
  • 35
  • 273
  • 306