Seite 1 von 1
Spaß am Freitag: Ranking erstellen
Verfasst: Freitag 13. Februar 2015, 10:17
von Hyperion
Moin moin,
ich las heute morgen auf
reddit eine nette Aufgabe und dachte, dass ich das mal selber probieren muss:
Rank Vectors
Given an array (or list) of scores, return the array of ranks for each value in the array. The largest value has rank 1, the second largest value has rank 2, and so on. Ties should be handled by assigning the same rank to all tied values. For example:
ranks([9,3,6,10]) = [2,4,3,1] and ranks([3,3,3,3,3,5,1]) = [2,2,2,2,2,1,7]
because there is one 1st place value, a five-way tie for 2nd place, and one in 7th place.
Meine Lösung so weit:
Code: Alles auswählen
def ranks(values):
counts = Counter(values)
rank_sum = 1
rank_values = {}
for value in sorted(counts.keys(), reverse=True):
rank_values[value] = rank_sum
rank_sum += counts[value]
return [rank_values[value] for value in values]
assert(ranks([9,3,6,10]) == [2,4,3,1])
assert(ranks([3,3,3,3,3,5,1]) == [2,2,2,2,2,1,7])
Abgesehen davon, dass es da für ``numpy`` ggf etwas fertiges gibt - wer hat andere interessante (und ggf. effizientere) Ansätze? Oder vielleicht auch in anderen Sprachen?
Man könnte das ganze natürlich als Iterator-Funktion schreiben, aber laut Aufgabe soll ja ein "Array" (was offenbar eine Liste sein soll) zurück gegeben werden.
Ich habe tatsächlich noch nicht auf reddit gespickt, was da so an Lösungen angeboten wird

Re: Spaß am Freitag: Ranking erstellen
Verfasst: Freitag 13. Februar 2015, 11:01
von bwbg
Ein erster Ansatz:
Code: Alles auswählen
#!/usr/bin/env python3
def rank(x, xs):
return len(xs) - sum( 1 for e in xs if x >= e ) + 1
def ranks(xs):
return [rank(e, xs) for e in xs]
assert ranks([9, 3, 6, 10]) == [2, 4, 3, 1]
assert ranks([3, 3, 3, 3, 3, 5, 1]) == [2, 2, 2, 2, 2, 1, 7]
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Freitag 13. Februar 2015, 11:27
von Hyperion
@bwbg: Hübsch kurz

