2009-10-21 8 views
8

Uczę się Clojure i chciałbym porady na temat używania idiomatycznego. Jako część małego pakietu statystyk, mam funkcję obliczania trybu zbioru danych. (Tło: tryb jest najczęstszą wartością w zbiorze danych, jest ich prawie tuzin opublikowanych algorytmów, z których korzysta się w "Podstawach biostatystyki", 6th Ed, Bernard Rosner.)Funkcja trybu Idiomatic w Clojure

(defn tally-map 
" Create a map where the keys are all of the unique elements in the input 
    sequence and the values represent the number of times those elements 
    occur. Note that the keys may not be formatted as conventional Clojure 
    keys, i.e. a colon preceding a symbol." 
    [aseq] 
    (apply merge-with + (map (fn [x] {x 1}) aseq))) 

(defn mode 
" Calculate the mode. Rosner p. 13. The mode is problematic in that it may 
    not be unique and may not exist at all for a particular group of data. 
    If there is a single unique mode, it is returned. If there are multiple 
    modes, they are returned as a list. If there is no mode, that is all 
    elements are present in equal frequency, nil is returned." 
    [aseq] 
    (let [amap (tally-map aseq) 
     mx (apply max (vals amap)) 
     k (keys amap) 
     f (fn [x] (not (nil? x))) 
     modes (filter f (map #(if (= mx (get amap %)) %) k)) 
     ] 
    (cond (= 1 (count modes)) (first modes) 
     (every? #(= mx %) (vals amap)) nil 
     :else modes) 
    ) 
) 

Istnieje kilka rzeczy, mam pytania dotyczące:

  1. argumentu. Funkcja akceptuje pojedynczą sekwencję. Czy bardziej idiomatyczne jest przyjmowanie zmiennej liczby argumentów, takich jak funkcja dodawania?
  2. Zapach kodu. Wygląda na to, że "let" jest nieco bardziej skomplikowany, niż powinien być - tak wiele zadań zmiennych. Czy przegapiłem oczywiste (lub nie tak oczywiste) zastosowania języka lub biblioteki, które sprawiłyby, że ta metoda byłaby bardziej zwięzła?

Z góry dziękuję za pomoc.

Odpowiedz

4

Oto moje zdanie.

  1. Istnieje wiele rdzeń funkcje clojure, które pobierają sekwencje jako argumenty, podczas gdy inne przyjmują wiele argumentów, więc th W mojej opinii nie jest to prawdziwa idiomatyczna droga. Jeśli masz już dane w sekwencji, użyłbym argumentu seq jako argumentu, ponieważ spowoduje to zapisanie wezwania do zastosowania.

  2. Nie napisałbym funkcji, która zwraca wartość w niektórych przypadkach i listę wartości w innych, ponieważ kod wywołujący zawsze będzie musiał sprawdzić wartość zwracaną przed użyciem. Zamiast tego zwrócę jeden tryb jako seq z tylko jednym elementem w nim. Ale możesz mieć swoje powody, w zależności od kodu, który wywołuje tę funkcję.

Oprócz tego chciałbym przepisać funkcję trybu tak:

(defn mode [aseq] 
    (let [amap (tally-map aseq) 
     mx (apply max (vals amap)) 
     modes (map key (filter #(= mx (val %)) amap)) 
     c (count modes)] 
    (cond 
     (= c 1) (first modes) 
     (= c (count amap)) nil 
     :default modes))) 

Zamiast definiować funkcję f można użyć funkcji tożsamości (chyba, że ​​dane zawierają wartości, które są logicznie fałszywe). Ale nawet tego nie potrzebujesz.Tryby odnajduję w inny sposób, który jest dla mnie bardziej czytelny: mapa amap działa jako sekwencja wpisów do mapy (pary klucz-wartość). Najpierw filtruję tylko te wpisy, które mają wartość mx. Następnie mapuję funkcję klucza na te, dając mi sekwencję klawiszy.

Aby sprawdzić, czy są jakieś tryby, nie zapętlałem ponownie mapy. Zamiast tego po prostu porównuję liczbę trybów z liczbą pozycji na mapie. Jeśli są równe, wszystkie elementy mają tę samą częstotliwość!

Oto funkcja, która zawsze zwraca nast:

(defn modes [aseq] 
    (let [amap (tally-map aseq) 
     mx (apply max (vals amap)) 
     modes (map key (filter #(= mx (val %)) amap))] 
    (when (< (count modes) (count amap)) modes))) 
+0

"Funkcja f, którą definiujesz, jest tak naprawdę funkcją tożsamości (ponieważ zero jest logicznie fałszywe)." Nie, z dala od tego.Porównaj wyniki (tożsamość mapy [true false nil 1]) i (map # (nie (nil?% 1)) [true false nil 1]). – pmf

+0

Masz rację, oczywiście, nie jest to ta sama funkcja. Chciałem powiedzieć, że mógłby użyć funkcji tożsamości w swoim miejscu w tym przykładzie. Poprawię to. –

+0

Dzięki za analizę i sugestię. To była tylko zmiana perspektywy, której szukałem. – clartaq

2

Wygląda dobrze dla mnie. Chciałbym wymienić

f (fn [x] (not (nil? x))) 
mode (filter f (map #(if (= mx (get amap %)) %) k)) 

z

mode (remove nil? (map #(if (= mx (get amap %)) %) k)) 

(nie wiem dlaczego coś not-nil? nie jest w clojure.core; to coś trzeba codziennie.)

Jeśli istnieje jeden unikalny tryb, jest on zwracany. Jeśli istnieje wiele trybów, są one zwracane jako lista. Jeśli nie ma trybu, to znaczy, że wszystkie elementy są obecne z jednakową częstotliwością, zwracana jest wartość zero. "

Można pomyśleć o zwykłym zwrocie seq za każdym razem (jeden element lub pusty jest w porządku), w przeciwnym razie przypadki . muszą być zróżnicowana w zależności od kodu wywołującego by zawsze wracać seq, Twój wynik będzie magicznie działać jako argument do innych funkcji, które oczekują nast

+0

Dzięki za sugestię. Sposób, w jaki ustawiono wartości zwracane, był nonsensem. Była to krótkotrwała, daremna nadzieja użycia funkcji w taki sam sposób, w jaki użyłem średniej i mediany, która zwraca pojedynczą wartość. – clartaq

5

Moim zdaniem niektórych funkcji mapowania nad kolekcją, a następnie natychmiast kondensacyjny listę w dół do jednej pozycji jest oznaką używać reduce.

(defn tally-map [coll] 
    (reduce (fn [h n] 
      (assoc h n (inc (h n 0)))) 
      {} coll)) 

W tym przypadku chciałbym napisać mode fn wziąć jeden zbiór jako argument, jak ty. Jedynym powodem, dla którego mogę myśleć o używaniu wielu argumentów dla takiej funkcji, jest to, że planujesz często pisać dosłowne argumenty.

Więc np. to jest dla interaktywnego skryptu REPL i często będziesz pisał literalnie (mode [1 2 1 2 3]) dosłownie, wtedy powinieneś mieć funkcję przyjmującą wiele argumentów, aby zaoszczędzić od wpisywania dodatkowego [] w wywołaniu funkcji przez cały czas. Jeśli zamierzasz odczytywać wiele liczb z pliku, a następnie wybrać tryb tych liczb, wówczas funkcja będzie przyjmować pojedynczy argument, który jest zbiorem, dzięki czemu możesz cały czas oszczędzać sobie korzystania z apply. Zgaduję, że twoim najczęstszym przypadkiem użycia jest ten drugi. Wierzę, że apply dodaje również obciążenie, które można uniknąć, gdy masz wywołanie funkcji, które pobiera argument kolekcji.

Zgadzam się z innymi, że powinieneś mieć mode zwrócić listę wyników, nawet jeśli jest tylko jeden; ułatwi ci to życie. Może zmień nazwę na modes, gdy jesteś na tym.

+0

Wziąłem twoją radę i zmieniłem nazwę mojej drugiej funkcji na tryby. :-) –

+0

(inc (lub (h n) 0)) jest taki sam jak (inc (h n 0)) :) –

+0

O, prawda, zawsze zapominam tę opcję wartości domyślnej. Dzięki. –

4

Oto miły zwięzły realizacja mode:

(defn mode [data] 
    (first (last (sort-by second (frequencies data))))) 

ta wykorzystuje następujące fakty:

  • Funkcja frequencies Zwraca mapę wartości -> częstotliwościach
  • można traktować mapę jako sekwencję par klucz-wartość
  • Jeśli sortujesz tę sekwencję według wartości (pozycja second w każdej parze), to ostatni element w sekwencji będzie reprezentować tryb

EDIT

Jeśli chcesz obsługiwać wiele przypadek trybu następnie można wstawić dodatkowy partition-by aby zachować wszystkie wartości z maksymalną częstotliwością:

(defn modes [data] 
    (->> data 
     frequencies 
     (sort-by second) 
     (partition-by second) 
     last 
     (map first))) 
+0

co w przypadku, gdy tryb nie jest unikalny lub nie istnieje? – georgek

+1

Dziękuję za odpowiedź na tak stare pytanie (wracam do pierwszych dni, w które wierzę). Funkcja częstotliwości nie istniała wtedy. Fajnie jest odebrać prod, aby przejrzeć stare funkcje, aby umożliwić aktualizację. – clartaq