6

I have this list of dicts:

[{'score': '1.9', 'id': 756, 'factors': [1.25, 2.25, 2.5, 2.0, 1.75]}, {'score': '2.0', 'id': 686, 'factors': [2.0, 2.25, 2.75, 1.5, 2.25]}, {'score': '2.0', 'id': 55, 'factors': [1.5, 3.0, 2.5, 1.5, 1.5]}, {'score': '1.9', 'id': 863, 'factors': [1.5, 3.0, 1.5, 2.5, 1.5]}]

I can sort by score with : sorted(l, key=lambda k: k['score'], reverse=True). However I have tied scores. How can I sort first by score and then by id asc or desc?

anvd
  • 3,997
  • 19
  • 65
  • 126
  • Is it your intention that `score` should be converted to a float before comparison, or did you want it to be compared lexically as a string? What if two scores were `'2.0'` and `'10.0'` for example? Which would be "higher"? – alani Jun 15 '20 at 03:14

4 Answers4

11

You can sort by a tuple:

sorted(l, key=lambda k: (float(k['score']), k['id']), reverse=True)

This will sort by score descending, then id descending. Note that since score is a string value, it needs to be converted to float for comparison.

[
 {'score': '2.0', 'id': 686, 'factors': [2.0, 2.25, 2.75, 1.5, 2.25]},
 {'score': '2.0', 'id': 55, 'factors': [1.5, 3.0, 2.5, 1.5, 1.5]},
 {'score': '1.9', 'id': 863, 'factors': [1.5, 3.0, 1.5, 2.5, 1.5]},
 {'score': '1.9', 'id': 756, 'factors': [1.25, 2.25, 2.5, 2.0, 1.75]}
]

To sort by id ascending, use -k['id'] (sorting negated numbers descending is equivalent to sorting the non-negated numbers ascending):

sorted(l, key=lambda k: (float(k['score']), -k['id']), reverse=True)