Aber natürlich sehr ineffizient - O(n^2) als Laufzeit...
(Mein Ansatz hat O(n log n))
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Freitag 13. Februar 2015, 17:07
von snafu
In Sachen Effizienz würde mir auch nichts besseres einfallen. Höchstens `groupby()` anstelle des `Counter()`s benutzen:
Code: Alles auswählen
from itertools import groupby
def ranks(values):
value_ranks = {}
rank = 1
grouped = ((key, list(group)) for key, group in groupby(values))
for key, group in sorted(grouped, reverse=True):
value_ranks[key] = rank
rank += len(group)
return [value_ranks[val] for val in values]
Das ändert aber nichts am generellen Laufzeitverhalten. Man könnte das Erstellen der Listen sogar als negativen Punkt ansehen.
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Freitag 13. Februar 2015, 19:29
von BlackJack
JavaScript, mit `underscore.js` als `_`:
Code: Alles auswählen
var ranks = function (values) {
var result = [];
_.chain(values)
.map(function (v, i) {return {value: v, index: i};})
.sortBy('value')
.reverse()
.each(
function (x, i) {
if (x.value === this.previousValue) {
this.correction++;
} else {
this.previousValue = x.value;
this.correction = -1;
}
result[x.index] = i - this.correction;
},
{previousValue: null, correction: -1}
);
return result;
};
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Freitag 13. Februar 2015, 23:40
von jerch
Bucketsort und unpythonisch macht der Sache Beine
Code: Alles auswählen
def ranks(values):
highest = max(values)
bucket = [0] * (highest + 1)
for value in values:
bucket[value] += 1
value2rank = {}
rank = 1
for i in xrange(len(bucket)-1, -1, -1):
if bucket[i]:
value2rank[i] = rank
rank += bucket[i]
return [value2rank[value] for value in values]
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Samstag 14. Februar 2015, 09:12
von Sirius3
@jerch: und warum kann man das ganze nicht auch pythonisch schreiben?
Code: Alles auswählen
def ranks(values):
bucket = [0] * (max(values) + 1)
for value in values:
bucket[value] += 1
rank = 1
ranks = []
for cnt in reversed(bucket):
ranks.append(rank)
rank += cnt
ranks.reverse()
return [ranks[value] for value in values]
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Samstag 14. Februar 2015, 11:14
von BlackJack
Den JavaScript-Code 1:1 in CoffeeScript umgeschrieben:
Code: Alles auswählen
ranks = (values) ->
result = []
_.chain(values)
.map (v, i) ->
value: v
index: i
.sortBy 'value'
.reverse()
.each (x, i) ->
if x.value == @previousValue
@correction++
else
@previousValue = x.value
@correction = -1
result[x.index] = i - @correction
,
previousValue: null
correction: -1
result
Edit: Und noch mal mehr mit CoffeeScript-Sprachmitteln:
Code: Alles auswählen
ranks = (values) ->
result = []
previousValue = null
correction = -1
for [value, index], i in _.sortBy(([v, i] for v, i in values), 0).reverse()
if value == previousValue
correction++
else
previousValue = value
correction = -1
result[index] = i - correction
result
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Samstag 14. Februar 2015, 17:42
von Hyperion
@snafu: ``groupby`` klappt aber nur eingeschränkt, wenn die Ränge nicht verteilt im Vektor vorkommen.
Am folgenden scheitert Dein Code:
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Sonntag 15. Februar 2015, 00:46
von Sr4l
Ohne imports und schnick schnack
Code: Alles auswählen
def ranks(score):
score_sorted = score[:]
score_sorted.sort(reverse=True)
rank = []
for e in score:
rank.append(score_sorted.index(e)+1)
return rank
assert ranks([9,3,6,10]) == [2,4,3,1]
assert ranks([3,3,3,3,3,5,1]) == [2,2,2,2,2,1,7]
*edit*
kürzer
Code: Alles auswählen
def ranks(score):
score_sorted = sorted(score, reverse=True)
return [score_sorted.index(e)+1 for e in score]
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Sonntag 15. Februar 2015, 01:01
von BlackJack
@Sr4l: Zählt Effizienz zu Schnick Schnack?

Das `index()` da drin sorgt für eine unschöne Laufzeit.
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Sonntag 15. Februar 2015, 01:21
von Sr4l

Bis 10.000 Einträge absolut vertretbar. Bei mehr muss man dann halt Hardware mäßig aufrüsten.
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Montag 16. Februar 2015, 08:09
von jerch
@Sirius3:
Haha schönes Ding - zeigt sehr eindrucksvoll, dass ein O(n) in einer Pythonschleife sehr viel mehr Konstantenanteil mitschleppt als in C. Von der Komplexität sind beide Versionen in O(n), allerdings hat Deine Variante 2 n-Durchläufe mehr drin (2x reversed). Und trotzdem läuft das selbst mit dünn besetzten Rängen schneller als das value2rank Mapping, welches hier eigentlich bei Speicherverbrauch und Laufzeit abkürzen kann.
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Montag 16. Februar 2015, 08:30
von Sirius3
@jerch: das erste reverse ist nur virtuell, braucht also gar keine Rechenzeit.
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Montag 16. Februar 2015, 11:17
von Hyperion
Sirius3 hat geschrieben:@jerch: das erste reverse ist nur virtuell, braucht also gar keine Rechenzeit.
Und das zweite kann man sich sparen, wenn man eine ``deque`` statt einer Liste für die Ränge verwendet:
Code: Alles auswählen
from collections import deque
def ranks(values):
bucket = [0] * (max(values) + 1)
for value in values:
bucket[value] += 1
rank = 1
ranks = deque()
for cnt in reversed(bucket):
ranks.appendleft(rank)
rank += cnt
return [ranks[value] for value in values]
assert(ranks([9,3,6,10]) == [2,4,3,1])
assert(ranks([3,3,3,3,3,5,1]) == [2,2,2,2,2,1,7])
assert ranks([9, 3, 10, 6, 10]) == [3, 5, 1, 4, 1])
Müsste man mal timen, was das so spart...
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Mittwoch 18. Februar 2015, 14:02
von jerch
@Hyperion:
deque kann hier nichts einsparen, da der Indexzugriff O(n) ist (was im return zu n*n wird). Und `reverse()` selbst arbeitet inplace mit Anfangs-/Endpointer-Vergleich, was die schnellste Variante in C sein dürfte.
@Sirius3:

