Domanda

Sto imparando Clojure e vorrei un consiglio sull'uso idiomatico. Come parte di un piccolo pacchetto di statistiche, ho una funzione per calcolare la modalità di un insieme di dati. (Sfondo: la modalità è il valore più comune in un insieme di dati. Esistono quasi una dozzina di algoritmi pubblicati per calcolarlo. Quello qui utilizzato è tratto da "Fundamentals of Biostatistics" e 6th Ed di Bernard Rosner.)

(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)
    )
  )

Ci sono un paio di cose su cui ho domande:

  1. L'argomento. La funzione accetta una singola sequenza. È più idiomatico accettare un numero variabile di argomenti come la funzione di addizione?
  2. Odore di codice. Sembra che il " let " è un po 'più complicato di quanto dovrebbe essere - così tante assegnazioni di variabili. Ho perso qualche uso ovvio (o non così ovvio) del linguaggio o della biblioteca che renderebbe questo metodo più conciso?

Grazie in anticipo per l'aiuto.

È stato utile?

Soluzione

Ecco la mia opinione:

  1. Ci sono molte funzioni di clojure di base che prendono sequenze come argomenti mentre altre prendono più argomenti, quindi secondo me non esiste un vero modo idiomatico. Se hai già i tuoi dati in una sequenza, userei un seq come argomento, dal momento che ti salverà una chiamata da applicare.

  2. Non scriverei una funzione che restituisce un valore in alcuni casi e un elenco di valori in altri, perché il codice chiamante dovrà sempre controllare il valore restituito prima di usarlo. Invece vorrei restituire una singola modalità come seq con solo un elemento in esso. Ma potresti avere le tue ragioni, a seconda del codice che chiama questa funzione.

A parte questo, riscriverei la funzione mode in questo modo:

(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)))

Invece di definire una funzione f potresti usare la funzione identità (a meno che i tuoi dati non contengano valori logicamente falsi). Ma non ne hai nemmeno bisogno. Trovo le modalità in un modo diverso, il che è più leggibile per me: la mappa map agisce come una sequenza di voci della mappa (coppie chiave-valore). In primo luogo filtrare solo quelle voci che hanno il valore mx. Quindi mappa la funzione dei tasti su questi, dandomi una sequenza di tasti.

Per verificare se ci sono delle modalità, non riesco più a scorrere sulla mappa. Invece ho solo confrontare il numero di modalità con il numero di voci della mappa. Se sono uguali, tutti gli elementi hanno la stessa frequenza!

Ecco la funzione che restituisce sempre un seq:

(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)))

Altri suggerimenti

A mio avviso, mappare alcune funzioni su una raccolta e quindi condensare immediatamente l'elenco in un elemento è un segno per usare riduci .

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

In questo caso scriverei la mode fn per prendere una singola raccolta come argomento, come hai fatto tu. L'unico motivo per cui riesco a pensare di usare più argomenti per una funzione come questa è se pensi di dover scrivere molto argomenti letterali.

Quindi se ad es. questo è per uno script REPL interattivo e stai spesso digitando (mode [1 2 1 2 3]) letteralmente, quindi dovresti avere la funzione prendere più argomenti, per salvarti dalla digitazione il [] extra nella chiamata di funzione in ogni momento. Se hai intenzione di leggere molti numeri da un file e quindi prendere la modalità di quei numeri, allora la funzione accetta un singolo argomento che è una raccolta in modo da poterti salvare dall'uso di apply sempre . Immagino che il tuo caso d'uso più comune sia il secondo. Credo che applica aggiunge anche un overhead che eviti quando hai una chiamata di funzione che accetta un argomento di raccolta.

Sono d'accordo con gli altri sul fatto che dovresti avere mode restituire un elenco di risultati anche se ce n'è solo uno; ti semplificherà la vita. Forse rinominalo mode mentre ci sei.

Ecco una bella concisa implementazione della modalità :

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

Questo sfrutta i seguenti fatti:

  • La funzione frequenze restituisce una mappa di valori - > frequenze
  • Puoi considerare una mappa come una sequenza di coppie chiave-valore
  • Se ordini questa sequenza per valore (l'elemento secondo in ciascuna coppia), l'ultimo elemento nella sequenza rappresenterà la modalità

Modifica

Se si desidera gestire il caso in modalità multipla, è possibile inserire una partizione per aggiuntiva per mantenere tutti i valori con la massima frequenza:

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

Mi sta benissimo. Sostituirei

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

con

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

(Non so perché qualcosa come non-zero? non è in clojure.core ; è qualcosa di cui hai bisogno ogni giorno.)

  

Se esiste un'unica modalità unica, viene restituita. Se esistono più modalità, vengono restituite come un elenco. Se non esiste alcuna modalità, ovvero tutti gli elementi sono presenti in uguale frequenza, viene restituito zero. & Quot;

Potresti pensare di restituire semplicemente un seq ogni volta (un elemento o vuoto va bene); in caso contrario, i casi devono essere differenziati dal codice chiamante. Restituendo sempre un seq, il tuo risultato funzionerà magicamente come argomento per altre funzioni che prevedono un seq.

Autorizzato sotto: CC-BY-SA insieme a attribuzione
Non affiliato a StackOverflow
scroll top