Guided randomness

Napisałem taki mały gem i może oprócz pochwalenia się, komuś się on jeszcze przyda :wink:

https://rubygems.org/gems/guided_randomness

Idea jest bardzo prosta - czasem potrzebujemy wybrać losowy element z tablicy, ale chcemy żeby prawdopodobieństwo wyboru pierwszego elementu wynosiło np. 0,6, drugiego 0,3, a trzeciego 0,1.

Taki problem rozwiązuje właśnie guided_randomness:

require 'guided_randomness' [1,2,3].get_rand [0.6, 0.3, 0.1]
to zwróci nam 1 z prawdopodobieństwem 60 procent, 2 z prawdopodobieństwem 30 procent, a 3 z prawdopodobieństwem 10 procent :slight_smile:

ew. można też:

[1,2,3].get_rand [20, 30, 10]

wtedy zostanie to potraktowane jako liczba żądanych wystąpień, na sumę elementów podanej tabeli jako argument
(czyli np. 1 zostanie zwrócona z prawdopodobieństwem 20/(20+30+10) czyli 1/3)

Z ciekawości zajrzałem i jestem mocno zawiedziony. Dawno nie widziałem takiej proporcji ilości kodu do liczby niedoróbek. Ponieważ na co dzień jestem tak samo krytyczny wobec cudzego jak i swojego kodu to powiem Ci co bym poprawił na Twoim miejscu:

  1. Monkey-patching - jeśli nie ma konkretnej przesłanki to nie stosować. Lepiej cały algorytm umieścić w klasie, np. GuidedRandomness/WeightedRandom i jeśli końcowy użytkownik będzie chciał MP to sobie takowy dorobi (ew. można umieścić dodatkowy plik guided_randomness/array_monkey_patch.rb, który będzie ładowany ręcznie przez usera).

  2. Nazwa get_rand - do kosza. Trzeba znaleźć jakąś adekwatną. Hm… weighted_random (jeśli przenosimy algorytm do wspomnianej klasy to np. GuidedRandomness.random, WeightedRandom.random etc.)?

  3. Formatowanie, brak konsekwencji przy białych znakach - masakra. Raz jest “|x,y|”, potem “|x, y|” (spacja), raz otaczasz spacją operator +/-, innym razem nie, jeszcze innym tylko z jednej strony. Je osobiście zawsze każdy operator typu +/-/* etc otaczam po 1 spacji z każdej strony. Zawsze. Niezależnie od preferencji konsekwentność to podstawa.

  4. Zakomentowany kod. Rozumiem, że “na potem, może się jeszcze przyda”?:wink:

  5. Nazwy zmiennych: hash, value (nieużywana), counter, arg, k, v (czyli prawie wszystkie) - do poprawy.

  6. Algorytm. Osobiście wyznaję zasadę, że najpierw niech coś działa, a potem dobrze. Twój działa, chyba dobrze, ale wolno (o czym pewnie zdajesz sobie sprawę). Niezależnie od jego złożoności czasowej mam ogromne podejrzenia co do magicznej zmiennej counter oraz niezrozumiałej dla mnie magii z rand(3) - 1. Być może za tym kryje się jakaś sprytna sztuczka, ja jej nie widzę.

Lepszy i wydajniejszy algorytm polega na:

  • zsumowaniu wszystkich wag elementów
  • wylosowaniu liczby z przedziału [0, suma)
  • znalezieniu pierwszej liczby, dla której wylosowana liczba jest <= niż suma wag wszystkich wcześniejszych + jej
  1. Wiem, że to nie wina Twojej biblioteki, ale nie działa z railsami, które dodają do klasy Array metodę rand co powoduje wywalenie się przy rand(3). Moja sugestia - wszystkie wywołania metody rand(x) zamienić na Kernel.rand(x).

Moja wersja (zostawiłem w klasie Array gdyż nie chciałem ruszać testów):

[code=ruby]class Array

def get_rand(weights)
raise “Wrong number of array elements!” unless weights.size == self.size

total = weights.inject(0, :+)
point = Kernel.rand * total

self.each_with_index do |element, i|
  return element if weights[i] >= point
  point -= weights[i]
end

end[/code]
Przed:

[code=shell]Loaded suite test/test
Started

Finished in 101.431378 seconds.

4 tests, 815 assertions, 0 failures, 0 errors

real 1m42.128s
user 1m34.898s
sys 0m2.431s[/code]
Po:

[code=shell]Loaded suite test/test
Started

Finished in 9.32682 seconds.

4 tests, 815 assertions, 0 failures, 0 errors

real 0m9.997s
user 0m9.587s
sys 0m0.281s[/code]

Zrobisz forka i pull requesta?

dzięki :slight_smile:

Poszło :).

dzięki wielkie :slight_smile:

nie ukrywam, że to mój pierwszy gem :smiley:

Ciekawy test z tym deviation. Ja bym po prostu stubował wywoałania rand lub ustawiał srand. Poza tym chyba:

point = Kernel.rand * total

to to samo co:

point = Kernel.rand(total)

Jednak nie jest!

Aż sobie zrefaktoryzuje… :slight_smile:

Niestety mój refactoring polegający na skorzystaniu z gema bsearch dodatkowo zaśmieca klasę Array. Za to w zamian dorzuciłem nowe ficzery takie jak samplowanie wiele razy tej samej tablicy za jednym zamachem oraz użycie binarnego wyszukiwania w celu znalezienia mapowania z losowej liczby na index wylosowanego elementu. Czas wykonania testów na moim komputerze wacha się około 4.7 s.

101 => 9.3 => 4.7 s .

Takie refactoringi lubimy :slight_smile:

Teraz jeszcze tylko 4 osoba powinna dodać licencję i napisać dobre readme + pewnie dobrze by było sprawić by metoda w Array była opcjonalna.

Z innych pomysłów: w testach można by użyć assert_in_delta dla deviation oraz skorzystać z assert_performance_linear .

I znowu ten pęd do jednolinijkowców…

@sums = weights.inject([]){|ary, weight| ary << ((ary.last || 0) + weight)}

Nawet nie chce mi się myśleć co ten kod robi…

Oj tam oj tam, chyba trochę przesadzasz :wink: Oczywiście można by to zapisać w jakimś eachu i w kilku linijkach, ale też bez przesady, to jest zwykły inject, który wrzuca do nowej tablicy sumę danego elementu i elementów poprzednich.

Sam implementacja jest moim zdaniem zrozumiała natomiast nie przeszkodziłoby by gdyby została wyciągnieta do osobnej metody o wymownym tytule stanowiącym komentarz jej działania.