Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Sort list of dicts by two keys [duplicate]

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?

like image 930
anvd Avatar asked Jun 15 '20 02:06

anvd


2 Answers

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]}
]
like image 59
Nick Avatar answered Oct 06 '22 01:10

Nick


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'}]
like image 42
alani Avatar answered Oct 05 '22 23:10

alani