440,092 Members | 1,546 Online
Need help? Post your question and get tips & solutions from a community of 440,092 IT Pros & Developers. It's quick & easy.

Sorting on multiple values, some ascending, some descending

 P: n/a I have successfully used the sort lambda construct described in http://mail.python.org/pipermail/pyt...il/377443.html. However, how do I take it one step further such that some values can be sorted ascending and others descending? Easy enough if the sort values are numeric (just negate the value), but what about text? Is the only option to define an external sorting function to loop through the list and perform the comparisons one value at a time? Jan 3 '07 #1
11 Replies

 P: n/a dwelden wrote: I have successfully used the sort lambda construct described in http://mail.python.org/pipermail/pyt...il/377443.html. However, how do I take it one step further such that some values can be sorted ascending and others descending? Easy enough if the sort values are numeric (just negate the value), but what about text? Is the only option to define an external sorting function to loop through the list and perform the comparisons one value at a time? The simplest way is to take advantage of sort-stability and do successive sorts. For example, to sort by a primary key ascending and a secondary key decending: L.sort(key=lambda r: r.secondary, reverse=True) L.sort(key=lambda r: r.primary) A less general technique is to transform fields in a way that reverses their comparison order: L.sort(key=lambda r: (-r.age, r.height)) # sorts descending age and ascending height Raymond Jan 3 '07 #2

 P: n/a On Wed, 2007-01-03 at 10:48 -0800, dwelden wrote: I have successfully used the sort lambda construct described in http://mail.python.org/pipermail/pyt...il/377443.html. However, how do I take it one step further such that some values can be sorted ascending and others descending? Easy enough if the sort values are numeric (just negate the value), but what about text? Is the only option to define an external sorting function to loop through the list and perform the comparisons one value at a time? If by "external sorting function" you mean a custom comparison function to pass to sort as the cmp argument, then that's one option, but not the only one. If you know in advance which columns contain strings, you could write a function that operates on strings and is strictly monotonically decreasing, i.e. it behaves such that f(x) < f(y) if and only if x y. This function could then be used in the sort key for sorting on a string column in descending order. The following function does the trick: def antistring(x): return [256-ord(c) for c in x]+[257] (Proof by vigorous handwaving.) Lastly, if your array is small enough that you can tolerate the performance hit of multiple passes, you could exploit the fact that sort() is guaranteed to be stable in sufficiently recent releases of CPython. Split your sort on n columns into n separate sorts on a single column each, and use the 'reverse' parameter to specify whether to sort forward or backwards. Hope this helps, Carsten. Jan 3 '07 #3

 P: n/a Raymond Hettinger: The simplest way is to take advantage of sort-stability and do successive sorts. For example, to sort by a primary key ascending and a secondary key decending: L.sort(key=lambda r: r.secondary, reverse=True) L.sort(key=lambda r: r.primary) That's probably the faster and simpler way. The following solution is probably slow, memory-hungry, and it's not tested much so it may be buggy too, but it shows my idea, and bugs can be fixed: class Inverter: def __init__(self, item, reversed=False): self.item = item self.reversed = reversed def __cmp__(self, other): if self.reversed: return cmp(other.item, self.item) else: return cmp(self.item, other.item) data = [[1, "a", "b"], [1, "b", "d"], [1, "d", "a"]] reverses = [True, False, False] print sorted(data, key=lambda subseq: map(Inverter, subseq, reverses)) Bye, bearophile Jan 3 '07 #4

 P: n/a Raymond Hettinger wrote: dwelden wrote: >I have successfully used the sort lambda construct described inhttp://mail.python.org/pipermail/pyt...il/377443.html.However, how do I take it one step further such that some values can besorted ascending and others descending? Easy enough if the sort valuesare numeric (just negate the value), but what about text?Is the only option to define an external sorting function to loopthrough the list and perform the comparisons one value at a time? The simplest way is to take advantage of sort-stability and do successive sorts. For example, to sort by a primary key ascending and a secondary key decending: L.sort(key=lambda r: r.secondary, reverse=True) L.sort(key=lambda r: r.primary) A less general technique is to transform fields in a way that reverses their comparison order: L.sort(key=lambda r: (-r.age, r.height)) # sorts descending age and ascending height You can get generality if you are willing to pay the performance penalty: >>items [(3, 1), (2, 2), (1, 1), (1, 3), (2, 1), (2, 3), (1, 2)] >>class Reverse: .... def __cmp__(self, other): .... return -cmp(self.value, other.value) .... def __init__(self, value): .... self.value = value .... >>items.sort(key=lambda (x, y): (x, Reverse(y)))items [(1, 3), (1, 2), (1, 1), (2, 3), (2, 2), (2, 1), (3, 1)] Peter Jan 3 '07 #5

 P: n/a dwelden wrote: I have successfully used the sort lambda construct described in http://mail.python.org/pipermail/pyt...il/377443.html. However, how do I take it one step further such that some values can be sorted ascending and others descending? Easy enough if the sort values are numeric (just negate the value), but what about text? You got already some good replies; here's yet another less-than-optimal way: class revstr(str): def __lt__(self, other): return str.__gt__(self,other) def __gt__(self, other): return str.__lt__(self,other) L.sort(key=lambda i: (i.somestring, revstr(i.someotherstring), i.anotherkey)) George Jan 3 '07 #6

 P: n/a The simplest way is to take advantage of sort-stability and do successive sorts. For example, to sort by a primary key ascending and a secondary key decending: L.sort(key=lambda r: r.secondary, reverse=True) L.sort(key=lambda r: r.primary) Excellent! That looks just like what I needed. A less general technique is to transform fields in a way that reverses their comparison order: L.sort(key=lambda r: (-r.age, r.height)) # sorts descending age and ascending height This one I had figured out, but could not see how to get it to work for strings. Much obliged. Jan 3 '07 #7

 P: n/a dwelden wrote: L.sort(key=lambda r: r.secondary, reverse=True) L.sort(key=lambda r: r.primary) Excellent! That looks just like what I needed. Note that there is the (probably little used) operator.attrgetter() too, with that you can avoid the possibly slow lambda: L.sort(key=attrgetter("primary") operator.itemgetter(n) is when you have items that can be be accessed by index. Bye, bearophile Jan 4 '07 #8

 P: n/a be************@lycos.com a écrit : Raymond Hettinger: >The simplest way is to take advantage of sort-stability and dosuccessive sorts. For example, to sort by a primary key ascending anda secondary key decending: L.sort(key=lambda r: r.secondary, reverse=True) L.sort(key=lambda r: r.primary) That's probably the faster and simpler way. The following solution is probably slow, memory-hungry, and it's not tested much so it may be buggy too, but it shows my idea, and bugs can be fixed: class Inverter: def __init__(self, item, reversed=False): self.item = item self.reversed = reversed def __cmp__(self, other): if self.reversed: return cmp(other.item, self.item) else: return cmp(self.item, other.item) data = [[1, "a", "b"], [1, "b", "d"], [1, "d", "a"]] reverses = [True, False, False] print sorted(data, key=lambda subseq: map(Inverter, subseq, reverses)) Nice trick, but don't get too caught up using classes ;) Try that one instead for much better performance : class Inverter: def __init__(self, item): self.item = item def __cmp__(self, other): return cmp(other, self.item) def invert_if(item, reversed=False): if reversed: return Inverter(item) return item data = [[1, "a", "b"], [1, "b", "d"], [1, "d", "a"]] reverses = [True, False, False] print sorted(data, key=lambda subseq: map(invert_, subseq, reverses)) Jan 4 '07 #9

 P: n/a On 2007-01-03, dwelden >a = [("Neil Cerutti", "8025552954"), ("Ted Smith", "8025552281"), ("Barny Fife", "8025551105")]b = [(" ".join(reversed(x.split())), y) for (x, y) in a]b [('Cerutti Neil', '8025552954'), ('Smith Ted', '8025552281'), ('Fife Barny', '8025551105')] >>b.sort()b [('Cerutti Neil', '8025552954'), ('Fife Barny', '8025551105'), ('Smith Ted', '8025552281')] >>a = [(" ".join(reversed(x.split())), y) for (x, y) in b]a [('Neil Cerutti', '8025552954'), ('Barny Fife', '8025551105'), ('Ted Smith', '8025552281')] -- Neil Cerutti Eddie Robinson is about one word: winning and losing. --Eddie Robinson's agent Paul Collier Jan 4 '07 #10

 P: n/a Neil Cerutti wrote: Another trick is to factor the key application out of the sort. This may be a good idea if when you want to minimize the number of times your key function is called. The idea is to mangle the list temporarily so you can use an unkeyed sort, and then unmangle the sorted data. Here's a silly example using a phone directory that's not stored in a format that's easy to sort. No need to jump through these hoops; list.sort(key=keyfunc) calls keyfunc() exactly once per list item: >>from random import shuffleitems = range(-5, 10)shuffle(items)count = 0def key(value): .... global count .... count += 1 .... return abs(value) .... >>items.sort(key=key)count 15 Peter Jan 4 '07 #11

 P: n/a On 2007-01-04, Peter Otten <__*******@web.dewrote: Neil Cerutti wrote: >Another trick is to factor the key application out of thesort. This may be a good idea if when you want to minimize thenumber of times your key function is called.The idea is to mangle the list temporarily so you can use anunkeyed sort, and then unmangle the sorted data. Here's asilly example using a phone directory that's not stored in aformat that's easy to sort. No need to jump through these hoops; list.sort(key=keyfunc) calls keyfunc() exactly once per list item: >>>from random import shuffleitems = range(-5, 10)shuffle(items)count = 0def key(value): ... global count ... count += 1 ... return abs(value) ... >>>items.sort(key=key)count 15 Thanks for the correction! That's very useful information. -- Neil Cerutti Jan 4 '07 #12