I have the the following array of numbers in Ruby (higher is better), and I'd like to rank them. In other words, I want to convert the following sorted list:
[89 52 52 36 18 18 5]
to the following ranks:
[1 2 2 4 5 5 7]
For instance, the winner gets first place, there's a tie for second place, and so on. The important point, obviously, is that ties are possible and those ties must then skip the corresponding ranks. Any number of ties might be possible (3 people sharing second place).
Is there an elegant way to perform this kind of operation?
Using Enumerable#group_by
:
a = [89, 52, 52, 36, 18, 18, 5]
rank = 1
a.group_by {|x| x}.map { |k,v|
ret = [rank] * v.size
rank += v.size
ret
}.flatten
# => [1, 2, 2, 4, 5, 5, 7]
UPDATE
rank, i = 1, 0
a.map { |x|
i += 1
x != a[i-2] ? rank = i : rank
}
# => [1, 2, 2, 4, 5, 5, 7]
Using Enumerable#each_with_index
to avoid going through the array for each iteration :
a = [89, 52, 52, 36, 18, 18, 5]
rank = 1
a.each_with_index.map{|value, i| a[i-1] == value ? rank : rank = i+1}
Edit : I guess I should also benchmark mine, here are the results
Calculating -------------------------------------
falsetru 344 i/100ms
sawa 436 i/100ms
Jordan 1174 i/100ms
Iceman 46 i/100ms
simongarnier 710 i/100ms
-------------------------------------------------
falsetru 3411.0 (±3.7%) i/s - 17200 in 5.049500s
sawa 4437.4 (±3.4%) i/s - 22236 in 5.017073s
Jordan 11746.5 (±2.3%) i/s - 59874 in 5.099797s
Iceman 463.4 (±2.4%) i/s - 2346 in 5.065717s
simongarnier 7442.1 (±3.2%) i/s - 37630 in 5.061725s
Not The best one, but still putting up a good fight!
a = [89, 52, 52, 36, 18, 18, 5]
a.map{ |e| a.index(e) + 1 }
# => [1, 2, 2, 4, 5, 5, 7]
Edit:
Benchmark from @Jordan gist (https://gist.github.com/jrunning/488de3a19428b9ebb488)
Calculating -------------------------------------
falsetru 419 i/100ms
sawa 514 i/100ms
Jordan 1438 i/100ms
Iceman 57 i/100ms
-------------------------------------------------
falsetru 4232.8 (±2.5%) i/s - 21369 in 5.051639s
sawa 5032.0 (±3.5%) i/s - 25186 in 5.011681s
Jordan 15057.5 (±2.7%) i/s - 76214 in 5.065319s
Iceman 575.7 (±1.9%) i/s - 2907 in 5.051481s
I don't know about "elegant," but here's a short, readable, super-straightforward solution:
# assume a pre-sorted non-sparse array
arr = [89, 52, 52, 36, 18, 18, 18, 18, 7]
run = rank = 0
last_n = nil
ranked = arr.map do |n|
run += 1
next rank if n == last_n
last_n = n
rank += run
run = 0
rank
end
p ranked
# => [1, 2, 2, 4, 5, 5, 5, 5, 9]
I guess we're doing this...
Edit: This post was getting way too long, so I've moved the benchmark code to a Gist: https://gist.github.com/jrunning/8549666d32a6bfa88e41
Here are the results:
falsetru 730.9 (±2.1%) i/s - 3672 in 5.025755s
falsetru (2) 1289.9 (±2.7%) i/s - 6500 in 5.042749s
sawa 986.9 (±2.1%) i/s - 5000 in 5.068450s
Jordan 1644.9 (±1.9%) i/s - 8250 in 5.017334s
Iceman 6.4 (±0.0%) i/s - 32 in 5.035015s
simongarnier 1053.9 (±1.9%) i/s - 5304 in 5.034452s
Cary Swoveland 511.4 (±3.5%) i/s - 2600 in 5.090605s
Edit: I had some stuff in here about Enumerator::Lazy, but it turns out I was using it wrong. It actually didn't improve performance in any case.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With