War immer davon ausgegangen, das `reversed()` die Eingabe klont. Manchmal hilft Doku-Lesen.
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Mittwoch 18. Februar 2015, 15:21
von Hyperion
jerch hat geschrieben:@Hyperion:
deque kann hier nichts einsparen, da der Indexzugriff O(n) ist (was im return zu n*n wird).
jerch hat geschrieben:
Manchmal hilft Doku-Lesen.
Ja, weil:
doku hat geschrieben:
In addition to the above, deques support iteration, pickling, len(d), reversed(d), copy.copy(d), copy.deepcopy(d), membership testing with the in operator, and subscript references such as d[-1]. Indexed access is O(1) at both ends but slows to O(n) in the middle. For fast random access, use lists instead.
Edit: Oops... im ``return``... stimmt

Re: Spaß am Freitag: Ranking erstellen
Verfasst: Mittwoch 18. Februar 2015, 15:33
von EyDu
jerch hat geschrieben:@Hyperion:
deque kann hier nichts einsparen, da der Indexzugriff O(n) ist (was im return zu n*n wird). Und `reverse()` selbst arbeitet inplace mit Anfangs-/Endpointer-Vergleich, was die schnellste Variante in C sein dürfte.
Man könnte vor dem Indexzugriff eine Liste daraus machen. Oder eine eigene Deque-Implementierung für das Problem basteln, da die Anzahl der Elemente vorher bekannt ist. Oder man lässt gleich die Deque weg, weil sie gar nicht benötigt wird:
Code: Alles auswählen
def ranks(values):
bucket = [0] * (max(values) + 1)
for value in values:
bucket[value] += 1
rank = 1
ranks = []
for cnt in reversed(bucket):
ranks.append(rank)
rank += cnt
return [ranks[-value-1] for value in values]
assert(ranks([9,3,6,10]) == [2,4,3,1])
assert(ranks([3,3,3,3,3,5,1]) == [2,2,2,2,2,1,7])
assert(ranks([9, 3, 10, 6, 10]) == [3, 5, 1, 4, 1])
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Mittwoch 18. Februar 2015, 16:34
von jerch
Hmm sehr seltsam, EyDus Variante läuft insgesamt schlechter, mit größerer Liste nimmt der Abstand sogar zu (Abhängigkeit zur Größe der Liste? WTH

). Hätt ich nicht erwartet, zumal man einen n-Durchlauf spart. Mag CPython keine negativen Indices? :K
@Hyperion:
Das O(n) bezog ich auf den "zufälligen" Indexzugriff (worst case). Das O(1) an den Rändern ist klar bei doppelt verlinkten Listen.
Edit:
Anscheinend ist der Zeitaufwand fürs Umrechnen der negativen Indices etwas größer als die Rechenzeit pro Eintrag in reverse(), was in der Summe die Abhängigkeit von der Größe erklären würde.
Re: Spaß am Freitag: Ranking erstellen
Verfasst: Mittwoch 18. Februar 2015, 17:32
von Hyperion
jerch hat geschrieben:
@Hyperion:
Das O(n) bezog ich auf den "zufälligen" Indexzugriff (worst case). Das O(1) an den Rändern ist klar bei doppelt verlinkten Listen.
Jo, das hatte ich just nach Absetzen des Postings erkannt
