Pergunta

Estou aprendendo Clojure e gostaria de alguns conselhos sobre o uso idiomático. Como parte de um pacote de estatísticas pequena, eu tenho uma função para calcular o modo de um conjunto de dados. (Fundo:... O modo é o valor mais comum em um conjunto de dados Há quase uma dúzia de algoritmos publicados para calculá-lo O usado aqui é de "Fundamentos de Bioestatística" 6ª Ed por 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)
    )
  )

Há um par de coisas que eu tiver dúvidas sobre: ??

  1. O argumento. A função aceita uma única sequência. É mais idiomática para aceitar um número variável de argumentos como a função disso?
  2. Código cheiro. Parece que o "Deixe" é um pouco mais complicado do que deveria ser - tantas atribuições de variáveis. Eu perdi quaisquer usos óbvios (ou não tão óbvias) da linguagem ou biblioteca que fariam este método mais conciso?

Agradecemos antecipadamente pela ajuda.

Foi útil?

Solução

Aqui é a minha opinião:

  1. Há muitas funções núcleo Clojure que levam seqüências como argumentos, enquanto outros tomam vários argumentos, então não há nenhuma maneira real idiomática em minha opinião. Se você já tem seus dados em uma seqüência, eu usaria uma seq como argumento, uma vez que você vai economizar uma chamada para aplicar.

  2. Eu não iria escrever uma função que retorna um valor em alguns casos e uma lista de valores em outros, porque o código de chamada será sempre tem que verificar o valor de retorno antes de usá-lo. Em vez disso eu gostaria de voltar um único modo como seq com apenas um item na mesma. Mas você pode ter suas razões, dependendo do código que chama esta função.

Além de que eu iria reescrever a função do modo como esta:

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

Em vez de definir uma função f você poderia usar a função de identidade (a menos que seus dados contém valores que são logicamente falso). Mas você não precisa mesmo disso. I encontrar os modos de uma maneira diferente, o que é mais legível para mim: O mapa amap atua como uma seqüência de entradas de mapa (pares chave-valor). Primeiro eu filtro apenas as entradas que têm o valor mx. Então eu mapear a função-chave sobre elas, dando-me uma seqüência de teclas.

Para verificar se existem modos de eu não fazer loop sobre o mapa novamente. Em vez disso eu só comparar o número de modos para o número de entradas do mapa. Se eles são iguais, todos os elementos têm a mesma frequência!

Aqui está a função que sempre retorna um seguintes:

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

Outras dicas

Na minha opinião, mapeando alguma função em uma coleção e logo em seguida condensar a lista para um item é um sinal para uso reduce.

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

Neste caso, eu ia escrever o mode fn para tomar uma única coleção como um argumento, como você fez. A única razão que eu posso pensar em usar vários argumentos para uma função como esta é se você pretende ter a digitar argumentos literais muito.

Assim, se por exemplo, isto é para um script REPL interativo e você vai muitas vezes ser digitação (mode [1 2 1 2 3]) literalmente, então você deve ter a função de levar vários argumentos, para salvá-lo de digitando o [] extra na chamada de função o tempo todo. Se você pretende ler lotes de números de um arquivo e, em seguida, tomar o modo desses números, em seguida, tem a função de dar um único argumento que é uma coleção para que você possa salvar-se de usar apply o tempo todo. Eu estou supondo que seu caso de uso mais comum é o último. Acredito apply também adiciona uma sobrecarga que você evite quando você tem uma chamada de função que leva um argumento coleção.

Concordo com os outros que você deve ter mode retornar uma lista de resultados, mesmo se houver apenas um; ele vai tornar sua vida mais fácil. Talvez mude o nome modes enquanto você está nisso.

Aqui está uma implementação concisa agradável de mode:

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

Este explora os seguintes fatos:

  • A função frequencies retorna um mapa de valores -> freqüências
  • Você pode tratar um mapa como uma sequência de pares chave-valor
  • Se você classificar esta seqüência por valor (o item second em cada par), em seguida, o último item na seqüência irá representar o modo

Editar

Se você quiser lidar com o caso modo múltiplo, em seguida, você pode inserir um partition-by extra para manter todos os valores com a frequência máxima:

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

Parece-me muito bem. Eu substituir o

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

com

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

(Eu não sei por que algo como not-nil? não está na clojure.core;. É algo que precisa todos os dias)

Se houver um único modo único, que é devolvido. Se houver vários modos, eles são retornados como uma lista. Se não houver nenhum modo, ou seja, todos os elementos estão presentes em igual frequência, nil é retornado ".

Você poderia pensar em simplesmente retornando uma seq cada vez (um elemento ou vazio é bom); caso contrário, os casos têm de ser diferenciados pelo código de chamada. Por sempre retornando uma seq, seu resultado será magicamente trabalho como um argumento para outras funções que esperam uma seq.

Licenciado em: CC-BY-SA com atribuição
Não afiliado a StackOverflow
scroll top