[
 {'score': '2.0', 'id': 55, 'factors': [1.5, 3.0, 2.5, 1.5, 1.5]},
 {'score': '2.0', 'id': 686, 'factors': [2.0, 2.25, 2.75, 1.5, 2.25]},
 {'score': '1.9', 'id': 756, 'factors': [1.25, 2.25, 2.5, 2.0, 1.75]},
 {'score': '1.9', 'id': 863, 'factors': [1.5, 3.0, 1.5, 2.5, 1.5]}
]
Nick
  • 138,499
  • 22
  • 57
  • 95
  • This works in the case that one of the two keys is a number, so can be negated. It would be good to offer a general solution for sorting by one key ascending and the other key descending, where both keys support comparison but not negation, e.g. strings. – alani Jun 15 '20 at 02:53
  • In python 2, you could use something like `sorted(l, cmp=lambda(d1, d2: (cmp(d1[k1],d2[k1]), cmp(d2[k2],d1[k2])))` but `cmp` has been abandoned in python 3, and I'd like to know an equivalent. – alani Jun 15 '20 at 03:02
  • On a more specific point for this particular question, the `score` element is a string, but I bet that the intended sort order is based on the equivalent float rather than a lexical sort, so you probably want `float(k['score'])` as the first element of the tuple in your lambda. You would need first to seek clarification from the OP, but I would be surprised if it is intended that `'2.0'` should be greater than `'10.0'`. – alani Jun 15 '20 at 03:11
  • @alaniwi that is a good point about `score`, I hadn't noticed, have updated answer. In terms of your other comments, the simple (simplest?) solution is a nested sort `sorted(sorted(l, key=lambda k: k['id']), key=lambda k: k['score'], reverse=True)` – Nick Jun 15 '20 at 03:18
  • I am not convinced about the nested sort. The second sort would simply re-sort by a new primary key, rather than supply a secondary key to use as a tie-breaker. – alani Jun 15 '20 at 03:21
  • @alaniwi python sort is [stable](https://docs.python.org/3/howto/sorting.html#sort-stability-and-complex-sorts), so you first sort by the secondary key, then the primary key, and when sorting by the primary key the ordering of the secondary key is maintained for like values of the primary key. – Nick Jun 15 '20 at 03:22
  • Yes, very good point, thanks. – alani Jun 15 '20 at 03:33
  • To avoid having to sort twice (sorting on `foo` ascending and `bar` descending) best I could come up with is: `from functools import cmp_to_key`, `def cmp(a, b): return (a > b) - (a < b)`, `def cmp2(k1, k2): return cmp(k1['foo'], k2['foo']) or cmp(k2['bar'], k1['bar'])`, `print(sorted(l, key=cmp_to_key(cmp2)))` – alani Jun 15 '20 at 03:37
  • @alaniwi yes, I think using `cmp_to_key` is probably the best alternative - otherwise you have to do all sorts of type testing in the key function – Nick Jun 15 '20 at 03:39
  • I've added another answer discussing the options in this case, and have credited you for suggesting the nested sort, although in an attached comment rather than the answer itself. – alani Jun 15 '20 at 03:56
  • Let us [continue this discussion in chat](https://chat.stackoverflow.com/rooms/216017/discussion-between-nick-and-alaniwi). – Nick Jun 15 '20 at 22:58
3

To supplement rather than repeat what is stated in the other answers, this question raises an interesting point (although not directly needed in the case in the question) about what happens if you want to sort on two keys, with the primary ascending and the secondary descending, but where neither key is numeric so you cannot employ the trick of negating one of the keys -- i.e. the keys support comparison but not negation.

Suppose you want to sort the following list of dictionaries with primary key 'name' ascending, and secondary key 'surname' descending.

l = [{'name': 'Fred', 'surname': 'Jones'}, {'name': 'Fred', 'surname': 'Roberts'},
     {'name': 'Diana', 'surname': 'Jackson'}]

Probably the simplest approach is to sort these twice, first on the secondary key, making use of the fact that sort is stable, so the second sort will preserve the order with respect to the secondary key that was achieved in the first sort.

l1 = sorted(l, key=lambda k:k['surname'], reverse=True)
l2 = sorted(l1, key=lambda k:k['name'])
print(l2)

Or the second sorted could be replaced by doing an in-place sort on the intermediate list:

l1 = sorted(l, key=lambda k:k['surname'], reverse=True)
l1.sort(key=lambda k:k['name'])
print(l1)

A possible solution to avoid having to sort twice would be to define a comparison function cmp2 returning -1, 0 or 1 as appropriate (note the reversed order of comparison for the secondary key), and then use functools.cmp_to_key to generate a sort key:

from functools import cmp_to_key

def cmp(a, b):
    return (a > b) - (a < b)

def cmp2(k1, k2):
    return cmp(k1['name'], k2['name']) or cmp(k2['surname'], k1['surname'])

print(sorted(l, key=cmp_to_key(cmp2)))

Both of the above options will output:

[{'name': 'Diana', 'surname': 'Jackson'}, {'name': 'Fred', 'surname': 'Roberts'}, {'name': 'Fred', 'surname': 'Jones'}]
Nick
  • 138,499
  • 22
  • 57
  • 95
alani
  • 12,573
  • 2
  • 13
  • 23
0

You can do that with a custom key:

d = [{'score': '1.9', 'id': 756, 'factors': [1.25, 2.25, 2.5, 2.0, 1.75]},
     {'score': '2.0', 'id': 686, 'factors': [2.0, 2.25, 2.75, 1.5, 2.25]},
     {'score': '2.0', 'id': 55, 'factors': [1.5, 3.0, 2.5, 1.5, 1.5]},
     {'score': '1.9', 'id': 863, 'factors': [1.5, 3.0, 1.5, 2.5, 1.5]}]

def k(d):
    return d['score'],d['id']

d = sorted(d, key=k, reverse=True)

print(d)

Output:

[{'score': '2.0', 'id': 686, 'factors': [2.0, 2.25, 2.75, 1.5, 2.25]},
 {'score': '2.0', 'id': 55, 'factors': [1.5, 3.0, 2.5, 1.5, 1.5]},
 {'score': '1.9', 'id': 863, 'factors': [1.5, 3.0, 1.5, 2.5, 1.5]},
 {'score': '1.9', 'id': 756, 'factors': [1.25, 2.25, 2.5, 2.0, 1.75]}]
Red
  • 26,798
  • 7
  • 36
  • 58
0

You can sort the list by second key first, then sort the result list by first key.

result = sorted(sorted(l, key=lambda k: k['id']), key=lambda k: k['score'], reverse=True)
Jason Yang
  • 11,284
  • 2
  • 9
  • 23